Files
boker/templates/league_session_detail.html

383 lines
20 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ session_model.display_label }} · {{ league.name }} · myboker.org{% endblock %}
{% block subnav %}{% include "_league_subnav.html" %}{% endblock %}
{% block content %}
<div class="hero" style="padding-top:0;padding-bottom:12px;display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;justify-content:space-between;">
<div>
<h1 class="hero__title">{{ session_model.display_label }}</h1>
<p class="hero__sub">{{ session_model.session_date.isoformat() | pretty_date }} · <span class="status-badge status-{{ session_model.status }}">{{ session_model.status }}</span></p>
</div>
<div class="button-row">
<a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.session_public_view', league_ref=league.url_ref, session_id=session_model.id) }}">Public view</a>
{% if can_manage %}
<button class="btn btn--ghost btn--sm" type="button" data-modal="modal-edit-session">Edit session</button>
{% if session_model.status == 'open' %}
<form method="post" action="{{ url_for('leagues.close_league_session', league_ref=league.url_ref, session_id=session_model.id) }}" style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn--ghost btn--sm" type="submit">Close session</button>
</form>
{% else %}
<button class="btn btn--ghost btn--sm" type="button" data-modal="modal-reopen-session">Reopen session</button>
{% endif %}
{% endif %}
{% if is_owner %}
<button class="btn btn--danger btn--sm" type="button" data-modal="modal-delete-session">Delete session</button>
{% endif %}
</div>
</div>
<div class="cash-strip">
<div class="stat stat--rail" style="--rail:var(--accent);">
<span class="kicker">Invested</span>
<div class="stat__val" style="color:var(--num)">{{ session.total_invested_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--rank-2);">
<span class="kicker">Cashout</span>
<div class="stat__val" style="color:var(--num)">{{ session.total_cash_out_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--pos);">
<span class="kicker">Paid out</span>
<div class="stat__val" style="color:var(--pos)">{{ session.total_paid_out_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--warn);">
<span class="kicker">Open items</span>
<div class="stat__val" style="color:var(--num)">{{ (session.total_current_due_to_player_cents + session.total_current_due_to_house_cents) | money }}</div>
</div>
</div>
<div class="league-manage-grid" style="margin-top:20px;align-items:stretch;">
{% if can_manage %}
<form class="panel form-card" method="post" id="appendForm" style="align-self:stretch;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<p class="eyebrow">Append event</p>
<h2 class="panel__title" style="margin-top:4px;">Ledger entry</h2>
</div>
{% if session_model.status != 'open' %}
<div class="session-locked-banner">
<strong>Session closed</strong>
<span>Books assumed settled. Reopen to add events.</span>
</div>
{% endif %}
<fieldset {% if session_model.status != 'open' %}disabled{% endif %} style="border:none;padding:0;margin:0;min-width:0;">
<label>
<span>Player</span>
<input
type="text"
id="playerSearch"
list="playerList"
placeholder="Start typing a name…"
autocomplete="off"
value="{{ players | selectattr('id', 'equalto', append_form.player_id) | map(attribute='display_name') | first | default('') }}"
>
<datalist id="playerList">
{% for player in players %}
<option value="{{ player.display_name }}"></option>
{% endfor %}
</datalist>
<input type="hidden" name="player_id" id="playerIdInput" value="{{ append_form.player_id or '' }}">
</label>
<label>
<span>Event type</span>
<select name="event_type" id="eventTypeSelect" required>
{% for value, label in event_types %}
<option value="{{ value }}" {{ 'selected' if append_form.event_type == value else '' }}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>
<span>Amount</span>
<input type="number" step="0.01" min="0" name="amount" value="{{ append_form.amount }}" placeholder="0.00">
</label>
<label>
<span>Note <span class="form-opt-hint">(optional)</span></span>
<textarea name="note" placeholder="Optional context">{{ append_form.note or '' }}</textarea>
</label>
<button class="btn btn--primary" type="submit" style="margin-top:8px;">Append event</button>
</fieldset>
</form>
{% else %}
<div class="panel empty-panel">
<h2 class="panel__title">Read only</h2>
<p class="muted-text">Your role can inspect this session but cannot append ledger events.</p>
</div>
{% endif %}
<div class="panel">
<div class="panel__head">
<div>
<p class="kicker" style="margin:0;">Results</p>
<h2 class="panel__title">Player totals</h2>
</div>
<span class="panel__tag">{{ session.entries|length }} players</span>
</div>
{% if session.entries %}
<div class="player-ledger-list">
{% for entry in session.entries %}
{% set row_cls = 'player-ledger-row--pos' if entry.net_cents > 0 else 'player-ledger-row--neg' if entry.net_cents < 0 else '' %}
<div class="player-ledger-row {{ row_cls }}">
<div class="player-ledger-row__header">
<span class="player-ledger-row__name">{{ entry.player_name }}</span>
{% if entry.current_due_to_house_cents > 0 %}
<span class="badge badge--owes">Owes {{ entry.current_due_to_house_cents | money }}</span>
{% elif entry.current_due_to_player_cents > 0 %}
<span class="badge badge--unpaid">Due {{ entry.current_due_to_player_cents | money }}</span>
{% else %}
<span class="badge badge--settled">Settled</span>
{% endif %}
</div>
<div class="player-ledger-row__fields">
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Buy in</span>
<span class="player-ledger-row__val">{{ entry.invested_cents | money }}</span>
</div>
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Cashout claim</span>
<span class="player-ledger-row__val">{{ entry.cash_out_cents | money }}</span>
</div>
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Paid out</span>
<span class="player-ledger-row__val" style="color:var(--pos)">{{ entry.paid_out_cents | money if entry.paid_out_cents else '—' }}</span>
</div>
<div class="player-ledger-row__field player-ledger-row__field--net">
<span class="player-ledger-row__lbl">Net</span>
<span class="player-ledger-row__val {{ 'num-pos' if entry.net_cents > 0 else 'num-neg' if entry.net_cents < 0 else 'num-muted' }}">
{{ '+' if entry.net_cents > 0 else '' }}{{ entry.net_cents | money }}
</span>
</div>
</div>
{% if entry.rollover_in_cents or entry.rollover_out_cents or entry.payout_carry_in_cents %}
<div class="player-ledger-row__fields player-ledger-row__fields--secondary">
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Rolled in</span>
<span class="player-ledger-row__val" style="color:var(--rolled)">{{ entry.rollover_in_cents | money }}</span>
</div>
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Carry in</span>
<span class="player-ledger-row__val" style="color:var(--rolled)">{{ entry.payout_carry_in_cents | money }}</span>
</div>
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Rolled out</span>
<span class="player-ledger-row__val" style="color:var(--rolled)">{{ entry.rollover_out_cents | money }}</span>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="muted-text" style="padding:28px;">No player ledger events yet.</p>
{% endif %}
</div>
</div>
<div class="panel" style="margin-top:20px;">
<div class="panel__head">
<div>
<p class="kicker" style="margin:0;">Audit trail</p>
<h2 class="panel__title">Raw ledger events</h2>
</div>
<span class="badge badge--closed">append-only</span>
</div>
<div class="event-feed feed feed--inline" style="max-height:none;">
{% for event in raw_events | reverse %}
<div class="event-card event-card--inline {% if event.voided_at %}event-card--voided{% endif %}">
<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.voided_at %}<span class="void-badge">voided</span>{% endif %}
{% if event.note and not event.voided_at %}<span class="event-note">{{ event.note }}</span>{% endif %}
</div>
<p class="event-amount">{{ event.amount_cents | money }}</p>
{% if event.voided_at %}
<p class="tiny-text void-reason">{{ event.void_reason }}</p>
{% else %}
<p class="tiny-text">{{ event.created_at }}</p>
{% if is_owner %}
<button class="btn-void-toggle" type="button"
data-void-id="{{ event.id }}"
data-void-player="{{ event.player_name or 'Session' }}"
data-void-type="{{ event.event_type | replace('_', ' ') }}"
data-void-amount="${{ '%.2f' % (event.amount_cents / 100) }}"
data-void-action="{{ url_for('leagues.void_event', league_ref=league.url_ref, event_id=event.id) }}">
void
</button>
{% endif %}
{% endif %}
</div>
{% else %}
<p class="muted-text" style="padding:20px;">No events recorded.</p>
{% endfor %}
</div>
</div>
<!-- Void event modal -->
{% if is_owner %}
<div class="modal-backdrop" id="modal-void-event" hidden>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-void-title">
<div class="modal-card__head">
<span class="kicker" style="margin:0;color:var(--warn);">Correct the record</span>
<h2 class="panel__title" id="modal-void-title">Void event</h2>
</div>
<p class="modal-card__text" id="modal-void-desc">This event will remain visible in the audit trail but will no longer count toward any calculations.</p>
<form id="modal-void-form" method="post" action="">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px;">
<span style="font:600 12px/1 var(--font-ui);color:var(--text-2);text-transform:uppercase;letter-spacing:.04em;">Reason</span>
<input type="text" name="void_reason" placeholder="What was wrong with this event?" required maxlength="200"
style="background:var(--field);border:1px solid var(--border-strong);border-radius:var(--r-sm);padding:9px 12px;font:400 14px/1.4 var(--font-ui);color:var(--text-1);">
</label>
<div class="modal-card__actions">
<button class="btn btn--ghost btn--sm" type="button" data-close-modal>Cancel</button>
<button class="btn btn--danger btn--sm" type="submit">Void this event</button>
</div>
</form>
</div>
</div>
{% endif %}
<!-- Delete confirmation modal -->
<div class="modal-backdrop" id="modal-delete-session" hidden>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-delete-title">
<div class="modal-card__head">
<span class="kicker" style="margin:0;color:var(--neg);">Destructive action</span>
<h2 class="panel__title" id="modal-delete-title">Delete {{ session_model.display_label }}?</h2>
</div>
<p class="modal-card__text">This will permanently delete the session and <strong>all {{ session_model.event_count if session_model.event_count is defined else 'its' }} ledger events</strong> attached to it. There is no undo.</p>
<div class="modal-card__actions">
<button class="btn btn--ghost btn--sm" type="button" data-close-modal>Cancel</button>
<form method="post" action="{{ url_for('leagues.delete_session', league_ref=league.url_ref, session_id=session_model.id) }}" style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn--danger btn--sm" type="submit">Yes, delete permanently</button>
</form>
</div>
</div>
</div>
<!-- Edit session modal -->
{% if can_manage %}
<div class="modal-backdrop" id="modal-edit-session" hidden>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-edit-session-title">
<div class="modal-card__head">
<span class="kicker" style="margin:0;">Session</span>
<h2 class="panel__title" id="modal-edit-session-title">Edit session</h2>
</div>
<form method="post" action="{{ url_for('leagues.edit_session', league_ref=league.url_ref, session_id=session_model.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-card" style="padding:0 20px 4px;gap:12px;">
<label>
<span>Date</span>
<input type="date" name="session_date" value="{{ session_model.session_date.isoformat() }}" required>
</label>
<label>
<span>Label <span class="form-opt-hint">(optional)</span></span>
<input type="text" name="label" value="{{ session_model.label or '' }}" placeholder="e.g. Main table">
</label>
<label>
<span>Notes <span class="form-opt-hint">(optional)</span></span>
<input type="text" name="notes" value="{{ session_model.notes or '' }}" placeholder="Optional context">
</label>
</div>
<div class="modal-card__actions">
<button class="btn btn--ghost btn--sm" type="button" data-close-modal>Cancel</button>
<button class="btn btn--primary btn--sm" type="submit">Save changes</button>
</div>
</form>
</div>
</div>
{% endif %}
<!-- Reopen confirmation modal -->
<div class="modal-backdrop" id="modal-reopen-session" hidden>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-reopen-title">
<div class="modal-card__head">
<span class="kicker" style="margin:0;">Settled session</span>
<h2 class="panel__title" id="modal-reopen-title">Reopen this session?</h2>
</div>
<p class="modal-card__text">This session has been closed and books are assumed to be settled. Reopening will allow new ledger events to be appended and may affect settlement totals.</p>
<div class="modal-card__actions">
<button class="btn btn--ghost btn--sm" type="button" data-close-modal>Cancel</button>
<form method="post" action="{{ url_for('leagues.open_league_session', league_ref=league.url_ref, session_id=session_model.id) }}" style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn--primary btn--sm" type="submit">Reopen session</button>
</form>
</div>
</div>
</div>
<script>
// Player search — map display name → player id
const playerMap = {
{% for player in players %}"{{ player.display_name | replace('"', '\\"') }}": "{{ player.id }}"{% if not loop.last %},{% endif %}
{% endfor %}
};
const searchInput = document.getElementById('playerSearch');
const idInput = document.getElementById('playerIdInput');
const eventTypeSelect = document.getElementById('eventTypeSelect');
if (searchInput && idInput) {
searchInput.addEventListener('input', function () {
idInput.value = playerMap[this.value.trim()] || '';
});
// Also handle datalist selection which fires change
searchInput.addEventListener('change', function () {
idInput.value = playerMap[this.value.trim()] || '';
});
}
// Clear player field when event type is "note"
if (eventTypeSelect && searchInput && idInput) {
eventTypeSelect.addEventListener('change', function () {
if (this.value === 'note') {
searchInput.value = '';
idInput.value = '';
searchInput.placeholder = 'No player (session note)';
} else {
searchInput.placeholder = 'Start typing a name…';
}
});
// Set initial placeholder if note is selected
if (eventTypeSelect.value === 'note') {
searchInput.placeholder = 'No player (session note)';
}
}
// Modal logic
document.querySelectorAll('[data-modal]').forEach(function(btn) {
btn.addEventListener('click', function() {
var target = document.getElementById(this.dataset.modal);
if (target) target.removeAttribute('hidden');
});
});
document.querySelectorAll('[data-close-modal]').forEach(function(btn) {
btn.addEventListener('click', function() {
this.closest('.modal-backdrop').setAttribute('hidden', '');
});
});
document.querySelectorAll('.modal-backdrop').forEach(function(backdrop) {
backdrop.addEventListener('click', function(e) {
if (e.target === this) this.setAttribute('hidden', '');
});
});
// Void event modal
document.querySelectorAll('[data-void-id]').forEach(function(btn) {
btn.addEventListener('click', function() {
var player = this.dataset.voidPlayer;
var type = this.dataset.voidType;
var amount = this.dataset.voidAmount;
document.getElementById('modal-void-title').textContent = 'Void ' + type + ' — ' + player;
document.getElementById('modal-void-desc').textContent =
'The ' + amount + ' ' + type + ' event for ' + player +
' will be marked voided and excluded from all calculations. It stays visible in the audit trail.';
var form = document.getElementById('modal-void-form');
form.action = this.dataset.voidAction;
form.querySelector('[name=void_reason]').value = '';
document.getElementById('modal-void-event').removeAttribute('hidden');
});
});
</script>
{% endblock %}