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 <noreply@anthropic.com>
This commit is contained in:
parent
b49911601d
commit
457ad30f12
5 changed files with 64 additions and 6 deletions
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(<ItemsTable rows={[stock]} onCostChange={noop} onSkuChange={noop} />);
|
||||
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(<ItemsTable rows={[row]} onCostChange={noop} onSkuChange={noop} onAddToPos={noop} />);
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
|||
<Tick
|
||||
name="SKU"
|
||||
state={t.sku}
|
||||
detail={`Ours: ${row.sku || "none"} / Document: ${doc?.code || "none"}`}
|
||||
detail={`Ours: ${effectiveSku(row) || "none"} / Document: ${doc?.code || "none"}`}
|
||||
/>
|
||||
<Tick
|
||||
name="Qty"
|
||||
|
|
@ -113,6 +114,11 @@ function AiTicks({ row }: { row: ConfirmRow }) {
|
|||
);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
type EditState = {
|
||||
id: string;
|
||||
value: string;
|
||||
|
|
@ -188,7 +194,7 @@ export function ItemsTable({
|
|||
const startSkuEdit = (row: ConfirmRow) => {
|
||||
setEdit({
|
||||
id: row.id,
|
||||
value: row.sku ?? "",
|
||||
value: shownSku(row) ?? "",
|
||||
error: null,
|
||||
field: "sku",
|
||||
});
|
||||
|
|
@ -329,8 +335,8 @@ export function ItemsTable({
|
|||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 font-mono text-xs">
|
||||
<span data-sku-value={row.sku ?? ""} className="min-w-0 flex-1 truncate">
|
||||
{row.sku ?? "—"}
|
||||
<span data-sku-value={shownSku(row) ?? ""} className="min-w-0 flex-1 truncate">
|
||||
{shownSku(row) ?? "—"}
|
||||
</span>
|
||||
{showsAiFilledSku(row, row.ai_check) ? (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -1384,6 +1384,23 @@ describe("listCollectedItems — AI check passthrough", () => {
|
|||
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: [
|
||||
|
|
|
|||
|
|
@ -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<ConfirmRow[]> {
|
|||
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<ConfirmRow[]> {
|
|||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue