diff --git a/src/components/collection-confirm/items-table-ai.test.tsx b/src/components/collection-confirm/items-table-ai.test.tsx index 0458a28..b2d642f 100644 --- a/src/components/collection-confirm/items-table-ai.test.tsx +++ b/src/components/collection-confirm/items-table-ai.test.tsx @@ -28,21 +28,30 @@ describe("ItemsTable AI layer", () => { expect(screen.queryByText(/add to pos/i)).not.toBeInTheDocument(); }); - it("shows only the POS column (no AI column, no AI min-width) when no row has ai_check", () => { + it("keeps the original six columns and shows POS status under the description when no row has ai_check", () => { const { container } = render( , ); const headers = screen.getAllByRole("columnheader").map((h) => h.textContent); - expect(headers).toEqual(["SKU", "Description", "Ord", "Coll", "Cost", "Surplus", "POS"]); - expect(container.querySelector("table")?.className).not.toContain("min-w-[860px]"); - expect(container.querySelectorAll("col")).toHaveLength(7); - expect(screen.getByText(/tap the pencil/i).closest("td")).toHaveAttribute("colspan", "7"); + expect(headers).toEqual(["SKU", "Description", "Ord", "Coll", "Cost", "Surplus"]); + expect(container.querySelector("table")?.className).toContain("min-w-[640px]"); + expect(container.querySelectorAll("col")).toHaveLength(6); + expect(screen.getByText(/tap the pencil/i).closest("td")).toHaveAttribute("colspan", "6"); + const extra = screen.getByTestId(`ai-pos-row-${plain.id}`); + expect(extra.closest("td")?.textContent).toContain(plain.description ?? ""); + expect(extra.textContent).toContain("Add to POS"); + expect(container.querySelector('[data-testid^="ai-ticks-"]')).toBeNull(); }); - it("adds AI and POS headers when a row has ai_check", () => { - render(); + it("shows the ticks under the description (no extra columns) when a row has ai_check", () => { + const row = makeAiRow(); + render(); const headers = screen.getAllByRole("columnheader").map((h) => h.textContent); - expect(headers).toEqual(["SKU", "Description", "Ord", "Coll", "Cost", "Surplus", "AI", "POS"]); + expect(headers).toEqual(["SKU", "Description", "Ord", "Coll", "Cost", "Surplus"]); + const ticks = screen.getByTestId(`ai-ticks-${row.id}`); + expect(ticks.closest("td")?.textContent).toContain(row.description ?? ""); + expect(ticks.textContent).toContain("SKU"); + expect(ticks.textContent).toContain("Price"); }); it("renders four ticks with aria-labels from computeLineTicks and ours-vs-document titles", () => { diff --git a/src/components/collection-confirm/items-table.tsx b/src/components/collection-confirm/items-table.tsx index f0c04e0..69422ee 100644 --- a/src/components/collection-confirm/items-table.tsx +++ b/src/components/collection-confirm/items-table.tsx @@ -73,9 +73,12 @@ function Tick({ name, state, detail }: { name: string; state: TickState; detail: title={detail} data-tick={name.toLowerCase()} data-state={state} - className={`inline-flex ${TICK_CLASS[state]}`} + className={`inline-flex items-center gap-0.5 ${TICK_CLASS[state]}`} > + + {name} + ); } @@ -90,7 +93,7 @@ function AiTicks({ row }: { row: ConfirmRow }) { const priceDoc = doc?.unit_price == null ? "none" : `R${doc.unit_price.toFixed(2)}`; const note = doc?.price_note ? ` (${doc.price_note})` : ""; return ( -
+
void }) { + if (row.in_pos) { + return row.pos_stock_item_id ? ( + + In POS + + ) : ( + In POS + ); + } + if (row.in_pos === false && onAddToPos) { + const hasSku = !!effectiveSku(row).trim(); + return ( + + ); + } + return null; +} + /** Stock rows show their own line SKU when set; everything else as before. */ function shownSku(row: ConfirmRow): string | undefined { return row.line_sku || row.sku; @@ -181,7 +216,7 @@ export function ItemsTable({ }: Props) { const aiColumn = showAi ?? rows.some((r) => !!r.ai_check); const posColumn = showAi ?? rows.some((r) => typeof r.in_pos === "boolean"); - const columnCount = 6 + (aiColumn ? 1 : 0) + (posColumn ? 1 : 0); + const columnCount = 6; const [edit, setEdit] = useState(null); const startCostEdit = (row: ConfirmRow) => { @@ -270,11 +305,7 @@ export function ItemsTable({ }; return ( - +
{/* Explicit column widths. `table-fixed` honours these, so a cell that goes into edit mode can no longer steal width from its @@ -295,8 +326,6 @@ export function ItemsTable({ - {aiColumn ? : null} - {posColumn ? : null} @@ -306,8 +335,6 @@ export function ItemsTable({ - {aiColumn ? : null} - {posColumn ? : null} @@ -378,6 +405,18 @@ export function ItemsTable({ {row.description ?? row.po_description ?? "—"} + {/* AI ticks and POS status sit under the description, not in + extra columns: the table shares its pane with the document + preview, and trailing columns ended up hidden behind it. */} + {(aiColumn && row.ai_check) || (posColumn && typeof row.in_pos === "boolean") ? ( +
+ {aiColumn && row.ai_check ? : null} + {posColumn ? : null} +
+ ) : null} - {aiColumn ? ( - - ) : null} - {posColumn ? ( - - ) : null} ); })} diff --git a/src/lib/server/collection-ai-service.test.ts b/src/lib/server/collection-ai-service.test.ts index 22bda67..3b8726e 100644 --- a/src/lib/server/collection-ai-service.test.ts +++ b/src/lib/server/collection-ai-service.test.ts @@ -728,7 +728,7 @@ describe("checkCollectionCard", () => { expect(r.skus_filled).toBe(1); }); - it("does not fill a SKU the AI couldn't read with certainty, even on a 'sure' pairing", async () => { + it("still fills a SKU the AI wasn't certain it read (the rep edits it if wrong)", async () => { seedNss(); const { deps } = makeDeps({ readDocument: vi.fn(async () => @@ -740,11 +740,9 @@ describe("checkCollectionCard", () => { const r = await checkCollectionCard(PO_CEM, { deps }); - expect(skuCalls).toEqual([]); - expect(table("order_items").get("l1")?.sku).toBe(""); - expect(aiCheck("order_items", "l1")?.sku_filled_value).toBe(""); - expect(aiCheck("order_items", "l1")?.doc_line?.code).toBe("0515315"); - expect(r.skus_filled).toBe(0); + expect(table("order_items").get("l1")?.sku).toBe("0515315"); + expect(aiCheck("order_items", "l1")?.sku_filled_value).toBe("0515315"); + expect(r.skus_filled).toBe(1); }); it("never changes an existing SKU, even on a 'sure' pairing", async () => { diff --git a/src/lib/server/collection-ai-service.ts b/src/lib/server/collection-ai-service.ts index 65aa527..bb73eca 100644 --- a/src/lib/server/collection-ai-service.ts +++ b/src/lib/server/collection-ai-service.ts @@ -828,10 +828,9 @@ async function checkCard( let skuFilled = previous?.sku_filled_value && str(fresh.sku) === previous.sku_filled_value ? previous.sku_filled_value : ""; const code = chosen.docLine?.code.trim() ?? ""; - // Only a code the AI read character-for-character may become a SKU: - // a sure pairing with a misread digit would still be a wrong SKU. - const codeCertain = chosen.docLine?.code_certain === true; - if (!ownSku && code && codeCertain && code.length <= MAX_SKU_LENGTH && chosen.decision?.same_item === "sure") { + // Owner's call (2026-10-17): fill even when a digit may be misread on a + // blurry photo; the rep edits it, and a near-miss code still helps. + if (!ownSku && code && code.length <= MAX_SKU_LENGTH && chosen.decision?.same_item === "sure") { // A failed fill must not cost the line its check. try { if (line.kind === "customer") {
Coll Cost SurplusAIPOS
{ordered} @@ -498,38 +537,6 @@ export function ItemsTable({ {surplus > 0 ? `+${surplus}` : "—"} - - - {row.in_pos ? ( - row.pos_stock_item_id ? ( - - In POS - - ) : ( - In POS - ) - ) : row.in_pos === false && onAddToPos ? ( - - ) : null} -