WFDASH-WEBAPP/docs/superpowers/plans/2026-08-06-buyer-tasks-resend-button.md
2026-08-06 17:34:28 +02:00

14 KiB
Raw Permalink Blame History

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):

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:

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):

<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:

{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):

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):

<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):

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:

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
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
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
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
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.