diff --git a/App/CLAUDE.md b/App/CLAUDE.md index 24ac5d3..1d44283 100644 --- a/App/CLAUDE.md +++ b/App/CLAUDE.md @@ -137,6 +137,10 @@ The pipeline (no mailto attachment — `mailto:` can't carry files): `prepareVen Inventory (`ItemInventory`, keyed by `productId` + `siteId`) is **incremented at PO approval** — not on close — for the ordered quantities, when the PO has a `siteId`. The whole inventory surface (site stock, consumption) is gated by `NEXT_PUBLIC_INVENTORY_ENABLED` (see `lib/feature-flags.ts`); the vendor/product catalogue used for PO creation stays available regardless. +### Product catalogue sync (`lib/product-catalog.ts`) + +`syncProductCatalog(poId, lineItems, vendorId, actorId)` registers a PO's line items as reusable **`Product`s** (the `/catalogue/items` catalogue): a line item with no `productId` is matched to an existing product by name (case-insensitive) or a new product is created, then the line item is linked back; `lastPrice`/`lastVendorId` and the per-vendor `ProductVendorPrice` are upserted. It runs **at approval** (`approvePo`) so an approved PO's items are immediately reusable in further POs, **and again at full payment** (`markPaid`) to refresh prices on the final figures. Idempotent — re-running matches the same product. (Import takes its own auto-create path.) + ### Import → Closed `/po/import` parses a Pelagia-format Excel PO and saves it **directly as `CLOSED`** (historical record, bypasses approval). It auto-detects the company (by header/code), auto-matches the vessel by code, **auto-creates the vendor and any unknown products**, and upserts per-vendor prices. diff --git a/App/app/(portal)/admin/products/[id]/page.tsx b/App/app/(portal)/admin/products/[id]/page.tsx index 9ef538f..66ccfd9 100644 --- a/App/app/(portal)/admin/products/[id]/page.tsx +++ b/App/app/(portal)/admin/products/[id]/page.tsx @@ -7,7 +7,7 @@ import { formatCurrency, formatDate } from "@/lib/utils"; import { distanceKm, formatDistance } from "@/lib/geo"; import { ToggleProductButton, EditProductButton } from "../product-form"; import { AddToCartButton } from "@/components/inventory/add-to-cart-button"; -import { ItemPriceChart } from "@/app/(portal)/inventory/items/[id]/item-price-chart"; +import { ItemPriceChart } from "@/app/(portal)/catalogue/items/[id]/item-price-chart"; import { SiteSelect } from "@/components/inventory/site-select"; import type { Metadata } from "next"; diff --git a/App/app/(portal)/admin/products/products-table.tsx b/App/app/(portal)/admin/products/products-table.tsx index e6e1090..3fc13f7 100644 --- a/App/app/(portal)/admin/products/products-table.tsx +++ b/App/app/(portal)/admin/products/products-table.tsx @@ -67,7 +67,7 @@ function ProductActionsMenu({ product }: { product: ProductRow }) { export function ProductsTable({ products, canManage, - detailBase = "/inventory/items", + detailBase = "/catalogue/items", }: { products: ProductRow[]; canManage: boolean; diff --git a/App/app/(portal)/admin/vendors/actions.ts b/App/app/(portal)/admin/vendors/actions.ts index 59da9fa..dd45b5a 100644 --- a/App/app/(portal)/admin/vendors/actions.ts +++ b/App/app/(portal)/admin/vendors/actions.ts @@ -95,7 +95,7 @@ export async function createVendor(formData: FormData): Promise { }); revalidatePath("/admin/vendors"); - revalidatePath("/inventory/vendors"); + revalidatePath("/catalogue/vendors"); return { ok: true }; } @@ -108,7 +108,7 @@ export async function verifyVendor(vendorId: string): Promise { await db.vendor.update({ where: { id: vendorId }, data: { isVerified: true } }); revalidatePath("/admin/vendors"); - revalidatePath("/inventory/vendors"); + revalidatePath("/catalogue/vendors"); revalidatePath(`/admin/vendors/${vendorId}`); return { ok: true }; } diff --git a/App/app/(portal)/approvals/[id]/actions.ts b/App/app/(portal)/approvals/[id]/actions.ts index 4c04098..3470cca 100644 --- a/App/app/(portal)/approvals/[id]/actions.ts +++ b/App/app/(portal)/approvals/[id]/actions.ts @@ -4,6 +4,7 @@ import { auth } from "@/auth"; import { db } from "@/lib/db"; import { canPerformAction } from "@/lib/po-state-machine"; import { approvePoSchema } from "@/lib/validations/po"; +import { syncProductCatalog } from "@/lib/product-catalog"; import { notify } from "@/lib/notifier"; import { revalidatePath } from "next/cache"; @@ -84,6 +85,12 @@ export async function approvePo({ revalidatePath(`/admin/sites/${siteId}`); } + // Register the line items in the product catalogue (/catalogue/items) on + // approval, so an approved PO's items are immediately reusable in further POs. + // Idempotent; payment re-syncs to refresh prices on the final figures. + await syncProductCatalog(poId, po.lineItems, po.vendorId, session.user.id); + revalidatePath("/catalogue/items"); + const accounts = await db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } }); await notify({ event: withNote ? "PO_APPROVED_WITH_NOTE" : "PO_APPROVED", diff --git a/App/app/(portal)/inventory/items/[id]/item-price-chart.tsx b/App/app/(portal)/catalogue/items/[id]/item-price-chart.tsx similarity index 100% rename from App/app/(portal)/inventory/items/[id]/item-price-chart.tsx rename to App/app/(portal)/catalogue/items/[id]/item-price-chart.tsx diff --git a/App/app/(portal)/inventory/items/[id]/page.tsx b/App/app/(portal)/catalogue/items/[id]/page.tsx similarity index 98% rename from App/app/(portal)/inventory/items/[id]/page.tsx rename to App/app/(portal)/catalogue/items/[id]/page.tsx index 2bff472..f910bab 100644 --- a/App/app/(portal)/inventory/items/[id]/page.tsx +++ b/App/app/(portal)/catalogue/items/[id]/page.tsx @@ -26,7 +26,7 @@ export default async function ItemDetailPage({ params, searchParams }: Props) { const { id } = await params; const { site: siteId } = await searchParams; - const baseHref = `/inventory/items/${id}`; + const baseHref = `/catalogue/items/${id}`; const [product, sites] = await Promise.all([ db.product.findUnique({ @@ -85,7 +85,7 @@ export default async function ItemDetailPage({ params, searchParams }: Props) {
{/* Breadcrumb */}
- Items + Items / {product.name}
diff --git a/App/app/(portal)/inventory/items/items-table.tsx b/App/app/(portal)/catalogue/items/items-table.tsx similarity index 98% rename from App/app/(portal)/inventory/items/items-table.tsx rename to App/app/(portal)/catalogue/items/items-table.tsx index 7c8a670..655a978 100644 --- a/App/app/(portal)/inventory/items/items-table.tsx +++ b/App/app/(portal)/catalogue/items/items-table.tsx @@ -108,7 +108,7 @@ export function ItemsTable({ value={currentSiteId ?? ""} onChange={(e) => { const id = e.target.value; - router.push(id ? `/inventory/items?siteId=${id}` : "/inventory/items"); + router.push(id ? `/catalogue/items?siteId=${id}` : "/catalogue/items"); }} className="flex-1 max-w-xs rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-1.5 text-sm text-neutral-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20" > @@ -254,7 +254,7 @@ export function ItemsTable({
e.stopPropagation()} className="font-medium text-neutral-800 hover:text-primary-600 hover:underline" > diff --git a/App/app/(portal)/inventory/items/page.tsx b/App/app/(portal)/catalogue/items/page.tsx similarity index 97% rename from App/app/(portal)/inventory/items/page.tsx rename to App/app/(portal)/catalogue/items/page.tsx index 99dd7be..df2b9cd 100644 --- a/App/app/(portal)/inventory/items/page.tsx +++ b/App/app/(portal)/catalogue/items/page.tsx @@ -20,7 +20,7 @@ export default async function InventoryItemsPage() { }, }); - // canManage lets managers/admins see the Edit/Delete controls even from /inventory/items + // canManage lets managers/admins see the Edit/Delete controls even from /catalogue/items const canManage = hasPermission(session.user.role, "manage_products"); return ( diff --git a/App/app/(portal)/inventory/vendors/[id]/page.tsx b/App/app/(portal)/catalogue/vendors/[id]/page.tsx similarity index 98% rename from App/app/(portal)/inventory/vendors/[id]/page.tsx rename to App/app/(portal)/catalogue/vendors/[id]/page.tsx index 184c08c..24e5ee2 100644 --- a/App/app/(portal)/inventory/vendors/[id]/page.tsx +++ b/App/app/(portal)/catalogue/vendors/[id]/page.tsx @@ -48,7 +48,7 @@ export default async function InventoryVendorDetailPage({ params }: Props) {
{/* Breadcrumb */}
- Vendors + Vendors / {vendor.name}
diff --git a/App/app/(portal)/inventory/vendors/[id]/vendor-items-table.tsx b/App/app/(portal)/catalogue/vendors/[id]/vendor-items-table.tsx similarity index 100% rename from App/app/(portal)/inventory/vendors/[id]/vendor-items-table.tsx rename to App/app/(portal)/catalogue/vendors/[id]/vendor-items-table.tsx diff --git a/App/app/(portal)/inventory/vendors/page.tsx b/App/app/(portal)/catalogue/vendors/page.tsx similarity index 100% rename from App/app/(portal)/inventory/vendors/page.tsx rename to App/app/(portal)/catalogue/vendors/page.tsx diff --git a/App/app/(portal)/inventory/vendors/vendors-table.tsx b/App/app/(portal)/catalogue/vendors/vendors-table.tsx similarity index 98% rename from App/app/(portal)/inventory/vendors/vendors-table.tsx rename to App/app/(portal)/catalogue/vendors/vendors-table.tsx index 01d9226..0759c69 100644 --- a/App/app/(portal)/inventory/vendors/vendors-table.tsx +++ b/App/app/(portal)/catalogue/vendors/vendors-table.tsx @@ -68,7 +68,7 @@ export function VendorsTable({ value={currentSiteId ?? ""} onChange={(e) => { const id = e.target.value; - router.push(id ? `/inventory/vendors?siteId=${id}` : "/inventory/vendors"); + router.push(id ? `/catalogue/vendors?siteId=${id}` : "/catalogue/vendors"); }} className="flex-1 max-w-xs rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-1.5 text-sm text-neutral-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20" > @@ -149,7 +149,7 @@ export function VendorsTable({
- + {vendor.name} {vendor.vendorId && ( diff --git a/App/app/(portal)/inventory/cart/cart-view.tsx b/App/app/(portal)/inventory/cart/cart-view.tsx index aa8cfef..cd94ca2 100644 --- a/App/app/(portal)/inventory/cart/cart-view.tsx +++ b/App/app/(portal)/inventory/cart/cart-view.tsx @@ -46,8 +46,8 @@ export function CartView() {

Your cart is empty

Browse Items or Vendors to add line items

- Browse Items - Browse Vendors + Browse Items + Browse Vendors
); @@ -108,7 +108,7 @@ export function CartView() {
- + + Add more items