171 lines
8.3 KiB
JavaScript
171 lines
8.3 KiB
JavaScript
import { useState, useEffect, useRef } from 'react'
|
|
import { Pencil, Trash2, Check, X } from 'lucide-react'
|
|
import { api } from '../utils/api'
|
|
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, ReferenceLine } from 'recharts'
|
|
import dayjs from 'dayjs'
|
|
import 'dayjs/locale/de'
|
|
dayjs.locale('de')
|
|
|
|
function rollingAvg(arr, window=7) {
|
|
return arr.map((d,i)=>{
|
|
const s=arr.slice(Math.max(0,i-window+1),i+1).map(x=>x.weight).filter(v=>v!=null)
|
|
return s.length ? {...d, avg: Math.round(s.reduce((a,b)=>a+b,0)/s.length*10)/10} : d
|
|
})
|
|
}
|
|
|
|
export default function WeightScreen() {
|
|
const [entries, setEntries] = useState([])
|
|
const [editing, setEditing] = useState(null) // {id, date, weight, note}
|
|
const [newDate, setNewDate] = useState(dayjs().format('YYYY-MM-DD'))
|
|
const [newWeight,setNewWeight]= useState('')
|
|
const [newNote, setNewNote] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
const [saved, setSaved] = useState(false)
|
|
|
|
const load = () => api.listWeight(365).then(data => setEntries(data))
|
|
useEffect(()=>{ load() },[])
|
|
|
|
const handleSave = async () => {
|
|
if (!newWeight) return
|
|
setSaving(true)
|
|
try {
|
|
await api.upsertWeight(newDate, parseFloat(newWeight), newNote)
|
|
setSaved(true); await load()
|
|
setTimeout(()=>setSaved(false), 2000)
|
|
setNewWeight(''); setNewNote('')
|
|
} finally { setSaving(false) }
|
|
}
|
|
|
|
const handleUpdate = async () => {
|
|
if (!editing) return
|
|
await api.updateWeight(editing.id, editing.date, parseFloat(editing.weight), editing.note||'')
|
|
setEditing(null); await load()
|
|
}
|
|
|
|
const handleDelete = async (id) => {
|
|
if (!confirm('Eintrag löschen?')) return
|
|
await api.deleteWeight(id); await load()
|
|
}
|
|
|
|
const sorted = [...entries].sort((a,b)=>a.date.localeCompare(b.date))
|
|
const withAvg = rollingAvg(sorted)
|
|
const chartData = withAvg.map(d=>({date:dayjs(d.date).format('DD.MM'), weight:d.weight, avg:d.avg}))
|
|
const weights = entries.map(e=>e.weight)
|
|
const avgAll = weights.length ? Math.round(weights.reduce((a,b)=>a+b,0)/weights.length*10)/10 : null
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="page-title">Gewicht</h1>
|
|
|
|
{/* Eingabe */}
|
|
<div className="card section-gap">
|
|
<div className="card-title">Eintrag hinzufügen</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Datum</label>
|
|
<input type="date" className="form-input" style={{width:140}}
|
|
value={newDate} onChange={e=>setNewDate(e.target.value)}/>
|
|
<span className="form-unit"/>
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Gewicht</label>
|
|
<input type="number" className="form-input" min={20} max={300} step={0.1}
|
|
placeholder="kg" value={newWeight} onChange={e=>setNewWeight(e.target.value)}
|
|
onKeyDown={e=>e.key==='Enter'&&handleSave()}/>
|
|
<span className="form-unit">kg</span>
|
|
</div>
|
|
<div className="form-row">
|
|
<label className="form-label">Notiz</label>
|
|
<input type="text" className="form-input" placeholder="optional"
|
|
value={newNote} onChange={e=>setNewNote(e.target.value)}/>
|
|
<span className="form-unit"/>
|
|
</div>
|
|
<button className="btn btn-primary btn-full" onClick={handleSave} disabled={saving||!newWeight}>
|
|
{saved ? <><Check size={15}/> Gespeichert!</>
|
|
: saving ? <><div className="spinner" style={{width:14,height:14}}/> …</>
|
|
: 'Speichern'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Chart */}
|
|
{chartData.length >= 2 && (
|
|
<div className="card section-gap">
|
|
<div className="card-title">Verlauf ({entries.length} Einträge)</div>
|
|
<ResponsiveContainer width="100%" height={180}>
|
|
<LineChart data={chartData} margin={{top:4,right:8,bottom:0,left:-20}}>
|
|
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3"/>
|
|
<XAxis dataKey="date" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false}
|
|
interval={Math.max(0,Math.floor(chartData.length/6)-1)}/>
|
|
<YAxis tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>
|
|
{avgAll && <ReferenceLine y={avgAll} stroke="var(--text3)" strokeDasharray="4 4" strokeWidth={1}/>}
|
|
<Tooltip contentStyle={{background:'var(--surface)',border:'1px solid var(--border)',borderRadius:8,fontSize:11}}
|
|
formatter={(v,n)=>[`${v} kg`, n==='weight'?'Gewicht':'Ø 7 Tage']}/>
|
|
<Line type="monotone" dataKey="weight" stroke="#378ADD" strokeWidth={1.5} dot={{r:2,fill:'#378ADD'}} name="weight"/>
|
|
<Line type="monotone" dataKey="avg" stroke="#1D9E75" strokeWidth={2.5} dot={false} name="avg"/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
{weights.length >= 2 && (
|
|
<div style={{display:'flex',gap:8,marginTop:10}}>
|
|
{[['Min',Math.min(...weights),'var(--accent)'],['Max',Math.max(...weights),'var(--warn)'],['Ø',avgAll,'var(--text2)']].map(([l,v,c])=>(
|
|
<div key={l} style={{flex:1,background:'var(--surface2)',borderRadius:8,padding:'6px 10px',textAlign:'center'}}>
|
|
<div style={{fontSize:15,fontWeight:700,color:c}}>{v} kg</div>
|
|
<div style={{fontSize:10,color:'var(--text3)'}}>{l}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Liste */}
|
|
<div className="card section-gap">
|
|
<div className="card-title">Alle Einträge</div>
|
|
{entries.length===0 && <p className="muted">Noch keine Einträge.</p>}
|
|
{entries.map((e,i)=>{
|
|
const prev = entries[i+1]
|
|
const delta = prev ? Math.round((e.weight-prev.weight)*10)/10 : null
|
|
const isEditing = editing?.id===e.id
|
|
return (
|
|
<div key={e.id} style={{borderBottom:'1px solid var(--border)',padding:'8px 0'}}>
|
|
{isEditing ? (
|
|
<div style={{display:'flex',flexDirection:'column',gap:6}}>
|
|
<div style={{display:'flex',gap:8}}>
|
|
<input type="date" className="form-input" style={{flex:1}}
|
|
value={editing.date} onChange={ev=>setEditing(ed=>({...ed,date:ev.target.value}))}/>
|
|
<input type="number" className="form-input" style={{width:80}} step={0.1}
|
|
value={editing.weight} onChange={ev=>setEditing(ed=>({...ed,weight:ev.target.value}))}/>
|
|
<span style={{alignSelf:'center',fontSize:13,color:'var(--text3)'}}>kg</span>
|
|
</div>
|
|
<input type="text" className="form-input" placeholder="Notiz"
|
|
value={editing.note||''} onChange={ev=>setEditing(ed=>({...ed,note:ev.target.value}))}/>
|
|
<div style={{display:'flex',gap:6}}>
|
|
<button className="btn btn-primary" style={{flex:1}} onClick={handleUpdate}><Check size={14}/> Speichern</button>
|
|
<button className="btn btn-secondary" style={{flex:1}} onClick={()=>setEditing(null)}><X size={14}/> Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div style={{display:'flex',alignItems:'center',gap:10}}>
|
|
<div style={{flex:1}}>
|
|
<div style={{fontSize:15,fontWeight:600}}>{e.weight} kg
|
|
{delta!==null && <span style={{marginLeft:8,fontSize:12,fontWeight:600,color:delta<0?'var(--accent)':delta>0?'var(--warn)':'var(--text3)'}}>
|
|
{delta>0?'+':''}{delta} kg
|
|
</span>}
|
|
</div>
|
|
<div style={{fontSize:11,color:'var(--text3)'}}>{dayjs(e.date).format('dd, DD. MMMM YYYY')}</div>
|
|
{e.note && <div style={{fontSize:11,color:'var(--text2)',fontStyle:'italic'}}>{e.note}</div>}
|
|
</div>
|
|
<button className="btn btn-secondary" style={{padding:'5px 8px'}} onClick={()=>setEditing({...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>
|
|
</div>
|
|
)
|
|
}
|