import { useState, useEffect, useRef } from 'react' import { Upload, CheckCircle, TrendingUp, Info } from 'lucide-react' import UsageBadge from '../components/UsageBadge' import NutritionFoodMap, { DayMarkButtons } from '../components/NutritionFoodMap' import { LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, ReferenceLine, ScatterChart, Scatter } from 'recharts' import { api as nutritionApi } from '../utils/api' import { MACRO_CHART } from '../utils/macroChartTheme' import dayjs from 'dayjs' import isoWeek from 'dayjs/plugin/isoWeek' dayjs.extend(isoWeek) // ── Helpers ─────────────────────────────────────────────────────────────────── const KCAL_PER_KG_FAT = 7700 function rollingAvg(arr, key, window=7) { return arr.map((d,i) => { const slice = arr.slice(Math.max(0,i-window+1),i+1).map(x=>x[key]).filter(v=>v!=null) return slice.length ? {...d, [`${key}_avg`]: Math.round(slice.reduce((a,b)=>a+b,0)/slice.length*10)/10} : d }) } // ── Entry Form (Create/Update) ─────────────────────────────────────────────── function EntryForm({ onSaved }) { const [date, setDate] = useState(dayjs().format('YYYY-MM-DD')) const [values, setValues] = useState({ kcal: '', protein_g: '', fat_g: '', carbs_g: '' }) const [existingId, setExistingId] = useState(null) const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) // Load data for selected date useEffect(() => { const load = async () => { if (!date) return setLoading(true) setError(null) try { const data = await nutritionApi.getNutritionByDate(date) if (data) { setValues({ kcal: data.kcal || '', protein_g: data.protein_g || '', fat_g: data.fat_g || '', carbs_g: data.carbs_g || '' }) setExistingId(data.id) } else { setValues({ kcal: '', protein_g: '', fat_g: '', carbs_g: '' }) setExistingId(null) } } catch(e) { console.error('Failed to load entry:', e) } finally { setLoading(false) } } load() }, [date]) const handleSave = async () => { if (!date || !values.kcal) { setError('Datum und Kalorien sind Pflichtfelder') return } setSaving(true) setError(null) setSuccess(null) try { const result = await nutritionApi.createNutrition( date, parseFloat(values.kcal) || 0, parseFloat(values.protein_g) || 0, parseFloat(values.fat_g) || 0, parseFloat(values.carbs_g) || 0 ) setSuccess(result.mode === 'created' ? 'Eintrag hinzugefügt' : 'Eintrag aktualisiert') setTimeout(() => setSuccess(null), 3000) onSaved() } catch(e) { if (e.message.includes('Limit erreicht')) { setError(e.message) } else { setError('Speichern fehlgeschlagen: ' + e.message) } setTimeout(() => setError(null), 5000) } finally { setSaving(false) } } return (
Eintrag hinzufügen / bearbeiten
{error && (
{error}
)} {success && (
✓ {success}
)}
setDate(e.target.value)} max={dayjs().format('YYYY-MM-DD')} style={{width:'100%'}} /> {existingId && !loading && (
ℹ️ Eintrag existiert bereits – wird beim Speichern aktualisiert
)}
setValues({...values, kcal: e.target.value})} placeholder="z.B. 2000" disabled={loading} style={{width:'100%'}} />
setValues({...values, protein_g: e.target.value})} placeholder="z.B. 150" disabled={loading} style={{width:'100%'}} />
setValues({...values, fat_g: e.target.value})} placeholder="z.B. 80" disabled={loading} style={{width:'100%'}} />
setValues({...values, carbs_g: e.target.value})} placeholder="z.B. 200" disabled={loading} style={{width:'100%'}} />
) } // ── Data Tab (Editable Entry List) ─────────────────────────────────────────── function DayItems({ date }) { const [items, setItems] = useState(null) const [open, setOpen] = useState(false) const [error, setError] = useState(null) const toggle = async () => { if (open) { setOpen(false) return } if (!items) { try { setItems(await nutritionApi.listNutritionItems(date)) } catch (e) { setError(e.message) return } } setOpen(true) } return (
{error &&

{error}

} {open && Array.isArray(items) && ( )}
) } function DataTab({ entries, onUpdate }) { const [editId, setEditId] = useState(null) const [editValues, setEditValues] = useState({}) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [filter, setFilter] = useState('30') // days to show (7, 30, 90, 'all') const startEdit = (e) => { setEditId(e.id) setEditValues({ kcal: e.kcal || 0, protein_g: e.protein_g || 0, fat_g: e.fat_g || 0, carbs_g: e.carbs_g || 0 }) } const cancelEdit = () => { setEditId(null) setEditValues({}) setError(null) } const saveEdit = async (id) => { setSaving(true) setError(null) try { await nutritionApi.updateNutrition( id, editValues.kcal, editValues.protein_g, editValues.fat_g, editValues.carbs_g ) setEditId(null) setEditValues({}) onUpdate() } catch(e) { setError('Speichern fehlgeschlagen: ' + e.message) } finally { setSaving(false) } } const deleteEntry = async (id, date) => { if (!confirm(`Eintrag vom ${dayjs(date).format('DD.MM.YYYY')} wirklich löschen?`)) return try { await nutritionApi.deleteNutrition(id) onUpdate() } catch(e) { setError('Löschen fehlgeschlagen: ' + e.message) } } // Filter entries by date range const filteredEntries = filter === 'all' ? entries : entries.filter(e => { const daysDiff = dayjs().diff(dayjs(e.date), 'day') return daysDiff <= parseInt(filter) }) if (entries.length === 0) { return (
Alle Einträge (0)

Noch keine Ernährungsdaten. Importiere FDDB CSV oben.

) } return (
Alle Einträge ({filteredEntries.length}{filteredEntries.length !== entries.length ? ` von ${entries.length}` : ''})
{error && (
{error}
)} {filteredEntries.map((e, i) => { const isEditing = editId === e.id return (
{!isEditing ? ( <>
{dayjs(e.date).format('dd, DD. MMMM YYYY')}
{Math.round(e.kcal || 0)} kcal
🥩 Protein: {Math.round(e.protein_g || 0)}g 🫙 Fett: {Math.round(e.fat_g || 0)}g 🍞 Kohlenhydrate: {Math.round(e.carbs_g || 0)}g
{e.source && (
Quelle: {e.source}{e.macro_origin ? ` · Makros: ${e.macro_origin}` : ''} {e.mark_type ? ` · ${e.mark_type === 'fasting' ? 'Fastentag' : 'unvollständig'}` : ''} {e.item_count ? ` · ${e.item_count} Lebensmittel` : ''}
)} {e.item_count > 0 ? ( ) : (
Keine einzelnen Lebensmittelzeilen
)}
) : ( <>
{dayjs(e.date).format('dd, DD. MMMM YYYY')}
setEditValues({...editValues, kcal: parseFloat(e.target.value)||0})} style={{width:'100%'}}/>
setEditValues({...editValues, protein_g: parseFloat(e.target.value)||0})} style={{width:'100%'}}/>
setEditValues({...editValues, fat_g: parseFloat(e.target.value)||0})} style={{width:'100%'}}/>
setEditValues({...editValues, carbs_g: parseFloat(e.target.value)||0})} style={{width:'100%'}}/>
)}
) })}
) } // ── Import History ──────────────────────────────────────────────────────────── function ImportHistory() { const [history, setHistory] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { const load = async () => { try { const data = await nutritionApi.nutritionImportHistory() setHistory(Array.isArray(data) ? data : []) } catch(e) { console.error('Failed to load import history:', e) } finally { setLoading(false) } } load() }, []) if (loading) return null if (!history.length) return null return (
Import-Historie
{history.map((h, i) => (
{dayjs(h.import_date).format('DD.MM.YYYY')} {dayjs(h.last_created).format('HH:mm')} Uhr
{h.count} {h.count === 1 ? 'Eintrag' : 'Einträge'} {h.date_from && h.date_to && ( ({dayjs(h.date_from).format('DD.MM.YY')} – {dayjs(h.date_to).format('DD.MM.YY')}) )}
))}
) } // ── Import Panel ────────────────────────────────────────────────────────────── function ImportPanel({ onImported, usage = null }) { const fileRef = useRef() const [status, setStatus] = useState(null) const [error, setError] = useState(null) const [dragging,setDragging]= useState(false) const [tab, setTab] = useState('file') // 'file' | 'paste' const [pasteText, setPasteText] = useState('') const [overwrite, setOverwrite] = useState(false) const [conflicts, setConflicts] = useState([]) const atLimit = usage && !usage.allowed const runImport = async (file) => { if (atLimit) return setStatus('loading'); setError(null); setConflicts([]) try { const result = await nutritionApi.importCsv(file, overwrite) if (result.days_imported === undefined) throw new Error(JSON.stringify(result)) setStatus(result) setConflicts(result.conflicts || []) onImported() } catch(err) { setError('Import fehlgeschlagen: ' + err.message) setStatus(null) } } const handleFile = async e => { const file = e.target.files[0]; if (!file) return await runImport(file) e.target.value = '' } const handleDrop = async e => { e.preventDefault(); setDragging(false) const file = e.dataTransfer.files[0] if (!file) return await runImport(file) } const handlePasteImport = async () => { if (!pasteText.trim() || atLimit) return const blob = new Blob([pasteText], { type: 'text/csv' }) const file = new File([blob], 'paste.csv', { type: 'text/csv' }) await runImport(file) } return (
📥 FDDB CSV Import {usage && }

In FDDB: Mein Tagebuch → Exportieren → CSV — dann hier importieren. Zeilen (Name, Menge, Uhrzeit) werden gespeichert, wenn die CSV sie enthält.

{/* Tab switcher */}
{[['file','📁 Datei / Drag & Drop'],['paste','📋 Text einfügen']].map(([k,l])=>( ))}
{tab==='file' && ( <> {/* Drag & Drop Zone */}
{ if(!atLimit){ e.preventDefault(); setDragging(true) }}} onDragLeave={()=>setDragging(false)} onDrop={e=>{ e.preventDefault(); setDragging(false); if(atLimit) return; handleDrop(e) }} onClick={()=>{ if(!atLimit) fileRef.current?.click() }} title={atLimit ? `Limit erreicht (${usage.used}/${usage.limit})` : ''} style={{ border:`2px dashed ${dragging?'var(--accent)':'var(--border2)'}`, borderRadius:10, padding:'24px 16px', textAlign:'center', background: dragging?'var(--accent-light)':'var(--surface2)', cursor:atLimit?'not-allowed':'pointer', opacity:atLimit?0.65:1, transition:'all 0.15s', }}>
{atLimit ? '🔒 Limit erreicht — Import nicht möglich' : dragging ? 'Datei loslassen…' : 'CSV hierher ziehen oder tippen zum Auswählen'}
.csv Dateien
)} {tab==='paste' && ( <>