banyaro/backend/static/js/pages/dog-profile.js
rene d8b9561fff 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)
2026-04-13 19:30:03 +02:00

472 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Hunde-Profil
Seiten-Modul: Profil anlegen / anzeigen / bearbeiten.
============================================================ */
window.Page_dog_profile = (() => {
let _container = null;
let _appState = null;
// ----------------------------------------------------------
// INIT / REFRESH / LIFECYCLE
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
// Event-Delegation auf dem persistenten Container — überlebt innerHTML-Ersatz
_container.addEventListener('click', e => {
if (e.target.closest('#dp-add-dog-btn')) {
_openCreateModal();
return;
}
if (e.target.closest('#dp-edit-btn')) {
if (_appState.activeDog) _openEditModal(_appState.activeDog);
return;
}
if (e.target.closest('#profile-goto-login')) {
App.navigate('settings');
}
});
await _render();
}
async function refresh() {
await _render();
}
async function onDogChange(dog) {
await _render();
}
// ----------------------------------------------------------
// HAUPTRENDER
// ----------------------------------------------------------
async function _render() {
if (!_appState.user) {
_container.innerHTML = UI.emptyState({
icon : '🐕',
title : 'Anmelden erforderlich',
text : 'Melde dich an, um ein Hundeprofil anzulegen.',
action: `<button class="btn btn-primary" id="profile-goto-login">Anmelden</button>`,
});
_container.querySelector('#profile-goto-login')
?.addEventListener('click', () => App.navigate('settings'));
return;
}
if (!_appState.activeDog) {
_renderCreateForm();
} else {
_renderProfile(_appState.activeDog);
}
}
// ----------------------------------------------------------
// PROFIL-ANSICHT
// ----------------------------------------------------------
function _renderProfile(dog) {
const geburtstag = dog.geburtstag
? new Date(dog.geburtstag + 'T00:00:00')
.toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })
: null;
_container.innerHTML = `
<div style="text-align:center;padding:var(--space-6) var(--space-2) var(--space-4)">
<!-- Profilfoto mit Upload-Button -->
<div style="position:relative;display:inline-block;margin-bottom:var(--space-4)">
${dog.foto_url
? `<img src="${dog.foto_url}" alt="${_esc(dog.name)}"
style="width:120px;height:120px;border-radius:50%;object-fit:cover;
border:3px solid var(--c-primary)">`
: `<div style="width:120px;height:120px;border-radius:50%;
background:var(--c-surface-2);display:flex;
align-items:center;justify-content:center;
font-size:3.5rem;border:3px solid var(--c-border)">🐕</div>`}
<label style="position:absolute;bottom:4px;right:4px;
background:var(--c-primary);color:#fff;border-radius:50%;
width:30px;height:30px;display:flex;align-items:center;
justify-content:center;cursor:pointer;font-size:14px"
title="Foto ändern">
📷
<input type="file" id="dp-photo-input" accept="image/*"
style="display:none">
</label>
</div>
<!-- Name + Rasse -->
<h2 style="font-size:var(--text-2xl);font-weight:700;
color:var(--c-text);margin:0 0 var(--space-1)">${_esc(dog.name)}</h2>
${dog.rasse
? `<p style="color:var(--c-text-secondary);margin:0 0 var(--space-5)">${_esc(dog.rasse)}</p>`
: `<p style="margin:0 0 var(--space-5)"></p>`}
<!-- Info-Grid -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);
margin-bottom:var(--space-5);text-align:left">
${geburtstag ? `
<div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px">🎂 Geburtstag</div>
<div style="font-weight:500;font-size:var(--text-sm)">${geburtstag}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
${_calcAlter(dog.geburtstag)}
</div>
</div>
` : ''}
${dog.geschlecht ? `
<div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px">${dog.geschlecht === 'm' ? '♂' : '♀'} Geschlecht</div>
<div style="font-weight:500;font-size:var(--text-sm)">
${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'}
</div>
</div>
` : ''}
${dog.gewicht_kg ? `
<div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px">⚖️ Gewicht</div>
<div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div>
</div>
` : ''}
${dog.chip_nr ? `
<div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px">💾 Chip-Nr.</div>
<div style="font-size:var(--text-xs);font-weight:500;
word-break:break-all">${_esc(dog.chip_nr)}</div>
</div>
` : ''}
</div>
${dog.bio ? `
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-5);text-align:left">
<p style="margin:0;color:var(--c-text-secondary);font-style:italic;line-height:1.6">
"${_esc(dog.bio)}"
</p>
</div>
` : ''}
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
<button class="btn btn-primary w-full" id="dp-edit-btn">
Profil bearbeiten
</button>
<button class="btn btn-secondary w-full" id="dp-add-dog-btn">
+ Weiteren Hund anlegen
</button>
</div>
</div>
`;
// Foto hochladen
document.getElementById('dp-photo-input')?.addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
try {
const fd = new FormData();
fd.append('file', file);
const result = await API.dogs.uploadPhoto(dog.id, fd);
dog.foto_url = result.foto_url;
_appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url };
_appState.dogs = _appState.dogs.map(d =>
d.id === dog.id ? _appState.activeDog : d
);
UI.toast.success('Foto gespeichert.');
_renderProfile(_appState.activeDog);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Hochladen.');
}
});
// Edit- und Add-Klicks laufen über Event-Delegation in init() — keine direkten Listener nötig.
}
// ----------------------------------------------------------
// NEU ANLEGEN (direkt auf der Seite, kein Modal)
// ----------------------------------------------------------
function _renderCreateForm() {
_container.innerHTML = `
<div style="padding:var(--space-4) 0 var(--space-2)">
<div style="text-align:center;margin-bottom:var(--space-5)">
<div style="font-size:3rem;margin-bottom:var(--space-2)">🐕</div>
<h2 style="font-size:var(--text-xl);font-weight:700;margin:0 0 var(--space-2)">
Hund anlegen
</h2>
<p style="color:var(--c-text-secondary);margin:0">
Erstelle das Profil für deinen Hund.
</p>
</div>
${_formHTML(null)}
</div>
`;
_bindForm(null, false);
}
// ----------------------------------------------------------
// NEUEN HUND ANLEGEN (Modal) — auch aufrufbar via addNew()
// ----------------------------------------------------------
function _openCreateModal() {
UI.modal.open({ title: 'Weiteren Hund anlegen', body: _formHTML(null, true) });
_bindForm(null, true);
}
// ----------------------------------------------------------
// BEARBEITEN (Modal)
// ----------------------------------------------------------
function _openEditModal(dog) {
UI.modal.open({ title: `${dog.name} bearbeiten`, body: _formHTML(dog, true) });
_bindForm(dog, true);
}
// ----------------------------------------------------------
// FORMULAR HTML
// ----------------------------------------------------------
function _formHTML(dog, inModal = false) {
const today = new Date().toISOString().slice(0, 10);
return `
<form id="dp-form" autocomplete="off">
<div class="form-group">
<label class="form-label">Name *</label>
<input class="form-control" type="text" name="name"
value="${_esc(dog?.name || '')}"
placeholder="z. B. Ban Yaro" required>
</div>
<div class="form-group">
<label class="form-label">
Rasse
<span style="color:var(--c-text-secondary)">(optional)</span>
</label>
<input class="form-control" type="text" name="rasse"
value="${_esc(dog?.rasse || '')}"
placeholder="z. B. Mischling, Golden Retriever…">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Geburtstag</label>
<input class="form-control" type="date" name="geburtstag"
value="${dog?.geburtstag || ''}" max="${today}">
</div>
<div class="form-group">
<label class="form-label">Geschlecht</label>
<select class="form-control" name="geschlecht">
<option value="" ${!dog?.geschlecht ? 'selected' : ''}></option>
<option value="m" ${dog?.geschlecht === 'm' ? 'selected' : ''}>Rüde</option>
<option value="w" ${dog?.geschlecht === 'w' ? 'selected' : ''}>Hündin</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Gewicht (kg)</label>
<input class="form-control" type="number" name="gewicht_kg"
value="${dog?.gewicht_kg || ''}"
min="0.1" max="120" step="0.1" placeholder="z. B. 28.5">
</div>
<div class="form-group">
<label class="form-label">Chip-Nummer</label>
<input class="form-control" type="text" name="chip_nr"
value="${_esc(dog?.chip_nr || '')}" placeholder="15-stellig">
</div>
</div>
<div class="form-group">
<label class="form-label">
Bio / Steckbrief
<span style="color:var(--c-text-secondary)">(optional)</span>
</label>
<textarea class="form-control" name="bio" rows="2"
placeholder="Kurze Beschreibung…">${_esc(dog?.bio || '')}</textarea>
</div>
<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_public" ${dog?.is_public ? 'checked' : ''}>
Öffentliches Profil (für NFC-Tag)
</label>
</div>
<div class="form-group">
<label class="form-label">Foto</label>
<div style="display:flex;align-items:center;gap:var(--space-3)">
<img id="dp-form-preview"
src="${dog?.foto_url || ''}"
style="width:64px;height:64px;border-radius:50%;object-fit:cover;
background:var(--c-surface-2);border:2px solid var(--c-border);
display:${dog?.foto_url ? 'block' : 'none'}">
<label class="btn btn-secondary btn-sm" style="cursor:pointer;margin:0">
📷 Foto auswählen
<input type="file" name="foto" accept="image/*" style="display:none"
id="dp-form-foto">
</label>
</div>
</div>
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-4)">
${(dog || inModal) ? `<button type="button" class="btn btn-secondary flex-1"
id="dp-form-cancel">Abbrechen</button>` : ''}
<button type="submit" class="btn btn-primary flex-1">
${dog ? 'Speichern' : '🐕 Hund anlegen'}
</button>
</div>
${dog ? `
<div style="margin-top:var(--space-5);padding-top:var(--space-4);
border-top:1px solid var(--c-border);text-align:center">
<button type="button" class="btn btn-ghost btn-sm" id="dp-delete-btn"
style="color:var(--c-danger)">
${dog.name} löschen
</button>
</div>
` : ''}
</form>
`;
}
// ----------------------------------------------------------
// FORMULAR EVENTS
// ----------------------------------------------------------
function _bindForm(dog, inModal) {
const form = document.getElementById('dp-form');
if (!form) return;
// Foto-Vorschau
const fotoInput = document.getElementById('dp-form-foto');
const fotoPreview = document.getElementById('dp-form-preview');
if (fotoInput && fotoPreview) {
fotoInput.addEventListener('change', () => {
const file = fotoInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
fotoPreview.src = e.target.result;
fotoPreview.style.display = 'block';
};
reader.readAsDataURL(file);
});
}
document.getElementById('dp-form-cancel')
?.addEventListener('click', UI.modal.close);
document.getElementById('dp-delete-btn')?.addEventListener('click', async () => {
const ok = await UI.modal.confirm({
title : `${dog.name} löschen?`,
message: 'Tagebuch-Einträge und Gesundheitsdaten werden ebenfalls gelöscht. Nicht rückgängig.',
confirmText: 'Löschen',
danger : true,
});
if (!ok) return;
try {
await API.dogs.delete(dog.id);
_appState.dogs = _appState.dogs.filter(d => d.id !== dog.id);
_appState.activeDog = _appState.dogs[0] || null;
if (inModal) UI.modal.close();
UI.toast.success(`${dog.name} wurde gelöscht.`);
await _render();
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Löschen.');
}
});
form.addEventListener('submit', async e => {
e.preventDefault();
const btn = form.querySelector('[type="submit"]');
const fd = UI.formData(form);
if (!fd.name?.trim()) {
UI.toast.warning('Bitte einen Namen eingeben.');
return;
}
await UI.asyncButton(btn, async () => {
const payload = {
name: fd.name.trim(),
rasse: fd.rasse || null,
geburtstag: fd.geburtstag || null,
geschlecht: fd.geschlecht || null,
gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null,
chip_nr: fd.chip_nr || null,
bio: fd.bio || null,
is_public: 'is_public' in fd,
};
let saved;
if (dog) {
saved = await API.dogs.update(dog.id, payload);
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? saved : d);
_appState.activeDog = saved;
if (inModal) UI.modal.close();
UI.toast.success('Profil gespeichert.');
} else {
saved = await API.dogs.create(payload);
_appState.dogs.push(saved);
_appState.activeDog = saved;
localStorage.setItem('by_active_dog', String(saved.id));
if (inModal) UI.modal.close();
UI.toast.success(`${saved.name} wurde angelegt! 🎉`);
}
// Foto hochladen wenn gewählt
const fotoFile = document.getElementById('dp-form-foto')?.files[0];
if (fotoFile) {
try {
const fd = new FormData();
fd.append('file', fotoFile);
const result = await API.dogs.uploadPhoto(saved.id, fd);
saved.foto_url = result.foto_url;
_appState.activeDog = { ...saved };
_appState.dogs = _appState.dogs.map(d => d.id === saved.id ? _appState.activeDog : d);
} catch {
UI.toast.warning('Profil gespeichert, Foto konnte nicht hochgeladen werden.');
}
}
// Dog Switcher in Header + Sidebar aktualisieren
App.renderDogSwitcher?.();
await _render();
});
});
}
// ----------------------------------------------------------
// HELPER
// ----------------------------------------------------------
function _calcAlter(geburtstag) {
const born = new Date(geburtstag + 'T00:00:00');
const tage = Math.floor((Date.now() - born) / 86400000);
if (tage < 0) return '';
if (tage < 30) return `${tage} Tag${tage !== 1 ? 'e' : ''} alt`;
if (tage < 365) {
const m = Math.floor(tage / 30);
return `${m} Monat${m !== 1 ? 'e' : ''} alt`;
}
const j = Math.floor(tage / 365);
const m = Math.floor((tage % 365) / 30);
return m > 0
? `${j} Jahr${j !== 1 ? 'e' : ''}, ${m} Monat${m !== 1 ? 'e' : ''} alt`
: `${j} Jahr${j !== 1 ? 'e' : ''} alt`;
}
function _esc(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
return { init, refresh, onDogChange, addNew: _openCreateModal };
})();