shinkan-jinkendo/frontend/src/pages/Dashboard.jsx
Lars 16eaf839e7
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
Enhance frontend testing setup and refactor TrainingPlanningPageRoot component
- 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.
2026-05-19 11:02:03 +02:00

295 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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