imrpoved session details page
This commit is contained in:
4 files changed
+222
-8
No files matched your search
@@ -14,6 +14,7 @@ from stats import (
|
||||
cumulative_profit_series,
|
||||
player_session_series,
|
||||
safe_date_label,
|
||||
session_breakdown_series,
|
||||
session_events,
|
||||
unique_player_names,
|
||||
)
|
||||
@@ -155,6 +156,7 @@ def session_detail(session_date: str) -> str:
|
||||
next_session=next_session,
|
||||
prev_session=prev_session,
|
||||
raw_events=session_events(events, session_date),
|
||||
chart_data=session_breakdown_series(target_session),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+105
-1
@@ -755,11 +755,102 @@ td a:hover {
|
||||
}
|
||||
|
||||
.leaderboard-layout,
|
||||
.session-layout,
|
||||
.admin-layout {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.session-layout {
|
||||
align-items: stretch;
|
||||
grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.85fr);
|
||||
}
|
||||
|
||||
.results-panel,
|
||||
.chart-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 430px;
|
||||
min-height: 430px;
|
||||
max-height: 430px;
|
||||
}
|
||||
|
||||
.results-panel .table-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: rgba(255, 160, 64, 0.88) rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.results-panel .table-scroll::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.results-panel .table-scroll::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.results-panel .table-scroll::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 186, 110, 0.95),
|
||||
rgba(255, 136, 48, 0.95)
|
||||
);
|
||||
border-radius: 999px;
|
||||
border: 2px solid rgba(10, 12, 18, 0.9);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 170, 90, 0.18),
|
||||
0 0 12px rgba(255, 140, 60, 0.18);
|
||||
}
|
||||
|
||||
.results-panel .table-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 200, 130, 1),
|
||||
rgba(255, 150, 60, 1)
|
||||
);
|
||||
}
|
||||
|
||||
.results-panel .table-scroll::-webkit-scrollbar-corner {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.chart-panel .chart-frame {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.session-results-table {
|
||||
min-width: 980px;
|
||||
}
|
||||
|
||||
.session-results-table th,
|
||||
.session-results-table td {
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-results-table th:last-child,
|
||||
.session-results-table td:last-child {
|
||||
min-width: 15rem;
|
||||
width: 18rem;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.session-results-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: rgba(16, 20, 28, 0.96);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
@@ -880,6 +971,19 @@ td a:hover {
|
||||
border: 1px solid rgba(203, 213, 225, 0.14);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 5.5rem;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.site-shell {
|
||||
width: min(100% - 24px, 100%);
|
||||
|
||||
@@ -78,6 +78,18 @@ class SessionSummary:
|
||||
def total_net_cents(self) -> int:
|
||||
return sum(entry.net_cents for entry in self.entries)
|
||||
|
||||
@property
|
||||
def total_payout_due_cents(self) -> int:
|
||||
return sum(entry.payout_due_cents for entry in self.entries)
|
||||
|
||||
@property
|
||||
def total_paid_cents(self) -> int:
|
||||
return sum(entry.paid_cents for entry in self.entries)
|
||||
|
||||
@property
|
||||
def total_remaining_cents(self) -> int:
|
||||
return sum(entry.payout_remaining_cents for entry in self.entries)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerStats:
|
||||
@@ -437,6 +449,20 @@ def player_session_series(
|
||||
}
|
||||
|
||||
|
||||
def session_breakdown_series(session: SessionSummary) -> dict[str, Any]:
|
||||
ordered_entries = sorted(
|
||||
session.entries,
|
||||
key=lambda entry: (entry.net_cents, entry.player_name.casefold()),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
return {
|
||||
"labels": [entry.player_name for entry in ordered_entries],
|
||||
"net_values": [round(entry.net_cents / 100, 2) for entry in ordered_entries],
|
||||
"net_colors": [net_tone(entry.net_cents) for entry in ordered_entries],
|
||||
}
|
||||
|
||||
|
||||
def session_events(events: list[EventRow], session_date: str) -> list[EventRow]:
|
||||
return [event for event in events if event["session_date"] == session_date]
|
||||
|
||||
|
||||
@@ -20,16 +20,35 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid three-col">
|
||||
<article class="panel stat-card">
|
||||
<span>Total cash-out due</span>
|
||||
<strong>{{ session.total_payout_due_cents | money }}</strong>
|
||||
</article>
|
||||
|
||||
<article class="panel stat-card">
|
||||
<span>Total paid</span>
|
||||
<strong class="positive">{{ session.total_paid_cents | money }}</strong>
|
||||
</article>
|
||||
|
||||
<article class="panel stat-card">
|
||||
<span>Total remaining</span>
|
||||
<strong class="{{ 'negative' if session.total_remaining_cents > 0 else 'positive' }}">
|
||||
{{ session.total_remaining_cents | money }}
|
||||
</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="grid two-col session-layout">
|
||||
<div class="panel">
|
||||
<div class="panel results-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">Results</p>
|
||||
<h2>Player totals</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<div class="table-wrap table-scroll">
|
||||
<table class="data-table session-results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
@@ -49,7 +68,7 @@
|
||||
<td>{{ entry.buy_in_cents | money }}</td>
|
||||
<td>{{ entry.cash_out_cents | money }}</td>
|
||||
<td>{{ entry.paid_cents | money }}</td>
|
||||
<td class="{{ 'positive' if entry.payout_remaining_cents == 0 else 'negative' if entry.payout_remaining_cents > 0 else '' }}">
|
||||
<td class="{{ 'negative' if entry.payout_remaining_cents > 0 else 'positive' }}">
|
||||
{{ entry.payout_remaining_cents | money }}
|
||||
</td>
|
||||
<td>
|
||||
@@ -78,7 +97,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel chart-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">Breakdown</p>
|
||||
<h2>Net by player</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-frame">
|
||||
<canvas id="sessionBreakdownChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">Audit trail</p>
|
||||
@@ -89,7 +121,7 @@
|
||||
{% for event in raw_events | reverse %}
|
||||
<article class="event-card">
|
||||
<div class="event-card-top">
|
||||
<strong>{{ event.player_name }}</strong>
|
||||
<strong>{{ event.player_name or "Session" }}</strong>
|
||||
<span class="pill pill-{{ event.event_type }}">{{ event.event_type }}</span>
|
||||
</div>
|
||||
<p class="event-amount">{{ event.amount_cents | money }}</p>
|
||||
@@ -102,6 +134,56 @@
|
||||
<p class="muted-text">No raw events recorded.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const sessionChartData = {{ chart_data | tojson }};
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
const textMuted = rootStyles.getPropertyValue('--text-muted').trim();
|
||||
const lineColor = rootStyles.getPropertyValue('--line').trim();
|
||||
|
||||
const sessionBreakdownCtx = document.getElementById('sessionBreakdownChart');
|
||||
if (sessionBreakdownCtx) {
|
||||
new Chart(sessionBreakdownCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: sessionChartData.labels,
|
||||
datasets: [{
|
||||
label: 'Session Net',
|
||||
data: sessionChartData.net_values,
|
||||
backgroundColor: sessionChartData.net_colors,
|
||||
borderColor: sessionChartData.net_colors,
|
||||
borderRadius: 8,
|
||||
borderSkipped: false,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: textMuted,
|
||||
usePointStyle: true,
|
||||
boxWidth: 10,
|
||||
boxHeight: 10,
|
||||
padding: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: textMuted, maxRotation: 0 },
|
||||
grid: { color: lineColor },
|
||||
},
|
||||
y: {
|
||||
ticks: { color: textMuted },
|
||||
grid: { color: lineColor },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user