shinkan-jinkendo/frontend/src/pages/TrainingPlanningPage.jsx

898 lines
33 KiB
React
Raw Normal View History

import React, { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'
import api from '../utils/api'
import { useAuth } from '../context/AuthContext'
import ExercisePickerModal from '../components/ExercisePickerModal'
import ExercisePeekModal from '../components/ExercisePeekModal'
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
import {
defaultSection,
normalizeUnitToForm,
enrichSectionsWithVariants,
buildSectionsPayload,
hydrateExercisePlanningRow,
} from '../utils/trainingUnitSectionsForm'
function TrainingPlanningPage() {
const { user } = useAuth()
const [groups, setGroups] = useState([])
const [selectedGroupId, setSelectedGroupId] = useState('')
const [units, setUnits] = useState([])
const [planTemplates, setPlanTemplates] = useState([])
const [loading, setLoading] = useState(true)
const [showModal, setShowModal] = useState(false)
const [editingUnit, setEditingUnit] = useState(null)
const [draftPlanTemplateId, setDraftPlanTemplateId] = useState('')
const [quickTemplateId, setQuickTemplateId] = useState('')
const [exercisePickerOpen, setExercisePickerOpen] = useState(false)
const [exercisePickerTarget, setExercisePickerTarget] = useState(null)
const [planningPeekCtx, setPlanningPeekCtx] = useState(null)
const today = new Date().toISOString().split('T')[0]
const thirtyDaysLater = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
const [startDate, setStartDate] = useState(today)
const [endDate, setEndDate] = useState(thirtyDaysLater)
const [formData, setFormData] = useState({
group_id: '',
planned_date: '',
planned_time_start: '',
planned_time_end: '',
planned_focus: '',
actual_date: '',
actual_time_start: '',
actual_time_end: '',
attendance_count: '',
status: 'planned',
notes: '',
trainer_notes: '',
sections: [defaultSection()]
})
useEffect(() => {
loadData()
}, [])
useEffect(() => {
if (selectedGroupId) {
loadUnits()
}
}, [selectedGroupId, startDate, endDate])
const loadPlanTemplates = useCallback(async () => {
try {
const tpl = await api.listTrainingPlanTemplates()
setPlanTemplates(tpl)
} catch (e) {
console.error('Vorlagen laden:', e)
}
}, [])
const loadData = async () => {
try {
const groupsData = await api.listTrainingGroups({ status: 'active' })
setGroups(groupsData)
await loadPlanTemplates()
if (groupsData.length > 0) {
const ownGroup = groupsData.find((g) => g.trainer_id === user?.id)
if (ownGroup) {
setSelectedGroupId(ownGroup.id)
} else if (groupsData.length === 1) {
setSelectedGroupId(groupsData[0].id)
}
}
} catch (err) {
console.error('Failed to load data:', err)
alert('Fehler beim Laden: ' + err.message)
} finally {
setLoading(false)
}
}
const loadUnits = async () => {
if (!selectedGroupId) return
try {
const unitsData = await api.listTrainingUnits({
group_id: selectedGroupId,
start_date: startDate,
end_date: endDate
})
setUnits(unitsData)
} catch (err) {
console.error('Failed to load units:', err)
}
}
const handleQuickCreate = async () => {
if (!selectedGroupId) {
alert('Bitte wähle zuerst eine Trainingsgruppe')
return
}
const date = prompt('Datum für neue Trainingseinheit (YYYY-MM-DD):', today)
if (!date) return
try {
const body = {
group_id: parseInt(selectedGroupId, 10),
planned_date: date
}
if (quickTemplateId) {
body.plan_template_id = parseInt(quickTemplateId, 10)
}
await api.quickCreateTrainingUnit(body)
await loadUnits()
} catch (err) {
alert('Fehler beim Erstellen: ' + err.message)
}
}
const handleCreate = () => {
if (!selectedGroupId) {
alert('Bitte wähle zuerst eine Trainingsgruppe')
return
}
const group = groups.find((g) => g.id === parseInt(selectedGroupId, 10))
setEditingUnit(null)
setDraftPlanTemplateId('')
setFormData({
group_id: selectedGroupId,
planned_date: today,
planned_time_start: group?.time_start?.slice(0, 5) || '',
planned_time_end: group?.time_end?.slice(0, 5) || '',
planned_focus: '',
actual_date: '',
actual_time_start: '',
actual_time_end: '',
attendance_count: '',
status: 'planned',
notes: '',
trainer_notes: '',
sections: [defaultSection('Hauptteil')]
})
setShowModal(true)
}
const applyTemplateFromSelect = async (templateId) => {
setDraftPlanTemplateId(templateId)
if (!templateId) return
try {
const tpl = await api.getTrainingPlanTemplate(parseInt(templateId, 10))
setFormData((fd) => ({
...fd,
sections: (tpl.sections || []).length
? tpl.sections.map((s) => ({
title: s.title,
guidance_notes: s.guidance_text || '',
items: []
}))
: [defaultSection()]
}))
} catch (err) {
alert('Vorlage laden: ' + err.message)
}
}
const handleEdit = async (unit) => {
try {
const fullUnit = await api.getTrainingUnit(unit.id)
setEditingUnit(fullUnit)
setDraftPlanTemplateId(fullUnit.plan_template_id ? String(fullUnit.plan_template_id) : '')
let sections = normalizeUnitToForm(fullUnit)
sections = await enrichSectionsWithVariants(sections)
setFormData({
group_id: fullUnit.group_id,
planned_date: fullUnit.planned_date || '',
planned_time_start: fullUnit.planned_time_start?.slice(0, 5) || '',
planned_time_end: fullUnit.planned_time_end?.slice(0, 5) || '',
planned_focus: fullUnit.planned_focus || '',
actual_date: fullUnit.actual_date || '',
actual_time_start: fullUnit.actual_time_start?.slice(0, 5) || '',
actual_time_end: fullUnit.actual_time_end?.slice(0, 5) || '',
attendance_count: fullUnit.attendance_count ?? '',
status: fullUnit.status || 'planned',
notes: fullUnit.notes || '',
trainer_notes: fullUnit.trainer_notes || '',
sections,
})
setShowModal(true)
} catch (err) {
alert('Fehler beim Laden: ' + err.message)
}
}
const handleSaveAsTemplate = async () => {
const name = window.prompt('Name für die neue Trainingsvorlage (nur Abschnitts-Gliederung):')
if (!name?.trim()) return
try {
await api.createTrainingPlanTemplate({
name: name.trim(),
sections: formData.sections.map((s) => ({
title: s.title || 'Abschnitt',
guidance_text: s.guidance_notes?.trim() ? s.guidance_notes.trim() : null
}))
})
await loadPlanTemplates()
alert('Vorlage gespeichert.')
} catch (err) {
alert('Speichern: ' + err.message)
}
}
const handleDelete = async (unit) => {
if (!confirm(`Trainingseinheit vom ${unit.planned_date} wirklich löschen?`)) return
try {
await api.deleteTrainingUnit(unit.id)
await loadUnits()
} catch (err) {
alert('Fehler beim Löschen: ' + err.message)
}
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!formData.group_id || !formData.planned_date) {
alert('Gruppe und Datum sind Pflichtfelder')
return
}
try {
const sectionsPayload = buildSectionsPayload(formData.sections)
const payload = {
planned_date: formData.planned_date,
planned_time_start: formData.planned_time_start || null,
planned_time_end: formData.planned_time_end || null,
planned_focus: formData.planned_focus || null,
actual_date: formData.actual_date || null,
actual_time_start: formData.actual_time_start || null,
actual_time_end: formData.actual_time_end || null,
attendance_count: formData.attendance_count ? parseInt(formData.attendance_count, 10) : null,
status: formData.status || 'planned',
notes: formData.notes || null,
trainer_notes: formData.trainer_notes || null,
sections: sectionsPayload
}
if (!editingUnit) {
payload.group_id = parseInt(formData.group_id, 10)
if (draftPlanTemplateId) {
payload.plan_template_id = parseInt(draftPlanTemplateId, 10)
}
}
if (editingUnit) {
await api.updateTrainingUnit(editingUnit.id, payload)
} else {
await api.createTrainingUnit(payload)
}
setShowModal(false)
await loadUnits()
} catch (err) {
alert('Fehler beim Speichern: ' + err.message)
}
}
const updateFormField = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
if (loading) {
return (
<div className="app-page" style={{ padding: '2rem 0', textAlign: 'center' }}>
<div className="spinner"></div>
<p>Laden...</p>
</div>
)
}
const selectedGroup = groups.find((g) => g.id === parseInt(selectedGroupId, 10))
return (
<div className="app-page">
<h1 style={{ marginBottom: '0.35rem' }}>Trainingsplanung</h1>
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', marginBottom: '1.25rem' }}>
Wähle eine Trainingsgruppe, lege dann Termine mit Inhalt (Abschnitte und Übungen) an — ein Plan entsteht aus einer oder mehreren{' '}
<strong>Trainingseinheiten</strong> im gewählten Zeitraum.
</p>
<div className="card" style={{ marginBottom: '1.25rem', padding: '12px 14px' }}>
<p style={{ margin: 0, fontSize: '0.92rem', color: 'var(--text2)' }}>
Mehrere Einheiten strukturieren auf einmal:{' '}
<Link to="/planning/framework-programs" style={{ fontWeight: 600, color: 'var(--accent-dark)' }}>
Trainingsrahmenprogramme
</Link>{' '}
(Ziele, Slots, Übungen als Vorlage).
</p>
</div>
{!loading && groups.length === 0 && (
<div
className="card"
style={{
marginBottom: '1.25rem',
borderLeft: '4px solid var(--accent)',
padding: '1rem 1.25rem'
}}
>
<h2 style={{ fontSize: '1.1rem', marginBottom: '0.5rem' }}>Erst Verein & Gruppe anlegen</h2>
<p style={{ color: 'var(--text2)', marginBottom: '0.85rem', lineHeight: 1.5 }}>
Ohne Trainingsgruppe kann hier nichts gebucht werden. Unter <strong>Vereine</strong> legst du einen Verein an
(kurzer Name genügt), optional eine Sparte, dann eine <strong>Trainingsgruppe</strong>. Wochentage, feste Zeiten oder
Eigenschaften sind optional und kannst du später ergänzen.
</p>
<Link to="/clubs" className="btn btn-primary" style={{ textDecoration: 'none' }}>
Zu Vereinen & Trainingsgruppen
</Link>
</div>
)}
<div className="card" style={{ marginBottom: '1.5rem' }}>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '1rem'
}}
>
<div>
<label className="form-label">Trainingsgruppe</label>
<select
className="form-input"
value={selectedGroupId}
onChange={(e) => setSelectedGroupId(e.target.value)}
>
<option value="">Bitte wählen</option>
{groups.map((g) => (
<option key={g.id} value={g.id}>
{g.name} ({g.club_name})
</option>
))}
</select>
</div>
<div>
<label className="form-label">Von</label>
<input
type="date"
className="form-input"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div>
<label className="form-label">Bis</label>
<input
type="date"
className="form-input"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
</div>
{selectedGroup && (
<div
style={{
marginTop: '1rem',
padding: '1rem',
background: 'var(--surface2)',
borderRadius: '8px'
}}
>
<p style={{ fontSize: '0.875rem', color: 'var(--text2)' }}>
{selectedGroup.location || 'Kein Ort angegeben'}
{selectedGroup.weekday && ` · ${selectedGroup.weekday}`}
{selectedGroup.time_start &&
` · ${selectedGroup.time_start.slice(0, 5)} - ${selectedGroup.time_end?.slice(0, 5)}`}
</p>
</div>
)}
<div
style={{
marginTop: '1.25rem',
paddingTop: '1rem',
borderTop: '1px solid var(--border, rgba(0,0,0,0.08))'
}}
>
<p style={{ fontSize: '0.88rem', color: 'var(--text2)', marginBottom: '0.75rem' }}>
<strong>Plan anlegen:</strong> neue Trainingseinheit mit Datum, Zeit und Ablauf — oder schnell nur mit Datum (Zeiten aus der Gruppe).
{!selectedGroupId && (
<span style={{ display: 'block', marginTop: '0.35rem' }}>
Wähle oben eine Trainingsgruppe, um die Schaltflächen zu aktivieren.
</span>
)}
{groups.length === 0 && (
<span style={{ display: 'block', marginTop: '0.35rem' }}>
Es gibt noch keine aktive Trainingsgruppe — unter{' '}
<Link to="/clubs">
Vereine
</Link>{' '}
anlegen oder aktivieren.
</span>
)}
</p>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
alignItems: 'center'
}}
>
<button
type="button"
className="btn btn-primary"
disabled={!selectedGroupId}
title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined}
onClick={handleCreate}
>
+ Neue Trainingseinheit planen
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
<label className="form-label" style={{ marginBottom: 0 }}>
Schnell (+ optional Vorlage):
</label>
<select
className="form-input"
style={{ minWidth: '180px', marginBottom: 0 }}
value={quickTemplateId}
onChange={(e) => setQuickTemplateId(e.target.value)}
disabled={!selectedGroupId}
title={!selectedGroupId ? 'Zuerst Trainingsgruppe wählen' : undefined}
>
<option value="">Standard (leer)</option>
{planTemplates.map((t) => (
<option key={t.id} value={String(t.id)}>
{t.name}
{typeof t.sections_count === 'number' ? ` (${t.sections_count} Abschn.)` : ''}
</option>
))}
</select>
<button
type="button"
className="btn btn-secondary"
disabled={!selectedGroupId}
title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined}
onClick={handleQuickCreate}
>
Schnell erstellen
</button>
</div>
</div>
</div>
</div>
{!selectedGroupId ? (
<div className="card">
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
Wähle oben eine Trainingsgruppe — danach kannst du mit <strong>„Neue Trainingseinheit planen“</strong> starten.
</p>
</div>
) : units.length === 0 ? (
<div className="card">
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
Keine Trainingseinheiten in diesem Zeitraum. Nutze oben <strong>„Neue Trainingseinheit planen“</strong> oder{' '}
<strong>„Schnell erstellen“</strong>, um den ersten Termin anzulegen.
</p>
</div>
) : (
<div style={{ display: 'grid', gap: '1rem' }}>
{units.map((unit) => (
<div key={unit.id} className="card">
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '12px',
marginBottom: '1rem'
}}
>
<div style={{ minWidth: 0, flex: '1 1 200px' }}>
<h3 style={{ marginBottom: '0.25rem' }}>
{unit.planned_date}
{unit.planned_time_start &&
` · ${unit.planned_time_start.slice(0, 5)} - ${unit.planned_time_end?.slice(0, 5)}`}
</h3>
{unit.planned_focus && (
<p
style={{
color: 'var(--text2)',
fontSize: '0.875rem',
marginBottom: '0.5rem'
}}
>
Fokus: {unit.planned_focus}
</p>
)}
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<span
style={{
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
background:
unit.status === 'completed'
? '#2ea44f'
: unit.status === 'cancelled'
? 'var(--danger)'
: 'var(--surface2)',
color:
unit.status === 'completed' || unit.status === 'cancelled'
? 'white'
: 'var(--text2)'
}}
>
{unit.status === 'planned' && 'Geplant'}
{unit.status === 'completed' && 'Durchgeführt'}
{unit.status === 'cancelled' && 'Abgesagt'}
</span>
{unit.attendance_count !== null && unit.attendance_count !== undefined && (
<span
style={{
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
background: 'var(--surface2)',
color: 'var(--text2)'
}}
>
{unit.attendance_count} Teilnehmer
</span>
)}
</div>
</div>
<div
style={{
display: 'flex',
gap: '0.5rem',
flexWrap: 'wrap',
justifyContent: 'flex-end',
flex: '1 1 180px',
minWidth: 0
}}
>
<Link
to={`/planning/run/${unit.id}`}
className="btn btn-secondary"
style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
>
Plan &amp; Ablauf
</Link>
<Link
to={`/planning/run/${unit.id}/coach`}
className="btn btn-primary"
style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
>
Im Training (Coach)
</Link>
<button className="btn btn-secondary" onClick={() => handleEdit(unit)}>
Bearbeiten
</button>
<button
className="btn"
style={{ background: 'var(--danger)', color: 'white', border: 'none' }}
onClick={() => handleDelete(unit)}
>
Löschen
</button>
</div>
</div>
{unit.notes && (
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginTop: '0.5rem' }}>
{unit.notes}
</p>
)}
</div>
))}
</div>
)}
{showModal && (
<div
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: 1000,
padding: '1rem',
overflowY: 'auto'
}}
>
<div
style={{
background: 'var(--surface)',
borderRadius: '12px',
padding: 'clamp(12px, 3vw, 2rem)',
maxWidth: 'min(1100px, 100%)',
width: '100%',
maxHeight: '92vh',
overflowY: 'auto',
margin: 'max(0px, env(safe-area-inset-top, 0px)) auto',
boxSizing: 'border-box',
minWidth: 0
}}
>
<h2 style={{ marginBottom: '1rem' }}>
{editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'}
</h2>
{!editingUnit && (
<div className="form-row" style={{ marginBottom: '1.25rem' }}>
<label className="form-label">Gliederungsvorlage (optional)</label>
<select
className="form-input"
value={draftPlanTemplateId}
onChange={(e) => applyTemplateFromSelect(e.target.value)}
>
<option value="">Keine Vorlage</option>
{planTemplates.map((t) => (
<option key={t.id} value={String(t.id)}>
{t.name}
</option>
))}
</select>
<p style={{ fontSize: '0.8rem', color: 'var(--text2)', marginTop: '0.35rem' }}>
Lädt die Abschnitte und Hinweise aus der Vorlage; Übungen fügst du hier ein.
</p>
</div>
)}
<form onSubmit={handleSubmit}>
<h3 style={{ marginBottom: '1rem' }}>Planung</h3>
<div className="responsive-grid-3" style={{ marginBottom: '1rem' }}>
<div className="form-row">
<label className="form-label">Datum *</label>
<input
type="date"
className="form-input"
value={formData.planned_date}
onChange={(e) => updateFormField('planned_date', e.target.value)}
required
/>
</div>
<div className="form-row">
<label className="form-label">Von</label>
<input
type="time"
className="form-input"
value={formData.planned_time_start}
onChange={(e) => updateFormField('planned_time_start', e.target.value)}
/>
</div>
<div className="form-row">
<label className="form-label">Bis</label>
<input
type="time"
className="form-input"
value={formData.planned_time_end}
onChange={(e) => updateFormField('planned_time_end', e.target.value)}
/>
</div>
</div>
<div className="form-row">
<label className="form-label">Trainingsfokus</label>
<input
type="text"
className="form-input"
value={formData.planned_focus}
onChange={(e) => updateFormField('planned_focus', e.target.value)}
placeholder="z.B. Grundlagen, Kinder altersgerecht"
/>
</div>
<div style={{ marginTop: '2rem' }}>
<TrainingUnitSectionsEditor
heading="Abschnitte & Übungen"
headingAccessory={
<button type="button" className="btn btn-secondary" onClick={handleSaveAsTemplate}>
Vorlage aus Aufbau speichern
</button>
}
sections={formData.sections}
wideExerciseGrid
onSectionsChange={(updater) =>
setFormData((prev) => ({
...prev,
sections: updater(prev.sections),
}))
}
onRequestExercisePick={({ sectionIndex, itemIndex, multi }) => {
setExercisePickerTarget({
sIdx: sectionIndex,
iIdx: typeof itemIndex === 'number' ? itemIndex : undefined,
multi: !!multi,
})
setExercisePickerOpen(true)
}}
onPeekExercise={(id, variantId) =>
setPlanningPeekCtx({ exerciseId: id, variantId: variantId ?? null })
}
showExecutionExtras={!!editingUnit}
/>
</div>
<div style={{ marginBottom: '1.75rem' }} />
{editingUnit && (
<>
<h3 style={{ marginTop: '0.5rem', marginBottom: '1rem' }}>Durchführung</h3>
<div className="responsive-grid-4" style={{ marginBottom: '1rem' }}>
<div className="form-row">
<label className="form-label">Tatsächliches Datum</label>
<input
type="date"
className="form-input"
value={formData.actual_date}
onChange={(e) => updateFormField('actual_date', e.target.value)}
/>
</div>
<div className="form-row">
<label className="form-label">Von</label>
<input
type="time"
className="form-input"
value={formData.actual_time_start}
onChange={(e) => updateFormField('actual_time_start', e.target.value)}
/>
</div>
<div className="form-row">
<label className="form-label">Bis</label>
<input
type="time"
className="form-input"
value={formData.actual_time_end}
onChange={(e) => updateFormField('actual_time_end', e.target.value)}
/>
</div>
<div className="form-row">
<label className="form-label">Teilnehmer</label>
<input
type="number"
className="form-input"
value={formData.attendance_count}
onChange={(e) => updateFormField('attendance_count', e.target.value)}
/>
</div>
</div>
<div className="form-row">
<label className="form-label">Status</label>
<select
className="form-input"
value={formData.status}
onChange={(e) => updateFormField('status', e.target.value)}
>
<option value="planned">Geplant</option>
<option value="completed">Durchgeführt</option>
<option value="cancelled">Abgesagt</option>
</select>
</div>
</>
)}
<h3 style={{ marginTop: '2rem', marginBottom: '1rem' }}>Notizen</h3>
<div className="form-row">
<label className="form-label">Öffentliche Notizen</label>
<textarea
className="form-input"
rows={3}
value={formData.notes}
onChange={(e) => updateFormField('notes', e.target.value)}
placeholder="Für Teilnehmer"
/>
</div>
<div className="form-row">
<label className="form-label">Trainernotizen</label>
<textarea
className="form-input"
rows={3}
value={formData.trainer_notes}
onChange={(e) => updateFormField('trainer_notes', e.target.value)}
/>
</div>
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1.5rem' }}>
<button type="submit" className="btn btn-primary" style={{ flex: 1 }}>
{editingUnit ? 'Speichern' : 'Erstellen'}
</button>
<button type="button" className="btn btn-secondary" onClick={() => setShowModal(false)}>
Abbrechen
</button>
</div>
</form>
</div>
</div>
)}
<ExercisePickerModal
open={exercisePickerOpen}
multiSelect={!!exercisePickerTarget?.multi}
onClose={() => {
setExercisePickerOpen(false)
setExercisePickerTarget(null)
}}
onSelectExercises={
exercisePickerTarget?.multi
? async (picked) => {
if (!exercisePickerTarget || !picked?.length) return
const { sIdx } = exercisePickerTarget
const rows = []
for (const ex of picked) {
const row = await hydrateExercisePlanningRow(ex)
if (row) rows.push(row)
}
if (!rows.length) return
setFormData((prev) => ({
...prev,
sections: prev.sections.map((s, si) =>
si !== sIdx ? s : { ...s, items: [...(s.items || []), ...rows] }
),
}))
setExercisePickerOpen(false)
setExercisePickerTarget(null)
}
: undefined
}
onSelectExercise={async (ex) => {
if (!exercisePickerTarget || exercisePickerTarget.multi) return
const row = await hydrateExercisePlanningRow(ex)
if (!row) return
const { sIdx, iIdx } = exercisePickerTarget
if (typeof iIdx !== 'number') return
setFormData((prev) => ({
...prev,
sections: prev.sections.map((s, si) =>
si !== sIdx
? s
: {
...s,
items: s.items.map((r2, ii) =>
ii !== iIdx
? r2
: r2.item_type !== 'exercise'
? r2
: {
...r2,
exercise_id: row.exercise_id,
exercise_variant_id: row.exercise_variant_id,
exercise_title: row.exercise_title,
variants: row.variants,
}
),
}
),
}))
setExercisePickerOpen(false)
setExercisePickerTarget(null)
}}
/>
<ExercisePeekModal
open={planningPeekCtx != null}
exerciseId={planningPeekCtx?.exerciseId}
variantId={planningPeekCtx?.variantId ?? undefined}
onClose={() => setPlanningPeekCtx(null)}
/>
</div>
)
}
export default TrainingPlanningPage