v0.6.1 — Pantry: categories, price tracking, and autocomplete

This commit is contained in:
Bonna Moon
2026-06-29 17:37:59 -05:00
parent 0d52ea5ad1
commit 62a0089ec2
8 changed files with 407 additions and 56 deletions

View File

@@ -15,6 +15,10 @@ const updateSchema = z.object({
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(),
});
@@ -35,11 +39,16 @@ export async function PATCH(req: Request, { params }: { params: { id: string } }
...(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 }),
},
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 } },
},

View File

@@ -0,0 +1,64 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { requireAuth } from "@/lib/auth";
// Returns distinct pantry item names + their most-recent settings for autocomplete.
export async function GET(req: Request) {
await requireAuth();
const { searchParams } = new URL(req.url);
const q = searchParams.get("q")?.trim().toLowerCase() ?? "";
// Grab all consumable items (active + inactive so we remember old entries).
const items = await db.item.findMany({
where: {
kind: "CONSUMABLE",
...(q ? { name: { contains: q, mode: "insensitive" } } : {}),
},
orderBy: { updatedAt: "desc" },
select: {
name: true, description: true, unit: true, source: true,
pantryCategory: true, pantrySubcategory: true,
priceUnit: true, pricePaid: true,
locationId: true, plantId: true,
location: { select: { id: true, name: true } },
},
take: 200,
});
// Deduplicate by name (case-insensitive), keep the most-recent record per name.
const seen = new Map<string, typeof items[number]>();
for (const item of items) {
const key = item.name.toLowerCase();
if (!seen.has(key)) seen.set(key, item);
}
// Also return distinct categories/subcategories for autocomplete dropdowns.
const allCats = await db.item.findMany({
where: { kind: "CONSUMABLE", pantryCategory: { not: null } },
select: { pantryCategory: true, pantrySubcategory: true },
distinct: ["pantryCategory", "pantrySubcategory"],
});
const categories = Array.from(new Set(allCats.map(c => c.pantryCategory).filter(Boolean))) as string[];
const subcategories = allCats
.filter(c => c.pantrySubcategory)
.map(c => ({ category: c.pantryCategory!, subcategory: c.pantrySubcategory! }));
return NextResponse.json({
suggestions: Array.from(seen.values()).map(i => ({
name: i.name,
description: i.description,
unit: i.unit,
source: i.source,
pantryCategory: i.pantryCategory,
pantrySubcategory: i.pantrySubcategory,
priceUnit: i.priceUnit,
pricePaid: i.pricePaid != null ? Number(i.pricePaid) : null,
locationId: i.locationId,
locationName: i.location?.name ?? null,
plantId: i.plantId,
})),
categories,
subcategories,
});
}

View File

@@ -7,6 +7,7 @@ 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 } },
} as const;
@@ -18,6 +19,10 @@ function serialize(i: any) {
packageSize: i.description,
storedAt: i.storedAt, bestBefore: i.bestBefore,
source: i.source, notes: i.notes,
pantryCategory: i.pantryCategory,
pantrySubcategory: i.pantrySubcategory,
pricePaid: i.pricePaid != null ? Number(i.pricePaid) : null,
priceUnit: i.priceUnit,
locationId: i.location?.id ?? null,
locationName: i.location?.name ?? null,
plantId: i.plant?.id ?? null,
@@ -46,6 +51,10 @@ const createSchema = z.object({
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(),
});
@@ -66,6 +75,10 @@ export async function POST(req: Request) {
bestBefore: data.bestBefore ? new Date(data.bestBefore) : null,
source: data.source ?? null,
plantId: data.plantId ?? null,
pantryCategory: data.pantryCategory ?? null,
pantrySubcategory: data.pantrySubcategory ?? null,
pricePaid: data.pricePaid ?? null,
priceUnit: data.priceUnit ?? null,
notes: data.notes ?? null,
},
select: PANTRY_SELECT,