All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 19s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m21s
- Added Vitest as a testing framework and included test scripts in package.json for improved testing capabilities. - Refactored TrainingPlanningPageRoot component by removing unused state variables and imports, streamlining the code for better readability and performance. - Introduced new utility functions for planning routes to enhance navigation within the training planning interface.
295 lines
13 KiB
JavaScript
295 lines
13 KiB
JavaScript
import React, { useState, useEffect, useMemo } from 'react'
|
||
import { Link } from 'react-router-dom'
|
||
import { CalendarCheck, ClipboardList, FilePenLine, Library } from 'lucide-react'
|
||
import { useAuth } from '../context/AuthContext'
|
||
import api from '../utils/api'
|
||
import { getTenantClubDependencyKey } from '../utils/activeClub'
|
||
import EmailVerificationBanner from '../components/EmailVerificationBanner'
|
||
import DashboardTrainingVisibilityWidget from '../components/DashboardTrainingVisibilityWidget'
|
||
import DashboardOrgInboxWidget from '../components/DashboardOrgInboxWidget'
|
||
|
||
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 formatCappedCount(n, capped) {
|
||
if (capped && n >= 1) return `${n}+`
|
||
return String(n)
|
||
}
|
||
|
||
function Dashboard() {
|
||
const [trainingHome, setTrainingHome] = useState(null)
|
||
const [phase0Stats, setPhase0Stats] = useState(null)
|
||
const [dashboardKpisErr, setDashboardKpisErr] = useState(null)
|
||
const { user, loading: authLoading } = useAuth()
|
||
const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])
|
||
|
||
useEffect(() => {
|
||
if (!user?.id) {
|
||
setTrainingHome(null)
|
||
setPhase0Stats(null)
|
||
setDashboardKpisErr(null)
|
||
return undefined
|
||
}
|
||
let cancelled = false
|
||
;(async () => {
|
||
setDashboardKpisErr(null)
|
||
try {
|
||
const data = await api.getDashboardKpis()
|
||
if (cancelled || !data || typeof data !== 'object') return
|
||
const th = data.training_home && typeof data.training_home === 'object' ? data.training_home : {}
|
||
setTrainingHome({
|
||
upcoming: Array.isArray(th.upcoming) ? th.upcoming : [],
|
||
reviewPending: Array.isArray(th.review_pending) ? th.review_pending : [],
|
||
plannedWithNotes: Array.isArray(th.planned_with_notes) ? th.planned_with_notes : [],
|
||
})
|
||
setPhase0Stats({
|
||
year: data.year,
|
||
draftCount: data.draft_count,
|
||
draftCapped: Boolean(data.draft_capped),
|
||
draftPreview: Array.isArray(data.draft_preview) ? data.draft_preview : [],
|
||
mineCount: data.mine_count ?? 0,
|
||
mineCapped: Boolean(data.mine_capped),
|
||
ytdCompletedCount: data.ytd_completed_count ?? 0,
|
||
ytdCapped: Boolean(data.ytd_capped),
|
||
})
|
||
} catch (e) {
|
||
if (!cancelled) {
|
||
console.error('Dashboard KPIs / Trainingsübersicht:', e)
|
||
setDashboardKpisErr(e.message || 'Konnte Dashboard-Daten nicht laden')
|
||
setTrainingHome(null)
|
||
setPhase0Stats(null)
|
||
}
|
||
}
|
||
})()
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [user?.id, tenantClubDepKey])
|
||
|
||
if (authLoading) {
|
||
return (
|
||
<div className="app-page" style={{ padding: '2rem 0', textAlign: 'center' }}>
|
||
<div className="spinner"></div>
|
||
<p>Laden...</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const draftsHref = '/exercises?status=draft&mine=1'
|
||
const mineHref = '/exercises?mine=1'
|
||
|
||
return (
|
||
<div className="app-page dashboard-page">
|
||
<div className="dashboard-greeting">
|
||
<div>
|
||
<h1 className="page-title" style={{ marginBottom: '6px' }}>
|
||
Dashboard
|
||
</h1>
|
||
<p className="muted" style={{ marginTop: 0 }}>
|
||
Willkommen, {user?.name || user?.email}! Shinkan unterstützt dich bei Übungen, Planung und
|
||
Vereinsstruktur.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{user ? <EmailVerificationBanner profile={user} /> : null}
|
||
|
||
{user?.id ? (
|
||
<>
|
||
<DashboardOrgInboxWidget />
|
||
<section className="dashboard-section" aria-labelledby="dash-phase0-title">
|
||
<div className="dashboard-section__header">
|
||
<div className="dashboard-section__headline">
|
||
<h2 id="dash-phase0-title" className="dashboard-section__title">
|
||
Kurzüberblick
|
||
</h2>
|
||
<p className="dashboard-section__description">
|
||
Trainings dieses Kalenderjahres beziehen sich auf den <strong>geplanten Termin</strong> (nicht
|
||
zwingend Abschlussdatum). Zahlen können bei sehr vielen Einträgen mit „+“ enden.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{dashboardKpisErr ? (
|
||
<p className="dashboard-phase0-kpis__err" role="alert">
|
||
{dashboardKpisErr}
|
||
</p>
|
||
) : null}
|
||
{!dashboardKpisErr && !phase0Stats ? (
|
||
<div className="dashboard-phase0-kpis__loading muted">Zahlen werden geladen…</div>
|
||
) : null}
|
||
{!dashboardKpisErr && phase0Stats ? (
|
||
<div className="dashboard-phase0-kpis">
|
||
<Link className="dashboard-kpi-card" to={draftsHref}>
|
||
<span className="dashboard-kpi-card__icon" aria-hidden>
|
||
<FilePenLine size={22} strokeWidth={2} />
|
||
</span>
|
||
<span className="dashboard-kpi-card__value">
|
||
{formatCappedCount(phase0Stats.draftCount, phase0Stats.draftCapped)}
|
||
</span>
|
||
<span className="dashboard-kpi-card__label">Übungs-Entwürfe</span>
|
||
<span className="dashboard-kpi-card__hint">finalisieren</span>
|
||
</Link>
|
||
<Link className="dashboard-kpi-card" to={mineHref}>
|
||
<span className="dashboard-kpi-card__icon" aria-hidden>
|
||
<Library size={22} strokeWidth={2} />
|
||
</span>
|
||
<span className="dashboard-kpi-card__value">
|
||
{formatCappedCount(phase0Stats.mineCount, phase0Stats.mineCapped)}
|
||
</span>
|
||
<span className="dashboard-kpi-card__label">Meine Übungen</span>
|
||
<span className="dashboard-kpi-card__hint">alle Status</span>
|
||
</Link>
|
||
<div className="dashboard-kpi-card dashboard-kpi-card--static">
|
||
<span className="dashboard-kpi-card__icon" aria-hidden>
|
||
<ClipboardList size={22} strokeWidth={2} />
|
||
</span>
|
||
<span className="dashboard-kpi-card__value">
|
||
{formatCappedCount(phase0Stats.ytdCompletedCount, phase0Stats.ytdCapped)}
|
||
</span>
|
||
<span className="dashboard-kpi-card__label">Gehalten {phase0Stats.year}</span>
|
||
<span className="dashboard-kpi-card__hint">abrechnungsnah</span>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
{!dashboardKpisErr && phase0Stats?.draftPreview?.length ? (
|
||
<div className="card dashboard-draft-preview" style={{ marginTop: '1rem' }}>
|
||
<h3 className="dashboard-preview-card__title" style={{ marginTop: 0 }}>
|
||
Entwürfe fertigstellen
|
||
</h3>
|
||
<p className="muted" style={{ marginTop: '0.35rem', marginBottom: '0.85rem', fontSize: '0.92rem' }}>
|
||
Private Übungs-Entwürfe (z. B. aus der Planung) — Ziel, Durchführung und Details in der Bearbeitung
|
||
ergänzen.
|
||
</p>
|
||
<ul className="dashboard-preview-card__list">
|
||
{phase0Stats.draftPreview.map((ex) => (
|
||
<li key={ex.id}>
|
||
<Link to={`/exercises/${ex.id}/edit`} className="dashboard-preview-card__link">
|
||
{ex.title}
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<p style={{ margin: '0.75rem 0 0', fontSize: '0.86rem' }}>
|
||
<Link to={draftsHref}>Alle Entwürfe in der Übersicht</Link>
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
|
||
<section className="dashboard-section" aria-labelledby="dash-trainings-title">
|
||
<div className="dashboard-section__header">
|
||
<div className="dashboard-section__headline">
|
||
<h2 id="dash-trainings-title" className="dashboard-section__title">
|
||
Trainings
|
||
</h2>
|
||
<p className="dashboard-section__description">
|
||
Einheiten, bei denen du als Leitung oder Co-Trainer eingetragen bist.
|
||
</p>
|
||
</div>
|
||
<div className="dashboard-section__actions">
|
||
<Link to="/planning" className="btn btn-secondary" style={{ textDecoration: 'none' }}>
|
||
<CalendarCheck size={16} strokeWidth={2} aria-hidden style={{ marginRight: 6 }} />
|
||
Planung
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
<div className="dashboard-training-preview-grid">
|
||
<div className="card dashboard-preview-card">
|
||
<h3 className="dashboard-preview-card__title">Nächste Termine</h3>
|
||
{dashboardKpisErr ? (
|
||
<p className="dashboard-preview-card__err">{dashboardKpisErr}</p>
|
||
) : trainingHome?.upcoming?.length ? (
|
||
<ul className="dashboard-preview-card__list">
|
||
{trainingHome.upcoming.map((u) => (
|
||
<li key={u.id}>
|
||
<Link to={`/planning/run/${u.id}`} className="dashboard-preview-card__link">
|
||
{unitWhenLabel(u)}
|
||
</Link>
|
||
{u.group_name ? (
|
||
<span className="dashboard-preview-card__meta">{` — ${u.group_name}`}</span>
|
||
) : null}
|
||
{u.lead_trainer_name ? (
|
||
<span className="dashboard-preview-card__sub">
|
||
Leitung: {u.lead_trainer_name}
|
||
</span>
|
||
) : null}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<p className="dashboard-preview-card__empty">
|
||
Keine anstehenden Termine.{' '}
|
||
<Link to="/planning">Zur Trainingsplanung</Link>
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="card dashboard-preview-card">
|
||
<h3 className="dashboard-preview-card__title">Hinweise (anstehend)</h3>
|
||
{dashboardKpisErr ? (
|
||
<p className="dashboard-preview-card__err">{dashboardKpisErr}</p>
|
||
) : trainingHome?.plannedWithNotes?.length ? (
|
||
<ul className="dashboard-preview-card__list dashboard-preview-card__list--notes">
|
||
{trainingHome.plannedWithNotes.map((u) => {
|
||
const snippet = (u.trainer_notes || u.notes || '').trim().slice(0, 120)
|
||
return (
|
||
<li key={`n-${u.id}`}>
|
||
<Link to={`/planning/run/${u.id}`} className="dashboard-preview-card__link">
|
||
{unitWhenLabel(u)}
|
||
</Link>
|
||
{u.group_name ? (
|
||
<span className="dashboard-preview-card__meta">{` · ${u.group_name}`}</span>
|
||
) : null}
|
||
<div className="dashboard-preview-card__note-snippet">
|
||
{snippet}
|
||
{(u.trainer_notes || u.notes || '').trim().length > 120 ? '…' : ''}
|
||
</div>
|
||
</li>
|
||
)
|
||
})}
|
||
</ul>
|
||
) : (
|
||
<p className="dashboard-preview-card__empty">
|
||
Keine Vermerke in den nächsten geplanten Terminen.
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="card dashboard-preview-card">
|
||
<h3 className="dashboard-preview-card__title">Offene Rückschau</h3>
|
||
{dashboardKpisErr ? (
|
||
<p className="dashboard-preview-card__err">{dashboardKpisErr}</p>
|
||
) : trainingHome?.reviewPending?.length ? (
|
||
<ul className="dashboard-preview-card__list">
|
||
{trainingHome.reviewPending.map((u) => (
|
||
<li key={`r-${u.id}`}>
|
||
<Link to={`/planning/units/${u.id}/edit`} className="dashboard-preview-card__link">
|
||
{(u.actual_date || u.planned_date || '').toString().slice(0, 10) || 'Datum'}
|
||
</Link>
|
||
{u.group_name ? (
|
||
<span className="dashboard-preview-card__meta">{` — ${u.group_name}`}</span>
|
||
) : null}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<p className="dashboard-preview-card__empty">
|
||
Keine durchgeführten Trainings mit offener Nachbereitung. Zum Abschluss der Rückschau in der
|
||
Planung „Rückschau erledigt“ aktivieren.
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<DashboardTrainingVisibilityWidget user={user} />
|
||
</section>
|
||
</>
|
||
) : null}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Dashboard
|