- Updated the dashboard layout schema to include new widgets: DashboardGreeting, QuickWeightToday, BodyStatStrip, StatusPills, ProfileGoalsProgress, TrendKcalWeight, NutritionActivitySummary, RecoverySleepRest, and TrainingTypeDistribution. - Improved widget configuration validation to support new features, including chart days for trend and distribution widgets. - Refactored the default lab layout to align with the updated widget catalog and ensure proper default activation. - Bumped app_dashboard version to 1.6.0 to reflect the addition of new widgets and configuration enhancements.
135 lines
4.3 KiB
JavaScript
135 lines
4.3 KiB
JavaScript
import { useState } from 'react'
|
||
import {
|
||
clampTileSpan,
|
||
DASHBOARD_TILE_GRID_COLS,
|
||
} from '../utils/dashboardLayout'
|
||
|
||
export const PILL_TOOLTIPS = {
|
||
WHR: 'Waist-Hip-Ratio: Taille ÷ Hüfte. Maß für Bauchfettverteilung. Ziel: <0,90 (M) / <0,85 (F)',
|
||
WHtR: 'Waist-to-Height-Ratio: Taille ÷ Körpergröße. Gesündestest Maß: Ziel unter 0,50.',
|
||
KF: 'Körperfettanteil in Prozent (aus Caliper-Messung).',
|
||
'Protein Ø7T':
|
||
'Durchschnittliche tägliche Proteinaufnahme der letzten 7 Tage vs. Zielbereich (1,6–2,2g/kg KG).',
|
||
}
|
||
|
||
export function Pill({ label, value, status, sub }) {
|
||
const [tip, setTip] = useState(false)
|
||
const color = status === 'good' ? 'var(--accent)' : status === 'warn' ? 'var(--warn)' : '#D85A30'
|
||
const bg =
|
||
status === 'good'
|
||
? 'var(--accent-light)'
|
||
: status === 'warn'
|
||
? 'var(--warn-bg)'
|
||
: '#FCEBEB'
|
||
const tipText = PILL_TOOLTIPS[label]
|
||
return (
|
||
<div style={{ position: 'relative' }}>
|
||
<div
|
||
role={tipText ? 'button' : undefined}
|
||
onClick={() => tipText && setTip((s) => !s)}
|
||
onKeyDown={(e) => tipText && e.key === 'Enter' && setTip((s) => !s)}
|
||
tabIndex={tipText ? 0 : undefined}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 5,
|
||
padding: '5px 10px',
|
||
borderRadius: 20,
|
||
background: bg,
|
||
border: `1px solid ${color}44`,
|
||
cursor: tipText ? 'help' : 'default',
|
||
}}
|
||
>
|
||
<div style={{ width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 }} />
|
||
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text2)' }}>{label}</span>
|
||
<span style={{ fontSize: 12, fontWeight: 700, color }}>{value}</span>
|
||
{sub && <span style={{ fontSize: 10, color: 'var(--text3)' }}>{sub}</span>}
|
||
{tipText && (
|
||
<span style={{ fontSize: 10, color: 'var(--text3)', opacity: 0.7 }} aria-hidden>
|
||
ⓘ
|
||
</span>
|
||
)}
|
||
</div>
|
||
{tip && tipText && (
|
||
<div
|
||
role="tooltip"
|
||
onClick={() => setTip(false)}
|
||
style={{
|
||
position: 'absolute',
|
||
bottom: '110%',
|
||
left: 0,
|
||
zIndex: 50,
|
||
background: 'var(--surface)',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: 8,
|
||
padding: '8px 10px',
|
||
fontSize: 11,
|
||
color: 'var(--text2)',
|
||
minWidth: 200,
|
||
maxWidth: 260,
|
||
lineHeight: 1.5,
|
||
boxShadow: '0 4px 16px rgba(0,0,0,0.15)',
|
||
}}
|
||
>
|
||
<strong>{label}</strong>
|
||
<br />
|
||
{tipText}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* KPI-Kachel (Dashboard-Raster).
|
||
*/
|
||
export function StatCard({
|
||
icon,
|
||
label,
|
||
value,
|
||
unit,
|
||
delta,
|
||
deltaGoodWhenNeg = false,
|
||
sub,
|
||
onClick,
|
||
color,
|
||
spanMobile = 1,
|
||
spanDesktop = 1,
|
||
}) {
|
||
const deltaColor =
|
||
delta == null
|
||
? null
|
||
: (deltaGoodWhenNeg ? delta < 0 : delta > 0)
|
||
? 'var(--accent)'
|
||
: 'var(--warn)'
|
||
const sm = clampTileSpan(spanMobile, DASHBOARD_TILE_GRID_COLS.mobile)
|
||
const lg = clampTileSpan(spanDesktop, DASHBOARD_TILE_GRID_COLS.desktop)
|
||
return (
|
||
<div
|
||
className="dashboard-stat-card"
|
||
onClick={onClick}
|
||
style={{
|
||
cursor: onClick ? 'pointer' : 'default',
|
||
'--tile-sm': String(sm),
|
||
'--tile-lg': String(lg),
|
||
}}
|
||
onMouseEnter={(e) => onClick && (e.currentTarget.style.borderColor = 'var(--accent)')}
|
||
onMouseLeave={(e) => onClick && (e.currentTarget.style.borderColor = 'var(--border)')}
|
||
>
|
||
<div style={{ fontSize: 18, marginBottom: 4 }}>{icon}</div>
|
||
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 2 }}>{label}</div>
|
||
<div style={{ fontSize: 19, fontWeight: 700, color: color || 'var(--text1)', lineHeight: 1.1 }}>
|
||
{value}
|
||
<span style={{ fontSize: 12, fontWeight: 400, color: 'var(--text3)', marginLeft: 2 }}>{unit}</span>
|
||
</div>
|
||
{delta != null && (
|
||
<div style={{ fontSize: 11, fontWeight: 600, color: deltaColor, marginTop: 2 }}>
|
||
{delta > 0 ? '+' : ''}
|
||
{delta} {unit}
|
||
</div>
|
||
)}
|
||
{sub && <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 2 }}>{sub}</div>}
|
||
</div>
|
||
)
|
||
}
|