From 56476e3ca07896bdfa91c4a644b35fe26bc2c62f Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 8 Sep 2026 18:45:13 +0200 Subject: [PATCH] fix(quote-sprint): propagate description edits to quote_items.description The sprint-board pricing modal was writing operator edits only to quote_items.quote_description, but the quote detail page renders quote_items.description. The 'process and open' flow writes to description directly, which is why edits appeared there but not after saving from the sprint board. Mirror the trimmed description into both quote_description and description on save, matching the process page's write path. Add three regression guards (edited description, cleared description, untouched save) to sprint-item-pricing-modal.test.tsx. --- .../sprint-item-pricing-modal.test.tsx | 178 +++++++++++++++++- src/components/sprint-item-pricing-modal.tsx | 22 +++ 2 files changed, 198 insertions(+), 2 deletions(-) diff --git a/src/components/sprint-item-pricing-modal.test.tsx b/src/components/sprint-item-pricing-modal.test.tsx index 6aced61..f4ff939 100644 --- a/src/components/sprint-item-pricing-modal.test.tsx +++ b/src/components/sprint-item-pricing-modal.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen, waitFor, fireEvent } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { SprintItemPricingModal } from "./sprint-item-pricing-modal"; @@ -20,17 +20,54 @@ import type { SprintItem } from "@/app/(app)/sprint/page"; // and fetch so the attachment list returns an empty array. The save // flow also goes through `pb.collection().update()` — we let that // resolve to undefined so the save handler runs end-to-end. +// +// `updateMock` is hoisted so the "save" describe block can assert +// against the payload written when the operator clicks "Save & next". +// The layout tests don't care about it but it must not interfere +// either — `vi.clearAllMocks()` in `beforeEach` resets it. + +const { updateMock } = vi.hoisted(() => ({ + updateMock: vi.fn().mockResolvedValue(undefined), +})); vi.mock("@/lib/pocketbase", () => ({ __esModule: true, default: () => ({ collection: () => ({ getOne: vi.fn(), - update: vi.fn().mockResolvedValue(undefined), + update: updateMock, }), }), })); +// Replace SupplierSearch with a plain onValueChange(e.target.value)} + > + + {suppliers.map((s) => ( + + ))} + + ), +})); + // Default item-shape helper: a claimed-by-me M20 Flat Washers row // already attached to a parent quote that has a customer expanded // through the standard `expand.quote.expand.customer` chain. Mirrors @@ -343,4 +380,141 @@ describe("SprintItemPricingModal — layout", () => { ); expect(togglePrecedesDescription).toBe(true); }); +}); + +// ─── Save flow ─────────────────────────────────────────────────────── +// +// These tests exercise the operator's "edit description → Save & next" +// path. The bug they guard against: the sprint modal used to only +// write `quote_description` on save, leaving `description` untouched. +// Because the quote detail page (`/quotes/[id]`) and the process page +// (`/quotes/[id]/process`) only render `item.description`, the +// operator's edit from the sprint board was invisible on the quote +// itself — it was only visible when the PDF was regenerated. The +// working "process and open" flow updates `description` directly, +// which is why opening the quote after editing on the process page +// shows the change. +// +// The fix mirrors the operator's edited `quoteDescription` value to +// both `quote_description` AND `description` so the edit appears on +// every surface that quotes the line item. + +describe("SprintItemPricingModal — save", () => { + beforeEach(() => { + updateMock.mockReset(); + updateMock.mockResolvedValue(undefined); + }); + + it("writes both `description` and `quote_description` on save so the edit propagates to the quote detail page", async () => { + // Original RFQ wording is on `description`; `quote_description` + // is empty so the modal pre-fills the textarea with the + // customer's wording. The operator types a cleaned-up version. + renderModal(); + + const textarea = screen.getByTestId("sprint-quote-description"); + fireEvent.change(textarea, { + target: { value: "M20 HT Nuts G8 HDG, 50mm" }, + }); + + // Pick a supplier — the save handler refuses to proceed without + // one. The SupplierSearch component is mocked to a plain