"use client"; import { BeakerIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, EllipsisHorizontalIcon, FireIcon, PlusIcon, ScaleIcon, TrashIcon, } from "@heroicons/react/24/outline"; import { useMemo, useState } from "react"; import { trpc } from "@/lib/trpc"; type MealType = "Frühstück" | "Mittagessen" | "Abendessen" | "Snack"; type ApiMealType = "breakfast" | "lunch" | "dinner" | "snack"; type FoodEntry = { id: string; mealType: MealType; name: string; amount: string; calories: number; protein: number; }; type DrinkEntry = { id: string; amountMl: number; }; type PlannedMeal = { id: string; mealType: MealType; name: string; note?: string; portions?: number; plannedCalories?: number; }; type DayData = { foods: FoodEntry[]; drinks: DrinkEntry[]; weightKg?: number; plannedMeals: PlannedMeal[]; }; 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", }; const fallbackGoals = { drinkMl: 2500, calories: 2000, carbs: 200, fat: 67, protein: 150, fastingHours: 12, }; function toDateKey(date: Date) { return date.toISOString().slice(0, 10); } function parseDateKey(key: string) { return new Date(`${key}T12:00:00`); } function addDays(key: string, delta: number) { const date = parseDateKey(key); date.setDate(date.getDate() + delta); return toDateKey(date); } function emptyDay(): DayData { return { foods: [], drinks: [], plannedMeals: [], }; } function formatDate(key: string) { return parseDateKey(key).toLocaleDateString("de-DE", { weekday: "long", day: "2-digit", month: "2-digit", }); } function progress(value: number, goal: number) { if (goal <= 0) return 0; return Math.min(100, Math.round((value / goal) * 100)); } 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", ]; const index = name.length % colors.length; return colors[index] ?? colors[0]; } function RecipeAvatar({ name }: { name: string }) { const initials = name .split(" ") .filter(Boolean) .slice(0, 2) .map((part) => part[0]) .join("") .toUpperCase(); return (
{initials || "HH"}
); } function WeekStrip({ selectedDate, onSelect, }: { selectedDate: string; onSelect: (date: string) => void; }) { const days = useMemo(() => { const selected = parseDateKey(selectedDate); const day = selected.getDay() === 0 ? 7 : selected.getDay(); const monday = new Date(selected); monday.setDate(selected.getDate() - day + 1); return Array.from({ length: 7 }, (_, index) => { const date = new Date(monday); date.setDate(monday.getDate() + index); return toDateKey(date); }); }, [selectedDate]); return (
{days.map((dateKey) => { const date = parseDateKey(dateKey); const active = dateKey === selectedDate; return ( ); })}
); } function MacroBar({ carbs, fat, protein }: { carbs: number; fat: number; protein: number }) { const total = Math.max(1, carbs + fat + protein); return (
); } function WaterGlasses({ amount, goal, onAdd, locked, }: { amount: number; goal: number; onAdd: () => void; locked: boolean; }) { const glasses = Math.max(6, Math.ceil(goal / 250)); const filled = Math.min(glasses, Math.floor(amount / 250)); return (
{Array.from({ length: glasses }, (_, index) => { const isFilled = index < filled; const isNext = index === filled && !locked; return ( ); })}
); } function PlanList({ meals, onRemove, canRemove, }: { meals: PlannedMeal[]; onRemove: (id: string) => void; canRemove: boolean; }) { if (meals.length === 0) { return (
Noch keine Mahlzeit geplant.
); } return (
{meals.map((meal) => (

{meal.name}

{meal.mealType} {meal.portions ? ` - ${meal.portions.toLocaleString("de-DE")} Portion` : ""} {meal.plannedCalories ? ` - ${meal.plannedCalories.toLocaleString("de-DE")} kcal` : ""} {meal.note ? ` - ${meal.note}` : ""}

{canRemove ? ( ) : null}
))}
); } export default function DashboardPage() { const utils = trpc.useUtils(); const todayKey = toDateKey(new Date()); const [selectedDate, setSelectedDate] = useState(todayKey); const [quickMode, setQuickMode] = useState<"meal" | "plan" | "drink" | "weight">("meal"); const [foodForm, setFoodForm] = useState({ mealType: "Frühstück" as MealType, name: "", amount: "", calories: "", protein: "", }); const [weightForm, setWeightForm] = useState(""); const [planForm, setPlanForm] = useState({ mealType: "Mittagessen" as MealType, name: "", note: "", portions: "1", plannedCalories: "", }); const overviewQuery = trpc.health.overview.useQuery({ date: selectedDate }); const statsSummaryQuery = trpc.health.statsSummary.useQuery({ referenceDate: todayKey }); const weeklyBudget = statsSummaryQuery.data?.week ?? null; const coachRecommendations = overviewQuery.data?.coachRecommendations ?? []; function invalidateDay() { utils.health.overview.invalidate({ date: selectedDate }); utils.health.statsSummary.invalidate({ referenceDate: todayKey }); } const addMealMutation = trpc.health.addMealLog.useMutation({ onSuccess: invalidateDay }); const addDrinkMutation = trpc.health.addDrinkLog.useMutation({ onSuccess: invalidateDay }); const saveWeightMutation = trpc.health.upsertWeightLog.useMutation({ onSuccess: () => { invalidateDay(); utils.health.weightHistory.invalidate(); }, }); const saveMealPlanMutation = trpc.health.upsertMealPlan.useMutation({ onSuccess: invalidateDay }); const deleteMealMutation = trpc.health.deleteMealLog.useMutation({ onSuccess: invalidateDay }); const deletePlanMutation = trpc.health.deleteMealPlan.useMutation({ onSuccess: invalidateDay }); const goals = overviewQuery.data?.goals ? { drinkMl: overviewQuery.data.goals.drinkMl, calories: overviewQuery.data.goals.calories, carbs: Number(overviewQuery.data.goals.carbsG) || fallbackGoals.carbs, fat: Number(overviewQuery.data.goals.fatG) || fallbackGoals.fat, protein: Number(overviewQuery.data.goals.proteinG) || fallbackGoals.protein, fastingHours: overviewQuery.data.goals.fastingHours, } : fallbackGoals; const day: DayData = overviewQuery.data?.day ? { foods: overviewQuery.data.day.mealLogs.map((meal) => ({ id: meal.id, mealType: mealTypeFromApi[meal.mealType as ApiMealType] ?? "Snack", name: meal.name, amount: meal.amountLabel ?? "1 Portion", calories: meal.calories, protein: Number(meal.proteinG) || 0, })), drinks: overviewQuery.data.day.drinkLogs.map((drink) => ({ id: drink.id, amountMl: drink.amountMl, })), weightKg: overviewQuery.data.day.weightLog ? Number(overviewQuery.data.day.weightLog.weightKg) : undefined, plannedMeals: overviewQuery.data.day.mealPlans.map((meal) => ({ id: meal.id, mealType: mealTypeFromApi[meal.mealType as ApiMealType] ?? "Snack", name: meal.name, note: meal.note ?? undefined, portions: Number(meal.portions) || 1, plannedCalories: Number(meal.plannedCalories) || 0, })), } : emptyDay(); const isFuture = selectedDate > todayKey; const isToday = selectedDate === todayKey; const isEditable = selectedDate <= todayKey; const totals = useMemo(() => { const calories = day.foods.reduce((sum, food) => sum + (Number(food.calories) || 0), 0); const protein = day.foods.reduce((sum, food) => sum + (Number(food.protein) || 0), 0); const drinkMl = day.drinks.reduce((sum, drink) => sum + (Number(drink.amountMl) || 0), 0); const carbs = Math.round((calories * 0.45) / 4); const fat = Math.round((calories * 0.3) / 9); return { calories, protein, drinkMl, carbs, fat }; }, [day.drinks, day.foods]); const remainingCalories = Math.max(0, goals.calories - totals.calories); const nutritionScore = Math.min(100, Math.round((progress(totals.protein, goals.protein) + progress(totals.drinkMl, goals.drinkMl)) / 2)); function addFoodEntry() { if (!isEditable || !foodForm.name.trim()) return; addMealMutation.mutate({ date: selectedDate, mealType: mealTypeToApi[foodForm.mealType], name: foodForm.name.trim(), amountLabel: foodForm.amount.trim() || "1 Portion", calories: Number(foodForm.calories) || 0, proteinG: Number(foodForm.protein) || 0, carbsG: 0, fatG: 0, }); setFoodForm((current) => ({ ...current, name: "", amount: "", calories: "", protein: "" })); } function addDrinkEntry() { if (!isEditable) return; addDrinkMutation.mutate({ date: selectedDate, amountMl: 250 }); } function saveWeight() { if (!isEditable) return; const weightKg = Number(weightForm); if (!weightKg || weightKg <= 0) return; saveWeightMutation.mutate({ date: selectedDate, weightKg, note: null }); setWeightForm(""); } function addPlannedMeal() { if (!planForm.name.trim()) return; saveMealPlanMutation.mutate({ date: selectedDate, mealType: mealTypeToApi[planForm.mealType], name: planForm.name.trim(), portions: Number(planForm.portions) || 1, plannedCalories: Number(planForm.plannedCalories) || 0, note: planForm.note.trim() || null, recipeId: null, }); setPlanForm((current) => ({ ...current, name: "", note: "", portions: "1", plannedCalories: "" })); } function removeEntry(kind: "foods" | "plannedMeals", id: string) { if (kind === "foods") deleteMealMutation.mutate({ id }); if (kind === "plannedMeals") deletePlanMutation.mutate({ id }); } return (

{isToday ? "Heute" : isFuture ? "Planung" : "Rückblick"}

{formatDate(selectedDate).split(",")[0]}

Hellth Hub

{isFuture ? "Zukunftstage sind für Tracking gesperrt, aber für den Essensplan frei." : "Tracking und Tagesziele für deinen Alltag."}

HH
{weeklyBudget ? (

Diese Woche

Flexibles Kalorienbudget ohne Tagesstress.

Planen

{weeklyBudget.baseBudget.toLocaleString("de-DE")}

Ziel kcal

{weeklyBudget.eatenCalories.toLocaleString("de-DE")}

gegessen/geplant

+{weeklyBudget.activityCalories.toLocaleString("de-DE")}

Sportbonus

{weeklyBudget.availableCalories.toLocaleString("de-DE")}

verfügbar

) : null}

Kalorien übrig

{remainingCalories.toLocaleString("de-DE")} kcal

Kohlenhydr.

{totals.carbs}/{goals.carbs} g

Fett

{totals.fat}/{goals.fat} g

Proteine

{Math.round(totals.protein)}/{goals.protein} g

Ziel: {goals.calories.toLocaleString("de-DE")} kcal
{coachRecommendations.length > 0 ? (

Coach

Persönliche Vorschläge aus deinem Fragebogen.

Anpassen
{coachRecommendations.slice(0, 3).map((item) => (

{item.title}

{item.body}

))}
) : null}

Ernährungsplan

Alle ansehen
removeEntry("plannedMeals", id)} canRemove={day.plannedMeals.length > 0} />

Wasser

{totals.drinkMl} ml

Tagesziel: {goals.drinkMl} ml

Fasten

Bereit zum Starten?

Ziel: {goals.fastingHours} h

Gewicht

{day.weightKg ? `${day.weightKg} kg` : "-"}

{isEditable ? "Ein Tageswert reicht." : "Für Zukunftstage gesperrt."}

{nutritionScore}
{isEditable ? (
setWeightForm(event.target.value)} placeholder={day.weightKg ? String(day.weightKg) : "kg"} className="rounded-full border border-gray-200 px-4 py-3 text-sm" />
) : null}

