feat: add club visibility queue for training exercises
All checks were successful
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 24s
All checks were successful
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 24s
- Implemented a new API endpoint to retrieve exercises in training units that are not yet visible to the target club, enhancing visibility management for trainers. - Introduced a new widget in the Dashboard to display these exercises, improving user access to relevant training information. - Added helper functions to determine exercise visibility based on club membership and status, streamlining the exercise promotion process.
This commit is contained in:
parent
f9d518fb78
commit
fe703ca414
|
|
@ -4,6 +4,7 @@ und wiederverwendbare Trainingsvorlagen (Sektions-Gliederung).
|
|||
|
||||
Governance: Sichtbarkeit wie Übungen (private / club / official); Schreiben nur Ersteller oder Plattform-Admin.
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
|
@ -692,6 +693,26 @@ def _group_club_id_for_scheduled_unit(cur, unit_id: int) -> Optional[int]:
|
|||
return int(r["club_id"])
|
||||
|
||||
|
||||
def _exercise_needs_club_visibility_for_target(ex: Dict[str, Any], target_club_id: int) -> bool:
|
||||
"""Übung für Mitglieder des Ziel-Vereins in der Durchführung sichtbar machen (Dashboard/Queue)."""
|
||||
if str(ex.get("status") or "").strip().lower() == "archived":
|
||||
return False
|
||||
vis = (ex.get("visibility") or "private").strip().lower()
|
||||
if vis == "official":
|
||||
return False
|
||||
if vis == "private":
|
||||
return True
|
||||
if vis == "club":
|
||||
raw = ex.get("club_id")
|
||||
if raw is None:
|
||||
return True
|
||||
try:
|
||||
return int(raw) != int(target_club_id)
|
||||
except (TypeError, ValueError):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _caller_may_promote_exercise_to_club(
|
||||
cur,
|
||||
exercise_created_by: Optional[int],
|
||||
|
|
@ -1209,6 +1230,163 @@ def list_training_units(
|
|||
return [r2d(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/training-units/exercises-club-visibility-queue")
|
||||
def exercises_club_visibility_queue(
|
||||
start_date: Optional[str] = Query(default=None),
|
||||
end_date: Optional[str] = Query(default=None),
|
||||
assigned_to_me: bool = Query(default=True),
|
||||
limit_units: int = Query(default=80, ge=1, le=150),
|
||||
tenant: TenantContext = Depends(get_tenant_context),
|
||||
):
|
||||
"""
|
||||
Übungen in deinen Trainingseinheiten (Zeitfenster), die für den jeweiligen Verein der Gruppe
|
||||
noch nicht vereinsweit sichtbar sind — für Dashboard & Freigabe-Workflow.
|
||||
"""
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
if start_date is None:
|
||||
start_date = (date.today() - timedelta(days=45)).isoformat()
|
||||
if end_date is None:
|
||||
end_date = (date.today() + timedelta(days=365)).isoformat()
|
||||
|
||||
units = list_training_units(
|
||||
group_id=None,
|
||||
club_id=None,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
status=None,
|
||||
assigned_to_me=assigned_to_me,
|
||||
debrief_pending=False,
|
||||
sort="asc",
|
||||
limit=limit_units,
|
||||
tenant=tenant,
|
||||
)
|
||||
unit_ids = [int(u["id"]) for u in units if u.get("id") is not None]
|
||||
if not unit_ids:
|
||||
return {"items": []}
|
||||
|
||||
placeholders = ",".join(["%s"] * len(unit_ids))
|
||||
items: List[Dict[str, Any]] = []
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT DISTINCT tu.id AS unit_id,
|
||||
tu.planned_date,
|
||||
tg.name AS group_name,
|
||||
tg.club_id AS target_club_id,
|
||||
c.name AS target_club_name,
|
||||
tusi.exercise_id AS exercise_id
|
||||
FROM training_units tu
|
||||
INNER JOIN training_groups tg ON tu.group_id = tg.id
|
||||
LEFT JOIN clubs c ON c.id = tg.club_id
|
||||
INNER JOIN training_unit_sections tus ON tus.training_unit_id = tu.id
|
||||
INNER JOIN training_unit_section_items tusi ON tusi.section_id = tus.id
|
||||
WHERE tu.id IN ({placeholders})
|
||||
AND tu.framework_slot_id IS NULL
|
||||
AND tusi.item_type = 'exercise'
|
||||
AND tusi.exercise_id IS NOT NULL
|
||||
""",
|
||||
tuple(unit_ids),
|
||||
)
|
||||
pairs = [r2d(r) for r in cur.fetchall()]
|
||||
if not pairs:
|
||||
return {"items": []}
|
||||
|
||||
ex_ids = sorted(
|
||||
{int(p["exercise_id"]) for p in pairs if p.get("exercise_id") is not None}
|
||||
)
|
||||
if not ex_ids:
|
||||
return {"items": []}
|
||||
|
||||
exercises_map: Dict[int, Dict[str, Any]] = {}
|
||||
ph = ",".join(["%s"] * len(ex_ids))
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT id, title, visibility, club_id, created_by, status
|
||||
FROM exercises
|
||||
WHERE id IN ({ph})
|
||||
""",
|
||||
tuple(ex_ids),
|
||||
)
|
||||
for r in cur.fetchall():
|
||||
d = r2d(r)
|
||||
exercises_map[int(d["id"])] = d
|
||||
|
||||
agg: Dict[tuple, Dict[str, Any]] = {}
|
||||
for p in pairs:
|
||||
try:
|
||||
ex_id = int(p["exercise_id"])
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
tc_raw = p.get("target_club_id")
|
||||
if tc_raw is None:
|
||||
continue
|
||||
tc = int(tc_raw)
|
||||
key = (ex_id, tc)
|
||||
if key not in agg:
|
||||
agg[key] = {
|
||||
"exercise_id": ex_id,
|
||||
"target_club_id": tc,
|
||||
"target_club_name": (p.get("target_club_name") or "").strip(),
|
||||
"units": [],
|
||||
}
|
||||
uid = p.get("unit_id")
|
||||
if uid is None:
|
||||
continue
|
||||
agg[key]["units"].append(
|
||||
{
|
||||
"id": int(uid),
|
||||
"planned_date": str(p["planned_date"]) if p.get("planned_date") is not None else "",
|
||||
"group_name": (p.get("group_name") or "").strip(),
|
||||
}
|
||||
)
|
||||
|
||||
for _key, blob in agg.items():
|
||||
ex_id = blob["exercise_id"]
|
||||
tc = blob["target_club_id"]
|
||||
ex = exercises_map.get(ex_id)
|
||||
if not ex:
|
||||
continue
|
||||
if not _exercise_needs_club_visibility_for_target(ex, tc):
|
||||
continue
|
||||
uniq_units = {u["id"]: u for u in blob["units"]}.values()
|
||||
ulist = sorted(
|
||||
uniq_units,
|
||||
key=lambda x: (x.get("planned_date") or "", x.get("id")),
|
||||
)
|
||||
cb = ex.get("created_by")
|
||||
cb_int = int(cb) if cb is not None else None
|
||||
can_promote = _caller_may_promote_exercise_to_club(cur, cb_int, profile_id, role, tc)
|
||||
vis = (ex.get("visibility") or "private").strip().lower()
|
||||
st = (ex.get("status") or "draft").strip().lower()
|
||||
ecid = ex.get("club_id")
|
||||
items.append(
|
||||
{
|
||||
"exercise_id": ex_id,
|
||||
"title": (ex.get("title") or f"Übung #{ex_id}").strip() or f"Übung #{ex_id}",
|
||||
"visibility": vis,
|
||||
"status": st,
|
||||
"club_id": int(ecid) if ecid is not None else None,
|
||||
"created_by": cb_int,
|
||||
"target_club_id": tc,
|
||||
"target_club_name": blob.get("target_club_name") or "",
|
||||
"can_promote": can_promote,
|
||||
"units": ulist,
|
||||
}
|
||||
)
|
||||
|
||||
items.sort(
|
||||
key=lambda x: (
|
||||
(x["units"][0].get("planned_date") if x["units"] else ""),
|
||||
x["title"],
|
||||
)
|
||||
)
|
||||
return {"items": items}
|
||||
|
||||
|
||||
@router.get("/training-units/{unit_id}")
|
||||
def get_training_unit(unit_id: int, tenant: TenantContext = Depends(get_tenant_context)):
|
||||
profile_id = tenant.profile_id
|
||||
|
|
|
|||
347
frontend/src/components/DashboardTrainingVisibilityWidget.jsx
Normal file
347
frontend/src/components/DashboardTrainingVisibilityWidget.jsx
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { CalendarDays, ClipboardList } from 'lucide-react'
|
||||
import api from '../utils/api'
|
||||
|
||||
const VIS_LABELS = { private: 'Privat', club: 'Verein', official: 'Offiziell' }
|
||||
const STATUS_LABELS = {
|
||||
draft: 'Entwurf',
|
||||
in_review: 'Prüfung',
|
||||
approved: 'Freigegeben',
|
||||
archived: 'Archiv',
|
||||
}
|
||||
|
||||
function rowKey(item) {
|
||||
return `${item.exercise_id}-${item.target_club_id}`
|
||||
}
|
||||
|
||||
function unitPlanTitle(u) {
|
||||
const d = (u.planned_date || '').toString().slice(0, 10)
|
||||
const g = (u.group_name || '').trim()
|
||||
return [d, g].filter(Boolean).join(' · ') || `Einheit #${u.id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard: Übungen aus eigenen Trainingseinheiten, die für den Verein der Gruppe noch nicht freigegeben sind.
|
||||
*/
|
||||
export default function DashboardTrainingVisibilityWidget({ user }) {
|
||||
const [items, setItems] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [err, setErr] = useState(null)
|
||||
const [selected, setSelected] = useState(() => new Set())
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [msg, setMsg] = useState(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!user?.id) return
|
||||
setErr(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await api.getTrainingExerciseClubVisibilityQueue({ limit_units: 100 })
|
||||
const list = Array.isArray(res?.items) ? res.items : []
|
||||
setItems(list)
|
||||
setSelected(new Set())
|
||||
setMsg(null)
|
||||
} catch (e) {
|
||||
setErr(e?.message || String(e))
|
||||
setItems([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [user?.id])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const promotableSelected = useMemo(() => {
|
||||
const out = []
|
||||
for (const k of selected) {
|
||||
const it = items.find((x) => rowKey(x) === k)
|
||||
if (it?.can_promote) out.push(it)
|
||||
}
|
||||
return out
|
||||
}, [items, selected])
|
||||
|
||||
const toggle = (key) => {
|
||||
setSelected((prev) => {
|
||||
const n = new Set(prev)
|
||||
if (n.has(key)) n.delete(key)
|
||||
else n.add(key)
|
||||
return n
|
||||
})
|
||||
}
|
||||
|
||||
const toggleAllPromotable = () => {
|
||||
const keys = items.filter((i) => i.can_promote).map(rowKey)
|
||||
setSelected((prev) => {
|
||||
if (keys.length && keys.every((k) => prev.has(k))) {
|
||||
return new Set()
|
||||
}
|
||||
return new Set(keys)
|
||||
})
|
||||
}
|
||||
|
||||
const promoteSelected = async () => {
|
||||
if (!promotableSelected.length) return
|
||||
setBusy(true)
|
||||
setMsg(null)
|
||||
try {
|
||||
const byClub = new Map()
|
||||
for (const it of promotableSelected) {
|
||||
const cid = it.target_club_id
|
||||
if (!byClub.has(cid)) byClub.set(cid, [])
|
||||
byClub.get(cid).push(it.exercise_id)
|
||||
}
|
||||
let anyFail = false
|
||||
for (const [clubId, ids] of byClub) {
|
||||
const uniq = [...new Set(ids)]
|
||||
const res = await api.bulkPatchExercisesMetadata({
|
||||
exercise_ids: uniq,
|
||||
visibility: 'club',
|
||||
club_id: clubId,
|
||||
})
|
||||
if ((res?.failed || []).length) {
|
||||
anyFail = true
|
||||
const f = res.failed[0]
|
||||
setMsg(f?.detail || 'Freigabe teilweise fehlgeschlagen')
|
||||
}
|
||||
}
|
||||
if (!anyFail) setMsg(null)
|
||||
await load()
|
||||
} catch (e) {
|
||||
setMsg(e?.message || String(e))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!user?.id) return null
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="card dashboard-preview-card" style={{ marginTop: '1rem' }}>
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
Vereinsfreigaben werden geladen…
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
<div className="card dashboard-preview-card" style={{ marginTop: '1rem' }} role="alert">
|
||||
<p style={{ margin: 0, color: 'var(--danger, #c0392b)' }}>{err}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<div className="card dashboard-preview-card" style={{ marginTop: '1rem' }}>
|
||||
<h3 className="dashboard-preview-card__title" style={{ marginTop: 0 }}>
|
||||
Vereinssichtbarkeit in deinen Trainings
|
||||
</h3>
|
||||
<p className="dashboard-preview-card__empty" style={{ margin: 0 }}>
|
||||
Keine Übungen in den abgefragten Einheiten, die noch auf Verein gestellt werden müssten.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const allPromo = items.filter((i) => i.can_promote)
|
||||
const allSelected = allPromo.length > 0 && allPromo.every((i) => selected.has(rowKey(i)))
|
||||
|
||||
return (
|
||||
<div className="card dashboard-preview-card dashboard-vis-queue" style={{ marginTop: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3 className="dashboard-preview-card__title" style={{ marginTop: 0 }}>
|
||||
Vereinssichtbarkeit in deinen Trainings
|
||||
</h3>
|
||||
<p
|
||||
className="muted"
|
||||
style={{ margin: '0.35rem 0 0', fontSize: '0.88rem', lineHeight: 1.45, maxWidth: '52rem' }}
|
||||
>
|
||||
Übungen in deinen Einheiten, die für den jeweiligen Verein noch nicht sichtbar sind — auf{' '}
|
||||
<strong>Verein</strong> setzen oder zur Bearbeitung / Planung springen.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' }}>
|
||||
{allPromo.length ? (
|
||||
<button type="button" className="btn btn-secondary" onClick={toggleAllPromotable}>
|
||||
{allSelected ? 'Auswahl leeren' : 'Alle wählbar'}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={!promotableSelected.length || busy}
|
||||
onClick={promoteSelected}
|
||||
>
|
||||
{busy ? 'Speichern…' : `Freigeben (${promotableSelected.length})`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '0.75rem', overflowX: 'auto' }}>
|
||||
<table
|
||||
className="dashboard-vis-queue__table"
|
||||
style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.86rem' }}
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
borderBottom: '1px solid var(--border2)',
|
||||
color: 'var(--text3)',
|
||||
fontSize: '0.78rem',
|
||||
}}
|
||||
>
|
||||
<th style={{ width: 32, padding: '6px 4px' }} aria-label="Auswahl" />
|
||||
<th style={{ padding: '6px 6px' }}>Übung</th>
|
||||
<th style={{ padding: '6px 6px', whiteSpace: 'nowrap' }}>Sichtbarkeit</th>
|
||||
<th style={{ padding: '6px 6px', whiteSpace: 'nowrap' }}>Status</th>
|
||||
<th style={{ padding: '6px 6px' }}>Verein</th>
|
||||
<th style={{ padding: '6px 4px', width: 76 }} title="Planung und Durchführung">
|
||||
Kontext
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((it) => {
|
||||
const k = rowKey(it)
|
||||
const on = selected.has(k)
|
||||
const visL = VIS_LABELS[it.visibility] || it.visibility
|
||||
const stL = STATUS_LABELS[it.status] || it.status
|
||||
const first = it.units && it.units[0]
|
||||
const restN = (it.units?.length || 0) - 1
|
||||
const tool = (it.units || []).map(unitPlanTitle).join('\n')
|
||||
const cn = (it.target_club_name || '').trim()
|
||||
return (
|
||||
<tr
|
||||
key={k}
|
||||
style={{ borderBottom: '1px solid var(--border2)' }}
|
||||
title={restN > 0 ? tool : undefined}
|
||||
>
|
||||
<td style={{ padding: '6px 4px', verticalAlign: 'top' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={on}
|
||||
disabled={!it.can_promote}
|
||||
onChange={() => toggle(k)}
|
||||
title={it.can_promote ? 'Zur Freigabe wählen' : 'Keine Berechtigung für diese Freigabe'}
|
||||
aria-label={`${it.title} auswählen`}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ padding: '6px 6px', verticalAlign: 'top', minWidth: 140 }}>
|
||||
<Link
|
||||
to={`/exercises/${it.exercise_id}/edit`}
|
||||
className="dashboard-preview-card__link"
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
{it.title}
|
||||
</Link>
|
||||
</td>
|
||||
<td style={{ padding: '6px 6px', verticalAlign: 'top', whiteSpace: 'nowrap' }}>{visL}</td>
|
||||
<td style={{ padding: '6px 6px', verticalAlign: 'top', whiteSpace: 'nowrap' }}>{stL}</td>
|
||||
<td style={{ padding: '6px 6px', verticalAlign: 'top', maxWidth: 200 }}>
|
||||
<span title={cn}>{cn.length > 28 ? `${cn.slice(0, 28)}…` : cn || '—'}</span>
|
||||
</td>
|
||||
<td style={{ padding: '6px 4px', verticalAlign: 'top' }}>
|
||||
{first ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={`/planning?unit=${first.id}`}
|
||||
className="btn-ghost"
|
||||
style={{
|
||||
padding: '2px 4px',
|
||||
display: 'inline-flex',
|
||||
borderRadius: 6,
|
||||
textDecoration: 'none',
|
||||
color: 'var(--text1)',
|
||||
}}
|
||||
title={
|
||||
restN > 0
|
||||
? `Planung (${unitPlanTitle(first)}; +${restN} weitere — siehe Tooltip ganze Zeile)`
|
||||
: `Planung öffnen: ${unitPlanTitle(first)}`
|
||||
}
|
||||
aria-label="Planung öffnen"
|
||||
>
|
||||
<CalendarDays size={16} strokeWidth={2} aria-hidden />
|
||||
</Link>
|
||||
<Link
|
||||
to={`/planning/run/${first.id}`}
|
||||
className="btn-ghost"
|
||||
style={{
|
||||
padding: '2px 4px',
|
||||
display: 'inline-flex',
|
||||
borderRadius: 6,
|
||||
textDecoration: 'none',
|
||||
color: 'var(--text1)',
|
||||
}}
|
||||
title="Durchführung / Ablauf"
|
||||
aria-label="Durchführung öffnen"
|
||||
>
|
||||
<ClipboardList size={16} strokeWidth={2} aria-hidden />
|
||||
</Link>
|
||||
{restN > 0 ? (
|
||||
<span className="muted" style={{ fontSize: '0.72rem' }} title={tool}>
|
||||
+{restN}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{items.some((i) => !i.can_promote) ? (
|
||||
<p className="muted" style={{ margin: '0.5rem 0 0', fontSize: '0.82rem', lineHeight: 1.45 }}>
|
||||
Ausgegraute Kästchen: keine direkte Freigabe-Berechtigung — Vereinsorga kontaktieren oder die Einheit in
|
||||
der Planung speichern (dann ggf. automatische Vereinsfreigabe).
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<p className="muted" style={{ margin: '0.5rem 0 0', fontSize: '0.8rem', lineHeight: 1.45 }}>
|
||||
Mehrere Termine: Kalender-Icon nutzt den frühesten; „+N“ listet alle Daten und Gruppen im Tooltip.
|
||||
</p>
|
||||
|
||||
{msg ? (
|
||||
<p
|
||||
style={{ margin: '0.65rem 0 0', fontSize: '0.84rem', color: 'var(--danger, #c0392b)' }}
|
||||
role="alert"
|
||||
>
|
||||
{msg}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<p style={{ margin: '0.65rem 0 0', fontSize: '0.82rem' }}>
|
||||
<Link to="/planning">Zur Trainingsplanung</Link>
|
||||
<span className="muted"> · Zeitraum ca. 45 Tage zurück bis 1 Jahr voraus; bis zu 100 Einheiten.</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { CalendarCheck, ClipboardList, FilePenLine, Library } from 'lucide-react
|
|||
import { useAuth } from '../context/AuthContext'
|
||||
import api from '../utils/api'
|
||||
import EmailVerificationBanner from '../components/EmailVerificationBanner'
|
||||
import DashboardTrainingVisibilityWidget from '../components/DashboardTrainingVisibilityWidget'
|
||||
|
||||
function unitWhenLabel(u) {
|
||||
const d = u.planned_date ? String(u.planned_date).slice(0, 10) : ''
|
||||
|
|
@ -367,6 +368,7 @@ function Dashboard() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DashboardTrainingVisibilityWidget user={user} />
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1004,6 +1004,19 @@ export async function listTrainingUnits(filters = {}) {
|
|||
return request(`/api/training-units${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
/** Dashboard: Übungen in geplanten Einheiten, die für den Verein noch auf Sichtbarkeit „Verein“ gehören. */
|
||||
export async function getTrainingExerciseClubVisibilityQueue(filters = {}) {
|
||||
const q = new URLSearchParams()
|
||||
if (filters.start_date) q.set('start_date', String(filters.start_date))
|
||||
if (filters.end_date) q.set('end_date', String(filters.end_date))
|
||||
if (filters.assigned_to_me === false) q.set('assigned_to_me', 'false')
|
||||
if (filters.limit_units != null && filters.limit_units !== '') {
|
||||
q.set('limit_units', String(filters.limit_units))
|
||||
}
|
||||
const qs = q.toString()
|
||||
return request(`/api/training-units/exercises-club-visibility-queue${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
export async function getTrainingUnit(id) {
|
||||
return request(`/api/training-units/${id}`)
|
||||
}
|
||||
|
|
@ -1192,6 +1205,7 @@ export const api = {
|
|||
|
||||
// Training Planning
|
||||
listTrainingUnits,
|
||||
getTrainingExerciseClubVisibilityQueue,
|
||||
getTrainingUnit,
|
||||
createTrainingUnit,
|
||||
updateTrainingUnit,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user