feat(buyer-tasks): show recipient + send date on PO Sent card
Adds a small muted caption under the Re-Send / Mark as ready buttons
on the PO Sent tab showing the email address the PO was sent to and
the send timestamp (en-GB short format like '5 Aug 2026, 14:23').
Data sources come from the existing BuyerTaskGroup.emailPayload
exposed in commit f61c173 — no new PB fields, no new types. The
caption only renders when sentAt is present, so legacy rows (or a
fresh draft flipped to po_sent without a successful send) stay clean.
Visible on mail_failed too: the operator can see who we tried to
reach and when, alongside the existing 'Mail failed' banner.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
f3052bfd40
commit
d2ffbea6bc
2 changed files with 168 additions and 28 deletions
|
|
@ -206,4 +206,111 @@ describe("BuyerTaskGroupCard", () => {
|
|||
screen.queryByTestId(`buyer-task-po-number-${baseGroup.groupId}`),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
describe("po_sent sent-info caption", () => {
|
||||
const sentEmail = "orders@acme.co.za";
|
||||
const sentAt = "2026-08-05T13:23:00.000Z";
|
||||
|
||||
it("renders the recipient and send date under the buttons", () => {
|
||||
render(
|
||||
<BuyerTaskGroupCard
|
||||
group={{
|
||||
...baseGroup,
|
||||
step: "po_sent",
|
||||
poStatus: "sent",
|
||||
emailPayload: { to: sentEmail, sentAt },
|
||||
}}
|
||||
onAction={vi.fn()}
|
||||
onResend={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const caption = screen.getByTestId(
|
||||
`buyer-task-sent-info-${baseGroup.groupId}`,
|
||||
);
|
||||
expect(caption).toHaveTextContent(new RegExp(sentEmail.replace(/\./g, "\\.")));
|
||||
expect(caption.textContent).toContain("Sent to");
|
||||
// 5 Aug 2026 — en-GB short month, exact day.
|
||||
expect(caption.textContent).toMatch(/5 Aug 2026/);
|
||||
// Time is rendered in 24-hour HH:mm form.
|
||||
expect(caption.textContent).toMatch(/\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
it("hides the caption when emailPayload.sentAt is missing", () => {
|
||||
render(
|
||||
<BuyerTaskGroupCard
|
||||
group={{
|
||||
...baseGroup,
|
||||
step: "po_sent",
|
||||
poStatus: "sent",
|
||||
emailPayload: { to: sentEmail }, // no sentAt
|
||||
}}
|
||||
onAction={vi.fn()}
|
||||
onResend={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId(`buyer-task-sent-info-${baseGroup.groupId}`),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("still renders the caption on mail_failed when sentAt is set", () => {
|
||||
// The mail-failed banner explains the failure — the operator still
|
||||
// wants to see who we tried and when, so the previous attempt's
|
||||
// recipient/timestamp stays visible above the banner.
|
||||
render(
|
||||
<BuyerTaskGroupCard
|
||||
group={{
|
||||
...baseGroup,
|
||||
step: "po_sent",
|
||||
poStatus: "mail_failed",
|
||||
emailError: "[webhook] 502",
|
||||
emailPayload: { to: sentEmail, sentAt },
|
||||
}}
|
||||
onAction={vi.fn()}
|
||||
onResend={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId(`buyer-task-sent-info-${baseGroup.groupId}`),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/mail failed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to a bare 'Sent' label when the recipient is missing", () => {
|
||||
render(
|
||||
<BuyerTaskGroupCard
|
||||
group={{
|
||||
...baseGroup,
|
||||
step: "po_sent",
|
||||
poStatus: "sent",
|
||||
emailPayload: { sentAt }, // no `to`
|
||||
}}
|
||||
onAction={vi.fn()}
|
||||
onResend={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const caption = screen.getByTestId(
|
||||
`buyer-task-sent-info-${baseGroup.groupId}`,
|
||||
);
|
||||
expect(caption.textContent).not.toMatch(/Sent to/);
|
||||
expect(caption.textContent).toMatch(/Sent\s+\d/);
|
||||
});
|
||||
|
||||
it("does not render the caption on the po_draft tab", () => {
|
||||
render(
|
||||
<BuyerTaskGroupCard
|
||||
group={{
|
||||
...baseGroup,
|
||||
step: "po_draft",
|
||||
poStatus: "draft",
|
||||
emailPayload: { to: sentEmail, sentAt },
|
||||
}}
|
||||
onAction={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId(`buyer-task-sent-info-${baseGroup.groupId}`),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -106,34 +106,67 @@ export function BuyerTaskGroupCard({
|
|||
</div>
|
||||
</div>
|
||||
{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>
|
||||
// PO Sent: two side-by-side buttons + a small "sent info"
|
||||
// caption below them showing who we sent the PO to and when.
|
||||
// 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). The caption is hidden when there's no
|
||||
// recorded `sentAt` (legacy rows predating email_payload,
|
||||
// or a fresh po_draft that got flipped straight to
|
||||
// po_sent without a successful send).
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<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>
|
||||
{group.emailPayload?.sentAt ? (
|
||||
<div
|
||||
className="text-xs text-muted-foreground text-right leading-tight"
|
||||
data-testid={`buyer-task-sent-info-${group.groupId}`}
|
||||
>
|
||||
{group.emailPayload.to ? (
|
||||
<span>
|
||||
Sent to {group.emailPayload.to}
|
||||
<span aria-hidden="true"> · </span>
|
||||
</span>
|
||||
) : (
|
||||
<span>Sent </span>
|
||||
)}
|
||||
<span>
|
||||
{new Date(group.emailPayload.sentAt).toLocaleString("en-GB", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
// needs_po / po_draft: single button (unchanged behaviour).
|
||||
|
|
|
|||
Loading…
Reference in a new issue