From f61c173a1a63dcd5a5745b8245d850b523174e9b Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Thu, 6 Aug 2026 18:01:58 +0200 Subject: [PATCH] fix(buyer-tasks): surface PO email_payload + attachments on group for resend modal prefill Co-Authored-By: Claude Co-authored-by: Orca --- src/app/api/v1/buyer-tasks/route.test.ts | 4 ++ .../buyer-task-group-card.test.tsx | 2 + .../buyer-tasks/buyer-tasks-page.test.tsx | 63 ++++++++++++++++++- .../buyer-tasks/buyer-tasks-page.tsx | 9 +-- src/lib/server/buyer-tasks-service.test.ts | 18 ++++++ src/lib/server/buyer-tasks-service.ts | 35 ++++++++++- 6 files changed, 124 insertions(+), 7 deletions(-) diff --git a/src/app/api/v1/buyer-tasks/route.test.ts b/src/app/api/v1/buyer-tasks/route.test.ts index 306d126..fc86758 100644 --- a/src/app/api/v1/buyer-tasks/route.test.ts +++ b/src/app/api/v1/buyer-tasks/route.test.ts @@ -35,6 +35,8 @@ describe("GET /api/v1/buyer-tasks", () => { poNumber: null, supplierEmail: null, emailError: null, + emailPayload: null, + additionalAttachments: [], step: "needs_po", source: "customer", }, @@ -65,6 +67,8 @@ describe("GET /api/v1/buyer-tasks", () => { poNumber: "PO-26080301", supplierEmail: "orders@steelco.example", emailError: "[webhook] Webhook returned HTTP 502", + emailPayload: null, + additionalAttachments: [], step: "po_draft", source: "customer", }, diff --git a/src/components/buyer-tasks/buyer-task-group-card.test.tsx b/src/components/buyer-tasks/buyer-task-group-card.test.tsx index c4d5734..5744a1f 100644 --- a/src/components/buyer-tasks/buyer-task-group-card.test.tsx +++ b/src/components/buyer-tasks/buyer-task-group-card.test.tsx @@ -19,6 +19,8 @@ const baseGroup = { poStatus: null, poNumber: null, emailError: null, + emailPayload: null, + additionalAttachments: [], supplierEmail: null, step: "needs_po" as const, source: "customer" as const, diff --git a/src/components/buyer-tasks/buyer-tasks-page.test.tsx b/src/components/buyer-tasks/buyer-tasks-page.test.tsx index 71bf1a3..365ed4d 100644 --- a/src/components/buyer-tasks/buyer-tasks-page.test.tsx +++ b/src/components/buyer-tasks/buyer-tasks-page.test.tsx @@ -2,6 +2,16 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { BuyerTasksPage } from "./buyer-tasks-page"; +vi.mock("@/lib/pocketbase", () => ({ + default: () => ({ + collection: () => ({ + // Keep the line-count lookup pending so this test observes the + // synchronous resend prefill rather than the async default-body preview. + getFullList: () => new Promise(() => {}), + }), + }), +})); + const sampleGroup = { groupId: "order-1::sup-1", orderId: "order-1", @@ -19,6 +29,8 @@ const sampleGroup = { poStatus: null, poNumber: null, supplierEmail: null, + emailPayload: null, + additionalAttachments: [], step: "needs_po" as const, }; @@ -83,7 +95,7 @@ describe("BuyerTasksPage", () => { it("clicking Re-Send on a po_sent card opens SendPoModal", async () => { const poSentGroup = { ...sampleGroup, - groupId: "order-1::sup-1::po-sent", + groupId: "order-1::sup-1::po-sent-open", purchaseOrderId: "po-1", poStatus: "sent" as const, poNumber: "PO-TEST-001", @@ -104,6 +116,55 @@ describe("BuyerTasksPage", () => { expect(await screen.findByText(/resend purchase order po-test-001/i)).toBeInTheDocument(); }); + it("clicking Re-Send pre-fills the modal with the previous email composition", 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", + emailPayload: { + to: "previous@example.com", + cc: "copy@example.com", + bcc: "blind@example.com", + subject: "Old subject", + body: "

Old body

", + bodyText: "Old body", + }, + additionalAttachments: ["drawing.pdf"], + 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(); + 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(); + + expect(document.querySelector("#send-to")).toHaveValue( + "previous@example.com", + ); + expect(document.querySelector("#send-cc")).toHaveValue( + "copy@example.com", + ); + expect(document.querySelector("#send-bcc")).toHaveValue( + "blind@example.com", + ); + expect(document.querySelector("#send-subject")).toHaveValue( + "Old subject", + ); + expect(document.querySelector("#send-message")).toHaveValue( + "Old body", + ); + }); + it("calls generate-po when the action button is clicked", async () => { vi.spyOn(global, "fetch") .mockResolvedValueOnce( diff --git a/src/components/buyer-tasks/buyer-tasks-page.tsx b/src/components/buyer-tasks/buyer-tasks-page.tsx index 05c5161..ab1fde2 100644 --- a/src/components/buyer-tasks/buyer-tasks-page.tsx +++ b/src/components/buyer-tasks/buyer-tasks-page.tsx @@ -129,10 +129,9 @@ export function BuyerTasksPage() { } else if (group.step === "po_draft") { // Open the shared send-PO modal with the group's PO. The modal // owns its form state and POSTs to the same send-email route the - // order detail page uses. We pass a minimal PurchaseOrder stub - // with just the id — the modal reads its own PO via the API - // when needed (supplier email, status, attachments are seeded - // lazily inside the modal). + // order detail page uses. Pass the PO fields needed for the + // supplier/status/email-composition/attachment prefill; the line + // count for the message preview is still loaded lazily by the modal. setSendPoFor(group); } else if (group.step === "po_sent") { setReadyError(null); @@ -247,6 +246,8 @@ export function BuyerTasksPage() { po_number: sendPoFor.poNumber ?? "", status: sendPoFor.poStatus ?? undefined, email_error: sendPoFor.emailError ?? undefined, + email_payload: sendPoFor.emailPayload ?? undefined, + additional_attachments: sendPoFor.additionalAttachments ?? [], expand: { supplier: { email: sendPoFor.supplierEmail ?? "", diff --git a/src/lib/server/buyer-tasks-service.test.ts b/src/lib/server/buyer-tasks-service.test.ts index 400f923..6af7095 100644 --- a/src/lib/server/buyer-tasks-service.test.ts +++ b/src/lib/server/buyer-tasks-service.test.ts @@ -392,6 +392,15 @@ describe("listBuyerTasks", () => { id: "po-1", status: "draft", po_number: "PO-26072901", + email_payload: { + to: "previous@example.com", + cc: "copy@example.com", + bcc: "blind@example.com", + subject: "Old subject", + body: "

Old body

", + bodyText: "Old body", + }, + additional_attachments: ["drawing.pdf"], expand: { supplier: { id: "sup-1", @@ -409,6 +418,15 @@ describe("listBuyerTasks", () => { expect(groups).toHaveLength(1); expect(groups[0].poNumber).toBe("PO-26072901"); expect(groups[0].supplierEmail).toBe("orders@steelco.example"); + expect(groups[0].emailPayload).toEqual({ + to: "previous@example.com", + cc: "copy@example.com", + bcc: "blind@example.com", + subject: "Old subject", + body: "

Old body

", + bodyText: "Old body", + }); + expect(groups[0].additionalAttachments).toEqual(["drawing.pdf"]); }); it("populates emailError from the related PO when the PO has an email_error", async () => { diff --git a/src/lib/server/buyer-tasks-service.ts b/src/lib/server/buyer-tasks-service.ts index 87bfdac..1281823 100644 --- a/src/lib/server/buyer-tasks-service.ts +++ b/src/lib/server/buyer-tasks-service.ts @@ -4,6 +4,7 @@ import { generatePurchaseOrderPdf, PurchaseOrderPdfValidationError, } from "@/lib/server/purchase-orders-service"; +import type { PurchaseOrderEmailPayload } from "@/lib/types"; export type BuyerTaskTab = "needs_po" | "po_draft" | "po_sent"; @@ -50,6 +51,10 @@ export type BuyerTaskGroup = { * layout. The full reason remains visible in the order page's send-modal * banner. */ emailError: string | null; + /** Last composed outbound email, used to prefill the resend modal. */ + emailPayload: PurchaseOrderEmailPayload | null; + /** File tokens for additional supplier attachments. */ + additionalAttachments: string[]; step: BuyerTaskTab; source: BuyerTaskGroupSource; }; @@ -111,7 +116,7 @@ export async function listBuyerTasks(tab: BuyerTaskTab): Promise> = []; - let stockItemMap = new Map>(); + const stockItemMap = new Map>(); if (tab === "po_sent") { stockPurchases = await admin.collection("stock_purchases").getFullList({ filter: `status = "sent" || status = "sending"`, @@ -153,7 +158,15 @@ export async function listBuyerTasks(tab: BuyerTaskTab): Promise sum + it.quantity * it.cost_price, 0, ); + const rawAdditionalAttachments = stock.additional_attachments; + const additionalAttachments = Array.isArray(rawAdditionalAttachments) + ? rawAdditionalAttachments + : rawAdditionalAttachments + ? [rawAdditionalAttachments] + : []; groups.set(groupId, { groupId, orderId: stock.id, @@ -267,6 +296,8 @@ export async function listBuyerTasks(tab: BuyerTaskTab): Promise