diff --git a/backend/version.py b/backend/version.py index 090590e..b3e05cf 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.126" +APP_VERSION = "0.8.128" BUILD_DATE = "2026-05-12" DB_SCHEMA_VERSION = "20260514062" @@ -36,6 +36,13 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.128", + "date": "2026-05-13", + "changes": [ + "Frontend Phase 3: TrainingPlanningModuleApplyModal (Trainingsmodul einfügen) aus Trainingsplanungsseite; gemeinsamer Callback onModuleApplySectionIndexChange.", + ], + }, { "version": "0.8.126", "date": "2026-05-13", diff --git a/frontend/src/components/planning/TrainingPlanningModuleApplyModal.jsx b/frontend/src/components/planning/TrainingPlanningModuleApplyModal.jsx new file mode 100644 index 0000000..8d9ae91 --- /dev/null +++ b/frontend/src/components/planning/TrainingPlanningModuleApplyModal.jsx @@ -0,0 +1,319 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import { trainingVisibilityShortDE } from '../../utils/trainingPlanningPageHelpers' + +/** + * Dialog: Trainingsmodul in die Abschnitte einer Einheit einfügen (Bibliothekskopie). + */ +export default function TrainingPlanningModuleApplyModal({ + open, + busy, + err, + placementLocked, + placementSummary, + sections, + sectionIx, + onSectionIndexChange, + insertSlot, + onInsertSlotChange, + targetItems, + searchQuery, + onSearchQueryChange, + filteredList, + fullList, + selectedModuleId, + onSelectModuleId, + modulePickPreview, + onConfirm, + onCancel, +}) { + if (!open) return null + + const handleBackdropMouseDown = (ev) => { + if (ev.target !== ev.currentTarget || busy) return + onCancel() + } + + return ( +
+
+

+ Trainingsmodul einfügen +

+

+ Alle Positionen des gewählten Moduls werden als neue Zeilen eingefügt (Kopie, mit klarer + Herkunft im Ablauf). Die Einheit brauchst du dafür nicht vorher gespeichert zu haben — Speichern am Ende + wie gewohnt. Vollständige Textsuche oder Modulkategorien planen wir serverseitig für + eine spätere Iteration; vorerst steht hier eine{' '} + Schnellsuche über Titel und Freitext-Felder zur Verfügung. +

+ + {err ? ( +

{err}

+ ) : null} + + {placementLocked ? ( + <> +

+ Aktuelle Einfügeposition: Abschnitt {placementSummary.secTitle}{' '} + / {placementSummary.positionDescription} +

+
+ Abschnitt oder Position ändern +
+
+ + +
+
+ + +
+
+
+ + ) : ( + <> +
+ + +
+ +
+ + +
+ + )} + +
+ + onSearchQueryChange(e.target.value)} + disabled={busy} + aria-label="Module durch Freitext filtern" + /> +
+ +
+ +
+
+ {!filteredList.length ? ( +

+ {!fullList.length ? 'Keine Module verfügbar oder keine Berechtigung.' : 'Kein Modul entspricht der Suche.'} +

+ ) : ( + filteredList.map((m) => { + const title = ((m.title || '').trim() || `Modul #${m.id}`).trim() + const visLbl = trainingVisibilityShortDE(m.visibility) + const nPos = typeof m.items_count === 'number' ? m.items_count : '—' + const selected = String(m.id) === String(selectedModuleId) + return ( + + ) + }) + )} +
+ + {selectedModuleId ? ( +
+
Ablauf-Vorschau (Bibliotheksmodul)
+ {modulePickPreview.loading ? ( +

+ Übungen und Hinweise laden … +

+ ) : modulePickPreview.err ? ( +

+ {modulePickPreview.err} +

+ ) : !modulePickPreview.exercises.length && !modulePickPreview.notes ? ( +

+ Keine Übungspositionen in diesem Eintrag gefunden (prüfen, ob Übungen im Modul gültige IDs haben). +

+ ) : ( + <> +
    + {(modulePickPreview.exercises.slice(0, 12)).map((t, qi) => ( +
  1. {t}
  2. + ))} +
+ {modulePickPreview.exercises.length > 12 ? ( +

+ … und noch {modulePickPreview.exercises.length - 12} weitere Übungen in genau dieser Modulreihenfolge. +

+ ) : null} + {modulePickPreview.notes > 0 ? ( +

+ zusätzlich {modulePickPreview.notes}{' '} + {modulePickPreview.notes === 1 ? 'Position mit Hinweis' : 'Positionen mit Hinweisen'}{' '} + (ohne Aufzählung) +

+ ) : null} + + )} +
+ ) : null} + +
+ + +
+ +

+ Neue Module kannst du unter{' '} + + Trainingsmodule + {' '} + anlegen. +

+
+
+ ) +} diff --git a/frontend/src/pages/TrainingPlanningPage.jsx b/frontend/src/pages/TrainingPlanningPage.jsx index 5cffedf..f491942 100644 --- a/frontend/src/pages/TrainingPlanningPage.jsx +++ b/frontend/src/pages/TrainingPlanningPage.jsx @@ -10,6 +10,7 @@ import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor import TrainingPlanExerciseVisibilityPanel from '../components/TrainingPlanExerciseVisibilityPanel' import PageSectionNav from '../components/PageSectionNav' import TrainingPlanningFrameworkImportModal from '../components/planning/TrainingPlanningFrameworkImportModal' +import TrainingPlanningModuleApplyModal from '../components/planning/TrainingPlanningModuleApplyModal' import { defaultSection, normalizeUnitToForm, @@ -19,7 +20,6 @@ import { insertTrainingModuleIntoPlanningSections, } from '../utils/trainingUnitSectionsForm' import { - trainingVisibilityShortDE, addDaysIsoDate, pad2, toIsoLocal, @@ -709,6 +709,13 @@ function TrainingPlanningPage() { } }, []) + const onModuleApplySectionIndexChange = useCallback((newIx) => { + setModuleApplySectionIx(newIx) + const secsNow = planningFormRef.current?.sections ?? [] + const len = Array.isArray(secsNow[newIx]?.items) ? secsNow[newIx].items.length : 0 + setModuleApplyInsertSlot(`before:${len}`) + }, []) + const handleApplyTrainingModuleConfirm = useCallback(async () => { const mid = parseInt(moduleApplyModuleId, 10) if (!Number.isFinite(mid)) { @@ -1987,306 +1994,31 @@ function TrainingPlanningPage() { ) : null} - {moduleApplyOpen && ( -
{ - if (ev.target !== ev.currentTarget || moduleApplyBusy) return - setModuleApplyOpen(false) - setModuleApplyPlacementLocked(false) - }} - > -
-

- Trainingsmodul einfügen -

-

- Alle Positionen des gewählten Moduls werden als neue Zeilen eingefügt (Kopie, mit klarer - Herkunft im Ablauf). Die Einheit brauchst du dafür nicht vorher gespeichert zu haben — Speichern am Ende - wie gewohnt. Vollständige Textsuche oder Modulkategorien planen wir serverseitig für - eine spätere Iteration; vorerst steht hier eine{' '} - Schnellsuche über Titel und Freitext-Felder zur Verfügung. -

- - {moduleApplyErr ? ( -

{moduleApplyErr}

- ) : null} - - {moduleApplyPlacementLocked ? ( - <> -

- Aktuelle Einfügeposition: Abschnitt {modulePlacementSummary.secTitle}{' '} - / {modulePlacementSummary.positionDescription} -

-
- Abschnitt oder Position ändern -
-
- - -
-
- - -
-
-
- - ) : ( - <> -
- - -
- -
- - -
- - )} - -
- - setModuleApplySearchQuery(e.target.value)} - disabled={moduleApplyBusy} - aria-label="Module durch Freitext filtern" - /> -
- -
- -
-
- {!moduleApplyFilteredList.length ? ( -

- {!moduleApplyList.length ? 'Keine Module verfügbar oder keine Berechtigung.' : 'Kein Modul entspricht der Suche.'} -

- ) : ( - moduleApplyFilteredList.map((m) => { - const title = ((m.title || '').trim() || `Modul #${m.id}`).trim() - const visLbl = trainingVisibilityShortDE(m.visibility) - const nPos = typeof m.items_count === 'number' ? m.items_count : '—' - const selected = String(m.id) === String(moduleApplyModuleId) - return ( - - ) - }) - )} -
- - {moduleApplyModuleId ? ( -
-
Ablauf-Vorschau (Bibliotheksmodul)
- {modulePickPreview.loading ? ( -

- Übungen und Hinweise laden … -

- ) : modulePickPreview.err ? ( -

- {modulePickPreview.err} -

- ) : !modulePickPreview.exercises.length && !modulePickPreview.notes ? ( -

- Keine Übungspositionen in diesem Eintrag gefunden (prüfen, ob Übungen im Modul gültige IDs haben). -

- ) : ( - <> -
    - {(modulePickPreview.exercises.slice(0, 12)).map((t, qi) => ( -
  1. {t}
  2. - ))} -
- {modulePickPreview.exercises.length > 12 ? ( -

- … und noch {modulePickPreview.exercises.length - 12} weitere Übungen in genau dieser Modulreihenfolge. -

- ) : null} - {modulePickPreview.notes > 0 ? ( -

- zusätzlich {modulePickPreview.notes}{' '} - {modulePickPreview.notes === 1 ? 'Position mit Hinweis' : 'Positionen mit Hinweisen'}{' '} - (ohne Aufzählung) -

- ) : null} - - )} -
- ) : null} - -
- - -
- -

- Neue Module kannst du unter{' '} - - Trainingsmodule - {' '} - anlegen. -

-
-
- )} + { + setModuleApplyOpen(false) + setModuleApplyPlacementLocked(false) + }} + />