Feature: Hilfe/FAQ, Übungen-Content, Navigation-Fixes (SW by-v727)
Hilfe & FAQ:
- Neue Seite /hilfe mit Akkordeon + Live-Suche (6 Kategorien, 25 Artikel)
- DB-Tabelle help_articles — Inhalte admin-seitig ohne Deploy änderbar
- Admin-Tab Hilfe/FAQ zum Bearbeiten aller Artikel
- Link in Einstellungen (unter Welten einrichten, über Abmelden)
- routes/help.py: GET (public), POST/PATCH/DELETE (Admin)
Übungen:
- 110 Übungen: beschreibung (kurz), schritte (JSON 4-6 Schritte), tipp — gutes Deutsch mit Umlauten
- Admin-Tab Übungen: Inline-Editor für alle drei Felder
- PUT /training/exercises/{id} (Admin) neu
- Übung-des-Tages Chip → scrollt jetzt korrekt zur Übung (exercise_id-Feldname-Fix)
Welten-Navigation:
- hide() stellt app-header + bottom-nav wieder her (worlds-hidden wurde nie entfernt)
- init() mit _setupDone-Guard (keine doppelten Event-Listener)
- Login ruft Worlds.init(_appState) statt show() — _state war null → falscher Render
- X-Button in Welten-Konfiguration: 30×30px, Icon 17px, besser sichtbar
Wetter:
- Motivation bei blockiertem Standort: 6-Schritte-iOS-Anleitung + Flugmodus-Tipp
- Auto-locate bleibt (kein Button-Only mehr)
achievements.py:
- my_achievements(): d.user_id → JOIN dogs (zweite Funktion war noch kaputt)
This commit is contained in:
parent
55069d246b
commit
05ecf3b94a
13 changed files with 1158 additions and 43 deletions
|
|
@ -23,6 +23,8 @@ window.Page_admin = (() => {
|
|||
{ id: 'partner', label: 'Partner', icon: 'handshake' },
|
||||
{ id: 'outreach', label: 'Outreach', icon: 'envelope-simple' },
|
||||
{ id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' },
|
||||
{ id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' },
|
||||
{ id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' },
|
||||
];
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -157,6 +159,8 @@ window.Page_admin = (() => {
|
|||
case 'outreach': await _renderOutreach(el); break;
|
||||
case 'audit': await _renderAudit(el); break;
|
||||
case 'bewerbungen': await _renderBewerbungen(el); break;
|
||||
case 'hilfe': await _renderHilfe(el); break;
|
||||
case 'uebungen_admin': await _renderUebungenAdmin(el); break;
|
||||
}
|
||||
} catch (e) {
|
||||
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
||||
|
|
@ -2809,6 +2813,469 @@ window.Page_admin = (() => {
|
|||
await _load();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB: HILFE / FAQ
|
||||
async function _renderHilfe(el) {
|
||||
const KAT_LABEL = {
|
||||
installation: 'Installation & PWA',
|
||||
erste_schritte: 'Erste Schritte',
|
||||
standort: 'Standort & Wetter',
|
||||
account: 'Account & Passwort',
|
||||
features: 'Features erklärt',
|
||||
probleme: 'Technische Probleme',
|
||||
};
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="padding:var(--space-4)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;
|
||||
margin-bottom:var(--space-4)">
|
||||
<h2 style="margin:0;font-size:var(--text-lg)">Hilfe / FAQ</h2>
|
||||
<button class="btn btn-primary btn-sm" id="adm-hilfe-neu">
|
||||
${UI.icon('plus')} Neuer Artikel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Neuer-Artikel-Formular (versteckt) -->
|
||||
<div id="adm-hilfe-form" style="display:none;background:var(--c-surface-2);
|
||||
border:1px solid var(--c-border);border-radius:var(--radius-lg);
|
||||
padding:var(--space-4);margin-bottom:var(--space-4)">
|
||||
<h3 style="margin:0 0 var(--space-3);font-size:var(--text-base)">Neuer Artikel</h3>
|
||||
<div style="display:grid;gap:var(--space-3)">
|
||||
<div>
|
||||
<label style="font-size:var(--text-sm);font-weight:500;display:block;
|
||||
margin-bottom:var(--space-1)">Kategorie</label>
|
||||
<select id="adm-hilfe-kat" style="width:100%;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)">
|
||||
${Object.entries(KAT_LABEL).map(([k,v]) =>
|
||||
`<option value="${k}">${_esc(v)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:var(--text-sm);font-weight:500;display:block;
|
||||
margin-bottom:var(--space-1)">Frage</label>
|
||||
<input id="adm-hilfe-frage" type="text" placeholder="Frage eingeben…"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);color:var(--c-text);
|
||||
font-size:var(--text-sm);box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:var(--text-sm);font-weight:500;display:block;
|
||||
margin-bottom:var(--space-1)">Antwort</label>
|
||||
<textarea id="adm-hilfe-antwort" rows="4" placeholder="Antwort eingeben…"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);color:var(--c-text);
|
||||
font-size:var(--text-sm);box-sizing:border-box;
|
||||
resize:vertical;font-family:inherit"></textarea>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
<label style="font-size:var(--text-sm);font-weight:500;margin-right:var(--space-2)">
|
||||
Reihenfolge
|
||||
</label>
|
||||
<input id="adm-hilfe-sort" type="number" value="0" min="0"
|
||||
style="width:80px;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)">
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-2);justify-content:flex-end">
|
||||
<button class="btn btn-secondary btn-sm" id="adm-hilfe-form-cancel">Abbrechen</button>
|
||||
<button class="btn btn-primary btn-sm" id="adm-hilfe-form-save">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Artikel-Liste -->
|
||||
<div id="adm-hilfe-list">
|
||||
<div style="text-align:center;padding:var(--space-6);color:var(--c-text-muted)">
|
||||
Lade…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function _load() {
|
||||
const listEl = el.querySelector('#adm-hilfe-list');
|
||||
try {
|
||||
const articles = await API.get('/help?all=1');
|
||||
if (!articles.length) {
|
||||
listEl.innerHTML = _emptyState('question', 'Noch keine FAQ-Artikel', '');
|
||||
return;
|
||||
}
|
||||
|
||||
// Gruppieren nach Kategorie
|
||||
const grouped = {};
|
||||
for (const a of articles) {
|
||||
if (!grouped[a.kategorie]) grouped[a.kategorie] = [];
|
||||
grouped[a.kategorie].push(a);
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const [kat, items] of Object.entries(grouped)) {
|
||||
const label = KAT_LABEL[kat] || kat;
|
||||
html += `
|
||||
<div style="margin-bottom:var(--space-5)">
|
||||
<div style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary);
|
||||
text-transform:uppercase;letter-spacing:0.05em;
|
||||
padding:var(--space-2) 0;margin-bottom:var(--space-2);
|
||||
border-bottom:1px solid var(--c-border)">
|
||||
${_esc(label)}
|
||||
</div>
|
||||
`;
|
||||
for (const a of items) {
|
||||
html += `
|
||||
<div class="adm-hilfe-row" data-id="${a.id}"
|
||||
style="border:1px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);margin-bottom:var(--space-2)">
|
||||
<!-- Zusammenfassung -->
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);
|
||||
padding:var(--space-3) var(--space-4)">
|
||||
<span style="flex:1;font-size:var(--text-sm);font-weight:500;
|
||||
${a.aktiv ? '' : 'opacity:0.45;text-decoration:line-through'}">
|
||||
${_esc(a.frage)}
|
||||
</span>
|
||||
<span style="font-size:var(--text-xs);color:var(--c-text-muted);
|
||||
white-space:nowrap">
|
||||
#${a.sort_order}
|
||||
</span>
|
||||
<button class="btn btn-sm adm-hilfe-edit-btn"
|
||||
style="padding:2px 8px;font-size:var(--text-xs)"
|
||||
data-id="${a.id}">
|
||||
${UI.icon('pencil-simple')} Bearbeiten
|
||||
</button>
|
||||
<button class="btn btn-sm adm-hilfe-toggle-btn"
|
||||
style="padding:2px 8px;font-size:var(--text-xs);
|
||||
background:${a.aktiv ? 'var(--c-warning-bg,#fef3c7)' : 'var(--c-success-bg,#d1fae5)'};
|
||||
color:${a.aktiv ? 'var(--c-warning,#92400e)' : 'var(--c-success,#065f46)'}"
|
||||
data-id="${a.id}" data-aktiv="${a.aktiv}">
|
||||
${a.aktiv ? UI.icon('eye-slash') + ' Ausblenden' : UI.icon('eye') + ' Einblenden'}
|
||||
</button>
|
||||
<button class="btn btn-sm adm-hilfe-del-btn"
|
||||
style="padding:2px 8px;font-size:var(--text-xs);
|
||||
background:var(--c-danger-bg,#fee2e2);color:var(--c-danger,#991b1b)"
|
||||
data-id="${a.id}" data-frage="${_esc(a.frage)}">
|
||||
${UI.icon('trash')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Edit-Formular (versteckt) -->
|
||||
<div class="adm-hilfe-edit-form" data-id="${a.id}"
|
||||
style="display:none;padding:0 var(--space-4) var(--space-4);
|
||||
border-top:1px solid var(--c-border)">
|
||||
<div style="display:grid;gap:var(--space-3);padding-top:var(--space-3)">
|
||||
<div>
|
||||
<label style="font-size:var(--text-xs);font-weight:600;display:block;
|
||||
margin-bottom:4px;color:var(--c-text-secondary)">Kategorie</label>
|
||||
<select class="adm-hilfe-edit-kat"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)">
|
||||
${Object.entries(KAT_LABEL).map(([k,v]) =>
|
||||
`<option value="${k}" ${k === a.kategorie ? 'selected' : ''}>${_esc(v)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:var(--text-xs);font-weight:600;display:block;
|
||||
margin-bottom:4px;color:var(--c-text-secondary)">Frage</label>
|
||||
<input type="text" class="adm-hilfe-edit-frage"
|
||||
value="${_esc(a.frage)}"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);color:var(--c-text);
|
||||
font-size:var(--text-sm);box-sizing:border-box">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:var(--text-xs);font-weight:600;display:block;
|
||||
margin-bottom:4px;color:var(--c-text-secondary)">Antwort</label>
|
||||
<textarea class="adm-hilfe-edit-antwort" rows="5"
|
||||
style="width:100%;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);color:var(--c-text);
|
||||
font-size:var(--text-sm);box-sizing:border-box;
|
||||
resize:vertical;font-family:inherit">${_esc(a.antwort)}</textarea>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||
<label style="font-size:var(--text-xs);font-weight:600;
|
||||
color:var(--c-text-secondary)">Reihenfolge</label>
|
||||
<input type="number" class="adm-hilfe-edit-sort"
|
||||
value="${a.sort_order}" min="0"
|
||||
style="width:70px;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)">
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn btn-secondary btn-sm adm-hilfe-edit-cancel" data-id="${a.id}"
|
||||
style="font-size:var(--text-xs)">Abbrechen</button>
|
||||
<button class="btn btn-primary btn-sm adm-hilfe-edit-save" data-id="${a.id}"
|
||||
style="font-size:var(--text-xs)">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
listEl.innerHTML = html;
|
||||
_bindListEvents(listEl);
|
||||
} catch (e) {
|
||||
listEl.innerHTML = _emptyState('warning', 'Fehler beim Laden', e.message || '');
|
||||
}
|
||||
}
|
||||
|
||||
function _bindListEvents(listEl) {
|
||||
// Edit-Button: Inline-Formular auf/zu klappen
|
||||
listEl.querySelectorAll('.adm-hilfe-edit-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.dataset.id;
|
||||
const form = listEl.querySelector(`.adm-hilfe-edit-form[data-id="${id}"]`);
|
||||
if (form) form.style.display = form.style.display === 'none' ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Edit-Cancel
|
||||
listEl.querySelectorAll('.adm-hilfe-edit-cancel').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.dataset.id;
|
||||
const form = listEl.querySelector(`.adm-hilfe-edit-form[data-id="${id}"]`);
|
||||
if (form) form.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Edit-Save
|
||||
listEl.querySelectorAll('.adm-hilfe-edit-save').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.dataset.id;
|
||||
const row = listEl.querySelector(`.adm-hilfe-edit-form[data-id="${id}"]`);
|
||||
const payload = {
|
||||
kategorie: row.querySelector('.adm-hilfe-edit-kat').value,
|
||||
frage: row.querySelector('.adm-hilfe-edit-frage').value.trim(),
|
||||
antwort: row.querySelector('.adm-hilfe-edit-antwort').value.trim(),
|
||||
sort_order: parseInt(row.querySelector('.adm-hilfe-edit-sort').value, 10) || 0,
|
||||
};
|
||||
if (!payload.frage || !payload.antwort) {
|
||||
UI.toast.error('Frage und Antwort sind Pflichtfelder.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await API.patch(`/help/${id}`, payload);
|
||||
UI.toast.success('Artikel gespeichert.');
|
||||
_load();
|
||||
} catch (e) { UI.toast.error(e.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle aktiv/inaktiv
|
||||
listEl.querySelectorAll('.adm-hilfe-toggle-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.dataset.id;
|
||||
const aktiv = parseInt(btn.dataset.aktiv, 10);
|
||||
try {
|
||||
await API.patch(`/help/${id}`, { aktiv: aktiv ? 0 : 1 });
|
||||
UI.toast.success(aktiv ? 'Artikel ausgeblendet.' : 'Artikel eingeblendet.');
|
||||
_load();
|
||||
} catch (e) { UI.toast.error(e.message || 'Fehler.'); }
|
||||
});
|
||||
});
|
||||
|
||||
// Delete
|
||||
listEl.querySelectorAll('.adm-hilfe-del-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.dataset.id;
|
||||
const frage = btn.dataset.frage;
|
||||
if (!window.confirm(`Artikel wirklich löschen?\n\n"${frage}"`)) return;
|
||||
try {
|
||||
await API.del(`/help/${id}`);
|
||||
UI.toast.success('Artikel gelöscht.');
|
||||
_load();
|
||||
} catch (e) { UI.toast.error(e.message || 'Fehler beim Löschen.'); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Neuer-Artikel-Button
|
||||
el.querySelector('#adm-hilfe-neu').addEventListener('click', () => {
|
||||
const form = el.querySelector('#adm-hilfe-form');
|
||||
form.style.display = form.style.display === 'none' ? '' : 'none';
|
||||
});
|
||||
|
||||
// Formular abbrechen
|
||||
el.querySelector('#adm-hilfe-form-cancel').addEventListener('click', () => {
|
||||
el.querySelector('#adm-hilfe-form').style.display = 'none';
|
||||
});
|
||||
|
||||
// Formular speichern
|
||||
el.querySelector('#adm-hilfe-form-save').addEventListener('click', async () => {
|
||||
const kat = el.querySelector('#adm-hilfe-kat').value;
|
||||
const frage = el.querySelector('#adm-hilfe-frage').value.trim();
|
||||
const antwort= el.querySelector('#adm-hilfe-antwort').value.trim();
|
||||
const sort = parseInt(el.querySelector('#adm-hilfe-sort').value, 10) || 0;
|
||||
if (!frage || !antwort) {
|
||||
UI.toast.error('Frage und Antwort sind Pflichtfelder.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await API.post('/help', { kategorie: kat, frage, antwort, sort_order: sort });
|
||||
UI.toast.success('Artikel angelegt.');
|
||||
el.querySelector('#adm-hilfe-form').style.display = 'none';
|
||||
el.querySelector('#adm-hilfe-frage').value = '';
|
||||
el.querySelector('#adm-hilfe-antwort').value = '';
|
||||
el.querySelector('#adm-hilfe-sort').value = '0';
|
||||
_load();
|
||||
} catch (e) { UI.toast.error(e.message || 'Fehler beim Anlegen.'); }
|
||||
});
|
||||
|
||||
await _load();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
async function _renderUebungenAdmin(el) {
|
||||
el.innerHTML = `<div style="padding:var(--space-6);text-align:center;color:var(--c-text-muted)">Lade Übungen…</div>`;
|
||||
|
||||
let byTab;
|
||||
try {
|
||||
byTab = await API.get('/training/exercises');
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div style="padding:var(--space-6);color:var(--c-danger)">Fehler: ${e.message}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Flatten to sorted list grouped by kategorie
|
||||
const allExercises = [];
|
||||
for (const [kat, list] of Object.entries(byTab)) {
|
||||
for (const ex of list) allExercises.push({ ...ex, _kat: kat });
|
||||
}
|
||||
allExercises.sort((a, b) => a._kat.localeCompare(b._kat) || a.name.localeCompare(b.name));
|
||||
|
||||
// Group by kategorie
|
||||
const grouped = {};
|
||||
for (const ex of allExercises) {
|
||||
grouped[ex._kat] = grouped[ex._kat] || [];
|
||||
grouped[ex._kat].push(ex);
|
||||
}
|
||||
|
||||
const KAT_LABELS = {
|
||||
'grundkommandos': 'Grundkommandos', 'tricks': 'Tricks',
|
||||
'problemverhalten': 'Problemverhalten', 'mentale-auslastung': 'Mentale Auslastung',
|
||||
'koerperpflege': 'Körperpflege', 'hundesport': 'Hundesport', 'welpe-basics': 'Welpe Basics',
|
||||
};
|
||||
|
||||
let html = `<div style="padding:var(--space-4)">
|
||||
<h2 style="font-size:var(--text-lg);font-weight:700;margin:0 0 var(--space-4)">
|
||||
Trainingsübungen bearbeiten
|
||||
</h2>`;
|
||||
|
||||
for (const [kat, list] of Object.entries(grouped)) {
|
||||
html += `<div style="margin-bottom:var(--space-6)">
|
||||
<div style="font-size:var(--text-xs);font-weight:700;text-transform:uppercase;
|
||||
letter-spacing:.06em;color:var(--c-text-secondary);
|
||||
padding:var(--space-2) 0 var(--space-2);
|
||||
border-bottom:1px solid var(--c-border);margin-bottom:var(--space-2)">
|
||||
${KAT_LABELS[kat] || kat} (${list.length})
|
||||
</div>`;
|
||||
for (const ex of list) {
|
||||
const schritte = Array.isArray(ex.schritte) ? ex.schritte.join('\n') : '';
|
||||
const exId = ex.exercise_id;
|
||||
html += `<div class="adm-ueb-row" data-ex-id="${exId}"
|
||||
style="padding:var(--space-2) 0;border-bottom:1px solid var(--c-border-light)">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||||
<span style="flex:1;font-size:var(--text-sm);font-weight:500">${ex.name}</span>
|
||||
<button class="adm-ueb-edit-btn" data-ex-id="${exId}"
|
||||
style="font-size:var(--text-xs);padding:2px 10px;border-radius:6px;
|
||||
border:1px solid var(--c-border);background:var(--c-surface);
|
||||
cursor:pointer;color:var(--c-text-secondary)">Bearbeiten</button>
|
||||
</div>
|
||||
<div class="adm-ueb-form" data-ex-id="${exId}"
|
||||
style="display:none;margin-top:var(--space-3);
|
||||
background:var(--c-surface-2);border-radius:8px;padding:var(--space-3)">
|
||||
<label style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary)">
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea class="adm-ueb-beschreibung" rows="2"
|
||||
style="width:100%;box-sizing:border-box;margin:4px 0 var(--space-2);
|
||||
font-size:var(--text-sm);padding:6px 8px;border-radius:6px;
|
||||
border:1px solid var(--c-border);background:var(--c-bg);
|
||||
color:var(--c-text);resize:vertical">${(ex.beschreibung || '').replace(/</g, '<')}</textarea>
|
||||
<label style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary)">
|
||||
Schritte (eine Zeile = ein Schritt)
|
||||
</label>
|
||||
<textarea class="adm-ueb-schritte" rows="6"
|
||||
style="width:100%;box-sizing:border-box;margin:4px 0 var(--space-2);
|
||||
font-size:var(--text-sm);padding:6px 8px;border-radius:6px;
|
||||
border:1px solid var(--c-border);background:var(--c-bg);
|
||||
color:var(--c-text);resize:vertical">${schritte.replace(/</g, '<')}</textarea>
|
||||
<label style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary)">
|
||||
Tipp
|
||||
</label>
|
||||
<input class="adm-ueb-tipp" type="text"
|
||||
value="${(ex.tipp || '').replace(/"/g, '"')}"
|
||||
style="width:100%;box-sizing:border-box;margin:4px 0 var(--space-3);
|
||||
font-size:var(--text-sm);padding:6px 8px;border-radius:6px;
|
||||
border:1px solid var(--c-border);background:var(--c-bg);color:var(--c-text)">
|
||||
<div style="display:flex;gap:var(--space-2);justify-content:flex-end">
|
||||
<button class="adm-ueb-cancel-btn" data-ex-id="${exId}"
|
||||
style="font-size:var(--text-xs);padding:4px 14px;border-radius:6px;
|
||||
border:1px solid var(--c-border);background:var(--c-surface);
|
||||
cursor:pointer;color:var(--c-text-secondary)">Abbrechen</button>
|
||||
<button class="adm-ueb-save-btn" data-ex-id="${exId}"
|
||||
style="font-size:var(--text-xs);padding:4px 14px;border-radius:6px;
|
||||
border:none;background:var(--c-primary);
|
||||
cursor:pointer;color:#fff;font-weight:600">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
el.innerHTML = html;
|
||||
|
||||
// Edit toggle
|
||||
el.querySelectorAll('.adm-ueb-edit-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const form = el.querySelector(`.adm-ueb-form[data-ex-id="${btn.dataset.exId}"]`);
|
||||
form.style.display = form.style.display === 'none' ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Cancel
|
||||
el.querySelectorAll('.adm-ueb-cancel-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
el.querySelector(`.adm-ueb-form[data-ex-id="${btn.dataset.exId}"]`).style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Save
|
||||
el.querySelectorAll('.adm-ueb-save-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.dataset.exId;
|
||||
const form = el.querySelector(`.adm-ueb-form[data-ex-id="${id}"]`);
|
||||
const beschreibung = form.querySelector('.adm-ueb-beschreibung').value.trim();
|
||||
const schritte = form.querySelector('.adm-ueb-schritte').value
|
||||
.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
const tipp = form.querySelector('.adm-ueb-tipp').value.trim();
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Speichert…';
|
||||
try {
|
||||
await API.put(`/training/exercises/${id}`, {
|
||||
beschreibung,
|
||||
schritte: JSON.stringify(schritte),
|
||||
tipp,
|
||||
});
|
||||
UI.toast.success('Übung gespeichert.');
|
||||
form.style.display = 'none';
|
||||
} catch (e) {
|
||||
UI.toast.error(e.message || 'Fehler beim Speichern.');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Speichern';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
return { init, refresh, onDogChange };
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue