- Volltext-Suche im Tagebuch (LIKE über Titel/Text/Tags, Debounce 350ms)
- Digitaler Heimtierausweis als druckbare HTML-Seite (/ausweis/{dog_id})
Enthält Impfungen, Medikamente, Allergien, Tierärzte, Chip-Nr.
- Hund teilen: Einladungslink-System (dog_shares-Tabelle, /teilen/{token})
Geteilte Hunde erscheinen in der Hundeliste, Tagebuch/Gesundheit lesbar
- Widget-Seite /#widget: zufälliges Tagebuchbild + nächste Erinnerung
Als PWA-Shortcut im Manifest verankert
- SW-Cache by-v144, APP_VER 117
691 lines
29 KiB
JavaScript
691 lines
29 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');
|
||
}
|
||
});
|
||
|
||
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)">
|
||
${dog.foto_url
|
||
? `<img src="${dog.foto_url}" alt="${_esc(dog.name)}"
|
||
style="width:120px;height:120px;border-radius:50%;object-fit:cover;
|
||
border:3px solid var(--c-primary)">`
|
||
: `<div style="width:120px;height:120px;border-radius:50%;
|
||
background:var(--c-surface-2);display:flex;
|
||
align-items:center;justify-content:center;
|
||
font-size:3.5rem;border:3px solid var(--c-border)">${UI.icon('dog')}</div>`}
|
||
<label style="position:absolute;bottom:4px;right:4px;
|
||
background:var(--c-primary);color:#fff;border-radius:50%;
|
||
width:30px;height:30px;display:flex;align-items:center;
|
||
justify-content:center;cursor:pointer;font-size:14px"
|
||
title="Foto ändern">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>
|
||
<input type="file" id="dp-photo-input" accept="image/*"
|
||
style="display:none">
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Name + Rasse -->
|
||
<h2 style="font-size:var(--text-2xl);font-weight:700;
|
||
color:var(--c-text);margin:0 0 var(--space-1)">${_esc(dog.name)}</h2>
|
||
${dog.rasse
|
||
? `<p style="color:var(--c-text-secondary);margin:0 0 var(--space-5)">${_esc(dog.rasse)}</p>`
|
||
: `<p style="margin:0 0 var(--space-5)"></p>`}
|
||
|
||
<!-- Info-Grid -->
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);
|
||
margin-bottom:var(--space-5);text-align:left">
|
||
${geburtstag ? `
|
||
<div class="card" style="padding:var(--space-3)">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||
margin-bottom:2px"><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)">
|
||
<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>
|
||
` : ''}
|
||
|
||
${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%">
|
||
<button class="btn btn-primary w-full" id="dp-edit-btn">
|
||
Profil bearbeiten
|
||
</button>
|
||
<div style="display:flex;gap:var(--space-2)">
|
||
<button class="btn btn-secondary" style="flex:1" id="dp-ausweis-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#identification-card"></use></svg>
|
||
Ausweis
|
||
</button>
|
||
<button class="btn btn-secondary" style="flex:1" id="dp-share-btn">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#share-network"></use></svg>
|
||
Teilen
|
||
</button>
|
||
</div>
|
||
<button class="btn btn-secondary w-full" id="dp-add-dog-btn">
|
||
+ Weiteren Hund anlegen
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
`;
|
||
|
||
// Foto hochladen
|
||
document.getElementById('dp-photo-input')?.addEventListener('change', async e => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
const result = await API.dogs.uploadPhoto(dog.id, fd);
|
||
dog.foto_url = result.foto_url;
|
||
_appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url };
|
||
_appState.dogs = _appState.dogs.map(d =>
|
||
d.id === dog.id ? _appState.activeDog : d
|
||
);
|
||
UI.toast.success('Foto gespeichert.');
|
||
_renderProfile(_appState.activeDog);
|
||
} catch (err) {
|
||
UI.toast.error(err.message || 'Fehler beim Hochladen.');
|
||
}
|
||
});
|
||
// NFC-Link kopieren
|
||
document.getElementById('dp-copy-link-btn')?.addEventListener('click', async () => {
|
||
const url = `https://banyaro.app/hund/${dog.id}`;
|
||
try {
|
||
await navigator.clipboard.writeText(url);
|
||
UI.toast.success('Link kopiert!');
|
||
} catch {
|
||
// Fallback für ältere Browser
|
||
const el = document.getElementById('dp-nfc-link');
|
||
const range = document.createRange();
|
||
range.selectNodeContents(el);
|
||
const sel = window.getSelection();
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
document.execCommand('copy');
|
||
sel.removeAllRanges();
|
||
UI.toast.success('Link kopiert!');
|
||
}
|
||
});
|
||
|
||
// Transponder "Eintragen"-Button
|
||
document.getElementById('dp-chip-edit-btn')?.addEventListener('click', () => {
|
||
_showChipEdit(dog);
|
||
});
|
||
|
||
document.getElementById('dp-ausweis-btn')?.addEventListener('click', () => {
|
||
window.open(`/ausweis/${dog.id}`, '_blank');
|
||
});
|
||
|
||
document.getElementById('dp-share-btn')?.addEventListener('click', () => {
|
||
_showShareModal(dog);
|
||
});
|
||
|
||
// Edit- und Add-Klicks laufen über Event-Delegation in init() — keine direkten Listener nötig.
|
||
}
|
||
|
||
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: `
|
||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||
<button class="btn btn-primary" id="chip-edit-save-btn">Speichern</button>`,
|
||
});
|
||
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.');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TEILEN
|
||
// ----------------------------------------------------------
|
||
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: `
|
||
<button type="button" class="btn btn-secondary flex-1" id="dp-form-cancel">Abbrechen</button>
|
||
<button type="submit" form="dp-form" class="btn btn-primary flex-1">${UI.icon('dog')} Hund anlegen</button>
|
||
`,
|
||
});
|
||
_bindForm(null, true);
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// BEARBEITEN (Modal)
|
||
// ----------------------------------------------------------
|
||
function _openEditModal(dog) {
|
||
UI.modal.open({
|
||
title: `${dog.name} bearbeiten`,
|
||
body: _formHTML(dog, true),
|
||
footer: `
|
||
<button type="button" class="btn btn-secondary flex-1" id="dp-form-cancel">Abbrechen</button>
|
||
<button type="submit" form="dp-form" class="btn btn-primary flex-1">Speichern</button>
|
||
`,
|
||
});
|
||
_bindForm(dog, true);
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// FORMULAR HTML
|
||
// ----------------------------------------------------------
|
||
function _formHTML(dog, inModal = false) {
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
return `
|
||
<form id="dp-form" autocomplete="off">
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Name *</label>
|
||
<input class="form-control" type="text" name="name"
|
||
value="${_esc(dog?.name || '')}"
|
||
placeholder="z. B. Ban Yaro" required>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
Rasse
|
||
<span style="color:var(--c-text-secondary)">(optional)</span>
|
||
</label>
|
||
<input class="form-control" type="text" name="rasse"
|
||
value="${_esc(dog?.rasse || '')}"
|
||
placeholder="z. B. Mischling, Golden Retriever…">
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Geburtstag</label>
|
||
<input class="form-control" type="date" name="geburtstag"
|
||
value="${dog?.geburtstag || ''}" max="${today}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Geschlecht</label>
|
||
<select class="form-control" name="geschlecht">
|
||
<option value="" ${!dog?.geschlecht ? 'selected' : ''}>–</option>
|
||
<option value="m" ${dog?.geschlecht === 'm' ? 'selected' : ''}>Rüde</option>
|
||
<option value="w" ${dog?.geschlecht === 'w' ? 'selected' : ''}>Hündin</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Gewicht (kg)</label>
|
||
<input class="form-control" type="number" name="gewicht_kg"
|
||
value="${dog?.gewicht_kg || ''}"
|
||
min="0.1" max="120" step="0.1" placeholder="z. B. 28.5">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Chip-Nummer</label>
|
||
<input class="form-control" type="text" name="chip_nr"
|
||
value="${_esc(dog?.chip_nr || '')}" placeholder="15-stellig">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
Bio / Steckbrief
|
||
<span style="color:var(--c-text-secondary)">(optional)</span>
|
||
</label>
|
||
<textarea class="form-control" name="bio" rows="2"
|
||
placeholder="Kurze Beschreibung…">${_esc(dog?.bio || '')}</textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
|
||
<input type="checkbox" name="is_public" ${dog?.is_public ? 'checked' : ''}>
|
||
Öffentliches Profil (für NFC-Tag)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Foto</label>
|
||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||
<img id="dp-form-preview"
|
||
src="${dog?.foto_url || ''}"
|
||
style="width:64px;height:64px;border-radius:50%;object-fit:cover;
|
||
background:var(--c-surface-2);border:2px solid var(--c-border);
|
||
display:${dog?.foto_url ? 'block' : 'none'}">
|
||
<label class="btn btn-secondary btn-sm" style="cursor:pointer;margin:0">
|
||
<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>
|
||
</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>` : ''}
|
||
|
||
${dog ? `
|
||
<div style="margin-top:var(--space-5);padding-top:var(--space-4);
|
||
border-top:1px solid var(--c-border);text-align:center">
|
||
<button type="button" class="btn btn-ghost btn-sm" id="dp-delete-btn"
|
||
style="color:var(--c-danger)">
|
||
${dog.name} löschen
|
||
</button>
|
||
</div>
|
||
` : ''}
|
||
|
||
</form>
|
||
`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// FORMULAR EVENTS
|
||
// ----------------------------------------------------------
|
||
function _bindForm(dog, inModal) {
|
||
const form = document.getElementById('dp-form');
|
||
if (!form) return;
|
||
|
||
// Foto-Vorschau
|
||
const fotoInput = document.getElementById('dp-form-foto');
|
||
const fotoPreview = document.getElementById('dp-form-preview');
|
||
if (fotoInput && fotoPreview) {
|
||
fotoInput.addEventListener('change', () => {
|
||
const file = fotoInput.files[0];
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = e => {
|
||
fotoPreview.src = e.target.result;
|
||
fotoPreview.style.display = 'block';
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
document.getElementById('dp-form-cancel')
|
||
?.addEventListener('click', UI.modal.close);
|
||
|
||
document.getElementById('dp-delete-btn')?.addEventListener('click', async () => {
|
||
const ok = await UI.modal.confirm({
|
||
title : `${dog.name} löschen?`,
|
||
message: 'Tagebuch-Einträge und Gesundheitsdaten werden ebenfalls gelöscht. Nicht rückgängig.',
|
||
confirmText: 'Löschen',
|
||
danger : true,
|
||
});
|
||
if (!ok) return;
|
||
try {
|
||
await API.dogs.delete(dog.id);
|
||
_appState.dogs = _appState.dogs.filter(d => d.id !== dog.id);
|
||
_appState.activeDog = _appState.dogs[0] || null;
|
||
if (inModal) UI.modal.close();
|
||
UI.toast.success(`${dog.name} wurde gelöscht.`);
|
||
await _render();
|
||
} catch (err) {
|
||
UI.toast.error(err.message || 'Fehler beim Löschen.');
|
||
}
|
||
});
|
||
|
||
form.addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const btn = 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,
|
||
geburtstag: fd.geburtstag || null,
|
||
geschlecht: fd.geschlecht || null,
|
||
gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null,
|
||
chip_nr: fd.chip_nr || null,
|
||
bio: fd.bio || null,
|
||
is_public: 'is_public' in fd,
|
||
};
|
||
|
||
let saved;
|
||
if (dog) {
|
||
saved = await API.dogs.update(dog.id, payload);
|
||
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? saved : d);
|
||
_appState.activeDog = saved;
|
||
if (inModal) UI.modal.close();
|
||
UI.toast.success('Profil gespeichert.');
|
||
} else {
|
||
saved = await API.dogs.create(payload);
|
||
_appState.dogs.push(saved);
|
||
_appState.activeDog = saved;
|
||
localStorage.setItem('by_active_dog', String(saved.id));
|
||
if (inModal) UI.modal.close();
|
||
UI.toast.success(`${saved.name} wurde angelegt! 🎉`);
|
||
}
|
||
|
||
// Foto hochladen wenn gewählt
|
||
const fotoFile = document.getElementById('dp-form-foto')?.files[0];
|
||
if (fotoFile) {
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('file', fotoFile);
|
||
const result = await API.dogs.uploadPhoto(saved.id, fd);
|
||
saved.foto_url = result.foto_url;
|
||
_appState.activeDog = { ...saved };
|
||
_appState.dogs = _appState.dogs.map(d => d.id === saved.id ? _appState.activeDog : d);
|
||
} catch {
|
||
UI.toast.warning('Profil gespeichert, Foto konnte nicht hochgeladen werden.');
|
||
}
|
||
}
|
||
|
||
// Dog Switcher in Header + Sidebar aktualisieren
|
||
App.renderDogSwitcher?.();
|
||
|
||
await _render();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// HELPER
|
||
// ----------------------------------------------------------
|
||
function _calcAlter(geburtstag) {
|
||
const born = new Date(geburtstag + 'T00:00:00');
|
||
const tage = Math.floor((Date.now() - born) / 86400000);
|
||
if (tage < 0) return '';
|
||
if (tage < 30) return `${tage} Tag${tage !== 1 ? 'e' : ''} alt`;
|
||
if (tage < 365) {
|
||
const m = Math.floor(tage / 30);
|
||
return `${m} Monat${m !== 1 ? 'e' : ''} alt`;
|
||
}
|
||
const j = Math.floor(tage / 365);
|
||
const m = Math.floor((tage % 365) / 30);
|
||
return m > 0
|
||
? `${j} Jahr${j !== 1 ? 'e' : ''}, ${m} Monat${m !== 1 ? 'e' : ''} alt`
|
||
: `${j} Jahr${j !== 1 ? 'e' : ''} alt`;
|
||
}
|
||
|
||
function _esc(str) {
|
||
if (!str) return '';
|
||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PUBLIC
|
||
// ----------------------------------------------------------
|
||
return { init, refresh, onDogChange, addNew: _openCreateModal };
|
||
|
||
})();
|