From 70f3230c36ae596fe06c01f05c1001d3e64a1e0c Mon Sep 17 00:00:00 2001 From: Hardik Date: Wed, 24 Jun 2026 04:59:47 +0530 Subject: [PATCH 1/3] feat(po): register line items in the product catalogue on approval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously a PO's free-text line items only became reusable catalogue products (/inventory/items) on full payment (markPaid → syncProductCatalog). An approved- but-unpaid PO's items weren't selectable for further POs yet. - extract syncProductCatalog into lib/product-catalog.ts (shared). - call it from approvePo so approved items are immediately catalogued (create product by name if unknown, link the line item, upsert last/per-vendor price); payment still re-syncs to refresh prices. Idempotent. - test: approving a PO with a free-text line creates + links the product and records the per-vendor price. Co-Authored-By: Claude Opus 4.8 (1M context) --- App/CLAUDE.md | 4 + App/app/(portal)/approvals/[id]/actions.ts | 7 ++ App/app/(portal)/payments/actions.ts | 97 +--------------- App/lib/product-catalog.ts | 105 ++++++++++++++++++ .../integration/approval-actions.test.ts | 42 ++++++- 5 files changed, 158 insertions(+), 97 deletions(-) create mode 100644 App/lib/product-catalog.ts diff --git a/App/CLAUDE.md b/App/CLAUDE.md index 4213e2a..008a3c6 100644 --- a/App/CLAUDE.md +++ b/App/CLAUDE.md @@ -132,6 +132,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 `/inventory/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)/approvals/[id]/actions.ts b/App/app/(portal)/approvals/[id]/actions.ts index 4c04098..0d9d288 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 (/inventory/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("/inventory/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)/payments/actions.ts b/App/app/(portal)/payments/actions.ts index 2e9f547..dc2092a 100644 --- a/App/app/(portal)/payments/actions.ts +++ b/App/app/(portal)/payments/actions.ts @@ -4,107 +4,12 @@ import { auth } from "@/auth"; import { db } from "@/lib/db"; import { canPerformAction } from "@/lib/po-state-machine"; import { processPaymentSchema } from "@/lib/validations/po"; +import { syncProductCatalog } from "@/lib/product-catalog"; import { notify } from "@/lib/notifier"; import { revalidatePath } from "next/cache"; type ActionResult = { ok: true } | { error: string }; -function nameToCode(name: string): string { - const slug = name.toUpperCase() - .replace(/[^A-Z0-9]+/g, "-") - .replace(/^-|-$/g, "") - .substring(0, 20); - return `${slug}-${Date.now().toString(36).toUpperCase().slice(-5)}`; -} - -// Sync product catalog after payment is confirmed: -// - Auto-create products for unlinked line items (matched by name or brand new) -// - Upsert per-vendor prices for all items -async function syncProductCatalog( - poId: string, - lineItems: { id: string; name: string; unitPrice: { toNumber(): number } | number; productId: string | null }[], - vendorId: string | null, - actorId: string -) { - const updatedProductIds: string[] = []; - - for (const li of lineItems) { - const unitPrice = typeof li.unitPrice === "number" ? li.unitPrice : li.unitPrice.toNumber(); - let productId = li.productId; - let priceChanged = false; - - if (!productId) { - // Try to find an existing product by name (case-insensitive) - const existing = await db.product.findFirst({ - where: { name: { equals: li.name, mode: "insensitive" }, isActive: true }, - select: { id: true, lastPrice: true }, - }); - - if (existing) { - productId = existing.id; - priceChanged = Number(existing.lastPrice ?? 0) !== unitPrice; - } else { - // Create a new product — first-time registration, not a price update - const code = nameToCode(li.name); - try { - const created = await db.product.create({ - data: { code, name: li.name, lastPrice: unitPrice, lastVendorId: vendorId }, - }); - productId = created.id; - } catch { - // Code collision (extremely unlikely) — add extra entropy - const created = await db.product.create({ - data: { - code: `${code}-${Math.random().toString(36).slice(2, 5).toUpperCase()}`, - name: li.name, - lastPrice: unitPrice, - lastVendorId: vendorId, - }, - }); - productId = created.id; - } - } - - // Link the line item to the product for future reference - await db.pOLineItem.update({ where: { id: li.id }, data: { productId } }); - } else { - const current = await db.product.findUnique({ - where: { id: productId }, - select: { lastPrice: true }, - }); - priceChanged = !current || Number(current.lastPrice ?? 0) !== unitPrice; - } - - // Always update lastPrice / lastVendorId on the product - await db.product.update({ - where: { id: productId }, - data: { lastPrice: unitPrice, lastVendorId: vendorId ?? undefined }, - }); - - // Upsert per-vendor price if PO has a vendor - if (vendorId) { - await db.productVendorPrice.upsert({ - where: { productId_vendorId: { productId, vendorId } }, - update: { price: unitPrice }, - create: { productId, vendorId, price: unitPrice }, - }); - } - - if (priceChanged) updatedProductIds.push(productId); - } - - if (updatedProductIds.length > 0) { - await db.pOAction.create({ - data: { - actionType: "PRODUCT_PRICE_UPDATED", - actorId, - poId, - metadata: { updatedProductIds }, - }, - }); - } -} - // Step 1: Accounts picks up the PO — MGR_APPROVED → SENT_FOR_PAYMENT export async function processPayment({ poId }: { poId: string }): Promise { const session = await auth(); diff --git a/App/lib/product-catalog.ts b/App/lib/product-catalog.ts new file mode 100644 index 0000000..d905011 --- /dev/null +++ b/App/lib/product-catalog.ts @@ -0,0 +1,105 @@ +import { db } from "@/lib/db"; + +/** + * Product catalogue sync — registers a PO's line items as reusable `Product`s + * (the `/inventory/items` catalogue) and keeps last/per-vendor prices fresh: + * - line items with no `productId` are matched to an existing product by name, + * or a brand-new product is created, and the line item is linked back; + * - `lastPrice`/`lastVendorId` and the per-vendor price are upserted. + * + * Called at **approval** (so approved items are immediately reusable in further + * POs) and again at **payment** (to refresh prices on the final figures). The + * function is idempotent — re-running matches the same product by name/id. + */ +function nameToCode(name: string): string { + const slug = name.toUpperCase() + .replace(/[^A-Z0-9]+/g, "-") + .replace(/^-|-$/g, "") + .substring(0, 20); + return `${slug}-${Date.now().toString(36).toUpperCase().slice(-5)}`; +} + +export async function syncProductCatalog( + poId: string, + lineItems: { id: string; name: string; unitPrice: { toNumber(): number } | number; productId: string | null }[], + vendorId: string | null, + actorId: string +) { + const updatedProductIds: string[] = []; + + for (const li of lineItems) { + const unitPrice = typeof li.unitPrice === "number" ? li.unitPrice : li.unitPrice.toNumber(); + let productId = li.productId; + let priceChanged = false; + + if (!productId) { + // Try to find an existing product by name (case-insensitive) + const existing = await db.product.findFirst({ + where: { name: { equals: li.name, mode: "insensitive" }, isActive: true }, + select: { id: true, lastPrice: true }, + }); + + if (existing) { + productId = existing.id; + priceChanged = Number(existing.lastPrice ?? 0) !== unitPrice; + } else { + // Create a new product — first-time registration, not a price update + const code = nameToCode(li.name); + try { + const created = await db.product.create({ + data: { code, name: li.name, lastPrice: unitPrice, lastVendorId: vendorId }, + }); + productId = created.id; + } catch { + // Code collision (extremely unlikely) — add extra entropy + const created = await db.product.create({ + data: { + code: `${code}-${Math.random().toString(36).slice(2, 5).toUpperCase()}`, + name: li.name, + lastPrice: unitPrice, + lastVendorId: vendorId, + }, + }); + productId = created.id; + } + } + + // Link the line item to the product for future reference + await db.pOLineItem.update({ where: { id: li.id }, data: { productId } }); + } else { + const current = await db.product.findUnique({ + where: { id: productId }, + select: { lastPrice: true }, + }); + priceChanged = !current || Number(current.lastPrice ?? 0) !== unitPrice; + } + + // Always update lastPrice / lastVendorId on the product + await db.product.update({ + where: { id: productId }, + data: { lastPrice: unitPrice, lastVendorId: vendorId ?? undefined }, + }); + + // Upsert per-vendor price if PO has a vendor + if (vendorId) { + await db.productVendorPrice.upsert({ + where: { productId_vendorId: { productId, vendorId } }, + update: { price: unitPrice }, + create: { productId, vendorId, price: unitPrice }, + }); + } + + if (priceChanged) updatedProductIds.push(productId); + } + + if (updatedProductIds.length > 0) { + await db.pOAction.create({ + data: { + actionType: "PRODUCT_PRICE_UPDATED", + actorId, + poId, + metadata: { updatedProductIds }, + }, + }); + } +} diff --git a/App/tests/integration/approval-actions.test.ts b/App/tests/integration/approval-actions.test.ts index 5006743..56e0a02 100644 --- a/App/tests/integration/approval-actions.test.ts +++ b/App/tests/integration/approval-actions.test.ts @@ -2,7 +2,7 @@ * Integration tests for manager approval server actions. * Covers: M-02 (approve / approve+note), M-03 (reject), M-04 (request edits, vendor ID), S-06 (provide vendor ID), S-07 (resubmit after edits). */ -import { vi, describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest"; +import { vi, describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest"; vi.mock("@/auth", () => ({ auth: vi.fn() })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); @@ -47,6 +47,12 @@ afterEach(async () => { await deletePosByTitle(PREFIX); }); +afterAll(async () => { + // Products auto-created by the catalogue-on-approval test. + await db.productVendorPrice.deleteMany({ where: { product: { name: { startsWith: PREFIX } } } }); + await db.product.deleteMany({ where: { name: { startsWith: PREFIX } } }); +}); + // Helper: create a PO in MGR_REVIEW state async function createSubmittedPo(title: string): Promise { vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(techId, "TECHNICAL")); @@ -159,6 +165,40 @@ describe("issue #92 — advance payment on approval", () => { }); }); +// ── Product catalogue registered on approval (so items are reusable) ───────── + +describe("product catalogue on approval", () => { + it("creates a catalogue product for a free-text line item and links it", async () => { + vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(techId, "TECHNICAL")); + const itemName = `${PREFIX}Starter VPS hosting`; + const form = makePoForm({ + title: `${PREFIX}CatApprove`, + vesselId, + accountId, + intent: "submit", + lineItems: [{ description: itemName, quantity: 1, unit: "pc", unitPrice: 459.95 }], + }); + const { id: poId } = (await createPo(form)) as { id: string }; + await db.purchaseOrder.update({ where: { id: poId }, data: { vendorId } }); + + // No catalogue product exists for this name before approval. + expect(await db.product.findFirst({ where: { name: { equals: itemName, mode: "insensitive" } } })).toBeNull(); + + vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(managerId, "MANAGER")); + expect(await approvePo({ poId })).toEqual({ ok: true }); + + const product = await db.product.findFirst({ where: { name: { equals: itemName, mode: "insensitive" } } }); + expect(product).not.toBeNull(); + expect(Number(product!.lastPrice)).toBe(459.95); + + // The line item is linked back to the new product, and a per-vendor price is recorded. + const li = await db.pOLineItem.findFirstOrThrow({ where: { poId } }); + expect(li.productId).toBe(product!.id); + const pvp = await db.productVendorPrice.findFirst({ where: { productId: product!.id, vendorId } }); + expect(pvp).not.toBeNull(); + }); +}); + // ── M-03: Reject ────────────────────────────────────────────────────────────── describe("M-03 — reject PO", () => { From d7b455ab7d7611f01160683c8d3f8d3707c24b49 Mon Sep 17 00:00:00 2001 From: Hardik Date: Wed, 24 Jun 2026 05:04:29 +0530 Subject: [PATCH 2/3] =?UTF-8?q?refactor(routes):=20move=20/inventory/{item?= =?UTF-8?q?s,vendors}=20=E2=86=92=20/catalogue/{items,vendors}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames the product-catalogue pages (items + vendors, incl. their [id] detail pages) out of /inventory into /catalogue. /inventory/cart is unchanged. All internal links, redirects, revalidatePath calls, sidebar nav, and tests are updated; next.config redirects keep old /inventory/{items,vendors}[/...] URLs working (permanent) so existing bookmarks don't 404. Co-Authored-By: Claude Opus 4.8 (1M context) --- App/CLAUDE.md | 2 +- App/app/(portal)/admin/products/[id]/page.tsx | 2 +- .../admin/products/products-table.tsx | 2 +- App/app/(portal)/admin/vendors/actions.ts | 4 +-- App/app/(portal)/approvals/[id]/actions.ts | 4 +-- .../items/[id]/item-price-chart.tsx | 0 .../items/[id]/page.tsx | 4 +-- .../items/items-table.tsx | 4 +-- .../{inventory => catalogue}/items/page.tsx | 2 +- .../vendors/[id]/page.tsx | 2 +- .../vendors/[id]/vendor-items-table.tsx | 0 .../{inventory => catalogue}/vendors/page.tsx | 0 .../vendors/vendors-table.tsx | 4 +-- App/app/(portal)/inventory/cart/cart-view.tsx | 6 ++--- App/app/(portal)/po/[id]/receipt/actions.ts | 2 +- App/app/(portal)/po/import/actions.ts | 2 +- App/components/layout/sidebar.tsx | 8 +++--- App/lib/product-catalog.ts | 2 +- App/next.config.ts | 8 ++++++ App/tests/e2e/inventory/cart-icon.spec.ts | 18 ++++++------- App/tests/e2e/inventory/items-tags.spec.ts | 26 +++++++++---------- App/tests/unit/vendors-table.test.tsx | 2 +- 22 files changed, 56 insertions(+), 48 deletions(-) rename App/app/(portal)/{inventory => catalogue}/items/[id]/item-price-chart.tsx (100%) rename App/app/(portal)/{inventory => catalogue}/items/[id]/page.tsx (98%) rename App/app/(portal)/{inventory => catalogue}/items/items-table.tsx (98%) rename App/app/(portal)/{inventory => catalogue}/items/page.tsx (97%) rename App/app/(portal)/{inventory => catalogue}/vendors/[id]/page.tsx (98%) rename App/app/(portal)/{inventory => catalogue}/vendors/[id]/vendor-items-table.tsx (100%) rename App/app/(portal)/{inventory => catalogue}/vendors/page.tsx (100%) rename App/app/(portal)/{inventory => catalogue}/vendors/vendors-table.tsx (98%) diff --git a/App/CLAUDE.md b/App/CLAUDE.md index 008a3c6..49b28b8 100644 --- a/App/CLAUDE.md +++ b/App/CLAUDE.md @@ -134,7 +134,7 @@ Inventory (`ItemInventory`, keyed by `productId` + `siteId`) is **incremented at ### Product catalogue sync (`lib/product-catalog.ts`) -`syncProductCatalog(poId, lineItems, vendorId, actorId)` registers a PO's line items as reusable **`Product`s** (the `/inventory/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.) +`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 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 0d9d288..3470cca 100644 --- a/App/app/(portal)/approvals/[id]/actions.ts +++ b/App/app/(portal)/approvals/[id]/actions.ts @@ -85,11 +85,11 @@ export async function approvePo({ revalidatePath(`/admin/sites/${siteId}`); } - // Register the line items in the product catalogue (/inventory/items) on + // 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("/inventory/items"); + revalidatePath("/catalogue/items"); const accounts = await db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } }); await notify({ 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