fix(quotes): wire no-quote toggle into process-page UI gates
The server-side PDF route was already no-quote-aware — when `quote.is_no_quote === true` it bypasses the pricing gate (`pdf/route.ts:156, 238, 240`) and the PDF component renders "Not quoted" cells + "—" totals (`quote-document.tsx:239, 242, 276/280/285`). But the client side was ignoring the flag, so flipping the toggle looked like a no-op. Five one-file fixes to close the gap: 1. `itemsReady` now short-circuits to true when no-quote is on. Before: `itemsReady = items.length > 0 && missingSupplier === 0 && missingPricing === 0;` (always evaluated pricing). After: prepend `isNoQuote ||` so the Generate Quote button enables regardless of cost/selling prices. (Bug A) 2. `applyPricing` markup>0 guard now bypassed when no-quote is on. Before: toast "Markup must be greater than 0" + refuse save when markup ≤ 0 — blocked the operator from saving a 0/0/0 row. After: `if (!isNoQuote && ...)` so the operator can save the row. (Bug B) 3. `handlePricingChange` auto-derive now skipped when no-quote is on. The cost↔selling recompute on every keystroke (`value * (1 + markup / 100)` and inverse) was firing even when no-quote was set, which is what produced the "I set 0/0/1% markup and it shows 1 cent" artefact. Three branches (markup/cost/selling) now short-circuit to pass-through under no-quote. (Bug C) 4. Helper text under the Generate Quote PDF button now explains "No-quote letter — prices ignored on PDF (renders 'Not quoted' cells)" when the toggle is on, instead of the misleading "Price every item + assign a supplier before generating". 5. Second helper line under the toggle's label explaining the full UI flow when on: "the pricing form stays editable but isn't required, and the PDF renders 'Not quoted' cells with no totals." Operator workflow now: flip toggle ON → enter description on a row at 0/0/0 → save → click Generate Quote → PDF renders decline letter with "Not quoted" cells and no totals. No PB migration needed (`is_no_quote` boolean column already present per `pb-exports/pb_schema-2.json:2178`). Build: clean. Typecheck: 0 errors. No tests added (manual flow; no existing test exercises this toggle path). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a861de1fb6
commit
4d8382c87f
1 changed files with 42 additions and 24 deletions
|
|
@ -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 (
|
||||
<div className="space-y-4 max-w-7xl mx-auto">
|
||||
|
|
@ -969,6 +980,11 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str
|
|||
<p className="text-xs text-muted-foreground">
|
||||
Send this as an official decline. Prices will be ignored.
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When on: the pricing form stays editable but isn't
|
||||
required, and the PDF renders "Not quoted"
|
||||
cells with no totals.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="is_no_quote"
|
||||
|
|
@ -1636,9 +1652,11 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str
|
|||
<div>
|
||||
<p className="text-sm font-medium">Generate Quote PDF</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{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)`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
|
|||
Loading…
Reference in a new issue