- Bumped application version to 0.8.11 and updated database schema version. - Added new API features for training units, including filtering by club and assigned trainer. - Enhanced the TrainingPlanningPage with options to filter training units by club and assigned trainer, improving user experience. - Implemented lead trainer assignment functionality, allowing users to take lead on training units. - Updated changelog with new version details and changes.
292 lines
11 KiB
JavaScript
292 lines
11 KiB
JavaScript
import React, { useState, useEffect } from 'react'
|
||
import { Link } from 'react-router-dom'
|
||
import { useAuth } from '../context/AuthContext'
|
||
import api from '../utils/api'
|
||
import EmailVerificationBanner from '../components/EmailVerificationBanner'
|
||
|
||
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 Dashboard() {
|
||
const [version, setVersion] = useState(null)
|
||
const [profile, setProfile] = useState(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [trainingHome, setTrainingHome] = useState(null)
|
||
const [trainingHomeErr, setTrainingHomeErr] = 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, recentRaw, 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,
|
||
status: 'completed',
|
||
sort: 'desc',
|
||
limit: 6
|
||
}),
|
||
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 : [],
|
||
recent: Array.isArray(recentRaw) ? recentRaw : [],
|
||
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])
|
||
|
||
const loadData = async () => {
|
||
try {
|
||
const [versionData, profileData] = await Promise.all([
|
||
api.getVersion(),
|
||
api.getCurrentProfile()
|
||
])
|
||
setVersion(versionData)
|
||
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>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="app-page">
|
||
<h1>Dashboard</h1>
|
||
<p style={{ color: 'var(--text2)', marginTop: '0.5rem' }}>
|
||
Willkommen, {user?.name || user?.email}!
|
||
</p>
|
||
{profile && <EmailVerificationBanner profile={profile} />}
|
||
{/* Welcome Card */}
|
||
<div className="card" style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
|
||
<h2>Willkommen bei Shinkan Jinkendo</h2>
|
||
<p style={{ color: 'var(--text2)' }}>
|
||
Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung
|
||
</p>
|
||
</div>
|
||
|
||
{user?.id && (
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))',
|
||
gap: '1rem',
|
||
marginBottom: '1.5rem'
|
||
}}
|
||
>
|
||
<div className="card">
|
||
<h3 style={{ fontSize: '1.05rem', marginBottom: '0.65rem' }}>Deine nächsten Trainings</h3>
|
||
{trainingHomeErr ? (
|
||
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>{trainingHomeErr}</p>
|
||
) : trainingHome?.upcoming?.length ? (
|
||
<ul style={{ margin: 0, paddingLeft: '1.15rem', color: 'var(--text2)', fontSize: '0.9rem', lineHeight: 1.55 }}>
|
||
{trainingHome.upcoming.map((u) => (
|
||
<li key={u.id} style={{ marginBottom: '0.35rem' }}>
|
||
<Link to={`/planning/run/${u.id}`} style={{ fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||
{unitWhenLabel(u)}
|
||
</Link>
|
||
{u.group_name ? (
|
||
<span style={{ color: 'var(--text3)' }}>{` — ${u.group_name}`}</span>
|
||
) : null}
|
||
{u.lead_trainer_name ? (
|
||
<span style={{ display: 'block', fontSize: '0.82rem', color: 'var(--text3)', marginTop: '2px' }}>
|
||
Leitung: {u.lead_trainer_name}
|
||
</span>
|
||
) : null}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', margin: 0 }}>
|
||
Keine anstehenden Termine mit dir als Leitung oder Co‑Trainer. Unter{' '}
|
||
<Link to="/planning" style={{ color: 'var(--accent-dark)' }}>
|
||
Trainingsplanung
|
||
</Link>{' '}
|
||
kannst du den Vereins‑ oder Gruppen‑Zeitraum einblenden.
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="card">
|
||
<h3 style={{ fontSize: '1.05rem', marginBottom: '0.65rem' }}>Vermerk / Hinweise (anstehend)</h3>
|
||
{trainingHomeErr ? (
|
||
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>{trainingHomeErr}</p>
|
||
) : trainingHome?.plannedWithNotes?.length ? (
|
||
<ul style={{ margin: 0, paddingLeft: '1.15rem', color: 'var(--text2)', fontSize: '0.88rem', lineHeight: 1.5 }}>
|
||
{trainingHome.plannedWithNotes.map((u) => {
|
||
const snippet = (u.trainer_notes || u.notes || '').trim().slice(0, 120)
|
||
return (
|
||
<li key={`n-${u.id}`} style={{ marginBottom: '0.5rem' }}>
|
||
<Link to={`/planning/run/${u.id}`} style={{ fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||
{unitWhenLabel(u)}
|
||
</Link>
|
||
{u.group_name ? <span style={{ color: 'var(--text3)' }}>{` · ${u.group_name}`}</span> : null}
|
||
<div style={{ color: 'var(--text2)', marginTop: '4px' }}>
|
||
{snippet}
|
||
{(u.trainer_notes || u.notes || '').trim().length > 120 ? '…' : ''}
|
||
</div>
|
||
</li>
|
||
)
|
||
})}
|
||
</ul>
|
||
) : (
|
||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', margin: 0 }}>
|
||
Keine Einträge mit Allgemein‑ oder Trainer‑Notizen in deinen nächsten geplanten Terminen.
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="card">
|
||
<h3 style={{ fontSize: '1.05rem', marginBottom: '0.65rem' }}>Rückschau (durchgeführt)</h3>
|
||
{trainingHomeErr ? (
|
||
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>{trainingHomeErr}</p>
|
||
) : trainingHome?.recent?.length ? (
|
||
<ul style={{ margin: 0, paddingLeft: '1.15rem', color: 'var(--text2)', fontSize: '0.9rem', lineHeight: 1.55 }}>
|
||
{trainingHome.recent.map((u) => (
|
||
<li key={`r-${u.id}`} style={{ marginBottom: '0.35rem' }}>
|
||
<Link to={`/planning/run/${u.id}`} style={{ fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||
{(u.actual_date || u.planned_date || '').toString().slice(0, 10) || 'Datum'}
|
||
</Link>
|
||
{u.group_name ? (
|
||
<span style={{ color: 'var(--text3)' }}>{` — ${u.group_name}`}</span>
|
||
) : null}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', margin: 0 }}>Noch keine abgeschlossenen Einheiten in der Kurzliste.</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Status Grid */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 260px), 1fr))',
|
||
gap: '1rem',
|
||
marginBottom: '1.5rem'
|
||
}}>
|
||
<div className="card">
|
||
<h3>✅ Fertig</h3>
|
||
<ul style={{ marginTop: '1rem', paddingLeft: '1.5rem' }}>
|
||
<li>Backend-Basis</li>
|
||
<li>Datenbank-Schema</li>
|
||
<li>Auth-System</li>
|
||
<li>Login & Registrierung</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="card">
|
||
<h3>🚧 In Arbeit</h3>
|
||
<ul style={{ marginTop: '1rem', paddingLeft: '1.5rem' }}>
|
||
<li>Übungsverwaltung</li>
|
||
<li>Trainingsplanung</li>
|
||
<li>Kataloge (Skills, Methods)</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="card">
|
||
<h3>📋 Geplant</h3>
|
||
<ul style={{ marginTop: '1rem', paddingLeft: '1.5rem' }}>
|
||
<li>MediaWiki-Import</li>
|
||
<li>Trainingsprogramme</li>
|
||
<li>Admin-Panel</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
{/* System Info */}
|
||
{version && (
|
||
<div className="card">
|
||
<h3>System-Information</h3>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '150px 1fr', gap: '0.5rem', marginTop: '1rem' }}>
|
||
<strong>Version:</strong>
|
||
<span>{version.app_version}</span>
|
||
|
||
<strong>Build:</strong>
|
||
<span>{version.build_date}</span>
|
||
|
||
<strong>Umgebung:</strong>
|
||
<span>{version.environment}</span>
|
||
|
||
<strong>DB Schema:</strong>
|
||
<span>{version.db_schema_version}</span>
|
||
|
||
<strong>Dein Tier:</strong>
|
||
<span style={{
|
||
padding: '0.25rem 0.5rem',
|
||
background: profile?.tier === 'premium' ? 'var(--accent)' : 'var(--surface2)',
|
||
color: profile?.tier === 'premium' ? 'white' : 'var(--text1)',
|
||
borderRadius: '4px',
|
||
display: 'inline-block'
|
||
}}>
|
||
{profile?.tier || 'free'}
|
||
</span>
|
||
|
||
<strong>Rolle:</strong>
|
||
<span>{profile?.role || 'user'}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Dashboard
|