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 = `
+ ${_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 `
+
+
+
+
+
+ ${sessionRows}
+
+
`;
+ }).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
// ----------------------------------------------------------