@voyantjs/bookings-ui 0.100.0 → 0.101.1
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/dist/components/booking-create-sheet.d.ts.map +1 -1
- package/dist/components/booking-create-sheet.js +242 -56
- package/dist/components/booking-create-utils.d.ts +19 -1
- package/dist/components/booking-create-utils.d.ts.map +1 -1
- package/dist/components/booking-create-utils.js +69 -3
- package/dist/components/option-units-stepper-section.d.ts.map +1 -1
- package/dist/components/option-units-stepper-section.js +27 -14
- package/dist/components/price-breakdown-section.d.ts +12 -1
- package/dist/components/price-breakdown-section.d.ts.map +1 -1
- package/dist/components/price-breakdown-section.js +113 -72
- package/dist/components/traveler-category-buttons.d.ts +5 -0
- package/dist/components/traveler-category-buttons.d.ts.map +1 -1
- package/dist/components/traveler-category-buttons.js +7 -0
- package/dist/components/travelers-section.d.ts +19 -1
- package/dist/components/travelers-section.d.ts.map +1 -1
- package/dist/components/travelers-section.js +101 -33
- package/dist/i18n/en.d.ts +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/messages.d.ts +1 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +2 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +1 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +1 -0
- package/package.json +32 -32
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-create-sheet.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-sheet.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKL,KAAK,aAAa,
|
|
1
|
+
{"version":3,"file":"booking-create-sheet.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-sheet.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKL,KAAK,aAAa,EAMnB,MAAM,0BAA0B,CAAA;AAsWjC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,GACd,EAAE,uBAAuB,2CA8BzB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CAymCxB"}
|
|
@@ -3,18 +3,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { availabilityQueryKeys, getSlotQueryOptions, useSlots, useSlotUnitAvailability, useVoyantAvailabilityContext, } from "@voyantjs/availability-react";
|
|
5
5
|
import { resolveBookingDraft, resolveBookingExtraLines, travelersToRows, } from "@voyantjs/bookings/pricing-assignment";
|
|
6
|
-
import { useBookingCreateMutation, useBookingTaxPreview, VoyantApiError, } from "@voyantjs/bookings-react";
|
|
6
|
+
import { useBookingCreateMutation, useBookingTaxPreview, usePricingPreview, VoyantApiError, } from "@voyantjs/bookings-react";
|
|
7
7
|
import { useOrganization, usePerson } from "@voyantjs/crm-react";
|
|
8
8
|
import { useProductExtras } from "@voyantjs/extras-react";
|
|
9
9
|
import { useAddresses } from "@voyantjs/identity-react";
|
|
10
|
-
import { getExtraPriceRulesQueryOptions, useVoyantPricingContext } from "@voyantjs/pricing-react";
|
|
10
|
+
import { getExtraPriceRulesQueryOptions, useOptionUnitPriceRules, usePricingCategories, useVoyantPricingContext, } from "@voyantjs/pricing-react";
|
|
11
11
|
import { useProduct, useProductMedia } from "@voyantjs/products-react";
|
|
12
12
|
import { Button, Checkbox, Label, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Textarea, } from "@voyantjs/ui/components";
|
|
13
13
|
import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
|
|
14
14
|
import { ImageIcon, Loader2 } from "lucide-react";
|
|
15
15
|
import * as React from "react";
|
|
16
16
|
import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault, } from "../i18n/provider.js";
|
|
17
|
-
import { getBookableDepartureSlots, getSelectedSharedRoomUnitId, getTravelerAssignableStepperUnits, isRealBookingEmail, itemLinesToRows, validateBillingPersonContact, } from "./booking-create-utils.js";
|
|
17
|
+
import { getBookableDepartureSlots, getOverCapacityInventoryAssignments, getSelectedSharedRoomUnitId, getTravelerAssignableStepperUnits, isRealBookingEmail, itemLinesToRows, validateBillingPersonContact, } from "./booking-create-utils.js";
|
|
18
18
|
import { emptyOptionUnitsStepperValue, OptionUnitsStepperSection, } from "./option-units-stepper-section.js";
|
|
19
19
|
import { emptyPaymentScheduleValue, PaymentScheduleSection, } from "./payment-schedule-section.js";
|
|
20
20
|
import { emptyPersonPickerValue, PersonPickerSection, } from "./person-picker-section.js";
|
|
@@ -116,10 +116,97 @@ function sameRoomUnits(left, right) {
|
|
|
116
116
|
unit.optionId === other.optionId &&
|
|
117
117
|
unit.optionUnitId === other.optionUnitId &&
|
|
118
118
|
unit.unitName === other.unitName &&
|
|
119
|
+
unit.unitCode === other.unitCode &&
|
|
120
|
+
unit.unitType === other.unitType &&
|
|
121
|
+
unit.minAge === other.minAge &&
|
|
122
|
+
unit.maxAge === other.maxAge &&
|
|
119
123
|
unit.occupancyMax === other.occupancyMax &&
|
|
120
124
|
unit.remaining === other.remaining);
|
|
121
125
|
});
|
|
122
126
|
}
|
|
127
|
+
function inferTravelerPricingCategoryId(traveler, categories) {
|
|
128
|
+
if (traveler.pricingCategoryId)
|
|
129
|
+
return traveler.pricingCategoryId;
|
|
130
|
+
const pool = traveler.inventoryUnitId
|
|
131
|
+
? categories.filter((category) => category.unitIds.includes(traveler.inventoryUnitId ?? ""))
|
|
132
|
+
: categories;
|
|
133
|
+
if (pool.length === 0)
|
|
134
|
+
return null;
|
|
135
|
+
const roleType = traveler.role === "child" || traveler.role === "infant" ? traveler.role : "adult";
|
|
136
|
+
return (pool.find((category) => category.categoryType === roleType)?.categoryId ??
|
|
137
|
+
pool[0]?.categoryId ??
|
|
138
|
+
null);
|
|
139
|
+
}
|
|
140
|
+
function toStepperUnitType(value) {
|
|
141
|
+
if (value === "person" ||
|
|
142
|
+
value === "group" ||
|
|
143
|
+
value === "room" ||
|
|
144
|
+
value === "vehicle" ||
|
|
145
|
+
value === "service" ||
|
|
146
|
+
value === "other") {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function normalizeBookingUnit(unit) {
|
|
152
|
+
return {
|
|
153
|
+
...unit,
|
|
154
|
+
unitType: unit.unitType ?? (unit.occupancyMax != null ? "room" : null),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function isBookingInventoryUnit(unit) {
|
|
158
|
+
return unit.unitType === "room" || unit.unitType === "vehicle" || unit.occupancyMax != null;
|
|
159
|
+
}
|
|
160
|
+
function pricingSnapshotRoomUnits(snapshot) {
|
|
161
|
+
if (!snapshot)
|
|
162
|
+
return [];
|
|
163
|
+
const unitsById = new Map();
|
|
164
|
+
for (const unitPrice of snapshot.unitPrices) {
|
|
165
|
+
if (unitPrice.unitType !== "room" && unitPrice.unitType !== "vehicle")
|
|
166
|
+
continue;
|
|
167
|
+
if (unitsById.has(unitPrice.unitId))
|
|
168
|
+
continue;
|
|
169
|
+
unitsById.set(unitPrice.unitId, {
|
|
170
|
+
optionId: unitPrice.optionId,
|
|
171
|
+
optionUnitId: unitPrice.unitId,
|
|
172
|
+
unitName: unitPrice.unitName ?? unitPrice.unitId,
|
|
173
|
+
unitCode: null,
|
|
174
|
+
minAge: null,
|
|
175
|
+
maxAge: null,
|
|
176
|
+
unitType: toStepperUnitType(unitPrice.unitType) ?? "room",
|
|
177
|
+
occupancyMax: unitPrice.occupancyMax,
|
|
178
|
+
initial: null,
|
|
179
|
+
reserved: 0,
|
|
180
|
+
remaining: null,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return Array.from(unitsById.values());
|
|
184
|
+
}
|
|
185
|
+
function mergePricingRoomMetadata(units, pricingUnits) {
|
|
186
|
+
if (pricingUnits.length === 0)
|
|
187
|
+
return units.map(normalizeBookingUnit);
|
|
188
|
+
const pricingUnitById = new Map(pricingUnits.map((unit) => [unit.optionUnitId, unit]));
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const merged = units.map((unit) => {
|
|
191
|
+
seen.add(unit.optionUnitId);
|
|
192
|
+
const pricingUnit = pricingUnitById.get(unit.optionUnitId);
|
|
193
|
+
if (!pricingUnit)
|
|
194
|
+
return normalizeBookingUnit(unit);
|
|
195
|
+
return normalizeBookingUnit({
|
|
196
|
+
...pricingUnit,
|
|
197
|
+
...unit,
|
|
198
|
+
optionId: unit.optionId ?? pricingUnit.optionId,
|
|
199
|
+
unitName: unit.unitName || pricingUnit.unitName,
|
|
200
|
+
unitType: unit.unitType ?? pricingUnit.unitType,
|
|
201
|
+
occupancyMax: unit.occupancyMax ?? pricingUnit.occupancyMax,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
for (const pricingUnit of pricingUnits) {
|
|
205
|
+
if (!seen.has(pricingUnit.optionUnitId))
|
|
206
|
+
merged.push(pricingUnit);
|
|
207
|
+
}
|
|
208
|
+
return merged;
|
|
209
|
+
}
|
|
123
210
|
function isPayloadResolverMismatchBody(body) {
|
|
124
211
|
if (typeof body !== "object" || body === null)
|
|
125
212
|
return false;
|
|
@@ -362,73 +449,65 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
362
449
|
slotId: slotId ?? undefined,
|
|
363
450
|
enabled: enabled && Boolean(slotId),
|
|
364
451
|
});
|
|
452
|
+
const pricingPreview = usePricingPreview({
|
|
453
|
+
productId: product.productId,
|
|
454
|
+
optionId: product.optionId,
|
|
455
|
+
enabled: enabled && Boolean(product.productId),
|
|
456
|
+
});
|
|
457
|
+
const pricingCategoriesQuery = usePricingCategories({
|
|
458
|
+
active: true,
|
|
459
|
+
limit: 200,
|
|
460
|
+
enabled: enabled && Boolean(product.productId),
|
|
461
|
+
});
|
|
462
|
+
const optionUnitPriceRulesQuery = useOptionUnitPriceRules({
|
|
463
|
+
optionId: product.optionId ?? selectedSlot?.optionId ?? undefined,
|
|
464
|
+
active: true,
|
|
465
|
+
limit: 200,
|
|
466
|
+
enabled: enabled && Boolean(product.productId),
|
|
467
|
+
});
|
|
365
468
|
const handleRoomUnitsChange = React.useCallback((units) => {
|
|
366
469
|
setRoomUnits((prev) => (sameRoomUnits(prev, units) ? prev : units));
|
|
367
470
|
}, []);
|
|
471
|
+
const pricingRoomUnits = React.useMemo(() => pricingSnapshotRoomUnits(pricingPreview.data?.data), [pricingPreview.data]);
|
|
472
|
+
const hasRoomPricingMatrix = pricingRoomUnits.length > 0;
|
|
473
|
+
const bookingUnits = React.useMemo(() => mergePricingRoomMetadata(roomUnits, pricingRoomUnits), [roomUnits, pricingRoomUnits]);
|
|
368
474
|
// Room choices presented to the traveler row are inventory options
|
|
369
475
|
// (e.g. "Standard double", "Junior suite upgrade"). The age-band
|
|
370
476
|
// lives separately on the traveler as a pricing unit.
|
|
371
477
|
const roomUnitOptions = React.useMemo(() => {
|
|
372
|
-
const sourceUnits =
|
|
373
|
-
const
|
|
374
|
-
for (const unit of sourceUnits) {
|
|
375
|
-
const key = unit.optionId ?? unit.optionUnitId;
|
|
376
|
-
quantityByOption.set(key, (quantityByOption.get(key) ?? 0) + (rooms.quantities[unit.optionUnitId] ?? 0));
|
|
377
|
-
}
|
|
378
|
-
const units = sourceUnits.filter((unit) => unit.unitType === "room" || unit.unitType === "vehicle");
|
|
478
|
+
const sourceUnits = bookingUnits.length > 0 ? bookingUnits : (slotUnitAvailability.data?.data ?? []);
|
|
479
|
+
const units = sourceUnits.filter(isBookingInventoryUnit);
|
|
379
480
|
if (units.length === 0)
|
|
380
481
|
return [];
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
existing.units.push(unit);
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
optionGroups.set(key, {
|
|
390
|
-
primaryUnitId: unit.optionUnitId,
|
|
391
|
-
// Strip the trailing " - Adult" / " - Child" suffix the
|
|
392
|
-
// upstream stepper appends when an option has multiple units.
|
|
393
|
-
optionName: stripUnitSuffix(unit.unitName),
|
|
394
|
-
units: [unit],
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return Array.from(optionGroups.values())
|
|
399
|
-
.filter((group) => {
|
|
400
|
-
const optionKey = group.units[0]?.optionId ?? group.primaryUnitId;
|
|
401
|
-
const totalQty = quantityByOption.get(optionKey) ?? 0;
|
|
402
|
-
return totalQty > 0;
|
|
403
|
-
})
|
|
404
|
-
.map((group) => {
|
|
405
|
-
const optionKey = group.units[0]?.optionId ?? group.primaryUnitId;
|
|
406
|
-
const totalQty = quantityByOption.get(optionKey) ?? 0;
|
|
407
|
-
const occupancyMax = Math.max(1, ...group.units.map((u) => u.occupancyMax ?? 1));
|
|
482
|
+
return units
|
|
483
|
+
.filter((unit) => (rooms.quantities[unit.optionUnitId] ?? 0) > 0)
|
|
484
|
+
.map((unit) => {
|
|
485
|
+
const totalQty = rooms.quantities[unit.optionUnitId] ?? 0;
|
|
486
|
+
const occupancyMax = Math.max(1, unit.occupancyMax ?? 1);
|
|
408
487
|
const seats = totalQty * occupancyMax;
|
|
409
|
-
const
|
|
410
|
-
const assigned = travelers.travelers.filter((traveler) => traveler.inventoryUnitId && optionUnitIds.has(traveler.inventoryUnitId)).length;
|
|
488
|
+
const assigned = travelers.travelers.filter((traveler) => traveler.inventoryUnitId === unit.optionUnitId).length;
|
|
411
489
|
return {
|
|
412
|
-
unitId:
|
|
413
|
-
unitName:
|
|
490
|
+
unitId: unit.optionUnitId,
|
|
491
|
+
unitName: stripOptionPrefix(unit.unitName),
|
|
414
492
|
remainingCapacity: Math.max(0, seats - assigned),
|
|
415
493
|
};
|
|
416
494
|
});
|
|
417
|
-
}, [
|
|
495
|
+
}, [bookingUnits, slotUnitAvailability.data, rooms.quantities, travelers.travelers]);
|
|
418
496
|
// Per-option breakdown of all configured units, with the
|
|
419
497
|
// attributes the TravelersSection's dynamic category buttons need
|
|
420
498
|
// (unitCode/min-max/unitType). Mirrors the grouping logic in
|
|
421
499
|
// `roomUnitOptions` but exposes every unit instead of collapsing
|
|
422
500
|
// to one primary.
|
|
423
501
|
const roomGroups = React.useMemo(() => {
|
|
424
|
-
if (
|
|
502
|
+
if (bookingUnits.length === 0)
|
|
425
503
|
return [];
|
|
426
504
|
const groups = new Map();
|
|
427
|
-
for (const
|
|
505
|
+
for (const rawUnit of bookingUnits) {
|
|
506
|
+
const u = normalizeBookingUnit(rawUnit);
|
|
428
507
|
if (!u.optionId)
|
|
429
508
|
continue;
|
|
430
509
|
const groupKey = u.optionId;
|
|
431
|
-
const isInventory = u
|
|
510
|
+
const isInventory = isBookingInventoryUnit(u);
|
|
432
511
|
const isAdultCoded = (u.unitCode ?? "").toUpperCase() === "ADULT";
|
|
433
512
|
const unit = {
|
|
434
513
|
unitId: u.optionUnitId,
|
|
@@ -461,7 +540,82 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
461
540
|
}
|
|
462
541
|
}
|
|
463
542
|
return Array.from(groups.values());
|
|
464
|
-
}, [
|
|
543
|
+
}, [bookingUnits]);
|
|
544
|
+
const travelerPricingCategories = React.useMemo(() => {
|
|
545
|
+
const snapshot = pricingPreview.data?.data;
|
|
546
|
+
const categoriesById = new Map();
|
|
547
|
+
const bookingUnitIds = new Set(bookingUnits.map((unit) => unit.optionUnitId));
|
|
548
|
+
for (const category of pricingCategoriesQuery.data?.data ?? []) {
|
|
549
|
+
categoriesById.set(category.id, category);
|
|
550
|
+
}
|
|
551
|
+
for (const category of snapshot?.pricingCategories ?? []) {
|
|
552
|
+
categoriesById.set(category.id, category);
|
|
553
|
+
}
|
|
554
|
+
const unitIdsByCategoryId = new Map();
|
|
555
|
+
for (const unitPrice of snapshot?.unitPrices ?? []) {
|
|
556
|
+
if (!unitPrice.pricingCategoryId)
|
|
557
|
+
continue;
|
|
558
|
+
if (bookingUnitIds.size > 0 && !bookingUnitIds.has(unitPrice.unitId))
|
|
559
|
+
continue;
|
|
560
|
+
const existing = unitIdsByCategoryId.get(unitPrice.pricingCategoryId) ?? new Set();
|
|
561
|
+
existing.add(unitPrice.unitId);
|
|
562
|
+
unitIdsByCategoryId.set(unitPrice.pricingCategoryId, existing);
|
|
563
|
+
}
|
|
564
|
+
for (const unitPriceRule of optionUnitPriceRulesQuery.data?.data ?? []) {
|
|
565
|
+
if (!unitPriceRule.pricingCategoryId)
|
|
566
|
+
continue;
|
|
567
|
+
if (bookingUnitIds.size > 0 && !bookingUnitIds.has(unitPriceRule.unitId))
|
|
568
|
+
continue;
|
|
569
|
+
const existing = unitIdsByCategoryId.get(unitPriceRule.pricingCategoryId) ?? new Set();
|
|
570
|
+
existing.add(unitPriceRule.unitId);
|
|
571
|
+
unitIdsByCategoryId.set(unitPriceRule.pricingCategoryId, existing);
|
|
572
|
+
}
|
|
573
|
+
return Array.from(unitIdsByCategoryId.entries())
|
|
574
|
+
.flatMap(([categoryId, unitIds]) => {
|
|
575
|
+
const category = categoriesById.get(categoryId);
|
|
576
|
+
if (!category)
|
|
577
|
+
return [];
|
|
578
|
+
return [
|
|
579
|
+
{
|
|
580
|
+
categoryId,
|
|
581
|
+
name: category.name,
|
|
582
|
+
code: category.code,
|
|
583
|
+
categoryType: category.categoryType,
|
|
584
|
+
minAge: category.minAge,
|
|
585
|
+
maxAge: category.maxAge,
|
|
586
|
+
unitIds: Array.from(unitIds),
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
})
|
|
590
|
+
.sort((left, right) => {
|
|
591
|
+
const leftSort = categoriesById.get(left.categoryId)?.sortOrder ?? 0;
|
|
592
|
+
const rightSort = categoriesById.get(right.categoryId)?.sortOrder ?? 0;
|
|
593
|
+
return leftSort - rightSort || left.name.localeCompare(right.name);
|
|
594
|
+
});
|
|
595
|
+
}, [
|
|
596
|
+
pricingPreview.data,
|
|
597
|
+
pricingCategoriesQuery.data?.data,
|
|
598
|
+
optionUnitPriceRulesQuery.data?.data,
|
|
599
|
+
bookingUnits,
|
|
600
|
+
]);
|
|
601
|
+
const travelerPricingCategoryLabels = React.useMemo(() => Object.fromEntries(travelerPricingCategories.map((category) => [category.categoryId, category.name])), [travelerPricingCategories]);
|
|
602
|
+
const travelerPricingCategoryQuantities = React.useMemo(() => {
|
|
603
|
+
const quantities = {};
|
|
604
|
+
if (travelerPricingCategories.length === 0)
|
|
605
|
+
return quantities;
|
|
606
|
+
for (const traveler of travelers.travelers) {
|
|
607
|
+
if (!traveler.inventoryUnitId)
|
|
608
|
+
continue;
|
|
609
|
+
const pricingCategoryId = inferTravelerPricingCategoryId(traveler, travelerPricingCategories);
|
|
610
|
+
if (!pricingCategoryId)
|
|
611
|
+
continue;
|
|
612
|
+
const unitCategoryQuantities = quantities[traveler.inventoryUnitId] ?? {};
|
|
613
|
+
unitCategoryQuantities[pricingCategoryId] =
|
|
614
|
+
(unitCategoryQuantities[pricingCategoryId] ?? 0) + 1;
|
|
615
|
+
quantities[traveler.inventoryUnitId] = unitCategoryQuantities;
|
|
616
|
+
}
|
|
617
|
+
return quantities;
|
|
618
|
+
}, [travelers.travelers, travelerPricingCategories]);
|
|
465
619
|
// Apply the same draft resolver we use at submit so live pricing
|
|
466
620
|
// and persisted item lines cannot drift. Person-priced options
|
|
467
621
|
// (excursions) derive line quantities from the traveler list;
|
|
@@ -469,8 +623,8 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
469
623
|
const displayDraft = React.useMemo(() => resolveBookingDraft({
|
|
470
624
|
quantities: rooms.quantities,
|
|
471
625
|
travelers: travelers.travelers,
|
|
472
|
-
units:
|
|
473
|
-
}), [rooms.quantities, travelers.travelers,
|
|
626
|
+
units: bookingUnits,
|
|
627
|
+
}), [rooms.quantities, travelers.travelers, bookingUnits]);
|
|
474
628
|
const displayQuantities = displayDraft.quantities;
|
|
475
629
|
const displayExtraLines = React.useMemo(() => resolveBookingExtraLines({
|
|
476
630
|
extraLines,
|
|
@@ -493,7 +647,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
493
647
|
const productSellCurrency = bookingProductQuery.data?.sellCurrency ?? null;
|
|
494
648
|
const pricingCurrency = productSellCurrency ?? pricing?.currency ?? currency;
|
|
495
649
|
const pricingTotalAmountCents = pricing?.confirmedAmountCents ?? undefined;
|
|
496
|
-
const roomUnitLabels = React.useMemo(() => Object.fromEntries(
|
|
650
|
+
const roomUnitLabels = React.useMemo(() => Object.fromEntries(bookingUnits.map((unit) => [unit.optionUnitId, unit.unitName])), [bookingUnits]);
|
|
497
651
|
const createBookingMutation = useBookingCreateMutation();
|
|
498
652
|
const queryClient = useQueryClient();
|
|
499
653
|
// Resolve the billing person/org once at the dialog level so we can
|
|
@@ -575,6 +729,15 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
575
729
|
setError(messages.bookingCreateDialog.validation.firstAndLastNameRequired);
|
|
576
730
|
return;
|
|
577
731
|
}
|
|
732
|
+
const overCapacity = getOverCapacityInventoryAssignments(bookingUnits, rooms.quantities, travelers.travelers)[0];
|
|
733
|
+
if (overCapacity) {
|
|
734
|
+
setError(formatMessage(messages.bookingCreateDialog.validation.roomCapacityExceeded, {
|
|
735
|
+
room: overCapacity.unitName,
|
|
736
|
+
assigned: overCapacity.assignedTravelers,
|
|
737
|
+
capacity: overCapacity.capacity,
|
|
738
|
+
}));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
578
741
|
try {
|
|
579
742
|
if (sharedRoom.enabled && sharedRoom.mode === "join" && !sharedRoom.groupId) {
|
|
580
743
|
setError(messages.bookingCreateDialog.validation.selectSharedRoomGroup);
|
|
@@ -595,8 +758,8 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
595
758
|
// "3 x Adult"); accommodation options keep operator-picked
|
|
596
759
|
// room quantities. Server gets `clientLineKey` + `travelerKeys`
|
|
597
760
|
// on each line so it can write `booking_item_travelers` rows.
|
|
598
|
-
const submitUnits =
|
|
599
|
-
?
|
|
761
|
+
const submitUnits = bookingUnits.length > 0
|
|
762
|
+
? bookingUnits
|
|
600
763
|
: getTravelerAssignableStepperUnits((slotUnitAvailability.data?.data ?? []).map((unit) => ({
|
|
601
764
|
...unit,
|
|
602
765
|
optionId: product.optionId,
|
|
@@ -614,10 +777,31 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
614
777
|
.filter((key) => Boolean(key))
|
|
615
778
|
: [],
|
|
616
779
|
]));
|
|
780
|
+
const travelerIndexesByUnitAndCategoryId = {};
|
|
781
|
+
const travelerKeysByUnitAndCategoryId = {};
|
|
782
|
+
for (const [unitId, indexes] of Object.entries(redistributed.travelerIndexesByUnitId)) {
|
|
783
|
+
for (const index of indexes) {
|
|
784
|
+
const traveler = redistributed.travelers[index];
|
|
785
|
+
if (!traveler)
|
|
786
|
+
continue;
|
|
787
|
+
const pricingCategoryId = inferTravelerPricingCategoryId(traveler, travelerPricingCategories);
|
|
788
|
+
if (!pricingCategoryId)
|
|
789
|
+
continue;
|
|
790
|
+
travelerIndexesByUnitAndCategoryId[unitId] ??= {};
|
|
791
|
+
travelerIndexesByUnitAndCategoryId[unitId][pricingCategoryId] ??= [];
|
|
792
|
+
travelerIndexesByUnitAndCategoryId[unitId][pricingCategoryId].push(index);
|
|
793
|
+
const key = traveler.clientTravelerKey;
|
|
794
|
+
if (key) {
|
|
795
|
+
travelerKeysByUnitAndCategoryId[unitId] ??= {};
|
|
796
|
+
travelerKeysByUnitAndCategoryId[unitId][pricingCategoryId] ??= [];
|
|
797
|
+
travelerKeysByUnitAndCategoryId[unitId][pricingCategoryId].push(key);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
617
801
|
const travelerKeys = redistributed.travelers
|
|
618
802
|
.map((traveler) => traveler.clientTravelerKey)
|
|
619
803
|
.filter((key) => Boolean(key));
|
|
620
|
-
const itemLines = itemLinesToRows(redistributed.quantities, submitUnits, pricing, redistributed.travelerIndexesByUnitId, travelerKeysByUnitId);
|
|
804
|
+
const itemLines = itemLinesToRows(redistributed.quantities, submitUnits, pricing, redistributed.travelerIndexesByUnitId, travelerKeysByUnitId, travelerIndexesByUnitAndCategoryId, travelerKeysByUnitAndCategoryId);
|
|
621
805
|
const resolvedExtraLines = resolveBookingExtraLines({
|
|
622
806
|
extraLines,
|
|
623
807
|
travelerCount: travelers.travelers.length,
|
|
@@ -785,7 +969,9 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
785
969
|
} })) : null, product.productId && slotId ? (_jsx(TravelersSection, { value: travelers, onChange: (next) => {
|
|
786
970
|
setPayloadMismatchUnitIds([]);
|
|
787
971
|
setTravelers(next);
|
|
788
|
-
}, roomUnits: roomUnitOptions.length > 0 ? roomUnitOptions : undefined, roomGroups: roomGroups.length > 0 ? roomGroups : undefined,
|
|
972
|
+
}, roomUnits: roomUnitOptions.length > 0 ? roomUnitOptions : undefined, roomGroups: roomGroups.length > 0 ? roomGroups : undefined, pricingCategories: hasRoomPricingMatrix || roomUnitOptions.length > 0
|
|
973
|
+
? travelerPricingCategories
|
|
974
|
+
: undefined, billingPersonId: (person.billTo ?? "person") === "person" ? person.personId : null, labels: {
|
|
789
975
|
heading: messages.bookingCreateDialog.labels.travelerHeading,
|
|
790
976
|
addTraveler: messages.bookingCreateDialog.labels.addTraveler,
|
|
791
977
|
person: messages.bookingCreateDialog.labels.travelerPerson,
|
|
@@ -808,7 +994,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
808
994
|
: messages.bookingCreateDialog.actions.createAwaitingPaymentBooking] })] })] }), _jsxs("div", { className: "flex flex-col gap-4 lg:col-span-4", children: [_jsx(BookingPreviewCard, { productId: product.productId, optionId: product.optionId, slotId: slotId, slotLabel: (() => {
|
|
809
995
|
const slot = slots.find((s) => s.id === slotId);
|
|
810
996
|
return slot ? formatSlotLabel(slot) : null;
|
|
811
|
-
})(), unitQuantities: displayQuantities, unitLabels: roomUnitLabels, extraLines: displayExtraLines, travelers: travelers.travelers, messages: messages, onPricingChange: setPricing }), product.productId && slotId ? (_jsx(VoucherPickerSection, { value: voucher, onChange: setVoucher, currency: currency, labels: {
|
|
997
|
+
})(), unitQuantities: displayQuantities, unitLabels: roomUnitLabels, pricingCategoryQuantities: travelerPricingCategoryQuantities, pricingCategoryLabels: travelerPricingCategoryLabels, extraLines: displayExtraLines, travelers: travelers.travelers, messages: messages, onPricingChange: setPricing }), product.productId && slotId ? (_jsx(VoucherPickerSection, { value: voucher, onChange: setVoucher, currency: currency, labels: {
|
|
812
998
|
heading: messages.bookingCreateDialog.labels.voucherHeading,
|
|
813
999
|
codePlaceholder: messages.bookingCreateDialog.labels.voucherCodePlaceholder,
|
|
814
1000
|
apply: messages.bookingCreateDialog.labels.voucherApply,
|
|
@@ -918,7 +1104,7 @@ function QuantityButtons({ value, max, onChange, }) {
|
|
|
918
1104
|
* confirmed price — so the operator gets a "what am I about to book"
|
|
919
1105
|
* summary without scrolling back through the form.
|
|
920
1106
|
*/
|
|
921
|
-
function BookingPreviewCard({ productId, optionId, slotId, slotLabel, unitQuantities, unitLabels, extraLines, travelers, messages, onPricingChange, }) {
|
|
1107
|
+
function BookingPreviewCard({ productId, optionId, slotId, slotLabel, unitQuantities, unitLabels, pricingCategoryQuantities, pricingCategoryLabels, extraLines, travelers, messages, onPricingChange, }) {
|
|
922
1108
|
const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
|
|
923
1109
|
const productQuery = useProduct(productId || undefined, { enabled: Boolean(productId) });
|
|
924
1110
|
const mediaQuery = useProductMedia(productId, { limit: 1, enabled: Boolean(productId) });
|
|
@@ -979,7 +1165,7 @@ function BookingPreviewCard({ productId, optionId, slotId, slotLabel, unitQuanti
|
|
|
979
1165
|
.join(" ")
|
|
980
1166
|
.trim();
|
|
981
1167
|
return (_jsxs("li", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "truncate text-muted-foreground", children: name || previewMessages.travelerUnnamed }), _jsx("span", { className: "shrink-0 text-xs uppercase tracking-wider text-muted-foreground", children: traveler.role })] }, traveler.personId ?? `traveler-${idx}`));
|
|
982
|
-
}) })] })) : null, showPriceBreakdown ? (_jsxs("div", { className: "border-t pt-3", children: [_jsx(PriceBreakdownSection, { flat: true, productId: productId, optionId: optionId, unitQuantities: unitQuantities, unitLabels: unitLabels, labels: {
|
|
1168
|
+
}) })] })) : null, showPriceBreakdown ? (_jsxs("div", { className: "border-t pt-3", children: [_jsx(PriceBreakdownSection, { flat: true, productId: productId, optionId: optionId, unitQuantities: unitQuantities, unitLabels: unitLabels, pricingCategoryQuantities: pricingCategoryQuantities, pricingCategoryLabels: pricingCategoryLabels, labels: {
|
|
983
1169
|
heading: labels.breakdownHeading,
|
|
984
1170
|
total: labels.breakdownTotal,
|
|
985
1171
|
onRequest: labels.breakdownOnRequest,
|
|
@@ -14,9 +14,11 @@ export interface BookingCreateUnitLineRecord {
|
|
|
14
14
|
}
|
|
15
15
|
export interface BookingCreatePricingLineRecord {
|
|
16
16
|
unitId: string;
|
|
17
|
+
pricingCategoryId?: string | null;
|
|
17
18
|
label: string;
|
|
18
19
|
unitAmountCents: number | null;
|
|
19
20
|
totalAmountCents: number | null;
|
|
21
|
+
quantity?: number;
|
|
20
22
|
}
|
|
21
23
|
export interface BookingCreatePricingRecord {
|
|
22
24
|
confirmedAmountCents: number | null;
|
|
@@ -28,6 +30,21 @@ export interface BookingCreateTravelerAssignableUnitRecord {
|
|
|
28
30
|
optionUnitId: string;
|
|
29
31
|
unitType?: BookingCreateStepperUnitType;
|
|
30
32
|
}
|
|
33
|
+
export interface BookingCreateCapacityUnitRecord {
|
|
34
|
+
optionUnitId: string;
|
|
35
|
+
unitName: string;
|
|
36
|
+
unitType?: BookingCreateStepperUnitType;
|
|
37
|
+
occupancyMax?: number | null;
|
|
38
|
+
}
|
|
39
|
+
export interface BookingCreateCapacityTravelerRecord {
|
|
40
|
+
inventoryUnitId?: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface BookingCreateOverCapacityAssignment {
|
|
43
|
+
optionUnitId: string;
|
|
44
|
+
unitName: string;
|
|
45
|
+
assignedTravelers: number;
|
|
46
|
+
capacity: number;
|
|
47
|
+
}
|
|
31
48
|
export declare function normalizeBookingSearchText(value: string): string;
|
|
32
49
|
export declare function matchesBookingSearchText(value: string | null | undefined, query: string): boolean;
|
|
33
50
|
export declare function productMatchesPickerSearch(product: ProductPickerSearchRecord | null | undefined, query: string): boolean;
|
|
@@ -38,11 +55,12 @@ export declare function validateBillingPersonContact(contact: {
|
|
|
38
55
|
phone?: string | null;
|
|
39
56
|
} | null | undefined): BillingPersonContactValidationResult;
|
|
40
57
|
export declare function getTravelerAssignableStepperUnits<TUnit extends BookingCreateTravelerAssignableUnitRecord>(units: readonly TUnit[]): TUnit[];
|
|
58
|
+
export declare function getOverCapacityInventoryAssignments(units: readonly BookingCreateCapacityUnitRecord[], quantities: Record<string, number>, travelers: readonly BookingCreateCapacityTravelerRecord[]): BookingCreateOverCapacityAssignment[];
|
|
41
59
|
export declare function getBookableDepartureSlots<TSlot extends DepartureSlotSearchRecord>(slots: readonly TSlot[], options: {
|
|
42
60
|
nowIso: string;
|
|
43
61
|
optionId: string | null;
|
|
44
62
|
}): TSlot[];
|
|
45
|
-
export declare function itemLinesToRows(quantities: Record<string, number>, units: BookingCreateUnitLineRecord[], pricing: BookingCreatePricingRecord | null, travelerIndexesByUnitId?: Record<string, number[]>, travelerKeysByUnitId?: Record<string, string[]
|
|
63
|
+
export declare function itemLinesToRows(quantities: Record<string, number>, units: BookingCreateUnitLineRecord[], pricing: BookingCreatePricingRecord | null, travelerIndexesByUnitId?: Record<string, number[]>, travelerKeysByUnitId?: Record<string, string[]>, travelerIndexesByUnitAndCategoryId?: Record<string, Record<string, number[]>>, travelerKeysByUnitAndCategoryId?: Record<string, Record<string, string[]>>): BookingCreateItemLineInput[];
|
|
46
64
|
export declare function getSelectedSharedRoomUnitId(quantities: Record<string, number>): string | null;
|
|
47
65
|
export {};
|
|
48
66
|
//# sourceMappingURL=booking-create-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE7D,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,MAAM,GAAG,cAAc,CAAC,CAAA;AAEpG,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -54,6 +54,31 @@ export function getTravelerAssignableStepperUnits(units) {
|
|
|
54
54
|
return !hasInventoryByOption.get(unit.optionId ?? unit.optionUnitId);
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
export function getOverCapacityInventoryAssignments(units, quantities, travelers) {
|
|
58
|
+
const inventoryUnits = units.filter((unit) => unit.unitType === "room" || unit.unitType === "vehicle");
|
|
59
|
+
const assignedByUnitId = new Map();
|
|
60
|
+
for (const traveler of travelers) {
|
|
61
|
+
if (!traveler.inventoryUnitId)
|
|
62
|
+
continue;
|
|
63
|
+
assignedByUnitId.set(traveler.inventoryUnitId, (assignedByUnitId.get(traveler.inventoryUnitId) ?? 0) + 1);
|
|
64
|
+
}
|
|
65
|
+
return inventoryUnits.flatMap((unit) => {
|
|
66
|
+
const quantity = Math.max(0, quantities[unit.optionUnitId] ?? 0);
|
|
67
|
+
const occupancy = Math.max(1, unit.occupancyMax ?? 1);
|
|
68
|
+
const capacity = quantity * occupancy;
|
|
69
|
+
const assignedTravelers = assignedByUnitId.get(unit.optionUnitId) ?? 0;
|
|
70
|
+
if (assignedTravelers <= capacity)
|
|
71
|
+
return [];
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
optionUnitId: unit.optionUnitId,
|
|
75
|
+
unitName: unit.unitName,
|
|
76
|
+
assignedTravelers,
|
|
77
|
+
capacity,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
57
82
|
export function getBookableDepartureSlots(slots, options) {
|
|
58
83
|
return slots
|
|
59
84
|
.filter((slot) => !slot.status || slot.status === "open")
|
|
@@ -65,10 +90,20 @@ export function getBookableDepartureSlots(slots, options) {
|
|
|
65
90
|
})
|
|
66
91
|
.sort((left, right) => left.startsAt.localeCompare(right.startsAt));
|
|
67
92
|
}
|
|
68
|
-
export function itemLinesToRows(quantities, units, pricing, travelerIndexesByUnitId = {}, travelerKeysByUnitId = {}) {
|
|
93
|
+
export function itemLinesToRows(quantities, units, pricing, travelerIndexesByUnitId = {}, travelerKeysByUnitId = {}, travelerIndexesByUnitAndCategoryId = {}, travelerKeysByUnitAndCategoryId = {}) {
|
|
69
94
|
const unitsById = new Map(units.map((unit) => [unit.optionUnitId, unit]));
|
|
70
95
|
const unitNames = new Map(units.map((unit) => [unit.optionUnitId, unit.unitName]));
|
|
71
|
-
const pricedLines = new Map((pricing?.lines ?? [])
|
|
96
|
+
const pricedLines = new Map((pricing?.lines ?? [])
|
|
97
|
+
.filter((line) => !line.pricingCategoryId)
|
|
98
|
+
.map((line) => [line.unitId, line]));
|
|
99
|
+
const categoryPricedLinesByUnitId = new Map();
|
|
100
|
+
for (const line of pricing?.lines ?? []) {
|
|
101
|
+
if (!line.pricingCategoryId)
|
|
102
|
+
continue;
|
|
103
|
+
const existing = categoryPricedLinesByUnitId.get(line.unitId) ?? [];
|
|
104
|
+
existing.push(line);
|
|
105
|
+
categoryPricedLinesByUnitId.set(line.unitId, existing);
|
|
106
|
+
}
|
|
72
107
|
const selectedLines = Object.entries(quantities).filter(([, quantity]) => quantity > 0);
|
|
73
108
|
const pricedTotal = selectedLines.reduce((sum, [optionUnitId]) => {
|
|
74
109
|
const total = pricedLines.get(optionUnitId)?.totalAmountCents;
|
|
@@ -80,7 +115,38 @@ export function itemLinesToRows(quantities, units, pricing, travelerIndexesByUni
|
|
|
80
115
|
? Math.max(0, pricing.confirmedAmountCents - pricedTotal)
|
|
81
116
|
: null;
|
|
82
117
|
let allocatedManualTotal = 0;
|
|
83
|
-
return selectedLines.
|
|
118
|
+
return selectedLines.flatMap(([optionUnitId, quantity]) => {
|
|
119
|
+
const categoryPricedLines = categoryPricedLinesByUnitId.get(optionUnitId) ?? [];
|
|
120
|
+
if (categoryPricedLines.length > 0) {
|
|
121
|
+
return categoryPricedLines.map((pricedLine) => {
|
|
122
|
+
const pricingCategoryId = pricedLine.pricingCategoryId;
|
|
123
|
+
const categoryQuantity = Math.max(1, pricedLine.quantity ?? 1);
|
|
124
|
+
const travelerIndexes = pricingCategoryId
|
|
125
|
+
? travelerIndexesByUnitAndCategoryId[optionUnitId]?.[pricingCategoryId]
|
|
126
|
+
: undefined;
|
|
127
|
+
const travelerKeys = pricingCategoryId
|
|
128
|
+
? travelerKeysByUnitAndCategoryId[optionUnitId]?.[pricingCategoryId]
|
|
129
|
+
: undefined;
|
|
130
|
+
const hasTravelerLinks = Boolean(travelerKeys?.length || travelerIndexes?.length);
|
|
131
|
+
return {
|
|
132
|
+
clientLineKey: hasTravelerLinks
|
|
133
|
+
? `unit:${optionUnitId}:category:${pricingCategoryId ?? "default"}`
|
|
134
|
+
: undefined,
|
|
135
|
+
optionId: unitsById.get(optionUnitId)?.optionId ?? null,
|
|
136
|
+
optionUnitId,
|
|
137
|
+
pricingCategoryId,
|
|
138
|
+
quantity: categoryQuantity,
|
|
139
|
+
title: pricedLine.label ?? unitNames.get(optionUnitId) ?? null,
|
|
140
|
+
unitSellAmountCents: pricedLine.unitAmountCents,
|
|
141
|
+
totalSellAmountCents: pricedLine.totalAmountCents,
|
|
142
|
+
...(travelerKeys?.length
|
|
143
|
+
? { travelerKeys }
|
|
144
|
+
: travelerIndexes?.length
|
|
145
|
+
? { travelerIndexes }
|
|
146
|
+
: {}),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
84
150
|
const pricedLine = pricedLines.get(optionUnitId);
|
|
85
151
|
let totalSellAmountCents = pricedLine?.totalAmountCents ?? null;
|
|
86
152
|
if (totalSellAmountCents == null && manualRemainder != null && unpricedQuantity > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"option-units-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/option-units-stepper-section.tsx"],"names":[],"mappings":"AAgBA,iEAAiE;AACjE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,4BAA4B,EAAE,uBAA4C,CAAA;AAEvF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAAA;IAC/E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,CAAA;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAClD,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,IAAI,CAAA;IACzD,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAc,EACd,aAAa,EACb,MAAM,EACN,qBAA6B,EAC7B,oBAAyB,GAC1B,EAAE,8BAA8B,
|
|
1
|
+
{"version":3,"file":"option-units-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/option-units-stepper-section.tsx"],"names":[],"mappings":"AAgBA,iEAAiE;AACjE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,4BAA4B,EAAE,uBAA4C,CAAA;AAEvF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAAA;IAC/E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,CAAA;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAClD,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,IAAI,CAAA;IACzD,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAc,EACd,aAAa,EACb,MAAM,EACN,qBAA6B,EAC7B,oBAAyB,GAC1B,EAAE,8BAA8B,2CA2PhC;AAED,wBAAgB,2BAA2B,CAAC,EAC1C,cAAc,EACd,KAAK,EACL,qBAAqB,EACrB,SAAS,EACT,SAAS,EACT,iBAAiB,GAClB,EAAE;IACD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,sBAAsB,EAAE,UAAU,CAAC,CAAC,CAAA;IAC9D,qBAAqB,EAAE,OAAO,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,GAAG,MAAM,CAMT;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAC,EAClE,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAAC,WAG1C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,aAAa,CAAC;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EACjD,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3C,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAC9B,MAAM,GAAG,IAAI,CAMf;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAC/C,WAAW,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAClD,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,EAAE,OAAO,GACf,sBAAsB,EAAE,CAM1B"}
|