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