feat(buyer-tasks): group card with color-coded action button
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
6fa44c55ff
commit
10dd4aff81
3 changed files with 130 additions and 1 deletions
63
src/components/buyer-tasks/buyer-task-group-card.test.tsx
Normal file
63
src/components/buyer-tasks/buyer-task-group-card.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
60
src/components/buyer-tasks/buyer-task-group-card.tsx
Normal file
60
src/components/buyer-tasks/buyer-task-group-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
Loading…
Reference in a new issue