/* ============================================================ 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 = {}; // { impfung:[], tierarzt:[], gewicht:[], medikament:[], allergie:[], dokument:[] } let _activeTab = 'impfung'; const TABS = [ { key: 'impfung', label: 'Impfpass', icon: '💉' }, { key: 'tierarzt', label: 'Tierarzt', icon: '🏥' }, { key: 'gewicht', label: 'Gewicht', icon: '⚖️' }, { key: 'medikament', label: 'Medikamente',icon: '💊' }, { key: 'allergie', label: 'Allergien', icon: '🌿' }, { key: 'dokument', label: 'Dokumente', icon: '📄' }, ]; // ---------------------------------------------------------- // LIFECYCLE // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; await _render(); } async function refresh() { if (!_appState.activeDog) return; if (!_container.querySelector('#health-tabs')) { await _render(); return; } await _loadAll(); _renderTab(); } async function onDogChange() { _data = {}; await _render(); } function openNew() { _showForm(null, _activeTab); } // ---------------------------------------------------------- // RENDER — Hauptstruktur // ---------------------------------------------------------- async function _render() { if (!_appState.activeDog) { _container.innerHTML = UI.emptyState({ icon: '💉', title: 'Noch kein Hund angelegt', text: 'Erstelle zuerst ein Hundeprofil.', action: ``, }); return; } _container.innerHTML = `
`; _renderTabBar(); _container.querySelector('#health-ki-btn') .addEventListener('click', _showKiSummary); await _loadAll(); _renderTab(); } function _renderTabBar() { const tabsEl = _container.querySelector('#health-tabs'); tabsEl.innerHTML = TABS.map(t => ` `).join(''); tabsEl.querySelectorAll('.health-tab').forEach(btn => { btn.addEventListener('click', () => { _activeTab = btn.dataset.tab; tabsEl.querySelectorAll('.health-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 = {}; TABS.forEach(t => { _data[t.key] = []; }); all.forEach(e => { if (_data[e.typ]) _data[e.typ].push(e); }); } catch (err) { UI.toast.error('Gesundheitsdaten konnten nicht geladen werden.'); } } // ---------------------------------------------------------- // TAB-INHALT RENDERN // ---------------------------------------------------------- function _renderTab() { const content = _container.querySelector('#health-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 'medikament': content.innerHTML = _renderMedikamente(entries); break; case 'allergie': content.innerHTML = _renderAllergien(entries); break; case 'dokument': content.innerHTML = _renderDokumente(entries); break; } _bindTabEvents(content); } // ---------------------------------------------------------- // IMPFUNGEN — mit Ampel-Status // ---------------------------------------------------------- function _renderImpfungen(entries) { const addBtn = ``; if (!entries.length) return UI.emptyState({ icon: '💉', title: 'Noch keine Impfungen', text: 'Trage alle Impfungen ein, um nichts zu verpassen.', action: addBtn }); const items = entries.map(e => { const ampel = _impfAmpel(e.naechstes); return `
${_esc(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')} ${e.tierarzt_name ? ` · ${_esc(e.tierarzt_name)}` : ''} ${e.charge_nr ? ` · Ch.-Nr: ${_esc(e.charge_nr)}` : ''}
${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 => `
${_esc(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')} ${e.tierarzt_name ? ` · ${_esc(e.tierarzt_name)}` : ''} ${e.kosten != null ? ` · ${Number(e.kosten).toFixed(2)} €` : ''}
${e.diagnose ? `
Diagnose: ${_esc(e.diagnose)}
` : ''} ${e.notiz ? `
${_esc(e.notiz)}
` : ''}
`).join(''); return `
${items}
${addBtn}
`; } // ---------------------------------------------------------- // GEWICHT — mit SVG-Diagramm // ---------------------------------------------------------- function _renderGewicht(entries) { const addBtn = ``; const sorted = [...entries].sort((a, b) => a.datum.localeCompare(b.datum)); const chart = sorted.length >= 2 ? _weightChart(sorted) : ''; if (!entries.length) return UI.emptyState({ icon: '⚖️', title: 'Noch keine Gewichtseinträge', action: addBtn }); const items = sorted.slice().reverse().map(e => `
${e.wert} ${e.einheit || 'kg'}
${UI.time.format(e.datum + 'T00:00:00')}
${e.notiz ? `
${_esc(e.notiz)}
` : ''}
`).join(''); return ` ${chart ? `
${chart}
` : ''}
${items}
${addBtn}
`; } function _weightChart(entries) { const W = 320, H = 120, PAD = 24; const vals = entries.map(e => parseFloat(e.wert)); const min = Math.min(...vals); const max = Math.max(...vals); const range = max - min || 1; const pts = entries.map((e, i) => { const x = PAD + (i / (entries.length - 1)) * (W - PAD * 2); const y = PAD + (1 - (parseFloat(e.wert) - min) / range) * (H - PAD * 2); return `${x},${y}`; }); const dots = entries.map((e, i) => { const [x, y] = pts[i].split(','); return ``; }).join(''); const labels = [ `${entries[0].datum.slice(5)}`, `${entries[entries.length - 1].datum.slice(5)}`, `${max.toFixed(1)}`, `${min.toFixed(1)}`, ].join(''); return ` ${dots} ${labels} `; } // ---------------------------------------------------------- // 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 ? `
${label}
${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 => `
${e.datei_url ? (e.datei_typ === 'pdf' ? `
📄
` : `Dokument`) : `
📎
`}
${_esc(e.bezeichnung)}
${UI.time.format(e.datum + 'T00:00:00')}
${e.notiz ? `
${_esc(e.notiz)}
` : ''}
`).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)); }); } // ---------------------------------------------------------- // DETAIL-ANSICHT // ---------------------------------------------------------- function _openDetail(entry) { const tabInfo = TABS.find(t => t.key === entry.typ) || TABS[0]; const fields = _detailFields(entry); const body = `
${fields} ${entry.datei_url ? (entry.datei_typ === 'pdf' ? `📄 PDF öffnen` : `Dokument`) : ''}
`; UI.modal.open({ title: `${tabInfo.icon} ${_esc(entry.bezeichnung)}`, body }); document.getElementById('health-detail-edit')?.addEventListener('click', () => { UI.modal.close(); _showForm(entry, entry.typ); }); document.getElementById('health-detail-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Eintrag löschen?', message: 'Dieser Vorgang kann nicht rückgängig gemacht werden.', confirmText: 'Löschen', danger: true, }); if (ok) { try { await API.health.delete(_appState.activeDog.id, entry.id); _data[entry.typ] = (_data[entry.typ] || []).filter(e => e.id !== entry.id); UI.modal.close(); _renderTab(); UI.toast.success('Eintrag gelöscht.'); } catch (err) { UI.toast.error(err.message || 'Fehler beim Löschen.'); } } }); } function _detailFields(e) { const rows = []; if (e.datum) rows.push(['Datum', UI.time.format(e.datum + 'T00:00:00')]); if (e.naechstes) rows.push(['Nächstes', UI.time.format(e.naechstes + 'T00:00:00')]); 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; const today = new Date().toISOString().slice(0, 10); const t = typ || _activeTab; const commonFields = `
`; const extraFields = _extraFormFields(entry, t); const notizField = `
`; const uploadField = t === 'dokument' ? `
` : ''; const body = `
${commonFields} ${extraFields} ${notizField} ${uploadField}
`; const tabInfo = TABS.find(tab => tab.key === t) || TABS[0]; UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body }); const form = document.getElementById('health-form'); setTimeout(() => form?.querySelector('[name="bezeichnung"]')?.focus(), 150); document.getElementById('health-form-cancel')?.addEventListener('click', UI.modal.close); form.addEventListener('submit', async e => { e.preventDefault(); const btn = 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', }; return ph[typ] || ''; } function _extraFormFields(entry, typ) { switch (typ) { case 'impfung': return `
`; case 'entwurmung': return `
`; case 'tierarzt': return `
`; case 'gewicht': return `
`; case 'medikament': return `
`; case 'allergie': return `
`; 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); if (fd.kosten) p.kosten = parseFloat(fd.kosten); if (typ === 'medikament') { p.aktiv = 'aktiv' in fd ? 1 : 0; } // Gewicht-Einheit p.einheit = fd.einheit || 'kg'; return p; } // ---------------------------------------------------------- // 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: '✨ 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 }; })();