05/23 Phase 3

This commit is contained in:
2026-05-23 11:18:25 -04:00
parent f33962cb15
commit 5dcf93a85f
7 changed files with 467 additions and 19 deletions
+215
View File
@@ -0,0 +1,215 @@
"""
ui/history.py
-------------
Draw History browser screen.
Shows a sortable, filterable Treeview of all draw records.
"""
import tkinter as tk
from tkinter import ttk
from db.models import get_all_games, get_game_by_name, get_draws_with_game
_COLUMNS = ("game", "date", "numbers", "bonus", "multiplier", "source")
_LABELS = {
"game": "Game",
"date": "Date",
"numbers": "Numbers",
"bonus": "Bonus",
"multiplier": "Mult.",
"source": "Source",
}
_WIDTHS = {
"game": 120,
"date": 100,
"numbers": 190,
"bonus": 55,
"multiplier": 55,
"source": 130,
}
_ANCHORS = {c: "center" for c in _COLUMNS}
_ANCHORS["numbers"] = "w"
_ANCHORS["source"] = "w"
def _fmt_numbers(raw: str) -> str:
"""'1,13,36,61,69''1 13 36 61 69'"""
return " ".join(raw.split(",")) if raw else ""
class HistoryScreen(ttk.Frame):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self._sort_col = "date"
self._sort_asc = False # newest first
self._build_ui()
self.refresh()
# ── UI construction ───────────────────────────────────────────────────────
def _build_ui(self):
# ── Filter bar ────────────────────────────────────────────────────────
bar = ttk.Frame(self, padding=(6, 6, 6, 4))
bar.pack(fill="x")
ttk.Label(bar, text="Game:").pack(side="left")
self._game_var = tk.StringVar(value="All Games")
self._game_cb = ttk.Combobox(
bar, textvariable=self._game_var, state="readonly", width=15
)
self._game_cb.pack(side="left", padx=(4, 14))
ttk.Label(bar, text="From:").pack(side="left")
self._from_entry = ttk.Entry(bar, width=11)
self._from_entry.pack(side="left", padx=(4, 0))
self._from_entry.insert(0, "YYYY-MM-DD")
self._from_entry.bind("<FocusIn>", lambda e: self._clear_hint(self._from_entry, "YYYY-MM-DD"))
self._from_entry.bind("<FocusOut>", lambda e: self._restore_hint(self._from_entry, "YYYY-MM-DD"))
ttk.Label(bar, text="To:", padding=(8, 0, 0, 0)).pack(side="left")
self._to_entry = ttk.Entry(bar, width=11)
self._to_entry.pack(side="left", padx=(4, 14))
self._to_entry.insert(0, "YYYY-MM-DD")
self._to_entry.bind("<FocusIn>", lambda e: self._clear_hint(self._to_entry, "YYYY-MM-DD"))
self._to_entry.bind("<FocusOut>", lambda e: self._restore_hint(self._to_entry, "YYYY-MM-DD"))
ttk.Button(bar, text="Search", command=self._apply_filter).pack(side="left", padx=(0, 4))
ttk.Button(bar, text="Clear", command=self._clear_filter).pack(side="left")
ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right")
# ── Treeview + scrollbars ─────────────────────────────────────────────
tree_frame = ttk.Frame(self)
tree_frame.pack(fill="both", expand=True, padx=6, pady=(2, 0))
self._tree = ttk.Treeview(
tree_frame, columns=_COLUMNS, show="headings", selectmode="browse"
)
for col in _COLUMNS:
self._tree.heading(
col, text=_LABELS[col],
command=lambda c=col: self._sort_by(c),
)
self._tree.column(col, width=_WIDTHS[col], anchor=_ANCHORS[col], stretch=(col == "numbers"))
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self._tree.yview)
hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self._tree.xview)
self._tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
self._tree.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")
tree_frame.rowconfigure(0, weight=1)
tree_frame.columnconfigure(0, weight=1)
# ── Row count ─────────────────────────────────────────────────────────
self._count_var = tk.StringVar(value="0 rows")
ttk.Label(self, textvariable=self._count_var, anchor="e",
padding=(6, 2)).pack(fill="x")
# ── Hint helpers ──────────────────────────────────────────────────────────
@staticmethod
def _clear_hint(entry: ttk.Entry, hint: str):
if entry.get() == hint:
entry.delete(0, "end")
@staticmethod
def _restore_hint(entry: ttk.Entry, hint: str):
if entry.get().strip() == "":
entry.insert(0, hint)
# ── Data loading ──────────────────────────────────────────────────────────
def refresh(self):
"""Reload games dropdown and re-run current filter."""
self._load_games()
self._apply_filter()
def _load_games(self):
games = get_all_games()
names = ["All Games"] + [g["name"] for g in games]
self._game_cb["values"] = names
if self._game_var.get() not in names:
self._game_var.set("All Games")
def _apply_filter(self):
game_name = self._game_var.get()
raw_from = self._from_entry.get().strip()
raw_to = self._to_entry.get().strip()
date_from = raw_from if raw_from and raw_from != "YYYY-MM-DD" else None
date_to = raw_to if raw_to and raw_to != "YYYY-MM-DD" else None
if game_name == "All Games":
game_id = None
else:
game = get_game_by_name(game_name)
game_id = game["id"] if game else None
rows = get_draws_with_game(
game_id=game_id,
date_from=date_from,
date_to=date_to,
order="DESC" if not self._sort_asc else "ASC",
)
self._populate(rows)
def _populate(self, rows):
self._tree.delete(*self._tree.get_children())
for row in rows:
self._tree.insert("", "end", values=(
row["game_name"],
row["draw_date"],
_fmt_numbers(row["numbers"]),
row["bonus"] or "",
row["multiplier"] or "",
row["source"] or "",
))
count = len(rows)
self._count_var.set(f"{count} row{'s' if count != 1 else ''}")
# ── Sorting ───────────────────────────────────────────────────────────────
def _sort_by(self, col):
if self._sort_col == col:
self._sort_asc = not self._sort_asc
else:
# Remove indicator from previous column
self._tree.heading(self._sort_col, text=_LABELS[self._sort_col])
self._sort_col = col
self._sort_asc = col != "date" # date defaults descending, others ascending
arrow = "" if self._sort_asc else ""
self._tree.heading(col, text=f"{_LABELS[col]} {arrow}")
col_idx = _COLUMNS.index(col)
items = [(self._tree.set(iid, col), iid) for iid in self._tree.get_children()]
# Numeric sort for bonus/multiplier columns
if col in ("bonus", "multiplier"):
def key(x):
try:
return (0, int(x[0]))
except (ValueError, TypeError):
return (1, x[0])
items.sort(key=key, reverse=not self._sort_asc)
else:
items.sort(key=lambda x: x[0], reverse=not self._sort_asc)
for i, (_, iid) in enumerate(items):
self._tree.move(iid, "", i)
# ── Clear filter ──────────────────────────────────────────────────────────
def _clear_filter(self):
self._game_var.set("All Games")
self._from_entry.delete(0, "end")
self._from_entry.insert(0, "YYYY-MM-DD")
self._to_entry.delete(0, "end")
self._to_entry.insert(0, "YYYY-MM-DD")
self._sort_col = "date"
self._sort_asc = False
for c in _COLUMNS:
self._tree.heading(c, text=_LABELS[c])
self._apply_filter()