/* ============================================================
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')}
${_esc(dog.name)}
${dog.rasse
? `
${_esc(dog.rasse)}
`
: `
`}
${(dog.hdm_wins?.length) ? `
${dog.hdm_wins.map(m => {
const [y, mo] = m.split('-');
const label = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' })
.format(new Date(+y, +mo - 1, 1));
return `🏆 ${label} `;
}).join('')}
` : `
`}
${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
` : ''}
Transponder
${dog.chip_nr
? `
${_esc(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
` : ''}
Ausweis
${!dog.is_guest ? `
Teilen
` : ''}
${!dog.is_guest ? `
+ Weiteren Hund anlegen
` : ''}
${dog.user_id === _appState.user?.id ? `
Sitter-Zugang
Gib einem Freund temporären Schreibzugang für diesen Hund
Lade…
` : ''}
`;
// Foto-Editor öffnen
document.getElementById('dp-photo-edit-btn')?.addEventListener('click', () => {
_showPhotoEditor(dog);
});
// Skills laden
_loadSkills(dog);
// Pflegetipps laden
_loadPflegeTipps(dog);
// 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-ausweis-btn')?.addEventListener('click', () => {
_showAusweisModal(dog.id);
});
document.getElementById('dp-share-btn')?.addEventListener('click', () => {
_showShareModal(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 `
${_esc(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 ${_esc(data.rasse_name)}` : ''}
${t ? `
${t.saisonal_aktuell ? '🌸 Aktuell & Saisonal' : '💡 Tipp des Tages'}
${kat_icons[t.kategorie]||_ph('paw-print')} ${_esc(t.titel)}
${_esc(t.beschreibung||'')}
${t.haeufigkeit ? `
🔄 ${_esc(t.haeufigkeit)}
` : ''}
${t.materialien ? `
🛒 ${_esc(t.materialien)}
` : ''}
${t.schritte?.length ? `
Anleitung anzeigen
${t.schritte.map(s=>`${_esc(s)} `).join('')}
${t.tipp ? `💜 ${_esc(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')} ${_esc(kat)}${katBadge}
${katTipps.map(tip => `
${_esc(tip.titel)}
${tip.saisonal_aktuell ? '● Aktuell ' : ''}
${_esc(tip.beschreibung||'')}
${tip.haeufigkeit ? `🔄 ${_esc(tip.haeufigkeit)}
` : ''}
${tip.schritte?.length ? `
${tip.schritte.map(s=>`${_esc(s)} `).join('')}
` : ''}
${tip.tipp ? `💜 ${_esc(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`;
}
});
}
function _esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
// ----------------------------------------------------------
// 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 => `
${_esc(s.sitter_name)}
· bis ${_esc(s.valid_until)}
`).join('');
}
const friendOptions = friends.length
? friends.map(f => `${_esc(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 {
const fd = new FormData();
fd.append('file', file);
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);
UI.modal.close();
App.renderDogSwitcher();
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) {
UI.modal.open({
title: 'Heimtierausweis',
body: ``,
footer: `Schließen
${UI.icon('printer')} Drucken `,
size: 'fullscreen',
});
}
// ----------------------------------------------------------
// TEILEN
// ----------------------------------------------------------
async function _showShareModal(dog) {
UI.modal.open({
title: `${_esc(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
? `${_esc(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
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);
});
}
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 = 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,
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, '&').replace(//g, '>')
.replace(/"/g, '"');
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
return { init, refresh, onDogChange, addNew: _openCreateModal };
})();