diff --git a/src/app/(app)/quotes/[id]/process/page.tsx b/src/app/(app)/quotes/[id]/process/page.tsx index b473296..99ade94 100644 --- a/src/app/(app)/quotes/[id]/process/page.tsx +++ b/src/app/(app)/quotes/[id]/process/page.tsx @@ -690,15 +690,12 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str const applyPricing = () => { if (!pricingModal) return; - // Markup must be present and strictly positive. A zero markup - // (cost = selling) is a giveaway, not a quote — the office - // can't run a business on it. A negative markup means we're - // selling below cost, which the supplier PO and accounting - // both reject. Surface a clear error and refuse to save; the - // operator can either type a positive value in the Markup - // field or adjust the cost/selling pair so the recomputed - // markup lands above zero. - if (!Number.isFinite(pricingForm.markup) || pricingForm.markup <= 0) { + // Markup must be present and strictly positive — UNLESS this is a + // no-quote (decline-letter). In no-quote mode the PDF route + // renders "Not quoted" cells and "—" totals, so a row at 0/0/0 + // is exactly the target. Forcing markup > 0 would block the + // operator from saving the row, which is the bug Bug B fixes. + if (!isNoQuote && (!Number.isFinite(pricingForm.markup) || pricingForm.markup <= 0)) { toast.error("Markup must be greater than 0 — adjust the cost or selling price so the markup is positive."); return; } @@ -728,9 +725,14 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str ) => { setPricingForm((prev) => { // Markup is always editable; it doesn't get locked. Recompute - // whichever side isn't locked. + // whichever side isn't locked — UNLESS this is a no-quote + // (decline-letter). In no-quote mode the prices don't matter + // for the PDF render, so we let the operator type whatever + // they want without the form auto-recomputing the other side + // (the Bug C fix). if (field === "markup") { const next = { ...prev, markup: value }; + if (isNoQuote) return next; if (pricingLock === "cost") { next.selling_price = round2(prev.cost_price * (1 + value / 100)); } else { @@ -742,21 +744,25 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str // Typing into cost — if selling is locked, switch the lock to cost. if (field === "cost_price") { if (pricingLock === "selling") setPricingLock("cost"); - return { - ...prev, - cost_price: value, - selling_price: round2(value * (1 + prev.markup / 100)), - }; + return isNoQuote + ? { ...prev, cost_price: value } + : { + ...prev, + cost_price: value, + selling_price: round2(value * (1 + prev.markup / 100)), + }; } // Typing into selling — if cost is locked, switch the lock to selling. // selling: field === "selling_price" if (pricingLock === "cost") setPricingLock("selling"); - return { - ...prev, - selling_price: value, - cost_price: prev.markup > -100 ? round2(value / (1 + prev.markup / 100)) : 0, - }; + return isNoQuote + ? { ...prev, selling_price: value } + : { + ...prev, + selling_price: value, + cost_price: prev.markup > -100 ? round2(value / (1 + prev.markup / 100)) : 0, + }; }); }; @@ -864,7 +870,12 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str const missingPricing = items.filter( (i) => !(i.cost_price && i.selling_price), ).length; - const itemsReady = items.length > 0 && missingSupplier === 0 && missingPricing === 0; + // No-quote (decline-letter) mode bypasses the pricing gate entirely + // — the PDF route renders every line with "Not quoted" cells and + // "—" totals, so a row with cost=0 / selling=0 / markup=0 is + // exactly what we want. See Bug A in the no-quote findings. + const itemsReady = + isNoQuote || (items.length > 0 && missingSupplier === 0 && missingPricing === 0); return (
@@ -969,6 +980,11 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str

Send this as an official decline. Prices will be ignored.

+

+ When on: the pricing form stays editable but isn't + required, and the PDF renders "Not quoted" + cells with no totals. +

Generate Quote PDF

- {itemsReady - ? "Select a letterhead template" - : `Price every item + assign a supplier before generating (${missingPricing} unpriced, ${missingSupplier} missing supplier)`} + {isNoQuote + ? "No-quote letter — prices ignored on PDF (renders 'Not quoted' cells)" + : itemsReady + ? "Select a letterhead template" + : `Price every item + assign a supplier before generating (${missingPricing} unpriced, ${missingSupplier} missing supplier)`}