diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 92ddad2..0e41e08 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,8 @@ "Bash(python -m pytest tests/test_analyzer.py -v)", "Bash(python -m pytest tests/test_predictor.py -v)", "Bash(python -m pytest)", - "Bash(python -m pytest tests/test_settings.py -v)" + "Bash(python -m pytest tests/test_settings.py -v)", + "Bash(python -m pytest tests/ -v --tb=short)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index 9475a11..6c1759b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -307,16 +307,21 @@ All actions are logged to console and optionally to a log file: --- -### 🔲 Phase 7 — Export + Polish -- [ ] Add export to Excel (`openpyxl`) for: - - [ ] Draw history - - [ ] Predictions - - [ ] Frequency analysis -- [ ] Add export to CSV -- [ ] Add `assets/icon.png` -- [ ] App title bar + icon -- [ ] Window min-size + resizable layout -- [ ] Error handling — network down, API timeout, bad CSV +### ✅ Phase 7 — Export + Polish +- [x] Write `core/exporter.py` + - [x] `export_draws_excel(filepath, game_id, date_from, date_to)` — openpyxl, styled header + - [x] `export_draws_csv(filepath, game_id, date_from, date_to)` — stdlib csv + - [x] `export_predictions_excel(filepath, game_id)` — openpyxl + - [x] `export_predictions_csv(filepath, game_id)` — stdlib csv + - [x] `export_frequency_excel(filepath, game_id, last_n)` — number + freq + gap + - [x] `create_icon_png(path, size)` — valid PNG, stdlib only (struct + zlib) +- [x] Add Export Excel + Export CSV buttons to History screen (filter-aware) +- [x] Add Export Excel + Export CSV buttons to Predictor screen (DB predictions) +- [x] Add Export Frequency button to Analysis screen (respects frequency window) +- [x] Auto-create `assets/icon.png` on startup if missing +- [x] Window min-size set (900×600) + resizable layout (all screens) +- [x] Error handling — try/except + messagebox.showerror on all export paths +- [x] 27 tests for exporter (165/165 total passing) --- diff --git a/core/exporter.py b/core/exporter.py new file mode 100644 index 0000000..c4085e8 --- /dev/null +++ b/core/exporter.py @@ -0,0 +1,202 @@ +""" +core/exporter.py +---------------- +Export draw history, predictions, and frequency analysis +to Excel (.xlsx via openpyxl) and CSV. + +All functions accept a filepath and write to it. +Callers (UI) handle file-dialog; tests pass tmp paths directly. +""" + +import csv +import os +import struct +import zlib +from datetime import datetime + +import openpyxl +from openpyxl.styles import Alignment, Font, PatternFill + +from db.models import get_draws_with_game, get_game_by_id, get_predictions + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +EXPORTS_DIR = os.path.join(BASE_DIR, "exports") + + +def ensure_exports_dir(): + os.makedirs(EXPORTS_DIR, exist_ok=True) + + +def default_path(stem: str, ext: str) -> str: + ensure_exports_dir() + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + return os.path.join(EXPORTS_DIR, f"{stem}_{ts}.{ext}") + + +# ── Shared helpers ──────────────────────────────────────────────────────────── + +def _header_row(ws, labels: list[str], fill_hex: str): + bold_white = Font(bold=True, color="FFFFFF") + fill = PatternFill(fill_type="solid", fgColor=fill_hex) + center = Alignment(horizontal="center") + for col, label in enumerate(labels, start=1): + cell = ws.cell(row=1, column=col, value=label) + cell.font = bold_white + cell.fill = fill + cell.alignment = center + + +def _autofit(ws): + for col in ws.columns: + width = max((len(str(cell.value or "")) for cell in col), default=0) + ws.column_dimensions[col[0].column_letter].width = min(width + 4, 60) + + +# ── Draw History ────────────────────────────────────────────────────────────── + +_DRAW_HEADERS = ["Game", "Date", "Numbers", "Bonus", "Multiplier", "Source"] + + +def export_draws_excel(filepath: str, game_id=None, date_from=None, date_to=None): + """Write draw history to filepath (.xlsx). Returns filepath.""" + rows = get_draws_with_game(game_id=game_id, date_from=date_from, + date_to=date_to, order="DESC") + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "Draw History" + _header_row(ws, _DRAW_HEADERS, "2C3E50") + + for i, row in enumerate(rows, start=2): + ws.cell(i, 1, row["game_name"]) + ws.cell(i, 2, row["draw_date"]) + ws.cell(i, 3, row["numbers"]) + ws.cell(i, 4, row["bonus"] or "") + ws.cell(i, 5, row["multiplier"] or "") + ws.cell(i, 6, row["source"] or "") + + _autofit(ws) + wb.save(filepath) + return filepath + + +def export_draws_csv(filepath: str, game_id=None, date_from=None, date_to=None): + """Write draw history to filepath (.csv). Returns filepath.""" + rows = get_draws_with_game(game_id=game_id, date_from=date_from, + date_to=date_to, order="DESC") + with open(filepath, "w", newline="", encoding="utf-8") as f: + w = csv.writer(f) + w.writerow(_DRAW_HEADERS) + for row in rows: + w.writerow([row["game_name"], row["draw_date"], row["numbers"], + row["bonus"] or "", row["multiplier"] or "", + row["source"] or ""]) + return filepath + + +# ── Predictions ─────────────────────────────────────────────────────────────── + +_PRED_HEADERS = ["ID", "Game", "Strategy", "Numbers", "Bonus", "Created"] + + +def export_predictions_excel(filepath: str, game_id=None): + """Write saved predictions to filepath (.xlsx). Returns filepath.""" + preds = get_predictions(game_id=game_id, limit=100_000) + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "Predictions" + _header_row(ws, _PRED_HEADERS, "1A5276") + + for i, p in enumerate(preds, start=2): + ws.cell(i, 1, p["id"]) + ws.cell(i, 2, p["game_name"]) + ws.cell(i, 3, p["strategy"]) + ws.cell(i, 4, p["numbers"]) + ws.cell(i, 5, p["bonus"] or "") + ws.cell(i, 6, p["created_at"][:16]) + + _autofit(ws) + wb.save(filepath) + return filepath + + +def export_predictions_csv(filepath: str, game_id=None): + """Write saved predictions to filepath (.csv). Returns filepath.""" + preds = get_predictions(game_id=game_id, limit=100_000) + with open(filepath, "w", newline="", encoding="utf-8") as f: + w = csv.writer(f) + w.writerow(_PRED_HEADERS) + for p in preds: + w.writerow([p["id"], p["game_name"], p["strategy"], + p["numbers"], p["bonus"] or "", p["created_at"][:16]]) + return filepath + + +# ── Frequency Analysis ──────────────────────────────────────────────────────── + +_FREQ_HEADERS = ["Number", "Frequency", "Gap (draws since last seen)"] + + +def export_frequency_excel(filepath: str, game_id: int, last_n=None): + """Write frequency + gap analysis to filepath (.xlsx). Returns filepath.""" + from core.analyzer import frequency_analysis, gap_analysis + + freq = frequency_analysis(game_id, last_n=last_n) + gaps = gap_analysis(game_id) + + game = get_game_by_id(game_id) + all_nums = range(1, game["main_max"] + 1) + + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "Frequency Analysis" + _header_row(ws, _FREQ_HEADERS, "1E8449") + + for i, num in enumerate(all_nums, start=2): + ws.cell(i, 1, num) + ws.cell(i, 2, freq.get(num, 0)) + ws.cell(i, 3, gaps.get(num, "—")) + + _autofit(ws) + wb.save(filepath) + return filepath + + +# ── App icon (stdlib only, no Pillow required) ──────────────────────────────── + +def create_icon_png(path: str, size: int = 48): + """ + Write a simple lottery-ball icon as a valid PNG to path. + Uses only stdlib (struct + zlib) — no Pillow needed. + """ + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) + cx = cy = size // 2 + r_outer = cx - 2 + r_inner = cx - 10 + + rows = [] + for y in range(size): + row = b"\x00" # filter byte = None + for x in range(size): + d2 = (x - cx) ** 2 + (y - cy) ** 2 + if d2 <= r_outer * r_outer: + if d2 <= r_inner * r_inner: + row += bytes([255, 255, 255]) # white centre + else: + row += bytes([44, 110, 203]) # blue ring + else: + row += bytes([235, 235, 235]) # light-grey bg + rows.append(row) + + def _chunk(tag: bytes, data: bytes) -> bytes: + payload = tag + data + return (struct.pack(">I", len(data)) + payload + + struct.pack(">I", zlib.crc32(payload) & 0xFFFFFFFF)) + + png = ( + b"\x89PNG\r\n\x1a\n" + + _chunk(b"IHDR", struct.pack(">II5B", size, size, 8, 2, 0, 0, 0)) + + _chunk(b"IDAT", zlib.compress(b"".join(rows), 9)) + + _chunk(b"IEND", b"") + ) + with open(path, "wb") as fh: + fh.write(png) diff --git a/main.py b/main.py index ec4133d..a07eb95 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ Wires auto-fetch on launch (background thread) and 24hr APScheduler job. """ import logging +import os import threading import tkinter as tk from tkinter import ttk @@ -15,6 +16,7 @@ from apscheduler.schedulers.background import BackgroundScheduler from db.database import init_db from core.fetcher import fetch_all +from core.exporter import create_icon_png from ui.statusbar import StatusBar from ui.history import HistoryScreen from ui.analysis import AnalysisScreen @@ -170,6 +172,9 @@ class LottoSightApp(tk.Tk): def main(): init_db() + if not os.path.exists("assets/icon.png"): + os.makedirs("assets", exist_ok=True) + create_icon_png("assets/icon.png") app = LottoSightApp() app.protocol("WM_DELETE_WINDOW", app.on_close) app.mainloop() diff --git a/tests/test_exporter.py b/tests/test_exporter.py new file mode 100644 index 0000000..5850105 --- /dev/null +++ b/tests/test_exporter.py @@ -0,0 +1,287 @@ +""" +tests/test_exporter.py +----------------------- +Tests for core/exporter.py — draw history, predictions, frequency analysis exports. +All tests write to tmp paths; no file-dialog interaction required. +""" + +import csv +import os + +import openpyxl +import pytest + +from db.models import get_game_by_name, insert_draw, insert_prediction +from core.exporter import ( + export_draws_excel, + export_draws_csv, + export_predictions_excel, + export_predictions_csv, + export_frequency_excel, + create_icon_png, + ensure_exports_dir, + default_path, + EXPORTS_DIR, +) + + +# ── Fixtures ────────────────────────────────────────────────────────────────── + +@pytest.fixture() +def pb(tmp_db): + return get_game_by_name("Powerball") + + +@pytest.fixture() +def mm(tmp_db): + return get_game_by_name("Mega Millions") + + +@pytest.fixture() +def populated_db(pb, mm): + insert_draw(pb["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7, multiplier="2x") + insert_draw(pb["id"], "2024-01-03", [5, 10, 20, 30, 40], bonus=15, multiplier="3x") + insert_draw(mm["id"], "2024-01-02", [2, 14, 37, 62, 70], bonus=5) + insert_prediction(pb["id"], "Hot Numbers", [1, 13, 36, 61, 69], bonus=7) + insert_prediction(mm["id"], "Due Numbers", [2, 14, 37, 62, 70], bonus=5) + return pb, mm + + +# ── Draw Excel ──────────────────────────────────────────────────────────────── + +def test_draws_excel_creates_file(tmp_path, populated_db): + fp = str(tmp_path / "draws.xlsx") + result = export_draws_excel(fp) + assert result == fp + assert os.path.exists(fp) + + +def test_draws_excel_header(tmp_path, populated_db): + fp = str(tmp_path / "draws.xlsx") + export_draws_excel(fp) + wb = openpyxl.load_workbook(fp) + ws = wb.active + headers = [ws.cell(1, c).value for c in range(1, 7)] + assert headers == ["Game", "Date", "Numbers", "Bonus", "Multiplier", "Source"] + + +def test_draws_excel_row_count(tmp_path, populated_db): + pb, mm = populated_db + fp = str(tmp_path / "draws.xlsx") + export_draws_excel(fp) + wb = openpyxl.load_workbook(fp) + ws = wb.active + # 3 draws inserted (header is row 1) + assert ws.max_row == 4 + + +def test_draws_excel_game_filter(tmp_path, populated_db): + pb, mm = populated_db + fp = str(tmp_path / "pb_only.xlsx") + export_draws_excel(fp, game_id=pb["id"]) + wb = openpyxl.load_workbook(fp) + ws = wb.active + games_in_file = {ws.cell(r, 1).value for r in range(2, ws.max_row + 1)} + assert games_in_file == {"Powerball"} + + +def test_draws_excel_date_filter(tmp_path, populated_db): + fp = str(tmp_path / "filtered.xlsx") + export_draws_excel(fp, date_from="2024-01-03", date_to="2024-01-03") + wb = openpyxl.load_workbook(fp) + ws = wb.active + # Only the 2024-01-03 draw + assert ws.max_row == 2 + + +def test_draws_excel_empty_db(tmp_path, tmp_db): + fp = str(tmp_path / "empty.xlsx") + export_draws_excel(fp) + wb = openpyxl.load_workbook(fp) + ws = wb.active + assert ws.max_row == 1 # header only + + +# ── Draw CSV ────────────────────────────────────────────────────────────────── + +def test_draws_csv_creates_file(tmp_path, populated_db): + fp = str(tmp_path / "draws.csv") + result = export_draws_csv(fp) + assert result == fp + assert os.path.exists(fp) + + +def test_draws_csv_header(tmp_path, populated_db): + fp = str(tmp_path / "draws.csv") + export_draws_csv(fp) + with open(fp, newline="", encoding="utf-8") as f: + reader = csv.reader(f) + header = next(reader) + assert header == ["Game", "Date", "Numbers", "Bonus", "Multiplier", "Source"] + + +def test_draws_csv_row_count(tmp_path, populated_db): + fp = str(tmp_path / "draws.csv") + export_draws_csv(fp) + with open(fp, newline="", encoding="utf-8") as f: + rows = list(csv.reader(f)) + assert len(rows) == 4 # 1 header + 3 draws + + +def test_draws_csv_game_filter(tmp_path, populated_db): + pb, _ = populated_db + fp = str(tmp_path / "pb.csv") + export_draws_csv(fp, game_id=pb["id"]) + with open(fp, newline="", encoding="utf-8") as f: + rows = list(csv.reader(f)) + data_rows = rows[1:] + assert all(r[0] == "Powerball" for r in data_rows) + + +# ── Predictions Excel ───────────────────────────────────────────────────────── + +def test_predictions_excel_creates_file(tmp_path, populated_db): + fp = str(tmp_path / "preds.xlsx") + result = export_predictions_excel(fp) + assert result == fp + assert os.path.exists(fp) + + +def test_predictions_excel_header(tmp_path, populated_db): + fp = str(tmp_path / "preds.xlsx") + export_predictions_excel(fp) + wb = openpyxl.load_workbook(fp) + ws = wb.active + headers = [ws.cell(1, c).value for c in range(1, 7)] + assert headers == ["ID", "Game", "Strategy", "Numbers", "Bonus", "Created"] + + +def test_predictions_excel_row_count(tmp_path, populated_db): + fp = str(tmp_path / "preds.xlsx") + export_predictions_excel(fp) + wb = openpyxl.load_workbook(fp) + ws = wb.active + assert ws.max_row == 3 # 1 header + 2 predictions + + +def test_predictions_excel_game_filter(tmp_path, populated_db): + pb, _ = populated_db + fp = str(tmp_path / "pb_preds.xlsx") + export_predictions_excel(fp, game_id=pb["id"]) + wb = openpyxl.load_workbook(fp) + ws = wb.active + games = {ws.cell(r, 2).value for r in range(2, ws.max_row + 1)} + assert games == {"Powerball"} + + +# ── Predictions CSV ─────────────────────────────────────────────────────────── + +def test_predictions_csv_creates_file(tmp_path, populated_db): + fp = str(tmp_path / "preds.csv") + result = export_predictions_csv(fp) + assert result == fp + assert os.path.exists(fp) + + +def test_predictions_csv_header(tmp_path, populated_db): + fp = str(tmp_path / "preds.csv") + export_predictions_csv(fp) + with open(fp, newline="", encoding="utf-8") as f: + header = next(csv.reader(f)) + assert header == ["ID", "Game", "Strategy", "Numbers", "Bonus", "Created"] + + +def test_predictions_csv_row_count(tmp_path, populated_db): + fp = str(tmp_path / "preds.csv") + export_predictions_csv(fp) + with open(fp, newline="", encoding="utf-8") as f: + rows = list(csv.reader(f)) + assert len(rows) == 3 # 1 header + 2 predictions + + +# ── Frequency Excel ─────────────────────────────────────────────────────────── + +def test_frequency_excel_creates_file(tmp_path, populated_db): + pb, _ = populated_db + fp = str(tmp_path / "freq.xlsx") + result = export_frequency_excel(fp, pb["id"]) + assert result == fp + assert os.path.exists(fp) + + +def test_frequency_excel_header(tmp_path, populated_db): + pb, _ = populated_db + fp = str(tmp_path / "freq.xlsx") + export_frequency_excel(fp, pb["id"]) + wb = openpyxl.load_workbook(fp) + ws = wb.active + headers = [ws.cell(1, c).value for c in range(1, 4)] + assert headers == ["Number", "Frequency", "Gap (draws since last seen)"] + + +def test_frequency_excel_covers_full_pool(tmp_path, populated_db): + pb, _ = populated_db + fp = str(tmp_path / "freq.xlsx") + export_frequency_excel(fp, pb["id"]) + wb = openpyxl.load_workbook(fp) + ws = wb.active + # Powerball main_max=69: 69 data rows + 1 header = 70 + assert ws.max_row == 70 + + +def test_frequency_excel_number_column(tmp_path, populated_db): + pb, _ = populated_db + fp = str(tmp_path / "freq.xlsx") + export_frequency_excel(fp, pb["id"]) + wb = openpyxl.load_workbook(fp) + ws = wb.active + numbers = [ws.cell(r, 1).value for r in range(2, ws.max_row + 1)] + assert numbers == list(range(1, 70)) + + +# ── Icon PNG ────────────────────────────────────────────────────────────────── + +def test_create_icon_png_creates_file(tmp_path): + path = str(tmp_path / "icon.png") + create_icon_png(path) + assert os.path.exists(path) + + +def test_create_icon_png_is_valid_png(tmp_path): + path = str(tmp_path / "icon.png") + create_icon_png(path) + with open(path, "rb") as f: + sig = f.read(8) + assert sig == b"\x89PNG\r\n\x1a\n" + + +def test_create_icon_png_default_size(tmp_path): + path = str(tmp_path / "icon.png") + create_icon_png(path) + size = os.path.getsize(path) + assert size > 100 # a valid non-empty PNG + + +def test_create_icon_png_custom_size(tmp_path): + path = str(tmp_path / "icon32.png") + create_icon_png(path, size=32) + assert os.path.exists(path) + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def test_ensure_exports_dir_creates_directory(tmp_path, monkeypatch): + target = str(tmp_path / "new_exports") + monkeypatch.setattr("core.exporter.EXPORTS_DIR", target) + import core.exporter as exp + exp.EXPORTS_DIR = target + exp.ensure_exports_dir() + assert os.path.isdir(target) + + +def test_default_path_returns_string_with_stem(tmp_path, monkeypatch): + import core.exporter as exp + monkeypatch.setattr(exp, "EXPORTS_DIR", str(tmp_path)) + path = exp.default_path("draws", "xlsx") + assert "draws" in os.path.basename(path) + assert path.endswith(".xlsx") diff --git a/ui/analysis.py b/ui/analysis.py index d85aa1c..67d7469 100644 --- a/ui/analysis.py +++ b/ui/analysis.py @@ -7,8 +7,9 @@ Analysis screen — three Matplotlib charts embedded in a ttk.Notebook. • Gap — draws since each number last appeared """ +import os import tkinter as tk -from tkinter import ttk +from tkinter import ttk, filedialog, messagebox import numpy as np import matplotlib @@ -18,6 +19,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolb from db.models import get_all_games, get_game_by_id, get_game_by_name from core.analyzer import frequency_analysis, gap_analysis, positional_frequency +from core.exporter import export_frequency_excel, ensure_exports_dir _LAST_N_OPTIONS = { "All draws": None, @@ -60,6 +62,7 @@ class AnalysisScreen(ttk.Frame): last_n_cb.bind("<>", lambda _: self._redraw_frequency()) ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right") + ttk.Button(bar, text="Export Frequency", command=self._export_frequency).pack(side="right", padx=(0, 4)) # Notebook with three chart tabs nb = ttk.Notebook(self) @@ -141,6 +144,25 @@ class AnalysisScreen(ttk.Frame): _draw_gap(ax, self._game_id) self._gap_canvas.draw() + def _export_frequency(self): + if self._game_id is None: + messagebox.showinfo("Export", "Select a game first.") + return + ensure_exports_dir() + fp = filedialog.asksaveasfilename( + title="Export frequency analysis", + defaultextension=".xlsx", + filetypes=[("Excel workbook", "*.xlsx")], + initialfile=f"frequency_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.xlsx", + ) + if not fp: + return + try: + last_n = _LAST_N_OPTIONS.get(self._last_n_var.get()) + export_frequency_excel(fp, self._game_id, last_n=last_n) + except Exception as e: + messagebox.showerror("Export failed", str(e)) + # ── Pure chart-drawing functions (no Tkinter, just axes) ───────────────────── diff --git a/ui/history.py b/ui/history.py index d5b5b86..f65922a 100644 --- a/ui/history.py +++ b/ui/history.py @@ -5,10 +5,14 @@ Draw History browser screen. Shows a sortable, filterable Treeview of all draw records. """ +import os import tkinter as tk -from tkinter import ttk +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, +) _COLUMNS = ("game", "date", "numbers", "bonus", "multiplier", "source") _LABELS = { @@ -77,7 +81,9 @@ class HistoryScreen(ttk.Frame): 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="↻ 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) @@ -213,3 +219,54 @@ class HistoryScreen(ttk.Frame): 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 _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)) diff --git a/ui/predictor_ui.py b/ui/predictor_ui.py index a7513a9..820e80b 100644 --- a/ui/predictor_ui.py +++ b/ui/predictor_ui.py @@ -5,14 +5,16 @@ Prediction generator screen. Pick a strategy + game + ticket count → generate → save to DB. """ +import os import tkinter as tk -from tkinter import ttk +from tkinter import ttk, filedialog, messagebox import logging from db.models import get_all_games, get_game_by_name, insert_prediction from core.predictor import ( hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, ) +from core.exporter import export_predictions_excel, export_predictions_csv, ensure_exports_dir logger = logging.getLogger(__name__) @@ -121,6 +123,14 @@ class PredictorScreen(ttk.Frame): bottom, text="Clear", command=self._clear ).pack(side="right", padx=(0, 4)) + ttk.Button( + bottom, text="Export CSV", command=self._export_csv, state="normal" + ).pack(side="right", padx=(0, 4)) + + ttk.Button( + bottom, text="Export Excel", command=self._export_excel, state="normal" + ).pack(side="right", padx=(0, 4)) + # ── Callbacks ───────────────────────────────────────────────────────────── def refresh(self): @@ -198,3 +208,37 @@ class PredictorScreen(ttk.Frame): self._tree.delete(*self._tree.get_children()) self._save_btn.config(state="disabled") self._status_var.set("") + + def _export_excel(self): + ensure_exports_dir() + fp = filedialog.asksaveasfilename( + title="Export predictions", + defaultextension=".xlsx", + filetypes=[("Excel workbook", "*.xlsx")], + initialfile=f"predictions_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.xlsx", + ) + if not fp: + return + try: + game_id = self._game_id + export_predictions_excel(fp, game_id=game_id) + self._status_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 predictions", + defaultextension=".csv", + filetypes=[("CSV file", "*.csv")], + initialfile=f"predictions_{__import__('datetime').datetime.now().strftime('%Y%m%d')}.csv", + ) + if not fp: + return + try: + game_id = self._game_id + export_predictions_csv(fp, game_id=game_id) + self._status_var.set(f"Exported → {os.path.basename(fp)}") + except Exception as e: + messagebox.showerror("Export failed", str(e))