Feature: Sprint31 — 9 Features merged (Streak, Ausgaben, KI-Tierarzt, Rückrufe, Adoption, Vet+Befunde, Hundepass, Playdate, Rassenerkennung)
- Trainings-Streak: streak.py, DB training_streaks, Scheduler 19:00, Widget in welcome.js, Ping in uebungen.js
- Ausgaben-Tracker: expenses.py, expenses.js, DB expenses-Tabelle
- KI-Tierarztfragen: ki.py /tierarzt, health.js Button+Modal, DB ki_tierarzt_log
- Rückruf-Alarm: recalls.py, recalls.js, DB feed_recalls, Scheduler 08:00 RASFF
- Adoption: adoption.py, adoption.js, DB adoption_cache
- Tierarzt-Favorit + Befunde: tieraerzte.py /my-favorite+/favorite, health_docs.py, health.js, api.js, DB favorite_vets+health_documents
- Digitaler Hundepass: passport.py, dog-profile.js, main.py /pass/{token}, DB vaccinations+medications+dog_passport_meta+passport_shares, requirements.txt fpdf2
- Playdate-Matching: playdate.py, playdate.js, DB playdate_listings+playdate_requests
- Rassen-Erkennung: ki.py /rasse-erkennung (Claude Vision), dog-profile.js+wiki.js, CSS .rasse-result-card, DB ki_rasse_log
This commit is contained in:
parent
031c6028ac
commit
742ad189e8
26 changed files with 5734 additions and 27 deletions
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
window.Page_health = (() => {
|
||||
|
||||
let _container = null;
|
||||
let _appState = null;
|
||||
let _data = {};
|
||||
let _praxen = [];
|
||||
let _activeTab = 'impfung';
|
||||
let _container = null;
|
||||
let _appState = null;
|
||||
let _data = {};
|
||||
let _praxen = [];
|
||||
let _activeTab = 'impfung';
|
||||
let _favoritVet = null;
|
||||
let _healthDocs = [];
|
||||
|
||||
const BASE_TABS = [
|
||||
{ key: 'impfung', label: 'Impfpass', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>' },
|
||||
|
|
@ -150,8 +152,12 @@ window.Page_health = (() => {
|
|||
<button class="btn btn-secondary btn-sm" id="health-ki-btn">
|
||||
${UI.icon('star')} KI-Zusammenfassung
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" id="health-ki-tierarzt-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#stethoscope"></use></svg> KI-Tierarzt
|
||||
</button>
|
||||
</div>
|
||||
${transponderHtml}
|
||||
<div id="health-mein-tierarzt"></div>
|
||||
<div id="health-ki-berichte"></div>
|
||||
<div id="health-terminvorschlaege"></div>
|
||||
<div id="health-reminders"></div>
|
||||
|
|
@ -162,6 +168,8 @@ window.Page_health = (() => {
|
|||
_renderTabBar();
|
||||
_container.querySelector('#health-ki-btn')
|
||||
.addEventListener('click', _showKiSummary);
|
||||
_container.querySelector('#health-ki-tierarzt-btn')
|
||||
.addEventListener('click', _showKiTierarzt);
|
||||
_container.querySelector('#health-transponder-edit')
|
||||
.addEventListener('click', () => _editTransponder(dog));
|
||||
|
||||
|
|
@ -170,6 +178,7 @@ window.Page_health = (() => {
|
|||
_renderTab();
|
||||
_loadKiBerichte(dog.id);
|
||||
_loadTerminvorschlaege(dog.id);
|
||||
_loadMeinTierarzt();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -342,6 +351,16 @@ window.Page_health = (() => {
|
|||
} catch (err) {
|
||||
_data['gewicht_chart'] = [];
|
||||
}
|
||||
try {
|
||||
_favoritVet = await API.tieraerzte.myFavorite();
|
||||
} catch (err) {
|
||||
_favoritVet = null;
|
||||
}
|
||||
try {
|
||||
_healthDocs = await API.healthDocs.list(dogId);
|
||||
} catch (err) {
|
||||
_healthDocs = [];
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -901,7 +920,8 @@ window.Page_health = (() => {
|
|||
}).join('');
|
||||
|
||||
return `<div class="health-list">${items}</div>
|
||||
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>`;
|
||||
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>
|
||||
${_renderBefundeSection()}`;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -957,6 +977,32 @@ window.Page_health = (() => {
|
|||
// Praxis hinzufügen
|
||||
content.querySelector('[data-action="add-praxis"]')
|
||||
?.addEventListener('click', () => _showPraxForm(null));
|
||||
// Favorit-Toggle für Praxen
|
||||
content.querySelectorAll('[data-action="toggle-fav"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const vetId = parseInt(btn.dataset.praxisId);
|
||||
await UI.asyncButton(btn, async () => {
|
||||
const res = await API.tieraerzte.toggleFavorite(vetId);
|
||||
if (res.is_favorite) {
|
||||
_favoritVet = _praxen.find(p => p.id === vetId) || null;
|
||||
UI.toast.success('Als Favorit-Tierarzt gespeichert.');
|
||||
} else {
|
||||
_favoritVet = null;
|
||||
UI.toast.success('Favorit entfernt.');
|
||||
}
|
||||
// is_favorite in _praxen aktualisieren
|
||||
_praxen = _praxen.map(p => ({ ...p, is_favorite: p.id === vetId ? res.is_favorite : false }));
|
||||
const elFav = _container.querySelector('#health-mein-tierarzt');
|
||||
if (elFav) _renderMeinTierarztKachel(elFav);
|
||||
_renderTab();
|
||||
});
|
||||
});
|
||||
});
|
||||
// Befunde & Dokumente
|
||||
if (_activeTab === 'dokument') {
|
||||
_bindBefundeEvents(content);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -1597,7 +1643,9 @@ window.Page_health = (() => {
|
|||
action: addBtn
|
||||
});
|
||||
|
||||
const renderCard = p => `
|
||||
const renderCard = p => {
|
||||
const isFav = _favoritVet?.id === p.id || p.is_favorite;
|
||||
return `
|
||||
<div class="health-card praxis-card${!p.aktiv ? ' health-card--inactive' : ''}"
|
||||
data-praxis-id="${p.id}" data-action="open-praxis">
|
||||
<div style="font-size:1.5rem"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${p.ist_notfallpraxis ? 'warning' : 'first-aid'}"></use></svg></div>
|
||||
|
|
@ -1626,10 +1674,21 @@ window.Page_health = (() => {
|
|||
onclick="event.stopPropagation()">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall
|
||||
</a>` : ''}
|
||||
<button class="btn btn-sm ${isFav ? 'btn-primary' : 'btn-secondary'}"
|
||||
data-action="toggle-fav" data-praxis-id="${p.id}"
|
||||
title="${isFav ? 'Favorit entfernen' : 'Als mein Tierarzt merken'}"
|
||||
style="flex-shrink:0"
|
||||
onclick="event.stopPropagation()">
|
||||
<svg class="ph-icon" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${isFav ? 'heart-fill' : 'heart'}"></use>
|
||||
</svg>
|
||||
${isFav ? 'Mein Tierarzt' : 'Als Favorit'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`};
|
||||
|
||||
|
||||
return `
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
|
||||
|
|
@ -2156,6 +2215,306 @@ window.Page_health = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// MEIN TIERARZT — Kachel
|
||||
// ----------------------------------------------------------
|
||||
async function _loadMeinTierarzt() {
|
||||
const el = _container.querySelector('#health-mein-tierarzt');
|
||||
if (!el) return;
|
||||
_renderMeinTierarztKachel(el);
|
||||
}
|
||||
|
||||
function _renderMeinTierarztKachel(el) {
|
||||
if (!el) return;
|
||||
const vet = _favoritVet;
|
||||
const adresse = vet
|
||||
? [vet.strasse, [vet.plz, vet.ort].filter(Boolean).join(' ')].filter(Boolean).join(', ')
|
||||
: '';
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="margin:var(--space-3) var(--space-4) 0">
|
||||
<div style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-muted);
|
||||
text-transform:uppercase;letter-spacing:.05em;margin-bottom:var(--space-2)">
|
||||
Mein Tierarzt
|
||||
</div>
|
||||
<div class="health-card" style="align-items:flex-start">
|
||||
<div style="font-size:1.6rem;flex-shrink:0">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>
|
||||
</div>
|
||||
<div class="health-card-body" style="flex:1;min-width:0">
|
||||
${vet ? `
|
||||
<div class="health-card-title">${_esc(vet.name)}</div>
|
||||
${adresse ? `<div class="health-card-meta">${_esc(adresse)}</div>` : ''}
|
||||
${vet.telefon ? `
|
||||
<div style="margin-top:var(--space-2)">
|
||||
<a href="tel:${_esc(vet.telefon)}" class="btn btn-secondary btn-sm"
|
||||
onclick="event.stopPropagation()">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg> ${_esc(vet.telefon)}
|
||||
</a>
|
||||
</div>` : ''}
|
||||
${vet.notfall_telefon ? `
|
||||
<div style="margin-top:var(--space-1)">
|
||||
<a href="tel:${_esc(vet.notfall_telefon)}" class="btn btn-danger btn-sm"
|
||||
onclick="event.stopPropagation()">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall: ${_esc(vet.notfall_telefon)}
|
||||
</a>
|
||||
</div>` : ''}
|
||||
` : `
|
||||
<div style="color:var(--c-text-muted);font-size:var(--text-sm)">
|
||||
Noch kein Tierarzt als Favorit gespeichert.
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" style="margin-top:var(--space-2)"
|
||||
id="health-suche-tierarzt-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg> Tierarzt suchen
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
${vet ? `
|
||||
<button class="btn btn-ghost btn-sm" id="health-remove-fav-btn"
|
||||
title="Als Favorit entfernen" style="flex-shrink:0;color:var(--c-danger)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#heart-fill"></use></svg>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.querySelector('#health-suche-tierarzt-btn')?.addEventListener('click', () => {
|
||||
App.navigate('map', { filter: 'tierarzt' });
|
||||
});
|
||||
|
||||
el.querySelector('#health-remove-fav-btn')?.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const btn = e.currentTarget;
|
||||
await UI.asyncButton(btn, async () => {
|
||||
await API.tieraerzte.toggleFavorite(_favoritVet.id);
|
||||
_favoritVet = null;
|
||||
const elAgain = _container.querySelector('#health-mein-tierarzt');
|
||||
if (elAgain) _renderMeinTierarztKachel(elAgain);
|
||||
UI.toast.success('Tierarzt-Favorit entfernt.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// BEFUNDE & DOKUMENTE — Sektion (unabhängig von den Tabs)
|
||||
// ----------------------------------------------------------
|
||||
// Diese Sektion erscheint im "dokument"-Tab als zweite Liste.
|
||||
// Wir ergänzen _renderDokumente um einen Abschnitt unten.
|
||||
|
||||
function _renderBefundeSection() {
|
||||
const dog = _appState.activeDog;
|
||||
const docs = _healthDocs;
|
||||
const DOC_ICONS = {
|
||||
blutbild: 'drop',
|
||||
roentgen: 'file-text',
|
||||
rezept: 'note',
|
||||
impfausweis:'certificate',
|
||||
sonstiges: 'file-text',
|
||||
};
|
||||
const DOC_LABELS = {
|
||||
blutbild: 'Blutbild',
|
||||
roentgen: 'Röntgen',
|
||||
rezept: 'Rezept',
|
||||
impfausweis:'Impfausweis',
|
||||
sonstiges: 'Sonstiges',
|
||||
};
|
||||
|
||||
const uploadBtn = `
|
||||
<button class="btn btn-primary btn-sm" id="health-docs-upload-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#upload"></use></svg> Befund hochladen
|
||||
</button>`;
|
||||
|
||||
const items = docs.length
|
||||
? docs.map(doc => {
|
||||
const icon = DOC_ICONS[doc.typ] || 'file-text';
|
||||
const label = DOC_LABELS[doc.typ] || doc.typ;
|
||||
const isImg = !['pdf'].includes(doc.file_type);
|
||||
const datum = doc.datum ? UI.time.format(doc.datum + 'T00:00:00') : '';
|
||||
return `
|
||||
<div class="health-card" style="align-items:flex-start">
|
||||
<div style="font-size:1.4rem;flex-shrink:0;color:var(--c-primary)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${_esc(icon)}"></use></svg>
|
||||
</div>
|
||||
<div class="health-card-body" style="flex:1;min-width:0">
|
||||
<div class="health-card-title">${_esc(doc.titel)}</div>
|
||||
<div class="health-card-meta">
|
||||
${_esc(label)}${datum ? ' · ' + datum : ''}
|
||||
${doc.vet_name ? ' · <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ' + _esc(doc.vet_name) : ''}
|
||||
</div>
|
||||
${doc.beschreibung ? `<div class="health-card-note">${_esc(doc.beschreibung)}</div>` : ''}
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);flex-wrap:wrap">
|
||||
<a href="${_esc(doc.file_path)}" target="_blank" rel="noopener"
|
||||
class="btn btn-secondary btn-sm" onclick="event.stopPropagation()">
|
||||
${isImg
|
||||
? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#image"></use></svg> Bild öffnen'
|
||||
: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF öffnen'}
|
||||
</a>
|
||||
<button class="btn btn-ghost btn-xs" style="color:var(--c-danger)"
|
||||
data-action="delete-hdoc" data-doc-id="${doc.id}"
|
||||
onclick="event.stopPropagation()">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')
|
||||
: `<p style="font-size:var(--text-sm);color:var(--c-text-muted);padding:var(--space-3) 0">
|
||||
Noch keine Befunde hochgeladen.
|
||||
</p>`;
|
||||
|
||||
return `
|
||||
<div style="margin-top:var(--space-5);padding-top:var(--space-4);
|
||||
border-top:1px solid var(--c-border)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-3)">
|
||||
<div style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-muted);
|
||||
text-transform:uppercase;letter-spacing:.05em">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> Befunde & Dokumente
|
||||
</div>
|
||||
${uploadBtn}
|
||||
</div>
|
||||
<div class="health-list" id="health-docs-list">${items}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _bindBefundeEvents(content) {
|
||||
content.querySelector('#health-docs-upload-btn')?.addEventListener('click', () => {
|
||||
_showBefundUploadModal();
|
||||
});
|
||||
content.querySelectorAll('[data-action="delete-hdoc"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const docId = parseInt(btn.dataset.docId);
|
||||
const ok = window.confirm('Befund wirklich löschen?');
|
||||
if (!ok) return;
|
||||
await UI.asyncButton(btn, async () => {
|
||||
await API.healthDocs.delete(docId);
|
||||
_healthDocs = _healthDocs.filter(d => d.id !== docId);
|
||||
_renderTab();
|
||||
UI.toast.success('Befund gelöscht.');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _showBefundUploadModal() {
|
||||
const aktivePraxen = _praxen.filter(p => p.aktiv);
|
||||
const dog = _appState.activeDog;
|
||||
|
||||
UI.modal.open({
|
||||
title: `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#upload"></use></svg> Befund hochladen`,
|
||||
body: `
|
||||
<form id="befund-form" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Art des Dokuments *</label>
|
||||
<select class="form-control" name="typ" required>
|
||||
<option value="">– bitte wählen –</option>
|
||||
<option value="blutbild">Blutbild</option>
|
||||
<option value="roentgen">Röntgen</option>
|
||||
<option value="rezept">Rezept</option>
|
||||
<option value="impfausweis">Impfausweis</option>
|
||||
<option value="sonstiges">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Titel *</label>
|
||||
<input class="form-control" type="text" name="titel" required
|
||||
placeholder="z.B. Blutbild März 2026">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Untersuchungsdatum</label>
|
||||
<input class="form-control" type="date" name="datum"
|
||||
value="${new Date().toISOString().slice(0,10)}">
|
||||
</div>
|
||||
${aktivePraxen.length ? `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tierarzt / Praxis</label>
|
||||
<select class="form-control" name="vet_id">
|
||||
<option value="">– optional –</option>
|
||||
${aktivePraxen.map(p =>
|
||||
`<option value="${p.id}">${_esc(p.name)}${p.ort ? ' · ' + _esc(p.ort) : ''}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
<div class="form-group">
|
||||
<label class="form-label">Beschreibung</label>
|
||||
<textarea class="form-control" name="beschreibung" rows="2"
|
||||
placeholder="Zusätzliche Infos (optional)"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Datei * (PDF, JPG, PNG, WebP — max. 10 MB)</label>
|
||||
<label class="btn btn-secondary btn-sm" style="cursor:pointer;display:inline-flex;
|
||||
align-items:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#plus"></use></svg> Datei auswählen
|
||||
<input type="file" name="file" id="befund-file-input"
|
||||
accept=".pdf,image/*"
|
||||
required
|
||||
style="position:absolute;opacity:0;width:1px;height:1px">
|
||||
</label>
|
||||
<div id="befund-file-preview" style="margin-top:var(--space-2);font-size:var(--text-sm);
|
||||
color:var(--c-text-secondary)"></div>
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
footer: `
|
||||
<button type="button" class="btn btn-secondary flex-1" id="befund-cancel">Abbrechen</button>
|
||||
<button type="submit" form="befund-form" class="btn btn-primary flex-1">Hochladen</button>
|
||||
`,
|
||||
});
|
||||
|
||||
document.getElementById('befund-cancel')?.addEventListener('click', UI.modal.close);
|
||||
|
||||
document.getElementById('befund-file-input')?.addEventListener('change', function () {
|
||||
const preview = document.getElementById('befund-file-preview');
|
||||
if (this.files?.length) {
|
||||
const f = this.files[0];
|
||||
preview.textContent = `${f.name} (${(f.size / 1024 / 1024).toFixed(2)} MB)`;
|
||||
} else {
|
||||
preview.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('befund-form')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.querySelector('[form="befund-form"][type="submit"]');
|
||||
const form = e.target;
|
||||
const fd = UI.formData(form);
|
||||
const fileInput = form.querySelector('[name="file"]');
|
||||
const file = fileInput?.files?.[0];
|
||||
|
||||
if (!fd.typ) { UI.toast.warning('Bitte Art des Dokuments wählen.'); return; }
|
||||
if (!fd.titel) { UI.toast.warning('Bitte Titel eingeben.'); return; }
|
||||
if (!file) { UI.toast.warning('Bitte eine Datei auswählen.'); return; }
|
||||
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
UI.toast.error('Datei ist zu groß. Maximum: 10 MB.');
|
||||
return;
|
||||
}
|
||||
|
||||
await UI.asyncButton(btn, async () => {
|
||||
const formData = new FormData();
|
||||
formData.append('dog_id', String(dog.id));
|
||||
formData.append('typ', fd.typ);
|
||||
formData.append('titel', fd.titel);
|
||||
formData.append('beschreibung', fd.beschreibung || '');
|
||||
formData.append('datum', fd.datum || '');
|
||||
if (fd.vet_id) formData.append('vet_id', fd.vet_id);
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const doc = await API.healthDocs.upload(formData);
|
||||
_healthDocs.unshift(doc);
|
||||
UI.modal.close();
|
||||
_renderTab();
|
||||
UI.toast.success('Befund hochgeladen.');
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Hochladen.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
async function _showKiSummary() {
|
||||
const btn = _container.querySelector('#health-ki-btn');
|
||||
|
|
@ -2323,6 +2682,129 @@ window.Page_health = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI-TIERARZTFRAGEN
|
||||
// ----------------------------------------------------------
|
||||
function _showKiTierarzt() {
|
||||
const dog = _appState.activeDog;
|
||||
const dogName = dog?.name || '';
|
||||
const rasse = dog?.rasse || '';
|
||||
const placeholder = dogName
|
||||
? `Welche Symptome zeigt ${dogName}? z.B. frisst nicht, schläft viel, lahmt...`
|
||||
: 'Welche Symptome zeigt dein Hund? z.B. frisst nicht, schläft viel, lahmt...';
|
||||
|
||||
UI.modal.open({
|
||||
title: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#stethoscope"></use></svg> KI-Tierarzt',
|
||||
body: `
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-3)">
|
||||
Beschreibe die Symptome deines Hundes. Die KI gibt eine erste Orientierung —
|
||||
kein Ersatz für einen echten Tierarzt.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<textarea id="ki-tierarzt-symptom" class="form-control" rows="4"
|
||||
placeholder="${_esc(placeholder)}"></textarea>
|
||||
</div>
|
||||
<div id="ki-tierarzt-result" style="display:none"></div>
|
||||
<div style="margin-top:var(--space-3);padding:var(--space-3);
|
||||
background:#fff3cd;border-radius:var(--radius-md);
|
||||
font-size:var(--text-xs);color:#856404;
|
||||
border:1px solid #ffc107">
|
||||
<strong>⚠️ Hinweis:</strong> Dies ist keine medizinische Diagnose.
|
||||
Bei ernsthaften oder sich verschlechternden Symptomen sofort zum Tierarzt.
|
||||
</div>`,
|
||||
footer: `
|
||||
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
|
||||
<button class="btn btn-primary" id="ki-tierarzt-submit-btn">Frage stellen</button>`,
|
||||
});
|
||||
|
||||
document.getElementById('ki-tierarzt-submit-btn')
|
||||
.addEventListener('click', async function () {
|
||||
const btn = this;
|
||||
const symptom = document.getElementById('ki-tierarzt-symptom').value.trim();
|
||||
const resultEl = document.getElementById('ki-tierarzt-result');
|
||||
|
||||
if (!symptom) {
|
||||
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('/ki/tierarzt', {
|
||||
symptom,
|
||||
dog_id: dog?.id || null,
|
||||
dog_name: dogName || null,
|
||||
rasse: rasse || null,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 429) {
|
||||
resultEl.innerHTML = `
|
||||
<div style="padding:var(--space-3);background:var(--c-surface);
|
||||
border-radius:var(--radius-md);border:1px solid var(--c-warning);
|
||||
font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clock"></use></svg>
|
||||
5 Anfragen pro Tag erreicht. Morgen wieder verfügbar.
|
||||
</div>`;
|
||||
} else if (err.status === 503) {
|
||||
resultEl.innerHTML = `
|
||||
<div style="padding:var(--space-3);background:var(--c-surface);
|
||||
border-radius:var(--radius-md);border:1px solid var(--c-danger);
|
||||
font-size:var(--text-sm)">
|
||||
KI momentan nicht verfügbar. Bitte später versuchen.
|
||||
</div>`;
|
||||
} else {
|
||||
UI.toast.error(err.message || 'Fehler bei der KI-Anfrage.');
|
||||
return;
|
||||
}
|
||||
resultEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const antwortHtml = _esc(result.antwort)
|
||||
.replace(/\n\n/g, '</p><p style="margin:var(--space-2) 0">')
|
||||
.replace(/\n/g, '<br>');
|
||||
const restHtml = result.limit - result.anfragen_heute > 0
|
||||
? `<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:var(--space-3) 0 0">
|
||||
Noch ${result.limit - result.anfragen_heute} von ${result.limit} Anfragen heute verfügbar.
|
||||
</p>`
|
||||
: `<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:var(--space-3) 0 0">
|
||||
Tageslimit erreicht. Morgen wieder verfügbar.
|
||||
</p>`;
|
||||
|
||||
resultEl.innerHTML = `
|
||||
<div style="margin-top:var(--space-4);padding:var(--space-4);
|
||||
background:var(--c-surface);border-radius:var(--radius-md);
|
||||
border:1px solid var(--c-border)">
|
||||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
margin-bottom:var(--space-2);color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#stethoscope"></use></svg>
|
||||
Einschätzung
|
||||
</div>
|
||||
<p style="font-size:var(--text-sm);line-height:1.7;margin:0">${antwortHtml}</p>
|
||||
${restHtml}
|
||||
</div>
|
||||
<div style="margin-top:var(--space-3);padding:var(--space-3);
|
||||
background:#fee2e2;border-radius:var(--radius-md);
|
||||
font-size:var(--text-xs);color:#991b1b;
|
||||
border:1px solid #fca5a5">
|
||||
<strong>⚠️ Dies ist keine medizinische Diagnose.</strong>
|
||||
Bei ernsthaften Symptomen sofort zum Tierarzt.
|
||||
</div>`;
|
||||
resultEl.style.display = '';
|
||||
|
||||
// Submit-Button ausblenden wenn Limit erschöpft
|
||||
if (result.anfragen_heute >= result.limit) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Limit erreicht';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, openNew, onDogChange };
|
||||
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue