/* ============================================================ 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'; const BASE_TABS = [ { key: 'impfung', label: 'Impfpass', icon: '' }, { key: 'tierarzt', label: 'Besuche', icon: '' }, { key: 'gewicht', label: 'Gewicht', icon: '' }, { key: 'medikament', label: 'Medikamente', icon: '' }, { key: 'allergie', label: 'Allergien', icon: '' }, { key: 'dokument', label: 'Dokumente', icon: '' }, { key: 'praxen', label: 'Praxen', icon: '' }, { key: 'symptomcheck', label: 'Symptom-Check', icon: '' }, ]; const LAEUFIGKEIT_TAB = { key: 'laeufigkeit', label: 'Läufigkeit', icon: '' }; 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) { _container = container; _appState = appState; await _render(); } async function refresh() { if (!_appState.activeDog) return; if (_appState.dogs.length > 1) { _renderDogPicker(); 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; } if (_appState.dogs.length > 1) { _renderDogPicker(); } else { await _renderHealth(); } } // ---------------------------------------------------------- // HUNDE-PICKER // ---------------------------------------------------------- function _renderDogPicker() { const activeDogId = _appState.activeDog?.id; const cards = _appState.dogs.map(dog => { const isActive = dog.id === activeDogId; const av = dog.foto_url ? `${_esc(dog.name)}` : `${UI.icon('dog')}`; return `
${av}
${_esc(dog.name)}
${dog.rasse ? `
${_esc(dog.rasse)}
` : ''}
`; }).join(''); _container.innerHTML = `

Wessen Gesundheitsakte?

${cards}
`; _container.querySelectorAll('.diary-picker-card').forEach(el => { el.addEventListener('click', async () => { const id = parseInt(el.dataset.dogId); if (id === _appState.activeDog?.id) { // Bereits aktiver Hund → direkt Health laden _data = {}; await _renderHealth(); } else { App.setActiveDog(id); // onDogChange() → _renderHealth() via _notifyDogChange() } }); }); } // ---------------------------------------------------------- // HEALTH-ANSICHT — Tabs mit Einträgen // ---------------------------------------------------------- async function _renderHealth() { const dog = _appState.activeDog; const transponderHtml = `
Transponder: ${dog?.chip_nr ? `${_esc(dog.chip_nr)}` : 'nicht eingetragen'}
`; _container.innerHTML = `
${transponderHtml}
`; _renderTabBar(); _container.querySelector('#health-ki-btn') .addEventListener('click', _showKiSummary); _container.querySelector('#health-transponder-edit') .addEventListener('click', () => _editTransponder(dog)); await _loadAll(); _renderErinnerungen(); _renderTab(); } // ---------------------------------------------------------- // 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: '', 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] || ''}
${_esc(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(''); 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'] || []; 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'] = []; } } // ---------------------------------------------------------- // 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 'allergie': content.innerHTML = _renderAllergien(entries); break; case 'dokument': content.innerHTML = _renderDokumente(entries); break; case 'praxen': content.innerHTML = _renderPraxen(); break; case 'symptomcheck': _renderSymptomCheck(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 `
${_esc(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')} ${e.charge_nr ? ` · Ch.-Nr: ${_esc(e.charge_nr)}` : ''}
${vetName ? `
${_esc(vetName)}
` : ''} ${e.naechstes ? `
Nächste Impfung: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
` : ''} ${e.notiz ? `
${_esc(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: '🟢' }; } // ---------------------------------------------------------- // 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 `
${_esc(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')} ${e.kosten != null ? ` · ${Number(e.kosten).toFixed(2)} €` : ''}
${praxisName ? `
${_esc(praxisName)}${praxisOrt ? ` · ${_esc(praxisOrt)}` : ''}
` : ''} ${e.diagnose ? `
Diagnose: ${_esc(e.diagnose)}
` : ''} ${e.notiz ? `
${_esc(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 ? `
${_esc(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 ? `
${_esc(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 => `
${_esc(e.bezeichnung)}
${e.dosierung ? _esc(e.dosierung) : ''} ${e.haeufigkeit ? ` · ${_esc(e.haeufigkeit)}` : ''} ${e.bis_datum ? ` · bis ${UI.time.format(e.bis_datum + 'T00:00:00')}` : ''}
${e.notiz ? `
${_esc(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] || '' : ''} ${_esc(e.bezeichnung)}
Erstmals: ${UI.time.format(e.datum + 'T00:00:00')} ${e.schweregrad ? ` · Schweregrad: ${_esc(e.schweregrad)}` : ''}
${e.reaktion ? `
Reaktion: ${_esc(e.reaktion)}
` : ''} ${e.notiz ? `
${_esc(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 => { const isPdf = e.datei_typ === 'pdf'; const hasFile = !!e.datei_url; return `
${hasFile && !isPdf ? `Vorschau` : `
`}
${_esc(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')}
${e.notiz ? `
${_esc(e.notiz)}
` : ''} ${hasFile ? `
${isPdf ? ' PDF öffnen' : ' Bild öffnen'}
` : `Noch keine Datei hochgeladen`}
`; }).join(''); return `
${items}
${addBtn}
`; } // ---------------------------------------------------------- // EVENTS BINDEN // ---------------------------------------------------------- function _bindTabEvents(content) { content.querySelectorAll('[data-action="add-entry"]').forEach(btn => { btn.addEventListener('click', () => _showForm(null, _activeTab)); }); content.querySelectorAll('[data-action="open-entry"]').forEach(card => { const id = parseInt(card.dataset.id); const entry = (_data[_activeTab] || []).find(e => e.id === id); if (entry) card.addEventListener('click', () => _openDetail(entry)); }); // Praxis öffnen 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) _showPraxForm(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)); } // ---------------------------------------------------------- // DETAIL-ANSICHT // ---------------------------------------------------------- function _openDetail(entry) { const tabInfo = _getTabs().find(t => t.key === entry.typ) || BASE_TABS[0]; const fields = _detailFields(entry); const body = `
${fields} ${entry.datei_url ? (entry.datei_typ === 'pdf' ? ` PDF öffnen` : `Dokument`) : ''}
`; 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} ${_esc(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.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 ? ` · ${_esc(praxis.telefon)}` : ''; rows.push(['Praxis', ` ${_esc(praxis.name)}${adresse ? `
${_esc(adresse)}${tel}` : tel}`]); } } else if (e.tierarzt_name) { rows.push(['Tierarzt', _esc(e.tierarzt_name)]); } if (e.charge_nr) rows.push(['Charge-Nr.', _esc(e.charge_nr)]); if (e.kosten != null) rows.push(['Kosten', `${Number(e.kosten).toFixed(2)} €`]); if (e.diagnose) rows.push(['Diagnose', _esc(e.diagnose)]); if (e.wert) rows.push(['Gewicht', `${e.wert} ${e.einheit || 'kg'}`]); if (e.dosierung) rows.push(['Dosierung', _esc(e.dosierung)]); if (e.haeufigkeit) rows.push(['Häufigkeit', _esc(e.haeufigkeit)]); if (e.bis_datum) rows.push(['Bis', UI.time.format(e.bis_datum + 'T00:00:00')]); if (e.schweregrad) rows.push(['Schweregrad',_esc(e.schweregrad)]); if (e.reaktion) rows.push(['Reaktion', _esc(e.reaktion)]); if (e.notiz) rows.push(['Notiz', _esc(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 = `
`; const uploadField = t === 'dokument' ? `
` : ''; const body = `
${commonFields} ${extraFields} ${notizField} ${uploadField}
`; const footer = `
${isEdit ? `` : ''}
`; const tabInfo = _getTabs().find(tab => tab.key === t) || BASE_TABS[0]; 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(); }); }, 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.'); } // Datei-Upload für Dokumente if (t === 'dokument' && form.querySelector('[name="datei"]')?.files[0]) { try { const formData = new FormData(); formData.append('file', form.querySelector('[name="datei"]').files[0]); const res = await API.health.uploadDokument(_appState.activeDog.id, saved.id, formData); saved.datei_url = res.datei_url; saved.datei_typ = res.datei_typ; } catch { UI.toast.warning('Eintrag erstellt, Datei 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', }; 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 '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}
`; } 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'; } 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 => `
${p.ist_notfallpraxis ? '🚨' : ''}
${_esc(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.telefon ? ` 📞 Anrufen ` : ''} ${p.notfall_telefon ? ` 🚨 Notfall ` : ''}
`; return `
${addBtn}
${aktive.map(renderCard).join('')} ${inaktive.length ? `

Ehemalige Praxen

${inaktive.map(renderCard).join('')}
` : ''}
`; } // ---------------------------------------------------------- // 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); 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, }; 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', label: '🟢 Beobachten' }, tierarzt: { badgeClass: 'badge-warning', label: '🟡 Zum Tierarzt' }, notfall: { badgeClass: 'badge-danger', label: '🔴 Notfall — sofort zum Tierarzt!' }, }; const d = DRINGLICHKEIT[result.dringlichkeit] || { badgeClass: 'badge-primary', label: _esc(result.dringlichkeit) }; const hinweiseHtml = (result.hinweise || []).length ? `` : ''; const zumTierarztHtml = result.zum_tierarzt_wenn ? `
Zum Tierarzt wenn: ${_esc(result.zum_tierarzt_wenn)}
` : ''; resultEl.innerHTML = `
${d.label}
${result.einschaetzung ? `

${_esc(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 ? `${_esc(nr)}` : 'nicht eingetragen'; } catch (e) { UI.setLoading(btn, false); UI.toast('Fehler beim Speichern', 'error'); } }); } // ---------------------------------------------------------- // KI-ZUSAMMENFASSUNG // ---------------------------------------------------------- async function _showKiSummary() { const btn = _container.querySelector('#health-ki-btn'); UI.setLoading(btn, true); try { const { zusammenfassung } = await API.health.kiZusammenfassung(_appState.activeDog.id); UI.modal.open({ title: `${UI.icon('star')} KI-Gesundheitsbericht`, body: `
${_esc(zusammenfassung)}
`, }); } 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 _esc(str) { if (!str) return ''; return String(str) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"'); } return { init, refresh, openNew, onDogChange }; })();