banyaro/backend/static/js/pages/ernaehrung.js
rene 459cd425f2 Design-System Sprint A: utilities.css + 948 Inline-Styles → Utility-Klassen, SW by-v1102
PHASE 1 — Sofort-Cleanup ohne Risiko:
- Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen:
  * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary
  * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3
  * flex-between, flex-1-min, mb-1/3, mt-1/3
  * icon-xs/sm/md/lg, label-block, caption
- index.html bindet utilities.css ein
- mb-3/mt-3 ergänzt (waren in design-system.css unvollständig)

PHASE 2 — .by-tab Modifier für Vereinheitlichung:
- .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.)
- .by-tabs.sticky (Desktop vertikale Tabs für Admin)
- .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll)
- .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border)

PHASE 3 — Inline-Style → Klassen-Migration (Python-Script):
- 948 Inline-Styles entfernt (5101 → 4153, -18%)
- 962 Migrationen über 47 Page-Dateien
- Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67),
  litters.js (62), settings.js (61), zuchthunde.js (51)
- Patterns: text-muted, text-secondary, text-danger, text-xs-muted,
  text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3,
  p-3/4, mb-2/3/4, hidden, w-full, flex-1, ...
- Bewahrt bestehende class-Attribute (mergt korrekt)

Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
2026-05-27 07:11:27 +02:00

