feat: extend VitalsPage with all new vital parameters
Form sections: - Morgenmessung: Ruhepuls, HRV - Blutdruck (Omron): Systolisch, Diastolisch, Puls - Fitness & Sauerstoff (Apple Watch): VO2 Max, SpO2, Atemfrequenz - Warnungen: Unregelmäßiger Herzschlag, Mögliches AFib (checkboxes) Display: - All vitals shown in entry list with icons - Blood pressure highlighted in red (🩸) - VO2 Max in green (🏃) - Warnings in orange (⚠️) Stats overview: - Dynamic grid showing available metrics - Avg blood pressure 7d - Latest VO2 Max - Avg SpO2 7d Save/Update: - Only non-empty fields included in payload - At least one vital must be provided Ready for manual testing + import implementation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4f53cfffab
commit
9634ca8909
|
|
@ -10,6 +10,14 @@ function empty() {
|
|||
date: dayjs().format('YYYY-MM-DD'),
|
||||
resting_hr: '',
|
||||
hrv: '',
|
||||
blood_pressure_systolic: '',
|
||||
blood_pressure_diastolic: '',
|
||||
pulse: '',
|
||||
vo2_max: '',
|
||||
spo2: '',
|
||||
respiratory_rate: '',
|
||||
irregular_heartbeat: false,
|
||||
possible_afib: false,
|
||||
note: ''
|
||||
}
|
||||
}
|
||||
|
|
@ -31,8 +39,13 @@ function EntryForm({ form, setForm, onSave, onCancel, saving, saveLabel = 'Speic
|
|||
<span className="form-unit" />
|
||||
</div>
|
||||
|
||||
{/* Section: Morgenmessung */}
|
||||
<div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600, fontSize: 13, color: 'var(--text2)' }}>
|
||||
Morgenmessung (vor dem Aufstehen)
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Ruhepuls *</label>
|
||||
<label className="form-label">Ruhepuls</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
|
|
@ -61,6 +74,126 @@ function EntryForm({ form, setForm, onSave, onCancel, saving, saveLabel = 'Speic
|
|||
<span className="form-unit">ms</span>
|
||||
</div>
|
||||
|
||||
{/* Section: Blutdruck */}
|
||||
<div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600, fontSize: 13, color: 'var(--text2)' }}>
|
||||
Blutdruck (Omron)
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Systolisch</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
min={50}
|
||||
max={250}
|
||||
step={1}
|
||||
placeholder="z.B. 125"
|
||||
value={form.blood_pressure_systolic || ''}
|
||||
onChange={e => set('blood_pressure_systolic', e.target.value)}
|
||||
/>
|
||||
<span className="form-unit">mmHg</span>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Diastolisch</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
min={30}
|
||||
max={150}
|
||||
step={1}
|
||||
placeholder="z.B. 83"
|
||||
value={form.blood_pressure_diastolic || ''}
|
||||
onChange={e => set('blood_pressure_diastolic', e.target.value)}
|
||||
/>
|
||||
<span className="form-unit">mmHg</span>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Puls</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
min={30}
|
||||
max={200}
|
||||
step={1}
|
||||
placeholder="z.B. 61"
|
||||
value={form.pulse || ''}
|
||||
onChange={e => set('pulse', e.target.value)}
|
||||
/>
|
||||
<span className="form-unit">bpm</span>
|
||||
</div>
|
||||
|
||||
{/* Section: Fitness & Sauerstoff */}
|
||||
<div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600, fontSize: 13, color: 'var(--text2)' }}>
|
||||
Fitness & Sauerstoff (Apple Watch)
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">VO2 Max</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
min={10}
|
||||
max={90}
|
||||
step={0.1}
|
||||
placeholder="z.B. 42.5"
|
||||
value={form.vo2_max || ''}
|
||||
onChange={e => set('vo2_max', e.target.value)}
|
||||
/>
|
||||
<span className="form-unit">ml/kg/min</span>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">SpO2</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
min={70}
|
||||
max={100}
|
||||
step={1}
|
||||
placeholder="z.B. 98"
|
||||
value={form.spo2 || ''}
|
||||
onChange={e => set('spo2', e.target.value)}
|
||||
/>
|
||||
<span className="form-unit">%</span>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Atemfrequenz</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
min={1}
|
||||
max={60}
|
||||
step={0.1}
|
||||
placeholder="z.B. 15.5"
|
||||
value={form.respiratory_rate || ''}
|
||||
onChange={e => set('respiratory_rate', e.target.value)}
|
||||
/>
|
||||
<span className="form-unit">/min</span>
|
||||
</div>
|
||||
|
||||
{/* Section: Warnungen */}
|
||||
<div style={{ marginTop: 16, marginBottom: 8 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, marginBottom: 4 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.irregular_heartbeat || false}
|
||||
onChange={e => set('irregular_heartbeat', e.target.checked)}
|
||||
/>
|
||||
Unregelmäßiger Herzschlag festgestellt
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.possible_afib || false}
|
||||
onChange={e => set('possible_afib', e.target.checked)}
|
||||
/>
|
||||
Mögliches Vorhofflimmern (AFib)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Notiz</label>
|
||||
<input
|
||||
|
|
@ -127,10 +260,22 @@ export default function VitalsPage() {
|
|||
// Only include fields if they have values
|
||||
if (form.resting_hr) payload.resting_hr = parseInt(form.resting_hr)
|
||||
if (form.hrv) payload.hrv = parseInt(form.hrv)
|
||||
if (form.blood_pressure_systolic) payload.blood_pressure_systolic = parseInt(form.blood_pressure_systolic)
|
||||
if (form.blood_pressure_diastolic) payload.blood_pressure_diastolic = parseInt(form.blood_pressure_diastolic)
|
||||
if (form.pulse) payload.pulse = parseInt(form.pulse)
|
||||
if (form.vo2_max) payload.vo2_max = parseFloat(form.vo2_max)
|
||||
if (form.spo2) payload.spo2 = parseInt(form.spo2)
|
||||
if (form.respiratory_rate) payload.respiratory_rate = parseFloat(form.respiratory_rate)
|
||||
if (form.irregular_heartbeat) payload.irregular_heartbeat = form.irregular_heartbeat
|
||||
if (form.possible_afib) payload.possible_afib = form.possible_afib
|
||||
if (form.note) payload.note = form.note
|
||||
|
||||
if (!payload.resting_hr && !payload.hrv) {
|
||||
setError('Mindestens Ruhepuls oder HRV muss angegeben werden')
|
||||
// Check if at least one vital is provided
|
||||
const hasData = payload.resting_hr || payload.hrv || payload.blood_pressure_systolic ||
|
||||
payload.blood_pressure_diastolic || payload.vo2_max || payload.spo2 ||
|
||||
payload.respiratory_rate
|
||||
if (!hasData) {
|
||||
setError('Mindestens ein Vitalwert muss angegeben werden')
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
|
|
@ -159,6 +304,14 @@ export default function VitalsPage() {
|
|||
if (editing.date) payload.date = editing.date
|
||||
if (editing.resting_hr) payload.resting_hr = parseInt(editing.resting_hr)
|
||||
if (editing.hrv) payload.hrv = parseInt(editing.hrv)
|
||||
if (editing.blood_pressure_systolic) payload.blood_pressure_systolic = parseInt(editing.blood_pressure_systolic)
|
||||
if (editing.blood_pressure_diastolic) payload.blood_pressure_diastolic = parseInt(editing.blood_pressure_diastolic)
|
||||
if (editing.pulse) payload.pulse = parseInt(editing.pulse)
|
||||
if (editing.vo2_max) payload.vo2_max = parseFloat(editing.vo2_max)
|
||||
if (editing.spo2) payload.spo2 = parseInt(editing.spo2)
|
||||
if (editing.respiratory_rate) payload.respiratory_rate = parseFloat(editing.respiratory_rate)
|
||||
if (editing.irregular_heartbeat !== undefined) payload.irregular_heartbeat = editing.irregular_heartbeat
|
||||
if (editing.possible_afib !== undefined) payload.possible_afib = editing.possible_afib
|
||||
if (editing.note) payload.note = editing.note
|
||||
|
||||
await api.updateVitals(editing.id, payload)
|
||||
|
|
@ -206,36 +359,73 @@ export default function VitalsPage() {
|
|||
{/* Stats Overview */}
|
||||
{stats && stats.total_entries > 0 && (
|
||||
<div className="card section-gap">
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', gap: 8 }}>
|
||||
{stats.avg_resting_hr_7d && (
|
||||
<div style={{
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 10px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: '#378ADD' }}>
|
||||
{stats.avg_resting_hr_7d ? Math.round(stats.avg_resting_hr_7d) : '—'}
|
||||
{Math.round(stats.avg_resting_hr_7d)}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Ø Ruhepuls 7d</div>
|
||||
</div>
|
||||
)}
|
||||
{stats.avg_hrv_7d && (
|
||||
<div style={{
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 10px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: '#1D9E75' }}>
|
||||
{stats.avg_hrv_7d ? Math.round(stats.avg_hrv_7d) : '—'}
|
||||
{Math.round(stats.avg_hrv_7d)}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Ø HRV 7d</div>
|
||||
</div>
|
||||
)}
|
||||
{stats.avg_bp_systolic_7d && (
|
||||
<div style={{
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 10px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: '#E74C3C' }}>
|
||||
{Math.round(stats.avg_bp_systolic_7d)}/{Math.round(stats.avg_bp_diastolic_7d || 0)}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Ø Blutdruck 7d</div>
|
||||
</div>
|
||||
)}
|
||||
{stats.latest_vo2_max && (
|
||||
<div style={{
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 10px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: '#1D9E75' }}>
|
||||
{stats.latest_vo2_max.toFixed(1)}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text3)' }}>VO2 Max</div>
|
||||
</div>
|
||||
)}
|
||||
{stats.avg_spo2_7d && (
|
||||
<div style={{
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 10px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: '#7B68EE' }}>
|
||||
{Math.round(stats.avg_spo2_7d)}%
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Ø SpO2 7d</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 10px',
|
||||
|
|
@ -377,9 +567,39 @@ export default function VitalsPage() {
|
|||
📊 HRV {e.hrv} ms
|
||||
</span>
|
||||
)}
|
||||
{e.blood_pressure_systolic && e.blood_pressure_diastolic && (
|
||||
<span style={{ fontSize: 12, color: '#E74C3C', fontWeight: 600 }}>
|
||||
🩸 {e.blood_pressure_systolic}/{e.blood_pressure_diastolic} mmHg
|
||||
</span>
|
||||
)}
|
||||
{e.pulse && (
|
||||
<span style={{ fontSize: 12, color: 'var(--text2)' }}>
|
||||
💓 {e.pulse} bpm
|
||||
</span>
|
||||
)}
|
||||
{e.vo2_max && (
|
||||
<span style={{ fontSize: 12, color: '#1D9E75', fontWeight: 600 }}>
|
||||
🏃 VO2 {e.vo2_max}
|
||||
</span>
|
||||
)}
|
||||
{e.spo2 && (
|
||||
<span style={{ fontSize: 12, color: 'var(--text2)' }}>
|
||||
🫁 SpO2 {e.spo2}%
|
||||
</span>
|
||||
)}
|
||||
{e.respiratory_rate && (
|
||||
<span style={{ fontSize: 12, color: 'var(--text2)' }}>
|
||||
💨 {e.respiratory_rate}/min
|
||||
</span>
|
||||
)}
|
||||
{(e.irregular_heartbeat || e.possible_afib) && (
|
||||
<span style={{ fontSize: 12, color: '#D85A30', fontWeight: 600 }}>
|
||||
⚠️ {e.irregular_heartbeat ? 'Unregelmäßig' : ''} {e.possible_afib ? 'AFib?' : ''}
|
||||
</span>
|
||||
)}
|
||||
{e.source !== 'manual' && (
|
||||
<span style={{ fontSize: 10, color: 'var(--text3)' }}>
|
||||
{e.source === 'apple_health' ? 'Apple Health' : e.source}
|
||||
{e.source === 'apple_health' ? 'Apple Health' : e.source === 'omron' ? 'Omron' : e.source}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user