From e4e362b0a9c17a058f16d3b190a69ecca97df7d6 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 14 May 2026 13:26:02 +0200 Subject: [PATCH] chore(version): update version and changelog for release 0.8.126 - Bumped APP_VERSION to 0.8.126 and updated the changelog to reflect recent changes. - Added the TrainingPlanningFrameworkImportModal component to the TrainingPlanningPage for improved training session management. - Implemented a new Playwright test to verify the functionality of the framework import dialog in the training planning page. --- backend/version.py | 9 +- .../TrainingPlanningFrameworkImportModal.jsx | 210 +++++++++++++++++ frontend/src/pages/TrainingPlanningPage.jsx | 215 ++---------------- tests/dev-smoke-test.spec.js | 22 ++ 4 files changed, 263 insertions(+), 193 deletions(-) create mode 100644 frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx diff --git a/backend/version.py b/backend/version.py index 6fdba72..090590e 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.125" +APP_VERSION = "0.8.126" BUILD_DATE = "2026-05-12" DB_SCHEMA_VERSION = "20260514062" @@ -36,6 +36,13 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.126", + "date": "2026-05-13", + "changes": [ + "Frontend Phase 3: TrainingPlanningFrameworkImportModal aus Trainingsplanungsseite; Playwright-Test 13 (Rahmen-Dialog, skip ohne Gruppe).", + ], + }, { "version": "0.8.125", "date": "2026-05-13", diff --git a/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx b/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx new file mode 100644 index 0000000..3a499e9 --- /dev/null +++ b/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx @@ -0,0 +1,210 @@ +import React from 'react' + +/** + * Modal: geplante Einheiten aus einem Trainingsrahmenprogramm (Blueprint-Slots) erzeugen. + */ +export default function TrainingPlanningFrameworkImportModal({ + open, + frameworkProgramsList, + fwImportProgramId, + onProgramChange, + fwImportLoading, + fwImportDetail, + fwImportSelectedSlots, + onToggleSlot, + fwImportSlotDates, + onSlotDateChange, + fwImportStartDate, + onFwImportStartDateChange, + fwImportIntervalDays, + onFwImportIntervalDaysChange, + fwImportSubmitting, + onApplyDateSuggestions, + onSubmit, + onClose, +}) { + if (!open) return null + + return ( +
+
+

Sessions aus Rahmen übernehmen

+

+ Wähle ein Trainingsrahmenprogramm und eine oder mehrere Sessions. Pro Session entsteht eine{' '} + eigene geplante Einheit in der aktuellen Gruppe (Kopie des Ablaufs). Die{' '} + Verknüpfung zum Rahmen-Slot wird gespeichert, damit die Herkunft sichtbar bleibt. +

+ +
+ + +
+ + {fwImportLoading ? ( +

Laden der Sessions…

+ ) : fwImportDetail?.slots?.length ? ( + <> +
+ + Sessions (mit Ablauf) + +
    + {[...fwImportDetail.slots] + .sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) + .map((slot) => { + const hasBp = !!slot.blueprint_training_unit_id + const checked = fwImportSelectedSlots.has(slot.id) + const label = + (slot.title || '').trim() || `Session ${(slot.sort_order ?? 0) + 1}` + return ( +
  • + +
  • + ) + })} +
+
+ +
+
+ + onFwImportStartDateChange(e.target.value)} + disabled={fwImportSubmitting} + /> +
+
+ + onFwImportIntervalDaysChange(parseInt(e.target.value, 10) || 0)} + disabled={fwImportSubmitting} + /> +
+
+ +
+
+ + ) : fwImportProgramId ? ( +

Keine Sessions in diesem Programm.

+ ) : null} + +
+ + +
+
+
+ ) +} diff --git a/frontend/src/pages/TrainingPlanningPage.jsx b/frontend/src/pages/TrainingPlanningPage.jsx index 240fb09..5cffedf 100644 --- a/frontend/src/pages/TrainingPlanningPage.jsx +++ b/frontend/src/pages/TrainingPlanningPage.jsx @@ -9,6 +9,7 @@ import ExercisePeekModal from '../components/ExercisePeekModal' import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor' import TrainingPlanExerciseVisibilityPanel from '../components/TrainingPlanExerciseVisibilityPanel' import PageSectionNav from '../components/PageSectionNav' +import TrainingPlanningFrameworkImportModal from '../components/planning/TrainingPlanningFrameworkImportModal' import { defaultSection, normalizeUnitToForm, @@ -2287,198 +2288,28 @@ function TrainingPlanningPage() { )} - {frameworkImportOpen && ( -
-
-

