2426 lines
106 KiB
JavaScript
2426 lines
106 KiB
JavaScript
/* ============================================================
|
||
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 : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>',
|
||
title : 'Anmelden erforderlich',
|
||
text : 'Melde dich an, um ein Hundeprofil anzulegen.',
|
||
action: `<button class="btn btn-primary" id="profile-goto-login">Anmelden</button>`,
|
||
});
|
||
_container.querySelector('#profile-goto-login')
|
||
?.addEventListener('click', () => App.navigate('settings'));
|
||
return;
|
||
}
|
||
|
||
if (!_appState.activeDog) {
|
||
_renderCreateForm();
|
||
} else {
|
||
_renderProfile(_appState.activeDog);
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PROFIL-ANSICHT
|
||
// ----------------------------------------------------------
|
||
function _renderProfile(dog) {
|
||
const geburtstag = dog.geburtstag
|
||
? new Date(dog.geburtstag + 'T00:00:00')
|
||
.toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })
|
||
: null;
|
||
|
||
_container.innerHTML = `
|
||
<div style="text-align:center;padding:var(--space-6) var(--space-2) var(--space-4)">
|
||
|
||
<!-- Profilfoto mit Upload-Button -->
|
||
<div style="position:relative;display:inline-block;margin-bottom:var(--space-4);padding:4px">
|
||
${dog.foto_url
|
||
? `<div class="dp-avatar-ring">
|
||
<img src="${dog.foto_url}" alt="${_esc(dog.name)}" class="dp-avatar-img"
|
||
style="transform:scale(${dog.foto_zoom||1}) translate(${dog.foto_offset_x||0}%,${dog.foto_offset_y||0}%)">
|
||
</div>`
|
||
: `<div class="dp-avatar-ring dp-avatar-empty">${UI.icon('dog')}</div>`}
|
||
<button class="dp-avatar-edit-btn" id="dp-photo-edit-btn">
|
||
${UI.icon('pencil-simple')}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Name + Rasse -->
|
||
<h2 style="font-size:var(--text-2xl);font-weight:700;
|
||
color:var(--c-text);margin:0 0 var(--space-1)">${_esc(dog.name)}</h2>
|
||
${dog.rasse
|
||
? `<p style="color:var(--c-text-secondary);margin:0 0 var(--space-2)">${_esc(dog.rasse)}</p>`
|
||
: `<p style="margin:0 0 var(--space-2)"></p>`}
|
||
|
||
<!-- Rassen-Community-Chip (wird async geladen) -->
|
||
<div id="dp-same-breed-chip" style="margin-bottom:var(--space-4)"></div>
|
||
|
||
<!-- Info-Grid -->
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);
|
||
margin-bottom:var(--space-5);text-align:left">
|
||
${geburtstag ? `
|
||
<div class="card" style="padding:var(--space-3)">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-dots"></use></svg> Geburtstag</div>
|
||
<div style="font-weight:500;font-size:var(--text-sm)">${geburtstag}</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
${_calcAlter(dog.geburtstag)}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
${dog.geschlecht ? `
|
||
<div class="card" style="padding:var(--space-3)">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||
margin-bottom:2px">${dog.geschlecht === 'm' ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-male"></use></svg>' : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-female"></use></svg>'} Geschlecht</div>
|
||
<div style="font-weight:500;font-size:var(--text-sm)">
|
||
${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
${dog.gewicht_kg ? `
|
||
<div class="card" style="padding:var(--space-3);cursor:pointer" data-action="goto-weight">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg> Gewicht</div>
|
||
<div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div>
|
||
</div>
|
||
` : ''}
|
||
<div class="card" style="padding:var(--space-3)">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||
margin-bottom:2px">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#wave-sine"></use></svg> Transponder
|
||
</div>
|
||
${dog.chip_nr
|
||
? `<div style="font-size:var(--text-xs);font-weight:500;word-break:break-all">${_esc(dog.chip_nr)}</div>`
|
||
: `<div style="font-size:var(--text-xs);color:var(--c-text-muted)">nicht eingetragen
|
||
<button class="btn btn-link btn-sm" id="dp-chip-edit-btn"
|
||
style="padding:0 0 0 var(--space-1);font-size:var(--text-xs)">Eintragen</button>
|
||
</div>`
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
${dog.bio ? `
|
||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-5);text-align:left">
|
||
<p style="margin:0;color:var(--c-text-secondary);font-style:italic;line-height:1.6">
|
||
"${_esc(dog.bio)}"
|
||
</p>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div id="dp-skills" style="margin-bottom:var(--space-5);text-align:left"></div>
|
||
<div id="dp-pflege" style="margin-bottom:var(--space-5);text-align:left"></div>
|
||
|
||
${dog.is_public ? `
|
||
<div style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
|
||
border-radius:var(--radius-md);padding:var(--space-4);
|
||
margin-bottom:var(--space-5);text-align:left">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||
margin-bottom:var(--space-2);font-weight:var(--weight-medium)">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#tag"></use></svg> NFC-Link
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);
|
||
flex-wrap:wrap">
|
||
<code id="dp-nfc-link"
|
||
style="flex:1;font-size:var(--text-sm);background:var(--c-surface);
|
||
border:1px solid var(--c-border);border-radius:var(--radius-sm);
|
||
padding:var(--space-2) var(--space-3);color:var(--c-text);
|
||
word-break:break-all">banyaro.app/hund/${dog.id}</code>
|
||
<button class="btn btn-secondary btn-sm" id="dp-copy-link-btn"
|
||
style="flex-shrink:0;padding:var(--space-2) var(--space-3);
|
||
font-size:var(--text-sm);min-height:36px">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg> Kopieren
|
||
</button>
|
||
</div>
|
||
<p style="margin:var(--space-2) 0 0;font-size:var(--text-xs);
|
||
color:var(--c-text-muted)">
|
||
Dieser Link kann auf ein NFC-Tag gebrannt werden
|
||
</p>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
|
||
${!dog.is_guest ? `<button class="btn btn-primary w-full" id="dp-edit-btn">
|
||
Profil bearbeiten
|
||
</button>` : ''}
|
||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-share-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#share-network"></use></svg>
|
||
Teilen
|
||
</button>` : ''}
|
||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-passport-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#notebook"></use></svg>
|
||
Hundepass
|
||
</button>` : ''}
|
||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-vcard-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#identification-card"></use></svg>
|
||
Visitenkarte teilen
|
||
</button>` : ''}
|
||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-add-dog-btn">
|
||
+ Weiteren Hund anlegen
|
||
</button>` : ''}
|
||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-wrapped-btn"
|
||
style="background:linear-gradient(135deg,#1a1a2e,#16213e);color:#e8c96e;
|
||
border-color:transparent;font-weight:700">
|
||
✨ Jahresrückblick ${new Date().getFullYear()}
|
||
</button>` : ''}
|
||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-buch-btn"
|
||
style="background:linear-gradient(135deg,#5c3a10,#7a4f1a);color:#f5e4c0;
|
||
border-color:transparent;font-weight:700">
|
||
📖 Hunde-Buch erstellen
|
||
</button>` : ''}
|
||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-timeline-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#timeline"></use></svg>
|
||
Lebens-Timeline 🐾
|
||
</button>` : ''}
|
||
</div>
|
||
|
||
</div>
|
||
|
||
${dog.user_id === _appState.user?.id ? `
|
||
<div class="card" style="margin-bottom:var(--space-5)">
|
||
<div style="padding:var(--space-4);border-bottom:1px solid var(--c-border)">
|
||
<div style="font-weight:600">Sitter-Zugang</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
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.
|
||
</div>
|
||
</div>
|
||
<div id="dp-sitting-access" style="padding:var(--space-4)">Lade…</div>
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
|
||
// 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 `<span style="
|
||
display:inline-flex;align-items:center;gap:4px;
|
||
padding:3px 10px;border-radius:var(--radius-full,999px);
|
||
font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
background:${isGreen ? '#f0fdf4' : '#fff7ed'};
|
||
color:${isGreen ? '#15803d' : '#c2410c'};
|
||
border:1px solid ${isGreen ? '#86efac' : '#fdba74'}">
|
||
<svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#${isGreen ? 'check' : 'fire'}"></use>
|
||
</svg>
|
||
${_esc(skill.exercise_name)}
|
||
</span>`;
|
||
};
|
||
|
||
const sitztBlock = sitzt.length ? `
|
||
<div style="margin-bottom:var(--space-3)">
|
||
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);margin-bottom:var(--space-2);
|
||
text-transform:uppercase;letter-spacing:.04em">Sitzt</div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2)">
|
||
${sitzt.map(s => badge(s, 'sitzt')).join('')}
|
||
</div>
|
||
</div>` : '';
|
||
|
||
const meistensBlock = meistens.length ? `
|
||
<div>
|
||
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);margin-bottom:var(--space-2);
|
||
text-transform:uppercase;letter-spacing:.04em">Übt noch</div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2)">
|
||
${meistens.map(s => badge(s, 'meistens')).join('')}
|
||
</div>
|
||
</div>` : '';
|
||
|
||
el.innerHTML = `
|
||
<div class="card" style="padding:var(--space-4)">
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#list-checks"></use>
|
||
</svg>
|
||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text)">
|
||
Kommandos & Fähigkeiten
|
||
</span>
|
||
</div>
|
||
${sitztBlock}
|
||
${meistensBlock}
|
||
</div>`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 => `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${n}"></use></svg>`;
|
||
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'
|
||
? `<span title="Dieses Fell wächst kontinuierlich und wird mit der Schere geschnitten"
|
||
style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px;
|
||
background:#dbeafe;color:#1d4ed8;margin-left:6px">✂️ Schneiden</span>`
|
||
: data.fell_pflege_art === 'trimmen'
|
||
? `<span title="Dieses Fell hat natürliche Wachstumsbegrenzung und wird durch Hand-Stripping gepflegt"
|
||
style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px;
|
||
background:#fef9c3;color:#92400e;margin-left:6px">✋ Trimmen</span>`
|
||
: '';
|
||
|
||
el.innerHTML = `
|
||
<div class="card" style="padding:var(--space-4)">
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||
<span style="font-size:1.1em">🛁</span>
|
||
<span style="font-size:var(--text-sm);font-weight:600">
|
||
Pflegetipps${data.rasse_name ? ` für ${_esc(data.rasse_name)}` : ''}
|
||
</span>
|
||
</div>
|
||
|
||
${t ? `
|
||
<!-- Tipp des Tages -->
|
||
<div style="background:var(--c-surface-2);border-radius:10px;padding:12px;
|
||
margin-bottom:var(--space-3);border-left:3px solid #a78bfa">
|
||
<div style="font-size:10px;font-weight:700;color:#a78bfa;text-transform:uppercase;
|
||
letter-spacing:.5px;margin-bottom:4px">
|
||
${t.saisonal_aktuell ? '🌸 Aktuell & Saisonal' : '💡 Tipp des Tages'}
|
||
</div>
|
||
<div style="font-weight:600;font-size:var(--text-sm);margin-bottom:4px">
|
||
${kat_icons[t.kategorie]||_ph('paw-print')} ${_esc(t.titel)}
|
||
</div>
|
||
<div style="font-size:12px;color:var(--c-text-secondary);margin-bottom:8px;
|
||
line-height:1.5">${_esc(t.beschreibung||'')}</div>
|
||
${t.haeufigkeit ? `<div style="font-size:11px;color:var(--c-text-muted)">
|
||
🔄 ${_esc(t.haeufigkeit)}</div>` : ''}
|
||
${t.materialien ? `<div style="font-size:11px;color:var(--c-text-muted)">
|
||
🛒 ${_esc(t.materialien)}</div>` : ''}
|
||
${t.schritte?.length ? `
|
||
<details style="margin-top:8px">
|
||
<summary style="font-size:12px;cursor:pointer;color:var(--c-primary);
|
||
font-weight:600">Anleitung anzeigen</summary>
|
||
<ol style="margin:8px 0 0 16px;padding:0;font-size:12px;
|
||
color:var(--c-text);line-height:1.6">
|
||
${t.schritte.map(s=>`<li style="margin-bottom:3px">${_esc(s)}</li>`).join('')}
|
||
</ol>
|
||
${t.tipp ? `<div style="margin-top:8px;font-size:11px;color:#a78bfa;
|
||
font-style:italic">💜 ${_esc(t.tipp)}</div>` : ''}
|
||
</details>` : ''}
|
||
</div>` : ''}
|
||
|
||
<!-- Alle Tipps Button -->
|
||
<button id="dp-pflege-alle" class="btn btn-secondary btn-sm"
|
||
style="width:100%;font-size:12px">
|
||
Alle ${data.tipps.length} Pflegetipps anzeigen
|
||
</button>
|
||
<div id="dp-pflege-liste" style="display:none;margin-top:var(--space-3)">
|
||
${data.kategorien.map(kat => {
|
||
const katTipps = data.tipps.filter(t=>t.kategorie===kat);
|
||
const katBadge = kat === 'Fell' ? pflegeArtBadge : '';
|
||
return `
|
||
<div style="margin-bottom:var(--space-3)">
|
||
<div style="font-size:11px;font-weight:700;color:var(--c-text-muted);
|
||
text-transform:uppercase;margin-bottom:8px;display:flex;align-items:center">
|
||
${kat_icons[kat]||_ph('paw-print')} ${_esc(kat)}${katBadge}</div>
|
||
${katTipps.map(tip => `
|
||
<details style="background:var(--c-surface-2);border-radius:8px;
|
||
padding:10px;margin-bottom:6px">
|
||
<summary style="font-size:var(--text-sm);font-weight:600;cursor:pointer;
|
||
list-style:none;display:flex;justify-content:space-between;
|
||
align-items:center">
|
||
${_esc(tip.titel)}
|
||
${tip.saisonal_aktuell ? '<span style="font-size:10px;color:#10b981">● Aktuell</span>' : ''}
|
||
</summary>
|
||
<div style="margin-top:8px;font-size:12px;color:var(--c-text-secondary);
|
||
line-height:1.5">${_esc(tip.beschreibung||'')}</div>
|
||
${tip.haeufigkeit ? `<div style="font-size:11px;color:var(--c-text-muted);
|
||
margin-top:4px">🔄 ${_esc(tip.haeufigkeit)}</div>` : ''}
|
||
${tip.schritte?.length ? `
|
||
<ol style="margin:8px 0 0 16px;padding:0;font-size:12px;line-height:1.6">
|
||
${tip.schritte.map(s=>`<li style="margin-bottom:3px">${_esc(s)}</li>`).join('')}
|
||
</ol>` : ''}
|
||
${tip.tipp ? `<div style="margin-top:6px;font-size:11px;color:#a78bfa;
|
||
font-style:italic">💜 ${_esc(tip.tipp)}</div>` : ''}
|
||
</details>`).join('')}
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
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,'>').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 => `
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);
|
||
padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
|
||
border-radius:var(--radius-md);margin-bottom:var(--space-2)">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg>
|
||
<div style="flex:1;font-size:var(--text-sm)">
|
||
<strong>${_esc(s.sitter_name)}</strong>
|
||
<span style="color:var(--c-text-muted)"> · bis ${_esc(s.valid_until)}</span>
|
||
</div>
|
||
<button class="btn btn-link btn-sm sa-revoke-btn" data-sub-id="${s.id}"
|
||
style="color:var(--c-danger);padding:0">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||
</button>
|
||
</div>`).join('');
|
||
}
|
||
|
||
const friendOptions = friends.length
|
||
? friends.map(f => `<option value="${f.friend_id}">${_esc(f.friend_name)}</option>`).join('')
|
||
: '<option value="" disabled>Keine Freunde vorhanden</option>';
|
||
|
||
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 ? `
|
||
<div style="margin-top:var(--space-3)">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
|
||
margin-bottom:var(--space-2);font-weight:600">Zugang gewähren</div>
|
||
<div style="display:grid;grid-template-columns:1fr auto;gap:var(--space-2);
|
||
align-items:end">
|
||
<div class="form-group" style="margin:0">
|
||
<label class="form-label" style="font-size:var(--text-xs)">Freund</label>
|
||
<select class="form-control form-control-sm" id="sa-friend-select">
|
||
<option value="">Freund wählen…</option>
|
||
${friendOptions}
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="margin:0">
|
||
<label class="form-label" style="font-size:var(--text-xs)">Gültig bis</label>
|
||
<input class="form-control form-control-sm" type="date" id="sa-until-input"
|
||
value="${defaultUntil}" min="${today}">
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm w-full" id="sa-grant-btn"
|
||
style="margin-top:var(--space-2)">
|
||
Zugang gewähren
|
||
</button>
|
||
</div>
|
||
` : `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0">
|
||
Füge zuerst Freunde hinzu, um ihnen Zugang zu gewähren.
|
||
</p>`}
|
||
`;
|
||
|
||
// 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 = '<p style="color:var(--c-danger);font-size:var(--text-sm)">Fehler beim Laden.</p>';
|
||
}
|
||
}
|
||
|
||
function _showChipEdit(dog) {
|
||
UI.modal.open({
|
||
title: 'Transpondernummer',
|
||
body: `
|
||
<div class="mb-3">
|
||
<label class="form-label">Chip-Nummer (15-stellig)</label>
|
||
<input id="chip-edit-input" class="form-control" type="text"
|
||
value="${_esc(dog.chip_nr || '')}" placeholder="z.B. 276009200123456" maxlength="20">
|
||
</div>`,
|
||
footer: `
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
|
||
<button class="btn btn-primary" id="chip-edit-save-btn" style="width:100%">Speichern</button>
|
||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||
</div>`,
|
||
});
|
||
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 = `
|
||
<div class="photo-editor">
|
||
<div class="photo-editor-preview" id="pe-preview">
|
||
${hasPhoto
|
||
? `<img src="${UI.escape(dog.foto_url)}" id="pe-img" draggable="false"
|
||
oncontextmenu="return false"
|
||
style="transform:scale(${zoom}) translate(${ox}%,${oy}%)">`
|
||
: `<div class="photo-editor-empty">${UI.icon('dog')}</div>`}
|
||
</div>
|
||
${hasPhoto ? `
|
||
<div class="photo-editor-controls">
|
||
<label class="form-label">Zoom</label>
|
||
<input type="range" id="pe-zoom" min="1" max="3" step="0.05" value="${zoom}"
|
||
style="width:100%">
|
||
</div>
|
||
` : ''}
|
||
<label class="btn btn-secondary" style="cursor:pointer">
|
||
${UI.icon('upload-simple')} Neues Foto wählen
|
||
<input type="file" id="pe-file-input" accept="image/*" style="display:none">
|
||
</label>
|
||
</div>
|
||
`;
|
||
|
||
const footer = `
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
|
||
${hasPhoto ? `<button class="btn btn-primary" id="pe-save-btn" style="width:100%">Speichern</button>` : ''}
|
||
<div style="display:flex;gap:var(--space-2)">
|
||
${hasPhoto ? `<button class="btn btn-danger" id="pe-delete-btn">${UI.icon('trash')} Löschen</button>` : ''}
|
||
<button class="btn btn-secondary flex-1" onclick="UI.modal.close()">Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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) {
|
||
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 = `
|
||
<div id="dp-vcard-canvas" style="
|
||
background:linear-gradient(135deg,#0f1a2b 0%,#1a2a45 60%,#0d2137 100%);
|
||
border-radius:20px;padding:24px 20px;color:white;
|
||
font-family:system-ui,-apple-system,sans-serif;
|
||
position:relative;overflow:hidden;max-width:340px;margin:0 auto;
|
||
box-shadow:0 8px 32px rgba(0,0,0,0.4)">
|
||
|
||
<!-- Deko-Kreis -->
|
||
<div style="position:absolute;top:-30px;right:-30px;width:120px;height:120px;
|
||
border-radius:50%;background:rgba(196,132,58,0.12);pointer-events:none"></div>
|
||
<div style="position:absolute;bottom:-40px;left:-20px;width:100px;height:100px;
|
||
border-radius:50%;background:rgba(196,132,58,0.07);pointer-events:none"></div>
|
||
|
||
<!-- Header -->
|
||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px">
|
||
${dog.foto_url
|
||
? `<img src="${_esc(dog.foto_url)}" style="width:52px;height:52px;border-radius:50%;object-fit:cover;
|
||
border:2px solid rgba(196,132,58,0.6);flex-shrink:0">`
|
||
: `<div style="width:52px;height:52px;border-radius:50%;background:rgba(196,132,58,0.2);
|
||
display:flex;align-items:center;justify-content:center;font-size:1.6rem;
|
||
flex-shrink:0;border:2px solid rgba(196,132,58,0.4)">🐾</div>`}
|
||
<div>
|
||
<div style="font-size:1.25rem;font-weight:800;color:#fff;line-height:1.2">${_esc(dog.name)}</div>
|
||
${metaLine ? `<div style="font-size:0.8rem;color:rgba(255,255,255,0.6);margin-top:2px">${_esc(metaLine)}</div>` : ''}
|
||
${wohnort ? `<div style="font-size:0.75rem;color:rgba(196,132,58,0.9);margin-top:3px">📍 ${_esc(wohnort)}</div>` : ''}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Divider -->
|
||
<div style="height:1px;background:rgba(255,255,255,0.1);margin-bottom:16px"></div>
|
||
|
||
<!-- Owner + QR -->
|
||
<div style="display:flex;align-items:flex-end;justify-content:space-between;gap:12px">
|
||
<div style="flex:1;min-width:0">
|
||
${ownerName ? `<div style="font-size:0.7rem;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Besitzer</div>
|
||
<div style="font-size:0.9rem;font-weight:600;color:rgba(255,255,255,0.85)">${_esc(ownerName)}</div>` : ''}
|
||
<div style="font-size:0.65rem;color:rgba(255,255,255,0.35);margin-top:8px">banyaro.app</div>
|
||
</div>
|
||
<div style="flex-shrink:0;text-align:center">
|
||
<img id="dp-vcard-qr" src="${_esc(qrUrl)}"
|
||
style="width:80px;height:80px;border-radius:10px;display:block"
|
||
alt="QR-Code">
|
||
<div style="font-size:0.6rem;color:rgba(255,255,255,0.35);margin-top:4px">Profil öffnen</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
UI.modal.open({
|
||
title: 'Visitenkarte',
|
||
body: `
|
||
<div style="margin-bottom:var(--space-4)">${cardHtml}</div>
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);text-align:center;margin-bottom:0">
|
||
QR-Code auf NFC-Tag oder Anhänger kleben — jeder kann das Profil von ${_esc(dog.name)} sofort öffnen.
|
||
</p>
|
||
`,
|
||
footer: `
|
||
<button class="btn btn-secondary" id="dp-vcard-copy-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#link"></use></svg>
|
||
Link kopieren
|
||
</button>
|
||
<button class="btn btn-primary" id="dp-vcard-share-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#share-network"></use></svg>
|
||
Teilen
|
||
</button>
|
||
`,
|
||
});
|
||
|
||
// 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: `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
||
Erstelle einen Einladungslink, den du per WhatsApp, Signal oder E-Mail teilen kannst.
|
||
Die eingeladene Person sieht Tagebuch und Gesundheitsakte nach dem Annehmen.
|
||
</p>
|
||
<div class="form-group">
|
||
<label class="form-label">Berechtigung</label>
|
||
<select class="form-control" id="share-role-select">
|
||
<option value="editor">Mitschreiben (Tagebuch & Gesundheit bearbeiten)</option>
|
||
<option value="viewer">Nur lesen</option>
|
||
</select>
|
||
</div>
|
||
<div id="share-link-result" style="display:none;margin-top:var(--space-4)">
|
||
<label class="form-label">Einladungslink</label>
|
||
<div style="display:flex;gap:var(--space-2);align-items:center">
|
||
<input class="form-control" id="share-link-input" type="text" readonly
|
||
style="font-size:var(--text-xs)">
|
||
<button class="btn btn-secondary btn-sm" id="share-link-copy">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg>
|
||
</button>
|
||
</div>
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2)">
|
||
Dieser Link kann einmalig angenommen werden.
|
||
</p>
|
||
</div>
|
||
<div id="share-list-wrap" style="margin-top:var(--space-4)"></div>`,
|
||
footer: `
|
||
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
|
||
<button class="btn btn-primary" id="share-create-btn">Link erstellen</button>`,
|
||
});
|
||
|
||
_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 = `
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">
|
||
Aktive Einladungen
|
||
</div>` +
|
||
shares.map(s => `
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);
|
||
padding:var(--space-2) var(--space-3);background:var(--c-surface);
|
||
border-radius:var(--radius-md);margin-bottom:var(--space-1)">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg>
|
||
<div style="flex:1;font-size:var(--text-sm)">
|
||
${s.shared_with_name
|
||
? `<strong>${_esc(s.shared_with_name)}</strong> · ${s.role}`
|
||
: `<em style="color:var(--c-text-muted)">Ausstehend</em> · ${s.role}`}
|
||
</div>
|
||
<button class="btn btn-link btn-sm share-revoke-btn" data-share-id="${s.id}"
|
||
style="color:var(--c-danger);padding:0">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||
</button>
|
||
</div>`).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 = `
|
||
<div style="padding:var(--space-4) 0 var(--space-2)">
|
||
<div style="text-align:center;margin-bottom:var(--space-5)">
|
||
<div style="font-size:3rem;margin-bottom:var(--space-2)">${UI.icon('dog')}</div>
|
||
<h2 style="font-size:var(--text-xl);font-weight:700;margin:0 0 var(--space-2)">
|
||
Hund anlegen
|
||
</h2>
|
||
<p style="color:var(--c-text-secondary);margin:0">
|
||
Erstelle das Profil für deinen Hund.
|
||
</p>
|
||
</div>
|
||
${_formHTML(null)}
|
||
</div>
|
||
`;
|
||
_bindForm(null, false);
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// NEUEN HUND ANLEGEN (Modal) — auch aufrufbar via addNew()
|
||
// ----------------------------------------------------------
|
||
function _openCreateModal() {
|
||
UI.modal.open({
|
||
title: 'Weiteren Hund anlegen',
|
||
body: _formHTML(null, true),
|
||
footer: `
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
|
||
<button type="submit" form="dp-form" class="btn btn-primary" style="width:100%">${UI.icon('dog')} Hund anlegen</button>
|
||
<button type="button" class="btn btn-secondary" id="dp-form-cancel">Abbrechen</button>
|
||
</div>
|
||
`,
|
||
});
|
||
_bindForm(null, true);
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// BEARBEITEN (Modal)
|
||
// ----------------------------------------------------------
|
||
function _openEditModal(dog) {
|
||
UI.modal.open({
|
||
title: `${dog.name} bearbeiten`,
|
||
body: _formHTML(dog, true),
|
||
footer: `
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
|
||
<button type="submit" form="dp-form" class="btn btn-primary" style="width:100%">Speichern</button>
|
||
<div style="display:flex;gap:var(--space-2)">
|
||
<button type="button" class="btn btn-danger" id="dp-delete-btn">Löschen</button>
|
||
<button type="button" class="btn btn-secondary flex-1" id="dp-form-cancel">Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
`,
|
||
});
|
||
_bindForm(dog, true);
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// FORMULAR HTML
|
||
// ----------------------------------------------------------
|
||
function _formHTML(dog, inModal = false) {
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
return `
|
||
<form id="dp-form" autocomplete="off">
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Name *</label>
|
||
<input class="form-control" type="text" name="name"
|
||
value="${_esc(dog?.name || '')}"
|
||
placeholder="z. B. Ban Yaro" required>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
Rasse
|
||
<span style="color:var(--c-text-secondary)">(optional)</span>
|
||
${UI.help('Verknüpfe deine Rasse mit unserem Wiki für personalisierte Pflegetipps.')}
|
||
</label>
|
||
<input class="form-control" type="text" name="rasse"
|
||
id="dp-rasse-input"
|
||
value="${_esc(dog?.rasse || '')}"
|
||
list="dp-rasse-list"
|
||
autocomplete="off"
|
||
placeholder="z. B. Mischling, Golden Retriever…">
|
||
<datalist id="dp-rasse-list"></datalist>
|
||
<input type="hidden" name="rasse_id" id="dp-rasse-id"
|
||
value="${dog?.rasse_id || ''}">
|
||
<div id="dp-rasse-match" style="display:none;margin-top:4px;font-size:11px;
|
||
color:var(--c-success);font-weight:600">
|
||
✓ Mit Wiki verknüpft — Pflegetipps werden auf diese Rasse angepasst
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Geburtstag</label>
|
||
<input class="form-control" type="date" name="geburtstag"
|
||
value="${dog?.geburtstag || ''}" max="${today}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Geschlecht</label>
|
||
<select class="form-control" name="geschlecht">
|
||
<option value="" ${!dog?.geschlecht ? 'selected' : ''}>–</option>
|
||
<option value="m" ${dog?.geschlecht === 'm' ? 'selected' : ''}>Rüde</option>
|
||
<option value="w" ${dog?.geschlecht === 'w' ? 'selected' : ''}>Hündin</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Gewicht (kg)</label>
|
||
<input class="form-control" type="number" name="gewicht_kg"
|
||
value="${dog?.gewicht_kg || ''}"
|
||
min="0.1" max="120" step="0.1" placeholder="z. B. 28.5">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
Chip-Nummer
|
||
${UI.help('Die 15-stellige Chip-Nummer findest du im Heimtierausweis oder beim Tierarzt.')}
|
||
</label>
|
||
<input class="form-control" type="text" name="chip_nr"
|
||
value="${_esc(dog?.chip_nr || '')}" placeholder="15-stellig">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
Felltyp
|
||
<span style="color:var(--c-text-secondary)">(optional)</span>
|
||
${UI.help('Der Felltyp wird für personalisierte Wetter-Hinweise genutzt.')}
|
||
</label>
|
||
<select class="form-control" name="fell_typ">
|
||
<option value="" ${!dog?.fell_typ ? 'selected' : ''}>– nicht angegeben –</option>
|
||
<option value="kurz" ${dog?.fell_typ === 'kurz' ? 'selected' : ''}>Kurzhaar (Labrador, Boxer)</option>
|
||
<option value="mittel" ${dog?.fell_typ === 'mittel' ? 'selected' : ''}>Mittellang (Spaniel, Husky)</option>
|
||
<option value="lang" ${dog?.fell_typ === 'lang' ? 'selected' : ''}>Langhaar (Collie, Berner Senne)</option>
|
||
<option value="drahtaar" ${dog?.fell_typ === 'drahtaar' ? 'selected' : ''}>Drahthaar (Terrier, Schnauzer)</option>
|
||
<option value="doppel" ${dog?.fell_typ === 'doppel' ? 'selected' : ''}>Doppeltes Unterfell (Husky, Malamute, Samojede)</option>
|
||
<option value="nackt" ${dog?.fell_typ === 'nackt' ? 'selected' : ''}>Nackthund (Chinese Crested, Xolo)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
Bio / Steckbrief
|
||
<span style="color:var(--c-text-secondary)">(optional)</span>
|
||
</label>
|
||
<textarea class="form-control" name="bio" rows="2"
|
||
placeholder="Kurze Beschreibung…">${_esc(dog?.bio || '')}</textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
|
||
<input type="checkbox" name="is_public" ${dog?.is_public ? 'checked' : ''}>
|
||
Öffentliches Profil (für NFC-Tag)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Foto</label>
|
||
<div style="display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap">
|
||
<img id="dp-form-preview"
|
||
src="${dog?.foto_url || ''}"
|
||
style="width:64px;height:64px;border-radius:50%;object-fit:cover;
|
||
background:var(--c-surface-2);border:2px solid var(--c-border);
|
||
display:${dog?.foto_url ? 'block' : 'none'}">
|
||
<label class="btn btn-secondary btn-sm" style="cursor:pointer;margin:0">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg> Foto auswählen
|
||
<input type="file" name="foto" accept="image/*" style="display:none"
|
||
id="dp-form-foto">
|
||
</label>
|
||
<button type="button" class="btn btn-secondary btn-sm" id="dp-rasse-erkennen-btn"
|
||
style="margin:0">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
||
Rasse erkennen
|
||
</button>
|
||
<input type="file" accept="image/jpeg,image/png,image/webp"
|
||
id="dp-rasse-foto-input" style="display:none">
|
||
</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:4px">
|
||
Foto hochladen um die Rasse per KI zu erkennen
|
||
</div>
|
||
</div>
|
||
|
||
${!inModal ? `
|
||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-4)">
|
||
${dog ? `<button type="button" class="btn btn-secondary flex-1"
|
||
id="dp-form-cancel">Abbrechen</button>` : ''}
|
||
<button type="submit" class="btn btn-primary flex-1">
|
||
${dog ? 'Speichern' : `${UI.icon('dog')} Hund anlegen`}
|
||
</button>
|
||
</div>` : ''}
|
||
|
||
|
||
</form>
|
||
`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 =>
|
||
`<option value="${r.name.replace(/"/g,'"')}" data-id="${r.id}">`
|
||
).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-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,
|
||
fell_typ: fd.fell_typ || null,
|
||
};
|
||
|
||
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();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#spinner"></use></svg> 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: `<div style="text-align:center;padding:var(--space-6) var(--space-2)">
|
||
<div style="font-size:3rem;margin-bottom:var(--space-3)">🐾</div>
|
||
<p style="color:var(--c-text-secondary)">
|
||
Auf diesem Foto konnte kein Hund erkannt werden.<br>
|
||
Bitte lade ein deutlicheres Foto hoch.
|
||
</p>
|
||
${data.hinweis ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-3)">${_esc(data.hinweis)}</p>` : ''}
|
||
</div>`,
|
||
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
|
||
});
|
||
return;
|
||
}
|
||
|
||
const rassen = data.rassen || [];
|
||
const cardsHtml = rassen.map((r, i) => {
|
||
const isTop = i === 0;
|
||
return `
|
||
<div class="rasse-result-card${isTop ? ' rasse-result-card--top' : ''}">
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<div class="rasse-result-name">${isTop ? '🐕 ' : ''}${_esc(r.name)}</div>
|
||
<span class="rasse-result-pct${isTop ? '' : ' rasse-result-pct--dim'}">${r.sicherheit}%</span>
|
||
</div>
|
||
<div class="rasse-result-bar-wrap">
|
||
<div class="rasse-result-bar${isTop ? '' : ' rasse-result-bar--dim'}"
|
||
style="width:${r.sicherheit}%"></div>
|
||
</div>
|
||
${r.beschreibung ? `<div class="rasse-result-desc">${_esc(r.beschreibung)}</div>` : ''}
|
||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3);flex-wrap:wrap">
|
||
${isTop ? `<button class="btn btn-primary btn-sm" data-action="uebernehmen"
|
||
data-rasse="${_esc(r.name)}" style="flex:1">
|
||
Rasse übernehmen
|
||
</button>` : `<button class="btn btn-secondary btn-sm" data-action="uebernehmen"
|
||
data-rasse="${_esc(r.name)}" style="flex:1">
|
||
Diese wählen
|
||
</button>`}
|
||
${r.wiki_slug ? `<button class="btn btn-ghost btn-sm" data-action="wiki"
|
||
data-slug="${_esc(r.wiki_slug)}">
|
||
Im Wiki
|
||
</button>` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
UI.modal.open({
|
||
title: 'Erkannte Rasse',
|
||
body: `
|
||
<div style="padding-bottom:var(--space-2)">
|
||
${data.hinweis ? `<div style="background:var(--c-surface-2);border-radius:var(--radius-md);
|
||
padding:var(--space-3);margin-bottom:var(--space-3);font-size:var(--text-sm);
|
||
color:var(--c-text-secondary)">ℹ️ ${_esc(data.hinweis)}</div>` : ''}
|
||
${cardsHtml}
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2);
|
||
text-align:center">
|
||
Noch ${data.verbleibende_anfragen} Erkennung${data.verbleibende_anfragen !== 1 ? 'en' : ''} heute verfügbar
|
||
</p>
|
||
</div>
|
||
`,
|
||
footer: `<button class="btn btn-secondary" id="dp-rasse-modal-schliessen">Schließen</button>`,
|
||
});
|
||
|
||
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`;
|
||
}
|
||
|
||
function _esc(str) {
|
||
if (!str) return '';
|
||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// HUNDEPASS
|
||
// ----------------------------------------------------------
|
||
async function _showPassportModal(dog) {
|
||
UI.modal.open({
|
||
title: `Hundepass — ${_esc(dog.name)}`,
|
||
body: `<div id="pp-body" style="min-height:200px">
|
||
<div style="text-align:center;padding:var(--space-6)">
|
||
<svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#spinner-gap"></use>
|
||
</svg>
|
||
</div>
|
||
</div>`,
|
||
footer: `
|
||
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;justify-content:flex-end">
|
||
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
|
||
<a class="btn btn-secondary" href="/ausweis/${dog.id}" target="_blank" rel="noopener">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#identification-card"></use></svg>
|
||
Ausweis öffnen
|
||
</a>
|
||
<button class="btn btn-secondary" id="pp-share-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#link"></use></svg>
|
||
Link teilen
|
||
</button>
|
||
<a class="btn btn-primary" id="pp-pdf-btn"
|
||
href="/api/passport/${dog.id}/pdf" target="_blank" download>
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-pdf"></use></svg>
|
||
PDF
|
||
</a>
|
||
</div>`,
|
||
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 = `<p style="color:var(--c-danger)">Fehler beim Laden: ${_esc(e.message)}</p>`;
|
||
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 = `
|
||
<!-- Meta: Blutgruppe, Allergien, Besonderheiten -->
|
||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-4)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;
|
||
margin-bottom:var(--space-3)">
|
||
<span style="font-weight:700;font-size:var(--text-sm)">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#info"></use></svg>
|
||
Gesundheits-Info
|
||
</span>
|
||
<button class="btn btn-secondary btn-sm" id="pp-meta-edit-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#pencil-simple"></use></svg>
|
||
Bearbeiten
|
||
</button>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">Blutgruppe</div>
|
||
<div id="pp-meta-blutgruppe" style="font-size:var(--text-sm);font-weight:500">
|
||
${_esc(meta.blutgruppe) || '<span style="color:var(--c-text-muted)">nicht eingetragen</span>'}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">Allergien</div>
|
||
<div id="pp-meta-allergien" style="font-size:var(--text-sm)">
|
||
${_esc(meta.allergien) || '<span style="color:var(--c-text-muted)">keine</span>'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
${meta.besonderheiten ? `
|
||
<div style="margin-top:var(--space-3)">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">Besonderheiten</div>
|
||
<div id="pp-meta-besonderheiten" style="font-size:var(--text-sm)">
|
||
${_esc(meta.besonderheiten)}
|
||
</div>
|
||
</div>` : ''}
|
||
</div>
|
||
|
||
<!-- Impfungen -->
|
||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-4)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;
|
||
margin-bottom:var(--space-3)">
|
||
<span style="font-weight:700;font-size:var(--text-sm)">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>
|
||
Impfungen
|
||
</span>
|
||
<button class="btn btn-primary btn-sm" id="pp-vacc-add-btn">+ Eintragen</button>
|
||
</div>
|
||
<div id="pp-vacc-list">
|
||
${vaccs.length === 0
|
||
? `<div style="text-align:center;padding:var(--space-4) var(--space-2);color:var(--c-text-muted)">
|
||
<svg class="ph-icon" style="width:32px;height:32px;margin-bottom:var(--space-2);opacity:.4" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>
|
||
<p style="font-size:var(--text-sm);margin:0">Noch keine Impfungen eingetragen.<br>Klicke auf „+ Eintragen" um loszulegen.</p>
|
||
</div>`
|
||
: vaccs.map(v => `
|
||
<div class="pp-vacc-row" data-id="${v.id}"
|
||
style="display:flex;align-items:flex-start;gap:var(--space-3);
|
||
padding:var(--space-3) 0;border-bottom:1px solid var(--c-border)">
|
||
<div style="flex:1">
|
||
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(v.krankheit)}</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
|
||
Gegeben: ${_fmt(v.datum)}
|
||
${v.naechste ? ` · Nächste: ${_fmt(v.naechste)}` : ''}
|
||
${v.tierarzt ? ` · ${_esc(v.tierarzt)}` : ''}
|
||
${v.charge_nr ? ` · Charge: ${_esc(v.charge_nr)}` : ''}
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-link btn-sm pp-vacc-del" data-id="${v.id}"
|
||
style="color:var(--c-danger);flex-shrink:0;padding:0">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||
</button>
|
||
</div>`).join('')
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Medikamente -->
|
||
<div class="card" style="padding:var(--space-4)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;
|
||
margin-bottom:var(--space-3)">
|
||
<span style="font-weight:700;font-size:var(--text-sm)">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#pill"></use></svg>
|
||
Medikamente
|
||
</span>
|
||
<button class="btn btn-primary btn-sm" id="pp-med-add-btn">+ Eintragen</button>
|
||
</div>
|
||
<div id="pp-med-list">
|
||
${meds.length === 0
|
||
? `<div style="text-align:center;padding:var(--space-4) var(--space-2);color:var(--c-text-muted)">
|
||
<svg class="ph-icon" style="width:32px;height:32px;margin-bottom:var(--space-2);opacity:.4" aria-hidden="true"><use href="/icons/phosphor.svg#pill"></use></svg>
|
||
<p style="font-size:var(--text-sm);margin:0">Noch keine Medikamente eingetragen.<br>Klicke auf „+ Eintragen" um loszulegen.</p>
|
||
</div>`
|
||
: meds.map(m => `
|
||
<div class="pp-med-row" data-id="${m.id}"
|
||
style="display:flex;align-items:flex-start;gap:var(--space-3);
|
||
padding:var(--space-3) 0;border-bottom:1px solid var(--c-border)">
|
||
<div style="flex:1">
|
||
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(m.name)}</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
|
||
${m.dosierung ? `${_esc(m.dosierung)} · ` : ''}
|
||
${m.von ? `Von ${_fmt(m.von)}` : ''}
|
||
${m.bis ? ` bis ${_fmt(m.bis)}` : m.von ? ' · dauerhaft' : ''}
|
||
${m.notiz ? ` · ${_esc(m.notiz)}` : ''}
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-link btn-sm pp-med-del" data-id="${m.id}"
|
||
style="color:var(--c-danger);flex-shrink:0;padding:0">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||
</button>
|
||
</div>`).join('')
|
||
}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 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: `
|
||
<div class="form-group">
|
||
<label class="form-label">Blutgruppe</label>
|
||
<input id="pp-meta-bg" class="form-control" type="text"
|
||
value="${_esc(current.blutgruppe || '')}" placeholder="z. B. DEA 1.1 positiv">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Allergien</label>
|
||
<textarea id="pp-meta-al" class="form-control" rows="2"
|
||
placeholder="z. B. Hühnchen, Flohspeichel">${_esc(current.allergien || '')}</textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Besonderheiten</label>
|
||
<textarea id="pp-meta-be" class="form-control" rows="2"
|
||
placeholder="z. B. Herzprobleme, Angstpatient">${_esc(current.besonderheiten || '')}</textarea>
|
||
</div>`,
|
||
footer: `
|
||
<div style="display:flex;gap:var(--space-2);justify-content:flex-end">
|
||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||
<button class="btn btn-primary" id="pp-meta-save">Speichern</button>
|
||
</div>`,
|
||
});
|
||
|
||
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: `
|
||
<div class="form-group">
|
||
<label class="form-label">Krankheit *</label>
|
||
<input id="pp-vacc-krankheit" class="form-control" type="text"
|
||
placeholder="z. B. Staupe, Parvovirose, Tollwut, DHPP" list="pp-vacc-list">
|
||
<datalist id="pp-vacc-list">
|
||
<option value="Staupe">
|
||
<option value="Parvovirose">
|
||
<option value="Hepatitis (HCC)">
|
||
<option value="Leptospirose">
|
||
<option value="Tollwut">
|
||
<option value="Kennel-Husten (Bordetella)">
|
||
<option value="Borreliose">
|
||
<option value="DHPP (Kombi)">
|
||
</datalist>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Datum *</label>
|
||
<input id="pp-vacc-datum" class="form-control" type="date" value="${today}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Nächste fällig</label>
|
||
<input id="pp-vacc-naechste" class="form-control" type="date">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Tierarzt</label>
|
||
<input id="pp-vacc-tierarzt" class="form-control" type="text" placeholder="Name der Praxis">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Charge-Nr.</label>
|
||
<input id="pp-vacc-charge" class="form-control" type="text" placeholder="Chargennummer des Impfstoffs">
|
||
</div>`,
|
||
footer: `
|
||
<div style="display:flex;gap:var(--space-2);justify-content:flex-end">
|
||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||
<button class="btn btn-primary" id="pp-vacc-save">Speichern</button>
|
||
</div>`,
|
||
});
|
||
|
||
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: `
|
||
<div class="form-group">
|
||
<label class="form-label">Medikament *</label>
|
||
<input id="pp-med-name" class="form-control" type="text"
|
||
placeholder="z. B. Frontline, Milbemax, Onsior">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Dosierung</label>
|
||
<input id="pp-med-dosierung" class="form-control" type="text"
|
||
placeholder="z. B. 1× täglich, 5 mg">
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Von</label>
|
||
<input id="pp-med-von" class="form-control" type="date" value="${today}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Bis <span style="color:var(--c-text-muted)">(leer = dauerhaft)</span></label>
|
||
<input id="pp-med-bis" class="form-control" type="date">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Notiz</label>
|
||
<input id="pp-med-notiz" class="form-control" type="text"
|
||
placeholder="z. B. nach dem Fressen geben">
|
||
</div>`,
|
||
footer: `
|
||
<div style="display:flex;gap:var(--space-2);justify-content:flex-end">
|
||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||
<button class="btn btn-primary" id="pp-med-save">Speichern</button>
|
||
</div>`,
|
||
});
|
||
|
||
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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)">
|
||
Dieser Link ist 30 Tage gültig. Tierärzte und Sitter können den Pass ohne Login öffnen.
|
||
</p>
|
||
<div style="display:flex;gap:var(--space-2);align-items:center">
|
||
<input id="pp-sharelink-input" class="form-control" type="text" readonly
|
||
value="${_esc(url)}" style="font-size:var(--text-xs)">
|
||
<button class="btn btn-secondary btn-sm" id="pp-sharelink-copy" style="flex-shrink:0">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg>
|
||
</button>
|
||
</div>
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2)">
|
||
Gültig bis: ${res.valid_until.split('-').reverse().join('.')}
|
||
</p>`;
|
||
UI.modal.open({
|
||
title: 'Hundepass-Link teilen',
|
||
body: shareWrap.innerHTML,
|
||
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
|
||
});
|
||
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 = _esc(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) =>
|
||
`<div style="min-height:320px;display:flex;flex-direction:column;
|
||
align-items:center;justify-content:center;text-align:center;
|
||
padding:32px 24px;gap:16px">${content}</div>`;
|
||
|
||
const cards = [
|
||
_card(`
|
||
<div style="font-size:3rem">🐾</div>
|
||
<div style="font-size:1.6rem;font-weight:800;color:#e8c96e;line-height:1.2">
|
||
Dein Jahr mit ${name}
|
||
</div>
|
||
<div style="font-size:1rem;color:#b8b0a0;font-weight:500">${year} in Zahlen</div>
|
||
`),
|
||
_card(`
|
||
<div style="font-size:2.5rem">👟</div>
|
||
<div style="font-size:3rem;font-weight:900;color:#e8c96e">${km} km</div>
|
||
<div style="font-size:1rem;color:#d0c8b8;font-weight:600">zusammen gelaufen</div>
|
||
${stadtpark > 0 ? `<div style="font-size:0.85rem;color:#888;margin-top:4px">= ${stadtpark}× um den Stadtpark</div>` : ''}
|
||
${konfetti ? `<div style="font-size:1.5rem;margin-top:8px">🎉 Über 100 km!</div>` : ''}
|
||
`),
|
||
_card(`
|
||
<div style="font-size:2.5rem">📔</div>
|
||
<div style="font-size:3rem;font-weight:900;color:#e8c96e">${data.eintraege_gesamt}</div>
|
||
<div style="font-size:1rem;color:#d0c8b8;font-weight:600">Tagebucheinträge</div>
|
||
${data.fotos_gesamt > 0 ? `<div style="font-size:1.1rem;color:#a0c890;font-weight:700;margin-top:4px">📷 ${data.fotos_gesamt} Fotos</div>` : ''}
|
||
${data.gassi_tage > 0 ? `<div style="font-size:0.9rem;color:#888;margin-top:4px">🐾 ${data.gassi_tage} aktive Tage</div>` : ''}
|
||
${data.lieblings_monat ? `<div style="font-size:0.85rem;color:#b89a6a;margin-top:4px">Meiste Einträge: ${_esc(data.lieblings_monat)}</div>` : ''}
|
||
${aktivitaet ? `<div style="font-size:0.85rem;color:#888">Lieblingsaktivität: ${_esc(aktivitaet)}</div>` : ''}
|
||
`),
|
||
_card(`
|
||
<div style="font-size:2rem">🌡️</div>
|
||
<div style="font-size:1.2rem;font-weight:700;color:#d0c8b8;margin-bottom:8px">Wetter-Tapferkeit</div>
|
||
<div style="display:flex;gap:32px;justify-content:center;flex-wrap:wrap">
|
||
<div><div style="font-size:2rem">❄️</div>
|
||
<div style="font-size:2rem;font-weight:900;color:#8ecfef">${data.wetter_kalt}</div>
|
||
<div style="font-size:0.8rem;color:#888">kalte Tage</div></div>
|
||
<div><div style="font-size:2rem">☀️</div>
|
||
<div style="font-size:2rem;font-weight:900;color:#f0b040">${data.wetter_warm}</div>
|
||
<div style="font-size:0.8rem;color:#888">heiße Tage</div></div>
|
||
</div>
|
||
${schneeheld ? `<div style="margin-top:12px;background:#1a3a5c;border-radius:8px;padding:6px 16px;font-size:0.9rem;font-weight:700;color:#8ecfef">❄️ Schneeheld!</div>` : ''}
|
||
${pfotalalarm ? `<div style="margin-top:12px;background:#3a2000;border-radius:8px;padding:6px 16px;font-size:0.9rem;font-weight:700;color:#f0b040">🔥 Pfoten-Alarm!</div>` : ''}
|
||
${data.training_sessions > 0 ? `<div style="margin-top:12px;font-size:0.85rem;color:#a0c890">🏋️ ${data.training_sessions} Training-Sessions</div>` : ''}
|
||
`),
|
||
_card(`
|
||
<div style="font-size:2.5rem">🐾</div>
|
||
<div style="font-size:1.3rem;font-weight:800;color:#e8c96e">Was für ein Jahr!</div>
|
||
<div style="font-size:0.95rem;color:#b8b0a0;line-height:1.5;max-width:280px">
|
||
${name} und du — ein unschlagbares Team.<br>${year} war unvergesslich.
|
||
</div>
|
||
<button id="dp-wrapped-copy-btn" style="
|
||
margin-top:12px;background:#e8c96e;color:#1a1a2e;font-weight:800;
|
||
border:none;border-radius:8px;padding:10px 20px;cursor:pointer;font-size:1rem">
|
||
📋 Text kopieren
|
||
</button>
|
||
`),
|
||
];
|
||
|
||
let currentCard = 0;
|
||
const totalCards = cards.length;
|
||
|
||
const renderDots = () => Array.from({ length: totalCards }, (_, i) =>
|
||
`<div style="width:8px;height:8px;border-radius:50%;background:${i === currentCard ? '#e8c96e' : '#444'};transition:background .3s"></div>`
|
||
).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 = `
|
||
<div style="display:flex;justify-content:flex-end;padding:max(16px,env(safe-area-inset-top)) 20px 0">
|
||
<button id="dp-wrapped-close" style="background:rgba(255,255,255,0.1);border:none;border-radius:50%;width:44px;height:44px;font-size:1.4rem;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center">×</button>
|
||
</div>
|
||
<div style="flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative">
|
||
<div id="dp-wrapped-card-container" style="width:100%;max-width:400px;color:#fff;">${cards[0]}</div>
|
||
<button id="dp-wrapped-prev" style="position:absolute;left:8px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,0.1);border:none;border-radius:50%;width:40px;height:40px;font-size:1.3rem;color:#fff;cursor:pointer;display:none;align-items:center;justify-content:center">‹</button>
|
||
<button id="dp-wrapped-next" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,0.1);border:none;border-radius:50%;width:40px;height:40px;font-size:1.3rem;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center">›</button>
|
||
</div>
|
||
<div id="dp-wrapped-dots" style="display:flex;gap:8px;justify-content:center;padding:16px 0 32px">${renderDots()}</div>
|
||
`;
|
||
|
||
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 `<button onclick="window._buchSetJahr('${y}')" style="
|
||
border:1px solid;border-radius:8px;padding:8px 16px;
|
||
font-size:0.9rem;cursor:pointer;font-family:inherit;
|
||
${active}
|
||
">${label}</button>`;
|
||
}).join('');
|
||
|
||
const togStyle = (active) =>
|
||
active
|
||
? 'background:#7a4f1a;color:#f5e4c0;border-color:#7a4f1a;'
|
||
: 'background:#f5f0e8;color:#444;border-color:#e0d4b8;';
|
||
|
||
modalEl.innerHTML = `
|
||
<div style="
|
||
background:#fff;border-radius:16px;padding:32px 24px;
|
||
max-width:420px;width:100%;box-shadow:0 8px 40px rgba(0,0,0,0.2);
|
||
font-family:system-ui,sans-serif;
|
||
">
|
||
<div style="font-size:1.4rem;font-weight:700;margin-bottom:4px">📖 Hunde-Buch erstellen</div>
|
||
<div style="font-size:0.9rem;color:#888;margin-bottom:24px">
|
||
Eine druckbare Ansicht der schönsten Einträge.<br>Im Browser als PDF speichern.
|
||
</div>
|
||
|
||
<div style="margin-bottom:20px">
|
||
<div style="font-weight:600;margin-bottom:10px;color:#555;font-size:0.85rem;text-transform:uppercase;letter-spacing:.05em">Jahrgang</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">${yearBtns}</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom:20px;display:flex;flex-direction:column;gap:10px">
|
||
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
|
||
<button onclick="window._buchToggleFotos()" style="
|
||
width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;
|
||
${togStyle(nurFotos)}
|
||
">${nurFotos ? '✓' : ''}</button>
|
||
<span style="font-size:0.95rem">Nur Einträge mit Fotos</span>
|
||
</label>
|
||
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
|
||
<button onclick="window._buchToggleMeilensteine()" style="
|
||
width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;
|
||
${togStyle(nurMeilensteine)}
|
||
">${nurMeilensteine ? '✓' : ''}</button>
|
||
<span style="font-size:0.95rem">Nur Meilensteine</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:10px">
|
||
<button onclick="window._buchOpen()" style="
|
||
flex:1;background:#7a4f1a;color:#f5e4c0;border:none;border-radius:10px;
|
||
padding:14px;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit;
|
||
">📖 Buch öffnen</button>
|
||
<button onclick="window._buchClose()" style="
|
||
background:#f0f0f0;color:#555;border:none;border-radius:10px;
|
||
padding:14px 18px;font-size:1rem;cursor:pointer;font-family:inherit;
|
||
">✕</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
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 — ${_esc(dog.name)}`,
|
||
body: `<div id="dp-timeline-body" style="min-height:200px;text-align:center;padding:var(--space-6)">
|
||
<svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#spinner-gap"></use>
|
||
</svg>
|
||
</div>`,
|
||
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
|
||
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 = `<p style="color:var(--c-danger)">Fehler: ${_esc(e.message)}</p>`;
|
||
return;
|
||
}
|
||
|
||
const wrap = document.getElementById('dp-timeline-body');
|
||
if (!wrap) return;
|
||
|
||
const events = data.events || [];
|
||
if (!events.length) {
|
||
wrap.innerHTML = `<p style="color:var(--c-text-secondary);padding:var(--space-6)">
|
||
Noch keine Einträge vorhanden. Beginne dein Tagebuch oder trage Gesundheitsdaten ein.
|
||
</p>`;
|
||
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 = '<div class="tl-wrap">';
|
||
|
||
for (const ev of events) {
|
||
const year = ev.datum ? ev.datum.substring(0, 4) : null;
|
||
if (year && year !== lastYear) {
|
||
html += `<div class="tl-year">${_esc(year)}</div>`;
|
||
lastYear = year;
|
||
}
|
||
|
||
const kat = _KAT[ev.kategorie] || _KAT.tagebuch;
|
||
const big = ev.is_milestone;
|
||
|
||
let label = _esc(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 += `
|
||
<div class="tl-item${big ? ' tl-item--big' : ''}">
|
||
<div class="tl-dot" style="width:${dotSize};height:${dotSize};
|
||
background:${kat.color};border:${dotBorder};
|
||
margin-left:${dotML};
|
||
box-shadow:${big ? `0 0 0 4px ${kat.color}22` : 'none'}"></div>
|
||
<div class="tl-card">
|
||
${big && ev.foto_url ? `
|
||
<div class="tl-foto" style="background-image:url(${_esc(ev.foto_url)})"></div>` : ''}
|
||
<div class="tl-meta">
|
||
<span class="tl-badge" style="background:${kat.color}22;color:${kat.color}">
|
||
<svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#${kat.icon}"></use>
|
||
</svg>
|
||
${_esc(kat.label)}
|
||
</span>
|
||
<span class="tl-date">${_fmtDate(ev.datum)}</span>
|
||
</div>
|
||
<div class="tl-title${big ? ' tl-title--big' : ''}">${label}</div>
|
||
${ev.distanz_km ? `<div class="tl-sub">${ev.distanz_km} km</div>` : ''}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
html += '</div>';
|
||
html += `
|
||
<style>
|
||
.tl-wrap { padding:var(--space-2) 0;position:relative; }
|
||
.tl-wrap::before {
|
||
content:'';position:absolute;left:15px;top:0;bottom:0;width:2px;
|
||
background:var(--c-border);border-radius:1px;
|
||
}
|
||
.tl-year {
|
||
padding:var(--space-2) 0 var(--space-2) 48px;
|
||
font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);text-transform:uppercase;letter-spacing:.08em;
|
||
}
|
||
.tl-item {
|
||
display:flex;align-items:flex-start;gap:var(--space-3);
|
||
margin-bottom:var(--space-3);position:relative;
|
||
}
|
||
.tl-dot {
|
||
flex-shrink:0;border-radius:50%;margin-top:4px;z-index:1;position:relative;
|
||
}
|
||
.tl-card {
|
||
flex:1;min-width:0;background:var(--c-surface-2);
|
||
border-radius:var(--radius-md);padding:var(--space-3);overflow:hidden;
|
||
}
|
||
.tl-item--big .tl-card { border-left:3px solid var(--c-primary); }
|
||
.tl-foto {
|
||
width:100%;height:120px;background-size:cover;background-position:center;
|
||
border-radius:var(--radius-sm);margin-bottom:var(--space-2);
|
||
}
|
||
.tl-meta {
|
||
display:flex;align-items:center;gap:var(--space-2);
|
||
margin-bottom:var(--space-1);flex-wrap:wrap;
|
||
}
|
||
.tl-badge {
|
||
display:inline-flex;align-items:center;gap:3px;
|
||
padding:2px 8px;border-radius:var(--radius-full);
|
||
font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
}
|
||
.tl-date { font-size:var(--text-xs);color:var(--c-text-secondary);margin-left:auto; }
|
||
.tl-title { font-size:var(--text-sm);color:var(--c-text);font-weight:var(--weight-medium); }
|
||
.tl-title--big { font-weight:var(--weight-semibold);font-size:var(--text-base); }
|
||
.tl-sub { font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px; }
|
||
</style>`;
|
||
|
||
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 ${_esc(hauptRasse)}-Halter in der App`
|
||
: `${data.count} andere ${_esc(hauptRasse)}-Halter in der App`;
|
||
|
||
el.innerHTML = `
|
||
<button class="breed-community-chip" id="dp-breed-chip-btn">
|
||
🐕 ${label} — Forum ansehen
|
||
</button>
|
||
`;
|
||
document.getElementById('dp-breed-chip-btn')?.addEventListener('click', () => {
|
||
App.navigate('forum', false, { search: hauptRasse });
|
||
});
|
||
} catch {}
|
||
}
|
||
|
||
|
||
// ----------------------------------------------------------
|
||
// PUBLIC
|
||
// ----------------------------------------------------------
|
||
return { init, refresh, onDogChange, addNew: _openCreateModal };
|
||
|
||
})();
|