From 0f072f47356c40533c79155aabf74046f6fb80da Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 21 Mar 2026 08:26:47 +0100 Subject: [PATCH] feat: add nutrition entry editing and import history Features: - Import history panel showing all CSV imports with date, count, and range - Edit/delete functionality for nutrition entries (inline editing) - New backend endpoints: GET /import-history, PUT /{id}, DELETE /{id} UI Changes: - Import history displayed under import panel - "Daten" tab now has edit/delete buttons per entry - Inline form for editing macros (kcal, protein, fat, carbs) - Confirmation dialog for deletion Backend: - nutrition.py: Added import_history, update_nutrition, delete_nutrition endpoints - Groups imports by created date to show history Frontend: - NutritionPage: New DataTab and ImportHistory components - api.js: Added nutritionImportHistory, updateNutrition, deleteNutrition Co-Authored-By: Claude Opus 4.6 --- backend/routers/nutrition.py | 58 +++++++ frontend/src/pages/NutritionPage.jsx | 247 +++++++++++++++++++++++---- frontend/src/utils/api.js | 3 + 3 files changed, 278 insertions(+), 30 deletions(-) diff --git a/backend/routers/nutrition.py b/backend/routers/nutrition.py index 223dacc..6738331 100644 --- a/backend/routers/nutrition.py +++ b/backend/routers/nutrition.py @@ -163,3 +163,61 @@ def nutrition_weekly(weeks: int=16, x_profile_id: Optional[str]=Header(default=N def avg(k): return round(sum(float(e.get(k) or 0) for e in en)/n,1) result.append({'week':wk,'days':n,'kcal':avg('kcal'),'protein_g':avg('protein_g'),'fat_g':avg('fat_g'),'carbs_g':avg('carbs_g')}) return result + + +@router.get("/import-history") +def import_history(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): + """Get import history by grouping entries by created timestamp.""" + pid = get_pid(x_profile_id) + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT + DATE(created) as import_date, + COUNT(*) as count, + MIN(date) as date_from, + MAX(date) as date_to, + MAX(created) as last_created + FROM nutrition_log + WHERE profile_id=%s AND source='csv' + GROUP BY DATE(created) + ORDER BY DATE(created) DESC + """, (pid,)) + return [r2d(r) for r in cur.fetchall()] + + +@router.put("/{entry_id}") +def update_nutrition(entry_id: str, kcal: float, protein_g: float, fat_g: float, carbs_g: float, + x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): + """Update nutrition entry macros.""" + pid = get_pid(x_profile_id) + with get_db() as conn: + cur = get_cursor(conn) + # Verify ownership + cur.execute("SELECT id FROM nutrition_log WHERE id=%s AND profile_id=%s", (entry_id, pid)) + if not cur.fetchone(): + raise HTTPException(404, "Eintrag nicht gefunden") + + cur.execute(""" + UPDATE nutrition_log + SET kcal=%s, protein_g=%s, fat_g=%s, carbs_g=%s + WHERE id=%s AND profile_id=%s + """, (round(kcal,1), round(protein_g,1), round(fat_g,1), round(carbs_g,1), entry_id, pid)) + + return {"success": True} + + +@router.delete("/{entry_id}") +def delete_nutrition(entry_id: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): + """Delete nutrition entry.""" + pid = get_pid(x_profile_id) + with get_db() as conn: + cur = get_cursor(conn) + # Verify ownership + cur.execute("SELECT id FROM nutrition_log WHERE id=%s AND profile_id=%s", (entry_id, pid)) + if not cur.fetchone(): + raise HTTPException(404, "Eintrag nicht gefunden") + + cur.execute("DELETE FROM nutrition_log WHERE id=%s AND profile_id=%s", (entry_id, pid)) + + return {"success": True} diff --git a/frontend/src/pages/NutritionPage.jsx b/frontend/src/pages/NutritionPage.jsx index c2531fa..7ab28e9 100644 --- a/frontend/src/pages/NutritionPage.jsx +++ b/frontend/src/pages/NutritionPage.jsx @@ -18,6 +18,220 @@ function rollingAvg(arr, key, window=7) { }) } +// ── 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 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) + } + } + + if (entries.length === 0) { + return ( +
+
Alle Einträge (0)
+

Noch keine Ernährungsdaten. Importiere FDDB CSV oben.

+
+ ) + } + + return ( +
+
Alle Einträge ({entries.length})
+ {error && ( +
+ {error} +
+ )} + {entries.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() @@ -337,7 +551,7 @@ export default function NutritionPage() { nutritionApi.nutritionCorrelations(), nutritionApi.nutritionWeekly(16), nutritionApi.listNutrition(365), // BUG-002 fix: load raw entries - api.getActiveProfile(), + nutritionApi.getActiveProfile(), ]) setCorr(Array.isArray(corr)?corr:[]) setWeekly(Array.isArray(wkly)?wkly:[]) @@ -355,6 +569,7 @@ export default function NutritionPage() {

Ernährung

+ {loading &&
} @@ -378,35 +593,7 @@ export default function NutritionPage() {
{tab==='data' && ( -
-
Alle Einträge ({entries.length})
- {entries.length === 0 && ( -

Noch keine Ernährungsdaten. Importiere FDDB CSV oben.

- )} - {entries.map((e, i) => ( -
-
- {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} -
- )} -
- ))} -
+ )} {tab==='overview' && ( diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 62a5181..e8cbfde 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -82,6 +82,9 @@ export const api = { listNutrition: (l=365) => req(`/nutrition?limit=${l}`), nutritionCorrelations: () => req('/nutrition/correlations'), nutritionWeekly: (w=16) => req(`/nutrition/weekly?weeks=${w}`), + nutritionImportHistory: () => req('/nutrition/import-history'), + updateNutrition: (id,kcal,protein,fat,carbs) => req(`/nutrition/${id}?kcal=${kcal}&protein_g=${protein}&fat_g=${fat}&carbs_g=${carbs}`,{method:'PUT'}), + deleteNutrition: (id) => req(`/nutrition/${id}`,{method:'DELETE'}), // Stats & AI getStats: () => req('/stats'),