/* ============================================================
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');
}
if (e.target.closest('[data-action="goto-weight"]')) {
App.navigate('health', true, { tab: 'gewicht', openForm: true });
return;
}
});
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: `Anmelden `,
});
_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 = `
${dog.foto_url
? `
`
: `
${UI.icon('dog')}
`}
${UI.icon('pencil-simple')}
${UI.escape(dog.name)}
${dog.rasse
? `
${UI.escape(dog.rasse)}
`
: `
`}
${geburtstag ? `
Geburtstag
${geburtstag}
${_calcAlter(dog.geburtstag)}
` : ''}
${dog.geschlecht ? `
${dog.geschlecht === 'm' ? ' ' : ' '} Geschlecht
${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'}
` : ''}
${dog.gewicht_kg ? `
Gewicht
${dog.gewicht_kg} kg
` : ''}
${dog.widerrist_cm ? `
Widerrist
${dog.widerrist_cm} cm
` : ''}
Transponder
${dog.chip_nr
? `
${UI.escape(dog.chip_nr)}
`
: `
nicht eingetragen
Eintragen
`
}
${dog.bio ? `
` : ''}
${dog.is_public ? `
NFC-Link
banyaro.app/hund/${dog.id}
Kopieren
Dieser Link kann auf ein NFC-Tag gebrannt werden
` : ''}
${!dog.is_guest ? `
Profil bearbeiten
` : ''}
${!dog.is_guest ? `
Teilen
` : ''}
${!dog.is_guest ? `
Hundepass
` : ''}
${!dog.is_guest ? `
Visitenkarte teilen
` : ''}
${!dog.is_guest && App.hasPro(_appState.user) ? `
+ Weiteren Hund anlegen
` : ''}
${!dog.is_guest ? `
✨ Jahresrückblick ${new Date().getFullYear()}
` : ''}
${!dog.is_guest ? `
📖 Hunde-Buch erstellen
` : ''}
${!dog.is_guest ? `
Lebens-Timeline 🐾
` : ''}
${dog.user_id === _appState.user?.id ? `
Sitter-Zugang
Gib einem Freund temporären Schreibzugang für diesen Hund.
Deine bestehenden Daten und Medien bleiben unsichtbar und privat — der Sitter kann nur neue Einträge anlegen.
Lade…
` : ''}
`;
// Foto-Editor öffnen
document.getElementById('dp-photo-edit-btn')?.addEventListener('click', () => {
_showPhotoEditor(dog);
});
// Skills laden
_loadSkills(dog);
// Pflegetipps laden
_loadPflegeTipps(dog);
// Rassen-Community-Chip laden (falls Rasse bekannt)
if (dog.rasse) _loadSameBreedChip();
// Sitter-Zugang laden (nur für Besitzer)
if (dog.user_id === _appState.user?.id) {
_loadSittingAccess(dog.id);
}
// NFC-Link kopieren
document.getElementById('dp-copy-link-btn')?.addEventListener('click', async () => {
const url = `https://banyaro.app/hund/${dog.id}`;
try {
await navigator.clipboard.writeText(url);
UI.toast.success('Link kopiert!');
} catch {
// Fallback für ältere Browser
const el = document.getElementById('dp-nfc-link');
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
document.execCommand('copy');
sel.removeAllRanges();
UI.toast.success('Link kopiert!');
}
});
// Transponder "Eintragen"-Button
document.getElementById('dp-chip-edit-btn')?.addEventListener('click', () => {
_showChipEdit(dog);
});
document.getElementById('dp-share-btn')?.addEventListener('click', () => {
_showShareModal(dog);
});
document.getElementById('dp-passport-btn')?.addEventListener('click', () => {
_showPassportModal(dog);
});
document.getElementById('dp-vcard-btn')?.addEventListener('click', () => {
_showVcardModal(dog);
});
document.getElementById('dp-wrapped-btn')?.addEventListener('click', () => {
_showWrappedModal(dog);
});
document.getElementById('dp-buch-btn')?.addEventListener('click', () => {
_showBuchModal(dog);
});
document.getElementById('dp-timeline-btn')?.addEventListener('click', () => {
_showTimelineModal(dog);
});
// Edit- und Add-Klicks laufen über Event-Delegation in init() — keine direkten Listener nötig.
}
// ----------------------------------------------------------
// FÄHIGKEITEN & KOMMANDOS
// ----------------------------------------------------------
async function _loadSkills(dog) {
const el = document.getElementById('dp-skills');
if (!el) return;
const skills = await API.dogs.getSkills(dog.id).catch(() => null);
if (!skills || !skills.length) { el.innerHTML = ''; return; }
const sitzt = skills.filter(s => s.status === 'sitzt');
const meistens = skills.filter(s => s.status === 'meistens');
const badge = (skill, type) => {
const isGreen = type === 'sitzt';
return `
${UI.escape(skill.exercise_name)}
`;
};
const sitztBlock = sitzt.length ? `
Sitzt
${sitzt.map(s => badge(s, 'sitzt')).join('')}
` : '';
const meistensBlock = meistens.length ? `
Übt noch
${meistens.map(s => badge(s, 'meistens')).join('')}
` : '';
el.innerHTML = `
Kommandos & Fähigkeiten
${sitztBlock}
${meistensBlock}
`;
}
// ----------------------------------------------------------
// PFLEGETIPPS
// ----------------------------------------------------------
async function _loadPflegeTipps(dog) {
const el = document.getElementById('dp-pflege');
if (!el) return;
let data;
try {
data = await API.get(`/dogs/${dog.id}/pflege`);
} catch { return; }
if (!data?.tipps?.length) return;
const t = data.tipp_des_tages;
const _ph = n => ` `;
const kat_icons = {
'Fell': _ph('scissors'), 'Krallen': _ph('scissors'),
'Zähne': _ph('tooth'), 'Ohren': _ph('ear'),
'Augen': _ph('eye'), 'Pfoten': _ph('paw-print'),
'Parasiten': _ph('bug'), 'Saisonal': _ph('flower'),
'Gesundheitsvorsorge':_ph('heart'), 'Welpen-Pflege': _ph('dog'),
};
const pflegeArtBadge = data.fell_pflege_art === 'schneiden'
? `✂️ Schneiden `
: data.fell_pflege_art === 'trimmen'
? `✋ Trimmen `
: '';
el.innerHTML = `
🛁
Pflegetipps${data.rasse_name ? ` für ${UI.escape(data.rasse_name)}` : ''}
${t ? `
${t.saisonal_aktuell ? '🌸 Aktuell & Saisonal' : '💡 Tipp des Tages'}
${kat_icons[t.kategorie]||_ph('paw-print')} ${UI.escape(t.titel)}
${UI.escape(t.beschreibung||'')}
${t.haeufigkeit ? `
🔄 ${UI.escape(t.haeufigkeit)}
` : ''}
${t.materialien ? `
🛒 ${UI.escape(t.materialien)}
` : ''}
${t.schritte?.length ? `
Anleitung anzeigen
${t.schritte.map(s=>`${UI.escape(s)} `).join('')}
${t.tipp ? `💜 ${UI.escape(t.tipp)}
` : ''}
` : ''}
` : ''}
Alle ${data.tipps.length} Pflegetipps anzeigen
${data.kategorien.map(kat => {
const katTipps = data.tipps.filter(t=>t.kategorie===kat);
const katBadge = kat === 'Fell' ? pflegeArtBadge : '';
return `
${kat_icons[kat]||_ph('paw-print')} ${UI.escape(kat)}${katBadge}
${katTipps.map(tip => `
${UI.escape(tip.titel)}
${tip.saisonal_aktuell ? '● Aktuell ' : ''}
${UI.escape(tip.beschreibung||'')}
${tip.haeufigkeit ? `🔄 ${UI.escape(tip.haeufigkeit)}
` : ''}
${tip.schritte?.length ? `
${tip.schritte.map(s=>`${UI.escape(s)} `).join('')}
` : ''}
${tip.tipp ? `💜 ${UI.escape(tip.tipp)}
` : ''}
`).join('')}
`;
}).join('')}
`;
el.querySelector('#dp-pflege-alle')?.addEventListener('click', e => {
const liste = el.querySelector('#dp-pflege-liste');
const btn = e.currentTarget;
if (liste.style.display === 'none') {
liste.style.display = '';
btn.textContent = 'Pflegetipps einklappen ▲';
} else {
liste.style.display = 'none';
btn.textContent = `Alle ${data.tipps.length} Pflegetipps anzeigen`;
}
});
}
// ----------------------------------------------------------
// SITTER-ZUGANG
// ----------------------------------------------------------
async function _loadSittingAccess(dogId) {
const wrap = document.getElementById('dp-sitting-access');
if (!wrap) return;
try {
const [accessData, friendsData] = await Promise.all([
API.sittingAccess.my(),
API.friends.list(),
]);
const active = (accessData.as_owner || []).filter(s => s.dog_id === dogId);
const friends = (friendsData?.friends || []);
let activeHtml = '';
if (active.length) {
activeHtml = active.map(s => `
${UI.escape(s.sitter_name)}
· bis ${UI.escape(s.valid_until)}
`).join('');
}
const friendOptions = friends.length
? friends.map(f => `${UI.escape(f.friend_name)} `).join('')
: 'Keine Freunde vorhanden ';
const today = new Date().toISOString().slice(0, 10);
const defaultUntil = new Date(Date.now() + 14 * 86400000).toISOString().slice(0, 10);
wrap.innerHTML = `
${activeHtml}
${friends.length ? `
Zugang gewähren
Zugang gewähren
` : `
Füge zuerst Freunde hinzu, um ihnen Zugang zu gewähren.
`}
`;
// Event-Listener
wrap.querySelectorAll('.sa-revoke-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const subId = parseInt(btn.dataset.subId);
try {
await API.sittingAccess.revoke(subId);
_loadSittingAccess(dogId);
} catch (e) {
UI.toast.error(e.message || 'Fehler beim Widerrufen.');
}
});
});
document.getElementById('sa-grant-btn')?.addEventListener('click', async () => {
const sitterId = parseInt(document.getElementById('sa-friend-select').value);
const validUntil = document.getElementById('sa-until-input').value;
if (!sitterId) { UI.toast.warning('Bitte einen Freund auswählen.'); return; }
if (!validUntil) { UI.toast.warning('Bitte ein Datum angeben.'); return; }
const btn = document.getElementById('sa-grant-btn');
UI.setLoading(btn, true);
try {
await API.sittingAccess.grant({ dog_id: dogId, sitter_id: sitterId, valid_until: validUntil });
UI.toast.success('Sitter-Zugang gewährt.');
_loadSittingAccess(dogId);
} catch (e) {
UI.setLoading(btn, false);
UI.toast.error(e.message || 'Fehler beim Gewähren.');
}
});
} catch (e) {
if (wrap) wrap.innerHTML = 'Fehler beim Laden.
';
}
}
function _showChipEdit(dog) {
UI.modal.open({
title: 'Transpondernummer',
body: `
Chip-Nummer (15-stellig)
`,
footer: `
Speichern
Abbrechen
`,
});
document.getElementById('chip-edit-save-btn').addEventListener('click', async () => {
const nr = document.getElementById('chip-edit-input').value.trim() || null;
const btn = document.getElementById('chip-edit-save-btn');
UI.setLoading(btn, true);
try {
await API.dogs.update(dog.id, { chip_nr: nr });
dog.chip_nr = nr;
_appState.activeDog = { ..._appState.activeDog, chip_nr: nr };
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d);
UI.modal.close();
UI.toast.success('Transpondernummer gespeichert.');
_renderProfile(_appState.activeDog);
} catch (e) {
UI.setLoading(btn, false);
UI.toast.error('Fehler beim Speichern.');
}
});
}
// ----------------------------------------------------------
// FOTO-EDITOR
// ----------------------------------------------------------
function _showPhotoEditor(dog) {
const hasPhoto = !!dog.foto_url;
const zoom = dog.foto_zoom || 1.0;
const ox = dog.foto_offset_x || 0.0;
const oy = dog.foto_offset_y || 0.0;
const body = `
${hasPhoto
? `
`
: `
${UI.icon('dog')}
`}
${hasPhoto ? `
Zoom
` : ''}
${UI.icon('upload-simple')} Neues Foto wählen
`;
const footer = `
${hasPhoto ? `
Speichern ` : ''}
${hasPhoto ? `${UI.icon('trash')} Löschen ` : ''}
Abbrechen
`;
UI.modal.open({ title: 'Foto bearbeiten', body, footer });
// State für Drag
let _zoom = zoom, _ox = ox, _oy = oy;
const img = document.getElementById('pe-img');
const zoomSlider = document.getElementById('pe-zoom');
// Offsets in % des Preview-Containers (200px) — konsistent mit Profil-Anzeige
const PREVIEW_PX = 200;
function _applyTransform() {
if (img) img.style.transform = `scale(${_zoom}) translate(${_ox}%,${_oy}%)`;
}
// Zoom-Slider
zoomSlider?.addEventListener('input', e => {
_zoom = parseFloat(e.target.value);
_applyTransform();
});
// Drag-to-pan
if (img) {
let _dragging = false, _startX = 0, _startY = 0, _baseX = _ox, _baseY = _oy;
img.addEventListener('pointerdown', e => {
_dragging = true; _startX = e.clientX; _startY = e.clientY;
_baseX = _ox; _baseY = _oy;
img.setPointerCapture(e.pointerId);
e.preventDefault();
});
img.addEventListener('pointermove', e => {
if (!_dragging) return;
// Pixel-Delta → Prozent des Preview-Containers, korrigiert um Zoom
_ox = _baseX + (e.clientX - _startX) / (PREVIEW_PX / 100) / _zoom;
_oy = _baseY + (e.clientY - _startY) / (PREVIEW_PX / 100) / _zoom;
_applyTransform();
});
img.addEventListener('pointerup', () => { _dragging = false; });
}
// Speichern
document.getElementById('pe-save-btn')?.addEventListener('click', async () => {
try {
await API.dogs.updatePhotoPosition(dog.id, _zoom, _ox, _oy);
_appState.activeDog = { ..._appState.activeDog, foto_zoom: _zoom, foto_offset_x: _ox, foto_offset_y: _oy };
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d);
UI.modal.close();
UI.toast.success('Position gespeichert.');
_renderProfile(_appState.activeDog);
App.renderDogSwitcher();
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Speichern.');
}
});
// Löschen
document.getElementById('pe-delete-btn')?.addEventListener('click', async () => {
if (!confirm('Foto wirklich löschen?')) return;
try {
await API.dogs.deletePhoto(dog.id);
_appState.activeDog = { ..._appState.activeDog, foto_url: null, foto_zoom: 1, foto_offset_x: 0, foto_offset_y: 0 };
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d);
UI.modal.close();
UI.toast.success('Foto gelöscht.');
_renderProfile(_appState.activeDog);
App.renderDogSwitcher();
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Löschen.');
}
});
// Neues Foto hochladen
document.getElementById('pe-file-input')?.addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
try {
// Client-Side-Kompression vor Upload (HEIC bleibt unverändert)
const toUpload = await API.compressImage(file);
const fd = new FormData();
fd.append('file', toUpload);
const result = await API.dogs.uploadPhoto(dog.id, fd);
// Position zurücksetzen
await API.dogs.updatePhotoPosition(dog.id, 1.0, 0.0, 0.0);
_appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url, foto_zoom: 1, foto_offset_x: 0, foto_offset_y: 0 };
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d);
// localStorage + SW-Cache invalidieren
const userId2 = _appState.user?.id || 'anon';
localStorage.removeItem(`w3_bg3_${userId2}_` + new Date().toISOString().slice(0, 10));
localStorage.removeItem('w3_dogs');
API.swCacheDelete('/api/dogs');
API.swCacheDelete(`/api/dogs/${dog.id}`);
API.swCacheDelete(`/api/dogs/${dog.id}/welcome-dashboard`);
UI.modal.close();
App.renderDogSwitcher?.();
window.Worlds?.refresh(_appState);
UI.toast.success('Foto hochgeladen.');
_renderProfile(_appState.activeDog);
// Editor neu öffnen damit User positionieren kann
_showPhotoEditor(_appState.activeDog);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Hochladen.');
}
});
}
// ----------------------------------------------------------
// AUSWEIS
// ----------------------------------------------------------
function _showAusweisModal(dogId) {
window.open(`/ausweis/${dogId}`, '_blank', 'noopener');
}
// ----------------------------------------------------------
// TEILEN
// ----------------------------------------------------------
// ----------------------------------------------------------
// HUNDE-VISITENKARTE MIT QR-CODE
// ----------------------------------------------------------
function _showVcardModal(dog) {
const passportUrl = `https://banyaro.app/hund/${dog.id}`;
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=140x140&color=ffffff&bgcolor=1a2035&data=${encodeURIComponent(passportUrl)}`;
const user = _appState?.user;
const ownerName = user?.name || '';
const wohnort = user?.wohnort || '';
// Alter errechnen
let alterStr = '';
if (dog.geburtstag) {
const birth = new Date(dog.geburtstag + 'T00:00:00');
const now = new Date();
const years = now.getFullYear() - birth.getFullYear()
- (now < new Date(now.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
alterStr = years < 1
? `${Math.max(1, Math.round((now - birth) / (30.5 * 86400000)))} Monate`
: years === 1 ? '1 Jahr' : `${years} Jahre`;
}
const metaLine = [dog.rasse, alterStr].filter(Boolean).join(' · ');
const cardHtml = `
${dog.foto_url
? `
`
: `
🐾
`}
${UI.escape(dog.name)}
${metaLine ? `
${UI.escape(metaLine)}
` : ''}
${wohnort ? `
📍 ${UI.escape(wohnort)}
` : ''}
${ownerName ? `
Besitzer
${UI.escape(ownerName)}
` : ''}
banyaro.app
Profil öffnen
`;
UI.modal.open({
title: 'Visitenkarte',
body: `
${cardHtml}
QR-Code auf NFC-Tag oder Anhänger kleben — jeder kann das Profil von ${UI.escape(dog.name)} sofort öffnen.
`,
footer: `
Link kopieren
Teilen
`,
});
// Link kopieren
document.getElementById('dp-vcard-copy-btn')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(passportUrl);
UI.toast.success('Link kopiert!');
} catch {
const inp = document.createElement('input');
inp.value = passportUrl;
document.body.appendChild(inp);
inp.select();
document.execCommand('copy');
inp.remove();
UI.toast.success('Link kopiert!');
}
});
// Native Share API
document.getElementById('dp-vcard-share-btn')?.addEventListener('click', async () => {
if (navigator.share) {
try {
await navigator.share({
title: `${dog.name} auf Ban Yaro`,
text: `Schau dir das Profil von ${dog.name} an!`,
url: passportUrl,
});
} catch {}
} else {
// Fallback: kopieren
try {
await navigator.clipboard.writeText(passportUrl);
UI.toast.success('Link kopiert!');
} catch {
UI.toast.error('Teilen nicht verfügbar.');
}
}
});
}
async function _showShareModal(dog) {
UI.modal.open({
title: `${UI.escape(dog.name)} teilen`,
body: `
Erstelle einen Einladungslink, den du per WhatsApp, Signal oder E-Mail teilen kannst.
Die eingeladene Person sieht Tagebuch und Gesundheitsakte nach dem Annehmen.
Berechtigung
Mitschreiben (Tagebuch & Gesundheit bearbeiten)
Nur lesen
`,
footer: `
Schließen
Link erstellen `,
});
_loadShareList(dog.id);
document.getElementById('share-create-btn').addEventListener('click', async () => {
const role = document.getElementById('share-role-select').value;
const btn = document.getElementById('share-create-btn');
UI.setLoading(btn, true);
try {
const res = await API.sharing.create(dog.id, role);
const link = `${location.origin}${res.invite_path}`;
const inp = document.getElementById('share-link-input');
inp.value = link;
document.getElementById('share-link-result').style.display = 'block';
document.getElementById('share-link-copy').onclick = async () => {
await navigator.clipboard.writeText(link).catch(() => {});
UI.toast.success('Link kopiert!');
};
UI.setLoading(btn, false);
_loadShareList(dog.id);
} catch (e) {
UI.setLoading(btn, false);
UI.toast.error(e.message || 'Fehler');
}
});
}
async function _loadShareList(dogId) {
const wrap = document.getElementById('share-list-wrap');
if (!wrap) return;
try {
const shares = await API.sharing.list(dogId);
if (!shares.length) { wrap.innerHTML = ''; return; }
wrap.innerHTML = `
Aktive Einladungen
` +
shares.map(s => `
${s.shared_with_name
? `${UI.escape(s.shared_with_name)} · ${s.role}`
: `Ausstehend · ${s.role}`}
`).join('');
wrap.querySelectorAll('.share-revoke-btn').forEach(btn => {
btn.addEventListener('click', async () => {
await API.sharing.revoke(dogId, parseInt(btn.dataset.shareId));
_loadShareList(dogId);
});
});
} catch (e) { /* ignore */ }
}
// ----------------------------------------------------------
// NEU ANLEGEN (direkt auf der Seite, kein Modal)
// ----------------------------------------------------------
function _renderCreateForm() {
_container.innerHTML = `
${UI.icon('dog')}
Hund anlegen
Erstelle das Profil für deinen Hund.
${_formHTML(null)}
`;
_bindForm(null, false);
}
// ----------------------------------------------------------
// NEUEN HUND ANLEGEN (Modal) — auch aufrufbar via addNew()
// ----------------------------------------------------------
function _openCreateModal() {
UI.modal.open({
title: 'Weiteren Hund anlegen',
body: _formHTML(null, true),
footer: `
${UI.icon('dog')} Hund anlegen
Abbrechen
`,
});
_bindForm(null, true);
}
// ----------------------------------------------------------
// BEARBEITEN (Modal)
// ----------------------------------------------------------
function _openEditModal(dog) {
UI.modal.open({
title: `${dog.name} bearbeiten`,
body: _formHTML(dog, true),
footer: `
Speichern
Löschen
Verstorben
Abbrechen
`,
});
_bindForm(dog, true);
}
// ----------------------------------------------------------
// FORMULAR HTML
// ----------------------------------------------------------
function _formHTML(dog, inModal = false) {
const today = new Date().toISOString().slice(0, 10);
return `
`;
}
// ----------------------------------------------------------
// FORMULAR EVENTS
// ----------------------------------------------------------
function _bindForm(dog, inModal) {
const form = document.getElementById('dp-form');
if (!form) return;
// Rassen-Autocomplete aus Wiki laden
let _wikiBreeds = [];
API.get('/wiki/rassen?limit=1000&offset=0').then(data => {
_wikiBreeds = data.rassen || [];
const list = document.getElementById('dp-rasse-list');
if (list) {
list.innerHTML = _wikiBreeds.map(r =>
``
).join('');
}
// Vorhandene Rasse: Match prüfen und Badge zeigen
const rasseInput = document.getElementById('dp-rasse-input');
const rasseIdInput = document.getElementById('dp-rasse-id');
const matchBadge = document.getElementById('dp-rasse-match');
if (rasseInput?.value) {
const match = _wikiBreeds.find(r =>
r.name.toLowerCase() === rasseInput.value.toLowerCase());
if (match && matchBadge) {
if (!rasseIdInput.value) rasseIdInput.value = match.id;
matchBadge.style.display = '';
}
}
}).catch(() => {});
// Rassen-Input: bei Änderung ID nachschlagen
document.getElementById('dp-rasse-input')?.addEventListener('input', e => {
const val = e.target.value.trim().toLowerCase();
const rasseIdInput = document.getElementById('dp-rasse-id');
const matchBadge = document.getElementById('dp-rasse-match');
const match = _wikiBreeds.find(r => r.name.toLowerCase() === val);
if (match) {
rasseIdInput.value = match.id;
if (matchBadge) matchBadge.style.display = '';
} else {
rasseIdInput.value = '';
if (matchBadge) matchBadge.style.display = 'none';
}
});
// 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);
});
}
// Rassen-Erkennung per KI
_bindRasseErkennung();
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?`,
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 = document.querySelector('[form="dp-form"][type="submit"]') || 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,
rasse_id: fd.rasse_id ? parseInt(fd.rasse_id) : null,
geburtstag: fd.geburtstag || null,
geschlecht: fd.geschlecht || null,
gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null,
widerrist_cm: fd.widerrist_cm ? parseFloat(fd.widerrist_cm) : null,
chip_nr: fd.chip_nr || null,
bio: fd.bio || null,
is_public: 'is_public' in fd,
fell_typ: fd.fell_typ || null,
};
// Datei-Referenz VOR Modal-Close sichern — DOM-Element wird beim Schließen entfernt
const fotoFile = document.getElementById('dp-form-foto')?.files[0];
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
if (fotoFile) {
try {
// Client-Side-Kompression vor Upload
const toUpload = await API.compressImage(fotoFile);
const fd = new FormData();
fd.append('file', toUpload);
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);
// localStorage + SW-Cache invalidieren damit Welten das neue Foto zeigen
const userId = _appState.user?.id || 'anon';
localStorage.removeItem(`w3_bg3_${userId}_` + new Date().toISOString().slice(0, 10));
localStorage.removeItem('w3_dogs');
API.swCacheDelete('/api/dogs');
API.swCacheDelete(`/api/dogs/${saved.id}`);
API.swCacheDelete(`/api/dogs/${saved.id}/welcome-dashboard`);
} catch {
UI.toast.warning('Profil gespeichert, Foto konnte nicht hochgeladen werden.');
}
}
// Dog Switcher in Header + Sidebar aktualisieren
App.renderDogSwitcher?.();
// Welten neu laden damit HUND-Welt den neuen Hund zeigt
window.Worlds?.refresh(_appState);
await _render();
});
});
}
// ----------------------------------------------------------
// RASSEN-ERKENNUNG PER KI (Formular)
// ----------------------------------------------------------
function _bindRasseErkennung() {
const btn = document.getElementById('dp-rasse-erkennen-btn');
const fileInput = document.getElementById('dp-rasse-foto-input');
if (!btn || !fileInput) return;
btn.addEventListener('click', () => {
fileInput.value = '';
fileInput.click();
});
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
UI.toast.error('Bild zu groß (max. 5 MB).');
return;
}
const origLabel = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = ` KI analysiert…`;
try {
const fd = new FormData();
fd.append('file', file);
const token = localStorage.getItem('by_token');
const resp = await fetch('/api/ki/rasse-erkennung', {
method: 'POST',
credentials: 'include',
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
body: fd,
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.detail || 'Fehler bei der Erkennung.');
btn.disabled = false;
btn.innerHTML = origLabel;
_showRasseErgebnis(data);
} catch (e) {
btn.disabled = false;
btn.innerHTML = origLabel;
UI.toast.error(e.message || 'Fehler bei der Rassen-Erkennung.');
}
});
}
function _showRasseErgebnis(data) {
if (!data.ist_hund) {
UI.modal.open({
title: 'Kein Hund erkannt',
body: `
🐾
Auf diesem Foto konnte kein Hund erkannt werden.
Bitte lade ein deutlicheres Foto hoch.
${data.hinweis ? `
${UI.escape(data.hinweis)}
` : ''}
`,
footer: `Schließen `,
});
return;
}
const rassen = data.rassen || [];
const cardsHtml = rassen.map((r, i) => {
const isTop = i === 0;
return `
${isTop ? '🐕 ' : ''}${UI.escape(r.name)}
${r.sicherheit}%
${r.beschreibung ? `
${UI.escape(r.beschreibung)}
` : ''}
${isTop ? `
Rasse übernehmen
` : `
Diese wählen
`}
${r.wiki_slug ? `
Im Wiki
` : ''}
`;
}).join('');
UI.modal.open({
title: 'Erkannte Rasse',
body: `
${data.hinweis ? `
ℹ️ ${UI.escape(data.hinweis)}
` : ''}
${cardsHtml}
Noch ${data.verbleibende_anfragen} Erkennung${data.verbleibende_anfragen !== 1 ? 'en' : ''} heute verfügbar
`,
footer: `Schließen `,
});
document.getElementById('dp-rasse-modal-schliessen')
?.addEventListener('click', UI.modal.close);
document.querySelectorAll('[data-action="uebernehmen"]').forEach(btn => {
btn.addEventListener('click', () => {
const rasse = btn.dataset.rasse;
const rasseInput = document.getElementById('dp-rasse-input');
const rasseIdInput = document.getElementById('dp-rasse-id');
const matchBadge = document.getElementById('dp-rasse-match');
if (rasseInput) {
rasseInput.value = rasse;
rasseInput.dispatchEvent(new Event('input'));
}
UI.modal.close();
UI.toast.success(`Rasse "${rasse}" übernommen.`);
});
});
document.querySelectorAll('[data-action="wiki"]').forEach(btn => {
btn.addEventListener('click', () => {
UI.modal.close();
App.navigate('wiki');
setTimeout(() => {
if (window.Page_wiki && typeof Page_wiki._openBreedDetail === 'function') {
Page_wiki._openBreedDetail(btn.dataset.slug);
}
}, 400);
});
});
}
// ----------------------------------------------------------
// 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`;
}
// ----------------------------------------------------------
// HUNDEPASS
// ----------------------------------------------------------
async function _showPassportModal(dog) {
UI.modal.open({
title: `Hundepass — ${UI.escape(dog.name)}`,
body: ``,
footer: `
`,
size: 'large',
});
document.getElementById('pp-share-btn')?.addEventListener('click', () => {
_createPassportShare(dog);
});
await _loadPassportBody(dog);
}
async function _loadPassportBody(dog) {
const wrap = document.getElementById('pp-body');
if (!wrap) return;
let data;
try {
data = await API.get(`/passport/${dog.id}`);
} catch (e) {
wrap.innerHTML = `Fehler beim Laden: ${UI.escape(e.message)}
`;
return;
}
const _fmt = d => {
if (!d) return '–';
try {
const p = d.substring(0, 10).split('-');
return `${p[2]}.${p[1]}.${p[0]}`;
} catch { return d; }
};
const meta = data.meta || {};
const vaccs = data.vaccinations || [];
const meds = data.medications || [];
wrap.innerHTML = `
Gesundheits-Info
Bearbeiten
Blutgruppe
${UI.escape(meta.blutgruppe) || 'nicht eingetragen '}
Allergien
${UI.escape(meta.allergien) || 'keine '}
${meta.besonderheiten ? `
Besonderheiten
${UI.escape(meta.besonderheiten)}
` : ''}
Impfungen
+ Eintragen
${vaccs.length === 0
? `
Noch keine Impfungen eingetragen. Klicke auf „+ Eintragen" um loszulegen.
`
: vaccs.map(v => `
${UI.escape(v.krankheit)}
Gegeben: ${_fmt(v.datum)}
${v.naechste ? ` · Nächste: ${_fmt(v.naechste)}` : ''}
${v.tierarzt ? ` · ${UI.escape(v.tierarzt)}` : ''}
${v.charge_nr ? ` · Charge: ${UI.escape(v.charge_nr)}` : ''}
`).join('')
}
Medikamente
+ Eintragen
${meds.length === 0
? `
Noch keine Medikamente eingetragen. Klicke auf „+ Eintragen" um loszulegen.
`
: meds.map(m => `
${UI.escape(m.name)}
${m.dosierung ? `${UI.escape(m.dosierung)} · ` : ''}
${m.von ? `Von ${_fmt(m.von)}` : ''}
${m.bis ? ` bis ${_fmt(m.bis)}` : m.von ? ' · dauerhaft' : ''}
${m.notiz ? ` · ${UI.escape(m.notiz)}` : ''}
`).join('')
}
`;
// Meta bearbeiten
document.getElementById('pp-meta-edit-btn')?.addEventListener('click', () => {
_editPassportMeta(dog, meta, () => _loadPassportBody(dog));
});
// Impfung hinzufügen
document.getElementById('pp-vacc-add-btn')?.addEventListener('click', () => {
_addVaccination(dog, () => _loadPassportBody(dog));
});
// Impfung löschen
wrap.querySelectorAll('.pp-vacc-del').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Impfung wirklich löschen?')) return;
try {
await API.del(`/passport/${dog.id}/vaccinations/${btn.dataset.id}`);
_loadPassportBody(dog);
} catch (e) {
UI.toast.error(e.message || 'Fehler');
}
});
});
// Medikament hinzufügen
document.getElementById('pp-med-add-btn')?.addEventListener('click', () => {
_addMedication(dog, () => _loadPassportBody(dog));
});
// Medikament löschen
wrap.querySelectorAll('.pp-med-del').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Medikament wirklich löschen?')) return;
try {
await API.del(`/passport/${dog.id}/medications/${btn.dataset.id}`);
_loadPassportBody(dog);
} catch (e) {
UI.toast.error(e.message || 'Fehler');
}
});
});
}
function _editPassportMeta(dog, current, onSave) {
UI.modal.open({
title: 'Gesundheits-Info bearbeiten',
body: `
Blutgruppe
Allergien
${UI.escape(current.allergien || '')}
Besonderheiten
${UI.escape(current.besonderheiten || '')}
`,
footer: `
Abbrechen
Speichern
`,
});
document.getElementById('pp-meta-save').addEventListener('click', async () => {
const btn = document.getElementById('pp-meta-save');
UI.setLoading(btn, true);
try {
await API.put(`/passport/${dog.id}/meta`, {
blutgruppe: document.getElementById('pp-meta-bg').value.trim() || null,
allergien: document.getElementById('pp-meta-al').value.trim() || null,
besonderheiten: document.getElementById('pp-meta-be').value.trim() || null,
});
UI.modal.close();
UI.toast.success('Gesundheits-Info gespeichert.');
onSave();
} catch (e) {
UI.setLoading(btn, false);
UI.toast.error(e.message || 'Fehler');
}
});
}
function _addVaccination(dog, onSave) {
const today = new Date().toISOString().slice(0, 10);
UI.modal.open({
title: 'Impfung eintragen',
body: `
Krankheit *
Tierarzt
Charge-Nr.
`,
footer: `
Abbrechen
Speichern
`,
});
document.getElementById('pp-vacc-save').addEventListener('click', async () => {
const krankheit = document.getElementById('pp-vacc-krankheit').value.trim();
const datum = document.getElementById('pp-vacc-datum').value;
if (!krankheit || !datum) {
UI.toast.warning('Bitte Krankheit und Datum angeben.');
return;
}
const btn = document.getElementById('pp-vacc-save');
UI.setLoading(btn, true);
try {
await API.post(`/passport/${dog.id}/vaccinations`, {
krankheit,
datum,
naechste: document.getElementById('pp-vacc-naechste').value || null,
tierarzt: document.getElementById('pp-vacc-tierarzt').value.trim() || null,
charge_nr: document.getElementById('pp-vacc-charge').value.trim() || null,
});
UI.modal.close();
UI.toast.success('Impfung eingetragen.');
onSave();
} catch (e) {
UI.setLoading(btn, false);
UI.toast.error(e.message || 'Fehler');
}
});
}
function _addMedication(dog, onSave) {
const today = new Date().toISOString().slice(0, 10);
UI.modal.open({
title: 'Medikament eintragen',
body: `
Medikament *
Dosierung
Notiz
`,
footer: `
Abbrechen
Speichern
`,
});
document.getElementById('pp-med-save').addEventListener('click', async () => {
const name = document.getElementById('pp-med-name').value.trim();
if (!name) {
UI.toast.warning('Bitte einen Namen angeben.');
return;
}
const btn = document.getElementById('pp-med-save');
UI.setLoading(btn, true);
try {
await API.post(`/passport/${dog.id}/medications`, {
name,
dosierung: document.getElementById('pp-med-dosierung').value.trim() || null,
von: document.getElementById('pp-med-von').value || null,
bis: document.getElementById('pp-med-bis').value || null,
notiz: document.getElementById('pp-med-notiz').value.trim() || null,
});
UI.modal.close();
UI.toast.success('Medikament eingetragen.');
onSave();
} catch (e) {
UI.setLoading(btn, false);
UI.toast.error(e.message || 'Fehler');
}
});
}
async function _createPassportShare(dog) {
const btn = document.getElementById('pp-share-btn');
if (btn) UI.setLoading(btn, true);
try {
const res = await API.post(`/passport/${dog.id}/share`, {});
const url = `${location.origin}${res.url}`;
if (btn) UI.setLoading(btn, false);
// Zeige Share-Link im Modal (window.confirm wäre zu kurz)
const shareWrap = document.createElement('div');
shareWrap.innerHTML = `
Dieser Link ist 30 Tage gültig. Tierärzte und Sitter können den Pass ohne Login öffnen.
Gültig bis: ${res.valid_until.split('-').reverse().join('.')}
`;
UI.modal.open({
title: 'Hundepass-Link teilen',
body: shareWrap.innerHTML,
footer: `Schließen `,
});
document.getElementById('pp-sharelink-copy')?.addEventListener('click', async () => {
await navigator.clipboard.writeText(url).catch(() => {});
UI.toast.success('Link kopiert!');
});
} catch (e) {
if (btn) UI.setLoading(btn, false);
UI.toast.error(e.message || 'Fehler beim Erstellen des Links.');
}
}
// ----------------------------------------------------------
// JAHRESRÜCKBLICK — WRAPPED
// ----------------------------------------------------------
async function _showWrappedModal(dog) {
const year = new Date().getFullYear();
let data = null;
try {
data = await API.get(`/dogs/${dog.id}/wrapped?year=${year}`);
} catch (e) {
UI.toast.error('Rückblick konnte nicht geladen werden.');
return;
}
const name = UI.escape(data.dog_name);
const km = data.gesamt_km || 0;
const konfetti = km > 100;
const _TYPEN = {
eintrag: 'Tagebuch', gassi: 'Gassi', training: 'Training',
tierarzt: 'Tierarzt', freizeit: 'Freizeit', milestone: 'Meilenstein',
};
const aktivitaet = data.lieblings_aktivitaet
? (_TYPEN[data.lieblings_aktivitaet] || data.lieblings_aktivitaet)
: null;
const stadtpark = km > 0 ? Math.round(km / 1.5) : 0;
const schneeheld = data.wetter_kalt >= 10;
const pfotalalarm = data.wetter_warm >= 10;
const _card = (content) =>
`${content}
`;
const cards = [
_card(`
🐾
Dein Jahr mit ${name}
${year} in Zahlen
`),
_card(`
👟
${km} km
zusammen gelaufen
${stadtpark > 0 ? `= ${stadtpark}× um den Stadtpark
` : ''}
${konfetti ? `🎉 Über 100 km!
` : ''}
`),
_card(`
📔
${data.eintraege_gesamt}
Tagebucheinträge
${data.fotos_gesamt > 0 ? `📷 ${data.fotos_gesamt} Fotos
` : ''}
${data.gassi_tage > 0 ? `🐾 ${data.gassi_tage} aktive Tage
` : ''}
${data.lieblings_monat ? `Meiste Einträge: ${UI.escape(data.lieblings_monat)}
` : ''}
${aktivitaet ? `Lieblingsaktivität: ${UI.escape(aktivitaet)}
` : ''}
`),
_card(`
🌡️
Wetter-Tapferkeit
❄️
${data.wetter_kalt}
kalte Tage
☀️
${data.wetter_warm}
heiße Tage
${schneeheld ? `❄️ Schneeheld!
` : ''}
${pfotalalarm ? `🔥 Pfoten-Alarm!
` : ''}
${data.training_sessions > 0 ? `🏋️ ${data.training_sessions} Training-Sessions
` : ''}
`),
_card(`
🐾
Was für ein Jahr!
${name} und du — ein unschlagbares Team. ${year} war unvergesslich.
📋 Text kopieren
`),
];
let currentCard = 0;
const totalCards = cards.length;
const renderDots = () => Array.from({ length: totalCards }, (_, i) =>
`
`
).join('');
const modalEl = document.createElement('div');
modalEl.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#0d0d1a;display:flex;flex-direction:column;overflow:hidden;';
modalEl.innerHTML = `
×
${renderDots()}
`;
document.body.appendChild(modalEl);
const cardContainer = modalEl.querySelector('#dp-wrapped-card-container');
const dotsEl = modalEl.querySelector('#dp-wrapped-dots');
const prevBtn = modalEl.querySelector('#dp-wrapped-prev');
const nextBtn = modalEl.querySelector('#dp-wrapped-next');
const updateCard = () => {
cardContainer.innerHTML = cards[currentCard];
dotsEl.innerHTML = renderDots();
prevBtn.style.display = currentCard > 0 ? 'flex' : 'none';
nextBtn.style.display = currentCard < totalCards - 1 ? 'flex' : 'none';
if (currentCard === totalCards - 1) {
cardContainer.querySelector('#dp-wrapped-copy-btn')?.addEventListener('click', async () => {
const shareText = `🐾 ${name} & ich — Jahresrückblick ${year}\n`
+ (km > 0 ? `👟 ${km} km gelaufen\n` : '')
+ (data.eintraege_gesamt > 0 ? `📔 ${data.eintraege_gesamt} Tagebucheinträge\n` : '')
+ (data.fotos_gesamt > 0 ? `📷 ${data.fotos_gesamt} Fotos\n` : '')
+ (data.training_sessions > 0 ? `🏋️ ${data.training_sessions} Training-Sessions\n` : '')
+ `\nbanyaro.app`;
try {
await navigator.clipboard.writeText(shareText);
UI.toast.success('Text kopiert!');
} catch {
UI.toast.error('Kopieren fehlgeschlagen.');
}
});
}
};
prevBtn.addEventListener('click', () => { if (currentCard > 0) { currentCard--; updateCard(); } });
nextBtn.addEventListener('click', () => { if (currentCard < totalCards - 1) { currentCard++; updateCard(); } });
modalEl.querySelector('#dp-wrapped-close').addEventListener('click', () => modalEl.remove());
let touchStartX = 0;
modalEl.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; }, { passive: true });
modalEl.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - touchStartX;
if (Math.abs(dx) > 50) {
if (dx < 0 && currentCard < totalCards - 1) { currentCard++; updateCard(); }
if (dx > 0 && currentCard > 0) { currentCard--; updateCard(); }
}
});
const onKey = e => { if (e.key === 'Escape') { modalEl.remove(); document.removeEventListener('keydown', onKey); } };
document.addEventListener('keydown', onKey);
}
// ----------------------------------------------------------
// HUNDE-BUCH
// ----------------------------------------------------------
function _showBuchModal(dog) {
const currentYear = new Date().getFullYear();
let selectedJahr = String(currentYear);
let nurFotos = false;
let nurMeilensteine = false;
const modalEl = document.createElement('div');
modalEl.style.cssText = `
position:fixed;inset:0;z-index:9999;
background:rgba(0,0,0,0.55);
display:flex;align-items:center;justify-content:center;padding:16px;
`;
const renderModal = () => {
const years = [String(currentYear - 1), String(currentYear), 'alle'];
const yearBtns = years.map(y => {
const active = selectedJahr === y
? 'background:#7a4f1a;color:#f5e4c0;border-color:#7a4f1a;'
: 'background:#f5f0e8;color:#444;border-color:#e0d4b8;';
const label = y === 'alle' ? 'Alle' : y;
return `${label} `;
}).join('');
const togStyle = (active) =>
active
? 'background:#7a4f1a;color:#f5e4c0;border-color:#7a4f1a;'
: 'background:#f5f0e8;color:#444;border-color:#e0d4b8;';
modalEl.innerHTML = `
📖 Hunde-Buch erstellen
Eine druckbare Ansicht der schönsten Einträge. Im Browser als PDF speichern.
${nurFotos ? '✓' : ''}
Nur Einträge mit Fotos
${nurMeilensteine ? '✓' : ''}
Nur Meilensteine
📖 Buch öffnen
✕
`;
};
window._buchSetJahr = (y) => { selectedJahr = y; renderModal(); };
window._buchToggleFotos = () => { nurFotos = !nurFotos; renderModal(); };
window._buchToggleMeilensteine = () => { nurMeilensteine = !nurMeilensteine; renderModal(); };
window._buchClose = () => {
modalEl.remove();
delete window._buchSetJahr;
delete window._buchToggleFotos;
delete window._buchToggleMeilensteine;
delete window._buchOpen;
delete window._buchClose;
};
window._buchOpen = () => {
const params = new URLSearchParams();
if (selectedJahr !== 'alle') params.set('jahr', selectedJahr);
if (nurFotos) params.set('nur_fotos', 'true');
if (nurMeilensteine) params.set('nur_meilensteine', 'true');
const url = `/api/dogs/${dog.id}/buch?${params.toString()}`;
window.open(url, '_blank');
};
renderModal();
document.body.appendChild(modalEl);
modalEl.addEventListener('click', e => { if (e.target === modalEl) window._buchClose(); });
const onKey = e => {
if (e.key === 'Escape') { window._buchClose(); document.removeEventListener('keydown', onKey); }
};
document.addEventListener('keydown', onKey);
}
// ----------------------------------------------------------
// LEBENS-TIMELINE
// ----------------------------------------------------------
async function _showTimelineModal(dog) {
UI.modal.open({
title: `Lebens-Timeline — ${UI.escape(dog.name)}`,
body: `
`,
footer: `Schließen `,
size: 'large',
});
let data;
try {
data = await API.get(`/dogs/${dog.id}/timeline`);
} catch (e) {
const b = document.getElementById('dp-timeline-body');
if (b) b.innerHTML = `Fehler: ${UI.escape(e.message)}
`;
return;
}
const wrap = document.getElementById('dp-timeline-body');
if (!wrap) return;
const events = data.events || [];
if (!events.length) {
wrap.innerHTML = `
Noch keine Einträge vorhanden. Beginne dein Tagebuch oder trage Gesundheitsdaten ein.
`;
return;
}
const _KAT = {
meilenstein: { color: '#8b5cf6', icon: 'star', label: 'Meilenstein' },
tagebuch: { color: 'var(--c-primary)', icon: 'book-open', label: 'Tagebuch' },
gesundheit: { color: '#ef4444', icon: 'heartbeat', label: 'Gesundheit' },
training: { color: '#22c55e', icon: 'target', label: 'Training' },
route: { color: '#3b82f6', icon: 'path', label: 'Route' },
};
const _fmtDate = d => {
if (!d) return '';
try {
const p = d.substring(0, 10).split('-');
return `${p[2]}.${p[1]}.${p[0]}`;
} catch { return d; }
};
let lastYear = null;
let html = '';
for (const ev of events) {
const year = ev.datum ? ev.datum.substring(0, 4) : null;
if (year && year !== lastYear) {
html += `
${UI.escape(year)}
`;
lastYear = year;
}
const kat = _KAT[ev.kategorie] || _KAT.tagebuch;
const big = ev.is_milestone;
let label = UI.escape(ev.titel);
if (ev.is_first && ev.kategorie === 'tagebuch') label = `🎉 Erster Tagebucheintrag — ${label}`;
if (ev.is_first && ev.kategorie === 'route') label = `🎉 Erste Route — ${label}`;
if (ev.is_first && ev.kategorie === 'training') label = `🎉 Erstes Training — ${label}`;
if (ev.typ === 'geburtstag') label = `🎂 ${label}`;
const dotSize = big ? '18px' : '12px';
const dotBorder = big ? `3px solid ${kat.color}` : `2px solid ${kat.color}`;
const dotML = big ? '6px' : '9px';
html += `
${big && ev.foto_url ? `
` : ''}
${UI.escape(kat.label)}
${_fmtDate(ev.datum)}
${label}
${ev.distanz_km ? `
${ev.distanz_km} km
` : ''}
`;
}
html += '
';
html += `
`;
wrap.innerHTML = html;
}
// ----------------------------------------------------------
// RASSEN-COMMUNITY-CHIP
// ----------------------------------------------------------
async function _loadSameBreedChip() {
const el = document.getElementById('dp-same-breed-chip');
if (!el) return;
try {
const data = await API.get('friends/same-breed');
if (!data || data.count === 0) return;
const hauptRasse = data.rassen[0]?.rasse || '';
const label = data.count === 1
? `1 anderer ${UI.escape(hauptRasse)}-Halter in der App`
: `${data.count} andere ${UI.escape(hauptRasse)}-Halter in der App`;
el.innerHTML = `
`;
document.getElementById('dp-breed-chip-btn')?.addEventListener('click', () => {
App.navigate('forum', false, { search: hauptRasse });
});
} catch {}
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
// ----------------------------------------------------------
// GEDENKEN-FLOW
// ----------------------------------------------------------
async function _openGedenkenFlow(dog) {
// Schritt 1: Würdevoller Übergangsdialog mit Datum-Eingabe
UI.modal.open({
title: `Abschied von ${dog.name}`,
body: `
${dog.name} hinterlässt eine riesige Lücke.
Die gemeinsamen Erinnerungen bleiben für immer.
Datum des Abschieds
`,
footer: `
Gedenkseite erstellen
Abbrechen
`,
});
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: `
` });
let data;
try { data = await API.get(`/dogs/${dogId}/gedenkseite`); }
catch { UI.modal.close(); return; }
const d = data;
const av = d.dog.foto_url
? ` `
: `
`;
const photoGrid = d.photos.length ? `
${d.photos.map(url => `
`).join('')}
` : '';
const statsHtml = `
${d.km_total ? `
${d.km_total}
km zusammen
` : ''}
${d.diary_count ? `
${d.diary_count}
Tagebucheinträge
` : ''}
${d.media_count ? `
` : ''}
${d.gemeinsam_tage ? `
${d.gemeinsam_tage}
gemeinsame Tage
` : ''}
`;
// Trauer-Support-Texte
const supportHtml = `
Für dich in dieser Zeit
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.
Sprich mit Freunden oder der Familie über ${d.dog.name} — Geschichten lebendig halten hilft.
Das Tagebuch bleibt erhalten — es ist ein kostbares Stück gemeinsamer Geschichte.
Professionelle Hilfe bei Tiertrauer: Tiertrauer-Hotline 0800 111 0 111 (kostenlos)
Persönlichen Abschiedstext erstellen
`;
const modal = UI.modal.open({
title: `🌈 Erinnerungen an ${UI.escape(d.dog.name)}`,
body: `
${av}
${UI.escape(d.dog.name)}
${d.dog.rasse ? `
${UI.escape(d.dog.rasse)}
` : ''}
${d.dog.verstorben_am ? `
Über die Regenbogenbrücke am ${new Date(d.dog.verstorben_am).toLocaleDateString('de-DE')}
` : ''}
${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 = `
"${UI.escape(result.text)}"
`;
});
});
}
return { init, refresh, onDogChange, addNew: _openCreateModal };
})();