mitai-jinkendo/frontend/src/pages/MeasureWizard.jsx

419 lines
19 KiB
React
Raw Normal View History

import { useState } from 'react'
import { ChevronRight, ChevronLeft, Check, X, BookOpen } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { api } from '../utils/api'
import { calcBodyFat, getBfCategory, METHOD_POINTS } from '../utils/calc'
import { CIRCUMFERENCE_POINTS, CALIPER_POINTS, CALIPER_METHODS } from '../utils/guideData'
import dayjs from 'dayjs'
// ── Circumference Wizard ──────────────────────────────────────────────────────
function CircumWizard({ onDone, onCancel }) {
const [step, setStep] = useState(0)
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'))
const [values, setValues] = useState({})
const [saving, setSaving] = useState(false)
const points = CIRCUMFERENCE_POINTS
const current = points[step]
const totalSteps = points.length
const handleNext = () => {
if (step < totalSteps - 1) setStep(s => s + 1)
else handleSave()
}
const handleSave = async () => {
setSaving(true)
try {
const payload = { date }
Object.entries(values).forEach(([k,v]) => { if(v) payload[k] = parseFloat(v) })
await api.upsertCirc(payload)
onDone()
} finally { setSaving(false) }
}
const progress = ((step + 1) / totalSteps) * 100
return (
<div style={{display:'flex',flexDirection:'column',minHeight:'calc(100vh - 120px)'}}>
{/* Header */}
<div style={{marginBottom:20}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<span style={{fontSize:12,color:'var(--text3)'}}>Schritt {step+1} von {totalSteps}</span>
<button onClick={onCancel} style={{background:'none',border:'none',cursor:'pointer',color:'var(--text3)'}}>
<X size={18}/>
</button>
</div>
<div style={{height:4,background:'var(--border)',borderRadius:2}}>
<div style={{height:'100%',background:'var(--accent)',borderRadius:2,width:`${progress}%`,transition:'width 0.3s'}}/>
</div>
</div>
{/* Datum (nur erster Schritt) */}
{step === 0 && (
<div className="card" style={{marginBottom:16}}>
<div className="form-row">
<label className="form-label">Datum</label>
<input type="date" className="form-input" style={{width:140}}
value={date} onChange={e=>setDate(e.target.value)}/>
<span className="form-unit"/>
</div>
</div>
)}
{/* Messpunkt */}
<div className="card" style={{flex:1,marginBottom:16}}>
{/* Punkt-Indikator */}
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:16}}>
<div style={{width:40,height:40,borderRadius:'50%',background:current.color,
display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}>
<span style={{fontSize:16,fontWeight:700,color:'white'}}>{step+1}</span>
</div>
<div>
<div style={{fontSize:18,fontWeight:700}}>{current.label}</div>
<div style={{fontSize:11,color:'var(--text3)'}}>Umfang messen</div>
</div>
</div>
{/* Anleitung */}
<div style={{display:'flex',flexDirection:'column',gap:10,marginBottom:20}}>
{[
['📍 Wo', current.where],
['🧍 Haltung', current.posture],
['📏 Maßband', current.how],
['💡 Tipp', current.tip],
].map(([label, text]) => (
<div key={label} style={{background:'var(--surface2)',borderRadius:8,padding:'10px 12px'}}>
<div style={{fontSize:11,fontWeight:600,color:'var(--text3)',marginBottom:3}}>{label}</div>
<div style={{fontSize:13,color:'var(--text1)',lineHeight:1.55}}>{text}</div>
</div>
))}
</div>
{/* Eingabe */}
<div style={{display:'flex',gap:10,alignItems:'center'}}>
<input
type="number" min={10} max={200} step={0.1}
className="form-input"
style={{flex:1,fontSize:24,fontWeight:700,textAlign:'center',height:56}}
placeholder="–"
value={values[current.id] || ''}
onChange={e => setValues(v => ({...v, [current.id]: e.target.value}))}
onKeyDown={e => e.key==='Enter' && handleNext()}
autoFocus
/>
<span style={{fontSize:18,color:'var(--text3)',fontWeight:500}}>cm</span>
</div>
{values[current.id] && (
<div style={{textAlign:'center',marginTop:8,fontSize:12,color:'var(--accent)'}}>
✓ {values[current.id]} cm erfasst
</div>
)}
</div>
{/* Navigation */}
<div style={{display:'flex',gap:8}}>
<button className="btn btn-secondary" style={{flex:1}} onClick={()=>setStep(s=>s-1)} disabled={step===0}>
<ChevronLeft size={16}/> Zurück
</button>
<button className="btn btn-primary" style={{flex:2}} onClick={handleNext} disabled={saving}>
{saving ? <div className="spinner" style={{width:14,height:14}}/> :
step === totalSteps-1
? <><Check size={16}/> Speichern</>
: <>Weiter <ChevronRight size={16}/></>}
</button>
</div>
{/* Übersicht bereits erfasster Werte */}
{Object.keys(values).length > 0 && (
<div style={{marginTop:14,padding:'10px 12px',background:'var(--surface2)',borderRadius:8}}>
<div style={{fontSize:11,fontWeight:600,color:'var(--text3)',marginBottom:6}}>Bisher erfasst:</div>
<div style={{display:'flex',flexWrap:'wrap',gap:6}}>
{points.slice(0, step+1).map(p => values[p.id] ? (
<span key={p.id} onClick={()=>setStep(points.indexOf(p))}
style={{fontSize:12,background:'var(--accent-light)',color:'var(--accent-dark)',
padding:'2px 8px',borderRadius:6,cursor:'pointer'}}>
{p.label}: {values[p.id]}
</span>
) : null)}
</div>
</div>
)}
</div>
)
}
// ── Caliper Wizard ────────────────────────────────────────────────────────────
function CaliperWizard({ onDone, onCancel, profile }) {
const [method, setMethod] = useState('jackson3')
const [step, setStep] = useState(-1) // -1 = method select
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'))
const [values, setValues] = useState({})
const [saving, setSaving] = useState(false)
const sex = profile?.sex || 'm'
const age = profile?.dob ? Math.floor((Date.now()-new Date(profile.dob))/(365.25*24*3600*1000)) : 30
const sfPoints = METHOD_POINTS[method]?.[sex] || []
const current = step >= 0 ? CALIPER_POINTS[sfPoints[step]] : null
const totalSteps = sfPoints.length
// Live BF calculation
const sfVals = {}
sfPoints.forEach(k => { const v=values[`sf_${k}`]; if(v) sfVals[k]=parseFloat(v) })
const bfPct = Object.keys(sfVals).length === sfPoints.length && sfPoints.length > 0
? Math.round(calcBodyFat(method, sfVals, sex, age)*10)/10 : null
const bfCat = bfPct ? getBfCategory(bfPct, sex) : null
const handleNext = () => {
if (step < totalSteps - 1) setStep(s => s+1)
else handleSave()
}
const handleSave = async () => {
setSaving(true)
try {
const payload = { date, sf_method: method }
Object.entries(values).forEach(([k,v]) => { if(v) payload[k]=parseFloat(v) })
if (bfPct) {
payload.body_fat_pct = bfPct
if (profile?.weight || values.weight) {
const w = parseFloat(profile?.weight || values.weight)
payload.lean_mass = Math.round(w*(1-bfPct/100)*10)/10
payload.fat_mass = Math.round(w*(bfPct/100)*10)/10
}
}
await api.upsertCaliper(payload)
onDone()
} finally { setSaving(false) }
}
const progress = step >= 0 ? ((step+1)/totalSteps)*100 : 0
// Method selection screen
if (step === -1) {
return (
<div>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:20}}>
<h2 style={{fontSize:18,fontWeight:700,margin:0}}>Caliper-Methode</h2>
<button onClick={onCancel} style={{background:'none',border:'none',cursor:'pointer',color:'var(--text3)'}}>
<X size={18}/>
</button>
</div>
<div className="form-row" style={{marginBottom:16}}>
<label className="form-label">Datum</label>
<input type="date" className="form-input" style={{width:140}}
value={date} onChange={e=>setDate(e.target.value)}/>
<span className="form-unit"/>
</div>
<div style={{display:'flex',flexDirection:'column',gap:10,marginBottom:20}}>
{Object.entries(CALIPER_METHODS).map(([k,m]) => (
<button key={k} onClick={()=>setMethod(k)}
style={{padding:'14px 16px',borderRadius:12,border:`2px solid ${method===k?'var(--accent)':'var(--border2)'}`,
background:method===k?'var(--accent-light)':'var(--surface)',cursor:'pointer',
fontFamily:'var(--font)',textAlign:'left',transition:'all 0.15s'}}>
<div style={{fontSize:15,fontWeight:600,color:method===k?'var(--accent-dark)':'var(--text1)'}}>{m.label}</div>
<div style={{fontSize:12,color:'var(--text3)',marginTop:2}}>{m.points_m.length} Messpunkte</div>
</button>
))}
</div>
<div style={{padding:'10px 12px',background:'var(--warn-bg)',borderRadius:8,marginBottom:16,fontSize:12,color:'var(--warn-text)'}}>
Immer rechte Körperseite · Falte 1 cm abheben · Caliper 2 Sek. · 3× messen, Mittelwert
</div>
<button className="btn btn-primary btn-full" onClick={()=>setStep(0)}>
Weiter mit {CALIPER_METHODS[method]?.label} <ChevronRight size={16}/>
</button>
</div>
)
}
return (
<div style={{display:'flex',flexDirection:'column',minHeight:'calc(100vh - 120px)'}}>
{/* Header */}
<div style={{marginBottom:20}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<span style={{fontSize:12,color:'var(--text3)'}}>Punkt {step+1} von {totalSteps} · {CALIPER_METHODS[method]?.label}</span>
<button onClick={onCancel} style={{background:'none',border:'none',cursor:'pointer',color:'var(--text3)'}}>
<X size={18}/>
</button>
</div>
<div style={{height:4,background:'var(--border)',borderRadius:2}}>
<div style={{height:'100%',background:'#D85A30',borderRadius:2,width:`${progress}%`,transition:'width 0.3s'}}/>
</div>
</div>
{/* Live BF Preview */}
{bfPct && (
<div style={{padding:'10px 14px',background:'var(--accent-light)',borderRadius:10,marginBottom:14,
display:'flex',alignItems:'center',gap:10}}>
<div style={{fontSize:22,fontWeight:700,color:bfCat?.color||'var(--accent)'}}>{bfPct}%</div>
{bfCat && <div style={{fontSize:12,color:'var(--accent-dark)'}}>{bfCat.label}</div>}
<div style={{flex:1,fontSize:11,color:'var(--accent-dark)',textAlign:'right'}}>Körperfett (live)</div>
</div>
)}
{/* Messpunkt */}
{current && (
<div className="card" style={{flex:1,marginBottom:16}}>
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:16}}>
<div style={{width:40,height:40,borderRadius:'50%',background:current.color,
display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}>
<span style={{fontSize:16,fontWeight:700,color:'white'}}>{step+1}</span>
</div>
<div>
<div style={{fontSize:18,fontWeight:700}}>{current.label}</div>
<div style={{fontSize:11,color:'var(--text3)'}}>Hautfalte messen</div>
</div>
</div>
<div style={{display:'flex',flexDirection:'column',gap:10,marginBottom:20}}>
{[
['📍 Wo', current.where],
['🧍 Haltung', current.posture],
['🔧 Technik', current.how],
['💡 Tipp', current.tip],
].map(([label, text]) => (
<div key={label} style={{background:'var(--surface2)',borderRadius:8,padding:'10px 12px'}}>
<div style={{fontSize:11,fontWeight:600,color:'var(--text3)',marginBottom:3}}>{label}</div>
<div style={{fontSize:13,color:'var(--text1)',lineHeight:1.55}}>{text}</div>
</div>
))}
</div>
<div style={{display:'flex',gap:10,alignItems:'center'}}>
<input
type="number" min={2} max={80} step={0.5}
className="form-input"
style={{flex:1,fontSize:24,fontWeight:700,textAlign:'center',height:56}}
placeholder="–"
value={values[`sf_${sfPoints[step]}`] || ''}
onChange={e => setValues(v => ({...v, [`sf_${sfPoints[step]}`]: e.target.value}))}
onKeyDown={e => e.key==='Enter' && handleNext()}
autoFocus
/>
<span style={{fontSize:18,color:'var(--text3)',fontWeight:500}}>mm</span>
</div>
{values[`sf_${sfPoints[step]}`] && (
<div style={{textAlign:'center',marginTop:8,fontSize:12,color:'var(--accent)'}}>
✓ {values[`sf_${sfPoints[step]}`]} mm erfasst
</div>
)}
</div>
)}
{/* Navigation */}
<div style={{display:'flex',gap:8}}>
<button className="btn btn-secondary" style={{flex:1}}
onClick={()=>setStep(s=>s<=0?-1:s-1)}>
<ChevronLeft size={16}/> Zurück
</button>
<button className="btn btn-primary" style={{flex:2}} onClick={handleNext} disabled={saving}>
{saving ? <div className="spinner" style={{width:14,height:14}}/> :
step === totalSteps-1
? <><Check size={16}/> Speichern</>
: <>Weiter <ChevronRight size={16}/></>}
</button>
</div>
{/* Übersicht */}
{Object.keys(values).length > 0 && (
<div style={{marginTop:14,padding:'10px 12px',background:'var(--surface2)',borderRadius:8}}>
<div style={{fontSize:11,fontWeight:600,color:'var(--text3)',marginBottom:6}}>Bisher erfasst:</div>
<div style={{display:'flex',flexWrap:'wrap',gap:6}}>
{sfPoints.slice(0,step+1).map((k,idx) => values[`sf_${k}`] ? (
<span key={k} onClick={()=>setStep(idx)}
style={{fontSize:12,background:'#D85A3022',color:'#D85A30',
padding:'2px 8px',borderRadius:6,cursor:'pointer'}}>
{CALIPER_POINTS[k]?.label}: {values[`sf_${k}`]}mm
</span>
) : null)}
</div>
</div>
)}
</div>
)
}
// ── Main Wizard Page ──────────────────────────────────────────────────────────
export default function MeasureWizard() {
const [mode, setMode] = useState(null) // null | 'circum' | 'caliper'
const [done, setDone] = useState(false)
const [profile, setProfile] = useState(null)
const nav = useNavigate()
useState(() => { api.getProfile().then(setProfile) })
if (done) {
return (
<div className="capture-page" style={{display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',
minHeight:'60vh',gap:16,textAlign:'center'}}>
<div style={{fontSize:48}}>✅</div>
<h2 style={{fontSize:20,fontWeight:700}}>Gespeichert!</h2>
<p style={{color:'var(--text2)',fontSize:14}}>Deine Messung wurde erfolgreich gespeichert.</p>
<div style={{display:'flex',gap:8}}>
<button className="btn btn-primary" onClick={()=>{ setDone(false); setMode(null) }}>
Weitere Messung
</button>
<button className="btn btn-secondary" onClick={()=>nav('/')}>
Zum Dashboard
</button>
</div>
</div>
)
}
if (mode === 'circum') return (
<div className="capture-page">
<CircumWizard onDone={()=>setDone(true)} onCancel={()=>setMode(null)}/>
</div>
)
if (mode === 'caliper') return (
<div className="capture-page">
<CaliperWizard onDone={()=>setDone(true)} onCancel={()=>setMode(null)} profile={profile}/>
</div>
)
return (
<div className="capture-page">
<h1 className="page-title">Assistent</h1>
<p style={{fontSize:13,color:'var(--text2)',marginBottom:20,lineHeight:1.6}}>
Der Assistent führt dich Schritt für Schritt durch die Messung – mit Anleitung für jeden Messpunkt.
</p>
<div style={{display:'flex',flexDirection:'column',gap:12}}>
<button onClick={()=>setMode('circum')}
style={{padding:'20px 16px',borderRadius:14,border:'1.5px solid var(--border2)',
background:'var(--surface)',cursor:'pointer',fontFamily:'var(--font)',textAlign:'left',
display:'flex',alignItems:'center',gap:14}}>
<div style={{fontSize:32}}>📏</div>
<div>
<div style={{fontSize:16,fontWeight:600}}>Umfänge messen</div>
<div style={{fontSize:12,color:'var(--text3)',marginTop:2}}>
9 Messpunkte · inkl. Oberarm kontrahiert + Oberarm entspannt
</div>
</div>
<ChevronRight size={20} style={{marginLeft:'auto',color:'var(--text3)'}}/>
</button>
<button onClick={()=>setMode('caliper')}
style={{padding:'20px 16px',borderRadius:14,border:'1.5px solid var(--border2)',
background:'var(--surface)',cursor:'pointer',fontFamily:'var(--font)',textAlign:'left',
display:'flex',alignItems:'center',gap:14}}>
<div style={{fontSize:32}}>📐</div>
<div>
<div style={{fontSize:16,fontWeight:600}}>Caliper Körperfett</div>
<div style={{fontSize:12,color:'var(--text3)',marginTop:2}}>
3–9 Messpunkte · Jackson/Pollock, Durnin oder Parrillo
</div>
</div>
<ChevronRight size={20} style={{marginLeft:'auto',color:'var(--text3)'}}/>
</button>
<div style={{marginTop:8,padding:'12px 14px',background:'var(--surface2)',borderRadius:10,
fontSize:12,color:'var(--text3)',lineHeight:1.6}}>
💡 Für schnelle Einzeleingaben oder Bearbeitung bestehender Werte nutze die direkten Screens
unter <strong>Gewicht</strong>, <strong>Umfänge</strong> und <strong>Caliper</strong>.
</div>
</div>
</div>
)
}