2026-05-05 15:13:00 +02:00
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
2026-04-28 19:46:09 +02:00
|
|
|
|
import { Link } from 'react-router-dom'
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
import api from '../utils/api'
|
|
|
|
|
|
import { useAuth } from '../context/AuthContext'
|
2026-04-29 06:15:26 +02:00
|
|
|
|
import ExercisePickerModal from '../components/ExercisePickerModal'
|
2026-04-29 06:49:16 +02:00
|
|
|
|
import ExercisePeekModal from '../components/ExercisePeekModal'
|
2026-05-05 13:39:30 +02:00
|
|
|
|
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
|
|
|
|
|
|
import {
|
|
|
|
|
|
defaultSection,
|
|
|
|
|
|
normalizeUnitToForm,
|
|
|
|
|
|
enrichSectionsWithVariants,
|
|
|
|
|
|
buildSectionsPayload,
|
2026-05-05 14:47:38 +02:00
|
|
|
|
hydrateExercisePlanningRow,
|
2026-05-05 13:39:30 +02:00
|
|
|
|
} from '../utils/trainingUnitSectionsForm'
|
2026-04-28 16:31:45 +02:00
|
|
|
|
|
2026-05-05 15:03:54 +02:00
|
|
|
|
function addDaysIsoDate(isoDay, daysDelta) {
|
|
|
|
|
|
const d = new Date(`${isoDay}T12:00:00`)
|
|
|
|
|
|
d.setDate(d.getDate() + daysDelta)
|
|
|
|
|
|
return d.toISOString().slice(0, 10)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 15:13:00 +02:00
|
|
|
|
function pad2(n) {
|
|
|
|
|
|
return String(n).padStart(2, '0')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toIsoLocal(d) {
|
|
|
|
|
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Montag = erster Wochentag (ISO-Woche UI) */
|
|
|
|
|
|
function mondayIndex(d) {
|
|
|
|
|
|
return (d.getDay() + 6) % 7
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Kalendarische Monatsansicht: erster und letzter Tag des sichtbaren Rasters (Mo–So) */
|
|
|
|
|
|
function getCalendarGridRange(ym) {
|
|
|
|
|
|
const parts = (ym || '').split('-').map(Number)
|
|
|
|
|
|
const y = parts[0]
|
|
|
|
|
|
const m = parts[1]
|
|
|
|
|
|
if (!y || !m || m < 1 || m > 12) {
|
|
|
|
|
|
const t = new Date()
|
|
|
|
|
|
return { gridStart: toIsoLocal(t), gridEnd: toIsoLocal(t) }
|
|
|
|
|
|
}
|
|
|
|
|
|
const first = new Date(y, m - 1, 1)
|
|
|
|
|
|
const last = new Date(y, m, 0)
|
|
|
|
|
|
const gridStart = new Date(first)
|
|
|
|
|
|
gridStart.setDate(first.getDate() - mondayIndex(first))
|
|
|
|
|
|
const lastMon = mondayIndex(last)
|
|
|
|
|
|
const gridEnd = new Date(last)
|
|
|
|
|
|
gridEnd.setDate(last.getDate() + (6 - lastMon))
|
|
|
|
|
|
return { gridStart: toIsoLocal(gridStart), gridEnd: toIsoLocal(gridEnd) }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shiftCalendarMonth(ym, delta) {
|
|
|
|
|
|
const parts = (ym || '').split('-').map(Number)
|
|
|
|
|
|
const y = parts[0] || new Date().getFullYear()
|
|
|
|
|
|
const m = parts[1] || 1
|
|
|
|
|
|
const d = new Date(y, m - 1 + delta, 1)
|
|
|
|
|
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function enumerateIsoDays(fromIso, toIso) {
|
|
|
|
|
|
const out = []
|
|
|
|
|
|
const cur = new Date(`${fromIso}T12:00:00`)
|
|
|
|
|
|
const end = new Date(`${toIso}T12:00:00`)
|
|
|
|
|
|
while (cur <= end) {
|
|
|
|
|
|
out.push(toIsoLocal(cur))
|
|
|
|
|
|
cur.setDate(cur.getDate() + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const WEEKDAYS_DE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
|
|
|
|
|
|
|
2026-05-05 23:35:41 +02:00
|
|
|
|
function toNumList(arr) {
|
|
|
|
|
|
if (!Array.isArray(arr)) return []
|
|
|
|
|
|
const out = []
|
|
|
|
|
|
for (const x of arr) {
|
|
|
|
|
|
const n = Number(x)
|
|
|
|
|
|
if (Number.isFinite(n) && n >= 1) out.push(n)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sessionAssignDefaults = () => ({
|
|
|
|
|
|
lead_trainer_profile_id: '',
|
|
|
|
|
|
session_assistants_inherit: true,
|
|
|
|
|
|
session_assistant_profile_ids: [],
|
|
|
|
|
|
})
|
2026-05-06 07:42:39 +02:00
|
|
|
|
|
|
|
|
|
|
/** Co_trainer_ids aus TrainingGroups (Liste/JSON) → Zahlenliste */
|
|
|
|
|
|
function normalizeGroupCoTrainerIds(raw) {
|
|
|
|
|
|
if (raw == null) return []
|
|
|
|
|
|
const arr = Array.isArray(raw) ? raw : []
|
|
|
|
|
|
const out = []
|
|
|
|
|
|
for (const x of arr) {
|
|
|
|
|
|
const n = Number(x)
|
|
|
|
|
|
if (Number.isFinite(n) && n >= 1) out.push(n)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Mitgliederverzeichnis-Einträge ohne effektiven Leitungsträger als Co‑Option */
|
|
|
|
|
|
function filterDirectoryExcludingLead(directory, excludeLeadPid) {
|
|
|
|
|
|
const ex =
|
|
|
|
|
|
excludeLeadPid != null && excludeLeadPid !== '' && Number.isFinite(Number(excludeLeadPid))
|
|
|
|
|
|
? Number(excludeLeadPid)
|
|
|
|
|
|
: null
|
|
|
|
|
|
if (ex == null) return directory
|
|
|
|
|
|
return directory.filter((m) => Number(m.id) !== ex)
|
|
|
|
|
|
}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
function TrainingPlanningPage() {
|
|
|
|
|
|
const { user } = useAuth()
|
|
|
|
|
|
const [groups, setGroups] = useState([])
|
|
|
|
|
|
const [selectedGroupId, setSelectedGroupId] = useState('')
|
|
|
|
|
|
const [units, setUnits] = useState([])
|
2026-04-28 16:31:45 +02:00
|
|
|
|
const [planTemplates, setPlanTemplates] = useState([])
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
|
const [showModal, setShowModal] = useState(false)
|
|
|
|
|
|
const [editingUnit, setEditingUnit] = useState(null)
|
2026-04-28 16:31:45 +02:00
|
|
|
|
const [draftPlanTemplateId, setDraftPlanTemplateId] = useState('')
|
|
|
|
|
|
const [quickTemplateId, setQuickTemplateId] = useState('')
|
2026-04-29 06:15:26 +02:00
|
|
|
|
const [exercisePickerOpen, setExercisePickerOpen] = useState(false)
|
|
|
|
|
|
const [exercisePickerTarget, setExercisePickerTarget] = useState(null)
|
2026-05-05 14:42:46 +02:00
|
|
|
|
const [planningPeekCtx, setPlanningPeekCtx] = useState(null)
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
|
|
|
|
|
const today = new Date().toISOString().split('T')[0]
|
|
|
|
|
|
const thirtyDaysLater = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
|
|
|
|
|
|
2026-05-05 15:03:54 +02:00
|
|
|
|
const [frameworkImportOpen, setFrameworkImportOpen] = useState(false)
|
|
|
|
|
|
const [frameworkProgramsList, setFrameworkProgramsList] = useState([])
|
|
|
|
|
|
const [fwImportProgramId, setFwImportProgramId] = useState('')
|
|
|
|
|
|
const [fwImportDetail, setFwImportDetail] = useState(null)
|
|
|
|
|
|
const [fwImportLoading, setFwImportLoading] = useState(false)
|
|
|
|
|
|
const [fwImportSelectedSlots, setFwImportSelectedSlots] = useState(() => new Set())
|
|
|
|
|
|
const [fwImportSlotDates, setFwImportSlotDates] = useState({})
|
|
|
|
|
|
const [fwImportStartDate, setFwImportStartDate] = useState(today)
|
|
|
|
|
|
const [fwImportIntervalDays, setFwImportIntervalDays] = useState(7)
|
|
|
|
|
|
const [fwImportSubmitting, setFwImportSubmitting] = useState(false)
|
|
|
|
|
|
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
const [startDate, setStartDate] = useState(today)
|
|
|
|
|
|
const [endDate, setEndDate] = useState(thirtyDaysLater)
|
2026-05-05 15:13:00 +02:00
|
|
|
|
const [planView, setPlanView] = useState('list')
|
|
|
|
|
|
const [calendarMonthStr, setCalendarMonthStr] = useState(() => today.slice(0, 7))
|
2026-05-05 15:40:29 +02:00
|
|
|
|
const [planScope, setPlanScope] = useState('group')
|
|
|
|
|
|
const [assignedToMeOnly, setAssignedToMeOnly] = useState(false)
|
2026-05-05 23:35:41 +02:00
|
|
|
|
const [clubDirectory, setClubDirectory] = useState([])
|
2026-05-06 07:18:30 +02:00
|
|
|
|
const [assignModalOpen, setAssignModalOpen] = useState(false)
|
|
|
|
|
|
const [assignDraft, setAssignDraft] = useState({
|
|
|
|
|
|
unit: null,
|
|
|
|
|
|
lead_trainer_profile_id: '',
|
|
|
|
|
|
session_assistants_inherit: true,
|
|
|
|
|
|
session_assistant_profile_ids: [],
|
|
|
|
|
|
})
|
|
|
|
|
|
const [assignSaving, setAssignSaving] = useState(false)
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
|
|
|
|
|
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: '',
|
2026-05-05 23:35:41 +02:00
|
|
|
|
sections: [defaultSection()],
|
|
|
|
|
|
...sessionAssignDefaults()
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-05 15:16:20 +02:00
|
|
|
|
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 = useCallback(async () => {
|
|
|
|
|
|
if (!selectedGroupId) return
|
|
|
|
|
|
let start = startDate
|
|
|
|
|
|
let end = endDate
|
|
|
|
|
|
if (planView === 'calendar') {
|
|
|
|
|
|
const r = getCalendarGridRange(calendarMonthStr)
|
|
|
|
|
|
start = r.gridStart
|
|
|
|
|
|
end = r.gridEnd
|
|
|
|
|
|
}
|
2026-05-05 15:40:29 +02:00
|
|
|
|
const gid = parseInt(selectedGroupId, 10)
|
|
|
|
|
|
const groupRow = groups.find((g) => g.id === gid)
|
|
|
|
|
|
const clubId = groupRow?.club_id
|
2026-05-05 15:16:20 +02:00
|
|
|
|
try {
|
2026-05-05 15:40:29 +02:00
|
|
|
|
const filters = {
|
2026-05-05 15:16:20 +02:00
|
|
|
|
start_date: start,
|
|
|
|
|
|
end_date: end
|
2026-05-05 15:40:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
if (assignedToMeOnly) {
|
|
|
|
|
|
filters.assigned_to_me = true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (planScope === 'club' && clubId) {
|
|
|
|
|
|
filters.club_id = clubId
|
|
|
|
|
|
} else {
|
|
|
|
|
|
filters.group_id = gid
|
|
|
|
|
|
}
|
|
|
|
|
|
const unitsData = await api.listTrainingUnits(filters)
|
2026-05-05 15:16:20 +02:00
|
|
|
|
setUnits(unitsData)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Failed to load units:', err)
|
|
|
|
|
|
}
|
2026-05-05 15:40:29 +02:00
|
|
|
|
}, [
|
|
|
|
|
|
selectedGroupId,
|
|
|
|
|
|
groups,
|
|
|
|
|
|
startDate,
|
|
|
|
|
|
endDate,
|
|
|
|
|
|
planView,
|
|
|
|
|
|
calendarMonthStr,
|
|
|
|
|
|
planScope,
|
|
|
|
|
|
assignedToMeOnly
|
|
|
|
|
|
])
|
2026-05-05 15:16:20 +02:00
|
|
|
|
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadData()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedGroupId) {
|
|
|
|
|
|
loadUnits()
|
|
|
|
|
|
}
|
2026-05-05 15:13:00 +02:00
|
|
|
|
}, [selectedGroupId, loadUnits])
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
2026-05-06 07:18:30 +02:00
|
|
|
|
const selectedGroupClubIdMemo = useMemo(() => {
|
|
|
|
|
|
const g = groups.find((gr) => gr.id === parseInt(selectedGroupId, 10))
|
|
|
|
|
|
return g?.club_id != null ? Number(g.club_id) : null
|
|
|
|
|
|
}, [groups, selectedGroupId])
|
|
|
|
|
|
|
|
|
|
|
|
const canClubOrgTraining = useMemo(() => {
|
|
|
|
|
|
const r = (user?.role || '').toLowerCase()
|
|
|
|
|
|
if (r === 'admin' || r === 'superadmin') return true
|
|
|
|
|
|
if (selectedGroupClubIdMemo == null || !Number.isFinite(selectedGroupClubIdMemo)) return false
|
2026-05-06 07:42:39 +02:00
|
|
|
|
const row = (user?.clubs || []).find((c) => Number(c.id) === selectedGroupClubIdMemo)
|
2026-05-06 07:18:30 +02:00
|
|
|
|
return Array.isArray(row?.roles) && row.roles.includes('club_admin')
|
2026-05-06 07:42:39 +02:00
|
|
|
|
}, [user?.role, user?.clubs, selectedGroupClubIdMemo])
|
|
|
|
|
|
|
|
|
|
|
|
const clubAdminClubIdSet = useMemo(() => {
|
|
|
|
|
|
const ids = []
|
|
|
|
|
|
for (const c of user?.clubs || []) {
|
|
|
|
|
|
if (Array.isArray(c.roles) && c.roles.includes('club_admin')) {
|
|
|
|
|
|
const id = Number(c.id)
|
|
|
|
|
|
if (Number.isFinite(id)) ids.push(id)
|
|
|
|
|
|
}
|
2026-05-05 23:35:41 +02:00
|
|
|
|
}
|
2026-05-06 07:42:39 +02:00
|
|
|
|
return new Set(ids)
|
|
|
|
|
|
}, [user?.clubs])
|
2026-05-06 07:18:30 +02:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const gid = parseInt(formData.group_id || selectedGroupId || '0', 10)
|
|
|
|
|
|
const gModal = Number.isFinite(gid) && gid >= 1 ? groups.find((x) => x.id === gid) : null
|
|
|
|
|
|
const clubForModal = gModal?.club_id != null ? Number(gModal.club_id) : null
|
2026-05-06 07:42:39 +02:00
|
|
|
|
|
|
|
|
|
|
let assignModalClubId = null
|
|
|
|
|
|
if (assignModalOpen && assignDraft.unit?.group_id != null) {
|
|
|
|
|
|
const ug = Number(assignDraft.unit.group_id)
|
|
|
|
|
|
const gAssign = Number.isFinite(ug) ? groups.find((x) => x.id === ug) : null
|
|
|
|
|
|
if (gAssign?.club_id != null) assignModalClubId = Number(gAssign.club_id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 07:18:30 +02:00
|
|
|
|
const loadClubId =
|
|
|
|
|
|
showModal && clubForModal != null && Number.isFinite(clubForModal)
|
|
|
|
|
|
? clubForModal
|
2026-05-06 07:42:39 +02:00
|
|
|
|
: assignModalOpen && assignModalClubId != null && Number.isFinite(assignModalClubId)
|
|
|
|
|
|
? assignModalClubId
|
|
|
|
|
|
: canClubOrgTraining && selectedGroupClubIdMemo != null && Number.isFinite(selectedGroupClubIdMemo)
|
|
|
|
|
|
? selectedGroupClubIdMemo
|
|
|
|
|
|
: null
|
2026-05-06 07:18:30 +02:00
|
|
|
|
|
|
|
|
|
|
if (loadClubId == null || !Number.isFinite(loadClubId)) {
|
2026-05-05 23:35:41 +02:00
|
|
|
|
setClubDirectory([])
|
|
|
|
|
|
return undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
;(async () => {
|
|
|
|
|
|
try {
|
2026-05-06 07:18:30 +02:00
|
|
|
|
const d = await api.clubMembersDirectory(loadClubId)
|
2026-05-05 23:35:41 +02:00
|
|
|
|
if (!cancelled) setClubDirectory(Array.isArray(d) ? d : [])
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
console.error('Mitgliederverzeichnis:', err)
|
|
|
|
|
|
setClubDirectory([])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
2026-05-06 07:18:30 +02:00
|
|
|
|
}, [
|
|
|
|
|
|
showModal,
|
2026-05-06 07:42:39 +02:00
|
|
|
|
assignModalOpen,
|
|
|
|
|
|
assignDraft.unit,
|
2026-05-06 07:18:30 +02:00
|
|
|
|
formData.group_id,
|
|
|
|
|
|
selectedGroupId,
|
|
|
|
|
|
groups,
|
|
|
|
|
|
canClubOrgTraining,
|
|
|
|
|
|
selectedGroupClubIdMemo,
|
|
|
|
|
|
])
|
2026-05-05 23:35:41 +02:00
|
|
|
|
|
2026-05-05 15:03:54 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!frameworkImportOpen) return
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
;(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const list = await api.listTrainingFrameworkPrograms()
|
|
|
|
|
|
if (!cancelled) setFrameworkProgramsList(Array.isArray(list) ? list : [])
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
console.error('Rahmenprogramme laden:', e)
|
|
|
|
|
|
setFrameworkProgramsList([])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [frameworkImportOpen])
|
|
|
|
|
|
|
|
|
|
|
|
const openFrameworkImportModal = useCallback(() => {
|
|
|
|
|
|
setFwImportProgramId('')
|
|
|
|
|
|
setFwImportDetail(null)
|
|
|
|
|
|
setFwImportSelectedSlots(new Set())
|
|
|
|
|
|
setFwImportSlotDates({})
|
|
|
|
|
|
setFwImportStartDate(new Date().toISOString().split('T')[0])
|
|
|
|
|
|
setFwImportIntervalDays(7)
|
|
|
|
|
|
setFrameworkImportOpen(true)
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const onFwImportProgramChange = async (idStr) => {
|
|
|
|
|
|
setFwImportProgramId(idStr)
|
|
|
|
|
|
if (!idStr) {
|
|
|
|
|
|
setFwImportDetail(null)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setFwImportLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const d = await api.getTrainingFrameworkProgram(parseInt(idStr, 10))
|
|
|
|
|
|
setFwImportDetail(d)
|
|
|
|
|
|
setFwImportSelectedSlots(new Set())
|
|
|
|
|
|
setFwImportSlotDates({})
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || 'Rahmenprogramm laden fehlgeschlagen')
|
|
|
|
|
|
setFwImportDetail(null)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setFwImportLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const toggleFwImportSlot = (slot) => {
|
|
|
|
|
|
if (!slot?.blueprint_training_unit_id) return
|
|
|
|
|
|
const sid = slot.id
|
|
|
|
|
|
setFwImportSelectedSlots((prev) => {
|
|
|
|
|
|
const n = new Set(prev)
|
|
|
|
|
|
if (n.has(sid)) n.delete(sid)
|
|
|
|
|
|
else n.add(sid)
|
|
|
|
|
|
return n
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const applyFwImportDateSuggestions = () => {
|
|
|
|
|
|
if (!fwImportDetail?.slots?.length) return
|
|
|
|
|
|
const sorted = [...fwImportDetail.slots].sort(
|
|
|
|
|
|
(a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
let offset = 0
|
|
|
|
|
|
const iv = Math.max(0, Number(fwImportIntervalDays) || 0)
|
|
|
|
|
|
const next = {}
|
|
|
|
|
|
for (const s of sorted) {
|
|
|
|
|
|
if (!fwImportSelectedSlots.has(s.id)) continue
|
|
|
|
|
|
if (!s.blueprint_training_unit_id) continue
|
|
|
|
|
|
next[String(s.id)] = addDaysIsoDate(fwImportStartDate, offset)
|
|
|
|
|
|
offset += iv
|
|
|
|
|
|
}
|
|
|
|
|
|
setFwImportSlotDates((prev) => ({ ...prev, ...next }))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const submitFrameworkImport = async () => {
|
|
|
|
|
|
if (!selectedGroupId) {
|
|
|
|
|
|
alert('Bitte zuerst eine Trainingsgruppe wählen.')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const gid = parseInt(selectedGroupId, 10)
|
|
|
|
|
|
if (!fwImportDetail?.slots?.length) return
|
|
|
|
|
|
const sorted = [...fwImportDetail.slots].sort(
|
|
|
|
|
|
(a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
const picks = sorted.filter(
|
|
|
|
|
|
(s) => fwImportSelectedSlots.has(s.id) && s.blueprint_training_unit_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!picks.length) {
|
|
|
|
|
|
alert('Bitte mindestens eine Session mit Ablauf (Blueprint) auswählen.')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const s of picks) {
|
|
|
|
|
|
const key = String(s.id)
|
|
|
|
|
|
const date = fwImportSlotDates[key] || fwImportStartDate
|
|
|
|
|
|
if (!date) {
|
|
|
|
|
|
alert('Bitte für jede ausgewählte Session ein Datum setzen (oder Datumsvorschläge nutzen).')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
setFwImportSubmitting(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (const s of picks) {
|
|
|
|
|
|
const key = String(s.id)
|
|
|
|
|
|
const date = fwImportSlotDates[key] || fwImportStartDate
|
|
|
|
|
|
await api.createTrainingUnitFromFrameworkSlot({
|
|
|
|
|
|
group_id: gid,
|
|
|
|
|
|
planned_date: date,
|
|
|
|
|
|
framework_slot_id: s.id,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
setFrameworkImportOpen(false)
|
|
|
|
|
|
await loadUnits()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || 'Übernahme fehlgeschlagen')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setFwImportSubmitting(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const frameworkLineageText = (unit) => {
|
|
|
|
|
|
const fpTitle = (unit.origin_framework_program_title || '').trim() || 'Rahmenprogramm'
|
|
|
|
|
|
const st = (unit.origin_framework_slot_title || '').trim()
|
|
|
|
|
|
const idx = unit.origin_framework_slot_sort_order
|
|
|
|
|
|
const slotBit =
|
|
|
|
|
|
st || (typeof idx === 'number' ? `Session ${idx + 1}` : 'Session')
|
|
|
|
|
|
return { fpTitle, slotBit, fpId: unit.origin_framework_program_id }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
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 {
|
2026-04-28 16:31:45 +02:00
|
|
|
|
const body = {
|
|
|
|
|
|
group_id: parseInt(selectedGroupId, 10),
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
planned_date: date
|
2026-04-28 16:31:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
if (quickTemplateId) {
|
|
|
|
|
|
body.plan_template_id = parseInt(quickTemplateId, 10)
|
|
|
|
|
|
}
|
|
|
|
|
|
await api.quickCreateTrainingUnit(body)
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
await loadUnits()
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
alert('Fehler beim Erstellen: ' + err.message)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCreate = () => {
|
|
|
|
|
|
if (!selectedGroupId) {
|
|
|
|
|
|
alert('Bitte wähle zuerst eine Trainingsgruppe')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-28 16:31:45 +02:00
|
|
|
|
const group = groups.find((g) => g.id === parseInt(selectedGroupId, 10))
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
setEditingUnit(null)
|
2026-04-28 16:31:45 +02:00
|
|
|
|
setDraftPlanTemplateId('')
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
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: '',
|
2026-05-05 23:35:41 +02:00
|
|
|
|
sections: [defaultSection('Hauptteil')],
|
|
|
|
|
|
...sessionAssignDefaults()
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
})
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 15:13:00 +02:00
|
|
|
|
const handleCreateForDate = (isoDay) => {
|
|
|
|
|
|
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: isoDay,
|
|
|
|
|
|
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: '',
|
2026-05-05 23:35:41 +02:00
|
|
|
|
sections: [defaultSection('Hauptteil')],
|
|
|
|
|
|
...sessionAssignDefaults()
|
2026-05-06 07:25:11 +02:00
|
|
|
|
})
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const applyTemplateFromSelect = async (templateId) => {
|
2026-04-28 16:31:45 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
const handleEdit = async (unit) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fullUnit = await api.getTrainingUnit(unit.id)
|
|
|
|
|
|
setEditingUnit(fullUnit)
|
2026-04-28 16:31:45 +02:00
|
|
|
|
setDraftPlanTemplateId(fullUnit.plan_template_id ? String(fullUnit.plan_template_id) : '')
|
2026-04-29 06:15:26 +02:00
|
|
|
|
let sections = normalizeUnitToForm(fullUnit)
|
|
|
|
|
|
sections = await enrichSectionsWithVariants(sections)
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
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) || '',
|
2026-04-28 16:31:45 +02:00
|
|
|
|
attendance_count: fullUnit.attendance_count ?? '',
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
status: fullUnit.status || 'planned',
|
|
|
|
|
|
notes: fullUnit.notes || '',
|
|
|
|
|
|
trainer_notes: fullUnit.trainer_notes || '',
|
2026-04-29 06:15:26 +02:00
|
|
|
|
sections,
|
2026-05-05 23:35:41 +02:00
|
|
|
|
lead_trainer_profile_id:
|
|
|
|
|
|
fullUnit.lead_trainer_profile_id != null && fullUnit.lead_trainer_profile_id !== ''
|
|
|
|
|
|
? String(fullUnit.lead_trainer_profile_id)
|
|
|
|
|
|
: '',
|
|
|
|
|
|
session_assistants_inherit:
|
|
|
|
|
|
fullUnit.assistant_trainer_profile_ids == null ||
|
|
|
|
|
|
fullUnit.assistant_trainer_profile_ids === undefined,
|
2026-05-06 07:42:39 +02:00
|
|
|
|
session_assistant_profile_ids: (() => {
|
|
|
|
|
|
const efLead =
|
|
|
|
|
|
fullUnit.effective_lead_trainer_profile_id != null
|
|
|
|
|
|
? Number(fullUnit.effective_lead_trainer_profile_id)
|
|
|
|
|
|
: null
|
|
|
|
|
|
let xs = toNumList(fullUnit.assistant_trainer_profile_ids)
|
|
|
|
|
|
if (efLead != null && Number.isFinite(efLead)) xs = xs.filter((id) => id !== efLead)
|
|
|
|
|
|
return xs
|
|
|
|
|
|
})(),
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
})
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
alert('Fehler beim Laden: ' + err.message)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 16:31:45 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 15:40:29 +02:00
|
|
|
|
const handleTakeLead = async (unit) => {
|
|
|
|
|
|
if (!user?.id) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.updateTrainingUnit(unit.id, { lead_trainer_profile_id: user.id })
|
|
|
|
|
|
await loadUnits()
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
alert(err.message || 'Leitung konnte nicht übernommen werden')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 07:18:30 +02:00
|
|
|
|
const openTrainerAssignModal = (unit) => {
|
2026-05-06 07:42:39 +02:00
|
|
|
|
const effLead =
|
|
|
|
|
|
unit.effective_lead_trainer_profile_id != null
|
|
|
|
|
|
? Number(unit.effective_lead_trainer_profile_id)
|
|
|
|
|
|
: null
|
|
|
|
|
|
let coIds = toNumList(unit.assistant_trainer_profile_ids)
|
|
|
|
|
|
if (effLead != null && Number.isFinite(effLead)) {
|
|
|
|
|
|
coIds = coIds.filter((id) => id !== effLead)
|
|
|
|
|
|
}
|
2026-05-06 07:18:30 +02:00
|
|
|
|
setAssignDraft({
|
|
|
|
|
|
unit,
|
|
|
|
|
|
lead_trainer_profile_id:
|
|
|
|
|
|
unit.lead_trainer_profile_id != null && unit.lead_trainer_profile_id !== ''
|
|
|
|
|
|
? String(unit.lead_trainer_profile_id)
|
|
|
|
|
|
: '',
|
|
|
|
|
|
session_assistants_inherit:
|
|
|
|
|
|
unit.assistant_trainer_profile_ids == null || unit.assistant_trainer_profile_ids === undefined,
|
2026-05-06 07:42:39 +02:00
|
|
|
|
session_assistant_profile_ids: coIds,
|
2026-05-06 07:18:30 +02:00
|
|
|
|
})
|
|
|
|
|
|
setAssignModalOpen(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveTrainerAssignModal = async () => {
|
|
|
|
|
|
if (!assignDraft.unit) return
|
|
|
|
|
|
setAssignSaving(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = {}
|
|
|
|
|
|
const leadStr = String(assignDraft.lead_trainer_profile_id || '').trim()
|
|
|
|
|
|
if (leadStr) payload.lead_trainer_profile_id = parseInt(leadStr, 10)
|
|
|
|
|
|
else payload.lead_trainer_profile_id = null
|
|
|
|
|
|
if (assignDraft.session_assistants_inherit) {
|
|
|
|
|
|
payload.assistant_trainer_profile_ids = null
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payload.assistant_trainer_profile_ids = [...assignDraft.session_assistant_profile_ids].sort((a, b) => a - b)
|
|
|
|
|
|
}
|
|
|
|
|
|
await api.updateTrainingUnit(assignDraft.unit.id, payload)
|
|
|
|
|
|
setAssignModalOpen(false)
|
|
|
|
|
|
setAssignDraft({
|
|
|
|
|
|
unit: null,
|
|
|
|
|
|
...sessionAssignDefaults(),
|
|
|
|
|
|
})
|
|
|
|
|
|
await loadUnits()
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
alert(err.message || 'Zuweisung konnte nicht gespeichert werden')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setAssignSaving(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
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 {
|
2026-04-28 16:31:45 +02:00
|
|
|
|
const sectionsPayload = buildSectionsPayload(formData.sections)
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
const payload = {
|
2026-04-28 16:31:45 +02:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-05-05 23:35:41 +02:00
|
|
|
|
const leadStr = String(formData.lead_trainer_profile_id || '').trim()
|
|
|
|
|
|
if (leadStr) {
|
|
|
|
|
|
payload.lead_trainer_profile_id = parseInt(leadStr, 10)
|
|
|
|
|
|
} else if (editingUnit) {
|
|
|
|
|
|
payload.lead_trainer_profile_id = null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (formData.session_assistants_inherit) {
|
|
|
|
|
|
if (editingUnit) payload.assistant_trainer_profile_ids = null
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payload.assistant_trainer_profile_ids = [...formData.session_assistant_profile_ids].sort(
|
|
|
|
|
|
(a, b) => a - b
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-04-28 16:31:45 +02:00
|
|
|
|
if (!editingUnit) {
|
|
|
|
|
|
payload.group_id = parseInt(formData.group_id, 10)
|
|
|
|
|
|
if (draftPlanTemplateId) {
|
|
|
|
|
|
payload.plan_template_id = parseInt(draftPlanTemplateId, 10)
|
|
|
|
|
|
}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) => {
|
2026-05-06 07:42:39 +02:00
|
|
|
|
setFormData((prev) => {
|
|
|
|
|
|
if (field !== 'lead_trainer_profile_id') return { ...prev, [field]: value }
|
|
|
|
|
|
const ts = typeof value === 'string' ? value.trim() : String(value ?? '').trim()
|
|
|
|
|
|
const strip = new Set()
|
|
|
|
|
|
if (ts !== '') {
|
|
|
|
|
|
const nid = parseInt(ts, 10)
|
|
|
|
|
|
if (Number.isFinite(nid)) strip.add(nid)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const gidParsed = parseInt(prev.group_id || selectedGroupId || '0', 10)
|
|
|
|
|
|
const gr =
|
|
|
|
|
|
Number.isFinite(gidParsed) && gidParsed >= 1
|
|
|
|
|
|
? groups.find((xg) => xg.id === gidParsed)
|
|
|
|
|
|
: null
|
|
|
|
|
|
if (gr?.trainer_id != null) {
|
|
|
|
|
|
const ht = Number(gr.trainer_id)
|
|
|
|
|
|
if (Number.isFinite(ht)) strip.add(ht)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const assistants = prev.session_assistant_profile_ids.filter((id) => !strip.has(id))
|
|
|
|
|
|
return { ...prev, lead_trainer_profile_id: value, session_assistant_profile_ids: assistants }
|
|
|
|
|
|
})
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 15:13:00 +02:00
|
|
|
|
const calendarGridDays = useMemo(() => {
|
|
|
|
|
|
const r = getCalendarGridRange(calendarMonthStr)
|
|
|
|
|
|
return enumerateIsoDays(r.gridStart, r.gridEnd)
|
|
|
|
|
|
}, [calendarMonthStr])
|
|
|
|
|
|
|
|
|
|
|
|
const unitsByPlannedDate = useMemo(() => {
|
|
|
|
|
|
const m = new Map()
|
|
|
|
|
|
for (const u of units) {
|
|
|
|
|
|
const raw = u.planned_date
|
|
|
|
|
|
if (!raw) continue
|
|
|
|
|
|
const key = String(raw).slice(0, 10)
|
|
|
|
|
|
if (!m.has(key)) m.set(key, [])
|
|
|
|
|
|
m.get(key).push(u)
|
|
|
|
|
|
}
|
|
|
|
|
|
return m
|
|
|
|
|
|
}, [units])
|
|
|
|
|
|
|
|
|
|
|
|
const calendarMonthTitle = useMemo(() => {
|
|
|
|
|
|
const p = calendarMonthStr.split('-').map(Number)
|
|
|
|
|
|
const y = p[0]
|
|
|
|
|
|
const mo = p[1]
|
|
|
|
|
|
if (!y || !mo) return ''
|
|
|
|
|
|
return new Date(y, mo - 1, 1).toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })
|
|
|
|
|
|
}, [calendarMonthStr])
|
|
|
|
|
|
|
2026-05-06 07:42:39 +02:00
|
|
|
|
const mayConfigureSessionAssignments = useCallback(
|
|
|
|
|
|
(unit) => {
|
|
|
|
|
|
if (!unit) return false
|
|
|
|
|
|
const pid = Number(user?.id)
|
|
|
|
|
|
if (!Number.isFinite(pid)) return false
|
|
|
|
|
|
const r = (user?.role || '').toLowerCase()
|
|
|
|
|
|
if (r === 'admin' || r === 'superadmin') return true
|
|
|
|
|
|
|
|
|
|
|
|
const gClub = unit.group_club_id != null ? Number(unit.group_club_id) : null
|
|
|
|
|
|
if (Number.isFinite(gClub) && clubAdminClubIdSet.has(gClub)) return true
|
|
|
|
|
|
|
|
|
|
|
|
const gid = Number(unit.group_id)
|
|
|
|
|
|
const g = groups.find((gr) => gr.id === gid)
|
|
|
|
|
|
if (!g) return false
|
|
|
|
|
|
|
|
|
|
|
|
const cb = unit.created_by != null ? Number(unit.created_by) : NaN
|
|
|
|
|
|
if (Number.isFinite(cb) && cb === pid) return true
|
|
|
|
|
|
|
|
|
|
|
|
const ht = g.trainer_id != null ? Number(g.trainer_id) : NaN
|
|
|
|
|
|
if (Number.isFinite(ht) && ht === pid) return true
|
|
|
|
|
|
|
|
|
|
|
|
return normalizeGroupCoTrainerIds(g.co_trainer_ids).includes(pid)
|
|
|
|
|
|
},
|
|
|
|
|
|
[user?.id, user?.role, groups, clubAdminClubIdSet]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
2026-05-05 12:39:15 +02:00
|
|
|
|
<div className="app-page" style={{ padding: '2rem 0', textAlign: 'center' }}>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<div className="spinner"></div>
|
|
|
|
|
|
<p>Laden...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 16:31:45 +02:00
|
|
|
|
const selectedGroup = groups.find((g) => g.id === parseInt(selectedGroupId, 10))
|
2026-05-06 07:42:39 +02:00
|
|
|
|
|
|
|
|
|
|
const gidTrainerForm = parseInt(formData.group_id || selectedGroupId || '0', 10)
|
|
|
|
|
|
const groupForTrainerForm =
|
|
|
|
|
|
Number.isFinite(gidTrainerForm) && gidTrainerForm >= 1
|
|
|
|
|
|
? groups.find((gr) => gr.id === gidTrainerForm)
|
|
|
|
|
|
: null
|
|
|
|
|
|
|
|
|
|
|
|
let formTrainerAssignLeadExcludeId = null
|
|
|
|
|
|
if (groupForTrainerForm?.trainer_id != null) formTrainerAssignLeadExcludeId = Number(groupForTrainerForm.trainer_id)
|
|
|
|
|
|
const leadDraftTrim = String(formData.lead_trainer_profile_id || '').trim()
|
|
|
|
|
|
if (leadDraftTrim !== '') {
|
|
|
|
|
|
const nl = parseInt(leadDraftTrim, 10)
|
|
|
|
|
|
if (Number.isFinite(nl)) formTrainerAssignLeadExcludeId = nl
|
|
|
|
|
|
}
|
|
|
|
|
|
if (editingUnit?.effective_lead_trainer_profile_id != null && leadDraftTrim === '') {
|
|
|
|
|
|
const el = Number(editingUnit.effective_lead_trainer_profile_id)
|
|
|
|
|
|
if (Number.isFinite(el)) formTrainerAssignLeadExcludeId = el
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const clubDirectoryForCo = filterDirectoryExcludingLead(clubDirectory, formTrainerAssignLeadExcludeId)
|
|
|
|
|
|
|
|
|
|
|
|
let assignExcludeLeadPid = null
|
|
|
|
|
|
if (assignModalOpen && assignDraft.unit) {
|
|
|
|
|
|
const dl = String(assignDraft.lead_trainer_profile_id || '').trim()
|
|
|
|
|
|
if (dl !== '') {
|
|
|
|
|
|
const n = parseInt(dl, 10)
|
|
|
|
|
|
assignExcludeLeadPid = Number.isFinite(n) ? n : null
|
|
|
|
|
|
} else if (assignDraft.unit.effective_lead_trainer_profile_id != null) {
|
|
|
|
|
|
const n = Number(assignDraft.unit.effective_lead_trainer_profile_id)
|
|
|
|
|
|
assignExcludeLeadPid = Number.isFinite(n) ? n : null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const clubDirectoryForAssignCo = filterDirectoryExcludingLead(clubDirectory, assignExcludeLeadPid)
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-05 12:41:59 +02:00
|
|
|
|
<div className="app-page">
|
2026-04-28 19:46:09 +02:00
|
|
|
|
<h1 style={{ marginBottom: '0.35rem' }}>Trainingsplanung</h1>
|
2026-05-05 15:18:54 +02:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '12px',
|
|
|
|
|
|
marginBottom: '1rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className="form-label"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: 0,
|
|
|
|
|
|
fontSize: '0.9rem',
|
|
|
|
|
|
color: 'var(--text2)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Ansicht
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div
|
|
|
|
|
|
role="group"
|
|
|
|
|
|
aria-label="Darstellung Liste oder Kalender"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'inline-flex',
|
|
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
border: '1.5px solid var(--border2)',
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
boxSizing: 'border-box',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-pressed={planView === 'list'}
|
|
|
|
|
|
onClick={() => setPlanView('list')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
padding: '10px 20px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '0.92rem',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
background: planView === 'list' ? 'var(--accent-dark)' : 'transparent',
|
|
|
|
|
|
color: planView === 'list' ? '#fff' : 'var(--text1)',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Liste
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-pressed={planView === 'calendar'}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setPlanView('calendar')
|
|
|
|
|
|
setCalendarMonthStr((prev) => {
|
|
|
|
|
|
const fromList = (startDate || '').slice(0, 7)
|
|
|
|
|
|
if (/^\d{4}-\d{2}$/.test(fromList)) return fromList
|
|
|
|
|
|
return prev || new Date().toISOString().slice(0, 7)
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderLeft: '1.5px solid var(--border2)',
|
|
|
|
|
|
padding: '10px 20px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '0.92rem',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
background: planView === 'calendar' ? 'var(--accent-dark)' : 'transparent',
|
|
|
|
|
|
color: planView === 'calendar' ? '#fff' : 'var(--text1)',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Kalender
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span style={{ fontSize: '0.8rem', color: 'var(--text3)', lineHeight: 1.35, maxWidth: '22rem' }}>
|
|
|
|
|
|
{planView === 'list'
|
|
|
|
|
|
? 'Zeitraum unten mit Von/Bis filtern.'
|
|
|
|
|
|
: 'Monat unten wechseln; Termine erscheinen im Raster.'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-28 19:46:09 +02:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-05-05 09:02:13 +02:00
|
|
|
|
<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>
|
2026-04-28 19:46:09 +02:00
|
|
|
|
{!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>
|
|
|
|
|
|
)}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
|
|
|
|
|
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
|
|
|
|
|
gap: '1rem'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<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>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
{groups.map((g) => (
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<option key={g.id} value={g.id}>
|
|
|
|
|
|
{g.name} ({g.club_name})
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-05 15:13:00 +02:00
|
|
|
|
{planView === 'list' ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<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
|
|
|
|
|
|
style={{
|
|
|
|
|
|
gridColumn: '1 / -1',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '0.65rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
|
|
|
|
aria-label="Voriger Monat"
|
|
|
|
|
|
onClick={() => setCalendarMonthStr((prev) => shiftCalendarMonth(prev, -1))}
|
|
|
|
|
|
>
|
|
|
|
|
|
←
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '1rem',
|
|
|
|
|
|
flex: '1 1 auto',
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
minWidth: '12rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{calendarMonthTitle}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
|
|
|
|
aria-label="Nächster Monat"
|
|
|
|
|
|
onClick={() => setCalendarMonthStr((prev) => shiftCalendarMonth(prev, 1))}
|
|
|
|
|
|
>
|
|
|
|
|
|
→
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
style={{ whiteSpace: 'nowrap', padding: '0.35rem 0.65rem', fontSize: '0.85rem' }}
|
|
|
|
|
|
onClick={() => setCalendarMonthStr(new Date().toISOString().slice(0, 7))}
|
|
|
|
|
|
>
|
|
|
|
|
|
Aktueller Monat
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-05-05 15:40:29 +02:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
gridColumn: '1 / -1',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '12px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="form-label" style={{ marginBottom: 0, alignSelf: 'center' }}>
|
|
|
|
|
|
Einblenden
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div
|
|
|
|
|
|
role="group"
|
|
|
|
|
|
aria-label="Gruppe oder ganzer Verein"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'inline-flex',
|
|
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
border: '1.5px solid var(--border2)',
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-pressed={planScope === 'group'}
|
|
|
|
|
|
disabled={!selectedGroupId}
|
|
|
|
|
|
onClick={() => setPlanScope('group')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
padding: '8px 14px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '0.85rem',
|
|
|
|
|
|
cursor: selectedGroupId ? 'pointer' : 'not-allowed',
|
|
|
|
|
|
opacity: selectedGroupId ? 1 : 0.55,
|
|
|
|
|
|
background: planScope === 'group' ? 'var(--accent-dark)' : 'transparent',
|
|
|
|
|
|
color: planScope === 'group' ? '#fff' : 'var(--text1)',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Nur diese Gruppe
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-pressed={planScope === 'club'}
|
|
|
|
|
|
disabled={!selectedGroupId}
|
|
|
|
|
|
onClick={() => setPlanScope('club')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderLeft: '1.5px solid var(--border2)',
|
|
|
|
|
|
padding: '8px 14px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '0.85rem',
|
|
|
|
|
|
cursor: selectedGroupId ? 'pointer' : 'not-allowed',
|
|
|
|
|
|
opacity: selectedGroupId ? 1 : 0.55,
|
|
|
|
|
|
background: planScope === 'club' ? 'var(--accent-dark)' : 'transparent',
|
|
|
|
|
|
color: planScope === 'club' ? '#fff' : 'var(--text1)',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Ganzer Verein
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<label
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'inline-flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
fontSize: '0.88rem',
|
|
|
|
|
|
color: 'var(--text2)',
|
|
|
|
|
|
cursor: selectedGroupId ? 'pointer' : 'default',
|
|
|
|
|
|
opacity: selectedGroupId ? 1 : 0.65,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={assignedToMeOnly}
|
|
|
|
|
|
disabled={!selectedGroupId}
|
|
|
|
|
|
onChange={(e) => setAssignedToMeOnly(e.target.checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
Nur meine Zuordnung (Leitung / Co)
|
|
|
|
|
|
</label>
|
2026-05-06 07:18:30 +02:00
|
|
|
|
<span style={{ fontSize: '0.78rem', color: 'var(--text3)', lineHeight: 1.4, flex: '1 1 240px' }}>
|
2026-05-06 07:42:39 +02:00
|
|
|
|
„Ganzer Verein“ bezieht sich auf denselben Verein wie die gewählte Gruppe: Dort siehst du Termine mehrerer Gruppen; neu angelegte Termine gelten weiter für die gesondert gewählte Gruppe.
|
|
|
|
|
|
{selectedGroupId ? (
|
2026-05-06 07:18:30 +02:00
|
|
|
|
<span style={{ display: 'block', marginTop: '6px', color: 'var(--text2)' }}>
|
2026-05-06 07:42:39 +02:00
|
|
|
|
Über <strong>Trainer</strong> oder <strong>Trainer zuweisen</strong>: Leitung und Co je Einheit bearbeitbar (berechtigt: Vereinsorganisation, Haupt-/Co‑Trainer der Gruppe sowie Erstellung der Einheit).
|
|
|
|
|
|
Das Mitgliederverzeichnis listet nur <strong>eigene Vereinsmitglieder</strong>; die Leitung erscheint nicht unter Co‑Trainer.
|
|
|
|
|
|
Gasttrainer aus anderen Vereinen (Zugriff nur auf eine Session, nicht auf den Verein insgesamt) sind für später vorgesehen.
|
2026-05-06 07:18:30 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
) : null}
|
2026-05-05 15:40:29 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{selectedGroup && (
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginTop: '1rem',
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
borderRadius: '8px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<p style={{ fontSize: '0.875rem', color: 'var(--text2)' }}>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
{selectedGroup.location || 'Kein Ort angegeben'}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
{selectedGroup.weekday && ` · ${selectedGroup.weekday}`}
|
2026-04-28 16:31:45 +02:00
|
|
|
|
{selectedGroup.time_start &&
|
|
|
|
|
|
` · ${selectedGroup.time_start.slice(0, 5)} - ${selectedGroup.time_end?.slice(0, 5)}`}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
2026-04-28 19:46:09 +02:00
|
|
|
|
marginTop: '1.25rem',
|
|
|
|
|
|
paddingTop: '1rem',
|
|
|
|
|
|
borderTop: '1px solid var(--border, rgba(0,0,0,0.08))'
|
2026-04-28 16:31:45 +02:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-04-28 19:46:09 +02:00
|
|
|
|
<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}
|
2026-04-28 16:31:45 +02:00
|
|
|
|
>
|
2026-04-28 19:46:09 +02:00
|
|
|
|
+ Neue Trainingseinheit planen
|
2026-04-28 16:31:45 +02:00
|
|
|
|
</button>
|
2026-04-28 19:46:09 +02:00
|
|
|
|
<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>
|
2026-05-05 15:03:54 +02:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
disabled={!selectedGroupId}
|
|
|
|
|
|
title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined}
|
|
|
|
|
|
onClick={openFrameworkImportModal}
|
|
|
|
|
|
>
|
|
|
|
|
|
Aus Rahmen übernehmen…
|
|
|
|
|
|
</button>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
</div>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</div>
|
2026-04-28 19:46:09 +02:00
|
|
|
|
</div>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
|
|
|
|
|
{!selectedGroupId ? (
|
|
|
|
|
|
<div className="card">
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
2026-04-28 19:46:09 +02:00
|
|
|
|
Wähle oben eine Trainingsgruppe — danach kannst du mit <strong>„Neue Trainingseinheit planen“</strong> starten.
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-05-05 15:13:00 +02:00
|
|
|
|
) : planView === 'calendar' ? (
|
|
|
|
|
|
<div className="card" style={{ padding: 'clamp(8px, 2vw, 14px)', overflowX: 'auto' }}>
|
|
|
|
|
|
{units.length === 0 ? (
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.88rem', margin: '0 0 12px', lineHeight: 1.45 }}>
|
|
|
|
|
|
Im sichtbaren Monatsbereich liegt noch keine Einheit. Über <strong>+</strong> in einem Tag legst du einen
|
|
|
|
|
|
neuen Termin mit Datum an.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns: 'repeat(7, minmax(0, 1fr))',
|
|
|
|
|
|
gap: '4px',
|
|
|
|
|
|
minWidth: 'min(960px, 100%)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
role="grid"
|
|
|
|
|
|
aria-label={`Kalender ${calendarMonthTitle}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{WEEKDAYS_DE.map((w) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={w}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '0.72rem',
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
padding: '6px 2px',
|
|
|
|
|
|
color: 'var(--text3)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
role="columnheader"
|
|
|
|
|
|
>
|
|
|
|
|
|
{w}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{calendarGridDays.map((dayIso) => {
|
|
|
|
|
|
const inMonth = dayIso.slice(0, 7) === calendarMonthStr
|
|
|
|
|
|
const dayNum = parseInt(dayIso.slice(8, 10), 10)
|
|
|
|
|
|
const isTodayMarker = dayIso === today
|
|
|
|
|
|
const dayUnits = unitsByPlannedDate.get(dayIso) || []
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={dayIso}
|
|
|
|
|
|
role="gridcell"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
minHeight: '96px',
|
|
|
|
|
|
border: '1px solid var(--border, rgba(0, 0, 0, 0.1))',
|
|
|
|
|
|
borderRadius: '6px',
|
|
|
|
|
|
padding: '5px',
|
|
|
|
|
|
background: inMonth ? 'var(--surface)' : 'var(--surface2)',
|
|
|
|
|
|
opacity: inMonth ? 1 : 0.58,
|
|
|
|
|
|
boxShadow: isTodayMarker ? 'inset 0 0 0 2px var(--accent-dark, #256)' : undefined,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
|
gap: '4px',
|
|
|
|
|
|
marginBottom: '6px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span style={{ fontWeight: 700, fontSize: '0.82rem', lineHeight: 1 }}>{dayNum}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
|
|
|
|
title={`Neue Trainingseinheit am ${dayIso}`}
|
|
|
|
|
|
aria-label={`Neue Trainingseinheit am ${dayIso}`}
|
|
|
|
|
|
onClick={() => handleCreateForDate(dayIso)}
|
|
|
|
|
|
disabled={!selectedGroupId}
|
|
|
|
|
|
style={{ padding: '2px 6px', flexShrink: 0 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
+
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
|
|
|
|
{dayUnits.slice(0, 3).map((unit) => (
|
2026-05-06 07:18:30 +02:00
|
|
|
|
<div
|
2026-05-05 15:13:00 +02:00
|
|
|
|
key={unit.id}
|
2026-05-06 07:18:30 +02:00
|
|
|
|
style={{ display: 'flex', flexDirection: 'column', gap: '4px', width: '100%' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handleEdit(unit)}
|
|
|
|
|
|
title={[
|
|
|
|
|
|
planScope === 'club' && unit.group_name ? unit.group_name : '',
|
|
|
|
|
|
unit.planned_time_start?.slice(0, 5) || '',
|
|
|
|
|
|
unit.lead_trainer_name?.trim(),
|
|
|
|
|
|
unit.planned_focus?.trim(),
|
2026-05-05 15:13:00 +02:00
|
|
|
|
unit.status === 'completed'
|
2026-05-06 07:18:30 +02:00
|
|
|
|
? 'Durchgeführt'
|
2026-05-05 15:13:00 +02:00
|
|
|
|
: unit.status === 'cancelled'
|
2026-05-06 07:18:30 +02:00
|
|
|
|
? 'Abgesagt'
|
|
|
|
|
|
: 'Geplant',
|
|
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(' · ')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
textAlign: 'left',
|
|
|
|
|
|
padding: '4px 5px',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
fontSize: '0.7rem',
|
|
|
|
|
|
lineHeight: 1.25,
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
borderLeftWidth: '3px',
|
|
|
|
|
|
borderLeftStyle: 'solid',
|
|
|
|
|
|
borderLeftColor:
|
|
|
|
|
|
unit.status === 'completed'
|
|
|
|
|
|
? '#2ea44f'
|
|
|
|
|
|
: unit.status === 'cancelled'
|
|
|
|
|
|
? 'var(--danger)'
|
|
|
|
|
|
: 'var(--accent-dark)',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
color: 'var(--text1)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span style={{ fontWeight: 600 }}>
|
|
|
|
|
|
{unit.planned_time_start
|
|
|
|
|
|
? `${unit.planned_time_start.slice(0, 5)}`
|
|
|
|
|
|
: 'Ganztags'}
|
2026-05-05 15:13:00 +02:00
|
|
|
|
</span>
|
2026-05-06 07:18:30 +02:00
|
|
|
|
{planScope === 'club' && (unit.group_name || '').trim() ? (
|
|
|
|
|
|
<span style={{ display: 'block', fontWeight: 500, color: 'var(--text1)' }}>
|
|
|
|
|
|
{(unit.group_name || '').trim().length > 22
|
|
|
|
|
|
? `${(unit.group_name || '').trim().slice(0, 22)}…`
|
|
|
|
|
|
: unit.group_name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{unit.lead_trainer_name?.trim() ? (
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'block',
|
|
|
|
|
|
color: 'var(--text3)',
|
|
|
|
|
|
fontWeight: 400,
|
|
|
|
|
|
fontSize: '0.62rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{unit.lead_trainer_name.trim().split(/\s+/).slice(-1)[0] ||
|
|
|
|
|
|
unit.lead_trainer_name.trim()}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{unit.planned_focus?.trim() ? (
|
|
|
|
|
|
<span style={{ display: 'block', color: 'var(--text2)', fontWeight: 400 }}>
|
|
|
|
|
|
{(unit.planned_focus || '').trim().length > 24
|
|
|
|
|
|
? `${(unit.planned_focus || '').trim().slice(0, 24)}…`
|
|
|
|
|
|
: unit.planned_focus}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</button>
|
2026-05-06 07:42:39 +02:00
|
|
|
|
{mayConfigureSessionAssignments(unit) ? (
|
2026-05-06 07:18:30 +02:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
|
|
|
|
style={{ fontSize: '0.62rem', padding: '2px 4px', alignSelf: 'stretch' }}
|
|
|
|
|
|
onClick={(ev) => {
|
|
|
|
|
|
ev.stopPropagation()
|
|
|
|
|
|
openTrainerAssignModal(unit)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Trainer
|
|
|
|
|
|
</button>
|
2026-05-05 15:13:00 +02:00
|
|
|
|
) : null}
|
2026-05-06 07:18:30 +02:00
|
|
|
|
</div>
|
2026-05-05 15:13:00 +02:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{dayUnits.length > 3 ? (
|
|
|
|
|
|
<p style={{ fontSize: '0.65rem', color: 'var(--text3)', margin: '4px 0 0', lineHeight: 1.3 }}>
|
|
|
|
|
|
+{dayUnits.length - 3} weitere
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
) : units.length === 0 ? (
|
|
|
|
|
|
<div className="card">
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
2026-04-28 19:46:09 +02:00
|
|
|
|
Keine Trainingseinheiten in diesem Zeitraum. Nutze oben <strong>„Neue Trainingseinheit planen“</strong> oder{' '}
|
|
|
|
|
|
<strong>„Schnell erstellen“</strong>, um den ersten Termin anzulegen.
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
2026-05-05 15:03:54 +02:00
|
|
|
|
{units.map((unit) => {
|
|
|
|
|
|
const lineage = unit.origin_framework_slot_id ? frameworkLineageText(unit) : null
|
2026-05-05 15:40:29 +02:00
|
|
|
|
const uid = user?.id != null ? Number(user.id) : null
|
|
|
|
|
|
const effLead =
|
|
|
|
|
|
unit.effective_lead_trainer_profile_id != null
|
|
|
|
|
|
? Number(unit.effective_lead_trainer_profile_id)
|
|
|
|
|
|
: null
|
|
|
|
|
|
const showTakeLead =
|
|
|
|
|
|
unit.status === 'planned' && uid != null && effLead != null && effLead !== uid
|
2026-05-05 15:03:54 +02:00
|
|
|
|
return (
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<div key={unit.id} className="card">
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
2026-05-05 12:39:15 +02:00
|
|
|
|
flexWrap: 'wrap',
|
2026-04-28 16:31:45 +02:00
|
|
|
|
justifyContent: 'space-between',
|
2026-05-05 12:39:15 +02:00
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
|
gap: '12px',
|
2026-04-28 16:31:45 +02:00
|
|
|
|
marginBottom: '1rem'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-05-05 12:39:15 +02:00
|
|
|
|
<div style={{ minWidth: 0, flex: '1 1 200px' }}>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<h3 style={{ marginBottom: '0.25rem' }}>
|
|
|
|
|
|
{unit.planned_date}
|
2026-04-28 16:31:45 +02:00
|
|
|
|
{unit.planned_time_start &&
|
|
|
|
|
|
` · ${unit.planned_time_start.slice(0, 5)} - ${unit.planned_time_end?.slice(0, 5)}`}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</h3>
|
2026-05-05 15:40:29 +02:00
|
|
|
|
{planScope === 'club' && (unit.group_name || '').trim() ? (
|
|
|
|
|
|
<p
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '0.85rem',
|
|
|
|
|
|
color: 'var(--text3)',
|
|
|
|
|
|
margin: '0 0 0.35rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{unit.group_name}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
<p style={{ fontSize: '0.82rem', color: 'var(--text2)', margin: '0 0 0.5rem' }}>
|
|
|
|
|
|
Leitung: {(unit.lead_trainer_name || '').trim() || '—'}
|
|
|
|
|
|
</p>
|
2026-05-05 23:35:41 +02:00
|
|
|
|
{(() => {
|
|
|
|
|
|
const coRaw = unit.effective_assistant_trainer_profile_ids
|
|
|
|
|
|
const co = Array.isArray(coRaw)
|
|
|
|
|
|
? coRaw.map(Number).filter((x) => Number.isFinite(x) && x >= 1)
|
|
|
|
|
|
: []
|
|
|
|
|
|
if (!co.length) return null
|
|
|
|
|
|
const src =
|
|
|
|
|
|
unit.assistant_trainer_profile_ids != null
|
|
|
|
|
|
? 'Session-Zuweisung'
|
|
|
|
|
|
: 'über Trainingsgruppe'
|
|
|
|
|
|
return (
|
|
|
|
|
|
<p
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '0.78rem',
|
|
|
|
|
|
color: 'var(--text3)',
|
|
|
|
|
|
margin: '0 0 0.5rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Co-Trainer ({src}): {co.length}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)
|
|
|
|
|
|
})()}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
{unit.planned_focus && (
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<p
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: 'var(--text2)',
|
|
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
|
|
marginBottom: '0.5rem'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Fokus: {unit.planned_focus}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
2026-05-05 15:03:54 +02:00
|
|
|
|
{lineage ? (
|
|
|
|
|
|
<p
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: 'var(--text2)',
|
|
|
|
|
|
fontSize: '0.82rem',
|
|
|
|
|
|
marginBottom: '0.5rem',
|
|
|
|
|
|
lineHeight: 1.45,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span style={{ fontWeight: 600, color: 'var(--text3)' }}>Aus Rahmen: </span>
|
|
|
|
|
|
{unit.origin_framework_program_id ? (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
to={`/planning/framework-programs/${unit.origin_framework_program_id}`}
|
|
|
|
|
|
style={{ color: 'var(--accent-dark)' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{lineage.fpTitle}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span>{lineage.fpTitle}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span style={{ color: 'var(--text2)' }}> · {lineage.slotBit}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
fontSize: '0.75rem',
|
|
|
|
|
|
padding: '0.25rem 0.5rem',
|
|
|
|
|
|
borderRadius: '4px',
|
2026-04-28 16:31:45 +02:00
|
|
|
|
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
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-05 12:39:15 +02:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
gap: '0.5rem',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
|
flex: '1 1 180px',
|
|
|
|
|
|
minWidth: 0
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-04-29 06:37:52 +02:00
|
|
|
|
<Link
|
|
|
|
|
|
to={`/planning/run/${unit.id}`}
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Plan & Ablauf
|
|
|
|
|
|
</Link>
|
2026-04-29 06:49:16 +02:00
|
|
|
|
<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>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<button className="btn btn-secondary" onClick={() => handleEdit(unit)}>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
Bearbeiten
|
|
|
|
|
|
</button>
|
2026-05-06 07:42:39 +02:00
|
|
|
|
{mayConfigureSessionAssignments(unit) ? (
|
2026-05-06 07:18:30 +02:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
onClick={() => openTrainerAssignModal(unit)}
|
|
|
|
|
|
title="Nur organisatorisch: Leitung und Co für diese Einheit"
|
|
|
|
|
|
>
|
|
|
|
|
|
Trainer zuweisen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
2026-05-05 15:40:29 +02:00
|
|
|
|
{showTakeLead ? (
|
|
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => handleTakeLead(unit)}>
|
|
|
|
|
|
Ich übernehme
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<button
|
|
|
|
|
|
className="btn"
|
2026-04-28 16:31:45 +02:00
|
|
|
|
style={{ background: 'var(--danger)', color: 'white', border: 'none' }}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
onClick={() => handleDelete(unit)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Löschen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{unit.notes && (
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginTop: '0.5rem' }}>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
{unit.notes}
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-05-05 15:03:54 +02:00
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-05-06 07:18:30 +02:00
|
|
|
|
{assignModalOpen && assignDraft.unit ? (
|
|
|
|
|
|
<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: 1020,
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
}}
|
|
|
|
|
|
role="presentation"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (!assignSaving) setAssignModalOpen(false)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
role="dialog"
|
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
|
aria-labelledby="trainer-assign-modal-title"
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'var(--surface)',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
padding: 'clamp(14px, 3vw, 1.75rem)',
|
|
|
|
|
|
maxWidth: 'min(460px, 100%)',
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
maxHeight: '90vh',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
boxSizing: 'border-box',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 id="trainer-assign-modal-title" style={{ marginBottom: '0.5rem', fontSize: '1.1rem' }}>
|
|
|
|
|
|
Trainer zuweisen (organisatorisch)
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p style={{ fontSize: '0.86rem', color: 'var(--text2)', marginBottom: '1rem', lineHeight: 1.45 }}>
|
|
|
|
|
|
{(assignDraft.unit.planned_date || '').toString().slice(0, 10)}
|
|
|
|
|
|
{assignDraft.unit.planned_time_start
|
|
|
|
|
|
? ` · ${String(assignDraft.unit.planned_time_start).slice(0, 5)}`
|
|
|
|
|
|
: ''}
|
|
|
|
|
|
{(assignDraft.unit.group_name || '').trim()
|
|
|
|
|
|
? ` · ${(assignDraft.unit.group_name || '').trim()}`
|
|
|
|
|
|
: null}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Leitung (diese Einheit)</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
value={assignDraft.lead_trainer_profile_id}
|
2026-05-06 07:42:39 +02:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const v = e.target.value
|
|
|
|
|
|
setAssignDraft((prev) => {
|
|
|
|
|
|
const exclude = []
|
|
|
|
|
|
const tr = String(v || '').trim()
|
|
|
|
|
|
if (tr !== '') {
|
|
|
|
|
|
const n = parseInt(tr, 10)
|
|
|
|
|
|
if (Number.isFinite(n)) exclude.push(n)
|
|
|
|
|
|
} else if (prev.unit?.effective_lead_trainer_profile_id != null) {
|
|
|
|
|
|
const ef = Number(prev.unit.effective_lead_trainer_profile_id)
|
|
|
|
|
|
if (Number.isFinite(ef)) exclude.push(ef)
|
|
|
|
|
|
}
|
|
|
|
|
|
const exSet = new Set(exclude)
|
|
|
|
|
|
const co = exclude.length
|
|
|
|
|
|
? prev.session_assistant_profile_ids.filter((x) => !exSet.has(x))
|
|
|
|
|
|
: prev.session_assistant_profile_ids
|
|
|
|
|
|
return { ...prev, lead_trainer_profile_id: v, session_assistant_profile_ids: co }
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
2026-05-06 07:18:30 +02:00
|
|
|
|
disabled={assignSaving}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">Standard (Haupttrainer der Gruppe)</option>
|
|
|
|
|
|
{clubDirectory.map((m) => {
|
|
|
|
|
|
const idStr = String(m.id)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<option key={idStr} value={idStr}>
|
|
|
|
|
|
{(m.name || '').trim() || m.email || `Profil ${m.id}`}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row" style={{ marginTop: '0.85rem' }}>
|
|
|
|
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={assignDraft.session_assistants_inherit}
|
|
|
|
|
|
disabled={assignSaving}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setAssignDraft((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
session_assistants_inherit: e.target.checked,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span style={{ fontSize: '0.9rem', color: 'var(--text1)' }}>
|
|
|
|
|
|
Co-Trainer wie in der Trainingsgruppe
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{!assignDraft.session_assistants_inherit ? (
|
|
|
|
|
|
<div style={{ marginTop: '10px', maxHeight: '180px', overflowY: 'auto' }}>
|
2026-05-06 07:42:39 +02:00
|
|
|
|
{clubDirectoryForAssignCo.map((m) => {
|
2026-05-06 07:18:30 +02:00
|
|
|
|
const mid = typeof m.id === 'number' ? m.id : parseInt(String(m.id), 10)
|
|
|
|
|
|
const labelText = `${(m.name || '').trim() || m.email || `Profil ${mid}`}`
|
|
|
|
|
|
const isOn = Number.isFinite(mid) && assignDraft.session_assistant_profile_ids.includes(mid)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<label
|
|
|
|
|
|
key={`assign-co-${mid}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
|
|
marginBottom: '6px',
|
|
|
|
|
|
cursor: assignSaving ? 'default' : 'pointer',
|
|
|
|
|
|
color: 'var(--text1)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={isOn}
|
|
|
|
|
|
disabled={assignSaving}
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setAssignDraft((prev) => {
|
|
|
|
|
|
const was = prev.session_assistant_profile_ids.includes(mid)
|
|
|
|
|
|
const nextIds = was
|
|
|
|
|
|
? prev.session_assistant_profile_ids.filter((x) => x !== mid)
|
|
|
|
|
|
: [...prev.session_assistant_profile_ids, mid].sort((a, b) => a - b)
|
|
|
|
|
|
return { ...prev, session_assistant_profile_ids: nextIds }
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span>{labelText}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{!clubDirectory.length ? (
|
|
|
|
|
|
<p style={{ marginTop: '10px', fontSize: '0.82rem', color: 'var(--text3)' }}>
|
|
|
|
|
|
Mitgliederverzeichnis konnte nicht geladen werden.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
gap: '0.65rem',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
|
marginTop: '1.25rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
disabled={assignSaving}
|
|
|
|
|
|
onClick={() => setAssignModalOpen(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Abbrechen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="btn btn-primary" disabled={assignSaving} onClick={saveTrainerAssignModal}>
|
|
|
|
|
|
{assignSaving ? 'Speichern …' : 'Speichern'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
2026-05-05 15:03:54 +02:00
|
|
|
|
{frameworkImportOpen && (
|
|
|
|
|
|
<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: 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) => onFwImportProgramChange(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={() => toggleFwImportSlot(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) =>
|
|
|
|
|
|
setFwImportSlotDates((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[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) => setFwImportStartDate(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) => setFwImportIntervalDays(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={applyFwImportDateSuggestions}
|
|
|
|
|
|
>
|
|
|
|
|
|
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={submitFrameworkImport}
|
|
|
|
|
|
>
|
|
|
|
|
|
{fwImportSubmitting ? 'Übernehmen…' : 'In Planung übernehmen'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
disabled={fwImportSubmitting}
|
|
|
|
|
|
onClick={() => setFrameworkImportOpen(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Abbrechen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showModal && (
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<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',
|
2026-05-05 12:39:15 +02:00
|
|
|
|
padding: 'clamp(12px, 3vw, 2rem)',
|
2026-05-05 14:47:38 +02:00
|
|
|
|
maxWidth: 'min(1100px, 100%)',
|
2026-04-28 16:31:45 +02:00
|
|
|
|
width: '100%',
|
|
|
|
|
|
maxHeight: '92vh',
|
|
|
|
|
|
overflowY: 'auto',
|
2026-05-05 12:39:15 +02:00
|
|
|
|
margin: 'max(0px, env(safe-area-inset-top, 0px)) auto',
|
|
|
|
|
|
boxSizing: 'border-box',
|
|
|
|
|
|
minWidth: 0
|
2026-04-28 16:31:45 +02:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 style={{ marginBottom: '1rem' }}>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
{editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'}
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
2026-05-05 15:03:54 +02:00
|
|
|
|
{editingUnit?.origin_framework_slot_id ? (() => {
|
|
|
|
|
|
const L = frameworkLineageText(editingUnit)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="card"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: '1.1rem',
|
|
|
|
|
|
padding: '12px 14px',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
fontSize: '0.9rem',
|
|
|
|
|
|
lineHeight: 1.5,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong style={{ color: 'var(--text1)' }}>Herkunft:</strong>{' '}
|
|
|
|
|
|
{editingUnit.origin_framework_program_id ? (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
to={`/planning/framework-programs/${editingUnit.origin_framework_program_id}`}
|
|
|
|
|
|
style={{ color: 'var(--accent-dark)' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{L.fpTitle}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
L.fpTitle
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span style={{ color: 'var(--text2)' }}> · {L.slotBit}</span>
|
|
|
|
|
|
<p style={{ margin: '0.5rem 0 0', fontSize: '0.82rem', color: 'var(--text2)' }}>
|
|
|
|
|
|
Inhalt stammt aus dem Session-Blueprint des Rahmenprogramms. Änderungen gelten nur für diese
|
|
|
|
|
|
geplante Einheit; die Zuordnung zum Rahmen bleibt zur Nachverfolgung erhalten.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})() : null}
|
|
|
|
|
|
|
2026-04-28 16:31:45 +02:00
|
|
|
|
{!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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
|
|
|
|
<h3 style={{ marginBottom: '1rem' }}>Planung</h3>
|
|
|
|
|
|
|
2026-05-05 12:39:15 +02:00
|
|
|
|
<div className="responsive-grid-3" style={{ marginBottom: '1rem' }}>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<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)}
|
2026-04-28 16:31:45 +02:00
|
|
|
|
placeholder="z.B. Grundlagen, Kinder altersgerecht"
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-05 23:35:41 +02:00
|
|
|
|
<div
|
|
|
|
|
|
className="card"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginTop: '1.25rem',
|
|
|
|
|
|
marginBottom: '0.25rem',
|
|
|
|
|
|
padding: '12px 14px',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h3 style={{ margin: '0 0 10px', fontSize: '1rem' }}>Trainerzuordnung (diese Einheit)</h3>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Leitung</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
value={formData.lead_trainer_profile_id}
|
|
|
|
|
|
onChange={(e) => updateFormField('lead_trainer_profile_id', e.target.value)}
|
|
|
|
|
|
disabled={!editingUnit && !formData.group_id}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">Standard (Haupttrainer der Gruppe)</option>
|
|
|
|
|
|
{clubDirectory.map((m) => {
|
|
|
|
|
|
const idStr = String(m.id)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<option key={idStr} value={idStr}>
|
|
|
|
|
|
{(m.name || '').trim() || m.email || `Profil ${m.id}`}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<p style={{ fontSize: '0.8rem', color: 'var(--text2)', marginTop: '0.35rem', lineHeight: 1.45 }}>
|
|
|
|
|
|
Für Vertretungen genügt in der Regel die Vereinsmitgliedschaft; Zuweisen dürfen u. a.
|
|
|
|
|
|
Haupt-/Co‑Trainer dieser Gruppe, der/die Ersteller:in der Einheit oder Vereinsadmins.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row" style={{ marginTop: '0.75rem' }}>
|
|
|
|
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={formData.session_assistants_inherit}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateFormField('session_assistants_inherit', e.target.checked)
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span style={{ fontSize: '0.9rem', color: 'var(--text1)' }}>
|
|
|
|
|
|
Co-Trainer wie in der Trainingsgruppe (Standard)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{!formData.session_assistants_inherit ? (
|
|
|
|
|
|
<div style={{ marginTop: '10px', maxHeight: '200px', overflowY: 'auto' }}>
|
2026-05-06 07:42:39 +02:00
|
|
|
|
{clubDirectoryForCo.map((m) => {
|
2026-05-05 23:35:41 +02:00
|
|
|
|
const mid = typeof m.id === 'number' ? m.id : parseInt(String(m.id), 10)
|
|
|
|
|
|
const labelText = `${(m.name || '').trim() || m.email || `Profil ${mid}`}`
|
|
|
|
|
|
const isOn = Number.isFinite(mid) && formData.session_assistant_profile_ids.includes(mid)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<label
|
|
|
|
|
|
key={`co-${mid}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
|
|
marginBottom: '6px',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
color: 'var(--text1)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={isOn}
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setFormData((prev) => {
|
|
|
|
|
|
const was = prev.session_assistant_profile_ids.includes(mid)
|
|
|
|
|
|
const nextIds = was
|
|
|
|
|
|
? prev.session_assistant_profile_ids.filter((x) => x !== mid)
|
|
|
|
|
|
: [...prev.session_assistant_profile_ids, mid].sort((a, b) => a - b)
|
|
|
|
|
|
return { ...prev, session_assistant_profile_ids: nextIds }
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span>{labelText}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{!clubDirectory.length && showModal ? (
|
|
|
|
|
|
<p style={{ margin: '10px 0 0', fontSize: '0.82rem', color: 'var(--text3)', lineHeight: 1.4 }}>
|
|
|
|
|
|
Keine Einträge im Vereins-Mitgliederverzeichnis oder noch nicht geladen (nur für Vereinsinterne).
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-05 14:47:38 +02:00
|
|
|
|
<div style={{ marginTop: '2rem' }}>
|
2026-05-05 13:39:30 +02:00
|
|
|
|
<TrainingUnitSectionsEditor
|
2026-05-05 14:47:38 +02:00
|
|
|
|
heading="Abschnitte & Übungen"
|
|
|
|
|
|
headingAccessory={
|
|
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={handleSaveAsTemplate}>
|
|
|
|
|
|
Vorlage aus Aufbau speichern
|
|
|
|
|
|
</button>
|
|
|
|
|
|
}
|
2026-05-05 13:39:30 +02:00
|
|
|
|
sections={formData.sections}
|
2026-05-05 14:47:38 +02:00
|
|
|
|
wideExerciseGrid
|
2026-05-05 13:39:30 +02:00
|
|
|
|
onSectionsChange={(updater) =>
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
sections: updater(prev.sections),
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
2026-05-05 14:52:19 +02:00
|
|
|
|
onRequestExercisePick={({ sectionIndex, itemIndex }) => {
|
2026-05-05 14:42:46 +02:00
|
|
|
|
setExercisePickerTarget({
|
|
|
|
|
|
sIdx: sectionIndex,
|
|
|
|
|
|
iIdx: typeof itemIndex === 'number' ? itemIndex : undefined,
|
|
|
|
|
|
})
|
2026-05-05 13:39:30 +02:00
|
|
|
|
setExercisePickerOpen(true)
|
|
|
|
|
|
}}
|
2026-05-05 14:42:46 +02:00
|
|
|
|
onPeekExercise={(id, variantId) =>
|
|
|
|
|
|
setPlanningPeekCtx({ exerciseId: id, variantId: variantId ?? null })
|
|
|
|
|
|
}
|
2026-05-05 13:39:30 +02:00
|
|
|
|
showExecutionExtras={!!editingUnit}
|
|
|
|
|
|
/>
|
2026-05-05 14:47:38 +02:00
|
|
|
|
</div>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
2026-05-05 13:39:30 +02:00
|
|
|
|
<div style={{ marginBottom: '1.75rem' }} />
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
|
|
|
|
|
{editingUnit && (
|
|
|
|
|
|
<>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<h3 style={{ marginTop: '0.5rem', marginBottom: '1rem' }}>Durchführung</h3>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
|
2026-05-05 12:39:15 +02:00
|
|
|
|
<div className="responsive-grid-4" style={{ marginBottom: '1rem' }}>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
<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)}
|
2026-04-28 16:31:45 +02:00
|
|
|
|
placeholder="Für Teilnehmer"
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
/>
|
|
|
|
|
|
</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>
|
2026-04-28 16:31:45 +02:00
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => setShowModal(false)}>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
Abbrechen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-04-29 06:15:26 +02:00
|
|
|
|
<ExercisePickerModal
|
|
|
|
|
|
open={exercisePickerOpen}
|
2026-05-05 14:52:19 +02:00
|
|
|
|
multiSelect
|
2026-04-29 06:15:26 +02:00
|
|
|
|
onClose={() => {
|
|
|
|
|
|
setExercisePickerOpen(false)
|
|
|
|
|
|
setExercisePickerTarget(null)
|
|
|
|
|
|
}}
|
2026-05-05 14:52:19 +02:00
|
|
|
|
onSelectExercises={async (picked) => {
|
|
|
|
|
|
if (!exercisePickerTarget || !picked?.length) return
|
|
|
|
|
|
const rows = []
|
|
|
|
|
|
for (const ex of picked) {
|
|
|
|
|
|
const row = await hydrateExercisePlanningRow(ex)
|
|
|
|
|
|
if (row) rows.push(row)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!rows.length) return
|
2026-04-29 06:15:26 +02:00
|
|
|
|
const { sIdx, iIdx } = exercisePickerTarget
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
2026-05-05 14:52:19 +02:00
|
|
|
|
sections: prev.sections.map((s, si) => {
|
|
|
|
|
|
if (si !== sIdx) return s
|
|
|
|
|
|
const items = [...(s.items || [])]
|
|
|
|
|
|
if (typeof iIdx === 'number') {
|
|
|
|
|
|
const cur = items[iIdx]
|
|
|
|
|
|
if (!cur || cur.item_type !== 'exercise') return s
|
|
|
|
|
|
const [first, ...tail] = rows
|
|
|
|
|
|
items[iIdx] = {
|
|
|
|
|
|
...cur,
|
|
|
|
|
|
exercise_id: first.exercise_id,
|
|
|
|
|
|
exercise_variant_id: first.exercise_variant_id,
|
|
|
|
|
|
exercise_title: first.exercise_title,
|
|
|
|
|
|
variants: first.variants,
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tail.length) items.splice(iIdx + 1, 0, ...tail)
|
|
|
|
|
|
return { ...s, items }
|
|
|
|
|
|
}
|
|
|
|
|
|
return { ...s, items: [...items, ...rows] }
|
|
|
|
|
|
}),
|
2026-04-29 06:15:26 +02:00
|
|
|
|
}))
|
|
|
|
|
|
setExercisePickerOpen(false)
|
|
|
|
|
|
setExercisePickerTarget(null)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-04-29 06:49:16 +02:00
|
|
|
|
<ExercisePeekModal
|
2026-05-05 14:42:46 +02:00
|
|
|
|
open={planningPeekCtx != null}
|
|
|
|
|
|
exerciseId={planningPeekCtx?.exerciseId}
|
|
|
|
|
|
variantId={planningPeekCtx?.variantId ?? undefined}
|
|
|
|
|
|
onClose={() => setPlanningPeekCtx(null)}
|
2026-04-29 06:49:16 +02:00
|
|
|
|
/>
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default TrainingPlanningPage
|