shinkan-jinkendo/frontend/src/pages/Dashboard.jsx
Lars 18fa4de055
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 6s
Test Suite / playwright-tests (push) Successful in 26s
Test Suite / pytest-backend (pull_request) Successful in 5s
Test Suite / lint-backend (pull_request) Successful in 1s
Test Suite / build-frontend (pull_request) Successful in 6s
Test Suite / playwright-tests (pull_request) Successful in 23s
feat: add system information page and update account settings
- Introduced a new SettingsSystemInfoPage to display technical system information.
- Updated AccountSettingsPage to include a link to the new system information page, enhancing user access to app version, build, environment, and database schema details.
- Removed unused version state from Dashboard component to streamline data handling.
2026-05-07 10:29:14 +02:00

378 lines
15 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 } 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 EmailVerificationBanner from '../components/EmailVerificationBanner'
import DashboardTrainingVisibilityWidget from '../components/DashboardTrainingVisibilityWidget'
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 [profile, setProfile] = useState(null)
const [loading, setLoading] = useState(true)
const [trainingHome, setTrainingHome] = useState(null)
const [trainingHomeErr, setTrainingHomeErr] = useState(null)
const [phase0Stats, setPhase0Stats] = useState(null)
const [phase0Err, setPhase0Err] = useState(null)
const { user } = useAuth()
useEffect(() => {
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, reviewPendingRaw, 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,
debrief_pending: true,
sort: 'desc',
limit: 8,
}),
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 : [],
reviewPending: Array.isArray(reviewPendingRaw) ? reviewPendingRaw : [],
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])
useEffect(() => {
if (!user?.id) {
setPhase0Stats(null)
setPhase0Err(null)
return undefined
}
let cancelled = false
;(async () => {
setPhase0Err(null)
try {
const year = new Date().getFullYear()
const yearStart = `${year}-01-01`
const yearEnd = `${year}-12-31`
const [draftList, mineList, ytdCompleted] = await Promise.all([
api.listExercises({ created_by_me: true, status: 'draft', limit: 100 }),
api.listExercises({ created_by_me: true, limit: 100 }),
api.listTrainingUnits({
assigned_to_me: true,
status: 'completed',
start_date: yearStart,
end_date: yearEnd,
limit: 250,
sort: 'desc',
}),
])
if (!cancelled) {
const drafts = Array.isArray(draftList) ? draftList : []
setPhase0Stats({
year,
draftCount: drafts.length,
draftCapped: drafts.length >= 100,
draftPreview: drafts.slice(0, 8).map((ex) => ({
id: ex.id,
title: ex.title || `Übung #${ex.id}`,
})),
mineCount: Array.isArray(mineList) ? mineList.length : 0,
mineCapped: Array.isArray(mineList) && mineList.length >= 100,
ytdCompletedCount: Array.isArray(ytdCompleted) ? ytdCompleted.length : 0,
ytdCapped: Array.isArray(ytdCompleted) && ytdCompleted.length >= 250,
})
}
} catch (e) {
if (!cancelled) {
console.error('Dashboard Übungs-Kennzahlen:', e)
setPhase0Err(e.message || 'Konnte Übungs-Kennzahlen nicht laden')
setPhase0Stats(null)
}
}
})()
return () => {
cancelled = true
}
}, [user?.id])
const loadData = async () => {
try {
const profileData = await api.getCurrentProfile()
setProfile(profileData)
} catch (err) {
console.error('Failed to load data:', err)
} finally {
setLoading(false)
}
}
if (loading) {
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>
{profile && <EmailVerificationBanner profile={profile} />}
{user?.id ? (
<>
<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>
{phase0Err ? (
<p className="dashboard-phase0-kpis__err" role="alert">
{phase0Err}
</p>
) : null}
{!phase0Err && !phase0Stats ? (
<div className="dashboard-phase0-kpis__loading muted">Zahlen werden geladen</div>
) : null}
{!phase0Err && 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}
{!phase0Err && 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>
{trainingHomeErr ? (
<p className="dashboard-preview-card__err">{trainingHomeErr}</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>
{trainingHomeErr ? (
<p className="dashboard-preview-card__err">{trainingHomeErr}</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>
{trainingHomeErr ? (
<p className="dashboard-preview-card__err">{trainingHomeErr}</p>
) : trainingHome?.reviewPending?.length ? (
<ul className="dashboard-preview-card__list">
{trainingHome.reviewPending.map((u) => (
<li key={`r-${u.id}`}>
<Link to={`/planning?unit=${u.id}`} 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