Feature: Alle 104 Übungen aus DB in Übungsseite — 9 Tabs, DB-basiert, abwärtskompatibel — SW by-v493, APP_VER 470
This commit is contained in:
parent
175984e80f
commit
6d9f4a097e
5 changed files with 109 additions and 24 deletions
|
|
@ -164,7 +164,11 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
|
||||||
day_num = (_dt.date.today() - _dt.date(2024, 1, 1)).days
|
day_num = (_dt.date.today() - _dt.date(2024, 1, 1)).days
|
||||||
|
|
||||||
# Nur exercise_ids im JS-Format (starten mit bekanntem Tab-Namen)
|
# Nur exercise_ids im JS-Format (starten mit bekanntem Tab-Namen)
|
||||||
_KNOWN_PREFIXES = ('grundkommandos_', 'tricks_', 'problemverhalten_', 'grundlagen_')
|
_KNOWN_PREFIXES = (
|
||||||
|
'grundkommandos_', 'tricks_', 'problemverhalten_',
|
||||||
|
'mentale-auslastung_', 'hundesport_', 'koerperpflege_', 'welpe-basics_',
|
||||||
|
'grundlagen_',
|
||||||
|
)
|
||||||
raw_progress = conn.execute(
|
raw_progress = conn.execute(
|
||||||
"""SELECT exercise_id FROM exercise_progress
|
"""SELECT exercise_id FROM exercise_progress
|
||||||
WHERE user_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens')
|
WHERE user_id = ? AND status IN ('noch-nicht', 'manchmal', 'meistens')
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,45 @@ from auth import get_current_user
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Alle Übungen aus DB (öffentlich, kein Auth)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/exercises")
|
||||||
|
async def get_exercises():
|
||||||
|
"""Alle Übungen aus der DB, gruppiert nach Tab-ID."""
|
||||||
|
import json as _json
|
||||||
|
CAT_TO_TAB = {
|
||||||
|
'Grundkommando': 'grundkommandos',
|
||||||
|
'Trick': 'tricks',
|
||||||
|
'Problemverhalten': 'problemverhalten',
|
||||||
|
'Mentale Auslastung': 'mentale-auslastung',
|
||||||
|
'Hundesport': 'hundesport',
|
||||||
|
'Koerperpflege': 'koerperpflege',
|
||||||
|
'Körperpflege': 'koerperpflege',
|
||||||
|
'Welpe Basics': 'welpe-basics',
|
||||||
|
}
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT exercise_id, name, kategorie, schwierigkeit, alter_ab,
|
||||||
|
dauer, beschreibung, schritte, tipp
|
||||||
|
FROM training_exercises ORDER BY kategorie, name
|
||||||
|
""").fetchall()
|
||||||
|
by_tab = {}
|
||||||
|
for r in rows:
|
||||||
|
tab = CAT_TO_TAB.get(r['kategorie'], r['kategorie'].lower().replace(' ', '-'))
|
||||||
|
by_tab.setdefault(tab, []).append({
|
||||||
|
'exercise_id': r['exercise_id'],
|
||||||
|
'name': r['name'],
|
||||||
|
'kategorie': tab,
|
||||||
|
'schwierigkeit': r['schwierigkeit'] or 'mittel',
|
||||||
|
'alter': r['alter_ab'],
|
||||||
|
'dauer': r['dauer'],
|
||||||
|
'beschreibung': r['beschreibung'],
|
||||||
|
'schritte': _json.loads(r['schritte'] or '[]'),
|
||||||
|
'tipp': r['tipp'],
|
||||||
|
})
|
||||||
|
return by_tab
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Übungs-Status
|
# Übungs-Status
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '469'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '470'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
|
|
||||||
const App = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,17 +36,23 @@ window.Page_uebungen = (() => {
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
let _statsData = null; // cached stats from /api/training/stats
|
let _statsData = null; // cached stats from /api/training/stats
|
||||||
let _badgesData = null; // cached badges from /api/achievements
|
let _badgesData = null; // cached badges from /api/achievements
|
||||||
|
let _exercisesByTab = {}; // aus API geladen
|
||||||
|
let _exercisesLoaded = false;
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// DATEN
|
// DATEN
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'grundkommandos', label: 'Grundkommandos' },
|
{ id: 'grundkommandos', label: 'Grundkommandos' },
|
||||||
{ id: 'tricks', label: 'Tricks & Beschäftigung' },
|
{ id: 'tricks', label: 'Tricks' },
|
||||||
{ id: 'problemverhalten', label: 'Problemverhalten' },
|
{ id: 'problemverhalten', label: 'Problemverhalten' },
|
||||||
{ id: 'grundlagen', label: 'Trainingsgrundlagen' },
|
{ id: 'mentale-auslastung', label: 'Mentale Auslastung' },
|
||||||
{ id: 'ki-trainer', label: 'KI-Trainer' },
|
{ id: 'hundesport', label: 'Hundesport' },
|
||||||
|
{ id: 'koerperpflege', label: 'Körperpflege' },
|
||||||
|
{ id: 'welpe-basics', label: 'Welpe Basics' },
|
||||||
|
{ id: 'grundlagen', label: 'Trainingsgrundlagen' },
|
||||||
|
{ id: 'ki-trainer', label: 'KI-Trainer' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -435,13 +441,20 @@ window.Page_uebungen = (() => {
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// DB-Kategorien → Tab-IDs
|
// DB-Kategorien → Tab-IDs
|
||||||
const _KAT_TO_TAB = {
|
const _KAT_TO_TAB = {
|
||||||
'grundkommando': 'grundkommandos',
|
'grundkommando': 'grundkommandos',
|
||||||
'grundkommandos': 'grundkommandos',
|
'grundkommandos': 'grundkommandos',
|
||||||
'trick': 'tricks',
|
'trick': 'tricks',
|
||||||
'tricks': 'tricks',
|
'tricks': 'tricks',
|
||||||
'problemverhalten':'problemverhalten',
|
'problemverhalten': 'problemverhalten',
|
||||||
'grundlagen': 'grundlagen',
|
'mentale auslastung': 'mentale-auslastung',
|
||||||
'ki-trainer': 'ki-trainer',
|
'mentale-auslastung': 'mentale-auslastung',
|
||||||
|
'hundesport': 'hundesport',
|
||||||
|
'körperpflege': 'koerperpflege',
|
||||||
|
'koerperpflege': 'koerperpflege',
|
||||||
|
'welpe basics': 'welpe-basics',
|
||||||
|
'welpe-basics': 'welpe-basics',
|
||||||
|
'grundlagen': 'grundlagen',
|
||||||
|
'ki-trainer': 'ki-trainer',
|
||||||
};
|
};
|
||||||
const _VALID_TABS = new Set(TABS.map(t => t.id));
|
const _VALID_TABS = new Set(TABS.map(t => t.id));
|
||||||
|
|
||||||
|
|
@ -461,6 +474,16 @@ window.Page_uebungen = (() => {
|
||||||
if (_VALID_TABS.has(mapped)) _activeTab = mapped;
|
if (_VALID_TABS.has(mapped)) _activeTab = mapped;
|
||||||
}
|
}
|
||||||
_render();
|
_render();
|
||||||
|
|
||||||
|
// Übungen aus DB laden (parallel mit Progress)
|
||||||
|
if (!_exercisesLoaded) {
|
||||||
|
API.get('/training/exercises').then(data => {
|
||||||
|
_exercisesByTab = data || {};
|
||||||
|
_exercisesLoaded = true;
|
||||||
|
_renderContent(); // neu rendern sobald Daten da
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
if (params.exercise_id || params.name) {
|
if (params.exercise_id || params.name) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Erst per exercise_id suchen (zuverlässig), dann per Name als Fallback
|
// Erst per exercise_id suchen (zuverlässig), dann per Name als Fallback
|
||||||
|
|
@ -911,7 +934,8 @@ window.Page_uebungen = (() => {
|
||||||
const el = _container.querySelector('#ueb-content');
|
const el = _container.querySelector('#ueb-content');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
const isExerciseTab = ['grundkommandos', 'tricks', 'problemverhalten'].includes(_activeTab);
|
const isExerciseTab = ['grundkommandos','tricks','problemverhalten',
|
||||||
|
'mentale-auslastung','hundesport','koerperpflege','welpe-basics'].includes(_activeTab);
|
||||||
const showIf = v => v ? '' : 'none';
|
const showIf = v => v ? '' : 'none';
|
||||||
|
|
||||||
const quickWrap = _container.querySelector('#ueb-quicksetup-btn')?.parentElement;
|
const quickWrap = _container.querySelector('#ueb-quicksetup-btn')?.parentElement;
|
||||||
|
|
@ -924,11 +948,23 @@ window.Page_uebungen = (() => {
|
||||||
if (bannerEl) bannerEl.style.display = showIf(isExerciseTab);
|
if (bannerEl) bannerEl.style.display = showIf(isExerciseTab);
|
||||||
|
|
||||||
switch (_activeTab) {
|
switch (_activeTab) {
|
||||||
case 'grundkommandos': el.innerHTML = _renderUebungsList(GRUNDKOMMANDOS); break;
|
case 'grundkommandos':
|
||||||
case 'tricks': el.innerHTML = _renderUebungsList(TRICKS); break;
|
case 'tricks':
|
||||||
case 'problemverhalten': el.innerHTML = _renderUebungsList(PROBLEMVERHALTEN); break;
|
case 'problemverhalten':
|
||||||
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break;
|
case 'mentale-auslastung':
|
||||||
case 'ki-trainer': el.innerHTML = _renderKiTrainer(); break;
|
case 'hundesport':
|
||||||
|
case 'koerperpflege':
|
||||||
|
case 'welpe-basics': {
|
||||||
|
const list = _exercisesByTab[_activeTab] || [];
|
||||||
|
el.innerHTML = list.length
|
||||||
|
? _renderUebungsList(list)
|
||||||
|
: `<div style="padding:var(--space-6);text-align:center;color:var(--c-text-muted)">
|
||||||
|
${UI.icon('spinner')} Übungen werden geladen…
|
||||||
|
</div>`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break;
|
||||||
|
case 'ki-trainer': el.innerHTML = _renderKiTrainer(); break;
|
||||||
}
|
}
|
||||||
_bindAccordions();
|
_bindAccordions();
|
||||||
_bindStatusButtons();
|
_bindStatusButtons();
|
||||||
|
|
@ -971,7 +1007,7 @@ window.Page_uebungen = (() => {
|
||||||
const currentId = _getStatus(_activeTab, u.name);
|
const currentId = _getStatus(_activeTab, u.name);
|
||||||
const sm = _statusMeta(currentId);
|
const sm = _statusMeta(currentId);
|
||||||
|
|
||||||
const hasBody = u.schritte.length > 0 || u.fehler.length > 0 || u.steigerung;
|
const hasBody = (u.schritte?.length > 0) || (u.fehler?.length > 0) || !!u.steigerung || !!u.tipp;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="card" style="padding:0;overflow:hidden" data-exercise-name="${_esc(u.name)}" data-exercise-id="${_esc(_progressKey(_activeTab, u.name))}">
|
<div class="card" style="padding:0;overflow:hidden" data-exercise-name="${_esc(u.name)}" data-exercise-id="${_esc(_progressKey(_activeTab, u.name))}">
|
||||||
|
|
@ -1078,7 +1114,7 @@ window.Page_uebungen = (() => {
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div id="${uid}" hidden style="padding:var(--space-4);border-top:1px solid var(--c-border)">
|
<div id="${uid}" hidden style="padding:var(--space-4);border-top:1px solid var(--c-border)">
|
||||||
${u.schritte.length ? `
|
${u.schritte?.length ? `
|
||||||
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||||
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
|
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
|
||||||
Schritt für Schritt
|
Schritt für Schritt
|
||||||
|
|
@ -1089,7 +1125,7 @@ window.Page_uebungen = (() => {
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</ol>
|
</ol>
|
||||||
` : ''}
|
` : ''}
|
||||||
${u.fehler.length ? `
|
${u.fehler?.length ? `
|
||||||
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||||
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
|
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
|
||||||
<svg class="ph-icon" style="width:12px;height:12px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
<svg class="ph-icon" style="width:12px;height:12px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||||||
|
|
@ -1110,6 +1146,12 @@ window.Page_uebungen = (() => {
|
||||||
<strong>Steigerung:</strong> ${_esc(u.steigerung)}
|
<strong>Steigerung:</strong> ${_esc(u.steigerung)}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
${u.tipp ? `
|
||||||
|
<div style="margin-top:var(--space-3);padding:var(--space-2) var(--space-3);
|
||||||
|
background:var(--c-primary-subtle);border-radius:var(--radius-sm);
|
||||||
|
font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||||
|
💡 ${_esc(u.tipp)}
|
||||||
|
</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v492';
|
const CACHE_VERSION = 'by-v493';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue