docs(spec): add Re-Send button on PO Sent tab of buyer-tasks

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-06 17:33:25 +02:00
parent 7f477ca949
commit 3a954982d0

View file

@ -0,0 +1,166 @@
# Re-Send button on PO Sent tab of buyer-tasks page
**Date:** 2026-08-06
**Status:** Approved (brainstorming complete)
**Scope:** Two component files (`buyer-task-group-card.tsx`, `buyer-tasks-page.tsx`) + their test files. No backend change. No new types.
## Context
Today the PO Sent tab of `src/app/(app)/orders/tasks/page.tsx` (rendered via `BuyerTasksPage` → `BuyerTaskGroupCard`) shows a single action button per card:
- **Default**: purple "Mark as ready" — opens the `MarkReadyDialog` to flag the PO group as ready for collection.
- **`poStatus === "mail_failed"`**: red "Resend" — **buggy**: opens the same `MarkReadyDialog` even though the label says "Resend". The operator can't actually resend from the buyer-tasks page today; they have to navigate to the order detail page.
The user has asked for two things:
1. Add a **Re-Send** button next to "Mark as ready" on the PO Sent tab. Clicking it opens the existing resend modal so the operator can resend the PO to the same or a different email.
2. Match the Mark-as-ready button's shape but in a different color. Use a **theme token** (the app's `info` token) so the color works across all themes.
Bundled in the same change: fix the mail-failed "Resend" label to actually open the resend modal instead of the Mark Ready dialog.
## Approach (chosen)
**Reuse the existing `SendPoModal`.** It already supports a resend flow:
- For a PO with `status === "sent"` or `"mail_failed"`, the modal pre-fills `to / cc / bcc / subject / body` from the previously-persisted `email_payload` and shows the previous `email_error` inline.
- The title flips from "Send" to "Resend" and the submit button reads "Resend".
- Submitting POSTs to the same `/api/v1/purchase-orders/[id]/send-email` route the order detail page uses — no new endpoint needed.
The buyer-tasks page already opens this modal for the `po_draft` step. Wiring it for `po_sent` is a single handler + a prop on the card.
The Mark-as-ready purple hardcoded color is replaced with the `primary` theme token so both buttons follow the same theme discipline.
Approaches considered and rejected:
- *Build a new "Re-Send" modal.* Redundant — the existing one already handles resend.
- *Use a hardcoded teal/Tailwind color.* Violates the theme-token rule the user has set; won't work in dark mode or future themes.
- *Add the button to all three tabs.* Only `po_sent` (and `po_draft` already) make sense — there's no PO to resend in `needs_po`.
## Architecture
```
BuyerTasksPage
├─ state: markReadyFor: BuyerTaskGroup | null (existing — Mark Ready dialog)
├─ state: sendPoFor: BuyerTaskGroup | null (existing — SendPoModal; wired for po_draft)
└─ handleAction(group)
├─ step === "needs_po" → setBusyGroupId → POST generate-po (existing)
├─ step === "po_draft" → setSendPoFor(group) (existing)
└─ step === "po_sent" → setMarkReadyFor(group) (existing)
[NEW] handleResend(group)
└─ step === "po_sent" → setSendPoFor(group) (NEW — reuses po_draft flow)
```
```
BuyerTaskGroupCard
└─ For step === "po_sent" only: render TWO buttons
├─ Left: "Re-Send" → onResend(group)
│ - default: bg-info text-info-foreground hover:bg-info/90
│ - mail_failed: bg-destructive text-destructive-foreground hover:bg-destructive/90
└─ Right: "Mark as ready" → onAction(group)
- bg-primary text-primary-foreground hover:bg-primary/90 (was: bg-purple-600)
For step === "needs_po" / "po_draft": single button (unchanged)
```
## Components
### `BuyerTaskGroupCard` — `src/components/buyer-tasks/buyer-task-group-card.tsx`
**Prop changes:**
- Add `onResend?: (group: BuyerTaskGroup) => void`. Optional, only used by `po_sent` step.
**Render change for `po_sent`:**
- Wrap the two buttons in a flex container (`flex items-center gap-2`) so they sit side-by-side, right-aligned next to the supplier info.
- Re-Send button:
- Label: `"Re-Send"` (with hyphen to match the user's exact wording).
- Default style: `bg-info text-info-foreground hover:bg-info/90`.
- When `poStatus === "mail_failed"`: override to `bg-destructive text-destructive-foreground hover:bg-destructive/90` and label becomes `"Resend"` (no hyphen — matches the existing error-state style elsewhere in the app).
- Mark-as-ready button:
- Label: `"Mark as ready"` (existing).
- Style: migrate from hardcoded `bg-purple-600 hover:bg-purple-700 text-white` to themed `bg-primary text-primary-foreground hover:bg-primary/90`.
**Render change for other steps:** unchanged.
### `BuyerTasksPage` — `src/components/buyer-tasks/buyer-tasks-page.tsx`
**New handler `handleResend`:**
```ts
function handleResend(group: BuyerTaskGroup) {
if (group.step !== "po_sent") return;
setSendPoFor(group);
}
```
The existing `SendPoModal` render block (`sendPoFor && <SendPoModal ... />`) is already wired for both Send (po_draft) and Resend (po_sent) modes — `poStatus` on the group sets the modal's prefill behavior via the stub `purchaseOrder` prop. No change needed to the JSX; we just trigger the same state from a new entry point.
**Pass `onResend` to `BuyerTaskGroupCard`:**
```tsx
<BuyerTaskGroupCard
key={group.groupId}
group={group}
onAction={handleAction}
onResend={handleResend} // NEW
disabled={busyGroupId === group.groupId}
/>
```
### Files NOT changing
- `src/components/purchase-orders/send-po-modal.tsx` — already handles resend mode; no change.
- `src/app/api/v1/purchase-orders/[id]/send-email/route.ts` — already accepts resends; no change.
- `src/lib/server/buyer-tasks-service.ts` — no change. The `poStatus` field is already on `BuyerTaskGroup`.
- Any PocketBase schema — no change.
- Any backend route — no change.
## Data flow
Unchanged. The card → page → existing SendPoModal → existing API route pipeline is reused as-is. The only new path is:
```
BuyerTaskGroupCard [Re-Send click]
→ BuyerTasksPage.handleResend(group)
→ setSendPoFor(group)
→ <SendPoModal open purchaseOrder={...group as PO stub...} />
→ POST /api/v1/purchase-orders/[id]/send-email
→ (existing pipeline)
```
## Error handling
- `SendPoModal` already surfaces pre-send validation failures (`!res.ok`, `webhookFired === false`) as error toasts. No new error paths.
- The existing `mail_failed` inline banner (`role="alert"`) stays above the items list, visible whether the operator clicks Re-Send or Mark as ready.
- Re-Send doesn't gate on `poStatus === "sent"` — the operator can re-trigger an in-flight send if they want to. The route handles concurrent sends the same way the order-detail flow does.
## Testing
### Existing tests to update
- `src/components/buyer-tasks/buyer-task-group-card.test.tsx`
- Add: `po_sent` renders two buttons (Re-Send + Mark as ready).
- Add: Re-Send button uses `bg-info`; Mark as ready uses `bg-primary` (no more hardcoded purple).
- Add: Re-Send click fires `onResend`; Mark as ready click still fires `onAction`.
- Add: `poStatus === "mail_failed"` Re-Send button uses `bg-destructive` and label becomes `"Resend"`.
- Existing: `po_draft` still renders a single "Send PO" button (no regression).
- `src/components/buyer-tasks/buyer-tasks-page.test.tsx`
- Add: clicking Re-Send on a `po_sent` card opens `SendPoModal` with the group's PO stub.
- Existing: clicking Mark as ready still opens `MarkReadyDialog` (no regression).
### Not changing
- Service-layer / API route tests — the API surface didn't change.
## Out of scope
- Migrating other hardcoded colors elsewhere in the app to theme tokens. Only the Mark as ready button is in scope here.
- Adding a Re-Send button to the `needs_po` tab (no PO exists yet).
- A "send to multiple addresses" feature.
- A "schedule send for later" feature.
- A bulk-resend flow.
## Implementation order
1. Update `BuyerTaskGroupCard` to accept `onResend` and render two buttons for `po_sent`; migrate the Mark-as-ready color.
2. Update `BuyerTasksPage` to add `handleResend` and pass it to the card.
3. Update the two test files with the new assertions.
4. Run the full test suite + lint + type-check.