2026-05-20 15:06:03 +02:00
|
|
|
import React, { useEffect, useMemo, useState } from 'react'
|
2026-05-20 16:05:18 +02:00
|
|
|
import FrameworkProgramsFilterBlock from './FrameworkProgramsFilterBlock'
|
2026-05-20 13:19:40 +02:00
|
|
|
import {
|
2026-05-20 15:06:03 +02:00
|
|
|
EMPTY_FRAMEWORK_IMPORT_FILTERS,
|
2026-05-20 13:19:40 +02:00
|
|
|
filterFrameworkPrograms,
|
|
|
|
|
frameworkProgramOptionLabel,
|
|
|
|
|
frameworkSessionDurationLabel,
|
|
|
|
|
} from '../../utils/frameworkProgramListHelpers'
|
|
|
|
|
import { formatDurationDisplay } from '../../utils/trainingDurationUtils'
|
2026-05-14 13:26:02 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Modal: geplante Einheiten aus einem Trainingsrahmenprogramm (Blueprint-Slots) erzeugen.
|
|
|
|
|
*/
|
|
|
|
|
export default function TrainingPlanningFrameworkImportModal({
|
|
|
|
|
open,
|
|
|
|
|
frameworkProgramsList,
|
2026-05-20 13:19:40 +02:00
|
|
|
catalogFocusAreas = [],
|
|
|
|
|
catalogTrainingTypes = [],
|
|
|
|
|
catalogTargetGroups = [],
|
2026-05-14 13:26:02 +02:00
|
|
|
fwImportProgramId,
|
|
|
|
|
onProgramChange,
|
|
|
|
|
fwImportLoading,
|
|
|
|
|
fwImportDetail,
|
|
|
|
|
fwImportSelectedSlots,
|
|
|
|
|
onToggleSlot,
|
|
|
|
|
fwImportSlotDates,
|
|
|
|
|
onSlotDateChange,
|
|
|
|
|
fwImportStartDate,
|
|
|
|
|
onFwImportStartDateChange,
|
|
|
|
|
fwImportIntervalDays,
|
|
|
|
|
onFwImportIntervalDaysChange,
|
|
|
|
|
fwImportSubmitting,
|
|
|
|
|
onApplyDateSuggestions,
|
|
|
|
|
onSubmit,
|
|
|
|
|
onClose,
|
|
|
|
|
}) {
|
2026-05-20 15:06:03 +02:00
|
|
|
const [filters, setFilters] = useState(() => ({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS }))
|
|
|
|
|
const [filterPanelOpen, setFilterPanelOpen] = useState(true)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
setFilters({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS })
|
|
|
|
|
setFilterPanelOpen(true)
|
|
|
|
|
}
|
|
|
|
|
}, [open])
|
|
|
|
|
|
2026-05-20 13:19:40 +02:00
|
|
|
const filteredPrograms = useMemo(
|
2026-05-20 15:06:03 +02:00
|
|
|
() => filterFrameworkPrograms(frameworkProgramsList, filters),
|
|
|
|
|
[frameworkProgramsList, filters]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const matchCount = filteredPrograms.length
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!fwImportProgramId) return
|
|
|
|
|
const stillVisible = filteredPrograms.some((p) => String(p.id) === String(fwImportProgramId))
|
|
|
|
|
if (!stillVisible) onProgramChange('')
|
|
|
|
|
}, [filteredPrograms, fwImportProgramId, onProgramChange])
|
|
|
|
|
|
2026-05-20 13:19:40 +02:00
|
|
|
const selectedProgramSummary = useMemo(() => {
|
|
|
|
|
if (!fwImportProgramId) return null
|
|
|
|
|
return (frameworkProgramsList || []).find((p) => String(p.id) === String(fwImportProgramId))
|
|
|
|
|
}, [frameworkProgramsList, fwImportProgramId])
|
|
|
|
|
|
2026-05-14 13:26:02 +02:00
|
|
|
if (!open) return null
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
2026-05-20 15:06:03 +02:00
|
|
|
className="fw-import-modal-backdrop"
|
2026-05-14 13:26:02 +02:00
|
|
|
data-testid="planning-framework-import-modal"
|
|
|
|
|
>
|
2026-05-20 15:06:03 +02:00
|
|
|
<div className="fw-import-modal-panel">
|
|
|
|
|
<h2 className="fw-import-modal-panel__title">Sessions aus Rahmen übernehmen</h2>
|
|
|
|
|
<p className="fw-import-modal-panel__lead">
|
2026-05-14 13:26:02 +02:00
|
|
|
Wähle ein Trainingsrahmenprogramm und eine oder mehrere Sessions. Pro Session entsteht eine{' '}
|
2026-05-20 15:06:03 +02:00
|
|
|
<strong>eigene geplante Einheit</strong> in der aktuellen Gruppe (Kopie des Ablaufs).
|
2026-05-14 13:26:02 +02:00
|
|
|
</p>
|
|
|
|
|
|
2026-05-20 16:05:18 +02:00
|
|
|
<FrameworkProgramsFilterBlock
|
|
|
|
|
programs={frameworkProgramsList}
|
|
|
|
|
filters={filters}
|
|
|
|
|
onFiltersChange={setFilters}
|
|
|
|
|
panelOpen={filterPanelOpen}
|
|
|
|
|
onPanelOpenChange={setFilterPanelOpen}
|
|
|
|
|
catalogFocusAreas={catalogFocusAreas}
|
|
|
|
|
catalogTrainingTypes={catalogTrainingTypes}
|
|
|
|
|
catalogTargetGroups={catalogTargetGroups}
|
|
|
|
|
disabled={fwImportSubmitting}
|
|
|
|
|
durationRadioName="fw-duration-mode"
|
|
|
|
|
/>
|
2026-05-20 13:19:40 +02:00
|
|
|
|
2026-05-20 15:06:03 +02:00
|
|
|
<div className="form-row fw-import-program-select">
|
|
|
|
|
<label className="form-label">
|
|
|
|
|
Rahmenprogramm
|
|
|
|
|
<span className="fw-import-program-select__count">
|
|
|
|
|
{' '}
|
|
|
|
|
({matchCount} {matchCount === 1 ? 'Treffer' : 'Treffer'})
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
2026-05-14 13:26:02 +02:00
|
|
|
<select
|
|
|
|
|
className="form-input"
|
|
|
|
|
value={fwImportProgramId}
|
|
|
|
|
onChange={(e) => onProgramChange(e.target.value)}
|
2026-05-20 15:06:03 +02:00
|
|
|
disabled={fwImportLoading || fwImportSubmitting || matchCount === 0}
|
2026-05-14 13:26:02 +02:00
|
|
|
>
|
2026-05-20 15:06:03 +02:00
|
|
|
<option value="">
|
|
|
|
|
{matchCount === 0 ? 'Kein Rahmenprogramm passt zum Filter' : 'Bitte wählen…'}
|
|
|
|
|
</option>
|
2026-05-20 13:19:40 +02:00
|
|
|
{filteredPrograms.map((fp) => (
|
2026-05-14 13:26:02 +02:00
|
|
|
<option key={fp.id} value={String(fp.id)}>
|
2026-05-20 13:19:40 +02:00
|
|
|
{frameworkProgramOptionLabel(fp)}
|
2026-05-14 13:26:02 +02:00
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
2026-05-20 13:19:40 +02:00
|
|
|
{selectedProgramSummary ? (
|
|
|
|
|
<p className="form-sub" style={{ marginTop: '6px' }}>
|
|
|
|
|
Session-Dauer: <strong>{frameworkSessionDurationLabel(selectedProgramSummary)}</strong>
|
|
|
|
|
{selectedProgramSummary.goal_titles_agg ? (
|
|
|
|
|
<>
|
|
|
|
|
{' '}
|
|
|
|
|
· Ziele: {selectedProgramSummary.goal_titles_agg}
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
2026-05-14 13:26:02 +02:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{fwImportLoading ? (
|
2026-05-20 15:06:03 +02:00
|
|
|
<p className="fw-import-muted">Laden der Sessions…</p>
|
2026-05-14 13:26:02 +02:00
|
|
|
) : fwImportDetail?.slots?.length ? (
|
|
|
|
|
<>
|
2026-05-20 15:06:03 +02:00
|
|
|
<fieldset className="fw-import-sessions">
|
|
|
|
|
<legend className="form-label">Sessions (mit Ablauf)</legend>
|
|
|
|
|
<ul className="fw-import-sessions__list">
|
2026-05-14 13:26:02 +02:00
|
|
|
{[...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}`
|
2026-05-20 13:19:40 +02:00
|
|
|
const slotDur =
|
|
|
|
|
slot.planned_duration_min != null
|
|
|
|
|
? formatDurationDisplay(slot.planned_duration_min)
|
|
|
|
|
: null
|
2026-05-14 13:26:02 +02:00
|
|
|
return (
|
2026-05-20 15:06:03 +02:00
|
|
|
<li key={slot.id} className="fw-import-sessions__item">
|
2026-05-14 13:26:02 +02:00
|
|
|
<label
|
2026-05-20 15:06:03 +02:00
|
|
|
className={
|
|
|
|
|
'fw-import-sessions__label' + (hasBp ? '' : ' fw-import-sessions__label--disabled')
|
|
|
|
|
}
|
2026-05-14 13:26:02 +02:00
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
2026-05-20 15:06:03 +02:00
|
|
|
className="fw-import-sessions__check"
|
2026-05-14 13:26:02 +02:00
|
|
|
checked={checked}
|
|
|
|
|
disabled={!hasBp || fwImportSubmitting}
|
|
|
|
|
onChange={() => onToggleSlot(slot)}
|
|
|
|
|
/>
|
2026-05-20 15:06:03 +02:00
|
|
|
<span className="fw-import-sessions__body">
|
|
|
|
|
<span className="fw-import-sessions__title-row">
|
|
|
|
|
<strong>{label}</strong>
|
|
|
|
|
{slotDur ? (
|
|
|
|
|
<span className="fw-import-sessions__dur">{slotDur}</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="fw-import-sessions__dur fw-import-sessions__dur--muted">
|
|
|
|
|
Dauer offen
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
2026-05-14 13:26:02 +02:00
|
|
|
{!hasBp ? (
|
2026-05-20 15:06:03 +02:00
|
|
|
<span className="fw-import-sessions__warn">
|
2026-05-14 13:26:02 +02:00
|
|
|
Ohne Session-Ablauf — Übernahme nicht möglich.
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
{hasBp && checked ? (
|
2026-05-20 15:06:03 +02:00
|
|
|
<span className="fw-import-sessions__date">
|
|
|
|
|
<span className="form-label">Termin (Datum)</span>
|
2026-05-14 13:26:02 +02:00
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
className="form-input"
|
|
|
|
|
value={fwImportSlotDates[String(slot.id)] || ''}
|
|
|
|
|
onChange={(e) => onSlotDateChange(String(slot.id), e.target.value)}
|
|
|
|
|
disabled={fwImportSubmitting}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
</li>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
2026-05-20 15:06:03 +02:00
|
|
|
<div className="fw-import-dates-panel responsive-grid-3">
|
2026-05-14 13:26:02 +02:00
|
|
|
<div className="form-row">
|
|
|
|
|
<label className="form-label">Startdatum (Vorschlag)</label>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
className="form-input"
|
|
|
|
|
value={fwImportStartDate}
|
|
|
|
|
onChange={(e) => onFwImportStartDateChange(e.target.value)}
|
|
|
|
|
disabled={fwImportSubmitting}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
<label className="form-label">Abstand (Tage)</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
min={0}
|
|
|
|
|
className="form-input"
|
|
|
|
|
value={fwImportIntervalDays}
|
|
|
|
|
onChange={(e) => onFwImportIntervalDaysChange(parseInt(e.target.value, 10) || 0)}
|
|
|
|
|
disabled={fwImportSubmitting}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-05-20 15:06:03 +02:00
|
|
|
<div className="form-row fw-import-dates-panel__action">
|
2026-05-14 13:26:02 +02:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
disabled={fwImportSubmitting}
|
|
|
|
|
onClick={onApplyDateSuggestions}
|
|
|
|
|
>
|
|
|
|
|
Datumsvorschläge setzen
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : fwImportProgramId ? (
|
2026-05-20 15:06:03 +02:00
|
|
|
<p className="fw-import-muted">Keine Sessions in diesem Programm.</p>
|
2026-05-14 13:26:02 +02:00
|
|
|
) : null}
|
|
|
|
|
|
2026-05-20 15:06:03 +02:00
|
|
|
<div className="fw-import-modal-panel__footer">
|
2026-05-14 13:26:02 +02:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
disabled={fwImportSubmitting || !fwImportDetail}
|
|
|
|
|
onClick={onSubmit}
|
|
|
|
|
>
|
|
|
|
|
{fwImportSubmitting ? 'Übernehmen…' : 'In Planung übernehmen'}
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" className="btn btn-secondary" disabled={fwImportSubmitting} onClick={onClose}>
|
|
|
|
|
Abbrechen
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|