Feat: Gesundheits-Erinnerungen mit Wiederkehrend-Intervall

- 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
This commit is contained in:
rene 2026-04-13 20:45:52 +02:00
parent 5c178f812b
commit b8a5dc7a66
4 changed files with 165 additions and 11 deletions

View file

@ -122,6 +122,7 @@ window.Page_health = (() => {
KI-Zusammenfassung
</button>
</div>
<div id="health-reminders"></div>
<div class="health-tabs" id="health-tabs"></div>
<div id="health-tab-content"></div>
`;
@ -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 = `
<div style="padding:var(--space-3) var(--space-4) 0">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2)">
📅 Anstehende Erinnerungen
</div>
${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 `
<div style="display:flex;align-items:center;gap:var(--space-3);
padding:var(--space-2) var(--space-3);margin-bottom:var(--space-1);
background:var(--c-surface);border-radius:var(--radius-md);
border-left:3px solid ${ampel.color === 'red' ? '#ef4444' : ampel.color === 'yellow' ? '#f59e0b' : '#22c55e'}">
<span style="font-size:1.2rem">${ICONS[e._typ] || '📋'}</span>
<div style="flex:1;min-width:0">
<div style="font-size:var(--text-sm);font-weight:var(--weight-medium);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(e.bezeichnung)}
</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
${ageLabel} · ${dateStr}
</div>
</div>
<button class="btn btn-sm ${e._tage < 0 ? 'btn-primary' : 'btn-secondary'}"
data-action="reminder-erledigt"
data-entry-id="${e.id}" data-entry-typ="${e._typ}"
style="flex-shrink:0;white-space:nowrap">
Erledigt
</button>
</div>`;
}).join('')}
</div>
`;
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 `
<div class="form-group">
<label class="form-label">Wiederholt sich</label>
<select class="form-control" name="intervall_tage">
${opts.map(([days, label]) =>
`<option value="${days ?? ''}" ${(v == days) ? 'selected' : ''}>${label}</option>`
).join('')}
</select>
</div>`;
}
// 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 `
<div class="form-group">
<label class="form-label">Nächste Impfung (optional)</label>
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Nächste Impfung</label>
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
</div>
${_intervallField(entry)}
</div>
${_praxisSelectField(entry)}
<div class="form-group">
@ -770,9 +913,12 @@ window.Page_health = (() => {
</div>
`;
case 'entwurmung': return `
<div class="form-group">
<label class="form-label">Nächste Behandlung (optional)</label>
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Nächste Behandlung</label>
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
</div>
${_intervallField(entry)}
</div>
${_praxisSelectField(entry)}
`;
@ -836,9 +982,12 @@ window.Page_health = (() => {
<input class="form-control" type="text" name="haeufigkeit"
value="${_esc(entry?.haeufigkeit || '')}" placeholder="z.B. täglich, 2x wöchentlich">
</div>
<div class="form-group">
<label class="form-label">Gabe bis (optional)</label>
<input class="form-control" type="date" name="bis_datum" value="${entry?.bis_datum || ''}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Gabe bis (optional)</label>
<input class="form-control" type="date" name="bis_datum" value="${entry?.bis_datum || ''}">
</div>
${_intervallField(entry)}
</div>
${_praxisSelectField(entry)}
<div class="form-group">
@ -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;

View file

@ -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)