Frontend Sprint 3+4: Dog-Switcher, Health-Seite, Multi-Dog Tagebuch

- app.js: vollständiger Dog-Switcher (Avatar im Header/Sidebar, Quickpicker
  bei 3+ Hunden, setActiveDog, localStorage-Persistenz), iOS Ghost-Click Fix,
  Loading-Guard, Logout State Reset
- index.html: Dog-Switcher HTML, Favicon-Links, Sidebar "+ Neu erstellen",
  Navigation Tab Karte → Gesundheit
- health.js (neu): vollständiges Health-Frontend mit Tabs (Impfung, Entwurmung,
  Tierarzt, Medikament, Gewicht-Kurve, Allergie, Dokument), Ampel-System,
  KI-Zusammenfassung
- dog-profile.js: "+ Weiteren Hund anlegen" Button + _openCreateModal(),
  Event-Delegation statt direkter Listener (kein Doppelaufruf)
- diary.js: Dog-Picker im Formular, Avatar-Reihe auf Karten, Dog-Chips
  im Detail-Modal, dog_ids im API-Payload
- poison.js: Erledigt-Dialog mit Grundauswahl (beseitigt/fehlerhaft/anderes)
- api.js: health-Endpoints (list, create, update, delete, upload, ki)
- ui.js: confirm() Fix (resolve vor close)
- layout.css: Dog-Switcher Styles, scrollbare Sidebar-Nav, User-Item fix
- components.css: Health-Styles, Diary Dog-Picker, Ampel-Punkte, Gewicht-SVG
- icons/: Favicon-Set (ico, 16px, 32px, 180px, 192px, 512px)
This commit is contained in:
rene 2026-04-13 19:30:03 +02:00
parent 6f48ec581d
commit d8b9561fff
16 changed files with 1597 additions and 91 deletions

View file

