diff --git a/frontend/src/pages/VitalsPage.jsx b/frontend/src/pages/VitalsPage.jsx
index d4887d1..d6fda55 100644
--- a/frontend/src/pages/VitalsPage.jsx
+++ b/frontend/src/pages/VitalsPage.jsx
@@ -1,12 +1,12 @@
import { useState, useEffect, useRef } from 'react'
-import { Pencil, Trash2, X, TrendingUp, TrendingDown, Minus, Upload } from 'lucide-react'
+import { Pencil, Trash2, X, Save, TrendingUp, TrendingDown, Minus, Upload } from 'lucide-react'
import { api } from '../utils/api'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
/**
- * VitalsPage - Refactored v9d Phase 2d
+ * VitalsPage - Refactored v9d Phase 2d (Mobile-optimized, Inline Editing, Smart Upsert)
*
* Separated vitals tracking:
* - Baseline Vitals: Once daily (morning, fasted) - RHR, HRV, VO2 Max, SpO2
@@ -22,6 +22,7 @@ function BaselineTab() {
const [entries, setEntries] = useState([])
const [stats, setStats] = useState(null)
const [form, setForm] = useState({
+ id: null, // Für Update
date: dayjs().format('YYYY-MM-DD'),
resting_hr: '',
hrv: '',
@@ -30,7 +31,8 @@ function BaselineTab() {
respiratory_rate: '',
note: ''
})
- const [editing, setEditing] = useState(null)
+ const [editingId, setEditingId] = useState(null)
+ const [editForm, setEditForm] = useState({})
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(false)
@@ -50,6 +52,47 @@ function BaselineTab() {
useEffect(() => { load() }, [])
+ // Smart Upsert: Beim Datum-Wechsel existierenden Eintrag laden
+ useEffect(() => {
+ const loadExisting = async () => {
+ if (!form.date) return
+ try {
+ const existing = await api.getBaselineByDate(form.date)
+ if (existing && existing.id) {
+ // Eintrag gefunden → Formular vorausfüllen
+ setForm({
+ id: existing.id,
+ date: existing.date,
+ resting_hr: existing.resting_hr || '',
+ hrv: existing.hrv || '',
+ vo2_max: existing.vo2_max || '',
+ spo2: existing.spo2 || '',
+ respiratory_rate: existing.respiratory_rate || '',
+ note: existing.note || ''
+ })
+ } else {
+ // Kein Eintrag → leeres Formular (nur Datum behalten)
+ setForm(f => ({
+ id: null,
+ date: f.date,
+ resting_hr: '',
+ hrv: '',
+ vo2_max: '',
+ spo2: '',
+ respiratory_rate: '',
+ note: ''
+ }))
+ }
+ } catch (err) {
+ // 404 ist ok (kein Eintrag vorhanden)
+ if (!err.message.includes('404')) {
+ console.error('Fehler beim Laden:', err)
+ }
+ }
+ }
+ loadExisting()
+ }, [form.date])
+
const handleSave = async () => {
setSaving(true)
setError(null)
@@ -68,12 +111,17 @@ function BaselineTab() {
return
}
- await api.createBaseline(payload)
+ if (form.id) {
+ await api.updateBaseline(form.id, payload)
+ } else {
+ 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: '' })
+ setForm({ id: null, date: dayjs().format('YYYY-MM-DD'), resting_hr: '', hrv: '', vo2_max: '', spo2: '', respiratory_rate: '', note: '' })
}, 1500)
} catch (err) {
setError(err.message)
@@ -92,10 +140,41 @@ function BaselineTab() {
}
}
- const getTrendIcon = (trend) => {
- if (trend === 'increasing') return
- if (trend === 'decreasing') return
- return
+ const startEdit = (entry) => {
+ setEditingId(entry.id)
+ setEditForm({
+ resting_hr: entry.resting_hr || '',
+ hrv: entry.hrv || '',
+ vo2_max: entry.vo2_max || '',
+ spo2: entry.spo2 || '',
+ respiratory_rate: entry.respiratory_rate || '',
+ note: entry.note || ''
+ })
+ }
+
+ const cancelEdit = () => {
+ setEditingId(null)
+ setEditForm({})
+ }
+
+ const saveEdit = async (id) => {
+ try {
+ const entry = entries.find(e => e.id === id)
+ const payload = { date: entry.date }
+ if (editForm.resting_hr) payload.resting_hr = parseInt(editForm.resting_hr)
+ if (editForm.hrv) payload.hrv = parseInt(editForm.hrv)
+ if (editForm.vo2_max) payload.vo2_max = parseFloat(editForm.vo2_max)
+ if (editForm.spo2) payload.spo2 = parseInt(editForm.spo2)
+ if (editForm.respiratory_rate) payload.respiratory_rate = parseFloat(editForm.respiratory_rate)
+ if (editForm.note) payload.note = editForm.note
+
+ await api.updateBaseline(id, payload)
+ setEditingId(null)
+ setEditForm({})
+ await load()
+ } catch (err) {
+ setError(err.message)
+ }
}
return (
@@ -129,56 +208,112 @@ function BaselineTab() {
{/* Form */}
Morgenmessung erfassen
-
- Einmal täglich, morgens vor dem Aufstehen (nüchtern)
+
+ Einmal täglich, morgens vor dem Aufstehen (nüchtern). {form.id && Eintrag wird aktualisiert.}
- {error &&
{error}
}
+ {error &&
{error}
}
-
-
-
setForm(f => ({ ...f, date: e.target.value }))} />
-
+ {/* Datum - volle Breite */}
+
+
+ setForm(f => ({ ...f, date: e.target.value }))}
+ />
-
-
-
setForm(f => ({ ...f, resting_hr: e.target.value }))} />
-
bpm
+ {/* Sektion: Herzfunktion */}
+
❤️ Herzfunktion
+
+
+ setForm(f => ({ ...f, resting_hr: e.target.value }))}
+ />
+
+
+
+ setForm(f => ({ ...f, hrv: e.target.value }))}
+ />
-
-
-
setForm(f => ({ ...f, hrv: e.target.value }))} />
-
ms
+ {/* Sektion: Fitness & Atmung */}
+
🏃 Fitness & Atmung
+
+
+ setForm(f => ({ ...f, vo2_max: e.target.value }))}
+ />
+
+
+
+ setForm(f => ({ ...f, spo2: e.target.value }))}
+ />
+
+
+
+ setForm(f => ({ ...f, respiratory_rate: e.target.value }))}
+ />
-
-
- setForm(f => ({ ...f, vo2_max: e.target.value }))} />
- ml/kg/min
-
-
-
-
- setForm(f => ({ ...f, spo2: e.target.value }))} />
- %
-
-
-
-
- setForm(f => ({ ...f, respiratory_rate: e.target.value }))} />
- /min
-
-
-
@@ -188,24 +323,109 @@ function BaselineTab() {
Letzte Messungen ({entries.length})
{entries.map(e => (
-
-
-
+ {editingId === e.id ? (
+ // Edit Mode
+
+
{dayjs(e.date).format('dd, DD. MMM YYYY')}
-
- {e.resting_hr &&
❤️ {e.resting_hr} bpm}
- {e.hrv &&
📊 HRV {e.hrv} ms}
- {e.vo2_max &&
🏃 VO2 {e.vo2_max}}
- {e.spo2 &&
🫁 SpO2 {e.spo2}%}
- {e.respiratory_rate &&
💨 {e.respiratory_rate}/min}
+
+
+ setEditForm(f => ({ ...f, resting_hr: e.target.value }))}
+ />
+
+
+
+ setEditForm(f => ({ ...f, hrv: e.target.value }))}
+ />
+
+
+
+ setEditForm(f => ({ ...f, vo2_max: e.target.value }))}
+ />
+
+
+
+ setEditForm(f => ({ ...f, spo2: e.target.value }))}
+ />
+
+
+
+ setEditForm(f => ({ ...f, respiratory_rate: e.target.value }))}
+ />
+
+
+
+ setEditForm(f => ({ ...f, note: e.target.value }))}
+ />
+
+
+
+
- {e.note &&
"{e.note}"
}
-
-
+ ) : (
+ // View Mode
+
+
+
+ {dayjs(e.date).format('dd, DD. MMM YYYY')}
+
+
+ {e.resting_hr && ❤️ {e.resting_hr} bpm}
+ {e.hrv && 📊 HRV {e.hrv} ms}
+ {e.vo2_max && 🏃 VO2 {e.vo2_max}}
+ {e.spo2 && 🫁 SpO2 {e.spo2}%}
+ {e.respiratory_rate && 💨 {e.respiratory_rate}/min}
+
+ {e.note &&
"{e.note}"
}
+
+
+
+
+
+
+ )}
))}
@@ -243,6 +463,8 @@ function BloodPressureTab() {
possible_afib: false,
note: ''
})
+ const [editingId, setEditingId] = useState(null)
+ const [editForm, setEditForm] = useState({})
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(false)
@@ -317,6 +539,50 @@ function BloodPressureTab() {
}
}
+ const startEdit = (entry) => {
+ const dt = dayjs(entry.measured_at)
+ setEditingId(entry.id)
+ setEditForm({
+ date: dt.format('YYYY-MM-DD'),
+ time: dt.format('HH:mm'),
+ systolic: entry.systolic,
+ diastolic: entry.diastolic,
+ pulse: entry.pulse || '',
+ context: entry.context,
+ irregular_heartbeat: entry.irregular_heartbeat,
+ possible_afib: entry.possible_afib,
+ note: entry.note || ''
+ })
+ }
+
+ const cancelEdit = () => {
+ setEditingId(null)
+ setEditForm({})
+ }
+
+ const saveEdit = async (id) => {
+ try {
+ const measured_at = `${editForm.date} ${editForm.time}:00`
+ const payload = {
+ measured_at,
+ systolic: parseInt(editForm.systolic),
+ diastolic: parseInt(editForm.diastolic),
+ pulse: editForm.pulse ? parseInt(editForm.pulse) : null,
+ context: editForm.context,
+ irregular_heartbeat: editForm.irregular_heartbeat,
+ possible_afib: editForm.possible_afib,
+ note: editForm.note || null
+ }
+
+ await api.updateBloodPressure(id, payload)
+ setEditingId(null)
+ setEditForm({})
+ 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' }
@@ -359,51 +625,92 @@ function BloodPressureTab() {
{/* Form */}
Blutdruck messen
-
+
Mehrfach täglich mit Kontext-Tagging
- {error &&
{error}
}
+ {error &&
{error}
}
-
-
-
- setForm(f => ({ ...f, date: e.target.value }))} />
-
-
-
- setForm(f => ({ ...f, time: e.target.value }))} />
-
+ {/* Datum + Uhrzeit - volle Breite */}
+
+
+ setForm(f => ({ ...f, date: e.target.value }))}
+ />
+
+
+
+ setForm(f => ({ ...f, time: e.target.value }))}
+ />
-
-
-
setForm(f => ({ ...f, systolic: e.target.value }))} />
-
mmHg
+ {/* Sektion: Blutdruck */}
+
🩸 Blutdruck
+
+
+ setForm(f => ({ ...f, systolic: e.target.value }))}
+ />
+
+
+
+ setForm(f => ({ ...f, diastolic: e.target.value }))}
+ />
+
+
+
+ setForm(f => ({ ...f, pulse: e.target.value }))}
+ />
-
-
- setForm(f => ({ ...f, diastolic: e.target.value }))} />
- mmHg
-
-
-
-
- setForm(f => ({ ...f, pulse: e.target.value }))} />
- bpm
-
-
-
-
-