feat: clear a meal-plan day or the whole week in one action

New bulk-delete route for the owner's own weekStart-scoped plan
(mirrors recipes/bulk's DELETE), plus ?ids= support added to the
existing shared-plan DELETE route (kept ?entryId= working
unchanged). UI: hover-reveal trash icon per day header, "Clear week"
button in the toolbar, one shared confirm dialog for both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-13 22:07:12 +02:00
parent a144052e46
commit aa67868b96
9 changed files with 151 additions and 11 deletions
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db, mealPlans, mealPlanEntries, eq, and, inArray } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
type Params = { params: Promise<{ weekStart: string }> };
const BulkDeleteSchema = z.object({
ids: z.array(z.string()).min(1).max(100),
});
// Clear-a-day / clear-a-week — the meal planner has no per-entry select mode,
// so this is only ever called with either one day's entry ids or the whole
// week's, scoped to the caller's own plan.
export async function DELETE(req: NextRequest, { params }: Params) {
const { session, response } = await requireSession();
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({ error: "Not found" }, { status: 404 });
const body = BulkDeleteSchema.safeParse(await req.json());
if (!body.success) return NextResponse.json({ error: "Validation error" }, { status: 400 });
await db.delete(mealPlanEntries).where(
and(inArray(mealPlanEntries.id, body.data.ids), eq(mealPlanEntries.mealPlanId, plan.id))
);
return NextResponse.json({ ok: true });
}
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db, mealPlanEntries, recipes, eq, and, or, ne } from "@epicure/db";
import { db, mealPlanEntries, recipes, eq, and, or, ne, inArray } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { getMealPlanAccessById, canWriteMealPlan } from "@/lib/meal-plan-access";
import { dispatchWebhook } from "@/lib/webhooks";
@@ -70,11 +70,15 @@ export async function DELETE(req: NextRequest, { params }: Params) {
if (!access) return NextResponse.json({ error: "Not found" }, { status: 404 });
if (!canWriteMealPlan(access.role)) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
// ?entryId=x for a single delete (existing behavior), or ?ids=a,b,c for a
// "clear this day"/"clear this week" bulk delete — same access check either way.
const entryId = req.nextUrl.searchParams.get("entryId");
if (!entryId) return NextResponse.json({ error: "entryId required" }, { status: 400 });
const idsParam = req.nextUrl.searchParams.get("ids");
const ids = idsParam ? idsParam.split(",").filter(Boolean) : entryId ? [entryId] : [];
if (ids.length === 0) return NextResponse.json({ error: "entryId or ids required" }, { status: 400 });
await db.delete(mealPlanEntries).where(
and(eq(mealPlanEntries.id, entryId), eq(mealPlanEntries.mealPlanId, mealPlanId))
and(inArray(mealPlanEntries.id, ids), eq(mealPlanEntries.mealPlanId, mealPlanId))
);
return new NextResponse(null, { status: 204 });