Feature: Protokoll-Tab Toggle 'Nach Datum / Nach Übung'
- Toggle-Buttons oben im Protokoll-Tab - 'Nach Übung': gruppiert alle Sessions pro Übung, sortiert nach zuletzt trainiert - Pro Übung: Ø-Erfolgsquote als Kreis, Trend-Pfeil (↑↓→★), Anzahl Einheiten + TOP-Count - Aufklappbare Session-Liste pro Übung (Datum · Emoji · % · Wdh.) - Hinweis wenn mehr Sessions vorhanden als geladen
This commit is contained in:
parent
738571d958
commit
dcb966ca54
1 changed files with 205 additions and 37 deletions
|
|
@ -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 = `<div id="verlauf-wrap" style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)"><div id="verlauf-list"></div></div>`;
|
||||
el.innerHTML = `<div id="verlauf-wrap" style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)">${_verlaufToggleHtml()}<div id="verlauf-list"></div></div>`;
|
||||
_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 = (() => {
|
|||
</div>`;
|
||||
}
|
||||
return `<div id="verlauf-wrap" style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)">
|
||||
${_verlaufToggleHtml()}
|
||||
<div id="verlauf-list" style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||
<div style="text-align:center;padding:var(--space-6);color:var(--c-text-muted)">
|
||||
<svg class="ph-icon" style="width:24px;height:24px;animation:spin 1s linear infinite" aria-hidden="true">
|
||||
|
|
@ -2010,6 +2014,23 @@ window.Page_uebungen = (() => {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
<button id="verlauf-btn-datum" style="${btnBase};${_verlaufView==='datum'?active:inactive}">
|
||||
Nach Datum
|
||||
</button>
|
||||
<button id="verlauf-btn-uebung" style="${btnBase};${_verlaufView==='uebung'?active:inactive}">
|
||||
Nach Übung
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 = (() => {
|
|||
</div>`;
|
||||
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
|
||||
? `<span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:999px;
|
||||
background:rgba(22,163,74,0.12);color:#15803d;border:1px solid rgba(22,163,74,0.3)">TOP</span>`
|
||||
: '';
|
||||
const noteHtml = s.notiz
|
||||
? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:3px;
|
||||
line-height:1.4;font-style:italic">${_esc(s.notiz)}</div>`
|
||||
: '';
|
||||
return `
|
||||
<div style="display:flex;align-items:flex-start;gap:var(--space-3);
|
||||
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
background:var(--c-surface-2)">
|
||||
<span style="font-size:1.2rem;flex-shrink:0;margin-top:1px">${erfolg}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
|
||||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text)">${_esc(s.exercise_name)}</span>
|
||||
${topBadge}
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:1px">
|
||||
${s.wiederholungen}× Wdh.${stimmung ? ' · ' + stimmung : ''}${s.zufriedenheit ? ' · ' + '⭐'.repeat(s.zufriedenheit) : ''}
|
||||
</div>
|
||||
${noteHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:999px;
|
||||
background:rgba(22,163,74,0.12);color:#15803d;border:1px solid rgba(22,163,74,0.3)">TOP</span>`
|
||||
: '';
|
||||
const noteHtml = s.notiz
|
||||
? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:3px;
|
||||
line-height:1.4;font-style:italic">${_esc(s.notiz)}</div>`
|
||||
: '';
|
||||
return `
|
||||
<div style="display:flex;align-items:flex-start;gap:var(--space-3);
|
||||
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
background:var(--c-surface-2)">
|
||||
<span style="font-size:1.2rem;flex-shrink:0;margin-top:1px">${erfolg}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
|
||||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text)">${_esc(s.exercise_name)}</span>
|
||||
${topBadge}
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:1px">
|
||||
${s.wiederholungen}× Wdh. ${stimmung ? '· ' + stimmung : ''}
|
||||
${s.zufriedenheit ? '· ' + '⭐'.repeat(s.zufriedenheit) : ''}
|
||||
</div>
|
||||
${noteHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div>
|
||||
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||
|
|
@ -2114,7 +2161,9 @@ window.Page_uebungen = (() => {
|
|||
letter-spacing:.05em;padding:var(--space-2) 0 var(--space-1)">
|
||||
${_esc(label)}
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-1)">${rows}</div>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-1)">
|
||||
${sessions.map(_sessionRow).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}).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 `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);
|
||||
padding:4px var(--space-2);border-radius:var(--radius-sm);
|
||||
font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
<span style="flex-shrink:0;min-width:52px">${_esc(dateLabel)}</span>
|
||||
<span style="flex-shrink:0">${erfolg}</span>
|
||||
<span style="flex-shrink:0">${s.erfolgsquote}%${top}</span>
|
||||
<span style="flex:1;min-width:0">${s.wiederholungen}× Wdh.${stimmung ? ' ' + stimmung : ''}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
<!-- Header (klickbar zum Aufklappen) -->
|
||||
<button class="verlauf-ex-btn" data-uid="${uid}"
|
||||
style="width:100%;padding:var(--space-3) var(--space-4);display:flex;
|
||||
align-items:center;gap:var(--space-3);background:none;border:none;
|
||||
cursor:pointer;text-align:left">
|
||||
<!-- Fortschrittsring -->
|
||||
<div style="flex-shrink:0;width:40px;height:40px;border-radius:50%;
|
||||
background:${barBg};display:flex;flex-direction:column;
|
||||
align-items:center;justify-content:center">
|
||||
<span style="font-size:11px;font-weight:700;color:${barColor};line-height:1">${ex.avg}%</span>
|
||||
<span style="font-size:9px;color:${barColor};line-height:1;margin-top:1px">
|
||||
${TREND_ICON[ex.trend]}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Info -->
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);line-height:1.3;
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||
${_esc(ex.name)}
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
|
||||
${ex.sessions.length} Einheit${ex.sessions.length !== 1 ? 'en' : ''}
|
||||
${ex.topCount ? ' · ' + ex.topCount + '× TOP' : ''}
|
||||
· ${_esc(lastLabel)}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Chevron -->
|
||||
<svg class="ph-icon verlauf-ex-chevron" data-uid="${uid}"
|
||||
style="width:16px;height:16px;flex-shrink:0;color:var(--c-text-muted);transition:transform .2s"
|
||||
aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#caret-down"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Eingeklappte Session-Liste -->
|
||||
<div id="${uid}" hidden
|
||||
style="border-top:1px solid var(--c-border);padding:var(--space-2) var(--space-3)">
|
||||
${sessionRows}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const hint = _verlaufHasMore
|
||||
? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;padding:var(--space-2) 0">
|
||||
Zeigt die letzten ${_verlaufSessions.length} Einheiten — ältere nicht berücksichtigt.
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
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
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue