""" ui/history.py ------------- Draw History browser screen. Shows a sortable, filterable Treeview of all draw records. """ import os import tkinter as tk from tkinter import ttk, filedialog, messagebox from db.models import get_all_games, get_game_by_name, get_draws_with_game from core.exporter import ( export_draws_excel, export_draws_csv, default_path, ensure_exports_dir, ) from ui.widgets import BallsBar _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("", lambda e: self._clear_hint(self._from_entry, "YYYY-MM-DD")) self._from_entry.bind("", 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("", lambda e: self._clear_hint(self._to_entry, "YYYY-MM-DD")) self._to_entry.bind("", lambda e: self._restore_hint(self._to_entry, "YYYY-MM-DD")) ttk.Label(bar, text="Number:", padding=(8, 0, 0, 0)).pack(side="left") self._number_var = tk.StringVar() self._number_entry = ttk.Entry(bar, textvariable=self._number_var, width=5) self._number_entry.pack(side="left", padx=(4, 14)) self._number_entry.bind("", lambda e: self._apply_filter()) 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") ttk.Button(bar, text="Export CSV", command=self._export_csv).pack(side="right", padx=(0, 4)) ttk.Button(bar, text="Export Excel",command=self._export_excel).pack(side="right", padx=(0, 4)) # ── 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) # ── Ball detail strip ───────────────────────────────────────────────── ttk.Separator(self, orient="horizontal").pack(fill="x", padx=6) self._detail_frame = ttk.Frame(self, padding=(8, 3, 8, 3)) self._detail_frame.pack(fill="x") ttk.Label(self._detail_frame, text="Select a row to preview.", foreground="#aaaaaa", font=("TkDefaultFont", 8)).pack(anchor="w") self._tree.bind("<>", self._on_row_select) # ── 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 raw_num = self._number_var.get().strip() number = int(raw_num) if raw_num.isdigit() 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", number=number, ) self._populate(rows) def _populate(self, rows): self._tree.delete(*self._tree.get_children()) self._clear_detail() 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 ''}") def _clear_detail(self): for w in self._detail_frame.winfo_children(): w.destroy() ttk.Label(self._detail_frame, text="Select a row to preview.", foreground="#aaaaaa", font=("TkDefaultFont", 8)).pack(anchor="w") def _on_row_select(self, _event=None): sel = self._tree.selection() if not sel: self._clear_detail() return vals = self._tree.item(sel[0], "values") # vals: (game, date, numbers_fmt, bonus, multiplier, source) nums_text = vals[2] # e.g. "1 13 36 61 69" bonus_text = str(vals[3]) try: nums = [int(x) for x in nums_text.split() if x.isdigit()] bonus = int(bonus_text) if bonus_text.isdigit() else None except Exception: self._clear_detail() return if not nums: self._clear_detail() return for w in self._detail_frame.winfo_children(): w.destroy() row_frame = ttk.Frame(self._detail_frame) row_frame.pack(anchor="w") ttk.Label(row_frame, text=vals[1], foreground="#555555", font=("TkDefaultFont", 8), width=11).pack(side="left") BallsBar(row_frame, numbers=nums, bonus=bonus).pack(side="left") # ── 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._number_var.set("") self._sort_col = "date" self._sort_asc = False for c in _COLUMNS: self._tree.heading(c, text=_LABELS[c]) self._apply_filter() # ── Export ──────────────────────────────────────────────────────────────── def _current_filter(self): """Return (game_id, date_from, date_to) for the active filter.""" name = self._game_var.get() if name == "All Games": game_id = None else: g = get_game_by_name(name) game_id = g["id"] if g else None 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 return game_id, date_from, date_to def _active_number(self): raw = self._number_var.get().strip() return int(raw) if raw.isdigit() else None def _export_excel(self): ensure_exports_dir() fp = filedialog.asksaveasfilename( title="Export draw history", defaultextension=".xlsx", filetypes=[("Excel workbook", "*.xlsx")], initialfile=f"draws_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.xlsx", ) if not fp: return try: game_id, date_from, date_to = self._current_filter() export_draws_excel(fp, game_id=game_id, date_from=date_from, date_to=date_to) self._count_var.set(f"Exported → {os.path.basename(fp)}") except Exception as e: messagebox.showerror("Export failed", str(e)) def _export_csv(self): ensure_exports_dir() fp = filedialog.asksaveasfilename( title="Export draw history", defaultextension=".csv", filetypes=[("CSV file", "*.csv")], initialfile=f"draws_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.csv", ) if not fp: return try: game_id, date_from, date_to = self._current_filter() export_draws_csv(fp, game_id=game_id, date_from=date_from, date_to=date_to) self._count_var.set(f"Exported → {os.path.basename(fp)}") except Exception as e: messagebox.showerror("Export failed", str(e))