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}"
)}
)}
)
})}
)}
)
}