@ticketboothapp/booking 1.2.77 → 1.2.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +113 -191
- package/src/components/booking/PriceBreakdown.tsx +2 -2
- package/src/components/booking/PriceSummary.tsx +20 -2
- package/src/components/booking/booking-flow.css +8 -0
- package/src/components/booking/useEditableSummaryLines.ts +244 -0
- package/src/lib/booking/change-booking-server-preview.ts +1 -0
package/package.json
CHANGED
|
@@ -104,6 +104,7 @@ import type { BookingFlowUiOptions } from './booking-flow-ui';
|
|
|
104
104
|
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
105
105
|
import type { ChangeBookingFlowProps, ChangeFlowSelectionPreview } from './booking-flow-types';
|
|
106
106
|
import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provider';
|
|
107
|
+
import { useEditableSummaryLines } from './useEditableSummaryLines';
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* ## Pricing contract (customer self-serve)
|
|
@@ -755,8 +756,6 @@ export function AdminChangeBookingFlow({
|
|
|
755
756
|
const [adminCustomReceiptLines, setAdminCustomReceiptLines] = useState<
|
|
756
757
|
Array<{ id: string; label: string; amountInput: string; amountSign: 1 | -1 }>
|
|
757
758
|
>([]);
|
|
758
|
-
const [editableSummaryLineAmountInputs, setEditableSummaryLineAmountInputs] = useState<Record<string, string>>({});
|
|
759
|
-
const [editableSummaryLineLabelInputs, setEditableSummaryLineLabelInputs] = useState<Record<string, string>>({});
|
|
760
759
|
const adminCustomLineIdRef = useRef(0);
|
|
761
760
|
|
|
762
761
|
// Auto-apply promo code when parent page passes one (e.g. partner pages).
|
|
@@ -937,6 +936,9 @@ export function AdminChangeBookingFlow({
|
|
|
937
936
|
} | null>(null);
|
|
938
937
|
const [changeQuoteLoading, setChangeQuoteLoading] = useState(false);
|
|
939
938
|
const [changeQuoteFetchError, setChangeQuoteFetchError] = useState<string | null>(null);
|
|
939
|
+
/** Dedupe quote calls while user input payload is unchanged. */
|
|
940
|
+
const lastCompletedQuoteInputsKeyRef = useRef<string | null>(null);
|
|
941
|
+
const inFlightQuoteInputsKeyRef = useRef<string | null>(null);
|
|
940
942
|
const selfServePricingConfirmed =
|
|
941
943
|
suppressSelfServeCurrencyUi &&
|
|
942
944
|
latestChangeQuote != null &&
|
|
@@ -3148,140 +3150,24 @@ export function AdminChangeBookingFlow({
|
|
|
3148
3150
|
),
|
|
3149
3151
|
[checkoutPriceSummaryLinesForCheckout],
|
|
3150
3152
|
);
|
|
3151
|
-
const
|
|
3152
|
-
|
|
3153
|
-
return line.kind === 'ticket'
|
|
3154
|
-
? `change-ticket-${line.category}-${index}`
|
|
3155
|
-
: `change-line-${line.label}-${line.type ?? 'line'}-${index}`;
|
|
3156
|
-
}, []);
|
|
3157
|
-
const editableCheckoutPriceSummaryLines = useMemo((): PriceSummaryLine[] => {
|
|
3158
|
-
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
3159
|
-
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
3160
|
-
);
|
|
3161
|
-
return checkoutPriceSummaryLinesForCheckout.map((line, index) => {
|
|
3162
|
-
const isBeforeSubtotal = firstTaxLineIndex < 0 || index < firstTaxLineIndex;
|
|
3163
|
-
const lineKey = getEditableSummaryLineKey(line, index);
|
|
3164
|
-
if (line.kind === 'ticket') {
|
|
3165
|
-
const amountInput = editableSummaryLineAmountInputs[lineKey];
|
|
3166
|
-
const parsedAmount = amountInput == null || amountInput.trim() === '' ? line.itemTotal : Number(amountInput);
|
|
3167
|
-
return {
|
|
3168
|
-
...line,
|
|
3169
|
-
lineKey,
|
|
3170
|
-
editable: isBeforeSubtotal,
|
|
3171
|
-
category: editableSummaryLineLabelInputs[lineKey] ?? line.category,
|
|
3172
|
-
itemTotal: Number.isFinite(parsedAmount) ? roundMoney(parsedAmount) : line.itemTotal,
|
|
3173
|
-
};
|
|
3174
|
-
}
|
|
3175
|
-
const amountInput = editableSummaryLineAmountInputs[lineKey];
|
|
3176
|
-
const parsedAmount = amountInput == null || amountInput.trim() === '' ? line.amount : Number(amountInput);
|
|
3177
|
-
return {
|
|
3178
|
-
...line,
|
|
3179
|
-
lineKey,
|
|
3180
|
-
editable: isBeforeSubtotal,
|
|
3181
|
-
label: editableSummaryLineLabelInputs[lineKey] ?? line.label,
|
|
3182
|
-
amount: Number.isFinite(parsedAmount) ? roundMoney(parsedAmount) : line.amount,
|
|
3183
|
-
};
|
|
3184
|
-
});
|
|
3185
|
-
}, [
|
|
3186
|
-
checkoutPriceSummaryLinesForCheckout,
|
|
3153
|
+
const {
|
|
3154
|
+
editableCheckoutPriceSummaryLines,
|
|
3187
3155
|
editableSummaryLineAmountInputs,
|
|
3188
3156
|
editableSummaryLineLabelInputs,
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
const line = editableCheckoutPriceSummaryLines[i];
|
|
3196
|
-
const key = line.lineKey;
|
|
3197
|
-
if (!key) continue;
|
|
3198
|
-
if (prev[key] != null) {
|
|
3199
|
-
next[key] = prev[key];
|
|
3200
|
-
continue;
|
|
3201
|
-
}
|
|
3202
|
-
next[key] = line.kind === 'ticket' ? String(line.itemTotal) : String(line.amount);
|
|
3203
|
-
}
|
|
3204
|
-
const prevKeys = Object.keys(prev);
|
|
3205
|
-
const nextKeys = Object.keys(next);
|
|
3206
|
-
if (
|
|
3207
|
-
prevKeys.length === nextKeys.length &&
|
|
3208
|
-
nextKeys.every((key) => prev[key] === next[key])
|
|
3209
|
-
) {
|
|
3210
|
-
return prev;
|
|
3211
|
-
}
|
|
3212
|
-
return next;
|
|
3213
|
-
});
|
|
3214
|
-
setEditableSummaryLineLabelInputs((prev) => {
|
|
3215
|
-
const next: Record<string, string> = {};
|
|
3216
|
-
for (let i = 0; i < editableCheckoutPriceSummaryLines.length; i += 1) {
|
|
3217
|
-
const line = editableCheckoutPriceSummaryLines[i];
|
|
3218
|
-
const key = line.lineKey;
|
|
3219
|
-
if (!key) continue;
|
|
3220
|
-
if (prev[key] != null) {
|
|
3221
|
-
next[key] = prev[key];
|
|
3222
|
-
continue;
|
|
3223
|
-
}
|
|
3224
|
-
next[key] = line.kind === 'ticket' ? line.category : line.label;
|
|
3225
|
-
}
|
|
3226
|
-
const prevKeys = Object.keys(prev);
|
|
3227
|
-
const nextKeys = Object.keys(next);
|
|
3228
|
-
if (
|
|
3229
|
-
prevKeys.length === nextKeys.length &&
|
|
3230
|
-
nextKeys.every((key) => prev[key] === next[key])
|
|
3231
|
-
) {
|
|
3232
|
-
return prev;
|
|
3233
|
-
}
|
|
3234
|
-
return next;
|
|
3235
|
-
});
|
|
3236
|
-
}, [editableCheckoutPriceSummaryLines]);
|
|
3237
|
-
const editableSummaryPreSubtotalDelta = useMemo(() => {
|
|
3238
|
-
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
3239
|
-
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
3240
|
-
);
|
|
3241
|
-
let delta = 0;
|
|
3242
|
-
for (let i = 0; i < checkoutPriceSummaryLinesForCheckout.length; i += 1) {
|
|
3243
|
-
if (firstTaxLineIndex >= 0 && i >= firstTaxLineIndex) break;
|
|
3244
|
-
const line = checkoutPriceSummaryLinesForCheckout[i];
|
|
3245
|
-
const lineKey = getEditableSummaryLineKey(line, i);
|
|
3246
|
-
const baselineAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
3247
|
-
const raw = editableSummaryLineAmountInputs[lineKey];
|
|
3248
|
-
const parsed = raw == null || raw.trim() === '' ? baselineAmount : Number(raw);
|
|
3249
|
-
const editedAmount = Number.isFinite(parsed) ? roundMoney(parsed) : baselineAmount;
|
|
3250
|
-
delta += editedAmount - baselineAmount;
|
|
3251
|
-
}
|
|
3252
|
-
return roundMoney(delta);
|
|
3253
|
-
}, [
|
|
3254
|
-
checkoutPriceSummaryLinesForCheckout,
|
|
3255
|
-
editableSummaryLineAmountInputs,
|
|
3256
|
-
getEditableSummaryLineKey,
|
|
3257
|
-
]);
|
|
3258
|
-
const editableSummaryPreSubtotalDebugRows = useMemo(() => {
|
|
3259
|
-
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
3260
|
-
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
|
|
3261
|
-
);
|
|
3262
|
-
const rows: Array<{ label: string; baseline: number; edited: number; delta: number }> = [];
|
|
3263
|
-
for (let i = 0; i < checkoutPriceSummaryLinesForCheckout.length; i += 1) {
|
|
3264
|
-
if (firstTaxLineIndex >= 0 && i >= firstTaxLineIndex) break;
|
|
3265
|
-
const line = checkoutPriceSummaryLinesForCheckout[i];
|
|
3266
|
-
const lineKey = getEditableSummaryLineKey(line, i);
|
|
3267
|
-
const baselineAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
3268
|
-
const raw = editableSummaryLineAmountInputs[lineKey];
|
|
3269
|
-
const parsed = raw == null || raw.trim() === '' ? baselineAmount : Number(raw);
|
|
3270
|
-
const editedAmount = Number.isFinite(parsed) ? roundMoney(parsed) : baselineAmount;
|
|
3271
|
-
const delta = roundMoney(editedAmount - baselineAmount);
|
|
3272
|
-
rows.push({
|
|
3273
|
-
label: line.kind === 'ticket' ? line.category : line.label,
|
|
3274
|
-
baseline: roundMoney(baselineAmount),
|
|
3275
|
-
edited: editedAmount,
|
|
3276
|
-
delta,
|
|
3277
|
-
});
|
|
3278
|
-
}
|
|
3279
|
-
return rows;
|
|
3280
|
-
}, [
|
|
3157
|
+
editableSummaryPreSubtotalDelta,
|
|
3158
|
+
editableSummaryPreSubtotalDebugRows,
|
|
3159
|
+
onLineAmountInputChange: handleEditableLineAmountInputChange,
|
|
3160
|
+
onLineAmountInputBlur: handleEditableLineAmountInputBlur,
|
|
3161
|
+
onLineAmountReset: handleEditableLineAmountReset,
|
|
3162
|
+
} = useEditableSummaryLines(
|
|
3281
3163
|
checkoutPriceSummaryLinesForCheckout,
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3164
|
+
{
|
|
3165
|
+
enabled: showProviderPricingInlineEditor,
|
|
3166
|
+
onChange: providerPricingUi?.onLineAmountInputChange,
|
|
3167
|
+
onBlur: providerPricingUi?.onLineAmountInputBlur,
|
|
3168
|
+
onReset: providerPricingUi?.onLineAmountReset,
|
|
3169
|
+
},
|
|
3170
|
+
);
|
|
3285
3171
|
|
|
3286
3172
|
// Promo discount from backend (order-level only; rates are pre-promo)
|
|
3287
3173
|
const [promoDiscountAmount, setPromoDiscountAmount] = useState(0);
|
|
@@ -3813,6 +3699,40 @@ export function AdminChangeBookingFlow({
|
|
|
3813
3699
|
!!lastName.trim();
|
|
3814
3700
|
|
|
3815
3701
|
const isChangeQuoteBlocked = isCustomerSelfServeChange && latestChangeQuote?.canProceed === false;
|
|
3702
|
+
const changeQuoteInputsKey = useMemo(() => JSON.stringify({
|
|
3703
|
+
bookingReference: initialValues?.bookingReference?.trim() ?? '',
|
|
3704
|
+
lastName: lastName.trim().toLowerCase(),
|
|
3705
|
+
optionId: selectedAvailability?.productOptionId?.trim() || activeOptions[0]?.optionId || '',
|
|
3706
|
+
dateTime: selectedAvailability?.dateTime ?? '',
|
|
3707
|
+
availabilityId: selectedAvailability?.availabilityId ?? null,
|
|
3708
|
+
pickupLocationId: pickupLocationId ?? null,
|
|
3709
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
3710
|
+
quantities,
|
|
3711
|
+
addOnSelections,
|
|
3712
|
+
adminCustomLinesAsAdditionalAdjustments,
|
|
3713
|
+
feReceiptSubtotal: adminFeAuthoritativeReceipt.subtotal,
|
|
3714
|
+
feReceiptTax: adminFeAuthoritativeReceipt.tax,
|
|
3715
|
+
feReceiptTotal: adminFeAuthoritativeReceipt.total,
|
|
3716
|
+
feReceiptLineItems: adminFeAuthoritativeReceipt.lineItems,
|
|
3717
|
+
useAdminFeAuthoritativeQuote,
|
|
3718
|
+
}), [
|
|
3719
|
+
initialValues?.bookingReference,
|
|
3720
|
+
lastName,
|
|
3721
|
+
selectedAvailability?.productOptionId,
|
|
3722
|
+
selectedAvailability?.dateTime,
|
|
3723
|
+
selectedAvailability?.availabilityId,
|
|
3724
|
+
activeOptions,
|
|
3725
|
+
pickupLocationId,
|
|
3726
|
+
selectedReturnOption?.returnAvailabilityId,
|
|
3727
|
+
quantities,
|
|
3728
|
+
addOnSelections,
|
|
3729
|
+
adminCustomLinesAsAdditionalAdjustments,
|
|
3730
|
+
adminFeAuthoritativeReceipt.subtotal,
|
|
3731
|
+
adminFeAuthoritativeReceipt.tax,
|
|
3732
|
+
adminFeAuthoritativeReceipt.total,
|
|
3733
|
+
adminFeAuthoritativeReceipt.lineItems,
|
|
3734
|
+
useAdminFeAuthoritativeQuote,
|
|
3735
|
+
]);
|
|
3816
3736
|
const requiresReturnInChangeFlow = isCustomerSelfServeChange && !!initialValues?.returnAvailabilityId?.trim();
|
|
3817
3737
|
const missingRequiredReturnSelection = requiresReturnInChangeFlow && !selectedReturnOption;
|
|
3818
3738
|
|
|
@@ -3871,7 +3791,11 @@ export function AdminChangeBookingFlow({
|
|
|
3871
3791
|
* UI-only gate — does not alter ticket line items or receipt-floor rules.
|
|
3872
3792
|
*/
|
|
3873
3793
|
const showChangeFlowManualPriceLines =
|
|
3874
|
-
changeFlowFinalPricePending || selfServePricingConfirmed;
|
|
3794
|
+
hasEffectiveChangeSelection && (changeFlowFinalPricePending || selfServePricingConfirmed);
|
|
3795
|
+
const showAdminCustomLineEditor =
|
|
3796
|
+
isAdmin &&
|
|
3797
|
+
selectedAvailability != null &&
|
|
3798
|
+
totalQuantity > 0;
|
|
3875
3799
|
|
|
3876
3800
|
const displayedChangeAmountsRaw = resolveChangeFlowDisplayedAmounts({
|
|
3877
3801
|
providerPreview: providerTotalsPreview,
|
|
@@ -4241,11 +4165,11 @@ export function AdminChangeBookingFlow({
|
|
|
4241
4165
|
return;
|
|
4242
4166
|
}
|
|
4243
4167
|
onPricePreviewChange({
|
|
4244
|
-
subtotal: originalReceipt ?
|
|
4168
|
+
subtotal: originalReceipt ? adminFeAuthoritativeReceipt.subtotal : effectiveSubtotalForCheckout,
|
|
4245
4169
|
tax:
|
|
4246
4170
|
!isTaxIncludedInPrice
|
|
4247
4171
|
? originalReceipt
|
|
4248
|
-
?
|
|
4172
|
+
? adminFeAuthoritativeReceipt.tax
|
|
4249
4173
|
: effectiveTax
|
|
4250
4174
|
: 0,
|
|
4251
4175
|
total: originalReceipt ? displayChangeFlowProposedTotalWithEditableLines : totalPrice,
|
|
@@ -4258,8 +4182,8 @@ export function AdminChangeBookingFlow({
|
|
|
4258
4182
|
effectiveSubtotalForCheckout,
|
|
4259
4183
|
effectiveTax,
|
|
4260
4184
|
displayChangeFlowProposedTotalWithEditableLines,
|
|
4261
|
-
|
|
4262
|
-
|
|
4185
|
+
adminFeAuthoritativeReceipt.subtotal,
|
|
4186
|
+
adminFeAuthoritativeReceipt.tax,
|
|
4263
4187
|
currency,
|
|
4264
4188
|
isTaxIncludedInPrice,
|
|
4265
4189
|
originalReceipt,
|
|
@@ -4287,6 +4211,17 @@ export function AdminChangeBookingFlow({
|
|
|
4287
4211
|
setLatestChangeQuote(null);
|
|
4288
4212
|
setChangeQuoteLoading(false);
|
|
4289
4213
|
setChangeQuoteFetchError(null);
|
|
4214
|
+
lastCompletedQuoteInputsKeyRef.current = null;
|
|
4215
|
+
inFlightQuoteInputsKeyRef.current = null;
|
|
4216
|
+
return;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
if (
|
|
4220
|
+
changeQuoteInputsKey &&
|
|
4221
|
+
(inFlightQuoteInputsKeyRef.current === changeQuoteInputsKey ||
|
|
4222
|
+
lastCompletedQuoteInputsKeyRef.current === changeQuoteInputsKey)
|
|
4223
|
+
) {
|
|
4224
|
+
setChangeQuoteLoading(false);
|
|
4290
4225
|
return;
|
|
4291
4226
|
}
|
|
4292
4227
|
|
|
@@ -4305,6 +4240,7 @@ export function AdminChangeBookingFlow({
|
|
|
4305
4240
|
const bookingReferenceForQuote = initialValues.bookingReference.trim();
|
|
4306
4241
|
const timer = window.setTimeout(() => {
|
|
4307
4242
|
void (async () => {
|
|
4243
|
+
inFlightQuoteInputsKeyRef.current = changeQuoteInputsKey;
|
|
4308
4244
|
const bookingItems = Object.entries(quantities)
|
|
4309
4245
|
.filter(([, count]) => count > 0)
|
|
4310
4246
|
.map(([category, count]) => ({ category, count }));
|
|
@@ -4331,11 +4267,18 @@ export function AdminChangeBookingFlow({
|
|
|
4331
4267
|
previousReturnAvailabilityId: initialValues.returnAvailabilityId ?? null,
|
|
4332
4268
|
},
|
|
4333
4269
|
};
|
|
4270
|
+
const deterministicClientProposedTotal = useAdminFeAuthoritativeQuote
|
|
4271
|
+
? adminFeAuthoritativeReceipt.total
|
|
4272
|
+
: changeFlowNewBookingTotal;
|
|
4273
|
+
const deterministicAmountDue = originalReceipt
|
|
4274
|
+
? roundMoney(adminFeAuthoritativeReceipt.total - originalReceipt.total)
|
|
4275
|
+
: roundMoney(changeFlowClientEstimateDue);
|
|
4334
4276
|
const quote = useAdminFeAuthoritativeQuote
|
|
4335
4277
|
? await quoteChangeBookingAdminFeReceipt({
|
|
4336
4278
|
...quoteRequestBase,
|
|
4337
4279
|
feReceipt: adminFeAuthoritativeReceipt,
|
|
4338
|
-
feAmountDueMajorUnits:
|
|
4280
|
+
feAmountDueMajorUnits: deterministicAmountDue,
|
|
4281
|
+
clientProposedTotal: deterministicClientProposedTotal,
|
|
4339
4282
|
})
|
|
4340
4283
|
: await quoteChangeBooking(quoteRequestBase);
|
|
4341
4284
|
if (seq !== changeQuoteRequestSeq.current) return;
|
|
@@ -4355,11 +4298,16 @@ export function AdminChangeBookingFlow({
|
|
|
4355
4298
|
tax: effectiveTax,
|
|
4356
4299
|
}, currency),
|
|
4357
4300
|
);
|
|
4301
|
+
lastCompletedQuoteInputsKeyRef.current = changeQuoteInputsKey;
|
|
4358
4302
|
} catch (e) {
|
|
4359
4303
|
if (seq !== changeQuoteRequestSeq.current) return;
|
|
4360
4304
|
setLatestChangeQuote(null);
|
|
4361
4305
|
setChangeQuoteFetchError(e instanceof Error ? e.message : 'Failed to get price for this change');
|
|
4306
|
+
lastCompletedQuoteInputsKeyRef.current = null;
|
|
4362
4307
|
} finally {
|
|
4308
|
+
if (inFlightQuoteInputsKeyRef.current === changeQuoteInputsKey) {
|
|
4309
|
+
inFlightQuoteInputsKeyRef.current = null;
|
|
4310
|
+
}
|
|
4363
4311
|
if (seq === changeQuoteRequestSeq.current) {
|
|
4364
4312
|
setChangeQuoteLoading(false);
|
|
4365
4313
|
}
|
|
@@ -4396,6 +4344,8 @@ export function AdminChangeBookingFlow({
|
|
|
4396
4344
|
adminCustomLinesAsAdditionalAdjustments,
|
|
4397
4345
|
adminFeAuthoritativeReceipt,
|
|
4398
4346
|
useAdminFeAuthoritativeQuote,
|
|
4347
|
+
changeQuoteInputsKey,
|
|
4348
|
+
originalReceipt?.total,
|
|
4399
4349
|
]);
|
|
4400
4350
|
|
|
4401
4351
|
// Auto-select product option when date is selected: most popular if set, otherwise first available.
|
|
@@ -4930,13 +4880,19 @@ export function AdminChangeBookingFlow({
|
|
|
4930
4880
|
? { manualLineAdjustments: adminCustomLinesAsAdditionalAdjustments }
|
|
4931
4881
|
: {}),
|
|
4932
4882
|
clientProposedTotal:
|
|
4933
|
-
|
|
4883
|
+
useAdminFeAuthoritativeQuote
|
|
4884
|
+
? adminFeAuthoritativeReceipt.total
|
|
4885
|
+
: (latestChangeQuote?.serverDisplay?.total ?? changeFlowNewBookingTotal),
|
|
4934
4886
|
};
|
|
4935
4887
|
const quote = useAdminFeAuthoritativeQuote
|
|
4936
4888
|
? await quoteChangeBookingAdminFeReceipt({
|
|
4937
4889
|
...quoteRequestBase,
|
|
4938
4890
|
feReceipt: adminFeAuthoritativeReceipt,
|
|
4939
|
-
feAmountDueMajorUnits: roundMoney(
|
|
4891
|
+
feAmountDueMajorUnits: roundMoney(
|
|
4892
|
+
originalReceipt
|
|
4893
|
+
? adminFeAuthoritativeReceipt.total - originalReceipt.total
|
|
4894
|
+
: changeFlowClientEstimateDue
|
|
4895
|
+
),
|
|
4940
4896
|
})
|
|
4941
4897
|
: await quoteChangeBooking(quoteRequestBase);
|
|
4942
4898
|
const quoteSlice = sliceChangeQuoteForUi(
|
|
@@ -5772,7 +5728,7 @@ export function AdminChangeBookingFlow({
|
|
|
5772
5728
|
<>
|
|
5773
5729
|
<CheckoutForm
|
|
5774
5730
|
priceSummaryLines={editableCheckoutPriceSummaryLines}
|
|
5775
|
-
replacePriceSummary={selfServeCheckoutPlaceholder}
|
|
5731
|
+
replacePriceSummary={hasChangeSelection ? selfServeCheckoutPlaceholder : undefined}
|
|
5776
5732
|
totalPrice={changeFlowAmountDue}
|
|
5777
5733
|
totalSummaryLabel={
|
|
5778
5734
|
changeFlowAmountDue < -0.005
|
|
@@ -5782,12 +5738,12 @@ export function AdminChangeBookingFlow({
|
|
|
5782
5738
|
? t('booking.totalOwedForBookingChange')
|
|
5783
5739
|
: 'Total owed for booking difference'
|
|
5784
5740
|
}
|
|
5785
|
-
subtotal={
|
|
5741
|
+
subtotal={adminFeAuthoritativeReceipt.subtotal}
|
|
5786
5742
|
taxAmount={
|
|
5787
5743
|
!isTaxIncludedInPrice &&
|
|
5788
|
-
|
|
5744
|
+
adminFeAuthoritativeReceipt.tax > 0 &&
|
|
5789
5745
|
!priceSummaryLinesIncludeTaxRow
|
|
5790
|
-
?
|
|
5746
|
+
? adminFeAuthoritativeReceipt.tax
|
|
5791
5747
|
: 0
|
|
5792
5748
|
}
|
|
5793
5749
|
taxRate={pricingConfig?.taxRate}
|
|
@@ -5813,9 +5769,11 @@ export function AdminChangeBookingFlow({
|
|
|
5813
5769
|
</>
|
|
5814
5770
|
}
|
|
5815
5771
|
extraBeforeSubtotal={
|
|
5816
|
-
showChangeFlowManualPriceLines ? (
|
|
5772
|
+
(showChangeFlowManualPriceLines || showAdminCustomLineEditor) ? (
|
|
5817
5773
|
<>
|
|
5818
|
-
{
|
|
5774
|
+
{showChangeFlowManualPriceLines &&
|
|
5775
|
+
showProviderPricingInlineEditor &&
|
|
5776
|
+
(providerPricingUi?.additionalAdjustments?.length ?? 0) > 0 ? (
|
|
5819
5777
|
<div className="space-y-1">
|
|
5820
5778
|
{providerPricingUi?.additionalAdjustments?.map((adj) => (
|
|
5821
5779
|
<div key={adj.id} className="flex items-center justify-between gap-2 text-sm">
|
|
@@ -5829,7 +5787,7 @@ export function AdminChangeBookingFlow({
|
|
|
5829
5787
|
</button>
|
|
5830
5788
|
<input
|
|
5831
5789
|
type="text"
|
|
5832
|
-
className="w-40 rounded border border-stone-300
|
|
5790
|
+
className="admin-custom-receipt-input w-40 rounded border border-stone-300 bg-white text-sm text-stone-800 placeholder:text-stone-400"
|
|
5833
5791
|
placeholder="Line description"
|
|
5834
5792
|
value={adj.label}
|
|
5835
5793
|
onChange={(e) =>
|
|
@@ -5853,7 +5811,7 @@ export function AdminChangeBookingFlow({
|
|
|
5853
5811
|
<input
|
|
5854
5812
|
type="text"
|
|
5855
5813
|
inputMode="decimal"
|
|
5856
|
-
className="
|
|
5814
|
+
className="admin-custom-receipt-input-amount w-[7rem] shrink-0 rounded-md border border-stone-300 bg-white text-right text-sm font-medium tabular-nums text-stone-800 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400/80"
|
|
5857
5815
|
placeholder="0.00"
|
|
5858
5816
|
value={adj.amountInput}
|
|
5859
5817
|
onChange={(e) =>
|
|
@@ -5873,7 +5831,7 @@ export function AdminChangeBookingFlow({
|
|
|
5873
5831
|
+ add line item
|
|
5874
5832
|
</button>
|
|
5875
5833
|
</div>
|
|
5876
|
-
) : showProviderPricingInlineEditor ? (
|
|
5834
|
+
) : showChangeFlowManualPriceLines && showProviderPricingInlineEditor ? (
|
|
5877
5835
|
<button
|
|
5878
5836
|
type="button"
|
|
5879
5837
|
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
@@ -5882,6 +5840,7 @@ export function AdminChangeBookingFlow({
|
|
|
5882
5840
|
+ add line item
|
|
5883
5841
|
</button>
|
|
5884
5842
|
) : null}
|
|
5843
|
+
{showAdminCustomLineEditor ? (
|
|
5885
5844
|
<div className="space-y-3">
|
|
5886
5845
|
<div className="flex justify-center">
|
|
5887
5846
|
<button
|
|
@@ -5995,6 +5954,7 @@ export function AdminChangeBookingFlow({
|
|
|
5995
5954
|
);
|
|
5996
5955
|
})}
|
|
5997
5956
|
</div>
|
|
5957
|
+
) : null}
|
|
5998
5958
|
</>
|
|
5999
5959
|
) : null
|
|
6000
5960
|
}
|
|
@@ -6061,48 +6021,10 @@ export function AdminChangeBookingFlow({
|
|
|
6061
6021
|
attributionConfirmed={partnerAttributionConfirmed}
|
|
6062
6022
|
onAttributionConfirmedChange={setPartnerAttributionConfirmed}
|
|
6063
6023
|
lineAmountInputs={editableSummaryLineAmountInputs}
|
|
6064
|
-
onLineAmountInputChange={
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
if (showProviderPricingInlineEditor) {
|
|
6068
|
-
providerPricingUi?.onLineAmountInputChange?.(lineKey, sanitized);
|
|
6069
|
-
}
|
|
6070
|
-
}}
|
|
6071
|
-
onLineAmountInputBlur={(lineKey) => {
|
|
6072
|
-
setEditableSummaryLineAmountInputs((prev) => {
|
|
6073
|
-
const raw = prev[lineKey] ?? '';
|
|
6074
|
-
if (raw.trim() === '' || raw === '-' || raw === '.'
|
|
6075
|
-
|| raw === '-.') {
|
|
6076
|
-
return prev;
|
|
6077
|
-
}
|
|
6078
|
-
const parsed = Number(raw);
|
|
6079
|
-
if (!Number.isFinite(parsed)) return prev;
|
|
6080
|
-
const normalized = roundMoney(parsed).toFixed(2);
|
|
6081
|
-
if (normalized === raw) return prev;
|
|
6082
|
-
return { ...prev, [lineKey]: normalized };
|
|
6083
|
-
});
|
|
6084
|
-
if (showProviderPricingInlineEditor) {
|
|
6085
|
-
providerPricingUi?.onLineAmountInputBlur?.(lineKey);
|
|
6086
|
-
}
|
|
6087
|
-
}}
|
|
6088
|
-
onLineAmountReset={(lineKey) => {
|
|
6089
|
-
const original = checkoutPriceSummaryLinesForCheckout.find(
|
|
6090
|
-
(line, index) => getEditableSummaryLineKey(line, index) === lineKey
|
|
6091
|
-
);
|
|
6092
|
-
if (!original) return;
|
|
6093
|
-
const nextValue =
|
|
6094
|
-
original.kind === 'ticket'
|
|
6095
|
-
? String(roundMoney(original.itemTotal))
|
|
6096
|
-
: String(roundMoney(original.amount));
|
|
6097
|
-
setEditableSummaryLineAmountInputs((prev) => ({ ...prev, [lineKey]: nextValue }));
|
|
6098
|
-
if (showProviderPricingInlineEditor) {
|
|
6099
|
-
providerPricingUi?.onLineAmountReset?.(lineKey);
|
|
6100
|
-
}
|
|
6101
|
-
}}
|
|
6024
|
+
onLineAmountInputChange={handleEditableLineAmountInputChange}
|
|
6025
|
+
onLineAmountInputBlur={handleEditableLineAmountInputBlur}
|
|
6026
|
+
onLineAmountReset={handleEditableLineAmountReset}
|
|
6102
6027
|
lineLabelInputs={editableSummaryLineLabelInputs}
|
|
6103
|
-
onLineLabelInputChange={(lineKey, value) => {
|
|
6104
|
-
setEditableSummaryLineLabelInputs((prev) => ({ ...prev, [lineKey]: value }));
|
|
6105
|
-
}}
|
|
6106
6028
|
/>
|
|
6107
6029
|
</>
|
|
6108
6030
|
)}
|
|
@@ -115,7 +115,7 @@ export function PriceBreakdown({
|
|
|
115
115
|
<input
|
|
116
116
|
type="text"
|
|
117
117
|
inputMode="decimal"
|
|
118
|
-
className="
|
|
118
|
+
className="admin-editable-summary-amount-input w-[7rem] shrink-0 rounded-md border border-stone-300 bg-white text-right text-sm font-medium tabular-nums text-stone-800 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400/80"
|
|
119
119
|
value={editableValue ?? String(itemTotal)}
|
|
120
120
|
onChange={(e) => onEditableChange(e.target.value)}
|
|
121
121
|
onBlur={onEditableBlur}
|
|
@@ -163,7 +163,7 @@ export function PriceBreakdown({
|
|
|
163
163
|
<input
|
|
164
164
|
type="text"
|
|
165
165
|
inputMode="decimal"
|
|
166
|
-
className="
|
|
166
|
+
className="admin-editable-summary-amount-input w-[7rem] shrink-0 rounded-md border border-stone-300 bg-white text-right text-sm font-medium tabular-nums text-stone-800 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400/80"
|
|
167
167
|
value={editableValue ?? String(itemTotal)}
|
|
168
168
|
onChange={(e) => onEditableChange(e.target.value)}
|
|
169
169
|
onBlur={onEditableBlur}
|
|
@@ -106,6 +106,24 @@ function formatLineAmount(
|
|
|
106
106
|
return formatted;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function shouldAppendQuantitySuffix(type: string | undefined, label: string, quantity: number | null | undefined): boolean {
|
|
110
|
+
if (quantity == null || quantity <= 1) return false;
|
|
111
|
+
const normalizedType = String(type ?? '').trim().toUpperCase();
|
|
112
|
+
const eligibleType =
|
|
113
|
+
normalizedType === 'TICKET' ||
|
|
114
|
+
normalizedType === 'FEE' ||
|
|
115
|
+
normalizedType === 'RETURN_OPTION' ||
|
|
116
|
+
normalizedType === 'RETURNOPTION' ||
|
|
117
|
+
normalizedType === 'RETURN';
|
|
118
|
+
if (!eligibleType) return false;
|
|
119
|
+
const normalizedLabel = label.toLowerCase();
|
|
120
|
+
const alreadyHasCountHint =
|
|
121
|
+
/\bx\s*\d+\b/.test(normalizedLabel) ||
|
|
122
|
+
/\(\s*x\s*\d+\s*\)/.test(normalizedLabel) ||
|
|
123
|
+
/\b\d+\s*(person|people|pax|traveler|travellers|ticket|tickets)\b/.test(normalizedLabel);
|
|
124
|
+
return !alreadyHasCountHint;
|
|
125
|
+
}
|
|
126
|
+
|
|
109
127
|
function getCurrencySymbol(currency: Currency, locale: Locale): string {
|
|
110
128
|
return formatCurrencyAmount(0, currency, locale).replace(/[0-9\s,.-]/g, '') || currency;
|
|
111
129
|
}
|
|
@@ -233,7 +251,7 @@ export function PriceSummary({
|
|
|
233
251
|
) : (
|
|
234
252
|
<span className="min-w-0 truncate">
|
|
235
253
|
{label}
|
|
236
|
-
{type
|
|
254
|
+
{shouldAppendQuantitySuffix(type, label, quantity) ? ` (x${quantity})` : ''}
|
|
237
255
|
</span>
|
|
238
256
|
)}
|
|
239
257
|
{tooltip && <InfoTooltip text={tooltip} />}
|
|
@@ -255,7 +273,7 @@ export function PriceSummary({
|
|
|
255
273
|
<input
|
|
256
274
|
type="text"
|
|
257
275
|
inputMode="decimal"
|
|
258
|
-
className="
|
|
276
|
+
className="admin-editable-summary-amount-input w-[7rem] shrink-0 rounded-md border border-stone-300 bg-white text-right text-sm font-medium tabular-nums text-stone-800 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400/80"
|
|
259
277
|
value={lineAmountInputs?.[lineKey] ?? String(amount)}
|
|
260
278
|
onChange={(e) => onLineAmountInputChange(lineKey, e.target.value)}
|
|
261
279
|
onBlur={() => onLineAmountInputBlur?.(lineKey)}
|
|
@@ -293,6 +293,14 @@
|
|
|
293
293
|
padding: 0.5rem 0.75rem;
|
|
294
294
|
min-height: 2.75rem;
|
|
295
295
|
}
|
|
296
|
+
.booking-flow-preflight input.admin-editable-summary-amount-input[type='text'] {
|
|
297
|
+
box-sizing: border-box;
|
|
298
|
+
padding: 0.5rem 0.75rem;
|
|
299
|
+
min-height: 2.75rem;
|
|
300
|
+
font-size: 0.875rem;
|
|
301
|
+
line-height: 1.25rem;
|
|
302
|
+
font-weight: 500;
|
|
303
|
+
}
|
|
296
304
|
.booking-flow-preflight .admin-custom-receipt-line .admin-custom-receipt-segment {
|
|
297
305
|
box-sizing: border-box;
|
|
298
306
|
padding: 0.5rem 0.65rem;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import type { PriceSummaryLine } from './PriceSummary';
|
|
3
|
+
import { roundMoney } from '../../lib/booking/change-flow-pricing';
|
|
4
|
+
|
|
5
|
+
type EditableSummaryLineInputState = {
|
|
6
|
+
amountInput: string;
|
|
7
|
+
labelInput: string;
|
|
8
|
+
amountDirty: boolean;
|
|
9
|
+
labelDirty: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ProviderAmountSync = {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
onChange?: (lineKey: string, value: string) => void;
|
|
15
|
+
onBlur?: (lineKey: string) => void;
|
|
16
|
+
onReset?: (lineKey: string) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getEditableSummaryLineKey(line: PriceSummaryLine, index: number): string {
|
|
20
|
+
if (line.lineKey) return line.lineKey;
|
|
21
|
+
return line.kind === 'ticket'
|
|
22
|
+
? `change-ticket-${line.category}-${index}`
|
|
23
|
+
: `change-line-${line.label}-${line.type ?? 'line'}-${index}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useEditableSummaryLines(
|
|
27
|
+
checkoutPriceSummaryLinesForCheckout: PriceSummaryLine[],
|
|
28
|
+
providerAmountSync: ProviderAmountSync,
|
|
29
|
+
) {
|
|
30
|
+
const [editableSummaryInputsByKey, setEditableSummaryInputsByKey] = useState<
|
|
31
|
+
Record<string, EditableSummaryLineInputState>
|
|
32
|
+
>({});
|
|
33
|
+
|
|
34
|
+
const editableSummaryLineAmountInputs = useMemo(() => {
|
|
35
|
+
const out: Record<string, string> = {};
|
|
36
|
+
for (const [lineKey, state] of Object.entries(editableSummaryInputsByKey)) {
|
|
37
|
+
out[lineKey] = state.amountInput;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}, [editableSummaryInputsByKey]);
|
|
41
|
+
|
|
42
|
+
const editableSummaryLineLabelInputs = useMemo(() => {
|
|
43
|
+
const out: Record<string, string> = {};
|
|
44
|
+
for (const [lineKey, state] of Object.entries(editableSummaryInputsByKey)) {
|
|
45
|
+
out[lineKey] = state.labelInput;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}, [editableSummaryInputsByKey]);
|
|
49
|
+
|
|
50
|
+
const editableCheckoutPriceSummaryLines = useMemo((): PriceSummaryLine[] => {
|
|
51
|
+
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
52
|
+
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX',
|
|
53
|
+
);
|
|
54
|
+
return checkoutPriceSummaryLinesForCheckout.map((line, index) => {
|
|
55
|
+
const isBeforeSubtotal = firstTaxLineIndex < 0 || index < firstTaxLineIndex;
|
|
56
|
+
const lineKey = getEditableSummaryLineKey(line, index);
|
|
57
|
+
const lineInputState = editableSummaryInputsByKey[lineKey];
|
|
58
|
+
if (line.kind === 'ticket') {
|
|
59
|
+
const amountInput = lineInputState?.amountInput;
|
|
60
|
+
const parsedAmount = amountInput == null || amountInput.trim() === '' ? line.itemTotal : Number(amountInput);
|
|
61
|
+
return {
|
|
62
|
+
...line,
|
|
63
|
+
lineKey,
|
|
64
|
+
editable: isBeforeSubtotal,
|
|
65
|
+
category: lineInputState?.labelInput ?? line.category,
|
|
66
|
+
itemTotal: Number.isFinite(parsedAmount) ? roundMoney(parsedAmount) : line.itemTotal,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const amountInput = lineInputState?.amountInput;
|
|
70
|
+
const parsedAmount = amountInput == null || amountInput.trim() === '' ? line.amount : Number(amountInput);
|
|
71
|
+
return {
|
|
72
|
+
...line,
|
|
73
|
+
lineKey,
|
|
74
|
+
editable: isBeforeSubtotal,
|
|
75
|
+
label: lineInputState?.labelInput ?? line.label,
|
|
76
|
+
amount: Number.isFinite(parsedAmount) ? roundMoney(parsedAmount) : line.amount,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}, [checkoutPriceSummaryLinesForCheckout, editableSummaryInputsByKey]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
setEditableSummaryInputsByKey((prev) => {
|
|
83
|
+
const next: Record<string, EditableSummaryLineInputState> = {};
|
|
84
|
+
for (let i = 0; i < checkoutPriceSummaryLinesForCheckout.length; i += 1) {
|
|
85
|
+
const line = checkoutPriceSummaryLinesForCheckout[i];
|
|
86
|
+
const key = getEditableSummaryLineKey(line, i);
|
|
87
|
+
const baselineAmount =
|
|
88
|
+
line.kind === 'ticket' ? String(roundMoney(line.itemTotal)) : String(roundMoney(line.amount));
|
|
89
|
+
const baselineLabel = line.kind === 'ticket' ? line.category : line.label;
|
|
90
|
+
const current = prev[key];
|
|
91
|
+
const amountDirty = current?.amountDirty === true;
|
|
92
|
+
const labelDirty = current?.labelDirty === true;
|
|
93
|
+
next[key] = {
|
|
94
|
+
amountInput: amountDirty ? (current?.amountInput ?? baselineAmount) : baselineAmount,
|
|
95
|
+
labelInput: labelDirty ? (current?.labelInput ?? baselineLabel) : baselineLabel,
|
|
96
|
+
amountDirty,
|
|
97
|
+
labelDirty,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const prevKeys = Object.keys(prev);
|
|
101
|
+
const nextKeys = Object.keys(next);
|
|
102
|
+
if (prevKeys.length !== nextKeys.length) return next;
|
|
103
|
+
const unchanged = nextKeys.every((key) => {
|
|
104
|
+
const a = prev[key];
|
|
105
|
+
const b = next[key];
|
|
106
|
+
return (
|
|
107
|
+
a?.amountInput === b.amountInput &&
|
|
108
|
+
a?.labelInput === b.labelInput &&
|
|
109
|
+
a?.amountDirty === b.amountDirty &&
|
|
110
|
+
a?.labelDirty === b.labelDirty
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
return unchanged ? prev : next;
|
|
114
|
+
});
|
|
115
|
+
}, [checkoutPriceSummaryLinesForCheckout]);
|
|
116
|
+
|
|
117
|
+
const editableSummaryPreSubtotalDelta = useMemo(() => {
|
|
118
|
+
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
119
|
+
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX',
|
|
120
|
+
);
|
|
121
|
+
let delta = 0;
|
|
122
|
+
for (let i = 0; i < checkoutPriceSummaryLinesForCheckout.length; i += 1) {
|
|
123
|
+
if (firstTaxLineIndex >= 0 && i >= firstTaxLineIndex) break;
|
|
124
|
+
const line = checkoutPriceSummaryLinesForCheckout[i];
|
|
125
|
+
const lineKey = getEditableSummaryLineKey(line, i);
|
|
126
|
+
const baselineAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
127
|
+
const raw = editableSummaryLineAmountInputs[lineKey];
|
|
128
|
+
const parsed = raw == null || raw.trim() === '' ? baselineAmount : Number(raw);
|
|
129
|
+
const editedAmount = Number.isFinite(parsed) ? roundMoney(parsed) : baselineAmount;
|
|
130
|
+
delta += editedAmount - baselineAmount;
|
|
131
|
+
}
|
|
132
|
+
return roundMoney(delta);
|
|
133
|
+
}, [checkoutPriceSummaryLinesForCheckout, editableSummaryLineAmountInputs]);
|
|
134
|
+
|
|
135
|
+
const editableSummaryPreSubtotalDebugRows = useMemo(() => {
|
|
136
|
+
const firstTaxLineIndex = checkoutPriceSummaryLinesForCheckout.findIndex(
|
|
137
|
+
(line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX',
|
|
138
|
+
);
|
|
139
|
+
const rows: Array<{ label: string; baseline: number; edited: number; delta: number }> = [];
|
|
140
|
+
for (let i = 0; i < checkoutPriceSummaryLinesForCheckout.length; i += 1) {
|
|
141
|
+
if (firstTaxLineIndex >= 0 && i >= firstTaxLineIndex) break;
|
|
142
|
+
const line = checkoutPriceSummaryLinesForCheckout[i];
|
|
143
|
+
const lineKey = getEditableSummaryLineKey(line, i);
|
|
144
|
+
const baselineAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
145
|
+
const raw = editableSummaryLineAmountInputs[lineKey];
|
|
146
|
+
const parsed = raw == null || raw.trim() === '' ? baselineAmount : Number(raw);
|
|
147
|
+
const editedAmount = Number.isFinite(parsed) ? roundMoney(parsed) : baselineAmount;
|
|
148
|
+
const delta = roundMoney(editedAmount - baselineAmount);
|
|
149
|
+
rows.push({
|
|
150
|
+
label: line.kind === 'ticket' ? line.category : line.label,
|
|
151
|
+
baseline: roundMoney(baselineAmount),
|
|
152
|
+
edited: editedAmount,
|
|
153
|
+
delta,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return rows;
|
|
157
|
+
}, [checkoutPriceSummaryLinesForCheckout, editableSummaryLineAmountInputs]);
|
|
158
|
+
|
|
159
|
+
const onLineAmountInputChange = (lineKey: string, value: string) => {
|
|
160
|
+
const sanitized = value.replace(/[^0-9.-]/g, '');
|
|
161
|
+
setEditableSummaryInputsByKey((prev) => {
|
|
162
|
+
const current = prev[lineKey] ?? {
|
|
163
|
+
amountInput: '',
|
|
164
|
+
labelInput: '',
|
|
165
|
+
amountDirty: false,
|
|
166
|
+
labelDirty: false,
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
...prev,
|
|
170
|
+
[lineKey]: { ...current, amountInput: sanitized, amountDirty: true },
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
if (providerAmountSync.enabled) providerAmountSync.onChange?.(lineKey, sanitized);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const onLineAmountInputBlur = (lineKey: string) => {
|
|
177
|
+
setEditableSummaryInputsByKey((prev) => {
|
|
178
|
+
const current = prev[lineKey];
|
|
179
|
+
if (!current) return prev;
|
|
180
|
+
const raw = current.amountInput ?? '';
|
|
181
|
+
if (raw.trim() === '' || raw === '-' || raw === '.' || raw === '-.') {
|
|
182
|
+
return prev;
|
|
183
|
+
}
|
|
184
|
+
const parsed = Number(raw);
|
|
185
|
+
if (!Number.isFinite(parsed)) return prev;
|
|
186
|
+
const normalized = roundMoney(parsed).toFixed(2);
|
|
187
|
+
if (normalized === raw) return prev;
|
|
188
|
+
return {
|
|
189
|
+
...prev,
|
|
190
|
+
[lineKey]: { ...current, amountInput: normalized },
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
if (providerAmountSync.enabled) providerAmountSync.onBlur?.(lineKey);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const onLineAmountReset = (lineKey: string) => {
|
|
197
|
+
const original = checkoutPriceSummaryLinesForCheckout.find(
|
|
198
|
+
(line, index) => getEditableSummaryLineKey(line, index) === lineKey,
|
|
199
|
+
);
|
|
200
|
+
if (!original) return;
|
|
201
|
+
const nextValue =
|
|
202
|
+
original.kind === 'ticket' ? String(roundMoney(original.itemTotal)) : String(roundMoney(original.amount));
|
|
203
|
+
setEditableSummaryInputsByKey((prev) => {
|
|
204
|
+
const current = prev[lineKey] ?? {
|
|
205
|
+
amountInput: nextValue,
|
|
206
|
+
labelInput: '',
|
|
207
|
+
amountDirty: false,
|
|
208
|
+
labelDirty: false,
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
...prev,
|
|
212
|
+
[lineKey]: { ...current, amountInput: nextValue, amountDirty: false },
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
if (providerAmountSync.enabled) providerAmountSync.onReset?.(lineKey);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const onLineLabelInputChange = (lineKey: string, value: string) => {
|
|
219
|
+
setEditableSummaryInputsByKey((prev) => {
|
|
220
|
+
const current = prev[lineKey] ?? {
|
|
221
|
+
amountInput: '',
|
|
222
|
+
labelInput: '',
|
|
223
|
+
amountDirty: false,
|
|
224
|
+
labelDirty: false,
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
...prev,
|
|
228
|
+
[lineKey]: { ...current, labelInput: value, labelDirty: true },
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
editableCheckoutPriceSummaryLines,
|
|
235
|
+
editableSummaryLineAmountInputs,
|
|
236
|
+
editableSummaryLineLabelInputs,
|
|
237
|
+
editableSummaryPreSubtotalDelta,
|
|
238
|
+
editableSummaryPreSubtotalDebugRows,
|
|
239
|
+
onLineAmountInputChange,
|
|
240
|
+
onLineAmountInputBlur,
|
|
241
|
+
onLineAmountReset,
|
|
242
|
+
onLineLabelInputChange,
|
|
243
|
+
};
|
|
244
|
+
}
|