Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations
Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil
Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware
Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
1971 lines
92 KiB
JavaScript
1971 lines
92 KiB
JavaScript
/* ============================================================
|
||
BAN YARO — Übungsbibliothek
|
||
Seiten-Modul: Grundkommandos, Tricks, Problemverhalten, Grundlagen.
|
||
============================================================ */
|
||
|
||
window.Page_uebungen = (() => {
|
||
|
||
let _container = null;
|
||
let _appState = null;
|
||
let _activeTab = 'grundkommandos';
|
||
|
||
// ----------------------------------------------------------
|
||
// API HELPERS
|
||
// ----------------------------------------------------------
|
||
function _dogId() {
|
||
return _appState?.activeDog?.id || null;
|
||
}
|
||
|
||
async function _apiPost(url, body) {
|
||
const r = await fetch(url, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(body)
|
||
});
|
||
return r.ok ? r.json() : null;
|
||
}
|
||
|
||
async function _apiGet(url) {
|
||
const r = await fetch(url, {credentials: 'include'});
|
||
return r.ok ? r.json() : null;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// STATS STATE
|
||
// ----------------------------------------------------------
|
||
let _statsData = null; // cached stats from /api/training/stats
|
||
let _badgesData = null; // cached badges from /api/achievements
|
||
|
||
// ----------------------------------------------------------
|
||
// 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, '_')}`;
|
||
}
|
||
|
||
// In-memory cache (loaded from API on init)
|
||
let _progressCache = {}; // key → statusId
|
||
let _exerciseStats = {}; // exercise_id → {recent_avg, session_count, trend}
|
||
|
||
function _progressKey(tab, name) {
|
||
return `${tab}_${name.replace(/[\s/]+/g, '_')}`;
|
||
}
|
||
|
||
function _getStatus(tab, name) {
|
||
const k = _progressKey(tab, name);
|
||
// Fallback to localStorage while API loads
|
||
return _progressCache[k] !== undefined
|
||
? _progressCache[k]
|
||
: localStorage.getItem(_statusKey(tab, name)) || null;
|
||
}
|
||
|
||
function _setStatus(tab, name, statusId) {
|
||
const k = _progressKey(tab, name);
|
||
_progressCache[k] = statusId;
|
||
localStorage.setItem(_statusKey(tab, name), statusId || ''); // keep localStorage in sync
|
||
API.training.setProgress(k, statusId).catch(() => {});
|
||
}
|
||
|
||
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änger–Fortgeschr.', color: '#eab308' },
|
||
};
|
||
|
||
const GRUNDKOMMANDOS = [
|
||
{
|
||
name: 'Sitz',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 8 Wochen',
|
||
dauer: '3–5 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 5–10x, 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: '3–5 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 10–15 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 (2–3 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: '5–10 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: '3–5 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: '3–5 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: '3–5 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: '5–10 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: '5–10 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 1–2 Stunden allein gelassen werden. Erwachsene Hunde maximal 4–6 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).',
|
||
},
|
||
{
|
||
name: 'Bellen / Kläffen',
|
||
schwierigkeit: 'Mittel',
|
||
alter: 'Ab 8 Wochen',
|
||
dauer: '5–10 Min täglich, konsequent',
|
||
material: 'Leckerlis, Geduld, Konsequenz',
|
||
beschreibung: 'Der Hund bellt nicht übermäßig auf Reize wie Klingel, Passanten oder andere Hunde — und beruhigt sich auf ein Signal hin.',
|
||
schritte: [
|
||
'Ursache kennen: Alarm-Bellen (Schutzinstinkt), Frust-Bellen (Leine, Zaun), Aufmerksamkeits-Bellen oder Angst-Bellen haben unterschiedliche Lösungswege.',
|
||
'Aufmerksamkeits-Bellen nie belohnen — auch nicht durch Zuwendung, Schimpfen oder Anfassen.',
|
||
'Signal „Ruhig" einführen: warten bis der Hund kurz pausiert, sofort markieren + belohnen.',
|
||
'Klingel / Türgeräusche: Reiz kontrolliert einüben (selbst klingeln), Hund ins Körbchen schicken und dort belohnen.',
|
||
'Bellt der Hund draußen auf Reize: Blickkontakt unterbrechen, Hund wegführen, Ablenkung mit Leckerli erst wenn er ruhig ist.',
|
||
'Ruhiges Verhalten konsequent belohnen — Hund lernt: Stille bringt Aufmerksamkeit, Bellen bringt nichts.',
|
||
],
|
||
fehler: [
|
||
'Schreien oder „Aus!"-Rufen während des Bellens — der Hund interpretiert es als Mitmachen.',
|
||
'Leckerli geben während der Hund noch bellt — das belohnt das Bellen.',
|
||
'Strafe: führt zu Angst, nicht zu weniger Bellen.',
|
||
],
|
||
steigerung: 'Reiz gezielt aufbauen (z.B. Klingel-Ton am Handy), erst in ruhiger Umgebung üben, dann mit echten Auslösern.',
|
||
hinweis: 'Anhaltend starkes Bellen kann auf Angst oder unerfüllte Bedürfnisse (Bewegung, Beschäftigung) hinweisen. Im Zweifel Verhaltensberater hinzuziehen.',
|
||
},
|
||
{
|
||
name: 'Enttriggern / Desensibilisierung',
|
||
schwierigkeit: 'Fortgeschrittener Anfänger',
|
||
alter: 'Ab 12 Wochen',
|
||
dauer: '5–15 Min, mehrmals wöchentlich',
|
||
material: 'Hochwertige Leckerlis, Geduld, Distanz',
|
||
beschreibung: 'Der Hund bleibt unter einem Reiz (Hund, Mensch, Geräusch, Fahrrad…) entspannt — kein Bellen, kein Zerren, keine Überreaktion.',
|
||
schritte: [
|
||
'Trigger identifizieren: Was genau löst die Reaktion aus? Ab welcher Distanz beginnt sie?',
|
||
'Schwellenabstand ermitteln: Die Entfernung, bei der der Hund den Reiz wahrnimmt aber noch nicht überreagiert — dort arbeiten.',
|
||
'Reiz zeigen → sofort hochwertiges Leckerli geben, bevor der Hund reagiert (Gegenkonditionierung: Reiz = super Sache).',
|
||
'Distanz ganz langsam verringern, nur wenn der Hund auf der aktuellen Stufe entspannt bleibt.',
|
||
'Reagiert der Hund doch: ruhig mehr Abstand schaffen, keine Strafe — er war schlicht zu nah.',
|
||
'Ziel: Hund schaut Trigger an und dreht sich dann entspannt zu dir um (Blickwechsel als Zeichen von Entspannung).',
|
||
],
|
||
fehler: [
|
||
'Zu schnell zu nah — der Hund gerät über die Reizschwelle und übt die Überreaktion ein.',
|
||
'Hund zwingen den Trigger anzuschauen oder ihm entgegenzugehen.',
|
||
'Training abbrechen wenn Hund bereits reagiert — lieber Abstand nehmen und neu ansetzen.',
|
||
],
|
||
steigerung: 'Erst mit einem einzelnen, vorhersehbaren Reiz üben. Dann wechselnde Reize, engere Distanz, schließlich Bewegung des Triggers.',
|
||
hinweis: 'Bei starker Reaktivität oder Aggression unbedingt mit einem zertifizierten Verhaltenstherapeuten (IAABC, BHV) zusammenarbeiten.',
|
||
},
|
||
];
|
||
|
||
// ----------------------------------------------------------
|
||
// INIT
|
||
// ----------------------------------------------------------
|
||
async function init(container, appState) {
|
||
_container = container;
|
||
_appState = appState;
|
||
_render();
|
||
|
||
// Progress vom Server laden
|
||
API.training.getProgress().then(rows => {
|
||
rows.forEach(r => { _progressCache[r.exercise_id] = r.status; });
|
||
// localStorage-Daten migrieren falls noch nicht im Backend
|
||
Object.keys(localStorage).filter(k => k.startsWith('ub_status_')).forEach(lsKey => {
|
||
const parts = lsKey.replace('ub_status_', '').split('_');
|
||
const tab = parts[0];
|
||
const name = parts.slice(1).join('_');
|
||
const apiKey = `${tab}_${name}`;
|
||
if (_progressCache[apiKey] === undefined) {
|
||
const val = localStorage.getItem(lsKey);
|
||
if (val) {
|
||
_progressCache[apiKey] = val;
|
||
API.training.setProgress(apiKey, val).catch(() => {});
|
||
}
|
||
}
|
||
});
|
||
_renderContent(); // Re-render with loaded progress
|
||
}).catch(() => {});
|
||
|
||
// Empfehlungen laden
|
||
API.training.getSuggestions().then(suggestions => {
|
||
if (suggestions.length) _showSuggestions(suggestions);
|
||
}).catch(() => {});
|
||
|
||
// Stats + Badges laden
|
||
_loadStatsAndBadges();
|
||
|
||
// Virtueller Trainer laden
|
||
_loadVirtualTrainer();
|
||
}
|
||
|
||
async function _loadStatsAndBadges() {
|
||
const dogId = _dogId();
|
||
if (!dogId) return;
|
||
const [stats, achievements] = await Promise.all([
|
||
_apiGet(`/api/training/stats?dog_id=${dogId}`),
|
||
_apiGet('/api/achievements'),
|
||
]);
|
||
_statsData = stats;
|
||
_badgesData = achievements;
|
||
_renderStatsBanner();
|
||
}
|
||
|
||
function refresh() {}
|
||
function onDogChange() {
|
||
_statsData = null;
|
||
_badgesData = null;
|
||
_loadStatsAndBadges();
|
||
_loadVirtualTrainer();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// HAUPT-RENDER
|
||
// ----------------------------------------------------------
|
||
function _render() {
|
||
_container.innerHTML = `
|
||
<div id="ueb-wrap">
|
||
${_renderTabs()}
|
||
<div style="padding:var(--space-3) var(--space-4) 0;display:flex;justify-content:flex-end">
|
||
<button id="ueb-quicksetup-btn"
|
||
style="font-size:var(--text-xs);padding:4px 10px;
|
||
background:var(--c-surface-2);border:1px solid var(--c-border);
|
||
border-radius:var(--radius-sm);cursor:pointer;color:var(--c-text-secondary);
|
||
display:flex;align-items:center;gap:4px">
|
||
<svg class="ph-icon" style="width:13px;height:13px" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#list-checks"></use>
|
||
</svg>
|
||
Stand erfassen
|
||
</button>
|
||
</div>
|
||
<div id="ueb-stats-banner" style="padding:var(--space-2) var(--space-4) 0"></div>
|
||
<div id="ueb-trainer" style="padding:0 var(--space-4);margin-bottom:var(--space-2)"></div>
|
||
<div id="ueb-suggestions" style="padding:0 var(--space-4);display:flex;flex-direction:column;gap:var(--space-2);margin-bottom:var(--space-2)"></div>
|
||
<div id="ueb-content"></div>
|
||
</div>
|
||
`;
|
||
_container.querySelector('#ueb-quicksetup-btn').addEventListener('click', _openQuickSetupModal);
|
||
_bindTabs();
|
||
_renderContent();
|
||
_renderStatsBanner();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// STATS BANNER
|
||
// ----------------------------------------------------------
|
||
function _renderStatsBanner() {
|
||
const el = _container && _container.querySelector('#ueb-stats-banner');
|
||
if (!el) return;
|
||
if (!_statsData || !_statsData.total_sessions) { el.innerHTML = ''; return; }
|
||
|
||
const s = _statsData;
|
||
const streakHtml = (s.streak_days >= 2)
|
||
? ` · ${s.streak_days}-Tage-Streak 🔥`
|
||
: '';
|
||
const avgHtml = s.avg_erfolgsquote != null
|
||
? ` · Ø ${Math.round(s.avg_erfolgsquote)}% Erfolg`
|
||
: '';
|
||
|
||
// Training-Badges filtern
|
||
let badgesHtml = '';
|
||
if (_badgesData && Array.isArray(_badgesData.user_badges)) {
|
||
const trainingBadges = _badgesData.user_badges.filter(b => b.badge_id && b.badge_id.startsWith('training_'));
|
||
if (trainingBadges.length) {
|
||
const visible = trainingBadges.slice(0, 3);
|
||
const rest = trainingBadges.length - visible.length;
|
||
const pills = visible.map(b => `
|
||
<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;
|
||
border-radius:var(--radius-full,999px);
|
||
background:var(--c-primary-subtle);color:var(--c-primary);
|
||
font-size:var(--text-xs);font-weight:var(--weight-semibold)">
|
||
${b.icon || '🏅'} ${_esc(b.name || b.badge_id)}
|
||
</span>
|
||
`).join('');
|
||
const more = rest > 0
|
||
? `<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">+${rest} weitere</span>`
|
||
: '';
|
||
badgesHtml = `<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-2)">${pills}${more}</div>`;
|
||
}
|
||
}
|
||
|
||
el.innerHTML = `
|
||
<div style="background:var(--c-surface-2);border:1px solid var(--c-border);
|
||
border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);
|
||
font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
<span style="font-weight:var(--weight-semibold);color:var(--c-text)">
|
||
${s.total_sessions} Einheit${s.total_sessions !== 1 ? 'en' : ''}
|
||
</span>${avgHtml}${streakHtml}
|
||
${badgesHtml}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// VIRTUELLER TRAINER
|
||
// ----------------------------------------------------------
|
||
async function _loadVirtualTrainer() {
|
||
const dogId = _dogId();
|
||
const el = _container?.querySelector('#ueb-trainer');
|
||
if (!el) return;
|
||
if (!dogId) { el.innerHTML = ''; return; }
|
||
|
||
el.innerHTML = `
|
||
<div style="background:var(--c-surface-2);border:1px solid var(--c-border);
|
||
border-radius:var(--radius-md);padding:var(--space-3) var(--space-4)">
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">Lade Trainingsplan…</div>
|
||
</div>`;
|
||
|
||
const data = await API.training.getRecommendations(dogId).catch(() => null);
|
||
if (!data) { el.innerHTML = ''; return; }
|
||
|
||
if (data.all_stats) {
|
||
_exerciseStats = data.all_stats;
|
||
_renderContent();
|
||
}
|
||
|
||
if (!data.recommendations?.length) { el.innerHTML = ''; return; }
|
||
_renderVirtualTrainer(el, data);
|
||
}
|
||
|
||
function _renderVirtualTrainer(el, data) {
|
||
const recs = data.recommendations;
|
||
const TYPE_CFG = {
|
||
'üben': { label: 'Üben', bg: 'rgba(234,88,12,0.10)', border: 'rgba(234,88,12,0.30)', text: '#fb923c', icon: 'fire' },
|
||
'festigen': { label: 'Festigen', bg: 'rgba(22,163,74,0.10)', border: 'rgba(22,163,74,0.30)', text: '#4ade80', icon: 'star' },
|
||
'entdecken': { label: 'Auffrischen',bg: 'var(--c-primary-subtle)', border: 'var(--c-primary-light)', text: 'var(--c-primary)', icon: 'clock' },
|
||
'levelup': { label: 'Nächster Level', bg: 'rgba(234,179,8,0.10)', border: 'rgba(234,179,8,0.30)', text: '#facc15', icon: 'graduation-cap' },
|
||
};
|
||
const TREND_ICON = { improving: '↑', declining: '↓', stable: '→', new: '★' };
|
||
const TREND_COLOR = { improving: 'var(--c-success,#16a34a)', declining: '#dc2626', stable: 'var(--c-text-secondary)', new: 'var(--c-primary)' };
|
||
|
||
const cards = recs.map((r, i) => {
|
||
const cfg = TYPE_CFG[r.type] || TYPE_CFG['üben'];
|
||
const trend = TREND_ICON[r.trend] || '';
|
||
const trendColor = TREND_COLOR[r.trend] || 'var(--c-text-secondary)';
|
||
const prognose = r.prognose_sessions
|
||
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
|
||
Prognose: ~${r.prognose_sessions} Einheiten bis 80%
|
||
</div>`
|
||
: '';
|
||
return `
|
||
<div style="background:${cfg.bg};border:1px solid ${cfg.border};
|
||
border-radius:var(--radius-md);padding:var(--space-3);
|
||
display:flex;flex-direction:column;gap:var(--space-2);flex:1;min-width:0">
|
||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||
<svg class="ph-icon" style="width:14px;height:14px;flex-shrink:0;color:${cfg.text}" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#${cfg.icon}"></use>
|
||
</svg>
|
||
<span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:${cfg.text};
|
||
text-transform:uppercase;letter-spacing:.04em">${cfg.label}</span>
|
||
</div>
|
||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);line-height:1.3">${_esc(r.exercise_name)}</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.4">${_esc(r.reason)}</div>
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2);margin-top:auto;padding-top:var(--space-1)">
|
||
<div>
|
||
<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">${r.suggested_reps}× empfohlen</span>
|
||
<span style="font-size:var(--text-xs);font-weight:700;color:${trendColor};margin-left:4px">${trend}</span>
|
||
${prognose}
|
||
</div>
|
||
<button class="ueb-trainer-btn btn btn-primary"
|
||
data-tab="${_esc(r.tab)}" data-name="${_esc(r.exercise_name)}" data-reps="${r.suggested_reps}"
|
||
style="font-size:var(--text-xs);padding:4px 10px;width:100%">
|
||
Üben
|
||
</button>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
|
||
el.innerHTML = `
|
||
<div style="margin-bottom:var(--space-2);display:flex;align-items:center;gap:var(--space-2)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#target"></use>
|
||
</svg>
|
||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text)">
|
||
Dein Plan für heute
|
||
</span>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||
${cards.join('')}
|
||
</div>`;
|
||
|
||
el.querySelectorAll('.ueb-trainer-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const tab = btn.dataset.tab;
|
||
const name = btn.dataset.name;
|
||
const reps = parseInt(btn.dataset.reps, 10) || 5;
|
||
// Tab wechseln falls nötig
|
||
if (tab && tab !== _activeTab) {
|
||
_activeTab = tab;
|
||
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(b =>
|
||
b.classList.toggle('active', b.dataset.tab === tab)
|
||
);
|
||
_renderContent();
|
||
}
|
||
_openLogModal(tab, name, reps);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// SCHNELL-SETUP: Stand aller Übungen erfassen
|
||
// ----------------------------------------------------------
|
||
function _openQuickSetupModal() {
|
||
const ALL = [
|
||
{ group: 'Grundkommandos', tab: 'grundkommandos', items: GRUNDKOMMANDOS },
|
||
{ group: 'Tricks', tab: 'tricks', items: TRICKS },
|
||
{ group: 'Problemverhalten', tab: 'problemverhalten', items: PROBLEMVERHALTEN },
|
||
];
|
||
|
||
const STATUS_QUICK = [
|
||
{ id: null, label: '—', color: 'var(--c-text-secondary)', bg: 'var(--c-surface-2)' },
|
||
{ id: 'noch-nicht',label: 'Nein', color: '#dc2626', bg: 'rgba(220,38,38,0.10)' },
|
||
{ id: 'manchmal', label: 'Manchmal', color: '#c2410c', bg: 'rgba(234,88,12,0.10)' },
|
||
{ id: 'meistens', label: 'Meistens', color: '#ca8a04', bg: 'rgba(234,179,8,0.10)' },
|
||
{ id: 'sitzt', label: 'Sitzt ✓', color: '#15803d', bg: 'rgba(22,163,74,0.10)' },
|
||
];
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;display:flex;align-items:flex-end;justify-content:center;background:rgba(0,0,0,0.5)';
|
||
|
||
const rows = ALL.flatMap(g => g.items.map(u => ({ tab: g.tab, name: u.name, group: g.group })));
|
||
let pending = {}; // key → statusId (nur geänderte)
|
||
|
||
overlay.innerHTML = `
|
||
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||
width:100%;max-width:560px;max-height:90vh;display:flex;flex-direction:column;
|
||
box-shadow:0 -4px 24px rgba(0,0,0,0.2)">
|
||
<div style="padding:var(--space-4) var(--space-4) var(--space-2);border-bottom:1px solid var(--c-border);flex-shrink:0">
|
||
<div style="width:36px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-3)"></div>
|
||
<div style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text)">Stand erfassen</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
|
||
Tippe auf den aktuellen Stand deines Hundes — wird sofort im Profil sichtbar.
|
||
</div>
|
||
</div>
|
||
<div style="overflow-y:auto;flex:1;padding:var(--space-2) var(--space-4)">
|
||
${ALL.map(g => `
|
||
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:var(--c-text-secondary);
|
||
text-transform:uppercase;letter-spacing:.05em;padding:var(--space-3) 0 var(--space-1)">
|
||
${_esc(g.group)}
|
||
</div>
|
||
${g.items.map(u => {
|
||
const key = _progressKey(g.tab, u.name);
|
||
const cur = _getStatus(g.tab, u.name);
|
||
return `
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);
|
||
padding:var(--space-2) 0;border-bottom:1px solid var(--c-border)" data-key="${_esc(key)}">
|
||
<span style="flex:1;font-size:var(--text-sm);color:var(--c-text);min-width:0;
|
||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(u.name)}</span>
|
||
<div style="display:flex;gap:3px;flex-shrink:0">
|
||
${STATUS_QUICK.map(s => `
|
||
<button class="qs-btn" data-key="${_esc(key)}" data-val="${s.id === null ? '' : _esc(s.id)}"
|
||
style="font-size:9px;font-weight:700;padding:3px 6px;border-radius:999px;cursor:pointer;
|
||
white-space:nowrap;transition:all .15s;
|
||
background:${cur === s.id ? s.bg : 'var(--c-surface-2)'};
|
||
color:${cur === s.id ? s.color : 'var(--c-text-secondary)'};
|
||
border:1px solid ${cur === s.id ? s.color + '66' : 'var(--c-border)'}">
|
||
${_esc(s.label)}
|
||
</button>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
}).join('')}
|
||
`).join('')}
|
||
</div>
|
||
<div style="padding:var(--space-4);border-top:1px solid var(--c-border);display:flex;gap:var(--space-3);flex-shrink:0">
|
||
<button id="qs-cancel" style="flex:1;padding:var(--space-3);border:1.5px solid var(--c-border);
|
||
background:var(--c-surface-2);border-radius:var(--radius-md);
|
||
font-size:var(--text-sm);cursor:pointer;color:var(--c-text)">Abbrechen</button>
|
||
<button id="qs-save" class="btn btn-primary" style="flex:2">Übernehmen</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
document.body.appendChild(overlay);
|
||
|
||
// Button-Interaktion
|
||
overlay.addEventListener('click', e => {
|
||
const btn = e.target.closest('.qs-btn');
|
||
if (!btn) return;
|
||
const key = btn.dataset.key;
|
||
const val = btn.dataset.val || null;
|
||
pending[key] = val;
|
||
|
||
// Aktiven Button in dieser Zeile highlighten
|
||
const row = overlay.querySelector(`[data-key="${CSS.escape(key)}"]`);
|
||
row?.querySelectorAll('.qs-btn').forEach(b => {
|
||
const s = STATUS_QUICK.find(s => (s.id ?? '') === (b.dataset.val));
|
||
const active = b.dataset.val === (val ?? '');
|
||
b.style.background = active ? s.bg : 'var(--c-surface-2)';
|
||
b.style.color = active ? s.color : 'var(--c-text-secondary)';
|
||
b.style.border = `1px solid ${active ? s.color + '66' : 'var(--c-border)'}`;
|
||
});
|
||
});
|
||
|
||
overlay.querySelector('#qs-cancel').addEventListener('click', () => overlay.remove());
|
||
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
|
||
|
||
overlay.querySelector('#qs-save').addEventListener('click', async () => {
|
||
if (!Object.keys(pending).length) { overlay.remove(); return; }
|
||
const saveBtn = overlay.querySelector('#qs-save');
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = 'Speichern…';
|
||
|
||
// Alle geänderten Status speichern
|
||
const parts = Object.entries(pending).map(([key, val]) => {
|
||
const [tab, ...rest] = key.split('_');
|
||
const name = rest.join('_').replace(/_/g, ' ');
|
||
_progressCache[key] = val || null;
|
||
localStorage.setItem(`ub_status_${key}`, val || '');
|
||
return API.training.setProgress(key, val || null);
|
||
});
|
||
await Promise.allSettled(parts);
|
||
|
||
overlay.remove();
|
||
_renderContent();
|
||
UI.toast.success('Stand gespeichert!');
|
||
});
|
||
}
|
||
|
||
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 _showSuggestions(suggestions) {
|
||
const el = _container.querySelector('#ueb-suggestions');
|
||
if (!el || !suggestions.length) return;
|
||
|
||
const COLORS = {
|
||
help: { bg: 'rgba(220,38,38,0.08)', border: 'rgba(220,38,38,0.25)', text: '#f87171' },
|
||
boost: { bg: 'rgba(234,88,12,0.08)', border: 'rgba(234,88,12,0.25)', text: '#fb923c' },
|
||
next: { bg: 'rgba(22,163,74,0.08)', border: 'rgba(22,163,74,0.25)', text: '#4ade80' },
|
||
start: { bg: 'var(--c-primary-subtle)', border: 'var(--c-primary-light)', text: 'var(--c-primary)' },
|
||
};
|
||
|
||
el.innerHTML = suggestions.map(s => {
|
||
const c = COLORS[s.type] || COLORS.start;
|
||
return `
|
||
<div style="background:${c.bg};border:1px solid ${c.border};border-radius:var(--radius-md);
|
||
padding:var(--space-3) var(--space-4);display:flex;gap:var(--space-3);align-items:flex-start;cursor:pointer"
|
||
data-action-tab="${_esc(s.action_tab || '')}"
|
||
data-action-name="${_esc(s.action_name || '')}"
|
||
class="ueb-suggestion-card">
|
||
<svg class="ph-icon" style="width:20px;height:20px;flex-shrink:0;color:${c.text};margin-top:2px" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#${_esc(s.icon)}"></use>
|
||
</svg>
|
||
<div style="min-width:0">
|
||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:${c.text};margin-bottom:2px">
|
||
${_esc(s.title)}
|
||
</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">
|
||
${_esc(s.text)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
el.querySelectorAll('.ueb-suggestion-card').forEach(card => {
|
||
card.addEventListener('click', () => {
|
||
const tab = card.dataset.actionTab;
|
||
if (tab && tab !== _activeTab) {
|
||
_activeTab = tab;
|
||
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(b =>
|
||
b.classList.toggle('active', b.dataset.tab === tab)
|
||
);
|
||
_renderContent();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
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;
|
||
|
||
const isExerciseTab = ['grundkommandos', 'tricks', 'problemverhalten'].includes(_activeTab);
|
||
const showIf = v => v ? '' : 'none';
|
||
|
||
const quickWrap = _container.querySelector('#ueb-quicksetup-btn')?.parentElement;
|
||
if (quickWrap) quickWrap.style.display = showIf(isExerciseTab);
|
||
const trainerEl = _container.querySelector('#ueb-trainer');
|
||
const suggestEl = _container.querySelector('#ueb-suggestions');
|
||
const bannerEl = _container.querySelector('#ueb-stats-banner');
|
||
if (trainerEl) trainerEl.style.display = showIf(isExerciseTab);
|
||
if (suggestEl) suggestEl.style.display = showIf(isExerciseTab);
|
||
if (bannerEl) bannerEl.style.display = showIf(isExerciseTab);
|
||
|
||
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();
|
||
_bindLogButtons();
|
||
_bindNotizButtons();
|
||
if (_activeTab === 'ki-trainer') _loadKiTrainerFeedback();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// Ü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 _sessionStatsChip(tab, name) {
|
||
const key = _progressKey(tab, name);
|
||
const stat = _exerciseStats[key];
|
||
if (!stat) return '';
|
||
const avg = Math.round(stat.recent_avg);
|
||
const color = avg >= 75 ? '#15803d' : avg >= 50 ? '#c2410c' : '#dc2626';
|
||
const bg = avg >= 75 ? 'rgba(22,163,74,0.10)' : avg >= 50 ? 'rgba(234,88,12,0.10)' : 'rgba(220,38,38,0.10)';
|
||
const border = avg >= 75 ? 'rgba(22,163,74,0.30)' : avg >= 50 ? 'rgba(234,88,12,0.30)' : 'rgba(220,38,38,0.30)';
|
||
const arrow = { improving: '↑', declining: '↓', stable: '→', new: '' }[stat.trend] || '';
|
||
return `
|
||
<span style="font-size:9px;font-weight:600;padding:1px 6px;border-radius:999px;
|
||
background:${bg};color:${color};border:1px solid ${border};white-space:nowrap">
|
||
${avg}%${arrow} · ${stat.session_count}×
|
||
</span>`;
|
||
}
|
||
|
||
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)">
|
||
<!-- Zeile 1: Name + Schwierigkeits-Badge -->
|
||
<div style="display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-2);margin-bottom:var(--space-2)">
|
||
<span style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);line-height:1.3">
|
||
${_esc(u.name)}
|
||
</span>
|
||
<span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);white-space:nowrap;
|
||
padding:2px var(--space-2);border-radius:var(--radius-sm);flex-shrink:0;
|
||
background:${diff.color}22;color:${diff.color}">
|
||
${_esc(diff.label)}
|
||
</span>
|
||
</div>
|
||
<!-- Zeile 2: Aktions-Buttons -->
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2);flex-wrap:wrap">
|
||
<button class="ueb-log-btn"
|
||
data-tab="${_esc(_activeTab)}"
|
||
data-name="${_esc(u.name)}"
|
||
title="Trainingseinheit loggen"
|
||
style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
|
||
color:var(--c-primary);cursor:pointer;padding:3px 9px;
|
||
display:flex;align-items:center;gap:3px;
|
||
font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
border-radius:var(--radius-sm)">
|
||
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#plus"></use>
|
||
</svg>
|
||
Einheit
|
||
</button>
|
||
${_sessionStatsChip(_activeTab, u.name)}
|
||
<button class="ueb-notiz-btn"
|
||
data-tab="${_esc(_activeTab)}"
|
||
data-name="${_esc(u.name)}"
|
||
title="Notiz hinzufügen"
|
||
style="background:none;border:1px solid var(--c-border);cursor:pointer;
|
||
padding:3px 7px;border-radius:var(--radius-sm);
|
||
display:flex;align-items:center;gap:3px;
|
||
font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#note-pencil"></use>
|
||
</svg>
|
||
Notiz
|
||
</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>
|
||
<span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap">
|
||
${_esc(sm.label)}
|
||
</span>
|
||
</button>
|
||
</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:#78350f22;border:1px solid #d9770644;border-radius:var(--radius-sm);
|
||
font-size:var(--text-xs);color:var(--c-text);line-height:1.4;
|
||
display:flex;align-items:flex-start;gap:var(--space-2)">
|
||
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||
<span>${_esc(u.hinweis)}</span>
|
||
</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:var(--c-warning)" 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);
|
||
if (next === 'sitzt') UI.toast.success(`🏆 „${name}" sitzt! Gut gemacht!`);
|
||
|
||
// 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>` : ''}
|
||
`;
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// LOG SESSION MODAL
|
||
// ----------------------------------------------------------
|
||
function _bindLogButtons() {
|
||
_container.querySelectorAll('.ueb-log-btn').forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
_openLogModal(btn.dataset.tab, btn.dataset.name);
|
||
});
|
||
});
|
||
}
|
||
|
||
function _bindNotizButtons() {
|
||
_container.querySelectorAll('.ueb-notiz-btn').forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
const exerciseId = `${btn.dataset.tab}_${btn.dataset.name.replace(/[\s/]+/g, '_')}`;
|
||
_openNotizModal(exerciseId, btn.dataset.name, btn);
|
||
});
|
||
});
|
||
}
|
||
|
||
async function _openNotizModal(exerciseId, exerciseName, triggerBtn) {
|
||
const modalId = 'ueb-notiz-modal';
|
||
document.getElementById(modalId)?.remove();
|
||
|
||
// Lade bestehende Notiz
|
||
let existingNote = null;
|
||
if (_appState?.user) {
|
||
try {
|
||
const notes = await API.notes.get('training_session', exerciseId.length > 0 ? exerciseId : 0);
|
||
if (notes && notes.length > 0) existingNote = notes[0];
|
||
} catch (_) {}
|
||
}
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = modalId;
|
||
overlay.style.cssText = `
|
||
position:fixed;inset:0;z-index:9999;
|
||
display:flex;align-items:flex-end;justify-content:center;
|
||
background:rgba(0,0,0,0.45);
|
||
`;
|
||
|
||
const noteText = existingNote?.text || '';
|
||
const meta = existingNote?.meta_json || {};
|
||
const currentErfolgsquote = meta.erfolgsquote || null;
|
||
const currentUmgebung = meta.umgebung || null;
|
||
const currentStimmung = meta.hund_stimmung || null;
|
||
|
||
overlay.innerHTML = `
|
||
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||
width:100%;max-width:480px;max-height:85vh;overflow-y:auto;
|
||
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
|
||
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
|
||
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);
|
||
margin:0 0 var(--space-4);text-align:center">
|
||
Notiz: ${_esc(exerciseName)}
|
||
</h3>
|
||
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||
|
||
<!-- Freitext -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Notiz</label>
|
||
<textarea id="ueb-notiz-text" rows="3"
|
||
placeholder="Was ist dir aufgefallen? Tipps für nächstes Mal…"
|
||
style="width:100%;padding:var(--space-3);border:1.5px solid var(--c-border);
|
||
border-radius:var(--radius-md);font-size:var(--text-sm);
|
||
font-family:var(--font-sans);background:var(--c-surface);
|
||
color:var(--c-text);resize:vertical;outline:none;
|
||
line-height:1.5">${_esc(noteText)}</textarea>
|
||
</div>
|
||
|
||
<!-- Erfolgsquote -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Bewertung (optional)</label>
|
||
<div style="display:flex;gap:var(--space-2)">
|
||
${[1,2,3,4,5].map(n => `
|
||
<button type="button" class="ueb-notiz-pfote" data-val="${n}"
|
||
style="font-size:1.4rem;border:1.5px solid var(--c-border);
|
||
border-radius:var(--radius-md);padding:4px 10px;cursor:pointer;
|
||
background:${currentErfolgsquote === n ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
|
||
border-color:${currentErfolgsquote === n ? 'var(--c-primary)' : 'var(--c-border)'};
|
||
transition:all 0.15s">🐾</button>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Umgebung -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Umgebung (optional)</label>
|
||
<div style="display:flex;gap:var(--space-2)">
|
||
${[['🏠','zuhause'],['🌿','natur'],['🌆','stadt']].map(([emoji, val]) => `
|
||
<button type="button" class="ueb-notiz-umgebung" data-val="${val}"
|
||
style="font-size:1.2rem;border:1.5px solid var(--c-border);
|
||
border-radius:var(--radius-md);padding:4px 12px;cursor:pointer;
|
||
background:${currentUmgebung === val ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
|
||
border-color:${currentUmgebung === val ? 'var(--c-primary)' : 'var(--c-border)'};
|
||
transition:all 0.15s">${emoji}</button>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hund-Stimmung -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Stimmung des Hundes (optional)</label>
|
||
<div style="display:flex;gap:var(--space-2)">
|
||
${[['😊','super'],['😐','ok'],['😔','mude']].map(([emoji, val]) => `
|
||
<button type="button" class="ueb-notiz-stimmung" data-val="${val}"
|
||
style="font-size:1.2rem;border:1.5px solid var(--c-border);
|
||
border-radius:var(--radius-md);padding:4px 12px;cursor:pointer;
|
||
background:${currentStimmung === val ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
|
||
border-color:${currentStimmung === val ? 'var(--c-primary)' : 'var(--c-border)'};
|
||
transition:all 0.15s">${emoji}</button>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Buttons -->
|
||
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
|
||
${existingNote ? `
|
||
<button id="ueb-notiz-delete" type="button"
|
||
style="padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);
|
||
border:1.5px solid var(--c-danger);background:none;
|
||
color:var(--c-danger);font-size:var(--text-sm);cursor:pointer">
|
||
Löschen
|
||
</button>
|
||
` : ''}
|
||
<button id="ueb-notiz-cancel" type="button"
|
||
style="flex:1;padding:var(--space-3);border-radius:var(--radius-md);
|
||
border:1.5px solid var(--c-border);background:none;
|
||
color:var(--c-text-secondary);font-size:var(--text-sm);cursor:pointer">
|
||
Abbrechen
|
||
</button>
|
||
<button id="ueb-notiz-save" type="button"
|
||
style="flex:2;padding:var(--space-3);border-radius:var(--radius-md);
|
||
border:none;background:var(--c-primary);
|
||
color:#fff;font-size:var(--text-sm);font-weight:var(--weight-semibold);cursor:pointer">
|
||
${existingNote ? 'Aktualisieren' : 'Speichern'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(overlay);
|
||
|
||
// State
|
||
let selectedErfolgsquote = currentErfolgsquote;
|
||
let selectedUmgebung = currentUmgebung;
|
||
let selectedStimmung = currentStimmung;
|
||
|
||
// Pfoten-Buttons
|
||
overlay.querySelectorAll('.ueb-notiz-pfote').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const val = parseInt(btn.dataset.val, 10);
|
||
selectedErfolgsquote = selectedErfolgsquote === val ? null : val;
|
||
overlay.querySelectorAll('.ueb-notiz-pfote').forEach(b => {
|
||
const active = parseInt(b.dataset.val, 10) === selectedErfolgsquote;
|
||
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
|
||
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
|
||
});
|
||
});
|
||
});
|
||
|
||
// Umgebung-Buttons
|
||
overlay.querySelectorAll('.ueb-notiz-umgebung').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
selectedUmgebung = selectedUmgebung === btn.dataset.val ? null : btn.dataset.val;
|
||
overlay.querySelectorAll('.ueb-notiz-umgebung').forEach(b => {
|
||
const active = b.dataset.val === selectedUmgebung;
|
||
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
|
||
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
|
||
});
|
||
});
|
||
});
|
||
|
||
// Stimmung-Buttons
|
||
overlay.querySelectorAll('.ueb-notiz-stimmung').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
selectedStimmung = selectedStimmung === btn.dataset.val ? null : btn.dataset.val;
|
||
overlay.querySelectorAll('.ueb-notiz-stimmung').forEach(b => {
|
||
const active = b.dataset.val === selectedStimmung;
|
||
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
|
||
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
|
||
});
|
||
});
|
||
});
|
||
|
||
function _closeNotizModal() {
|
||
overlay.remove();
|
||
}
|
||
|
||
overlay.addEventListener('click', e => { if (e.target === overlay) _closeNotizModal(); });
|
||
overlay.querySelector('#ueb-notiz-cancel').addEventListener('click', _closeNotizModal);
|
||
|
||
// Speichern
|
||
overlay.querySelector('#ueb-notiz-save').addEventListener('click', async () => {
|
||
const text = overlay.querySelector('#ueb-notiz-text').value.trim();
|
||
if (!text) { UI.toast.warning('Bitte gib eine Notiz ein.'); return; }
|
||
|
||
const saveBtn = overlay.querySelector('#ueb-notiz-save');
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = 'Speichern…';
|
||
|
||
const meta = {};
|
||
if (selectedErfolgsquote) meta.erfolgsquote = selectedErfolgsquote;
|
||
if (selectedUmgebung) meta.umgebung = selectedUmgebung;
|
||
if (selectedStimmung) meta.hund_stimmung = selectedStimmung;
|
||
|
||
const payload = {
|
||
text,
|
||
meta_json: Object.keys(meta).length > 0 ? meta : null,
|
||
};
|
||
|
||
try {
|
||
if (existingNote) {
|
||
await API.notes.update(existingNote.id, payload);
|
||
} else {
|
||
await API.notes.create('training_session', exerciseId, payload);
|
||
}
|
||
_closeNotizModal();
|
||
UI.toast.success('Notiz gespeichert.');
|
||
// Notiz-Button leicht hervorheben
|
||
if (triggerBtn) {
|
||
triggerBtn.style.borderColor = 'var(--c-primary)';
|
||
triggerBtn.style.color = 'var(--c-primary)';
|
||
}
|
||
} catch (err) {
|
||
saveBtn.disabled = false;
|
||
saveBtn.textContent = existingNote ? 'Aktualisieren' : 'Speichern';
|
||
UI.toast.error('Speichern fehlgeschlagen.');
|
||
}
|
||
});
|
||
|
||
// Löschen
|
||
overlay.querySelector('#ueb-notiz-delete')?.addEventListener('click', async () => {
|
||
if (!existingNote) return;
|
||
try {
|
||
await API.notes.delete(existingNote.id);
|
||
_closeNotizModal();
|
||
UI.toast.success('Notiz gelöscht.');
|
||
if (triggerBtn) {
|
||
triggerBtn.style.borderColor = '';
|
||
triggerBtn.style.color = '';
|
||
}
|
||
} catch (_) {
|
||
UI.toast.error('Löschen fehlgeschlagen.');
|
||
}
|
||
});
|
||
}
|
||
|
||
function _openLogModal(tab, exerciseName, initialReps) {
|
||
// Build the modal HTML
|
||
const modalId = 'ueb-log-modal';
|
||
const formId = 'ueb-log-form';
|
||
// Remove existing if present
|
||
document.getElementById(modalId)?.remove();
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = modalId;
|
||
overlay.style.cssText = `
|
||
position:fixed;inset:0;z-index:9999;
|
||
display:flex;align-items:flex-end;justify-content:center;
|
||
background:rgba(0,0,0,0.45);
|
||
`;
|
||
|
||
overlay.innerHTML = `
|
||
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||
width:100%;max-width:480px;max-height:92vh;overflow-y:auto;
|
||
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
|
||
<!-- Handle -->
|
||
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
|
||
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);
|
||
margin:0 0 var(--space-4);text-align:center">
|
||
Einheit loggen: ${_esc(exerciseName)}
|
||
</h3>
|
||
|
||
<form id="${formId}" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||
|
||
<!-- Wiederholungen -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Wiederholungen</label>
|
||
<div style="display:flex;align-items:center;gap:var(--space-3);justify-content:center">
|
||
<button type="button" id="ueb-rep-minus"
|
||
style="width:36px;height:36px;border-radius:50%;border:1.5px solid var(--c-border);
|
||
background:var(--c-surface-2);font-size:1.2rem;cursor:pointer;
|
||
display:flex;align-items:center;justify-content:center;color:var(--c-text)">−</button>
|
||
<span id="ueb-rep-val" style="font-size:var(--text-xl);font-weight:700;color:var(--c-text);min-width:32px;text-align:center">5</span>
|
||
<button type="button" id="ueb-rep-plus"
|
||
style="width:36px;height:36px;border-radius:50%;border:1.5px solid var(--c-border);
|
||
background:var(--c-surface-2);font-size:1.2rem;cursor:pointer;
|
||
display:flex;align-items:center;justify-content:center;color:var(--c-text)">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Wie lief's -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Wie lief's?</label>
|
||
<div style="display:flex;gap:var(--space-2);justify-content:center;flex-wrap:wrap">
|
||
${[['😓','0'],['😐','25'],['🙂','50'],['😊','75'],['🎉','100']].map(([emoji, val]) => `
|
||
<button type="button" class="ueb-erfolg-btn"
|
||
data-val="${val}"
|
||
style="font-size:1.5rem;padding:var(--space-2) var(--space-3);
|
||
border-radius:var(--radius-md);border:1.5px solid var(--c-border);
|
||
background:var(--c-surface-2);cursor:pointer;transition:all 0.15s"
|
||
title="${val}%">${emoji}</button>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stimmung des Hundes -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Stimmung des Hundes</label>
|
||
<div style="display:flex;gap:var(--space-2);justify-content:center;flex-wrap:wrap">
|
||
${[['🎯','aufmerksam'],['😴','müde'],['🌪️','abgelenkt'],['⚡','super']].map(([emoji, val]) => `
|
||
<button type="button" class="ueb-stimmung-btn"
|
||
data-val="${val}"
|
||
style="display:flex;flex-direction:column;align-items:center;gap:2px;
|
||
font-size:1.2rem;padding:var(--space-2);min-width:60px;
|
||
border-radius:var(--radius-md);border:1.5px solid var(--c-border);
|
||
background:var(--c-surface-2);cursor:pointer;transition:all 0.15s">
|
||
${emoji}
|
||
<span style="font-size:9px;color:var(--c-text-secondary)">${_esc(val)}</span>
|
||
</button>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Zufriedenheit -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Wie zufrieden bist du?</label>
|
||
<div style="display:flex;gap:var(--space-2);justify-content:center">
|
||
${[1,2,3,4,5].map(n => `
|
||
<button type="button" class="ueb-stern-btn"
|
||
data-val="${n}"
|
||
style="font-size:1.5rem;background:none;border:none;cursor:pointer;
|
||
padding:2px;opacity:0.35;transition:opacity 0.15s">⭐</button>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notiz -->
|
||
<div>
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">Notiz</label>
|
||
<textarea id="ueb-log-notiz" rows="2" placeholder="Optional: Was ist aufgefallen?"
|
||
style="width:100%;box-sizing:border-box;padding:var(--space-2) var(--space-3);
|
||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||
font-size:var(--text-sm);font-family:inherit;resize:none;
|
||
background:var(--c-surface);color:var(--c-text);line-height:1.5"></textarea>
|
||
</div>
|
||
|
||
<!-- Meilenstein-Checkbox (initially hidden) -->
|
||
<label id="ueb-log-milestone-wrap" hidden
|
||
style="display:flex;align-items:flex-start;gap:var(--space-2);cursor:pointer;
|
||
padding:var(--space-3);background:var(--c-primary-subtle);
|
||
border:1px solid var(--c-primary-light);border-radius:var(--radius-md)">
|
||
<input type="checkbox" id="ueb-log-milestone"
|
||
style="margin-top:2px;flex-shrink:0;accent-color:var(--c-primary);width:16px;height:16px">
|
||
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">
|
||
📖 Als Meilenstein ins Tagebuch eintragen
|
||
</span>
|
||
</label>
|
||
|
||
</form>
|
||
|
||
<!-- Footer Buttons -->
|
||
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
|
||
<button id="ueb-log-cancel"
|
||
style="flex:1;padding:var(--space-3);border:1.5px solid var(--c-border);
|
||
background:var(--c-surface-2);border-radius:var(--radius-md);
|
||
font-size:var(--text-sm);cursor:pointer;color:var(--c-text)">
|
||
Abbrechen
|
||
</button>
|
||
<button id="ueb-log-save" form="${formId}"
|
||
class="btn btn-primary" style="flex:2"
|
||
type="button">
|
||
Einheit speichern
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(overlay);
|
||
|
||
// State
|
||
let wiederholungen = initialReps || 5;
|
||
let erfolgsquote = null; // must be selected
|
||
let stimmung = null;
|
||
let zufriedenheit = null;
|
||
|
||
// Close helpers
|
||
function _closeModal() { overlay.remove(); }
|
||
overlay.addEventListener('click', e => { if (e.target === overlay) _closeModal(); });
|
||
overlay.querySelector('#ueb-log-cancel').addEventListener('click', _closeModal);
|
||
|
||
// Stepper
|
||
const repVal = overlay.querySelector('#ueb-rep-val');
|
||
repVal.textContent = wiederholungen;
|
||
overlay.querySelector('#ueb-rep-minus').addEventListener('click', () => {
|
||
if (wiederholungen > 1) { wiederholungen--; repVal.textContent = wiederholungen; }
|
||
});
|
||
overlay.querySelector('#ueb-rep-plus').addEventListener('click', () => {
|
||
wiederholungen++;
|
||
repVal.textContent = wiederholungen;
|
||
});
|
||
|
||
// Erfolg-Buttons
|
||
overlay.querySelectorAll('.ueb-erfolg-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
erfolgsquote = parseInt(btn.dataset.val, 10);
|
||
overlay.querySelectorAll('.ueb-erfolg-btn').forEach(b => {
|
||
b.style.background = 'var(--c-surface-2)';
|
||
b.style.borderColor = 'var(--c-border)';
|
||
b.style.transform = '';
|
||
});
|
||
btn.style.background = 'var(--c-primary-subtle)';
|
||
btn.style.borderColor = 'var(--c-primary)';
|
||
btn.style.transform = 'scale(1.15)';
|
||
_checkMilestoneVisibility();
|
||
});
|
||
});
|
||
|
||
// Stimmung-Buttons
|
||
overlay.querySelectorAll('.ueb-stimmung-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
stimmung = btn.dataset.val;
|
||
overlay.querySelectorAll('.ueb-stimmung-btn').forEach(b => {
|
||
b.style.background = 'var(--c-surface-2)';
|
||
b.style.borderColor = 'var(--c-border)';
|
||
});
|
||
btn.style.background = 'var(--c-primary-subtle)';
|
||
btn.style.borderColor = 'var(--c-primary)';
|
||
});
|
||
});
|
||
|
||
// Stern-Buttons
|
||
overlay.querySelectorAll('.ueb-stern-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
zufriedenheit = parseInt(btn.dataset.val, 10);
|
||
overlay.querySelectorAll('.ueb-stern-btn').forEach(b => {
|
||
b.style.opacity = parseInt(b.dataset.val, 10) <= zufriedenheit ? '1' : '0.35';
|
||
});
|
||
_checkMilestoneVisibility();
|
||
});
|
||
});
|
||
|
||
function _checkMilestoneVisibility() {
|
||
const wrap = overlay.querySelector('#ueb-log-milestone-wrap');
|
||
if (!wrap) return;
|
||
const show = erfolgsquote != null && erfolgsquote >= 75 && zufriedenheit != null && zufriedenheit >= 4;
|
||
wrap.hidden = !show;
|
||
}
|
||
|
||
// Save
|
||
overlay.querySelector('#ueb-log-save').addEventListener('click', async () => {
|
||
const dogId = _dogId();
|
||
if (!dogId) { UI.toast.warning('Kein Hund ausgewählt.'); return; }
|
||
if (erfolgsquote === null) { UI.toast.warning('Bitte wähle aus, wie es gelaufen ist.'); return; }
|
||
|
||
const saveBtn = overlay.querySelector('#ueb-log-save');
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = 'Speichern…';
|
||
|
||
const exerciseId = `${tab}_${exerciseName.replace(/[\s/]+/g, '_')}`;
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
const tagebuch = !overlay.querySelector('#ueb-log-milestone-wrap').hidden &&
|
||
overlay.querySelector('#ueb-log-milestone').checked;
|
||
|
||
const body = {
|
||
dog_id: dogId,
|
||
exercise_id: exerciseId,
|
||
exercise_name: exerciseName,
|
||
datum: today,
|
||
wiederholungen: wiederholungen,
|
||
erfolgsquote: erfolgsquote,
|
||
hund_stimmung: stimmung || null,
|
||
zufriedenheit: zufriedenheit || null,
|
||
notiz: overlay.querySelector('#ueb-log-notiz').value.trim() || null,
|
||
tagebuch_eintrag: tagebuch,
|
||
};
|
||
|
||
try {
|
||
const resp = await _apiPost('/api/training/sessions', body);
|
||
_closeModal();
|
||
if (!resp) { UI.toast.error('Speichern fehlgeschlagen.'); return; }
|
||
|
||
if (resp.ist_top) {
|
||
UI.toast.success('🎉 Top-Training! Das war eine eurer besten Einheiten.');
|
||
} else {
|
||
UI.toast.success('Einheit gespeichert!');
|
||
}
|
||
|
||
if (resp.badges && resp.badges.length) {
|
||
resp.badges.forEach((badge, idx) => {
|
||
setTimeout(() => {
|
||
UI.toast.success(`🏅 Neues Abzeichen: "${badge.name || badge}"!`);
|
||
}, 1000 * (idx + 1));
|
||
});
|
||
}
|
||
|
||
if (resp.diary_entry_id) {
|
||
setTimeout(() => {
|
||
UI.toast.success('📖 Als Meilenstein im Tagebuch gespeichert.');
|
||
}, resp.badges?.length ? (resp.badges.length + 1) * 1000 : 1000);
|
||
}
|
||
|
||
// Stats-Banner + Trainer aktualisieren
|
||
_statsData = null;
|
||
_loadStatsAndBadges();
|
||
_loadVirtualTrainer();
|
||
} catch (err) {
|
||
saveBtn.disabled = false;
|
||
saveBtn.textContent = 'Einheit speichern';
|
||
UI.toast.error('Speichern fehlgeschlagen.');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// KI-TRAINER (neu: hundebasiertes Feedback)
|
||
// ----------------------------------------------------------
|
||
function _renderKiTrainer() {
|
||
const dogId = _dogId();
|
||
if (!dogId) {
|
||
return `
|
||
<div style="padding:var(--space-6) var(--space-4);text-align:center;color:var(--c-text-secondary)">
|
||
<svg class="ph-icon" style="width:40px;height:40px;margin-bottom:var(--space-3);color:var(--c-border)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#robot"></use>
|
||
</svg>
|
||
<p style="font-size:var(--text-sm);margin:0">Wähle einen Hund aus um den KI-Trainer zu nutzen.</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return `
|
||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)" id="ki-trainer-panel">
|
||
|
||
<!-- Header -->
|
||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||
<div style="width:48px;height:48px;border-radius:50%;background:var(--c-primary-subtle);
|
||
display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||
<svg class="ph-icon" style="width:26px;height:26px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#robot"></use>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<div style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text)">
|
||
KI-Trainer
|
||
</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
Personalisiertes Feedback basierend auf deinen Trainingseinheiten
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lade-Spinner -->
|
||
<div id="ki-loading" style="text-align:center;padding:var(--space-6);color:var(--c-text-secondary)">
|
||
<svg class="ph-icon" style="width:32px;height:32px;animation:spin 1s linear infinite;margin-bottom:var(--space-2)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#spinner-gap"></use>
|
||
</svg>
|
||
<p style="font-size:var(--text-sm);margin:0">Lade KI-Feedback…</p>
|
||
</div>
|
||
|
||
<!-- Kein Sessions-Hinweis -->
|
||
<div id="ki-no-sessions" hidden
|
||
style="text-align:center;padding:var(--space-6) var(--space-4);color:var(--c-text-secondary)">
|
||
<svg class="ph-icon" style="width:40px;height:40px;margin-bottom:var(--space-3);color:var(--c-border)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#clipboard-text"></use>
|
||
</svg>
|
||
<p style="font-size:var(--text-sm);margin:0">
|
||
Logge deine erste Trainingseinheit um KI-Feedback zu erhalten.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Feedback-Card -->
|
||
<div id="ki-feedback-card" hidden>
|
||
<div class="card" style="border-left:3px solid var(--c-primary);background:var(--c-surface)">
|
||
<div id="ki-feedback-text"
|
||
style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7;white-space:pre-wrap"></div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:var(--space-3)">
|
||
<span id="ki-feedback-meta" style="font-size:var(--text-xs);color:var(--c-text-muted)"></span>
|
||
<button id="ki-regenerate"
|
||
style="font-size:var(--text-xs);padding:var(--space-1) var(--space-3);
|
||
border-radius:var(--radius-md);border:1px solid var(--c-border);
|
||
background:var(--c-surface-2);cursor:pointer;color:var(--c-text-secondary)">
|
||
Neu generieren
|
||
</button>
|
||
</div>
|
||
</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.
|
||
</p>
|
||
|
||
</div>
|
||
<style>
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
</style>
|
||
`;
|
||
}
|
||
|
||
async function _loadKiTrainerFeedback(forceRefresh) {
|
||
const loading = _container.querySelector('#ki-loading');
|
||
const noSessions = _container.querySelector('#ki-no-sessions');
|
||
const feedbackCard = _container.querySelector('#ki-feedback-card');
|
||
const feedbackText = _container.querySelector('#ki-feedback-text');
|
||
const feedbackMeta = _container.querySelector('#ki-feedback-meta');
|
||
const regenBtn = _container.querySelector('#ki-regenerate');
|
||
if (!loading) return; // not on ki-trainer tab
|
||
|
||
const dogId = _dogId();
|
||
if (!dogId) return;
|
||
|
||
// Show loading
|
||
loading.hidden = false;
|
||
noSessions.hidden = true;
|
||
feedbackCard.hidden = true;
|
||
|
||
// Check if there are any sessions
|
||
const stats = _statsData || await _apiGet(`/api/training/stats?dog_id=${dogId}`);
|
||
if (!stats || !stats.total_sessions) {
|
||
loading.hidden = true;
|
||
noSessions.hidden = false;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const resp = await _apiPost('/api/training/ki-feedback', { dog_id: dogId });
|
||
loading.hidden = true;
|
||
if (!resp || !resp.feedback) {
|
||
noSessions.hidden = false;
|
||
return;
|
||
}
|
||
feedbackText.textContent = resp.feedback;
|
||
const cachedInfo = resp.cached ? 'Gespeichertes Feedback' : 'Gerade generiert';
|
||
const sessionInfo = stats.total_sessions
|
||
? ` · ${stats.total_sessions} Einheit${stats.total_sessions !== 1 ? 'en' : ''}`
|
||
: '';
|
||
const limitInfo = (resp.daily_limit && !resp.cached)
|
||
? ` · ${resp.daily_used}/${resp.daily_limit} heute` : '';
|
||
feedbackMeta.textContent = `${cachedInfo}${sessionInfo}${limitInfo}`;
|
||
if (regenBtn) {
|
||
const limitReached = resp.daily_used >= resp.daily_limit && !resp.cached;
|
||
regenBtn.textContent = resp.cached ? 'Neu generieren' : 'Aktualisieren';
|
||
regenBtn.style.opacity = (resp.cached && !limitReached) ? '0.6' : '1';
|
||
regenBtn.disabled = limitReached;
|
||
if (limitReached) regenBtn.title = `Tages-Limit erreicht (${resp.daily_limit}/Tag)`;
|
||
}
|
||
feedbackCard.hidden = false;
|
||
} catch (err) {
|
||
loading.hidden = true;
|
||
if (err?.status === 429 || (err?.message || '').includes('429')) {
|
||
feedbackMeta.textContent = `Tages-Limit erreicht (${10}/Tag) — morgen wieder verfügbar.`;
|
||
feedbackCard.hidden = false;
|
||
} else {
|
||
noSessions.hidden = false;
|
||
}
|
||
}
|
||
|
||
// Bind regenerate button
|
||
if (regenBtn) {
|
||
regenBtn.addEventListener('click', async () => {
|
||
regenBtn.disabled = true;
|
||
regenBtn.textContent = 'Generiere…';
|
||
loading.hidden = false;
|
||
feedbackCard.hidden = true;
|
||
await _loadKiTrainerFeedback(true);
|
||
regenBtn.disabled = false;
|
||
}, { once: true });
|
||
}
|
||
}
|
||
|
||
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" style="padding: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);display:flex;align-items:center;gap:var(--space-2)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary);flex-shrink:0" 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">
|
||
<div style="padding:var(--space-4) var(--space-4) var(--space-3)">
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin:0;display:flex;align-items:center;gap:var(--space-2)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary);flex-shrink:0" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#star"></use>
|
||
</svg>
|
||
Wann belohnen?
|
||
</h3>
|
||
</div>
|
||
<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">
|
||
<div style="padding:var(--space-4) var(--space-4) var(--space-3)">
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin:0 0 var(--space-2);display:flex;align-items:center;gap:var(--space-2)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary);flex-shrink:0" 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">
|
||
Nicht alle Leckerlis sind gleich. Bei Ablenkung oder schwierigen Übungen brauchst du hochwertigere Belohnungen:
|
||
</p>
|
||
</div>
|
||
<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="padding:var(--space-4);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);display:flex;align-items:center;gap:var(--space-2)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary);flex-shrink:0" 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 (5–10 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PUBLIC
|
||
// ----------------------------------------------------------
|
||
return { init, refresh, onDogChange };
|
||
|
||
})();
|