Feature: Pflege-Routinen (Zecken-/Flohschutz, Krallen, Fellpflege) — neuer Pflege-Tab mit Erledigt+Auto-Wiedervorlage, Push-Erinnerungen, intervall_tage-Fix im INSERT, SW v1132

This commit is contained in:
rene 2026-05-29 10:32:05 +02:00
parent 8c2bc0c445
commit a356626d39
9 changed files with 187 additions and 26 deletions

View file

@ -19,6 +19,7 @@ window.Page_health = (() => {
{ key: 'tierarzt', label: 'Besuche', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
{ key: 'gewicht', label: 'Gewicht', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>' },
{ key: 'medikament', label: 'Medikamente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
{ key: 'pflege', label: 'Pflege', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>' },
{ key: 'allergie', label: 'Allergien', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#leaf"></use></svg>' },
{ key: 'dokument', label: 'Dokumente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg>' },
{ key: 'praxen', label: 'Praxen', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
@ -27,6 +28,14 @@ window.Page_health = (() => {
];
const LAEUFIGKEIT_TAB = { key: 'laeufigkeit', label: 'Läufigkeit', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-female"></use></svg>' };
// Pflege-Routinen — wiederkehrende Pflege-Aufgaben, gebündelt im 'pflege'-Tab
const PFLEGE_TYPEN = ['parasit', 'krallen', 'fellpflege'];
const PFLEGE_META = {
parasit: { label: 'Zecken-/Flohschutz', icon: 'bug-beetle', placeholder: 'z.B. Frontline, Seresto-Halsband' },
krallen: { label: 'Krallen schneiden', icon: 'scissors', placeholder: 'z.B. Krallen kürzen' },
fellpflege: { label: 'Fellpflege', icon: 'wind', placeholder: 'z.B. Bürsten, Trimmen, Baden' },
};
function _getTabs() {
const tabs = [...BASE_TABS];
if (_appState?.activeDog?.geschlecht === 'w') tabs.splice(2, 0, LAEUFIGKEIT_TAB);
@ -290,6 +299,8 @@ window.Page_health = (() => {
_data = {};
_getTabs().forEach(t => { _data[t.key] = []; });
_data['laeufigkeit'] = _data['laeufigkeit'] || [];
// Pflege-Routinen: eigene Listen je Typ (Tab 'pflege' bündelt sie beim Rendern)
PFLEGE_TYPEN.forEach(t => { _data[t] = []; });
all.forEach(e => {
if (_data[e.typ] !== undefined) _data[e.typ].push(e);
});
@ -333,6 +344,7 @@ window.Page_health = (() => {
case 'gewicht': content.innerHTML = _renderGewicht(entries); break;
case 'laeufigkeit': content.innerHTML = _renderLaeufigkeit(entries); break;
case 'medikament': content.innerHTML = _renderMedikamente(entries); break;
case 'pflege': content.innerHTML = _renderPflege(); break;
case 'allergie': content.innerHTML = _renderAllergien(entries); break;
case 'dokument': content.innerHTML = _renderDokumente(entries); break;
case 'praxen': content.innerHTML = _renderPraxen(); break;
@ -410,6 +422,65 @@ window.Page_health = (() => {
return { color: 'green', label: 'Aktuell', icon: '🟢' };
}
// ----------------------------------------------------------
// PFLEGE-ROUTINEN (Zecken-/Flohschutz, Krallen, Fellpflege)
// ----------------------------------------------------------
function _intervallLabel(tage) {
if (!tage) return '';
const m = { 30: 'monatlich', 60: 'alle 2 Monate', 90: 'vierteljährlich', 180: 'halbjährlich', 365: 'jährlich' };
return m[tage] || `alle ${tage} Tage`;
}
function _renderPflege() {
const addButtons = `
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-3)">
${PFLEGE_TYPEN.map(t => `
<button class="btn btn-secondary btn-sm" data-action="add-routine" data-typ="${t}">
${UI.icon(PFLEGE_META[t].icon)} ${PFLEGE_META[t].label}
</button>`).join('')}
</div>`;
const all = PFLEGE_TYPEN.flatMap(t => (_data[t] || []).map(e => ({ ...e, _typ: t })));
if (!all.length) return addButtons + _emptyState(
'paw-print',
'Noch keine Pflege-Routinen',
'Lege wiederkehrende Routinen wie Zecken-/Flohschutz, Krallenschneiden oder Fellpflege an — wir erinnern dich rechtzeitig.'
);
// Fällige zuerst (nach naechstes), Einträge ohne Folgedatum ans Ende
all.sort((a, b) => {
if (!a.naechstes) return 1;
if (!b.naechstes) return -1;
return a.naechstes.localeCompare(b.naechstes);
});
const items = all.map(e => {
const meta = PFLEGE_META[e._typ];
const ampel = e.naechstes ? _impfAmpel(e.naechstes) : null;
const interv = _intervallLabel(e.intervall_tage);
return `
<div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry">
${ampel ? `<div class="health-card-ampel ampel-${ampel.color}" title="${ampel.label}"></div>` : ''}
<div class="list-item-body">
<div class="list-item-title">${UI.icon(meta.icon)} ${UI.escape(e.bezeichnung || meta.label)}</div>
<div class="list-item-meta-row">
${meta.label}${e.datum ? ` · zuletzt ${UI.time.format(e.datum + 'T00:00:00')}` : ''}${interv ? ` · ${interv}` : ''}
</div>
${e.naechstes ? `<div class="health-card-next ampel-text-${ampel.color}">
Nächste: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
</div>` : ''}
</div>
<button class="btn btn-sm btn-primary" data-action="routine-erledigt" data-id="${e.id}"
style="flex-shrink:0;white-space:nowrap">
${UI.icon('check')} Erledigt
</button>
</div>`;
}).join('');
return addButtons + `<div class="health-list">${items}</div>`;
}
// ----------------------------------------------------------
// TIERARZTBESUCHE
// ----------------------------------------------------------
@ -883,14 +954,44 @@ window.Page_health = (() => {
// ----------------------------------------------------------
// EVENTS BINDEN
// ----------------------------------------------------------
// Sucht einen Eintrag in der/den Liste(n) des aktiven Tabs.
// Im Pflege-Tab sind die Einträge auf mehrere Typ-Listen verteilt.
function _entriesForActiveTab() {
if (_activeTab === 'pflege') return PFLEGE_TYPEN.flatMap(t => _data[t] || []);
return _data[_activeTab] || [];
}
function _bindTabEvents(content) {
content.querySelectorAll('[data-action="add-entry"]').forEach(btn => {
btn.addEventListener('click', () => _showForm(null, _activeTab));
});
// Pflege: pro-Typ-Button "+ Routine" → Formular mit festem Typ
content.querySelectorAll('[data-action="add-routine"]').forEach(btn => {
btn.addEventListener('click', () => _showForm(null, btn.dataset.typ));
});
// Pflege: Routine als erledigt markieren → Backend schreibt naechstes fort
content.querySelectorAll('[data-action="routine-erledigt"]').forEach(btn => {
btn.addEventListener('click', async e => {
e.stopPropagation();
const id = parseInt(btn.dataset.id);
await UI.asyncButton(btn, async () => {
const saved = await API.health.complete(_appState.activeDog.id, id);
const list = _data[saved.typ];
if (list) {
const idx = list.findIndex(x => x.id === id);
if (idx !== -1) list[idx] = saved;
}
_renderTab();
_renderErinnerungen();
UI.toast.success('Als erledigt eingetragen.');
});
});
});
content.querySelectorAll('[data-action="open-entry"]').forEach(card => {
const id = parseInt(card.dataset.id);
const entry = (_data[_activeTab] || []).find(e => e.id === id);
if (entry) card.addEventListener('click', () => _openDetail(entry));
const entry = _entriesForActiveTab().find(e => e.id === id);
if (entry) card.addEventListener('click', () =>
_activeTab === 'pflege' ? _showForm(entry, entry.typ) : _openDetail(entry));
});
content.querySelectorAll('[data-action="open-note"]').forEach(btn => {
btn.addEventListener('click', e => {
@ -980,8 +1081,19 @@ window.Page_health = (() => {
// ----------------------------------------------------------
// DETAIL-ANSICHT
// ----------------------------------------------------------
// Tab-Info (Icon + Label) für einen Typ — kennt auch die Pflege-Routine-Typen,
// die keinen eigenen Tab haben (sie liegen im gebündelten 'pflege'-Tab).
function _typInfo(typ) {
const meta = PFLEGE_META[typ];
if (meta) return {
icon: `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${meta.icon}"></use></svg>`,
label: meta.label,
};
return _getTabs().find(t => t.key === typ) || BASE_TABS[0];
}
function _openDetail(entry) {
const tabInfo = _getTabs().find(t => t.key === entry.typ) || BASE_TABS[0];
const tabInfo = _typInfo(entry.typ);
const fields = _detailFields(entry);
// Media-Items zusammenstellen (neue + legacy)
@ -1151,7 +1263,7 @@ window.Page_health = (() => {
</div>
`;
const tabInfo = _getTabs().find(tab => tab.key === t) || BASE_TABS[0];
const tabInfo = _typInfo(t);
UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body, footer });
const form = document.getElementById('health-form');
@ -1294,6 +1406,9 @@ window.Page_health = (() => {
allergie: 'z.B. Hühnchen, Gras, Hausstaub',
dokument: 'z.B. Impfpass, Blutbild',
laeufigkeit: 'Läufigkeit',
parasit: 'z.B. Frontline, Seresto-Halsband',
krallen: 'z.B. Krallen kürzen',
fellpflege: 'z.B. Bürsten, Trimmen, Baden',
};
return ph[typ] || '';
}
@ -1363,6 +1478,17 @@ window.Page_health = (() => {
</div>
${_praxisSelectField(entry)}
`;
case 'parasit':
case 'krallen':
case 'fellpflege': return `
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Nächste Fälligkeit</label>
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
</div>
${_intervallField(entry)}
</div>
`;
case 'tierarzt': {
const aktivePraxen = _praxen.filter(p => p.aktiv);
const praxisField = aktivePraxen.length