"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Plus, Trash2, AlertTriangle, Package, Pencil, ChevronDown, Merge } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { formatQuantity, hasQuantity } from "@/lib/fractions"; import { PantryScanDialog } from "@/components/pantry/pantry-scan-dialog"; import { PantryItemDialog, type PantryItem } from "@/components/pantry/pantry-item-dialog"; import { EmptyState } from "@/components/shared/empty-state"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; const OTHER_KEY = "__other__"; function daysUntilExpiry(dateStr: string): number { const diff = new Date(dateStr).getTime() - Date.now(); return Math.ceil(diff / (1000 * 60 * 60 * 24)); } export function PantryManager({ initialItems }: { initialItems: PantryItem[] }) { const t = useTranslations("pantry"); const tShopping = useTranslations("shoppingLists"); const tCommon = useTranslations("common"); const router = useRouter(); const [items, setItems] = useState(initialItems); const [name, setName] = useState(""); const [quantity, setQuantity] = useState(""); const [unit, setUnit] = useState(""); const [expiresAt, setExpiresAt] = useState(""); const [adding, setAdding] = useState(false); const [confirmId, setConfirmId] = useState(null); const [editingItem, setEditingItem] = useState(null); const [collapsed, setCollapsed] = useState>(new Set()); const [merging, setMerging] = useState(false); async function mergeDuplicates() { setMerging(true); try { const res = await fetch("/api/v1/pantry/merge-duplicates", { method: "POST" }); if (!res.ok) { toast.error(t("mergeDuplicatesFailed")); return; } const { removed } = await res.json() as { mergedGroups: number; removed: number }; if (removed === 0) toast.success(t("mergeDuplicatesNoneFound")); else toast.success(t("mergeDuplicatesSuccess", { count: removed })); router.refresh(); } finally { setMerging(false); } } async function add() { if (!name.trim()) return; setAdding(true); try { const res = await fetch("/api/v1/pantry", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rawName: name.trim(), quantity: quantity.trim() || undefined, unit: unit.trim() || undefined, expiresAt: expiresAt ? new Date(expiresAt).toISOString() : undefined, }), }); if (!res.ok) { toast.error(t("addFailed")); return; } const { id } = await res.json() as { id: string }; setItems((prev) => [ ...prev, { id, rawName: name.trim(), quantity: quantity || null, unit: unit || null, notes: null, aisle: null, expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null }, ]); setName(""); setQuantity(""); setUnit(""); setExpiresAt(""); } finally { setAdding(false); } } async function remove(id: string) { const res = await fetch(`/api/v1/pantry/${id}`, { method: "DELETE" }); if (res.ok) setItems((prev) => prev.filter((i) => i.id !== id)); else toast.error(t("removeFailed")); } function toggleCollapsed(key: string) { setCollapsed((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); } const itemPendingDelete = items.find((i) => i.id === confirmId) ?? null; const sortWithinGroup = (a: PantryItem, b: PantryItem) => { if (a.expiresAt && b.expiresAt) return new Date(a.expiresAt).getTime() - new Date(b.expiresAt).getTime(); if (a.expiresAt) return -1; if (b.expiresAt) return 1; return a.rawName.localeCompare(b.rawName); }; const grouped = new Map(); for (const item of items) { const key = item.aisle ?? OTHER_KEY; const group = grouped.get(key) ?? []; group.push(item); grouped.set(key, group); } for (const group of grouped.values()) group.sort(sortWithinGroup); function categoryLabel(key: string): string { return key === OTHER_KEY ? tShopping("aisleOther") : tShopping(`categories.${key}`); } const sortedGroupKeys = [...grouped.keys()].sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b))); const showGroupHeaders = grouped.size > 1; return (
{/* Add form */}
setName(e.target.value)} placeholder={t("itemNamePlaceholder")} className="flex-1 min-w-[160px]" onKeyDown={(e) => e.key === "Enter" && add()} /> setQuantity(e.target.value)} placeholder={t("qtyPlaceholder")} className="w-20" /> setUnit(e.target.value)} placeholder={t("unitPlaceholder")} className="w-20" /> setExpiresAt(e.target.value)} className="w-36" /> router.refresh()} />
{/* Item list */} {items.length === 0 ? ( ) : (
{sortedGroupKeys.map((key) => { const groupItems = grouped.get(key)!; const isCollapsed = collapsed.has(key); return (
{showGroupHeaders && ( )} {!isCollapsed && (
{groupItems.map((item) => { const days = item.expiresAt ? daysUntilExpiry(item.expiresAt) : null; const expiring = days !== null && days <= 3; const expired = days !== null && days < 0; return (
{item.rawName} {hasQuantity(item.quantity) && ( {formatQuantity(parseFloat(item.quantity!))}{item.unit ? ` ${item.unit}` : ""} )} {expired && {t("expired")}} {expiring && !expired && {t("expiresInDays", { days })}}
{item.expiresAt && !expired && !expiring && (

{t("expiresOn", { date: new Date(item.expiresAt).toLocaleDateString() })}

)} {item.notes &&

{item.notes}

}
); })}
)}
); })}
)} !open && setConfirmId(null)}> {t("removeConfirmTitle")} {itemPendingDelete ? t("removeConfirmDescription", { name: itemPendingDelete.rawName }) : ""} {tCommon("cancel")} { if (confirmId) void remove(confirmId); setConfirmId(null); }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {tCommon("delete")} {editingItem && ( !open && setEditingItem(null)} onSaved={(updated) => { setItems((prev) => prev.map((i) => (i.id === updated.id ? updated : i))); setEditingItem(null); }} /> )}
); }