From 6c508f4ef3257ddf275e4aa081bef5ede43780a4 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Sat, 21 Mar 2026 01:30:42 -0700 Subject: [PATCH] update admin layout --- app.py | 97 +++++++++++++++- static/css/style.css | 32 ++++++ templates/admin_dashboard.html | 204 +++++++++++++++++++-------------- 3 files changed, 247 insertions(+), 86 deletions(-) diff --git a/app.py b/app.py index a7fbbb2..d8fd051 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,22 @@ #!/usr/bin/env python3 from __future__ import annotations +import csv import os +import shutil +from datetime import datetime, timezone from pathlib import Path -from flask import Flask, flash, redirect, render_template, request, session, url_for +from flask import ( + Flask, + flash, + redirect, + render_template, + request, + send_file, + session, + url_for, +) from stats import ( apply_rank_changes, @@ -236,6 +248,87 @@ def admin_session_state() -> str: return redirect(url_for("admin_dashboard")) +@app.get("/admin/export") +def admin_export_csv(): + if not is_admin(): + flash("Admin login required.", "error") + return redirect(url_for("admin_login")) + + export_name = ( + f"entries_export_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.csv" + ) + + return send_file( + DATA_PATH, + as_attachment=True, + download_name=export_name, + mimetype="text/csv", + ) + + +@app.post("/admin/import") +def admin_import_csv(): + if not is_admin(): + flash("Admin login required.", "error") + return redirect(url_for("admin_login")) + + uploaded_file = request.files.get("csv_file") + if uploaded_file is None or uploaded_file.filename == "": + flash("Choose a CSV file to import.", "error") + return redirect(url_for("admin_dashboard")) + + if not uploaded_file.filename.lower().endswith(".csv"): + flash("Only CSV files are allowed.", "error") + return redirect(url_for("admin_dashboard")) + + try: + uploaded_text = uploaded_file.stream.read().decode("utf-8-sig") + except UnicodeDecodeError: + flash("CSV file must be valid UTF-8 text.", "error") + return redirect(url_for("admin_dashboard")) + + uploaded_lines = uploaded_text.splitlines() + if not uploaded_lines: + flash("CSV file is empty.", "error") + return redirect(url_for("admin_dashboard")) + + try: + current_header = next( + csv.reader(open(DATA_PATH, "r", encoding="utf-8", newline="")) + ) + except Exception: + flash("Could not read the current audit CSV header.", "error") + return redirect(url_for("admin_dashboard")) + + try: + uploaded_header = next(csv.reader(uploaded_lines)) + except Exception: + flash("Could not read the uploaded CSV header.", "error") + return redirect(url_for("admin_dashboard")) + + if uploaded_header != current_header: + flash("CSV header does not match the current audit format.", "error") + return redirect(url_for("admin_dashboard")) + + data_dir = os.path.dirname(DATA_PATH) + backup_name = ( + f"entries_backup_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.csv" + ) + backup_path = os.path.join(data_dir, backup_name) + + try: + shutil.copy2(DATA_PATH, backup_path) + + with open(DATA_PATH, "w", encoding="utf-8", newline="") as handle: + handle.write(uploaded_text) + + flash(f"CSV imported successfully. Backup created: {backup_name}", "success") + except Exception: + flash("Import failed. Existing CSV was not updated.", "error") + + return redirect(url_for("admin_dashboard")) + + @app.route("/admin", methods=["GET", "POST"]) def admin_dashboard() -> str: if not is_admin(): @@ -286,7 +379,7 @@ def admin_dashboard() -> str: sessions = build_session_summaries(events) recent_sessions = sessions[:6] open_sessions = [session for session in sessions if session.status == "open"] - recent_events = list(reversed(events[-20:])) + recent_events = list(reversed(events[-8:])) return render_template( "admin_dashboard.html", diff --git a/static/css/style.css b/static/css/style.css index 890d74b..ce7fbef 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1000,6 +1000,38 @@ td a:hover { border-color: rgba(255, 166, 77, 0.35); } +.admin-tools-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.import-form input[type="file"] { + width: 100%; + margin-top: 0.5rem; + margin-bottom: 1rem; + color: var(--text-muted); +} + +.import-form input[type="file"]::file-selector-button { + appearance: none; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.05); + color: var(--text-strong); + border-radius: 10px; + padding: 0.55rem 0.8rem; + font: inherit; + font-size: 0.9rem; + font-weight: 600; + margin-right: 0.75rem; + cursor: pointer; +} + +.import-form input[type="file"]::file-selector-button:hover { + background: rgba(255, 166, 77, 0.12); + border-color: rgba(255, 166, 77, 0.28); +} + @media (max-width: 980px) { .site-shell { width: min(100% - 24px, 100%); diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html index 029e231..5255252 100644 --- a/templates/admin_dashboard.html +++ b/templates/admin_dashboard.html @@ -52,101 +52,137 @@ -
-

Session state

-

Open or close a session

+
+ +

Session state

+

Open or close a session

- + -
- - -
- +
+ + +
+ -
-
+
-

Recent sessions

-

Current totals

+

Tools

+

Audit CSV

-
- - - - - - - - - - - - - {% for session in recent_sessions %} - - - - - - - - - {% else %} - - - - {% endfor %} - -
DatePlayersBuy-insCash-outsStatusAction
- - {{ 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 %} -
-
No sessions yet.
-
-
-
-
-
-

Recent raw events

-

Ledger feed

-
-
-
- {% for event in recent_events %} -
-
- {{ event.player_name }} - {{ event.event_type }} +
+
+

+ Export the current audit trail or import a replacement CSV. Import creates a timestamped backup first. +

+ -

{{ event.amount_cents | money }}

-

{{ event.session_date | pretty_date }}

- {% if event.note %}

{{ event.note }}

{% endif %} -
- {% else %} -

No recent events.

- {% endfor %} +
+ +
+ + +
+ +
+
+ + + +
+
+
+

Recent sessions

+

Current totals

+
+
+
+ + + + + + + + + + + + + {% for session in recent_sessions %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
DatePlayersBuy-insCash-outsStatusAction
+ + {{ 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 %} +
+
No sessions yet.
+
+
+ +
+
+
+

Recent raw events

+

Ledger feed

+
+
+
+ {% for event in recent_events %} +
+
+ {{ event.player_name }} + {{ event.event_type }} +
+

{{ event.amount_cents | money }}

+

{{ event.session_date | pretty_date }}

+ {% if event.note %}

{{ event.note }}

{% endif %} +
+ {% else %} +

No recent events.

+ {% endfor %}
{% endblock %}