add session open/closed state to prevent modifications
This commit is contained in:
6 files changed
+210
-17
No files matched your search
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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%);
|
||||||
|
|||||||
@@ -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] = []
|
||||||
SessionSummary(
|
for session_date in session_dates_seen:
|
||||||
session_date=session_date,
|
entries = sorted(
|
||||||
entries=sorted(entries, key=lambda entry: entry.player_name.casefold()),
|
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)
|
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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user