refactor: update fitness dashboard integration and terminology
All checks were successful
Deploy Development / deploy (push) Successful in 54s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

- Changed the fitness overview path from `/activity` to `/history` and updated related navigation labels to reflect this change.
- Refactored the `FitnessDashboardOverview` component to accept external period control and conditionally display the period selector.
- Integrated the `FitnessDashboardOverview` into the `History` page, enhancing the user experience with consistent terminology and layout.
- Removed the fitness overview from the `ActivityPage` to streamline the interface and focus on activity data collection.
This commit is contained in:
Lars 2026-04-19 21:37:12 +02:00
parent b5c5f2f612
commit 22c5f695c9
5 changed files with 60 additions and 35 deletions

View File

@ -8,7 +8,7 @@
## Ziel ## Ziel
- Eine **Fitness-Übersicht** auf `/activity` (Capture-Hub: „Fitness“), die **keine parallelen Berechnungen** im Client führt. - Eine **Fitness-Übersicht** auf **`/history`** (Tab Fitness), analog Körper/Ernährung — **keine parallelen Berechnungen** im Client für Layer 2b.
- **Single Source of Truth:** `data_layer/activity_metrics` (und Scores/Focus wie bei den Platzhaltern), identische Chart-Payloads wie die bestehenden Chart-Endpunkte A1/A2. - **Single Source of Truth:** `data_layer/activity_metrics` (und Scores/Focus wie bei den Platzhaltern), identische Chart-Payloads wie die bestehenden Chart-Endpunkte A1/A2.
--- ---
@ -35,8 +35,8 @@
|-------------|------| |-------------|------|
| API-Client | `getFitnessDashboardViz(days)` in `frontend/src/utils/api.js` | | API-Client | `getFitnessDashboardViz(days)` in `frontend/src/utils/api.js` |
| Darstellung | `frontend/src/components/FitnessDashboardOverview.jsx` | | Darstellung | `frontend/src/components/FitnessDashboardOverview.jsx` |
| Einbindung | `frontend/src/pages/ActivityPage.jsx` (oben, vor Tabs) | | Einbindung | `frontend/src/pages/History.jsx` → `ActivitySection` (gemeinsamer `PeriodSelector` wie die Liste darunter) |
| Navigation Capture | `frontend/src/config/captureNav.js` Label **Fitness**, Route `/activity` | | Erfassung | `/activity` bleibt reine Erfassung; Capture-Hub-Label **Aktivität** |
--- ---

View File

@ -22,9 +22,20 @@ const PERIODS = [
/** /**
* Layer 2b: Kennzahlen und Charts nur aus GET /api/charts/fitness-dashboard-viz (activity_metrics). * Layer 2b: Kennzahlen und Charts nur aus GET /api/charts/fitness-dashboard-viz (activity_metrics).
*
* @param {number} [period] gesteuert von außen (z. B. Verlauf `PeriodSelector`); mit `onPeriodChange` koppeln.
* @param {(n: number) => void} [onPeriodChange]
* @param {boolean} [hidePeriodSelector] eigenes Zeitraum-Dropdown ausblenden (wenn die Seite oben schon einen Zeitraum wählt).
*/ */
export default function FitnessDashboardOverview() { export default function FitnessDashboardOverview({
const [period, setPeriod] = useState(28) period: periodProp,
onPeriodChange,
hidePeriodSelector = false,
}) {
const [internalPeriod, setInternalPeriod] = useState(28)
const controlled = periodProp !== undefined && typeof onPeriodChange === 'function'
const period = controlled ? periodProp : internalPeriod
const setPeriod = controlled ? onPeriodChange : setInternalPeriod
const [viz, setViz] = useState(null) const [viz, setViz] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [err, setErr] = useState(null) const [err, setErr] = useState(null)
@ -104,25 +115,31 @@ export default function FitnessDashboardOverview() {
const wUsed = viz.training_volume_weeks_used const wUsed = viz.training_volume_weeks_used
const dTyp = viz.training_type_dist_days_used const dTyp = viz.training_type_dist_days_used
const showPeriodDropdown = !hidePeriodSelector && !controlled
return ( return (
<div className="card section-gap"> <div className="card section-gap">
<div className="card-title" style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 }}> <div className="card-title" style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 }}>
<span>Fitness-Übersicht</span> <span>Fitness-Übersicht</span>
<label style={{ fontSize: 12, fontWeight: 500, color: 'var(--text3)', display: 'flex', alignItems: 'center', gap: 8 }}> {showPeriodDropdown ? (
Zeitraum <label
<select style={{ fontSize: 12, fontWeight: 500, color: 'var(--text3)', display: 'flex', alignItems: 'center', gap: 8 }}
className="form-input"
style={{ maxWidth: 140, padding: '6px 10px', fontSize: 13 }}
value={period}
onChange={(e) => setPeriod(Number(e.target.value))}
> >
{PERIODS.map((p) => ( Zeitraum
<option key={p.v} value={p.v}> <select
{p.label} className="form-input"
</option> style={{ maxWidth: 140, padding: '6px 10px', fontSize: 13 }}
))} value={period}
</select> onChange={(e) => setPeriod(Number(e.target.value))}
</label> >
{PERIODS.map((p) => (
<option key={p.v} value={p.v}>
{p.label}
</option>
))}
</select>
</label>
) : null}
</div> </div>
<p style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 10 }}> <p style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 10 }}>