{isFuture ? "Gericht planen" : "Schnellerfassung"}

{isFuture ? Tracking gesperrt : null}
{(["meal", "plan", "drink", "weight"] as const).map((mode) => ( ))}
{quickMode === "meal" ? (
setFoodForm((current) => ({ ...current, name: event.target.value }))} placeholder="z. B. Skyr mit Haferflocken" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm lg:col-span-4" /> setFoodForm((current) => ({ ...current, amount: event.target.value }))} placeholder="Menge" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm lg:col-span-2" /> setFoodForm((current) => ({ ...current, calories: event.target.value }))} placeholder="kcal" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm lg:col-span-1" /> setFoodForm((current) => ({ ...current, protein: event.target.value }))} placeholder="Protein" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm lg:col-span-1" />
) : null} {quickMode === "plan" ? (
setPlanForm((current) => ({ ...current, name: event.target.value }))} placeholder="Geplantes Gericht" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm lg:col-span-4" /> setPlanForm((current) => ({ ...current, note: event.target.value }))} placeholder="Notiz / Reste" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm lg:col-span-3" /> setPlanForm((current) => ({ ...current, portions: event.target.value }))} placeholder="Portionen" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm lg:col-span-1" /> setPlanForm((current) => ({ ...current, plannedCalories: event.target.value }))} placeholder="kcal" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm lg:col-span-1" />
) : null} {quickMode === "drink" ? ( ) : null} {quickMode === "weight" ? (
setWeightForm(event.target.value)} placeholder="kg" className="rounded-2xl border border-gray-200 px-4 py-3 text-sm" />
) : null} {day.foods.length > 0 ? (
{day.foods.map((food) => (

{food.name}

{food.mealType} - {food.amount} - {food.calories ?? 0} kcal

{isEditable ? ( ) : null}
))}
) : null}
); }