2026-03-16 13:35:11 +01:00
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'
2026-03-21 06:43:10 +01:00
import UsageBadge from '../components/UsageBadge'
2026-03-16 13:35:11 +01:00
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 )
2026-03-21 07:14:34 +01:00
const [ error , setError ] = useState ( null )
2026-03-21 06:43:10 +01:00
const [ weightUsage , setWeightUsage ] = useState ( null ) // Phase 3: Usage badge
2026-03-16 13:35:11 +01:00
const load = ( ) => api . listWeight ( 365 ) . then ( data => setEntries ( data ) )
2026-03-21 06:43:10 +01:00
2026-03-21 07:14:34 +01:00
const loadUsage = ( ) => {
2026-03-21 06:43:10 +01:00
// Load feature usage for badge
api . getFeatureUsage ( ) . then ( features => {
const weightFeature = features . find ( f => f . feature _id === 'weight_entries' )
setWeightUsage ( weightFeature )
} ) . catch ( err => console . error ( 'Failed to load usage:' , err ) )
2026-03-21 07:14:34 +01:00
}
useEffect ( ( ) => {
load ( )
loadUsage ( )
2026-03-21 06:43:10 +01:00
} , [ ] )
2026-03-16 13:35:11 +01:00
const handleSave = async ( ) => {
if ( ! newWeight ) return
setSaving ( true )
2026-03-21 07:14:34 +01:00
setError ( null )
2026-03-16 13:35:11 +01:00
try {
await api . upsertWeight ( newDate , parseFloat ( newWeight ) , newNote )
2026-03-21 07:14:34 +01:00
setSaved ( true )
await load ( )
await loadUsage ( ) // Reload usage after save
2026-03-16 13:35:11 +01:00
setTimeout ( ( ) => setSaved ( false ) , 2000 )
setNewWeight ( '' ) ; setNewNote ( '' )
2026-03-21 07:14:34 +01:00
} catch ( err ) {
console . error ( 'Save failed:' , err )
setError ( err . message || 'Fehler beim Speichern' )
setTimeout ( ( ) => setError ( null ) , 5000 )
} finally {
setSaving ( false )
}
2026-03-16 13:35:11 +01:00
}
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 (
2026-04-05 10:14:07 +02:00
< div className = "capture-page" >
2026-03-16 13:35:11 +01:00
< h1 className = "page-title" > Gewicht < / h1 >
{ /* Eingabe */ }
< div className = "card section-gap" >
2026-03-21 06:54:45 +01:00
< div className = "card-title badge-container-right" >
2026-03-21 06:50:12 +01:00
< span > Eintrag hinzufügen < / span >
2026-03-21 06:43:10 +01:00
{ weightUsage && < UsageBadge { ...weightUsage } / > }
< / div >
2026-03-16 13:35:11 +01:00
< 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 >
2026-03-21 07:14:34 +01:00
{ error && (
< div style = { { padding : '10px' , background : 'var(--danger-bg)' , border : '1px solid var(--danger)' , borderRadius : 8 , fontSize : 13 , color : 'var(--danger)' , marginBottom : 12 } } >
{ error }
< / div >
) }
2026-03-21 07:25:47 +01:00
< div
2026-03-21 07:14:34 +01:00
title = { weightUsage && ! weightUsage . allowed ? ` Limit erreicht ( ${ weightUsage . used } / ${ weightUsage . limit } ). Kontaktiere den Admin oder warte bis zum nächsten Reset. ` : '' }
2026-03-21 07:25:47 +01:00
style = { { display : 'inline-block' , width : '100%' } }
2026-03-21 07:14:34 +01:00
>
2026-03-21 07:25:47 +01:00
< button
className = "btn btn-primary btn-full"
onClick = { handleSave }
disabled = { saving || ! newWeight || ( weightUsage && ! weightUsage . allowed ) }
style = { { cursor : ( weightUsage && ! weightUsage . allowed ) ? 'not-allowed' : 'pointer' } }
>
{ saved ? < > < Check size = { 15 } / > Gespeichert ! < / >
: saving ? < > < div className = "spinner" style = { { width : 14 , height : 14 } } / > … < / >
: ( weightUsage && ! weightUsage . allowed ) ? '🔒 Limit erreicht'
: 'Speichern' }
< / button >
< / div >
2026-03-16 13:35:11 +01:00
< / 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 >
)
}