Feature: Trauer-Feature, Futter-Verträglichkeit, Multi-Hund-Fixes, Wetter-Ort (Sprint 47)

- dog-profile.js: Verstorben-Button, Gedenkseite, KI-Abschiedstext
- database.py: futter_eintraege/reaktionen, route_dogs, exercise_progress.dog_id
- routes/ernaehrung.py: Futter-Verträglichkeit mit 20 Reaktionstypen + Analyse
- routes/routen.py: route_dogs Many-to-Many, Routen editierbar
- routes/training.py: exercise_progress per dog_id
- routes/ki.py: /ki/abschied Trauer-KI
- weather.py: Nominatim Ortsname parallel geladen
- ui.js: dogChip/bindDogChip, visualViewport-Modal
- api.js: gedenken, gedenkseite, futter-Methoden, route_dogs
- worlds.js: Ortsname im Wetter-Chip
- uebungen.js: _progressLoaded-Flag, dog-spezifischer Fortschritt
- trainingsplaene.js: dog_id Unterstützung
- diary.js/health.js: P-Badge Cleanup
- map.js: Wetter-Ort-Anzeige entfernt
- wetter.js: Ort in Wetter-Detail
This commit is contained in:
rene 2026-05-11 19:28:38 +02:00
parent 1ce802c8dc
commit bda61a0e40
16 changed files with 713 additions and 181 deletions

View file

