diff --git a/frontend/src/pages/VitalsPage.jsx b/frontend/src/pages/VitalsPage.jsx
index ef21f4f..fcfa6f6 100644
--- a/frontend/src/pages/VitalsPage.jsx
+++ b/frontend/src/pages/VitalsPage.jsx
@@ -1,868 +1,73 @@
-import { useState, useEffect, useRef } from 'react'
-import { Pencil, Trash2, X, TrendingUp, TrendingDown, Minus, Upload } from 'lucide-react'
-import { api } from '../utils/api'
+import { useState, useEffect } from 'react'
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 && (
-
- )}
-
-
- )
-}
-
+/**
+ * VitalsPage - Refactored v9d Phase 2d
+ *
+ * Separated into:
+ * - Tab 1: Morgenmessung (Baseline) - once daily
+ * - Tab 2: Blutdruck (BP) - multiple daily with context
+ * - Tab 3: Import
+ */
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 = ''
- }
- }
- }
+ const [tab, setTab] = useState('baseline')
return (
Vitalwerte
-
- {/* Stats Overview */}
- {stats && stats.total_entries > 0 && (
+ {tab === 'baseline' && (
-
- {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
-
+
Morgenmessung (Baseline-Vitals)
+
+ Einmal täglich, morgens vor dem Aufstehen:
+ Ruhepuls, HRV, VO2 Max, SpO2, Atemfrequenz
+
+
+ 🚧 Frontend-Refactoring läuft noch...
+ Nutze vorerst die alten Endpoints oder warte auf Update.
)}
- {tab === 'add' && (
+ {tab === 'bp' && (
-
Vitalwerte erfassen
-
- Morgens nach dem Aufwachen, vor dem Aufstehen: Ruhepuls messen (z.B. mit Apple Watch oder Smartwatch).
- HRV ist optional.
+
Blutdruck (mehrfach täglich)
+
+ Mehrfach täglich mit Kontext-Tagging:
+ Nüchtern, Nach dem Essen, Vor/Nach Training, Abends, Stress
- {error && (
-
- {error}
-
- )}
-
+
+ 🚧 Frontend-Refactoring läuft noch...
+ Nutze vorerst die alten Endpoints oder warte auf Update.
+
)}
{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'}
-
-
-
+
CSV Import
+
+ Omron (Blutdruck) + Apple Health (Baseline-Vitals)
+
+
+ 🚧 Frontend-Refactoring läuft noch...
+ Import-Funktionen folgen im nächsten Update.
-
-
- 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}"
-
- )}
-
-
-
setEditing({ ...e })}
- >
-
-
-
handleDelete(e.id)}
- >
-
-
-
-
-
- )}
-
- )
- })}
)}