mitai-jinkendo/frontend/src/pages/CircumScreen.jsx
Lars Stommer 89b6c0b072
Some checks are pending
Deploy to Raspberry Pi / deploy (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Build Test / lint-backend (push) Waiting to run
feat: initial commit – Mitai Jinkendo v9a
2026-03-16 13:35:11 +01:00

166 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef } from 'react'
import { Pencil, Trash2, Check, X, Camera, BookOpen } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { api } from '../utils/api'
import { CIRCUMFERENCE_POINTS } from '../utils/guideData'
import dayjs from 'dayjs'
const FIELDS = ['c_neck','c_chest','c_waist','c_belly','c_hip','c_thigh','c_calf','c_arm']
const LABELS = {c_neck:'Hals',c_chest:'Brust',c_waist:'Taille',c_belly:'Bauch',c_hip:'Hüfte',c_thigh:'Oberschenkel',c_calf:'Wade',c_arm:'Oberarm'}
function empty() { return {date:dayjs().format('YYYY-MM-DD'), c_neck:'',c_chest:'',c_waist:'',c_belly:'',c_hip:'',c_thigh:'',c_calf:'',c_arm:'',notes:'',photo_id:''} }
export default function CircumScreen() {
const [entries, setEntries] = useState([])
const [form, setForm] = useState(empty())
const [editing, setEditing] = useState(null)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [photoFile, setPhotoFile] = useState(null)
const [photoPreview, setPhotoPreview] = useState(null)
const fileRef = useRef()
const nav = useNavigate()
const load = () => api.listCirc().then(setEntries)
useEffect(()=>{ load() },[])
const set = (k,v) => setForm(f=>({...f,[k]:v}))
const handleSave = async () => {
setSaving(true)
try {
const payload = {}
payload.date = form.date
FIELDS.forEach(k=>{ if(form[k]!=='') payload[k]=parseFloat(form[k]) })
if(form.notes) payload.notes = form.notes
if(photoFile) {
const pr = await api.uploadPhoto(photoFile, form.date)
payload.photo_id = pr.id
}
await api.upsertCirc(payload)
setSaved(true); await load()
setTimeout(()=>setSaved(false),2000)
setForm(empty()); setPhotoFile(null); setPhotoPreview(null)
} finally { setSaving(false) }
}
const startEdit = (e) => setEditing({...e})
const cancelEdit = () => setEditing(null)
const handleUpdate = async () => {
const payload = {}
payload.date = editing.date
FIELDS.forEach(k=>{ if(editing[k]!=null && editing[k]!=='') payload[k]=parseFloat(editing[k]) })
if(editing.notes) payload.notes=editing.notes
await api.updateCirc(editing.id, payload)
setEditing(null); await load()
}
const handleDelete = async (id) => {
if(!confirm('Eintrag löschen?')) return
await api.deleteCirc(id); await load()
}
return (
<div>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:16}}>
<h1 className="page-title" style={{margin:0}}>Umfänge</h1>
<button className="btn btn-secondary" style={{fontSize:12,padding:'6px 10px'}} onClick={()=>nav('/guide')}>
<BookOpen size={13}/> Anleitung
</button>
</div>
{/* Eingabe */}
<div className="card section-gap">
<div className="card-title">Neue Messung</div>
<div className="form-row">
<label className="form-label">Datum</label>
<input type="date" className="form-input" style={{width:140}} value={form.date} onChange={e=>set('date',e.target.value)}/>
<span className="form-unit"/>
</div>
{CIRCUMFERENCE_POINTS.map(p=>(
<div key={p.id} className="form-row">
<label className="form-label" title={p.where}>{p.label}</label>
<input type="number" className="form-input" min={10} max={200} step={0.1}
placeholder="" value={form[p.id]||''} onChange={e=>set(p.id,e.target.value)}/>
<span className="form-unit">cm</span>
</div>
))}
<div className="form-row">
<label className="form-label">Notiz</label>
<input type="text" className="form-input" placeholder="optional"
value={form.notes} onChange={e=>set('notes',e.target.value)}/>
<span className="form-unit"/>
</div>
{/* Photo */}
<input ref={fileRef} type="file" accept="image/*" capture="environment" style={{display:'none'}}
onChange={e=>{ const f=e.target.files[0]; if(f){ setPhotoFile(f); setPhotoPreview(URL.createObjectURL(f)) }}}/>
{photoPreview && <img src={photoPreview} style={{width:'100%',borderRadius:8,marginBottom:8}} alt="preview"/>}
<button className="btn btn-secondary btn-full" onClick={()=>fileRef.current.click()}>
<Camera size={14}/> {photoPreview?'Foto ändern':'Foto hinzufügen'}
</button>
<button className="btn btn-primary btn-full" style={{marginTop:8}} onClick={handleSave} disabled={saving}>
{saved ? <><Check size={14}/> Gespeichert!</> : saving ? '' : 'Speichern'}
</button>
</div>
{/* Liste */}
<div className="section-gap">
<div className="card-title" style={{marginBottom:8}}>Verlauf ({entries.length})</div>
{entries.length===0 && <p className="muted">Noch keine Einträge.</p>}
{entries.map((e,i)=>{
const prev = entries[i+1]
const isEd = editing?.id===e.id
return (
<div key={e.id} className="card" style={{marginBottom:8}}>
{isEd ? (
<div>
<div className="form-row">
<label className="form-label">Datum</label>
<input type="date" className="form-input" style={{width:140}}
value={editing.date} onChange={ev=>setEditing(d=>({...d,date:ev.target.value}))}/>
<span className="form-unit"/>
</div>
{CIRCUMFERENCE_POINTS.map(p=>(
<div key={p.id} className="form-row">
<label className="form-label">{p.label}</label>
<input type="number" className="form-input" step={0.1} placeholder=""
value={editing[p.id]||''} onChange={ev=>setEditing(d=>({...d,[p.id]:ev.target.value}))}/>
<span className="form-unit">cm</span>
</div>
))}
<div style={{display:'flex',gap:6,marginTop:8}}>
<button className="btn btn-primary" style={{flex:1}} onClick={handleUpdate}><Check size={13}/> Speichern</button>
<button className="btn btn-secondary" style={{flex:1}} onClick={cancelEdit}><X size={13}/> Abbrechen</button>
</div>
</div>
) : (
<div>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<div style={{fontWeight:600,fontSize:14}}>{dayjs(e.date).format('DD. MMMM YYYY')}</div>
<div style={{display:'flex',gap:6}}>
<button className="btn btn-secondary" style={{padding:'5px 8px'}} onClick={()=>startEdit(e)}><Pencil size={13}/></button>
<button className="btn btn-danger" style={{padding:'5px 8px'}} onClick={()=>handleDelete(e.id)}><Trash2 size={13}/></button>
</div>
</div>
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'3px 12px'}}>
{FIELDS.map(k=>e[k]!=null?(
<div key={k} style={{display:'flex',justifyContent:'space-between',fontSize:13,padding:'2px 0',borderBottom:'1px solid var(--border)'}}>
<span style={{color:'var(--text3)'}}>{LABELS[k]}</span>
<span>{e[k]} cm{prev?.[k]!=null&&<span style={{fontSize:11,color:e[k]<prev[k]?'var(--accent)':e[k]>prev[k]?'var(--warn)':'var(--text3)',marginLeft:4}}>
{e[k]-prev[k]>0?'+':''}{Math.round((e[k]-prev[k])*10)/10}
</span>}</span>
</div>
):null)}
</div>
{e.notes && <p style={{fontSize:12,color:'var(--text2)',fontStyle:'italic',marginTop:6}}>"{e.notes}"</p>}
</div>
)}
</div>
)
})}
</div>
</div>
)
}