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

210 lines
9.5 KiB
React
Raw Normal View History

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 UsageBadge from '../components/UsageBadge'
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 [error, setError] = useState(null)
const [photoFile, setPhotoFile] = useState(null)
const [photoPreview, setPhotoPreview] = useState(null)
const [circumUsage, setCircumUsage] = useState(null) // Phase 4: Usage badge
const fileRef = useRef()
const nav = useNavigate()
const load = () => api.listCirc().then(setEntries)
const loadUsage = () => {
api.getFeatureUsage().then(features => {
const circumFeature = features.find(f => f.feature_id === 'circumference_entries')
setCircumUsage(circumFeature)
}).catch(err => console.error('Failed to load usage:', err))
}
useEffect(()=>{
load()
loadUsage()
},[])
const set = (k,v) => setForm(f=>({...f,[k]:v}))
const handleSave = async () => {
setSaving(true)
setError(null)
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()
await loadUsage() // Reload usage after save
setTimeout(()=>setSaved(false),2000)
setForm(empty()); setPhotoFile(null); setPhotoPreview(null)
} catch (err) {
console.error('Save failed:', err)
setError(err.message || 'Fehler beim Speichern')
setTimeout(()=>setError(null), 5000)
} 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 badge-container-right">
<span>Neue Messung</span>
{circumUsage && <UsageBadge {...circumUsage} />}
</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>
{error && (
<div style={{padding:'10px',background:'var(--danger-bg)',border:'1px solid var(--danger)',borderRadius:8,fontSize:13,color:'var(--danger)',marginTop:8}}>
{error}
</div>
)}
<div
title={circumUsage && !circumUsage.allowed ? `Limit erreicht (${circumUsage.used}/${circumUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
style={{display:'inline-block',width:'100%',marginTop:8}}
>
<button
className="btn btn-primary btn-full"
style={{cursor: (circumUsage && !circumUsage.allowed) ? 'not-allowed' : 'pointer'}}
onClick={handleSave}
disabled={saving || (circumUsage && !circumUsage.allowed)}
>
{saved ? <><Check size={14}/> Gespeichert!</>
: saving ? '…'
: (circumUsage && !circumUsage.allowed) ? '🔒 Limit erreicht'
: 'Speichern'}
</button>
</div>
</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>
)
}