shinkan-jinkendo/frontend/src/pages/Dashboard.jsx

295 lines
13 KiB
React
Raw Normal View History

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