From 982a114eb5d6e0912f11333ebefe83568332148f Mon Sep 17 00:00:00 2001 From: Hardik Date: Sun, 31 May 2026 06:14:15 +0530 Subject: [PATCH] feat(products): edit support on /admin/products; Purchasing Items -> /inventory/items - updateProduct server action (name + description, code locked) - EditProductButton dialog in product-form.tsx - Edit entry wired into ProductActionsMenu - Sidebar Purchasing Items for Manager/Admin now points to /inventory/items (cart view) Co-Authored-By: Claude Sonnet 4.6 --- App/app/(portal)/admin/products/actions.ts | 23 ++++++++ .../(portal)/admin/products/product-form.tsx | 59 ++++++++++++++++++- .../admin/products/products-table.tsx | 9 ++- App/components/layout/sidebar.tsx | 2 +- 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/App/app/(portal)/admin/products/actions.ts b/App/app/(portal)/admin/products/actions.ts index ec6953f..cf4ec56 100644 --- a/App/app/(portal)/admin/products/actions.ts +++ b/App/app/(portal)/admin/products/actions.ts @@ -35,6 +35,29 @@ export async function createProduct(formData: FormData): Promise { return { ok: true }; } +export async function updateProduct(formData: FormData): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_products")) { + return { error: "Forbidden" }; + } + + const id = formData.get("id") as string; + if (!id) return { error: "Product ID required" }; + + const parsed = z.object({ + name: z.string().min(1).max(200), + description: z.string().optional(), + }).safeParse({ + name: formData.get("name"), + description: formData.get("description") || undefined, + }); + if (!parsed.success) return { error: parsed.error.errors[0].message }; + + await db.product.update({ where: { id }, data: parsed.data }); + revalidatePath("/admin/products"); + return { ok: true }; +} + export async function deleteProduct(id: string): Promise { const session = await auth(); if (!session?.user || !hasPermission(session.user.role, "manage_products")) return { error: "Forbidden" }; diff --git a/App/app/(portal)/admin/products/product-form.tsx b/App/app/(portal)/admin/products/product-form.tsx index 06965c2..dd7f6c0 100644 --- a/App/app/(portal)/admin/products/product-form.tsx +++ b/App/app/(portal)/admin/products/product-form.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { AdminDialog } from "@/components/ui/admin-dialog"; -import { createProduct, toggleProductActive } from "./actions"; +import { createProduct, updateProduct, toggleProductActive } from "./actions"; type ProductRow = { id: string; @@ -78,6 +78,63 @@ export function AddProductButton() { ); } +export function EditProductButton({ + product, + open: controlledOpen, + onOpenChange, +}: { + product: ProductRow; + open?: boolean; + onOpenChange?: (v: boolean) => void; +}) { + const router = useRouter(); + const [internalOpen, setInternalOpen] = useState(false); + const [pending, setPending] = useState(false); + const [error, setError] = useState(""); + + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : internalOpen; + const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setPending(true); + setError(""); + const fd = new FormData(e.currentTarget); + fd.set("id", product.id); + const result = await updateProduct(fd); + if ("error" in result) { setError(result.error); setPending(false); } + else { setPending(false); setOpen(false); router.refresh(); } + } + + return ( + <> + {!isControlled && ( + + )} + setOpen(false)}> +
+ + {error &&

{error}

} +
+ + +
+ +
+ + ); +} + export function ToggleProductButton({ product }: { product: ProductRow }) { const router = useRouter(); const [toggling, setToggling] = useState(false); diff --git a/App/app/(portal)/admin/products/products-table.tsx b/App/app/(portal)/admin/products/products-table.tsx index 80f086c..74c0bb9 100644 --- a/App/app/(portal)/admin/products/products-table.tsx +++ b/App/app/(portal)/admin/products/products-table.tsx @@ -5,7 +5,7 @@ import { useState } from "react"; import { formatCurrency, formatDate } from "@/lib/utils"; import { useTableControls } from "@/components/ui/use-table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls"; -import { AddProductButton } from "./product-form"; +import { AddProductButton, EditProductButton } from "./product-form"; import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; @@ -26,12 +26,14 @@ export type ProductRow = { const CHIPS = ["Active", "Inactive"]; function ProductActionsMenu({ product }: { product: ProductRow }) { + const [editOpen, setEditOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [toggleOpen, setToggleOpen] = useState(false); return ( <> + setEditOpen(true)}>Edit setToggleOpen(true)}> {product.isActive ? "Deactivate" : "Activate"} @@ -39,6 +41,11 @@ function ProductActionsMenu({ product }: { product: ProductRow }) { setDeleteOpen(true)}>Delete +