Files
boker/templates/league_dashboard.html

345 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ league.name }} · myboker.org{% endblock %}
{% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
{% block page_class %}page--app{% endblock %}
{% block content %}
{# ── Header ── #}
<div class="db-header">
<div class="db-header__info">
<div class="db-header__chips">
<span class="db-chip">{{ league.visibility }}</span>
{% if is_owner %}<span class="db-chip db-chip--accent">Owner</span>{% elif can_manage %}<span class="db-chip db-chip--accent">Manager</span>{% endif %}
</div>
<h1 class="db-header__title">{{ league.name }}</h1>
{% if league.description %}<p class="db-header__sub">{{ league.description }}</p>{% endif %}
</div>
{% if can_manage %}
<div class="db-header__actions">
<a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.league_settings', league_ref=league.url_ref) }}">Settings</a>
<a class="btn btn--primary btn--sm" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">New session</a>
</div>
{% endif %}
</div>
{# ── Live session banner ── #}
{% if live_session %}
<div class="db-live-banner">
<div class="db-live-banner__top">
<div class="db-live-banner__status">
<span class="db-live-dot"></span>
<span class="db-live-label">Live now</span>
<span class="db-live-name">{{ session_label(live_session) }}</span>
{% if live_session.opened_at %}
<span class="db-live-since">open since {{ live_session.opened_at[:5] }}</span>
{% endif %}
</div>
<a class="btn btn--primary btn--sm" href="{{ url_for('leagues.session_detail', league_ref=league.url_ref, session_id=live_session.session_id) }}">Open session →</a>
</div>
<div class="db-live-banner__stats">
<div class="db-live-stat">
<span class="db-live-stat__label">Seated</span>
<strong class="db-live-stat__val">{{ live_session.entries | length }}</strong>
</div>
<div class="db-live-stat">
<span class="db-live-stat__label">On the table</span>
<strong class="db-live-stat__val db-live-stat__val--accent">{{ live_session.total_real_cash_in_cents | money }}</strong>
</div>
<div class="db-live-stat">
<span class="db-live-stat__label">Buy-ins</span>
<strong class="db-live-stat__val">{{ live_session.total_buy_in_cents | money }}</strong>
</div>
{% if live_session.entries %}
<div class="db-live-stat db-live-stat--players">
<span class="db-live-stat__label">At the table</span>
<span class="db-live-stat__names">{{ live_session.entries | map(attribute='player_name') | join(' · ') }}</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
{# ── Overview stats strip ── #}
<div class="db-overview-strip">
<div class="db-overview-stat db-overview-stat--players">
<span class="db-overview-stat__label">Players</span>
<strong class="db-overview-stat__val">{{ counts.players }}</strong>
<span class="db-overview-stat__sub">{{ counts.players }} active</span>
</div>
<div class="db-overview-stat db-overview-stat--sessions">
<span class="db-overview-stat__label">Sessions</span>
<strong class="db-overview-stat__val">{{ total_sessions }}</strong>
{% if counts.open_sessions > 0 %}<span class="db-overview-stat__sub" style="color:var(--pos);">{{ counts.open_sessions }} live now</span>{% else %}<span class="db-overview-stat__sub">all closed</span>{% endif %}
</div>
<div class="db-overview-stat db-overview-stat--cashin">
<span class="db-overview-stat__label">Cash in</span>
<strong class="db-overview-stat__val">{{ cash_stats.total_cash_in | money }}</strong>
<span class="db-overview-stat__sub">across {{ total_sessions }} sessions</span>
</div>
<div class="db-overview-stat db-overview-stat--paidout">
<span class="db-overview-stat__label">Paid out</span>
<strong class="db-overview-stat__val">{{ cash_stats.total_paid_out | money }}</strong>
<span class="db-overview-stat__sub">to {{ counts.players }} players</span>
</div>
<div class="db-overview-stat">
<span class="db-overview-stat__label">House balance</span>
<strong class="db-overview-stat__val {{ 'num-neg' if house_balance_cents > 0 else 'num-pos' if house_balance_cents < 0 else '' }}">
{{ '+' if house_balance_cents < 0 else ('' if house_balance_cents > 0 else '') }}{{ house_balance_cents | abs | money }}
</strong>
<span class="db-overview-stat__sub">{{ 'paid out over cash in' if house_balance_cents > 0 else 'cash in over paid out' if house_balance_cents < 0 else 'balanced' }}</span>
</div>
</div>
{# ── League averages ── #}
{% if cash_stats.has_data %}
<div class="db-averages">
<p class="db-averages__title">League averages</p>
<div class="db-averages__grid">
<div class="db-avg-row">
<div class="db-avg-item">
<span class="db-avg-label">Avg pot / session</span>
<strong class="db-avg-val">{{ cash_stats.avg_pot | money }}</strong>
</div>
<div class="db-avg-item">
<span class="db-avg-label">Avg players</span>
<strong class="db-avg-val">{{ cash_stats.avg_players }}</strong>
</div>
<div class="db-avg-item">
<span class="db-avg-label">Avg buy-in</span>
<strong class="db-avg-val">{{ cash_stats.avg_buyin | money }}</strong>
</div>
<div class="db-avg-item">
<span class="db-avg-label">Biggest session</span>
<strong class="db-avg-val">{{ cash_stats.biggest_pot | money }}</strong>
</div>
{% if cash_stats.avg_sessions_per_month > 0 %}
<div class="db-avg-item">
<span class="db-avg-label">Sessions / month</span>
<strong class="db-avg-val">{{ cash_stats.avg_sessions_per_month }}</strong>
</div>
{% endif %}
</div>
<div class="db-avg-row">
{% if cash_stats.total_fronts > 0 %}
<div class="db-avg-item">
<span class="db-avg-label">Fronts issued</span>
<strong class="db-avg-val">{{ cash_stats.total_fronts | money }}</strong>
</div>
{% endif %}
<div class="db-avg-item">
<span class="db-avg-label">Ledger events</span>
<strong class="db-avg-val">{{ counts.events }}</strong>
</div>
<div class="db-avg-item">
<span class="db-avg-label">Seasons</span>
<strong class="db-avg-val">{{ counts.seasons }}</strong>
</div>
</div>
</div>
</div>
{% endif %}
{# ── Session history heatmap ── #}
{% if cash_stats.has_sessions %}
<div class="db-cal"
id="db-cal"
data-sessions='{{ cash_stats.session_map | tojson }}'
data-first-year="{{ cash_stats.first_session_year }}">
<div class="db-cal__hdr">
<div>
<span class="kicker" style="margin:0 0 2px;display:block;">Session history</span>
<span class="db-cal__meta">{{ total_sessions }} session{{ 's' if total_sessions != 1 }} · {{ cash_stats.total_cash_in | money }} in pots</span>
</div>
<div class="db-cal__nav-group">
<button class="db-cal__nav" id="db-cal-prev" aria-label="Previous year">&#8249;</button>
<span class="db-cal__year" id="db-cal-year"></span>
<button class="db-cal__nav" id="db-cal-next" aria-label="Next year">&#8250;</button>
</div>
</div>
<div class="db-cal__scroll"><div class="db-cal__grid" id="db-cal-grid"></div></div>
<div class="db-cal__tip" id="db-cal-tip" hidden></div>
</div>
<script>
(function () {
var cal = document.getElementById('db-cal');
if (!cal) return;
var sessions = JSON.parse(cal.dataset.sessions);
var firstYear = parseInt(cal.dataset.firstYear) || new Date().getFullYear();
var MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var today = new Date(); today.setHours(0,0,0,0);
var todayStr = today.toISOString().slice(0,10);
var currentYear = today.getFullYear();
var year = currentYear;
var grid = document.getElementById('db-cal-grid');
var yearLbl = document.getElementById('db-cal-year');
var prevBtn = document.getElementById('db-cal-prev');
var nextBtn = document.getElementById('db-cal-next');
var tip = document.getElementById('db-cal-tip');
function ds(d) { return d.toISOString().slice(0,10); }
function buildCalendar(y) {
var start = new Date(y, 0, 1);
var dow = start.getDay();
start.setDate(start.getDate() - (dow === 0 ? 6 : dow - 1));
var end = new Date(y, 11, 31);
var edow = end.getDay();
if (edow !== 0) end.setDate(end.getDate() + (7 - edow));
var weeks = [], cur = new Date(start);
while (cur <= end) {
var days = [], monthLabel = '';
for (var i = 0; i < 7; i++) {
var d = new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + i);
var s = ds(d);
if (d.getDate() === 1 && !monthLabel) monthLabel = MONTHS[d.getMonth()];
days.push({ date: s, count: sessions[s] || 0, future: s > todayStr, out: d.getFullYear() !== y });
}
weeks.push({ days: days, month: monthLabel });
cur.setDate(cur.getDate() + 7);
}
return weeks;
}
function render(y) {
var weeks = buildCalendar(y);
grid.innerHTML = '';
weeks.forEach(function(week) {
var mc = document.createElement('div');
mc.className = 'db-cal__mc';
mc.textContent = week.month;
grid.appendChild(mc);
week.days.forEach(function(day) {
var cell = document.createElement('div');
var cls = 'db-cal__cell';
if (day.out) cls += ' db-cal__cell--out';
else if (day.future) cls += ' db-cal__cell--dim';
else if (day.count) cls += ' db-cal__cell--on' + (day.count > 1 ? ' db-cal__cell--multi' : '');
cell.className = cls;
cell.dataset.date = day.date;
cell.dataset.count = day.count;
grid.appendChild(cell);
});
});
yearLbl.textContent = y;
prevBtn.disabled = y <= firstYear;
nextBtn.disabled = y >= currentYear;
}
function showTip(cell, e) {
var d = cell.dataset.date;
if (!d) return;
var parts = d.split('-').map(Number);
var dateObj = new Date(parts[0], parts[1] - 1, parts[2]);
var label = dateObj.toLocaleDateString('en-US', { weekday:'short', month:'short', day:'numeric', year:'numeric' });
var count = parseInt(cell.dataset.count) || 0;
tip.textContent = count ? label + ' · ' + count + ' session' + (count > 1 ? 's' : '') : label;
tip.hidden = false;
moveTip(e);
}
function moveTip(e) {
var tw = tip.offsetWidth, th = tip.offsetHeight;
var x = e.clientX - tw / 2, y = e.clientY - th - 10;
x = Math.max(8, Math.min(x, window.innerWidth - tw - 8));
if (y < 8) y = e.clientY + 14;
tip.style.left = x + 'px';
tip.style.top = y + 'px';
}
grid.addEventListener('mouseover', function(e) {
var cell = e.target.closest('.db-cal__cell');
if (cell && !cell.classList.contains('db-cal__cell--out')) showTip(cell, e);
else tip.hidden = true;
});
grid.addEventListener('mousemove', function(e) { if (!tip.hidden) moveTip(e); });
grid.addEventListener('mouseleave', function() { tip.hidden = true; });
prevBtn.addEventListener('click', function() { if (year > firstYear) render(--year); });
nextBtn.addEventListener('click', function() { if (year < currentYear) render(++year); });
render(year);
})();
</script>
{% endif %}
{# ── Recent sessions + Leaderboard ── #}
<div class="db-two-col">
<div class="db-panel">
<div class="db-panel__head">
<span class="db-panel__title">Recent sessions</span>
<a class="db-panel__link" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">All {{ total_sessions }} →</a>
</div>
{% if recent_sessions %}
<div class="db-table">
{% for s in recent_sessions %}
<a class="db-session-row{% if s.is_open %} db-session-row--live{% endif %}" href="{{ url_for('leagues.session_detail', league_ref=league.url_ref, session_id=s.session_id) }}">
<span class="db-session-row__num">{{ session_label(s) }}</span>
<span class="db-session-row__meta">{{ s.entries | length }} player{{ 's' if s.entries | length != 1 }}{% if s.is_open %} · <span class="db-live-tag">live</span>{% endif %}</span>
<strong class="db-session-row__pot {{ 'db-session-row__pot--live' if s.is_open }}">{{ s.total_real_cash_in_cents | money }}</strong>
</a>
{% endfor %}
</div>
{% else %}
<p class="db-panel__empty">No sessions yet.</p>
{% endif %}
</div>
<div class="db-panel">
<div class="db-panel__head">
<span class="db-panel__title">Leaderboard <span class="db-panel__sub">Net</span></span>
<a class="db-panel__link" href="{{ url_for('leagues.leaderboard', league_ref=league.url_ref) }}">Full board →</a>
</div>
{% if leaderboard_top %}
<div class="db-table">
{% for player in leaderboard_top %}
<div class="db-leader-row db-leader-row--{{ loop.index }}">
<span class="db-leader-rank">{{ loop.index }}</span>
<span class="db-leader-name">{{ player.player_name }}</span>
<span class="db-leader-sess">{{ player.sessions_played }} sess</span>
<strong class="db-leader-net {{ 'num-pos' if player.total_net_cents > 0 else 'num-neg' if player.total_net_cents < 0 else 'num-muted' }}">
{{ '+' if player.total_net_cents > 0 else '' }}{{ player.total_net_cents | money }}
</strong>
</div>
{% endfor %}
</div>
{% else %}
<p class="db-panel__empty">No data yet.</p>
{% endif %}
</div>
</div>
{# ── Quick nav links (flat, no card background) ── #}
<div class="db-quicknav">
<a class="db-quicknav__item" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">
<div class="db-quicknav__title">Sessions {% if counts.open_sessions > 0 %}<span class="live-badge">{{ counts.open_sessions }} live</span>{% endif %} →</div>
<div class="db-quicknav__desc">Buy-ins, cashouts, live session ledger</div>
</a>
<a class="db-quicknav__item" href="{{ url_for('leagues.players', league_ref=league.url_ref) }}">
<div class="db-quicknav__title">Players →</div>
<div class="db-quicknav__desc">Session history, net trend</div>
</a>
<a class="db-quicknav__item" href="{{ url_for('leagues.leaderboard', league_ref=league.url_ref) }}">
<div class="db-quicknav__title">Leaderboard →</div>
<div class="db-quicknav__desc">Net profit, win rate, rank delta</div>
</a>
<a class="db-quicknav__item" href="{{ url_for('leagues.seasons', league_ref=league.url_ref) }}">
<div class="db-quicknav__title">Seasons →</div>
<div class="db-quicknav__desc">Browse by season, standings</div>
</a>
{% if can_manage %}
<a class="db-quicknav__item" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">
<div class="db-quicknav__title">Ledger →</div>
<div class="db-quicknav__desc">House balance, fronts, open items</div>
</a>
{% endif %}
</div>
{% endblock %}