Dentolize · Quotation Tax Fix & New Design Walkthrough
On this pageBusiness viewTechnical view

Per-Procedure Line Items

Business view

Before this PR, a quotation had one discount % and one tax % that applied to the whole estimate. If a patient needed a filling and a crown, and only the crown was insured, staff had no way to discount or tax those two procedures differently — the whole quotation had to use the same numbers.

Now every procedure line in a quotation is its own mini line-item with:

  • Its own price and quantity.
  • Its own discount amount.
  • Its own insurance-covered amount (with a checkbox to mark whether insurance applies to that specific procedure at all).
  • Its own tax toggle — a procedure can be marked taxable or not, independent of the rest of the quotation.
  • (Saudi Arabia branches only) an "allow max limit" checkbox, tied to the patient's insurance policy class daily/annual limit.
  • A free-text note and a manual order number, so procedures can be reordered on the printed estimate independent of the order they were added.

The quotation-level discount/insurance/tax fields at the bottom still exist — typing into them now redistributes that value back down across all the line items, so staff who just want "10% off everything" don't have to touch each row individually. But per-procedure control is now available when the estimate needs it.

Technical view

Data shape

Both editors keep a client-side array of line items, one object per procedure, with fields: subtotal, discount, discountPercent, insurance, insurancePercent, tax, taxPercent, taxApplied, total, insured, allowMaxLimit, order, notes.

  • Web (QuotationDrawer.js): a Form-managed array watched via Form.useWatch('lineItems', form), rendered through Form.List name="lineItems" (QuotationDrawer.js:1441).
  • Mobile (NewQuotationScreen.js): a useFieldArray-backed array built by buildLineItemsFromOps (NewQuotationScreen.js:187-342).

On save, the whole per-procedure breakdown is serialized into the operationInsurance JSON string that's stored on the Quotation row (operationInsurance: String field, unchanged shape, but now populated with per-op priceDiscount, insurance, taxApplied, taxPercent instead of just a flat discount amount). The server's recalculation logic (quotationUtils.js:16-24) reads this JSON back out via storedByOp, so the per-line breakdown survives round-trips through the backend, not just the UI session.

Web UI (QuotationDrawer.js)

The per-procedure Table, columns defined at QuotationDrawer.js:979-1181:

ColumnHandlerLines
Discount button → OperationDiscountModalhandleSelectOperation / handleUpdateOperationDiscount1019-1028, 850-892
Tax toggle (✓ / ✕ icon)handleToggleTax1029-1052, 894-925
Insurance checkbox + amount inputhandleToggleOperationInsured / handleChangeInsuranceDiscountAmount1094-1130, 759-797, 810-843
"Allow max limit" checkbox (Saudi Arabia only, requires a policy class max limit)handleToggleOperationMaxLimit1131-1151
Notes textarea1152-1170
Order / sequence input1074-1093

The bottom summary table (QuotationDrawer.js:1453-1732) still shows aggregate discount%/discount, tax%/tax, and insurance%/insurance inputs (handleUpdateDiscountPercent/handleUpdateDiscount, handleUpdateTaxPercent/handleUpdateTax, handleUpdateInsurancePercent/handleUpdateInsurance) that redistribute proportionally into the line items — plus a note showing patientVatPercent/insuranceVatPercent side by side when they diverge (lines 1529-1541), and an InvoiceInsuranceDetailsPopover (line 1598) breaking down the tax split for review before saving.

Mobile UI (NewQuotationScreen.js)

The equivalent per-row controls:

  • Discount amount editor — navigates to a numeric input screen (handleEditOperationDiscounthandleUpdateOperationDiscount, lines 909-922, 828-854), rendered 1140-1150.
  • Tax toggle — handleToggleTax, lines 856-877, rendered 1152-1168 (only shown when taxPercent || insuranceTaxPercent.current !== patientTaxPercent.current, i.e. hidden when there's nothing to toggle).
  • Insurance checkbox + amount — handleToggleOperationInsuranceDiscount / editInsurance, lines 765-792, 894-907, rendered 1170-1198.
  • "Allow max limit" (Saudi only) — lines 794-802, 1199-1223.
  • Order field and notes — 1228-1260.
  • Patient-vs-insurance VAT display text, shown only when the two rates diverge — lines 1437-1447.

Quotation-level discount/tax/insurance fields exist as aggregate editors the same way as web (handleUpdateDiscount, handleUpdateTax, handleUpdateInsurance, lines 602-763) and become disabled once the patient/insurance tax rates diverge (lines 1421, 1434) for the same reason described in Patient vs. Insurance Tax Split.

Legacy quotations

Quotations created before this PR don't have per-operation priceDiscount/taxApplied/patientTaxPercent in their stored JSON. buildLineItemsFromOps (mobile, lines 194-224) and its web counterpart carry explicit fallback branches for this — old data loads into the new per-line UI with reasonable defaults rather than breaking. This is a natural regression-test target: loading and re-saving a pre-PR quotation should not corrupt its numbers.

  • QuotationOperations.js — a procedure row previously labeled the insurance-covered amount as "discount"; it now shows the price discount and the insurance amount as two separate, correctly labeled figures.
  • QuotationExpendedRow.jsdiscountPercent/taxPercent display now defensively guards with .toFixed(2) against undefined values (previously could render NaN% or crash on quotations missing those fields).
  • ConvertInvoiceButton.js — converting a quotation to an invoice now passes each procedure's own initialDiscount (pulled from operationInsurance.operations[].discount) instead of one aggregate discount/insurance blob, so the invoice created from a quotation inherits the same per-procedure split instead of flattening it back to one number.