diff --git a/backend/static/js/pages/uebungen.js b/backend/static/js/pages/uebungen.js index 057a942..7606218 100644 --- a/backend/static/js/pages/uebungen.js +++ b/backend/static/js/pages/uebungen.js @@ -550,6 +550,7 @@ window.Page_uebungen = (() => { _verlaufSessions = []; _verlaufOffset = 0; _verlaufLoading = false; + _verlaufView = 'datum'; _render(); _loadStatsAndBadges(); _loadVirtualTrainer(); @@ -1018,12 +1019,13 @@ window.Page_uebungen = (() => { case 'grundlagen': el.innerHTML = _renderGrundlagen(); break; case 'verlauf': { if (_verlaufSessions.length > 0) { - el.innerHTML = `
`; + el.innerHTML = `
${_verlaufToggleHtml()}
`; _renderVerlaufList(el.querySelector('#verlauf-list')); } else { el.innerHTML = _renderVerlaufShell(); _loadVerlauf(); } + _bindVerlaufToggle(); break; } case 'ki-trainer': @@ -1987,6 +1989,7 @@ window.Page_uebungen = (() => { let _verlaufOffset = 0; let _verlaufHasMore = false; let _verlaufLoading = false; + let _verlaufView = 'datum'; // 'datum' | 'uebung' const _VERLAUF_LIMIT = 30; const _ERFOLG_EMOJI = { 0: '😓', 25: '😐', 50: '🙂', 75: '😊', 100: '🎉' }; @@ -2000,6 +2003,7 @@ window.Page_uebungen = (() => { `; } return `
+ ${_verlaufToggleHtml()}
`; } + function _verlaufToggleHtml() { + const btnBase = `padding:var(--space-2) var(--space-3);border-radius:var(--radius-md); + font-size:var(--text-xs);font-weight:var(--weight-semibold);cursor:pointer; + border:1px solid var(--c-border);transition:all .15s`; + const active = `background:var(--c-primary);color:#fff;border-color:var(--c-primary)`; + const inactive = `background:var(--c-surface-2);color:var(--c-text-secondary)`; + return ` +
+ + +
`; + } + async function _loadVerlauf(append = false) { if (_verlaufLoading) return; const dogId = _dogId(); @@ -2043,6 +2064,28 @@ window.Page_uebungen = (() => { _renderVerlaufList(el); } + function _bindVerlaufToggle() { + const wrap = _container?.querySelector('#verlauf-wrap'); + if (!wrap) return; + const btnDatum = wrap.querySelector('#verlauf-btn-datum'); + const btnUebung = wrap.querySelector('#verlauf-btn-uebung'); + const setActive = view => { + _verlaufView = view; + const active = `var(--c-primary)`; + const inBg = `var(--c-surface-2)`; + btnDatum.style.background = view === 'datum' ? active : inBg; + btnDatum.style.color = view === 'datum' ? '#fff' : 'var(--c-text-secondary)'; + btnDatum.style.borderColor = view === 'datum' ? active : 'var(--c-border)'; + btnUebung.style.background = view === 'uebung' ? active : inBg; + btnUebung.style.color = view === 'uebung' ? '#fff' : 'var(--c-text-secondary)'; + btnUebung.style.borderColor = view === 'uebung' ? active : 'var(--c-border)'; + const listEl = wrap.querySelector('#verlauf-list'); + if (listEl) _renderVerlaufList(listEl); + }; + btnDatum?.addEventListener('click', () => setActive('datum')); + btnUebung?.addEventListener('click', () => setActive('uebung')); + } + function _renderVerlaufList(el) { if (!_verlaufSessions.length) { el.innerHTML = ` @@ -2057,8 +2100,44 @@ window.Page_uebungen = (() => {
`; return; } + if (_verlaufView === 'uebung') { + _renderVerlaufByUebung(el); + } else { + _renderVerlaufByDatum(el); + } + } - // Nach Datum gruppieren + function _sessionRow(s) { + const erfolg = _ERFOLG_EMOJI[s.erfolgsquote] || '🙂'; + const stimmung = s.hund_stimmung ? (_STIMMUNG_EMOJI[s.hund_stimmung] || '') : ''; + const topBadge = s.ist_top + ? `TOP` + : ''; + const noteHtml = s.notiz + ? `
${_esc(s.notiz)}
` + : ''; + return ` +
+ ${erfolg} +
+
+ ${_esc(s.exercise_name)} + ${topBadge} +
+
+ ${s.wiederholungen}× Wdh.${stimmung ? ' · ' + stimmung : ''}${s.zufriedenheit ? ' · ' + '⭐'.repeat(s.zufriedenheit) : ''} +
+ ${noteHtml} +
+
`; + } + + function _renderVerlaufByDatum(el) { const today = new Date().toISOString().slice(0, 10); const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10); const groups = {}; @@ -2069,44 +2148,12 @@ window.Page_uebungen = (() => { const html = Object.entries(groups).map(([datum, sessions]) => { let label; - if (datum === today) label = 'Heute'; + if (datum === today) label = 'Heute'; else if (datum === yesterday) label = 'Gestern'; else { const d = new Date(datum + 'T00:00:00'); label = d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short' }); } - - const rows = sessions.map(s => { - const erfolg = _ERFOLG_EMOJI[s.erfolgsquote] || '🙂'; - const stimmung = s.hund_stimmung ? (_STIMMUNG_EMOJI[s.hund_stimmung] || '') : ''; - const topBadge = s.ist_top - ? `TOP` - : ''; - const noteHtml = s.notiz - ? `
${_esc(s.notiz)}
` - : ''; - return ` -
- ${erfolg} -
-
- ${_esc(s.exercise_name)} - ${topBadge} -
-
- ${s.wiederholungen}× Wdh. ${stimmung ? '· ' + stimmung : ''} - ${s.zufriedenheit ? '· ' + '⭐'.repeat(s.zufriedenheit) : ''} -
- ${noteHtml} -
-
`; - }).join(''); - return `
${_esc(label)}
-
${rows}
+
+ ${sessions.map(_sessionRow).join('')} +
`; }).join(''); @@ -2129,10 +2178,129 @@ window.Page_uebungen = (() => { : ''; el.innerHTML = html + moreBtn; - el.querySelector('#verlauf-more')?.addEventListener('click', () => _loadVerlauf(true)); } + function _renderVerlaufByUebung(el) { + // Sessions nach Übungsname gruppieren + const groups = {}; + _verlaufSessions.forEach(s => { + if (!groups[s.exercise_name]) groups[s.exercise_name] = []; + groups[s.exercise_name].push(s); + }); + + // Pro Gruppe Stats berechnen + const today = new Date().toISOString().slice(0, 10); + const exerciseStats = Object.entries(groups).map(([name, sessions]) => { + const avg = Math.round(sessions.reduce((a, s) => a + s.erfolgsquote, 0) / sessions.length); + const recent = sessions.slice(0, 3); + const older = sessions.slice(3, 6); + let trend = 'new'; + if (older.length) { + const rAvg = recent.reduce((a, s) => a + s.erfolgsquote, 0) / recent.length; + const oAvg = older.reduce((a, s) => a + s.erfolgsquote, 0) / older.length; + trend = rAvg - oAvg > 10 ? 'up' : rAvg - oAvg < -10 ? 'down' : 'stable'; + } + const lastDate = sessions[0].datum; + const daysSince = Math.floor((new Date(today) - new Date(lastDate)) / 86400000); + return { name, sessions, avg, trend, lastDate, daysSince, topCount: sessions.filter(s => s.ist_top).length }; + }); + + // Sortieren: zuletzt trainiert zuerst + exerciseStats.sort((a, b) => a.daysSince - b.daysSince); + + const TREND_ICON = { up: '↑', down: '↓', stable: '→', new: '★' }; + const TREND_COLOR = { up: '#15803d', down: '#dc2626', stable: 'var(--c-text-secondary)', new: 'var(--c-primary)' }; + + const cards = exerciseStats.map((ex, i) => { + const uid = `vl-ex-${i}`; + const barColor = ex.avg >= 75 ? '#15803d' : ex.avg >= 50 ? '#c2410c' : '#dc2626'; + const barBg = ex.avg >= 75 ? 'rgba(22,163,74,0.15)' : ex.avg >= 50 ? 'rgba(194,65,12,0.15)' : 'rgba(220,38,38,0.15)'; + const lastLabel = ex.daysSince === 0 ? 'Heute' + : ex.daysSince === 1 ? 'Gestern' + : `vor ${ex.daysSince} Tagen`; + const sessionRows = ex.sessions.map(s => { + const d = new Date(s.datum + 'T00:00:00'); + const dateLabel = d.toLocaleDateString('de-DE', { day: 'numeric', month: 'short' }); + const erfolg = _ERFOLG_EMOJI[s.erfolgsquote] || '🙂'; + const stimmung = s.hund_stimmung ? (_STIMMUNG_EMOJI[s.hund_stimmung] || '') : ''; + const top = s.ist_top ? ' ★' : ''; + return ` +
+ ${_esc(dateLabel)} + ${erfolg} + ${s.erfolgsquote}%${top} + ${s.wiederholungen}× Wdh.${stimmung ? ' ' + stimmung : ''} +
`; + }).join(''); + + return ` +
+ + + + +
`; + }).join(''); + + const hint = _verlaufHasMore + ? `
+ Zeigt die letzten ${_verlaufSessions.length} Einheiten — ältere nicht berücksichtigt. +
` + : ''; + + el.innerHTML = cards + hint; + + // Akkordeon-Binding + el.querySelectorAll('.verlauf-ex-btn').forEach(btn => { + btn.addEventListener('click', () => { + const uid = btn.dataset.uid; + const body = document.getElementById(uid); + const chev = el.querySelector(`.verlauf-ex-chevron[data-uid="${uid}"]`); + const isOpen = !body.hidden; + body.hidden = isOpen; + if (chev) chev.style.transform = isOpen ? '' : 'rotate(180deg)'; + }); + }); + } + // ---------------------------------------------------------- // TRAININGSGRUNDLAGEN // ----------------------------------------------------------