Sessions aus Rahmen übernehmen

-

- Wähle ein Trainingsrahmenprogramm und eine oder mehrere Sessions. Pro Session entsteht eine{' '} - eigene geplante Einheit in der aktuellen Gruppe (Kopie des Ablaufs). Die{' '} - Verknüpfung zum Rahmen-Slot wird gespeichert, damit die Herkunft sichtbar bleibt. -

- -
- - -
- - {fwImportLoading ? ( -

Laden der Sessions…

- ) : fwImportDetail?.slots?.length ? ( - <> -
- - Sessions (mit Ablauf) - -
    - {[...fwImportDetail.slots] - .sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) - .map((slot) => { - const hasBp = !!slot.blueprint_training_unit_id - const checked = fwImportSelectedSlots.has(slot.id) - const label = - (slot.title || '').trim() || - `Session ${(slot.sort_order ?? 0) + 1}` - return ( -
  • - -
  • - ) - })} -
-
- -
-
- - setFwImportStartDate(e.target.value)} - disabled={fwImportSubmitting} - /> -
-
- - setFwImportIntervalDays(parseInt(e.target.value, 10) || 0)} - disabled={fwImportSubmitting} - /> -
-
- -
-
- - ) : fwImportProgramId ? ( -

Keine Sessions in diesem Programm.

- ) : null} - -
- - -
-
-
- )} + + setFwImportSlotDates((prev) => ({ ...prev, [slotId]: value })) + } + fwImportStartDate={fwImportStartDate} + onFwImportStartDateChange={setFwImportStartDate} + fwImportIntervalDays={fwImportIntervalDays} + onFwImportIntervalDaysChange={setFwImportIntervalDays} + fwImportSubmitting={fwImportSubmitting} + onApplyDateSuggestions={applyFwImportDateSuggestions} + onSubmit={submitFrameworkImport} + onClose={() => setFrameworkImportOpen(false)} + /> {showModal && (
{ console.log('✓ Trainingsplanung: Grundansicht'); }); +test('13. Trainingsplanung: Rahmen-Import-Dialog öffnet und schließt', async ({ page }, testInfo) => { + await login(page); + await page.goto('/planning', { waitUntil: 'networkidle' }); + const main = page.locator('.app-main'); + await expect(main.locator('.spinner')).toHaveCount(0, { timeout: 25000 }); + await expect(main.getByRole('heading', { level: 1, name: 'Trainingsplanung' })).toBeVisible({ + timeout: 20000, + }); + const openBtn = main.getByRole('button', { name: /Aus Rahmen übernehmen/i }); + if (await openBtn.isDisabled()) { + testInfo.skip(true, 'Keine Trainingsgruppe — Button bleibt deaktiviert'); + return; + } + await openBtn.click(); + const dlg = page.getByTestId('planning-framework-import-modal'); + await expect(dlg).toBeVisible({ timeout: 10000 }); + await expect(dlg.getByRole('heading', { name: /Sessions aus Rahmen übernehmen/i })).toBeVisible(); + await dlg.getByRole('button', { name: 'Abbrechen' }).click(); + await expect(dlg).toHaveCount(0); + console.log('✓ Trainingsplanung: Rahmen-Import-Dialog Smoke'); +}); + test('P-12: sessionStorage wird bei Logout bereinigt (sj_coach_* Schlüssel)', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 800 }); await login(page);