diff --git a/src/components/buyer-tasks/buyer-task-group-card.test.tsx b/src/components/buyer-tasks/buyer-task-group-card.test.tsx index 6224c1c..c4d5734 100644 --- a/src/components/buyer-tasks/buyer-task-group-card.test.tsx +++ b/src/components/buyer-tasks/buyer-task-group-card.test.tsx @@ -50,6 +50,87 @@ describe("BuyerTaskGroupCard", () => { } }); + it("renders two buttons on the PO Sent tab", () => { + const onResend = vi.fn(); + render( + + ); + expect(screen.getByTestId(/buyer-task-resend-/)).toBeInTheDocument(); + expect(screen.getByTestId(/buyer-task-action-.*-po_sent/)).toBeInTheDocument(); + }); + + it("Re-Send uses the info theme token by default", () => { + render( + + ); + const btn = screen.getByTestId(/buyer-task-resend-/); + expect(btn.className).toMatch(/bg-info/); + expect(btn.className).toMatch(/text-info-foreground/); + }); + + it("Re-Send switches to destructive styling when mail_failed", () => { + render( + + ); + const btn = screen.getByTestId(/buyer-task-resend-/) as HTMLButtonElement; + expect(btn.className).toMatch(/bg-destructive/); + expect(btn.textContent).toBe("Resend"); + }); + + it("Mark as ready uses the primary theme token (no hardcoded purple)", () => { + render( + + ); + const btn = screen.getByTestId(/buyer-task-action-.*-po_sent/); + expect(btn.className).toMatch(/bg-primary/); + expect(btn.className).not.toMatch(/purple-600/); + }); + + it("Re-Send click fires onResend; Mark as ready click fires onAction", () => { + const onResend = vi.fn(); + const onAction = vi.fn(); + render( + + ); + fireEvent.click(screen.getByTestId(/buyer-task-resend-/)); + expect(onResend).toHaveBeenCalledTimes(1); + expect(onAction).not.toHaveBeenCalled(); + fireEvent.click(screen.getByTestId(/buyer-task-action-.*-po_sent/)); + expect(onAction).toHaveBeenCalledTimes(1); + expect(onResend).toHaveBeenCalledTimes(1); // unchanged from first click + }); + + it("po_draft still renders a single Send PO button (no regression)", () => { + render( + + ); + expect(screen.queryByTestId(/buyer-task-resend-/)).not.toBeInTheDocument(); + expect(screen.getByTestId(/buyer-task-action-.*-po_draft/)).toBeInTheDocument(); + }); + it("calls onAction when the button is clicked", () => { const onAction = vi.fn(); render(); diff --git a/src/components/buyer-tasks/buyer-task-group-card.tsx b/src/components/buyer-tasks/buyer-task-group-card.tsx index 9df0a7f..bd23cd6 100644 --- a/src/components/buyer-tasks/buyer-task-group-card.tsx +++ b/src/components/buyer-tasks/buyer-task-group-card.tsx @@ -18,16 +18,26 @@ const ACTION_LABEL: Record = { const ACTION_CLASS: Record = { 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", + po_sent: "bg-primary text-primary-foreground hover:bg-primary/90", }; +// Resend button styling for the po_sent step. Uses the themed `info` +// token so it works in light/dark themes and any future reskin. When +// the PO is mail_failed we override to destructive to keep the +// existing visual signal that something went wrong. +const RESEND_DEFAULT_CLASS = "bg-info text-info-foreground hover:bg-info/90"; +const RESEND_FAILED_CLASS = + "bg-destructive text-destructive-foreground hover:bg-destructive/90"; + export function BuyerTaskGroupCard({ group, onAction, + onResend, disabled, }: { group: BuyerTaskGroup; onAction: (group: BuyerTaskGroup) => void; + onResend?: (group: BuyerTaskGroup) => void; disabled?: boolean; }) { // Deterministic supplier stripe colour. Same supplier โ†’ same colour @@ -95,20 +105,53 @@ export function BuyerTaskGroupCard({ {group.itemCount} item{group.itemCount === 1 ? "" : "s"} ยท Cost R{group.totalCost.toFixed(2)} - + {group.step === "po_sent" ? ( + // PO Sent: two side-by-side buttons. Re-Send opens the resend modal + // (reuses the SendPoModal wired up for po_draft on the parent page). + // Mark as ready opens the MarkReadyDialog. The mail_failed override + // now lands on the Re-Send button (the Mark as ready button stays + // primary โ€” collection can proceed independently of mail state). +
+ + +
+ ) : ( + // needs_po / po_draft: single button (unchanged behaviour). + + )} {group.poStatus === "mail_failed" && group.emailError && (
{ }); }); + it("clicking Re-Send on a po_sent card opens SendPoModal", async () => { + const poSentGroup = { + ...sampleGroup, + groupId: "order-1::sup-1::po-sent", + purchaseOrderId: "po-1", + poStatus: "sent" as const, + poNumber: "PO-TEST-001", + supplierEmail: "supplier@example.com", + step: "po_sent" as const, + }; + vi.spyOn(global, "fetch").mockImplementation(async (input) => { + const url = String(input); + const groups = url.includes("tab=po_sent") ? [poSentGroup] : []; + return new Response(JSON.stringify({ groups }), { status: 200 }); + }); + + render(); + fireEvent.click(screen.getByTestId("buyer-task-tab-po_sent")); + + const resendBtn = await screen.findByTestId(/buyer-task-resend-/); + fireEvent.click(resendBtn); + expect(await screen.findByText(/resend purchase order po-test-001/i)).toBeInTheDocument(); + }); + it("calls generate-po when the action button is clicked", async () => { vi.spyOn(global, "fetch") .mockResolvedValueOnce( diff --git a/src/components/buyer-tasks/buyer-tasks-page.tsx b/src/components/buyer-tasks/buyer-tasks-page.tsx index 8d2b216..05c5161 100644 --- a/src/components/buyer-tasks/buyer-tasks-page.tsx +++ b/src/components/buyer-tasks/buyer-tasks-page.tsx @@ -140,6 +140,15 @@ export function BuyerTasksPage() { } } + function handleResend(group: BuyerTaskGroup) { + // The same SendPoModal flow used for po_draft handles po_sent resends + // โ€” the modal pre-fills from the previous email_payload and shows + // "Resend" in the title/button when status is sent or mail_failed. + // We guard on step in case a caller passes the wrong step. + if (group.step !== "po_sent") return; + setSendPoFor(group); + } + async function handleMarkReadyConfirm(note: string) { if (!markReadyFor) return; const group = markReadyFor; @@ -210,6 +219,7 @@ export function BuyerTasksPage() { key={group.groupId} group={group} onAction={handleAction} + onResend={handleResend} disabled={busyGroupId === group.groupId} /> ))}