import { NextRequest, NextResponse } from "next/server"; import { db, mealPlans, mealPlanEntries, userNutritionGoals, eq, and } from "@epicure/db"; import { requireSessionOrApiKey } from "@/lib/api-auth"; type Params = { params: Promise<{ weekStart: string }> }; type Weekday = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun"; type Totals = { calories: number; protein: number; carbs: number; fat: number }; function emptyTotals(): Totals { return { calories: 0, protein: 0, carbs: 0, fat: 0 }; } export async function GET(req: NextRequest, { params }: Params) { const { session, response } = await requireSessionOrApiKey(req); if (response) return response; const { weekStart } = await params; const userId = session!.user.id; const plan = await db.query.mealPlans.findFirst({ where: and(eq(mealPlans.userId, userId), eq(mealPlans.weekStart, weekStart)), }); const weekTotals = emptyTotals(); const byDay: Record = { mon: emptyTotals(), tue: emptyTotals(), wed: emptyTotals(), thu: emptyTotals(), fri: emptyTotals(), sat: emptyTotals(), sun: emptyTotals(), }; let unknownCount = 0; let plannedDays = 0; if (plan) { // batchDishId entries always carry their parent recipeId too (enforced at // entry-creation time), so joining on `recipe` already covers batch // dishes — there's no separate per-dish nutrition data to look up. const entries = await db.query.mealPlanEntries.findMany({ where: eq(mealPlanEntries.mealPlanId, plan.id), with: { recipe: { columns: { id: true, baseServings: true, nutritionData: true }, }, }, }); const daysWithEntries = new Set(); for (const entry of entries) { daysWithEntries.add(entry.day); const recipe = entry.recipe; if (!recipe || !recipe.nutritionData?.perServing) { unknownCount++; continue; } const { calories, proteinG, carbsG, fatG } = recipe.nutritionData.perServing; const servings = entry.servings ?? recipe.baseServings; const day = byDay[entry.day as Weekday]; const cals = Math.round(calories * servings); const protein = Math.round(proteinG * servings); const carbs = Math.round(carbsG * servings); const fat = Math.round(fatG * servings); weekTotals.calories += cals; weekTotals.protein += protein; weekTotals.carbs += carbs; weekTotals.fat += fat; day.calories += cals; day.protein += protein; day.carbs += carbs; day.fat += fat; } plannedDays = daysWithEntries.size; } const goals = await db.query.userNutritionGoals.findFirst({ where: eq(userNutritionGoals.userId, userId), }); const goalsData = goals ? { caloriesKcal: goals.caloriesKcal, proteinG: goals.proteinG, carbsG: goals.carbsG, fatG: goals.fatG } : null; // Goals are daily targets (see the nutrition diary, which compares a single // day's totals against them directly) — a week's raw total is ~7x a daily // goal, so coverage must compare against the daily AVERAGE across days that // actually have planned meals, not the week's total. const divisor = plannedDays || 1; const dailyAverage: Totals = { calories: Math.round(weekTotals.calories / divisor), protein: Math.round(weekTotals.protein / divisor), carbs: Math.round(weekTotals.carbs / divisor), fat: Math.round(weekTotals.fat / divisor), }; const coverage = { calories: goalsData?.caloriesKcal ? Math.round((dailyAverage.calories / goalsData.caloriesKcal) * 100) : 0, protein: goalsData?.proteinG ? Math.round((dailyAverage.protein / goalsData.proteinG) * 100) : 0, carbs: goalsData?.carbsG ? Math.round((dailyAverage.carbs / goalsData.carbsG) * 100) : 0, fat: goalsData?.fatG ? Math.round((dailyAverage.fat / goalsData.fatG) * 100) : 0, }; return NextResponse.json({ totals: weekTotals, dailyAverage, byDay, plannedDays, unknownCount, goals: goalsData, coverage, }); }