diff --git a/frontend/src/pages/TrainingPlanningPage.jsx b/frontend/src/pages/TrainingPlanningPage.jsx index 6576d58..dfedb74 100644 --- a/frontend/src/pages/TrainingPlanningPage.jsx +++ b/frontend/src/pages/TrainingPlanningPage.jsx @@ -87,6 +87,28 @@ const sessionAssignDefaults = () => ({ session_assistants_inherit: true, session_assistant_profile_ids: [], }) + +/** 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) +} function TrainingPlanningPage() { const { user } = useAuth() const [groups, setGroups] = useState([]) @@ -123,7 +145,6 @@ function TrainingPlanningPage() { const [planScope, setPlanScope] = useState('group') const [assignedToMeOnly, setAssignedToMeOnly] = useState(false) const [clubDirectory, setClubDirectory] = useState([]) - const [meProfile, setMeProfile] = useState(null) const [assignModalOpen, setAssignModalOpen] = useState(false) const [assignDraft, setAssignDraft] = useState({ unit: null, @@ -241,39 +262,41 @@ function TrainingPlanningPage() { const r = (user?.role || '').toLowerCase() if (r === 'admin' || r === 'superadmin') return true if (selectedGroupClubIdMemo == null || !Number.isFinite(selectedGroupClubIdMemo)) return false - const row = (meProfile?.clubs || []).find((c) => Number(c.id) === selectedGroupClubIdMemo) + const row = (user?.clubs || []).find((c) => Number(c.id) === selectedGroupClubIdMemo) return Array.isArray(row?.roles) && row.roles.includes('club_admin') - }, [user?.role, selectedGroupClubIdMemo, meProfile]) + }, [user?.role, user?.clubs, selectedGroupClubIdMemo]) - useEffect(() => { - if (!user?.id) { - setMeProfile(null) - return undefined + 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) + } } - let cancelled = false - api - .getCurrentProfile() - .then((p) => { - if (!cancelled) setMeProfile(p) - }) - .catch(() => { - if (!cancelled) setMeProfile(null) - }) - return () => { - cancelled = true - } - }, [user?.id]) + return new Set(ids) + }, [user?.clubs]) 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 + + 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) + } + const loadClubId = showModal && clubForModal != null && Number.isFinite(clubForModal) ? clubForModal - : planScope === 'club' && canClubOrgTraining && selectedGroupClubIdMemo != null - ? selectedGroupClubIdMemo - : null + : assignModalOpen && assignModalClubId != null && Number.isFinite(assignModalClubId) + ? assignModalClubId + : canClubOrgTraining && selectedGroupClubIdMemo != null && Number.isFinite(selectedGroupClubIdMemo) + ? selectedGroupClubIdMemo + : null if (loadClubId == null || !Number.isFinite(loadClubId)) { setClubDirectory([]) @@ -296,10 +319,11 @@ function TrainingPlanningPage() { } }, [ showModal, + assignModalOpen, + assignDraft.unit, formData.group_id, selectedGroupId, groups, - planScope, canClubOrgTraining, selectedGroupClubIdMemo, ]) @@ -559,7 +583,15 @@ function TrainingPlanningPage() { session_assistants_inherit: fullUnit.assistant_trainer_profile_ids == null || fullUnit.assistant_trainer_profile_ids === undefined, - session_assistant_profile_ids: toNumList(fullUnit.assistant_trainer_profile_ids), + 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 + })(), }) setShowModal(true) } catch (err) { @@ -596,6 +628,14 @@ function TrainingPlanningPage() { } const openTrainerAssignModal = (unit) => { + 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) + } setAssignDraft({ unit, lead_trainer_profile_id: @@ -604,7 +644,7 @@ function TrainingPlanningPage() { : '', session_assistants_inherit: unit.assistant_trainer_profile_ids == null || unit.assistant_trainer_profile_ids === undefined, - session_assistant_profile_ids: toNumList(unit.assistant_trainer_profile_ids), + session_assistant_profile_ids: coIds, }) setAssignModalOpen(true) } @@ -701,7 +741,27 @@ function TrainingPlanningPage() { } const updateFormField = (field, value) => { - setFormData((prev) => ({ ...prev, [field]: value })) + 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 } + }) } const calendarGridDays = useMemo(() => { @@ -729,6 +789,32 @@ function TrainingPlanningPage() { return new Date(y, mo - 1, 1).toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }) }, [calendarMonthStr]) + 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] + ) + if (loading) { return (
@@ -739,7 +825,39 @@ function TrainingPlanningPage() { } const selectedGroup = groups.find((g) => g.id === parseInt(selectedGroupId, 10)) - const showOrgTrainerAssignControls = planScope === 'club' && canClubOrgTraining + + 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) return (
@@ -1038,11 +1156,12 @@ function TrainingPlanningPage() { Nur meine Zuordnung (Leitung / Co) - „Ganzer Verein“ nutzt den Verein der gewählten Gruppe; neue Termine unten gelten weiter für die gewählte Gruppe. - {planScope === 'club' && canClubOrgTraining ? ( + „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 ? ( - Vereinsorganisation: Über Trainer zuweisen kannst du pro Termin die Leitung und Co-Trainer - anpassen (auch in der Kalenderansicht). + Über Trainer oder Trainer zuweisen: Leitung und Co je Einheit bearbeitbar (berechtigt: Vereinsorganisation, Haupt-/Co‑Trainer der Gruppe sowie Erstellung der Einheit). + Das Mitgliederverzeichnis listet nur eigene Vereinsmitglieder; 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. ) : null} @@ -1307,7 +1426,7 @@ function TrainingPlanningPage() { ) : null} - {showOrgTrainerAssignControls ? ( + {mayConfigureSessionAssignments(unit) ? ( - {showOrgTrainerAssignControls ? ( + {mayConfigureSessionAssignments(unit) ? (
{!assignDraft.session_assistants_inherit ? (
- {clubDirectory.map((m) => { + {clubDirectoryForAssignCo.map((m) => { 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) @@ -2075,7 +2210,7 @@ function TrainingPlanningPage() {
{!formData.session_assistants_inherit ? (
- {clubDirectory.map((m) => { + {clubDirectoryForCo.map((m) => { 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)