"use client"; import { useState, useEffect, useRef } from "react"; import { Plus, Pencil, Sparkles, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Combobox } from "@/components/ui/combobox"; import { CATEGORY_OPTIONS, getSubcategoryOptions } from "@/lib/pantry/categories"; import type { PantryItem, LocationOption, PlantOption } from "./pantry-client"; const SOURCES = [ { value: "GARDEN", label: "Our garden" }, { value: "HOMEMADE", label: "Homemade" }, { value: "STORE", label: "Store" }, { value: "FARMERS_MARKET", label: "Farmers market" }, { value: "GIFTED", label: "Gifted" }, { value: "OTHER", label: "Other" }, ]; const COMMON_UNITS = ["lbs", "oz", "g", "kg", "bags", "jars", "quart jars", "pint jars", "gallon bags", "quart bags", "containers", "cans", "bottles", "bunches", "loaves", "portions", "each"]; const PRICE_UNITS = ["package", "lb", "oz", "kg", "each", "dozen", "bunch"]; type Suggestion = { name: string; description: string | null; unit: string | null; source: string | null; pantryCategory: string | null; pantrySubcategory: string | null; priceUnit: string | null; pricePaid: number | null; locationId: string | null; locationName: string | null; plantId: string | null; }; type CatalogData = { suggestions: Suggestion[]; categories: string[]; subcategories: { category: string; subcategory: string }[]; labels: string[]; }; type Form = { name: string; description: string; quantity: string; unit: string; locationId: string; storedAt: string; bestBefore: string; source: string; plantId: string; pantryCategory: string; pantrySubcategory: string; pricePaid: string; priceUnit: string; notes: string; }; type Props = { item?: PantryItem; locations: LocationOption[]; plants: PlantOption[]; onSuccess: () => void; }; export function PantryItemDialog({ item, locations, plants, onSuccess }: Props) { const [open, setOpen] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [catalog, setCatalog] = useState(null); const [nameSuggestions, setNameSuggestions] = useState([]); const [showDropdown, setShowDropdown] = useState(false); const [autofillBanner, setAutofillBanner] = useState(null); const [tags, setTags] = useState(item?.tags ?? []); const [tagInput, setTagInput] = useState(""); const [tagSuggestions, setTagSuggestions] = useState([]); const [showTagDropdown, setShowTagDropdown] = useState(false); const nameRef = useRef(null); const tagInputRef = useRef(null); const debounceRef = useRef>(); const blankForm = (): Form => ({ name: item?.name ?? "", description: item?.description ?? "", quantity: item?.quantity?.toString() ?? "1", unit: item?.unit ?? "", locationId: item?.locationId ?? "", storedAt: item?.storedAt ? new Date(item.storedAt).toISOString().slice(0, 10) : "", bestBefore: item?.bestBefore ? new Date(item.bestBefore).toISOString().slice(0, 10) : "", source: item?.source ?? "", plantId: item?.plantId ?? "", pantryCategory: (item as any)?.pantryCategory ?? "", pantrySubcategory: (item as any)?.pantrySubcategory ?? "", pricePaid: (item as any)?.pricePaid?.toString() ?? "", priceUnit: (item as any)?.priceUnit ?? "package", notes: item?.notes ?? "", }); const [form, setForm] = useState
(blankForm); function set(key: keyof Form, val: string) { setForm(f => ({ ...f, [key]: val })); } function addTag(name: string) { const t = name.trim(); if (!t || tags.includes(t)) return; setTags(prev => [...prev, t]); setTagInput(""); setShowTagDropdown(false); } function removeTag(name: string) { setTags(prev => prev.filter(t => t !== name)); } function handleTagKeyDown(e: React.KeyboardEvent) { if ((e.key === "Enter" || e.key === ",") && tagInput.trim()) { e.preventDefault(); addTag(tagInput); } else if (e.key === "Backspace" && !tagInput && tags.length) { removeTag(tags[tags.length - 1]); } } // Reset tag state when dialog opens/closes useEffect(() => { if (open) { setTags(item?.tags ?? []); setTagInput(""); } }, [open]); // Load catalog on open (for name autocomplete suggestions) useEffect(() => { if (!open) return; fetch("/api/v1/pantry/catalog") .then(r => r.json()) .then(setCatalog) .catch(() => {}); }, [open]); // Tag autocomplete from catalog labels useEffect(() => { const q = tagInput.trim().toLowerCase(); if (!q || !catalog?.labels) { setTagSuggestions([]); setShowTagDropdown(false); return; } const matches = catalog.labels.filter(l => l.toLowerCase().includes(q) && !tags.includes(l)); setTagSuggestions(matches.slice(0, 6)); setShowTagDropdown(matches.length > 0 || q.length > 0); }, [tagInput, catalog, tags]); // Debounced name search for autocomplete function handleNameChange(val: string) { set("name", val); setAutofillBanner(null); clearTimeout(debounceRef.current); if (val.length < 2) { setNameSuggestions([]); setShowDropdown(false); return; } debounceRef.current = setTimeout(async () => { const res = await fetch(`/api/v1/pantry/catalog?q=${encodeURIComponent(val)}`); if (!res.ok) return; const data: CatalogData = await res.json(); setNameSuggestions(data.suggestions.slice(0, 6)); setShowDropdown(data.suggestions.length > 0); }, 200); } function applyAutofill(s: Suggestion) { setForm(f => ({ ...f, name: s.name, description: s.description ?? f.description, unit: s.unit ?? f.unit, source: s.source ?? f.source, pantryCategory: s.pantryCategory ?? f.pantryCategory, pantrySubcategory: s.pantrySubcategory ?? f.pantrySubcategory, priceUnit: s.priceUnit ?? f.priceUnit, pricePaid: s.pricePaid != null ? s.pricePaid.toString() : f.pricePaid, locationId: s.locationId ?? f.locationId, plantId: s.plantId ?? f.plantId, })); setShowDropdown(false); setAutofillBanner(null); } function selectSuggestion(s: Suggestion) { setShowDropdown(false); setAutofillBanner(s); set("name", s.name); } // Merge static category list with any custom categories previously saved to the DB. const catalogCategories: string[] = catalog?.categories ?? []; const knownCategoryValues = new Set(CATEGORY_OPTIONS.map(o => o.value.toLowerCase())); const extraCategoryOptions = catalogCategories .filter(c => !knownCategoryValues.has(c.toLowerCase())) .map(c => ({ value: c, label: c })); const allCategoryOptions = [...CATEGORY_OPTIONS, ...extraCategoryOptions]; // Static subcategories for this category + any custom ones from the catalog. const staticSubcategoryOptions = getSubcategoryOptions(form.pantryCategory); const catalogSubcategories: { category: string; subcategory: string }[] = catalog?.subcategories ?? []; const knownSubValues = new Set(staticSubcategoryOptions.map(o => o.value.toLowerCase())); const extraSubOptions = catalogSubcategories .filter(cs => cs.category.toLowerCase() === form.pantryCategory.toLowerCase() && !knownSubValues.has(cs.subcategory.toLowerCase())) .map(cs => ({ value: cs.subcategory, label: cs.subcategory })); const subcategoryOptions = [...staticSubcategoryOptions, ...extraSubOptions]; async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setSaving(true); setError(null); try { const body = { name: form.name.trim(), description: form.description.trim() || null, quantity: parseFloat(form.quantity) || 1, unit: form.unit.trim() || null, locationId: form.locationId || null, storedAt: form.storedAt || null, bestBefore: form.bestBefore || null, source: form.source || null, plantId: form.plantId || null, pantryCategory: form.pantryCategory.trim() || null, pantrySubcategory: form.pantrySubcategory.trim() || null, pricePaid: form.pricePaid ? parseFloat(form.pricePaid) : null, priceUnit: form.priceUnit || null, notes: form.notes.trim() || null, kind: "CONSUMABLE", tags, }; const res = await fetch(item ? `/api/v1/pantry/${item.id}` : "/api/v1/pantry", { method: item ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) throw new Error((await res.json()).error ?? "Failed"); setOpen(false); setForm(blankForm()); setAutofillBanner(null); setTags([]); setTagInput(""); onSuccess(); } catch (err) { setError(String(err).replace("Error: ", "")); } finally { setSaving(false); } } const inputCls = "w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"; return ( <> {item ? ( ) : ( )} { setOpen(o); if (!o) { setAutofillBanner(null); setShowDropdown(false); } }}> {item ? "Edit item" : "Add pantry item"} {/* Name with autocomplete */}
handleNameChange(e.target.value)} onBlur={() => setTimeout(() => setShowDropdown(false), 150)} required placeholder="e.g. Blueberries, Ground beef, Chicken broth" /> {showDropdown && nameSuggestions.length > 0 && (

Previously added

{nameSuggestions.map(s => ( ))}
)}
{/* Autofill banner */} {autofillBanner && (
Fill in previous settings for {autofillBanner.name}?
)} {/* Category */}
{ set("pantryCategory", v); set("pantrySubcategory", ""); }} placeholder="Select or add category…" allowCustom />
set("pantrySubcategory", v)} placeholder="Select or add…" disabled={!form.pantryCategory} emptyText="Type to add a subcategory" allowCustom />
{/* Description */}
set("description", e.target.value)} placeholder="e.g. 1-gallon freezer bag, quart jar, vacuum-sealed" />
{/* Quantity + unit */}
set("quantity", e.target.value)} required />
set("unit", e.target.value)} placeholder="bags, lbs, jars…" /> {COMMON_UNITS.map(u =>
{/* Price */}
$ set("pricePaid", e.target.value)} placeholder="0.00" />
{/* Location */}
{/* Dates */}
set("storedAt", e.target.value)} />
set("bestBefore", e.target.value)} />
{/* Source */}
{form.source === "GARDEN" && (
)}