feat: enhance TrainingPlanningPage with new utility functions and state management improvements
All checks were successful
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Successful in 23s
All checks were successful
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Successful in 23s
- Added utility functions to normalize co-trainer IDs and filter directory entries, improving data handling for training groups. - Updated state management to remove reliance on the user profile for club admin checks, enhancing performance and clarity. - Improved session assignment logic to ensure effective lead trainers are excluded from assistant trainer lists. - Enhanced form field updates to manage session assistant IDs more effectively, streamlining the assignment process.
This commit is contained in:
parent
9dbd3cbd5f
commit
56ea36ea25
|
|
@ -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 (
|
||||
<div className="app-page" style={{ padding: '2rem 0', textAlign: 'center' }}>
|
||||
|
|
@ -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 (
|
||||
<div className="app-page">
|
||||
|
|
@ -1038,11 +1156,12 @@ function TrainingPlanningPage() {
|
|||
Nur meine Zuordnung (Leitung / Co)
|
||||
</label>
|
||||
<span style={{ fontSize: '0.78rem', color: 'var(--text3)', lineHeight: 1.4, flex: '1 1 240px' }}>
|
||||
„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 ? (
|
||||
<span style={{ display: 'block', marginTop: '6px', color: 'var(--text2)' }}>
|
||||
Vereinsorganisation: Über <strong>Trainer zuweisen</strong> kannst du pro Termin die Leitung und Co-Trainer
|
||||
anpassen (auch in der Kalenderansicht).
|
||||
Ü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.
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
|
@ -1307,7 +1426,7 @@ function TrainingPlanningPage() {
|
|||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
{showOrgTrainerAssignControls ? (
|
||||
{mayConfigureSessionAssignments(unit) ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
|
|
@ -1504,7 +1623,7 @@ function TrainingPlanningPage() {
|
|||
<button className="btn btn-secondary" onClick={() => handleEdit(unit)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
{showOrgTrainerAssignControls ? (
|
||||
{mayConfigureSessionAssignments(unit) ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
|
|
@ -1594,9 +1713,25 @@ function TrainingPlanningPage() {
|
|||
<select
|
||||
className="form-input"
|
||||
value={assignDraft.lead_trainer_profile_id}
|
||||
onChange={(e) =>
|
||||
setAssignDraft((prev) => ({ ...prev, lead_trainer_profile_id: e.target.value }))
|
||||
}
|
||||
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 }
|
||||
})
|
||||
}}
|
||||
disabled={assignSaving}
|
||||
>
|
||||
<option value="">Standard (Haupttrainer der Gruppe)</option>
|
||||
|
|
@ -1630,7 +1765,7 @@ function TrainingPlanningPage() {
|
|||
</div>
|
||||
{!assignDraft.session_assistants_inherit ? (
|
||||
<div style={{ marginTop: '10px', maxHeight: '180px', overflowY: 'auto' }}>
|
||||
{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() {
|
|||
</div>
|
||||
{!formData.session_assistants_inherit ? (
|
||||
<div style={{ marginTop: '10px', maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user