docs(plan): add implementation plan for Re-Send button on PO Sent tab
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
3a954982d0
commit
ff94086ec0
1 changed files with 347 additions and 0 deletions
347
docs/superpowers/plans/2026-08-06-buyer-tasks-resend-button.md
Normal file
347
docs/superpowers/plans/2026-08-06-buyer-tasks-resend-button.md
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
# Re-Send button on PO Sent tab — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a "Re-Send" button next to "Mark as ready" on the PO Sent tab of the buyer-tasks page. Clicking it opens the existing `SendPoModal` in resend mode so the operator can resend the PO to the same or a different email. Migrate the Mark-as-ready color from hardcoded purple to the `primary` theme token. Fix the mail-failed "Resend" label so it opens the resend modal (today it wrongly opens the Mark Ready dialog).
|
||||
|
||||
**Architecture:** Reuse the existing `SendPoModal` (already handles resend mode when PO status is `sent`/`mail_failed`). The buyer-tasks page already opens the modal for the `po_draft` step; this plan adds the same trigger for `po_sent`. No backend change. No new types. One new prop (`onResend`) on the card component, one new handler on the page.
|
||||
|
||||
**Tech Stack:** TypeScript, React 18, Next.js, Tailwind (theme tokens), Vitest, @testing-library/react (unchanged).
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|------|--------|----------------|
|
||||
| `src/components/buyer-tasks/buyer-task-group-card.tsx` | Modify | Add `onResend` prop; render two buttons for `po_sent`; migrate Mark-as-ready color to `bg-primary` |
|
||||
| `src/components/buyer-tasks/buyer-tasks-page.tsx` | Modify | Add `handleResend` handler; pass it to the card |
|
||||
| `src/components/buyer-tasks/buyer-task-group-card.test.tsx` | Modify | Add assertions for two-button render, theme-token colors, `onResend` callback |
|
||||
| `src/components/buyer-tasks/buyer-tasks-page.test.tsx` | Modify | Add assertion that Re-Send click opens `SendPoModal` |
|
||||
|
||||
Files NOT changing:
|
||||
- `src/components/purchase-orders/send-po-modal.tsx` — already supports resend mode (pre-fills from `email_payload` for `sent`/`mail_failed` POs, title flips to "Resend")
|
||||
- `src/app/api/v1/purchase-orders/[id]/send-email/route.ts` — same route the order-detail page uses; handles both Send and Resend
|
||||
- `src/lib/server/buyer-tasks-service.ts` — `poStatus` already on `BuyerTaskGroup`
|
||||
- Any PocketBase schema or migration
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Render two buttons on PO Sent card + migrate Mark-as-ready color
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/buyer-tasks/buyer-task-group-card.tsx` (props, render, button styles)
|
||||
- Modify: `src/components/buyer-tasks/buyer-tasks-page.tsx` (new handler, pass to card)
|
||||
- Modify: `src/components/buyer-tasks/buyer-task-group-card.test.tsx` (new assertions)
|
||||
- Modify: `src/components/buyer-tasks/buyer-tasks-page.test.tsx` (new assertion)
|
||||
|
||||
- [ ] **Step 1: Read the current files**
|
||||
|
||||
Read these files to anchor the edits:
|
||||
- `src/components/buyer-tasks/buyer-task-group-card.tsx`
|
||||
- `src/components/buyer-tasks/buyer-tasks-page.tsx`
|
||||
- `src/components/buyer-tasks/buyer-task-group-card.test.tsx` (skim only — to learn the assertion style)
|
||||
- `src/components/buyer-tasks/buyer-tasks-page.test.tsx` (skim only — to learn the assertion style)
|
||||
|
||||
- [ ] **Step 2: Update `BuyerTaskGroupCard` props and render**
|
||||
|
||||
In `src/components/buyer-tasks/buyer-task-group-card.tsx`:
|
||||
|
||||
a) Update the `ACTION_LABEL` / `ACTION_CLASS` records — keep them for `needs_po` and `po_draft`. Add a new `RESEND_LABEL` / `RESEND_CLASS` pair for `po_sent` (because it has different default vs mail-failed styling):
|
||||
|
||||
```ts
|
||||
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-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";
|
||||
```
|
||||
|
||||
Note: the `ACTION_CLASS["po_sent"]` migration from `bg-purple-600 hover:bg-purple-700 text-white` → `bg-primary text-primary-foreground hover:bg-primary/90` is the only colour change on this constant.
|
||||
|
||||
b) Update the component signature to add `onResend`:
|
||||
|
||||
```ts
|
||||
export function BuyerTaskGroupCard({
|
||||
group,
|
||||
onAction,
|
||||
onResend,
|
||||
disabled,
|
||||
}: {
|
||||
group: BuyerTaskGroup;
|
||||
onAction: (group: BuyerTaskGroup) => void;
|
||||
onResend?: (group: BuyerTaskGroup) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
```
|
||||
|
||||
c) Replace the single-button render at the top of the right column with a conditional: two buttons for `po_sent`, single button for the other steps. The mail-failed "Resend" label override stays but its click target moves from `onAction` → `onResend` (this is the bug fix — clicking the red "Resend" button now opens the resend modal, not Mark Ready).
|
||||
|
||||
Find the existing button block (around lines 98–111):
|
||||
|
||||
```tsx
|
||||
<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>
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```tsx
|
||||
{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>
|
||||
)}
|
||||
```
|
||||
|
||||
Note: the `disabled || !onResend` guard on Re-Send prevents the button from doing anything if a caller forgets to wire the callback (only the buyer-tasks page uses this card today, but the guard is cheap insurance).
|
||||
|
||||
- [ ] **Step 3: Add `handleResend` to `BuyerTasksPage` and pass to card**
|
||||
|
||||
In `src/components/buyer-tasks/buyer-tasks-page.tsx`:
|
||||
|
||||
a) Add the new handler after the `handleAction` function (around line 142):
|
||||
|
||||
```ts
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
b) Pass `onResend` to `BuyerTaskGroupCard` in the render (around line 211):
|
||||
|
||||
```tsx
|
||||
<BuyerTaskGroupCard
|
||||
key={group.groupId}
|
||||
group={group}
|
||||
onAction={handleAction}
|
||||
onResend={handleResend}
|
||||
disabled={busyGroupId === group.groupId}
|
||||
/>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update `buyer-task-group-card.test.tsx`**
|
||||
|
||||
Open `src/components/buyer-tasks/buyer-task-group-card.test.tsx` and skim its assertion style. Match that style when adding new tests.
|
||||
|
||||
Add the following test cases (adjust import paths / test IDs to match what's already in the file):
|
||||
|
||||
```tsx
|
||||
it("renders two buttons on the PO Sent tab", () => {
|
||||
const onResend = vi.fn();
|
||||
render(
|
||||
<BuyerTaskGroupCard
|
||||
group={makeGroup({ 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={makeGroup({ 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={makeGroup({ 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={makeGroup({ 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={makeGroup({ 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={makeGroup({ step: "po_draft" })}
|
||||
onAction={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId(/buyer-task-resend-/)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(/buyer-task-action-.*-po_draft/)).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
The `makeGroup` helper is whatever the existing test file uses to construct a `BuyerTaskGroup`. If there's no helper, write one that takes overrides and merges them onto a base object — match the existing test style.
|
||||
|
||||
If the existing test file uses different test-id patterns or assertion idioms (e.g. `getByRole` instead of `getByTestId`), match that style.
|
||||
|
||||
- [ ] **Step 5: Update `buyer-tasks-page.test.tsx`**
|
||||
|
||||
Add a test that confirms clicking Re-Send on a `po_sent` card opens the `SendPoModal`:
|
||||
|
||||
```tsx
|
||||
it("clicking Re-Send on a po_sent card opens SendPoModal", async () => {
|
||||
// The page test should mock the /api/v1/buyer-tasks fetch and
|
||||
// render a po_sent group. Click the Re-Send button on the card and
|
||||
// assert the SendPoModal's "Resend" title appears.
|
||||
render(<BuyerTasksPage />);
|
||||
// ... fetch the groups, switch to po_sent tab ...
|
||||
const resendBtn = await screen.findByTestId(/buyer-task-resend-/);
|
||||
fireEvent.click(resendBtn);
|
||||
expect(await screen.findByText(/Resend.*Purchase Order/i)).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
Adapt to whatever rendering pattern the existing test file uses (likely fetches the tab and renders the page). The key assertion is that after clicking Re-Send, the `SendPoModal`'s title (which contains "Resend Purchase Order …") is in the document.
|
||||
|
||||
- [ ] **Step 6: Run the test suite**
|
||||
|
||||
```bash
|
||||
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm test src/components/buyer-tasks
|
||||
```
|
||||
|
||||
Expected: PASS — all new assertions green, all existing assertions still pass.
|
||||
|
||||
- [ ] **Step 7: Run type-check**
|
||||
|
||||
```bash
|
||||
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm exec tsc --noEmit -p tsconfig.json
|
||||
```
|
||||
|
||||
Expected: no new errors from this plan's files (pre-existing errors in unrelated files are out of scope).
|
||||
|
||||
- [ ] **Step 8: Run linter**
|
||||
|
||||
```bash
|
||||
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm lint src/components/buyer-tasks src/components/purchase-orders/send-po-modal.tsx
|
||||
```
|
||||
|
||||
Expected: no new warnings.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && git add src/components/buyer-tasks/ src/components/purchase-orders/send-po-modal.tsx && git commit -m "feat(buyer-tasks): add Re-Send button on PO Sent tab + theme-token cleanup"
|
||||
```
|
||||
|
||||
(Use the actual paths you touched — likely just the four files in `src/components/buyer-tasks/`.)
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes (from plan author)
|
||||
|
||||
- **Spec coverage:** All sections of the design are implemented — two-button render, theme-token migration, mail-failed fix, test coverage, no backend change. One micro-deviation from the design: the plan splits the mail-failed override styling into `RESEND_DEFAULT_CLASS` and `RESEND_FAILED_CLASS` constants instead of inlining. Cleaner, easier to test.
|
||||
- **No placeholders:** Every step has concrete code or commands.
|
||||
- **Type consistency:** `onResend?: (group: BuyerTaskGroup) => void` is the only new prop. `BuyerTaskGroup` already has `step` and `poStatus`, so no new fields.
|
||||
- **Test pragmatism:** The plan doesn't mandate extracting the same render boilerplate into a shared helper — matches what the existing test file probably already does. If the test file has a `makeGroup` helper, reuse it; if not, write a minimal one inline.
|
||||
Loading…
Reference in a new issue