From 7f62b6ceeeb7ece0212a5bf96b02ef4daa91035a Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 21 May 2026 14:54:32 +0200 Subject: [PATCH] Enhance Exercise Form Functionality with Variant Management Improvements - Introduced snapshot and dirty check functions for variant payloads, enabling better tracking of unsaved changes. - Implemented synchronization of saved variant snapshots to improve data integrity during edits. - Enhanced the variant saving process with validation for variant names and automatic saving of changes. - Updated the UI to reflect changes in variant management, ensuring a smoother user experience when editing exercise variants. --- .../exercises/ExerciseFormPageRoot.jsx | 123 ++++++++++++++++-- 1 file changed, 111 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/exercises/ExerciseFormPageRoot.jsx b/frontend/src/components/exercises/ExerciseFormPageRoot.jsx index 84eaac0..f9504a7 100644 --- a/frontend/src/components/exercises/ExerciseFormPageRoot.jsx +++ b/frontend/src/components/exercises/ExerciseFormPageRoot.jsx @@ -190,6 +190,26 @@ function buildVariantPayloadFromRow(row) { } } +function snapshotVariantPayload(row) { + return JSON.stringify(buildVariantPayloadFromRow(row)) +} + +function variantDraftHasContent(draft) { + if (!draft) return false + const p = buildVariantPayloadFromRow(draft) + return ( + p.variant_name.length > 0 || + Boolean(p.description) || + Boolean(p.execution_changes) || + p.duration_min != null || + p.duration_max != null || + (Array.isArray(p.equipment_changes) && p.equipment_changes.length > 0) || + Boolean(p.difficulty_adjustment) || + (p.progression_level != null && p.progression_level !== 1) || + p.prerequisite_variant_id != null + ) +} + /** Gemeinsame Felder für „Variante bearbeiten“ und „Neue Variante“. */ function ExerciseVariantFields({ row, @@ -480,6 +500,24 @@ function ExerciseFormPageRoot() { const [variantBusy, setVariantBusy] = useState(false) const [variantEditSelection, setVariantEditSelection] = useState(null) const variantsDetailsRef = useRef(null) + const variantsSavedSnapshotRef = useRef({}) + + const syncVariantsSavedSnapshot = useCallback((rows) => { + const snap = {} + for (const v of rows || []) { + if (v?.id != null) snap[v.id] = snapshotVariantPayload(v) + } + variantsSavedSnapshotRef.current = snap + }, []) + + const getDirtyVariantRows = useCallback((rows) => { + return (rows || []).filter((v) => { + if (v?.id == null) return false + const saved = variantsSavedSnapshotRef.current[v.id] + if (saved == null) return true + return snapshotVariantPayload(v) !== saved + }) + }, []) const [mediaFields, setMediaFields] = useState({}) const [mediaSavingId, setMediaSavingId] = useState(null) @@ -588,9 +626,11 @@ function ExerciseFormPageRoot() { try { const exercise = await api.getExercise(exerciseId) if (cancelled) return + const variantRows = (exercise.variants || []).map(apiVariantToRow) setFormData(detailToForm(exercise)) setMediaList(exercise.media || []) - setVariants((exercise.variants || []).map(apiVariantToRow)) + setVariants(variantRows) + syncVariantsSavedSnapshot(variantRows) setVariantDraft(emptyVariantDraft()) setVariantEditSelection(null) setFormDirty(false) @@ -774,12 +814,73 @@ function ExerciseFormPageRoot() { ) } + const refreshVariants = useCallback(async () => { + if (!exerciseId) return + const ex = await api.getExercise(exerciseId) + const rows = (ex.variants || []).map(apiVariantToRow) + syncVariantsSavedSnapshot(rows) + setVariants(rows) + }, [exerciseId, syncVariantsSavedSnapshot]) + + const persistPendingVariantChanges = useCallback(async () => { + if (!exerciseId) return true + + const dirtyRows = getDirtyVariantRows(variants) + if (dirtyRows.length > 0) { + setVariantBusy(true) + try { + for (const row of dirtyRows) { + const payload = buildVariantPayloadFromRow(row) + if (payload.variant_name.length < 3) { + toast.error(`Variante „${row.variant_name || `#${row.id}`}“: Name mindestens 3 Zeichen`) + return false + } + setVariantSavingId(row.id) + await api.updateExerciseVariant(exerciseId, row.id, payload) + } + await refreshVariants() + } catch (e) { + toast.error(e.message || String(e)) + return false + } finally { + setVariantSavingId(null) + setVariantBusy(false) + } + } + + if (variantDraftHasContent(variantDraft)) { + const payload = buildVariantPayloadFromRow(variantDraft) + if (payload.variant_name.length < 3) { + toast.error('Variantenentwurf: Name mindestens 3 Zeichen, sonst Felder verwerfen oder ausfüllen.') + return false + } + setVariantBusy(true) + try { + const created = await api.createExerciseVariant(exerciseId, payload) + setVariantDraft(emptyVariantDraft()) + if (created?.id != null) setVariantEditSelection(created.id) + await refreshVariants() + } catch (e) { + toast.error(e.message || String(e)) + return false + } finally { + setVariantBusy(false) + } + } + + return true + }, [exerciseId, variantDraft, variants, getDirtyVariantRows, refreshVariants, toast]) + const performSaveAttempt = useCallback( async ({ fromUnsavedDialog = false, closeAfter = false } = {}) => { if (!formData.title || formData.title.trim().length < 3) { toast.error('Titel mindestens 3 Zeichen') return false } + if (isEdit && exerciseId) { + const variantsOk = await persistPendingVariantChanges() + if (!variantsOk) return false + } const payloadBase = { ...formData, equipment: @@ -876,7 +977,9 @@ function ExerciseFormPageRoot() { } const ex = await api.getExercise(exerciseId) setMediaList(ex.media || []) - setVariants((ex.variants || []).map(apiVariantToRow)) + const variantRows = (ex.variants || []).map(apiVariantToRow) + setVariants(variantRows) + syncVariantsSavedSnapshot(variantRows) setFormDirty(false) toast.success('Gespeichert.') if (closeAfter) goBack() @@ -898,7 +1001,7 @@ function ExerciseFormPageRoot() { setSaving(false) } }, - [exerciseId, formData, isEdit, navigate, location, toast, goBack], + [exerciseId, formData, isEdit, navigate, location, toast, goBack, persistPendingVariantChanges, syncVariantsSavedSnapshot], ) const handleSubmit = useCallback( @@ -1028,12 +1131,6 @@ function ExerciseFormPageRoot() { } } - const refreshVariants = async () => { - if (!exerciseId) return - const ex = await api.getExercise(exerciseId) - setVariants((ex.variants || []).map(apiVariantToRow)) - } - const updateVariantField = (id, patch) => { setFormDirty(true) setVariants((prev) => prev.map((v) => (v.id === id ? { ...v, ...patch } : v))) @@ -1863,7 +1960,8 @@ function ExerciseFormPageRoot() {

Pro Durchgang nur eine Variante bearbeiten – weniger Scrollen. Reihenfolge entspricht Planung und Auswahl im - Training; „Voraussetzung“ nutzt ihr später für Progressions-Serien. + Training; „Voraussetzung“ nutzt ihr später für Progressions-Serien. Offene Varianten-Änderungen werden mit + „Speichern“ in der Aktionsleiste (oder in der Sicherheitsabfrage beim Verlassen) automatisch mitgespeichert.

{variants.length > 0 && ( @@ -1973,12 +2071,13 @@ function ExerciseFormPageRoot() {