v0.21.0 — Pantry: bulk pull dialog

New "Pull from pantry" button opens a dialog where you search/filter
all items, type a quantity next to each one you're taking, pick or type
a reason, and submit all in one shot. Items with amounts entered
highlight; over-stock amounts show inline error. Backed by new
POST /api/pantry/bulk-use (transactional quantity deduction + ItemLog).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bonna Moon
2026-06-29 18:31:14 -05:00
parent d3af826e3a
commit 6b75e47ee9
5 changed files with 281 additions and 2 deletions

View File

@@ -0,0 +1,61 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
import { requireAuth } from "@/lib/auth";
const schema = z.object({
pulls: z.array(z.object({
id: z.string(),
amount: z.number().positive(),
})).min(1),
notes: z.string().nullable().optional(),
});
export async function POST(req: Request) {
try {
const session = await requireAuth();
const { pulls, notes } = schema.parse(await req.json());
const ids = pulls.map(p => p.id);
const items = await db.item.findMany({
where: { id: { in: ids }, kind: "CONSUMABLE", active: true },
select: { id: true, name: true, quantity: true, unit: true },
});
const itemMap = new Map(items.map(i => [i.id, i]));
const results: { id: string; remaining: number }[] = [];
await db.$transaction(async tx => {
for (const pull of pulls) {
const item = itemMap.get(pull.id);
if (!item) continue;
const remaining = Math.max(0, Number(item.quantity) - pull.amount);
await tx.item.update({ where: { id: pull.id }, data: { quantity: remaining } });
await tx.itemLog.create({
data: {
itemId: pull.id,
type: "NOTE",
notes: `Used ${pull.amount} ${item.unit ?? ""}${notes ? `${notes}` : ""}`.trim(),
actorId: session.user.id,
},
});
results.push({ id: pull.id, remaining });
}
await tx.auditEvent.create({
data: {
action: "pantry.bulk-use",
actorId: session.user.id,
payload: { pulls: pulls.map(p => ({ id: p.id, amount: p.amount })), notes },
},
});
});
return NextResponse.json({ ok: true, results });
} 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 });
}
}