feat: enhance dashboard and exercises list with new filtering and UI components
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 23s
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 23s
- Added a new filter option in the list_exercises function to allow users to view exercises created by the current profile. - Introduced new CSS styles for the dashboard, including KPI tiles and training previews, improving layout and user experience. - Updated the Dashboard component to fetch and display statistics related to user-created exercises, enhancing visibility of personal metrics. - Enhanced the ExercisesListPage to support filtering based on user-specific exercise creation, improving data relevance for users.
This commit is contained in:
parent
3210796139
commit
3bf0af001a
|
|
@ -1009,6 +1009,10 @@ def list_exercises(
|
|||
default=False,
|
||||
description="Archivierte einbeziehen; Standard false (außer Statusfilter enthält archived)",
|
||||
),
|
||||
created_by_me: bool = Query(
|
||||
default=False,
|
||||
description="Nur Übungen, die vom aktuellen Profil angelegt wurden (created_by = Profil)",
|
||||
),
|
||||
tenant: TenantContext = Depends(get_tenant_context),
|
||||
):
|
||||
"""
|
||||
|
|
@ -1036,6 +1040,10 @@ def list_exercises(
|
|||
where.append(vis_sql)
|
||||
params.extend(vis_params)
|
||||
|
||||
if created_by_me:
|
||||
where.append("e.created_by = %s")
|
||||
params.append(profile_id)
|
||||
|
||||
vis_list = _merge_str_any(visibility_any, visibility)
|
||||
if vis_list:
|
||||
ph = ",".join(["%s"] * len(vis_list))
|
||||
|
|
|
|||
|
|
@ -3595,6 +3595,199 @@ a.analysis-split__nav-item {
|
|||
}
|
||||
}
|
||||
|
||||
/* Dashboard Phase 0: KPI-Kacheln + Trainingsvorschau */
|
||||
.dashboard-phase0-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 156px), 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard-phase0-kpis__err {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--danger);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dashboard-phase0-kpis__loading {
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dashboard-kpi-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
padding: 14px 14px 12px;
|
||||
background: linear-gradient(165deg, var(--surface2) 0%, var(--surface) 100%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset;
|
||||
transition: border-color 0.15s, box-shadow 0.15s, transform 0.12s;
|
||||
min-height: 112px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dashboard-kpi-card:hover {
|
||||
border-color: var(--border2);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dashboard-kpi-card--static {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dashboard-kpi-card--static:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.dashboard-kpi-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: var(--accent-light);
|
||||
color: var(--accent-dark);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.dashboard-kpi-card__value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
.dashboard-kpi-card__label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.dashboard-kpi-card__hint {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text3);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.dashboard-training-preview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.dashboard-preview-card__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
.dashboard-preview-card__list {
|
||||
margin: 0;
|
||||
padding-left: 1.15rem;
|
||||
color: var(--text2);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.dashboard-preview-card__list--notes {
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dashboard-preview-card__list li {
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.dashboard-preview-card__link {
|
||||
font-weight: 600;
|
||||
color: var(--accent-dark);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dashboard-preview-card__link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dashboard-preview-card__meta {
|
||||
color: var(--text3);
|
||||
}
|
||||
|
||||
.dashboard-preview-card__sub {
|
||||
display: block;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text3);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.dashboard-preview-card__note-snippet {
|
||||
margin-top: 5px;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.dashboard-preview-card__empty {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.dashboard-preview-card__empty a {
|
||||
color: var(--accent-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-preview-card__err {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.dashboard-sys-card__title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-sys-card__grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 120px) 1fr;
|
||||
gap: 0.5rem 1rem;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dashboard-sys-card__pill {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: var(--surface2);
|
||||
color: var(--text1);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dashboard-sys-card__pill--accent {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* --- Übungen: Rich-Text & Kacheln --- */
|
||||
.rich-text-editor-wrap {
|
||||
border: 1px solid var(--border);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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'
|
||||
|
|
@ -11,12 +12,19 @@ function unitWhenLabel(u) {
|
|||
return bits.length ? bits.join(' · ') : 'Termin'
|
||||
}
|
||||
|
||||
function formatCappedCount(n, capped) {
|
||||
if (capped && n >= 1) return `${n}+`
|
||||
return String(n)
|
||||
}
|
||||
|
||||
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 [phase0Stats, setPhase0Stats] = useState(null)
|
||||
const [phase0Err, setPhase0Err] = useState(null)
|
||||
const { user } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -40,21 +48,21 @@ function Dashboard() {
|
|||
status: 'planned',
|
||||
start_date: today,
|
||||
sort: 'asc',
|
||||
limit: 8
|
||||
limit: 8,
|
||||
}),
|
||||
api.listTrainingUnits({
|
||||
assigned_to_me: true,
|
||||
status: 'completed',
|
||||
sort: 'desc',
|
||||
limit: 6
|
||||
limit: 6,
|
||||
}),
|
||||
api.listTrainingUnits({
|
||||
assigned_to_me: true,
|
||||
status: 'planned',
|
||||
start_date: today,
|
||||
sort: 'asc',
|
||||
limit: 40
|
||||
})
|
||||
limit: 40,
|
||||
}),
|
||||
])
|
||||
const noteHits = (plannedPool || []).filter((u) => {
|
||||
const tn = (u.trainer_notes || '').trim()
|
||||
|
|
@ -65,7 +73,7 @@ function Dashboard() {
|
|||
setTrainingHome({
|
||||
upcoming: Array.isArray(upcomingRaw) ? upcomingRaw : [],
|
||||
recent: Array.isArray(recentRaw) ? recentRaw : [],
|
||||
plannedWithNotes: noteHits
|
||||
plannedWithNotes: noteHits,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -81,12 +89,58 @@ function Dashboard() {
|
|||
}
|
||||
}, [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) {
|
||||
setPhase0Stats({
|
||||
year,
|
||||
draftCount: Array.isArray(draftList) ? draftList.length : 0,
|
||||
draftCapped: Array.isArray(draftList) && draftList.length >= 100,
|
||||
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 [versionData, profileData] = await Promise.all([
|
||||
api.getVersion(),
|
||||
api.getCurrentProfile()
|
||||
])
|
||||
const [versionData, profileData] = await Promise.all([api.getVersion(), api.getCurrentProfile()])
|
||||
setVersion(versionData)
|
||||
setProfile(profileData)
|
||||
} catch (err) {
|
||||
|
|
@ -105,6 +159,9 @@ function Dashboard() {
|
|||
)
|
||||
}
|
||||
|
||||
const draftsHref = '/exercises?status=draft&mine=1'
|
||||
const mineHref = '/exercises?mine=1'
|
||||
|
||||
return (
|
||||
<div className="app-page dashboard-page">
|
||||
<div className="dashboard-greeting">
|
||||
|
|
@ -113,142 +170,204 @@ function Dashboard() {
|
|||
Dashboard
|
||||
</h1>
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
Willkommen, {user?.name || user?.email}! Shinkan unterstützt dich bei Übungen, Planung und Vereinsstruktur.
|
||||
Willkommen, {user?.name || user?.email}! Shinkan unterstützt dich bei Übungen, Planung und
|
||||
Vereinsstruktur.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{profile && <EmailVerificationBanner profile={profile} />}
|
||||
|
||||
{user?.id && (
|
||||
<div
|
||||
className="dashboard-training-grid"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 280px), 1fr))',
|
||||
gap: '1rem',
|
||||
alignItems: 'stretch',
|
||||
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, bei denen du als Leitung oder Co-Trainer dieser Einheit eingetragen
|
||||
bist. Unter{' '}
|
||||
<Link to="/planning" style={{ color: 'var(--accent-dark)' }}>
|
||||
Trainingsplanung
|
||||
</Link>{' '}
|
||||
kannst du Zeiträume und Zuordnungen bearbeiten.
|
||||
{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>
|
||||
<div className="dashboard-phase0-kpis">
|
||||
{phase0Err ? (
|
||||
<p className="dashboard-phase0-kpis__err" role="alert">
|
||||
{phase0Err}
|
||||
</p>
|
||||
) : null}
|
||||
{!phase0Err && phase0Stats ? (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
) : !phase0Err ? (
|
||||
<div className="dashboard-phase0-kpis__loading muted">Zahlen werden geladen…</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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)' }}>
|
||||
<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 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>
|
||||
{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 style={{ color: 'var(--text2)', fontSize: '0.9rem', margin: 0 }}>
|
||||
Keine Einträge mit Allgemein‑ oder Trainer‑Notizen in deinen nächsten geplanten Terminen.
|
||||
</p>
|
||||
)}
|
||||
))}
|
||||
</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">Rückschau</h3>
|
||||
{trainingHomeErr ? (
|
||||
<p className="dashboard-preview-card__err">{trainingHomeErr}</p>
|
||||
) : trainingHome?.recent?.length ? (
|
||||
<ul className="dashboard-preview-card__list">
|
||||
{trainingHome.recent.map((u) => (
|
||||
<li key={`r-${u.id}`}>
|
||||
<Link to={`/planning/run/${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">Noch keine Einträge in der Kurzliste.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
{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'
|
||||
}}>
|
||||
{version ? (
|
||||
<div className="card dashboard-sys-card">
|
||||
<h3 className="dashboard-sys-card__title">System</h3>
|
||||
<div className="dashboard-sys-card__grid">
|
||||
<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>
|
||||
<span
|
||||
className={
|
||||
profile?.tier === 'premium' ? 'dashboard-sys-card__pill dashboard-sys-card__pill--accent' : 'dashboard-sys-card__pill'
|
||||
}
|
||||
>
|
||||
{profile?.tier || 'free'}
|
||||
</span>
|
||||
|
||||
<strong>Rolle:</strong>
|
||||
<span>{profile?.role || 'user'}</span>
|
||||
</div>
|
||||
</span>
|
||||
<strong>Rolle</strong>
|
||||
<span>{profile?.role || 'user'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,10 +123,32 @@ function levelOptionShort(levelStr) {
|
|||
return o ? String(o.level) : String(levelStr)
|
||||
}
|
||||
|
||||
function exerciseListFiltersWithDashboardUrl(mergedFilters) {
|
||||
try {
|
||||
const sp = new URLSearchParams(window.location.search)
|
||||
if (sp.get('status') !== 'draft') return mergedFilters
|
||||
return {
|
||||
...mergedFilters,
|
||||
status_rules: [{ key: 'url-dashboard-draft', id: 'draft', mode: 'require' }],
|
||||
}
|
||||
} catch {
|
||||
return mergedFilters
|
||||
}
|
||||
}
|
||||
|
||||
function ExercisesListPage() {
|
||||
const { user, checkAuth } = useAuth()
|
||||
const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
||||
|
||||
const [mineOnly, setMineOnly] = useState(() => {
|
||||
try {
|
||||
const sp = new URLSearchParams(window.location.search)
|
||||
return sp.get('mine') === '1' || sp.get('created_by_me') === '1'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const [exercises, setExercises] = useState([])
|
||||
const [catalogs, setCatalogs] = useState({
|
||||
focusAreas: [],
|
||||
|
|
@ -169,7 +191,14 @@ function ExercisesListPage() {
|
|||
useEffect(() => {
|
||||
if (!user?.id) return
|
||||
if (prefsAppliedRef.current) return
|
||||
setFilters(mergeExerciseListPrefsFromApi(user.exercise_list_prefs))
|
||||
const merged = mergeExerciseListPrefsFromApi(user.exercise_list_prefs)
|
||||
setFilters(exerciseListFiltersWithDashboardUrl(merged))
|
||||
try {
|
||||
const sp = new URLSearchParams(window.location.search)
|
||||
if (sp.get('mine') === '1' || sp.get('created_by_me') === '1') setMineOnly(true)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
prefsAppliedRef.current = true
|
||||
}, [user?.id, user?.exercise_list_prefs])
|
||||
|
||||
|
|
@ -445,8 +474,9 @@ function ExercisesListPage() {
|
|||
if (filters.include_archived) q.include_archived = true
|
||||
if (debouncedSearch) q.search = debouncedSearch
|
||||
if (debouncedAiSearch) q.ai_search = debouncedAiSearch
|
||||
if (mineOnly) q.created_by_me = true
|
||||
return q
|
||||
}, [filters, debouncedSearch, debouncedAiSearch])
|
||||
}, [filters, debouncedSearch, debouncedAiSearch, mineOnly])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIds(new Set())
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user