Feature: Trainingsprotokoll-Tab in Übungen, kein Tagebuch-Spam

- Neuer Tab 'Protokoll' in der Übungen-Seite: zeigt alle Trainingseinheiten
  chronologisch nach Datum gruppiert (Heute/Gestern/Datum-Label)
- Jede Einheit: Übungsname, Wdh., Erfolgs-Emoji, Stimmung, Sterne, Notiz, TOP-Badge
- 'Weitere laden' Pagination (30 Einheiten pro Seite)
- Backend: Training erstellt keine Tagebuch-Einträge mehr (weder bei ist_top noch manuell)
- Frontend: 'Als Meilenstein ins Tagebuch' Checkbox komplett entfernt
- onDogChange setzt Verlauf-State zurück
This commit is contained in:
rene 2026-05-19 18:17:50 +02:00
parent d2c2c59abb
commit cc841ef6d7
2 changed files with 167 additions and 82 deletions

View file

@ -56,6 +56,7 @@ window.Page_uebungen = (() => {
{ id: 'welpe-basics', label: 'Welpe Basics' },
{ id: 'grundlagen', label: 'Trainingsgrundlagen' },
{ id: 'ki-trainer', label: 'KI-Trainer' },
{ id: 'verlauf', label: 'Protokoll' },
];
// ----------------------------------------------------------
@ -541,11 +542,13 @@ window.Page_uebungen = (() => {
_renderContent();
}
function onDogChange() {
_statsData = null;
_badgesData = null;
_progressCache = {};
_statsData = null;
_badgesData = null;
_progressCache = {};
_progressLoaded = false;
_exerciseStats = {};
_exerciseStats = {};
_verlaufSessions = [];
_verlaufOffset = 0;
_render();
_loadStatsAndBadges();
_loadVirtualTrainer();
@ -980,6 +983,7 @@ window.Page_uebungen = (() => {
const isExerciseTab = ['grundkommandos','tricks','problemverhalten',
'mentale-auslastung','hundesport','koerperpflege','welpe-basics'].includes(_activeTab);
const isVerlauf = _activeTab === 'verlauf';
const showIf = v => v ? '' : 'none';
const quickWrap = _container.querySelector('#ueb-quicksetup-btn')?.parentElement;
@ -990,6 +994,7 @@ window.Page_uebungen = (() => {
if (trainerEl) trainerEl.style.display = showIf(isExerciseTab);
if (suggestEl) suggestEl.style.display = showIf(isExerciseTab);
if (bannerEl) bannerEl.style.display = showIf(isExerciseTab);
if (isVerlauf) _loadVerlauf();
switch (_activeTab) {
case 'grundkommandos':
@ -1011,6 +1016,7 @@ window.Page_uebungen = (() => {
break;
}
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break;
case 'verlauf': el.innerHTML = _renderVerlaufShell(); break;
case 'ki-trainer':
if (!App.hasPro(_appState?.user)) {
el.innerHTML = `<div style="padding:var(--space-6);text-align:center;color:var(--c-text-muted)">
@ -1647,18 +1653,6 @@ window.Page_uebungen = (() => {
background:var(--c-surface);color:var(--c-text);line-height:1.5"></textarea>
</div>
<!-- Meilenstein-Checkbox (initially hidden) -->
<label id="ueb-log-milestone-wrap" hidden
style="display:flex;align-items:flex-start;gap:var(--space-2);cursor:pointer;
padding:var(--space-3);background:var(--c-primary-subtle);
border:1px solid var(--c-primary-light);border-radius:var(--radius-md)">
<input type="checkbox" id="ueb-log-milestone"
style="margin-top:2px;flex-shrink:0;accent-color:var(--c-primary);width:16px;height:16px">
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">
📖 Als Meilenstein ins Tagebuch eintragen
</span>
</label>
</form>
<!-- Footer Buttons -->
@ -1714,7 +1708,6 @@ window.Page_uebungen = (() => {
btn.style.background = 'var(--c-primary-subtle)';
btn.style.borderColor = 'var(--c-primary)';
btn.style.transform = 'scale(1.15)';
_checkMilestoneVisibility();
});
});
@ -1738,17 +1731,9 @@ window.Page_uebungen = (() => {
overlay.querySelectorAll('.ueb-stern-btn').forEach(b => {
b.style.opacity = parseInt(b.dataset.val, 10) <= zufriedenheit ? '1' : '0.35';
});
_checkMilestoneVisibility();
});
});
function _checkMilestoneVisibility() {
const wrap = overlay.querySelector('#ueb-log-milestone-wrap');
if (!wrap) return;
const show = erfolgsquote != null && erfolgsquote >= 75 && zufriedenheit != null && zufriedenheit >= 4;
wrap.hidden = !show;
}
// Save
overlay.querySelector('#ueb-log-save').addEventListener('click', async () => {
const dogId = _dogId();
@ -1761,20 +1746,17 @@ window.Page_uebungen = (() => {
const exerciseId = `${tab}_${exerciseName.replace(/[\s/]+/g, '_')}`;
const today = new Date().toISOString().slice(0, 10);
const tagebuch = !overlay.querySelector('#ueb-log-milestone-wrap').hidden &&
overlay.querySelector('#ueb-log-milestone').checked;
const body = {
dog_id: dogId,
exercise_id: exerciseId,
exercise_name: exerciseName,
datum: today,
wiederholungen: wiederholungen,
erfolgsquote: erfolgsquote,
hund_stimmung: stimmung || null,
zufriedenheit: zufriedenheit || null,
notiz: overlay.querySelector('#ueb-log-notiz').value.trim() || null,
tagebuch_eintrag: tagebuch,
dog_id: dogId,
exercise_id: exerciseId,
exercise_name: exerciseName,
datum: today,
wiederholungen: wiederholungen,
erfolgsquote: erfolgsquote,
hund_stimmung: stimmung || null,
zufriedenheit: zufriedenheit || null,
notiz: overlay.querySelector('#ueb-log-notiz').value.trim() || null,
};
try {
@ -1806,12 +1788,6 @@ window.Page_uebungen = (() => {
});
}
if (resp.diary_entry_id) {
setTimeout(() => {
UI.toast.success('📖 Als Meilenstein im Tagebuch gespeichert.');
}, resp.badges?.length ? (resp.badges.length + 1) * 1000 : 1000);
}
// Stats-Banner + Trainer aktualisieren
_statsData = null;
_loadStatsAndBadges();
@ -1995,6 +1971,153 @@ window.Page_uebungen = (() => {
});
}
// ----------------------------------------------------------
// TRAININGSPROTOKOLL (Verlauf-Tab)
// ----------------------------------------------------------
let _verlaufSessions = [];
let _verlaufOffset = 0;
let _verlaufHasMore = false;
const _VERLAUF_LIMIT = 30;
const _ERFOLG_EMOJI = { 0: '😓', 25: '😐', 50: '🙂', 75: '😊', 100: '🎉' };
const _STIMMUNG_EMOJI = { aufmerksam: '🎯', müde: '😴', abgelenkt: '🌪️', super: '⚡' };
function _renderVerlaufShell() {
const dogId = _dogId();
if (!dogId) {
return `<div style="padding:var(--space-8);text-align:center;color:var(--c-text-muted)">
<p style="font-size:var(--text-sm)">Wähle einen Hund aus um das Protokoll zu sehen.</p>
</div>`;
}
return `<div id="verlauf-wrap" style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)">
<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">
<use href="/icons/phosphor.svg#spinner-gap"></use>
</svg>
</div>
</div>
</div>`;
}
async function _loadVerlauf(append = false) {
const dogId = _dogId();
if (!dogId) return;
const el = _container.querySelector('#verlauf-list');
if (!el) return;
if (!append) {
_verlaufSessions = [];
_verlaufOffset = 0;
}
const data = await _apiGet(
`/api/training/sessions?dog_id=${dogId}&limit=${_VERLAUF_LIMIT + 1}&offset=${_verlaufOffset}`
).catch(() => null);
if (!data) {
if (!append) el.innerHTML = `<div style="padding:var(--space-6);text-align:center;color:var(--c-text-muted);font-size:var(--text-sm)">Fehler beim Laden.</div>`;
return;
}
_verlaufHasMore = data.length > _VERLAUF_LIMIT;
const rows = data.slice(0, _VERLAUF_LIMIT);
_verlaufSessions = append ? [..._verlaufSessions, ...rows] : rows;
_verlaufOffset += rows.length;
_renderVerlaufList(el);
}
function _renderVerlaufList(el) {
if (!_verlaufSessions.length) {
el.innerHTML = `
<div style="padding:var(--space-8);text-align:center;color:var(--c-text-muted)">
<svg class="ph-icon" style="width:40px;height:40px;margin-bottom:var(--space-3);color:var(--c-border)" aria-hidden="true">
<use href="/icons/phosphor.svg#clipboard-text"></use>
</svg>
<p style="font-size:var(--text-sm);margin:0">Noch keine Trainingseinheiten geloggt.</p>
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-1)">
Tippe in einer Übung auf "+ Einheit" um zu starten.
</p>
</div>`;
return;
}
// Nach Datum gruppieren
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' });
}
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);
color:var(--c-text-secondary);text-transform:uppercase;
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>`;
}).join('');
const moreBtn = _verlaufHasMore
? `<button id="verlauf-more"
style="width:100%;padding:var(--space-3);border:1px solid var(--c-border);
background:var(--c-surface-2);border-radius:var(--radius-md);
font-size:var(--text-sm);color:var(--c-text-secondary);cursor:pointer;
margin-top:var(--space-2)">
Weitere laden
</button>`
: '';
el.innerHTML = html + moreBtn;
el.querySelector('#verlauf-more')?.addEventListener('click', () => _loadVerlauf(true));
}
// ----------------------------------------------------------
// TRAININGSGRUNDLAGEN
// ----------------------------------------------------------