mitai-jinkendo/frontend/src/components/DashboardStatKit.jsx
Lars 3d498d03c1
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s
feat: Enhance dashboard widget configuration and introduce new widgets
- 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.
2026-04-07 14:19:45 +02:00

135 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,62,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>
)
}