/* ============================================================ 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, 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; 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(); _loadKiBerichte(dog.id); _loadTerminvorschlaege(dog.id); } // ---------------------------------------------------------- // 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] || ''}
${_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(''); 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'] || []; 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 => { // 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` : `
`}
${_esc(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')} ${count > 1 ? ` · ${count} Dateien` : ''}
${e.notiz ? `
${_esc(e.notiz)}
` : ''} ${count ? `
${mediaList.slice(0, 3).map(m => m.media_type === 'pdf' ? ` PDF ` : ` Bild ` ).join('')}
` : `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)); }); 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 || ''; _openNoteModal('health', id, label, null); }); }); // 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); // 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} ${_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)}` : ''; const oh = praxis.opening_hours ? `
🕐 ${_esc(_fmtOeffnungszeiten(praxis.opening_hours))}` : ''; rows.push(['Praxis', ` ${_esc(praxis.name)}${adresse ? `
${_esc(adresse)}${tel}` : tel}${oh}`]); } } 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 = `
`; // 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 = _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(); }); // 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
${_esc(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 fd = new FormData(); fd.append('file', f); 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', }; 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 => `
${_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.opening_hours ? `
${_esc(_fmtOeffnungszeiten(p.opening_hours))}
` : ''}
${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); // 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 => `
${_esc(h.name)}
${h.opening_hours_fmt ? `
${_esc(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: _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-GESUNDHEITSBERICHTE (gespeicherte automatische Berichte) // ---------------------------------------------------------- async function _loadKiBerichte(dogId) { const el = _container.querySelector('#health-ki-berichte'); if (!el) return; try { const berichte = 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 ? _esc(neuester.bericht.slice(0, 180)) + '…' : _esc(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', () => { const listeHtml = berichte.map((b, i) => { const d = b.erstellt_at ? new Date(b.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : ''; return `
${d ? `
${d}
` : ''}
${_esc(b.bericht)}
`; }).join(''); UI.modal.open({ title: `${UI.icon('star')} KI-Gesundheitsberichte`, body: listeHtml, }); }); } 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 `
${_esc(v.bezeichnung)}
${_esc(v.label)}${v.praxis_name ? ' · ' + _esc(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.'); }); }); } // ---------------------------------------------------------- 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, '"'); } function _showPoiKorrekturModal(osmId, poiName, currentOh) { UI.modal.open({ title: 'Öffnungszeiten korrigieren', body: `

Korrektur für ${_esc(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) // ---------------------------------------------------------- async function _openNoteModal(parentType, parentId, parentLabel, locationName) { // Vorhandenes Modal entfernen falls noch offen document.getElementById('by-note-modal')?.remove(); const overlay = document.createElement('div'); overlay.id = 'by-note-modal'; overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center'; overlay.innerHTML = `
Notiz
${_esc(parentLabel)}
`; document.body.appendChild(overlay); const textarea = document.getElementById('by-note-text'); const saveBtn = document.getElementById('by-note-save'); const cancelBtn = document.getElementById('by-note-cancel'); const closeBtn = document.getElementById('by-note-close'); let existingNoteId = null; // Vorhandene Notiz laden try { const existing = await API.notes.get(parentType, parentId); if (existing?.id) { existingNoteId = existing.id; textarea.value = existing.text || ''; } } catch (_) { /* keine Notiz vorhanden — ok */ } setTimeout(() => textarea.focus(), 100); const _close = () => overlay.remove(); closeBtn.addEventListener('click', _close); cancelBtn.addEventListener('click', _close); overlay.addEventListener('click', e => { if (e.target === overlay) _close(); }); document.getElementById('by-note-form').addEventListener('submit', async e => { e.preventDefault(); const text = textarea.value.trim(); UI.setLoading(saveBtn, true); try { const payload = { text, parent_label: parentLabel, location_name: locationName }; if (existingNoteId) { await API.notes.update(existingNoteId, payload); } else { await API.notes.create(parentType, parentId, payload); } UI.toast.success('Notiz gespeichert.'); _close(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); UI.setLoading(saveBtn, false); } }); } return { init, refresh, openNew, onDogChange }; })();