v9d Phase 2d: Vitals Module Refactoring (Baseline + Blood Pressure) #22

Merged
Lars merged 29 commits from develop into main 2026-03-23 16:27:03 +01:00
Showing only changes of commit 7f10286e02 - Show all commits

View File

@ -1,4 +1,6 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { Pencil, Trash2, X, TrendingUp, TrendingDown, Minus, Upload } from 'lucide-react'
import { api } from '../utils/api'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
@ -6,13 +8,629 @@ dayjs.locale('de')
/**
* VitalsPage - Refactored v9d Phase 2d
*
* Separated into:
* - Tab 1: Morgenmessung (Baseline) - once daily
* - Tab 2: Blutdruck (BP) - multiple daily with context
* - Tab 3: Import
* Separated vitals tracking:
* - Baseline Vitals: Once daily (morning, fasted) - RHR, HRV, VO2 Max, SpO2
* - Blood Pressure: Multiple daily with context - Systolic/Diastolic + context tagging
* - Imports: Omron CSV (BP) + Apple Health CSV (Baseline)
*/
//
// BASELINE VITALS TAB (Once daily, morning)
//
function BaselineTab() {
const [entries, setEntries] = useState([])
const [stats, setStats] = useState(null)
const [form, setForm] = useState({
date: dayjs().format('YYYY-MM-DD'),
resting_hr: '',
hrv: '',
vo2_max: '',
spo2: '',
respiratory_rate: '',
note: ''
})
const [editing, setEditing] = useState(null)
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(false)
const load = async () => {
try {
const [e, s] = await Promise.all([
api.listBaseline(90),
api.getBaselineStats(30)
])
setEntries(e)
setStats(s)
} catch (err) {
setError(err.message)
}
}
useEffect(() => { load() }, [])
const handleSave = async () => {
setSaving(true)
setError(null)
try {
const payload = { date: form.date }
if (form.resting_hr) payload.resting_hr = parseInt(form.resting_hr)
if (form.hrv) payload.hrv = parseInt(form.hrv)
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.note) payload.note = form.note
if (!payload.resting_hr && !payload.hrv && !payload.vo2_max && !payload.spo2 && !payload.respiratory_rate) {
setError('Mindestens ein Wert muss angegeben werden')
setSaving(false)
return
}
await api.createBaseline(payload)
setSuccess(true)
await load()
setTimeout(() => {
setSuccess(false)
setForm({ date: dayjs().format('YYYY-MM-DD'), resting_hr: '', hrv: '', vo2_max: '', spo2: '', respiratory_rate: '', note: '' })
}, 1500)
} catch (err) {
setError(err.message)
} finally {
setSaving(false)
}
}
const handleDelete = async (id) => {
if (!confirm('Eintrag löschen?')) return
try {
await api.deleteBaseline(id)
await load()
} catch (err) {
setError(err.message)
}
}
const getTrendIcon = (trend) => {
if (trend === 'increasing') return <TrendingUp size={14} style={{ color: '#D85A30' }} />
if (trend === 'decreasing') return <TrendingDown size={14} style={{ color: '#1D9E75' }} />
return <Minus size={14} style={{ color: 'var(--text3)' }} />
}
return (
<div>
{/* Stats */}
{stats && stats.total_entries > 0 && (
<div className="card" style={{ marginBottom: 12 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', gap: 8 }}>
{stats.avg_rhr_7d && (
<div style={{ background: 'var(--surface2)', borderRadius: 8, padding: '8px 10px', textAlign: 'center' }}>
<div style={{ fontSize: 18, fontWeight: 700, color: '#378ADD' }}>{Math.round(stats.avg_rhr_7d)}</div>
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Ø RHR 7d</div>
</div>
)}
{stats.avg_hrv_7d && (
<div style={{ background: 'var(--surface2)', borderRadius: 8, padding: '8px 10px', textAlign: 'center' }}>
<div style={{ fontSize: 18, fontWeight: 700, color: '#1D9E75' }}>{Math.round(stats.avg_hrv_7d)}</div>
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Ø HRV 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>
)}
</div>
</div>
)}
{/* Form */}
<div className="card" style={{ marginBottom: 12 }}>
<div className="card-title">Morgenmessung erfassen</div>
<p style={{ fontSize: 12, color: 'var(--text3)', marginBottom: 12 }}>
Einmal täglich, morgens vor dem Aufstehen (nüchtern)
</p>
{error && <div style={{ padding: 10, background: '#FCEBEB', border: '1px solid #D85A30', borderRadius: 8, fontSize: 13, color: '#D85A30', marginBottom: 8 }}>{error}</div>}
<div className="form-row">
<label className="form-label">Datum</label>
<input type="date" className="form-input" style={{ width: 140 }} value={form.date} onChange={e => setForm(f => ({ ...f, date: e.target.value }))} />
<span className="form-unit" />
</div>
<div className="form-row">
<label className="form-label">Ruhepuls</label>
<input type="number" className="form-input" min={30} max={120} placeholder="z.B. 58" value={form.resting_hr} onChange={e => setForm(f => ({ ...f, resting_hr: e.target.value }))} />
<span className="form-unit">bpm</span>
</div>
<div className="form-row">
<label className="form-label">HRV</label>
<input type="number" className="form-input" min={1} max={300} placeholder="optional" value={form.hrv} onChange={e => setForm(f => ({ ...f, hrv: e.target.value }))} />
<span className="form-unit">ms</span>
</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="optional" value={form.vo2_max} onChange={e => setForm(f => ({ ...f, 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} placeholder="optional" value={form.spo2} onChange={e => setForm(f => ({ ...f, 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="optional" value={form.respiratory_rate} onChange={e => setForm(f => ({ ...f, respiratory_rate: e.target.value }))} />
<span className="form-unit">/min</span>
</div>
<div className="form-row">
<label className="form-label">Notiz</label>
<input type="text" className="form-input" placeholder="optional" value={form.note} onChange={e => setForm(f => ({ ...f, note: e.target.value }))} />
<span className="form-unit" />
</div>
<button className="btn btn-primary btn-full" onClick={handleSave} disabled={saving}>
{success ? '✓ Gespeichert!' : saving ? 'Speichere...' : 'Speichern'}
</button>
</div>
{/* List */}
{entries.length > 0 && (
<div>
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>Letzte Messungen ({entries.length})</div>
{entries.map(e => (
<div key={e.id} className="card" style={{ marginBottom: 8, padding: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 4 }}>
{dayjs(e.date).format('dd, DD. MMM YYYY')}
</div>
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', fontSize: 12, color: 'var(--text2)' }}>
{e.resting_hr && <span> {e.resting_hr} bpm</span>}
{e.hrv && <span>📊 HRV {e.hrv} ms</span>}
{e.vo2_max && <span style={{ color: '#1D9E75', fontWeight: 600 }}>🏃 VO2 {e.vo2_max}</span>}
{e.spo2 && <span>🫁 SpO2 {e.spo2}%</span>}
{e.respiratory_rate && <span>💨 {e.respiratory_rate}/min</span>}
</div>
{e.note && <p style={{ fontSize: 12, color: 'var(--text3)', fontStyle: 'italic', marginTop: 4 }}>"{e.note}"</p>}
</div>
<button className="btn btn-danger" style={{ padding: '5px 8px' }} onClick={() => handleDelete(e.id)}>
<Trash2 size={13} />
</button>
</div>
</div>
))}
</div>
)}
</div>
)
}
//
// BLOOD PRESSURE TAB (Multiple daily with context)
//
const CONTEXT_OPTIONS = [
{ value: 'morning_fasted', label: 'Nüchtern (morgens)' },
{ value: 'after_meal', label: 'Nach dem Essen' },
{ value: 'before_training', label: 'Vor dem Training' },
{ value: 'after_training', label: 'Nach dem Training' },
{ value: 'evening', label: 'Abends' },
{ value: 'stress', label: 'Bei Stress' },
{ value: 'resting', label: 'Ruhemessung' },
{ value: 'other', label: 'Sonstiges' },
]
function BloodPressureTab() {
const [entries, setEntries] = useState([])
const [stats, setStats] = useState(null)
const [form, setForm] = useState({
date: dayjs().format('YYYY-MM-DD'),
time: dayjs().format('HH:mm'),
systolic: '',
diastolic: '',
pulse: '',
context: 'morning_fasted',
irregular_heartbeat: false,
possible_afib: false,
note: ''
})
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(false)
const load = async () => {
try {
const [e, s] = await Promise.all([
api.listBloodPressure(90),
api.getBPStats(30)
])
setEntries(e)
setStats(s)
} catch (err) {
setError(err.message)
}
}
useEffect(() => { load() }, [])
const handleSave = async () => {
if (!form.systolic || !form.diastolic) {
setError('Systolisch und Diastolisch sind Pflichtfelder')
return
}
setSaving(true)
setError(null)
try {
const measured_at = `${form.date} ${form.time}:00`
const payload = {
measured_at,
systolic: parseInt(form.systolic),
diastolic: parseInt(form.diastolic),
pulse: form.pulse ? parseInt(form.pulse) : null,
context: form.context,
irregular_heartbeat: form.irregular_heartbeat,
possible_afib: form.possible_afib,
note: form.note || null
}
await api.createBloodPressure(payload)
setSuccess(true)
await load()
setTimeout(() => {
setSuccess(false)
setForm({
date: dayjs().format('YYYY-MM-DD'),
time: dayjs().format('HH:mm'),
systolic: '',
diastolic: '',
pulse: '',
context: 'morning_fasted',
irregular_heartbeat: false,
possible_afib: false,
note: ''
})
}, 1500)
} catch (err) {
setError(err.message)
} finally {
setSaving(false)
}
}
const handleDelete = async (id) => {
if (!confirm('Messung löschen?')) return
try {
await api.deleteBloodPressure(id)
await load()
} catch (err) {
setError(err.message)
}
}
const getBPCategory = (sys, dia) => {
if (sys < 120 && dia < 80) return { label: 'Optimal', color: '#1D9E75' }
if (sys < 130 && dia < 85) return { label: 'Normal', color: '#1D9E75' }
if (sys < 140 && dia < 90) return { label: 'Hochnormal', color: '#EF9F27' }
if (sys < 160 && dia < 100) return { label: 'Hypertonie Grad 1', color: '#D85A30' }
if (sys < 180 && dia < 110) return { label: 'Hypertonie Grad 2', color: '#D85A30' }
return { label: 'Hypertonie Grad 3', color: '#C41E3A' }
}
return (
<div>
{/* Stats */}
{stats && stats.total_measurements > 0 && (
<div className="card" style={{ marginBottom: 12 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: 8 }}>
{stats.avg_systolic && (
<div style={{ background: 'var(--surface2)', borderRadius: 8, padding: '8px 10px', textAlign: 'center' }}>
<div style={{ fontSize: 16, fontWeight: 700, color: '#E74C3C' }}>
{Math.round(stats.avg_systolic)}/{Math.round(stats.avg_diastolic)}
</div>
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Ø Blutdruck 30d</div>
</div>
)}
{stats.bp_category && (
<div style={{ background: 'var(--surface2)', borderRadius: 8, padding: '8px 10px', textAlign: 'center' }}>
<div style={{ fontSize: 12, fontWeight: 600, color: getBPCategory(stats.avg_systolic, stats.avg_diastolic).color }}>
{getBPCategory(stats.avg_systolic, stats.avg_diastolic).label}
</div>
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Kategorie</div>
</div>
)}
<div style={{ background: 'var(--surface2)', borderRadius: 8, padding: '8px 10px', textAlign: 'center' }}>
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--text2)' }}>{stats.total_measurements}</div>
<div style={{ fontSize: 10, color: 'var(--text3)' }}>Messungen</div>
</div>
</div>
</div>
)}
{/* Form */}
<div className="card" style={{ marginBottom: 12 }}>
<div className="card-title">Blutdruck messen</div>
<p style={{ fontSize: 12, color: 'var(--text3)', marginBottom: 12 }}>
Mehrfach täglich mit Kontext-Tagging
</p>
{error && <div style={{ padding: 10, background: '#FCEBEB', border: '1px solid #D85A30', borderRadius: 8, fontSize: 13, color: '#D85A30', marginBottom: 8 }}>{error}</div>}
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<div style={{ flex: 1 }}>
<label className="form-label">Datum</label>
<input type="date" className="form-input" value={form.date} onChange={e => setForm(f => ({ ...f, date: e.target.value }))} />
</div>
<div style={{ flex: 1 }}>
<label className="form-label">Uhrzeit</label>
<input type="time" className="form-input" value={form.time} onChange={e => setForm(f => ({ ...f, time: e.target.value }))} />
</div>
</div>
<div className="form-row">
<label className="form-label">Systolisch</label>
<input type="number" className="form-input" min={50} max={250} placeholder="z.B. 125" value={form.systolic} onChange={e => setForm(f => ({ ...f, 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} placeholder="z.B. 83" value={form.diastolic} onChange={e => setForm(f => ({ ...f, 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} placeholder="optional" value={form.pulse} onChange={e => setForm(f => ({ ...f, pulse: e.target.value }))} />
<span className="form-unit">bpm</span>
</div>
<div className="form-row">
<label className="form-label">Kontext</label>
<select className="form-input" value={form.context} onChange={e => setForm(f => ({ ...f, context: e.target.value }))}>
{CONTEXT_OPTIONS.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
<span className="form-unit" />
</div>
<div style={{ marginBottom: 8 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, marginBottom: 4 }}>
<input type="checkbox" checked={form.irregular_heartbeat} onChange={e => setForm(f => ({ ...f, irregular_heartbeat: e.target.checked }))} />
Unregelmäßiger Herzschlag
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
<input type="checkbox" checked={form.possible_afib} onChange={e => setForm(f => ({ ...f, possible_afib: e.target.checked }))} />
Mögliches Vorhofflimmern (AFib)
</label>
</div>
<div className="form-row">
<label className="form-label">Notiz</label>
<input type="text" className="form-input" placeholder="optional" value={form.note} onChange={e => setForm(f => ({ ...f, note: e.target.value }))} />
<span className="form-unit" />
</div>
<button className="btn btn-primary btn-full" onClick={handleSave} disabled={saving}>
{success ? '✓ Gespeichert!' : saving ? 'Speichere...' : 'Speichern'}
</button>
</div>
{/* List */}
{entries.length > 0 && (
<div>
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8 }}>Letzte Messungen ({entries.length})</div>
{entries.map(e => {
const cat = getBPCategory(e.systolic, e.diastolic)
const ctx = CONTEXT_OPTIONS.find(o => o.value === e.context)
return (
<div key={e.id} className="card" style={{ marginBottom: 8, padding: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 2 }}>
{dayjs(e.measured_at).format('dd, DD. MMM • HH:mm')} Uhr
</div>
<div style={{ fontSize: 16, fontWeight: 700, color: cat.color, marginBottom: 4 }}>
{e.systolic}/{e.diastolic} mmHg
{e.pulse && <span style={{ fontSize: 12, fontWeight: 400, marginLeft: 8 }}>💓 {e.pulse} bpm</span>}
</div>
<div style={{ fontSize: 11, color: 'var(--text3)' }}>
{ctx?.label} · {cat.label}
{(e.irregular_heartbeat || e.possible_afib) && <span style={{ color: '#D85A30', marginLeft: 8 }}> {e.irregular_heartbeat ? 'Unregelmäßig' : ''} {e.possible_afib ? 'AFib?' : ''}</span>}
</div>
{e.note && <p style={{ fontSize: 12, color: 'var(--text3)', fontStyle: 'italic', marginTop: 4 }}>"{e.note}"</p>}
</div>
<button className="btn btn-danger" style={{ padding: '5px 8px' }} onClick={() => handleDelete(e.id)}>
<Trash2 size={13} />
</button>
</div>
</div>
)
})}
</div>
)}
</div>
)
}
//
// IMPORT TAB (CSV imports)
//
function ImportTab({ onImportComplete }) {
const [importingOmron, setImportingOmron] = useState(false)
const [importingApple, setImportingApple] = useState(false)
const [draggingOmron, setDraggingOmron] = useState(false)
const [draggingApple, setDraggingApple] = useState(false)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
const omronRef = useRef(null)
const appleRef = useRef(null)
const handleOmronImport = async (file) => {
if (!file || !file.name.endsWith('.csv')) {
setError('Bitte eine CSV-Datei auswählen')
return
}
setImportingOmron(true)
setResult(null)
setError(null)
try {
const res = await api.importBPOmron(file)
setResult({ type: 'omron', ...res })
if (onImportComplete) onImportComplete()
} catch (err) {
setError('Omron Import fehlgeschlagen: ' + err.message)
} finally {
setImportingOmron(false)
if (omronRef.current) omronRef.current.value = ''
}
}
const handleAppleImport = async (file) => {
if (!file || !file.name.endsWith('.csv')) {
setError('Bitte eine CSV-Datei auswählen')
return
}
setImportingApple(true)
setResult(null)
setError(null)
try {
const res = await api.importBaselineAppleHealth(file)
setResult({ type: 'apple_health', ...res })
if (onImportComplete) onImportComplete()
} catch (err) {
setError('Apple Health Import fehlgeschlagen: ' + err.message)
} finally {
setImportingApple(false)
if (appleRef.current) appleRef.current.value = ''
}
}
return (
<div>
{error && <div style={{ padding: 10, background: '#FCEBEB', border: '1px solid #D85A30', borderRadius: 8, fontSize: 13, color: '#D85A30', marginBottom: 12 }}>{error}</div>}
{result && (
<div style={{ padding: 12, background: '#E8F7F0', border: '1px solid #1D9E75', borderRadius: 8, fontSize: 13, color: '#085041', marginBottom: 12 }}>
<strong>Import erfolgreich ({result.type === 'omron' ? 'Omron' : 'Apple Health'}):</strong><br />
{result.inserted} neu · {result.updated} aktualisiert · {result.skipped} übersprungen
{result.errors > 0 && <> · {result.errors} Fehler</>}
</div>
)}
{/* Omron */}
<div className="card" style={{ marginBottom: 12 }}>
<div className="card-title">Omron Blutdruckmessgerät</div>
<p style={{ fontSize: 12, color: 'var(--text3)', marginBottom: 12 }}>
Exportiere CSV aus der Omron Connect App (Blutdruck + Puls + Warnungen)
</p>
<div
onDragOver={e => { e.preventDefault(); setDraggingOmron(true) }}
onDragLeave={() => setDraggingOmron(false)}
onDrop={e => {
e.preventDefault()
setDraggingOmron(false)
const file = e.dataTransfer.files[0]
if (file) handleOmronImport(file)
}}
onClick={() => omronRef.current?.click()}
style={{
border: `2px dashed ${draggingOmron ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 10,
padding: '20px 16px',
textAlign: 'center',
background: draggingOmron ? 'var(--accent)14' : 'var(--surface2)',
cursor: importingOmron ? 'not-allowed' : 'pointer',
transition: 'all 0.15s',
opacity: importingOmron ? 0.6 : 1
}}>
{importingOmron ? (
<>
<div className="spinner" style={{ width: 24, height: 24, margin: '0 auto 8px' }} />
<div style={{ fontSize: 14, fontWeight: 500, color: 'var(--text2)' }}>Importiere Omron-Daten...</div>
</>
) : (
<>
<Upload size={28} style={{ color: draggingOmron ? 'var(--accent)' : 'var(--text3)', marginBottom: 8 }} />
<div style={{ fontSize: 14, fontWeight: 500, color: draggingOmron ? 'var(--accent-dark)' : 'var(--text2)' }}>
{draggingOmron ? 'CSV loslassen...' : 'Omron CSV hierher ziehen oder tippen'}
</div>
</>
)}
</div>
<input ref={omronRef} type="file" accept=".csv" onChange={e => { const file = e.target.files[0]; if (file) handleOmronImport(file) }} style={{ display: 'none' }} />
</div>
{/* Apple Health */}
<div className="card">
<div className="card-title">Apple Health</div>
<p style={{ fontSize: 12, color: 'var(--text3)', marginBottom: 12 }}>
Exportiere Health-Daten (Ruhepuls, HRV, VO2 Max, SpO2, Atemfrequenz)
</p>
<div
onDragOver={e => { e.preventDefault(); setDraggingApple(true) }}
onDragLeave={() => setDraggingApple(false)}
onDrop={e => {
e.preventDefault()
setDraggingApple(false)
const file = e.dataTransfer.files[0]
if (file) handleAppleImport(file)
}}
onClick={() => appleRef.current?.click()}
style={{
border: `2px dashed ${draggingApple ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 10,
padding: '20px 16px',
textAlign: 'center',
background: draggingApple ? 'var(--accent)14' : 'var(--surface2)',
cursor: importingApple ? 'not-allowed' : 'pointer',
transition: 'all 0.15s',
opacity: importingApple ? 0.6 : 1
}}>
{importingApple ? (
<>
<div className="spinner" style={{ width: 24, height: 24, margin: '0 auto 8px' }} />
<div style={{ fontSize: 14, fontWeight: 500, color: 'var(--text2)' }}>Importiere Apple Health-Daten...</div>
</>
) : (
<>
<Upload size={28} style={{ color: draggingApple ? 'var(--accent)' : 'var(--text3)', marginBottom: 8 }} />
<div style={{ fontSize: 14, fontWeight: 500, color: draggingApple ? 'var(--accent-dark)' : 'var(--text2)' }}>
{draggingApple ? 'CSV loslassen...' : 'Apple Health CSV hierher ziehen oder tippen'}
</div>
</>
)}
</div>
<input ref={appleRef} type="file" accept=".csv" onChange={e => { const file = e.target.files[0]; if (file) handleAppleImport(file) }} style={{ display: 'none' }} />
</div>
</div>
)
}
//
// MAIN COMPONENT
//
export default function VitalsPage() {
const [tab, setTab] = useState('baseline')
const [refreshKey, setRefreshKey] = useState(0)
const handleImportComplete = () => {
setRefreshKey(k => k + 1)
}
return (
<div>
@ -30,46 +648,11 @@ export default function VitalsPage() {
</button>
</div>
{tab === 'baseline' && (
<div className="card section-gap">
<div className="card-title">Morgenmessung (Baseline-Vitals)</div>
<p style={{ fontSize: 13, color: 'var(--text3)' }}>
Einmal täglich, morgens vor dem Aufstehen:
Ruhepuls, HRV, VO2 Max, SpO2, Atemfrequenz
</p>
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text3)' }}>
🚧 Frontend-Refactoring läuft noch...<br />
Nutze vorerst die alten Endpoints oder warte auf Update.
</div>
</div>
)}
{tab === 'bp' && (
<div className="card section-gap">
<div className="card-title">Blutdruck (mehrfach täglich)</div>
<p style={{ fontSize: 13, color: 'var(--text3)' }}>
Mehrfach täglich mit Kontext-Tagging:
Nüchtern, Nach dem Essen, Vor/Nach Training, Abends, Stress
</p>
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text3)' }}>
🚧 Frontend-Refactoring läuft noch...<br />
Nutze vorerst die alten Endpoints oder warte auf Update.
</div>
</div>
)}
{tab === 'import' && (
<div className="card section-gap">
<div className="card-title">CSV Import</div>
<p style={{ fontSize: 13, color: 'var(--text3)' }}>
Omron (Blutdruck) + Apple Health (Baseline-Vitals)
</p>
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text3)' }}>
🚧 Frontend-Refactoring läuft noch...<br />
Import-Funktionen folgen im nächsten Update.
</div>
</div>
)}
<div style={{ paddingBottom: 80 }}>
{tab === 'baseline' && <BaselineTab key={`baseline-${refreshKey}`} />}
{tab === 'bp' && <BloodPressureTab key={`bp-${refreshKey}`} />}
{tab === 'import' && <ImportTab onImportComplete={handleImportComplete} />}
</div>
</div>
)
}