mitai-jinkendo/frontend/src/components/pilot/PilotQuickCapture.jsx
Lars c0cb995a7b
All checks were successful
Deploy Development / deploy (push) Successful in 56s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 23s
feat: Remove deprecated pilot widgets and layout management
- Deleted unused components: GoalsSnapshotWidget, ReferenceValuesSummaryWidget, WeightKpiWidget, and PilotVizAdminCard.
- Removed associated layout storage and widget registry logic to streamline the pilot visualization module.
- Updated PilotVizPage to integrate new components for improved user experience and functionality.
2026-04-07 11:07:33 +02:00

199 lines
6.5 KiB
JavaScript

import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Check } from 'lucide-react'
import dayjs from 'dayjs'
import { api } from '../../utils/api'
/**
* Schnelleingabe: Gewicht + Baseline Vitals (Ruhepuls, HRV, VO₂max) für heute.
*/
export default function PilotQuickCapture({ onSaved }) {
const today = dayjs().format('YYYY-MM-DD')
const [weightInput, setWeightInput] = useState('')
const [weightSaving, setWeightSaving] = useState(false)
const [weightSaved, setWeightSaved] = useState(false)
const [weightErr, setWeightErr] = useState(null)
const [vForm, setVForm] = useState({
id: null,
resting_hr: '',
hrv: '',
vo2_max: '',
})
const [vSaving, setVSaving] = useState(false)
const [vErr, setVErr] = useState(null)
const [vOk, setVOk] = useState(false)
useEffect(() => {
api.weightStats().then((s) => {
if (s?.latest?.date === today) setWeightInput(String(s.latest.weight))
}).catch(() => {})
}, [today])
useEffect(() => {
let cancelled = false
;(async () => {
try {
const existing = await api.getBaselineByDate(today)
if (cancelled || !existing?.id) return
setVForm({
id: existing.id,
resting_hr: existing.resting_hr != null ? String(existing.resting_hr) : '',
hrv: existing.hrv != null ? String(existing.hrv) : '',
vo2_max: existing.vo2_max != null ? String(existing.vo2_max) : '',
})
} catch (err) {
const msg = String(err?.message || '')
if (msg.includes('404') || msg.toLowerCase().includes('nicht gefunden')) {
setVForm((f) => ({ ...f, id: null, resting_hr: '', hrv: '', vo2_max: '' }))
}
}
})()
return () => {
cancelled = true
}
}, [today])
const saveWeight = async () => {
const w = parseFloat(weightInput)
if (!w || w < 20 || w > 300) return
setWeightSaving(true)
setWeightErr(null)
try {
await api.upsertWeight(today, w)
setWeightSaved(true)
onSaved?.()
setTimeout(() => setWeightSaved(false), 2000)
} catch (e) {
setWeightErr(e.message || 'Fehler')
} finally {
setWeightSaving(false)
}
}
const saveVitals = async () => {
setVSaving(true)
setVErr(null)
setVOk(false)
try {
const payload = { date: today }
if (vForm.resting_hr) payload.resting_hr = parseInt(vForm.resting_hr, 10)
if (vForm.hrv) payload.hrv = parseInt(vForm.hrv, 10)
if (vForm.vo2_max) payload.vo2_max = parseFloat(vForm.vo2_max)
if (!payload.resting_hr && !payload.hrv && !payload.vo2_max) {
setVErr('Mindestens Ruhepuls, HRV oder VO₂max angeben.')
setVSaving(false)
return
}
if (vForm.id) {
await api.updateBaseline(vForm.id, payload)
} else {
const created = await api.createBaseline(payload)
if (created?.id) setVForm((f) => ({ ...f, id: created.id }))
}
setVOk(true)
onSaved?.()
setTimeout(() => setVOk(false), 2000)
} catch (e) {
setVErr(e.message || 'Speichern fehlgeschlagen')
} finally {
setVSaving(false)
}
}
const cellStyle = {
flex: '1 1 140px',
minWidth: 0,
padding: 12,
borderRadius: 10,
border: '1px solid var(--border)',
background: 'var(--surface2)',
}
return (
<div className="card section-gap">
<div className="card-title">Schnelleingabe (heute)</div>
<p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12, lineHeight: 1.5 }}>
Gewicht separat; Vitalwerte typischerweise gemeinsam.{' '}
<Link to="/vitals" style={{ color: 'var(--accent)', fontSize: 12 }}>
Volle Vitalwerte-Seite
</Link>
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
<div style={cellStyle}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text3)', marginBottom: 6 }}>Gewicht</div>
{weightErr && (
<div style={{ fontSize: 11, color: 'var(--danger)', marginBottom: 6 }}>{weightErr}</div>
)}
<div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
<input
type="number"
className="form-input"
min={20}
max={300}
step={0.1}
style={{ flex: 1, minWidth: 72 }}
placeholder="kg"
value={weightInput}
onChange={(e) => setWeightInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && saveWeight()}
/>
<button
type="button"
className="btn btn-primary"
style={{ padding: '6px 12px' }}
disabled={weightSaving}
onClick={saveWeight}
>
{weightSaved ? <Check size={15} /> : weightSaving ? <div className="spinner" style={{ width: 14, height: 14 }} /> : 'OK'}
</button>
</div>
</div>
<div style={{ ...cellStyle, flex: '2 1 280px' }}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text3)', marginBottom: 6 }}>
Vitalwerte (Baseline)
</div>
{vErr && <div style={{ fontSize: 11, color: 'var(--danger)', marginBottom: 6 }}>{vErr}</div>}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(88px, 1fr))', gap: 8 }}>
<input
type="number"
className="form-input"
placeholder="Ruhepuls"
value={vForm.resting_hr}
onChange={(e) => setVForm((f) => ({ ...f, resting_hr: e.target.value }))}
/>
<input
type="number"
className="form-input"
placeholder="HRV"
value={vForm.hrv}
onChange={(e) => setVForm((f) => ({ ...f, hrv: e.target.value }))}
/>
<input
type="number"
className="form-input"
step={0.1}
placeholder="VO₂max"
value={vForm.vo2_max}
onChange={(e) => setVForm((f) => ({ ...f, vo2_max: e.target.value }))}
/>
</div>
<button
type="button"
className="btn btn-primary btn-full"
style={{ marginTop: 10 }}
disabled={vSaving}
onClick={saveVitals}
>
{vOk ? '✓ Gespeichert' : vSaving ? '…' : 'Vitalwerte speichern'}
</button>
</div>
</div>
</div>
)
}