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