visual overhaul + accounting corrections

This commit is contained in:
SowinskiBraeden committed 2026-06-22 13:51:38 -07:00
1 parent 77198e5b47
commit 70e62342ee
17 files changed
+1268 -389

No files matched your search

+198 -52
View File
@@ -31,7 +31,14 @@ from stats import (
session_sort_key,
unique_player_names,
)
from storage import CSV_HEADERS, append_event, ensure_data_file, load_events
from storage import (
CSV_HEADERS,
EventRow,
append_event,
ensure_data_file,
load_events,
write_events,
)
BASE_DIR = Path(__file__).resolve().parent
DATA_PATH = BASE_DIR / "data" / "entries.csv"
@@ -40,13 +47,15 @@ ENV_PATH = BASE_DIR / ".env"
ELIGIBLE_MIN_SESSIONS = 3
ADMIN_EVENT_TYPES = [
("buyin", "Buy-in", "var(--accent)"),
("front", "Fronted", "var(--warn)"),
("front", "Front", "var(--warn)"),
("rollover_in", "Rollover-in", "var(--rolled)"),
("cashout", "Cash-out", "var(--rank-2)"),
("paid", "Paid-out", "var(--pos)"),
("payout_carry_in", "Payout carry-in", "var(--rolled)"),
("cashout", "Cashout result", "var(--rank-2)"),
("paid_out", "Paid out cash", "var(--pos)"),
("rollover_out", "Rollover-out", "var(--rolled)"),
("note", "Note", "var(--muted)"),
]
SESSION_MARKER_TYPES = {"session_open", "session_close"}
def load_local_env(env_path: Path) -> None:
@@ -80,6 +89,54 @@ def next_session_id(sessions, session_date: str) -> str:
return f"{session_date}-{highest + 1:02d}"
def prunable_empty_session_ids(events: list[EventRow], sessions) -> set[str]:
empty_session_ids = {
session.session_id for session in sessions if len(session.entries) == 0
}
session_events: dict[str, list[EventRow]] = {}
for event in events:
session_id = event["session_id"].strip() or event["session_date"].strip()
if session_id in empty_session_ids:
session_events.setdefault(session_id, []).append(event)
return {
session_id
for session_id, rows in session_events.items()
if rows
and all(
row["event_type"] in SESSION_MARKER_TYPES
and not row["player_name"].strip()
and row["amount_cents"] == 0
for row in rows
)
}
def pending_payout_carry_items(sessions) -> list[dict[str, object]]:
carried_out: dict[str, int] = {}
carried_in: dict[str, int] = {}
for session_summary in sessions:
for entry in session_summary.entries:
carried_out[entry.player_name] = (
carried_out.get(entry.player_name, 0) + entry.rollover_out_cents
)
carried_in[entry.player_name] = (
carried_in.get(entry.player_name, 0)
+ entry.rollover_in_cents
+ entry.payout_carry_in_cents
)
items = []
for player_name, amount_out in carried_out.items():
pending_cents = amount_out - carried_in.get(player_name, 0)
if pending_cents > 0:
items.append({"player_name": player_name, "amount_cents": pending_cents})
return sorted(items, key=lambda item: str(item["player_name"]).casefold())
load_local_env(ENV_PATH)
app = Flask(__name__)
@@ -219,33 +276,24 @@ def session_detail(session_id: str) -> str:
flash("That session was not found.", "error")
return redirect(url_for("sessions"))
target_index = next(
(
index
for index, session in enumerate(sessions)
if session.session_id == session_id
),
None,
)
if target_index is None:
flash("That session was not found.", "error")
return redirect(url_for("sessions"))
target_session = sessions[target_index]
chronological_sessions = sorted(sessions, key=session_sort_key)
session_number = next(
(
index
for index, session in enumerate(chronological_sessions, start=1)
if session.session_id == session_id
),
1,
chronological_index = next(
index
for index, session in enumerate(chronological_sessions)
if session.session_id == session_id
)
target_session = chronological_sessions[chronological_index]
session_number = chronological_index + 1
prev_session = (
sessions[target_index + 1] if target_index < len(sessions) - 1 else None
chronological_sessions[chronological_index - 1]
if chronological_index > 0
else None
)
next_session = (
chronological_sessions[chronological_index + 1]
if chronological_index < len(chronological_sessions) - 1
else None
)
next_session = sessions[target_index - 1] if target_index > 0 else None
return render_template(
"session_detail.html",
@@ -478,7 +526,7 @@ def admin_write_off_front() -> str:
)
if entry is None or entry.player_owes_cents <= 0:
flash("That player does not have an outstanding front debt.", "error")
flash("That player does not have an outstanding debt.", "error")
return redirect(url_for("admin_dashboard"))
if amount_cents > entry.player_owes_cents:
@@ -493,13 +541,13 @@ def admin_write_off_front() -> str:
session_id=target.session_id,
session_date=target.session_date,
player_name=entry.player_name,
event_type="front_writeoff",
event_type="writeoff",
amount_cents=amount_cents,
note=note or "Front debt written off.",
note=note or "Debt written off.",
actor=app.config["ADMIN_USERNAME"],
)
flash("Front debt written off.", "success")
flash("Debt written off.", "success")
return redirect(url_for("admin_dashboard"))
@@ -546,7 +594,7 @@ def admin_collect_front() -> str:
)
if entry is None or entry.player_owes_cents <= 0:
flash("That player does not have an outstanding front debt.", "error")
flash("That player does not have an outstanding debt.", "error")
return redirect(url_for("admin_dashboard"))
if amount_cents > entry.player_owes_cents:
@@ -561,13 +609,13 @@ def admin_collect_front() -> str:
session_id=target.session_id,
session_date=target.session_date,
player_name=entry.player_name,
event_type="front_collected",
event_type="debt_repayment",
amount_cents=amount_cents,
note=note or "Front debt collected.",
note=note or "Debt repayment collected.",
actor=app.config["ADMIN_USERNAME"],
)
flash("Front debt collected.", "success")
flash("Debt repayment collected.", "success")
return redirect(url_for("admin_dashboard"))
@@ -601,6 +649,86 @@ def admin_open_session() -> str:
return redirect(url_for("admin_dashboard"))
@app.post("/admin/prune-empty-sessions")
def admin_prune_empty_sessions() -> str:
if not is_admin():
flash("Admin login required.", "error")
return redirect(url_for("admin_login"))
events = load_events(DATA_PATH)
sessions = build_session_summaries(events)
prunable_session_ids = prunable_empty_session_ids(events, sessions)
if not prunable_session_ids:
flash("No empty marker-only sessions to prune.", "success")
return redirect(url_for("admin_dashboard"))
pruned_events = [
event
for event in events
if (event["session_id"].strip() or event["session_date"].strip())
not in prunable_session_ids
]
write_events(DATA_PATH, pruned_events)
flash(
f"Pruned {len(prunable_session_ids)} empty session"
f"{'s' if len(prunable_session_ids) != 1 else ''}.",
"success",
)
return redirect(url_for("admin_dashboard"))
@app.post("/admin/apply-payout-carry-in")
def admin_apply_payout_carry_in() -> str:
if not is_admin():
flash("Admin login required.", "error")
return redirect(url_for("admin_login"))
session_id = request.form.get("session_id", "").strip()
player_name = request.form.get("player_name", "").strip()
events = load_events(DATA_PATH)
sessions = build_session_summaries(events)
by_id = {session.session_id: session for session in sessions}
target = by_id.get(session_id)
if target is None or target.status != "open":
flash("Select a valid open session.", "error")
return redirect(url_for("admin_dashboard"))
if not any(entry.player_name == player_name for entry in target.entries):
flash("Add that player to the open session before applying carry-in.", "error")
return redirect(url_for("admin_dashboard"))
pending_by_player = {
str(item["player_name"]): int(item["amount_cents"])
for item in pending_payout_carry_items(sessions)
}
amount_cents = pending_by_player.get(player_name, 0)
if amount_cents <= 0:
flash("No pending payout carry-in found for that player.", "error")
return redirect(url_for("admin_dashboard"))
append_event(
DATA_PATH,
session_id=target.session_id,
session_date=target.session_date,
player_name=player_name,
event_type="payout_carry_in",
amount_cents=amount_cents,
note="Payout carry-in from prior carry-out.",
actor=app.config["ADMIN_USERNAME"],
)
flash(
f"Applied {cents_to_dollars(amount_cents)} payout carry-in for {player_name}.",
"success",
)
return redirect(url_for("admin_dashboard"))
@app.route("/admin", methods=["GET", "POST"])
def admin_dashboard():
if not is_admin():
@@ -658,8 +786,18 @@ def admin_dashboard():
events = load_events(DATA_PATH)
sessions = build_session_summaries(events)
empty_session_count = len(prunable_empty_session_ids(events, sessions))
pending_carry_items = pending_payout_carry_items(sessions)
pending_carry_total_cents = sum(
int(item["amount_cents"]) for item in pending_carry_items
)
open_sessions = [session for session in sessions if session.status == "open"]
live_session = open_sessions[0] if open_sessions else None
live_session_player_names = (
{entry.player_name for entry in live_session.entries}
if live_session is not None
else set()
)
live_session_no = ""
live_unresolved_count = 0
if live_session is not None:
@@ -670,7 +808,7 @@ def admin_dashboard():
if entry.current_due_cents > 0 or entry.player_owes_cents > 0
)
recent_sessions = sessions[:8]
recent_sessions = sessions
recent_events = list(reversed(events[-20:]))
event_type_colors = {value: color for value, _label, color in ADMIN_EVENT_TYPES}
event_type_labels = {value: label for value, label, _color in ADMIN_EVENT_TYPES}
@@ -689,29 +827,34 @@ def admin_dashboard():
if entry.player_owes_cents > 0
]
cash_in_cents = sum(session.total_banker_cash_in_cents for session in sessions)
cash_out_cents = sum(session.total_banker_cash_out_cents for session in sessions)
house_owes_players_cents = (
sum(session.total_current_due_to_player_cents for session in sessions)
+ pending_carry_total_cents
)
players_owe_house_cents = sum(
session.total_current_due_to_house_cents for session in sessions
)
admin_totals = {
"cash_in_cents": sum(session.total_cash_in_cents for session in sessions),
"cash_out_cents": sum(session.total_paid_out_cents for session in sessions),
"house_owes_players_cents": sum(
session.total_current_due_to_player_cents for session in sessions
),
"players_owe_house_cents": sum(
session.total_current_due_to_house_cents for session in sessions
),
"cash_in_cents": cash_in_cents,
"cash_out_cents": cash_out_cents,
"house_owes_players_cents": house_owes_players_cents,
"players_owe_house_cents": players_owe_house_cents,
"pending_carry_cents": pending_carry_total_cents,
"collected_cents": sum(
session.total_front_collected_cents for session in sessions
session.total_debt_repayment_cents for session in sessions
),
"written_off_cents": sum(
session.total_front_writeoff_cents for session in sessions
session.total_writeoff_cents for session in sessions
),
"net_book_position_cents": sum(
session.total_net_book_position_cents for session in sessions
"net_book_position_cents": (
cash_in_cents
- cash_out_cents
+ players_owe_house_cents
- house_owes_players_cents
),
}
admin_totals["paid_out_cents"] = admin_totals["cash_out_cents"]
admin_totals["still_owed_cents"] = admin_totals["house_owes_players_cents"]
admin_totals["players_owe_cents"] = admin_totals["players_owe_house_cents"]
admin_totals["open_balance_cents"] = admin_totals["net_book_position_cents"]
return render_template(
"admin_dashboard.html",
@@ -720,8 +863,10 @@ def admin_dashboard():
recent_events=recent_events,
debt_entries=debt_entries,
live_session=live_session,
live_session_player_names=live_session_player_names,
live_session_no=live_session_no,
live_unresolved_count=live_unresolved_count,
pending_carry_items=pending_carry_items,
player_names=unique_player_names(events),
append_form=append_form,
event_types=ADMIN_EVENT_TYPES,
@@ -730,6 +875,7 @@ def admin_dashboard():
admin_today=datetime.now().date().isoformat(),
session_label=session_label,
admin_totals=admin_totals,
empty_session_count=empty_session_count,
)