Files
Moonbase/src/app/api/pantry/[id]/route.ts
Bonna Moon dd7af9e8d4 v0.21.1 — Pantry: delete items with inline confirm
Trash icon on each row; clicking shows "Delete? Yes / No" inline
(no dialog needed). Soft-deletes via active=false. Removed the
admin-only guard on DELETE /api/pantry/[id] since anyone logged in
should be able to manage their own pantry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 18:32:30 -05:00

110 lines
4.9 KiB
TypeScript

import { NextResponse } from "next/server";
import { z } from "zod";
import { ItemSource } from "@prisma/client";
import { db } from "@/lib/db";
import { requireAuth } from "@/lib/auth";
const PANTRY_SELECT = {
id: true, name: true, description: true, quantity: true, unit: true,
storedAt: true, bestBefore: true, source: true, plantId: true, notes: true,
pantryCategory: true, pantrySubcategory: true, pricePaid: true, priceUnit: true,
location: { select: { id: true, name: true } },
plant: { select: { id: true, commonName: true, variety: true } },
labels: { select: { label: { select: { id: true, name: true } } } },
} as const;
async function syncTags(itemId: string, tags: string[]) {
const names = Array.from(new Set(tags.map(t => t.trim()).filter(Boolean)));
const labels = await Promise.all(
names.map(name => db.label.upsert({ where: { name }, update: {}, create: { name }, select: { id: true } }))
);
await db.labelOnItem.deleteMany({ where: { itemId } });
if (labels.length) {
await db.labelOnItem.createMany({
data: labels.map(l => ({ itemId, labelId: l.id })),
skipDuplicates: true,
});
}
}
const updateSchema = z.object({
name: z.string().min(1).optional(),
description: z.string().nullable().optional(),
quantity: z.number().min(0).optional(),
unit: z.string().nullable().optional(),
packageSize: z.string().nullable().optional(),
locationId: z.string().nullable().optional(),
storedAt: z.string().nullable().optional(),
bestBefore: z.string().nullable().optional(),
source: z.nativeEnum(ItemSource).nullable().optional(),
plantId: z.string().nullable().optional(),
pantryCategory: z.string().nullable().optional(),
pantrySubcategory: z.string().nullable().optional(),
pricePaid: z.number().nullable().optional(),
priceUnit: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
tags: z.array(z.string()).optional(),
});
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
try {
const session = await requireAuth();
const data = updateSchema.parse(await req.json());
await db.item.update({
where: { id: params.id },
data: {
...(data.name !== undefined && { name: data.name }),
...(data.description !== undefined && { description: data.description ?? data.packageSize ?? null }),
...(data.quantity !== undefined && { quantity: data.quantity }),
...(data.unit !== undefined && { unit: data.unit ?? null }),
...(data.locationId !== undefined && { locationId: data.locationId ?? null }),
...(data.storedAt !== undefined && { storedAt: data.storedAt ? new Date(data.storedAt) : null }),
...(data.bestBefore !== undefined && { bestBefore: data.bestBefore ? new Date(data.bestBefore) : null }),
...(data.source !== undefined && { source: data.source ?? null }),
...(data.plantId !== undefined && { plantId: data.plantId ?? null }),
...(data.pantryCategory !== undefined && { pantryCategory: data.pantryCategory ?? null }),
...(data.pantrySubcategory !== undefined && { pantrySubcategory: data.pantrySubcategory ?? null }),
...(data.pricePaid !== undefined && { pricePaid: data.pricePaid ?? null }),
...(data.priceUnit !== undefined && { priceUnit: data.priceUnit ?? null }),
...(data.notes !== undefined && { notes: data.notes ?? null }),
},
});
if (data.tags !== undefined) await syncTags(params.id, data.tags);
const item = await db.item.findUniqueOrThrow({ where: { id: params.id }, select: PANTRY_SELECT });
await db.auditEvent.create({
data: { action: "pantry.update", entityId: item.id, actorId: session.user.id, payload: { name: item.name } },
});
return NextResponse.json({
...item,
quantity: Number(item.quantity),
pricePaid: item.pricePaid != null ? Number(item.pricePaid) : null,
tags: item.labels.map(l => l.label.name),
});
} catch (err) {
if (err instanceof z.ZodError) return NextResponse.json({ error: "Invalid input" }, { status: 400 });
if (err instanceof Error && err.message === "Unauthorized") return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
console.error(err);
return NextResponse.json({ error: "Server error" }, { status: 500 });
}
}
export async function DELETE(_req: Request, { params }: { params: { id: string } }) {
try {
const session = await requireAuth();
await db.item.update({ where: { id: params.id }, data: { active: false } });
await db.auditEvent.create({
data: { action: "pantry.delete", entityId: params.id, actorId: session.user.id },
});
return NextResponse.json({ ok: true });
} catch (err) {
if (err instanceof Error && err.message === "Unauthorized") return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
console.error(err);
return NextResponse.json({ error: "Server error" }, { status: 500 });
}
}