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.
+
+
+
+ Rahmenprogramm
+ onProgramChange(e.target.value)}
+ disabled={fwImportLoading || fwImportSubmitting}
+ >
+ Bitte wählen…
+ {frameworkProgramsList.map((fp) => (
+
+ {(fp.title || '').trim() || `Rahmen #${fp.id}`}
+
+ ))}
+
+
+
+ {fwImportLoading ? (
+
Laden der Sessions…
+ ) : fwImportDetail?.slots?.length ? (
+ <>
+
+
+ Sessions (mit Ablauf)
+
+
+
+
+
+
+ Startdatum (Vorschlag)
+ onFwImportStartDateChange(e.target.value)}
+ disabled={fwImportSubmitting}
+ />
+
+
+ Abstand (Tage)
+ onFwImportIntervalDaysChange(parseInt(e.target.value, 10) || 0)}
+ disabled={fwImportSubmitting}
+ />
+
+
+
+ Datumsvorschläge setzen
+
+
+
+ >
+ ) : fwImportProgramId ? (
+
Keine Sessions in diesem Programm.
+ ) : null}
+
+
+
+ {fwImportSubmitting ? 'Übernehmen…' : 'In Planung übernehmen'}
+
+
+ Abbrechen
+
+
+
+
+ )
+}
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.
-
-
-
- Rahmenprogramm
- onFwImportProgramChange(e.target.value)}
- disabled={fwImportLoading || fwImportSubmitting}
- >
- Bitte wählen…
- {frameworkProgramsList.map((fp) => (
-
- {(fp.title || '').trim() || `Rahmen #${fp.id}`}
-
- ))}
-
-
-
- {fwImportLoading ? (
-
Laden der Sessions…
- ) : fwImportDetail?.slots?.length ? (
- <>
-
-
- Sessions (mit Ablauf)
-
-
-
-
-
-
- Startdatum (Vorschlag)
- setFwImportStartDate(e.target.value)}
- disabled={fwImportSubmitting}
- />
-
-
- Abstand (Tage)
- setFwImportIntervalDays(parseInt(e.target.value, 10) || 0)}
- disabled={fwImportSubmitting}
- />
-
-
-
- Datumsvorschläge setzen
-
-
-
- >
- ) : fwImportProgramId ? (
-
Keine Sessions in diesem Programm.
- ) : null}
-
-
-
- {fwImportSubmitting ? 'Übernehmen…' : 'In Planung übernehmen'}
-
- setFrameworkImportOpen(false)}
- >
- Abbrechen
-
-
-
-
- )}
+
+ 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);