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.
This commit is contained in:
parent
e69de82028
commit
0748990328
11
backend/migrations/038_training_unit_lead_trainer.sql
Normal file
11
backend/migrations/038_training_unit_lead_trainer.sql
Normal file
|
|
@ -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;
|
||||||
|
|
@ -68,6 +68,7 @@ def _training_unit_guard_row(cur, unit_id: int) -> Dict[str, Any]:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT tu.id, tu.created_by, tu.group_id, tu.plan_template_id, tu.framework_slot_id,
|
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,
|
tg.trainer_id, tg.co_trainer_ids,
|
||||||
fwp.created_by AS framework_created_by
|
fwp.created_by AS framework_created_by
|
||||||
FROM training_units tu
|
FROM training_units tu
|
||||||
|
|
@ -103,6 +104,7 @@ def _assert_training_unit_permission(
|
||||||
unit_row["created_by"] != profile_id
|
unit_row["created_by"] != profile_id
|
||||||
and unit_row["trainer_id"] != profile_id
|
and unit_row["trainer_id"] != profile_id
|
||||||
and profile_id not in co_trainers
|
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")
|
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")
|
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
|
# Nachverfolgbarkeit: Übernahmen aus Rahmenprogramm über origin_framework_slot_id
|
||||||
_ORIGIN_LINEAGE_JOIN = """
|
_ORIGIN_LINEAGE_JOIN = """
|
||||||
LEFT JOIN training_framework_slots origin_slot ON origin_slot.id = tu.origin_framework_slot_id
|
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")
|
@router.get("/training-units")
|
||||||
def list_training_units(
|
def list_training_units(
|
||||||
group_id: Optional[int] = Query(default=None),
|
group_id: Optional[int] = Query(default=None),
|
||||||
|
club_id: Optional[int] = Query(default=None),
|
||||||
start_date: Optional[str] = Query(default=None),
|
start_date: Optional[str] = Query(default=None),
|
||||||
end_date: Optional[str] = Query(default=None),
|
end_date: Optional[str] = Query(default=None),
|
||||||
status: 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),
|
session=Depends(require_auth),
|
||||||
):
|
):
|
||||||
profile_id = session["profile_id"]
|
profile_id = session["profile_id"]
|
||||||
role = session.get("role")
|
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:
|
with get_db() as conn:
|
||||||
cur = get_cursor(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 = """
|
query = """
|
||||||
SELECT tu.*,
|
SELECT tu.*,
|
||||||
tg.name as group_name,
|
tg.name as group_name,
|
||||||
tg.weekday as group_weekday,
|
tg.weekday as group_weekday,
|
||||||
|
tg.club_id AS group_club_id,
|
||||||
c.name as club_name,
|
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 += "," + _ORIGIN_LINEAGE_FIELDS
|
||||||
query += """
|
query += """
|
||||||
FROM training_units tu
|
FROM training_units tu
|
||||||
LEFT JOIN training_groups tg ON tu.group_id = tg.id
|
LEFT JOIN training_groups tg ON tu.group_id = tg.id
|
||||||
LEFT JOIN clubs c ON tg.club_id = c.id
|
LEFT JOIN clubs c ON tg.club_id = c.id
|
||||||
LEFT JOIN profiles p ON tu.created_by = p.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
|
query += _ORIGIN_LINEAGE_JOIN
|
||||||
|
|
||||||
|
|
@ -663,14 +769,28 @@ def list_training_units(
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
if role not in ["admin", "superadmin"]:
|
if role not in ["admin", "superadmin"]:
|
||||||
where.append("(tu.created_by = %s OR tg.trainer_id = %s)")
|
where.append(
|
||||||
params.extend([profile_id, profile_id])
|
"(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")
|
where.append("tu.framework_slot_id IS NULL")
|
||||||
|
|
||||||
if group_id:
|
if gid:
|
||||||
where.append("tu.group_id = %s")
|
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:
|
if start_date:
|
||||||
where.append("tu.planned_date >= %s")
|
where.append("tu.planned_date >= %s")
|
||||||
|
|
@ -687,7 +807,10 @@ def list_training_units(
|
||||||
if where:
|
if where:
|
||||||
query += " WHERE " + " AND ".join(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)
|
cur.execute(query, params)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
@ -712,11 +835,17 @@ def get_training_unit(unit_id: int, session=Depends(require_auth)):
|
||||||
tg.location as group_location,
|
tg.location as group_location,
|
||||||
c.name as club_name,
|
c.name as club_name,
|
||||||
p.name as trainer_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() + """
|
""" + _ORIGIN_LINEAGE_FIELDS.strip() + """
|
||||||
FROM training_units tu
|
FROM training_units tu
|
||||||
LEFT JOIN training_groups tg ON tu.group_id = tg.id
|
LEFT JOIN training_groups tg ON tu.group_id = tg.id
|
||||||
LEFT JOIN clubs c ON tg.club_id = c.id
|
LEFT JOIN clubs c ON tg.club_id = c.id
|
||||||
LEFT JOIN profiles p ON tu.created_by = p.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() + """
|
""" + _ORIGIN_LINEAGE_JOIN.strip() + """
|
||||||
WHERE tu.id = %s
|
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:
|
if unit["created_by"] != profile_id and cb != profile_id:
|
||||||
raise HTTPException(status_code=403, detail="Keine Berechtigung")
|
raise HTTPException(status_code=403, detail="Keine Berechtigung")
|
||||||
else:
|
else:
|
||||||
gid = unit.get("group_id")
|
if not unit.get("group_id"):
|
||||||
if not gid:
|
|
||||||
raise HTTPException(status_code=404, detail="Trainingseinheit nicht gefunden")
|
raise HTTPException(status_code=404, detail="Trainingseinheit nicht gefunden")
|
||||||
|
_assert_training_unit_permission(cur, unit, profile_id, role)
|
||||||
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")
|
|
||||||
|
|
||||||
_hydrate_training_unit_payload(cur, unit)
|
_hydrate_training_unit_payload(cur, unit)
|
||||||
return unit
|
return unit
|
||||||
|
|
@ -892,8 +1014,21 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)
|
||||||
tuple(blueprint_params),
|
tuple(blueprint_params),
|
||||||
)
|
)
|
||||||
else:
|
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(
|
cur.execute(
|
||||||
"""
|
f"""
|
||||||
UPDATE training_units SET
|
UPDATE training_units SET
|
||||||
planned_date = COALESCE(%s, planned_date),
|
planned_date = COALESCE(%s, planned_date),
|
||||||
planned_time_start = %s,
|
planned_time_start = %s,
|
||||||
|
|
@ -908,6 +1043,7 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)
|
||||||
trainer_notes = %s,
|
trainer_notes = %s,
|
||||||
plan_template_id = COALESCE(%s, plan_template_id),
|
plan_template_id = COALESCE(%s, plan_template_id),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
|
{lead_sql}
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
|
@ -923,8 +1059,9 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)
|
||||||
data.get("notes"),
|
data.get("notes"),
|
||||||
trainer_notes_val,
|
trainer_notes_val,
|
||||||
tpl_id_val,
|
tpl_id_val,
|
||||||
unit_id,
|
)
|
||||||
),
|
+ tuple(lead_params)
|
||||||
|
+ (unit_id,),
|
||||||
)
|
)
|
||||||
|
|
||||||
content_handled = False
|
content_handled = False
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.10"
|
APP_VERSION = "0.8.11"
|
||||||
BUILD_DATE = "2026-05-05"
|
BUILD_DATE = "2026-05-05"
|
||||||
DB_SCHEMA_VERSION = "20260505037"
|
DB_SCHEMA_VERSION = "20260505038"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"auth": "1.0.0",
|
"auth": "1.0.0",
|
||||||
|
|
@ -14,7 +14,7 @@ MODULE_VERSIONS = {
|
||||||
"exercises": "2.3.0", # Progressionsgraph: Sequenz-API, Varianten-Knoten, UX Ketten (Migration 034)
|
"exercises": "2.3.0", # Progressionsgraph: Sequenz-API, Varianten-Knoten, UX Ketten (Migration 034)
|
||||||
"training_units": "0.1.0",
|
"training_units": "0.1.0",
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.5.0",
|
"planning": "0.6.0",
|
||||||
"import_wiki": "1.0.0",
|
"import_wiki": "1.0.0",
|
||||||
"admin": "1.0.0",
|
"admin": "1.0.0",
|
||||||
"membership": "1.0.0",
|
"membership": "1.0.0",
|
||||||
|
|
@ -23,6 +23,15 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
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",
|
"version": "0.8.10",
|
||||||
"date": "2026-05-05",
|
"date": "2026-05-05",
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,86 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import EmailVerificationBanner from '../components/EmailVerificationBanner'
|
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() {
|
function Dashboard() {
|
||||||
const [version, setVersion] = useState(null)
|
const [version, setVersion] = useState(null)
|
||||||
const [profile, setProfile] = useState(null)
|
const [profile, setProfile] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [trainingHome, setTrainingHome] = useState(null)
|
||||||
|
const [trainingHomeErr, setTrainingHomeErr] = useState(null)
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
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 () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const [versionData, profileData] = await Promise.all([
|
const [versionData, profileData] = await Promise.all([
|
||||||
|
|
@ -52,6 +120,101 @@ function Dashboard() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{user?.id && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))',
|
||||||
|
gap: '1rem',
|
||||||
|
marginBottom: '1.5rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="card">
|
||||||
|
<h3 style={{ fontSize: '1.05rem', marginBottom: '0.65rem' }}>Deine nächsten Trainings</h3>
|
||||||
|
{trainingHomeErr ? (
|
||||||
|
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>{trainingHomeErr}</p>
|
||||||
|
) : trainingHome?.upcoming?.length ? (
|
||||||
|
<ul style={{ margin: 0, paddingLeft: '1.15rem', color: 'var(--text2)', fontSize: '0.9rem', lineHeight: 1.55 }}>
|
||||||
|
{trainingHome.upcoming.map((u) => (
|
||||||
|
<li key={u.id} style={{ marginBottom: '0.35rem' }}>
|
||||||
|
<Link to={`/planning/run/${u.id}`} style={{ fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||||||
|
{unitWhenLabel(u)}
|
||||||
|
</Link>
|
||||||
|
{u.group_name ? (
|
||||||
|
<span style={{ color: 'var(--text3)' }}>{` — ${u.group_name}`}</span>
|
||||||
|
) : null}
|
||||||
|
{u.lead_trainer_name ? (
|
||||||
|
<span style={{ display: 'block', fontSize: '0.82rem', color: 'var(--text3)', marginTop: '2px' }}>
|
||||||
|
Leitung: {u.lead_trainer_name}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', margin: 0 }}>
|
||||||
|
Keine anstehenden Termine mit dir als Leitung oder Co‑Trainer. Unter{' '}
|
||||||
|
<Link to="/planning" style={{ color: 'var(--accent-dark)' }}>
|
||||||
|
Trainingsplanung
|
||||||
|
</Link>{' '}
|
||||||
|
kannst du den Vereins‑ oder Gruppen‑Zeitraum einblenden.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3 style={{ fontSize: '1.05rem', marginBottom: '0.65rem' }}>Vermerk / Hinweise (anstehend)</h3>
|
||||||
|
{trainingHomeErr ? (
|
||||||
|
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>{trainingHomeErr}</p>
|
||||||
|
) : trainingHome?.plannedWithNotes?.length ? (
|
||||||
|
<ul style={{ margin: 0, paddingLeft: '1.15rem', color: 'var(--text2)', fontSize: '0.88rem', lineHeight: 1.5 }}>
|
||||||
|
{trainingHome.plannedWithNotes.map((u) => {
|
||||||
|
const snippet = (u.trainer_notes || u.notes || '').trim().slice(0, 120)
|
||||||
|
return (
|
||||||
|
<li key={`n-${u.id}`} style={{ marginBottom: '0.5rem' }}>
|
||||||
|
<Link to={`/planning/run/${u.id}`} style={{ fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||||||
|
{unitWhenLabel(u)}
|
||||||
|
</Link>
|
||||||
|
{u.group_name ? <span style={{ color: 'var(--text3)' }}>{` · ${u.group_name}`}</span> : null}
|
||||||
|
<div style={{ color: 'var(--text2)', marginTop: '4px' }}>
|
||||||
|
{snippet}
|
||||||
|
{(u.trainer_notes || u.notes || '').trim().length > 120 ? '…' : ''}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', margin: 0 }}>
|
||||||
|
Keine Einträge mit Allgemein‑ oder Trainer‑Notizen in deinen nächsten geplanten Terminen.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3 style={{ fontSize: '1.05rem', marginBottom: '0.65rem' }}>Rückschau (durchgeführt)</h3>
|
||||||
|
{trainingHomeErr ? (
|
||||||
|
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>{trainingHomeErr}</p>
|
||||||
|
) : trainingHome?.recent?.length ? (
|
||||||
|
<ul style={{ margin: 0, paddingLeft: '1.15rem', color: 'var(--text2)', fontSize: '0.9rem', lineHeight: 1.55 }}>
|
||||||
|
{trainingHome.recent.map((u) => (
|
||||||
|
<li key={`r-${u.id}`} style={{ marginBottom: '0.35rem' }}>
|
||||||
|
<Link to={`/planning/run/${u.id}`} style={{ fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||||||
|
{(u.actual_date || u.planned_date || '').toString().slice(0, 10) || 'Datum'}
|
||||||
|
</Link>
|
||||||
|
{u.group_name ? (
|
||||||
|
<span style={{ color: 'var(--text3)' }}>{` — ${u.group_name}`}</span>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', margin: 0 }}>Noch keine abgeschlossenen Einheiten in der Kurzliste.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Status Grid */}
|
{/* Status Grid */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,8 @@ function TrainingPlanningPage() {
|
||||||
const [endDate, setEndDate] = useState(thirtyDaysLater)
|
const [endDate, setEndDate] = useState(thirtyDaysLater)
|
||||||
const [planView, setPlanView] = useState('list')
|
const [planView, setPlanView] = useState('list')
|
||||||
const [calendarMonthStr, setCalendarMonthStr] = useState(() => today.slice(0, 7))
|
const [calendarMonthStr, setCalendarMonthStr] = useState(() => today.slice(0, 7))
|
||||||
|
const [planScope, setPlanScope] = useState('group')
|
||||||
|
const [assignedToMeOnly, setAssignedToMeOnly] = useState(false)
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
group_id: '',
|
group_id: '',
|
||||||
|
|
@ -162,17 +164,37 @@ function TrainingPlanningPage() {
|
||||||
start = r.gridStart
|
start = r.gridStart
|
||||||
end = r.gridEnd
|
end = r.gridEnd
|
||||||
}
|
}
|
||||||
|
const gid = parseInt(selectedGroupId, 10)
|
||||||
|
const groupRow = groups.find((g) => g.id === gid)
|
||||||
|
const clubId = groupRow?.club_id
|
||||||
try {
|
try {
|
||||||
const unitsData = await api.listTrainingUnits({
|
const filters = {
|
||||||
group_id: selectedGroupId,
|
|
||||||
start_date: start,
|
start_date: start,
|
||||||
end_date: end
|
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)
|
setUnits(unitsData)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load units:', err)
|
console.error('Failed to load units:', err)
|
||||||
}
|
}
|
||||||
}, [selectedGroupId, startDate, endDate, planView, calendarMonthStr])
|
}, [
|
||||||
|
selectedGroupId,
|
||||||
|
groups,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
planView,
|
||||||
|
calendarMonthStr,
|
||||||
|
planScope,
|
||||||
|
assignedToMeOnly
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
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) => {
|
const handleDelete = async (unit) => {
|
||||||
if (!confirm(`Trainingseinheit vom ${unit.planned_date} wirklich löschen?`)) return
|
if (!confirm(`Trainingseinheit vom ${unit.planned_date} wirklich löschen?`)) return
|
||||||
try {
|
try {
|
||||||
|
|
@ -759,6 +791,93 @@ function TrainingPlanningPage() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<span style={{ fontSize: '0.78rem', color: 'var(--text3)', lineHeight: 1.4, flex: '1 1 200px' }}>
|
||||||
|
„Ganzer Verein“ nutzt den Verein der gewählten Gruppe; neue Termine unten gelten weiter für die gewählte Gruppe.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedGroup && (
|
{selectedGroup && (
|
||||||
|
|
@ -950,7 +1069,9 @@ function TrainingPlanningPage() {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleEdit(unit)}
|
onClick={() => handleEdit(unit)}
|
||||||
title={[
|
title={[
|
||||||
|
planScope === 'club' && unit.group_name ? unit.group_name : '',
|
||||||
unit.planned_time_start?.slice(0, 5) || '',
|
unit.planned_time_start?.slice(0, 5) || '',
|
||||||
|
unit.lead_trainer_name?.trim(),
|
||||||
unit.planned_focus?.trim(),
|
unit.planned_focus?.trim(),
|
||||||
unit.status === 'completed'
|
unit.status === 'completed'
|
||||||
? 'Durchgeführt'
|
? 'Durchgeführt'
|
||||||
|
|
@ -986,6 +1107,18 @@ function TrainingPlanningPage() {
|
||||||
? `${unit.planned_time_start.slice(0, 5)}`
|
? `${unit.planned_time_start.slice(0, 5)}`
|
||||||
: 'Ganztags'}
|
: 'Ganztags'}
|
||||||
</span>
|
</span>
|
||||||
|
{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() ? (
|
{unit.planned_focus?.trim() ? (
|
||||||
<span style={{ display: 'block', color: 'var(--text2)', fontWeight: 400 }}>
|
<span style={{ display: 'block', color: 'var(--text2)', fontWeight: 400 }}>
|
||||||
{(unit.planned_focus || '').trim().length > 24
|
{(unit.planned_focus || '').trim().length > 24
|
||||||
|
|
@ -1017,6 +1150,13 @@ function TrainingPlanningPage() {
|
||||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
{units.map((unit) => {
|
{units.map((unit) => {
|
||||||
const lineage = unit.origin_framework_slot_id ? frameworkLineageText(unit) : null
|
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 (
|
return (
|
||||||
<div key={unit.id} className="card">
|
<div key={unit.id} className="card">
|
||||||
<div
|
<div
|
||||||
|
|
@ -1035,6 +1175,20 @@ function TrainingPlanningPage() {
|
||||||
{unit.planned_time_start &&
|
{unit.planned_time_start &&
|
||||||
` · ${unit.planned_time_start.slice(0, 5)} - ${unit.planned_time_end?.slice(0, 5)}`}
|
` · ${unit.planned_time_start.slice(0, 5)} - ${unit.planned_time_end?.slice(0, 5)}`}
|
||||||
</h3>
|
</h3>
|
||||||
|
{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>
|
||||||
{unit.planned_focus && (
|
{unit.planned_focus && (
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -1134,6 +1288,11 @@ function TrainingPlanningPage() {
|
||||||
<button className="btn btn-secondary" onClick={() => handleEdit(unit)}>
|
<button className="btn btn-secondary" onClick={() => handleEdit(unit)}>
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
</button>
|
</button>
|
||||||
|
{showTakeLead ? (
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => handleTakeLead(unit)}>
|
||||||
|
Ich übernehme
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
className="btn"
|
className="btn"
|
||||||
style={{ background: 'var(--danger)', color: 'white', border: 'none' }}
|
style={{ background: 'var(--danger)', color: 'white', border: 'none' }}
|
||||||
|
|
|
||||||
|
|
@ -882,15 +882,21 @@ export async function deleteTrainerContext(id) {
|
||||||
// Training Planning
|
// Training Planning
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/** Query-Parameter wie GET /api/training-units (group_id, start_date, end_date, status). */
|
/** Query-Parameter wie GET /api/training-units. */
|
||||||
export async function listTrainingUnits(filters = {}) {
|
export async function listTrainingUnits(filters = {}) {
|
||||||
const q = new URLSearchParams()
|
const q = new URLSearchParams()
|
||||||
if (filters.group_id != null && filters.group_id !== '') {
|
if (filters.group_id != null && filters.group_id !== '') {
|
||||||
q.set('group_id', String(filters.group_id))
|
q.set('group_id', String(filters.group_id))
|
||||||
}
|
}
|
||||||
|
if (filters.club_id != null && filters.club_id !== '') {
|
||||||
|
q.set('club_id', String(filters.club_id))
|
||||||
|
}
|
||||||
if (filters.start_date) q.set('start_date', filters.start_date)
|
if (filters.start_date) q.set('start_date', filters.start_date)
|
||||||
if (filters.end_date) q.set('end_date', filters.end_date)
|
if (filters.end_date) q.set('end_date', filters.end_date)
|
||||||
if (filters.status) q.set('status', filters.status)
|
if (filters.status) q.set('status', filters.status)
|
||||||
|
if (filters.assigned_to_me === true) q.set('assigned_to_me', 'true')
|
||||||
|
if (filters.sort) q.set('sort', String(filters.sort))
|
||||||
|
if (filters.limit != null && filters.limit !== '') q.set('limit', String(filters.limit))
|
||||||
const qs = q.toString()
|
const qs = q.toString()
|
||||||
return request(`/api/training-units${qs ? `?${qs}` : ''}`)
|
return request(`/api/training-units${qs ? `?${qs}` : ''}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user