/* ============================================================
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
? `
`
: `${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 `
`;
}
// ----------------------------------------------------------
// 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
${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 ? `
${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 => {
// 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
? `
})
`
: `
`}
${_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 ``;
}).join('');
const uploadField = `
`;
const body = `
`;
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 `
${_praxisSelectField(entry)}
`;
case 'entwurmung': return `
${_praxisSelectField(entry)}
`;
case 'tierarzt': {
const aktivePraxen = _praxen.filter(p => p.aktiv);
const praxisField = aktivePraxen.length
? `
`
: ``;
return `
${praxisField}
`;
}
case 'gewicht': return `
`;
case 'medikament': return `
${_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: `
`,
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
? `
${result.hinweise.map(h => `- ${_esc(h)}
`).join('')}
`
: '';
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 };
})();