- 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.
199 lines
6.5 KiB
JavaScript
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>
|
|
)
|
|
}
|