@@ -1327,8 +1346,9 @@ window.Page_dog_profile = (() => {
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,
+ gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null,
+ widerrist_cm: fd.widerrist_cm ? parseFloat(fd.widerrist_cm) : null,
+ chip_nr: fd.chip_nr || null,
bio: fd.bio || null,
is_public: 'is_public' in fd,
fell_typ: fd.fell_typ || null,
diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js
index b984515..bf68615 100644
--- a/backend/static/js/pages/health.js
+++ b/backend/static/js/pages/health.js
@@ -22,6 +22,8 @@ window.Page_health = (() => {
{ key: 'allergie', label: 'Allergien', icon: '
' },
{ key: 'dokument', label: 'Dokumente', icon: '
' },
{ key: 'praxen', label: 'Praxen', icon: '
' },
+ { key: 'versicherung', label: 'Versicherung', icon: '
' },
+ { key: 'verhalten', label: 'Verhalten', icon: '
' },
];
const LAEUFIGKEIT_TAB = { key: 'laeufigkeit', label: 'Läufigkeit', icon: '
' };
@@ -111,12 +113,14 @@ window.Page_health = (() => {
+
`;
_renderTabBar();
UI.bindDogChip(_container, _appState);
+ _loadRemindersBanner();
_container.querySelector('#health-ki-btn')
.addEventListener('click', _showKiSummary);
_container.querySelector('#health-ki-tierarzt-btn')
@@ -332,6 +336,8 @@ window.Page_health = (() => {
case 'allergie': content.innerHTML = _renderAllergien(entries); break;
case 'dokument': content.innerHTML = _renderDokumente(entries); break;
case 'praxen': content.innerHTML = _renderPraxen(); break;
+ case 'versicherung': _renderVersicherung(content); break;
+ case 'verhalten': _renderVerhalten(content); break;
}
_bindTabEvents(content);
@@ -3050,6 +3056,331 @@ window.Page_health = (() => {
});
}
+ // ==============================================================
+ // BEVORSTEHENDE ERINNERUNGEN (Banner oben in der Health-Seite)
+ // ==============================================================
+ async function _loadRemindersBanner() {
+ const dog = _appState?.activeDog;
+ if (!dog) return;
+ const wrap = _container?.querySelector('#health-reminders-banner');
+ if (!wrap) return;
+ let items;
+ try { items = await API.health.reminders(dog.id); }
+ catch { return; }
+ if (!items.length) { wrap.style.display = 'none'; return; }
+
+ const TYPE_LABEL = { impfung: 'Impfung', entwurmung: 'Entwurmung', medikament: 'Medikament' };
+ const fmt = d => { try { const p = d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } };
+
+ wrap.style.display = '';
+ wrap.innerHTML = items.slice(0, 3).map(r => {
+ const overdue = r.ueberfaellig;
+ const color = overdue ? 'var(--c-danger,#ef4444)' : r.delta_tage <= 3 ? '#f59e0b' : 'var(--c-primary)';
+ const bg = overdue ? 'rgba(239,68,68,0.08)' : r.delta_tage <= 3 ? 'rgba(245,158,11,0.08)' : 'var(--c-primary-subtle)';
+ const label = overdue ? `Überfällig seit ${Math.abs(r.delta_tage)} Tag${Math.abs(r.delta_tage)!==1?'en':''}` :
+ r.delta_tage === 0 ? 'Heute fällig' :
+ `in ${r.delta_tage} Tag${r.delta_tage!==1?'en':''}`;
+ return `
+
+
+
+ ${_esc(r.bezeichnung)}
+ ${TYPE_LABEL[r.typ] || r.typ}
+
+
${label}
+
`;
+ }).join('');
+ }
+
+ // ==============================================================
+ // TAB: VERSICHERUNG
+ // ==============================================================
+ async function _renderVersicherung(content) {
+ const dog = _appState?.activeDog;
+ if (!dog) return;
+ content.innerHTML = `
`;
+
+ let policies;
+ try { policies = await API.health.insuranceList(dog.id); }
+ catch { content.innerHTML = `
Fehler beim Laden.
`; return; }
+
+ const _fmtDate = d => { if (!d) return '–'; try { const p=d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } };
+ const _fmtEur = v => v ? `${v.toFixed(2).replace('.',',')} €/Jahr` : '–';
+
+ const cardsHtml = policies.length ? policies.map(p => `
+
+
+
+
${_esc(p.anbieter)}
+ ${p.police_nr ? `
Police: ${_esc(p.police_nr)}
` : ''}
+
+
+
+
+
+
+
+
Jahresbeitrag
${_fmtEur(p.jahresbeitrag)}
+
Läuft ab
${_fmtDate(p.ablaufdatum)}
+ ${p.kontakt ? `
Kontakt
${_esc(p.kontakt)}
` : ''}
+ ${p.notizen ? `
Notizen
${_esc(p.notizen)}
` : ''}
+
+
`).join('') : `
+
+
+
Noch keine Versicherung eingetragen.
+
`;
+
+ content.innerHTML = `
+ ${cardsHtml}
+
+
`;
+
+ content.querySelector('#ins-add-btn')?.addEventListener('click', () => _openInsuranceForm(dog, null, () => _renderVersicherung(content)));
+ content.querySelectorAll('.ins-edit-btn').forEach(btn => {
+ const pol = policies.find(p => p.id === parseInt(btn.dataset.id));
+ btn.addEventListener('click', () => _openInsuranceForm(dog, pol, () => _renderVersicherung(content)));
+ });
+ content.querySelectorAll('.ins-del-btn').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ if (!window.confirm('Versicherung löschen?')) return;
+ await API.health.insuranceDelete(dog.id, parseInt(btn.dataset.id));
+ _renderVersicherung(content);
+ });
+ });
+ }
+
+ function _openInsuranceForm(dog, existing, onSave) {
+ const id = `ins-form-${Date.now()}`;
+ const body = `
`;
+ const footer = `
+
+
`;
+ UI.modal.open({ title: existing ? 'Versicherung bearbeiten' : 'Versicherung eintragen', body, footer });
+ setTimeout(() => {
+ document.getElementById('ins-save-btn')?.addEventListener('click', async ev => {
+ ev.preventDefault();
+ const form = document.getElementById(id);
+ if (!form) return;
+ const fd = new FormData(form);
+ const data = {
+ anbieter: (fd.get('anbieter')||'').trim(),
+ police_nr: fd.get('police_nr')||null,
+ jahresbeitrag: fd.get('jahresbeitrag') ? parseFloat(fd.get('jahresbeitrag')) : null,
+ ablaufdatum: fd.get('ablaufdatum')||null,
+ kontakt: fd.get('kontakt')||null,
+ notizen: fd.get('notizen')||null,
+ };
+ if (!data.anbieter) { UI.toast.warning('Bitte Anbieter angeben.'); return; }
+ await UI.asyncButton(document.getElementById('ins-save-btn'), async () => {
+ try {
+ if (existing) await API.health.insuranceUpdate(dog.id, existing.id, data);
+ else await API.health.insuranceCreate(dog.id, data);
+ UI.modal.close();
+ UI.toast.success('Gespeichert.');
+ onSave();
+ } catch (err) { UI.toast.error(err.message || 'Fehler.'); }
+ });
+ });
+ }, 50);
+ }
+
+ // ==============================================================
+ // TAB: VERHALTEN
+ // ==============================================================
+ const _KAT_LABELS = {
+ angst: 'Angst / Panik', aggression: 'Aggression', ueberreaktion: 'Überreaktion',
+ ressource: 'Ressourcenverteidigung', separation: 'Trennungsangst',
+ leine: 'Leinenprobleme', sozial: 'Sozialkompetenz', sonstiges: 'Sonstiges',
+ };
+ const _KAT_COLORS = {
+ angst: '#3b82f6', aggression: '#ef4444', ueberreaktion: '#f59e0b',
+ ressource: '#8b5cf6', separation: '#ec4899', leine: '#06b6d4',
+ sozial: '#22c55e', sonstiges: '#6b7280',
+ };
+ const _TRIGGER_LABELS = {
+ fremde_hunde: 'Fremde Hunde', fremde_menschen: 'Fremde Menschen', kinder: 'Kinder',
+ laerm_feuerwerk: 'Feuerwerk', laerm_gewitter: 'Gewitter', auto_fahrrad: 'Autos/Fahrräder',
+ tierarzt: 'Tierarztbesuch', allein_zuhause: 'Allein zuhause',
+ andere_tiere: 'Andere Tiere', besucher_zuhause: 'Besucher', sonstiges: 'Sonstiges',
+ };
+
+ async function _renderVerhalten(content) {
+ const dog = _appState?.activeDog;
+ if (!dog) return;
+ content.innerHTML = `
`;
+
+ let resp;
+ try { resp = await API.health.behaviorList(dog.id); }
+ catch { content.innerHTML = `
Fehler beim Laden.
`; return; }
+
+ const entries = resp.entries || [];
+ const fmtDate = d => { try { const p=d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } };
+
+ const listHtml = entries.length ? entries.map(e => {
+ const color = _KAT_COLORS[e.kategorie] || '#6b7280';
+ const katLabel = _KAT_LABELS[e.kategorie] || e.kategorie;
+ const trigLabel = _TRIGGER_LABELS[e.trigger] || e.trigger || '';
+ const dots = Array.from({length: 5}, (_,i) =>
+ `
`
+ ).join('');
+ return `
+
+
+
+
+ ${_esc(katLabel)}
+ ${trigLabel ? `${_esc(trigLabel)}` : ''}
+ ${fmtDate(e.datum)}${e.uhrzeit ? ' ' + e.uhrzeit : ''}
+
+
${dots}
+ ${e.notiz ? `
${_esc(e.notiz)}
` : ''}
+
+
+
`;
+ }).join('') : `
+
+
+
Noch keine Einträge. Protokolliere auffälliges Verhalten um Muster zu erkennen.
+
`;
+
+ content.innerHTML = `
+ ${listHtml}
+
+
`;
+
+ content.querySelector('#beh-add-btn')?.addEventListener('click', () => _openBehaviorForm(dog, () => _renderVerhalten(content)));
+ content.querySelectorAll('.beh-del-btn').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ if (!window.confirm('Eintrag löschen?')) return;
+ await API.health.behaviorDelete(dog.id, parseInt(btn.dataset.id));
+ _renderVerhalten(content);
+ });
+ });
+ }
+
+ function _openBehaviorForm(dog, onSave) {
+ const id = `beh-form-${Date.now()}`;
+ const today = new Date().toISOString().slice(0, 10);
+ const nowTime = (() => { const d=new Date(); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; })();
+ const body = `
`;
+ const footer = `
+
+
`;
+ UI.modal.open({ title: 'Verhalten erfassen', body, footer });
+ setTimeout(() => {
+ document.querySelectorAll('.beh-int-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const val = parseInt(btn.dataset.val);
+ document.querySelectorAll('.beh-int-btn').forEach((b,i) => {
+ b.style.background = i < val ? 'var(--c-primary)' : 'var(--c-bg-card)';
+ b.style.color = i < val ? '#fff' : 'var(--c-text-secondary)';
+ });
+ const hi = document.querySelector('[name="intensitaet"]');
+ if (hi) hi.value = val;
+ });
+ });
+ document.getElementById('beh-save-btn')?.addEventListener('click', async ev => {
+ ev.preventDefault();
+ const form = document.getElementById(id);
+ if (!form) return;
+ const fd = new FormData(form);
+ const data = {
+ datum: fd.get('datum'),
+ uhrzeit: fd.get('uhrzeit')||null,
+ kategorie: fd.get('kategorie'),
+ intensitaet: parseInt(fd.get('intensitaet')||'3'),
+ trigger: fd.get('trigger')||null,
+ notiz: (fd.get('notiz')||'').trim()||null,
+ };
+ if (!data.kategorie) { UI.toast.warning('Bitte Kategorie wählen.'); return; }
+ await UI.asyncButton(document.getElementById('beh-save-btn'), async () => {
+ try {
+ await API.health.behaviorCreate(dog.id, data);
+ UI.modal.close();
+ UI.toast.success('Eintrag gespeichert.');
+ onSave();
+ } catch (err) { UI.toast.error(err.message || 'Fehler.'); }
+ });
+ });
+ }, 50);
+ }
+
return { init, refresh, openNew, onDogChange };
})();
diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js
index de58da5..dda7473 100644
--- a/backend/static/js/pages/map.js
+++ b/backend/static/js/pages/map.js
@@ -75,7 +75,7 @@ window.Page_map = (() => {
// z: zIndexOffset — höher = weiter oben bei Überlappung
const TYPEN = {
- restaurant: { icon: '
', label: 'Restaurant', color: '#F97316', z: 10 },
+ restaurant: { icon: '
', label: 'Hundefreundl. Café/Restaurant', color: '#F97316', z: 10 },
freilauf: { icon: '
', label: 'Freilauf', color: '#22C55E', z: 20 },
shop: { icon: '
', label: 'Shop', color: '#3B82F6', z: 15 },
kotbeutel: { icon: '
', label: 'Kotbeutel', color: '#84A98C', z: 5 },
@@ -92,6 +92,7 @@ window.Page_map = (() => {
treffpunkt: { icon: '
', label: 'Treffpunkt', color: '#7C3AED', z: 25 },
community: { icon: '
', label: 'Sonstiges', color: '#F59E0B', z: 30 },
zuechter: { icon: '
', label: 'Züchter', color: '#7C3AED', z: 50 },
+ hotel: { icon: '
', label: 'Hundefreundl. Hotel', color: '#0369a1', z: 20 },
};
// Frontend-Layer → Backend-Typ Mapping
@@ -109,6 +110,7 @@ window.Page_map = (() => {
parkplatz: 'parkplatz',
treffpunkt: 'treffpunkt',
community: 'sonstiges',
+ hotel: 'hotel',
};
// Gefahren-Radius-Kreis: prominente rote Fläche
diff --git a/backend/static/sw.js b/backend/static/sw.js
index e9b1388..797d5b7 100644
--- a/backend/static/sw.js
+++ b/backend/static/sw.js
@@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
-const CACHE_VERSION = 'by-v872';
+const CACHE_VERSION = 'by-v875';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache