implement rollover tracking + simple view session summary
This commit is contained in:
1 parent
3248731f51
commit
c24f7d5810
8 files changed
+354
-119
No files matched your search
@@ -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"))
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
session_date = request.form.get("session_date", "").strip()
|
session_date = request.form.get("session_date", "").strip()
|
||||||
player_name = request.form.get("player_name", "").strip()
|
if not session_date:
|
||||||
event_type = request.form.get("event_type", "").strip()
|
flash("Session date is required.", "error")
|
||||||
amount_raw = request.form.get("amount", "0").strip()
|
|
||||||
note = request.form.get("note", "").strip()
|
|
||||||
|
|
||||||
if not session_date or not player_name or not event_type:
|
|
||||||
flash("Session date, player name, and event type are required.", "error")
|
|
||||||
return redirect(url_for("admin_dashboard"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
amount_cents = 0 if event_type == "note" else round(float(amount_raw) * 100)
|
|
||||||
except ValueError:
|
|
||||||
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)
|
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}
|
session_id = next_session_id(sessions, session_date)
|
||||||
|
|
||||||
if status_by_date.get(session_date, "closed") != "open":
|
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":
|
||||||
|
session_id = request.form.get("session_id", "").strip()
|
||||||
|
player_name = request.form.get("player_name", "").strip()
|
||||||
|
event_type = request.form.get("event_type", "").strip()
|
||||||
|
note = request.form.get("note", "").strip()
|
||||||
|
|
||||||
|
amount_raw = request.form.get("amount", "0").strip()
|
||||||
|
try:
|
||||||
|
amount_cents = int(round(float(amount_raw) * 100))
|
||||||
|
except ValueError:
|
||||||
|
flash("Amount must be a number.", "error")
|
||||||
|
return redirect(url_for("admin_dashboard"))
|
||||||
|
|
||||||
|
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:
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user