diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py index 02ffcc6..8fd392e 100644 --- a/backend/routers/exercises.py +++ b/backend/routers/exercises.py @@ -2095,7 +2095,17 @@ def list_exercises( FROM exercise_training_types ett JOIN training_types tt ON tt.id = ett.training_type_id WHERE ett.exercise_id = e.id - ) AS training_type_names + ) AS training_type_names, + ( + SELECT COUNT(*)::int + FROM exercise_variants ev + WHERE ev.exercise_id = e.id + ) AS variant_count, + ( + SELECT COUNT(*)::int + FROM exercise_media em + WHERE em.exercise_id = e.id + ) AS media_count {variants_sql} FROM exercises e LEFT JOIN profiles p ON e.created_by = p.id @@ -2118,6 +2128,8 @@ def list_exercises( d["focus_area_names"] = _coerce_json_str_list(d.get("focus_area_names")) d["style_direction_names"] = _coerce_json_str_list(d.get("style_direction_names")) d["training_type_names"] = _coerce_json_str_list(d.get("training_type_names")) + d["variant_count"] = int(d.get("variant_count") or 0) + d["media_count"] = int(d.get("media_count") or 0) if include_variants: v = d.get("variants") if isinstance(v, str): diff --git a/frontend/src/app.css b/frontend/src/app.css index 0c500ce..f5bcf83 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -4819,6 +4819,34 @@ html.modal-scroll-locked .app-main { min-height: 44px; box-sizing: border-box; } +.exercise-card__footer-meta { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + min-width: 0; + flex-shrink: 1; +} +.exercise-card__content-stats { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--text3); +} +.exercise-card__stat { + display: inline-flex; + align-items: center; + gap: 3px; + color: var(--text3); + font-size: 12px; + line-height: 1; +} +.exercise-card__stat-num { + font-variant-numeric: tabular-nums; + font-weight: 600; + font-size: 12px; + line-height: 1; +} .exercise-card__meta-compact { display: inline-flex; align-items: center; diff --git a/frontend/src/components/exercises/ExerciseFormPageRoot.jsx b/frontend/src/components/exercises/ExerciseFormPageRoot.jsx index 5d4a94a..2e1aa9d 100644 --- a/frontend/src/components/exercises/ExerciseFormPageRoot.jsx +++ b/frontend/src/components/exercises/ExerciseFormPageRoot.jsx @@ -26,6 +26,7 @@ import { COMBINATION_ARCHETYPE_OPTIONS, ARCHETYPE_DEFAULT_REP_SERIES_COUNT, defa import { readSlotProfilesV1, normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../../utils/combinationMethodProfileUi' import { GripVertical } from 'lucide-react' import UnsavedChangesPrompt from '../UnsavedChangesPrompt' +import PageFormEditorChrome from '../PageFormEditorChrome' import { useBeforeUnloadWhen, useUnsavedChangesBlocker } from '../../hooks/useUnsavedChangesBlocker' const INTENSITY_OPTIONS = [ @@ -836,7 +837,7 @@ function ExerciseFormPageRoot() { } const performSaveAttempt = useCallback( - async ({ fromUnsavedDialog = false } = {}) => { + async ({ fromUnsavedDialog = false, closeAfter = false } = {}) => { if (!formData.title || formData.title.trim().length < 3) { toast.error('Titel mindestens 3 Zeichen') return false @@ -940,12 +941,15 @@ function ExerciseFormPageRoot() { setVariants((ex.variants || []).map(apiVariantToRow)) setFormDirty(false) toast.success('Gespeichert.') + if (closeAfter) navigate('/exercises') return true } const created = await api.createExercise(payload) setFormDirty(false) toast.success('Übung angelegt.') - if (!fromUnsavedDialog) { + if (closeAfter) { + navigate('/exercises') + } else if (!fromUnsavedDialog) { navigate(`/exercises/${created.id}/edit`, { replace: true }) } return true @@ -959,10 +963,39 @@ function ExerciseFormPageRoot() { [exerciseId, formData, isEdit, navigate, toast], ) - const handleSubmit = async (e) => { - e.preventDefault() - await performSaveAttempt({ fromUnsavedDialog: false }) - } + const handleSubmit = useCallback( + async (e) => { + e?.preventDefault?.() + await performSaveAttempt({ fromUnsavedDialog: false, closeAfter: false }) + }, + [performSaveAttempt], + ) + + const handleSaveAndClose = useCallback( + async (e) => { + e?.preventDefault?.() + await performSaveAttempt({ fromUnsavedDialog: false, closeAfter: true }) + }, + [performSaveAttempt], + ) + + const goBackToList = useCallback(() => { + navigate('/exercises') + }, [navigate]) + + const actionConfig = useMemo( + () => ({ + formId: 'exercise-form', + saving, + isNew: !isEdit, + onSave: handleSubmit, + onSaveAndClose: handleSaveAndClose, + onCancel: goBackToList, + showSave: true, + showSaveAndClose: true, + }), + [saving, isEdit, handleSubmit, handleSaveAndClose, goBackToList], + ) const handleUnsavedDialogSave = async () => { const ok = await performSaveAttempt({ fromUnsavedDialog: true }) @@ -1162,27 +1195,28 @@ function ExerciseFormPageRoot() { } return ( -