diff --git a/backend/database.py b/backend/database.py index 8a00a29..2ff12c3 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1469,3 +1469,16 @@ def _migrate(conn_factory): ex ) logger.info(f"Migration: Übung '{ex[1]}' eingefügt.") + + # Gespeicherte KI-Jahresberichte für Züchter + conn.executescript(""" + CREATE TABLE IF NOT EXISTS breeder_jahresberichte ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + breeder_id INTEGER NOT NULL, + jahr INTEGER NOT NULL, + text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_bj_user ON breeder_jahresberichte(user_id, jahr DESC); + """) diff --git a/backend/routes/zucht_ki.py b/backend/routes/zucht_ki.py index 0d366c2..e25c49c 100644 --- a/backend/routes/zucht_ki.py +++ b/backend/routes/zucht_ki.py @@ -462,7 +462,45 @@ Zeitraum: letzte 2 Jahre (bis {date.today().isoformat()}) requires_premium=False, user_id=user["id"], ) - return {"text": text} except Exception as e: logger.warning(f"KI nicht verfügbar: {e}") - return {"text": _FALLBACK} + text = _FALLBACK + + # Bericht speichern + jahr = date.today().year + with db() as conn: + bp2 = conn.execute("SELECT id FROM breeder_profiles WHERE user_id=?", (user["id"],)).fetchone() + if bp2: + conn.execute( + "INSERT INTO breeder_jahresberichte (user_id, breeder_id, jahr, text) VALUES (?,?,?,?)", + (user["id"], bp2["id"], jahr, text) + ) + saved_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] + else: + saved_id = None + + return {"text": text, "saved_id": saved_id, "jahr": jahr} + + +# GET gespeicherte Berichte +# ------------------------------------------------------------------ +@router.get("/zucht-ki/jahresbericht") +async def jahresbericht_list(user=Depends(_require_breeder)): + with db() as conn: + rows = conn.execute( + "SELECT id, jahr, created_at FROM breeder_jahresberichte WHERE user_id=? ORDER BY created_at DESC LIMIT 20", + (user["id"],) + ).fetchall() + return [{"id": r["id"], "jahr": r["jahr"], "created_at": r["created_at"]} for r in rows] + + +@router.get("/zucht-ki/jahresbericht/{bericht_id}") +async def jahresbericht_get(bericht_id: int, user=Depends(_require_breeder)): + with db() as conn: + row = conn.execute( + "SELECT id, jahr, text, created_at FROM breeder_jahresberichte WHERE id=? AND user_id=?", + (bericht_id, user["id"]) + ).fetchone() + if not row: + raise HTTPException(404, "Bericht nicht gefunden.") + return dict(row) diff --git a/backend/static/js/api.js b/backend/static/js/api.js index ea6ad6d..e4623f0 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -692,6 +692,8 @@ const API = (() => { }, hundBeschreibung(hundId) { return post('/zucht-ki/hund-beschreibung', { hund_id: hundId }); }, jahresbericht() { return post('/zucht-ki/jahresbericht', {}); }, + jahresberichtList() { return get('/zucht-ki/jahresbericht'); }, + jahresberichtGet(id) { return get(`/zucht-ki/jahresbericht/${id}`); }, }; const osm = { diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 1d17ac9..6c32af2 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '481'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '482'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/zuchthunde.js b/backend/static/js/pages/zuchthunde.js index 0b515cc..78fbb3e 100644 --- a/backend/static/js/pages/zuchthunde.js +++ b/backend/static/js/pages/zuchthunde.js @@ -117,6 +117,9 @@ window.Page_zuchthunde = (() => { ${_appState?.user?.ki_zucht_jahresbericht !== 0 ? ` ${UI.icon('chart-bar')} Jahresbericht + + + ${UI.icon('archive')} ` : ''}
@@ -132,6 +135,7 @@ window.Page_zuchthunde = (() => { document.getElementById('zh-new-btn')?.addEventListener('click', () => _showHundForm(null)); document.getElementById('zh-trial-btn')?.addEventListener('click', () => _showTrialMatingModal()); document.getElementById('zh-jahresbericht-btn')?.addEventListener('click', () => _showJahresbericht()); + document.getElementById('zh-jahresbericht-archiv-btn')?.addEventListener('click', () => _showJahresberichtArchiv()); document.getElementById('zh-search')?.addEventListener('input', e => { _query = e.target.value.toLowerCase().trim(); @@ -1285,7 +1289,7 @@ window.Page_zuchthunde = (() => { } // ---------------------------------------------------------- - // KI: Jahresbericht + // KI: Jahresbericht generieren // ---------------------------------------------------------- async function _showJahresbericht() { UI.modal.open({ @@ -1294,10 +1298,12 @@ window.Page_zuchthunde = (() => { footer: '', }); - let text = ''; + let text = '', savedId = null, jahr = new Date().getFullYear(); try { const result = await API.zuchtKi.jahresbericht(); - text = result.text || result.content || result.bericht || JSON.stringify(result); + text = result.text || result.content || result.bericht || JSON.stringify(result); + savedId = result.saved_id; + jahr = result.jahr || jahr; } catch (err) { UI.modal.open({ title: `${UI.icon('chart-bar')} KI-Jahresbericht`, @@ -1307,23 +1313,93 @@ window.Page_zuchthunde = (() => { return; } + _renderBerichtModal(text, jahr, savedId); + } + + function _renderBerichtModal(text, jahr, savedId) { UI.modal.open({ - title: `${UI.icon('chart-bar')} KI-Jahresbericht`, - body: `
${_esc(text)}
`, + title: `${UI.icon('chart-bar')} KI-Jahresbericht ${jahr}`, + body: ` + ${savedId ? `

+ ${UI.icon('check-circle')} Automatisch gespeichert

` : ''} +
${_esc(text)}
`, footer: ` - - `, + + `, }); document.getElementById('ki-bericht-copy')?.addEventListener('click', async () => { - try { - await navigator.clipboard.writeText(text); - UI.toast.success('Bericht kopiert.'); - } catch { - UI.toast.error('Kopieren nicht möglich.'); - } + try { await navigator.clipboard.writeText(text); UI.toast.success('Bericht kopiert.'); } + catch { UI.toast.error('Kopieren nicht möglich.'); } + }); + + document.getElementById('ki-bericht-download')?.addEventListener('click', () => { + const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = `ban-yaro-jahresbericht-${jahr}.txt`; + a.click(); URL.revokeObjectURL(url); + }); + } + + // KI: Archiv früherer Berichte + // ---------------------------------------------------------- + async function _showJahresberichtArchiv() { + UI.modal.open({ + title: `${UI.icon('archive')} Gespeicherte Jahresberichte`, + body: `

Lädt…

`, + footer: ``, + }); + + let berichte = []; + try { berichte = await API.zuchtKi.jahresberichtList(); } + catch { UI.toast.error('Berichte konnten nicht geladen werden.'); return; } + + if (!berichte.length) { + UI.modal.open({ + title: `${UI.icon('archive')} Gespeicherte Jahresberichte`, + body: `

Noch keine Berichte gespeichert.

`, + footer: ``, + }); + return; + } + + const listHtml = berichte.map(b => ` +
+
+
+ Jahresbericht ${b.jahr} +
+
+ ${new Date(b.created_at).toLocaleDateString('de', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})} +
+
+ +
`).join(''); + + UI.modal.open({ + title: `${UI.icon('archive')} Gespeicherte Jahresberichte`, + body: `
${listHtml}
`, + footer: ``, + }); + + document.querySelectorAll('[data-bericht-id]').forEach(btn => { + btn.addEventListener('click', async () => { + const id = Number(btn.dataset.berichtId); + const jahr = Number(btn.dataset.berichtJahr); + try { + const r = await API.zuchtKi.jahresberichtGet(id); + _renderBerichtModal(r.text, r.jahr || jahr, r.id); + } catch { UI.toast.error('Bericht konnte nicht geladen werden.'); } + }); }); } diff --git a/backend/static/sw.js b/backend/static/sw.js index 5dd6300..55c0b13 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v504'; +const CACHE_VERSION = 'by-v505'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten