Feature: KI-Jahresberichte speichern + Archiv + Download — SW by-v505, APP_VER 482
This commit is contained in:
parent
b4de0aa27c
commit
9832cd24d8
6 changed files with 146 additions and 17 deletions
|
|
@ -1469,3 +1469,16 @@ def _migrate(conn_factory):
|
||||||
ex
|
ex
|
||||||
)
|
)
|
||||||
logger.info(f"Migration: Übung '{ex[1]}' eingefügt.")
|
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);
|
||||||
|
""")
|
||||||
|
|
|
||||||
|
|
@ -462,7 +462,45 @@ Zeitraum: letzte 2 Jahre (bis {date.today().isoformat()})
|
||||||
requires_premium=False,
|
requires_premium=False,
|
||||||
user_id=user["id"],
|
user_id=user["id"],
|
||||||
)
|
)
|
||||||
return {"text": text}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"KI nicht verfügbar: {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)
|
||||||
|
|
|
||||||
|
|
@ -692,6 +692,8 @@ const API = (() => {
|
||||||
},
|
},
|
||||||
hundBeschreibung(hundId) { return post('/zucht-ki/hund-beschreibung', { hund_id: hundId }); },
|
hundBeschreibung(hundId) { return post('/zucht-ki/hund-beschreibung', { hund_id: hundId }); },
|
||||||
jahresbericht() { return post('/zucht-ki/jahresbericht', {}); },
|
jahresbericht() { return post('/zucht-ki/jahresbericht', {}); },
|
||||||
|
jahresberichtList() { return get('/zucht-ki/jahresbericht'); },
|
||||||
|
jahresberichtGet(id) { return get(`/zucht-ki/jahresbericht/${id}`); },
|
||||||
};
|
};
|
||||||
|
|
||||||
const osm = {
|
const osm = {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,9 @@ window.Page_zuchthunde = (() => {
|
||||||
${_appState?.user?.ki_zucht_jahresbericht !== 0 ? `
|
${_appState?.user?.ki_zucht_jahresbericht !== 0 ? `
|
||||||
<a class="btn btn-ghost btn-sm" id="zh-jahresbericht-btn">
|
<a class="btn btn-ghost btn-sm" id="zh-jahresbericht-btn">
|
||||||
${UI.icon('chart-bar')} Jahresbericht
|
${UI.icon('chart-bar')} Jahresbericht
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-ghost btn-sm" id="zh-jahresbericht-archiv-btn" title="Frühere Berichte">
|
||||||
|
${UI.icon('archive')}
|
||||||
</a>` : ''}
|
</a>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:0 0 var(--space-3)">
|
<div style="padding:0 0 var(--space-3)">
|
||||||
|
|
@ -132,6 +135,7 @@ window.Page_zuchthunde = (() => {
|
||||||
document.getElementById('zh-new-btn')?.addEventListener('click', () => _showHundForm(null));
|
document.getElementById('zh-new-btn')?.addEventListener('click', () => _showHundForm(null));
|
||||||
document.getElementById('zh-trial-btn')?.addEventListener('click', () => _showTrialMatingModal());
|
document.getElementById('zh-trial-btn')?.addEventListener('click', () => _showTrialMatingModal());
|
||||||
document.getElementById('zh-jahresbericht-btn')?.addEventListener('click', () => _showJahresbericht());
|
document.getElementById('zh-jahresbericht-btn')?.addEventListener('click', () => _showJahresbericht());
|
||||||
|
document.getElementById('zh-jahresbericht-archiv-btn')?.addEventListener('click', () => _showJahresberichtArchiv());
|
||||||
|
|
||||||
document.getElementById('zh-search')?.addEventListener('input', e => {
|
document.getElementById('zh-search')?.addEventListener('input', e => {
|
||||||
_query = e.target.value.toLowerCase().trim();
|
_query = e.target.value.toLowerCase().trim();
|
||||||
|
|
@ -1285,7 +1289,7 @@ window.Page_zuchthunde = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// KI: Jahresbericht
|
// KI: Jahresbericht generieren
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
async function _showJahresbericht() {
|
async function _showJahresbericht() {
|
||||||
UI.modal.open({
|
UI.modal.open({
|
||||||
|
|
@ -1294,10 +1298,12 @@ window.Page_zuchthunde = (() => {
|
||||||
footer: '',
|
footer: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
let text = '';
|
let text = '', savedId = null, jahr = new Date().getFullYear();
|
||||||
try {
|
try {
|
||||||
const result = await API.zuchtKi.jahresbericht();
|
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) {
|
} catch (err) {
|
||||||
UI.modal.open({
|
UI.modal.open({
|
||||||
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
|
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
|
||||||
|
|
@ -1307,23 +1313,93 @@ window.Page_zuchthunde = (() => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderBerichtModal(text, jahr, savedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderBerichtModal(text, jahr, savedId) {
|
||||||
UI.modal.open({
|
UI.modal.open({
|
||||||
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
|
title: `${UI.icon('chart-bar')} KI-Jahresbericht ${jahr}`,
|
||||||
body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`,
|
body: `
|
||||||
|
${savedId ? `<p style="font-size:var(--text-xs);color:var(--c-success);margin:0 0 var(--space-3);display:flex;align-items:center;gap:4px">
|
||||||
|
${UI.icon('check-circle')} Automatisch gespeichert</p>` : ''}
|
||||||
|
<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`,
|
||||||
footer: `
|
footer: `
|
||||||
<button class="btn btn-secondary flex-1" id="ki-bericht-copy">
|
<button class="btn btn-ghost btn-sm" id="ki-bericht-copy">
|
||||||
${UI.icon('clipboard-text')} Kopieren
|
${UI.icon('clipboard-text')} Kopieren
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary flex-1" data-modal-close>Schließen</button>`,
|
<button class="btn btn-ghost btn-sm" id="ki-bericht-download">
|
||||||
|
${UI.icon('download-simple')} Herunterladen
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" data-modal-close>Schließen</button>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('ki-bericht-copy')?.addEventListener('click', async () => {
|
document.getElementById('ki-bericht-copy')?.addEventListener('click', async () => {
|
||||||
try {
|
try { await navigator.clipboard.writeText(text); UI.toast.success('Bericht kopiert.'); }
|
||||||
await navigator.clipboard.writeText(text);
|
catch { UI.toast.error('Kopieren nicht möglich.'); }
|
||||||
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: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-4)">Lädt…</p>`,
|
||||||
|
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
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: `<p style="color:var(--c-text-muted);text-align:center;padding:var(--space-6)">Noch keine Berichte gespeichert.</p>`,
|
||||||
|
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listHtml = berichte.map(b => `
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;
|
||||||
|
padding:var(--space-3) 0;border-bottom:1px solid var(--c-border-light)">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
|
||||||
|
Jahresbericht ${b.jahr}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||||
|
${new Date(b.created_at).toLocaleDateString('de', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" data-bericht-id="${b.id}" data-bericht-jahr="${b.jahr}">
|
||||||
|
${UI.icon('eye')} Lesen
|
||||||
|
</button>
|
||||||
|
</div>`).join('');
|
||||||
|
|
||||||
|
UI.modal.open({
|
||||||
|
title: `${UI.icon('archive')} Gespeicherte Jahresberichte`,
|
||||||
|
body: `<div style="padding:0 var(--space-1)">${listHtml}</div>`,
|
||||||
|
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
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.'); }
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v504';
|
const CACHE_VERSION = 'by-v505';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue