feat(collection-confirm): add PoCard composing PhotoStrip+ItemsTable
Wraps a single PO group on the collection-confirm review page. Renders the photo strip + items table + confirm button, with the PO number and supplier name in the header. The cost edit/read flow stays inside ItemsTable; the parent owns onCostChange optimistic updates and the actual confirm mutation. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
4310e4b211
commit
0df6fb1ff2
2 changed files with 175 additions and 0 deletions
109
src/components/collection-confirm/po-card.test.tsx
Normal file
109
src/components/collection-confirm/po-card.test.tsx
Normal file
|
|
@ -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(<PoCard rows={baseRows} {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("po-card-title")).toHaveTextContent(
|
||||
"PO PO-1 — Acme Steel",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the item count in the header (pluralised)", () => {
|
||||
const { rerender } = render(<PoCard rows={baseRows} {...defaultProps} />);
|
||||
expect(screen.getByText(/2 items/i)).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<PoCard rows={[baseRows[0]]} {...defaultProps} />,
|
||||
);
|
||||
expect(screen.getByText(/1 item$/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the photo strip empty state when no attachments are present", () => {
|
||||
render(<PoCard rows={baseRows} {...defaultProps} />);
|
||||
|
||||
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(<PoCard rows={rowsWithPhoto} {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: /Invoice 1 invoice\.pdf/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the items table with R-formatted cost values", () => {
|
||||
render(<PoCard rows={baseRows} {...defaultProps} />);
|
||||
|
||||
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(<PoCard rows={baseRows} {...defaultProps} onConfirm={onConfirm} />);
|
||||
|
||||
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(
|
||||
<PoCard rows={baseRows} {...defaultProps} onCostChange={onCostChange} />,
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
66
src/components/collection-confirm/po-card.tsx
Normal file
66
src/components/collection-confirm/po-card.tsx
Normal file
|
|
@ -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 (
|
||||
<section
|
||||
className="rounded-xl border bg-card p-4 shadow-sm"
|
||||
data-testid={`po-card-${poNumber}`}
|
||||
>
|
||||
<header className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold" data-testid="po-card-title">
|
||||
PO {poNumber} — {supplier.name}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{itemCount} item{itemCount === 1 ? "" : "s"}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mb-4">
|
||||
<PhotoStrip rows={rows} />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<ItemsTable rows={rows} onCostChange={onCostChange} />
|
||||
</div>
|
||||
|
||||
<footer className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
data-testid={`po-card-confirm-${poNumber}`}
|
||||
>
|
||||
Confirm for invoicing
|
||||
</Button>
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue