From 457ad30f12d7d621f3cca447274907691b7b4be8 Mon Sep 17 00:00:00 2001 From: Dave Date: Sat, 17 Oct 2026 10:14:17 +0200 Subject: [PATCH] fix(collection-ai): stock rows tick and show their own line SKU I4: ConfirmRow.line_sku carries stock_purchase_items.sku; the SKU tick, the AI robot and (for stock rows) the SKU column use line_sku before the catalogue sku via effectiveSku. The SKU pencil keeps line_sku in step. Co-Authored-By: Claude Opus 5.5 --- src/app/(app)/collection-confirm/page.tsx | 16 ++++++++++++++-- .../collection-confirm/items-table-ai.test.tsx | 15 +++++++++++++++ .../collection-confirm/items-table.tsx | 14 ++++++++++---- .../server/collection-confirm-service.test.ts | 17 +++++++++++++++++ src/lib/server/collection-confirm-service.ts | 8 ++++++++ 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/app/(app)/collection-confirm/page.tsx b/src/app/(app)/collection-confirm/page.tsx index b027cd3..0490f30 100644 --- a/src/app/(app)/collection-confirm/page.tsx +++ b/src/app/(app)/collection-confirm/page.tsx @@ -267,9 +267,13 @@ export default function CollectionConfirmPage() { async (id: string, sku: string) => { const prevRow = rows?.find((r) => r.id === id) ?? null; const prevSku = prevRow?.sku; + const prevLineSku = prevRow?.line_sku; const endpoint = prevRow ? endpointFor(prevRow, "sku") : null; + // Stock rows display their own line SKU, so keep it in step. setRows((prev) => - prev?.map((r) => (r.id === id ? { ...r, sku } : r)) ?? null, + prev?.map((r) => + r.id === id ? { ...r, sku, ...(r.source === "stock" ? { line_sku: sku } : {}) } : r, + ) ?? null, ); if (!endpoint) return; try { @@ -285,7 +289,15 @@ export default function CollectionConfirmPage() { const message = err instanceof Error ? err.message : String(err); toast.error(message); setRows((prev) => - prev?.map((r) => (r.id === id ? { ...r, sku: prevSku ?? undefined } : r)) ?? null, + prev?.map((r) => + r.id === id + ? { + ...r, + sku: prevSku ?? undefined, + ...(r.source === "stock" ? { line_sku: prevLineSku } : {}), + } + : r, + ) ?? null, ); } }, diff --git a/src/components/collection-confirm/items-table-ai.test.tsx b/src/components/collection-confirm/items-table-ai.test.tsx index 60a08d0..0439df2 100644 --- a/src/components/collection-confirm/items-table-ai.test.tsx +++ b/src/components/collection-confirm/items-table-ai.test.tsx @@ -90,6 +90,21 @@ describe("ItemsTable AI layer", () => { expect(screen.queryByTestId("ai-sku-robot-b")).not.toBeInTheDocument(); }); + it("stock rows: shows, ticks and robot-marks the line's own SKU (line_sku)", () => { + const stock = makeAiRow({ + id: "s1", + source: "stock", + sku: undefined, + line_sku: "SK-001", + ai_check: makeCheck({ sku_filled_value: "SK-001" }), + }); + render(); + const rowEl = screen.getByTestId("ai-ticks-s1").closest("tr")!; + expect(within(rowEl).getByText("SK-001")).toBeInTheDocument(); + expect(screen.getByTestId("ai-sku-robot-s1")).toBeInTheDocument(); + expect(screen.getByLabelText(/^SKU: matches/)).toBeInTheDocument(); + }); + it("shows In POS with a stock link when in_pos", () => { const row = makeAiRow({ in_pos: true, pos_stock_item_id: "stock-77" }); render(); diff --git a/src/components/collection-confirm/items-table.tsx b/src/components/collection-confirm/items-table.tsx index 61556c8..1c85ec6 100644 --- a/src/components/collection-confirm/items-table.tsx +++ b/src/components/collection-confirm/items-table.tsx @@ -14,6 +14,7 @@ import { useState } from "react"; import { Input } from "@/components/ui/input"; import { computeLineTicks, + effectiveSku, showsAiFilledSku, type TickState, } from "@/lib/collection-ai-ticks"; @@ -92,7 +93,7 @@ function AiTicks({ row }: { row: ConfirmRow }) { { setEdit({ id: row.id, - value: row.sku ?? "", + value: shownSku(row) ?? "", error: null, field: "sku", }); @@ -329,8 +335,8 @@ export function ItemsTable({ /> ) : (
- - {row.sku ?? "—"} + + {shownSku(row) ?? "—"} {showsAiFilledSku(row, row.ai_check) ? ( { expect(byId.get("oi-3")).toMatchObject({ in_pos: false, pos_stock_item_id: null }); }); + it("carries a stock row's own SKU as line_sku (and leaves sku as before)", async () => { + const { admin } = makeAiAdmin({ + stockItems: [ + { id: "spi-1", description: "x", quantity: 2, quantity_received: 1, status: "collected", stock_item: "st-1", sku: "SM2NUT9B506" }, + { id: "spi-2", description: "y", quantity: 2, quantity_received: 1, status: "collected", stock_item: "st-2", sku: "" }, + ], + }); + vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); + + const rows = await listCollectedItems(); + const byId = new Map(rows.map((r) => [r.id, r])); + + expect(byId.get("spi-1")?.line_sku).toBe("SM2NUT9B506"); + expect(byId.get("spi-2")?.line_sku).toBe(""); + expect(byId.get("spi-1")?.sku).toBeUndefined(); + }); + it("marks stock rows in_pos with their catalogue item", async () => { const { admin, posGetFullList } = makeAiAdmin({ stockItems: [ diff --git a/src/lib/server/collection-confirm-service.ts b/src/lib/server/collection-confirm-service.ts index 7ab761d..7f8ce05 100644 --- a/src/lib/server/collection-confirm-service.ts +++ b/src/lib/server/collection-confirm-service.ts @@ -109,6 +109,12 @@ export type ConfirmRow = { // collection-confirm page's supplier-SKU pencil and written via // `updateStockPurchaseItem` (patchable whitelist member). supplier_sku?: string; + /** + * Stock-only: the line's own `stock_purchase_items.sku` (what a rep or + * the AI fill writes). Shown, ticked and robot-marked in place of `sku` + * when present. + */ + line_sku?: string; supplier?: string; poNumber?: string; /** AI check of this line against the driver's invoice photo. */ @@ -339,6 +345,7 @@ export async function listCollectedItems(): Promise { stockRows as unknown as Array<{ id: string; stock_item: string; + sku?: string | null; description: string; quantity: number; quantity_received: number; @@ -369,6 +376,7 @@ export async function listCollectedItems(): Promise { quantity: s.quantity, item_status: s.status ?? undefined, sku: s.expand?.stock_item?.sku, + line_sku: typeof s.sku === "string" ? s.sku.trim() : "", supplier: s.expand?.purchase?.expand?.supplier?.id ?? undefined, stock_line_status: s.status ?? null, ai_check: (s as { ai_check?: CollectionAiCheck | null }).ai_check ?? null,