Feature: Sprint31 — 9 Features merged (Streak, Ausgaben, KI-Tierarzt, Rückrufe, Adoption, Vet+Befunde, Hundepass, Playdate, Rassenerkennung)
- Trainings-Streak: streak.py, DB training_streaks, Scheduler 19:00, Widget in welcome.js, Ping in uebungen.js
- Ausgaben-Tracker: expenses.py, expenses.js, DB expenses-Tabelle
- KI-Tierarztfragen: ki.py /tierarzt, health.js Button+Modal, DB ki_tierarzt_log
- Rückruf-Alarm: recalls.py, recalls.js, DB feed_recalls, Scheduler 08:00 RASFF
- Adoption: adoption.py, adoption.js, DB adoption_cache
- Tierarzt-Favorit + Befunde: tieraerzte.py /my-favorite+/favorite, health_docs.py, health.js, api.js, DB favorite_vets+health_documents
- Digitaler Hundepass: passport.py, dog-profile.js, main.py /pass/{token}, DB vaccinations+medications+dog_passport_meta+passport_shares, requirements.txt fpdf2
- Playdate-Matching: playdate.py, playdate.js, DB playdate_listings+playdate_requests
- Rassen-Erkennung: ki.py /rasse-erkennung (Claude Vision), dog-profile.js+wiki.js, CSS .rasse-result-card, DB ki_rasse_log
This commit is contained in:
parent
031c6028ac
commit
742ad189e8
26 changed files with 5734 additions and 27 deletions
|
|
@ -97,19 +97,8 @@ window.Page_dog_profile = (() => {
|
|||
<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>`}
|
||||
|
||||
${(dog.hdm_wins?.length) ? `
|
||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);justify-content:center;margin-bottom:var(--space-5)">
|
||||
${dog.hdm_wins.map(m => {
|
||||
const [y, mo] = m.split('-');
|
||||
const label = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' })
|
||||
.format(new Date(+y, +mo - 1, 1));
|
||||
return `<span class="dp-hdm-badge" title="Hund des Monats ${label}">🏆 ${label}</span>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
` : `<div style="margin-bottom:var(--space-5)"></div>`}
|
||||
? `<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);
|
||||
|
|
@ -208,6 +197,10 @@ window.Page_dog_profile = (() => {
|
|||
Teilen
|
||||
</button>` : ''}
|
||||
</div>
|
||||
${!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-add-dog-btn">
|
||||
+ Weiteren Hund anlegen
|
||||
</button>` : ''}
|
||||
|
|
@ -276,6 +269,10 @@ window.Page_dog_profile = (() => {
|
|||
_showShareModal(dog);
|
||||
});
|
||||
|
||||
document.getElementById('dp-passport-btn')?.addEventListener('click', () => {
|
||||
_showPassportModal(dog);
|
||||
});
|
||||
|
||||
// Edit- und Add-Klicks laufen über Event-Delegation in init() — keine direkten Listener nötig.
|
||||
}
|
||||
|
||||
|
|
@ -1007,7 +1004,7 @@ window.Page_dog_profile = (() => {
|
|||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto</label>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||
<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;
|
||||
|
|
@ -1018,6 +1015,16 @@ window.Page_dog_profile = (() => {
|
|||
<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>
|
||||
|
||||
|
|
@ -1097,6 +1104,9 @@ window.Page_dog_profile = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// Rassen-Erkennung per KI
|
||||
_bindRasseErkennung();
|
||||
|
||||
document.getElementById('dp-form-cancel')
|
||||
?.addEventListener('click', UI.modal.close);
|
||||
|
||||
|
|
@ -1182,6 +1192,152 @@ window.Page_dog_profile = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 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
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -1207,6 +1363,431 @@ window.Page_dog_profile = (() => {
|
|||
.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>
|
||||
<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 herunterladen
|
||||
</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-link btn-sm" id="pp-meta-edit-btn">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
|
||||
? '<p style="color:var(--c-text-muted);font-size:var(--text-sm);margin:0">Keine Impfungen eingetragen.</p>'
|
||||
: 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
|
||||
? '<p style="color:var(--c-text-muted);font-size:var(--text-sm);margin:0">Keine Medikamente eingetragen.</p>'
|
||||
: 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.');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue