Karte: - Frankfurt-Fallback (Zoom 10→14 flyTo) mit _frankfurtTimer-Cancel wenn echter Standort eintrifft - OSM-Tile-Fetch parallelisiert (asyncio.Semaphore(3)) - Bounds-Fix: invalidateSize() + pad(0.15) vor getBounds() - map-pin-slash Icon für gesperrten Standort - Scan-Done-Flash: Statusbar-Pill grün bei 100% - Schnüffelhund: outer div (by-wander X) + inner SVG (by-sniff Y) für natürlichere zweiachsige Bewegung Backend: - City-Prewarm-Job: ~70 deutsche Großstädte beim Start (+90s) und wöchentlich (So 01:00), Fortschritts-Mails alle 5h an ADMIN_EMAIL - ADMIN_EMAIL Env-Var in .env.example dokumentiert Bugfixes: - Profil-Edit: /api/profile → /profile (doppelter Prefix) - Friends: Mobile-Portrait-Layout (flex-wrap, overflow-x:hidden) - Trainingspläne: Pills text-wrap (flex + white-space:normal)
612 lines
29 KiB
JavaScript
612 lines
29 KiB
JavaScript
/* ============================================================
|
||
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
|
||
|
||
// ----------------------------------------------------------
|
||
// HELPER
|
||
// ----------------------------------------------------------
|
||
function _esc(str) {
|
||
if (!str) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function _icon(name) {
|
||
return `<svg class="ph-icon" aria-hidden="true" style="width:1em;height:1em;vertical-align:-0.15em"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
|
||
}
|
||
|
||
function _lsKey(planId, goalIdx) {
|
||
return `tp_${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 => `<th style="padding:6px 10px;background:var(--c-surface-2);font-weight:var(--weight-semibold);font-size:var(--text-sm);white-space:nowrap">${_esc(h)}</th>`).join('');
|
||
const trs = rows.map(row => {
|
||
const tds = row.map((cell, i) => `<td style="padding:6px 10px;font-size:var(--text-sm);color:var(--c-text);border-top:1px solid var(--c-border);${i === 0 ? 'white-space:nowrap;font-weight:var(--weight-semibold)' : ''}">${_esc(cell)}</td>`).join('');
|
||
return `<tr>${tds}</tr>`;
|
||
}).join('');
|
||
return `
|
||
<div style="overflow-x:auto;margin:var(--space-3) 0">
|
||
<table style="width:100%;border-collapse:collapse;min-width:400px">
|
||
<thead><tr>${ths}</tr></thead>
|
||
<tbody>${trs}</tbody>
|
||
</table>
|
||
</div>`;
|
||
}
|
||
|
||
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 `
|
||
<label style="display:flex;align-items:flex-start;gap:var(--space-2);cursor:pointer;padding:var(--space-1) 0" class="tp-goal-label">
|
||
<input type="checkbox" data-lskey="${_esc(key)}" ${checked ? 'checked' : ''}
|
||
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">${_esc(goal)}</span>
|
||
</label>`;
|
||
}).join('');
|
||
|
||
const progress = total > 0 ? Math.round((doneCount / total) * 100) : 0;
|
||
return `
|
||
<div class="tp-goals" data-plan="${_esc(planId)}" data-total="${total}">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-2)">
|
||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text-secondary)">
|
||
${_icon('check-circle')} Lernziele
|
||
</span>
|
||
<span class="tp-progress-label" style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
${doneCount} von ${total} erreicht
|
||
</span>
|
||
</div>
|
||
<div style="background:var(--c-surface-2);border-radius:4px;height:6px;overflow:hidden;margin-bottom:var(--space-3)">
|
||
<div class="tp-progress-bar" style="width:${progress}%;height:6px;background:var(--c-primary);border-radius:4px;transition:width 0.3s"></div>
|
||
</div>
|
||
${items}
|
||
</div>`;
|
||
}
|
||
|
||
function _renderAccordionPhase(id, title, content) {
|
||
return `
|
||
<div class="tp-acc" id="tp-acc-${_esc(id)}">
|
||
<button class="tp-acc-head" data-acc="${_esc(id)}"
|
||
style="width:100%;display:flex;justify-content:space-between;align-items:center;padding:var(--space-3) var(--space-4);background:none;border:none;border-top:1px solid var(--c-border);cursor:pointer;text-align:left">
|
||
<span style="font-weight:var(--weight-semibold);color:var(--c-text)">${title}</span>
|
||
<span class="tp-acc-arrow">${_icon('caret-down')}</span>
|
||
</button>
|
||
<div class="tp-acc-body" id="tp-acc-body-${_esc(id)}" hidden
|
||
style="padding:var(--space-3) var(--space-4) var(--space-4)">
|
||
${content}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function _renderHintBox(text) {
|
||
return `
|
||
<div style="background:var(--c-surface-2);border-left:3px solid var(--c-primary);border-radius:var(--radius-sm);padding:var(--space-3) var(--space-4);margin:var(--space-3) 0;font-size:var(--text-sm);color:var(--c-text);line-height:1.6">
|
||
${_icon('info')} ${_esc(text)}
|
||
</div>`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PLAN SELECTOR
|
||
// ----------------------------------------------------------
|
||
function _renderPlanSelector() {
|
||
const plans = [
|
||
{ id: 'welpe', label: '🐶 Welpe', sub: '0–6 Monate' },
|
||
{ id: 'junior', label: '🐕 Junior', sub: '6–18 Monate' },
|
||
{ id: 'erwachsen',label: '🦮 Erwachsener Hund', sub: 'Grund- & Aufbaukurs' },
|
||
];
|
||
const btns = plans.map(p => `
|
||
<button class="by-tab${_activePlan === p.id ? ' active' : ''}" data-plan="${p.id}"
|
||
style="flex:1;min-width:90px;display:flex;flex-direction:column;align-items:center;
|
||
justify-content:center;gap:2px;white-space:normal;text-align:center;line-height:1.2">
|
||
<span style="font-size:1.1rem">${p.label}</span>
|
||
<span style="font-size:var(--text-xs);opacity:0.8">${_esc(p.sub)}</span>
|
||
</button>`).join('');
|
||
return `<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4);flex-wrap:wrap">${btns}</div>`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// WELPENPLAN
|
||
// ----------------------------------------------------------
|
||
function _renderWelpe() {
|
||
const intro = `
|
||
<p style="color:var(--c-text);line-height:1.6;margin-bottom:var(--space-2)">
|
||
<strong>Voraussetzungen:</strong> Welpe ist eingezogen, Grundvertrauen wird aufgebaut.<br>
|
||
<strong>Ziel am Ende:</strong> Sitz, Platz, Hier, Warte, Leine ohne Ziehen, Alleine bleiben bis 1 Stunde, keine Begrüßungssprünge.
|
||
</p>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Name, erste Bindung, Stubenreinheit, keine Übungen erzwingen.</p>
|
||
${_renderTable(
|
||
['Tag', 'Einheit 1', 'Einheit 2', 'Einheit 3'],
|
||
[['Mo–So', 'Name lernen', 'Markerwort einführen', 'Freies Erkunden + Beobachten']]
|
||
)}
|
||
<ul style="font-size:var(--text-sm);color:var(--c-text);line-height:1.6;padding-left:var(--space-4);margin:var(--space-2) 0">
|
||
<li><strong>Name lernen:</strong> Namen sagen → Hund schaut → sofort Markerwort + Leckerli. 10–15× täglich.</li>
|
||
<li><strong>Markerwort einführen:</strong> Markerwort sagen → sofort Leckerli (10× wiederholen). Ab jetzt: Markerwort immer vor dem Leckerli.</li>
|
||
<li><strong>Stubenreinheit:</strong> Alle 1–2 Stunden raus, nach Schlafen/Fressen/Spielen. Kein Schimpfen bei Unfällen.</li>
|
||
</ul>`;
|
||
|
||
const w34 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Sitz, Warte (Futter), Nicht springen.</p>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Platz, Leinengewöhnung, Hier auf kurze Distanz.</p>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Bleib (Dauer), Alleine bleiben aufbauen, Fuß vorbereiten.</p>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Alle Kommandos in Alltagssituationen, erste leichte Ablenkungen.</p>
|
||
${_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}
|
||
<div class="card" style="padding:0;overflow:hidden">
|
||
${_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)}
|
||
</div>`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// JUNIORPLAN
|
||
// ----------------------------------------------------------
|
||
function _renderJunior() {
|
||
const intro = `
|
||
<p style="color:var(--c-text);line-height:1.6;margin-bottom:var(--space-2)">
|
||
<strong>Voraussetzungen:</strong> Grundkommandos bekannt, aber pubertätsbedingt unzuverlässig.<br>
|
||
<strong>Ziel am Ende:</strong> Alle Grundkommandos auch bei Ablenkung, zuverlässiger Rückruf, Leinenführigkeit in der Stadt.
|
||
</p>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Alle bekannten Kommandos mit höherer Ablenkung neu festigen.</p>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Zuverlässiger Rückruf — das wichtigste Kommando in dieser Phase.</p>
|
||
<ol style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7;padding-left:var(--space-5);margin:var(--space-2) 0">
|
||
<li><strong>Rückruf mit Schleppleine (10 Meter):</strong> Hund beschäftigt sich → "Hier!" → leichtes Anziehen wenn nötig → große Belohnung beim Ankommen</li>
|
||
<li><strong>Versteckspiel:</strong> Im Wald/Park verstecken → "Hier!" rufen → Hund sucht und findet → größte Belohnung</li>
|
||
<li><strong>Rückruf aus der Gruppe:</strong> Hund spielt mit anderen Hunden → "Hier!" → kommt er: Jackpot (5 Leckerlis + Streicheln)</li>
|
||
<li><strong>Notfallrückruf einführen:</strong> Anderes Wort (z.B. "Rapid!") nur für echte Notfälle — immer mit höchster Belohnung</li>
|
||
</ol>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Ruhiges Gehen auch bei Fahrrädern, anderen Hunden, Kinderwagen.</p>
|
||
<ol style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7;padding-left:var(--space-5);margin:var(--space-2) 0">
|
||
<li>Ruhige Straße → belohnen alle 10 Schritte bei locker hängender Leine</li>
|
||
<li>Fahrrad kommt entgegen → Sitz → warten → Fahrrad vorbei → weitergehen + Leckerli</li>
|
||
<li>Anderer Hund in Sichtweite → Fokus auf dich (Augenkontakt trainieren) → Leckerli</li>
|
||
<li>Belebte Fußgängerzone: erst beobachten lassen, dann durch</li>
|
||
</ol>
|
||
<p style="font-size:var(--text-sm);color:var(--c-text);margin:var(--space-2) 0"><strong>Augenkontakt:</strong> "Schau" sagen → Hund schaut in deine Augen → sofort Markerwort + Leckerli. In Ablenkungssituationen: "Schau" → fokussiert → dann erst weitergehen.</p>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)"><strong>Fokus:</strong> Festigung aller Kommandos, erste Aufbauübungen, Tricks für mentale Auslastung.</p>
|
||
${_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}
|
||
<div class="card" style="padding:0;overflow:hidden">
|
||
${_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)}
|
||
</div>`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// ERWACHSENER HUND
|
||
// ----------------------------------------------------------
|
||
function _renderErwachsenTabs() {
|
||
return `
|
||
<div class="by-tabs" style="margin-bottom:var(--space-4)">
|
||
<button class="by-tab${_activeAdultTab === 'grundkurs' ? ' active' : ''}" data-tab="grundkurs">Grundkurs</button>
|
||
<button class="by-tab${_activeAdultTab === 'aufbaukurs' ? ' active' : ''}" data-tab="aufbaukurs">Aufbaukurs</button>
|
||
<button class="by-tab${_activeAdultTab === 'uebersicht' ? ' active' : ''}" data-tab="uebersicht">Übersicht</button>
|
||
</div>`;
|
||
}
|
||
|
||
function _renderGrundkurs() {
|
||
const intro = `
|
||
<p style="color:var(--c-text);line-height:1.6;margin-bottom:var(--space-2)">
|
||
<strong>Voraussetzungen:</strong> Keine oder wenige Vorkenntnisse.<br>
|
||
<strong>Ziel:</strong> Alle Grundkommandos in 8 Wochen.
|
||
</p>
|
||
${_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 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text);line-height:1.6;margin-bottom:var(--space-2)">Alle Kommandos in Alltagssituationen:</p>
|
||
<ul style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7;padding-left:var(--space-4);margin:0 0 var(--space-3)">
|
||
<li>Sitz vor dem Überqueren der Straße</li>
|
||
<li>Platz wenn Besuch kommt</li>
|
||
<li>Warte vor der Haustür</li>
|
||
<li>Hier beim Freilauf im Garten</li>
|
||
<li>Fuß auf dem Bürgersteig</li>
|
||
<li>Aus bei gefundenem Gegenstand</li>
|
||
</ul>
|
||
${_renderGoals('erwachsen_gk', [
|
||
'Sitz, Platz, Bleib (30 Sek), Hier, Fuß, Aus, Warte',
|
||
'Alle Kommandos im Alltag einsetzbar',
|
||
'Leine ohne starkes Ziehen',
|
||
])}`;
|
||
|
||
return `
|
||
${intro}
|
||
<div class="card" style="padding:0;overflow:hidden">
|
||
${_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)}
|
||
</div>`;
|
||
}
|
||
|
||
function _renderAufbaukurs() {
|
||
const intro = `
|
||
<p style="color:var(--c-text);line-height:1.6;margin-bottom:var(--space-2)">
|
||
<strong>Voraussetzungen:</strong> Grundkurs abgeschlossen oder Kommandos bekannt.<br>
|
||
<strong>Dauer:</strong> 8 Wochen | <strong>Ziel:</strong> Kommandos bei Ablenkung, Distanzkommandos, Tricks, mentale Auslastung.
|
||
</p>`;
|
||
|
||
const aw12 = `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text);margin-bottom:var(--space-2)">Alle bekannten Kommandos werden mit steigender Ablenkung trainiert. Immer bei der niedrigsten Stufe anfangen die noch klappt.</p>
|
||
${_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 = `
|
||
<ul style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7;padding-left:var(--space-4);margin:0 0 var(--space-3)">
|
||
<li>Sitz aus 5 Metern → 10 Metern</li>
|
||
<li>Platz aus 5 Metern → 10 Metern</li>
|
||
<li>Bleib mit 10 Metern Distanz</li>
|
||
<li>Hier vom Freilauf (mit Schleppleine absichern)</li>
|
||
</ul>
|
||
<p style="font-size:var(--text-sm);color:var(--c-text);line-height:1.6"><strong>Übung "Fernbedienung":</strong> Hund steht 5 Meter entfernt → "Sitz" → kurz warten → "Platz" → kurz warten → "Sitz" → "Hier". Wirkt beeindruckend, festigt Kommandos enorm.</p>`;
|
||
|
||
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 = `
|
||
<ul style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7;padding-left:var(--space-4);margin:0 0 var(--space-3)">
|
||
<li>Schwächstes Kommando intensiv üben</li>
|
||
<li>Neue Umgebungen aufsuchen (anderer Wald, andere Stadt)</li>
|
||
<li>Hundesport ausprobieren: Agility, Mantrailing, Obedience, Trickdogging</li>
|
||
</ul>
|
||
${_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}
|
||
<div class="card" style="padding:0;overflow:hidden">
|
||
${_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)}
|
||
</div>`;
|
||
}
|
||
|
||
function _renderUebersicht() {
|
||
return `
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);margin-bottom:var(--space-3);color:var(--c-text)">
|
||
${_icon('clipboard-text')} Welcher Plan für welchen Hund?
|
||
</h3>
|
||
${_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 = `
|
||
<p style="color:var(--c-text);line-height:1.6;margin-bottom:var(--space-3)">
|
||
Wähle deinen Kurs oder sieh dir die Schnellübersicht an.
|
||
</p>`;
|
||
|
||
let content = '';
|
||
if (_activeAdultTab === 'grundkurs') {
|
||
content = _renderGrundkurs();
|
||
} else if (_activeAdultTab === 'aufbaukurs') {
|
||
content = _renderAufbaukurs();
|
||
} else {
|
||
content = `<div class="card">${_renderUebersicht()}</div>`;
|
||
}
|
||
|
||
return `${intro}${_renderErwachsenTabs()}${content}`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// BIND EVENTS
|
||
// ----------------------------------------------------------
|
||
function _bindEvents() {
|
||
// 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();
|
||
|
||
_container.innerHTML = `
|
||
<div style="padding-bottom:var(--space-8)">
|
||
<h2 style="font-size:var(--text-lg);font-weight:700;margin:var(--space-4) 0 var(--space-4)">
|
||
${_icon('clipboard-text')} Trainingspläne
|
||
</h2>
|
||
${_renderPlanSelector()}
|
||
${planContent}
|
||
</div>`;
|
||
|
||
_bindEvents();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PUBLIC API
|
||
// ----------------------------------------------------------
|
||
async function init(container, appState) {
|
||
_container = container;
|
||
_appState = appState;
|
||
_render();
|
||
}
|
||
|
||
function refresh() {}
|
||
function onDogChange() {}
|
||
|
||
return { init, refresh, onDogChange };
|
||
|
||
})();
|