mitai-jinkendo/frontend/src/pages/WeightScreen.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

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>
)
}