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') function empty() { return { 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: '' } } function EntryForm({ form, setForm, onSave, onCancel, saving, saveLabel = 'Speichern' }) { const set = (k, v) => setForm(f => ({ ...f, [k]: v })) return (
set('date', e.target.value)} />
{/* Section: Morgenmessung */}
Morgenmessung (vor dem Aufstehen)
set('resting_hr', e.target.value)} /> bpm
set('hrv', e.target.value)} /> ms
{/* Section: Blutdruck */}
Blutdruck (Omron)
set('blood_pressure_systolic', e.target.value)} /> mmHg
set('blood_pressure_diastolic', e.target.value)} /> mmHg
set('pulse', e.target.value)} /> bpm
{/* Section: Fitness & Sauerstoff */}
Fitness & Sauerstoff (Apple Watch)
set('vo2_max', e.target.value)} /> ml/kg/min
set('spo2', e.target.value)} /> %
set('respiratory_rate', e.target.value)} /> /min
{/* Section: Warnungen */}
set('note', e.target.value)} />
{onCancel && ( )}
) } export default function VitalsPage() { const [entries, setEntries] = useState([]) const [stats, setStats] = useState(null) const [tab, setTab] = useState('list') const [form, setForm] = useState(empty()) const [editing, setEditing] = useState(null) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [error, setError] = useState(null) // Import states const [importingOmron, setImportingOmron] = useState(false) const [importingApple, setImportingApple] = useState(false) const [draggingOmron, setDraggingOmron] = useState(false) const [draggingApple, setDraggingApple] = useState(false) const [importResult, setImportResult] = useState(null) const omronFileInputRef = useRef(null) const appleFileInputRef = useRef(null) const load = async () => { try { const [e, s] = await Promise.all([api.listVitals(90), api.getVitalsStats(30)]) setEntries(e) setStats(s) } catch (err) { console.error('Load failed:', err) setError(err.message) } } useEffect(() => { load() }, []) const handleSave = async () => { setSaving(true) setError(null) try { const payload = { date: form.date } // 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 // 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 } await api.createVitals(payload) setSaved(true) await load() setTimeout(() => { setSaved(false) setForm(empty()) }, 1500) } catch (err) { console.error('Save failed:', err) setError(err.message || 'Fehler beim Speichern') setTimeout(() => setError(null), 5000) } finally { setSaving(false) } } const handleUpdate = async () => { try { const payload = {} // Only include fields if they have values 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) setEditing(null) await load() } catch (err) { console.error('Update failed:', err) setError(err.message) } } const handleDelete = async (id) => { if (!confirm('Eintrag löschen?')) return try { await api.deleteVitals(id) await load() } catch (err) { console.error('Delete failed:', err) setError(err.message) } } const getTrendIcon = (trend) => { if (trend === 'increasing') return if (trend === 'decreasing') return return } // Import handlers const handleOmronImport = async (file) => { if (!file || !file.name.endsWith('.csv')) { setError('Bitte eine CSV-Datei auswählen') setTimeout(() => setError(null), 3000) return } setImportingOmron(true) setImportResult(null) setError(null) try { const result = await api.importVitalsOmron(file) setImportResult({ type: 'omron', inserted: result.inserted || 0, updated: result.updated || 0, skipped: result.skipped || 0, errors: result.errors || 0 }) await load() } catch (err) { setError('Omron Import fehlgeschlagen: ' + err.message) setTimeout(() => setError(null), 5000) } finally { setImportingOmron(false) if (omronFileInputRef.current) { omronFileInputRef.current.value = '' } } } const handleAppleImport = async (file) => { if (!file || !file.name.endsWith('.csv')) { setError('Bitte eine CSV-Datei auswählen') setTimeout(() => setError(null), 3000) return } setImportingApple(true) setImportResult(null) setError(null) try { const result = await api.importVitalsAppleHealth(file) setImportResult({ type: 'apple_health', inserted: result.inserted || 0, updated: result.updated || 0, skipped: result.skipped || 0, errors: result.errors || 0 }) await load() } catch (err) { setError('Apple Health Import fehlgeschlagen: ' + err.message) setTimeout(() => setError(null), 5000) } finally { setImportingApple(false) if (appleFileInputRef.current) { appleFileInputRef.current.value = '' } } } return (

Vitalwerte

{/* Stats Overview */} {stats && stats.total_entries > 0 && (
{stats.avg_resting_hr_7d && (
{Math.round(stats.avg_resting_hr_7d)}
Ø Ruhepuls 7d
)} {stats.avg_hrv_7d && (
{Math.round(stats.avg_hrv_7d)}
Ø HRV 7d
)} {stats.avg_bp_systolic_7d && (
{Math.round(stats.avg_bp_systolic_7d)}/{Math.round(stats.avg_bp_diastolic_7d || 0)}
Ø Blutdruck 7d
)} {stats.latest_vo2_max && (
{stats.latest_vo2_max.toFixed(1)}
VO2 Max
)} {stats.avg_spo2_7d && (
{Math.round(stats.avg_spo2_7d)}%
Ø SpO2 7d
)}
{stats.total_entries}
Einträge
)} {tab === 'add' && (
Vitalwerte erfassen

Morgens nach dem Aufwachen, vor dem Aufstehen: Ruhepuls messen (z.B. mit Apple Watch oder Smartwatch). HRV ist optional.

{error && (
{error}
)}
)} {tab === 'import' && (
{error && (
{error}
)} {importResult && (
Import erfolgreich ({importResult.type === 'omron' ? 'Omron' : 'Apple Health'}):
{importResult.inserted} neu · {importResult.updated} aktualisiert · {importResult.skipped} übersprungen {importResult.errors > 0 && <> · {importResult.errors} Fehler}
)} {/* Omron Import */}
Omron Blutdruckmessgerät

Exportiere CSV aus der Omron Connect App:
• Blutdruck (Systolisch/Diastolisch)
• Puls
• Unregelmäßiger Herzschlag & AFib-Warnungen

{ e.preventDefault(); setDraggingOmron(true) }} onDragLeave={() => setDraggingOmron(false)} onDrop={e => { e.preventDefault() setDraggingOmron(false) const file = e.dataTransfer.files[0] if (file) handleOmronImport(file) }} onClick={() => omronFileInputRef.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 ? ( <>
Importiere Omron-Daten...
) : ( <>
{draggingOmron ? 'CSV loslassen...' : 'Omron CSV hierher ziehen oder tippen'}
)}
{ const file = e.target.files[0] if (file) handleOmronImport(file) }} style={{ display: 'none' }} />
{/* Apple Health Import */}
Apple Health

Exportiere Health-Daten von der Health-App:
• Ruhepuls (Resting Heart Rate)
• HRV (Heart Rate Variability)
• VO2 Max
• SpO2 (Blutsauerstoffsättigung)
• Atemfrequenz

{ e.preventDefault(); setDraggingApple(true) }} onDragLeave={() => setDraggingApple(false)} onDrop={e => { e.preventDefault() setDraggingApple(false) const file = e.dataTransfer.files[0] if (file) handleAppleImport(file) }} onClick={() => appleFileInputRef.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 ? ( <>
Importiere Apple Health-Daten...
) : ( <>
{draggingApple ? 'CSV loslassen...' : 'Apple Health CSV hierher ziehen oder tippen'}
)}
{ const file = e.target.files[0] if (file) handleAppleImport(file) }} style={{ display: 'none' }} />
)} {tab === 'stats' && stats && (
Trend-Analyse (14 Tage)
Ruhepuls
{getTrendIcon(stats.trend_resting_hr)} {stats.trend_resting_hr === 'increasing' ? 'Steigend' : stats.trend_resting_hr === 'decreasing' ? 'Sinkend' : 'Stabil'}
HRV
{getTrendIcon(stats.trend_hrv)} {stats.trend_hrv === 'increasing' ? 'Steigend' : stats.trend_hrv === 'decreasing' ? 'Sinkend' : 'Stabil'}
Interpretation:
• Ruhepuls sinkend = bessere Fitness 💪
• HRV steigend = bessere Erholung ✨
• Ruhepuls steigend + HRV sinkend = Übertraining-Signal ⚠️
)} {tab === 'list' && (
{entries.length === 0 && (

Keine Einträge

Erfasse deine ersten Vitalwerte im Tab "Erfassen".

)} {entries.map(e => { const isEd = editing?.id === e.id return (
{isEd ? ( setEditing(null)} saveLabel="Speichern" /> ) : (
{dayjs(e.date).format('dd, DD. MMMM YYYY')}
{e.resting_hr && ( ❤️ {e.resting_hr} bpm )} {e.hrv && ( 📊 HRV {e.hrv} ms )} {e.blood_pressure_systolic && e.blood_pressure_diastolic && ( 🩸 {e.blood_pressure_systolic}/{e.blood_pressure_diastolic} mmHg )} {e.pulse && ( 💓 {e.pulse} bpm )} {e.vo2_max && ( 🏃 VO2 {e.vo2_max} )} {e.spo2 && ( 🫁 SpO2 {e.spo2}% )} {e.respiratory_rate && ( 💨 {e.respiratory_rate}/min )} {(e.irregular_heartbeat || e.possible_afib) && ( ⚠️ {e.irregular_heartbeat ? 'Unregelmäßig' : ''} {e.possible_afib ? 'AFib?' : ''} )} {e.source !== 'manual' && ( {e.source === 'apple_health' ? 'Apple Health' : e.source === 'omron' ? 'Omron' : e.source} )}
{e.note && (

"{e.note}"

)}
)}
) })}
)}
) }