@ -1060,6 +1060,12 @@ window.Page_dog_profile = (() => {
<button type="submit" form="dp-form" class="btn btn-primary" style="width:100%">Speichern</button>
<div style="display:flex;gap:var(--space-2)">
<button type="button" class="btn btn-danger" id="dp-delete-btn">Löschen</button>
<button type="button" id="dp-gedenken-btn"
style="flex:1;padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
border:none;background:#1a1a1a;color:#C4843A;
font-size:var(--text-sm);font-weight:600;cursor:pointer">
Verstorben
</button>
<button type="button" class="btn btn-secondary flex-1" id="dp-form-cancel">Abbrechen</button>
</div>
</div>
@ -1279,6 +1285,11 @@ window.Page_dog_profile = (() => {
document.getElementById('dp-form-cancel')
?.addEventListener('click', UI.modal.close);
document.getElementById('dp-gedenken-btn')?.addEventListener('click', async () => {
UI.modal.close();
_openGedenkenFlow(dog);
});
document.getElementById('dp-delete-btn')?.addEventListener('click', async () => {
const ok = await UI.modal.confirm({
title : `${dog.name} löschen?`,
@ -2414,6 +2425,178 @@ window.Page_dog_profile = (() => {
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
// ----------------------------------------------------------
// GEDENKEN-FLOW
// ----------------------------------------------------------
async function _openGedenkenFlow(dog) {
// Schritt 1: Würdevoller Übergangsdialog mit Datum-Eingabe
UI.modal.open({
title: `Abschied von ${dog.name}`,
body: `
<div style="text-align:center;padding:var(--space-2) 0 var(--space-4)">
<svg class="ph-icon" style="width:48px;height:48px;color:var(--c-primary);opacity:0.7" aria-hidden="true">
<use href="/icons/phosphor.svg#heart"></use>
</svg>
<p style="color:var(--c-text-secondary);margin:var(--space-3) 0 var(--space-4);line-height:1.6">
${dog.name} hinterlässt eine riesige Lücke.<br>
Die gemeinsamen Erinnerungen bleiben für immer.
</p>
</div>
<form id="gedenken-form">
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">
Datum des Abschieds
</label>
<input type="date" id="gedenken-datum" name="datum"
value="${new Date().toISOString().slice(0,10)}"
max="${new Date().toISOString().slice(0,10)}"
style="width:100%;padding:10px 12px;border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-bg-card);color:var(--c-text);font-size:var(--text-sm);box-sizing:border-box">
</form>`,
footer: `
<div class="w3-btn-stack">
<button type="submit" form="gedenken-form" id="gedenken-save-btn" class="btn btn-primary" style="width:100%">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#heart"></use></svg>
Gedenkseite erstellen
</button>
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
</div>`,
});
document.getElementById('gedenken-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = document.getElementById('gedenken-save-btn');
const datum = document.getElementById('gedenken-datum').value;
await UI.asyncButton(btn, async () => {
await API.post(`/dogs/${dog.id}/gedenken`, { verstorben_am: datum });
// Aus aktiver Hundeliste entfernen
_appState.dogs = _appState.dogs.filter(d => d.id !== dog.id);
_appState.activeDog = _appState.dogs[0] || null;
UI.modal.close();
// Gedenkseite öffnen
await _openGedenkseite(dog.id, dog.name);
await _render();
});
});
}
async function _openGedenkseite(dogId, dogName) {
UI.modal.open({ title: `Erinnerungen an ${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">
<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="${UI.escape(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)"><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="${UI.escape(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">
<svg class="ph-icon" style="width:20px;height:20px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg>
<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">
<svg class="ph-icon" style="width:20px;height:20px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>
<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.media_count ? `<div class="card" style="padding:var(--space-3);text-align:center">
<svg class="ph-icon" style="width:20px;height:20px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#images"></use></svg>
<div style="font-size:var(--text-xl);font-weight:800;color:var(--c-primary)">${d.media_count}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">Fotos</div>
</div>` : ''}
${d.gemeinsam_tage ? `<div class="card" style="padding:var(--space-3);text-align:center">
<svg class="ph-icon" style="width:20px;height:20px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-heart"></use></svg>
<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>`;
// Trauer-Support-Texte
const supportHtml = `
<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">
<div style="font-weight:700;margin-bottom:var(--space-2);display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" style="width:18px;height:18px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#heartbeat"></use></svg>
Für dich in dieser Zeit
</div>
<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. Du musst nicht stark sein.
Lass dich trauern so lange du brauchst. Die Erinnerungen bleiben immer bei dir.
</p>
</div>
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.8">
<div style="display:flex;align-items:flex-start;gap:var(--space-2);margin-bottom:var(--space-2)">
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:3px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#heart"></use></svg>
Sprich mit Freunden oder der Familie über ${d.dog.name} Geschichten lebendig halten hilft.
</div>
<div style="display:flex;align-items:flex-start;gap:var(--space-2);margin-bottom:var(--space-2)">
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:3px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>
Das Tagebuch bleibt erhalten es ist ein kostbares Stück gemeinsamer Geschichte.
</div>
<div style="display:flex;align-items:flex-start;gap:var(--space-2)">
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:3px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#chat-circle-dots"></use></svg>
Professionelle Hilfe bei Tiertrauer: <strong>Tiertrauer-Hotline 0800 111 0 111</strong> (kostenlos)
</div>
</div>
<div id="gedenk-ki-wrap" style="margin-top:var(--space-4)">
<button id="gedenk-ki-btn" class="btn btn-secondary" style="width:100%">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#sparkle"></use></svg>
Persönlichen Abschiedstext erstellen
</button>
</div>`;
const modal = UI.modal.open({
title: `🌈 Erinnerungen an ${UI.escape(d.dog.name)}`,
body: `
<div style="text-align:center;margin-bottom:var(--space-4)">
${av}
<div style="font-size:var(--text-xl);font-weight:800;margin-top:var(--space-3)">${UI.escape(d.dog.name)}</div>
${d.dog.rasse ? `<div style="color:var(--c-text-secondary);font-size:var(--text-sm)">${UI.escape(d.dog.rasse)}</div>` : ''}
${d.dog.verstorben_am ? `<div style="color:var(--c-text-muted);font-size:var(--text-xs);margin-top:4px">
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#rainbow"></use></svg>
Über die Regenbogenbrücke am ${new Date(d.dog.verstorben_am).toLocaleDateString('de-DE')}
</div>` : ''}
</div>
${photoGrid}
${statsHtml}
${supportHtml}`,
});
document.getElementById('gedenk-ki-btn')?.addEventListener('click', async () => {
const btn = document.getElementById('gedenk-ki-btn');
await UI.asyncButton(btn, async () => {
const result = await API.post('/ki/abschied', {
dog_id: dogId,
name: d.dog.name,
rasse: d.dog.rasse,
km_total: d.km_total,
diary_count: d.diary_count,
gemeinsam_tage: d.gemeinsam_tage,
});
const wrap = document.getElementById('gedenk-ki-wrap');
if (wrap) wrap.innerHTML = `
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-4);
font-size:var(--text-sm);line-height:1.7;color:var(--c-text);font-style:italic">
"${UI.escape(result.text)}"
</div>`;
});
});
}
return { init, refresh, onDogChange, addNew: _openCreateModal };
})();