@ -38,6 +38,11 @@ window.Page_diary = (() => {
// ----------------------------------------------------------
async function refresh() {
if (!_appState.activeDog) return;
// Wenn vorher "kein Hund"-Zustand: #diary-list existiert nicht → voll neu rendern
if (!_container.querySelector('#diary-list')) {
await _render();
return;
}
_offset = 0;
_entries = [];
await _load();
@ -185,6 +190,9 @@ window.Page_diary = (() => {
? `<p class="diary-card-text">${_escape(e.text.slice(0, 140))}${e.text.length > 140 ? '…' : ''}</p>`
: '';
// Mehrere Hunde: kleine Avatare in der Karte
const dogAvatars = _dogAvatarRow(e.dog_ids || []);
return `
<div class="diary-card${isMile ? ' diary-card--milestone' : ''}" data-entry-id="${e.id}">
${photo}
@ -196,11 +204,24 @@ window.Page_diary = (() => {
${e.titel ? `<div class="diary-card-title">${_escape(e.titel)}</div>` : ''}
${textPreview}
${tagsHtml}
${dogAvatars}
</div>
</div>
`;
}
function _dogAvatarRow(dogIds) {
if (!dogIds || dogIds.length <= 1) return '';
const avatars = dogIds.map(did => {
const dog = _appState.dogs.find(d => d.id === did);
if (!dog) return '';
return `<div class="diary-dog-av" title="${_escape(dog.name)}">
${dog.foto_url ? `<img src="${_escape(dog.foto_url)}" alt="">` : '<span>🐕</span>'}
</div>`;
}).join('');
return `<div class="diary-dog-row">${avatars}</div>`;
}
// ----------------------------------------------------------
// DETAIL-ANSICHT
// ----------------------------------------------------------
@ -217,6 +238,22 @@ window.Page_diary = (() => {
style="width:100%;border-radius:var(--radius-md);margin-bottom:var(--space-4)">`
: '';
// Hunde-Anzeige wenn mehrere beteiligt
const dogIds = entry.dog_ids || [entry.dog_id];
const dogsHtml = dogIds.length > 1
? `<div class="diary-detail-dogs">
${dogIds.map(did => {
const dog = _appState.dogs.find(d => d.id === did);
return dog ? `<div class="diary-dog-chip">
<div class="diary-dog-av">
${dog.foto_url ? `<img src="${_escape(dog.foto_url)}" alt="">` : '<span>🐕</span>'}
</div>
<span>${_escape(dog.name)}</span>
</div>` : '';
}).join('')}
</div>`
: '';
const body = `
${isMile ? '<div class="diary-detail-milestone-badge">🏆 Meilenstein</div>' : ''}
${photo}
@ -226,6 +263,7 @@ window.Page_diary = (() => {
${entry.datum ? UI.time.format(entry.datum + 'T00:00:00') : ''}
</span>
</div>
${dogsHtml}
${entry.text
? `<p style="white-space:pre-wrap;line-height:1.6;color:var(--c-text)">${_escape(entry.text)}</p>`
: ''}
@ -263,13 +301,33 @@ window.Page_diary = (() => {
// FORMULAR — Neu erstellen / Bearbeiten
// ----------------------------------------------------------
function _showForm(entry) {
const isEdit = !!entry;
const today = new Date().toISOString().slice(0, 10);
const typOpts = Object.entries(TYPEN)
const isEdit = !!entry;
const today = new Date().toISOString().slice(0, 10);
const activeDog = _appState.activeDog;
const typOpts = Object.entries(TYPEN)
.map(([val, { icon, label }]) =>
`<option value="${val}" ${entry?.typ === val ? 'selected' : ''}>${icon} ${label}</option>`)
.join('');
// Weitere Hunde: alle außer dem aktiven
const otherDogs = _appState.dogs.filter(d => d.id !== activeDog?.id);
const entryDogIds = entry?.dog_ids || [activeDog?.id];
const dogPickerHtml = otherDogs.length > 0 ? `
<div class="form-group">
<label class="form-label">Betrifft auch</label>
<div class="diary-dog-picker">
${otherDogs.map(d => `
<label class="diary-dog-pick-item ${entryDogIds.includes(d.id) ? 'checked' : ''}">
<input type="checkbox" name="extra_dog" value="${d.id}"
${entryDogIds.includes(d.id) ? 'checked' : ''}>
<div class="diary-dog-av">
${d.foto_url ? `<img src="${_escape(d.foto_url)}" alt="">` : '<span>🐕</span>'}
</div>
<span>${_escape(d.name)}</span>
</label>`).join('')}
</div>
</div>` : '';
const body = `
<form id="diary-form" autocomplete="off">
<div class="form-group">
@ -291,6 +349,7 @@ window.Page_diary = (() => {
<textarea class="form-control" name="text" rows="5"
placeholder="Was ist passiert? Besonderheiten, Gedanken…">${_escape(entry?.text || '')}</textarea>
</div>
${dogPickerHtml}
<div class="form-group">
<label class="form-label" style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
<input type="checkbox" name="is_milestone" ${entry?.is_milestone ? 'checked' : ''}>
@ -318,6 +377,9 @@ window.Page_diary = (() => {
const form = document.getElementById('diary-form');
// Fokus auf Titel-Feld → öffnet Keyboard auf Mobile, zeigt dem User was zu tun ist
setTimeout(() => form?.querySelector('[name="titel"]')?.focus(), 150);
// Foto-Vorschau
const photoInput = form.querySelector('[name="photo"]');
const photoPreview = document.getElementById('diary-photo-preview');
@ -330,11 +392,25 @@ window.Page_diary = (() => {
document.getElementById('diary-form-cancel')?.addEventListener('click', UI.modal.close);
// Checked-Klasse auf Dog-Picker-Items toggeln
form.querySelectorAll('.diary-dog-pick-item input').forEach(cb => {
cb.addEventListener('change', () => {
cb.closest('.diary-dog-pick-item').classList.toggle('checked', cb.checked);
});
});
form.addEventListener('submit', async e => {
e.preventDefault();
const submitBtn = form.querySelector('[type="submit"]');
const fd = UI.formData(form);
// dog_ids zusammenbauen: aktiver Hund + gewählte weitere
const dogIds = [_appState.activeDog.id];
form.querySelectorAll('.diary-dog-pick-item input:checked').forEach(cb => {
const id = parseInt(cb.value);
if (!dogIds.includes(id)) dogIds.push(id);
});
await UI.asyncButton(submitBtn, async () => {
const payload = {
datum: fd.datum || null,
@ -342,6 +418,7 @@ window.Page_diary = (() => {
titel: fd.titel || null,
text: fd.text || null,
is_milestone: 'is_milestone' in fd,
dog_ids: dogIds,
};
if (isEdit) {