From 10772d1f801f0b43cb735e3d248ec77dba799f24 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 16:19:53 +0100 Subject: [PATCH] feat: VitalsPage mobile-optimized with inline editing & smart upsert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Full-width fields with section headers (mobile-friendly) - Inline editing for all measurements (edit mode per row) - Smart upsert: date change loads existing entry → update instead of duplicate - Units integrated into labels (no overflow) - Baseline: auto-detects existing entry and switches to update mode - Blood Pressure: inline editing with all fields (date, time, BP, context, flags) - Edit/Save/Cancel buttons with lucide-react icons Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/VitalsPage.jsx | 649 ++++++++++++++++++++++++------ 1 file changed, 536 insertions(+), 113 deletions(-) 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 -
- -
- - setForm(f => ({ ...f, note: e.target.value }))} /> - + {/* Notiz */} +
+ + setForm(f => ({ ...f, note: e.target.value }))} + />
@@ -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 -
- -
- - setForm(f => ({ ...f, context: e.target.value }))} + > {CONTEXT_OPTIONS.map(opt => )} -
-
-
- -
+ ) : ( + // View Mode +
+
+
+ {dayjs(e.measured_at).format('dd, DD. MMM • HH:mm')} Uhr +
+
+ {e.systolic}/{e.diastolic} mmHg + {e.pulse && 💓 {e.pulse} bpm} +
+
+ {ctx?.label} · {cat.label} + {(e.irregular_heartbeat || e.possible_afib) && ⚠️ {e.irregular_heartbeat ? 'Unregelmäßig' : ''} {e.possible_afib ? 'AFib?' : ''}} +
+ {e.note &&

"{e.note}"

} +
+
+ + +
+
+ )}
) })}