refactor: update fitness dashboard integration and terminology
- 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:
parent
b5c5f2f612
commit
22c5f695c9
|
|
@ -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** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }}>
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user