Welten (worlds.js): - Swipe-Hints beim ersten Öffnen (JETZT ← → WELT animiert, einmalig) - Kein-Hund-Onboarding: Feature-Preview-Grid statt leerer Karte - Hintergrund-Foto-Hint: Kamera-Karte wenn noch kein Tagebuchfoto - worlds-back: navigiert zu Welcome wenn kein User eingeloggt - Nach Logout: worlds-back Button sofort ausgeblendet Wetter (wetter.js): - Standort-Fehlerseite zu Motivations-Seite umgebaut - Feature-Preview: Gassi-Score, 7-Tage, Regenradar, Rekorde - CTA: Standort freigeben + Registrieren (nur für Gäste) Settings (settings.js): - Logo in Auth-Form: display:block + margin:0 auto zentriert - Header bleibt sichtbar (FAB/Zurück-Navigation funktioniert) Jobs (jobs.js): - 2-Spalten-Grid auf Mobile: auto-fit statt festes 1fr 1fr - Kein doppeltes Padding im Wrapper Backend: - weather.py, achievements.py: diary JOIN fix (d.user_id → dogs JOIN) - Neue Wetter-Badges: wetter_tapfer, jahreszeiten, schnee - Ernährungs-, Reise-, Ausgaben-Seite: diverse UX-Verbesserungen - Presse-Seite erweitert - Ban Yaro Foto-Assets (WebP + HIRES JPG)
635 lines
28 KiB
JavaScript
635 lines
28 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>' },
|
||
];
|
||
|
||
// ------------------------------------------------------------------
|
||
// 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 = {};
|
||
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 class="by-tabs" id="ern-tabs"></div>
|
||
<div id="ern-tab-content"></div>
|
||
`;
|
||
|
||
_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;
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// 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 style="margin-bottom:var(--space-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 style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
(~350 kcal/100g)
|
||
</div>
|
||
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
|
||
${trocken} g / Tag
|
||
</div>
|
||
<div style="font-size:var(--text-sm);color:var(--c-text-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 style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
(~85 kcal/100g)
|
||
</div>
|
||
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
|
||
${nass} g / Tag
|
||
</div>
|
||
<div style="font-size:var(--text-sm);color:var(--c-text-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 style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
(~150 kcal/100g)
|
||
</div>
|
||
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
|
||
${barf} g / Tag
|
||
</div>
|
||
<div style="font-size:var(--text-sm);color:var(--c-text-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:#fff3cd;border:1px solid #ffc107;border-radius:var(--radius-md);
|
||
padding:var(--space-3);margin-bottom:var(--space-4);
|
||
font-size:var(--text-sm)">
|
||
<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:#fff5f5;border:1px solid #fed7d7;
|
||
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)">${_esc(item.name)}</div>
|
||
<div style="font-size:var(--text-xs);color:#c53030">${_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)}"
|
||
style="font-size:var(--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 style="display:flex;gap:var(--space-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}" style="margin-bottom:var(--space-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;
|
||
});
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// PUBLIC API
|
||
// ------------------------------------------------------------------
|
||
return { init, refresh, onDogChange };
|
||
|
||
})();
|