add session open/closed state to prevent modifications

This commit is contained in:
SowinskiBraeden committed 2026-03-21 00:10:52 -07:00
1 parent 7648bf6e6a
commit 5d10136ee4
6 files changed
+208 -15

No files matched your search

+45 -1
View File
@@ -204,6 +204,36 @@ def admin_logout() -> str:
return redirect(url_for("leaderboard")) 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"]) @app.route("/admin", methods=["GET", "POST"])
def admin_dashboard() -> str: def admin_dashboard() -> str:
if not is_admin(): if not is_admin():
@@ -227,6 +257,17 @@ def admin_dashboard() -> str:
flash("Amount must be a valid number.", "error") flash("Amount must be a valid number.", "error")
return redirect(url_for("admin_dashboard")) 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( append_event(
DATA_PATH, DATA_PATH,
session_date=session_date, session_date=session_date,
@@ -242,10 +283,13 @@ def admin_dashboard() -> str:
events = load_events(DATA_PATH) events = load_events(DATA_PATH)
sessions = build_session_summaries(events) sessions = build_session_summaries(events)
recent_sessions = sessions[:6] 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( return render_template(
"admin_dashboard.html", "admin_dashboard.html",
recent_sessions=recent_sessions, recent_sessions=recent_sessions,
open_sessions=open_sessions,
recent_events=recent_events, recent_events=recent_events,
player_names=unique_player_names(events), player_names=unique_player_names(events),
) )
+68
View File
@@ -760,6 +760,74 @@ td a:hover {
align-items: stretch; 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) { @media (max-width: 980px) {
.site-shell { .site-shell {
width: min(100% - 24px, 100%); width: min(100% - 24px, 100%);
+50 -11
View File
@@ -41,6 +41,11 @@ class SessionEntry:
class SessionSummary: class SessionSummary:
session_date: str session_date: str
entries: list[SessionEntry] entries: list[SessionEntry]
status: str = "closed"
@property
def is_open(self) -> bool:
return self.status == "open"
@property @property
def total_buy_in_cents(self) -> int: 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]: def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]:
grouped: dict[tuple[str, str], SessionEntry] = {} 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: 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: if key not in grouped:
grouped[key] = SessionEntry( grouped[key] = SessionEntry(
session_date=event["session_date"], session_date=session_date,
player_name=event["player_name"], player_name=player_name,
) )
entry = grouped[key] entry = grouped[key]
if event["event_type"] == "buyin":
if event_type == "buyin":
entry.buy_in_cents += event["amount_cents"] entry.buy_in_cents += event["amount_cents"]
elif event["event_type"] == "cashout": elif event_type == "cashout":
entry.cash_out_cents += event["amount_cents"] entry.cash_out_cents += event["amount_cents"]
if event["note"]: if event["note"]:
entry.notes.append(event["note"]) entry.notes.append(event["note"])
by_session: dict[str, list[SessionEntry]] = defaultdict(list)
for entry in grouped.values(): for entry in grouped.values():
by_session[entry.session_date].append(entry) by_session[entry.session_date].append(entry)
sessions = [ sessions: list[SessionSummary] = []
for session_date in session_dates_seen:
entries = sorted(
by_session.get(session_date, []),
key=lambda entry: entry.player_name.casefold(),
)
sessions.append(
SessionSummary( SessionSummary(
session_date=session_date, session_date=session_date,
entries=sorted(entries, key=lambda entry: entry.player_name.casefold()), entries=entries,
status=session_status.get(session_date, "closed"),
) )
for session_date, entries in by_session.items() )
]
sessions.sort(key=lambda session: session.session_date, reverse=True) sessions.sort(key=lambda session: session.session_date, reverse=True)
return sessions 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]: 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,
)
+4 -2
View File
@@ -30,7 +30,7 @@ class EventRow(TypedDict):
actor: str 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: 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 return events
+37 -1
View File
@@ -51,6 +51,21 @@
<button class="primary-button" type="submit">Add event</button> <button class="primary-button" type="submit">Add event</button>
</form> </form>
<form class="panel form-card" method="post" action="{{ url_for('admin_session_state') }}">
<p class="eyebrow">Session state</p>
<h2>Open or close a session</h2>
<label>
<span>Session date</span>
<input type="date" name="session_date" required>
</label>
<div class="button-row">
<button class="primary-button" type="submit" name="state" value="open">Open session</button>
<button class="secondary-button" type="submit" name="state" value="closed">Close session</button>
</div>
</form>
<div class="stack-gap"> <div class="stack-gap">
<section class="panel"> <section class="panel">
<div class="panel-header"> <div class="panel-header">
@@ -67,15 +82,36 @@
<th>Players</th> <th>Players</th>
<th>Buy-ins</th> <th>Buy-ins</th>
<th>Cash-outs</th> <th>Cash-outs</th>
<th>Status</th>
<th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for session in recent_sessions %} {% for session in recent_sessions %}
<tr> <tr>
<td><a href="{{ url_for('session_detail', session_date=session.session_date) }}">{{ session.session_date | pretty_date }}</a></td> <td>
<a href="{{ url_for('session_detail', session_date=session.session_date) }}">
{{ session.session_date | pretty_date }}
</a>
</td>
<td>{{ session.entries|length }}</td> <td>{{ session.entries|length }}</td>
<td>{{ session.total_buy_in_cents | money }}</td> <td>{{ session.total_buy_in_cents | money }}</td>
<td>{{ session.total_cash_out_cents | money }}</td> <td>{{ session.total_cash_out_cents | money }}</td>
<td>
<span class="status-badge status-{{ session.status }}">
{{ session.status }}
</span>
</td>
<td>
<form method="post" action="{{ url_for('admin_session_state') }}" class="inline-action-form">
<input type="hidden" name="session_date" value="{{ session.session_date }}">
{% if session.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> </tr>
{% else %} {% else %}
<tr> <tr>
+4
View File
@@ -21,6 +21,7 @@
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Status</th>
<th>Players</th> <th>Players</th>
<th>Total Buy-ins</th> <th>Total Buy-ins</th>
<th>Total Cash-outs</th> <th>Total Cash-outs</th>
@@ -31,6 +32,9 @@
{% for session in sessions %} {% for session in sessions %}
<tr> <tr>
<td><a href="{{ url_for('session_detail', session_date=session.session_date) }}">{{ session.session_date | pretty_date }}</a></td> <td><a href="{{ url_for('session_detail', session_date=session.session_date) }}">{{ session.session_date | pretty_date }}</a></td>
<td><span class="status-badge status-{{ session.status }}">
{{ session.status }}
</span></td>
<td>{{ session.entries|length }}</td> <td>{{ session.entries|length }}</td>
<td>{{ session.total_buy_in_cents | money }}</td> <td>{{ session.total_buy_in_cents | money }}</td>
<td>{{ session.total_cash_out_cents | money }}</td> <td>{{ session.total_cash_out_cents | money }}</td>