import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { db, mealPlans, mealPlanEntries, recipes, recipeBatchDishes, eq, and, or, ne } from "@epicure/db"; import { requireSessionOrApiKey } from "@/lib/api-auth"; import { dispatchWebhook } from "@/lib/webhooks"; type Params = { params: Promise<{ weekStart: string }> }; const Schema = z.object({ day: z.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"]), mealType: z.enum(["breakfast", "lunch", "dinner", "snack"]), recipeId: z.string().optional(), batchDishId: z.string().optional(), servings: z.number().int().min(1).max(100).default(2), note: z.string().max(500).optional(), }); async function getOrCreatePlan(userId: string, weekStart: string) { const existing = await db.query.mealPlans.findFirst({ where: and(eq(mealPlans.userId, userId), eq(mealPlans.weekStart, weekStart)), }); if (existing) return existing; const id = crypto.randomUUID(); await db.insert(mealPlans).values({ id, userId, weekStart }); return { id, userId, weekStart }; } // Polled by MealPlanner (components/meal-plan/meal-planner.tsx) so entries // added/removed by a collaborator via the shared view show up here too — a // lean shape (no photos join) unlike the full plan GET at // meal-plans/[weekStart]/route.ts, since this runs every few seconds. export async function GET(req: NextRequest, { params }: Params) { const { session, response } = await requireSessionOrApiKey(req); if (response) return response; const { weekStart } = await params; const plan = await db.query.mealPlans.findFirst({ where: and(eq(mealPlans.userId, session!.user.id), eq(mealPlans.weekStart, weekStart)), }); if (!plan) return NextResponse.json({ entries: [] }); const entries = await db.query.mealPlanEntries.findMany({ where: eq(mealPlanEntries.mealPlanId, plan.id), with: { recipe: { columns: { id: true, title: true } }, batchDish: { columns: { id: true, name: true } }, }, }); return NextResponse.json({ entries: entries.map((e) => ({ id: e.id, day: e.day, mealType: e.mealType, servings: e.servings, recipe: e.recipe, batchDish: e.batchDish, note: e.note, })), }); } export async function POST(req: NextRequest, { params }: Params) { const { session, response } = await requireSessionOrApiKey(req); if (response) return response; const { weekStart } = await params; const body = await req.json() as unknown; const parsed = Schema.safeParse(body); if (!parsed.success) return NextResponse.json({ error: "Validation error" }, { status: 400 }); if (parsed.data.recipeId) { const recipe = await db.query.recipes.findFirst({ where: and( eq(recipes.id, parsed.data.recipeId), or(eq(recipes.authorId, session!.user.id), ne(recipes.visibility, "private")) ), }); if (!recipe) return NextResponse.json({ error: "Recipe not found" }, { status: 404 }); } if (parsed.data.batchDishId) { if (!parsed.data.recipeId) return NextResponse.json({ error: "batchDishId requires recipeId" }, { status: 400 }); const dish = await db.query.recipeBatchDishes.findFirst({ where: and(eq(recipeBatchDishes.id, parsed.data.batchDishId), eq(recipeBatchDishes.recipeId, parsed.data.recipeId)), }); if (!dish) return NextResponse.json({ error: "Dish not found" }, { status: 404 }); } const plan = await getOrCreatePlan(session!.user.id, weekStart); // Remove existing entry for same day+mealType before inserting await db.delete(mealPlanEntries).where( and( eq(mealPlanEntries.mealPlanId, plan.id), eq(mealPlanEntries.day, parsed.data.day), eq(mealPlanEntries.mealType, parsed.data.mealType) ) ); const entryId = crypto.randomUUID(); await db.insert(mealPlanEntries).values({ id: entryId, mealPlanId: plan.id, day: parsed.data.day, mealType: parsed.data.mealType, recipeId: parsed.data.recipeId, batchDishId: parsed.data.batchDishId, servings: parsed.data.servings, note: parsed.data.note, }); void dispatchWebhook(session!.user.id, "meal_plan.updated", { weekStart, day: parsed.data.day, mealType: parsed.data.mealType }); return NextResponse.json({ id: entryId }, { status: 201 }); }