View File

@ -56,7 +56,7 @@ export const CAPTURE_HUB_TILES = [
}, },
{ {
icon: '🏋️', icon: '🏋️',
label: 'Fitness', label: 'Aktivität',
sub: 'Training manuell oder Apple Health importieren', sub: 'Training manuell oder Apple Health importieren',
to: '/activity', to: '/activity',
color: '#D4537E', color: '#D4537E',

View File

@ -3,7 +3,6 @@ import { Upload, Pencil, Trash2, Check, X, CheckCircle } from 'lucide-react'
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts' import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'
import { api } from '../utils/api' import { api } from '../utils/api'
import UsageBadge from '../components/UsageBadge' import UsageBadge from '../components/UsageBadge'
import FitnessDashboardOverview from '../components/FitnessDashboardOverview'
import TrainingTypeSelect from '../components/TrainingTypeSelect' import TrainingTypeSelect from '../components/TrainingTypeSelect'
import BulkCategorize from '../components/BulkCategorize' import BulkCategorize from '../components/BulkCategorize'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -913,12 +912,7 @@ export default function ActivityPage() {
return ( return (
<div className="capture-page"> <div className="capture-page">
<h1 className="page-title">Fitness</h1> <h1 className="page-title">Aktivität</h1>
<p style={{ fontSize: 12, color: 'var(--text3)', marginTop: -8, marginBottom: 12 }}>
Auswertung (Data-Layer) und Erfassung an einem Ort.
</p>
<FitnessDashboardOverview />
<div className="tabs" style={{overflowX:'auto',flexWrap:'nowrap'}}> <div className="tabs" style={{overflowX:'auto',flexWrap:'nowrap'}}>
<button className={'tab'+(tab==='list'?' active':'')} onClick={()=>setTab('list')}>Verlauf</button> <button className={'tab'+(tab==='list'?' active':'')} onClick={()=>setTab('list')}>Verlauf</button>

View File

@ -14,6 +14,7 @@ import { getStatusColor, getStatusBg } from '../utils/interpret'
import { MACRO_CHART, macroFillByName, NUTRITION_MACRO_CHART_BLOCK_PX } from '../utils/macroChartTheme' import { MACRO_CHART, macroFillByName, NUTRITION_MACRO_CHART_BLOCK_PX } from '../utils/macroChartTheme'
import Markdown from '../utils/Markdown' import Markdown from '../utils/Markdown'
import TrainingTypeDistribution from '../components/TrainingTypeDistribution' import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
import FitnessDashboardOverview from '../components/FitnessDashboardOverview'
import NutritionCharts, { WeeklyMacroDistributionPanel } from '../components/NutritionCharts' import NutritionCharts, { WeeklyMacroDistributionPanel } from '../components/NutritionCharts'
import RecoveryCharts from '../components/RecoveryCharts' import RecoveryCharts from '../components/RecoveryCharts'
import KpiTilesOverview from '../components/KpiTilesOverview' import KpiTilesOverview from '../components/KpiTilesOverview'
@ -1097,16 +1098,14 @@ function NutritionSection({ profile, insights, onRequest, loadingSlug, filterAct
) )
} }
// Activity Section // Activity Section Layer 2b Fitness-Bundle wie Körper/Ernährung auf /history
function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs, globalQualityLevel }) { function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs, globalQualityLevel }) {
const [period, setPeriod] = useState(30) const [period, setPeriod] = useState(30)
if (!activities?.length) return (
<EmptySection text="Noch keine Aktivitätsdaten." to="/activity" toLabel="Fitness erfassen"/>
)
const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD') const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD')
// Issue #31: Backend already filters by global quality level - only filter by period here // Issue #31: Backend already filters by global quality level - only filter by period here
const filtA = activities.filter(d => period === 9999 || d.date >= cutoff) const actList = activities || []
const filtA = actList.filter(d => period === 9999 || d.date >= cutoff)
const byDate={} const byDate={}
filtA.forEach(a=>{ byDate[a.date]=(byDate[a.date]||0)+(a.kcal_active||0) }) filtA.forEach(a=>{ byDate[a.date]=(byDate[a.date]||0)+(a.kcal_active||0) })
@ -1130,13 +1129,24 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA
value:consistency+'%' value:consistency+'%'
}] }]
const hasList = actList.length > 0
return ( return (
<div> <div>
<SectionHeader title="🏋️ Fitness" to="/activity" toLabel="Alle Einträge" lastUpdated={activities[0]?.date}/> <SectionHeader title="🏋️ Fitness" to="/activity" toLabel="Alle Einträge" lastUpdated={actList[0]?.date}/>
<PeriodSelector value={period} onChange={setPeriod}/> <PeriodSelector value={period} onChange={setPeriod}/>
<p style={{ fontSize: 11, color: 'var(--text3)', lineHeight: 1.45, marginBottom: 10 }}>
Fitness-Kennzahlen und Diagramme (Layer 2b) kommen aus dem Aktivitäts-Data-Layer dieselbe Quelle wie die
KI-Platzhalter. Zeitraum gilt auch für die Liste unten.
</p>
<FitnessDashboardOverview period={period} onPeriodChange={setPeriod} hidePeriodSelector />
{!hasList ? (
<EmptySection text="Noch keine Aktivitätsdaten im Verlauf." to="/activity" toLabel="Aktivität erfassen" />
) : null}
{/* Issue #31: Show active global quality filter */} {/* Issue #31: Show active global quality filter */}
{globalQualityLevel && globalQualityLevel !== 'all' && ( {hasList && globalQualityLevel && globalQualityLevel !== 'all' && (
<div style={{ <div style={{
marginBottom:12, padding:'8px 12px', borderRadius:8, marginBottom:12, padding:'8px 12px', borderRadius:8,
background:'var(--surface2)', border:'1px solid var(--border)', background:'var(--surface2)', border:'1px solid var(--border)',
@ -1156,6 +1166,8 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA
</div> </div>
)} )}
{!hasList ? null : (
<>
<div style={{display:'flex',gap:6,marginBottom:12}}> <div style={{display:'flex',gap:6,marginBottom:12}}>
{[['Trainings',filtA.length,'var(--text1)'],['Kcal',totalKcal,'#EF9F27'], {[['Trainings',filtA.length,'var(--text1)'],['Kcal',totalKcal,'#EF9F27'],
['Stunden',Math.round(totalMin/60*10)/10,'#378ADD'], ['Stunden',Math.round(totalMin/60*10)/10,'#378ADD'],
@ -1186,7 +1198,7 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA
<div key={type} style={{display:'flex',alignItems:'center',gap:8,padding:'4px 0',borderBottom:'1px solid var(--border)'}}> <div key={type} style={{display:'flex',alignItems:'center',gap:8,padding:'4px 0',borderBottom:'1px solid var(--border)'}}>
<div style={{flex:1,fontSize:13}}>{type}</div> <div style={{flex:1,fontSize:13}}>{type}</div>
<div style={{fontSize:12,color:'var(--text3)'}}>{count}×</div> <div style={{fontSize:12,color:'var(--text3)'}}>{count}×</div>
<div style={{width:Math.max(4,Math.round(count/filtA.length*80)),height:6,background:'#EF9F2788',borderRadius:3}}/> <div style={{width:Math.max(4,Math.round(count/Math.max(1,filtA.length)*80)),height:6,background:'#EF9F2788',borderRadius:3}}/>
</div> </div>
))} ))}
</div> </div>
@ -1199,6 +1211,8 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA
{actRules.map((item,i)=><RuleCard key={i} item={item}/>)} {actRules.map((item,i)=><RuleCard key={i} item={item}/>)}
</div> </div>
<InsightBox insights={insights} slugs={filterActiveSlugs(['aktivitaet'])} onRequest={onRequest} loading={loadingSlug}/> <InsightBox insights={insights} slugs={filterActiveSlugs(['aktivitaet'])} onRequest={onRequest} loading={loadingSlug}/>
</>
)}
</div> </div>
) )
} }