/* ============================================================ BAN YARO — Gesundheit & Impfpass (Sprint 3) Tabs: Impfungen | Tierarzt | Gewicht | Medikamente | Allergien | Dokumente + KI-Gesundheitszusammenfassung ============================================================ */ window.Page_health = (() => { let _container = null; let _appState = null; let _data = {}; let _praxen = []; let _activeTab = 'impfung'; let _favoritVet = null; let _healthDocs = []; const BASE_TABS = [ { key: 'impfung', label: 'Impfpass', icon: '' }, { key: 'tierarzt', label: 'Besuche', icon: '' }, { key: 'gewicht', label: 'Gewicht', icon: '' }, { key: 'medikament', label: 'Medikamente', icon: '' }, { key: 'pflege', label: 'Pflege', icon: '' }, { 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: '' }; // Pflege-Routinen — wiederkehrende Pflege-Aufgaben, gebündelt im 'pflege'-Tab const PFLEGE_TYPEN = ['parasit', 'krallen', 'fellpflege']; const PFLEGE_META = { parasit: { label: 'Zecken-/Flohschutz', icon: 'bug-beetle', placeholder: 'z.B. Frontline, Seresto-Halsband' }, krallen: { label: 'Krallen schneiden', icon: 'scissors', placeholder: 'z.B. Krallen kürzen' }, fellpflege: { label: 'Fellpflege', icon: 'wind', placeholder: 'z.B. Bürsten, Trimmen, Baden' }, }; function _getTabs() { const tabs = [...BASE_TABS]; if (_appState?.activeDog?.geschlecht === 'w') tabs.splice(2, 0, LAEUFIGKEIT_TAB); return tabs; } // ---------------------------------------------------------- // LIFECYCLE // ---------------------------------------------------------- async function init(container, appState, params) { _container = container; _appState = appState; if (params?.tab) { const valid = _getTabs().some(t => t.key === params.tab); if (valid) _activeTab = params.tab; } await _render(); if (params?.openForm) { setTimeout(() => _showForm(null, _activeTab), 200); } } async function refresh() { if (!_appState.activeDog) return; _data = {}; await _renderHealth(); } async function onDogChange() { _data = {}; await _renderHealth(); } function openNew() { _showForm(null, _activeTab); } // ---------------------------------------------------------- // RENDER — Einstieg: Picker bei mehreren Hunden, sonst direkt // ---------------------------------------------------------- async function _render() { if (!_appState.activeDog) { _container.innerHTML = UI.emptyState({ icon: '', title: 'Noch kein Hund angelegt', text: 'Erstelle zuerst ein Hundeprofil.', action: ``, }); return; } await _renderHealth(); } // ---------------------------------------------------------- // HEALTH-ANSICHT — Tabs mit Einträgen // ---------------------------------------------------------- async function _renderHealth() { const dog = _appState.activeDog; const transponderHtml = `
Transponder: ${dog?.chip_nr ? `${UI.escape(dog.chip_nr)}` : 'nicht eingetragen'}
`; _container.innerHTML = ` ${UI.dogChip(_appState)}
${transponderHtml}
`; _renderTabBar(); UI.bindDogChip(_container, _appState); _loadRemindersBanner(); _container.querySelector('#health-ki-btn') .addEventListener('click', _showKiSummary); _container.querySelector('#health-ki-tierarzt-btn') .addEventListener('click', _showKiTierarzt); _container.querySelector('#health-transponder-edit') .addEventListener('click', () => _editTransponder(dog)); await _loadAll(); _renderErinnerungen(); _renderTab(); _loadKiBerichte(dog.id); _loadTerminvorschlaege(dog.id); _loadMeinTierarzt(); } // ---------------------------------------------------------- // ERINNERUNGEN — Banner über den Tabs // ---------------------------------------------------------- function _getErinnerungen() { const REMINDER_TABS = ['impfung', 'entwurmung', 'medikament', 'laeufigkeit']; const now = Date.now(); const items = []; REMINDER_TABS.forEach(typ => { (_data[typ] || []).forEach(e => { if (!e.naechstes) return; const tage = Math.ceil((new Date(e.naechstes).getTime() - now) / 86400000); if (tage <= 60) items.push({ ...e, _tage: tage, _typ: typ }); }); }); return items.sort((a, b) => a._tage - b._tage); } function _renderErinnerungen() { const el = _container.querySelector('#health-reminders'); if (!el) return; const items = _getErinnerungen(); // Nav-Badge aktualisieren (Anzahl überfälliger/bald fälliger Einträge) const overdueCount = items.filter(e => e._tage < 0).length; _updateHealthBadge(overdueCount || (items.length ? items.length : 0)); if (!items.length) { el.innerHTML = ''; return; } const ICONS = { impfung: '', entwurmung: '', medikament: '', tierarzt: '', gewicht: '', allergie: '', laeufigkeit: '', }; el.innerHTML = `
Anstehende Erinnerungen
${items.map(e => { const ampel = _impfAmpel(e.naechstes); const dateStr = UI.time.format(e.naechstes + 'T00:00:00'); const ageLabel = e._tage < 0 ? `Überfällig seit ${Math.abs(e._tage)} Tagen` : e._tage === 0 ? 'Heute fällig' : `In ${e._tage} Tagen`; return `
${ICONS[e._typ] || ''}
${UI.escape(e.bezeichnung)}
${ageLabel} · ${dateStr}
`; }).join('')}
`; el.querySelectorAll('[data-action="reminder-erledigt"]').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.dataset.entryId); const typ = btn.dataset.entryTyp; const entry = (_data[typ] || []).find(e => e.id === id); if (!entry) return; // Neues Formular ohne id → Neu-Eintrag, vorausgefüllt const today = new Date().toISOString().slice(0, 10); let naechstes = ''; if (entry.intervall_tage) { const next = new Date(); next.setDate(next.getDate() + entry.intervall_tage); naechstes = next.toISOString().slice(0, 10); } _showForm({ bezeichnung: entry.bezeichnung, datum: today, naechstes, intervall_tage: entry.intervall_tage, tierarzt_id: entry.tierarzt_id, tierarzt_name: entry.tierarzt_name, charge_nr: entry.charge_nr, }, typ); }); }); } function _updateHealthBadge(count) { ['[data-page="health"] .nav-item-icon', '[data-page="health"] .sidebar-item-icon'].forEach(sel => { document.querySelectorAll(sel).forEach(el => { let badge = el.querySelector('.nav-badge'); if (count > 0) { if (!badge) { badge = document.createElement('span'); badge.className = 'nav-badge'; el.appendChild(badge); } badge.textContent = count > 9 ? '9+' : count; } else if (badge) { badge.remove(); } }); }); } function _renderTabBar() { const tabsEl = _container.querySelector('#by-tabs'); tabsEl.innerHTML = _getTabs().map(t => ` `).join(''); const count = tabsEl.querySelectorAll('.by-tab').length; tabsEl.style.setProperty('--health-tab-cols', Math.ceil(count / 2)); tabsEl.querySelectorAll('.by-tab').forEach(btn => { btn.addEventListener('click', () => { _activeTab = btn.dataset.tab; tabsEl.querySelectorAll('.by-tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); _renderTab(); }); }); } // ---------------------------------------------------------- // DATEN LADEN // ---------------------------------------------------------- async function _loadAll() { const dogId = _appState.activeDog.id; try { const all = await API.health.list(dogId); _data = {}; _getTabs().forEach(t => { _data[t.key] = []; }); _data['laeufigkeit'] = _data['laeufigkeit'] || []; // Pflege-Routinen: eigene Listen je Typ (Tab 'pflege' bündelt sie beim Rendern) PFLEGE_TYPEN.forEach(t => { _data[t] = []; }); all.forEach(e => { if (_data[e.typ] !== undefined) _data[e.typ].push(e); }); } catch (err) { UI.toast.error('Gesundheitsdaten konnten nicht geladen werden.'); } try { _praxen = await API.tieraerzte.list(); } catch (err) { // silent fail } try { _data['gewicht_chart'] = await API.health.gewichtVerlauf(dogId); } catch (err) { _data['gewicht_chart'] = []; } try { _favoritVet = await API.tieraerzte.myFavorite(); } catch (err) { _favoritVet = null; } try { _healthDocs = await API.healthDocs.list(dogId); } catch (err) { _healthDocs = []; } } // ---------------------------------------------------------- // TAB-INHALT RENDERN // ---------------------------------------------------------- function _renderTab() { const content = _container.querySelector('#by-tab-content'); if (!content) return; const entries = _data[_activeTab] || []; switch (_activeTab) { case 'impfung': content.innerHTML = _renderImpfungen(entries); break; case 'tierarzt': content.innerHTML = _renderTierarzt(entries); break; case 'gewicht': content.innerHTML = _renderGewicht(entries); break; case 'laeufigkeit': content.innerHTML = _renderLaeufigkeit(entries); break; case 'medikament': content.innerHTML = _renderMedikamente(entries); break; case 'pflege': content.innerHTML = _renderPflege(); break; 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); } // ---------------------------------------------------------- // EMPTY-STATE HELPER // ---------------------------------------------------------- function _emptyState(icon, title, text, cta = '') { return `
${title}
${text ? `

${text}

` : ''} ${cta ? `
${cta}
` : ''}
`; } // ---------------------------------------------------------- // IMPFUNGEN — mit Ampel-Status // ---------------------------------------------------------- function _renderImpfungen(entries) { const addBtn = ``; const helpIcon = UI.help('Trage Impfdatum und nächsten Termin ein — wir erinnern dich rechtzeitig.'); if (!entries.length) return _emptyState( 'syringe', 'Noch keine Impfungen', `Trage alle Impfungen ein, um nichts zu verpassen. ${helpIcon}`, addBtn ); const items = entries.map(e => { const ampel = _impfAmpel(e.naechstes); const praxis = _praxen.find(p => p.id === e.tierarzt_id); const vetName = praxis?.name || e.tierarzt_name || ''; return `
${UI.escape(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')} ${e.charge_nr ? ` · Ch.-Nr: ${UI.escape(e.charge_nr)}` : ''}
${vetName ? `
${UI.escape(vetName)}
` : ''} ${e.naechstes ? `
Nächste Impfung: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
` : ''} ${e.notiz ? `
${UI.escape(e.notiz)}
` : ''}
`; }).join(''); return `
${items}
${addBtn}
`; } function _impfAmpel(naechstesStr) { if (!naechstesStr) return { color: 'grey', label: 'Kein Folgedatum', icon: '' }; const diff = (new Date(naechstesStr) - Date.now()) / 86400000; // Tage if (diff < 0) return { color: 'red', label: 'Überfällig!', icon: '🔴' }; if (diff < 60) return { color: 'yellow', label: 'Bald fällig', icon: '🟡' }; return { color: 'green', label: 'Aktuell', icon: '🟢' }; } // ---------------------------------------------------------- // PFLEGE-ROUTINEN (Zecken-/Flohschutz, Krallen, Fellpflege) // ---------------------------------------------------------- function _intervallLabel(tage) { if (!tage) return ''; const m = { 30: 'monatlich', 60: 'alle 2 Monate', 90: 'vierteljährlich', 180: 'halbjährlich', 365: 'jährlich' }; return m[tage] || `alle ${tage} Tage`; } function _renderPflege() { const addButtons = `
${PFLEGE_TYPEN.map(t => ` `).join('')}
`; const all = PFLEGE_TYPEN.flatMap(t => (_data[t] || []).map(e => ({ ...e, _typ: t }))); if (!all.length) return addButtons + _emptyState( 'paw-print', 'Noch keine Pflege-Routinen', 'Lege wiederkehrende Routinen wie Zecken-/Flohschutz, Krallenschneiden oder Fellpflege an — wir erinnern dich rechtzeitig.' ); // Fällige zuerst (nach naechstes), Einträge ohne Folgedatum ans Ende all.sort((a, b) => { if (!a.naechstes) return 1; if (!b.naechstes) return -1; return a.naechstes.localeCompare(b.naechstes); }); const items = all.map(e => { const meta = PFLEGE_META[e._typ]; const ampel = e.naechstes ? _impfAmpel(e.naechstes) : null; const interv = _intervallLabel(e.intervall_tage); return `
${ampel ? `
` : ''}
${UI.icon(meta.icon)} ${UI.escape(e.bezeichnung || meta.label)}
${meta.label}${e.datum ? ` · zuletzt ${UI.time.format(e.datum + 'T00:00:00')}` : ''}${interv ? ` · ${interv}` : ''}
${e.naechstes ? `
Nächste: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
` : ''}
`; }).join(''); return addButtons + `
${items}
`; } // ---------------------------------------------------------- // TIERARZTBESUCHE // ---------------------------------------------------------- function _renderTierarzt(entries) { const addBtn = ``; if (!entries.length) return UI.emptyState({ icon: '', title: 'Noch keine Tierarztbesuche', text: 'Halte alle Tierarztbesuche fest.', action: addBtn }); const items = entries.map(e => { const praxis = _praxen.find(p => p.id === e.tierarzt_id); const praxisName = praxis?.name || e.tierarzt_name || ''; const praxisOrt = praxis ? [praxis.plz, praxis.ort].filter(Boolean).join(' ') : ''; return `
${UI.escape(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')} ${e.kosten != null ? ` · ${Number(e.kosten).toFixed(2)} €` : ''}
${praxisName ? `
${UI.escape(praxisName)}${praxisOrt ? ` · ${UI.escape(praxisOrt)}` : ''}
` : ''} ${e.diagnose ? `
Diagnose: ${UI.escape(e.diagnose)}
` : ''} ${e.notiz ? `
${UI.escape(e.notiz)}
` : ''}
`; }).join(''); return `
${items}
${addBtn}
`; } // ---------------------------------------------------------- // GEWICHT — Großanzeige + SVG-Diagramm // ---------------------------------------------------------- function _renderGewicht(entries) { const addBtn = ``; if (!entries.length) return UI.emptyState({ icon: '', title: 'Noch keine Gewichtseinträge', action: addBtn }); const sorted = [...entries].sort((a, b) => a.datum.localeCompare(b.datum)); const latest = sorted[sorted.length - 1]; const prev = sorted.length >= 2 ? sorted[sorted.length - 2] : null; const delta = prev ? (parseFloat(latest.wert) - parseFloat(prev.wert)) : null; const deltaHtml = delta !== null ? (() => { const sign = delta > 0 ? '+' : ''; const color = delta > 0 ? 'var(--c-warning)' : delta < 0 ? 'var(--c-success)' : 'var(--c-text-muted)'; const arrow = delta > 0 ? '' : delta < 0 ? '' : ''; return `
${arrow} ${sign}${delta.toFixed(1)} kg seit letzter Messung
`; })() : ''; const chartEntries = _data['gewicht_chart'] || []; const chart = _renderWeightChart(chartEntries); const items = sorted.slice().reverse().map(e => `
${UI.time.format(e.datum + 'T00:00:00')} ${e.wert} ${e.einheit || 'kg'}
${e.notiz ? `
${UI.escape(e.notiz)}
` : ''}
`).join(''); return `
${latest.wert} kg
${deltaHtml}
${chart ? `
Gewichtsverlauf ${UI.help('Wird aus allen Einträgen mit Gewichtsangabe berechnet.')}
${chart}
` : ''}
${items}
${addBtn}
`; } function _weightChart(entries) { const W = 340, H = 160, PX = 36, PY = 16, PB = 28; const vals = entries.map(e => parseFloat(e.wert)); const minV = Math.min(...vals); const maxV = Math.max(...vals); const range = maxV - minV || 1; const innerW = W - PX * 2; const innerH = H - PY - PB; const toX = i => PX + (i / (entries.length - 1)) * innerW; const toY = v => PY + (1 - (v - minV) / range) * innerH; const pts = entries.map((e, i) => [toX(i), toY(parseFloat(e.wert))]); // Smooth bezier path const linePath = pts.reduce((acc, [x, y], i) => { if (i === 0) return `M ${x},${y}`; const [px, py] = pts[i - 1]; const cpx = (px + x) / 2; return `${acc} C ${cpx},${py} ${cpx},${y} ${x},${y}`; }, ''); const fillPath = `${linePath} L ${pts[pts.length - 1][0]},${H - PB} L ${pts[0][0]},${H - PB} Z`; // Grid lines const gridLines = [0, 0.5, 1].map(frac => { const v = maxV - frac * range; const y = toY(v); return ` ${v.toFixed(1)} `; }).join(''); // X-axis labels const xIdxs = entries.length <= 3 ? entries.map((_, i) => i) : [0, Math.round((entries.length - 1) / 2), entries.length - 1]; const xLabels = [...new Set(xIdxs)].map(i => { const [m, d] = entries[i].datum.slice(5).split('-'); return `${d}.${m}.`; }).join(''); // Dots const dots = pts.map(([x, y], i) => { const isLast = i === pts.length - 1; return ``; }).join(''); const gId = `wg${Math.random().toString(36).slice(2, 7)}`; return ` ${gridLines} ${dots} ${xLabels} `; } // ---------------------------------------------------------- // GEWICHTSVERLAUF-CHART (dedizierter Endpoint /health/gewicht) // ---------------------------------------------------------- function _renderWeightChart(entries) { // entries: [{datum, gewicht}, ...] if (!entries || entries.length < 2) { return '

Mindestens 2 Gewichtseinträge für den Verlauf nötig.

'; } const W = 300, H = 120, PAD = 24; const weights = entries.map(e => e.gewicht); const min = Math.min(...weights), max = Math.max(...weights); const range = max - min || 1; // x: gleichmäßig verteilt, y: normalisiert const pts = entries.map((e, i) => { const x = PAD + (i / (entries.length - 1)) * (W - 2 * PAD); const y = H - PAD - ((e.gewicht - min) / range) * (H - 2 * PAD); return { x, y, ...e }; }); const polyline = pts.map(p => `${p.x},${p.y}`).join(' '); const area = `${pts[0].x},${H - PAD} ` + polyline + ` ${pts[pts.length - 1].x},${H - PAD}`; // Datenpunkte + Tooltips als title-Elemente const circles = pts.map(p => ` ${p.datum}: ${p.gewicht} kg ` ).join(''); const gId = `wg${Math.random().toString(36).slice(2, 7)}`; return `
Gewichtsverlauf
${circles} ${min} ${max}
${entries[0].datum} ${entries[entries.length - 1].datum}
`; } // ---------------------------------------------------------- // LÄUFIGKEIT — Timeline + Vorhersage // ---------------------------------------------------------- function _renderLaeufigkeit(entries) { const addBtn = ``; const sorted = [...entries].sort((a, b) => b.datum.localeCompare(a.datum)); // neueste zuerst // Durchschnittlicher Abstand berechnen let avgInterval = null; if (sorted.length >= 2) { const asc = [...sorted].reverse(); const diffs = []; for (let i = 1; i < asc.length; i++) { diffs.push(Math.round((new Date(asc[i].datum) - new Date(asc[i-1].datum)) / 86400000)); } avgInterval = Math.round(diffs.reduce((a, b) => a + b, 0) / diffs.length); } // Nächste vorhergesagte Läufigkeit const last = sorted[0]; let nextPrediction = null; if (last?.naechstes) { nextPrediction = last.naechstes; } else if (last?.datum && (last?.intervall_tage || avgInterval)) { const iv = last.intervall_tage || avgInterval; const d = new Date(last.datum); d.setDate(d.getDate() + iv); nextPrediction = d.toISOString().slice(0, 10); } // Banner für nächste Läufigkeit let banner = ''; if (nextPrediction) { const ampel = _impfAmpel(nextPrediction); const tage = Math.ceil((new Date(nextPrediction) - Date.now()) / 86400000); const label = tage < 0 ? `Überfällig seit ${Math.abs(tage)} Tagen` : tage === 0 ? 'Könnte heute beginnen' : tage <= 14 ? `In ${tage} Tagen` : UI.time.format(nextPrediction + 'T00:00:00'); banner = `
${UI.icon('gender-female')}
Nächste Läufigkeit erwartet
${label} ${avgInterval ? ` · Ø ${avgInterval} Tage Abstand` : ''}
`; } if (!sorted.length) return ` ${UI.emptyState({ icon: UI.icon('gender-female'), title: 'Noch keine Läufigkeit eingetragen', text: 'Trage Läufigkeiten ein, um den Zyklus zu verfolgen.', action: addBtn, })}`; const items = sorted.map((e, i) => { const prev = sorted[i + 1]; const interval = prev ? Math.round((new Date(e.datum) - new Date(prev.datum)) / 86400000) : null; return `
${UI.icon('gender-female')}
Läufigkeit · ${UI.time.format(e.datum + 'T00:00:00')}
${e.wert ? `Dauer: ${e.wert} Tage` : 'Dauer nicht angegeben'} ${interval ? ` · Abstand zur Vorherigen: ${interval} Tage` : ''}
${e.notiz ? `
${UI.escape(e.notiz)}
` : ''}
`; }).join(''); return ` ${banner}
${items}
${addBtn}
`; } // ---------------------------------------------------------- // MEDIKAMENTE // ---------------------------------------------------------- function _renderMedikamente(entries) { const addBtn = ``; if (!entries.length) return UI.emptyState({ icon: '', title: 'Noch keine Medikamente', action: addBtn }); const aktive = entries.filter(e => e.aktiv); const inaktive = entries.filter(e => !e.aktiv); const renderGroup = (items, label) => items.length ? ` ${items.map(e => `
${UI.escape(e.bezeichnung)}
${e.dosierung ? UI.escape(e.dosierung) : ''} ${e.haeufigkeit ? ` · ${UI.escape(e.haeufigkeit)}` : ''} ${e.bis_datum ? ` · bis ${UI.time.format(e.bis_datum + 'T00:00:00')}` : ''}
${e.notiz ? `
${UI.escape(e.notiz)}
` : ''}
`).join('')} ` : ''; return `
${renderGroup(aktive, ' Aktuelle Medikamente')} ${renderGroup(inaktive, 'Vergangene Medikamente')}
${addBtn}
`; } // ---------------------------------------------------------- // ALLERGIEN // ---------------------------------------------------------- function _renderAllergien(entries) { const addBtn = ``; if (!entries.length) return UI.emptyState({ icon: '', title: 'Noch keine Allergien eingetragen', action: addBtn }); const SCHWEREGRAD = { leicht: '🟡', mittel: '🟠', schwer: '🔴' }; const items = entries.map(e => `
${e.schweregrad ? SCHWEREGRAD[e.schweregrad] || '' : ''} ${UI.escape(e.bezeichnung)}
Erstmals: ${UI.time.format(e.datum + 'T00:00:00')} ${e.schweregrad ? ` · Schweregrad: ${UI.escape(e.schweregrad)}` : ''}
${e.reaktion ? `
Reaktion: ${UI.escape(e.reaktion)}
` : ''} ${e.notiz ? `
${UI.escape(e.notiz)}
` : ''}
`).join(''); return `
${items}
${addBtn}
`; } // ---------------------------------------------------------- // DOKUMENTE // ---------------------------------------------------------- function _renderDokumente(entries) { const addBtn = ``; if (!entries.length) return UI.emptyState({ icon: '', title: 'Noch keine Dokumente', text: 'Lade Impfpässe, Befunde und mehr hoch.', action: addBtn }); const items = entries.map(e => { // media_items bevorzugen, legacy datei_url als Fallback const mediaList = e.media_items?.length ? e.media_items : (e.datei_url ? [{ id: null, url: e.datei_url, media_type: e.datei_typ || 'image' }] : []); const firstImg = mediaList.find(m => m.media_type !== 'pdf'); const hasPdf = mediaList.some(m => m.media_type === 'pdf'); const count = mediaList.length; return `
${firstImg ? `Vorschau` : `
`}
${UI.escape(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')} ${count > 1 ? ` · ${count} Dateien` : ''}
${e.notiz ? `
${UI.escape(e.notiz)}
` : ''} ${count ? `
${mediaList.slice(0, 3).map(m => m.media_type === 'pdf' ? ` PDF ` : ` Bild ` ).join('')}
` : `Noch keine Datei hochgeladen`}
`; }).join(''); return `
${items}
${addBtn}
${_renderBefundeSection()}`; } // ---------------------------------------------------------- // EVENTS BINDEN // ---------------------------------------------------------- // Sucht einen Eintrag in der/den Liste(n) des aktiven Tabs. // Im Pflege-Tab sind die Einträge auf mehrere Typ-Listen verteilt. function _entriesForActiveTab() { if (_activeTab === 'pflege') return PFLEGE_TYPEN.flatMap(t => _data[t] || []); return _data[_activeTab] || []; } function _bindTabEvents(content) { content.querySelectorAll('[data-action="add-entry"]').forEach(btn => { btn.addEventListener('click', () => _showForm(null, _activeTab)); }); // Pflege: pro-Typ-Button "+ Routine" → Formular mit festem Typ content.querySelectorAll('[data-action="add-routine"]').forEach(btn => { btn.addEventListener('click', () => _showForm(null, btn.dataset.typ)); }); // Pflege: Routine als erledigt markieren → Backend schreibt naechstes fort content.querySelectorAll('[data-action="routine-erledigt"]').forEach(btn => { btn.addEventListener('click', async e => { e.stopPropagation(); const id = parseInt(btn.dataset.id); await UI.asyncButton(btn, async () => { const saved = await API.health.complete(_appState.activeDog.id, id); const list = _data[saved.typ]; if (list) { const idx = list.findIndex(x => x.id === id); if (idx !== -1) list[idx] = saved; } _renderTab(); _renderErinnerungen(); UI.toast.success('Als erledigt eingetragen.'); }); }); }); content.querySelectorAll('[data-action="open-entry"]').forEach(card => { const id = parseInt(card.dataset.id); const entry = _entriesForActiveTab().find(e => e.id === id); if (entry) card.addEventListener('click', () => _activeTab === 'pflege' ? _showForm(entry, entry.typ) : _openDetail(entry)); }); content.querySelectorAll('[data-action="open-note"]').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); const id = parseInt(btn.dataset.entryId); const label = btn.dataset.label || ''; UI.noteModal('health', id, label, null); }); }); // Praxis öffnen → Detail-Modal mit Bewertungen content.querySelectorAll('[data-action="open-praxis"]').forEach(el => { el.addEventListener('click', () => { const id = parseInt(el.dataset.praxisId); const p = _praxen.find(x => x.id === id); if (p) _showPraxisDetail(p); }); }); // Praxis bearbeiten content.querySelectorAll('[data-action="edit-praxis"]').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.dataset.praxisId); const p = _praxen.find(x => x.id === id); if (p) _showPraxForm(p); }); }); // Bewertung abgeben content.querySelectorAll('[data-action="bewerten"]').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.dataset.praxisId); const p = _praxen.find(x => x.id === id); if (p) _showBewertungModal(p); }); }); // Dokument löschen content.querySelectorAll('[data-action="delete-dok"]').forEach(btn => { btn.addEventListener('click', async () => { const id = parseInt(btn.dataset.id); const dogId = _appState.activeDog.id; const ok = await UI.modal.confirm({ title: 'Dokument löschen', text: 'Die Datei wird unwiderruflich gelöscht.', confirmText: 'Löschen', danger: true, }); if (!ok) return; await UI.asyncButton(btn, async () => { await API.health.deleteDocument(dogId, id); const list = _data[_activeTab] || []; const entry = list.find(e => e.id === id); if (entry) { entry.datei_url = null; entry.datei_typ = null; } _renderTab(); UI.toast.success('Dokument gelöscht.'); }); }); }); // Praxis hinzufügen content.querySelector('[data-action="add-praxis"]') ?.addEventListener('click', () => _showPraxForm(null)); // Favorit-Toggle für Praxen content.querySelectorAll('[data-action="toggle-fav"]').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const vetId = parseInt(btn.dataset.praxisId); await UI.asyncButton(btn, async () => { const res = await API.tieraerzte.toggleFavorite(vetId); if (res.is_favorite) { _favoritVet = _praxen.find(p => p.id === vetId) || null; UI.toast.success('Als Favorit-Tierarzt gespeichert.'); } else { _favoritVet = null; UI.toast.success('Favorit entfernt.'); } // is_favorite in _praxen aktualisieren _praxen = _praxen.map(p => ({ ...p, is_favorite: p.id === vetId ? res.is_favorite : false })); const elFav = _container.querySelector('#health-mein-tierarzt'); if (elFav) _renderMeinTierarztKachel(elFav); _renderTab(); }); }); }); // Befunde & Dokumente if (_activeTab === 'dokument') { _bindBefundeEvents(content); } } // ---------------------------------------------------------- // DETAIL-ANSICHT // ---------------------------------------------------------- // Tab-Info (Icon + Label) für einen Typ — kennt auch die Pflege-Routine-Typen, // die keinen eigenen Tab haben (sie liegen im gebündelten 'pflege'-Tab). function _typInfo(typ) { const meta = PFLEGE_META[typ]; if (meta) return { icon: ``, label: meta.label, }; return _getTabs().find(t => t.key === typ) || BASE_TABS[0]; } function _openDetail(entry) { const tabInfo = _typInfo(entry.typ); const fields = _detailFields(entry); // Media-Items zusammenstellen (neue + legacy) const mediaItems = entry.media_items?.length ? entry.media_items : (entry.datei_url ? [{ id: null, url: entry.datei_url, media_type: entry.datei_typ || 'image' }] : []); const mediaHtml = mediaItems.length ? `` : ''; const body = `
${fields} ${mediaHtml}
`; const modalTitle = entry.typ === 'gewicht' ? `${tabInfo.icon} ${entry.wert} ${entry.einheit || 'kg'}` : entry.typ === 'laeufigkeit' ? `${tabInfo.icon} Läufigkeit ${UI.time.format(entry.datum + 'T00:00:00')}` : `${tabInfo.icon} ${UI.escape(entry.bezeichnung)}`; UI.modal.open({ title: modalTitle, body }); document.getElementById('health-detail-edit')?.addEventListener('click', () => { UI.modal.close(); _showForm(entry, entry.typ); }); } function _detailFields(e) { const rows = []; if (e.datum) rows.push(['Datum', UI.time.format(e.datum + 'T00:00:00')]); if (e.typ === 'laeufigkeit' && e.wert) rows.push(['Dauer', `${e.wert} Tage`]); if (e.naechstes) rows.push([e.typ === 'laeufigkeit' ? 'Nächste erwartet' : 'Nächstes', UI.time.format(e.naechstes + 'T00:00:00')]); if (e.typ === 'laeufigkeit' && e.deckdatum) rows.push(['Deckdatum', UI.time.format(e.deckdatum + 'T00:00:00')]); if (e.typ === 'laeufigkeit' && e.wurftermin) { const wurfDate = new Date(e.wurftermin + 'T00:00:00'); const today = new Date(); today.setHours(0,0,0,0); const diffDays = Math.round((wurfDate - today) / 86400000); const zukunft = diffDays > 0 ? ` in ${diffDays} Tagen` : ''; rows.push(['Wurftermin', UI.time.format(e.wurftermin + 'T00:00:00') + zukunft]); } if (e.tierarzt_id) { const praxis = _praxen.find(p => p.id === e.tierarzt_id); if (praxis) { const adresse = [praxis.strasse, [praxis.plz, praxis.ort].filter(Boolean).join(' ')].filter(Boolean).join(', '); const tel = praxis.telefon ? ` · ${UI.escape(praxis.telefon)}` : ''; const oh = praxis.opening_hours ? `
🕐 ${UI.escape(_fmtOeffnungszeiten(praxis.opening_hours))}` : ''; rows.push(['Praxis', ` ${UI.escape(praxis.name)}${adresse ? `
${UI.escape(adresse)}${tel}` : tel}${oh}`]); } } else if (e.tierarzt_name) { rows.push(['Tierarzt', UI.escape(e.tierarzt_name)]); } if (e.charge_nr) rows.push(['Charge-Nr.', UI.escape(e.charge_nr)]); if (e.kosten != null) rows.push(['Kosten', `${Number(e.kosten).toFixed(2)} €`]); if (e.diagnose) rows.push(['Diagnose', UI.escape(e.diagnose)]); if (e.wert) rows.push(['Gewicht', `${e.wert} ${e.einheit || 'kg'}`]); if (e.dosierung) rows.push(['Dosierung', UI.escape(e.dosierung)]); if (e.haeufigkeit) rows.push(['Häufigkeit', UI.escape(e.haeufigkeit)]); if (e.bis_datum) rows.push(['Bis', UI.time.format(e.bis_datum + 'T00:00:00')]); if (e.schweregrad) rows.push(['Schweregrad',UI.escape(e.schweregrad)]); if (e.reaktion) rows.push(['Reaktion', UI.escape(e.reaktion)]); if (e.notiz) rows.push(['Notiz', UI.escape(e.notiz)]); return `
${ rows.map(([k, v]) => `
${k}
${v}
`).join('') }
`; } // ---------------------------------------------------------- // FORMULAR — Neu / Bearbeiten // ---------------------------------------------------------- function _showForm(entry, typ) { const isEdit = !!(entry?.id); const today = new Date().toISOString().slice(0, 10); const t = typ || _activeTab; const commonFields = ` ${t !== 'gewicht' && t !== 'laeufigkeit' ? `
` : ''}
`; const extraFields = _extraFormFields(entry, t); const notizField = `
`; // Multi-Upload-Bereich — zeige vorhandene media_items + neuen Upload const existingMedia = (entry?.media_items || []); const legacyFile = (!existingMedia.length && entry?.datei_url) ? [{ id: null, url: entry.datei_url, media_type: entry.datei_typ || 'image', _legacy: true }] : []; const allMedia = [...existingMedia, ...legacyFile]; const mediaThumbsHtml = allMedia.map(m => { const isImg = m.media_type !== 'pdf'; const removeBtn = m.id ? `` : ''; return `
${isImg ? `Vorschau` : `
PDF
`} ${removeBtn}
`; }).join(''); const uploadField = `
${mediaThumbsHtml}
`; const body = `
${commonFields} ${extraFields} ${notizField} ${uploadField}
`; const footer = `
${isEdit ? `` : ''}
`; const tabInfo = _typInfo(t); UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body, footer }); const form = document.getElementById('health-form'); setTimeout(() => { form?.querySelector('[name="bezeichnung"]')?.focus(); // "Praxis anlegen" Button im Formular form?.querySelector('[data-action="goto-praxen"]')?.addEventListener('click', () => { UI.modal.close(); _activeTab = 'praxen'; _renderTab(); }); // File-Input: Vorschau für ausstehende Uploads const fileInput = document.getElementById('health-file-input'); const pendingBox = document.getElementById('health-file-pending'); if (fileInput && pendingBox) { fileInput.addEventListener('change', () => { pendingBox.innerHTML = ''; Array.from(fileInput.files || []).forEach(f => { const isPdf = f.name.toLowerCase().endsWith('.pdf'); const thumb = document.createElement('div'); thumb.className = 'health-media-thumb health-media-thumb--pending'; if (isPdf) { thumb.innerHTML = `
PDF
${UI.escape(f.name.slice(0, 18))}`; } else { const img = document.createElement('img'); img.src = URL.createObjectURL(f); thumb.appendChild(img); } pendingBox.appendChild(thumb); }); }); } // X-Buttons für vorhandene Media-Items document.querySelectorAll('#health-media-grid .health-media-remove').forEach(btn => { btn.addEventListener('click', async () => { const mediaId = parseInt(btn.dataset.mediaId); const dogId = _appState.activeDog.id; if (!mediaId || !entry?.id) return; try { await API.health.deleteMedia(dogId, entry.id, mediaId); // Aus entry.media_items entfernen if (entry.media_items) entry.media_items = entry.media_items.filter(m => m.id !== mediaId); btn.closest('.health-media-thumb').remove(); // Auch in _data aktualisieren const list = _data[t] || []; const idx = list.findIndex(x => x.id === entry.id); if (idx !== -1) list[idx].media_items = (list[idx].media_items || []).filter(m => m.id !== mediaId); UI.toast.success('Datei entfernt.'); } catch (err) { UI.toast.error('Fehler beim Löschen.'); } }); }); }, 150); document.getElementById('health-form-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('health-form-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Eintrag löschen?', message: 'Nicht rückgängig.', confirmText: 'Löschen', danger: true, }); if (ok) { try { await API.health.delete(_appState.activeDog.id, entry.id); _data[t] = (_data[t] || []).filter(e => e.id !== entry.id); UI.modal.close(); _renderTab(); UI.toast.success('Eintrag gelöscht.'); } catch (err) { UI.toast.error(err.message || 'Fehler.'); } } }); form.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector('[form="health-form"][type="submit"]') || form.querySelector('[type="submit"]'); const fd = UI.formData(form); await UI.asyncButton(btn, async () => { const payload = _buildPayload(fd, t); let saved; if (isEdit) { saved = await API.health.update(_appState.activeDog.id, entry.id, payload); const idx = (_data[t] || []).findIndex(x => x.id === entry.id); if (idx !== -1) _data[t][idx] = saved; UI.toast.success('Gespeichert.'); } else { saved = await API.health.create(_appState.activeDog.id, { ...payload, typ: t }); if (!_data[t]) _data[t] = []; _data[t].unshift(saved); UI.toast.success('Eintrag erstellt.'); } // Gewicht im App-State aktualisieren (für neuen Eintrag UND bei Bearbeitung) if (t === 'gewicht' && saved.wert) { _appState.activeDog.gewicht_kg = saved.wert; _appState.dogs = _appState.dogs.map(d => d.id === _appState.activeDog.id ? { ...d, gewicht_kg: saved.wert } : d ); } // Multi-File-Upload const fileInput = form.querySelector('[name="datei_neu"]'); const files = fileInput ? Array.from(fileInput.files || []) : []; if (files.length) { const dogId = _appState.activeDog.id; if (!saved.media_items) saved.media_items = []; for (const f of files) { try { const toUpload = await API.compressImage(f); const fd = new FormData(); fd.append('file', toUpload); const res = await API.health.uploadMedia(dogId, saved.id, fd); saved.media_items.push({ id: res.id, url: res.url, media_type: res.media_type }); // Rückwärtskompatibilität: erste Datei auch als datei_url sichern if (!saved.datei_url) { saved.datei_url = res.url; saved.datei_typ = res.media_type; } } catch { UI.toast.warning(`Datei "${f.name}" konnte nicht hochgeladen werden.`); } } } UI.modal.close(); _renderTab(); }); }); } function _formPlaceholder(typ) { const ph = { impfung: 'z.B. Tollwut, DHPP, Leptospirose', tierarzt: 'z.B. Vorsorgeuntersuchung, Impfung', gewicht: '', medikament: 'z.B. Frontline, Milbemax', allergie: 'z.B. Hühnchen, Gras, Hausstaub', dokument: 'z.B. Impfpass, Blutbild', laeufigkeit: 'Läufigkeit', parasit: 'z.B. Frontline, Seresto-Halsband', krallen: 'z.B. Krallen kürzen', fellpflege: 'z.B. Bürsten, Trimmen, Baden', }; return ph[typ] || ''; } // Intervall-Auswahl für wiederkehrende Einträge function _intervallField(entry) { const v = entry?.intervall_tage; const opts = [ [null, 'Einmalig'], [30, 'Monatlich (30 Tage)'], [60, 'Alle 2 Monate'], [90, 'Vierteljährlich (90 Tage)'], [180, 'Halbjährlich'], [365, 'Jährlich'], ]; return `
`; } // Wiederverwendbares Praxis-Dropdown für alle Formulare function _praxisSelectField(entry) { const aktivePraxen = _praxen.filter(p => p.aktiv); if (!aktivePraxen.length) return ''; return `
`; } function _extraFormFields(entry, typ) { switch (typ) { case 'impfung': return `
${_intervallField(entry)}
${_praxisSelectField(entry)}
`; case 'entwurmung': return `
${_intervallField(entry)}
${_praxisSelectField(entry)} `; case 'parasit': case 'krallen': case 'fellpflege': return `
${_intervallField(entry)}
`; case 'tierarzt': { const aktivePraxen = _praxen.filter(p => p.aktiv); const praxisField = aktivePraxen.length ? `
` : `
Noch keine Praxis angelegt —
`; return ` ${praxisField}
`; } case 'gewicht': return `
`; case 'medikament': return `
${_intervallField(entry)}
${_praxisSelectField(entry)}
`; case 'allergie': return `
`; case 'laeufigkeit': { const prevCycles = (_data['laeufigkeit'] || []).filter(e => e !== entry && e?.datum); const sorted = [...prevCycles].sort((a, b) => a.datum.localeCompare(b.datum)); const lastCycle = sorted[sorted.length - 1]; // Abstand zur letzten Läufigkeit (in Tagen) let daysSinceLast = null; if (lastCycle) { daysSinceLast = Math.round((new Date() - new Date(lastCycle.datum)) / 86400000); } // Durchschnittlicher Zyklus aus ≥2 Einträgen, sonst gemessener Abstand let avgInterval = 0; if (sorted.length >= 2) { const intervals = []; for (let i = 1; i < sorted.length; i++) { intervals.push(Math.round((new Date(sorted[i].datum) - new Date(sorted[i-1].datum)) / 86400000)); } avgInterval = Math.round(intervals.reduce((a, b) => a + b, 0) / intervals.length); } else if (daysSinceLast !== null) { avgInterval = daysSinceLast; // erster gemessener Abstand als Vorschlag } const defaultInterval = avgInterval || (entry?.intervall_tage) || 180; const lastInfo = lastCycle ? `
Letzte Läufigkeit: ${UI.time.format(lastCycle.datum + 'T00:00:00')} — vor ${daysSinceLast} Tagen
` : ''; return ` ${lastInfo}
${['breeder', 'admin'].includes(_appState.user?.rolle) ? `
Zucht (optional)
` : ''} `; } default: return ''; } } function _buildPayload(fd, typ) { const p = { bezeichnung: fd.bezeichnung || null, datum: fd.datum || null, notiz: fd.notiz || null, naechstes: fd.naechstes || null, tierarzt_name: fd.tierarzt_name || null, charge_nr: fd.charge_nr || null, diagnose: fd.diagnose || null, dosierung: fd.dosierung || null, haeufigkeit: fd.haeufigkeit || null, bis_datum: fd.bis_datum || null, schweregrad: fd.schweregrad || null, reaktion: fd.reaktion || null, }; if (fd.wert) { p.wert = parseFloat(fd.wert.toString().replace(',', '.')); if (typ === 'gewicht') p.bezeichnung = `${p.wert} kg`; } if (typ === 'laeufigkeit') { p.bezeichnung = p.bezeichnung || 'Läufigkeit'; p.deckdatum = fd.deckdatum || null; p.wurftermin = fd.wurftermin || null; } if (fd.kosten) p.kosten = parseFloat(fd.kosten.toString().replace(',', '.')); if (fd.tierarzt_id) { p.tierarzt_id = parseInt(fd.tierarzt_id); // Praxisname auch als tierarzt_name sichern (bleibt lesbar wenn Praxis inaktiv/gelöscht) const praxis = _praxen.find(x => x.id === p.tierarzt_id); if (praxis) p.tierarzt_name = praxis.name; } if (typ === 'medikament') { p.aktiv = 'aktiv' in fd ? 1 : 0; } p.intervall_tage = fd.intervall_tage ? parseInt(fd.intervall_tage) : null; // Gewicht-Einheit p.einheit = fd.einheit || 'kg'; return p; } // ---------------------------------------------------------- // PRAXEN — Liste // ---------------------------------------------------------- function _renderPraxen() { const addBtn = ``; const aktive = _praxen.filter(p => p.aktiv); const inaktive = _praxen.filter(p => !p.aktiv); if (!_praxen.length) return UI.emptyState({ icon: '', title: 'Noch keine Praxis eingetragen', text: 'Trage deine Tierarztpraxis ein für schnellen Zugriff.', action: addBtn }); const renderCard = p => { const isFav = _favoritVet?.id === p.id || p.is_favorite; const hasRating = p.anz_bewertungen > 0; const stars = hasRating ? _renderStarsReadonly(p.avg_rating) : ''; const ratingHtml = hasRating ? `
${stars} ${p.avg_rating.toFixed(1)} (${p.anz_bewertungen} Bew.)
` : `
Noch keine Bewertungen
`; return `
${UI.escape(p.name)} ${!p.aktiv ? ' · Ehemalig' : ''}
${(p.strasse || p.plz || p.ort) ? `
${[p.strasse, [p.plz, p.ort].filter(Boolean).join(' ')].filter(Boolean).map(_esc).join(', ')}
` : ''} ${p.opening_hours ? `
${UI.escape(_fmtOeffnungszeiten(p.opening_hours))}
` : ''} ${ratingHtml}
${p.telefon ? ` Anrufen ` : ''} ${p.notfall_telefon ? ` Notfall ` : ''}
`}; const favCard = _favoritVet ? `
${UI.icon('heart')} Mein Tierarzt
${renderCard(_favoritVet)}
` : ''; const ohneGesetzt = aktive.filter(p => p.id !== _favoritVet?.id); return `
${addBtn}
${favCard}
${ohneGesetzt.map(renderCard).join('')} ${inaktive.length ? `

Ehemalige Praxen

${inaktive.map(renderCard).join('')}
` : ''}
`; } // ---------------------------------------------------------- // PRAXEN — Sterne-Hilfs-Funktionen // ---------------------------------------------------------- /** Rendert 5 Sterne (readonly, filled bis `rating`). */ function _renderStarsReadonly(rating) { const full = Math.round(rating); return Array.from({ length: 5 }, (_, i) => { const filled = i < full; return ``; }).join(''); } /** Rendert 5 klickbare Sterne mit data-val. */ function _renderStarsInput(name, current) { return `
${Array.from({ length: 5 }, (_, i) => { const val = i + 1; const filled = current >= val; return ``; }).join('')}
`; } // ---------------------------------------------------------- // PRAXEN — Detail-Modal (Bewertungen anzeigen) // ---------------------------------------------------------- async function _showPraxisDetail(praxis) { // Erst mit Lade-Spinner öffnen, dann Daten laden UI.modal.open({ title: UI.escape(praxis.name), body: `
`, footer: ` `, }); document.getElementById('detail-bewerten-btn') ?.addEventListener('click', () => { UI.modal.close(); _showBewertungModal(praxis); }); let data; try { data = await API.tieraerzte.bewertungen(praxis.id); } catch { UI.modal.open({ title: praxis.name, body: '

Bewertungen konnten nicht geladen werden.

' }); return; } const { avg_rating, anz_bewertungen, verteilung, kommentare } = data; // Balkendiagramm const balken = [5, 4, 3, 2, 1].map(s => { const n = verteilung[String(s)] || 0; const pct = anz_bewertungen > 0 ? Math.round((n / anz_bewertungen) * 100) : 0; return `
${s}
${n}
`; }).join(''); const kommentarHtml = kommentare.length ? kommentare.map(k => `
${_renderStarsReadonly(k.gesamt)} ${k.created_at ? k.created_at.slice(0, 10) : ''}
${k.wartezeit || k.freundlichkeit || k.kompetenz ? `
${k.wartezeit ? `Wartezeit: ${_renderStarsReadonly(k.wartezeit)}` : ''} ${k.freundlichkeit ? `Freundlichkeit: ${_renderStarsReadonly(k.freundlichkeit)}` : ''} ${k.kompetenz ? `Kompetenz: ${_renderStarsReadonly(k.kompetenz)}` : ''}
` : ''}

${UI.escape(k.text || '')}

`).join('') : `

Noch keine Kommentare.

`; const bewBody = anz_bewertungen === 0 ? `

Noch keine Bewertungen — sei der Erste!

` : `
${avg_rating.toFixed(1)}
${_renderStarsReadonly(avg_rating)}
${anz_bewertungen} Bewertung${anz_bewertungen !== 1 ? 'en' : ''}
${balken}
${kommentarHtml}
`; // Modal-Body aktualisieren (ohne Modal neu zu öffnen) const modalBody = document.querySelector('.modal-body'); if (modalBody) modalBody.innerHTML = bewBody; } // ---------------------------------------------------------- // PRAXEN — Bewertungs-Modal // ---------------------------------------------------------- async function _showBewertungModal(praxis) { // Ggf. bestehende Bewertung laden let existing = null; try { existing = await API.tieraerzte.meineBewertung(praxis.id); } catch { /* ok */ } const cur = existing || {}; const body = `
${_renderStarsInput('gesamt', cur.gesamt || 0)}
${_renderStarsInput('wartezeit', cur.wartezeit || 0)}
${_renderStarsInput('freundlichkeit', cur.freundlichkeit || 0)}
${_renderStarsInput('kompetenz', cur.kompetenz || 0)}
max. 500 Zeichen
`; UI.modal.open({ title: `${UI.escape(praxis.name)} bewerten`, body, footer: ` `, }); // Sterne-Interaktion document.querySelectorAll('.bew-stars').forEach(group => { const name = group.dataset.name; const hidden = document.getElementById(`bew-${name}`); const stars = group.querySelectorAll('.bew-star'); const paint = val => { stars.forEach(s => { s.style.color = parseInt(s.dataset.val) <= val ? 'var(--c-warning,#f59e0b)' : 'var(--c-border)'; }); }; stars.forEach(s => { s.addEventListener('mouseover', () => paint(parseInt(s.dataset.val))); s.addEventListener('mouseleave', () => paint(parseInt(hidden.value))); s.addEventListener('click', () => { hidden.value = s.dataset.val; paint(parseInt(s.dataset.val)); }); }); paint(parseInt(hidden.value)); }); // Submit document.getElementById('bew-submit-btn').addEventListener('click', async (e) => { e.preventDefault(); const form = document.getElementById('bew-form'); const gesamt = parseInt(document.getElementById('bew-gesamt').value); if (!gesamt) { UI.toast.error('Bitte vergib mindestens einen Stern für den Gesamteindruck.'); return; } const payload = { gesamt }; const wz = parseInt(document.getElementById('bew-wartezeit').value); const fr = parseInt(document.getElementById('bew-freundlichkeit').value); const ko = parseInt(document.getElementById('bew-kompetenz').value); if (wz) payload.wartezeit = wz; if (fr) payload.freundlichkeit = fr; if (ko) payload.kompetenz = ko; const txt = form.querySelector('textarea[name="text"]').value.trim(); if (txt) payload.text = txt; await UI.asyncButton(document.getElementById('bew-submit-btn'), async () => { const saved = await API.tieraerzte.bewertungAbgeben(praxis.id, payload); // _praxen-Cache aktualisieren _praxen = _praxen.map(p => p.id === praxis.id ? { ...p, avg_rating: saved.avg_rating, anz_bewertungen: saved.anz_bewertungen } : p ); UI.modal.close(); UI.toast.success('Bewertung gespeichert.'); _renderTab(); }); }); } // ---------------------------------------------------------- // PRAXEN — Formular (Neu / Bearbeiten) // ---------------------------------------------------------- function _showPraxForm(praxis) { const isEdit = !!praxis; UI.modal.open({ title: isEdit ? `${praxis.name} bearbeiten` : 'Praxis hinzufügen', body: `
${isEdit ? `
` : ''}
`, footer: ` `, }); document.getElementById('praxis-cancel')?.addEventListener('click', UI.modal.close); // OSM-Lookup: Tierarztpraxen in der Nähe suchen und Öffnungszeiten übernehmen document.getElementById('praxis-osm-lookup')?.addEventListener('click', async btn => { const lookupBtn = document.getElementById('praxis-osm-lookup'); const resultsEl = document.getElementById('praxis-osm-results'); lookupBtn.disabled = true; lookupBtn.textContent = 'Suche…'; try { const pos = await API.getLocation(); const hits = await API.tieraerzte.osmNearby(pos.lat, pos.lon); if (!hits.length) { resultsEl.style.display = 'block'; resultsEl.innerHTML = '

Keine Praxen in der Nähe im OSM-Cache gefunden.

'; } else { resultsEl.style.display = 'block'; resultsEl.innerHTML = hits.map(h => `
${UI.escape(h.name)}
${h.opening_hours_fmt ? `
${UI.escape(h.opening_hours_fmt)}
` : '
Öffnungszeiten unbekannt
'}
${h.distanz_km} km entfernt
`).join(''); resultsEl.querySelectorAll('[data-action="pick-osm"]').forEach(el => { el.addEventListener('click', () => { const nameInput = document.querySelector('[name="name"]'); const ohInput = document.getElementById('praxis-opening-hours'); const telInput = document.querySelector('[name="telefon"]'); if (nameInput && !nameInput.value) nameInput.value = el.dataset.name; if (ohInput) ohInput.value = el.dataset.oh; if (telInput && !telInput.value) telInput.value = el.dataset.phone; resultsEl.style.display = 'none'; UI.toast.success('Daten übernommen.'); }); }); resultsEl.querySelectorAll('[data-action="korrigieren"]').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); _showPoiKorrekturModal(btn.dataset.osmId, btn.dataset.poiName, btn.dataset.currentOh); }); }); } } catch (err) { UI.toast.warning('Standort nicht verfügbar oder kein OSM-Cache in der Nähe.'); } finally { lookupBtn.disabled = false; lookupBtn.textContent = '📍 Aus Karte laden'; } }); document.getElementById('praxis-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector('[form="praxis-form"][type="submit"]') || e.target.querySelector('[type="submit"]'); const fd = UI.formData(e.target); await UI.asyncButton(btn, async () => { const payload = { name: fd.name?.trim(), strasse: fd.strasse || null, plz: fd.plz || null, ort: fd.ort || null, telefon: fd.telefon || null, notfall_telefon: fd.notfall_telefon || null, email: fd.email || null, notizen: fd.notizen || null, ist_notfallpraxis: 'ist_notfallpraxis' in fd, opening_hours: fd.opening_hours || null, }; if (!payload.name) { UI.toast.warning('Bitte einen Namen eingeben.'); return; } let saved; if (isEdit) { payload.aktiv = !('inaktiv' in fd); saved = await API.tieraerzte.update(praxis.id, payload); _praxen = _praxen.map(p => p.id === praxis.id ? saved : p); UI.toast.success('Praxis gespeichert.'); } else { saved = await API.tieraerzte.create(payload); _praxen.push(saved); UI.toast.success(`${saved.name} hinzugefügt.`); } UI.modal.close(); _renderTab(); }); }); } // ---------------------------------------------------------- // SYMPTOM-CHECK // ---------------------------------------------------------- function _renderSymptomCheck(content) { content.innerHTML = `

Beschreibe die Symptome deines Hundes. Die KI gibt eine erste Einschätzung — kein Ersatz für den Tierarzt.

`; content.querySelector('#symptom-submit-btn').addEventListener('click', async function () { const btn = this; const textarea = content.querySelector('#symptom-input'); const resultEl = content.querySelector('#symptom-result'); const symptoms = textarea.value.trim(); if (!symptoms) { UI.toast.warning('Bitte Symptome eingeben.'); return; } await UI.asyncButton(btn, async () => { resultEl.style.display = 'none'; resultEl.innerHTML = ''; let result; try { result = await API.post( `/dogs/${_appState.activeDog.id}/health/symptom-check`, { symptoms } ); } catch (err) { if (err.status === 402) { resultEl.innerHTML = `

Dieses Feature benötigt Ban Yaro Plus oder einen laufenden KI-Server.

`; } else if (err.status === 503) { resultEl.innerHTML = `

KI-Server nicht erreichbar. Bitte später versuchen.

`; } else { UI.toast.error(err.message || 'Fehler bei der Symptomanalyse.'); return; } resultEl.style.display = ''; return; } const DRINGLICHKEIT = { beobachten: { badgeClass: 'badge-success', icon: 'check-circle', label: 'Beobachten' }, tierarzt_heute:{ badgeClass: 'badge-warning', icon: 'warning', label: 'Heute zum Tierarzt' }, tierarzt: { badgeClass: 'badge-warning', icon: 'warning', label: 'Zum Tierarzt' }, tierarzt_sofort:{ badgeClass: 'badge-danger', icon: 'first-aid-kit', label: 'Sofort zum Tierarzt!' }, notfall: { badgeClass: 'badge-danger', icon: 'first-aid-kit', label: 'Notfall — sofort zum Tierarzt!' }, }; const d = DRINGLICHKEIT[result.dringlichkeit] || { badgeClass: 'badge-primary', icon: 'info', label: UI.escape(result.dringlichkeit) }; const hinweiseHtml = (result.hinweise || []).length ? `` : ''; const zumTierarztHtml = result.zum_tierarzt_wenn ? `
Zum Tierarzt wenn: ${UI.escape(result.zum_tierarzt_wenn)}
` : ''; resultEl.innerHTML = `
${d.label}
${result.einschaetzung ? `

${UI.escape(result.einschaetzung)}

` : ''} ${hinweiseHtml} ${zumTierarztHtml} `; resultEl.style.display = ''; }); }); } // ---------------------------------------------------------- // TRANSPONDER-BEARBEITUNG // ---------------------------------------------------------- async function _editTransponder(dog) { const currentNr = dog?.chip_nr || ''; UI.modal.open({ title: 'Transpondernummer', body: `
`, footer: ` `, }); document.getElementById('transponder-save-btn').addEventListener('click', async () => { const nr = document.getElementById('transponder-input').value.trim() || null; const btn = document.getElementById('transponder-save-btn'); UI.setLoading(btn, true); try { await API.dogs.update(dog.id, { chip_nr: nr }); _appState.activeDog.chip_nr = nr; UI.modal.close(); const nrEl = _container.querySelector('#health-transponder-nr'); if (nrEl) nrEl.innerHTML = nr ? `${UI.escape(nr)}` : 'nicht eingetragen'; } catch (e) { UI.setLoading(btn, false); UI.toast('Fehler beim Speichern', 'error'); } }); } // ---------------------------------------------------------- // KI-GESUNDHEITSBERICHTE (gespeicherte automatische Berichte) // ---------------------------------------------------------- async function _loadKiBerichte(dogId, force = false) { const el = _container.querySelector('#health-ki-berichte'); if (!el) return; try { // force=true: Cache-Buster damit SW den neuen Bericht nicht übersieht const berichte = force ? await API.get(`/dogs/${dogId}/health/ki-berichte?_t=${Date.now()}`) : await API.health.kiBerichte(dogId); if (!berichte || berichte.length === 0) return; const neuester = berichte[0]; const datum = neuester.erstellt_at ? new Date(neuester.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : ''; const preview = neuester.bericht.length > 180 ? UI.escape(neuester.bericht.slice(0, 180)) + '…' : UI.escape(neuester.bericht); el.innerHTML = `
KI-Gesundheitsbericht ${datum ? `${datum}` : ''}
${preview}
${berichte.length > 1 ? `
${berichte.length} Berichte gespeichert — zum Öffnen tippen
` : ''}
`; el.querySelector('.health-ki-bericht-banner').addEventListener('click', () => { let idx = 0; const fmtDate = b => b.erstellt_at ? new Date(b.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : ''; function showBericht() { const b = berichte[idx]; const nav = berichte.length > 1 ? `
${idx+1} / ${berichte.length}
` : ''; UI.modal.open({ title: `${UI.icon('star')} KI-Gesundheitsberichte`, body: `${nav}
${fmtDate(b)}
${UI.escape(b.bericht)}
`, }); // Inline-onclick wird von der CSP blockiert → per addEventListener verdrahten. document.querySelector('[data-ki-nav="prev"]') ?.addEventListener('click', () => { if (idx < berichte.length - 1) { idx++; showBericht(); } }); document.querySelector('[data-ki-nav="next"]') ?.addEventListener('click', () => { if (idx > 0) { idx--; showBericht(); } }); } showBericht(); }); } catch (_) { // Silently ignore — Berichte sind optional } } // ---------------------------------------------------------- // KI-ZUSAMMENFASSUNG // ---------------------------------------------------------- // TERMINVORSCHLÄGE // ---------------------------------------------------------- async function _loadTerminvorschlaege(dogId) { const el = _container.querySelector('#health-terminvorschlaege'); if (!el) return; try { const vorschlaege = await API.health.terminvorschlaege(dogId); if (!vorschlaege || !vorschlaege.length) return; const _fmtDatum = iso => new Date(iso + 'T00:00:00').toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' }); el.innerHTML = `
Terminvorschläge
${vorschlaege.map(v => { const badge = v.ueberfaellig ? `Überfällig seit ${_fmtDatum(v.naechstes)}` : `Fällig am ${_fmtDatum(v.naechstes)}`; return `
${UI.escape(v.bezeichnung)}
${UI.escape(v.label)}${v.praxis_name ? ' · ' + UI.escape(v.praxis_name) : ''}
${badge}
Vorschlag
${_fmtDatum(v.datum_vorschlag)}
${v.uhrzeit_vorschlag} Uhr
`; }).join('')}
`; el.querySelectorAll('[data-action="termin-anlegen"]').forEach(btn => { btn.addEventListener('click', async () => { let v; try { v = JSON.parse(btn.dataset.v); } catch { return; } await _terminAnlegen(v, btn); }); }); } catch { /* still show health page if this fails */ } } async function _terminAnlegen(v, btn) { const titel = v.beim_tierarzt ? `${v.label}: ${v.bezeichnung} (Tierarzt)` : `${v.label}: ${v.bezeichnung}`; const beschreibung = v.praxis_name ? `Praxis: ${v.praxis_name}` : v.ueberfaellig ? `Überfällig seit ${v.naechstes}` : `Fällig am ${v.naechstes}`; UI.modal.open({ title: `${UI.icon('calendar-plus')} Termin in Kalender eintragen`, body: `
`, footer: ` `, }); document.getElementById('termin-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('termin-form')?.addEventListener('submit', async e => { e.preventDefault(); const saveBtn = document.querySelector('[form="termin-form"][type="submit"]'); const fd = UI.formData(e.target); await UI.asyncButton(saveBtn, async () => { await API.events.create({ titel: fd.titel, datum: fd.datum, uhrzeit: fd.uhrzeit || null, beschreibung: fd.beschreibung || null, typ: v.beim_tierarzt ? 'tierarzt' : 'sonstiges', lat: v.praxis_lat ?? null, lon: v.praxis_lon ?? null, ort_name: v.praxis_name ?? null, }); UI.modal.close(); UI.toast.success('Termin gespeichert — erscheint in deinem Kalender.'); }); }); } // ---------------------------------------------------------- // MEIN TIERARZT — Kachel // ---------------------------------------------------------- async function _loadMeinTierarzt() { const el = _container.querySelector('#health-mein-tierarzt'); if (!el) return; _renderMeinTierarztKachel(el); } function _renderMeinTierarztKachel(el) { if (!el) return; const vet = _favoritVet; const adresse = vet ? [vet.strasse, [vet.plz, vet.ort].filter(Boolean).join(' ')].filter(Boolean).join(', ') : ''; el.innerHTML = `
Mein Tierarzt
${vet ? `
${UI.escape(vet.name)}
${adresse ? `
${UI.escape(adresse)}
` : ''} ${vet.telefon ? ` ` : ''} ${vet.notfall_telefon ? ` ` : ''} ` : `
Noch kein Tierarzt als Favorit gespeichert.
`}
${vet ? ` ` : ''}
`; el.querySelector('#health-suche-tierarzt-btn')?.addEventListener('click', () => { App.navigate('map', { filter: 'tierarzt' }); }); el.querySelector('#health-remove-fav-btn')?.addEventListener('click', async (e) => { e.stopPropagation(); const btn = e.currentTarget; await UI.asyncButton(btn, async () => { await API.tieraerzte.toggleFavorite(_favoritVet.id); _favoritVet = null; const elAgain = _container.querySelector('#health-mein-tierarzt'); if (elAgain) _renderMeinTierarztKachel(elAgain); UI.toast.success('Tierarzt-Favorit entfernt.'); }); }); } // ---------------------------------------------------------- // BEFUNDE & DOKUMENTE — Sektion (unabhängig von den Tabs) // ---------------------------------------------------------- // Diese Sektion erscheint im "dokument"-Tab als zweite Liste. // Wir ergänzen _renderDokumente um einen Abschnitt unten. function _renderBefundeSection() { const dog = _appState.activeDog; const docs = _healthDocs; const DOC_ICONS = { blutbild: 'drop', roentgen: 'file-text', rezept: 'note', impfausweis:'certificate', sonstiges: 'file-text', }; const DOC_LABELS = { blutbild: 'Blutbild', roentgen: 'Röntgen', rezept: 'Rezept', impfausweis:'Impfausweis', sonstiges: 'Sonstiges', }; const uploadBtn = ` `; const items = docs.length ? docs.map(doc => { const icon = DOC_ICONS[doc.typ] || 'file-text'; const label = DOC_LABELS[doc.typ] || doc.typ; const isImg = !['pdf'].includes(doc.file_type); const datum = doc.datum ? UI.time.format(doc.datum + 'T00:00:00') : ''; return `
${UI.escape(doc.titel)}
${UI.escape(label)}${datum ? ' · ' + datum : ''} ${doc.vet_name ? ' · ' + UI.escape(doc.vet_name) : ''}
${doc.beschreibung ? `
${UI.escape(doc.beschreibung)}
` : ''}
${isImg ? ' Bild öffnen' : ' PDF öffnen'}
`; }).join('') : `

Noch keine Befunde hochgeladen.

`; return `
Befunde & Dokumente
${uploadBtn}
${items}
`; } function _bindBefundeEvents(content) { content.querySelector('#health-docs-upload-btn')?.addEventListener('click', () => { _showBefundUploadModal(); }); content.querySelectorAll('[data-action="delete-hdoc"]').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const docId = parseInt(btn.dataset.docId); const ok = window.confirm('Befund wirklich löschen?'); if (!ok) return; await UI.asyncButton(btn, async () => { await API.healthDocs.delete(docId); _healthDocs = _healthDocs.filter(d => d.id !== docId); _renderTab(); UI.toast.success('Befund gelöscht.'); }); }); }); } function _showBefundUploadModal() { const aktivePraxen = _praxen.filter(p => p.aktiv); const dog = _appState.activeDog; UI.modal.open({ title: ` Befund hochladen`, body: `
${aktivePraxen.length ? `
` : ''}
`, footer: ` `, }); document.getElementById('befund-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('befund-file-input')?.addEventListener('change', function () { const preview = document.getElementById('befund-file-preview'); if (this.files?.length) { const f = this.files[0]; preview.textContent = `${f.name} (${(f.size / 1024 / 1024).toFixed(2)} MB)`; } else { preview.textContent = ''; } }); document.getElementById('befund-form')?.addEventListener('submit', async (e) => { e.preventDefault(); const btn = document.querySelector('[form="befund-form"][type="submit"]'); const form = e.target; const fd = UI.formData(form); const fileInput = form.querySelector('[name="file"]'); const file = fileInput?.files?.[0]; if (!fd.typ) { UI.toast.warning('Bitte Art des Dokuments wählen.'); return; } if (!fd.titel) { UI.toast.warning('Bitte Titel eingeben.'); return; } if (!file) { UI.toast.warning('Bitte eine Datei auswählen.'); return; } if (file.size > 10 * 1024 * 1024) { UI.toast.error('Datei ist zu groß. Maximum: 10 MB.'); return; } await UI.asyncButton(btn, async () => { // Client-Side-Kompression nur wenn Bild (PDFs etc. unverändert durchgereicht) const toUpload = await API.compressImage(file); const formData = new FormData(); formData.append('dog_id', String(dog.id)); formData.append('typ', fd.typ); formData.append('titel', fd.titel); formData.append('beschreibung', fd.beschreibung || ''); formData.append('datum', fd.datum || ''); if (fd.vet_id) formData.append('vet_id', fd.vet_id); formData.append('file', toUpload); try { const doc = await API.healthDocs.upload(formData); _healthDocs.unshift(doc); UI.modal.close(); _renderTab(); UI.toast.success('Befund hochgeladen.'); } catch (err) { UI.toast.error(err.message || 'Fehler beim Hochladen.'); } }); }); } // ---------------------------------------------------------- async function _showKiSummary() { const btn = _container.querySelector('#health-ki-btn'); UI.setLoading(btn, true); try { const res = await API.health.kiZusammenfassung(_appState.activeDog.id); const zusammenfassung = res.zusammenfassung ?? res; if (res.save_error) UI.toast.warning(`Speichern fehlgeschlagen: ${res.save_error}`); else if (res.saved_count !== undefined) UI.toast.info(`${res.saved_count} Bericht(e) in DB`, { duration: 8000 }); UI.modal.open({ title: `${UI.icon('star')} KI-Gesundheitsbericht`, body: `
${UI.escape(zusammenfassung)}
`, }); // Berichte-Liste nach Generierung frisch laden (Cache-Buster) _loadKiBerichte(_appState.activeDog.id, true); } catch (err) { if (err.status === 503) { UI.toast.error('KI ist momentan nicht verfügbar. Bitte später erneut versuchen.'); } else if (err.status === 402) { UI.toast.warning('Diese Funktion ist Teil von Ban Yaro Premium.'); } else { UI.toast.error(err.message || 'Fehler bei der KI-Zusammenfassung.'); } } finally { UI.setLoading(btn, false); } } // ---------------------------------------------------------- // HELPER // ---------------------------------------------------------- function _showPoiKorrekturModal(osmId, poiName, currentOh) { UI.modal.open({ title: 'Öffnungszeiten korrigieren', body: `

Korrektur für ${UI.escape(poiName)}.
Dein Vorschlag wird von einem Moderator geprüft und dann für alle übernommen.

`, footer: ` `, }); document.getElementById('poi-kor-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('poi-korrektur-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector('[form="poi-korrektur-form"][type="submit"]'); const fd = UI.formData(e.target); await UI.asyncButton(btn, async () => { await API.post(`/osm/pois/${encodeURIComponent(osmId)}/edit`, { poi_name: poiName, field: 'opening_hours', new_value: fd.new_value.trim(), }); UI.modal.close(); UI.toast.success('Danke! Dein Vorschlag wird geprüft.'); }); }); } function _fmtOeffnungszeiten(raw) { if (!raw) return ''; if (raw.trim().toLowerCase() === '24/7') return '24/7 geöffnet'; return raw.split(';').map(s => s.trim()).filter(Boolean).join(' · '); } // ---------------------------------------------------------- // NOTIZ-MODAL (custom DOM, kein UI.modal um Konflikte zu vermeiden) // ---------------------------------------------------------- // ---------------------------------------------------------- // KI-TIERARZTFRAGEN // ---------------------------------------------------------- function _showKiTierarzt() { const dog = _appState.activeDog; const dogName = dog?.name || ''; const rasse = dog?.rasse || ''; const placeholder = dogName ? `Welche Symptome zeigt ${dogName}? z.B. frisst nicht, schläft viel, lahmt...` : 'Welche Symptome zeigt dein Hund? z.B. frisst nicht, schläft viel, lahmt...'; UI.modal.open({ title: ' KI-Tierarzt', body: `

Beschreibe die Symptome deines Hundes. Die KI gibt eine erste Orientierung — kein Ersatz für einen echten Tierarzt.

⚠️ Hinweis: Dies ist keine medizinische Diagnose. Bei ernsthaften oder sich verschlechternden Symptomen sofort zum Tierarzt.
`, footer: ` `, }); document.getElementById('ki-tierarzt-submit-btn') .addEventListener('click', async function () { const btn = this; const symptom = document.getElementById('ki-tierarzt-symptom').value.trim(); const resultEl = document.getElementById('ki-tierarzt-result'); if (!symptom) { UI.toast.warning('Bitte Symptome eingeben.'); return; } await UI.asyncButton(btn, async () => { resultEl.style.display = 'none'; resultEl.innerHTML = ''; let result; try { result = await API.post('/ki/tierarzt', { symptom, dog_id: dog?.id || null, dog_name: dogName || null, rasse: rasse || null, }); } catch (err) { if (err.status === 429) { resultEl.innerHTML = `
5 Anfragen pro Tag erreicht. Morgen wieder verfügbar.
`; } else if (err.status === 503) { resultEl.innerHTML = `
KI momentan nicht verfügbar. Bitte später versuchen.
`; } else { UI.toast.error(err.message || 'Fehler bei der KI-Anfrage.'); return; } resultEl.style.display = ''; return; } const antwortHtml = UI.escape(result.antwort) .replace(/\n\n/g, '

') .replace(/\n/g, '
'); const restHtml = result.limit - result.anfragen_heute > 0 ? `

Noch ${result.limit - result.anfragen_heute} von ${result.limit} Anfragen heute verfügbar.

` : `

Tageslimit erreicht. Morgen wieder verfügbar.

`; resultEl.innerHTML = `
Einschätzung

${antwortHtml}

${restHtml}
⚠️ Dies ist keine medizinische Diagnose. Bei ernsthaften Symptomen sofort zum Tierarzt.
`; resultEl.style.display = ''; // Submit-Button ausblenden wenn Limit erschöpft if (result.anfragen_heute >= result.limit) { btn.disabled = true; btn.textContent = 'Limit erreicht'; } }); }); } // ============================================================== // 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.classList.add('list-reminders-banner'); wrap.innerHTML = items.slice(0, 3).map(r => { const overdue = r.ueberfaellig; const modifier = overdue ? 'list-reminder-item--urgent' : r.delta_tage <= 3 ? 'list-reminder-item--warning' : ''; const color = overdue ? 'var(--c-danger,#ef4444)' : r.delta_tage <= 3 ? '#f59e0b' : 'var(--c-primary)'; 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 `
${UI.escape(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 => `
${UI.escape(p.anbieter)}
${p.police_nr ? `
Police: ${UI.escape(p.police_nr)}
` : ''}
Jahresbeitrag
${_fmtEur(p.jahresbeitrag)}
Läuft ab
${_fmtDate(p.ablaufdatum)}
${p.kontakt ? `
Kontakt
${UI.escape(p.kontakt)}
` : ''} ${p.notizen ? `
Notizen
${UI.escape(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 `
${UI.escape(katLabel)} ${trigLabel ? `${UI.escape(trigLabel)}` : ''} ${fmtDate(e.datum)}${e.uhrzeit ? ' ' + e.uhrzeit : ''}
${dots}
${e.notiz ? `
${UI.escape(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 = `
${[1,2,3,4,5].map(n => ``).join('')}
`; 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 }; })();