Files
boker/templates/admin_dashboard.html
T

437 lines
21 KiB
HTML

{% extends "base.html" %}
{% block title %}Admin · myboker.org{% endblock %}
{% block page_class %}page--admin{% endblock %}
{% block content %}
<div class="hero" style="padding-bottom:12px;">
<div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;">
<p class="hero__kicker" style="margin:0;">Admin</p>
{% if open_sessions %}
<span class="live-badge">{{ open_sessions|length }} session{{ 's' if open_sessions|length != 1 else '' }} live</span>
{% endif %}
</div>
<h1 class="hero__title">Ledger controls</h1>
<p class="hero__sub">Append events to the audit trail. Player ledger history stays permanent.</p>
</div>
<!-- Banker cash/book position. Cash tiles are real money only; rollovers only
affect receivables/payables through the session settlement formulas. -->
<div class="cash-strip">
<div class="stat stat--rail" style="--rail:var(--accent);">
<span class="kicker">Cash in</span>
<div class="stat__val" style="color:var(--num)">{{ admin_totals.cash_in_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--pos);">
<span class="kicker">Cash paid out</span>
<div class="stat__val" style="color:var(--pos)">{{ admin_totals.cash_out_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--neg);">
<span class="kicker">House owes players</span>
<div class="stat__val {{ 'num-neg' if admin_totals.house_owes_players_cents > 0 else '' }}">{{ admin_totals.house_owes_players_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--owes);">
<span class="kicker">Players owe house</span>
<div class="stat__val {{ 'num-neg' if admin_totals.players_owe_house_cents > 0 else '' }}">{{ admin_totals.players_owe_house_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:{{ 'var(--pos)' if admin_totals.net_book_position_cents == 0 else 'var(--warn)' }};">
<span class="kicker">Net book position</span>
<div class="stat__val {% if admin_totals.net_book_position_cents == 0 %}num-muted{% elif admin_totals.net_book_position_cents > 0 %}num-pos{% else %}num-neg{% endif %}">
{{ admin_totals.net_book_position_cents | money }}
</div>
<div class="stat__sub">cash in - cash paid out + receivables - payables</div>
</div>
</div>
<!-- Admin workspace -->
<div class="admin-workspace">
<form class="panel append" method="post" action="{{ url_for('admin_dashboard') }}">
<div class="panel__head">
<h2 class="panel__title">Append ledger event</h2>
<span class="panel__tag">fast entry</span>
</div>
<div class="append__body">
<div class="append__grid">
<label class="field">
<span class="field__label">Session</span>
<select class="control" name="session_id" required>
<option value="">Select session</option>
{% for s in open_sessions %}
<option value="{{ s.session_id }}" {{ 'selected' if append_form.session_id == s.session_id else '' }}>
{{ session_label(s) }}{{ ' (live)' if s.status == 'open' else '' }}
</option>
{% endfor %}
</select>
</label>
<label class="field">
<span class="field__label">Player</span>
<input class="control" list="player_names_list" type="text" name="player_name" placeholder="Player name" value="{{ append_form.player_name }}" required>
<datalist id="player_names_list">
{% for name in player_names %}<option value="{{ name }}"></option>{% endfor %}
</datalist>
</label>
<label class="field">
<span class="field__label">Event type</span>
<select class="control" name="event_type" required id="eventTypeSelect">
{% for value, label, color in event_types %}
<option value="{{ value }}" data-color="{{ color }}" data-label="{{ label }}" {{ 'selected' if append_form.event_type == value else '' }}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label class="field">
<span class="field__label">Amount</span>
<span class="control control--money">
<span class="control__prefix">$</span>
<input name="amount" inputmode="decimal" placeholder="0" autofocus>
</span>
</label>
</div>
<label class="field append__note">
<span class="field__label">Note <span class="field__opt">(optional)</span></span>
<textarea class="control append__textarea" name="note" placeholder="e.g. fronted buy-in, settle next week"></textarea>
</label>
<div class="append__footer">
<button type="submit" class="btn btn--primary">Append event</button>
<span class="append__preview">Will record as
<span class="evtag-pill" id="eventTypePreview" style="color:{{ event_type_colors[append_form.event_type] }}">{{ event_type_labels[append_form.event_type] | upper }}</span>
</span>
</div>
{% if pending_carry_items %}
<div class="carry-recs">
<div class="micro">Recommended carry-ins</div>
{% for item in pending_carry_items %}
<div class="carry-rec">
<span>{{ item.player_name }}</span>
<strong>{{ item.amount_cents | money }}</strong>
{% if live_session and item.player_name in live_session_player_names %}
<form method="post" action="{{ url_for('admin_apply_payout_carry_in') }}">
<input type="hidden" name="session_id" value="{{ live_session.session_id }}">
<input type="hidden" name="player_name" value="{{ item.player_name }}">
<button class="table-action-button" type="submit">Apply</button>
</form>
{% elif live_session %}
<em>Add buy-in first</em>
{% else %}
<em>Open session</em>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
</div>
</form>
<div class="panel panel--pad session-control-panel">
<h2 class="panel__title panel__title--mb">Session controls</h2>
<div class="session-summary-strip">
<div>
<span class="micro">Today</span>
<strong>{{ admin_today }}</strong>
</div>
<div>
<span class="micro">Empty</span>
<strong>{{ empty_session_count }}</strong>
</div>
</div>
{% if live_session %}
<div class="livecard">
<div class="livecard__head">
<span class="dot dot--live"></span>
<span class="livecard__name">{{ session_label(live_session) }}</span>
<span class="livecard__no">{{ live_session_no }}</span>
</div>
<div class="livecard__metrics">
<div><div class="micro">Invested</div><div class="mono16">{{ live_session.total_invested_cents | money }}</div></div>
<div><div class="micro">Seated</div><div class="mono16">{{ live_session.entries|length }}</div></div>
<div><div class="micro">Open</div><div class="mono16 mono16--warn">{{ live_unresolved_count }}</div></div>
</div>
<form method="post" action="{{ url_for('admin_session_state') }}"
{% if live_unresolved_count > 0 %}data-close-warning="true"{% endif %}>
<input type="hidden" name="session_id" value="{{ live_session.session_id }}">
<button class="btn-close" type="submit" name="state" value="closed">Close session</button>
</form>
</div>
{% else %}
<div class="livecard livecard--empty">
<div class="livecard__head">
<span class="dot dot--idle"></span>
<span class="livecard__name">No open session</span>
<span class="livecard__no">idle</span>
</div>
<div class="livecard__metrics">
<div><div class="micro">Invested</div><div class="mono16">{{ 0 | money }}</div></div>
<div><div class="micro">Seated</div><div class="mono16">0</div></div>
<div><div class="micro">Open</div><div class="mono16">0</div></div>
</div>
</div>
{% endif %}
<form method="post" action="{{ url_for('admin_open_session') }}">
<input type="hidden" name="session_date" value="{{ admin_today }}">
<button class="btn-newsession {{ 'btn-newsession--primary' if not live_session else '' }}" type="submit">+ Open new session</button>
</form>
<form method="post" action="{{ url_for('admin_prune_empty_sessions') }}" class="prune-session-form">
<button class="secondary-button" type="submit" {% if empty_session_count == 0 %}disabled{% endif %}>
Prune empty sessions{% if empty_session_count > 0 %} ({{ empty_session_count }}){% endif %}
</button>
</form>
</div>
<div class="panel panel--pad import-export-panel">
<h2 class="panel__title" style="margin:0 0 4px">Import / export</h2>
<p class="csv__desc">Back up or migrate the raw event ledger.</p>
<form method="post" action="{{ url_for('admin_import_csv') }}" enctype="multipart/form-data" class="import-form">
<div class="csv__btns">
<a class="csv-btn-ghost" href="{{ url_for('admin_export_csv') }}">&darr; Export CSV</a>
<label class="csv-btn-ghost" for="csvTopImport">&uarr; Import CSV</label>
</div>
<input id="csvTopImport" type="file" accept=".csv,text/csv" name="csv_file" hidden onchange="this.form.submit()">
<label class="csv__drop" id="csvDrop" for="csvTopImport">Drop ledger.csv to import</label>
</form>
</div>
</div><!-- /.admin-workspace -->
<!-- Debt resolution -->
<div class="panel debt-panel">
<div class="panel__head">
<div>
<span class="kicker" style="margin:0;">Front receivables</span>
<h2 class="panel__title">Debt resolution</h2>
</div>
<div class="debt-summary">
<span class="badge badge--owes">Open {{ admin_totals.players_owe_house_cents | money }}</span>
<span class="badge badge--paid">Cash repaid {{ admin_totals.collected_cents | money }}</span>
<span class="badge badge--written_off">Written off {{ admin_totals.written_off_cents | money }}</span>
</div>
</div>
{% if debt_entries %}
<div class="debt-layout">
<div class="debt-list">
{% for item in debt_entries %}
{% set s = item.session %}{% set e = item.entry %}
<div class="debt-list-item">
<div>
<strong>{{ e.player_name }}</strong>
<span>{{ session_label(s) }}</span>
</div>
<b>{{ e.player_owes_cents | money }}</b>
</div>
{% endfor %}
</div>
<div class="debt-action-grid">
<form class="form-card debt-form" method="post" action="{{ url_for('admin_collect_front') }}">
<h3>Collect debt</h3>
<label>
<span>Outstanding debt</span>
<select name="debt_key" required>
<option value="">Select debt</option>
{% for item in debt_entries %}
{% set s = item.session %}{% set e = item.entry %}
<option value="{{ s.session_id }}||{{ e.player_name }}">
{{ e.player_name }} · {{ session_label(s) }} · owes {{ e.player_owes_cents | money }}
</option>
{% endfor %}
</select>
</label>
<label>
<span>Amount collected</span>
<input type="number" step="0.01" name="amount" placeholder="0.00" required>
</label>
<label>
<span>Note</span>
<textarea name="note" rows="2" placeholder="E-transfer received, cash, etc."></textarea>
</label>
<button class="primary-button" type="submit">Mark collected</button>
</form>
<form class="form-card debt-form" method="post" action="{{ url_for('admin_write_off_front') }}">
<h3>Write off debt</h3>
<label>
<span>Outstanding debt</span>
<select name="debt_key" required>
<option value="">Select debt</option>
{% for item in debt_entries %}
{% set s = item.session %}{% set e = item.entry %}
<option value="{{ s.session_id }}||{{ e.player_name }}">
{{ e.player_name }} · {{ session_label(s) }} · owes {{ e.player_owes_cents | money }}
</option>
{% endfor %}
</select>
</label>
<label>
<span>Amount to write off</span>
<input type="number" step="0.01" name="amount" placeholder="0.00" required>
</label>
<label>
<span>Note</span>
<textarea name="note" rows="2" placeholder="Won't be collected."></textarea>
</label>
<button class="secondary-button" type="submit">Write off debt</button>
</form>
</div>
</div>
{% else %}
<div class="debt-empty">
<div class="debt-empty__mark">$0</div>
<p>No open player debts right now.</p>
</div>
{% endif %}
</div>
<div class="admin-bottom-grid">
<div class="panel">
<div class="panel__head">
<div>
<span class="kicker" style="margin:0;">All sessions</span>
<h2 class="panel__title">Current totals</h2>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Session</th>
<th>Players</th>
<th>Cash in</th>
<th>Cash paid out</th>
<th>House owes players</th>
<th>Players owe house</th>
<th>Book position</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for s in recent_sessions %}
<tr>
<td><a href="{{ url_for('session_detail', session_id=s.session_id) }}">{{ session_label(s) }}</a></td>
<td style="font-family:var(--font-mono);color:var(--num)">{{ s.entries|length }}</td>
<td style="font-family:var(--font-mono);color:var(--num)">{{ s.total_banker_cash_in_cents | money }}</td>
<td style="font-family:var(--font-mono);color:var(--pos)">{{ s.total_banker_cash_out_cents | money }}</td>
<td class="{{ 'negative' if s.total_current_due_to_player_cents > 0 else '' }}" style="font-family:var(--font-mono)">{{ s.total_current_due_to_player_cents | money }}</td>
<td class="{{ 'negative' if s.total_current_due_to_house_cents > 0 else '' }}" style="font-family:var(--font-mono)">{{ s.total_current_due_to_house_cents | money }}</td>
<td class="{% if s.total_net_book_position_cents > 0 %}positive{% elif s.total_net_book_position_cents < 0 %}negative{% endif %}" style="font-family:var(--font-mono)">{{ s.total_net_book_position_cents | money }}</td>
<td><span class="status-badge status-{{ s.status }}">{{ s.status }}</span></td>
<td>
<form method="post" action="{{ url_for('admin_session_state') }}" class="inline-action-form">
<input type="hidden" name="session_id" value="{{ s.session_id }}">
{% if s.status == "open" %}
<button class="table-action-button" type="submit" name="state" value="closed">Close</button>
{% else %}
<button class="table-action-button" type="submit" name="state" value="open">Reopen</button>
{% endif %}
</form>
</td>
</tr>
{% else %}
<tr><td colspan="9">No sessions yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="panel">
<div class="panel__head">
<div>
<span class="kicker" style="margin:0;">Recent events</span>
<h2 class="panel__title">Ledger feed</h2>
</div>
<span class="badge badge--closed" style="font-size:9.5px;">append-only</span>
</div>
<div class="event-feed feed feed--inline">
{% for event in recent_events %}
<div class="event-card event-card--inline">
<div class="event-card-top">
<strong>{{ event.player_name or "Session" }}</strong>
<span class="pill pill-{{ event.event_type }}">{{ event.event_type | replace('_', ' ') }}</span>
{% if event.note %}<span class="event-note">{{ event.note }}</span>{% endif %}
</div>
<p class="event-amount">{{ event.amount_cents | money }}</p>
<p class="tiny-text">{{ event.session_date | pretty_date }}</p>
</div>
{% else %}
<p class="muted-text" style="padding:20px;">No recent events.</p>
{% endfor %}
</div>
</div>
</div>
<div class="modal-backdrop" id="closeSessionModal" hidden>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="closeSessionModalTitle">
<div class="modal-card__head">
<span class="kicker" style="margin:0;">Open settlement</span>
<h2 class="panel__title" id="closeSessionModalTitle">Close session?</h2>
</div>
<p class="modal-card__text">This session has unpaid or uncarried cashouts, or unresolved debts. Closing stops table entry, but the open settlement items will remain tracked.</p>
<div class="modal-card__actions">
<button class="secondary-button" type="button" id="cancelCloseSession">Keep open</button>
<button class="btn btn--primary" type="button" id="confirmCloseSession">Close session</button>
</div>
</div>
</div>
<script>
const eventTypeSelect = document.getElementById('eventTypeSelect');
const eventTypePreview = document.getElementById('eventTypePreview');
eventTypeSelect?.addEventListener('change', () => {
const option = eventTypeSelect.selectedOptions[0];
if (!option || !eventTypePreview) return;
eventTypePreview.style.color = option.dataset.color || 'var(--muted)';
eventTypePreview.textContent = (option.dataset.label || option.value).toUpperCase();
});
const csvDrop = document.getElementById('csvDrop');
const csvInput = document.getElementById('csvTopImport');
csvDrop?.addEventListener('dragover', (event) => {
event.preventDefault();
csvDrop.classList.add('is-drag');
});
csvDrop?.addEventListener('dragleave', () => csvDrop.classList.remove('is-drag'));
csvDrop?.addEventListener('drop', (event) => {
event.preventDefault();
csvDrop.classList.remove('is-drag');
if (!csvInput || !event.dataTransfer?.files?.length) return;
csvInput.files = event.dataTransfer.files;
csvInput.form?.submit();
});
const closeSessionModal = document.getElementById('closeSessionModal');
const cancelCloseSession = document.getElementById('cancelCloseSession');
const confirmCloseSession = document.getElementById('confirmCloseSession');
let pendingCloseForm = null;
document.querySelectorAll('form[data-close-warning="true"]').forEach((form) => {
form.addEventListener('submit', (event) => {
if (form.dataset.confirmed === 'true') return;
event.preventDefault();
pendingCloseForm = form;
closeSessionModal.hidden = false;
confirmCloseSession?.focus();
});
});
cancelCloseSession?.addEventListener('click', () => {
closeSessionModal.hidden = true;
pendingCloseForm = null;
});
closeSessionModal?.addEventListener('click', (event) => {
if (event.target !== closeSessionModal) return;
closeSessionModal.hidden = true;
pendingCloseForm = null;
});
confirmCloseSession?.addEventListener('click', () => {
if (!pendingCloseForm) return;
pendingCloseForm.dataset.confirmed = 'true';
pendingCloseForm.submit();
});
</script>
{% endblock %}