- 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.
114 lines
3.3 KiB
JavaScript
114 lines
3.3 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { Check } from 'lucide-react'
|
|
import dayjs from 'dayjs'
|
|
import { api } from '../utils/api'
|
|
|
|
/**
|
|
* Tagesgewicht erfassen (wie Dashboard „Gewicht heute“).
|
|
*/
|
|
export default function QuickWeightEntry({ onSaved }) {
|
|
const [input, setInput] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
const [saved, setSaved] = useState(false)
|
|
const [error, setError] = useState(null)
|
|
const [weightUsage, setWeightUsage] = useState(null)
|
|
const today = dayjs().format('YYYY-MM-DD')
|
|
|
|
const loadUsage = () => {
|
|
api
|
|
.getFeatureUsage()
|
|
.then((features) => {
|
|
const weightFeature = features.find((f) => f.feature_id === 'weight_entries')
|
|
setWeightUsage(weightFeature)
|
|
})
|
|
.catch((err) => console.error('Failed to load usage:', err))
|
|
}
|
|
|
|
useEffect(() => {
|
|
api.weightStats().then((s) => {
|
|
if (s?.latest?.date === today) setInput(String(s.latest.weight))
|
|
})
|
|
loadUsage()
|
|
}, [today])
|
|
|
|
const handleSave = async () => {
|
|
const w = parseFloat(input)
|
|
if (!w || w < 20 || w > 300) return
|
|
setSaving(true)
|
|
setError(null)
|
|
try {
|
|
await api.upsertWeight(today, w)
|
|
setSaved(true)
|
|
await loadUsage()
|
|
onSaved?.()
|
|
setTimeout(() => setSaved(false), 2000)
|
|
} catch (err) {
|
|
console.error('Save failed:', err)
|
|
setError(err.message || 'Fehler beim Speichern')
|
|
setTimeout(() => setError(null), 5000)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const isDisabled = saving || !input || (weightUsage && !weightUsage.allowed)
|
|
const tooltipText =
|
|
weightUsage && !weightUsage.allowed
|
|
? `Limit erreicht (${weightUsage.used}/${weightUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.`
|
|
: ''
|
|
|
|
return (
|
|
<div>
|
|
{error && (
|
|
<div
|
|
style={{
|
|
padding: '8px 10px',
|
|
background: 'var(--danger-bg)',
|
|
border: '1px solid var(--danger)',
|
|
borderRadius: 8,
|
|
fontSize: 12,
|
|
color: 'var(--danger)',
|
|
marginBottom: 8,
|
|
}}
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
<input
|
|
type="number"
|
|
min={20}
|
|
max={300}
|
|
step={0.1}
|
|
className="form-input"
|
|
style={{ flex: 1, fontSize: 17, fontWeight: 600, textAlign: 'center' }}
|
|
placeholder="kg eingeben"
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && !isDisabled && handleSave()}
|
|
/>
|
|
<span style={{ fontSize: 13, color: 'var(--text3)' }}>kg</span>
|
|
<div title={tooltipText} style={{ display: 'inline-block' }}>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
style={{ padding: '8px 14px', cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
|
onClick={handleSave}
|
|
disabled={isDisabled}
|
|
>
|
|
{saved ? (
|
|
<Check size={15} />
|
|
) : saving ? (
|
|
<div className="spinner" style={{ width: 14, height: 14 }} />
|
|
) : weightUsage && !weightUsage.allowed ? (
|
|
'🔒 Limit'
|
|
) : (
|
|
'Speichern'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|