/* ============================================================
BAN YARO — Trainingspläne
Seiten-Modul: Welpe, Junior, Erwachsener Hund
Alle Inhalte hardcoded, Checkboxen via localStorage
============================================================ */
window.Page_trainingsplaene = (() => {
let _container = null;
let _appState = null;
let _activePlan = 'welpe'; // welpe | junior | erwachsen
let _activeAdultTab = 'grundkurs'; // grundkurs | aufbaukurs
// ----------------------------------------------------------
// API HELPERS
// ----------------------------------------------------------
function _dogId() {
return _appState?.activeDog?.id || null;
}
async function _apiGet(url) {
const r = await fetch(url, {credentials: 'include'});
return r.ok ? r.json() : null;
}
// ----------------------------------------------------------
// HELPER
// ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function _icon(name) {
return ` `;
}
function _lsKey(planId, goalIdx) {
const dogId = _dogId() || 'x';
return `tp_d${dogId}_${planId}_${goalIdx}`;
}
function _saveGoal(key, checked) {
localStorage.setItem(key, checked ? 'true' : 'false');
}
function _loadGoal(key) {
return localStorage.getItem(key) === 'true';
}
// ----------------------------------------------------------
// RENDER HELPERS
// ----------------------------------------------------------
function _renderTable(headers, rows) {
const ths = headers.map(h => `
${_esc(h)} `).join('');
const trs = rows.map(row => {
const tds = row.map((cell, i) => `${_esc(cell)} `).join('');
return `${tds} `;
}).join('');
return `
`;
}
function _renderGoals(planId, goals) {
const total = goals.length;
let doneCount = 0;
const items = goals.map((goal, idx) => {
const key = _lsKey(planId, idx);
const checked = _loadGoal(key);
if (checked) doneCount++;
return `
${_esc(goal)}
`;
}).join('');
const progress = total > 0 ? Math.round((doneCount / total) * 100) : 0;
return `
${_icon('check-circle')} Lernziele
${doneCount} von ${total} erreicht
${items}
`;
}
function _renderAccordionPhase(id, title, content) {
return `
${title}
${_icon('caret-down')}
${content}
`;
}
function _renderHintBox(text) {
return `
${_icon('info')} ${_esc(text)}
`;
}
// ----------------------------------------------------------
// PLAN SELECTOR
// ----------------------------------------------------------
function _renderPlanSelector() {
const plans = [
{ id: 'welpe', label: 'Welpe', icon: 'dog', sub: '0–6 Monate' },
{ id: 'junior', label: 'Junior', icon: 'dog', sub: '6–18 Monate' },
{ id: 'erwachsen',label: 'Erwachsener Hund', icon: 'dog', sub: 'Grund- & Aufbaukurs' },
];
const btns = plans.map(p => `
${p.label}
${_esc(p.sub)}
`).join('');
return `${btns}
`;
}
// ----------------------------------------------------------
// WELPENPLAN
// ----------------------------------------------------------
function _renderWelpe() {
const intro = `
Voraussetzungen: Welpe ist eingezogen, Grundvertrauen wird aufgebaut.
Ziel am Ende: Sitz, Platz, Hier, Warte, Leine ohne Ziehen, Alleine bleiben bis 1 Stunde, keine Begrüßungssprünge.
${_renderHintBox('Welpen sind schnell überfordert. Lieber 3×5 Minuten täglich als einmal 20 Minuten. Sozialisation (Menschen, Geräusche, Orte) hat in dieser Phase genauso Priorität wie Training.')}`;
const w12 = `
Fokus: Name, erste Bindung, Stubenreinheit, keine Übungen erzwingen.
${_renderTable(
['Tag', 'Einheit 1', 'Einheit 2', 'Einheit 3'],
[['Mo–So', 'Name lernen', 'Markerwort einführen', 'Freies Erkunden + Beobachten']]
)}
Name lernen: Namen sagen → Hund schaut → sofort Markerwort + Leckerli. 10–15× täglich.
Markerwort einführen: Markerwort sagen → sofort Leckerli (10× wiederholen). Ab jetzt: Markerwort immer vor dem Leckerli.
Stubenreinheit: Alle 1–2 Stunden raus, nach Schlafen/Fressen/Spielen. Kein Schimpfen bei Unfällen.
`;
const w34 = `
Fokus: Sitz, Warte (Futter), Nicht springen.
${_renderTable(
['Tag', 'Einheit 1', 'Einheit 2', 'Einheit 3'],
[
['Mo', 'Sitz einführen', 'Sitz wiederholen', 'Warte vor Futter'],
['Di', 'Sitz mit Handsignal', 'Nicht springen üben', 'Sitz aus verschiedenen Positionen'],
['Mi', 'Warte vor Futter', 'Sitz 5×', 'Freispiel + Name'],
['Do', 'Sitz festigen', 'Warte 5×', 'Nicht springen'],
['Fr', 'Sitz + Warte kombiniert', 'Freispiel', 'Leckerli-Suche im Gras'],
['Sa', 'Wiederholung Woche', 'Spaziergang mit Leine (ohne Ziel)', 'Entspannen auf Decke'],
['So', 'Ruhiger Tag', 'Kurze Sitz-Einheit', 'Sozialisation (Geräusche, Menschen)'],
]
)}
${_renderGoals('welpe_w34', [
'Sitz auf Handsignal (ohne Wort)',
'Sitz auf Wort "Sitz"',
'Wartet vor der Futterschüssel bis "Okay"',
'Springt nicht mehr bei Begrüßung (in Übung)',
])}`;
const w56 = `
Fokus: Platz, Leinengewöhnung, Hier auf kurze Distanz.
${_renderTable(
['Tag', 'Einheit 1', 'Einheit 2', 'Einheit 3'],
[
['Mo', 'Platz einführen', 'Sitz wiederholen', 'Leine anlegen + Leckerli'],
['Di', 'Platz üben', 'Hier (2 Meter, innen)', 'Leine: erste Schritte'],
['Mi', 'Platz festigen', 'Sitz + Platz abwechselnd', 'Hier aus anderem Zimmer'],
['Do', 'Leine im Garten', 'Hier mit Freude', 'Platz 5×'],
['Fr', 'Sitz + Platz + Hier kombiniert', 'Spaziergang kurz', 'Nasenarbeit (Leckerli unter Becher)'],
['Sa', 'Wiederholungstag', 'Sozialisation (Markt, Park)', 'Freispiel'],
['So', 'Ruhiger Tag', 'Kurze Einheit nach Wahl', 'Entspannen'],
]
)}
${_renderGoals('welpe_w56', [
'Platz auf Handsignal',
'Platz auf Wort "Platz"',
'Kommt auf "Hier" aus 3 Metern',
'Läuft 5 Schritte an lockerer Leine',
])}`;
const w78 = `
Fokus: Bleib (Dauer), Alleine bleiben aufbauen, Fuß vorbereiten.
${_renderTable(
['Tag', 'Einheit 1', 'Einheit 2', 'Einheit 3'],
[
['Mo', 'Bleib einführen (5 Sek)', 'Alleine bleiben (30 Sek)', 'Sitz + Platz Wiederholung'],
['Di', 'Bleib (10 Sek)', 'Alleine bleiben (1 Min)', 'Fuß: erste Schritte'],
['Mi', 'Bleib mit 1 Schritt Distanz', 'Hier aus Garten', 'Nasenarbeit'],
['Do', 'Bleib (20 Sek)', 'Alleine bleiben (2 Min)', 'Leine üben'],
['Fr', 'Sitz + Bleib + Hier kombiniert', 'Fuß 10 Schritte', 'Freispiel'],
['Sa', 'Ausflug (neue Umgebung)', 'Kurze Übungen unterwegs', 'Sozialisation'],
['So', 'Ruhiger Tag', 'Wiederholung nach Wahl', 'Entspannen'],
]
)}
${_renderGoals('welpe_w78', [
'Bleibt 30 Sekunden im Sitz',
'Bleibt alleine bis 5 Minuten ohne Stress',
'Kommt zuverlässig auf "Hier" (innen + Garten)',
'Fuß: 10 Schritte an lockerer Leine',
])}`;
const w912 = `
Fokus: Alle Kommandos in Alltagssituationen, erste leichte Ablenkungen.
${_renderTable(
['Woche', 'Schwerpunkt'],
[
['9', 'Alle Kommandos im Garten mit leichter Ablenkung'],
['10', 'Sitz + Bleib auf der Straße, Fuß auf kurzen Spaziergängen'],
['11', 'Hier mit Schleppleine im Park, Alleine bleiben bis 30 Min'],
['12', 'Wiederholung + erste Tricks (Pfote, Dreh)'],
]
)}
${_renderGoals('welpe_w912', [
'Alle Grundkommandos auf Wort und Handsignal',
'Bleibt alleine bis 1 Stunde ohne Stress',
'Kommt zuverlässig auf "Hier" im Garten',
'Läuft entspannt an der Leine in ruhiger Umgebung',
'Erster Trick gelernt (Pfote oder Dreh)',
])}`;
return `
${intro}
${_renderAccordionPhase('welpe-w12', 'Woche 1–2: Ankommen & Vertrauen', w12)}
${_renderAccordionPhase('welpe-w34', 'Woche 3–4: Erste Kommandos', w34)}
${_renderAccordionPhase('welpe-w56', 'Woche 5–6: Platz & erste Leine', w56)}
${_renderAccordionPhase('welpe-w78', 'Woche 7–8: Bleib & Alleine bleiben', w78)}
${_renderAccordionPhase('welpe-w912', 'Woche 9–12: Festigung & Alltagsintegration', w912)}
`;
}
// ----------------------------------------------------------
// JUNIORPLAN
// ----------------------------------------------------------
function _renderJunior() {
const intro = `
Voraussetzungen: Grundkommandos bekannt, aber pubertätsbedingt unzuverlässig.
Ziel am Ende: Alle Grundkommandos auch bei Ablenkung, zuverlässiger Rückruf, Leinenführigkeit in der Stadt.
${_renderHintBox('Die Pubertät (ca. 6–12 Monate) ist normal. Der Hund "vergisst" scheinbar alles — er testet Grenzen und Reize sind überwältigend. Konsequenz und Geduld, kein Rückschritt im Denken.')}`;
const m1 = `
Fokus: Alle bekannten Kommandos mit höherer Ablenkung neu festigen.
${_renderTable(
['Tag', 'Einheit 1 (5 Min)', 'Einheit 2 (5–10 Min)'],
[
['Mo', 'Sitz + Platz mit Ablenkung (Ball auf dem Boden)', 'Leine üben in neuer Umgebung'],
['Di', 'Hier mit Schleppleine im Park', 'Bleib mit Distanz (3–5 Meter)'],
['Mi', 'Fuß auf belebtem Gehweg', 'Nasenarbeit / Trick'],
['Do', 'Aus + Warte kombiniert', 'Alleine bleiben (bis 2 Stunden)'],
['Fr', 'Alle Kommandos — kurze Runde', 'Freispiel'],
['Sa', 'Ausflug + Training in neuer Umgebung', 'Sozialisation'],
['So', 'Ruhiger Tag — leichte Einheit', 'Entspannen'],
]
)}
${_renderGoals('junior_m1', [
'Sitz + Platz trotz Ball/anderen Hunden in der Nähe',
'Bleibt 1 Minute mit 5 Metern Distanz',
'Kommt auf "Hier" im Park (mit Schleppleine)',
'Fuß auf 50 Meter in ruhiger Straße',
])}`;
const m2 = `
Fokus: Zuverlässiger Rückruf — das wichtigste Kommando in dieser Phase.
Rückruf mit Schleppleine (10 Meter): Hund beschäftigt sich → "Hier!" → leichtes Anziehen wenn nötig → große Belohnung beim Ankommen
Versteckspiel: Im Wald/Park verstecken → "Hier!" rufen → Hund sucht und findet → größte Belohnung
Rückruf aus der Gruppe: Hund spielt mit anderen Hunden → "Hier!" → kommt er: Jackpot (5 Leckerlis + Streicheln)
Notfallrückruf einführen: Anderes Wort (z.B. "Rapid!") nur für echte Notfälle — immer mit höchster Belohnung
${_renderGoals('junior_m2', [
'Kommt auf "Hier" aus 20 Metern mit Schleppleine',
'Kommt aus Spielsituation mit anderen Hunden zurück',
'Notfallrückruf eingeführt',
])}`;
const m3 = `
Fokus: Ruhiges Gehen auch bei Fahrrädern, anderen Hunden, Kinderwagen.
Ruhige Straße → belohnen alle 10 Schritte bei locker hängender Leine
Fahrrad kommt entgegen → Sitz → warten → Fahrrad vorbei → weitergehen + Leckerli
Anderer Hund in Sichtweite → Fokus auf dich (Augenkontakt trainieren) → Leckerli
Belebte Fußgängerzone: erst beobachten lassen, dann durch
Augenkontakt: "Schau" sagen → Hund schaut in deine Augen → sofort Markerwort + Leckerli. In Ablenkungssituationen: "Schau" → fokussiert → dann erst weitergehen.
${_renderGoals('junior_m3', [
'Läuft 500 Meter an lockerer Leine in der Stadt',
'Bleibt bei Ablenkung (Fahrrad, Jogger) fokussiert',
'Augenkontakt auf Kommando "Schau"',
])}`;
const m46 = `
Fokus: Festigung aller Kommandos, erste Aufbauübungen, Tricks für mentale Auslastung.
${_renderTable(
['Monat', 'Schwerpunkt'],
[
['4', 'Bleib auf Distanz (10 Meter), Freifolge (ohne Leine in sicherer Umgebung)'],
['5', 'Platz auf Distanz, Hier vom Freilauf, Trick-Erweiterung (Decke, Suchspiel)'],
['6', 'Alle Kommandos zuverlässig, erstes Hundesport-Schnuppern optional'],
]
)}
${_renderGoals('junior_m46', [
'Alle Grundkommandos bei mittlerer Ablenkung',
'Freifolge auf kurze Distanz (eingezäuntes Gelände)',
'2–3 Tricks beherrscht',
'Rückruf vom Freilauf (Schleppleine)',
])}`;
return `
${intro}
${_renderAccordionPhase('junior-m1', 'Monat 1: Grundkommandos neu aufbauen', m1)}
${_renderAccordionPhase('junior-m2', 'Monat 2: Rückruf vertiefen', m2)}
${_renderAccordionPhase('junior-m3', 'Monat 3: Leinenführigkeit in der Stadt', m3)}
${_renderAccordionPhase('junior-m46', 'Monat 4–6: Aufbau & Tricks', m46)}
`;
}
// ----------------------------------------------------------
// ERWACHSENER HUND
// ----------------------------------------------------------
function _renderErwachsenTabs() {
return `
Grundkurs
Aufbaukurs
Übersicht
`;
}
function _renderGrundkurs() {
const intro = `
Voraussetzungen: Keine oder wenige Vorkenntnisse.
Ziel: Alle Grundkommandos in 8 Wochen.
${_renderHintBox('Erwachsene Hunde lernen genauso gut wie Welpen — oft sogar konzentrierter. Alte Gewohnheiten brauchen länger zum Überschreiben, aber mit konsequentem Training klappt es.')}`;
const gw12 = `
${_renderTable(
['Tag', 'Einheit 1', 'Einheit 2'],
[
['Mo', 'Markerwort einführen', 'Sitz einführen'],
['Di', 'Sitz festigen', 'Warte vor Futter'],
['Mi', 'Sitz aus Bewegung', 'Markerwort in Alltagssituationen'],
['Do', 'Sitz + Warte kombiniert', 'Leine: erste Einschätzung'],
['Fr', 'Wiederholung', 'Freispiel'],
['Sa/So', 'Alltagsintegration', 'Kurze Einheit'],
]
)}`;
const gw34 = `
${_renderTable(
['Tag', 'Einheit 1', 'Einheit 2'],
[
['Mo', 'Platz einführen', 'Hier (Wohnung, 3 Meter)'],
['Di', 'Platz festigen', 'Leinenführigkeit einschätzen + üben'],
['Mi', 'Sitz + Platz abwechselnd', 'Hier aus anderem Zimmer'],
['Do', 'Hier im Garten', 'Fuß vorbereiten'],
['Fr', 'Leine 10 Minuten üben', 'Platz + Hier kombiniert'],
['Sa/So', 'Spaziergang mit Training', 'Nasenarbeit'],
]
)}`;
const gw56 = `
${_renderTable(
['Tag', 'Einheit 1', 'Einheit 2'],
[
['Mo', 'Bleib einführen (10 Sek)', 'Fuß einführen'],
['Di', 'Bleib (30 Sek, 1 Schritt)', 'Aus einführen'],
['Mi', 'Fuß 20 Schritte', 'Bleib mit Distanz'],
['Do', 'Aus mit Spielzeug', 'Fuß in der Straße'],
['Fr', 'Alle Kommandos kombiniert', 'Trick nach Wahl'],
['Sa/So', 'Ausflug mit Training', 'Sozialisation'],
]
)}`;
const gw78 = `
Alle Kommandos in Alltagssituationen:
Sitz vor dem Überqueren der Straße
Platz wenn Besuch kommt
Warte vor der Haustür
Hier beim Freilauf im Garten
Fuß auf dem Bürgersteig
Aus bei gefundenem Gegenstand
${_renderGoals('erwachsen_gk', [
'Sitz, Platz, Bleib (30 Sek), Hier, Fuß, Aus, Warte',
'Alle Kommandos im Alltag einsetzbar',
'Leine ohne starkes Ziehen',
])}`;
return `
${intro}
${_renderAccordionPhase('gk-w12', 'Woche 1–2: Markerwort + Sitz + Warte', gw12)}
${_renderAccordionPhase('gk-w34', 'Woche 3–4: Platz + Hier + Leine', gw34)}
${_renderAccordionPhase('gk-w56', 'Woche 5–6: Bleib + Fuß + Aus', gw56)}
${_renderAccordionPhase('gk-w78', 'Woche 7–8: Festigung + Alltagsintegration', gw78)}
`;
}
function _renderAufbaukurs() {
const intro = `
Voraussetzungen: Grundkurs abgeschlossen oder Kommandos bekannt.
Dauer: 8 Wochen | Ziel: Kommandos bei Ablenkung, Distanzkommandos, Tricks, mentale Auslastung.
`;
const aw12 = `
Alle bekannten Kommandos werden mit steigender Ablenkung trainiert. Immer bei der niedrigsten Stufe anfangen die noch klappt.
${_renderTable(
['Ablenkungsstufe', 'Beispiele'],
[
['Stufe 1', 'Leckerli auf dem Boden, Ball in Sichtweite'],
['Stufe 2', 'Anderer Mensch im Raum, Geräusche'],
['Stufe 3', 'Anderer Hund in Sichtweite'],
['Stufe 4', 'Belebter Park, Straße'],
['Stufe 5', 'Freilauf, Spielsituation unterbrechen'],
]
)}`;
const aw34 = `
Sitz aus 5 Metern → 10 Metern
Platz aus 5 Metern → 10 Metern
Bleib mit 10 Metern Distanz
Hier vom Freilauf (mit Schleppleine absichern)
Übung "Fernbedienung": Hund steht 5 Meter entfernt → "Sitz" → kurz warten → "Platz" → kurz warten → "Sitz" → "Hier". Wirkt beeindruckend, festigt Kommandos enorm.
`;
const aw56 = `
${_renderTable(
['Woche', 'Trick', 'Nasenarbeit'],
[
['5', 'Pfote + Dreh', 'Leckerli unter 3 Bechern suchen'],
['6', 'Decke/Matte', 'Leckerli im Gras suchen, Gegenstand apportieren'],
]
)}`;
const aw78 = `
Schwächstes Kommando intensiv üben
Neue Umgebungen aufsuchen (anderer Wald, andere Stadt)
Hundesport ausprobieren: Agility, Mantrailing, Obedience, Trickdogging
${_renderGoals('erwachsen_ak', [
'Alle Kommandos bei Stufe 3–4 Ablenkung',
'Sitz + Platz auf 10 Meter Distanz',
'Hier vom Freilauf (Schleppleine)',
'3–4 Tricks',
'Nasenarbeit als regelmäßige Beschäftigung',
])}`;
return `
${intro}
${_renderAccordionPhase('ak-w12', 'Woche 1–2: Ablenkungstraining', aw12)}
${_renderAccordionPhase('ak-w34', 'Woche 3–4: Distanzkommandos', aw34)}
${_renderAccordionPhase('ak-w56', 'Woche 5–6: Tricks & Nasenarbeit', aw56)}
${_renderAccordionPhase('ak-w78', 'Woche 7–8: Freie Gestaltung', aw78)}
`;
}
function _renderUebersicht() {
return `
${_icon('clipboard-text')} Welcher Plan für welchen Hund?
${_renderTable(
['Situation', 'Empfohlener Plan'],
[
['Neuer Welpe (8–16 Wochen)', 'Welpenplan Woche 1–4'],
['Welpe 4–6 Monate', 'Welpenplan Woche 5–12'],
['Junghund 6–12 Monate (Pubertät)', 'Juniorplan Monat 1–3'],
['Junghund 12–18 Monate', 'Juniorplan Monat 4–6'],
['Erwachsener Hund ohne Training', 'Grundkurs Erwachsener'],
['Erwachsener Hund mit Grundwissen', 'Aufbaukurs Erwachsener'],
['Neu eingezogener Hund (unbekannte Vorgeschichte)', 'Grundkurs Erwachsener, Tempo anpassen'],
]
)}`;
}
function _renderErwachsen() {
const intro = `
Wähle deinen Kurs oder sieh dir die Schnellübersicht an.
`;
let content = '';
if (_activeAdultTab === 'grundkurs') {
content = _renderGrundkurs();
} else if (_activeAdultTab === 'aufbaukurs') {
content = _renderAufbaukurs();
} else {
content = `${_renderUebersicht()}
`;
}
return `${intro}${_renderErwachsenTabs()}${content}`;
}
// ----------------------------------------------------------
// BIND EVENTS
// ----------------------------------------------------------
function _bindEvents() {
UI.bindDogChip(_container, _appState);
// Notiz-Button
const dogId = _dogId();
_container.querySelector('#tp-note-btn')?.addEventListener('click', e => {
e.stopPropagation();
const planLabel = _activePlan === 'welpe' ? 'Welpe 0–6 Monate'
: _activePlan === 'junior' ? 'Junior 6–18 Monate'
: `Erwachsener Hund – ${_activeAdultTab}`;
_openNoteModal('trainingsplan', dogId, planLabel, null);
});
// Plan selector
_container.querySelectorAll('[data-plan]').forEach(btn => {
btn.addEventListener('click', () => {
_activePlan = btn.dataset.plan;
_render();
});
});
// Adult sub-tabs
_container.querySelectorAll('[data-tab]').forEach(btn => {
btn.addEventListener('click', () => {
_activeAdultTab = btn.dataset.tab;
_render();
});
});
// Accordion
_container.querySelectorAll('.tp-acc-head').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.acc;
const body = document.getElementById(`tp-acc-body-${id}`);
const arrow = btn.querySelector('.tp-acc-arrow');
if (!body) return;
const isOpen = !body.hidden;
body.hidden = isOpen;
arrow.innerHTML = isOpen ? _icon('caret-down') : _icon('caret-up');
});
});
// Checkboxes
_container.querySelectorAll('input[data-lskey]').forEach(cb => {
cb.addEventListener('change', () => {
const key = cb.dataset.lskey;
_saveGoal(key, cb.checked);
_updateProgress(cb);
});
});
}
function _updateProgress(cb) {
const goalsEl = cb.closest('.tp-goals');
if (!goalsEl) return;
const total = parseInt(goalsEl.dataset.total, 10) || 0;
const done = goalsEl.querySelectorAll('input[type=checkbox]:checked').length;
const label = goalsEl.querySelector('.tp-progress-label');
const bar = goalsEl.querySelector('.tp-progress-bar');
if (label) label.textContent = `${done} von ${total} erreicht`;
if (bar) bar.style.width = total > 0 ? `${Math.round((done / total) * 100)}%` : '0%';
}
// ----------------------------------------------------------
// MAIN RENDER
// ----------------------------------------------------------
function _render() {
let planContent = '';
if (_activePlan === 'welpe') planContent = _renderWelpe();
else if (_activePlan === 'junior') planContent = _renderJunior();
else planContent = _renderErwachsen();
const dogId = _dogId();
const planLabel = _activePlan === 'welpe' ? 'Welpe 0–6 Monate'
: _activePlan === 'junior' ? 'Junior 6–18 Monate'
: `Erwachsener Hund – ${_activeAdultTab}`;
_container.innerHTML = `
${UI.dogChip(_appState)}
${_icon('clipboard-text')} Trainingspläne
${dogId ? `
Notiz
` : ''}
${_renderPlanSelector()}
${planContent}
${_icon('calendar')} Trainingskalender
`;
_bindEvents();
_loadCalendar();
}
// ----------------------------------------------------------
// TRAININGSKALENDER
// ----------------------------------------------------------
async function _loadCalendar() {
const dogId = _dogId();
const wrap = _container.querySelector('#tp-calendar-wrap');
if (!wrap) return;
if (!dogId) {
wrap.innerHTML = `Kein Hund ausgewählt.
`;
return;
}
const now = new Date();
const thisYear = now.getFullYear();
const thisMon = now.getMonth() + 1; // 1-based
// Letzter Monat
const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const prevYear = prevDate.getFullYear();
const prevMon = prevDate.getMonth() + 1;
const [dataCurr, dataPrev] = await Promise.all([
_apiGet(`/api/training/calendar?dog_id=${dogId}&year=${thisYear}&month=${thisMon}`),
_apiGet(`/api/training/calendar?dog_id=${dogId}&year=${prevYear}&month=${prevMon}`),
]);
wrap.innerHTML = `
${_renderCalendarMonth(prevYear, prevMon, dataPrev?.days || {})}
${_renderCalendarMonth(thisYear, thisMon, dataCurr?.days || {})}
kein Training
Training
Top-Training
`;
}
function _renderCalendarMonth(year, month, days) {
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
'Juli','August','September','Oktober','November','Dezember'];
const WDAYS = ['Mo','Di','Mi','Do','Fr','Sa','So'];
const firstDay = new Date(year, month - 1, 1);
// Monday = 0 offset: getDay() returns 0=Sun, so we shift
let startOffset = firstDay.getDay(); // 0=Sun
startOffset = startOffset === 0 ? 6 : startOffset - 1; // Mon=0 … Sun=6
const daysInMonth = new Date(year, month, 0).getDate();
// Header Wochentage
const wdayHeader = WDAYS.map(d =>
`${d}
`
).join('');
// Leere Slots am Anfang
let cells = '';
for (let i = 0; i < startOffset; i++) {
cells += `
`;
}
const today = new Date();
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = `${year}-${String(month).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const dayData = days[dateStr];
const isToday = dateStr === todayStr;
let bg = 'transparent';
let border = '1.5px solid var(--c-border)';
let color = 'var(--c-text-secondary)';
let title = '';
if (dayData) {
if (dayData.top) {
bg = 'var(--c-primary)';
border = '1.5px solid var(--c-primary)';
color = '#fff';
title = `Top-Training! ${dayData.count} Einheit${dayData.count !== 1 ? 'en' : ''}`;
} else {
bg = 'var(--c-primary-subtle)';
border = '1.5px solid var(--c-primary)';
color = 'var(--c-primary)';
title = `${dayData.count} Einheit${dayData.count !== 1 ? 'en' : ''}`;
}
}
const todayRing = isToday ? 'box-shadow:0 0 0 2px var(--c-primary);' : '';
cells += `
${d}
`;
}
return `
${_esc(MONTHS[month - 1])} ${year}
${wdayHeader}
${cells}
`;
}
// ----------------------------------------------------------
// NOTIZ-MODAL
// ----------------------------------------------------------
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
// Vorhandenes Modal entfernen falls noch offen
document.getElementById('by-note-modal')?.remove();
const overlay = document.createElement('div');
overlay.id = 'by-note-modal';
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
overlay.innerHTML = `
Notiz
${_esc(parentLabel)}
×
Abbrechen
Speichern
`;
document.body.appendChild(overlay);
const textarea = document.getElementById('by-note-text');
const saveBtn = document.getElementById('by-note-save');
const cancelBtn = document.getElementById('by-note-cancel');
const closeBtn = document.getElementById('by-note-close');
let existingNoteId = null;
try {
const existing = await API.notes.get(parentType, String(parentId));
if (existing?.id) {
existingNoteId = existing.id;
textarea.value = existing.text || '';
}
} catch (_) { /* keine Notiz vorhanden — ok */ }
setTimeout(() => textarea.focus(), 100);
const _close = () => overlay.remove();
closeBtn.addEventListener('click', _close);
cancelBtn.addEventListener('click', _close);
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
document.getElementById('by-note-form').addEventListener('submit', async e => {
e.preventDefault();
const text = textarea.value.trim();
UI.setLoading(saveBtn, true);
try {
const payload = { text, parent_label: parentLabel, location_name: locationName };
if (existingNoteId) {
await API.notes.update(existingNoteId, payload);
} else {
await API.notes.create(parentType, String(parentId), payload);
}
UI.toast.success('Notiz gespeichert.');
_close();
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Speichern.');
UI.setLoading(saveBtn, false);
}
});
}
// ----------------------------------------------------------
// PUBLIC API
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
_render();
}
function refresh() {}
function onDogChange() {
_render();
}
return { init, refresh, onDogChange };
})();