779 lines
31 KiB
JavaScript
779 lines
31 KiB
JavaScript
/* ============================================================
|
|
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 (_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: `<button class="btn btn-primary" onclick="App.navigate('dog-profile')">Profil erstellen</button>`,
|
|
});
|
|
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
|
|
? `<img src="${_esc(dog.foto_url)}" alt="${_esc(dog.name)}">`
|
|
: `<span style="font-size:2.5rem">🐕</span>`;
|
|
return `
|
|
<div class="diary-picker-card${isActive ? ' diary-picker-card--active' : ''}"
|
|
data-dog-id="${dog.id}">
|
|
<div class="diary-picker-av">${av}</div>
|
|
<div class="diary-picker-name">${_esc(dog.name)}</div>
|
|
${dog.rasse ? `<div class="diary-picker-rasse">${_esc(dog.rasse)}</div>` : ''}
|
|
</div>`;
|
|
}).join('');
|
|
|
|
_container.innerHTML = `
|
|
<div class="diary-picker-wrap">
|
|
<p class="diary-picker-hint">Wessen Gesundheitsakte?</p>
|
|
<div class="diary-picker-grid">${cards}</div>
|
|
</div>`;
|
|
|
|
_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() {
|
|
_container.innerHTML = `
|
|
<div class="health-header">
|
|
<button class="btn btn-secondary btn-sm" id="health-ki-btn">
|
|
✨ KI-Zusammenfassung
|
|
</button>
|
|
</div>
|
|
<div class="health-tabs" id="health-tabs"></div>
|
|
<div id="health-tab-content"></div>
|
|
`;
|
|
|
|
_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 => `
|
|
<button class="health-tab${t.key === _activeTab ? ' active' : ''}"
|
|
data-tab="${t.key}">
|
|
${t.icon} ${t.label}
|
|
</button>
|
|
`).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 = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Impfung eintragen</button>`;
|
|
|
|
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 `
|
|
<div class="health-card" data-id="${e.id}" data-action="open-entry">
|
|
<div class="health-card-ampel ampel-${ampel.color}" title="${ampel.label}"></div>
|
|
<div class="health-card-body">
|
|
<div class="health-card-title">${_esc(e.bezeichnung)}</div>
|
|
<div class="health-card-meta">
|
|
${UI.time.format(e.datum + 'T00:00:00')}
|
|
${e.tierarzt_name ? ` · ${_esc(e.tierarzt_name)}` : ''}
|
|
${e.charge_nr ? ` · Ch.-Nr: ${_esc(e.charge_nr)}` : ''}
|
|
</div>
|
|
${e.naechstes ? `<div class="health-card-next ampel-text-${ampel.color}">
|
|
Nächste Impfung: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
|
|
</div>` : ''}
|
|
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
return `<div class="health-list">${items}</div>
|
|
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>`;
|
|
}
|
|
|
|
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 = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Besuch eintragen</button>`;
|
|
if (!entries.length) return UI.emptyState({
|
|
icon: '🏥', title: 'Noch keine Tierarztbesuche', text: 'Halte alle Tierarztbesuche fest.', action: addBtn
|
|
});
|
|
|
|
const items = entries.map(e => `
|
|
<div class="health-card" data-id="${e.id}" data-action="open-entry">
|
|
<div class="health-card-body">
|
|
<div class="health-card-title">${_esc(e.bezeichnung)}</div>
|
|
<div class="health-card-meta">
|
|
${UI.time.format(e.datum + 'T00:00:00')}
|
|
${e.tierarzt_name ? ` · ${_esc(e.tierarzt_name)}` : ''}
|
|
${e.kosten != null ? ` · ${Number(e.kosten).toFixed(2)} €` : ''}
|
|
</div>
|
|
${e.diagnose ? `<div class="health-card-note"><b>Diagnose:</b> ${_esc(e.diagnose)}</div>` : ''}
|
|
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
return `<div class="health-list">${items}</div>
|
|
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// GEWICHT — mit SVG-Diagramm
|
|
// ----------------------------------------------------------
|
|
function _renderGewicht(entries) {
|
|
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Gewicht eintragen</button>`;
|
|
|
|
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 => `
|
|
<div class="health-card" data-id="${e.id}" data-action="open-entry">
|
|
<div class="health-card-body">
|
|
<div class="health-card-title" style="font-size:var(--text-xl);font-weight:700">
|
|
${e.wert} ${e.einheit || 'kg'}
|
|
</div>
|
|
<div class="health-card-meta">${UI.time.format(e.datum + 'T00:00:00')}</div>
|
|
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
return `
|
|
${chart ? `<div class="health-chart-wrap">${chart}</div>` : ''}
|
|
<div class="health-list">${items}</div>
|
|
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>
|
|
`;
|
|
}
|
|
|
|
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 `<circle cx="${x}" cy="${y}" r="4" fill="var(--c-primary)"/>`;
|
|
}).join('');
|
|
|
|
const labels = [
|
|
`<text x="${PAD}" y="${H - 4}" font-size="10" fill="var(--c-text-secondary)">${entries[0].datum.slice(5)}</text>`,
|
|
`<text x="${W - PAD}" y="${H - 4}" font-size="10" fill="var(--c-text-secondary)" text-anchor="end">${entries[entries.length - 1].datum.slice(5)}</text>`,
|
|
`<text x="${PAD - 2}" y="${PAD + 4}" font-size="10" fill="var(--c-text-secondary)" text-anchor="end">${max.toFixed(1)}</text>`,
|
|
`<text x="${PAD - 2}" y="${H - PAD + 4}" font-size="10" fill="var(--c-text-secondary)" text-anchor="end">${min.toFixed(1)}</text>`,
|
|
].join('');
|
|
|
|
return `
|
|
<svg viewBox="0 0 ${W} ${H}" style="width:100%;max-width:${W}px;display:block;margin:0 auto">
|
|
<polyline points="${pts.join(' ')}"
|
|
fill="none" stroke="var(--c-primary)" stroke-width="2" stroke-linejoin="round"/>
|
|
${dots}
|
|
${labels}
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// MEDIKAMENTE
|
|
// ----------------------------------------------------------
|
|
function _renderMedikamente(entries) {
|
|
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Medikament eintragen</button>`;
|
|
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 ? `
|
|
<div class="health-group-label">${label}</div>
|
|
${items.map(e => `
|
|
<div class="health-card${e.aktiv ? '' : ' health-card--inactive'}" data-id="${e.id}" data-action="open-entry">
|
|
<div class="health-card-body">
|
|
<div class="health-card-title">${_esc(e.bezeichnung)}</div>
|
|
<div class="health-card-meta">
|
|
${e.dosierung ? _esc(e.dosierung) : ''}
|
|
${e.haeufigkeit ? ` · ${_esc(e.haeufigkeit)}` : ''}
|
|
${e.bis_datum ? ` · bis ${UI.time.format(e.bis_datum + 'T00:00:00')}` : ''}
|
|
</div>
|
|
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
` : '';
|
|
|
|
return `
|
|
<div class="health-list">
|
|
${renderGroup(aktive, '💊 Aktuelle Medikamente')}
|
|
${renderGroup(inaktive, 'Vergangene Medikamente')}
|
|
</div>
|
|
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>
|
|
`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// ALLERGIEN
|
|
// ----------------------------------------------------------
|
|
function _renderAllergien(entries) {
|
|
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Allergie eintragen</button>`;
|
|
if (!entries.length) return UI.emptyState({
|
|
icon: '🌿', title: 'Noch keine Allergien eingetragen', action: addBtn
|
|
});
|
|
|
|
const SCHWEREGRAD = { leicht: '🟡', mittel: '🟠', schwer: '🔴' };
|
|
|
|
const items = entries.map(e => `
|
|
<div class="health-card" data-id="${e.id}" data-action="open-entry">
|
|
<div class="health-card-body">
|
|
<div class="health-card-title">
|
|
${e.schweregrad ? SCHWEREGRAD[e.schweregrad] || '' : ''} ${_esc(e.bezeichnung)}
|
|
</div>
|
|
<div class="health-card-meta">
|
|
Erstmals: ${UI.time.format(e.datum + 'T00:00:00')}
|
|
${e.schweregrad ? ` · Schweregrad: ${_esc(e.schweregrad)}` : ''}
|
|
</div>
|
|
${e.reaktion ? `<div class="health-card-note"><b>Reaktion:</b> ${_esc(e.reaktion)}</div>` : ''}
|
|
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
return `<div class="health-list">${items}</div>
|
|
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// DOKUMENTE
|
|
// ----------------------------------------------------------
|
|
function _renderDokumente(entries) {
|
|
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Dokument hinzufügen</button>`;
|
|
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 => `
|
|
<div class="health-card" data-id="${e.id}" data-action="open-entry">
|
|
<div class="health-card-body" style="display:flex;gap:var(--space-3);align-items:center">
|
|
${e.datei_url
|
|
? (e.datei_typ === 'pdf'
|
|
? `<div class="health-doc-icon">📄</div>`
|
|
: `<img src="${e.datei_url}" class="health-doc-thumb" alt="Dokument">`)
|
|
: `<div class="health-doc-icon">📎</div>`}
|
|
<div>
|
|
<div class="health-card-title">${_esc(e.bezeichnung)}</div>
|
|
<div class="health-card-meta">${UI.time.format(e.datum + 'T00:00:00')}</div>
|
|
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
return `<div class="health-list">${items}</div>
|
|
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// 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 = `
|
|
<div class="health-detail">
|
|
${fields}
|
|
${entry.datei_url
|
|
? (entry.datei_typ === 'pdf'
|
|
? `<a href="${entry.datei_url}" target="_blank" class="btn btn-secondary btn-sm" style="margin-top:var(--space-3)">📄 PDF öffnen</a>`
|
|
: `<img src="${entry.datei_url}" style="width:100%;border-radius:var(--radius-md);margin-top:var(--space-3)" alt="Dokument">`)
|
|
: ''}
|
|
</div>
|
|
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-5)">
|
|
<button class="btn btn-secondary flex-1" id="health-detail-edit">Bearbeiten</button>
|
|
<button class="btn btn-danger flex-1" id="health-detail-delete">Löschen</button>
|
|
</div>
|
|
`;
|
|
|
|
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 `<dl class="health-detail-dl">${
|
|
rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('')
|
|
}</dl>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// FORMULAR — Neu / Bearbeiten
|
|
// ----------------------------------------------------------
|
|
function _showForm(entry, typ) {
|
|
const isEdit = !!entry;
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const t = typ || _activeTab;
|
|
|
|
const commonFields = `
|
|
<div class="form-group">
|
|
<label class="form-label">Bezeichnung *</label>
|
|
<input class="form-control" type="text" name="bezeichnung"
|
|
value="${_esc(entry?.bezeichnung || '')}" required
|
|
placeholder="${_formPlaceholder(t)}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Datum *</label>
|
|
<input class="form-control" type="date" name="datum"
|
|
value="${entry?.datum || today}" required>
|
|
</div>
|
|
`;
|
|
|
|
const extraFields = _extraFormFields(entry, t);
|
|
const notizField = `
|
|
<div class="form-group">
|
|
<label class="form-label">Notiz</label>
|
|
<textarea class="form-control" name="notiz" rows="3">${_esc(entry?.notiz || '')}</textarea>
|
|
</div>
|
|
`;
|
|
|
|
const uploadField = t === 'dokument' ? `
|
|
<div class="form-group">
|
|
<label class="form-label">Datei (JPG, PNG, PDF)</label>
|
|
<input class="form-control" type="file" name="datei" accept="image/*,.pdf">
|
|
</div>
|
|
` : '';
|
|
|
|
const body = `
|
|
<form id="health-form" autocomplete="off">
|
|
${commonFields}
|
|
${extraFields}
|
|
${notizField}
|
|
${uploadField}
|
|
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-4)">
|
|
<button type="button" class="btn btn-secondary flex-1" id="health-form-cancel">Abbrechen</button>
|
|
<button type="submit" class="btn btn-primary flex-1">${isEdit ? 'Speichern' : 'Erstellen'}</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
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 `
|
|
<div class="form-group">
|
|
<label class="form-label">Nächste Impfung (optional)</label>
|
|
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Tierarzt</label>
|
|
<input class="form-control" type="text" name="tierarzt_name" value="${_esc(entry?.tierarzt_name || '')}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Chargen-Nr.</label>
|
|
<input class="form-control" type="text" name="charge_nr" value="${_esc(entry?.charge_nr || '')}">
|
|
</div>
|
|
`;
|
|
case 'entwurmung': return `
|
|
<div class="form-group">
|
|
<label class="form-label">Nächste Behandlung (optional)</label>
|
|
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Tierarzt</label>
|
|
<input class="form-control" type="text" name="tierarzt_name" value="${_esc(entry?.tierarzt_name || '')}">
|
|
</div>
|
|
`;
|
|
case 'tierarzt': return `
|
|
<div class="form-group">
|
|
<label class="form-label">Tierarzt / Praxis</label>
|
|
<input class="form-control" type="text" name="tierarzt_name" value="${_esc(entry?.tierarzt_name || '')}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Diagnose</label>
|
|
<textarea class="form-control" name="diagnose" rows="2">${_esc(entry?.diagnose || '')}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Kosten (€)</label>
|
|
<input class="form-control" type="number" step="0.01" min="0" name="kosten"
|
|
value="${entry?.kosten ?? ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Nächster Termin (optional)</label>
|
|
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
|
|
</div>
|
|
`;
|
|
case 'gewicht': return `
|
|
<div class="form-group">
|
|
<label class="form-label">Gewicht (kg) *</label>
|
|
<input class="form-control" type="number" step="0.1" min="0" name="wert"
|
|
value="${entry?.wert ?? ''}" required>
|
|
</div>
|
|
`;
|
|
case 'medikament': return `
|
|
<div class="form-group">
|
|
<label class="form-label">Dosierung</label>
|
|
<input class="form-control" type="text" name="dosierung"
|
|
value="${_esc(entry?.dosierung || '')}" placeholder="z.B. 1 Tablette">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Häufigkeit</label>
|
|
<input class="form-control" type="text" name="haeufigkeit"
|
|
value="${_esc(entry?.haeufigkeit || '')}" placeholder="z.B. täglich, 2x wöchentlich">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Gabe bis (optional)</label>
|
|
<input class="form-control" type="date" name="bis_datum" value="${entry?.bis_datum || ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
|
|
<input type="checkbox" name="aktiv" ${entry?.aktiv !== 0 ? 'checked' : ''}>
|
|
Aktuell aktiv
|
|
</label>
|
|
</div>
|
|
`;
|
|
case 'allergie': return `
|
|
<div class="form-group">
|
|
<label class="form-label">Schweregrad</label>
|
|
<select class="form-control" name="schweregrad">
|
|
<option value="">— unbekannt —</option>
|
|
${['leicht', 'mittel', 'schwer'].map(s =>
|
|
`<option value="${s}" ${entry?.schweregrad === s ? 'selected' : ''}>${s}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Reaktion / Symptome</label>
|
|
<textarea class="form-control" name="reaktion" rows="2">${_esc(entry?.reaktion || '')}</textarea>
|
|
</div>
|
|
`;
|
|
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: `<div style="white-space:pre-wrap;line-height:1.7;font-size:var(--text-sm)">${_esc(zusammenfassung)}</div>`,
|
|
});
|
|
} 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, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
return { init, refresh, openNew, onDogChange };
|
|
|
|
})();
|