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:
parent
d2c2c59abb
commit
cc841ef6d7
2 changed files with 167 additions and 82 deletions
|
|
@ -326,7 +326,7 @@ class SessionCreate(BaseModel):
|
|||
hund_stimmung: str = "aufmerksam"
|
||||
zufriedenheit: int = 3
|
||||
notiz: Optional[str] = None
|
||||
tagebuch_eintrag: bool = False
|
||||
tagebuch_eintrag: bool = False # ignoriert — Training hat eigenes Protokoll
|
||||
|
||||
|
||||
@router.post("/sessions")
|
||||
|
|
@ -363,42 +363,6 @@ async def log_session(body: SessionCreate, user=Depends(get_current_user)):
|
|||
# Badges prüfen
|
||||
new_badges = _check_badges(conn, uid, dog_name)
|
||||
|
||||
# Tagebucheintrag erstellen?
|
||||
diary_entry_id = None
|
||||
if body.tagebuch_eintrag or ist_top:
|
||||
stimmung_label = STIMMUNGS_LABELS.get(body.hund_stimmung, body.hund_stimmung)
|
||||
if ist_top:
|
||||
titel = f"\U0001f3af {body.exercise_name} \u2014 Top-Training!"
|
||||
else:
|
||||
titel = f"\U0001f3af Training: {body.exercise_name}"
|
||||
text_parts = [
|
||||
f"{body.wiederholungen} Wiederholungen \u00b7 "
|
||||
f"Erfolgsquote: {body.erfolgsquote}% \u00b7 "
|
||||
f"Stimmung: {stimmung_label}"
|
||||
]
|
||||
if body.notiz:
|
||||
text_parts.append(f"\n\n{body.notiz}")
|
||||
eintrag_text = "".join(text_parts)
|
||||
|
||||
diary_cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO diary (dog_id, datum, typ, titel, text)
|
||||
VALUES (?,?,?,?,?)
|
||||
""",
|
||||
(body.dog_id, datum, "training", titel, eintrag_text)
|
||||
)
|
||||
diary_entry_id = diary_cur.lastrowid
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id) VALUES (?,?)",
|
||||
(diary_entry_id, body.dog_id)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
"UPDATE training_sessions SET diary_entry_id=? WHERE id=?",
|
||||
(diary_entry_id, session_id)
|
||||
)
|
||||
|
||||
session = {
|
||||
"id": session_id,
|
||||
"user_id": uid,
|
||||
|
|
@ -412,14 +376,12 @@ async def log_session(body: SessionCreate, user=Depends(get_current_user)):
|
|||
"zufriedenheit": body.zufriedenheit,
|
||||
"notiz": body.notiz,
|
||||
"ist_top": bool(ist_top),
|
||||
"diary_entry_id": diary_entry_id,
|
||||
}
|
||||
|
||||
return {
|
||||
"session": session,
|
||||
"ist_top": bool(ist_top),
|
||||
"badges": new_badges,
|
||||
"diary_entry_id": diary_entry_id,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue