import { useState, useEffect, useRef } from 'react' import { Upload, CheckCircle, TrendingUp, Info } from 'lucide-react' import { LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, ReferenceLine, ScatterChart, Scatter } from 'recharts' import { api as nutritionApi } from '../utils/api' 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 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}
)} ) : ( <>
{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 }) { 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 runImport = async (file) => { setStatus('loading'); setError(null) try { const result = await nutritionApi.importCsv(file) if (result.days_imported === undefined) throw new Error(JSON.stringify(result)) setStatus(result) 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()) 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

In FDDB: Mein Tagebuch → Exportieren → CSV — dann hier importieren.

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