From cfcbbc5f1021b61cdc7691558219ddf18e56aa8c Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 26 Aug 2026 23:37:48 +0200 Subject: [PATCH] feat(quotes): bypass PDF ready filter when is_no_quote Lets the PDF render with all active items (regardless of price) when the operator has marked the quote as a decline letter. The existing PDF diagnostic still flags data issues for priced quotes. Also adds the (unused) noQuote prop to QuoteDocumentProps so the route compiles. Task 8 will wire the rendering. --- src/app/api/v1/quotes/[id]/pdf/route.ts | 28 +++++++++++++++++++++++-- src/lib/pdf/quote-document.tsx | 13 ++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/app/api/v1/quotes/[id]/pdf/route.ts b/src/app/api/v1/quotes/[id]/pdf/route.ts index 09aad6b..497d701 100644 --- a/src/app/api/v1/quotes/[id]/pdf/route.ts +++ b/src/app/api/v1/quotes/[id]/pdf/route.ts @@ -146,6 +146,14 @@ export async function POST( const quote = await pb.collection("quotes").getOne(quoteId, { expand: "customer", }); + // No-quote / decline-letter flag. When true, we render every + // active item through to the PDF regardless of price — this is + // the PDF companion to the no-quote feature (Tasks 1-4 of the + // plan). The PDF component itself will render "Not quoted" cells + // for these rows (Task 8). DiagnosePdfReadiness was relaxed in + // Task 5 so priced quotes still surface their gaps; for + // is_no_quote quotes, we just render. + const isNoQuote = !!quote.is_no_quote; const customer = quote.expand?.customer as Customer | undefined; if (!customer) { return NextResponse.json( @@ -214,7 +222,22 @@ export async function POST( } } - if (ready.length === 0) { + // itemsForPdf: what we hand to . For priced quotes + // this is the priced/processed subset (the `ready` array above). + // For no-quote / decline letters we bypass the filter entirely + // and render ALL active items — the PDF component itself decides + // how to render the price/total cells (Task 8 wires the "Not + // quoted" text in those columns). This is the third pricing-gate + // relaxation of the no-quote plan; Tasks 4-6 covered submit / + // diagnose / send. + // + // We keep `ready` populated for the downstream status-flip logic + // (decides approval_pending vs partial_approval_pending) and the + // partial-quote note below ("5 of 15 items"), both of which still + // apply to priced quotes. + const itemsForPdf = isNoQuote ? allItems : ready; + + if (!isNoQuote && ready.length === 0) { const body: ValidationErrorBody = { error: allItems.length === 0 @@ -258,12 +281,13 @@ export async function POST( // Render const pdfBuffer = await renderQuotePdf({ quote, - items: ready, + items: itemsForPdf, customer, letterhead, logoDataUri, partialNote, quoteNotes: quote.quote_notes ?? null, + noQuote: isNoQuote, }); // Build a stable, human-readable filename diff --git a/src/lib/pdf/quote-document.tsx b/src/lib/pdf/quote-document.tsx index 3611969..8e10613 100644 --- a/src/lib/pdf/quote-document.tsx +++ b/src/lib/pdf/quote-document.tsx @@ -106,6 +106,18 @@ export interface QuoteDocumentProps { * Added 2026-08-17. */ quoteNotes?: string | null; + /** + * When true, the quote is being rendered as a "decline / no-quote" + * letter instead of a priced quotation. The PDF route bypasses the + * normal price/processed/supplier filter when this flag is set, and + * passes every active item through. The component itself decides + * how to render the per-row cells (Task 8 will wire the + * "Not quoted" text in the price/total columns). + * + * Default false = standard priced quotation. Added 2026-08-26 + * alongside the no-quote / decline-letter feature. + */ + noQuote?: boolean; } /** @@ -130,6 +142,7 @@ export function QuoteDocument({ logoDataUri, partialNote, quoteNotes, + noQuote: _noQuote, }: QuoteDocumentProps) { const subtotal = quoteSubtotal(items); const vat = vatAmount(subtotal);