From 07489903286b938993d5d3447ff7c7fbbec5ac0b Mon Sep 17 00:00:00 2001
From: Lars
Date: Tue, 5 May 2026 15:40:29 +0200
Subject: [PATCH] feat: update version and enhance training unit management
features
- Bumped application version to 0.8.11 and updated database schema version.
- Added new API features for training units, including filtering by club and assigned trainer.
- Enhanced the TrainingPlanningPage with options to filter training units by club and assigned trainer, improving user experience.
- Implemented lead trainer assignment functionality, allowing users to take lead on training units.
- Updated changelog with new version details and changes.
---
.../038_training_unit_lead_trainer.sql | 11 ++
backend/routers/training_planning.py | 173 ++++++++++++++++--
backend/version.py | 15 +-
frontend/src/pages/Dashboard.jsx | 163 +++++++++++++++++
frontend/src/pages/TrainingPlanningPage.jsx | 167 ++++++++++++++++-
frontend/src/utils/api.js | 8 +-
6 files changed, 511 insertions(+), 26 deletions(-)
create mode 100644 backend/migrations/038_training_unit_lead_trainer.sql
diff --git a/backend/migrations/038_training_unit_lead_trainer.sql b/backend/migrations/038_training_unit_lead_trainer.sql
new file mode 100644
index 0000000..1f76c2d
--- /dev/null
+++ b/backend/migrations/038_training_unit_lead_trainer.sql
@@ -0,0 +1,11 @@
+-- Migration 038: Optionale verantwortliche Person pro Trainingstermin (Vertretung)
+-- Für Vereins-/Trainerübersicht: COALESCE(tu.lead_trainer_profile_id, tg.trainer_id) = wirksamer Leitungstrainer.
+
+ALTER TABLE training_units
+ADD COLUMN IF NOT EXISTS lead_trainer_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL;
+
+COMMENT ON COLUMN training_units.lead_trainer_profile_id IS 'Vertretung / expliziter Leiter dieses Terms; NULL = Standard (Haupttrainer der Gruppe)';
+
+CREATE INDEX IF NOT EXISTS idx_training_units_lead_trainer
+ ON training_units(lead_trainer_profile_id)
+ WHERE lead_trainer_profile_id IS NOT NULL;
diff --git a/backend/routers/training_planning.py b/backend/routers/training_planning.py
index fd31bdf..2cd7f03 100644
--- a/backend/routers/training_planning.py
+++ b/backend/routers/training_planning.py
@@ -68,6 +68,7 @@ def _training_unit_guard_row(cur, unit_id: int) -> Dict[str, Any]:
cur.execute(
"""
SELECT tu.id, tu.created_by, tu.group_id, tu.plan_template_id, tu.framework_slot_id,
+ tu.lead_trainer_profile_id,
tg.trainer_id, tg.co_trainer_ids,
fwp.created_by AS framework_created_by
FROM training_units tu
@@ -103,6 +104,7 @@ def _assert_training_unit_permission(
unit_row["created_by"] != profile_id
and unit_row["trainer_id"] != profile_id
and profile_id not in co_trainers
+ and unit_row.get("lead_trainer_profile_id") != profile_id
):
raise HTTPException(status_code=403, detail="Keine Berechtigung")
@@ -112,6 +114,69 @@ def _assert_delete_training_unit(role: str, created_by: int, profile_id: int) ->
raise HTTPException(status_code=403, detail="Keine Berechtigung")
+def _assert_club_visible_for_trainer(cur, club_id: int, profile_id: int, role: str) -> None:
+ """Nicht-Admin: mindestens eine aktive Gruppe im Verein als Trainer/Co-Trainer."""
+ if role in ("admin", "superadmin"):
+ return
+ cur.execute(
+ """
+ SELECT 1 FROM training_groups g
+ WHERE g.club_id = %s AND g.status = 'active'
+ AND (
+ g.trainer_id = %s
+ OR (g.co_trainer_ids IS NOT NULL AND g.co_trainer_ids @> jsonb_build_array(%s::int))
+ )
+ LIMIT 1
+ """,
+ (club_id, profile_id, profile_id),
+ )
+ if not cur.fetchone():
+ raise HTTPException(status_code=403, detail="Kein Zugriff auf diesen Verein")
+
+
+def _normalize_lead_trainer_profile_id(
+ cur,
+ group_id: int,
+ raw_lead: Any,
+ profile_id: int,
+ role: str,
+) -> Optional[int]:
+ """NULL = Vertretung aufheben; sonst Profil-ID mit Profil-Check und Gruppenkontext."""
+ if raw_lead is None:
+ return None
+ if raw_lead in ("", []):
+ return None
+ try:
+ nid = int(raw_lead)
+ except (TypeError, ValueError):
+ raise HTTPException(status_code=400, detail="lead_trainer_profile_id ungültig")
+ if nid < 1:
+ raise HTTPException(status_code=400, detail="lead_trainer_profile_id ungültig")
+ cur.execute("SELECT 1 FROM profiles WHERE id = %s", (nid,))
+ if not cur.fetchone():
+ raise HTTPException(status_code=400, detail="Profil für Leitung nicht gefunden")
+ if role in ("admin", "superadmin"):
+ return nid
+ if nid == profile_id:
+ return nid
+ cur.execute(
+ "SELECT trainer_id, co_trainer_ids FROM training_groups WHERE id = %s",
+ (group_id,),
+ )
+ gr = cur.fetchone()
+ if not gr:
+ raise HTTPException(status_code=400, detail="Trainingsgruppe nicht gefunden")
+ eligible = {gr["trainer_id"]} if gr.get("trainer_id") else set()
+ for x in gr.get("co_trainer_ids") or []:
+ eligible.add(x)
+ if nid in eligible:
+ return nid
+ raise HTTPException(
+ status_code=403,
+ detail="Lead-Trainer kann nur eigene Person, Haupttrainer oder Co-Trainer der Gruppe sein",
+ )
+
+
# Nachverfolgbarkeit: Übernahmen aus Rahmenprogramm über origin_framework_slot_id
_ORIGIN_LINEAGE_JOIN = """
LEFT JOIN training_framework_slots origin_slot ON origin_slot.id = tu.origin_framework_slot_id
@@ -633,29 +698,70 @@ def delete_training_plan_template(template_id: int, session=Depends(require_auth
@router.get("/training-units")
def list_training_units(
group_id: Optional[int] = Query(default=None),
+ club_id: Optional[int] = Query(default=None),
start_date: Optional[str] = Query(default=None),
end_date: Optional[str] = Query(default=None),
status: Optional[str] = Query(default=None),
+ assigned_to_me: bool = Query(default=False),
+ sort: str = Query(default="desc"),
+ limit: Optional[int] = Query(default=None),
session=Depends(require_auth),
):
profile_id = session["profile_id"]
role = session.get("role")
+ gid = _optional_positive_int(group_id, "group_id") if group_id else None
+ cid = _optional_positive_int(club_id, "club_id") if club_id else None
+ if gid and cid:
+ raise HTTPException(status_code=400, detail="Nur eines der Parameter group_id oder club_id angeben")
+
with get_db() as conn:
cur = get_cursor(conn)
+ if cid and role not in ["admin", "superadmin"]:
+ _assert_club_visible_for_trainer(cur, cid, profile_id, role)
+
+ if gid and role not in ["admin", "superadmin"]:
+ cur.execute(
+ "SELECT trainer_id, co_trainer_ids FROM training_groups WHERE id = %s AND status = 'active'",
+ (gid,),
+ )
+ gr = cur.fetchone()
+ if not gr:
+ raise HTTPException(status_code=404, detail="Trainingsgruppe nicht gefunden")
+ cob = gr["co_trainer_ids"] or []
+ if gr["trainer_id"] != profile_id and profile_id not in cob:
+ raise HTTPException(status_code=403, detail="Keine Berechtigung für diese Gruppe")
+
+ order_dir = "ASC" if (sort or "").strip().lower() == "asc" else "DESC"
+ lim: Optional[int] = None
+ if limit is not None:
+ try:
+ lim = int(limit)
+ except (TypeError, ValueError):
+ raise HTTPException(status_code=400, detail="limit ungültig")
+ if lim < 1:
+ raise HTTPException(status_code=400, detail="limit ungültig")
+ lim = min(lim, 250)
+
query = """
SELECT tu.*,
tg.name as group_name,
tg.weekday as group_weekday,
+ tg.club_id AS group_club_id,
c.name as club_name,
- p.name as trainer_name"""
+ p.name as trainer_name,
+ p.name as creator_name,
+ COALESCE(tu.lead_trainer_profile_id, tg.trainer_id) AS effective_lead_trainer_profile_id,
+ leadp.name AS lead_trainer_name
+ """
query += "," + _ORIGIN_LINEAGE_FIELDS
query += """
FROM training_units tu
LEFT JOIN training_groups tg ON tu.group_id = tg.id
LEFT JOIN clubs c ON tg.club_id = c.id
LEFT JOIN profiles p ON tu.created_by = p.id
+ LEFT JOIN profiles leadp ON leadp.id = COALESCE(tu.lead_trainer_profile_id, tg.trainer_id)
"""
query += _ORIGIN_LINEAGE_JOIN
@@ -663,14 +769,28 @@ def list_training_units(
params = []
if role not in ["admin", "superadmin"]:
- where.append("(tu.created_by = %s OR tg.trainer_id = %s)")
- params.extend([profile_id, profile_id])
+ where.append(
+ "(tu.created_by = %s OR tg.trainer_id = %s OR "
+ "(tg.co_trainer_ids IS NOT NULL AND tg.co_trainer_ids @> jsonb_build_array(%s::int)))"
+ )
+ params.extend([profile_id, profile_id, profile_id])
where.append("tu.framework_slot_id IS NULL")
- if group_id:
+ if gid:
where.append("tu.group_id = %s")
- params.append(group_id)
+ params.append(gid)
+
+ if cid:
+ where.append("tg.club_id = %s")
+ params.append(cid)
+
+ if assigned_to_me:
+ where.append(
+ "(COALESCE(tu.lead_trainer_profile_id, tg.trainer_id) = %s OR "
+ "(tg.co_trainer_ids IS NOT NULL AND tg.co_trainer_ids @> jsonb_build_array(%s::int)))"
+ )
+ params.extend([profile_id, profile_id])
if start_date:
where.append("tu.planned_date >= %s")
@@ -687,7 +807,10 @@ def list_training_units(
if where:
query += " WHERE " + " AND ".join(where)
- query += " ORDER BY tu.planned_date DESC, tu.planned_time_start DESC"
+ query += f" ORDER BY tu.planned_date {order_dir}, tu.planned_time_start {order_dir} NULLS LAST"
+ if lim is not None:
+ query += " LIMIT %s"
+ params.append(lim)
cur.execute(query, params)
rows = cur.fetchall()
@@ -712,11 +835,17 @@ def get_training_unit(unit_id: int, session=Depends(require_auth)):
tg.location as group_location,
c.name as club_name,
p.name as trainer_name,
+ p.name as creator_name,
+ tg.trainer_id AS trainer_id,
+ tg.co_trainer_ids AS co_trainer_ids,
+ COALESCE(tu.lead_trainer_profile_id, tg.trainer_id) AS effective_lead_trainer_profile_id,
+ leadp.name AS lead_trainer_name,
""" + _ORIGIN_LINEAGE_FIELDS.strip() + """
FROM training_units tu
LEFT JOIN training_groups tg ON tu.group_id = tg.id
LEFT JOIN clubs c ON tg.club_id = c.id
LEFT JOIN profiles p ON tu.created_by = p.id
+ LEFT JOIN profiles leadp ON leadp.id = COALESCE(tu.lead_trainer_profile_id, tg.trainer_id)
""" + _ORIGIN_LINEAGE_JOIN.strip() + """
WHERE tu.id = %s
""",
@@ -744,16 +873,9 @@ def get_training_unit(unit_id: int, session=Depends(require_auth)):
if unit["created_by"] != profile_id and cb != profile_id:
raise HTTPException(status_code=403, detail="Keine Berechtigung")
else:
- gid = unit.get("group_id")
- if not gid:
+ if not unit.get("group_id"):
raise HTTPException(status_code=404, detail="Trainingseinheit nicht gefunden")
-
- cur.execute("SELECT trainer_id FROM training_groups WHERE id = %s", (gid,))
- group = cur.fetchone()
-
- if role not in ["admin", "superadmin"]:
- if unit["created_by"] != profile_id and (not group or group["trainer_id"] != profile_id):
- raise HTTPException(status_code=403, detail="Keine Berechtigung")
+ _assert_training_unit_permission(cur, unit, profile_id, role)
_hydrate_training_unit_payload(cur, unit)
return unit
@@ -892,8 +1014,21 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)
tuple(blueprint_params),
)
else:
+ lead_sql = ""
+ lead_params: List[Any] = []
+ if "lead_trainer_profile_id" in data:
+ nl = _normalize_lead_trainer_profile_id(
+ cur,
+ unit_row["group_id"],
+ data.get("lead_trainer_profile_id"),
+ profile_id,
+ role,
+ )
+ lead_sql = ", lead_trainer_profile_id = %s"
+ lead_params.append(nl)
+
cur.execute(
- """
+ f"""
UPDATE training_units SET
planned_date = COALESCE(%s, planned_date),
planned_time_start = %s,
@@ -908,6 +1043,7 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)
trainer_notes = %s,
plan_template_id = COALESCE(%s, plan_template_id),
updated_at = NOW()
+ {lead_sql}
WHERE id = %s
""",
(
@@ -923,8 +1059,9 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)
data.get("notes"),
trainer_notes_val,
tpl_id_val,
- unit_id,
- ),
+ )
+ + tuple(lead_params)
+ + (unit_id,),
)
content_handled = False
diff --git a/backend/version.py b/backend/version.py
index 3bf97bb..00b7e14 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,8 +1,8 @@
# Shinkan Jinkendo Version Information
-APP_VERSION = "0.8.10"
+APP_VERSION = "0.8.11"
BUILD_DATE = "2026-05-05"
-DB_SCHEMA_VERSION = "20260505037"
+DB_SCHEMA_VERSION = "20260505038"
MODULE_VERSIONS = {
"auth": "1.0.0",
@@ -14,7 +14,7 @@ MODULE_VERSIONS = {
"exercises": "2.3.0", # Progressionsgraph: Sequenz-API, Varianten-Knoten, UX Ketten (Migration 034)
"training_units": "0.1.0",
"training_programs": "0.1.0",
- "planning": "0.5.0",
+ "planning": "0.6.0",
"import_wiki": "1.0.0",
"admin": "1.0.0",
"membership": "1.0.0",
@@ -23,6 +23,15 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
+ {
+ "version": "0.8.11",
+ "date": "2026-05-05",
+ "changes": [
+ "DB 038: training_units.lead_trainer_profile_id (Vertretung / Leitung pro Termin)",
+ "API GET /api/training-units: club_id, assigned_to_me, sort, limit; Co-Trainer in Sichtbarkeit; lead_trainer_name / effective_lead_trainer_profile_id",
+ "API PUT /api/training-units/{id}: lead_trainer_profile_id (Validierung über Gruppe)",
+ ],
+ },
{
"version": "0.8.10",
"date": "2026-05-05",
diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx
index 5431f5e..f941894 100644
--- a/frontend/src/pages/Dashboard.jsx
+++ b/frontend/src/pages/Dashboard.jsx
@@ -1,18 +1,86 @@
import React, { useState, useEffect } from 'react'
+import { Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import api from '../utils/api'
import EmailVerificationBanner from '../components/EmailVerificationBanner'
+function unitWhenLabel(u) {
+ const d = u.planned_date ? String(u.planned_date).slice(0, 10) : ''
+ const t = u.planned_time_start ? String(u.planned_time_start).slice(0, 5) : ''
+ const bits = [d, t].filter(Boolean)
+ return bits.length ? bits.join(' · ') : 'Termin'
+}
+
function Dashboard() {
const [version, setVersion] = useState(null)
const [profile, setProfile] = useState(null)
const [loading, setLoading] = useState(true)
+ const [trainingHome, setTrainingHome] = useState(null)
+ const [trainingHomeErr, setTrainingHomeErr] = useState(null)
const { user } = useAuth()
useEffect(() => {
loadData()
}, [])
+ useEffect(() => {
+ if (!user?.id) {
+ setTrainingHome(null)
+ setTrainingHomeErr(null)
+ return undefined
+ }
+ let cancelled = false
+ ;(async () => {
+ setTrainingHomeErr(null)
+ try {
+ const today = new Date().toISOString().slice(0, 10)
+ const [upcomingRaw, recentRaw, plannedPool] = await Promise.all([
+ api.listTrainingUnits({
+ assigned_to_me: true,
+ status: 'planned',
+ start_date: today,
+ sort: 'asc',
+ limit: 8
+ }),
+ api.listTrainingUnits({
+ assigned_to_me: true,
+ status: 'completed',
+ sort: 'desc',
+ limit: 6
+ }),
+ api.listTrainingUnits({
+ assigned_to_me: true,
+ status: 'planned',
+ start_date: today,
+ sort: 'asc',
+ limit: 40
+ })
+ ])
+ const noteHits = (plannedPool || []).filter((u) => {
+ const tn = (u.trainer_notes || '').trim()
+ const n = (u.notes || '').trim()
+ return Boolean(tn || n)
+ }).slice(0, 5)
+ if (!cancelled) {
+ setTrainingHome({
+ upcoming: Array.isArray(upcomingRaw) ? upcomingRaw : [],
+ recent: Array.isArray(recentRaw) ? recentRaw : [],
+ plannedWithNotes: noteHits
+ })
+ }
+ } catch (e) {
+ if (!cancelled) {
+ console.error('Dashboard Trainingsübersicht:', e)
+ setTrainingHomeErr(e.message || 'Konnte Trainingsdaten nicht laden')
+ setTrainingHome(null)
+ }
+ }
+ })()
+ return () => {
+ cancelled = true
+ }
+ }, [user?.id])
+
const loadData = async () => {
try {
const [versionData, profileData] = await Promise.all([
@@ -52,6 +120,101 @@ function Dashboard() {
+ {user?.id && (
+
+
+
Deine nächsten Trainings
+ {trainingHomeErr ? (
+
{trainingHomeErr}
+ ) : trainingHome?.upcoming?.length ? (
+
+ {trainingHome.upcoming.map((u) => (
+
+
+ {unitWhenLabel(u)}
+
+ {u.group_name ? (
+ {` — ${u.group_name}`}
+ ) : null}
+ {u.lead_trainer_name ? (
+
+ Leitung: {u.lead_trainer_name}
+
+ ) : null}
+
+ ))}
+
+ ) : (
+
+ Keine anstehenden Termine mit dir als Leitung oder Co‑Trainer. Unter{' '}
+
+ Trainingsplanung
+ {' '}
+ kannst du den Vereins‑ oder Gruppen‑Zeitraum einblenden.
+
+ )}
+
+
+
+
Vermerk / Hinweise (anstehend)
+ {trainingHomeErr ? (
+
{trainingHomeErr}
+ ) : trainingHome?.plannedWithNotes?.length ? (
+
+ ) : (
+
+ Keine Einträge mit Allgemein‑ oder Trainer‑Notizen in deinen nächsten geplanten Terminen.
+
+ )}
+
+
+
+
Rückschau (durchgeführt)
+ {trainingHomeErr ? (
+
{trainingHomeErr}
+ ) : trainingHome?.recent?.length ? (
+
+ {trainingHome.recent.map((u) => (
+
+
+ {(u.actual_date || u.planned_date || '').toString().slice(0, 10) || 'Datum'}
+
+ {u.group_name ? (
+ {` — ${u.group_name}`}
+ ) : null}
+
+ ))}
+
+ ) : (
+
Noch keine abgeschlossenen Einheiten in der Kurzliste.
+ )}
+
+
+ )}
+
{/* Status Grid */}
today.slice(0, 7))
+ const [planScope, setPlanScope] = useState('group')
+ const [assignedToMeOnly, setAssignedToMeOnly] = useState(false)
const [formData, setFormData] = useState({
group_id: '',
@@ -162,17 +164,37 @@ function TrainingPlanningPage() {
start = r.gridStart
end = r.gridEnd
}
+ const gid = parseInt(selectedGroupId, 10)
+ const groupRow = groups.find((g) => g.id === gid)
+ const clubId = groupRow?.club_id
try {
- const unitsData = await api.listTrainingUnits({
- group_id: selectedGroupId,
+ const filters = {
start_date: start,
end_date: end
- })
+ }
+ 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)
setUnits(unitsData)
} catch (err) {
console.error('Failed to load units:', err)
}
- }, [selectedGroupId, startDate, endDate, planView, calendarMonthStr])
+ }, [
+ selectedGroupId,
+ groups,
+ startDate,
+ endDate,
+ planView,
+ calendarMonthStr,
+ planScope,
+ assignedToMeOnly
+ ])
useEffect(() => {
loadData()
@@ -455,6 +477,16 @@ function TrainingPlanningPage() {
}
}
+ 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')
+ }
+ }
+
const handleDelete = async (unit) => {
if (!confirm(`Trainingseinheit vom ${unit.planned_date} wirklich löschen?`)) return
try {
@@ -759,6 +791,93 @@ function TrainingPlanningPage() {
)}
+
+
+
+ Einblenden
+
+
+ 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
+
+ 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
+
+
+
+ setAssignedToMeOnly(e.target.checked)}
+ />
+ 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.
+
+
{selectedGroup && (
@@ -950,7 +1069,9 @@ function TrainingPlanningPage() {
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(),
unit.status === 'completed'
? 'Durchgeführt'
@@ -986,6 +1107,18 @@ function TrainingPlanningPage() {
? `${unit.planned_time_start.slice(0, 5)}`
: 'Ganztags'}
+ {planScope === 'club' && (unit.group_name || '').trim() ? (
+
+ {(unit.group_name || '').trim().length > 22
+ ? `${(unit.group_name || '').trim().slice(0, 22)}…`
+ : unit.group_name}
+
+ ) : null}
+ {unit.lead_trainer_name?.trim() ? (
+
+ {unit.lead_trainer_name.trim().split(/\s+/).slice(-1)[0] || unit.lead_trainer_name.trim()}
+
+ ) : null}
{unit.planned_focus?.trim() ? (
{(unit.planned_focus || '').trim().length > 24
@@ -1017,6 +1150,13 @@ function TrainingPlanningPage() {
{units.map((unit) => {
const lineage = unit.origin_framework_slot_id ? frameworkLineageText(unit) : null
+ 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
return (
+ {planScope === 'club' && (unit.group_name || '').trim() ? (
+
+ {unit.group_name}
+
+ ) : null}
+
+ Leitung: {(unit.lead_trainer_name || '').trim() || '—'}
+
{unit.planned_focus && (
handleEdit(unit)}>
Bearbeiten
+ {showTakeLead ? (
+ handleTakeLead(unit)}>
+ Ich übernehme
+
+ ) : null}