import { NextRequest, NextResponse } from "next/server"; import { db, cookingHistory, recipes, userNutritionGoals, eq, and, gte, lt } from "@epicure/db"; import { requireSession } from "@/lib/api-auth"; function isValidDate(value: string): boolean { return /^\d{4}-\d{2}-\d{2}$/.test(value) && !isNaN(new Date(`${value}T00:00:00.000Z`).getTime()); } const VALID_RANGES = [7, 30, 90]; export async function GET(req: NextRequest) { const { session, response } = await requireSession(); if (response) return response; const userId = session!.user.id; const rangeParam = req.nextUrl.searchParams.get("range"); const range = rangeParam ? parseInt(rangeParam, 10) : null; if (range && VALID_RANGES.includes(range)) { return getTrend(userId, range); } const dateParam = req.nextUrl.searchParams.get("date"); const date = dateParam && isValidDate(dateParam) ? dateParam : new Date().toISOString().slice(0, 10); const dayStart = new Date(`${date}T00:00:00.000Z`); const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000); const rows = await db .select({ id: cookingHistory.id, recipeId: cookingHistory.recipeId, servings: cookingHistory.servings, cookedAt: cookingHistory.cookedAt, recipeTitle: recipes.title, baseServings: recipes.baseServings, nutritionData: recipes.nutritionData, }) .from(cookingHistory) .leftJoin(recipes, eq(cookingHistory.recipeId, recipes.id)) .where( and( eq(cookingHistory.userId, userId), gte(cookingHistory.cookedAt, dayStart), lt(cookingHistory.cookedAt, dayEnd) ) ) .orderBy(cookingHistory.cookedAt); const totals = { calories: 0, proteinG: 0, carbsG: 0, fatG: 0, fiberG: 0, sodiumMg: 0 }; const entries: { id: string; recipeId: string; title: string; servings: number; cookedAt: string; nutritionKnown: boolean; }[] = []; let unknownCount = 0; for (const row of rows) { const servings = row.servings ?? row.baseServings ?? 1; const perServing = row.nutritionData?.perServing; const nutritionKnown = !!perServing; if (perServing) { totals.calories += perServing.calories * servings; totals.proteinG += perServing.proteinG * servings; totals.carbsG += perServing.carbsG * servings; totals.fatG += perServing.fatG * servings; totals.fiberG += perServing.fiberG * servings; totals.sodiumMg += perServing.sodiumMg * servings; } else { unknownCount += 1; } entries.push({ id: row.id, recipeId: row.recipeId, title: row.recipeTitle ?? "Unknown recipe", servings, cookedAt: row.cookedAt.toISOString(), nutritionKnown, }); } for (const key of Object.keys(totals) as (keyof typeof totals)[]) { totals[key] = Math.round(totals[key]); } const goalsRow = await db.query.userNutritionGoals.findFirst({ where: eq(userNutritionGoals.userId, userId), }); const goals = goalsRow ? { caloriesKcal: goalsRow.caloriesKcal, proteinG: goalsRow.proteinG, carbsG: goalsRow.carbsG, fatG: goalsRow.fatG, } : null; const coverage = { calories: goals?.caloriesKcal ? Math.round((totals.calories / goals.caloriesKcal) * 100) : 0, protein: goals?.proteinG ? Math.round((totals.proteinG / goals.proteinG) * 100) : 0, carbs: goals?.carbsG ? Math.round((totals.carbsG / goals.carbsG) * 100) : 0, fat: goals?.fatG ? Math.round((totals.fatG / goals.fatG) * 100) : 0, }; return NextResponse.json({ date, totals, goals, coverage, entries, unknownCount }); } /** Multi-day trend: daily calorie/macro totals over the last N days, one * bucket per calendar day (UTC, matching the single-day endpoint's own * dayStart/dayEnd math above) — days with nothing cooked still appear with * zero totals so the chart has a continuous x-axis. */ async function getTrend(userId: string, days: number): Promise { const todayStr = new Date().toISOString().slice(0, 10); const since = new Date(`${todayStr}T00:00:00.000Z`); since.setUTCDate(since.getUTCDate() - (days - 1)); const rows = await db .select({ servings: cookingHistory.servings, cookedAt: cookingHistory.cookedAt, baseServings: recipes.baseServings, nutritionData: recipes.nutritionData, }) .from(cookingHistory) .leftJoin(recipes, eq(cookingHistory.recipeId, recipes.id)) .where(and(eq(cookingHistory.userId, userId), gte(cookingHistory.cookedAt, since))); const buckets = new Map(); for (let i = 0; i < days; i++) { const d = new Date(since); d.setUTCDate(d.getUTCDate() + i); buckets.set(d.toISOString().slice(0, 10), { calories: 0, proteinG: 0, carbsG: 0, fatG: 0 }); } for (const row of rows) { const perServing = row.nutritionData?.perServing; if (!perServing) continue; const servings = row.servings ?? row.baseServings ?? 1; const key = row.cookedAt.toISOString().slice(0, 10); const bucket = buckets.get(key); if (!bucket) continue; bucket.calories += perServing.calories * servings; bucket.proteinG += perServing.proteinG * servings; bucket.carbsG += perServing.carbsG * servings; bucket.fatG += perServing.fatG * servings; } const goalsRow = await db.query.userNutritionGoals.findFirst({ where: eq(userNutritionGoals.userId, userId) }); const daysOut = [...buckets.entries()].map(([date, totals]) => ({ date, calories: Math.round(totals.calories), proteinG: Math.round(totals.proteinG), carbsG: Math.round(totals.carbsG), fatG: Math.round(totals.fatG), })); return NextResponse.json({ range: days, days: daysOut, goalCalories: goalsRow?.caloriesKcal ?? null }); }