v0.6.0 — Kitchen: Pantry & Freezer inventory
This commit is contained in:
75
src/app/api/pantry/[id]/route.ts
Normal file
75
src/app/api/pantry/[id]/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { ItemSource } from "@prisma/client";
|
||||
import { db } from "@/lib/db";
|
||||
import { requireAuth, isAdmin } from "@/lib/auth";
|
||||
|
||||
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(),
|
||||
notes: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
const data = updateSchema.parse(await req.json());
|
||||
|
||||
const item = 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.notes !== undefined && { notes: data.notes ?? null }),
|
||||
},
|
||||
select: {
|
||||
id: true, name: true, description: true, quantity: true, unit: true,
|
||||
storedAt: true, bestBefore: true, source: true, plantId: true, notes: true,
|
||||
location: { select: { id: true, name: true } },
|
||||
plant: { select: { id: true, commonName: true, variety: true } },
|
||||
},
|
||||
});
|
||||
|
||||
await db.auditEvent.create({
|
||||
data: { action: "pantry.update", entityId: item.id, actorId: session.user.id, payload: { name: item.name } },
|
||||
});
|
||||
|
||||
return NextResponse.json(item);
|
||||
} 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();
|
||||
if (!isAdmin(session.user.role)) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
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 });
|
||||
}
|
||||
}
|
||||
48
src/app/api/pantry/[id]/use/route.ts
Normal file
48
src/app/api/pantry/[id]/use/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
const schema = z.object({
|
||||
amount: z.number().positive(),
|
||||
notes: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export async function POST(req: Request, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
const { amount, notes } = schema.parse(await req.json());
|
||||
|
||||
const item = await db.item.findUnique({ where: { id: params.id } });
|
||||
if (!item || !item.active) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
const newQty = Math.max(0, Number(item.quantity) - amount);
|
||||
|
||||
await db.item.update({ where: { id: params.id }, data: { quantity: newQty } });
|
||||
|
||||
await db.itemLog.create({
|
||||
data: {
|
||||
itemId: params.id,
|
||||
type: "NOTE",
|
||||
notes: `Used ${amount} ${item.unit ?? ""}${notes ? ` — ${notes}` : ""}`.trim(),
|
||||
actorId: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
await db.auditEvent.create({
|
||||
data: {
|
||||
action: "pantry.use",
|
||||
entityId: params.id,
|
||||
actorId: session.user.id,
|
||||
payload: { amount, remaining: newQty, notes },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, remaining: newQty });
|
||||
} 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 });
|
||||
}
|
||||
}
|
||||
85
src/app/api/pantry/route.ts
Normal file
85
src/app/api/pantry/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
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,
|
||||
location: { select: { id: true, name: true } },
|
||||
plant: { select: { id: true, commonName: true, variety: true } },
|
||||
} as const;
|
||||
|
||||
function serialize(i: any) {
|
||||
return {
|
||||
id: i.id, name: i.name, description: i.description,
|
||||
quantity: Number(i.quantity), unit: i.unit,
|
||||
packageSize: i.description,
|
||||
storedAt: i.storedAt, bestBefore: i.bestBefore,
|
||||
source: i.source, notes: i.notes,
|
||||
locationId: i.location?.id ?? null,
|
||||
locationName: i.location?.name ?? null,
|
||||
plantId: i.plant?.id ?? null,
|
||||
plantName: i.plant ? `${i.plant.commonName}${i.plant.variety ? ` (${i.plant.variety})` : ""}` : null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
await requireAuth();
|
||||
const items = await db.item.findMany({
|
||||
where: { kind: "CONSUMABLE", active: true },
|
||||
orderBy: [{ location: { name: "asc" } }, { name: "asc" }],
|
||||
select: PANTRY_SELECT,
|
||||
});
|
||||
return NextResponse.json(items.map(serialize));
|
||||
}
|
||||
|
||||
const createSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
description: z.string().nullable().optional(),
|
||||
quantity: z.number().min(0).default(1),
|
||||
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(),
|
||||
notes: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const session = await requireAuth();
|
||||
const data = createSchema.parse(await req.json());
|
||||
|
||||
const item = await db.item.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description ?? data.packageSize ?? null,
|
||||
kind: "CONSUMABLE",
|
||||
quantity: data.quantity,
|
||||
unit: data.unit ?? null,
|
||||
locationId: data.locationId ?? null,
|
||||
storedAt: data.storedAt ? new Date(data.storedAt) : null,
|
||||
bestBefore: data.bestBefore ? new Date(data.bestBefore) : null,
|
||||
source: data.source ?? null,
|
||||
plantId: data.plantId ?? null,
|
||||
notes: data.notes ?? null,
|
||||
},
|
||||
select: PANTRY_SELECT,
|
||||
});
|
||||
|
||||
await db.auditEvent.create({
|
||||
data: { action: "pantry.create", entityId: item.id, actorId: session.user.id, payload: { name: item.name } },
|
||||
});
|
||||
|
||||
return NextResponse.json(serialize(item), { status: 201 });
|
||||
} 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user