implement rollover tracking + simple view session summary

This commit is contained in:
SowinskiBraeden committed 2026-03-22 13:52:59 -07:00
1 parent f8a167074b
commit d7543ea13d
8 files changed
+348 -113

No files matched your search

+109 -41
View File
@@ -28,6 +28,7 @@ from stats import (
safe_date_label, safe_date_label,
session_breakdown_series, session_breakdown_series,
session_events, session_events,
session_label,
unique_player_names, unique_player_names,
) )
from storage import append_event, ensure_data_file, load_events from storage import append_event, ensure_data_file, load_events
@@ -52,6 +53,22 @@ def load_local_env(env_path: Path) -> None:
os.environ.setdefault(key, value) os.environ.setdefault(key, value)
def next_session_id(sessions, session_date: str) -> str:
matching = [session for session in sessions if session.session_date == session_date]
highest = 0
for s in matching:
if s.session_id == session_date:
highest = max(highest, 1)
continue
suffix = s.session_id.replace(f"{session_date}-", "")
if suffix.isdigit():
highest = max(highest, int(suffix))
return f"{session_date}-{highest + 1:02d}"
load_local_env(ENV_PATH) load_local_env(ENV_PATH)
app = Flask(__name__) app = Flask(__name__)
@@ -126,21 +143,26 @@ def leaderboard() -> str:
@app.get("/sessions") @app.get("/sessions")
def sessions() -> str: def sessions():
events = load_events(DATA_PATH) events = load_events(DATA_PATH)
session_summaries = build_session_summaries(events) sessions = build_session_summaries(events)
return render_template("sessions.html", sessions=session_summaries)
return render_template(
"sessions.html",
sessions=sessions,
session_label=session_label,
)
@app.get("/sessions/<session_date>") @app.get("/sessions/<session_id>")
def session_detail(session_date: str) -> str: def session_detail(session_id: str) -> str:
events = load_events(DATA_PATH) events = load_events(DATA_PATH)
sessions = build_session_summaries(events) sessions = build_session_summaries(events)
target_session = next( target_session = next(
( (
session_summary session_summary
for session_summary in sessions for session_summary in sessions
if session_summary.session_date == session_date if session_summary.session_id == session_id
), ),
None, None,
) )
@@ -148,27 +170,33 @@ def session_detail(session_date: str) -> str:
flash("That session was not found.", "error") flash("That session was not found.", "error")
return redirect(url_for("sessions")) return redirect(url_for("sessions"))
# sessions are in reverse order target_index = next(
idx = sessions.index(target_session) (
next_session_idx = idx - 1 index
prev_session_idx = idx + 1 for index, session in enumerate(sessions)
if session.session_id == session_id
),
None,
)
next_session = None if target_index is None:
prev_session = None flash("That session was not found.", "error")
return redirect(url_for("sessions"))
if next_session_idx >= 0: target_session = sessions[target_index]
next_session = sessions[next_session_idx] prev_session = (
sessions[target_index + 1] if target_index < len(sessions) - 1 else None
if prev_session_idx < len(sessions): )
prev_session = sessions[prev_session_idx] next_session = sessions[target_index - 1] if target_index > 0 else None
return render_template( return render_template(
"session_detail.html", "session_detail.html",
session=target_session, session=target_session,
next_session=next_session, next_session=next_session,
prev_session=prev_session, prev_session=prev_session,
raw_events=session_events(events, session_date), raw_events=session_events(events, session_id),
chart_data=session_breakdown_series(target_session), chart_data=session_breakdown_series(target_session),
session_label=session_label,
) )
@@ -224,11 +252,16 @@ def admin_session_state() -> str:
flash("Admin login required.", "error") flash("Admin login required.", "error")
return redirect(url_for("admin_login")) return redirect(url_for("admin_login"))
session_date = request.form.get("session_date", "").strip() session_id = request.form.get("session_id", "").strip()
state = request.form.get("state", "").strip() state = request.form.get("state", "").strip()
if not session_date: events = load_events(DATA_PATH)
flash("Session date is required.", "error") 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:
flash("Session not found.", "error")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
if state not in {"open", "closed"}: if state not in {"open", "closed"}:
@@ -237,13 +270,15 @@ def admin_session_state() -> str:
append_event( append_event(
DATA_PATH, DATA_PATH,
session_date=session_date, session_id=target.session_id,
session_date=target.session_date,
amount_cents=0,
player_name="", player_name="",
event_type="session_open" if state == "open" else "session_close", event_type="session_open" if state == "open" else "session_close",
amount_cents=0,
note=f"Session marked {state}.", note=f"Session marked {state}.",
actor=app.config["ADMIN_USERNAME"], actor=app.config["ADMIN_USERNAME"],
) )
flash(f"Session marked {state}.", "success") flash(f"Session marked {state}.", "success")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
@@ -329,64 +364,97 @@ def admin_import_csv():
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
@app.route("/admin", methods=["GET", "POST"]) @app.post("/admin/open-session")
def admin_dashboard() -> str: def admin_open_session() -> str:
if not is_admin(): if not is_admin():
flash("Admin login required.", "error") flash("Admin login required.", "error")
return redirect(url_for("admin_login")) return redirect(url_for("admin_login"))
session_date = request.form.get("session_date", "").strip()
if not session_date:
flash("Session date is required.", "error")
return redirect(url_for("admin_dashboard"))
events = load_events(DATA_PATH)
sessions = build_session_summaries(events)
session_id = next_session_id(sessions, session_date)
append_event(
DATA_PATH,
session_id=session_id,
session_date=session_date,
amount_cents=0,
player_name="",
event_type="session_open",
note="Session opened.",
actor=app.config["ADMIN_USERNAME"],
)
flash(f"Opened session {session_id}.", "success")
return redirect(url_for("admin_dashboard"))
@app.route("/admin", methods=["GET", "POST"])
def admin_dashboard():
if not is_admin():
return redirect(url_for("admin_login"))
if request.method == "POST": if request.method == "POST":
session_date = request.form.get("session_date", "").strip() session_id = request.form.get("session_id", "").strip()
player_name = request.form.get("player_name", "").strip() player_name = request.form.get("player_name", "").strip()
event_type = request.form.get("event_type", "").strip() event_type = request.form.get("event_type", "").strip()
amount_raw = request.form.get("amount", "0").strip()
note = request.form.get("note", "").strip() note = request.form.get("note", "").strip()
if not session_date or not player_name or not event_type: amount_raw = request.form.get("amount", "0").strip()
flash("Session date, player name, and event type are required.", "error")
return redirect(url_for("admin_dashboard"))
try: try:
amount_cents = 0 if event_type == "note" else round(float(amount_raw) * 100) amount_cents = int(round(float(amount_raw) * 100))
except ValueError: except ValueError:
flash("Amount must be a valid number.", "error") flash("Amount must be a number.", "error")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
events = load_events(DATA_PATH) events = load_events(DATA_PATH)
sessions = build_session_summaries(events) sessions = build_session_summaries(events)
status_by_date = {session.session_date: session.status for session in sessions} by_id = {session.session_id: session for session in sessions}
target = by_id.get(session_id)
if status_by_date.get(session_date, "closed") != "open": if target is None:
flash("Select a valid open session.", "error")
return redirect(url_for("admin_dashboard"))
if target.status != "open":
flash( flash(
"That session is closed. Open it first before adding player events.", "That session is closed. Reopen it first if you need to add events.",
"error", "error",
) )
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
append_event( append_event(
DATA_PATH, DATA_PATH,
session_date=session_date, session_id=target.session_id,
session_date=target.session_date,
player_name=player_name, player_name=player_name,
event_type=event_type, event_type=event_type,
amount_cents=amount_cents, amount_cents=amount_cents,
note=note, note=note,
actor=app.config["ADMIN_USERNAME"], actor=app.config["ADMIN_USERNAME"],
) )
flash("Event added to the ledger.", "success")
flash("Event added.", "success")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
events = load_events(DATA_PATH) events = load_events(DATA_PATH)
sessions = build_session_summaries(events) sessions = build_session_summaries(events)
recent_sessions = sessions[:6]
open_sessions = [session for session in sessions if session.status == "open"] open_sessions = [session for session in sessions if session.status == "open"]
recent_events = list(reversed(events[-8:])) recent_sessions = sessions[:8]
recent_events = list(reversed(events[-20:]))
return render_template( return render_template(
"admin_dashboard.html", "admin_dashboard.html",
recent_sessions=recent_sessions,
open_sessions=open_sessions, open_sessions=open_sessions,
recent_sessions=recent_sessions,
recent_events=recent_events, recent_events=recent_events,
player_names=unique_player_names(events), player_names=unique_player_names(events),
session_label=session_label,
) )
+15
View File
@@ -1054,6 +1054,21 @@ td a:hover {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.table-toolbar {
display: flex;
gap: 0.65rem;
margin-bottom: 1rem;
}
.table-view-toggle.is-active {
background: rgba(255, 166, 77, 0.12);
border-color: rgba(255, 166, 77, 0.28);
}
.session-results-table.compact .detail-col {
display: none;
}
@media (max-width: 1100px) { @media (max-width: 1100px) {
.payout-summary-grid { .payout-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
+104 -35
View File
@@ -26,17 +26,20 @@ PLAYER_PALETTE = [
@dataclass @dataclass
class SessionEntry: class SessionEntry:
session_id: str
session_date: str session_date: str
player_name: str player_name: str
buy_in_cents: int = 0 buy_in_cents: int = 0
front_cents: int = 0 front_cents: int = 0
cash_out_cents: int = 0 cash_out_cents: int = 0
paid_cents: int = 0 paid_cents: int = 0
rollover_in_cents: int = 0
rollover_out_cents: int = 0
notes: list[str] = field(default_factory=list) notes: list[str] = field(default_factory=list)
@property @property
def invested_cents(self) -> int: def invested_cents(self) -> int:
return self.buy_in_cents + self.front_cents return self.buy_in_cents + self.front_cents + self.rollover_in_cents
@property @property
def net_cents(self) -> int: def net_cents(self) -> int:
@@ -47,31 +50,45 @@ class SessionEntry:
return max(self.cash_out_cents - self.front_cents, 0) return max(self.cash_out_cents - self.front_cents, 0)
@property @property
def player_owes_cents(self) -> int: def payout_remaining_cents(self) -> int:
return max(self.front_cents - self.cash_out_cents, 0) return max(self.payout_due_cents - self.paid_cents - self.rollover_out_cents, 0)
@property @property
def payout_remaining_cents(self) -> int: def gross_payout_cents(self) -> int:
return max(self.payout_due_cents - self.paid_cents, 0) return max(self.cash_out_cents - self.front_cents, 0)
@property
def settled_cents(self) -> int:
return self.paid_cents + self.rollover_out_cents
@property
def current_due_cents(self) -> int:
return max(self.gross_payout_cents - self.settled_cents, 0)
@property
def player_owes_cents(self) -> int:
return max(self.front_cents - self.cash_out_cents, 0)
@property @property
def payout_status(self) -> str: def payout_status(self) -> str:
if self.player_owes_cents > 0: if self.player_owes_cents > 0:
return "owes" return "owes"
if self.payout_due_cents <= 0: if self.gross_payout_cents <= 0:
return "none" return "none"
if self.paid_cents <= 0: if self.current_due_cents <= 0:
return "unpaid"
if self.payout_remaining_cents <= 0:
return "paid" return "paid"
if self.settled_cents <= 0:
return "unpaid"
return "partial" return "partial"
@dataclass @dataclass
class SessionSummary: class SessionSummary:
session_id: str
session_date: str session_date: str
entries: list[SessionEntry] entries: list[SessionEntry]
status: str = "closed" status: str = "closed"
opened_at: str = ""
@property @property
def is_open(self) -> bool: def is_open(self) -> bool:
@@ -85,6 +102,10 @@ class SessionSummary:
def total_front_cents(self) -> int: def total_front_cents(self) -> int:
return sum(entry.front_cents for entry in self.entries) return sum(entry.front_cents for entry in self.entries)
@property
def total_rollover_in_cents(self) -> int:
return sum(entry.rollover_in_cents for entry in self.entries)
@property @property
def total_invested_cents(self) -> int: def total_invested_cents(self) -> int:
return sum(entry.invested_cents for entry in self.entries) return sum(entry.invested_cents for entry in self.entries)
@@ -93,18 +114,10 @@ class SessionSummary:
def total_cash_out_cents(self) -> int: def total_cash_out_cents(self) -> int:
return sum(entry.cash_out_cents for entry in self.entries) return sum(entry.cash_out_cents for entry in self.entries)
@property
def total_paid_cents(self) -> int:
return sum(entry.paid_cents for entry in self.entries)
@property @property
def total_payout_due_cents(self) -> int: def total_payout_due_cents(self) -> int:
return sum(entry.payout_due_cents for entry in self.entries) return sum(entry.payout_due_cents for entry in self.entries)
@property
def total_player_owes_cents(self) -> int:
return sum(entry.player_owes_cents for entry in self.entries)
@property @property
def total_remaining_cents(self) -> int: def total_remaining_cents(self) -> int:
return sum(entry.payout_remaining_cents for entry in self.entries) return sum(entry.payout_remaining_cents for entry in self.entries)
@@ -113,6 +126,26 @@ class SessionSummary:
def total_net_cents(self) -> int: def total_net_cents(self) -> int:
return sum(entry.net_cents for entry in self.entries) return sum(entry.net_cents for entry in self.entries)
@property
def total_gross_payout_cents(self) -> int:
return sum(entry.gross_payout_cents for entry in self.entries)
@property
def total_paid_cents(self) -> int:
return sum(entry.paid_cents for entry in self.entries)
@property
def total_rollover_out_cents(self) -> int:
return sum(entry.rollover_out_cents for entry in self.entries)
@property
def total_current_due_cents(self) -> int:
return sum(entry.current_due_cents for entry in self.entries)
@property
def total_player_owes_cents(self) -> int:
return sum(entry.player_owes_cents for entry in self.entries)
@dataclass @dataclass
class PlayerStats: class PlayerStats:
@@ -128,8 +161,11 @@ class PlayerStats:
biggest_loss_cents: int biggest_loss_cents: int
total_buy_in_cents: int total_buy_in_cents: int
total_front_cents: int total_front_cents: int
total_rollover_in_cents: int
total_invested_cents: int total_invested_cents: int
total_cash_out_cents: int total_cash_out_cents: int
total_paid_cents: int
total_rollover_out_cents: int
total_net_cents: int total_net_cents: int
roi_pct: float roi_pct: float
current_win_streak: int current_win_streak: int
@@ -164,11 +200,21 @@ def cents_to_dollars(cents: int) -> str:
return f"${value:,.2f}" return f"${value:,.2f}"
def safe_date_label(session_date: str) -> str: def safe_date_label(raw_date: str) -> str:
try: try:
return datetime.strptime(session_date, "%Y-%m-%d").strftime("%b %d, %Y") return datetime.strptime(raw_date, "%Y-%m-%d").strftime("%b %d, %Y")
except ValueError: except ValueError:
return session_date return raw_date
def session_label(session: SessionSummary) -> str:
if session.session_id == session.session_date:
return safe_date_label(session.session_date)
suffix = session.session_id.replace(f"{session.session_date}-", "")
if suffix.isdigit():
return f"{safe_date_label(session.session_date)} · S{int(suffix)}"
return f"{safe_date_label(session.session_date)} · {suffix}"
def color_for_name(name: str, names: list[str]) -> str: def color_for_name(name: str, names: list[str]) -> str:
@@ -191,32 +237,36 @@ 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) by_session: dict[str, list[SessionEntry]] = defaultdict(list)
session_status: dict[str, str] = {} session_status: dict[str, str] = {}
session_dates_seen: set[str] = set() session_dates: dict[str, str] = {}
session_opened_at: dict[str, str] = {}
for event in events: for event in events:
session_date = event["session_date"] session_id = event["session_id"].strip() or event["session_date"].strip()
event_type = event["event_type"] session_date = event["session_date"].strip()
event_type = event["event_type"].strip()
if not session_date: if not session_id or not session_date:
continue continue
session_dates_seen.add(session_date) session_dates[session_id] = session_date
session_opened_at.setdefault(session_id, event["created_at"])
if event_type == "session_open": if event_type == "session_open":
session_status[session_date] = "open" session_status[session_id] = "open"
continue continue
if event_type == "session_close": if event_type == "session_close":
session_status[session_date] = "closed" session_status[session_id] = "closed"
continue continue
player_name = event["player_name"].strip() player_name = event["player_name"].strip()
if not player_name: if not player_name:
continue continue
key = (session_date, player_name) key = (session_id, player_name)
if key not in grouped: if key not in grouped:
grouped[key] = SessionEntry( grouped[key] = SessionEntry(
session_id=session_id,
session_date=session_date, session_date=session_date,
player_name=player_name, player_name=player_name,
) )
@@ -227,33 +277,46 @@ def build_session_summaries(events: list[EventRow]) -> list[SessionSummary]:
entry.buy_in_cents += event["amount_cents"] entry.buy_in_cents += event["amount_cents"]
elif event_type == "front": elif event_type == "front":
entry.front_cents += event["amount_cents"] entry.front_cents += event["amount_cents"]
elif event_type == "rollover_in":
entry.rollover_in_cents += event["amount_cents"]
elif event_type == "cashout": elif event_type == "cashout":
entry.cash_out_cents += event["amount_cents"] entry.cash_out_cents += event["amount_cents"]
elif event_type == "paid": elif event_type == "paid":
entry.paid_cents += event["amount_cents"] entry.paid_cents += event["amount_cents"]
elif event_type == "rollover_out":
entry.rollover_out_cents += event["amount_cents"]
if event["note"]: if event["note"]:
entry.notes.append(event["note"]) entry.notes.append(event["note"])
for entry in grouped.values(): for entry in grouped.values():
by_session[entry.session_date].append(entry) by_session[entry.session_id].append(entry)
sessions: list[SessionSummary] = [] sessions: list[SessionSummary] = []
for session_date in session_dates_seen: for session_id, session_date in session_dates.items():
entries = sorted( entries = sorted(
by_session.get(session_date, []), by_session.get(session_id, []),
key=lambda entry: entry.player_name.casefold(), key=lambda entry: entry.player_name.casefold(),
) )
sessions.append( sessions.append(
SessionSummary( SessionSummary(
session_id=session_id,
session_date=session_date, session_date=session_date,
entries=entries, entries=entries,
status=session_status.get(session_date, "closed"), status=session_status.get(session_id, "closed"),
opened_at=session_opened_at.get(session_id, ""),
) )
) )
sessions.sort(key=lambda session: session.session_date, reverse=True) sessions.sort(
key=lambda session: (
session.session_date,
session.opened_at,
session.session_id,
),
reverse=True,
)
return sessions return sessions
@@ -346,8 +409,11 @@ def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]:
total_buy_in = sum(entry.buy_in_cents for entry in entries) total_buy_in = sum(entry.buy_in_cents for entry in entries)
total_front = sum(entry.front_cents for entry in entries) total_front = sum(entry.front_cents for entry in entries)
total_rollover_in = sum(entry.rollover_in_cents for entry in entries)
total_invested = sum(entry.invested_cents for entry in entries) total_invested = sum(entry.invested_cents for entry in entries)
total_cash_out = sum(entry.cash_out_cents for entry in entries) total_cash_out = sum(entry.cash_out_cents for entry in entries)
total_paid = sum(entry.paid_cents for entry in entries)
total_rollover_out = sum(entry.rollover_out_cents for entry in entries)
total_net = sum(entry.net_cents for entry in entries) total_net = sum(entry.net_cents for entry in entries)
roi_pct = (total_net / total_invested * 100) if total_invested else 0.0 roi_pct = (total_net / total_invested * 100) if total_invested else 0.0
@@ -365,8 +431,11 @@ def build_leaderboard(sessions: list[SessionSummary]) -> list[PlayerStats]:
biggest_loss_cents=biggest_loss, biggest_loss_cents=biggest_loss,
total_buy_in_cents=total_buy_in, total_buy_in_cents=total_buy_in,
total_front_cents=total_front, total_front_cents=total_front,
total_rollover_in_cents=total_rollover_in,
total_invested_cents=total_invested, total_invested_cents=total_invested,
total_cash_out_cents=total_cash_out, total_cash_out_cents=total_cash_out,
total_paid_cents=total_paid,
total_rollover_out_cents=total_rollover_out,
total_net_cents=total_net, total_net_cents=total_net,
roi_pct=roi_pct, roi_pct=roi_pct,
current_win_streak=run_summary["current_win_streak"], current_win_streak=run_summary["current_win_streak"],
@@ -496,8 +565,8 @@ def session_breakdown_series(session: SessionSummary) -> dict[str, Any]:
} }
def session_events(events: list[EventRow], session_date: str) -> list[EventRow]: def session_events(events: list[EventRow], session_id: str) -> list[EventRow]:
return [event for event in events if event["session_date"] == session_date] return [event for event in events if event["session_id"] == session_id]
def unique_player_names(events: list[EventRow]) -> list[str]: def unique_player_names(events: list[EventRow]) -> list[str]:
+8 -1
View File
@@ -10,6 +10,7 @@ from typing import TypedDict
CSV_HEADERS = [ CSV_HEADERS = [
"id", "id",
"created_at", "created_at",
"session_id",
"session_date", "session_date",
"player_name", "player_name",
"event_type", "event_type",
@@ -22,6 +23,7 @@ CSV_HEADERS = [
class EventRow(TypedDict): class EventRow(TypedDict):
id: str id: str
created_at: str created_at: str
session_id: str
session_date: str session_date: str
player_name: str player_name: str
event_type: str event_type: str
@@ -32,9 +34,11 @@ class EventRow(TypedDict):
VALID_EVENT_TYPES = { VALID_EVENT_TYPES = {
"buyin", "buyin",
"cashout",
"front", "front",
"cashout",
"paid", "paid",
"rollover_in",
"rollover_out",
"note", "note",
"session_open", "session_open",
"session_close", "session_close",
@@ -62,6 +66,7 @@ def load_events(csv_path: Path) -> list[EventRow]:
EventRow( EventRow(
id=row["id"], id=row["id"],
created_at=row["created_at"], created_at=row["created_at"],
session_id=row["session_id"],
session_date=row["session_date"], session_date=row["session_date"],
player_name=row["player_name"], player_name=row["player_name"],
event_type=row["event_type"], event_type=row["event_type"],
@@ -79,6 +84,7 @@ def load_events(csv_path: Path) -> list[EventRow]:
def append_event( def append_event(
csv_path: Path, csv_path: Path,
session_id: str,
session_date: str, session_date: str,
player_name: str, player_name: str,
event_type: str, event_type: str,
@@ -98,6 +104,7 @@ def append_event(
event = EventRow( event = EventRow(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
created_at=datetime.now(timezone.utc).isoformat(), created_at=datetime.now(timezone.utc).isoformat(),
session_id=session_id.strip(),
session_date=session_date.strip(), session_date=session_date.strip(),
player_name=player_name.strip(), player_name=player_name.strip(),
event_type=normalized_type, event_type=normalized_type,
+17 -9
View File
@@ -15,8 +15,15 @@
<h2>Add to the ledger</h2> <h2>Add to the ledger</h2>
<label> <label>
<span>Session date</span> <span>Open session</span>
<input type="date" name="session_date" required> <select name="session_id" required>
<option value="">Select session</option>
{% for session in open_sessions %}
<option value="{{ session.session_id }}">
{{ session_label(session) }}
</option>
{% endfor %}
</select>
</label> </label>
<label> <label>
@@ -34,8 +41,10 @@
<select name="event_type" required> <select name="event_type" required>
<option value="buyin">Buy-in</option> <option value="buyin">Buy-in</option>
<option value="front">Front</option> <option value="front">Front</option>
<option value="rollover_in">Rollover in</option>
<option value="cashout">Cash-out</option> <option value="cashout">Cash-out</option>
<option value="paid">Paid out</option> <option value="paid">Paid out</option>
<option value="rollover_out">Rollover out</option>
<option value="note">Note only</option> <option value="note">Note only</option>
</select> </select>
</label> </label>
@@ -54,9 +63,9 @@
</form> </form>
<section class="grid"> <section class="grid">
<form class="panel form-card" method="post" action="{{ url_for('admin_session_state') }}"> <form class="panel form-card" method="post" action="{{ url_for('admin_open_session') }}">
<p class="eyebrow">Session state</p> <p class="eyebrow">Session state</p>
<h2>Open or close a session</h2> <h2>Open a new session</h2>
<label> <label>
<span>Session date</span> <span>Session date</span>
@@ -64,8 +73,7 @@
</label> </label>
<div class="button-row"> <div class="button-row">
<button class="primary-button" type="submit" name="state" value="open">Open session</button> <button class="primary-button" type="submit">Open session</button>
<button class="secondary-button" type="submit" name="state" value="closed">Close session</button>
</div> </div>
</form> </form>
@@ -130,8 +138,8 @@
{% for session in recent_sessions %} {% for session in recent_sessions %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('session_detail', session_date=session.session_date) }}"> <a href="{{ url_for('session_detail', session_id=session.session_id) }}">
{{ session.session_date | pretty_date }} {{ session_label(session) }}
</a> </a>
</td> </td>
<td>{{ session.entries|length }}</td> <td>{{ session.entries|length }}</td>
@@ -144,7 +152,7 @@
</td> </td>
<td> <td>
<form method="post" action="{{ url_for('admin_session_state') }}" class="inline-action-form"> <form method="post" action="{{ url_for('admin_session_state') }}" class="inline-action-form">
<input type="hidden" name="session_date" value="{{ session.session_date }}"> <input type="hidden" name="session_id" value="{{ session.session_id }}">
{% if session.status == "open" %} {% if session.status == "open" %}
<button class="table-action-button" type="submit" name="state" value="closed">Close</button> <button class="table-action-button" type="submit" name="state" value="closed">Close</button>
{% else %} {% else %}
+7 -1
View File
@@ -14,8 +14,9 @@
<article class="panel stat-card"><span>Win %</span><strong>{{ "%.1f"|format(player.win_pct) }}%</strong></article> <article class="panel stat-card"><span>Win %</span><strong>{{ "%.1f"|format(player.win_pct) }}%</strong></article>
<article class="panel stat-card"><span>ROI</span><strong>{{ "%.1f"|format(player.roi_pct) }}%</strong></article> <article class="panel stat-card"><span>ROI</span><strong>{{ "%.1f"|format(player.roi_pct) }}%</strong></article>
<article class="panel stat-card"><span>Total invested</span><strong>{{ player.total_invested_cents | money }}</strong></article> <article class="panel stat-card"><span>Total invested</span><strong>{{ player.total_invested_cents | money }}</strong></article>
<article class="panel stat-card"><span>Paid Buy-ins</span><strong>{{ player.total_buy_in_cents | money }}</strong></article> <article class="panel stat-card"><span>Paid Buy-in</span><strong>{{ player.total_buy_in_cents | money }}</strong></article>
<article class="panel stat-card"><span>Fronted</span><strong>{{ player.total_front_cents | money }}</strong></article> <article class="panel stat-card"><span>Fronted</span><strong>{{ player.total_front_cents | money }}</strong></article>
<!--<article class="panel stat-card"><span>Rolled in</span><strong>{{ player.total_rollover_in_cents | money }}</strong></article>-->
<article class="panel stat-card"><span>Cash-outs</span><strong>{{ player.total_cash_out_cents | money }}</strong></article> <article class="panel stat-card"><span>Cash-outs</span><strong>{{ player.total_cash_out_cents | money }}</strong></article>
<article class="panel stat-card"><span>Sessions</span><strong>{{ player.sessions_played }}</strong></article> <article class="panel stat-card"><span>Sessions</span><strong>{{ player.sessions_played }}</strong></article>
<article class="panel stat-card"> <article class="panel stat-card">
@@ -84,6 +85,11 @@
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<tbody> <tbody>
<tr><th>Paid Buy-ins</th><td>{{ player.total_buy_in_cents | money }}</td></tr>
<tr><th>Fronted</th><td>{{ player.total_front_cents | money }}</td></tr>
<tr><th>Rolled In</th><td>{{ player.total_rollover_in_cents | money }}</td></tr>
<tr><th>Total Invested</th><td>{{ player.total_invested_cents | money }}</td></tr>
<tr><th>Rolled Out</th><td>{{ player.total_rollover_out_cents | money }}</td></tr>
<tr><th>Winning Sessions</th><td>{{ player.winning_sessions }}</td></tr> <tr><th>Winning Sessions</th><td>{{ player.winning_sessions }}</td></tr>
<tr><th>Losing Sessions</th><td>{{ player.losing_sessions }}</td></tr> <tr><th>Losing Sessions</th><td>{{ player.losing_sessions }}</td></tr>
<tr><th>Break-even Sessions</th><td>{{ player.break_even_sessions }}</td></tr> <tr><th>Break-even Sessions</th><td>{{ player.break_even_sessions }}</td></tr>
+87 -25
View File
@@ -5,16 +5,21 @@
<div style="display: flex; justify-content: space-between;"> <div style="display: flex; justify-content: space-between;">
<div> <div>
<p class="eyebrow">Session detail</p> <p class="eyebrow">Session detail</p>
<h1>{{ session.session_date | pretty_date }}</h1> <h1>{{ session_label(session) }}</h1>
<p class="muted-text">Current totals are derived from the event log for this date.</p> <p class="muted-text">Current totals are derived from the event log for this date.</p>
</div> </div>
<div style="align-content: center;"> <div style="align-self: center;">
{% if prev_session %} {% if prev_session %}
<a class="secondary-button" href="{{ url_for('session_detail', session_date=prev_session.session_date) }}">Previous Session</a> <a class="secondary-button" href="{{ url_for('session_detail', session_id=prev_session.session_id) }}">
Previous Session
</a>
{% endif %} {% endif %}
{% if next_session %} {% if next_session %}
<a class="secondary-button" href="{{ url_for('session_detail', session_date=next_session.session_date) }}">Next Session</a> <a class="secondary-button" href="{{ url_for('session_detail', session_id=next_session.session_id) }}">
Next Session
</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -22,8 +27,13 @@
<section class="grid payout-summary-grid"> <section class="grid payout-summary-grid">
<article class="panel stat-card"> <article class="panel stat-card">
<span>Total payout due</span> <span>Gross payout</span>
<strong>{{ session.total_payout_due_cents | money }}</strong> <strong>{{ session.total_gross_payout_cents | money }}</strong>
</article>
<article class="panel stat-card">
<span>Total rolled out</span>
<strong>{{ session.total_rollover_out_cents | money }}</strong>
</article> </article>
<article class="panel stat-card"> <article class="panel stat-card">
@@ -32,17 +42,12 @@
</article> </article>
<article class="panel stat-card"> <article class="panel stat-card">
<span>Total remaining</span> <span>Still owed</span>
<strong class="{{ 'negative' if session.total_remaining_cents > 0 else 'positive' }}"> <strong class="{{ 'negative' if session.total_current_due_cents > 0 else 'positive' }}">
{{ session.total_remaining_cents | money }} {{ session.total_current_due_cents | money }}
</strong> </strong>
</article> </article>
<article class="panel stat-card">
<span>Total fronted</span>
<strong>{{ session.total_front_cents | money }}</strong>
</article>
<article class="panel stat-card"> <article class="panel stat-card">
<span>Players still owe</span> <span>Players still owe</span>
<strong class="{{ 'negative' if session.total_player_owes_cents > 0 else '' }}"> <strong class="{{ 'negative' if session.total_player_owes_cents > 0 else '' }}">
@@ -51,12 +56,22 @@
</article> </article>
</section> </section>
<section class="panel results-panel results-panel-full"> <section class="panel results-panel results-panel-full">
<div class="panel-header"> <div class="panel-header">
<div> <div>
<p class="eyebrow">Results</p> <p class="eyebrow">Results</p>
<h2>Player totals</h2> <h2>Player totals</h2>
</div> </div>
<div class="table-toolbar">
<button type="button" class="secondary-button table-view-toggle is-active" data-view="compact">
Compact
</button>
<button type="button" class="secondary-button table-view-toggle" data-view="full">
Full details
</button>
</div>
</div> </div>
<div class="table-scroll table-scroll-open"> <div class="table-scroll table-scroll-open">
@@ -64,13 +79,16 @@
<thead> <thead>
<tr> <tr>
<th>Player</th> <th>Player</th>
<th>Buy-in</th> <th>In</th>
<th>Fronted</th> <th class="detail-col">Fronted</th>
<th>Cash-out</th> <th class="detail-col">Rolled in</th>
<th>Out</th>
<th>Net</th> <th>Net</th>
<th class="detail-col">Gross due</th>
<th class="detail-col">Paid</th>
<th class="detail-col">Rolled out</th>
<th>Still owed</th>
<th>Status</th> <th>Status</th>
<th>Paid</th>
<th>Remaining</th>
<th>Notes</th> <th>Notes</th>
</tr> </tr>
</thead> </thead>
@@ -82,21 +100,41 @@
{{ entry.player_name }} {{ entry.player_name }}
</a> </a>
</td> </td>
<td>{{ entry.buy_in_cents | money }}</td>
<td>{{ entry.front_cents | money }}</td> <td>{{ entry.invested_cents | money }}</td>
<td class="detail-col">{{ entry.front_cents | money }}</td>
<td class="detail-col">{{ entry.rollover_in_cents | money }}</td>
<td>{{ entry.cash_out_cents | money }}</td> <td>{{ entry.cash_out_cents | money }}</td>
<td class="{{ 'positive' if entry.net_cents > 0 else 'negative' if entry.net_cents < 0 else '' }}"> <td class="{{ 'positive' if entry.net_cents > 0 else 'negative' if entry.net_cents < 0 else '' }}">
{{ entry.net_cents | money }} {{ entry.net_cents | money }}
</td> </td>
<td class="detail-col">
{% if entry.player_owes_cents > 0 %}
{% else %}
{{ entry.gross_payout_cents | money }}
{% endif %}
</td>
<td class="detail-col">{{ entry.paid_cents | money }}</td>
<td class="detail-col">{{ entry.rollover_out_cents | money }}</td>
<td class="{% if entry.player_owes_cents > 0 %}negative{% elif entry.current_due_cents > 0 %}negative{% else %}positive{% endif %}">
{% if entry.player_owes_cents > 0 %}
Owes {{ entry.player_owes_cents | money }}
{% else %}
{{ entry.current_due_cents | money }}
{% endif %}
</td>
<td> <td>
<span class="status-badge status-{{ entry.payout_status }}"> <span class="status-badge status-{{ entry.payout_status }}">
{{ entry.payout_status }} {{ entry.payout_status }}
</span> </span>
</td> </td>
<td class="{{ 'positive' if entry.paid_cents > 0 else '' }}">{{ entry.paid_cents | money }}</td>
<td class="{{ 'negative' if entry.payout_remaining_cents > 0 else '' }}">
{{ entry.payout_remaining_cents | money }}
</td>
<td> <td>
{% if entry.notes %} {% if entry.notes %}
<ul class="note-list"> <ul class="note-list">
@@ -270,4 +308,28 @@
}); });
} }
</script> </script>
<script>
const sessionTable = document.querySelector('.session-results-table');
const viewButtons = document.querySelectorAll('.table-view-toggle');
if (sessionTable && viewButtons.length) {
sessionTable.classList.add('compact');
viewButtons.forEach((button) => {
button.addEventListener('click', () => {
const view = button.dataset.view;
viewButtons.forEach((btn) => btn.classList.remove('is-active'));
button.classList.add('is-active');
if (view === 'full') {
sessionTable.classList.remove('compact');
} else {
sessionTable.classList.add('compact');
}
});
});
}
</script>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -31,7 +31,7 @@
<tbody> <tbody>
{% 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_id=session.session_id) }}">{{ session_label(session) }}</a></td>
<td><span class="status-badge status-{{ session.status }}"> <td><span class="status-badge status-{{ session.status }}">
{{ session.status }} {{ session.status }}
</span></td> </span></td>