All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m30s
- 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.
211 lines
7.9 KiB
JavaScript
211 lines
7.9 KiB
JavaScript
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 (
|
|
<div
|
|
data-testid="planning-framework-import-modal"
|
|
style={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: 'rgba(0,0,0,0.5)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
zIndex: 1010,
|
|
padding: '1rem',
|
|
overflowY: 'auto',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
background: 'var(--surface)',
|
|
borderRadius: '12px',
|
|
padding: 'clamp(14px, 3vw, 1.75rem)',
|
|
maxWidth: 'min(620px, 100%)',
|
|
width: '100%',
|
|
maxHeight: '90vh',
|
|
overflowY: 'auto',
|
|
boxSizing: 'border-box',
|
|
minWidth: 0,
|
|
}}
|
|
>
|
|
<h2 style={{ marginBottom: '0.65rem' }}>Sessions aus Rahmen übernehmen</h2>
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', marginBottom: '1rem', lineHeight: 1.5 }}>
|
|
Wähle ein Trainingsrahmenprogramm und eine oder mehrere Sessions. Pro Session entsteht eine{' '}
|
|
<strong>eigene geplante Einheit</strong> in der aktuellen Gruppe (Kopie des Ablaufs). Die{' '}
|
|
<strong>Verknüpfung zum Rahmen-Slot</strong> wird gespeichert, damit die Herkunft sichtbar bleibt.
|
|
</p>
|
|
|
|
<div className="form-row">
|
|
<label className="form-label">Rahmenprogramm</label>
|
|
<select
|
|
className="form-input"
|
|
value={fwImportProgramId}
|
|
onChange={(e) => onProgramChange(e.target.value)}
|
|
disabled={fwImportLoading || fwImportSubmitting}
|
|
>
|
|
<option value="">Bitte wählen…</option>
|
|
{frameworkProgramsList.map((fp) => (
|
|
<option key={fp.id} value={String(fp.id)}>
|
|
{(fp.title || '').trim() || `Rahmen #${fp.id}`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{fwImportLoading ? (
|
|
<p style={{ color: 'var(--text2)', marginTop: '1rem' }}>Laden der Sessions…</p>
|
|
) : fwImportDetail?.slots?.length ? (
|
|
<>
|
|
<fieldset style={{ border: 'none', margin: '1rem 0', padding: 0 }}>
|
|
<legend className="form-label" style={{ padding: 0, marginBottom: '0.5rem' }}>
|
|
Sessions (mit Ablauf)
|
|
</legend>
|
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
|
{[...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 (
|
|
<li key={slot.id} style={{ marginBottom: '10px' }}>
|
|
<label
|
|
style={{
|
|
display: 'flex',
|
|
gap: '10px',
|
|
alignItems: 'flex-start',
|
|
cursor: hasBp ? 'pointer' : 'not-allowed',
|
|
opacity: hasBp ? 1 : 0.55,
|
|
}}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={checked}
|
|
disabled={!hasBp || fwImportSubmitting}
|
|
onChange={() => onToggleSlot(slot)}
|
|
style={{ marginTop: '0.2rem', flexShrink: 0 }}
|
|
/>
|
|
<span style={{ flex: 1, minWidth: 0 }}>
|
|
<strong>{label}</strong>
|
|
{!hasBp ? (
|
|
<span style={{ display: 'block', fontSize: '0.82rem', color: 'var(--danger)' }}>
|
|
Ohne Session-Ablauf — Übernahme nicht möglich.
|
|
</span>
|
|
) : null}
|
|
{hasBp && checked ? (
|
|
<span style={{ display: 'block', marginTop: '6px' }}>
|
|
<span className="form-label" style={{ fontSize: '0.78rem' }}>
|
|
Termin (Datum)
|
|
</span>
|
|
<input
|
|
type="date"
|
|
className="form-input"
|
|
style={{ maxWidth: '200px', marginTop: '4px' }}
|
|
value={fwImportSlotDates[String(slot.id)] || ''}
|
|
onChange={(e) => onSlotDateChange(String(slot.id), e.target.value)}
|
|
disabled={fwImportSubmitting}
|
|
/>
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
</label>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</fieldset>
|
|
|
|
<div
|
|
className="responsive-grid-3"
|
|
style={{
|
|
marginBottom: '0.75rem',
|
|
padding: '12px',
|
|
background: 'var(--surface2)',
|
|
borderRadius: '8px',
|
|
}}
|
|
>
|
|
<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>
|
|
<div className="form-row" style={{ alignSelf: 'end' }}>
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
style={{ width: '100%' }}
|
|
disabled={fwImportSubmitting}
|
|
onClick={onApplyDateSuggestions}
|
|
>
|
|
Datumsvorschläge setzen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : fwImportProgramId ? (
|
|
<p style={{ color: 'var(--text2)', marginTop: '0.75rem' }}>Keine Sessions in diesem Programm.</p>
|
|
) : null}
|
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginTop: '1.25rem' }}>
|
|
<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>
|
|
)
|
|
}
|