PHASE 1 — Sofort-Cleanup ohne Risiko: - Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen: * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3 * flex-between, flex-1-min, mb-1/3, mt-1/3 * icon-xs/sm/md/lg, label-block, caption - index.html bindet utilities.css ein - mb-3/mt-3 ergänzt (waren in design-system.css unvollständig) PHASE 2 — .by-tab Modifier für Vereinheitlichung: - .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.) - .by-tabs.sticky (Desktop vertikale Tabs für Admin) - .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll) - .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border) PHASE 3 — Inline-Style → Klassen-Migration (Python-Script): - 948 Inline-Styles entfernt (5101 → 4153, -18%) - 962 Migrationen über 47 Page-Dateien - Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67), litters.js (62), settings.js (61), zuchthunde.js (51) - Patterns: text-muted, text-secondary, text-danger, text-xs-muted, text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3, p-3/4, mb-2/3/4, hidden, w-full, flex-1, ... - Bewahrt bestehende class-Attribute (mergt korrekt) Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
875 lines
40 KiB
JavaScript
875 lines
40 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
|
||
|
||
// ----------------------------------------------------------
|
||
// 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, '>')
|
||
.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) {
|
||
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 => `<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 text-xs-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', 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 => `
|
||
<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">
|
||
<svg class="ph-icon" aria-hidden="true" style="width:22px;height:22px"><use href="/icons/phosphor.svg#${p.icon}"></use></svg>
|
||
<span style="font-size:var(--text-sm);font-weight:600">${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 mb-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() {
|
||
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 = `
|
||
<div style="padding:var(--space-4) var(--space-4) var(--space-8)">
|
||
${UI.dogChip(_appState)}
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-4)">
|
||
<h2 style="font-size:var(--text-lg);font-weight:700;margin:0">
|
||
${_icon('clipboard-text')} Trainingspläne
|
||
</h2>
|
||
${dogId ? `<button class="btn btn-ghost btn-xs" id="tp-note-btn" title="Notiz zum Plan">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz
|
||
</button>` : ''}
|
||
</div>
|
||
${_renderPlanSelector()}
|
||
${planContent}
|
||
<!-- Trainingskalender -->
|
||
<div id="tp-calendar-section" style="margin-top:var(--space-6)">
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin:0 0 var(--space-3)">
|
||
${_icon('calendar')} Trainingskalender
|
||
</h3>
|
||
<div id="tp-calendar-wrap" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||
<div style="color:var(--c-text-secondary);font-size:var(--text-sm);text-align:center;padding:var(--space-4)">
|
||
Lade Kalender…
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
_bindEvents();
|
||
_loadCalendar();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TRAININGSKALENDER
|
||
// ----------------------------------------------------------
|
||
async function _loadCalendar() {
|
||
const dogId = _dogId();
|
||
const wrap = _container.querySelector('#tp-calendar-wrap');
|
||
if (!wrap) return;
|
||
|
||
if (!dogId) {
|
||
wrap.innerHTML = `<p style="font-size:var(--text-sm);color:var(--c-text-secondary);text-align:center;padding:var(--space-3)">Kein Hund ausgewählt.</p>`;
|
||
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 = `
|
||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-4)">
|
||
${_renderCalendarMonth(prevYear, prevMon, dataPrev?.days || {})}
|
||
${_renderCalendarMonth(thisYear, thisMon, dataCurr?.days || {})}
|
||
</div>
|
||
<!-- Legende -->
|
||
<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-top:var(--space-2);
|
||
font-size:var(--text-xs);color:var(--c-text-secondary);align-items:center">
|
||
<span style="display:flex;align-items:center;gap:5px">
|
||
<span style="width:14px;height:14px;border-radius:3px;border:1.5px solid var(--c-border);display:inline-block"></span>
|
||
kein Training
|
||
</span>
|
||
<span style="display:flex;align-items:center;gap:5px">
|
||
<span style="width:14px;height:14px;border-radius:3px;background:var(--c-primary-subtle);border:1.5px solid var(--c-primary);display:inline-block"></span>
|
||
Training
|
||
</span>
|
||
<span style="display:flex;align-items:center;gap:5px">
|
||
<span style="width:14px;height:14px;border-radius:3px;background:var(--c-primary);display:inline-block"></span>
|
||
Top-Training
|
||
</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 =>
|
||
`<div style="text-align:center;font-size:10px;font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);padding-bottom:4px">${d}</div>`
|
||
).join('');
|
||
|
||
// Leere Slots am Anfang
|
||
let cells = '';
|
||
for (let i = 0; i < startOffset; i++) {
|
||
cells += `<div></div>`;
|
||
}
|
||
|
||
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 += `
|
||
<div title="${title}"
|
||
style="aspect-ratio:1;border-radius:4px;background:${bg};border:${border};
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:10px;font-weight:${dayData ? 'var(--weight-semibold)' : '400'};
|
||
color:${color};${todayRing}cursor:default">
|
||
${d}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return `
|
||
<div style="flex:1;min-width:220px">
|
||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">
|
||
${_esc(MONTHS[month - 1])} ${year}
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:3px">
|
||
${wdayHeader}
|
||
${cells}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 = `
|
||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||
<div>
|
||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${_esc(parentLabel)}</div>
|
||
</div>
|
||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||
</div>
|
||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||
<form id="by-note-form">
|
||
<textarea id="by-note-text" class="form-control" rows="5"
|
||
placeholder="Notiz eingeben…"
|
||
style="width:100%;resize:vertical"></textarea>
|
||
</form>
|
||
</div>
|
||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 };
|
||
|
||
})();
|