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 = [];
|
_verlaufSessions = [];
|
||||||
_verlaufOffset = 0;
|
_verlaufOffset = 0;
|
||||||
_verlaufLoading = false;
|
_verlaufLoading = false;
|
||||||
|
_verlaufView = 'datum';
|
||||||
_render();
|
_render();
|
||||||
_loadStatsAndBadges();
|
_loadStatsAndBadges();
|
||||||
_loadVirtualTrainer();
|
_loadVirtualTrainer();
|
||||||
|
|
@ -1018,12 +1019,13 @@ window.Page_uebungen = (() => {
|
||||||
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break;
|
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break;
|
||||||
case 'verlauf': {
|
case 'verlauf': {
|
||||||
if (_verlaufSessions.length > 0) {
|
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'));
|
_renderVerlaufList(el.querySelector('#verlauf-list'));
|
||||||
} else {
|
} else {
|
||||||
el.innerHTML = _renderVerlaufShell();
|
el.innerHTML = _renderVerlaufShell();
|
||||||
_loadVerlauf();
|
_loadVerlauf();
|
||||||
}
|
}
|
||||||
|
_bindVerlaufToggle();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ki-trainer':
|
case 'ki-trainer':
|
||||||
|
|
@ -1987,6 +1989,7 @@ window.Page_uebungen = (() => {
|
||||||
let _verlaufOffset = 0;
|
let _verlaufOffset = 0;
|
||||||
let _verlaufHasMore = false;
|
let _verlaufHasMore = false;
|
||||||
let _verlaufLoading = false;
|
let _verlaufLoading = false;
|
||||||
|
let _verlaufView = 'datum'; // 'datum' | 'uebung'
|
||||||
const _VERLAUF_LIMIT = 30;
|
const _VERLAUF_LIMIT = 30;
|
||||||
|
|
||||||
const _ERFOLG_EMOJI = { 0: '😓', 25: '😐', 50: '🙂', 75: '😊', 100: '🎉' };
|
const _ERFOLG_EMOJI = { 0: '😓', 25: '😐', 50: '🙂', 75: '😊', 100: '🎉' };
|
||||||
|
|
@ -2000,6 +2003,7 @@ window.Page_uebungen = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
return `<div id="verlauf-wrap" style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)">
|
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 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)">
|
<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">
|
<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>`;
|
</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) {
|
async function _loadVerlauf(append = false) {
|
||||||
if (_verlaufLoading) return;
|
if (_verlaufLoading) return;
|
||||||
const dogId = _dogId();
|
const dogId = _dogId();
|
||||||
|
|
@ -2043,6 +2064,28 @@ window.Page_uebungen = (() => {
|
||||||
_renderVerlaufList(el);
|
_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) {
|
function _renderVerlaufList(el) {
|
||||||
if (!_verlaufSessions.length) {
|
if (!_verlaufSessions.length) {
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
|
|
@ -2057,26 +2100,14 @@ window.Page_uebungen = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_verlaufView === 'uebung') {
|
||||||
// Nach Datum gruppieren
|
_renderVerlaufByUebung(el);
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
} else {
|
||||||
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
_renderVerlaufByDatum(el);
|
||||||
const groups = {};
|
}
|
||||||
_verlaufSessions.forEach(s => {
|
|
||||||
groups[s.datum] = groups[s.datum] || [];
|
|
||||||
groups[s.datum].push(s);
|
|
||||||
});
|
|
||||||
|
|
||||||
const html = Object.entries(groups).map(([datum, sessions]) => {
|
|
||||||
let label;
|
|
||||||
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 => {
|
function _sessionRow(s) {
|
||||||
const erfolg = _ERFOLG_EMOJI[s.erfolgsquote] || '🙂';
|
const erfolg = _ERFOLG_EMOJI[s.erfolgsquote] || '🙂';
|
||||||
const stimmung = s.hund_stimmung ? (_STIMMUNG_EMOJI[s.hund_stimmung] || '') : '';
|
const stimmung = s.hund_stimmung ? (_STIMMUNG_EMOJI[s.hund_stimmung] || '') : '';
|
||||||
const topBadge = s.ist_top
|
const topBadge = s.ist_top
|
||||||
|
|
@ -2099,14 +2130,30 @@ window.Page_uebungen = (() => {
|
||||||
${topBadge}
|
${topBadge}
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:1px">
|
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:1px">
|
||||||
${s.wiederholungen}× Wdh. ${stimmung ? '· ' + stimmung : ''}
|
${s.wiederholungen}× Wdh.${stimmung ? ' · ' + stimmung : ''}${s.zufriedenheit ? ' · ' + '⭐'.repeat(s.zufriedenheit) : ''}
|
||||||
${s.zufriedenheit ? '· ' + '⭐'.repeat(s.zufriedenheit) : ''}
|
|
||||||
</div>
|
</div>
|
||||||
${noteHtml}
|
${noteHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}
|
||||||
|
|
||||||
|
function _renderVerlaufByDatum(el) {
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
||||||
|
const groups = {};
|
||||||
|
_verlaufSessions.forEach(s => {
|
||||||
|
groups[s.datum] = groups[s.datum] || [];
|
||||||
|
groups[s.datum].push(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = Object.entries(groups).map(([datum, sessions]) => {
|
||||||
|
let label;
|
||||||
|
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' });
|
||||||
|
}
|
||||||
return `
|
return `
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
<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)">
|
letter-spacing:.05em;padding:var(--space-2) 0 var(--space-1)">
|
||||||
${_esc(label)}
|
${_esc(label)}
|
||||||
</div>
|
</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>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
|
@ -2129,10 +2178,129 @@ window.Page_uebungen = (() => {
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
el.innerHTML = html + moreBtn;
|
el.innerHTML = html + moreBtn;
|
||||||
|
|
||||||
el.querySelector('#verlauf-more')?.addEventListener('click', () => _loadVerlauf(true));
|
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
|
// TRAININGSGRUNDLAGEN
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue