@ticketboothapp/booking 1.2.72 → 1.2.75
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/CHANGE_BOOKING_BE_HANDOFF.md +16 -0
- package/README.md +9 -0
- package/docs/local-ticketbooth-provider-dashboard.md +60 -0
- package/package.json +1 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +271 -76
- package/src/components/booking/PickupLocationDialog.module.css +390 -0
- package/src/components/booking/PickupLocationDialog.tsx +397 -0
- package/src/components/booking/PriceSummary.tsx +15 -1
- package/src/components/booking/booking-flow.css +36 -0
- package/src/components/booking/provider-dashboard-change-booking.ts +1 -0
- package/src/index.ts +7 -0
- package/src/lib/booking-api.ts +6 -0
- package/src/lib/format-booking-ref.ts +12 -0
|
@@ -84,3 +84,19 @@ Pickers show **catalog** prices by default (same booking **currency** as the ori
|
|
|
84
84
|
- **Currency**: align optional maps and line `amount` values with `currency` / receipt currency; change booking stays in the booking’s sold currency end-to-end on FE.
|
|
85
85
|
- **Cancellation policy on change**: FE sends the booked policy on reserve/confirm paths; it is **not** chosen from product config during change — preserve the booking’s policy server-side unless product rules say otherwise.
|
|
86
86
|
- **`serverPreview.completeness`**: the package sets this to **`full`** whenever a preview is built (informational); UI does not gate on `totals_only` vs `full`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Admin / provider manual line items (signed adjustments)
|
|
91
|
+
|
|
92
|
+
**Frontend (this package):** `AdminChangeBookingFlow` lets staff add custom receipt lines (label + signed amount). The same values are:
|
|
93
|
+
|
|
94
|
+
1. **`POST …/change/quote`** — optional request field **`manualLineAdjustments`**: `Array<{ label: string; amount: number }>` in **major units** (same currency as the booking). Amounts are signed (credits negative). Omitted when there are no custom lines.
|
|
95
|
+
2. **Provider apply** — `onChangeBooking` payload field **`pricingAdjustment`**, with **`mode: 'MANUAL_LINES'`** and **`additionalAdjustments`** equal to the **provider** inline editor rows **plus** these admin custom lines (concatenated). `newTotalAmount` remains the full proposed total (already includes those lines on the client).
|
|
96
|
+
|
|
97
|
+
**Backend expectations**
|
|
98
|
+
|
|
99
|
+
- **Quote:** When `manualLineAdjustments` is present, add each line to the server’s priced subtotal (before tax) for the change intent, and return quote totals / `amountDue` / line items that **include** these rows so the UI and `clientProposedTotal` stay in tolerance.
|
|
100
|
+
- **Provider change API:** When `pricingAdjustment.additionalAdjustments` includes the same rows, persist them on the new receipt and charge the **same** total as `newTotalAmount` (within your rounding rules).
|
|
101
|
+
|
|
102
|
+
If the server ignores `manualLineAdjustments` or `additionalAdjustments`, the customer will see the FE total but pay a different amount.
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# `@ticketboothapp/booking`
|
|
2
|
+
|
|
3
|
+
Published as `@ticketboothapp/booking` (see `package.json`).
|
|
4
|
+
|
|
5
|
+
**Local development in TicketBooth provider-dashboard** (link this package without publishing):
|
|
6
|
+
|
|
7
|
+
- [docs/local-ticketbooth-provider-dashboard.md](./docs/local-ticketbooth-provider-dashboard.md)
|
|
8
|
+
|
|
9
|
+
**In this monorepo:** apps use `workspace:*` / `transpilePackages`; run e.g. `npm run dev:booking` or `npm run dev:staff` from the website repo root.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Test `@ticketboothapp/booking` in TicketBooth provider-dashboard (local link)
|
|
2
|
+
|
|
3
|
+
Use this when developing in **viavia-website** and you want **provider-dashboard** (`ticketbooth-fe`) to load your **workspace package** without publishing to npm.
|
|
4
|
+
|
|
5
|
+
Provider-dashboard already sets `transpilePackages: ['@ticketboothapp/booking']`, so Next will compile the linked package from source.
|
|
6
|
+
|
|
7
|
+
## Option A — `npm link` (quick toggle)
|
|
8
|
+
|
|
9
|
+
**1. Register this package globally**
|
|
10
|
+
|
|
11
|
+
From this repo’s booking package directory:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd /path/to/viavia-website/packages/viavia-booking
|
|
15
|
+
npm link
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**2. Point provider-dashboard at that link**
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd /path/to/ticketbooth-fe/apps/provider-dashboard
|
|
22
|
+
npm link @ticketboothapp/booking
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**3. Run provider-dashboard**
|
|
26
|
+
|
|
27
|
+
From the TicketBooth frontend monorepo root (typical for that repo):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd /path/to/ticketbooth-fe
|
|
31
|
+
npm run dev:provider
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Undo** (back to the version from the registry):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd /path/to/ticketbooth-fe/apps/provider-dashboard
|
|
38
|
+
npm unlink @ticketboothapp/booking
|
|
39
|
+
npm install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If the workspace layout is odd, run `npm install` once at the `ticketbooth-fe` root after unlink.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Option B — `file:` in `package.json` (stays on until you revert)
|
|
47
|
+
|
|
48
|
+
In `apps/provider-dashboard/package.json`, set:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
"@ticketboothapp/booking": "file:/absolute/path/to/viavia-website/packages/viavia-booking"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then from `ticketbooth-fe` root: `npm install` and `npm run dev:provider`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Option C — `overrides` in `ticketbooth-fe` root `package.json`
|
|
59
|
+
|
|
60
|
+
Add an `overrides` entry for `@ticketboothapp/booking` pointing at the same `file:` path, then `npm install` at the monorepo root.
|
package/package.json
CHANGED
|
@@ -726,6 +726,11 @@ export function AdminChangeBookingFlow({
|
|
|
726
726
|
);
|
|
727
727
|
/** Fetched add-ons for the selected product option */
|
|
728
728
|
const [addOns, setAddOns] = useState<AddOn[]>([]);
|
|
729
|
+
/** Admin receipt-only adjustments (signed amount); affects FE subtotal/tax/total until persisted server-side. */
|
|
730
|
+
const [adminCustomReceiptLines, setAdminCustomReceiptLines] = useState<
|
|
731
|
+
Array<{ id: string; label: string; amountInput: string; amountSign: 1 | -1 }>
|
|
732
|
+
>([]);
|
|
733
|
+
const adminCustomLineIdRef = useRef(0);
|
|
729
734
|
|
|
730
735
|
// Auto-apply promo code when parent page passes one (e.g. partner pages).
|
|
731
736
|
// Seed input only; validate/apply runs after date/time + tickets exist (debounced + handleApplyPromo).
|
|
@@ -2909,6 +2914,22 @@ export function AdminChangeBookingFlow({
|
|
|
2909
2914
|
: subtotal;
|
|
2910
2915
|
const effectiveSubtotal = effectiveSubtotalBeforeAddOns + addOnTotal;
|
|
2911
2916
|
|
|
2917
|
+
const adminCustomAdjustmentTotal = useMemo(
|
|
2918
|
+
() =>
|
|
2919
|
+
roundMoney(
|
|
2920
|
+
adminCustomReceiptLines.reduce((sum, line) => {
|
|
2921
|
+
const raw = line.amountInput.trim();
|
|
2922
|
+
if (raw === '' || raw === '+' || raw === '-') return sum;
|
|
2923
|
+
const n = Number(raw);
|
|
2924
|
+
if (!Number.isFinite(n)) return sum;
|
|
2925
|
+
const sign = line.amountSign ?? 1;
|
|
2926
|
+
return sum + sign * Math.abs(n);
|
|
2927
|
+
}, 0)
|
|
2928
|
+
),
|
|
2929
|
+
[adminCustomReceiptLines]
|
|
2930
|
+
);
|
|
2931
|
+
const effectiveSubtotalForCheckout = roundMoney(effectiveSubtotal + adminCustomAdjustmentTotal);
|
|
2932
|
+
|
|
2912
2933
|
/** Stable signature for promo discount API (avoid effect re-fire on object identity churn). */
|
|
2913
2934
|
const quantitiesSignature = useMemo(
|
|
2914
2935
|
() =>
|
|
@@ -3123,7 +3144,7 @@ export function AdminChangeBookingFlow({
|
|
|
3123
3144
|
selectedAvailability.availabilityId ?? '',
|
|
3124
3145
|
currency,
|
|
3125
3146
|
quantitiesSignature,
|
|
3126
|
-
String(Math.round(
|
|
3147
|
+
String(Math.round(effectiveSubtotalForCheckout * 100)),
|
|
3127
3148
|
lockedPromoCode ? String(Math.round((originalReceipt?.promoAmount ?? 0) * 100)) : '',
|
|
3128
3149
|
].join('::');
|
|
3129
3150
|
}, [
|
|
@@ -3136,7 +3157,7 @@ export function AdminChangeBookingFlow({
|
|
|
3136
3157
|
product.productId,
|
|
3137
3158
|
currency,
|
|
3138
3159
|
quantitiesSignature,
|
|
3139
|
-
|
|
3160
|
+
effectiveSubtotalForCheckout,
|
|
3140
3161
|
lockedPromoCode,
|
|
3141
3162
|
originalReceipt?.promoAmount,
|
|
3142
3163
|
]);
|
|
@@ -3144,7 +3165,7 @@ export function AdminChangeBookingFlow({
|
|
|
3144
3165
|
promoDiscountParamsRef.current = {
|
|
3145
3166
|
selectedAvailability,
|
|
3146
3167
|
ticketLineItems: ticketLineItems.map((l) => ({ category: l.category, qty: l.qty })),
|
|
3147
|
-
effectiveSubtotal,
|
|
3168
|
+
effectiveSubtotal: effectiveSubtotalForCheckout,
|
|
3148
3169
|
appliedPromoCode,
|
|
3149
3170
|
changeBookingPromo: lockedPromoCode
|
|
3150
3171
|
? { priorAmount: originalReceipt?.promoAmount }
|
|
@@ -3225,12 +3246,12 @@ export function AdminChangeBookingFlow({
|
|
|
3225
3246
|
: 0;
|
|
3226
3247
|
const effectivePromoDiscountAmount =
|
|
3227
3248
|
promoDiscountAmount > 0 ? promoDiscountAmount : lockedPromoFallbackAmount;
|
|
3228
|
-
const taxOnSubtotal = isTaxIncludedInPrice ? 0 :
|
|
3249
|
+
const taxOnSubtotal = isTaxIncludedInPrice ? 0 : effectiveSubtotalForCheckout * (pricingConfig?.taxRate ?? 0);
|
|
3229
3250
|
const effectiveTax =
|
|
3230
3251
|
effectivePromoDiscountAmount > 0 && !isGiftCard && !isVoucher
|
|
3231
|
-
? (
|
|
3252
|
+
? (effectiveSubtotalForCheckout - effectivePromoDiscountAmount) * (pricingConfig?.taxRate ?? 0)
|
|
3232
3253
|
: taxOnSubtotal;
|
|
3233
|
-
const totalPrice =
|
|
3254
|
+
const totalPrice = effectiveSubtotalForCheckout + effectiveTax - effectivePromoDiscountAmount;
|
|
3234
3255
|
/**
|
|
3235
3256
|
* FE cart rollup for line math, breakdowns, and `clientProposedTotal` hint to the API. Self-serve **footer** totals
|
|
3236
3257
|
* prefer `latestChangeQuote` once quote succeeds — this value is not an alternate source of truth for checkout.
|
|
@@ -3661,9 +3682,12 @@ export function AdminChangeBookingFlow({
|
|
|
3661
3682
|
return currentAmount !== originalAmount || currentLabel !== originalLabel || currentMode !== originalMode;
|
|
3662
3683
|
});
|
|
3663
3684
|
const hasEffectiveChangeSelection =
|
|
3664
|
-
hasChangeSelection ||
|
|
3685
|
+
hasChangeSelection ||
|
|
3686
|
+
providerHasEditedLineOverrides ||
|
|
3687
|
+
providerHasAdditionalAdjustments ||
|
|
3688
|
+
Math.abs(adminCustomAdjustmentTotal) >= 0.005;
|
|
3665
3689
|
|
|
3666
|
-
const
|
|
3690
|
+
const displayedChangeAmountsRaw = resolveChangeFlowDisplayedAmounts({
|
|
3667
3691
|
providerPreview: providerTotalsPreview,
|
|
3668
3692
|
serverQuotePreview:
|
|
3669
3693
|
isCustomerSelfServeChange && latestChangeQuote?.serverDisplay
|
|
@@ -3671,13 +3695,26 @@ export function AdminChangeBookingFlow({
|
|
|
3671
3695
|
: null,
|
|
3672
3696
|
fromCart: {
|
|
3673
3697
|
total: changeFlowNewBookingTotal,
|
|
3674
|
-
subtotal:
|
|
3698
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
3675
3699
|
tax: effectiveTax,
|
|
3676
3700
|
},
|
|
3677
3701
|
});
|
|
3678
|
-
|
|
3679
|
-
const
|
|
3680
|
-
|
|
3702
|
+
/** When cart drives display, {@link effectiveSubtotalForCheckout} already includes admin adjustments — do not add twice. */
|
|
3703
|
+
const displayLayerUsesExternalPricing =
|
|
3704
|
+
Boolean(providerTotalsPreview) ||
|
|
3705
|
+
(isCustomerSelfServeChange && Boolean(latestChangeQuote?.serverDisplay));
|
|
3706
|
+
const adminTaxDeltaForExternalDisplay =
|
|
3707
|
+
displayLayerUsesExternalPricing && !isTaxIncludedInPrice && Math.abs(adminCustomAdjustmentTotal) >= 0.0005
|
|
3708
|
+
? roundMoney(adminCustomAdjustmentTotal * (pricingConfig?.taxRate ?? 0))
|
|
3709
|
+
: 0;
|
|
3710
|
+
const displayChangeFlowSubtotal = roundMoney(
|
|
3711
|
+
displayedChangeAmountsRaw.subtotal +
|
|
3712
|
+
(displayLayerUsesExternalPricing ? adminCustomAdjustmentTotal : 0)
|
|
3713
|
+
);
|
|
3714
|
+
const displayChangeFlowTax = roundMoney(displayedChangeAmountsRaw.tax + adminTaxDeltaForExternalDisplay);
|
|
3715
|
+
const displayChangeFlowProposedTotal = roundMoney(
|
|
3716
|
+
displayedChangeAmountsRaw.total + adminCustomAdjustmentTotal + adminTaxDeltaForExternalDisplay
|
|
3717
|
+
);
|
|
3681
3718
|
|
|
3682
3719
|
const changeFlowClientEstimateDue = (() => {
|
|
3683
3720
|
if (!originalReceipt) return totalPrice;
|
|
@@ -3772,6 +3809,25 @@ export function AdminChangeBookingFlow({
|
|
|
3772
3809
|
.filter((v): v is { label: string; amount: number } => v != null)
|
|
3773
3810
|
: [];
|
|
3774
3811
|
|
|
3812
|
+
/** Same rows as checkout breakdown — sent on provider apply + optional public change-quote for BE parity. */
|
|
3813
|
+
const adminCustomLinesAsAdditionalAdjustments = useMemo((): Array<{ label: string; amount: number }> => {
|
|
3814
|
+
return adminCustomReceiptLines.flatMap((line) => {
|
|
3815
|
+
const raw = line.amountInput.trim();
|
|
3816
|
+
if (raw === '' || raw === '+' || raw === '-') return [];
|
|
3817
|
+
const n = Number(raw);
|
|
3818
|
+
if (!Number.isFinite(n) || Math.abs(n) < 0.0005) return [];
|
|
3819
|
+
const sign = line.amountSign ?? 1;
|
|
3820
|
+
const amount = roundMoney(sign * Math.abs(n));
|
|
3821
|
+
if (Math.abs(amount) < 0.005) return [];
|
|
3822
|
+
return [{ label: line.label.trim() || 'Adjustment', amount }];
|
|
3823
|
+
});
|
|
3824
|
+
}, [adminCustomReceiptLines]);
|
|
3825
|
+
|
|
3826
|
+
const mergedProviderAdditionalAdjustments = useMemo(
|
|
3827
|
+
() => [...providerAdditionalAdjustments, ...adminCustomLinesAsAdditionalAdjustments],
|
|
3828
|
+
[providerAdditionalAdjustments, adminCustomLinesAsAdditionalAdjustments]
|
|
3829
|
+
);
|
|
3830
|
+
|
|
3775
3831
|
const changeFlowSelectionPreview = useMemo((): ChangeFlowSelectionPreview | null => {
|
|
3776
3832
|
if (!selectedAvailability || totalQuantity <= 0) return null;
|
|
3777
3833
|
const ticketsLine = formatTicketLineItemsForSummary(
|
|
@@ -3847,7 +3903,7 @@ export function AdminChangeBookingFlow({
|
|
|
3847
3903
|
return;
|
|
3848
3904
|
}
|
|
3849
3905
|
onPricePreviewChange({
|
|
3850
|
-
subtotal: originalReceipt ? displayChangeFlowSubtotal :
|
|
3906
|
+
subtotal: originalReceipt ? displayChangeFlowSubtotal : effectiveSubtotalForCheckout,
|
|
3851
3907
|
tax:
|
|
3852
3908
|
!isTaxIncludedInPrice
|
|
3853
3909
|
? originalReceipt
|
|
@@ -3861,7 +3917,7 @@ export function AdminChangeBookingFlow({
|
|
|
3861
3917
|
onPricePreviewChange,
|
|
3862
3918
|
selectedAvailability,
|
|
3863
3919
|
totalQuantity,
|
|
3864
|
-
|
|
3920
|
+
effectiveSubtotalForCheckout,
|
|
3865
3921
|
effectiveTax,
|
|
3866
3922
|
displayChangeFlowProposedTotal,
|
|
3867
3923
|
displayChangeFlowSubtotal,
|
|
@@ -3926,6 +3982,9 @@ export function AdminChangeBookingFlow({
|
|
|
3926
3982
|
newPassengerCounts: bookingItems,
|
|
3927
3983
|
// Omit when empty: backend treats [] as "clear all"; missing = preserve stored selections (BookingChangeIntentService).
|
|
3928
3984
|
...(addOnSelections.length > 0 ? { newAddOnSelections: addOnSelections } : {}),
|
|
3985
|
+
...(adminCustomLinesAsAdditionalAdjustments.length > 0
|
|
3986
|
+
? { manualLineAdjustments: adminCustomLinesAsAdditionalAdjustments }
|
|
3987
|
+
: {}),
|
|
3929
3988
|
clientProposedTotal: changeFlowNewBookingTotal,
|
|
3930
3989
|
capacitySeatCredit: {
|
|
3931
3990
|
enabled: true,
|
|
@@ -3939,7 +3998,7 @@ export function AdminChangeBookingFlow({
|
|
|
3939
3998
|
quote,
|
|
3940
3999
|
{
|
|
3941
4000
|
total: changeFlowNewBookingTotal,
|
|
3942
|
-
subtotal:
|
|
4001
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
3943
4002
|
tax: effectiveTax,
|
|
3944
4003
|
},
|
|
3945
4004
|
currency
|
|
@@ -3947,7 +4006,7 @@ export function AdminChangeBookingFlow({
|
|
|
3947
4006
|
setLatestChangeQuote(
|
|
3948
4007
|
mergeQuoteSliceWithServerPreview(slice, quote, {
|
|
3949
4008
|
total: changeFlowNewBookingTotal,
|
|
3950
|
-
subtotal:
|
|
4009
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
3951
4010
|
tax: effectiveTax,
|
|
3952
4011
|
}, currency),
|
|
3953
4012
|
);
|
|
@@ -3981,13 +4040,14 @@ export function AdminChangeBookingFlow({
|
|
|
3981
4040
|
addOnSelections,
|
|
3982
4041
|
changeFlowInitialTicketCount,
|
|
3983
4042
|
changeFlowNewBookingTotal,
|
|
3984
|
-
|
|
4043
|
+
effectiveSubtotalForCheckout,
|
|
3985
4044
|
effectiveTax,
|
|
3986
4045
|
totalPrice,
|
|
3987
4046
|
currency,
|
|
3988
4047
|
activeOptions,
|
|
3989
4048
|
initialValues?.availabilityId,
|
|
3990
4049
|
initialValues?.returnAvailabilityId,
|
|
4050
|
+
adminCustomLinesAsAdditionalAdjustments,
|
|
3991
4051
|
]);
|
|
3992
4052
|
|
|
3993
4053
|
// Auto-select product option when date is selected: most popular if set, otherwise first available.
|
|
@@ -4518,6 +4578,9 @@ export function AdminChangeBookingFlow({
|
|
|
4518
4578
|
newReturnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
4519
4579
|
newPassengerCounts: bookingItems,
|
|
4520
4580
|
...(addOnSelections.length > 0 ? { newAddOnSelections: addOnSelections } : {}),
|
|
4581
|
+
...(adminCustomLinesAsAdditionalAdjustments.length > 0
|
|
4582
|
+
? { manualLineAdjustments: adminCustomLinesAsAdditionalAdjustments }
|
|
4583
|
+
: {}),
|
|
4521
4584
|
clientProposedTotal:
|
|
4522
4585
|
latestChangeQuote?.serverDisplay?.total ?? changeFlowNewBookingTotal,
|
|
4523
4586
|
});
|
|
@@ -4525,7 +4588,7 @@ export function AdminChangeBookingFlow({
|
|
|
4525
4588
|
quote,
|
|
4526
4589
|
{
|
|
4527
4590
|
total: changeFlowNewBookingTotal,
|
|
4528
|
-
subtotal:
|
|
4591
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
4529
4592
|
tax: effectiveTax,
|
|
4530
4593
|
},
|
|
4531
4594
|
currency
|
|
@@ -4536,7 +4599,7 @@ export function AdminChangeBookingFlow({
|
|
|
4536
4599
|
setLatestChangeQuote(
|
|
4537
4600
|
mergeQuoteSliceWithServerPreview(quoteSlice, quote, {
|
|
4538
4601
|
total: changeFlowNewBookingTotal,
|
|
4539
|
-
subtotal:
|
|
4602
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
4540
4603
|
tax: effectiveTax,
|
|
4541
4604
|
}, currency),
|
|
4542
4605
|
);
|
|
@@ -4679,6 +4742,21 @@ export function AdminChangeBookingFlow({
|
|
|
4679
4742
|
type: 'FEE' as const,
|
|
4680
4743
|
quantity: totalQuantity,
|
|
4681
4744
|
})),
|
|
4745
|
+
...adminCustomReceiptLines.flatMap((adjLine) => {
|
|
4746
|
+
const raw = adjLine.amountInput.trim();
|
|
4747
|
+
if (raw === '' || raw === '+' || raw === '-') return [];
|
|
4748
|
+
const n = Number(raw);
|
|
4749
|
+
if (!Number.isFinite(n) || Math.abs(n) < 0.0005) return [];
|
|
4750
|
+
const sign = adjLine.amountSign ?? 1;
|
|
4751
|
+
const amount = sign * Math.abs(n);
|
|
4752
|
+
return [
|
|
4753
|
+
{
|
|
4754
|
+
label: adjLine.label.trim() || 'Adjustment',
|
|
4755
|
+
amount,
|
|
4756
|
+
type: 'FEE' as const,
|
|
4757
|
+
},
|
|
4758
|
+
];
|
|
4759
|
+
}),
|
|
4682
4760
|
...(!isTaxIncludedInPrice && taxForBreakdown > 0
|
|
4683
4761
|
? [
|
|
4684
4762
|
{
|
|
@@ -4772,7 +4850,7 @@ export function AdminChangeBookingFlow({
|
|
|
4772
4850
|
returnPriceAdjustment: checkoutReturnLineAmount,
|
|
4773
4851
|
cancellationPolicyFee,
|
|
4774
4852
|
cancellationPolicyLabel: effectiveCancellationPolicyLabel,
|
|
4775
|
-
subtotal:
|
|
4853
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
4776
4854
|
tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
|
|
4777
4855
|
totalQuantity,
|
|
4778
4856
|
isTaxIncludedInPrice,
|
|
@@ -4833,7 +4911,7 @@ export function AdminChangeBookingFlow({
|
|
|
4833
4911
|
returnPriceAdjustment: checkoutReturnLineAmount,
|
|
4834
4912
|
cancellationPolicyFee,
|
|
4835
4913
|
cancellationPolicyLabel: effectiveCancellationPolicyLabel,
|
|
4836
|
-
subtotal:
|
|
4914
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
4837
4915
|
tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
|
|
4838
4916
|
total: amountDueForCheckout,
|
|
4839
4917
|
totalQuantity,
|
|
@@ -4954,11 +5032,13 @@ export function AdminChangeBookingFlow({
|
|
|
4954
5032
|
newTotalAmount: displayChangeFlowProposedTotal,
|
|
4955
5033
|
additionalHoursCount: null,
|
|
4956
5034
|
pricingAdjustment:
|
|
4957
|
-
providerPricingOverrides.length > 0 ||
|
|
5035
|
+
providerPricingOverrides.length > 0 || mergedProviderAdditionalAdjustments.length > 0
|
|
4958
5036
|
? {
|
|
4959
5037
|
mode: 'MANUAL_LINES',
|
|
4960
|
-
lineOverrides: providerPricingOverrides,
|
|
4961
|
-
|
|
5038
|
+
...(providerPricingOverrides.length > 0 ? { lineOverrides: providerPricingOverrides } : {}),
|
|
5039
|
+
...(mergedProviderAdditionalAdjustments.length > 0
|
|
5040
|
+
? { additionalAdjustments: mergedProviderAdditionalAdjustments }
|
|
5041
|
+
: {}),
|
|
4962
5042
|
}
|
|
4963
5043
|
: undefined,
|
|
4964
5044
|
capacitySeatCredit: {
|
|
@@ -5361,73 +5441,188 @@ export function AdminChangeBookingFlow({
|
|
|
5361
5441
|
</>
|
|
5362
5442
|
}
|
|
5363
5443
|
extraBeforeSubtotal={
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
<div className="flex
|
|
5369
|
-
<
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5444
|
+
<>
|
|
5445
|
+
{showProviderPricingInlineEditor && (providerPricingUi?.additionalAdjustments?.length ?? 0) > 0 ? (
|
|
5446
|
+
<div className="space-y-1">
|
|
5447
|
+
{providerPricingUi?.additionalAdjustments?.map((adj) => (
|
|
5448
|
+
<div key={adj.id} className="flex items-center justify-between gap-2 text-sm">
|
|
5449
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
5450
|
+
<button
|
|
5451
|
+
type="button"
|
|
5452
|
+
className="rounded border border-stone-300 px-1 text-xs text-stone-600 hover:bg-stone-100"
|
|
5453
|
+
onClick={() => providerPricingUi?.onRemoveAdditionalAdjustment?.(adj.id)}
|
|
5454
|
+
>
|
|
5455
|
+
-
|
|
5456
|
+
</button>
|
|
5457
|
+
<input
|
|
5458
|
+
type="text"
|
|
5459
|
+
className="w-40 rounded border border-stone-300 px-2 py-0.5 text-sm"
|
|
5460
|
+
placeholder="Line description"
|
|
5461
|
+
value={adj.label}
|
|
5462
|
+
onChange={(e) =>
|
|
5463
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, { label: e.target.value })
|
|
5464
|
+
}
|
|
5465
|
+
/>
|
|
5466
|
+
</div>
|
|
5467
|
+
<div className="flex items-center gap-1">
|
|
5468
|
+
<select
|
|
5469
|
+
className="rounded border border-stone-300 px-1 py-0.5 text-xs"
|
|
5470
|
+
value={adj.mode}
|
|
5471
|
+
onChange={(e) =>
|
|
5472
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
5473
|
+
mode: e.target.value as 'DISCOUNT' | 'CHARGE',
|
|
5474
|
+
})
|
|
5475
|
+
}
|
|
5476
|
+
>
|
|
5477
|
+
<option value="DISCOUNT">-</option>
|
|
5478
|
+
<option value="CHARGE">+</option>
|
|
5479
|
+
</select>
|
|
5480
|
+
<input
|
|
5481
|
+
type="text"
|
|
5482
|
+
inputMode="decimal"
|
|
5483
|
+
className="h-6 w-24 rounded border border-stone-300 bg-white px-2 py-0.5 text-right text-sm font-medium leading-none text-stone-700"
|
|
5484
|
+
placeholder="0.00"
|
|
5485
|
+
value={adj.amountInput}
|
|
5486
|
+
onChange={(e) =>
|
|
5487
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
5488
|
+
amountInput: e.target.value,
|
|
5489
|
+
})
|
|
5490
|
+
}
|
|
5491
|
+
/>
|
|
5492
|
+
</div>
|
|
5493
|
+
</div>
|
|
5494
|
+
))}
|
|
5495
|
+
<button
|
|
5496
|
+
type="button"
|
|
5497
|
+
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
5498
|
+
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
5499
|
+
>
|
|
5500
|
+
+ add line item
|
|
5501
|
+
</button>
|
|
5502
|
+
</div>
|
|
5503
|
+
) : showProviderPricingInlineEditor ? (
|
|
5504
|
+
<button
|
|
5505
|
+
type="button"
|
|
5506
|
+
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
5507
|
+
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
5508
|
+
>
|
|
5509
|
+
+ add line item
|
|
5510
|
+
</button>
|
|
5511
|
+
) : null}
|
|
5512
|
+
<div className="space-y-3">
|
|
5513
|
+
<div className="flex justify-center">
|
|
5514
|
+
<button
|
|
5515
|
+
type="button"
|
|
5516
|
+
className="admin-custom-receipt-line-add inline-flex items-center gap-1.5 rounded-full border border-dashed border-stone-300 bg-stone-50/80 text-xs font-medium text-stone-600 hover:border-stone-400 hover:bg-stone-100"
|
|
5517
|
+
onClick={() => {
|
|
5518
|
+
adminCustomLineIdRef.current += 1;
|
|
5519
|
+
setAdminCustomReceiptLines((prev) => [
|
|
5520
|
+
...prev,
|
|
5521
|
+
{
|
|
5522
|
+
id: `admin-rcpt-${adminCustomLineIdRef.current}`,
|
|
5523
|
+
label: '',
|
|
5524
|
+
amountInput: '',
|
|
5525
|
+
amountSign: 1,
|
|
5526
|
+
},
|
|
5527
|
+
]);
|
|
5528
|
+
}}
|
|
5529
|
+
>
|
|
5530
|
+
<span className="text-base font-semibold leading-none text-stone-700" aria-hidden>
|
|
5531
|
+
+
|
|
5532
|
+
</span>
|
|
5533
|
+
Add custom line
|
|
5534
|
+
</button>
|
|
5535
|
+
</div>
|
|
5536
|
+
{adminCustomReceiptLines.map((adj) => {
|
|
5537
|
+
const sign = adj.amountSign ?? 1;
|
|
5538
|
+
return (
|
|
5539
|
+
<div
|
|
5540
|
+
key={adj.id}
|
|
5541
|
+
className="admin-custom-receipt-line flex flex-wrap items-center gap-2 text-sm sm:flex-nowrap"
|
|
5542
|
+
>
|
|
5376
5543
|
<input
|
|
5377
5544
|
type="text"
|
|
5378
|
-
className="w-
|
|
5379
|
-
placeholder="
|
|
5545
|
+
className="admin-custom-receipt-input min-w-[8rem] flex-1 rounded-md border border-stone-300 bg-white text-sm text-stone-800 placeholder:text-stone-400"
|
|
5546
|
+
placeholder="Description"
|
|
5380
5547
|
value={adj.label}
|
|
5381
5548
|
onChange={(e) =>
|
|
5382
|
-
|
|
5549
|
+
setAdminCustomReceiptLines((prev) =>
|
|
5550
|
+
prev.map((l) => (l.id === adj.id ? { ...l, label: e.target.value } : l))
|
|
5551
|
+
)
|
|
5383
5552
|
}
|
|
5384
5553
|
/>
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
value={adj.mode}
|
|
5390
|
-
onChange={(e) =>
|
|
5391
|
-
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
5392
|
-
mode: e.target.value as 'DISCOUNT' | 'CHARGE',
|
|
5393
|
-
})
|
|
5394
|
-
}
|
|
5554
|
+
<div
|
|
5555
|
+
className="flex shrink-0 rounded-lg border border-stone-300 bg-stone-100/90 p-0.5 shadow-sm"
|
|
5556
|
+
role="group"
|
|
5557
|
+
aria-label="Line adds to total or reduces it"
|
|
5395
5558
|
>
|
|
5396
|
-
<
|
|
5397
|
-
|
|
5398
|
-
|
|
5559
|
+
<button
|
|
5560
|
+
type="button"
|
|
5561
|
+
className={`admin-custom-receipt-segment rounded-md text-xs font-semibold transition-colors sm:text-sm ${
|
|
5562
|
+
sign === 1
|
|
5563
|
+
? 'bg-white text-emerald-800 shadow-sm ring-1 ring-stone-200/80'
|
|
5564
|
+
: 'text-stone-500 hover:bg-stone-200/60 hover:text-stone-700'
|
|
5565
|
+
}`}
|
|
5566
|
+
aria-pressed={sign === 1}
|
|
5567
|
+
onClick={() =>
|
|
5568
|
+
setAdminCustomReceiptLines((prev) =>
|
|
5569
|
+
prev.map((l) => (l.id === adj.id ? { ...l, amountSign: 1 } : l))
|
|
5570
|
+
)
|
|
5571
|
+
}
|
|
5572
|
+
>
|
|
5573
|
+
Charge (+)
|
|
5574
|
+
</button>
|
|
5575
|
+
<button
|
|
5576
|
+
type="button"
|
|
5577
|
+
className={`admin-custom-receipt-segment rounded-md text-xs font-semibold transition-colors sm:text-sm ${
|
|
5578
|
+
sign === -1
|
|
5579
|
+
? 'bg-white text-red-800 shadow-sm ring-1 ring-stone-200/80'
|
|
5580
|
+
: 'text-stone-500 hover:bg-stone-200/60 hover:text-stone-700'
|
|
5581
|
+
}`}
|
|
5582
|
+
aria-pressed={sign === -1}
|
|
5583
|
+
onClick={() =>
|
|
5584
|
+
setAdminCustomReceiptLines((prev) =>
|
|
5585
|
+
prev.map((l) => (l.id === adj.id ? { ...l, amountSign: -1 } : l))
|
|
5586
|
+
)
|
|
5587
|
+
}
|
|
5588
|
+
>
|
|
5589
|
+
Credit (−)
|
|
5590
|
+
</button>
|
|
5591
|
+
</div>
|
|
5399
5592
|
<input
|
|
5400
5593
|
type="text"
|
|
5401
5594
|
inputMode="decimal"
|
|
5402
|
-
className="
|
|
5595
|
+
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"
|
|
5403
5596
|
placeholder="0.00"
|
|
5597
|
+
aria-label="Amount"
|
|
5404
5598
|
value={adj.amountInput}
|
|
5405
|
-
onChange={(e) =>
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5599
|
+
onChange={(e) => {
|
|
5600
|
+
const v = e.target.value.replace(/[^0-9.]/g, '');
|
|
5601
|
+
const parts = v.split('.');
|
|
5602
|
+
const normalized =
|
|
5603
|
+
parts.length <= 1 ? v : `${parts[0]}.${parts.slice(1).join('').replace(/\./g, '')}`;
|
|
5604
|
+
setAdminCustomReceiptLines((prev) =>
|
|
5605
|
+
prev.map((l) => (l.id === adj.id ? { ...l, amountInput: normalized } : l))
|
|
5606
|
+
);
|
|
5607
|
+
}}
|
|
5410
5608
|
/>
|
|
5609
|
+
<button
|
|
5610
|
+
type="button"
|
|
5611
|
+
className="admin-custom-receipt-remove ml-auto shrink-0 rounded-md text-stone-400 hover:bg-stone-100 hover:text-stone-700 sm:ml-0"
|
|
5612
|
+
aria-label="Remove this line"
|
|
5613
|
+
onClick={() =>
|
|
5614
|
+
setAdminCustomReceiptLines((prev) => prev.filter((l) => l.id !== adj.id))
|
|
5615
|
+
}
|
|
5616
|
+
>
|
|
5617
|
+
<span className="block text-xl leading-none" aria-hidden>
|
|
5618
|
+
×
|
|
5619
|
+
</span>
|
|
5620
|
+
</button>
|
|
5411
5621
|
</div>
|
|
5412
|
-
|
|
5413
|
-
)
|
|
5414
|
-
<button
|
|
5415
|
-
type="button"
|
|
5416
|
-
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
5417
|
-
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
5418
|
-
>
|
|
5419
|
-
+ add line item
|
|
5420
|
-
</button>
|
|
5622
|
+
);
|
|
5623
|
+
})}
|
|
5421
5624
|
</div>
|
|
5422
|
-
|
|
5423
|
-
<button
|
|
5424
|
-
type="button"
|
|
5425
|
-
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
5426
|
-
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
5427
|
-
>
|
|
5428
|
-
+ add line item
|
|
5429
|
-
</button>
|
|
5430
|
-
) : undefined
|
|
5625
|
+
</>
|
|
5431
5626
|
}
|
|
5432
5627
|
firstName={firstName}
|
|
5433
5628
|
lastName={lastName}
|