fix layout + collect/writeoff debts

This commit is contained in:
SowinskiBraeden committed 2026-06-11 13:27:00 -07:00
1 parent 7ec95ee24a
commit 7e7b3a7011
8 files changed
+263 -35

No files matched your search

+3 -1
View File
@@ -12,7 +12,7 @@ The idea is pretty simple: public pages for stats and session history, plus a sm
- admin login for recording events
- open / closed session tracking
- payout tracking with `paid` events
- front debt write-offs with `front_writeoff` events
- front debt collections and write-offs with `front_collected` / `front_writeoff` events
- session and player charts
- CSV import / export from the admin page
- append-only `entries.csv` ledger instead of overwriting old rows
@@ -38,6 +38,7 @@ Current event types:
- `buyin`
- `front`
- `front_collected`
- `front_writeoff`
- `cashout`
- `paid`
@@ -52,6 +53,7 @@ A few examples:
- another `buyin` for a rebuy
- another `cashout` if chip counts are corrected later
- a `paid` event when someone is actually settled up
- a `front_collected` event when a front is repaid outside poker
- a `front_writeoff` event when a front will not be collected
- a `note` event for bookkeeping context
- `session_open` / `session_close` to mark whether a game night is still live
+71
View File
@@ -438,6 +438,74 @@ def admin_write_off_front() -> str:
return redirect(url_for("admin_dashboard"))
@app.post("/admin/collect-front")
def admin_collect_front() -> str:
if not is_admin():
flash("Admin login required.", "error")
return redirect(url_for("admin_login"))
debt_key = request.form.get("debt_key", "").strip()
note = request.form.get("note", "").strip()
amount_raw = request.form.get("amount", "0").strip()
try:
amount_cents = int(round(float(amount_raw) * 100))
except ValueError:
flash("Amount must be a number.", "error")
return redirect(url_for("admin_dashboard"))
if amount_cents <= 0:
flash("Collected amount must be greater than zero.", "error")
return redirect(url_for("admin_dashboard"))
try:
session_id, player_name = debt_key.split("||", 1)
except ValueError:
flash("Select a valid player debt.", "error")
return redirect(url_for("admin_dashboard"))
events = load_events(DATA_PATH)
sessions = build_session_summaries(events)
target = next(
(session for session in sessions if session.session_id == session_id),
None,
)
if target is None:
flash("Session not found.", "error")
return redirect(url_for("admin_dashboard"))
entry = next(
(entry for entry in target.entries if entry.player_name == player_name),
None,
)
if entry is None or entry.player_owes_cents <= 0:
flash("That player does not have an outstanding front debt.", "error")
return redirect(url_for("admin_dashboard"))
if amount_cents > entry.player_owes_cents:
flash(
f"Collection cannot exceed {cents_to_dollars(entry.player_owes_cents)}.",
"error",
)
return redirect(url_for("admin_dashboard"))
append_event(
DATA_PATH,
session_id=target.session_id,
session_date=target.session_date,
player_name=entry.player_name,
event_type="front_collected",
amount_cents=amount_cents,
note=note or "Front debt collected.",
actor=app.config["ADMIN_USERNAME"],
)
flash("Front debt collected.", "success")
return redirect(url_for("admin_dashboard"))
@app.post("/admin/open-session")
def admin_open_session() -> str:
if not is_admin():
@@ -537,6 +605,9 @@ def admin_dashboard():
"players_owe_cents": sum(
session.total_player_owes_cents for session in sessions
),
"collected_cents": sum(
session.total_front_collected_cents for session in sessions
),
"written_off_cents": sum(
session.total_front_writeoff_cents for session in sessions
),
+87 -4
View File
@@ -294,6 +294,14 @@ h2 {
line-height: 1.15;
}
h3 {
position: relative;
z-index: 1;
margin: 0;
font-size: 1.05rem;
line-height: 1.2;
}
.muted-text {
margin: 0;
color: var(--text-muted);
@@ -383,6 +391,10 @@ h2 {
min-height: 112px;
}
.compact-form textarea {
min-height: 92px;
}
.form-card input:focus,
.form-card select:focus,
.form-card textarea:focus {
@@ -394,6 +406,14 @@ h2 {
align-self: start;
}
.admin-layout {
grid-template-columns: minmax(420px, 1.15fr) minmax(360px, 0.85fr);
}
.admin-layout > .grid {
gap: 18px;
}
.panel-header {
position: relative;
z-index: 1;
@@ -763,11 +783,14 @@ td a:hover {
}
.leaderboard-layout,
.session-layout,
.admin-layout {
.session-layout {
align-items: stretch;
}
.admin-layout {
align-items: start;
}
.results-panel-full {
display: block;
}
@@ -958,6 +981,12 @@ td a:hover {
border: 1px solid rgba(251, 146, 60, 0.22);
}
.pill-front_collected {
color: #22c55e;
background: rgba(34, 197, 94, 0.12);
border: 1px solid rgba(34, 197, 94, 0.22);
}
.pill-front_writeoff {
color: #f87171;
background: rgba(248, 113, 113, 0.12);
@@ -995,6 +1024,12 @@ td a:hover {
border: 1px solid rgba(248, 113, 113, 0.24);
}
.status-collected {
color: #22c55e;
background: rgba(34, 197, 94, 0.12);
border: 1px solid rgba(34, 197, 94, 0.24);
}
.inline-action-form {
margin: 0;
}
@@ -1038,6 +1073,39 @@ td a:hover {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.debt-panel {
display: grid;
gap: 18px;
}
.debt-summary {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.debt-summary span {
display: inline-flex;
min-height: 34px;
align-items: center;
border: 1px solid var(--line);
border-radius: 999px;
padding: 0 12px;
color: var(--text-muted);
font-size: 0.86rem;
font-weight: 700;
}
.debt-action-grid {
position: relative;
z-index: 1;
display: grid;
gap: 18px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.import-form input[type="file"] {
width: 100%;
margin-top: 0.5rem;
@@ -1088,20 +1156,34 @@ td a:hover {
.admin-cash-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: repeat(6, minmax(150px, 1fr));
margin-bottom: 1rem;
}
@media (max-width: 1200px) {
@media (max-width: 1180px) {
.admin-cash-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1180px) {
.profile-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.admin-cash-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.debt-action-grid {
grid-template-columns: 1fr;
}
.debt-summary {
justify-content: flex-start;
}
}
@media (max-width: 520px) {
@@ -1135,6 +1217,7 @@ td a:hover {
.two-col,
.three-col,
.four-col,
.leaderboard-layout,
.session-layout,
.admin-layout {
+27 -2
View File
@@ -31,6 +31,7 @@ class SessionEntry:
player_name: str
buy_in_cents: int = 0
front_cents: int = 0
front_collected_cents: int = 0
front_writeoff_cents: int = 0
cash_out_cents: int = 0
paid_cents: int = 0
@@ -68,7 +69,7 @@ class SessionEntry:
@property
def player_owes_cents(self) -> int:
return max(self.raw_player_owes_cents - self.front_writeoff_applied_cents, 0)
return max(self.raw_player_owes_cents - self.front_resolved_cents, 0)
@property
def raw_player_owes_cents(self) -> int:
@@ -84,7 +85,18 @@ class SessionEntry:
@property
def front_writeoff_applied_cents(self) -> int:
return min(self.front_writeoff_cents, self.raw_player_owes_cents)
return min(
self.front_writeoff_cents,
max(self.raw_player_owes_cents - self.front_collected_applied_cents, 0),
)
@property
def front_collected_applied_cents(self) -> int:
return min(self.front_collected_cents, self.raw_player_owes_cents)
@property
def front_resolved_cents(self) -> int:
return self.front_collected_applied_cents + self.front_writeoff_applied_cents
@property
def payout_status(self) -> str:
@@ -92,6 +104,8 @@ class SessionEntry:
return "owes"
if self.front_writeoff_applied_cents > 0:
return "written_off"
if self.front_collected_applied_cents > 0:
return "collected"
if self.gross_payout_cents <= 0:
return "none"
if self.current_due_cents <= 0:
@@ -169,6 +183,10 @@ class SessionSummary:
def total_front_writeoff_cents(self) -> int:
return sum(entry.front_writeoff_applied_cents for entry in self.entries)
@property
def total_front_collected_cents(self) -> int:
return sum(entry.front_collected_applied_cents for entry in self.entries)
@property
def total_cash_in_cents(self) -> int:
return sum(entry.buy_in_cents for entry in self.entries)
@@ -196,6 +214,7 @@ class PlayerStats:
biggest_loss_cents: int
total_buy_in_cents: int
total_front_cents: int
total_front_collected_cents: int
total_front_writeoff_cents: int
current_player_owes_cents: int
total_rollover_in_cents: int
@@ -347,6 +366,8 @@ def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]:
entry.buy_in_cents += event["amount_cents"]
elif event_type == "front":
entry.front_cents += event["amount_cents"]
elif event_type == "front_collected":
entry.front_collected_cents += event["amount_cents"]
elif event_type == "front_writeoff":
entry.front_writeoff_cents += event["amount_cents"]
elif event_type == "rollover_in":
@@ -481,6 +502,9 @@ def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]:
total_buy_in = sum(entry.buy_in_cents for entry in entries)
total_front = sum(entry.front_cents for entry in entries)
total_front_collected = sum(
entry.front_collected_applied_cents for entry in entries
)
total_front_writeoff = sum(
entry.front_writeoff_applied_cents for entry in entries
)
@@ -507,6 +531,7 @@ def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]:
biggest_loss_cents=biggest_loss,
total_buy_in_cents=total_buy_in,
total_front_cents=total_front,
total_front_collected_cents=total_front_collected,
total_front_writeoff_cents=total_front_writeoff,
current_player_owes_cents=current_player_owes,
total_rollover_in_cents=total_rollover_in,
+1
View File
@@ -35,6 +35,7 @@ class EventRow(TypedDict):
VALID_EVENT_TYPES = {
"buyin",
"front",
"front_collected",
"front_writeoff",
"cashout",
"paid",
+60 -4
View File
@@ -113,9 +113,58 @@
</div>
</section>
<form class="panel form-card" method="post" action="{{ url_for('admin_write_off_front') }}">
</section>
</section>
<section class="panel debt-panel">
<div class="panel-header">
<div>
<p class="eyebrow">Front debt</p>
<h2>Write off debt</h2>
<h2>Debt resolution</h2>
</div>
<div class="debt-summary">
<span>Open {{ admin_totals.players_owe_cents | money }}</span>
<span>Collected {{ admin_totals.collected_cents | money }}</span>
<span>Written off {{ admin_totals.written_off_cents | money }}</span>
</div>
</div>
{% if debt_entries %}
<div class="debt-action-grid">
<form class="form-card compact-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 session = item.session %}
{% set entry = item.entry %}
<option value="{{ session.session_id }}||{{ entry.player_name }}">
{{ entry.player_name }} · {{ session_label(session) }} · owes {{ entry.player_owes_cents | money }}
</option>
{% endfor %}
</select>
</label>
<label>
<span>Amount collected</span>
<input type="number" step="0.01" name="amount" placeholder="5.00" required>
</label>
<label>
<span>Note</span>
<textarea name="note" rows="3" placeholder="E-transfer received, cash collected, etc."></textarea>
</label>
<div class="button-row">
<button class="primary-button" type="submit">Mark collected</button>
</div>
</form>
<form class="form-card compact-form" method="post" action="{{ url_for('admin_write_off_front') }}">
<h3>Write off debt</h3>
<label>
<span>Outstanding debt</span>
@@ -145,7 +194,10 @@
<button class="secondary-button" type="submit">Write off front</button>
</div>
</form>
</section>
</div>
{% else %}
<p class="muted-text">No outstanding front debt.</p>
{% endif %}
</section>
<section class="grid admin-cash-grid">
@@ -205,6 +257,7 @@
<th>Paid out</th>
<th>Still owed</th>
<th>Players owe</th>
<th>Collected</th>
<th>Written off</th>
<th>Open balance</th>
<th>Status</th>
@@ -228,6 +281,9 @@
<td class="{{ 'negative' if session.total_player_owes_cents > 0 else '' }}">
{{ session.total_player_owes_cents | money }}
</td>
<td class="{{ 'positive' if session.total_front_collected_cents > 0 else '' }}">
{{ session.total_front_collected_cents | money }}
</td>
<td class="{{ 'negative' if session.total_front_writeoff_cents > 0 else '' }}">
{{ session.total_front_writeoff_cents | money }}
</td>
@@ -252,7 +308,7 @@
</tr>
{% else %}
<tr>
<td colspan="10">No sessions yet.</td>
<td colspan="11">No sessions yet.</td>
</tr>
{% endfor %}
</tbody>
+9 -23
View File
@@ -9,33 +9,13 @@
</div>
</section>
<section class="grid three-col">
<section class="grid three-col profile-summary-grid">
<article class="panel stat-card"><span>Total Net</span><strong class="{{ 'positive' if player.total_net_cents > 0 else 'negative' if player.total_net_cents < 0 else 'neutral' }}">{{ player.total_net_cents | money }}</strong></article>
<article class="panel stat-card"><span>Win %</span><strong>{{ "%.1f"|format(player.win_pct) }}%</strong></article>
<article class="panel stat-card"><span>ROI</span><strong>{{ "%.1f"|format(player.roi_pct) }}%</strong></article>
<article class="panel stat-card"><span>Total invested</span><strong>{{ player.total_invested_cents | money }}</strong></article>
<article class="panel stat-card"><span>Paid Buy-in</span><strong>{{ player.total_buy_in_cents | money }}</strong></article>
<article class="panel stat-card"><span>Fronted</span><strong>{{ player.total_front_cents | money }}</strong></article>
<article class="panel stat-card"><span>Current debt</span><strong class="{{ 'negative' if player.current_player_owes_cents > 0 else '' }}">{{ player.current_player_owes_cents | money }}</strong></article>
<article class="panel stat-card"><span>Written off</span><strong class="{{ 'negative' if player.total_front_writeoff_cents > 0 else '' }}">{{ player.total_front_writeoff_cents | money }}</strong></article>
<!--<article class="panel stat-card"><span>Rolled in</span><strong>{{ player.total_rollover_in_cents | money }}</strong></article>-->
<article class="panel stat-card"><span>Cash-outs</span><strong>{{ player.total_cash_out_cents | money }}</strong></article>
<article class="panel stat-card"><span>Current debt</span><strong class="{{ 'negative' if player.current_player_owes_cents > 0 else '' }}">{{ player.current_player_owes_cents | money }}</strong></article>
<article class="panel stat-card"><span>Win %</span><strong>{{ "%.1f"|format(player.win_pct) }}%</strong></article>
<article class="panel stat-card"><span>Sessions</span><strong>{{ player.sessions_played }}</strong></article>
<article class="panel stat-card">
<span>Current Streak</span>
<strong>
{% if player.current_win_streak > 0 %}
W{{ player.current_win_streak }}
{% elif player.current_loss_streak > 0 %}
L{{ player.current_loss_streak }}
{% else %}
-
{% endif %}
</strong>
</article>
<!--<article class="panel stat-card"><span>Longest win streak</span><strong>{{ player.longest_win_streak }}</strong></article>
<article class="panel stat-card"><span>Longest loss streak</span><strong>{{ player.longest_loss_streak }}</strong></article>-->
</section>
<section class="grid one-col">
@@ -87,12 +67,18 @@
<div class="table-wrap">
<table>
<tbody>
<tr><th>Total Net</th><td>{{ player.total_net_cents | money }}</td></tr>
<tr><th>Win %</th><td>{{ "%.1f"|format(player.win_pct) }}%</td></tr>
<tr><th>ROI</th><td>{{ "%.1f"|format(player.roi_pct) }}%</td></tr>
<tr><th>Sessions</th><td>{{ player.sessions_played }}</td></tr>
<tr><th>Paid Buy-ins</th><td>{{ player.total_buy_in_cents | money }}</td></tr>
<tr><th>Fronted</th><td>{{ player.total_front_cents | money }}</td></tr>
<tr><th>Debt Collected</th><td>{{ player.total_front_collected_cents | money }}</td></tr>
<tr><th>Current Debt</th><td>{{ player.current_player_owes_cents | money }}</td></tr>
<tr><th>Written Off</th><td>{{ player.total_front_writeoff_cents | money }}</td></tr>
<tr><th>Rolled In</th><td>{{ player.total_rollover_in_cents | money }}</td></tr>
<tr><th>Total Invested</th><td>{{ player.total_invested_cents | money }}</td></tr>
<tr><th>Cash-outs</th><td>{{ player.total_cash_out_cents | money }}</td></tr>
<tr><th>Rolled Out</th><td>{{ player.total_rollover_out_cents | money }}</td></tr>
<tr><th>Winning Sessions</th><td>{{ player.winning_sessions }}</td></tr>
<tr><th>Losing Sessions</th><td>{{ player.losing_sessions }}</td></tr>
+5 -1
View File
@@ -94,6 +94,7 @@
<th class="detail-col">Gross due</th>
<th class="detail-col">Paid</th>
<th class="detail-col">Rolled out</th>
<th class="detail-col">Collected</th>
<th class="detail-col">Written off</th>
<th>Still owed</th>
<th>Status</th>
@@ -128,13 +129,16 @@
<td class="detail-col">{{ entry.paid_cents | money }}</td>
<td class="detail-col">{{ entry.rollover_out_cents | money }}</td>
<td class="detail-col">{{ entry.front_collected_applied_cents | money }}</td>
<td class="detail-col">{{ entry.front_writeoff_applied_cents | money }}</td>
<td class="{% if entry.player_owes_cents > 0 %}negative{% elif entry.front_writeoff_applied_cents > 0 %}negative{% elif entry.current_due_cents > 0 %}negative{% else %}positive{% endif %}">
<td class="{% if entry.player_owes_cents > 0 %}negative{% elif entry.front_writeoff_applied_cents > 0 %}negative{% elif entry.front_collected_applied_cents > 0 %}positive{% elif entry.current_due_cents > 0 %}negative{% else %}positive{% endif %}">
{% if entry.player_owes_cents > 0 %}
Owes {{ entry.player_owes_cents | money }}
{% elif entry.front_writeoff_applied_cents > 0 %}
Written off {{ entry.front_writeoff_applied_cents | money }}
{% elif entry.front_collected_applied_cents > 0 %}
Collected {{ entry.front_collected_applied_cents | money }}
{% else %}
{{ entry.current_due_cents | money }}
{% endif %}