mitai-jinkendo/frontend/src/pages/CircumScreen.jsx
Lars d13c2c7e25
All checks were successful
Deploy Development / deploy (push) Successful in 35s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
fix: add dashboard weight enforcement and fix hover tooltips
- Dashboard QuickWeight: Feature limit enforcement hinzugefügt
- Hover-Tooltip Fix: Button in div wrapper (disabled buttons zeigen keine nativen tooltips)
- Error handling für Dashboard weight input
- Konsistentes UX über alle Eingabe-Screens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 07:25:47 +01:00

210 lines
9.5 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 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>
)
}