internal statistics & tooling
This commit is contained in:
12 files changed
+2065
-2
No files matched your search
@@ -0,0 +1,56 @@
|
||||
<aside class="sidebar sidebar--admin">
|
||||
<div class="sidebar__header">
|
||||
<div class="sidebar__badge sidebar__badge--admin">
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" width="16" height="16">
|
||||
<path d="M8 1L14 4v5c0 3.5-2.5 6-6 6.5C2.5 15 0 12.5 0 9V4l8-3z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" fill="none"/>
|
||||
<path d="M5.5 8l2 2 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="sidebar__meta">
|
||||
<span class="sidebar__league-name">Admin Panel</span>
|
||||
<span class="sidebar__role">site admin</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar__nav" aria-label="Admin navigation">
|
||||
<a class="sidebar__link {{ 'is-active' if request.endpoint == 'internal.dashboard' }}"
|
||||
href="{{ url_for('internal.dashboard') }}">
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="1" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
|
||||
<rect x="9" y="1" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
|
||||
<rect x="1" y="9" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
|
||||
<rect x="9" y="9" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
|
||||
</svg>
|
||||
<span>Overview</span>
|
||||
</a>
|
||||
|
||||
<a class="sidebar__link {{ 'is-active' if request.endpoint in ('internal.users', 'internal.user_detail') }}"
|
||||
href="{{ url_for('internal.users') }}">
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<circle cx="6" cy="5.5" r="2.5" stroke="currentColor" stroke-width="1.4"/>
|
||||
<path d="M1 14c0-2.761 2.239-5 5-5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||
<circle cx="11.5" cy="5.5" r="2" stroke="currentColor" stroke-width="1.3"/>
|
||||
<path d="M9 14c0-1.657 1.119-3.084 2.5-3.084S14 12.343 14 14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
|
||||
<a class="sidebar__link {{ 'is-active' if request.endpoint in ('internal.leagues', 'internal.league_detail') }}"
|
||||
href="{{ url_for('internal.leagues') }}">
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M3 14V6l5-3 5 3v8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 14v-4h4v4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>Leagues</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar__footer">
|
||||
<a class="sidebar__back" href="{{ url_for('leagues.index') }}">
|
||||
<svg viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="M7 2L3 6l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Back to app
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -0,0 +1,374 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Overview · Admin · myboker.org{% endblock %}
|
||||
{% block sidebar %}{% include "_internal_sidebar.html" %}{% endblock %}
|
||||
{% block page_class %}page--app{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="db-header int-page-header">
|
||||
<div>
|
||||
<div class="db-header__chips">
|
||||
<span class="badge badge--paid">Site Admin</span>
|
||||
{% if stats.open_sessions > 0 %}
|
||||
<span class="badge badge--open">{{ stats.open_sessions }} live session{{ 's' if stats.open_sessions != 1 }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1 class="db-header__title">Overview</h1>
|
||||
<p class="db-header__sub">Signed in as {{ admin_user.email }}. Site health, growth, and recent activity at a glance.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero metrics -->
|
||||
<div class="int-hero-grid">
|
||||
<div class="int-hero-card">
|
||||
<div class="int-hero-card__icon int-hero-card__icon--users">
|
||||
<svg viewBox="0 0 20 20" fill="none" width="18" height="18">
|
||||
<path d="M13 18v-1.5a3.5 3.5 0 0 0-3.5-3.5h-5A3.5 3.5 0 0 0 1 16.5V18" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="6.5" r="3.5" stroke="currentColor" stroke-width="1.6"/>
|
||||
<path d="M17 18v-1.5a3.5 3.5 0 0 0-2.5-3.37M13.5 3.13a3.5 3.5 0 0 1 0 6.74" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="int-hero-card__num">{{ stats.users }}</span>
|
||||
<span class="int-hero-card__label">Total users</span>
|
||||
<span class="int-hero-card__sub">{{ stats.active_users }} active · +{{ stats.new_users_7d }} this week</span>
|
||||
</div>
|
||||
<div class="int-hero-card">
|
||||
<div class="int-hero-card__icon int-hero-card__icon--leagues">
|
||||
<svg viewBox="0 0 20 20" fill="none" width="18" height="18">
|
||||
<path d="M2 18V8l8-5 8 5v10" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 18v-6h6v6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="int-hero-card__num">{{ stats.active_leagues }}</span>
|
||||
<span class="int-hero-card__label">Active leagues</span>
|
||||
<span class="int-hero-card__sub">{{ stats.public_leagues }} public · {{ stats.archived_leagues }} archived · {{ '%.1f'|format(stats.avg_sessions_per_active_league) }} avg sessions</span>
|
||||
</div>
|
||||
<div class="int-hero-card">
|
||||
<div class="int-hero-card__icon int-hero-card__icon--sessions">
|
||||
<svg viewBox="0 0 20 20" fill="none" width="18" height="18">
|
||||
<rect x="2" y="3" width="16" height="14" rx="2.5" stroke="currentColor" stroke-width="1.6"/>
|
||||
<path d="M6 1v4M14 1v4M2 8h16" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="int-hero-card__num">{{ stats.sessions }}</span>
|
||||
<span class="int-hero-card__label">Total sessions</span>
|
||||
<span class="int-hero-card__sub">{{ stats.open_sessions }} open · {{ stats.sessions_7d }} this week · {{ '%.1f'|format(stats.avg_players_per_session) }} avg players</span>
|
||||
</div>
|
||||
<div class="int-hero-card">
|
||||
<div class="int-hero-card__icon int-hero-card__icon--health">
|
||||
<svg viewBox="0 0 20 20" fill="none" width="18" height="18">
|
||||
<path d="M10 2.5l6.5 3v4.7c0 4.1-2.75 7-6.5 7.8-3.75-.8-6.5-3.7-6.5-7.8V5.5l6.5-3z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
<path d="M7.2 10.1l1.8 1.8 3.9-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="int-hero-card__num">{{ stats.active_users_30d }}</span>
|
||||
<span class="int-hero-card__label">Active users · 30d</span>
|
||||
<span class="int-hero-card__sub">{{ stats.returning_users_30d }} returning · {{ stats.unverified_users }} unverified</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="int-trend-grid int-section-grid">
|
||||
{% for card in trend_cards %}
|
||||
<section class="int-trend-card">
|
||||
<div>
|
||||
<span class="int-trend-card__label">{{ card.label }}</span>
|
||||
<span class="int-trend-card__value">{{ card.value }}</span>
|
||||
<span class="int-trend-card__delta int-trend-card__delta--{{ card.delta.direction }}">
|
||||
{% if card.delta.pct is none %}
|
||||
New
|
||||
{% else %}
|
||||
{{ '+' if card.delta.current > card.delta.previous else '' }}{{ '%.1f'|format(card.delta.pct) }}%
|
||||
{% endif %}
|
||||
<span>vs previous</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="int-sparkline-wrap">
|
||||
<canvas id="trend-{{ card.key }}"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Charts row 1: growth -->
|
||||
<div class="cards-2 int-section-grid">
|
||||
<section class="panel int-chart-panel">
|
||||
<div class="int-chart-head">
|
||||
<p class="kicker" style="margin:0;">Growth</p>
|
||||
<h2 class="panel__title">User signups · 8 weeks</h2>
|
||||
</div>
|
||||
<div class="int-chart-wrap">
|
||||
<canvas id="chart-users"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel int-chart-panel">
|
||||
<div class="int-chart-head">
|
||||
<p class="kicker" style="margin:0;">Activity</p>
|
||||
<h2 class="panel__title">Sessions per week · 8 weeks</h2>
|
||||
</div>
|
||||
<div class="int-chart-wrap">
|
||||
<canvas id="chart-sessions"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Charts row 2: cash + weekday -->
|
||||
<div class="cards-2 int-section-grid">
|
||||
<section class="panel int-chart-panel">
|
||||
<div class="int-chart-head">
|
||||
<p class="kicker" style="margin:0;">Recorded ledger volume</p>
|
||||
<h2 class="panel__title">Buy-ins and collections · 8 weeks</h2>
|
||||
</div>
|
||||
<div class="int-chart-wrap">
|
||||
<canvas id="chart-cash"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel int-chart-panel">
|
||||
<div class="int-chart-head">
|
||||
<p class="kicker" style="margin:0;">Patterns</p>
|
||||
<h2 class="panel__title">Sessions by day of week</h2>
|
||||
</div>
|
||||
<div class="int-chart-wrap">
|
||||
<canvas id="chart-weekday"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Breakdown + top leagues -->
|
||||
<div class="cards-2 int-section-grid">
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Activity breakdown</h2>
|
||||
<span class="panel__tag">24h / 7d</span>
|
||||
</div>
|
||||
<div class="panel__body">
|
||||
<div class="int-breakdown-grid">
|
||||
<div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">New users (24h)</span><span class="int-breakdown-row__val">{{ stats.new_users_24h }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">New users (7d)</span><span class="int-breakdown-row__val">{{ stats.new_users_7d }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Active users (24h)</span><span class="int-breakdown-row__val">{{ stats.active_users_24h }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Active users (7d)</span><span class="int-breakdown-row__val">{{ stats.active_users_7d }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Active users (30d)</span><span class="int-breakdown-row__val">{{ stats.active_users_30d }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Returning users (7d)</span><span class="int-breakdown-row__val">{{ stats.returning_users_7d }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Returning users (30d)</span><span class="int-breakdown-row__val">{{ stats.returning_users_30d }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Users in leagues</span><span class="int-breakdown-row__val">{{ stats.users_in_leagues }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">No-league signups</span><span class="int-breakdown-row__val">{{ stats.orphan_users }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Verified accounts</span><span class="int-breakdown-row__val">{{ stats.verified_users }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Unverified</span><span class="int-breakdown-row__val">{{ stats.unverified_users }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Disabled</span><span class="int-breakdown-row__val">{{ stats.disabled_users }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Site admins</span><span class="int-breakdown-row__val">{{ stats.site_admins }}</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Sessions (24h)</span><span class="int-breakdown-row__val">{{ stats.sessions_24h }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Sessions (7d)</span><span class="int-breakdown-row__val">{{ stats.sessions_7d }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Open sessions</span><span class="int-breakdown-row__val">{{ stats.open_sessions }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Leagues active (7d)</span><span class="int-breakdown-row__val">{{ stats.leagues_active_7d }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Leagues with no sessions</span><span class="int-breakdown-row__val">{{ stats.leagues_without_sessions }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Sessions with no ledger records</span><span class="int-breakdown-row__val">{{ stats.sessions_without_events }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Avg players/session</span><span class="int-breakdown-row__val">{{ '%.1f'|format(stats.avg_players_per_session) }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Ledger events (24h)</span><span class="int-breakdown-row__val">{{ stats.ledger_events_24h }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Ledger events (7d)</span><span class="int-breakdown-row__val">{{ stats.ledger_events_7d }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Total ledger events</span><span class="int-breakdown-row__val">{{ stats.ledger_events }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Voided ledger records</span><span class="int-breakdown-row__val">{{ stats.voided_events }} · {{ '%.1f'|format(stats.void_rate) }}%</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Avg sessions / league</span><span class="int-breakdown-row__val">{{ stats.avg_sessions_per_active_league }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Top leagues</h2>
|
||||
<a class="panel__tag" href="{{ url_for('internal.leagues') }}">All leagues →</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead><tr><th>League</th><th>Owner</th><th style="text-align:right;">Sessions</th></tr></thead>
|
||||
<tbody>
|
||||
{% for league, session_count in top_leagues %}
|
||||
{% set owner = owners.get(league.id) %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('internal.league_detail', league_id=league.id) }}">{{ league.name }}</a>
|
||||
{% if league.visibility == 'public' %}<span class="badge" style="font-size:10px;margin-left:4px;">public</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{% if owner %}{{ owner.email }}{% else %}<span class="muted-text">—</span>{% endif %}</td>
|
||||
<td style="text-align:right;font-variant-numeric:tabular-nums;font-weight:600;">{{ session_count }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="muted-text">No leagues yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel int-section-grid">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Recorded ledger volume</h2>
|
||||
<span class="panel__tag">Not platform revenue</span>
|
||||
</div>
|
||||
<div class="panel__body">
|
||||
<div class="int-breakdown-grid">
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Recorded buy-ins and collections</span><span class="int-breakdown-row__val">${{ '%.2f'|format(stats.cash_in_cents / 100) }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Recorded payouts</span><span class="int-breakdown-row__val">${{ '%.2f'|format(stats.cash_out_cents / 100) }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Average recorded volume / session</span><span class="int-breakdown-row__val">${{ '%.2f'|format(stats.avg_cash_in_per_session_cents / 100) }}</span></div>
|
||||
<div class="int-breakdown-row"><span class="int-breakdown-row__label">Ledger records</span><span class="int-breakdown-row__val">{{ stats.ledger_events }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Recent users + leagues -->
|
||||
<div class="cards-2 int-section-grid int-section-grid--last">
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Recent users</h2>
|
||||
<a class="panel__tag" href="{{ url_for('internal.users') }}">All users →</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead><tr><th>Email</th><th>Status</th><th>Joined</th></tr></thead>
|
||||
<tbody>
|
||||
{% for user in recent_users %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('internal.user_detail', user_id=user.id) }}">{{ user.email }}</a>
|
||||
{% if user.is_site_admin %}<span class="badge badge--paid" style="font-size:10px;">Admin</span>{% endif %}
|
||||
</td>
|
||||
<td>{% if user.disabled_at %}<span class="badge badge--closed">Disabled</span>{% elif not user.email_verified_at %}<span class="badge">Unverified</span>{% else %}<span class="badge badge--open">Active</span>{% endif %}</td>
|
||||
<td style="color:var(--muted);font-size:12px;">{{ user.created_at.strftime('%b %-d') if user.created_at else '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Recent leagues</h2>
|
||||
<a class="panel__tag" href="{{ url_for('internal.leagues') }}">All leagues →</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead><tr><th>League</th><th>Visibility</th><th>Created</th></tr></thead>
|
||||
<tbody>
|
||||
{% for league in recent_leagues %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('internal.league_detail', league_id=league.id) }}">{{ league.name }}</a></td>
|
||||
<td><span class="badge">{{ league.visibility }}</span></td>
|
||||
<td style="color:var(--muted);font-size:12px;">{{ league.created_at.strftime('%b %-d') if league.created_at else '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const labels = {{ growth.labels | tojson }};
|
||||
const userSignups = {{ growth.user_signups | tojson }};
|
||||
const sessionData = {{ growth.session_counts | tojson }};
|
||||
const cashData = {{ growth.cash_in_weekly | tojson }};
|
||||
const wdLabels = {{ weekday_data.labels | tojson }};
|
||||
const wdData = {{ weekday_data.data | tojson }};
|
||||
const trendCards = {{ trend_cards | tojson }};
|
||||
|
||||
const axisStyle = {
|
||||
grid: { color: 'rgba(255,255,255,.05)' },
|
||||
ticks: { color: 'rgba(255,255,255,.38)', font: { size: 11 } },
|
||||
};
|
||||
const tooltipStyle = {
|
||||
backgroundColor: 'rgba(20,20,28,.92)',
|
||||
borderColor: 'rgba(255,255,255,.08)',
|
||||
borderWidth: 1,
|
||||
titleColor: 'rgba(255,255,255,.5)',
|
||||
bodyColor: '#fff',
|
||||
padding: 10,
|
||||
};
|
||||
const baseOpts = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false }, tooltip: tooltipStyle },
|
||||
scales: {
|
||||
x: axisStyle,
|
||||
y: { ...axisStyle, beginAtZero: true, ticks: { ...axisStyle.ticks, precision: 0 } },
|
||||
},
|
||||
};
|
||||
|
||||
const lineDataset = (data, color) => ({
|
||||
data,
|
||||
borderColor: color,
|
||||
backgroundColor: color.replace('1)', '.12)'),
|
||||
borderWidth: 2,
|
||||
pointRadius: 2,
|
||||
pointHoverRadius: 4,
|
||||
tension: .35,
|
||||
fill: true,
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chart-users'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [lineDataset(userSignups, 'rgba(155,140,240,1)')]
|
||||
},
|
||||
options: baseOpts,
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chart-sessions'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [lineDataset(sessionData, 'rgba(111,192,147,1)')]
|
||||
},
|
||||
options: baseOpts,
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chart-cash'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{ data: cashData, backgroundColor: 'rgba(120,180,240,.45)', borderColor: 'rgba(120,180,240,.9)', borderWidth: 1, borderRadius: 4 }]
|
||||
},
|
||||
options: {
|
||||
...baseOpts,
|
||||
scales: {
|
||||
x: axisStyle,
|
||||
y: { ...axisStyle, beginAtZero: true, ticks: { ...axisStyle.ticks, callback: v => '$' + v } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chart-weekday'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: wdLabels,
|
||||
datasets: [{ data: wdData, backgroundColor: 'rgba(224,177,92,.45)', borderColor: 'rgba(224,177,92,.9)', borderWidth: 1, borderRadius: 4 }]
|
||||
},
|
||||
options: baseOpts,
|
||||
});
|
||||
|
||||
trendCards.forEach((card) => {
|
||||
const canvas = document.getElementById('trend-' + card.key);
|
||||
if (!canvas) return;
|
||||
new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: card.labels,
|
||||
datasets: [lineDataset(card.series, 'rgba(155,140,240,1)')]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
||||
scales: { x: { display: false }, y: { display: false, beginAtZero: true } },
|
||||
elements: { point: { radius: 0 } },
|
||||
},
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,128 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ league.name }} · Admin · myboker.org{% endblock %}
|
||||
{% block sidebar %}{% include "_internal_sidebar.html" %}{% endblock %}
|
||||
{% block page_class %}page--app{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="db-header" style="margin-bottom:20px;">
|
||||
<div>
|
||||
<div class="db-header__chips">
|
||||
<a class="badge" href="{{ url_for('internal.leagues') }}">← Leagues</a>
|
||||
{% if league.archived_at %}<span class="badge badge--closed">Archived</span>{% else %}<span class="badge badge--open">Active</span>{% endif %}
|
||||
<span class="badge">{{ league.visibility }}</span>
|
||||
</div>
|
||||
<h1 class="db-header__title">{{ league.name }}</h1>
|
||||
<p class="db-header__sub">Owner: {% if owner %}<a href="{{ url_for('internal.user_detail', user_id=owner.id) }}">{{ owner.email }}</a>{% else %}<span class="muted-text">missing</span>{% endif %}</p>
|
||||
</div>
|
||||
<a class="btn btn--primary btn--sm" href="{{ url_for('leagues.league_settings', league_ref=league.url_ref) }}">Manage league</a>
|
||||
</div>
|
||||
|
||||
<div class="db-stat-grid">
|
||||
<div class="db-stat"><span class="db-stat__num">{{ stats.members }}</span><span class="db-stat__label">Active members</span></div>
|
||||
<div class="db-stat"><span class="db-stat__num">{{ stats.players }}</span><span class="db-stat__label">Players</span></div>
|
||||
<div class="db-stat"><span class="db-stat__num">{{ stats.sessions }}</span><span class="db-stat__label">Sessions</span></div>
|
||||
<div class="db-stat"><span class="db-stat__num">{{ stats.events }}</span><span class="db-stat__label">Ledger events</span></div>
|
||||
</div>
|
||||
|
||||
<div class="cards-2">
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Members</h2>
|
||||
<span class="panel__tag">{{ members|length }}</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead><tr><th>Email</th><th>Role</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for membership, member_user in members %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('internal.user_detail', user_id=member_user.id) }}">{{ member_user.email }}</a></td>
|
||||
<td>{{ membership.role }}</td>
|
||||
<td>{% if membership.disabled_at %}<span class="badge badge--closed">Removed</span>{% else %}<span class="badge badge--open">Active</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="muted-text">No members.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">League settings</h2>
|
||||
<span class="panel__tag">Internal manage</span>
|
||||
</div>
|
||||
<div class="panel__body">
|
||||
<form method="post" action="{{ url_for('internal.update_league_settings', league_id=league.id) }}" class="form-card form-stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<label class="field-label" for="league-name">Name</label>
|
||||
<input class="field-input" id="league-name" name="name" value="{{ league.name }}" required>
|
||||
|
||||
<label class="field-label" for="league-description">Description</label>
|
||||
<textarea class="field-input" id="league-description" name="description" rows="3">{{ league.description or '' }}</textarea>
|
||||
|
||||
<label class="field-label" for="league-visibility">Visibility</label>
|
||||
<select class="field-input" id="league-visibility" name="visibility">
|
||||
<option value="private" {% if league.visibility == 'private' %}selected{% endif %}>Private</option>
|
||||
<option value="public" {% if league.visibility == 'public' %}selected{% endif %}>Public</option>
|
||||
</select>
|
||||
|
||||
<div class="inline-form">
|
||||
<div>
|
||||
<label class="field-label" for="eligible-min">Eligible minimum</label>
|
||||
<input class="field-input" id="eligible-min" type="number" min="1" max="100" name="eligible_min_sessions" value="{{ league.eligible_min_sessions }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label" for="break-even">Break-even dollars</label>
|
||||
<input class="field-input" id="break-even" type="number" min="0" max="100" step="0.01" name="break_even_dollars" value="{{ '%.2f'|format(league.break_even_cents / 100) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn--primary btn--sm" type="submit">Save settings</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="cards-2">
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">League actions</h2>
|
||||
<span class="panel__tag">Support tools</span>
|
||||
</div>
|
||||
<div class="panel__body form-stack">
|
||||
<form method="post" action="{{ url_for('internal.transfer_league_owner', league_id=league.id) }}" class="form-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<label class="field-label" for="new-owner-email">Transfer ownership to active user</label>
|
||||
<div class="inline-form">
|
||||
<input class="field-input" id="new-owner-email" type="email" name="new_owner_email" placeholder="owner@example.com" required>
|
||||
<button class="btn btn--warning btn--sm" type="submit">Transfer</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if league.archived_at %}
|
||||
<form method="post" action="{{ url_for('internal.restore_league', league_id=league.id) }}" class="form-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn--primary btn--sm" type="submit">Restore league</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('internal.archive_league', league_id=league.id) }}" class="form-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn--warning btn--sm" type="submit">Archive league</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('internal.delete_internal_league', league_id=league.id) }}" class="form-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<label class="field-label" for="confirm-delete-league">Type DELETE to permanently delete this league</label>
|
||||
<div class="inline-form">
|
||||
<input class="field-input" id="confirm-delete-league" name="confirm" autocomplete="off">
|
||||
<button class="btn btn--danger btn--sm" type="submit">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Leagues · Admin · myboker.org{% endblock %}
|
||||
{% block sidebar %}{% include "_internal_sidebar.html" %}{% endblock %}
|
||||
{% block page_class %}page--app{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="db-header int-page-header">
|
||||
<div>
|
||||
<h1 class="db-header__title">Leagues</h1>
|
||||
<p class="db-header__sub">Search by league name, slug, or owner email.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-list-tools">
|
||||
<form class="admin-searchbar" method="get" action="{{ url_for('internal.leagues') }}">
|
||||
<input class="field-input" type="search" name="q" value="{{ q }}" placeholder="Search league or owner">
|
||||
<button class="btn btn--primary btn--sm" type="submit">Search</button>
|
||||
{% if q %}<a class="btn btn--ghost btn--sm" href="{{ url_for('internal.leagues') }}">Clear</a>{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<section class="panel admin-list-panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Matching leagues</h2>
|
||||
<span class="panel__tag">{{ pagination.start }}-{{ pagination.end }} of {{ pagination.total }}</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>League</th>
|
||||
<th>Owner</th>
|
||||
<th>Visibility</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for league in leagues %}
|
||||
{% set owner = owners.get(league.id) %}
|
||||
<tr>
|
||||
<td class="admin-table__primary"><a href="{{ url_for('internal.league_detail', league_id=league.id) }}">{{ league.name }}</a></td>
|
||||
<td>{% if owner %}<a href="{{ url_for('internal.user_detail', user_id=owner.id) }}">{{ owner.email }}</a>{% else %}<span class="muted-text">Missing</span>{% endif %}</td>
|
||||
<td><span class="badge">{{ league.visibility }}</span></td>
|
||||
<td>{% if league.archived_at %}<span class="badge badge--closed">Archived</span>{% else %}<span class="badge badge--open">Active</span>{% endif %}</td>
|
||||
<td class="tbl__actions"><a class="btn btn--ghost btn--sm" href="{{ url_for('internal.league_detail', league_id=league.id) }}">View</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="muted-text">No leagues found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="pagination-row">
|
||||
{% if pagination.has_prev %}
|
||||
<a class="btn btn--ghost btn--sm" href="{{ url_for('internal.leagues', q=q, page=pagination.prev_page) }}">Previous</a>
|
||||
{% else %}
|
||||
<span class="btn btn--ghost btn--sm is-disabled">Previous</span>
|
||||
{% endif %}
|
||||
<span class="pagination-row__label">Page {{ pagination.page }} of {{ pagination.pages }}</span>
|
||||
{% if pagination.has_next %}
|
||||
<a class="btn btn--ghost btn--sm" href="{{ url_for('internal.leagues', q=q, page=pagination.next_page) }}">Next</a>
|
||||
{% else %}
|
||||
<span class="btn btn--ghost btn--sm is-disabled">Next</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,148 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ user.email }} · Admin · myboker.org{% endblock %}
|
||||
{% block sidebar %}{% include "_internal_sidebar.html" %}{% endblock %}
|
||||
{% block page_class %}page--app{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="db-header" style="margin-bottom:20px;">
|
||||
<div>
|
||||
<div class="db-header__chips">
|
||||
<a class="badge" href="{{ url_for('internal.users') }}">← Users</a>
|
||||
{% if user.disabled_at %}<span class="badge badge--closed">Disabled</span>{% else %}<span class="badge badge--open">Active</span>{% endif %}
|
||||
{% if user.is_site_admin %}<span class="badge badge--paid">Site admin</span>{% endif %}
|
||||
</div>
|
||||
<h1 class="db-header__title">{{ user.email }}</h1>
|
||||
<p class="db-header__sub">Created {{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else 'unknown' }}.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-stat-grid">
|
||||
<div class="db-stat">
|
||||
<span class="db-stat__num">{{ 'Yes' if user.email_verified_at else 'No' }}</span>
|
||||
<span class="db-stat__label">Email verified</span>
|
||||
</div>
|
||||
<div class="db-stat">
|
||||
<span class="db-stat__num">{{ memberships|length }}</span>
|
||||
<span class="db-stat__label">League memberships</span>
|
||||
</div>
|
||||
<div class="db-stat">
|
||||
<span class="db-stat__num">{{ owned_leagues|length }}</span>
|
||||
<span class="db-stat__label">Owned leagues</span>
|
||||
</div>
|
||||
<div class="db-stat">
|
||||
<span class="db-stat__num">{{ event_count }}</span>
|
||||
<span class="db-stat__label">Ledger events created</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cards-2">
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Account</h2>
|
||||
<span class="panel__tag">Support tools</span>
|
||||
</div>
|
||||
<div class="panel__body form-stack">
|
||||
<form method="post" action="{{ url_for('internal.update_user_email', user_id=user.id) }}" class="form-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<label class="field-label" for="email">Email</label>
|
||||
<div class="inline-form">
|
||||
<input class="field-input" id="email" type="email" name="email" value="{{ user.email }}" required>
|
||||
<button class="btn btn--primary btn--sm" type="submit">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="form-card">
|
||||
<p class="muted-text">Last login: {{ user.last_login_at.strftime('%Y-%m-%d %H:%M') if user.last_login_at else 'Never' }}</p>
|
||||
<div class="button-row">
|
||||
<form method="post" action="{{ url_for('internal.send_user_password_reset', user_id=user.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn--ghost btn--sm" type="submit" {% if user.disabled_at %}disabled{% endif %}>Email reset link</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('internal.create_user_temporary_password', user_id=user.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn--ghost btn--sm" type="submit" {% if user.disabled_at %}disabled{% endif %}>Email temp password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Danger zone</h2>
|
||||
<span class="panel__tag">Account state</span>
|
||||
</div>
|
||||
<div class="panel__body form-stack">
|
||||
{% if user.disabled_at %}
|
||||
<form method="post" action="{{ url_for('internal.restore_user', user_id=user.id) }}" class="form-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn--primary btn--sm" type="submit">Restore account</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('internal.disable_user', user_id=user.id) }}" class="form-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn--warning btn--sm" type="submit" {% if user.id == current_admin_id %}disabled{% endif %}>Disable account</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('internal.delete_user', user_id=user.id) }}" class="form-card">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<label class="field-label" for="confirm-delete-user">Type DELETE to permanently delete this account</label>
|
||||
<div class="inline-form">
|
||||
<input class="field-input" id="confirm-delete-user" name="confirm" autocomplete="off">
|
||||
<button class="btn btn--danger btn--sm" type="submit" {% if user.id == current_admin_id %}disabled{% endif %}>Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="cards-2">
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Memberships</h2>
|
||||
<span class="panel__tag">{{ memberships|length }}</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead><tr><th>League</th><th>Role</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for membership, league in memberships %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('internal.league_detail', league_id=league.id) }}">{{ league.name }}</a></td>
|
||||
<td>{{ membership.role }}</td>
|
||||
<td>{% if membership.disabled_at %}<span class="badge badge--closed">Removed</span>{% else %}<span class="badge badge--open">Active</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="muted-text">No memberships.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Owned leagues</h2>
|
||||
<span class="panel__tag">{{ owned_leagues|length }}</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl">
|
||||
<thead><tr><th>League</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for league in owned_leagues %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('internal.league_detail', league_id=league.id) }}">{{ league.name }}</a></td>
|
||||
<td>{% if league.archived_at %}<span class="badge badge--closed">Archived</span>{% else %}<span class="badge badge--open">Active</span>{% endif %}</td>
|
||||
<td class="tbl__actions"><a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.league_settings', league_ref=league.url_ref) }}">Manage</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="muted-text">No owned leagues.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,73 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Users · Admin · myboker.org{% endblock %}
|
||||
{% block sidebar %}{% include "_internal_sidebar.html" %}{% endblock %}
|
||||
{% block page_class %}page--app{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="db-header int-page-header">
|
||||
<div>
|
||||
<h1 class="db-header__title">Users</h1>
|
||||
<p class="db-header__sub">Search accounts by email and inspect account status.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-list-tools">
|
||||
<form class="admin-searchbar" method="get" action="{{ url_for('internal.users') }}">
|
||||
<input class="field-input" type="search" name="q" value="{{ q }}" placeholder="Search email">
|
||||
<button class="btn btn--primary btn--sm" type="submit">Search</button>
|
||||
{% if q %}<a class="btn btn--ghost btn--sm" href="{{ url_for('internal.users') }}">Clear</a>{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<section class="panel admin-list-panel">
|
||||
<div class="panel__head">
|
||||
<h2 class="panel__title">Matching users</h2>
|
||||
<span class="panel__tag">{{ pagination.start }}-{{ pagination.end }} of {{ pagination.total }}</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="tbl admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Verified</th>
|
||||
<th>Created</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="admin-table__primary">
|
||||
<a href="{{ url_for('internal.user_detail', user_id=user.id) }}">{{ user.email }}</a>
|
||||
{% if user.is_site_admin %}<span class="badge badge--paid">Admin</span>{% endif %}
|
||||
</td>
|
||||
<td>{% if user.disabled_at %}<span class="badge badge--closed">Disabled</span>{% else %}<span class="badge badge--open">Active</span>{% endif %}</td>
|
||||
<td>{% if user.email_verified_at %}Yes{% else %}<span class="muted-text">No</span>{% endif %}</td>
|
||||
<td class="admin-table__muted">{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'Unknown' }}</td>
|
||||
<td class="tbl__actions"><a class="btn btn--ghost btn--sm" href="{{ url_for('internal.user_detail', user_id=user.id) }}">View</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="muted-text">No users found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="pagination-row">
|
||||
{% if pagination.has_prev %}
|
||||
<a class="btn btn--ghost btn--sm" href="{{ url_for('internal.users', q=q, page=pagination.prev_page) }}">Previous</a>
|
||||
{% else %}
|
||||
<span class="btn btn--ghost btn--sm is-disabled">Previous</span>
|
||||
{% endif %}
|
||||
<span class="pagination-row__label">Page {{ pagination.page }} of {{ pagination.pages }}</span>
|
||||
{% if pagination.has_next %}
|
||||
<a class="btn btn--ghost btn--sm" href="{{ url_for('internal.users', q=q, page=pagination.next_page) }}">Next</a>
|
||||
{% else %}
|
||||
<span class="btn btn--ghost btn--sm is-disabled">Next</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user