feat(jobs/new): live price quote with discount breakdown for the clinic
Clinics couldn't see what an order would cost them before publishing.
Added a server-priced quote box that updates as the lab, prosthetic type
and tooth selection change. Three rendered states:
1. Discounted (custom unit price or discountPercent):
Katalog (4 × 1.500 ₺) 6.000 ₺
Klinik indirimi (%15) -900 ₺
──────────────────────
Toplam 5.100 ₺
2. Plain catalog match (no clinic override):
4 × 1.500 ₺ 6.000 ₺
'Katalog fiyatı uygulanıyor'
3. No catalog row for the chosen type:
'Fiyat işe başlanırken laboratuvar tarafından belirlenecek'
Bits to make this work
- calculateJobPrice now returns catalogUnitPrice, catalogTotal, savings
and teethCount alongside the final amount, so the client can render
a breakdown without reaching for any other table.
- POST /api/pricing/quote endpoint guards on clinic kind + an approved
connection before exposing pricing — no leaking catalog data to
unconnected clinics, and no per-lab discount snooping.
- NewJobForm's lab and prosthetic Selects became controlled (hidden
inputs mirror state to the form action), and a debounced effect
(250ms) re-fetches the quote when any of {lab, type, teeth.length}
changes. AbortController cancels in-flight requests when inputs change
again.
This commit is contained in:
@@ -17,6 +17,14 @@ export type JobPriceQuote = {
|
||||
currency: string;
|
||||
source: "clinic_custom" | "clinic_discount" | "catalog";
|
||||
unitPrice: number;
|
||||
/** Lab's listed catalog price before any clinic override, if a catalog row exists. */
|
||||
catalogUnitPrice?: number;
|
||||
/** catalogUnitPrice × teethCount; useful for showing the clinic the saving. */
|
||||
catalogTotal?: number;
|
||||
/** catalogTotal - amount; positive when the clinic is getting a discount. */
|
||||
savings?: number;
|
||||
/** teethCount passed in, echoed back so the client can format breakdowns. */
|
||||
teethCount: number;
|
||||
discountPercent?: number;
|
||||
};
|
||||
|
||||
@@ -77,14 +85,25 @@ export async function calculateJobPrice(opts: {
|
||||
const labSettings = labSettingsRes.rows[0] as unknown as TenantSettings | undefined;
|
||||
|
||||
const fallbackCurrency = labSettings?.defaultCurrency ?? "TRY";
|
||||
const catalogUnitPrice = catalog?.unitPrice;
|
||||
const catalogTotal =
|
||||
catalogUnitPrice !== undefined ? catalogUnitPrice * opts.teethCount : undefined;
|
||||
|
||||
if (override?.customUnitPrice !== undefined && override.customUnitPrice !== null) {
|
||||
const unit = override.customUnitPrice;
|
||||
const amount = unit * opts.teethCount;
|
||||
return {
|
||||
amount: unit * opts.teethCount,
|
||||
amount,
|
||||
currency: override.currency || catalog?.currency || fallbackCurrency,
|
||||
source: "clinic_custom",
|
||||
unitPrice: unit,
|
||||
catalogUnitPrice,
|
||||
catalogTotal,
|
||||
savings:
|
||||
catalogTotal !== undefined && catalogTotal > amount
|
||||
? catalogTotal - amount
|
||||
: undefined,
|
||||
teethCount: opts.teethCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,12 +115,17 @@ export async function calculateJobPrice(opts: {
|
||||
override.discountPercent > 0
|
||||
) {
|
||||
const discounted = catalog.unitPrice * (1 - override.discountPercent / 100);
|
||||
const amount = discounted * opts.teethCount;
|
||||
return {
|
||||
amount: discounted * opts.teethCount,
|
||||
amount,
|
||||
currency: override.currency || catalog.currency || fallbackCurrency,
|
||||
source: "clinic_discount",
|
||||
unitPrice: discounted,
|
||||
catalogUnitPrice: catalog.unitPrice,
|
||||
catalogTotal,
|
||||
savings: catalogTotal !== undefined ? catalogTotal - amount : undefined,
|
||||
discountPercent: override.discountPercent,
|
||||
teethCount: opts.teethCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,5 +134,8 @@ export async function calculateJobPrice(opts: {
|
||||
currency: catalog.currency || fallbackCurrency,
|
||||
source: "catalog",
|
||||
unitPrice: catalog.unitPrice,
|
||||
catalogUnitPrice: catalog.unitPrice,
|
||||
catalogTotal,
|
||||
teethCount: opts.teethCount,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user