diff --git a/src/components/collection-confirm/po-card.test.tsx b/src/components/collection-confirm/po-card.test.tsx new file mode 100644 index 0000000..2316465 --- /dev/null +++ b/src/components/collection-confirm/po-card.test.tsx @@ -0,0 +1,109 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { ConfirmRow } from "@/lib/server/collection-confirm-service"; +import { PoCard } from "./po-card"; + +const baseRows: ConfirmRow[] = [ + { + id: "row-1", + sku: "SK-001", + description: "Item 1", + order_qty: 10, + qty_collected: 10, + cost_price: 45, + surplus_qty: 0, + }, + { + id: "row-2", + sku: "SK-002", + description: "Item 2", + order_qty: 20, + qty_collected: 22, + cost_price: 12, + surplus_qty: 2, + }, +]; + +const defaultProps = { + poNumber: "PO-1", + supplier: { id: "s-1", name: "Acme Steel" }, + onCostChange: vi.fn(), + onConfirm: vi.fn(), +}; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("PoCard", () => { + it("renders the PO number and supplier name in the header", () => { + render(); + + expect(screen.getByTestId("po-card-title")).toHaveTextContent( + "PO PO-1 — Acme Steel", + ); + }); + + it("renders the item count in the header (pluralised)", () => { + const { rerender } = render(); + expect(screen.getByText(/2 items/i)).toBeInTheDocument(); + + rerender( + , + ); + expect(screen.getByText(/1 item$/i)).toBeInTheDocument(); + }); + + it("renders the photo strip empty state when no attachments are present", () => { + render(); + + expect(screen.getByText(/No invoice photo attached/i)).toBeInTheDocument(); + }); + + it("renders photo strip chips for attached invoices", () => { + const rowsWithPhoto: ConfirmRow[] = [ + { ...baseRows[0], order_item_attachements: ["invoice.pdf"] }, + ]; + + render(); + + expect( + screen.getByRole("button", { name: /Invoice 1 invoice\.pdf/i }), + ).toBeInTheDocument(); + }); + + it("renders the items table with R-formatted cost values", () => { + render(); + + expect(screen.getByText("R45.00")).toBeInTheDocument(); + expect(screen.getByText("R12.00")).toBeInTheDocument(); + }); + + it("calls onConfirm when the Confirm button is clicked", async () => { + const user = userEvent.setup(); + const onConfirm = vi.fn(); + render(); + + await user.click(screen.getByTestId("po-card-confirm-PO-1")); + + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + + it("forwards onCostChange when a row cost is edited and saved", async () => { + const user = userEvent.setup(); + const onCostChange = vi.fn(); + render( + , + ); + + await user.click(screen.getByTestId("cost-edit-row-1")); + const input = screen.getByTestId("cost-input-row-1"); + await user.clear(input); + await user.type(input, "50"); + await user.keyboard("{Enter}"); + + expect(onCostChange).toHaveBeenCalledWith("row-1", 50); + }); +}); diff --git a/src/components/collection-confirm/po-card.tsx b/src/components/collection-confirm/po-card.tsx new file mode 100644 index 0000000..8f790a5 --- /dev/null +++ b/src/components/collection-confirm/po-card.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import type { ConfirmRow } from "@/lib/server/collection-confirm-service"; +import { ItemsTable } from "./items-table"; +import { PhotoStrip } from "./photo-strip"; + +type Props = { + rows: ConfirmRow[]; + poNumber: string; + supplier: { id: string; name: string }; + onCostChange: (id: string, cost: number) => void; + onConfirm: () => void; +}; + +/** + * Single PO group on the collection-confirm review page. + * + * Composes the photo strip + items table into one card with a confirm + * button at the bottom. The cost edit / read flow lives in `ItemsTable`; + * the parent owns the optimistic update + the actual confirm mutation. + * + * Layout: + * - Header: PO number + supplier name + item count + * - Photo strip (or empty state) + * - Items table + * - Confirm button (footer) + */ +export function PoCard({ rows, poNumber, supplier, onCostChange, onConfirm }: Props) { + const itemCount = rows.length; + return ( + + + + + PO {poNumber} — {supplier.name} + + + {itemCount} item{itemCount === 1 ? "" : "s"} + + + + + + + + + + + + + + + ); +}
+ {itemCount} item{itemCount === 1 ? "" : "s"} +