feat(buyer-tasks): add Re-Send button on PO Sent tab + theme-token cleanup
Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
ff94086ec0
commit
6008f17c9a
4 changed files with 173 additions and 15 deletions
|
|
@ -50,6 +50,87 @@ describe("BuyerTaskGroupCard", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("renders two buttons on the PO Sent tab", () => {
|
||||
const onResend = vi.fn();
|
||||
render(
|
||||
<BuyerTaskGroupCard
|
||||
group={{ ...baseGroup, step: "po_sent" }}
|
||||
onAction={vi.fn()}
|
||||
onResend={onResend}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<BuyerTaskGroupCard
|
||||
group={{ ...baseGroup, step: "po_sent" }}
|
||||
onAction={vi.fn()}
|
||||
onResend={vi.fn()}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<BuyerTaskGroupCard
|
||||
group={{ ...baseGroup, step: "po_sent", poStatus: "mail_failed" }}
|
||||
onAction={vi.fn()}
|
||||
onResend={vi.fn()}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<BuyerTaskGroupCard
|
||||
group={{ ...baseGroup, step: "po_sent" }}
|
||||
onAction={vi.fn()}
|
||||
onResend={vi.fn()}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<BuyerTaskGroupCard
|
||||
group={{ ...baseGroup, step: "po_sent" }}
|
||||
onAction={onAction}
|
||||
onResend={onResend}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<BuyerTaskGroupCard
|
||||
group={{ ...baseGroup, step: "po_draft" }}
|
||||
onAction={vi.fn()}
|
||||
/>
|
||||
);
|
||||
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(<BuyerTaskGroupCard group={baseGroup} onAction={onAction} />);
|
||||
|
|
|
|||
|
|
@ -18,16 +18,26 @@ const ACTION_LABEL: Record<BuyerTaskGroup["step"], string> = {
|
|||
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",
|
||||
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)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => onAction(group)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
ACTION_CLASS[group.step],
|
||||
group.poStatus === "mail_failed" &&
|
||||
"bg-destructive hover:bg-destructive/90 text-destructive-foreground",
|
||||
)}
|
||||
data-testid={`buyer-task-action-${group.groupId}-${group.step}`}
|
||||
data-po-status={group.poStatus ?? "none"}
|
||||
>
|
||||
{group.poStatus === "mail_failed" ? "Resend" : ACTION_LABEL[group.step]}
|
||||
</Button>
|
||||
{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).
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => onResend?.(group)}
|
||||
disabled={disabled || !onResend}
|
||||
className={cn(
|
||||
RESEND_DEFAULT_CLASS,
|
||||
group.poStatus === "mail_failed" && RESEND_FAILED_CLASS,
|
||||
)}
|
||||
data-testid={`buyer-task-resend-${group.groupId}`}
|
||||
data-po-status={group.poStatus ?? "none"}
|
||||
>
|
||||
{group.poStatus === "mail_failed" ? "Resend" : "Re-Send"}
|
||||
</Button>
|
||||
<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>
|
||||
) : (
|
||||
// needs_po / po_draft: single button (unchanged behaviour).
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => onAction(group)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
ACTION_CLASS[group.step],
|
||||
group.poStatus === "mail_failed" &&
|
||||
"bg-destructive hover:bg-destructive/90 text-destructive-foreground",
|
||||
)}
|
||||
data-testid={`buyer-task-action-${group.groupId}-${group.step}`}
|
||||
data-po-status={group.poStatus ?? "none"}
|
||||
>
|
||||
{group.poStatus === "mail_failed" ? "Resend" : ACTION_LABEL[group.step]}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{group.poStatus === "mail_failed" && group.emailError && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -80,6 +80,30 @@ describe("BuyerTasksPage", () => {
|
|||
});
|
||||
});
|
||||
|
||||
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(<BuyerTasksPage />);
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Reference in a new issue