feat(buyer-tasks): group card with color-coded action button

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-07-29 20:11:39 +02:00
parent 6fa44c55ff
commit 10dd4aff81
3 changed files with 130 additions and 1 deletions

View file

@ -0,0 +1,63 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { BuyerTaskGroupCard } from "./buyer-task-group-card";
const baseGroup = {
groupId: "order-1::sup-1",
orderId: "order-1",
orderNumber: "ORD-001",
supplierId: "sup-1",
supplierName: "Steel Co",
customerName: "J. Smith",
companyName: "Acme",
items: [
{ id: "item-1", description: "HSS drill", quantity: 2, costPrice: 25, sellingPrice: 50 },
],
itemCount: 1,
totalCost: 50,
purchaseOrderId: null,
poStatus: null,
step: "needs_po" as const,
};
describe("BuyerTaskGroupCard", () => {
it("renders order, supplier, customer info", () => {
render(<BuyerTaskGroupCard group={baseGroup} onAction={vi.fn()} />);
expect(screen.getByText("ORD-001")).toBeInTheDocument();
expect(screen.getByText("Steel Co")).toBeInTheDocument();
expect(screen.getByText("J. Smith", { exact: true })).toBeInTheDocument();
expect(screen.getByText("Acme", { exact: true })).toBeInTheDocument();
});
it("renders the right action button per step", () => {
const cases: Array<{ step: "needs_po" | "po_draft" | "po_sent"; label: RegExp }> = [
{ step: "needs_po", label: /generate po/i },
{ step: "po_draft", label: /send po/i },
{ step: "po_sent", label: /mark as ready/i },
];
for (const { step, label } of cases) {
const { unmount } = render(
<BuyerTaskGroupCard group={{ ...baseGroup, step }} onAction={vi.fn()} />
);
expect(
screen.getByTestId(`buyer-task-action-${baseGroup.groupId}-${step}`)
).toHaveTextContent(label);
unmount();
}
});
it("calls onAction when the button is clicked", () => {
const onAction = vi.fn();
render(<BuyerTaskGroupCard group={baseGroup} onAction={onAction} />);
fireEvent.click(
screen.getByTestId(`buyer-task-action-${baseGroup.groupId}-${baseGroup.step}`)
);
expect(onAction).toHaveBeenCalledWith(baseGroup);
});
it("renders the items list", () => {
render(<BuyerTaskGroupCard group={baseGroup} onAction={vi.fn()} />);
fireEvent.click(screen.getByTestId("buyer-task-items-toggle"));
expect(screen.getByText("HSS drill")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,60 @@
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { BuyerTaskItemsList, type BuyerTaskItem } from "./buyer-task-items-list";
import type { BuyerTaskGroup } from "@/lib/server/buyer-tasks-service";
const ACTION_LABEL: Record<BuyerTaskGroup["step"], string> = {
needs_po: "Generate PO",
po_draft: "Send PO",
po_sent: "Mark as ready",
};
const ACTION_CLASS: Record<BuyerTaskGroup["step"], string> = {
needs_po: "bg-blue-600 hover:bg-blue-700 text-white",
po_draft: "bg-amber-500 hover:bg-amber-600 text-white",
po_sent: "bg-purple-600 hover:bg-purple-700 text-white",
};
export function BuyerTaskGroupCard({
group,
onAction,
disabled,
}: {
group: BuyerTaskGroup;
onAction: (group: BuyerTaskGroup) => void;
disabled?: boolean;
}) {
return (
<Card data-testid={`buyer-task-group-${group.groupId}`}>
<CardContent className="p-0">
<div className="flex items-start gap-3 p-3">
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2">
<span className="font-mono text-sm font-medium">{group.orderNumber}</span>
<span className="text-sm text-muted-foreground">·</span>
<span className="text-sm font-medium">{group.supplierName}</span>
</div>
<div className="text-xs text-muted-foreground mt-0.5">
<span>{group.customerName}</span><span aria-hidden="true"> · </span><span>{group.companyName}</span>
</div>
<div className="text-xs text-muted-foreground mt-1">
{group.itemCount} item{group.itemCount === 1 ? "" : "s"} · Cost R{group.totalCost.toFixed(2)}
</div>
</div>
<Button
type="button"
onClick={() => onAction(group)}
disabled={disabled}
className={ACTION_CLASS[group.step]}
data-testid={`buyer-task-action-${group.groupId}-${group.step}`}
>
{ACTION_LABEL[group.step]}
</Button>
</div>
<BuyerTaskItemsList items={group.items as BuyerTaskItem[]} />
</CardContent>
</Card>
);
}

View file

@ -1 +1,7 @@
import "@testing-library/jest-dom/vitest";
import "@testing-library/jest-dom/vitest";
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
afterEach(() => {
cleanup();
});