banyaro/backend/static/js/pages/uebungen.js
rene b58789373c Sprint 12: UI-Vereinheitlichung + Läufigkeits-Tracker
- by-tabs/by-tab: einheitliche Tab/Pill-Navigation in allen Seiten
- by-section-label, by-toolbar: einheitliche Section-Labels und Toolbars
- Design-Tokens: fehlende --c-amber, --c-primary-soft ergänzt, Fallback-Werte entfernt
- sitting.js: sitting-layout für konsistentes flush-Layout (wie walks)
- Läufigkeits-Tracker: neuer Health-Tab für Hündinnen mit Zyklusvorhersage,
  Timeline vergangener Läufigkeiten, Erinnerungen und auto-berechnetem Nächst-Datum
- emptyState-Bug: icon-Parameter muss SVG sein, nicht Icon-Name (dog/bell/warning gefixt)
- SW-Cache: by-v103, APP_VER: 79
2026-04-16 22:31:33 +02:00

884 lines
41 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 — Übungsbibliothek
Seiten-Modul: Grundkommandos, Tricks, Problemverhalten, Grundlagen.
============================================================ */
window.Page_uebungen = (() => {
let _container = null;
let _appState = null;
let _activeTab = 'grundkommandos';
// ----------------------------------------------------------
// DATEN
// ----------------------------------------------------------
const TABS = [
{ id: 'grundkommandos', label: 'Grundkommandos' },
{ id: 'tricks', label: 'Tricks & Beschäftigung' },
{ id: 'problemverhalten', label: 'Problemverhalten' },
{ id: 'grundlagen', label: 'Trainingsgrundlagen' },
{ id: 'ki-trainer', label: 'KI-Trainer' },
];
// ----------------------------------------------------------
// ÜBUNGS-STATUS
// ----------------------------------------------------------
const STATUS = [
{ id: null, icon: 'flag', color: 'var(--c-border)', label: 'Noch nicht geübt' },
{ id: 'noch-nicht', icon: 'x', color: 'var(--c-danger)', label: 'Klappt noch nicht' },
{ id: 'manchmal', icon: 'fire', color: '#f59e0b', label: 'Manchmal klappt es' },
{ id: 'meistens', icon: 'star', color: '#eab308', label: 'Meistens klappt es' },
{ id: 'sitzt', icon: 'trophy', color: 'var(--c-primary)', label: 'Sitzt!' },
];
function _statusKey(tab, name) {
return `ub_status_${tab}_${name.replace(/\s+/g, '_')}`;
}
function _getStatus(tab, name) {
return localStorage.getItem(_statusKey(tab, name)) || null;
}
function _setStatus(tab, name, statusId) {
if (statusId === null) {
localStorage.removeItem(_statusKey(tab, name));
} else {
localStorage.setItem(_statusKey(tab, name), statusId);
}
}
function _nextStatus(currentId) {
const idx = STATUS.findIndex(s => s.id === currentId);
const next = (idx + 1) % STATUS.length;
return STATUS[next].id;
}
function _statusMeta(statusId) {
return STATUS.find(s => s.id === statusId) || STATUS[0];
}
const DIFF_META = {
'Anfänger': { label: 'Anfänger', color: 'var(--c-success)' },
'Fortgeschrittener Anfänger': { label: 'Fortgeschr. Anfänger', color: '#eab308' },
'Mittel': { label: 'Mittel', color: 'var(--c-primary)' },
'Anfänger bis Fortgeschrittener': { label: 'AnfängerFortgeschr.', color: '#eab308' },
};
const GRUNDKOMMANDOS = [
{
name: 'Sitz',
schwierigkeit: 'Anfänger',
alter: 'Ab 8 Wochen',
dauer: '35 Minuten',
material: 'Leckerlis, ruhige Umgebung',
beschreibung: 'Der Hund setzt sich auf ein Signal hin. Das ist meist das erste Kommando und bildet die Basis für viele weitere Übungen.',
schritte: [
'Halte ein Leckerli knapp vor die Nase deines Hundes.',
'Führe das Leckerli langsam nach oben und leicht nach hinten über seinen Kopf.',
'Der Hund folgt mit der Nase — sein Hinterteil senkt sich automatisch.',
'Sobald er sitzt: sofort Markerwort ("Ja!" oder Klicker) + Leckerli.',
'Wiederhole 510x, bevor du das Wort "Sitz" hinzufügst.',
'Ab Wiederholung 10: Sage "Sitz" kurz bevor du die Handbewegung machst.',
],
fehler: [
'Leckerli zu hoch halten → Hund springt hoch statt zu sitzen',
'Kommando zu früh einführen → Hund lernt das Wort bevor er die Bewegung kennt',
'Zu lange Einheiten → Hund wird unkonzentriert',
],
steigerung: 'Sitz mit Ablenkung → Sitz aus Bewegung → Sitz auf Distanz',
},
{
name: 'Platz',
schwierigkeit: 'Anfänger',
alter: 'Ab 10 Wochen',
dauer: '35 Minuten',
material: 'Leckerlis, ruhige Umgebung',
beschreibung: 'Der Hund legt sich auf ein Signal hin. Wichtig für Ruhephasen, Wartesituationen und als Basis für "Bleib".',
schritte: [
'Beginne mit dem Hund im Sitz.',
'Halte ein Leckerli vor seine Nase und führe es langsam senkrecht nach unten zwischen seine Vorderpfoten.',
'Der Hund folgt mit der Nase und legt sich ab.',
'Sobald Ellenbogen und Hinterteil den Boden berühren: Markerwort + Leckerli.',
'Klappt es nicht: Leckerli unter ein angewinkeltes Knie halten — der Hund kriecht darunter durch und legt sich dabei ab.',
'Wort "Platz" erst nach 1015 erfolgreichen Wiederholungen einführen.',
],
fehler: [
'Hund steht auf statt sich hinzulegen Leckerli-Führung zu weit weg',
'Hund liegt nur kurz zu früh belohnen, Dauer schrittweise aufbauen',
],
steigerung: 'Platz mit Dauer Platz mit Distanz Platz bei Ablenkung',
},
{
name: 'Bleib',
schwierigkeit: 'Anfänger bis Fortgeschrittener',
alter: 'Ab 12 Wochen',
dauer: '5 Minuten',
material: 'Leckerlis, Geduld',
beschreibung: 'Der Hund hält eine Position (Sitz oder Platz) bis er freigegeben wird. Drei Dimensionen: Dauer, Distanz, Ablenkung immer nur eine auf einmal steigern.',
schritte: [
'Hund ins Sitz oder Platz bringen.',
'Einen Moment warten (2 Sekunden) Markerwort + Leckerli.',
'Freigabewort einführen: "Okay" oder "Frei" danach darf der Hund aufstehen.',
'Dauer schrittweise erhöhen: 2 5 10 30 Sekunden.',
'Erst wenn Dauer stabil ist: einen kleinen Schritt zurücktreten.',
'Zurückkehren zum Hund, belohnen nicht den Hund zu dir kommen lassen.',
],
fehler: [
'Zu schnell Distanz aufbauen Hund bricht ab',
'Hund wird zur Person gelobt statt an Ort Hund kommt herangelaufen',
'Freigabewort vergessen Hund weiß nicht wann er aufstehen darf',
],
steigerung: 'Bleib 1 Min Bleib mit Sichtkontaktverlust Bleib bei Ablenkung (Ball rollen, andere Person)',
},
{
name: 'Hier / Komm',
schwierigkeit: 'Anfänger',
alter: 'Ab 8 Wochen',
dauer: '5 Minuten',
material: 'Leckerlis oder Spielzeug, Schleppleine empfohlen',
beschreibung: 'Der Hund kommt zuverlässig zurück wenn er gerufen wird. Eines der wichtigsten Kommandos im Zweifel lebensrettend.',
schritte: [
'Beginne in der Wohnung auf kurze Distanz (23 Meter).',
'Knie dich hin, öffne die Arme, freudige Stimme: "Hier!" oder "Komm!"',
'Sobald der Hund ankommt: große Freude, Leckerli, Streicheln.',
'Niemals rufen und dann etwas Unangenehmes tun (Bad, Krallen schneiden).',
'Im Garten: Schleppleine verwenden Hund kann nicht wegbleiben, Erfolg ist garantiert.',
'Ruf nur einmal wer mehrfach ruft trainiert den Hund aufs Ignorieren.',
],
fehler: [
'Hund rufen wenn er sicher nicht kommt schlechte Gewohnheit',
'Hund bestrafen nach dem Kommen nächstes Mal kommt er nicht',
'Monotone Stimme Hund motiviert sich nicht',
],
steigerung: 'Kurze Distanz innen Garten mit Schleppleine Park mit wenig Ablenkung Freilauf',
},
{
name: 'Fuß',
schwierigkeit: 'Fortgeschrittener Anfänger',
alter: 'Ab 12 Wochen',
dauer: '510 Minuten',
material: 'Leckerlis, Leine',
beschreibung: 'Der Hund läuft ruhig an der Leine neben dir, ohne zu ziehen. Die Leine hängt locker durch.',
schritte: [
'Hund an deine linke Seite stellen (klassisch), Leckerli in der linken Hand.',
'Einen Schritt vorwärts gehen, Leckerli auf Höhe deiner linken Hüfte halten.',
'Hund folgt dem Leckerli Markerwort + belohnen.',
'Schrittweise mehr Schritte, immer wieder belohnen wenn Leine locker ist.',
'Zieht der Hund: stehen bleiben oder Richtung wechseln nie mitziehen lassen.',
'Wort "Fuß" einführen sobald der Hund die Position versteht.',
],
fehler: [
'Leine straff halten Hund lernt Zug als Normalzustand',
'Zu selten belohnen am Anfang Hund verliert Interesse an der Position',
'Zu lange Einheiten Überforderung',
],
steigerung: 'Fuß in der Wohnung ruhige Straße belebte Umgebung ohne Leckerli in der Hand',
},
{
name: 'Aus / Lass es',
schwierigkeit: 'Anfänger',
alter: 'Ab 10 Wochen',
dauer: '35 Minuten',
material: 'Leckerlis, Spielzeug oder Gegenstand',
beschreibung: 'Der Hund lässt einen Gegenstand auf Kommando los. Wichtig für Sicherheit (Gefährliches fallen lassen) und Spielsituationen.',
schritte: [
'Gib dem Hund ein Spielzeug oder lass ihn etwas halten.',
'Halte ein Leckerli vor seine Nase er lässt den Gegenstand fallen.',
'Sofort Markerwort + Leckerli geben.',
'Gegenstand kurz aufheben, dann wieder zurückgeben Hund lernt: Loslassen lohnt sich.',
'Wort "Aus" kurz vor dem Leckerli-Zeigen einführen.',
],
fehler: [
'Gegenstand wegziehen Hund lernt festhalten',
'Immer wegnehmen nach "Aus" Hund gibt nicht mehr freiwillig her',
],
steigerung: 'Spielzeug Leckerli auf dem Boden Gegenstand unterwegs hochwertige Beute',
},
{
name: 'Warte',
schwierigkeit: 'Anfänger',
alter: 'Ab 10 Wochen',
dauer: '35 Minuten',
material: 'Leckerlis, Türschwelle oder Futterschüssel',
beschreibung: 'Der Hund wartet kurz in einer Situation bis er freigegeben wird z.B. vor der Tür, vor dem Futter, beim Aussteigen aus dem Auto.',
schritte: [
'Stelle die Futterschüssel auf den Boden Hund stürmt drauf zu.',
'Schüssel mit der Hand abdecken oder hochhalten.',
'Sobald der Hund zurückweicht oder sitzt: Schüssel freigeben + "Okay".',
'Alternativ: an der Türschwelle Tür öffnen, Hund wartet bis "Okay".',
'Wort "Warte" einführen sobald das Verhalten klar ist.',
],
fehler: [
'Hund wird nie freigegeben verliert Vertrauen ins System',
'Zu lange warten lassen am Anfang Frustration',
],
steigerung: null,
},
];
const TRICKS = [
{
name: 'Pfote / Schütteln',
schwierigkeit: 'Anfänger',
alter: 'Ab 12 Wochen',
dauer: '3 Minuten',
material: 'Leckerlis',
beschreibung: null,
schritte: [
'Hund ins Sitz bringen.',
'Leckerli in der Faust verstecken, Faust auf Kniehöhe halten.',
'Hund schnuppert, kratzt irgendwann mit der Pfote an der Faust.',
'Sofort öffnen: Markerwort + Leckerli.',
'Wort "Pfote" einführen sobald die Bewegung zuverlässig kommt.',
'Auf flache offene Hand umstellen Hund legt Pfote rein.',
],
fehler: [],
steigerung: null,
},
{
name: 'Dreh / Runde',
schwierigkeit: 'Anfänger',
alter: 'Ab 12 Wochen',
dauer: '35 Minuten',
material: 'Leckerlis',
beschreibung: null,
schritte: [
'Leckerli vor die Nase des Hundes halten.',
'Langsam einen Kreis in der Luft führen der Hund folgt mit der Nase.',
'Volle Drehung Markerwort + Leckerli.',
'Wort "Dreh" (links) und "Runde" (rechts) einführen.',
'Handbewegung schrittweise kleiner machen.',
],
fehler: [],
steigerung: null,
},
{
name: 'Platz auf Decke / Matte',
schwierigkeit: 'Fortgeschrittener Anfänger',
alter: 'Ab 4 Monate',
dauer: '510 Minuten',
material: 'Leckerlis, Decke oder Matte',
beschreibung: 'Der Hund geht selbstständig auf seine Decke und legt sich. Ideal für Besuche, Restaurant, ruhige Phasen.',
schritte: [
'Decke auf den Boden legen. Hund beschnuppert sie Leckerli auf die Decke werfen.',
'Jedes Mal wenn der Hund die Decke betritt: Markerwort + Leckerli.',
'Warten bis der Hund sich spontan auf die Decke legt große Belohnung.',
'"Platz" zeigen sobald er auf der Decke steht.',
'Decke schrittweise weiter wegstellen.',
'Wort "Decke" oder "Platz geh" einführen.',
],
fehler: [],
steigerung: null,
},
{
name: 'Suchspiel / Nasenarbeit',
schwierigkeit: 'Anfänger',
alter: 'Ab 8 Wochen',
dauer: '510 Minuten',
material: 'Leckerlis, optional Döschen',
beschreibung: 'Nasenarbeit ist mentale Auslastung 10 Minuten Suchen ermüdet mehr als 1 Stunde Spaziergang.',
schritte: [
'Leckerli vor den Augen des Hundes unter einem Becher verstecken.',
'Hund darf suchen findet er es: Markerwort + Leckerli.',
'Steigerung: mehrere Becher, Hund muss den richtigen finden.',
'Später: Leckerlis im Gras verstecken "Such!"',
'Noch später: Spielzeug oder Schlüssel suchen lassen.',
],
fehler: [],
steigerung: null,
},
];
const PROBLEMVERHALTEN = [
{
name: 'Nicht springen / Begrüßung',
schwierigkeit: 'Anfänger',
alter: 'Ab 8 Wochen',
dauer: 'Bei jeder Begrüßung',
material: 'Konsequenz aller Haushaltsmitglieder',
beschreibung: 'Der Hund begrüßt Menschen mit allen vier Pfoten auf dem Boden.',
schritte: [
'Springt der Hund hoch: keine Reaktion (kein "Nein", kein Wegdrücken, kein Augenkontakt).',
'Sobald alle vier Pfoten unten sind: sofort Markerwort + Leckerli + Aufmerksamkeit.',
'Konsequenz ist entscheidend alle im Haushalt müssen gleich reagieren.',
'Alternative: Hund ins Sitz schicken bei Begrüßung dann begrüßen.',
],
fehler: [],
steigerung: null,
},
{
name: 'Alleine bleiben',
schwierigkeit: 'Mittel',
alter: 'Ab 10 Wochen',
dauer: 'Mehrmals täglich kurze Einheiten',
material: 'Geduld, Zeit, Kong oder Kauartikel',
beschreibung: 'Der Hund bleibt ruhig wenn er allein ist ohne Stress, Bellen oder Zerstören.',
schritte: [
'Hund beschäftigen (Kong mit Futter gefüllt, Kauartikel).',
'Zimmer verlassen für 10 Sekunden zurückkommen, ruhig begrüßen.',
'Zeit schrittweise erhöhen: 30 Sek 2 Min 5 Min 15 Min.',
'Nie dramatisch verabschieden oder begrüßen Kommen und Gehen normalisieren.',
'Hund nicht bestrafen wenn er etwas angestellt hat er versteht den Zusammenhang nicht mehr.',
],
fehler: [],
steigerung: null,
hinweis: 'Welpen sollten nie länger als 12 Stunden allein gelassen werden. Erwachsene Hunde maximal 46 Stunden.',
},
{
name: 'Leinenführigkeit Nicht ziehen',
schwierigkeit: 'Mittel',
alter: 'Ab 12 Wochen',
dauer: 'Jeder Spaziergang',
material: 'Leine, Leckerlis, Geduld',
beschreibung: null,
schritte: [
'Beginne den Spaziergang ruhig aufgeregte Starts fördern das Ziehen.',
'Zieht der Hund: stehen bleiben. Warten bis Leine locker ist.',
'Oder: Richtung wechseln sobald die Leine straff wird.',
'Locker Leine = Bewegung vorwärts + gelegentlich Leckerli.',
'Kein Ruck an der Leine Hund lernt dadurch nicht.',
],
fehler: [],
steigerung: null,
hinweis: 'Hilfsmittel bei hartnäckigem Ziehen: Brustgeschirr mit vorderer Befestigung (kein Würge- oder Stachelband).',
},
];
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
_render();
}
function refresh() {}
function onDogChange() {}
// ----------------------------------------------------------
// HAUPT-RENDER
// ----------------------------------------------------------
function _render() {
_container.innerHTML = `
<div id="ueb-wrap">
${_renderTabs()}
<div id="ueb-content"></div>
</div>
`;
_bindTabs();
_renderContent();
}
function _renderTabs() {
return `
<div class="by-tabs" style="padding:var(--space-4) var(--space-4) 0" id="ueb-tabs">
${TABS.map(t => `
<button class="by-tab${t.id === _activeTab ? ' active' : ''}"
data-tab="${t.id}">
${_esc(t.label)}
</button>
`).join('')}
</div>
`;
}
function _bindTabs() {
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(btn => {
btn.addEventListener('click', () => {
_activeTab = btn.dataset.tab;
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(b =>
b.classList.toggle('active', b.dataset.tab === _activeTab)
);
_renderContent();
});
});
}
function _renderContent() {
const el = _container.querySelector('#ueb-content');
if (!el) return;
switch (_activeTab) {
case 'grundkommandos': el.innerHTML = _renderUebungsList(GRUNDKOMMANDOS); break;
case 'tricks': el.innerHTML = _renderUebungsList(TRICKS); break;
case 'problemverhalten': el.innerHTML = _renderUebungsList(PROBLEMVERHALTEN); break;
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break;
case 'ki-trainer': el.innerHTML = _renderKiTrainer(); break;
}
_bindAccordions();
_bindStatusButtons();
if (_activeTab === 'ki-trainer') _bindKiTrainer();
}
// ----------------------------------------------------------
// ÜBUNGS-CARDS
// ----------------------------------------------------------
function _renderUebungsList(list) {
return `
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
${list.map((u, i) => _renderCard(u, i)).join('')}
</div>
`;
}
function _renderCard(u, i) {
const diff = DIFF_META[u.schwierigkeit] || { label: u.schwierigkeit, color: 'var(--c-text-secondary)' };
const uid = `ueb-acc-${_activeTab}-${i}`;
const currentId = _getStatus(_activeTab, u.name);
const sm = _statusMeta(currentId);
const hasBody = u.schritte.length > 0 || u.fehler.length > 0 || u.steigerung;
return `
<div class="card" style="padding:0;overflow:hidden">
<!-- Header -->
<div style="padding:var(--space-4) var(--space-4) var(--space-3)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-2)">
<span style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text)">
${_esc(u.name)}
</span>
<div style="display:flex;align-items:center;gap:var(--space-2)">
<!-- Status-Button -->
<button class="ueb-status-btn"
data-tab="${_esc(_activeTab)}"
data-name="${_esc(u.name)}"
title="${_esc(sm.label)}"
style="background:none;border:none;cursor:pointer;padding:2px;
display:flex;align-items:center;gap:4px;
font-size:var(--text-xs);color:${sm.color};
border-radius:var(--radius-sm)">
<svg class="ph-icon" style="width:18px;height:18px;flex-shrink:0" aria-hidden="true">
<use href="/icons/phosphor.svg#${sm.icon}"></use>
</svg>
${currentId ? `<span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap">${_esc(sm.label)}</span>` : ''}
</button>
<!-- Schwierigkeits-Badge -->
<span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
padding:2px var(--space-2);border-radius:var(--radius-sm);
background:${diff.color}22;color:${diff.color}">
${_esc(diff.label)}
</span>
</div>
</div>
<!-- Meta -->
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:${u.beschreibung ? 'var(--space-3)' : '0'}">
<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#clock"></use></svg>
${_esc(u.dauer)}
</span>
<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
${_esc(u.alter)}
</span>
<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#package"></use></svg>
${_esc(u.material)}
</span>
</div>
${u.beschreibung ? `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5;margin:0">
${_esc(u.beschreibung)}
</p>
` : ''}
${u.hinweis ? `
<div style="margin-top:var(--space-2);padding:var(--space-2) var(--space-3);
background:var(--c-warning-bg,#fef3c7);border-radius:var(--radius-sm);
font-size:var(--text-xs);color:var(--c-text);line-height:1.4">
<svg class="ph-icon" style="width:12px;height:12px;color:#d97706" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
${_esc(u.hinweis)}
</div>
` : ''}
</div>
${hasBody ? `
<!-- Akkordeon Toggle -->
<button class="ueb-acc-btn"
data-acc="${uid}"
style="width:100%;display:flex;align-items:center;justify-content:space-between;
padding:var(--space-2) var(--space-4);
background:var(--c-surface-2);border:none;border-top:1px solid var(--c-border);
cursor:pointer;font-size:var(--text-sm);color:var(--c-text-secondary)">
<span>Anleitung anzeigen</span>
<svg class="ph-icon ueb-chevron" data-acc="${uid}" aria-hidden="true" style="width:16px;height:16px;transition:transform 0.2s">
<use href="/icons/phosphor.svg#caret-down"></use>
</svg>
</button>
<div id="${uid}" hidden style="padding:var(--space-4);border-top:1px solid var(--c-border)">
${u.schritte.length ? `
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
Schritt für Schritt
</p>
<ol style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
${u.schritte.map(s => `
<li style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${_esc(s)}</li>
`).join('')}
</ol>
` : ''}
${u.fehler.length ? `
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
<svg class="ph-icon" style="width:12px;height:12px;color:#f59e0b" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
Häufige Fehler
</p>
<ul style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
${u.fehler.map(f => `
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${_esc(f)}</li>
`).join('')}
</ul>
` : ''}
${u.steigerung ? `
<div style="padding:var(--space-3);background:var(--c-surface-2);border-radius:var(--radius-md);
font-size:var(--text-sm);color:var(--c-text);line-height:1.5">
<svg class="ph-icon" style="width:14px;height:14px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#arrow-right"></use>
</svg>
<strong>Steigerung:</strong> ${_esc(u.steigerung)}
</div>
` : ''}
</div>
` : ''}
</div>
`;
}
function _bindStatusButtons() {
_container.querySelectorAll('.ueb-status-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const tab = btn.dataset.tab;
const name = btn.dataset.name;
const cur = _getStatus(tab, name);
const next = _nextStatus(cur);
_setStatus(tab, name, next);
// Update button in place (no full re-render)
const sm = _statusMeta(next);
btn.title = sm.label;
btn.style.color = sm.color;
btn.innerHTML = `
<svg class="ph-icon" style="width:18px;height:18px;flex-shrink:0" aria-hidden="true">
<use href="/icons/phosphor.svg#${sm.icon}"></use>
</svg>
${next ? `<span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap">${_esc(sm.label)}</span>` : ''}
`;
});
});
}
// ----------------------------------------------------------
// KI-TRAINER
// ----------------------------------------------------------
function _renderKiTrainer() {
return `
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
<!-- Intro -->
<div class="card" style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light)">
<div style="display:flex;gap:var(--space-3);align-items:flex-start">
<svg class="ph-icon" style="width:24px;height:24px;flex-shrink:0;color:var(--c-primary);margin-top:2px" aria-hidden="true">
<use href="/icons/phosphor.svg#robot"></use>
</svg>
<div>
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);margin:0 0 var(--space-1)">
KI-Hundetrainer
</h3>
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0;line-height:1.5">
Beschreibe ein konkretes Problem oder Verhalten deines Hundes —
du bekommst individuelle Trainingstipps.
</p>
</div>
</div>
</div>
<!-- Eingabe -->
<div class="card" style="padding:var(--space-4)">
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">
Rasse &amp; Alter (optional)
</label>
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4)">
<input id="ki-rasse" type="text" placeholder="z.B. Labrador"
style="flex:1;padding:var(--space-2) var(--space-3);border:1.5px solid var(--c-border);
border-radius:var(--radius-md);font-size:var(--text-sm);
background:var(--c-surface);color:var(--c-text);font-family:inherit">
<input id="ki-alter" type="text" placeholder="z.B. 2 Jahre"
style="flex:1;padding:var(--space-2) var(--space-3);border:1.5px solid var(--c-border);
border-radius:var(--radius-md);font-size:var(--text-sm);
background:var(--c-surface);color:var(--c-text);font-family:inherit">
</div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">
Problem beschreiben *
</label>
<textarea id="ki-problem" rows="4"
placeholder="z.B. Mein Hund bellt bei jedem Klingeln an der Tür und lässt sich kaum beruhigen. Er springt Besucher an und ist sehr aufgedreht..."
style="width:100%;box-sizing:border-box;padding:var(--space-3);
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
font-size:var(--text-sm);font-family:inherit;resize:vertical;
background:var(--c-surface);color:var(--c-text);line-height:1.5;
min-height:100px"></textarea>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:var(--space-3)">
<span id="ki-char-count" style="font-size:var(--text-xs);color:var(--c-text-muted)">0 / 1000</span>
<button id="ki-submit" class="btn btn-primary">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg>
Tipps holen
</button>
</div>
</div>
<!-- Antwort -->
<div id="ki-result" hidden></div>
<!-- Hinweis -->
<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin:0">
KI-Tipps ersetzen keinen professionellen Hundetrainer. Bei Aggression oder starker Angst
wende dich an einen zertifizierten Trainer vor Ort.
</p>
</div>
`;
}
function _bindKiTrainer() {
const textarea = _container.querySelector('#ki-problem');
const charCount = _container.querySelector('#ki-char-count');
const submitBtn = _container.querySelector('#ki-submit');
const result = _container.querySelector('#ki-result');
if (!textarea || !submitBtn) return;
textarea.addEventListener('input', () => {
charCount.textContent = `${textarea.value.length} / 1000`;
});
submitBtn.addEventListener('click', async () => {
const problem = textarea.value.trim();
if (problem.length < 10) {
UI.toast('Bitte beschreibe das Problem etwas genauer.', 'warning');
return;
}
const rasse = _container.querySelector('#ki-rasse')?.value.trim() || null;
const alter = _container.querySelector('#ki-alter')?.value.trim() || null;
submitBtn.disabled = true;
submitBtn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#spinner"></use></svg> Denke nach…`;
result.hidden = true;
result.innerHTML = '';
try {
const resp = await API.post('/ki/training', { problem, rasse, alter });
const text = resp.antwort || '';
// Render with simple markdown-like formatting (text already escaped by API)
const safeText = text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const html = safeText
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/^(\d+)\. (.+)$/gm, '<li><strong>$1.</strong> $2</li>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>');
result.innerHTML = `
<div class="card" style="border-left:3px solid var(--c-primary)">
<div style="display:flex;gap:var(--space-2);align-items:center;margin-bottom:var(--space-3)">
<svg class="ph-icon" style="color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#robot"></use>
</svg>
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text)">
Empfehlung des KI-Trainers
</span>
</div>
<div style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7">
<p>${html}</p>
</div>
</div>
`;
result.hidden = false;
} catch (err) {
UI.toast(err.message || 'KI momentan nicht verfügbar.', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg> Tipps holen`;
}
});
}
function _bindAccordions() {
_container.querySelectorAll('.ueb-acc-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.acc;
const body = document.getElementById(id);
const chevron = _container.querySelector(`.ueb-chevron[data-acc="${id}"]`);
if (!body) return;
const isOpen = !body.hidden;
body.hidden = isOpen;
if (chevron) chevron.style.transform = isOpen ? '' : 'rotate(180deg)';
btn.querySelector('span').textContent = isOpen ? 'Anleitung anzeigen' : 'Anleitung ausblenden';
});
});
}
// ----------------------------------------------------------
// TRAININGSGRUNDLAGEN
// ----------------------------------------------------------
function _renderGrundlagen() {
return `
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
<!-- Markerwort -->
<div class="card">
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
color:var(--c-text);margin:0 0 var(--space-3)">
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#megaphone-simple"></use>
</svg>
Das Markerwort
</h3>
<p style="font-size:var(--text-sm);color:var(--c-text);line-height:1.6;margin:0 0 var(--space-3)">
Ein Markerwort (z.B. <strong>"Ja!"</strong> oder ein Klicker) signalisiert dem Hund den <strong>exakten Moment</strong>
des richtigen Verhaltens. Es überbrückt die Zeit bis das Leckerli in seinem Mund ist.
</p>
<ul style="margin:0;padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">
Einmalig einführen: Markerwort sagen → sofort Leckerli (10x wiederholen)
</li>
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">
Immer nur ein Markerwort verwenden
</li>
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">
Das Leckerli kommt <strong>immer</strong> nach dem Markerwort — sonst verliert es seinen Wert
</li>
</ul>
</div>
<!-- Wann belohnen -->
<div class="card">
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
color:var(--c-text);margin:0 0 var(--space-3)">
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#star"></use>
</svg>
Wann belohnen?
</h3>
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-size:var(--text-sm)">
<thead>
<tr style="background:var(--c-surface-2)">
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold);
color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">Phase</th>
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold);
color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">Belohnungshäufigkeit</th>
</tr>
</thead>
<tbody>
${[
['Neue Übung lernen', 'Jede korrekte Wiederholung'],
['Übung bekannt', 'Jede 2.3. Wiederholung'],
['Übung gefestigt', 'Unregelmäßig (stärkt Motivation)'],
['Ablenkungstraining', 'Wieder häufiger (höhere Anforderung)'],
].map(([p, b], i) => `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text);border-bottom:1px solid var(--c-border)">${_esc(p)}</td>
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">${_esc(b)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
<!-- Leckerli-Hierarchie -->
<div class="card">
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
color:var(--c-text);margin:0 0 var(--space-3)">
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#trophy"></use>
</svg>
Leckerli-Hierarchie
</h3>
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5;margin:0 0 var(--space-3)">
Nicht alle Leckerlis sind gleich. Bei Ablenkung oder schwierigen Übungen brauchst du hochwertigere Belohnungen:
</p>
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-size:var(--text-sm)">
<thead>
<tr style="background:var(--c-surface-2)">
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold);
color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">Stufe</th>
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold);
color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">Beispiele</th>
</tr>
</thead>
<tbody>
${[
['Niedrig', 'Trockenfutter, normale Hundekekse', '#22c55e'],
['Mittel', 'Käse, Wurst, Hühnchen (gekocht)', '#eab308'],
['Hoch', 'Leberwurst, Lachs, Pansen (für besondere Momente)', '#ef4444'],
].map(([s, b, c], i) => `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">
<span style="font-weight:var(--weight-semibold);color:${c}">${_esc(s)}</span>
</td>
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">${_esc(b)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
<!-- Trainingsregeln -->
<div class="card" style="margin-bottom:var(--space-4)">
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
color:var(--c-text);margin:0 0 var(--space-3)">
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#list-checks"></use>
</svg>
Trainingsregeln auf einen Blick
</h3>
<ul style="margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:var(--space-2)">
${[
[true, 'Kurze Einheiten (510 Min), lieber mehrmals täglich'],
[true, 'Immer mit Erfolg beenden'],
[true, 'Ein Kommando — eine Bedeutung'],
[true, 'Konsequenz bei allen Haushaltsmitgliedern'],
[false, 'Nie bestrafen, schreien oder Zwang anwenden'],
[false, 'Kommando nicht wiederholen wenn der Hund nicht reagiert'],
[false, 'Nicht trainieren wenn Hund müde, krank oder aufgewühlt ist'],
].map(([ok, text]) => `
<li style="display:flex;gap:var(--space-2);align-items:flex-start">
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:2px;
color:${ok ? 'var(--c-success)' : 'var(--c-danger)'}" aria-hidden="true">
<use href="/icons/phosphor.svg#${ok ? 'check-circle' : 'x-circle'}"></use>
</svg>
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${_esc(text)}</span>
</li>
`).join('')}
</ul>
</div>
</div>
`;
}
// ----------------------------------------------------------
// HELPER
// ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ----------------------------------------------------------
// PUBLIC
// ----------------------------------------------------------
return { init, refresh, onDogChange };
})();