/* ============================================================ 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: ``, }); _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 ? `
${_esc(dog.name)}
` : `
${UI.icon('dog')}
`}

${_esc(dog.name)}

${dog.rasse ? `

${_esc(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 ? `
${_esc(dog.chip_nr)}
` : `
nicht eingetragen
` }
${dog.bio ? `

"${_esc(dog.bio)}"

` : ''}
${dog.is_public ? `
NFC-Link
banyaro.app/hund/${dog.id}

Dieser Link kann auf ein NFC-Tag gebrannt werden

` : ''}
${!dog.is_guest ? `` : ''} ${!dog.is_guest ? `` : ''} ${!dog.is_guest ? `` : ''} ${!dog.is_guest ? `` : ''} ${!dog.is_guest && App.hasPro(_appState.user) ? `` : ''} ${!dog.is_guest ? `` : ''} ${!dog.is_guest ? `` : ''} ${!dog.is_guest ? `` : ''}
${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 ` ${_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=>`
  1. ${_esc(s)}
  2. `).join('')}
${t.tipp ? `
💜 ${_esc(t.tipp)}
` : ''}
` : ''}
` : ''}
`; 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 => ``).join('') : ''; 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
` : `

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: `
`, footer: `
`, }); 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 ? `
` : ''}
`; const footer = `
${hasPhoto ? `` : ''}
${hasPhoto ? `` : ''}
`; 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); // localStorage + SW-Cache invalidieren const userId2 = _appState.user?.id || 'anon'; localStorage.removeItem(`w3_bg3_${userId2}_` + new Date().toISOString().slice(0, 10)); 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 ? `` : `
🐾
`}
${_esc(dog.name)}
${metaLine ? `
${_esc(metaLine)}
` : ''} ${wohnort ? `
📍 ${_esc(wohnort)}
` : ''}
${ownerName ? `
Besitzer
${_esc(ownerName)}
` : ''}
banyaro.app
QR-Code
Profil öffnen
`; UI.modal.open({ title: 'Visitenkarte', body: `
${cardHtml}

QR-Code auf NFC-Tag oder Anhänger kleben — jeder kann das Profil von ${_esc(dog.name)} sofort öffnen.

`, footer: ` `, }); // 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: `${_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.

`, footer: ` `, }); _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: `
`, }); _bindForm(null, true); } // ---------------------------------------------------------- // BEARBEITEN (Modal) // ---------------------------------------------------------- function _openEditModal(dog) { UI.modal.open({ title: `${dog.name} bearbeiten`, body: _formHTML(dog, true), footer: `
`, }); _bindForm(dog, true); } // ---------------------------------------------------------- // FORMULAR HTML // ---------------------------------------------------------- function _formHTML(dog, inModal = false) { const today = new Date().toISOString().slice(0, 10); return `
Foto hochladen um die Rasse per KI zu erkennen
${!inModal ? `
${dog ? `` : ''}
` : ''}
`; } // ---------------------------------------------------------- // 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 => `