"use client"; import { ArrowLeftIcon, Cog6ToothIcon, PlusIcon, ShoppingCartIcon, TrashIcon, } from "@heroicons/react/24/outline"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { CatalogFood, MealType, PortionUnit, Recipe, calcRecipe, todayKey, weekDates, } from "@/lib/hellth-data"; import { trpc } from "@/lib/trpc"; type ApiMealType = "breakfast" | "lunch" | "dinner" | "snack"; const mealTypes: MealType[] = ["Frühstück", "Mittagessen", "Abendessen", "Snack"]; const mealTypeToApi: Record = { Frühstück: "breakfast", Mittagessen: "lunch", Abendessen: "dinner", Snack: "snack", }; const mealTypeFromApi: Record = { breakfast: "Frühstück", lunch: "Mittagessen", dinner: "Abendessen", snack: "Snack", }; function toNumber(value: unknown) { return Number(value) || 0; } function parseDateKey(key: string) { return new Date(`${key}T12:00:00`); } function apiFoodToCatalog(food: { id: string; name: string; brand: string | null; category: string; kcalPer100g: unknown; proteinPer100g: unknown; carbsPer100g: unknown; fatPer100g: unknown; servingG: unknown; preferredUnit: PortionUnit | null; densityGPerMl: unknown | null; source: "seed" | "manual" | "openfoodfacts" | "ocr" | "community"; barcode: string | null; availableEverywhere: boolean; }): CatalogFood { return { id: food.id, name: food.name, brand: food.brand ?? undefined, category: food.category, kcalPer100g: toNumber(food.kcalPer100g), proteinPer100g: toNumber(food.proteinPer100g), carbsPer100g: toNumber(food.carbsPer100g), fatPer100g: toNumber(food.fatPer100g), servingG: toNumber(food.servingG) || 100, preferredUnit: food.preferredUnit ?? "g", densityGPerMl: food.densityGPerMl == null ? undefined : toNumber(food.densityGPerMl), source: food.source, barcode: food.barcode ?? undefined, availableEverywhere: food.availableEverywhere, }; } function apiRecipeToRecipe(recipe: { id: string; name: string; portions: number; minutes: number; notes: string | null; ingredients: Array<{ foodId: string; amountG: unknown; displayAmount: unknown | null; displayUnit: PortionUnit | null; portionId: string | null; }>; }): Recipe { return { id: recipe.id, name: recipe.name, portions: recipe.portions, minutes: recipe.minutes, notes: recipe.notes ?? undefined, ingredients: recipe.ingredients.map((ingredient) => ({ foodId: ingredient.foodId, amountG: toNumber(ingredient.amountG), amount: ingredient.displayAmount == null ? undefined : toNumber(ingredient.displayAmount), unit: ingredient.displayUnit ?? undefined, portionId: ingredient.portionId ?? undefined, })), }; } function foodSwatch(name: string) { const colors = [ "from-amber-100 to-orange-200 text-orange-700", "from-emerald-100 to-lime-200 text-emerald-700", "from-sky-100 to-blue-200 text-blue-700", "from-rose-100 to-pink-200 text-rose-700", ]; return colors[name.length % colors.length] ?? colors[0]; } function RecipeAvatar({ name }: { name: string }) { const initials = name .split(" ") .filter(Boolean) .slice(0, 2) .map((part) => part[0]) .join("") .toUpperCase(); return (
{initials}
); } export default function PlanungPage() { const utils = trpc.useUtils(); const [selectedDate, setSelectedDate] = useState(todayKey()); const [recipeId, setRecipeId] = useState(""); const [mealType, setMealType] = useState("Mittagessen"); const [portions, setPortions] = useState("1"); const [plannedCalories, setPlannedCalories] = useState(""); const [status, setStatus] = useState(null); const days = useMemo(() => weekDates(selectedDate), [selectedDate]); const startsOn = days[0]!; const endsOn = days[6]!; const foodsQuery = trpc.health.listFoods.useQuery({ query: "" }); const recipesQuery = trpc.health.listRecipes.useQuery({ query: "" }); const plansQuery = trpc.health.mealPlansRange.useQuery({ startsOn, endsOn }); const savePlanMutation = trpc.health.upsertMealPlan.useMutation({ onSuccess: async () => { await utils.health.mealPlansRange.invalidate({ startsOn, endsOn }); setStatus("Gericht geplant."); }, }); const deletePlanMutation = trpc.health.deleteMealPlan.useMutation({ onSuccess: async () => { await utils.health.mealPlansRange.invalidate({ startsOn, endsOn }); setStatus("Eintrag entfernt."); }, }); const generateShoppingListMutation = trpc.health.generateShoppingList.useMutation({ onSuccess: async () => { await utils.health.shoppingLists.invalidate(); setStatus("Einkaufsliste aus der aktuellen Woche erstellt."); }, }); const foods = useMemo(() => (foodsQuery.data ?? []).map(apiFoodToCatalog), [foodsQuery.data]); const recipes = useMemo(() => (recipesQuery.data ?? []).map(apiRecipeToRecipe), [recipesQuery.data]); useEffect(() => { if (recipeId || !recipes[0]) return; setRecipeId(recipes[0].id); }, [recipeId, recipes]); const selectedMeals = useMemo( () => (plansQuery.data ?? []).filter((meal) => meal.date === selectedDate), [plansQuery.data, selectedDate], ); const selectedRecipe = useMemo(() => recipes.find((item) => item.id === recipeId) ?? null, [recipeId, recipes]); const calculatedCalories = useMemo(() => { if (!selectedRecipe) return 0; const totals = calcRecipe(selectedRecipe, foods, selectedRecipe.portions); return Math.round(totals.kcal * (Number(portions) || 1)); }, [foods, portions, selectedRecipe]); const mealsByType = useMemo(() => { return mealTypes.reduce>((groups, type) => { groups[type] = selectedMeals.filter((meal) => mealTypeFromApi[meal.mealType as ApiMealType] === type); return groups; }, {} as Record); }, [selectedMeals]); const rangeLabel = `${parseDateKey(startsOn).toLocaleDateString("de-DE", { day: "2-digit", month: "short" })} - ${parseDateKey(endsOn).toLocaleDateString("de-DE", { day: "2-digit", month: "short" })}`; function addPlannedMeal() { const recipe = selectedRecipe; if (!recipe) return; savePlanMutation.mutate({ date: selectedDate, mealType: mealTypeToApi[mealType], recipeId: recipe.id, name: recipe.name, portions: Number(portions) || 1, plannedCalories: Number(plannedCalories) || calculatedCalories, note: recipe.notes ?? null, }); } function refreshShoppingList() { generateShoppingListMutation.mutate({ startsOn, endsOn, name: `Einkauf ${rangeLabel}`, }); } return (

Ernährungsplan

{rangeLabel}

{days.map((dateKey) => { const date = parseDateKey(dateKey); const active = dateKey === selectedDate; const count = (plansQuery.data ?? []).filter((meal) => meal.date === dateKey).length; return ( ); })}
setPortions(event.target.value)} placeholder="Portionen" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm font-semibold" /> setPlannedCalories(event.target.value)} placeholder={`${calculatedCalories || 0} kcal`} className="rounded-2xl border border-gray-200 px-4 py-3 text-sm font-semibold" />
{status ?

{status}

: null}
{mealTypes.map((type) => (

{type}

{mealsByType[type].map((meal) => { const recipe = recipes.find((item) => item.id === meal.recipeId); const totals = recipe ? calcRecipe(recipe, foods, recipe.portions) : null; const calories = Number(meal.plannedCalories) || (totals ? Math.round(totals.kcal * Number(meal.portions)) : 0); return (

{meal.name}

{Number(meal.portions)} Portion(en) {calories ? ` · ${calories} kcal geplant` : ""}

); })} {!mealsByType[type].length ?

Noch nichts geplant.

: null}
))}
); }