Fix: Ernährung Hund-spezifisch, Erinnerungen in Settings, Übung des Tages per Hund (SW by-v872)

- ernaehrung.js: onDogChange setzt activeTab zurück, Hund klar sichtbar
- settings.js: Erinnerungen-Sektion lädt verstorbene Hunde + öffnet Gedenkseite
- dogs.py: GET /dogs/verstorben Endpoint (korrekte Route-Reihenfolge vor /{dog_id})
- dogs.py: Übung des Tages filtert jetzt nach dog_id statt user_id (sitzt-Übungen korrekt ausgeschlossen)
- Routen zeigen verstorbene Hunde korrekt als Teilnehmer (route_dogs ohne verstorben-Filter)
This commit is contained in:
rene 2026-05-11 19:25:00 +02:00
parent 265d3d4fe2
commit 1ce802c8dc
8 changed files with 1106 additions and 28 deletions

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '856'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '872'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
@ -905,6 +905,12 @@ const App = (() => {
if (!dog || dog.id === state.activeDog?.id) return;
state.activeDog = dog;
localStorage.setItem('by_active_dog', String(dogId));
// SW-Cache für hund-spezifische Daten invalidieren
navigator.serviceWorker?.controller?.postMessage({
type: 'INVALIDATE_CACHE',
paths: ['/api/training/progress', '/api/training/plan-progress',
'/api/training/suggestions', `/api/dogs/${dogId}/welcome-dashboard`],
});
_renderDogSwitcher();
_notifyDogChange();
}

View file

@ -15,6 +15,7 @@ window.Page_ernaehrung = (() => {
{ key: 'guide', label: 'Futter-Guide', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>' },
{ key: 'gift', label: 'Giftliste', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>' },
{ key: 'ki', label: 'KI-Berater', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>' },
{ key: 'vertraeglichkeit', label: 'Verträglichkeit', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#heartbeat"></use></svg>' },
];
// ------------------------------------------------------------------
@ -47,6 +48,7 @@ window.Page_ernaehrung = (() => {
async function onDogChange() {
_profil = {};
_activeTab = 'rechner'; // Tab zurücksetzen damit neuer Hund frisch startet
await _render();
}
@ -73,10 +75,12 @@ window.Page_ernaehrung = (() => {
}
_container.innerHTML = `
<div style="padding:var(--space-3) var(--space-4) 0">${UI.dogChip(_appState)}</div>
<div class="by-tabs" id="ern-tabs"></div>
<div id="ern-tab-content"></div>
`;
UI.bindDogChip(_container, _appState);
_renderTabBar();
_renderTab();
}
@ -106,10 +110,11 @@ window.Page_ernaehrung = (() => {
const el = _container.querySelector('#ern-tab-content');
if (!el) return;
switch (_activeTab) {
case 'rechner': _renderRechner(el); break;
case 'guide': _renderGuide(el); break;
case 'gift': _renderGift(el); break;
case 'ki': _renderKi(el); break;
case 'rechner': _renderRechner(el); break;
case 'guide': _renderGuide(el); break;
case 'gift': _renderGift(el); break;
case 'ki': _renderKi(el); break;
case 'vertraeglichkeit': _renderVertraeglichkeit(el); break;
}
}
@ -630,6 +635,531 @@ window.Page_ernaehrung = (() => {
});
}
// ------------------------------------------------------------------
// TAB 5: VERTRÄGLICHKEIT
// ------------------------------------------------------------------
async function _renderVertraeglichkeit(el) {
const dog = _appState?.activeDog;
if (!dog) { el.innerHTML = ''; return; }
el.innerHTML = `
<div style="padding:var(--space-4) 0">
<!-- Schnell-Erfassung -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-bottom:var(--space-4)">
<button class="btn btn-primary" id="vert-btn-futter">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bowl-food"></use></svg>
Futter erfassen
</button>
<button class="btn btn-secondary" id="vert-btn-reaktion">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#heartbeat"></use></svg>
Reaktion erfassen
</button>
</div>
<!-- Info-Banner Haut/Fell -->
<div style="display:flex;align-items:flex-start;gap:var(--space-2);
background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-md);padding:var(--space-3);
margin-bottom:var(--space-4);font-size:var(--text-xs);
color:var(--c-text-secondary);line-height:1.5">
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-primary);margin-top:1px">
<use href="/icons/phosphor.svg#info"></use>
</svg>
<span>Haut- &amp; Fellsymptome zeigen sich erst nach Wochen trage regelmäßig ein um Muster zu erkennen.</span>
</div>
<!-- Analyse -->
<div id="vert-analyse" style="margin-bottom:var(--space-5)">
<div style="text-align:center;color:var(--c-text-muted);padding:var(--space-4)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#circle-notch"></use></svg>
Lade Analyse
</div>
</div>
<!-- Verlauf -->
<div id="vert-verlauf"></div>
</div>
`;
el.querySelector('#vert-btn-futter').addEventListener('click', () => _openFutterModal(el, dog));
el.querySelector('#vert-btn-reaktion').addEventListener('click', () => _openReaktionModal(el, dog));
await _loadAnalyse(el, dog);
await _loadVerlauf(el, dog);
}
function _todayStr() {
return new Date().toISOString().slice(0, 10);
}
function _nowTimeStr() {
const d = new Date();
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
function _openFutterModal(el, dog) {
const id = `fm-${Date.now()}`;
const body = `
<form id="${id}">
<div class="by-form-group">
<label class="by-label">Datum</label>
<input type="date" name="datum" class="form-control by-input" value="${_todayStr()}" required>
</div>
<div class="by-form-group">
<label class="by-label">Uhrzeit</label>
<input type="time" name="uhrzeit" class="form-control by-input" value="${_nowTimeStr()}" required>
</div>
<div class="by-form-group">
<label class="by-label">Futter-Name</label>
<input type="text" name="futter_name" class="form-control by-input"
list="vert-futter-datalist" placeholder="z. B. Wolfsblut Adult" required>
<datalist id="vert-futter-datalist"></datalist>
</div>
<div class="by-form-group">
<label class="by-label">Futter-Typ</label>
<select name="futter_typ" class="form-control by-select">
<option value="trockenfutter">Trockenfutter</option>
<option value="nassfutter">Nassfutter</option>
<option value="barf">BARF</option>
<option value="snack">Snack</option>
<option value="sonstiges">Sonstiges</option>
</select>
</div>
<div class="by-form-group">
<label class="by-label">Menge (g, optional)</label>
<input type="number" name="menge_g" class="form-control by-input" min="1" placeholder="">
</div>
<div class="by-form-group">
<label class="by-label">Notiz (optional)</label>
<textarea name="notiz" class="form-control by-input" rows="2" placeholder=""></textarea>
</div>
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" id="vert-futter-save-btn" form="${id}">Speichern</button>
`;
UI.modal.open({ title: 'Futter erfassen', body, footer });
// Datalist mit bekannten Futter-Namen füllen
API.dogs.futterList(dog.id).then(list => {
const dl = document.getElementById('vert-futter-datalist');
if (!dl) return;
const names = [...new Set((list || []).map(e => e.futter_name))];
dl.innerHTML = names.map(n => `<option value="${_esc(n)}">`).join('');
}).catch(() => {});
setTimeout(() => {
const saveBtn = document.getElementById('vert-futter-save-btn');
if (!saveBtn) return;
saveBtn.addEventListener('click', async (ev) => {
ev.preventDefault();
const form = document.getElementById(id);
if (!form) return;
const fd = new FormData(form);
const data = {
datum: fd.get('datum'),
uhrzeit: fd.get('uhrzeit'),
futter_name: (fd.get('futter_name') || '').trim(),
futter_typ: fd.get('futter_typ') || 'trockenfutter',
menge_g: fd.get('menge_g') ? parseInt(fd.get('menge_g')) : null,
notiz: (fd.get('notiz') || '').trim() || null,
};
if (!data.futter_name) { UI.toast.warning('Bitte Futter-Name angeben.'); return; }
await UI.asyncButton(saveBtn, async () => {
try {
await API.dogs.futterCreate(dog.id, data);
UI.modal.close();
UI.toast.success('Futter gespeichert.');
const tabEl = _container.querySelector('#ern-tab-content');
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Speichern.');
}
});
});
}, 50);
}
function _openReaktionModal(el, dog) {
const id = `rm-${Date.now()}`;
const body = `
<form id="${id}">
<div class="by-form-group">
<label class="by-label">Datum</label>
<input type="date" name="datum" class="form-control by-input" value="${_todayStr()}" required>
</div>
<div class="by-form-group">
<label class="by-label">Uhrzeit</label>
<input type="time" name="uhrzeit" class="form-control by-input" value="${_nowTimeStr()}" required>
</div>
<div class="by-form-group">
<label class="by-label">Reaktion</label>
<select name="reaktion_typ" class="form-control by-select" required>
<optgroup label="✓ Positiv">
<option value="fell_glaenzend">Glänzendes Fell</option>
<option value="verdauung_gut">Gute Verdauung</option>
<option value="energie_hoch">Viel Energie</option>
</optgroup>
<optgroup label="Magen &amp; Darm">
<option value="erbrechen">Erbrechen</option>
<option value="durchfall">Durchfall</option>
<option value="blaehungen">Blähungen</option>
<option value="weicher_stuhl">Weicher Stuhl</option>
<option value="appetitlosigkeit">Appetitlosigkeit</option>
</optgroup>
<optgroup label="Haut &amp; Fell">
<option value="juckreiz">Juckreiz / Kratzen</option>
<option value="haarausfall">Haarausfall</option>
<option value="stumpfes_fell">Stumpfes Fell</option>
<option value="schuppenbildung">Schuppenbildung</option>
<option value="roetungen">Hautrötungen / Entzündung</option>
<option value="pfotenlecken">Pfoten lecken (chronisch)</option>
<option value="ohrentzuendung">Ohrentzündung</option>
<option value="fettiges_fell">Fettiges Fell / Seborrhö</option>
</optgroup>
<optgroup label="Allgemeinbefinden">
<option value="schlappheit">Schlappheit / Apathie</option>
<option value="nervositaet">Nervosität / Unruhe</option>
<option value="viel_trinken">Ungewöhnlich viel trinken</option>
<option value="sonstiges">Sonstiges</option>
</optgroup>
</select>
</div>
<div class="by-form-group">
<label class="by-label">Intensität (15)</label>
<div class="vert-stern-gruppe" style="display:flex;gap:6px;flex-wrap:wrap">
${[1,2,3,4,5].map(n => `
<button type="button" class="vert-stern${n <= 3 ? ' active' : ''}"
data-val="${n}"
style="width:40px;height:40px;border-radius:8px;
border:1.5px solid var(--c-border);
background:${n <= 3 ? 'var(--c-primary)' : 'var(--c-bg-card)'};
color:${n <= 3 ? '#fff' : 'var(--c-text-secondary)'};
font-weight:700;cursor:pointer;">${n}</button>
`).join('')}
</div>
<input type="hidden" name="intensitaet" value="3">
</div>
<div class="by-form-group">
<label class="by-label">Notiz (optional)</label>
<textarea name="notiz" class="form-control by-input" rows="2" placeholder=""></textarea>
</div>
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" id="vert-reaktion-save-btn" form="${id}">Speichern</button>
`;
UI.modal.open({ title: 'Reaktion erfassen', body, footer });
setTimeout(() => {
// Stern-Buttons
document.querySelectorAll('.vert-stern').forEach(btn => {
btn.addEventListener('click', () => {
const val = parseInt(btn.dataset.val);
const form = document.getElementById(id);
if (form) form.querySelector('[name=intensitaet]').value = val;
document.querySelectorAll('.vert-stern').forEach(b => {
const v = parseInt(b.dataset.val);
b.style.background = v <= val ? 'var(--c-primary)' : 'var(--c-bg-card)';
b.style.color = v <= val ? '#fff' : 'var(--c-text-secondary)';
});
});
});
const saveBtn = document.getElementById('vert-reaktion-save-btn');
if (!saveBtn) return;
saveBtn.addEventListener('click', async (ev) => {
ev.preventDefault();
const form = document.getElementById(id);
if (!form) return;
const fd = new FormData(form);
const data = {
datum: fd.get('datum'),
uhrzeit: fd.get('uhrzeit'),
reaktion_typ: fd.get('reaktion_typ'),
intensitaet: parseInt(fd.get('intensitaet')) || 3,
notiz: (fd.get('notiz') || '').trim() || null,
};
await UI.asyncButton(saveBtn, async () => {
try {
await API.dogs.reaktionCreate(dog.id, data);
UI.modal.close();
UI.toast.success('Reaktion gespeichert.');
const tabEl = _container.querySelector('#ern-tab-content');
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Speichern.');
}
});
});
}, 50);
}
async function _loadAnalyse(el, dog) {
const analyseEl = el.querySelector('#vert-analyse');
if (!analyseEl) return;
let data;
try {
data = await API.dogs.futterAnalyse(dog.id);
} catch (_) {
analyseEl.innerHTML = `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Analyse nicht verfügbar.</p>`;
return;
}
if (!data.futter || data.futter.length === 0) {
analyseEl.innerHTML = `
<div style="text-align:center;padding:var(--space-4);color:var(--c-text-muted);font-size:var(--text-sm)">
<svg class="ph-icon" style="width:2rem;height:2rem;margin-bottom:8px;display:block;margin-inline:auto" aria-hidden="true">
<use href="/icons/phosphor.svg#chart-bar"></use>
</svg>
Noch keine Einträge. Erfasse Futter und Reaktionen um die Analyse zu sehen.
</div>
`;
return;
}
const STATUS_CFG = {
gut: { label: 'Gut verträglich', color: 'var(--c-success,#22c55e)', bg: 'rgba(34,197,94,0.12)' },
neutral: { label: 'Neutral', color: 'var(--c-warning,#f59e0b)', bg: 'rgba(245,158,11,0.12)' },
problematisch:{ label: 'Problematisch', color: 'var(--c-danger,#ef4444)', bg: 'rgba(239,68,68,0.12)' },
neu: { label: 'Zu wenig Daten', color: 'var(--c-text-muted)', bg: 'var(--c-surface)' },
};
const TYP_LABELS = {
trockenfutter: 'Trockenfutter', nassfutter: 'Nassfutter',
barf: 'BARF', snack: 'Snack', sonstiges: 'Sonstiges',
};
const KAT_LABELS = {
gastro_negativ: 'Magen & Darm',
haut_negativ: 'Haut & Fell',
allgemein_negativ: 'Allgemein',
positiv: 'Positiv',
sonstiges: 'Sonstiges',
};
const hinweisHtml = data.hinweis ? `
<div style="display:flex;align-items:flex-start;gap:var(--space-2);
background:rgba(245,158,11,0.12);border:1px solid var(--c-warning,#f59e0b);
border-radius:var(--radius-md);padding:var(--space-3);
margin-bottom:var(--space-3);font-size:var(--text-xs);
color:var(--c-text);line-height:1.5">
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-warning,#f59e0b);margin-top:1px">
<use href="/icons/phosphor.svg#warning-circle"></use>
</svg>
<span>${_esc(data.hinweis)}</span>
</div>
` : '';
analyseEl.innerHTML = `
<h4 style="font-size:var(--text-sm);font-weight:700;margin-bottom:var(--space-3);color:var(--c-text)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#chart-bar"></use></svg>
Verträglichkeits-Analyse
<span style="font-weight:400;color:var(--c-text-muted);font-size:var(--text-xs)">
(${data.eintraege_count} Mahlzeiten, ${data.reaktionen_count} Reaktionen)
</span>
</h4>
${hinweisHtml}
<div style="display:grid;gap:var(--space-2)">
${data.futter.map(f => {
const cfg = STATUS_CFG[f.status] || STATUS_CFG.neu;
// Symptom-Kategorien des Futters als Chips
const katChips = Object.entries(f.kategorien || {})
.filter(([kat]) => kat !== 'positiv')
.map(([kat, cnt]) => {
const isHaut = kat === 'haut_negativ';
const isGastro = kat === 'gastro_negativ';
const chipColor = isHaut ? 'var(--c-warning,#f59e0b)' :
isGastro ? 'var(--c-danger,#ef4444)' :
'var(--c-text-muted)';
return `<span style="font-size:10px;font-weight:600;padding:2px 6px;
border-radius:999px;border:1px solid ${chipColor};
color:${chipColor};white-space:nowrap">
${_esc(KAT_LABELS[kat] || kat)} ×${cnt}
</span>`;
}).join('');
return `
<div style="background:${cfg.bg};border:1px solid ${cfg.color};
border-radius:var(--radius-md);padding:var(--space-3);
display:flex;align-items:center;justify-content:space-between;gap:var(--space-2)">
<div style="min-width:0;flex:1">
<div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(f.name)}
</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
${_esc(TYP_LABELS[f.typ] || f.typ)} &middot; ${f.mahlzeiten} Mahlzeit${f.mahlzeiten !== 1 ? 'en' : ''}
${f.status !== 'neu' ? `&middot; <span style="color:var(--c-success,#22c55e)">+${f.positiv}</span> / <span style="color:var(--c-danger,#ef4444)">-${f.negativ}</span>` : ''}
</div>
${katChips ? `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">${katChips}</div>` : ''}
</div>
<span style="flex-shrink:0;font-size:var(--text-xs);font-weight:700;
color:${cfg.color};white-space:nowrap">
${_esc(cfg.label)}
</span>
</div>
`;
}).join('')}
</div>
`;
}
async function _loadVerlauf(el, dog) {
const verlaufEl = el.querySelector('#vert-verlauf');
if (!verlaufEl) return;
let eintraege = [], reaktionen = [];
try {
[eintraege, reaktionen] = await Promise.all([
API.dogs.futterList(dog.id),
API.dogs.reaktionList(dog.id),
]);
} catch (_) { return; }
// Letzten 10 Futter + 5 Reaktionen, gemischt chronologisch
const items = [
...(eintraege || []).slice(0, 10).map(e => ({ ...e, _art: 'futter' })),
...(reaktionen || []).slice(0, 5).map(r => ({ ...r, _art: 'reaktion' })),
].sort((a, b) => {
const ta = `${a.datum}T${a.uhrzeit}`;
const tb = `${b.datum}T${b.uhrzeit}`;
return tb.localeCompare(ta);
});
if (items.length === 0) {
verlaufEl.innerHTML = '';
return;
}
const REAK_LABELS = {
// Positiv
verdauung_gut: 'Gute Verdauung',
energie_hoch: 'Viel Energie',
fell_glaenzend: 'Glänzendes Fell',
// Gastro
erbrechen: 'Erbrechen',
durchfall: 'Durchfall',
blaehungen: 'Blähungen',
weicher_stuhl: 'Weicher Stuhl',
appetitlosigkeit: 'Appetitlosigkeit',
// Haut & Fell
juckreiz: 'Juckreiz / Kratzen',
haarausfall: 'Haarausfall',
stumpfes_fell: 'Stumpfes Fell',
schuppenbildung: 'Schuppenbildung',
roetungen: 'Hautrötungen / Entzündung',
pfotenlecken: 'Pfoten lecken (chronisch)',
ohrentzuendung: 'Ohrentzündung',
fettiges_fell: 'Fettiges Fell / Seborrhö',
// Allgemein
schlappheit: 'Schlappheit / Apathie',
nervositaet: 'Nervosität / Unruhe',
viel_trinken: 'Ungewöhnlich viel trinken',
sonstiges: 'Sonstiges',
};
const NEGATIV_TYPEN = new Set([
'erbrechen','durchfall','blaehungen','weicher_stuhl','appetitlosigkeit',
'juckreiz','haarausfall','stumpfes_fell','schuppenbildung','roetungen',
'pfotenlecken','ohrentzuendung','fettiges_fell',
'schlappheit','nervositaet','viel_trinken',
]);
const POSITIV_TYPEN = new Set(['verdauung_gut','energie_hoch','fell_glaenzend']);
verlaufEl.innerHTML = `
<h4 style="font-size:var(--text-sm);font-weight:700;margin-bottom:var(--space-2);color:var(--c-text)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clock-counter-clockwise"></use></svg>
Verlauf
</h4>
<div style="display:grid;gap:var(--space-2)">
${items.map(item => {
if (item._art === 'futter') {
return `
<div data-futter-id="${item.id}"
style="background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-primary)">
<use href="/icons/phosphor.svg#bowl-food"></use>
</svg>
<div style="flex:1;min-width:0">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(item.futter_name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
${_esc(item.datum)} ${_esc(item.uhrzeit)}
${item.menge_g ? ` &middot; ${item.menge_g} g` : ''}
</div>
</div>
<button class="btn-icon vert-del-futter" data-id="${item.id}"
style="flex-shrink:0;background:none;border:none;cursor:pointer;padding:4px;
color:var(--c-text-muted)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>
</div>
`;
} else {
const isNeg = NEGATIV_TYPEN.has(item.reaktion_typ);
const isPos = POSITIV_TYPEN.has(item.reaktion_typ);
const col = isNeg ? 'var(--c-danger,#ef4444)' : isPos ? 'var(--c-success,#22c55e)' : 'var(--c-text-muted)';
return `
<div data-reaktion-id="${item.id}"
style="background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:${col}">
<use href="/icons/phosphor.svg#heartbeat"></use>
</svg>
<div style="flex:1;min-width:0">
<div style="font-weight:600;font-size:var(--text-sm);color:${col}">
${_esc(REAK_LABELS[item.reaktion_typ] || item.reaktion_typ)}
<span style="font-weight:400;color:var(--c-text-muted)">(${item.intensitaet}/5)</span>
</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
${_esc(item.datum)} ${_esc(item.uhrzeit)}
</div>
</div>
<button class="btn-icon vert-del-reaktion" data-id="${item.id}"
style="flex-shrink:0;background:none;border:none;cursor:pointer;padding:4px;
color:var(--c-text-muted)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>
</div>
`;
}
}).join('')}
</div>
`;
// Löschen-Buttons
verlaufEl.querySelectorAll('.vert-del-futter').forEach(btn => {
btn.addEventListener('click', async () => {
if (!window.confirm('Eintrag löschen?')) return;
try {
await API.dogs.futterDelete(dog.id, parseInt(btn.dataset.id));
UI.toast.success('Eintrag gelöscht.');
const tabEl = _container.querySelector('#ern-tab-content');
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
} catch (err) {
UI.toast.error(err.message || 'Fehler.');
}
});
});
verlaufEl.querySelectorAll('.vert-del-reaktion').forEach(btn => {
btn.addEventListener('click', async () => {
if (!window.confirm('Reaktion löschen?')) return;
try {
await API.dogs.reaktionDelete(dog.id, parseInt(btn.dataset.id));
UI.toast.success('Reaktion gelöscht.');
const tabEl = _container.querySelector('#ern-tab-content');
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
} catch (err) {
UI.toast.error(err.message || 'Fehler.');
}
});
});
}
// ------------------------------------------------------------------
// PUBLIC API
// ------------------------------------------------------------------

View file

@ -245,6 +245,7 @@ window.Page_settings = (() => {
<span>Hunde-Profile</span>
<span style="margin-left:auto;color:var(--c-text-secondary)"></span>
</div>
<div id="settings-erinnerungen-wrap"></div>
<div class="sidebar-item" id="settings-push-btn"
style="padding:var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bell"></use></svg>
@ -444,6 +445,38 @@ window.Page_settings = (() => {
</div>
`;
// Verstorbene Hunde in Erinnerungen-Sektion laden
API.get('/dogs/verstorben').then(dogs => {
const el = document.getElementById('settings-erinnerungen-wrap');
if (!el || !dogs.length) return;
el.innerHTML = dogs.map(d => {
const av = d.foto_url
? `<img src="${_esc(d.foto_url)}" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0">`
: `<div style="width:36px;height:36px;border-radius:50%;background:var(--c-surface-2);display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg class="ph-icon" style="width:18px;height:18px;color:var(--c-text-muted)" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
</div>`;
const jahr = d.verstorben_am ? d.verstorben_am.slice(0, 4) : '';
return `
<div class="sidebar-item settings-erinnerung-btn" data-dog-id="${d.id}" data-dog-name="${_esc(d.name)}"
style="padding:var(--space-3) var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border);cursor:pointer">
${av}
<div style="display:flex;flex-direction:column;gap:1px;flex:1;min-width:0">
<span style="font-weight:600;font-size:var(--text-sm)">${_esc(d.name)}</span>
<span style="font-size:var(--text-xs);color:var(--c-text-muted)">
<svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true"><use href="/icons/phosphor.svg#heart-break"></use></svg>
Erinnerungen${jahr ? ' · ' + jahr : ''}
</span>
</div>
<span style="margin-left:auto;color:var(--c-text-secondary)"></span>
</div>`;
}).join('');
el.querySelectorAll('.settings-erinnerung-btn').forEach(btn => {
btn.addEventListener('click', () => _openGedenkseite(
parseInt(btn.dataset.dogId), btn.dataset.dogName
));
});
}).catch(() => {});
// Achievements laden (Streak + Stats + Badges)
API.get('/achievements/me').then(a => {
const statsEl = document.getElementById('settings-stats-body');
@ -1456,6 +1489,80 @@ window.Page_settings = (() => {
} catch { el.innerHTML = ''; }
}
// ----------------------------------------------------------
// GEDENKSEITE — für verstorbene Hunde
// ----------------------------------------------------------
async function _openGedenkseite(dogId, dogName) {
UI.modal.open({ title: `Erinnerungen an ${_esc(dogName)}`, body: `
<div style="text-align:center;padding:var(--space-4)">
<svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary);animation:spin 1s linear infinite" aria-hidden="true">
<use href="/icons/phosphor.svg#spinner"></use>
</svg>
</div>` });
let data;
try { data = await API.get(`/dogs/${dogId}/gedenkseite`); }
catch { UI.modal.close(); return; }
const d = data;
const av = d.dog.foto_url
? `<img src="${_esc(d.dog.foto_url)}" style="width:100px;height:100px;border-radius:50%;object-fit:cover;border:3px solid var(--c-primary)">`
: `<div style="width:100px;height:100px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;border:3px solid var(--c-primary)"><svg class="ph-icon" style="width:48px;height:48px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg></div>`;
const photoGrid = d.photos?.length ? `
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin:var(--space-4) 0">
${d.photos.map(url => `<img src="${_esc(url)}" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px">`).join('')}
</div>` : '';
const statsHtml = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin:var(--space-4) 0">
${d.km_total ? `<div class="card" style="padding:var(--space-3);text-align:center">
<div style="font-size:var(--text-xl);font-weight:800;color:var(--c-primary)">${d.km_total}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">km zusammen</div>
</div>` : ''}
${d.diary_count ? `<div class="card" style="padding:var(--space-3);text-align:center">
<div style="font-size:var(--text-xl);font-weight:800;color:var(--c-primary)">${d.diary_count}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">Tagebucheinträge</div>
</div>` : ''}
${d.gemeinsam_tage ? `<div class="card" style="padding:var(--space-3);text-align:center">
<div style="font-size:var(--text-xl);font-weight:800;color:var(--c-primary)">${d.gemeinsam_tage}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">gemeinsame Tage</div>
</div>` : ''}
</div>`;
const passed = d.dog.verstorben_am;
const passedStr = passed
? new Date(passed).toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })
: '';
UI.modal.open({
title: `Erinnerungen an ${_esc(d.dog.name)}`,
body: `
<div style="text-align:center;margin-bottom:var(--space-4)">
${av}
<div style="margin-top:var(--space-3);font-size:var(--text-lg);font-weight:700">${_esc(d.dog.name)}</div>
${passedStr ? `<div style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:4px">
<svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#heart-break"></use></svg>
${passedStr}
</div>` : ''}
</div>
${statsHtml}
${photoGrid}
<div style="background:var(--c-primary-subtle);border-left:3px solid var(--c-primary);
border-radius:0 var(--radius-md) var(--radius-md) 0;padding:var(--space-4);margin:var(--space-4) 0">
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0;line-height:1.6">
Der Schmerz über den Verlust eines Hundes ist real und tief. Lass dich trauern die Erinnerungen bleiben immer bei dir.
</p>
</div>
${d.ki_abschied ? `<div style="font-style:italic;font-size:var(--text-sm);color:var(--c-text-secondary);
line-height:1.7;padding:var(--space-3);background:var(--c-surface);
border-radius:var(--radius-md);border:1px solid var(--c-border)">
"${_esc(d.ki_abschied)}"
</div>` : ''}
`,
});
}
// ----------------------------------------------------------
// NICHT EINGELOGGT — Login / Registrierung
// ----------------------------------------------------------