1168 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Ernährung
Tabs: Kalorien-Rechner | Futter-Guide | Giftliste | KI-Berater
============================================================ */
window.Page_ernaehrung = (() => {
let _container = null;
let _appState = null;
let _activeTab = 'rechner';
let _profil = {};
const TABS = [
{ key: 'rechner', label: 'Kalorien', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calculator"></use></svg>' },
{ key: 'guide', label: 'Futter-Guide', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>' },
{ key: 'gift', label: 'Giftliste', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>' },
{ key: 'ki', label: 'KI-Berater', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>' },
{ key: 'vertraeglichkeit', label: 'Verträglichkeit', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#heartbeat"></use></svg>' },
];
// ------------------------------------------------------------------
// Escape helper
// ------------------------------------------------------------------
function _esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ------------------------------------------------------------------
// LIFECYCLE
// ------------------------------------------------------------------
async function init(container, appState, params) {
_container = container;
_appState = appState;
if (params?.tab && TABS.some(t => t.key === params.tab)) {
_activeTab = params.tab;
}
await _render();
}
async function refresh() {
await _render();
}
async function onDogChange() {
_profil = {};
_activeTab = 'rechner'; // Tab zurücksetzen damit neuer Hund frisch startet
await _render();
}
// ------------------------------------------------------------------
// RENDER
// ------------------------------------------------------------------
async function _render() {
if (!_appState.activeDog) {
_container.innerHTML = UI.emptyState({
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bowl-food"></use></svg>',
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;
}
// Profil laden
const dog = _appState.activeDog;
try {
_profil = await API.get(`/dogs/${dog.id}/ernaehrung`);
} catch (_) {
_profil = {};
}
_container.innerHTML = `
<div style="padding:var(--space-3) var(--space-4) 0">${UI.dogChip(_appState)}</div>
<div class="by-tabs" id="ern-tabs"></div>
<div id="ern-tab-content"></div>
`;
UI.bindDogChip(_container, _appState);
_renderTabBar();
_renderTab();
}
// ------------------------------------------------------------------
// TAB-BAR
// ------------------------------------------------------------------
function _renderTabBar() {
const el = _container.querySelector('#ern-tabs');
if (!el) return;
el.innerHTML = TABS.map(t => `
<button class="by-tab${t.key === _activeTab ? ' active' : ''}"
data-tab="${t.key}">
${t.icon} ${t.label}
</button>`).join('');
el.querySelectorAll('.by-tab').forEach(btn => {
btn.addEventListener('click', () => {
_activeTab = btn.dataset.tab;
el.querySelectorAll('.by-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
_renderTab();
});
});
}
function _renderTab() {
const el = _container.querySelector('#ern-tab-content');
if (!el) return;
switch (_activeTab) {
case 'rechner': _renderRechner(el); break;
case 'guide': _renderGuide(el); break;
case 'gift': _renderGift(el); break;
case 'ki': _renderKi(el); break;
case 'vertraeglichkeit': _renderVertraeglichkeit(el); break;
}
}
// ------------------------------------------------------------------
// TAB 1: KALORIEN-RECHNER
// ------------------------------------------------------------------
function _renderRechner(el) {
const dog = _appState.activeDog;
// Auto-Werte aus Hundeprofil
const gewichtDefault = dog?.gewicht || '';
const alterDefault = dog?.alter || '';
el.innerHTML = `
<div style="padding:var(--space-4) 0">
<style>
.ern-pill-group { display:flex; gap:8px; flex-wrap:wrap; }
.ern-pill {
flex:1; min-width:0; padding:10px 8px; border-radius:12px;
border:1.5px solid var(--c-border); background:var(--c-bg-card);
color:var(--c-text-secondary); font-size:var(--text-xs); font-weight:600;
cursor:pointer; text-align:center; transition:all .15s; line-height:1.3;
}
.ern-pill.active {
background:var(--c-primary); color:#fff; border-color:var(--c-primary);
}
.ern-input-row {
display:grid; grid-template-columns:1fr 1fr; gap:var(--space-3);
margin-bottom:var(--space-4);
}
.ern-field { display:flex; flex-direction:column; gap:6px; }
.ern-field label { font-size:var(--text-xs); color:var(--c-text-secondary); font-weight:600; text-transform:uppercase; letter-spacing:.04em; }
.ern-field input { padding:12px 14px; border-radius:12px; border:1.5px solid var(--c-border); background:var(--c-bg-card); color:var(--c-text); font-size:var(--text-base); font-weight:700; width:100%; box-sizing:border-box; }
.ern-section-label { font-size:var(--text-xs); color:var(--c-text-secondary); font-weight:600; text-transform:uppercase; letter-spacing:.04em; margin-bottom:8px; }
</style>
<!-- Gewicht + Alter nebeneinander -->
<div class="ern-input-row">
<div class="ern-field">
<label>⚖️ Gewicht (kg)</label>
<input id="ern-gewicht" type="number" step="0.1" min="0.5" max="100"
value="${_esc(gewichtDefault)}" placeholder="15">
</div>
<div class="ern-field">
<label>🎂 Alter (Jahre)</label>
<input id="ern-alter" type="number" step="0.5" min="0" max="25"
value="${_esc(alterDefault)}" placeholder="3">
</div>
</div>
<!-- Aktivität als Pill-Buttons -->
<div class="mb-4">
<div class="ern-section-label">🏃 Aktivität</div>
<div class="ern-pill-group">
<button class="ern-pill" data-akt="gering">🛋️ Gemütlich</button>
<button class="ern-pill active" data-akt="normal">🚶 Normal</button>
<button class="ern-pill" data-akt="aktiv">🏃 Aktiv</button>
<button class="ern-pill" data-akt="sport">🏅 Sportlich</button>
</div>
</div>
<!-- Kastriert als Pill-Buttons -->
<div style="margin-bottom:var(--space-5)">
<div class="ern-section-label">✂️ Kastriert / Sterilisiert</div>
<div class="ern-pill-group">
<button class="ern-pill active" data-kas="nein" style="flex:none;width:calc(50% - 4px)">Nein</button>
<button class="ern-pill" data-kas="ja" style="flex:none;width:calc(50% - 4px)">Ja</button>
</div>
</div>
<button class="btn btn-primary" id="ern-rechner-btn" style="width:100%;padding:14px;font-size:var(--text-base)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calculator"></use></svg>
Kalorienbedarf berechnen
</button>
<div id="ern-rechner-result" style="display:none;margin-top:var(--space-5)"></div>
<!-- Profil speichern -->
<div id="ern-profil-speichern" style="display:none;margin-top:var(--space-4)">
<h4 style="font-size:var(--text-sm);margin-bottom:var(--space-2)">Profil speichern</h4>
<div style="display:grid;gap:var(--space-3)">
<div class="by-form-group" style="margin:0">
<label class="by-label">Futter-Typ</label>
<select id="ern-prof-typ" class="by-select">
<option value="">-- wählen --</option>
<option value="trocken"${_profil.futter_typ === 'trocken' ? ' selected' : ''}>Trockenfutter</option>
<option value="nass"${_profil.futter_typ === 'nass' ? ' selected' : ''}>Nassfutter</option>
<option value="barf"${_profil.futter_typ === 'barf' ? ' selected' : ''}>BARF</option>
<option value="mix"${_profil.futter_typ === 'mix' ? ' selected' : ''}>Mix</option>
</select>
</div>
<div class="by-form-group" style="margin:0">
<label class="by-label">Marke / Produkt</label>
<input id="ern-prof-marke" type="text" class="by-input"
value="${_esc(_profil.marke)}" placeholder="z. B. Royal Canin">
</div>
<div class="by-form-group" style="margin:0">
<label class="by-label">Portionen pro Tag</label>
<input id="ern-prof-portionen" type="number" min="1" max="6"
class="by-input" value="${_profil.portionen || 2}">
</div>
<div class="by-form-group" style="margin:0">
<label class="by-label">Notizen</label>
<textarea id="ern-prof-notizen" class="by-input" rows="2"
placeholder="Besonderheiten, Allergien...">${_esc(_profil.notizen)}</textarea>
</div>
<button class="btn btn-secondary" id="ern-prof-save-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#floppy-disk"></use></svg>
Profil speichern
</button>
</div>
</div>
</div>
`;
// Aktivität Pills
el.querySelectorAll('[data-akt]').forEach(btn => {
btn.addEventListener('click', () => {
el.querySelectorAll('[data-akt]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Kastriert Pills
el.querySelectorAll('[data-kas]').forEach(btn => {
btn.addEventListener('click', () => {
el.querySelectorAll('[data-kas]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
el.querySelector('#ern-rechner-btn').addEventListener('click', () => _berechne(el));
}
function _berechne(el) {
const gewicht = parseFloat(el.querySelector('#ern-gewicht').value);
const aktivitaet = el.querySelector('[data-akt].active')?.dataset.akt || 'normal';
const kastriert = el.querySelector('[data-kas].active')?.dataset.kas === 'ja';
if (!gewicht || gewicht < 0.5) {
UI.toast.warning('Bitte ein gültiges Gewicht eingeben.');
return;
}
const rer = 70 * Math.pow(gewicht, 0.75);
const faktoren = {
gering: { intakt: 1.2, kastriert: 1.0 },
normal: { intakt: 1.6, kastriert: 1.4 },
aktiv: { intakt: 1.8, kastriert: 1.6 },
sport: { intakt: 2.1, kastriert: 1.9 },
};
const kcal = Math.round(rer * faktoren[aktivitaet][kastriert ? 'kastriert' : 'intakt']);
// Umrechnung in Futtermengen
const trocken = Math.round(kcal / 3.5); // ~350 kcal/100g
const nass = Math.round(kcal / 0.85); // ~85 kcal/100g
const barf = Math.round(kcal / 1.5); // ~150 kcal/100g
const kcalFormatted = kcal.toLocaleString('de-DE');
const resultEl = el.querySelector('#ern-rechner-result');
resultEl.style.display = '';
resultEl.innerHTML = `
<div style="text-align:center;padding:var(--space-4);
background:var(--c-primary);color:#fff;
border-radius:var(--radius-lg);margin-bottom:var(--space-4)">
<div style="font-size:var(--text-2xl);font-weight:700">ca. ${kcalFormatted} kcal</div>
<div style="font-size:var(--text-sm);opacity:0.85">pro Tag</div>
</div>
<div style="display:grid;gap:var(--space-3)">
<div style="background:var(--c-surface);border-radius:var(--radius-md);
padding:var(--space-3);border:1px solid var(--c-border)">
<div style="font-weight:600;margin-bottom:4px">🌾 Trockenfutter</div>
<div class="text-sm-secondary">
(~350 kcal/100g)
</div>
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
${trocken} g / Tag
</div>
<div class="text-sm-secondary">
= ${Math.round(trocken/2)} g morgens + ${Math.round(trocken/2)} g abends
</div>
</div>
<div style="background:var(--c-surface);border-radius:var(--radius-md);
padding:var(--space-3);border:1px solid var(--c-border)">
<div style="font-weight:600;margin-bottom:4px">🥫 Nassfutter</div>
<div class="text-sm-secondary">
(~85 kcal/100g)
</div>
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
${nass} g / Tag
</div>
<div class="text-sm-secondary">
= ${Math.round(nass/2)} g morgens + ${Math.round(nass/2)} g abends
</div>
</div>
<div style="background:var(--c-surface);border-radius:var(--radius-md);
padding:var(--space-3);border:1px solid var(--c-border)">
<div style="font-weight:600;margin-bottom:4px">🥩 BARF</div>
<div class="text-sm-secondary">
(~150 kcal/100g)
</div>
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
${barf} g / Tag
</div>
<div class="text-sm-secondary">
= ${Math.round(barf/2)} g morgens + ${Math.round(barf/2)} g abends
</div>
</div>
</div>
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-3)">
Richtwert nach Nationaler Forschungsratsformel (NRC). Immer den Körperzustand beobachten.
</p>
`;
// Profil-Speichern einblenden und kcal vorbelegen
const profilSection = el.querySelector('#ern-profil-speichern');
profilSection.style.display = '';
// kcal für Speichern merken
profilSection.dataset.kcal = kcal;
el.querySelector('#ern-prof-save-btn').onclick = () => _speichereProfil(el, kcal);
}
async function _speichereProfil(el, kcal) {
const dog = _appState.activeDog;
const futter_typ = el.querySelector('#ern-prof-typ').value || null;
const marke = el.querySelector('#ern-prof-marke').value.trim() || null;
const portionen = parseInt(el.querySelector('#ern-prof-portionen').value) || 2;
const notizen = el.querySelector('#ern-prof-notizen').value.trim() || null;
const btn = el.querySelector('#ern-prof-save-btn');
await UI.asyncButton(btn, async () => {
try {
_profil = await API.put(`/dogs/${dog.id}/ernaehrung`, {
futter_typ, marke, kcal_tag: kcal, portionen, notizen,
});
UI.toast.success('Profil gespeichert.');
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Speichern.');
}
});
}
// ------------------------------------------------------------------
// TAB 2: FUTTER-GUIDE
// ------------------------------------------------------------------
function _renderGuide(el) {
const cards = [
{
id: 'barf',
emoji: '🥩',
titel: 'BARF (Rohfütterung)',
inhalt: `
<p><strong>Zusammensetzung:</strong> 70 % Muskelfleisch, 10 % rohe Knochen, 10 % Organe, 10 % Gemüse & Obst</p>
<p><strong>Vorteile:</strong> Naturnahste Ernährungsform, glänzendes Fell, weniger Kot, keine Zusatzstoffe</p>
<p><strong>Risiken:</strong> Keimbelastung durch rohes Fleisch, Calcium-Phosphor-Balance muss stimmen, zeitaufwändig und teurer</p>
<p><strong>Tipp:</strong> Niemals BARF und Trockenfutter in derselben Mahlzeit mischen — unterschiedliche Verdauungszeiten können zu Problemen führen.</p>
`,
},
{
id: 'nass',
emoji: '🥫',
titel: 'Nassfutter',
inhalt: `
<p><strong>Zusammensetzung:</strong> 7080 % Wasseranteil, meist höherer Fleischanteil als Trockenfutter</p>
<p><strong>Vorteile:</strong> Hunde trinken automatisch mehr (gut für die Niere), schmackhafter, gut für wählerische Hunde</p>
<p><strong>Worauf achten:</strong> Erste Zutat auf der Liste = Fleisch (nicht „Tierische Nebenerzeugnisse"), kein Zucker, kein Karamell</p>
<p><strong>Zähne:</strong> Schlechter für die Zahngesundheit als Trockenfutter — öfter Zähne putzen oder Kauartikel geben.</p>
`,
},
{
id: 'trocken',
emoji: '🌾',
titel: 'Trockenfutter',
inhalt: `
<p><strong>Zusammensetzung:</strong> 610 % Wasser, ca. 350400 kcal/100 g, konzentrierte Nährstoffe</p>
<p><strong>Gute Zutaten:</strong> Benanntes Fleisch an erster Stelle (Huhn, Lachs), mind. 40 % Tierprotein, kein Getreide als Hauptzutat</p>
<p><strong>Schlechte Zutaten:</strong> „Getreide" als erste Zutat, Zucker, Karamell, Konservierungsstoffe E320 / E321</p>
<p><strong>Wichtig:</strong> Immer frisches Wasser bereitstellen — Trockenfutter enthält kaum Feuchtigkeit.</p>
`,
},
];
el.innerHTML = `
<div style="padding:var(--space-4) 0">
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
Klicke auf eine Karte für Details.
</p>
${cards.map(c => `
<div class="ern-guide-card" data-id="${c.id}"
style="background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-lg);margin-bottom:var(--space-3);
overflow:hidden;cursor:pointer">
<div class="ern-guide-head"
style="display:flex;align-items:center;justify-content:space-between;
padding:var(--space-3) var(--space-4)">
<span style="font-weight:600;font-size:var(--text-base)">
${c.emoji} ${c.titel}
</span>
<svg class="ph-icon ern-guide-chevron" aria-hidden="true"
style="transition:transform .2s">
<use href="/icons/phosphor.svg#caret-down"></use>
</svg>
</div>
<div class="ern-guide-body"
style="display:none;padding:0 var(--space-4) var(--space-3);
font-size:var(--text-sm);color:var(--c-text-secondary);
line-height:1.6">
${c.inhalt}
</div>
</div>
`).join('')}
</div>
`;
el.querySelectorAll('.ern-guide-card').forEach(card => {
card.querySelector('.ern-guide-head').addEventListener('click', () => {
const body = card.querySelector('.ern-guide-body');
const chevron = card.querySelector('.ern-guide-chevron');
const open = body.style.display !== 'none';
body.style.display = open ? 'none' : '';
chevron.style.transform = open ? '' : 'rotate(180deg)';
});
});
}
// ------------------------------------------------------------------
// TAB 3: GIFTLISTE
// ------------------------------------------------------------------
function _renderGift(el) {
const items = [
{ emoji: '🍫', name: 'Schokolade', grund: 'Theobromin → Herzrasen, Krämpfe, kann tödlich sein' },
{ emoji: '🍇', name: 'Trauben & Rosinen', grund: 'Nierenversagen — auch kleinste Mengen gefährlich' },
{ emoji: '🧅', name: 'Zwiebeln & Knoblauch', grund: 'Zerstören rote Blutkörperchen → Anämie' },
{ emoji: '🥑', name: 'Avocado', grund: 'Persin → Erbrechen, Durchfall, Atemnot' },
{ emoji: '🌰', name: 'Macadamia-Nüsse', grund: 'Lähmungserscheinungen, Zittern, Erbrechen' },
{ emoji: '🍬', name: 'Xylitol (Süßstoff)', grund: 'Schwere Leberschäden, Unterzucker — oft in Kaugummi' },
{ emoji: '🥛', name: 'Milch & Milchprodukte', grund: 'Laktose-Intoleranz bei vielen Hunden → Durchfall' },
{ emoji: '🦴', name: 'Gekochte Knochen', grund: 'Splitter → innere Verletzungen, Darmverschluss' },
{ emoji: '☕', name: 'Koffein (Kaffee, Tee)', grund: 'Herzrasen, Zittern, Nervensystem' },
{ emoji: '🧂', name: 'Salz', grund: 'Natriumvergiftung → Erbrechen, Krämpfe' },
];
el.innerHTML = `
<div style="padding:var(--space-4) 0">
<div style="background:var(--c-warning-subtle,rgba(255,193,7,0.15));
border:1px solid var(--c-warning,#ffc107);
border-radius:var(--radius-md);padding:var(--space-3);
margin-bottom:var(--space-4);font-size:var(--text-sm);
color:var(--c-text)">
<strong>⚠️ Notfall-Tierarzt:</strong> Bei Verdacht auf Vergiftung sofort zum Tierarzt.
Nicht abwarten, auch wenn noch keine Symptome sichtbar sind.
</div>
<div style="display:grid;gap:var(--space-2)">
${items.map(item => `
<div style="background:var(--c-danger-subtle,rgba(220,38,38,0.08));
border:1px solid var(--c-danger-border,rgba(220,38,38,0.25));
border-radius:var(--radius-md);padding:var(--space-3)">
<div style="display:flex;align-items:center;gap:var(--space-2)">
<span style="font-size:1.4rem">${item.emoji}</span>
<div>
<div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text)">${_esc(item.name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-danger)">${_esc(item.grund)}</div>
</div>
</div>
</div>
`).join('')}
</div>
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-4)">
Diese Liste ist nicht vollständig. Im Zweifel gilt: lieber weglassen.
</p>
</div>
`;
}
// ------------------------------------------------------------------
// TAB 4: KI-FUTTERBERATER
// ------------------------------------------------------------------
function _renderKi(el) {
const dog = _appState.activeDog;
el.innerHTML = `
<div style="padding:var(--space-4) 0">
<div style="background:var(--c-surface-2,var(--c-surface));border-radius:var(--radius-md);
padding:var(--space-3);margin-bottom:var(--space-4);
font-size:var(--text-sm);color:var(--c-text-secondary);
border:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>
Der KI-Futterberater beantwortet Ernährungsfragen für
<strong>${_esc(dog?.name || 'deinen Hund')}</strong>.
Bei Gesundheitsfragen immer den Tierarzt zurate ziehen.
</div>
<!-- Vorschläge -->
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-3)">
${[
'Welches Futter empfiehlst du für meine Rasse?',
'Wie oft soll ich meinen Hund füttern?',
'Ist Getreide im Futter schlecht?',
'Welche Leckerlis sind gesund?',
].map(q => `
<button class="btn btn-sm btn-secondary ern-ki-vorschlag"
data-q="${_esc(q)}"
class="text-xs">${_esc(q)}</button>
`).join('')}
</div>
<!-- Chat-Verlauf -->
<div id="ern-ki-chat" style="min-height:80px;margin-bottom:var(--space-3)"></div>
<!-- Eingabe -->
<div class="flex-gap-2">
<textarea id="ern-ki-frage" class="by-input" rows="2"
placeholder="Deine Frage zur Ernährung..."
style="flex:1;resize:vertical"></textarea>
<button class="btn btn-primary" id="ern-ki-send-btn"
style="align-self:flex-end">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg>
</button>
</div>
</div>
`;
// Vorschläge
el.querySelectorAll('.ern-ki-vorschlag').forEach(btn => {
btn.addEventListener('click', () => {
el.querySelector('#ern-ki-frage').value = btn.dataset.q;
el.querySelector('#ern-ki-frage').focus();
});
});
// Senden
el.querySelector('#ern-ki-send-btn').addEventListener('click', () => _kiSenden(el));
el.querySelector('#ern-ki-frage').addEventListener('keydown', e => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) _kiSenden(el);
});
}
async function _kiSenden(el) {
const dog = _appState.activeDog;
const frageEl = el.querySelector('#ern-ki-frage');
const frage = frageEl.value.trim();
if (!frage) {
UI.toast.warning('Bitte eine Frage eingeben.');
return;
}
const chatEl = el.querySelector('#ern-ki-chat');
const sendBtn = el.querySelector('#ern-ki-send-btn');
// Userfrage anzeigen
chatEl.insertAdjacentHTML('beforeend', `
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-2)">
<div style="background:var(--c-primary);color:#fff;border-radius:var(--radius-md);
padding:var(--space-2) var(--space-3);max-width:80%;font-size:var(--text-sm)">
${_esc(frage)}
</div>
</div>
`);
frageEl.value = '';
// KI-Antwort Placeholder
const placeholderId = `ern-ki-placeholder-${Date.now()}`;
chatEl.insertAdjacentHTML('beforeend', `
<div id="${placeholderId}" class="mb-3">
<div style="background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
font-size:var(--text-sm);color:var(--c-text-muted)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#circle-notch"></use></svg>
Denke nach…
</div>
</div>
`);
chatEl.scrollTop = chatEl.scrollHeight;
await UI.asyncButton(sendBtn, async () => {
let antwort = '';
try {
const result = await API.post(`/dogs/${dog.id}/ernaehrung/ki-beratung`, {
frage,
dog_name: dog?.name || null,
rasse: dog?.rasse || null,
alter: dog?.alter != null ? String(dog.alter) : null,
gewicht: dog?.gewicht || null,
aktiv: false,
});
antwort = result.antwort || 'Keine Antwort erhalten.';
} catch (err) {
if (err.status === 503) {
antwort = 'Die KI ist momentan nicht verfügbar. Bitte später versuchen.';
} else {
antwort = 'Fehler bei der KI-Anfrage. Bitte später erneut versuchen.';
}
}
const antwortHtml = _esc(antwort)
.replace(/\n\n/g, '</p><p style="margin:var(--space-1) 0">')
.replace(/\n/g, '<br>');
const placeholder = document.getElementById(placeholderId);
if (placeholder) {
placeholder.innerHTML = `
<div style="background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
font-size:var(--text-sm);line-height:1.6;max-width:90%">
<p style="margin:0">${antwortHtml}</p>
</div>
`;
}
chatEl.scrollTop = chatEl.scrollHeight;
});
}
// ------------------------------------------------------------------
// TAB 5: VERTRÄGLICHKEIT
// ------------------------------------------------------------------
async function _renderVertraeglichkeit(el) {
const dog = _appState?.activeDog;
if (!dog) { el.innerHTML = ''; return; }
el.innerHTML = `
<div style="padding:var(--space-4) 0">
<!-- Schnell-Erfassung -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-bottom:var(--space-4)">
<button class="btn btn-primary" id="vert-btn-futter">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bowl-food"></use></svg>
Futter erfassen
</button>
<button class="btn btn-secondary" id="vert-btn-reaktion">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#heartbeat"></use></svg>
Reaktion erfassen
</button>
</div>
<!-- Info-Banner Haut/Fell -->
<div style="display:flex;align-items:flex-start;gap:var(--space-2);
background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-md);padding:var(--space-3);
margin-bottom:var(--space-4);font-size:var(--text-xs);
color:var(--c-text-secondary);line-height:1.5">
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-primary);margin-top:1px">
<use href="/icons/phosphor.svg#info"></use>
</svg>
<span>Haut- &amp; Fellsymptome zeigen sich erst nach Wochen — trage regelmäßig ein um Muster zu erkennen.</span>
</div>
<!-- Analyse -->
<div id="vert-analyse" style="margin-bottom:var(--space-5)">
<div style="text-align:center;color:var(--c-text-muted);padding:var(--space-4)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#circle-notch"></use></svg>
Lade Analyse…
</div>
</div>
<!-- Verlauf -->
<div id="vert-verlauf"></div>
</div>
`;
el.querySelector('#vert-btn-futter').addEventListener('click', () => _openFutterModal(el, dog));
el.querySelector('#vert-btn-reaktion').addEventListener('click', () => _openReaktionModal(el, dog));
await _loadAnalyse(el, dog);
await _loadVerlauf(el, dog);
}
function _todayStr() {
return new Date().toISOString().slice(0, 10);
}
function _nowTimeStr() {
const d = new Date();
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
function _openFutterModal(el, dog) {
const id = `fm-${Date.now()}`;
const body = `
<form id="${id}">
<div class="by-form-group">
<label class="by-label">Datum</label>
<input type="date" name="datum" class="form-control by-input" value="${_todayStr()}" required>
</div>
<div class="by-form-group">
<label class="by-label">Uhrzeit</label>
<input type="time" name="uhrzeit" class="form-control by-input" value="${_nowTimeStr()}" required>
</div>
<div class="by-form-group">
<label class="by-label">Futter-Name</label>
<input type="text" name="futter_name" class="form-control by-input"
list="vert-futter-datalist" placeholder="z. B. Wolfsblut Adult" required>
<datalist id="vert-futter-datalist"></datalist>
</div>
<div class="by-form-group">
<label class="by-label">Futter-Typ</label>
<select name="futter_typ" class="form-control by-select">
<option value="trockenfutter">Trockenfutter</option>
<option value="nassfutter">Nassfutter</option>
<option value="barf">BARF</option>
<option value="snack">Snack</option>
<option value="sonstiges">Sonstiges</option>
</select>
</div>
<div class="by-form-group">
<label class="by-label">Menge (g, optional)</label>
<input type="number" name="menge_g" class="form-control by-input" min="1" placeholder="">
</div>
<div class="by-form-group">
<label class="by-label">Notiz (optional)</label>
<textarea name="notiz" class="form-control by-input" rows="2" placeholder=""></textarea>
</div>
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" id="vert-futter-save-btn" form="${id}">Speichern</button>
`;
UI.modal.open({ title: 'Futter erfassen', body, footer });
// Datalist mit bekannten Futter-Namen füllen
API.dogs.futterList(dog.id).then(list => {
const dl = document.getElementById('vert-futter-datalist');
if (!dl) return;
const names = [...new Set((list || []).map(e => e.futter_name))];
dl.innerHTML = names.map(n => `<option value="${_esc(n)}">`).join('');
}).catch(() => {});
setTimeout(() => {
const saveBtn = document.getElementById('vert-futter-save-btn');
if (!saveBtn) return;
saveBtn.addEventListener('click', async (ev) => {
ev.preventDefault();
const form = document.getElementById(id);
if (!form) return;
const fd = new FormData(form);
const data = {
datum: fd.get('datum'),
uhrzeit: fd.get('uhrzeit'),
futter_name: (fd.get('futter_name') || '').trim(),
futter_typ: fd.get('futter_typ') || 'trockenfutter',
menge_g: fd.get('menge_g') ? parseInt(fd.get('menge_g')) : null,
notiz: (fd.get('notiz') || '').trim() || null,
};
if (!data.futter_name) { UI.toast.warning('Bitte Futter-Name angeben.'); return; }
await UI.asyncButton(saveBtn, async () => {
try {
await API.dogs.futterCreate(dog.id, data);
UI.modal.close();
UI.toast.success('Futter gespeichert.');
const tabEl = _container.querySelector('#ern-tab-content');
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Speichern.');
}
});
});
}, 50);
}
function _openReaktionModal(el, dog) {
const id = `rm-${Date.now()}`;
const body = `
<form id="${id}">
<div class="by-form-group">
<label class="by-label">Datum</label>
<input type="date" name="datum" class="form-control by-input" value="${_todayStr()}" required>
</div>
<div class="by-form-group">
<label class="by-label">Uhrzeit</label>
<input type="time" name="uhrzeit" class="form-control by-input" value="${_nowTimeStr()}" required>
</div>
<div class="by-form-group">
<label class="by-label">Reaktion</label>
<select name="reaktion_typ" class="form-control by-select" required>
<optgroup label="✓ Positiv">
<option value="fell_glaenzend">Glänzendes Fell</option>
<option value="verdauung_gut">Gute Verdauung</option>
<option value="energie_hoch">Viel Energie</option>
</optgroup>
<optgroup label="Magen &amp; Darm">
<option value="erbrechen">Erbrechen</option>
<option value="durchfall">Durchfall</option>
<option value="blaehungen">Blähungen</option>
<option value="weicher_stuhl">Weicher Stuhl</option>
<option value="appetitlosigkeit">Appetitlosigkeit</option>
</optgroup>
<optgroup label="Haut &amp; Fell">
<option value="juckreiz">Juckreiz / Kratzen</option>
<option value="haarausfall">Haarausfall</option>
<option value="stumpfes_fell">Stumpfes Fell</option>
<option value="schuppenbildung">Schuppenbildung</option>
<option value="roetungen">Hautrötungen / Entzündung</option>
<option value="pfotenlecken">Pfoten lecken (chronisch)</option>
<option value="ohrentzuendung">Ohrentzündung</option>
<option value="fettiges_fell">Fettiges Fell / Seborrhö</option>
</optgroup>
<optgroup label="Allgemeinbefinden">
<option value="schlappheit">Schlappheit / Apathie</option>
<option value="nervositaet">Nervosität / Unruhe</option>
<option value="viel_trinken">Ungewöhnlich viel trinken</option>
<option value="sonstiges">Sonstiges</option>
</optgroup>
</select>
</div>
<div class="by-form-group">
<label class="by-label">Intensität (15)</label>
<div class="vert-stern-gruppe" style="display:flex;gap:6px;flex-wrap:wrap">
${[1,2,3,4,5].map(n => `
<button type="button" class="vert-stern${n <= 3 ? ' active' : ''}"
data-val="${n}"
style="width:40px;height:40px;border-radius:8px;
border:1.5px solid var(--c-border);
background:${n <= 3 ? 'var(--c-primary)' : 'var(--c-bg-card)'};
color:${n <= 3 ? '#fff' : 'var(--c-text-secondary)'};
font-weight:700;cursor:pointer;">${n}</button>
`).join('')}
</div>
<input type="hidden" name="intensitaet" value="3">
</div>
<div class="by-form-group">
<label class="by-label">Notiz (optional)</label>
<textarea name="notiz" class="form-control by-input" rows="2" placeholder=""></textarea>
</div>
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" id="vert-reaktion-save-btn" form="${id}">Speichern</button>
`;
UI.modal.open({ title: 'Reaktion erfassen', body, footer });
setTimeout(() => {
// Stern-Buttons
document.querySelectorAll('.vert-stern').forEach(btn => {
btn.addEventListener('click', () => {
const val = parseInt(btn.dataset.val);
const form = document.getElementById(id);
if (form) form.querySelector('[name=intensitaet]').value = val;
document.querySelectorAll('.vert-stern').forEach(b => {
const v = parseInt(b.dataset.val);
b.style.background = v <= val ? 'var(--c-primary)' : 'var(--c-bg-card)';
b.style.color = v <= val ? '#fff' : 'var(--c-text-secondary)';
});
});
});
const saveBtn = document.getElementById('vert-reaktion-save-btn');
if (!saveBtn) return;
saveBtn.addEventListener('click', async (ev) => {
ev.preventDefault();
const form = document.getElementById(id);
if (!form) return;
const fd = new FormData(form);
const data = {
datum: fd.get('datum'),
uhrzeit: fd.get('uhrzeit'),
reaktion_typ: fd.get('reaktion_typ'),
intensitaet: parseInt(fd.get('intensitaet')) || 3,
notiz: (fd.get('notiz') || '').trim() || null,
};
await UI.asyncButton(saveBtn, async () => {
try {
await API.dogs.reaktionCreate(dog.id, data);
UI.modal.close();
UI.toast.success('Reaktion gespeichert.');
const tabEl = _container.querySelector('#ern-tab-content');
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Speichern.');
}
});
});
}, 50);
}
async function _loadAnalyse(el, dog) {
const analyseEl = el.querySelector('#vert-analyse');
if (!analyseEl) return;
let data;
try {
data = await API.dogs.futterAnalyse(dog.id);
} catch (_) {
analyseEl.innerHTML = `<p class="text-sm-muted">Analyse nicht verfügbar.</p>`;
return;
}
if (!data.futter || data.futter.length === 0) {
analyseEl.innerHTML = `
<div style="text-align:center;padding:var(--space-4);color:var(--c-text-muted);font-size:var(--text-sm)">
<svg class="ph-icon" style="width:2rem;height:2rem;margin-bottom:8px;display:block;margin-inline:auto" aria-hidden="true">
<use href="/icons/phosphor.svg#chart-bar"></use>
</svg>
Noch keine Einträge. Erfasse Futter und Reaktionen um die Analyse zu sehen.
</div>
`;
return;
}
const STATUS_CFG = {
gut: { label: 'Gut verträglich', color: 'var(--c-success,#22c55e)', bg: 'rgba(34,197,94,0.12)' },
neutral: { label: 'Neutral', color: 'var(--c-warning,#f59e0b)', bg: 'rgba(245,158,11,0.12)' },
problematisch:{ label: 'Problematisch', color: 'var(--c-danger,#ef4444)', bg: 'rgba(239,68,68,0.12)' },
neu: { label: 'Zu wenig Daten', color: 'var(--c-text-muted)', bg: 'var(--c-surface)' },
};
const TYP_LABELS = {
trockenfutter: 'Trockenfutter', nassfutter: 'Nassfutter',
barf: 'BARF', snack: 'Snack', sonstiges: 'Sonstiges',
};
const KAT_LABELS = {
gastro_negativ: 'Magen & Darm',
haut_negativ: 'Haut & Fell',
allgemein_negativ: 'Allgemein',
positiv: 'Positiv',
sonstiges: 'Sonstiges',
};
const hinweisHtml = data.hinweis ? `
<div style="display:flex;align-items:flex-start;gap:var(--space-2);
background:rgba(245,158,11,0.12);border:1px solid var(--c-warning,#f59e0b);
border-radius:var(--radius-md);padding:var(--space-3);
margin-bottom:var(--space-3);font-size:var(--text-xs);
color:var(--c-text);line-height:1.5">
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-warning,#f59e0b);margin-top:1px">
<use href="/icons/phosphor.svg#warning-circle"></use>
</svg>
<span>${_esc(data.hinweis)}</span>
</div>
` : '';
analyseEl.innerHTML = `
<h4 style="font-size:var(--text-sm);font-weight:700;margin-bottom:var(--space-3);color:var(--c-text)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#chart-bar"></use></svg>
Verträglichkeits-Analyse
<span style="font-weight:400;color:var(--c-text-muted);font-size:var(--text-xs)">
(${data.eintraege_count} Mahlzeiten, ${data.reaktionen_count} Reaktionen)
</span>
</h4>
${hinweisHtml}
<div style="display:grid;gap:var(--space-2)">
${data.futter.map(f => {
const cfg = STATUS_CFG[f.status] || STATUS_CFG.neu;
// Symptom-Kategorien des Futters als Chips
const katChips = Object.entries(f.kategorien || {})
.filter(([kat]) => kat !== 'positiv')
.map(([kat, cnt]) => {
const isHaut = kat === 'haut_negativ';
const isGastro = kat === 'gastro_negativ';
const chipColor = isHaut ? 'var(--c-warning,#f59e0b)' :
isGastro ? 'var(--c-danger,#ef4444)' :
'var(--c-text-muted)';
return `<span style="font-size:10px;font-weight:600;padding:2px 6px;
border-radius:999px;border:1px solid ${chipColor};
color:${chipColor};white-space:nowrap">
${_esc(KAT_LABELS[kat] || kat)} ×${cnt}
</span>`;
}).join('');
return `
<div style="background:${cfg.bg};border:1px solid ${cfg.color};
border-radius:var(--radius-md);padding:var(--space-3);
display:flex;align-items:center;justify-content:space-between;gap:var(--space-2)">
<div style="min-width:0;flex:1">
<div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(f.name)}
</div>
<div class="text-xs-muted">
${_esc(TYP_LABELS[f.typ] || f.typ)} &middot; ${f.mahlzeiten} Mahlzeit${f.mahlzeiten !== 1 ? 'en' : ''}
${f.status !== 'neu' ? `&middot; <span style="color:var(--c-success,#22c55e)">+${f.positiv}</span> / <span style="color:var(--c-danger,#ef4444)">-${f.negativ}</span>` : ''}
</div>
${katChips ? `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">${katChips}</div>` : ''}
</div>
<span style="flex-shrink:0;font-size:var(--text-xs);font-weight:700;
color:${cfg.color};white-space:nowrap">
${_esc(cfg.label)}
</span>
</div>
`;
}).join('')}
</div>
`;
}
async function _loadVerlauf(el, dog) {
const verlaufEl = el.querySelector('#vert-verlauf');
if (!verlaufEl) return;
let eintraege = [], reaktionen = [];
try {
[eintraege, reaktionen] = await Promise.all([
API.dogs.futterList(dog.id),
API.dogs.reaktionList(dog.id),
]);
} catch (_) { return; }
// Letzten 10 Futter + 5 Reaktionen, gemischt chronologisch
const items = [
...(eintraege || []).slice(0, 10).map(e => ({ ...e, _art: 'futter' })),
...(reaktionen || []).slice(0, 5).map(r => ({ ...r, _art: 'reaktion' })),
].sort((a, b) => {
const ta = `${a.datum}T${a.uhrzeit}`;
const tb = `${b.datum}T${b.uhrzeit}`;
return tb.localeCompare(ta);
});
if (items.length === 0) {
verlaufEl.innerHTML = '';
return;
}
const REAK_LABELS = {
// Positiv
verdauung_gut: 'Gute Verdauung',
energie_hoch: 'Viel Energie',
fell_glaenzend: 'Glänzendes Fell',
// Gastro
erbrechen: 'Erbrechen',
durchfall: 'Durchfall',
blaehungen: 'Blähungen',
weicher_stuhl: 'Weicher Stuhl',
appetitlosigkeit: 'Appetitlosigkeit',
// Haut & Fell
juckreiz: 'Juckreiz / Kratzen',
haarausfall: 'Haarausfall',
stumpfes_fell: 'Stumpfes Fell',
schuppenbildung: 'Schuppenbildung',
roetungen: 'Hautrötungen / Entzündung',
pfotenlecken: 'Pfoten lecken (chronisch)',
ohrentzuendung: 'Ohrentzündung',
fettiges_fell: 'Fettiges Fell / Seborrhö',
// Allgemein
schlappheit: 'Schlappheit / Apathie',
nervositaet: 'Nervosität / Unruhe',
viel_trinken: 'Ungewöhnlich viel trinken',
sonstiges: 'Sonstiges',
};
const NEGATIV_TYPEN = new Set([
'erbrechen','durchfall','blaehungen','weicher_stuhl','appetitlosigkeit',
'juckreiz','haarausfall','stumpfes_fell','schuppenbildung','roetungen',
'pfotenlecken','ohrentzuendung','fettiges_fell',
'schlappheit','nervositaet','viel_trinken',
]);
const POSITIV_TYPEN = new Set(['verdauung_gut','energie_hoch','fell_glaenzend']);
verlaufEl.innerHTML = `
<h4 style="font-size:var(--text-sm);font-weight:700;margin-bottom:var(--space-2);color:var(--c-text)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clock-counter-clockwise"></use></svg>
Verlauf
</h4>
<div style="display:grid;gap:var(--space-2)">
${items.map(item => {
if (item._art === 'futter') {
return `
<div data-futter-id="${item.id}"
style="background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-primary)">
<use href="/icons/phosphor.svg#bowl-food"></use>
</svg>
<div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(item.futter_name)}</div>
<div class="text-xs-muted">
${_esc(item.datum)} ${_esc(item.uhrzeit)}
${item.menge_g ? ` &middot; ${item.menge_g} g` : ''}
</div>
</div>
<button class="btn-icon vert-del-futter" data-id="${item.id}"
style="flex-shrink:0;background:none;border:none;cursor:pointer;padding:4px;
color:var(--c-text-muted)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>
</div>
`;
} else {
const isNeg = NEGATIV_TYPEN.has(item.reaktion_typ);
const isPos = POSITIV_TYPEN.has(item.reaktion_typ);
const col = isNeg ? 'var(--c-danger,#ef4444)' : isPos ? 'var(--c-success,#22c55e)' : 'var(--c-text-muted)';
return `
<div data-reaktion-id="${item.id}"
style="background:var(--c-surface);border:1px solid var(--c-border);
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:${col}">
<use href="/icons/phosphor.svg#heartbeat"></use>
</svg>
<div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm);color:${col}">
${_esc(REAK_LABELS[item.reaktion_typ] || item.reaktion_typ)}
<span style="font-weight:400;color:var(--c-text-muted)">(${item.intensitaet}/5)</span>
</div>
<div class="text-xs-muted">
${_esc(item.datum)} ${_esc(item.uhrzeit)}
</div>
</div>
<button class="btn-icon vert-del-reaktion" data-id="${item.id}"
style="flex-shrink:0;background:none;border:none;cursor:pointer;padding:4px;
color:var(--c-text-muted)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>
</div>
`;
}
}).join('')}
</div>
`;
// Löschen-Buttons
verlaufEl.querySelectorAll('.vert-del-futter').forEach(btn => {
btn.addEventListener('click', async () => {
if (!window.confirm('Eintrag löschen?')) return;
try {
await API.dogs.futterDelete(dog.id, parseInt(btn.dataset.id));
UI.toast.success('Eintrag gelöscht.');
const tabEl = _container.querySelector('#ern-tab-content');
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
} catch (err) {
UI.toast.error(err.message || 'Fehler.');
}
});
});
verlaufEl.querySelectorAll('.vert-del-reaktion').forEach(btn => {
btn.addEventListener('click', async () => {
if (!window.confirm('Reaktion löschen?')) return;
try {
await API.dogs.reaktionDelete(dog.id, parseInt(btn.dataset.id));
UI.toast.success('Reaktion gelöscht.');
const tabEl = _container.querySelector('#ern-tab-content');
if (tabEl) await _loadAnalyse(tabEl, dog), await _loadVerlauf(tabEl, dog);
} catch (err) {
UI.toast.error(err.message || 'Fehler.');
}
});
});
}
// ------------------------------------------------------------------
// PUBLIC API
// ------------------------------------------------------------------
return { init, refresh, onDogChange };
})();