From b8a5dc7a663556c78b481468402b302d04e8933e Mon Sep 17 00:00:00 2001 From: rene Date: Mon, 13 Apr 2026 20:45:52 +0200 Subject: [PATCH] Feat: Gesundheits-Erinnerungen mit Wiederkehrend-Intervall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - intervall_tage Feld (monatlich/vierteljährlich/jährlich etc.) - Impfung, Entwurmung, Medikament: Intervall-Auswahl im Formular - Erinnerungs-Banner über den Tabs: zeigt alle Einträge die in ≤60 Tagen fällig sind - Ampel-Farbe am linken Rand (rot=überfällig, gelb=bald, grün=ok) - "✓ Erledigt"-Button öffnet neues Formular vorausgefüllt mit heute + nächstem Termin - Nav-Badge (Zahl) auf Gesundheit-Icon wenn Einträge überfällig/bald fällig - _showForm: isEdit prüft entry?.id statt !!entry (Reminder-Flow) - SW-Cache → by-v16 --- backend/database.py | 2 + backend/routes/health.py | 2 + backend/static/js/pages/health.js | 170 ++++++++++++++++++++++++++++-- backend/static/sw.js | 2 +- 4 files changed, 165 insertions(+), 11 deletions(-) diff --git a/backend/database.py b/backend/database.py index 0bb98d6..3559537 100644 --- a/backend/database.py +++ b/backend/database.py @@ -301,6 +301,8 @@ def _migrate(conn_factory): ("tieraerzte", "strasse", "TEXT"), ("tieraerzte", "plz", "TEXT"), ("tieraerzte", "ort", "TEXT"), + # Gesundheit: Erinnerungsintervall für wiederkehrende Einträge + ("health", "intervall_tage", "INTEGER"), ] with conn_factory() as conn: for table, column, col_type in migrations: diff --git a/backend/routes/health.py b/backend/routes/health.py index e3eba24..6bb2592 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -42,6 +42,7 @@ class HealthCreate(BaseModel): schweregrad: Optional[str] = None # leicht | mittel | schwer reaktion: Optional[str] = None erinnerung: Optional[int] = 1 + intervall_tage: Optional[int] = None # Wiederkehrend alle X Tage # Tierarzt-Verknüpfung tierarzt_id: Optional[int] = None @@ -64,6 +65,7 @@ class HealthUpdate(BaseModel): schweregrad: Optional[str] = None reaktion: Optional[str] = None erinnerung: Optional[int] = None + intervall_tage: Optional[int] = None tierarzt_id: Optional[int] = None diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js index 094ffa9..89b1d11 100644 --- a/backend/static/js/pages/health.js +++ b/backend/static/js/pages/health.js @@ -122,6 +122,7 @@ window.Page_health = (() => { ✨ KI-Zusammenfassung +
`; @@ -131,9 +132,126 @@ window.Page_health = (() => { .addEventListener('click', _showKiSummary); await _loadAll(); + _renderErinnerungen(); _renderTab(); } + // ---------------------------------------------------------- + // ERINNERUNGEN — Banner über den Tabs + // ---------------------------------------------------------- + function _getErinnerungen() { + const REMINDER_TABS = ['impfung', 'entwurmung', 'medikament']; + const now = Date.now(); + const items = []; + REMINDER_TABS.forEach(typ => { + (_data[typ] || []).forEach(e => { + if (!e.naechstes) return; + const tage = Math.ceil((new Date(e.naechstes).getTime() - now) / 86400000); + if (tage <= 60) items.push({ ...e, _tage: tage, _typ: typ }); + }); + }); + return items.sort((a, b) => a._tage - b._tage); + } + + function _renderErinnerungen() { + const el = _container.querySelector('#health-reminders'); + if (!el) return; + + const items = _getErinnerungen(); + + // Nav-Badge aktualisieren (Anzahl überfälliger/bald fälliger Einträge) + const overdueCount = items.filter(e => e._tage < 0).length; + _updateHealthBadge(overdueCount || (items.length ? items.length : 0)); + + if (!items.length) { el.innerHTML = ''; return; } + + const ICONS = { impfung: '💉', entwurmung: '🪱', medikament: '💊' }; + + el.innerHTML = ` +
+
+ 📅 Anstehende Erinnerungen +
+ ${items.map(e => { + const ampel = _impfAmpel(e.naechstes); + const dateStr = UI.time.format(e.naechstes + 'T00:00:00'); + const ageLabel = e._tage < 0 + ? `Überfällig seit ${Math.abs(e._tage)} Tagen` + : e._tage === 0 ? 'Heute fällig' + : `In ${e._tage} Tagen`; + return ` +
+ ${ICONS[e._typ] || '📋'} +
+
+ ${_esc(e.bezeichnung)} +
+
+ ${ageLabel} · ${dateStr} +
+
+ +
`; + }).join('')} +
+ `; + + el.querySelectorAll('[data-action="reminder-erledigt"]').forEach(btn => { + btn.addEventListener('click', () => { + const id = parseInt(btn.dataset.entryId); + const typ = btn.dataset.entryTyp; + const entry = (_data[typ] || []).find(e => e.id === id); + if (!entry) return; + // Neues Formular ohne id → Neu-Eintrag, vorausgefüllt + const today = new Date().toISOString().slice(0, 10); + let naechstes = ''; + if (entry.intervall_tage) { + const next = new Date(); + next.setDate(next.getDate() + entry.intervall_tage); + naechstes = next.toISOString().slice(0, 10); + } + _showForm({ + bezeichnung: entry.bezeichnung, + datum: today, + naechstes, + intervall_tage: entry.intervall_tage, + tierarzt_id: entry.tierarzt_id, + tierarzt_name: entry.tierarzt_name, + charge_nr: entry.charge_nr, + }, typ); + }); + }); + } + + function _updateHealthBadge(count) { + ['[data-page="health"] .nav-item-icon', + '[data-page="health"] .sidebar-item-icon'].forEach(sel => { + document.querySelectorAll(sel).forEach(el => { + let badge = el.querySelector('.nav-badge'); + if (count > 0) { + if (!badge) { + badge = document.createElement('span'); + badge.className = 'nav-badge'; + el.appendChild(badge); + } + badge.textContent = count > 9 ? '9+' : count; + } else if (badge) { + badge.remove(); + } + }); + }); + } + function _renderTabBar() { const tabsEl = _container.querySelector('#health-tabs'); tabsEl.innerHTML = TABS.map(t => ` @@ -624,7 +742,7 @@ window.Page_health = (() => { // FORMULAR — Neu / Bearbeiten // ---------------------------------------------------------- function _showForm(entry, typ) { - const isEdit = !!entry; + const isEdit = !!(entry?.id); const today = new Date().toISOString().slice(0, 10); const t = typ || _activeTab; @@ -739,6 +857,28 @@ window.Page_health = (() => { return ph[typ] || ''; } + // Intervall-Auswahl für wiederkehrende Einträge + function _intervallField(entry) { + const v = entry?.intervall_tage; + const opts = [ + [null, 'Einmalig'], + [30, 'Monatlich (30 Tage)'], + [60, 'Alle 2 Monate'], + [90, 'Vierteljährlich (90 Tage)'], + [180, 'Halbjährlich'], + [365, 'Jährlich'], + ]; + return ` +
+ + +
`; + } + // Wiederverwendbares Praxis-Dropdown für alle Formulare function _praxisSelectField(entry) { const aktivePraxen = _praxen.filter(p => p.aktiv); @@ -759,9 +899,12 @@ window.Page_health = (() => { function _extraFormFields(entry, typ) { switch (typ) { case 'impfung': return ` -
- - +
+
+ + +
+ ${_intervallField(entry)}
${_praxisSelectField(entry)}
@@ -770,9 +913,12 @@ window.Page_health = (() => {
`; case 'entwurmung': return ` -
- - +
+
+ + +
+ ${_intervallField(entry)}
${_praxisSelectField(entry)} `; @@ -836,9 +982,12 @@ window.Page_health = (() => {
-
- - +
+
+ + +
+ ${_intervallField(entry)}
${_praxisSelectField(entry)}
@@ -896,6 +1045,7 @@ window.Page_health = (() => { if (typ === 'medikament') { p.aktiv = 'aktiv' in fd ? 1 : 0; } + p.intervall_tage = fd.intervall_tage ? parseInt(fd.intervall_tage) : null; // Gewicht-Einheit p.einheit = fd.einheit || 'kg'; return p; diff --git a/backend/static/sw.js b/backend/static/sw.js index 62304ca..cc5fa3a 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications ============================================================ */ -const CACHE_VERSION = 'by-v15'; +const CACHE_VERSION = 'by-v16'; const CACHE_STATIC = `${CACHE_VERSION}-static`; // Diese Dateien werden beim Install gecacht (App Shell)