From 5d10136ee4c0717360de279d35d42464a94df06f Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Sat, 21 Mar 2026 00:10:52 -0700 Subject: [PATCH] add session open/closed state to prevent modifications --- app.py | 46 ++++++++++++++++++++++- static/css/style.css | 68 ++++++++++++++++++++++++++++++++++ stats.py | 65 +++++++++++++++++++++++++------- storage.py | 6 ++- templates/admin_dashboard.html | 38 ++++++++++++++++++- templates/sessions.html | 4 ++ 6 files changed, 210 insertions(+), 17 deletions(-) diff --git a/app.py b/app.py index 72159bc..0efde3b 100644 --- a/app.py +++ b/app.py @@ -204,6 +204,36 @@ def admin_logout() -> str: return redirect(url_for("leaderboard")) +@app.post("/admin/session-state") +def admin_session_state() -> str: + if not is_admin(): + flash("Admin login required.", "error") + return redirect(url_for("admin_login")) + + session_date = request.form.get("session_date", "").strip() + state = request.form.get("state", "").strip() + + if not session_date: + flash("Session date is required.", "error") + return redirect(url_for("admin_dashboard")) + + if state not in {"open", "closed"}: + flash("Invalid session state.", "error") + return redirect(url_for("admin_dashboard")) + + append_event( + DATA_PATH, + session_date=session_date, + player_name="", + event_type="session_open" if state == "open" else "session_close", + amount_cents=0, + note=f"Session marked {state}.", + actor=app.config["ADMIN_USERNAME"], + ) + flash(f"Session marked {state}.", "success") + return redirect(url_for("admin_dashboard")) + + @app.route("/admin", methods=["GET", "POST"]) def admin_dashboard() -> str: if not is_admin(): @@ -227,6 +257,17 @@ def admin_dashboard() -> str: flash("Amount must be a valid number.", "error") return redirect(url_for("admin_dashboard")) + events = load_events(DATA_PATH) + sessions = build_session_summaries(events) + status_by_date = {session.session_date: session.status for session in sessions} + + if status_by_date.get(session_date, "closed") != "open": + flash( + "That session is closed. Open it first before adding player events.", + "error", + ) + return redirect(url_for("admin_dashboard")) + append_event( DATA_PATH, session_date=session_date, @@ -242,10 +283,13 @@ def admin_dashboard() -> str: events = load_events(DATA_PATH) sessions = build_session_summaries(events) recent_sessions = sessions[:6] - recent_events = list(reversed(events[-10:])) + open_sessions = [session for session in sessions if session.status == "open"] + recent_events = list(reversed(events[-20:])) + return render_template( "admin_dashboard.html", recent_sessions=recent_sessions, + open_sessions=open_sessions, recent_events=recent_events, player_names=unique_player_names(events), ) diff --git a/static/css/style.css b/static/css/style.css index b679fa4..1d4d563 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -760,6 +760,74 @@ td a:hover { align-items: stretch; } +.button-row { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.secondary-button { + appearance: none; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); + color: var(--text-strong); + border-radius: 12px; + padding: 0.8rem 1rem; + font: inherit; + font-weight: 600; + cursor: pointer; +} + +.secondary-button:hover { + background: rgba(255, 255, 255, 0.07); +} + +.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; +} + +.status-open { + color: #f59e0b; + background: rgba(245, 158, 11, 0.12); + border: 1px solid rgba(245, 158, 11, 0.24); +} + +.status-closed { + color: #94a3b8; + background: rgba(148, 163, 184, 0.1); + border: 1px solid rgba(148, 163, 184, 0.2); +} + +.inline-action-form { + margin: 0; +} + +.table-action-button { + appearance: none; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.04); + color: var(--text-strong); + border-radius: 10px; + padding: 0.45rem 0.7rem; + font: inherit; + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; +} + +.table-action-button:hover { + background: rgba(255, 255, 255, 0.07); +} + @media (max-width: 980px) { .site-shell { width: min(100% - 24px, 100%); diff --git a/stats.py b/stats.py index 107daf9..5d18234 100644 --- a/stats.py +++ b/stats.py @@ -41,6 +41,11 @@ class SessionEntry: class SessionSummary: session_date: str entries: list[SessionEntry] + status: str = "closed" + + @property + def is_open(self) -> bool: + return self.status == "open" @property def total_buy_in_cents(self) -> int: @@ -120,35 +125,66 @@ def net_tone(value_cents: int) -> str: def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]: grouped: dict[tuple[str, str], SessionEntry] = {} + by_session: dict[str, list[SessionEntry]] = defaultdict(list) + session_status: dict[str, str] = {} + session_dates_seen: set[str] = set() for event in events: - key = (event["session_date"], event["player_name"]) + session_date = event["session_date"] + event_type = event["event_type"] + + if not session_date: + continue + + session_dates_seen.add(session_date) + + if event_type == "session_open": + session_status[session_date] = "open" + continue + + if event_type == "session_close": + session_status[session_date] = "closed" + continue + + player_name = event["player_name"].strip() + if not player_name: + continue + + key = (session_date, player_name) if key not in grouped: grouped[key] = SessionEntry( - session_date=event["session_date"], - player_name=event["player_name"], + session_date=session_date, + player_name=player_name, ) entry = grouped[key] - if event["event_type"] == "buyin": + + if event_type == "buyin": entry.buy_in_cents += event["amount_cents"] - elif event["event_type"] == "cashout": + elif event_type == "cashout": entry.cash_out_cents += event["amount_cents"] if event["note"]: entry.notes.append(event["note"]) - by_session: dict[str, list[SessionEntry]] = defaultdict(list) for entry in grouped.values(): by_session[entry.session_date].append(entry) - sessions = [ - SessionSummary( - session_date=session_date, - entries=sorted(entries, key=lambda entry: entry.player_name.casefold()), + sessions: list[SessionSummary] = [] + for session_date in session_dates_seen: + entries = sorted( + by_session.get(session_date, []), + key=lambda entry: entry.player_name.casefold(), ) - for session_date, entries in by_session.items() - ] + + sessions.append( + SessionSummary( + session_date=session_date, + entries=entries, + status=session_status.get(session_date, "closed"), + ) + ) + sessions.sort(key=lambda session: session.session_date, reverse=True) return sessions @@ -306,4 +342,7 @@ def session_events(events: list[EventRow], session_date: str) -> list[EventRow]: def unique_player_names(events: list[EventRow]) -> list[str]: - return sorted({event["player_name"] for event in events}, key=str.casefold) + return sorted( + {event["player_name"] for event in events if event["player_name"].strip()}, + key=str.casefold, + ) diff --git a/storage.py b/storage.py index b696604..d2d1ee4 100644 --- a/storage.py +++ b/storage.py @@ -30,7 +30,7 @@ class EventRow(TypedDict): actor: str -VALID_EVENT_TYPES = {"buyin", "cashout", "note"} +VALID_EVENT_TYPES = {"buyin", "cashout", "note", "session_open", "session_close"} def ensure_data_file(csv_path: Path) -> None: @@ -63,7 +63,9 @@ def load_events(csv_path: Path) -> list[EventRow]: ) ) - events.sort(key=lambda event: (event["session_date"], event["created_at"], event["id"])) + events.sort( + key=lambda event: (event["session_date"], event["created_at"], event["id"]) + ) return events diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html index 52c2715..6ee5f48 100644 --- a/templates/admin_dashboard.html +++ b/templates/admin_dashboard.html @@ -51,6 +51,21 @@ +
+

Session state

+

Open or close a session

+ + + +
+ + +
+
+
@@ -67,15 +82,36 @@ Players Buy-ins Cash-outs + Status + Action {% for session in recent_sessions %} - {{ session.session_date | pretty_date }} + + + {{ session.session_date | pretty_date }} + + {{ session.entries|length }} {{ session.total_buy_in_cents | money }} {{ session.total_cash_out_cents | money }} + + + {{ session.status }} + + + +
+ + {% if session.status == "open" %} + + {% else %} + + {% endif %} +
+ {% else %} diff --git a/templates/sessions.html b/templates/sessions.html index e4437bb..61a51f8 100644 --- a/templates/sessions.html +++ b/templates/sessions.html @@ -21,6 +21,7 @@ Date + Status Players Total Buy-ins Total Cash-outs @@ -31,6 +32,9 @@ {% for session in sessions %} {{ session.session_date | pretty_date }} + + {{ session.status }} + {{ session.entries|length }} {{ session.total_buy_in_cents | money }} {{ session.total_cash_out_cents | money }}