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).
1168 lines
51 KiB
JavaScript
1168 lines
51 KiB
JavaScript
/* ============================================================
|
||
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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// 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> 70–80 % 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> 6–10 % Wasser, ca. 350–400 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- & 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 & 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 & 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 (1–5)</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)} · ${f.mahlzeiten} Mahlzeit${f.mahlzeiten !== 1 ? 'en' : ''}
|
||
${f.status !== 'neu' ? `· <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 ? ` · ${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 };
|
||
|
||
})();
|