@voyantjs/bookings-ui 0.80.18 → 0.81.5
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-dialog.d.ts +0 -18
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +126 -221
- package/dist/components/booking-create-utils.d.ts +1 -1
- package/dist/components/booking-create-utils.d.ts.map +1 -1
- package/dist/components/booking-create-utils.js +26 -8
- package/dist/components/booking-detail-page.d.ts.map +1 -1
- package/dist/components/booking-detail-page.js +3 -1
- package/dist/components/booking-payments-summary.d.ts +4 -1
- package/dist/components/booking-payments-summary.d.ts.map +1 -1
- package/dist/components/booking-payments-summary.js +21 -4
- package/dist/components/option-units-stepper-section.d.ts +4 -1
- package/dist/components/option-units-stepper-section.d.ts.map +1 -1
- package/dist/components/option-units-stepper-section.js +7 -2
- package/dist/components/traveler-category-buttons.d.ts +1 -1
- package/dist/components/traveler-category-buttons.d.ts.map +1 -1
- package/dist/components/traveler-category-buttons.js +3 -3
- package/dist/components/travelers-section.d.ts +12 -7
- package/dist/components/travelers-section.d.ts.map +1 -1
- package/dist/components/travelers-section.js +148 -139
- package/dist/i18n/en.d.ts +5 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +5 -0
- package/dist/i18n/messages.d.ts +5 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +10 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +5 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +32 -30
|
@@ -1,22 +1,4 @@
|
|
|
1
1
|
import { type BookingRecord } from "@voyantjs/bookings-react";
|
|
2
|
-
import { type OptionUnitsStepperUnit } from "./option-units-stepper-section.js";
|
|
3
|
-
/**
|
|
4
|
-
* Pick the unit for a traveler. Priorities:
|
|
5
|
-
* 1. If we have an age (from DOB) and it falls into a unit's
|
|
6
|
-
* `[minAge, maxAge]` window, use that unit.
|
|
7
|
-
* 2. Otherwise honor an explicit role hint (Child / Infant / Adult
|
|
8
|
-
* buttons) by mapping the hint to a representative age and
|
|
9
|
-
* matching the age band. This works for products whose units
|
|
10
|
-
* encode the band in the code (`child_0_5`, `child_6_12`) instead
|
|
11
|
-
* of bare `CHILD`/`INFANT`.
|
|
12
|
-
* 3. Fall back to code/name matching for legacy products that don't
|
|
13
|
-
* configure min/max ages.
|
|
14
|
-
*
|
|
15
|
-
* `roleHint` covers the common case where the operator knows the
|
|
16
|
-
* traveler is a child but doesn't have the exact DOB. Without it, a
|
|
17
|
-
* roleless traveler would silently default to Adult pricing.
|
|
18
|
-
*/
|
|
19
|
-
export declare function pickUnitForAge(units: OptionUnitsStepperUnit[], age: number | null, roleHint?: "adult" | "child" | "infant" | null): OptionUnitsStepperUnit | undefined;
|
|
20
2
|
export interface BookingCreateDialogProps {
|
|
21
3
|
open: boolean;
|
|
22
4
|
onOpenChange: (open: boolean) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAeA,OAAO,EAKL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AAkQjC,MAAM,WAAW,wBAAwB;IACvC,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,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,GACd,EAAE,wBAAwB,2CAsB1B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CA89BxB"}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useQueries, useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { getSlotQueryOptions, useSlots, useSlotUnitAvailability, useVoyantAvailabilityContext, } from "@voyantjs/availability-react";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveBookingDraft, resolveBookingExtraLines, travelersToRows, } from "@voyantjs/bookings/pricing-assignment";
|
|
6
|
+
import { useBookingCreateMutation, useBookingTaxPreview, VoyantApiError, } from "@voyantjs/bookings-react";
|
|
6
7
|
import { useOrganization, usePerson } from "@voyantjs/crm-react";
|
|
7
8
|
import { useProductExtras } from "@voyantjs/extras-react";
|
|
8
9
|
import { useAddresses } from "@voyantjs/identity-react";
|
|
@@ -20,7 +21,7 @@ import { emptyPersonPickerValue, PersonPickerSection, } from "./person-picker-se
|
|
|
20
21
|
import { PriceBreakdownSection } from "./price-breakdown-section.js";
|
|
21
22
|
import { ProductPickerSection } from "./product-picker-section.js";
|
|
22
23
|
import { emptySharedRoomValue, SharedRoomSection, } from "./shared-room-section.js";
|
|
23
|
-
import {
|
|
24
|
+
import { emptyTravelerListValue, TravelersSection, } from "./travelers-section.js";
|
|
24
25
|
import { emptyVoucherPickerValue, VoucherPickerSection, } from "./voucher-picker-section.js";
|
|
25
26
|
function generateBookingNumber() {
|
|
26
27
|
const now = new Date();
|
|
@@ -117,186 +118,6 @@ function stripOptionPrefix(name) {
|
|
|
117
118
|
const idx = name.indexOf(" - ");
|
|
118
119
|
return idx > 0 ? name.slice(idx + 3) : name;
|
|
119
120
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Pick the unit for a traveler. Priorities:
|
|
122
|
-
* 1. If we have an age (from DOB) and it falls into a unit's
|
|
123
|
-
* `[minAge, maxAge]` window, use that unit.
|
|
124
|
-
* 2. Otherwise honor an explicit role hint (Child / Infant / Adult
|
|
125
|
-
* buttons) by mapping the hint to a representative age and
|
|
126
|
-
* matching the age band. This works for products whose units
|
|
127
|
-
* encode the band in the code (`child_0_5`, `child_6_12`) instead
|
|
128
|
-
* of bare `CHILD`/`INFANT`.
|
|
129
|
-
* 3. Fall back to code/name matching for legacy products that don't
|
|
130
|
-
* configure min/max ages.
|
|
131
|
-
*
|
|
132
|
-
* `roleHint` covers the common case where the operator knows the
|
|
133
|
-
* traveler is a child but doesn't have the exact DOB. Without it, a
|
|
134
|
-
* roleless traveler would silently default to Adult pricing.
|
|
135
|
-
*/
|
|
136
|
-
export function pickUnitForAge(units, age, roleHint = null) {
|
|
137
|
-
if (units.length === 0)
|
|
138
|
-
return undefined;
|
|
139
|
-
const personUnits = units.filter((u) => u.unitType == null || u.unitType === "person");
|
|
140
|
-
const pool = personUnits.length > 0 ? personUnits : units;
|
|
141
|
-
const sorted = [...pool].sort((a, b) => (a.minAge ?? 0) - (b.minAge ?? 0));
|
|
142
|
-
const matchByAge = (target) => sorted.find((u) => (u.minAge == null || target >= u.minAge) && (u.maxAge == null || target <= u.maxAge));
|
|
143
|
-
if (age != null) {
|
|
144
|
-
const match = matchByAge(age);
|
|
145
|
-
if (match)
|
|
146
|
-
return match;
|
|
147
|
-
}
|
|
148
|
-
if (roleHint) {
|
|
149
|
-
const HINT_AGE = { adult: 30, child: 8, infant: 1 };
|
|
150
|
-
const hintAge = HINT_AGE[roleHint];
|
|
151
|
-
// Only consider units with at least one explicit age bound. Without
|
|
152
|
-
// this, legacy units with null min/max (just bare ADULT/CHILD codes)
|
|
153
|
-
// would all match every hint age and collapse onto the first sorted
|
|
154
|
-
// entry (almost always Adult). Code-matching below handles those.
|
|
155
|
-
const banded = sorted.filter((u) => u.minAge != null || u.maxAge != null);
|
|
156
|
-
const match = banded.find((u) => (u.minAge == null || hintAge >= u.minAge) && (u.maxAge == null || hintAge <= u.maxAge));
|
|
157
|
-
if (match)
|
|
158
|
-
return match;
|
|
159
|
-
}
|
|
160
|
-
const findByCode = (code) => sorted.find((u) => (u.unitCode ?? "").toUpperCase() === code) ??
|
|
161
|
-
sorted.find((u) => new RegExp(`\\b${code}\\b`, "i").test(u.unitName));
|
|
162
|
-
if (roleHint === "child")
|
|
163
|
-
return findByCode("CHILD") ?? sorted[0];
|
|
164
|
-
if (roleHint === "infant")
|
|
165
|
-
return findByCode("INFANT") ?? sorted[0];
|
|
166
|
-
return findByCode("ADULT") ?? sorted[sorted.length - 1] ?? sorted[0];
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Take the operator-picked per-option quantities (which are tracked
|
|
170
|
-
* against each option's primary "Adult" unit by the stepper) plus the
|
|
171
|
-
* travelers list, and redistribute both so that:
|
|
172
|
-
* - each traveler's `roomUnitId` points at the age-banded unit
|
|
173
|
-
* matching their DOB (Adult / Child / Infant / etc.)
|
|
174
|
-
* - `quantities` reflects the per-unit counts after redistribution —
|
|
175
|
-
* a 3-pax "Standard double" with 2 adults + 1 child becomes
|
|
176
|
-
* `{ adultUnit: 2, childUnit: 1 }` instead of `{ adultUnit: 3 }`.
|
|
177
|
-
*
|
|
178
|
-
* Slots without a configured `dateOfBirth` keep the option's adult
|
|
179
|
-
* default so partially-filled bookings still typecheck.
|
|
180
|
-
*/
|
|
181
|
-
/**
|
|
182
|
-
* Rebuild stepper quantities from per-traveler unit assignments.
|
|
183
|
-
*
|
|
184
|
-
* Each traveler's `roomUnitId` is now the operator's explicit choice
|
|
185
|
-
* (DOB-pre-picked at attach, overridable via the dynamic category
|
|
186
|
-
* buttons), so we count assignments directly and add any per-option
|
|
187
|
-
* residual on the adult/primary unit when the stepper qty exceeds the
|
|
188
|
-
* number of travelers actually assigned. Unlike the older
|
|
189
|
-
* DOB-driven rewrite, this never moves a traveler off their chosen
|
|
190
|
-
* unit — operator selection always wins.
|
|
191
|
-
*/
|
|
192
|
-
function redistributeByAge(quantities, travelers, units) {
|
|
193
|
-
if (units.length === 0)
|
|
194
|
-
return { quantities, travelers };
|
|
195
|
-
const unitsByOption = new Map();
|
|
196
|
-
for (const unit of units) {
|
|
197
|
-
if (!unit.optionId)
|
|
198
|
-
continue;
|
|
199
|
-
const list = unitsByOption.get(unit.optionId);
|
|
200
|
-
if (list)
|
|
201
|
-
list.push(unit);
|
|
202
|
-
else
|
|
203
|
-
unitsByOption.set(unit.optionId, [unit]);
|
|
204
|
-
}
|
|
205
|
-
const unitToOption = new Map(units.map((u) => [u.optionUnitId, u.optionId]));
|
|
206
|
-
const unitById = new Map(units.map((u) => [u.optionUnitId, u]));
|
|
207
|
-
// Per-option total from the stepper. This is the count the operator
|
|
208
|
-
// committed to when picking rooms.
|
|
209
|
-
const totalByOption = new Map();
|
|
210
|
-
for (const [unitId, qty] of Object.entries(quantities)) {
|
|
211
|
-
if (qty <= 0)
|
|
212
|
-
continue;
|
|
213
|
-
const optionId = unitToOption.get(unitId);
|
|
214
|
-
if (!optionId)
|
|
215
|
-
continue;
|
|
216
|
-
totalByOption.set(optionId, (totalByOption.get(optionId) ?? 0) + qty);
|
|
217
|
-
}
|
|
218
|
-
const assignedForDefaulting = new Map();
|
|
219
|
-
for (const traveler of travelers) {
|
|
220
|
-
if (!traveler.roomUnitId)
|
|
221
|
-
continue;
|
|
222
|
-
const optionId = unitToOption.get(traveler.roomUnitId);
|
|
223
|
-
if (!optionId)
|
|
224
|
-
continue;
|
|
225
|
-
assignedForDefaulting.set(optionId, (assignedForDefaulting.get(optionId) ?? 0) + 1);
|
|
226
|
-
}
|
|
227
|
-
const optionDemand = Array.from(totalByOption.entries());
|
|
228
|
-
const nextTravelers = travelers.map((traveler) => {
|
|
229
|
-
if (traveler.roomUnitId && unitById.has(traveler.roomUnitId))
|
|
230
|
-
return traveler;
|
|
231
|
-
const optionId = optionDemand.find(([candidate, total]) => (assignedForDefaulting.get(candidate) ?? 0) < total)?.[0] ?? optionDemand[0]?.[0];
|
|
232
|
-
if (!optionId)
|
|
233
|
-
return traveler;
|
|
234
|
-
const age = computeAgeYears(traveler.dateOfBirth);
|
|
235
|
-
const roleHint = traveler.role === "adult" || traveler.role === "child" || traveler.role === "infant"
|
|
236
|
-
? traveler.role
|
|
237
|
-
: null;
|
|
238
|
-
const unit = pickUnitForAge(unitsByOption.get(optionId) ?? [], age, roleHint);
|
|
239
|
-
if (!unit)
|
|
240
|
-
return traveler;
|
|
241
|
-
assignedForDefaulting.set(optionId, (assignedForDefaulting.get(optionId) ?? 0) + 1);
|
|
242
|
-
return { ...traveler, roomUnitId: unit.optionUnitId };
|
|
243
|
-
});
|
|
244
|
-
// Count actual traveler assignments per unit + per option.
|
|
245
|
-
const next = {};
|
|
246
|
-
const assignedByOption = new Map();
|
|
247
|
-
for (const t of nextTravelers) {
|
|
248
|
-
if (!t.roomUnitId)
|
|
249
|
-
continue;
|
|
250
|
-
const optionId = unitToOption.get(t.roomUnitId);
|
|
251
|
-
if (!optionId)
|
|
252
|
-
continue;
|
|
253
|
-
next[t.roomUnitId] = (next[t.roomUnitId] ?? 0) + 1;
|
|
254
|
-
assignedByOption.set(optionId, (assignedByOption.get(optionId) ?? 0) + 1);
|
|
255
|
-
}
|
|
256
|
-
// Residual = operator picked N rooms but only added M travelers; put
|
|
257
|
-
// the leftover on the option's adult/primary unit so the price total
|
|
258
|
-
// matches the stepper.
|
|
259
|
-
for (const [optionId, total] of totalByOption) {
|
|
260
|
-
const assigned = assignedByOption.get(optionId) ?? 0;
|
|
261
|
-
const residual = Math.max(0, total - assigned);
|
|
262
|
-
if (residual === 0)
|
|
263
|
-
continue;
|
|
264
|
-
const adult = pickUnitForAge(unitsByOption.get(optionId) ?? [], null);
|
|
265
|
-
if (!adult)
|
|
266
|
-
continue;
|
|
267
|
-
next[adult.optionUnitId] = (next[adult.optionUnitId] ?? 0) + residual;
|
|
268
|
-
}
|
|
269
|
-
return { quantities: next, travelers: nextTravelers };
|
|
270
|
-
}
|
|
271
|
-
function travelersToRows(value) {
|
|
272
|
-
return value.travelers.map((traveler) => {
|
|
273
|
-
// Age-derived category (DOB-driven). The `role` field still
|
|
274
|
-
// carries the `lead` flag separately for the booking primary; the
|
|
275
|
-
// demographic category comes from age, not from a manual select.
|
|
276
|
-
const age = computeAgeYears(traveler.dateOfBirth);
|
|
277
|
-
const ageCategory = age == null
|
|
278
|
-
? traveler.role === "child" || traveler.role === "infant" || traveler.role === "adult"
|
|
279
|
-
? traveler.role
|
|
280
|
-
: null
|
|
281
|
-
: age < 2
|
|
282
|
-
? "infant"
|
|
283
|
-
: age < 18
|
|
284
|
-
? "child"
|
|
285
|
-
: "adult";
|
|
286
|
-
return {
|
|
287
|
-
personId: traveler.personId,
|
|
288
|
-
firstName: traveler.firstName.trim(),
|
|
289
|
-
lastName: traveler.lastName.trim(),
|
|
290
|
-
email: traveler.email.trim() || null,
|
|
291
|
-
phone: traveler.phone.trim() || null,
|
|
292
|
-
preferredLanguage: traveler.preferredLanguage.trim() || null,
|
|
293
|
-
participantType: "traveler",
|
|
294
|
-
travelerCategory: ageCategory,
|
|
295
|
-
isPrimary: traveler.role === "lead",
|
|
296
|
-
roomUnitId: traveler.roomUnitId,
|
|
297
|
-
};
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
121
|
function sameRoomUnits(left, right) {
|
|
301
122
|
if (left.length !== right.length)
|
|
302
123
|
return false;
|
|
@@ -310,6 +131,37 @@ function sameRoomUnits(left, right) {
|
|
|
310
131
|
unit.remaining === other.remaining);
|
|
311
132
|
});
|
|
312
133
|
}
|
|
134
|
+
function isPayloadResolverMismatchBody(body) {
|
|
135
|
+
if (typeof body !== "object" || body === null)
|
|
136
|
+
return false;
|
|
137
|
+
const candidate = body;
|
|
138
|
+
return (candidate.code === "payload_resolver_mismatch" &&
|
|
139
|
+
Array.isArray(candidate.mismatches) &&
|
|
140
|
+
candidate.mismatches.every((mismatch) => {
|
|
141
|
+
if (typeof mismatch !== "object" || mismatch === null)
|
|
142
|
+
return false;
|
|
143
|
+
const item = mismatch;
|
|
144
|
+
return ((item.kind === "qty" || item.kind === "missing" || item.kind === "extra") &&
|
|
145
|
+
typeof item.optionUnitId === "string" &&
|
|
146
|
+
typeof item.submittedQuantity === "number" &&
|
|
147
|
+
typeof item.resolvedQuantity === "number");
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
function formatPayloadResolverMismatchError(body, unitLabels, validationMessages) {
|
|
151
|
+
const details = body.mismatches
|
|
152
|
+
.map((mismatch) => {
|
|
153
|
+
const label = unitLabels[mismatch.optionUnitId] ?? mismatch.optionUnitId;
|
|
154
|
+
return formatMessage(validationMessages.payloadResolverMismatchLine, {
|
|
155
|
+
label,
|
|
156
|
+
resolvedQuantity: mismatch.resolvedQuantity,
|
|
157
|
+
submittedQuantity: mismatch.submittedQuantity,
|
|
158
|
+
});
|
|
159
|
+
})
|
|
160
|
+
.join("; ");
|
|
161
|
+
return details
|
|
162
|
+
? formatMessage(validationMessages.payloadResolverMismatchDetails, { details })
|
|
163
|
+
: validationMessages.payloadResolverMismatchFallback;
|
|
164
|
+
}
|
|
313
165
|
/**
|
|
314
166
|
* Operator booking-create dialog. Composes the booking-create picker
|
|
315
167
|
* sections — product, departure, rooms, person, shared-room, travelers,
|
|
@@ -361,6 +213,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
361
213
|
// bundle. Defaults on so the operator opts out, not in.
|
|
362
214
|
const [notifyTraveler, setNotifyTraveler] = React.useState(true);
|
|
363
215
|
const [error, setError] = React.useState(null);
|
|
216
|
+
const [payloadMismatchUnitIds, setPayloadMismatchUnitIds] = React.useState([]);
|
|
364
217
|
const { formatDate } = useBookingsUiI18nOrDefault();
|
|
365
218
|
const messages = useBookingsUiMessagesOrDefault();
|
|
366
219
|
const availabilityClient = useVoyantAvailabilityContext();
|
|
@@ -392,6 +245,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
392
245
|
setCreateAsDraft(false);
|
|
393
246
|
setNotifyTraveler(true);
|
|
394
247
|
setError(null);
|
|
248
|
+
setPayloadMismatchUnitIds([]);
|
|
395
249
|
}
|
|
396
250
|
else if (resolvedDefaultProductId) {
|
|
397
251
|
setProduct((prev) => {
|
|
@@ -415,6 +269,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
415
269
|
setRoomUnits([]);
|
|
416
270
|
setSharedRoom(emptySharedRoomValue);
|
|
417
271
|
setExtraLines([]);
|
|
272
|
+
setPayloadMismatchUnitIds([]);
|
|
418
273
|
}, [product.productId, defaultSlotId]);
|
|
419
274
|
const [slotsFromIso, setSlotsFromIso] = React.useState(() => new Date().toISOString());
|
|
420
275
|
React.useEffect(() => {
|
|
@@ -443,6 +298,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
443
298
|
}, [slotsData?.data, slotsFromIso, product.optionId, allOpenSlots]);
|
|
444
299
|
const selectedSlot = React.useMemo(() => slots.find((slot) => slot.id === slotId) ?? (defaultSlot?.id === slotId ? defaultSlot : null), [slots, slotId, defaultSlot]);
|
|
445
300
|
const setSelectedSlot = React.useCallback((nextSlotId) => {
|
|
301
|
+
setPayloadMismatchUnitIds([]);
|
|
446
302
|
const selectedSlot = nextSlotId ? allOpenSlots.find((slot) => slot.id === nextSlotId) : null;
|
|
447
303
|
if (selectedSlot?.optionId && selectedSlot.optionId !== product.optionId) {
|
|
448
304
|
setProduct((prev) => ({ ...prev, optionId: selectedSlot.optionId }));
|
|
@@ -452,6 +308,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
452
308
|
React.useEffect(() => {
|
|
453
309
|
setRooms(emptyOptionUnitsStepperValue);
|
|
454
310
|
setRoomUnits([]);
|
|
311
|
+
setPayloadMismatchUnitIds([]);
|
|
455
312
|
if (!slotId || !product.optionId)
|
|
456
313
|
return;
|
|
457
314
|
const selectedDeparture = allOpenSlots.find((slot) => slot.id === slotId) ??
|
|
@@ -482,29 +339,25 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
482
339
|
const handleRoomUnitsChange = React.useCallback((units) => {
|
|
483
340
|
setRoomUnits((prev) => (sameRoomUnits(prev, units) ? prev : units));
|
|
484
341
|
}, []);
|
|
485
|
-
// Room choices presented to the traveler row are
|
|
486
|
-
// "Standard double", "Junior suite upgrade")
|
|
487
|
-
//
|
|
488
|
-
// traveler and only affects pricing; both an adult and a child sit
|
|
489
|
-
// in the same Standard double room. Each entry's `unitId` is set to
|
|
490
|
-
// the option's primary unit so existing `roomUnitId`-keyed plumbing
|
|
491
|
-
// (assignment, redistribution) keeps working — `redistributeByAge`
|
|
492
|
-
// moves the traveler to the matching age-banded unit at submit.
|
|
342
|
+
// Room choices presented to the traveler row are inventory options
|
|
343
|
+
// (e.g. "Standard double", "Junior suite upgrade"). The age-band
|
|
344
|
+
// lives separately on the traveler as a pricing unit.
|
|
493
345
|
const roomUnitOptions = React.useMemo(() => {
|
|
494
|
-
const
|
|
346
|
+
const sourceUnits = roomUnits.length > 0 ? roomUnits : (slotUnitAvailability.data?.data ?? []);
|
|
347
|
+
const quantityByOption = new Map();
|
|
348
|
+
for (const unit of sourceUnits) {
|
|
349
|
+
const key = unit.optionId ?? unit.optionUnitId;
|
|
350
|
+
quantityByOption.set(key, (quantityByOption.get(key) ?? 0) + (rooms.quantities[unit.optionUnitId] ?? 0));
|
|
351
|
+
}
|
|
352
|
+
const units = sourceUnits.filter((unit) => unit.unitType === "room" || unit.unitType === "vehicle");
|
|
495
353
|
if (units.length === 0)
|
|
496
354
|
return [];
|
|
497
355
|
const optionGroups = new Map();
|
|
498
356
|
for (const unit of units) {
|
|
499
357
|
const key = unit.optionId ?? unit.optionUnitId;
|
|
500
|
-
// Prefer an ADULT-coded primary; the stepper routes per-option
|
|
501
|
-
// qty through the same unit so seat math stays consistent.
|
|
502
|
-
const isAdult = (unit.unitCode ?? "").toUpperCase() === "ADULT";
|
|
503
358
|
const existing = optionGroups.get(key);
|
|
504
359
|
if (existing) {
|
|
505
360
|
existing.units.push(unit);
|
|
506
|
-
if (isAdult)
|
|
507
|
-
existing.primaryUnitId = unit.optionUnitId;
|
|
508
361
|
}
|
|
509
362
|
else {
|
|
510
363
|
optionGroups.set(key, {
|
|
@@ -518,15 +371,17 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
518
371
|
}
|
|
519
372
|
return Array.from(optionGroups.values())
|
|
520
373
|
.filter((group) => {
|
|
521
|
-
const
|
|
374
|
+
const optionKey = group.units[0]?.optionId ?? group.primaryUnitId;
|
|
375
|
+
const totalQty = quantityByOption.get(optionKey) ?? 0;
|
|
522
376
|
return totalQty > 0;
|
|
523
377
|
})
|
|
524
378
|
.map((group) => {
|
|
525
|
-
const
|
|
379
|
+
const optionKey = group.units[0]?.optionId ?? group.primaryUnitId;
|
|
380
|
+
const totalQty = quantityByOption.get(optionKey) ?? 0;
|
|
526
381
|
const occupancyMax = Math.max(1, ...group.units.map((u) => u.occupancyMax ?? 1));
|
|
527
382
|
const seats = totalQty * occupancyMax;
|
|
528
383
|
const optionUnitIds = new Set(group.units.map((u) => u.optionUnitId));
|
|
529
|
-
const assigned = travelers.travelers.filter((traveler) => traveler.
|
|
384
|
+
const assigned = travelers.travelers.filter((traveler) => traveler.inventoryUnitId && optionUnitIds.has(traveler.inventoryUnitId)).length;
|
|
530
385
|
return {
|
|
531
386
|
unitId: group.primaryUnitId,
|
|
532
387
|
unitName: group.optionName,
|
|
@@ -540,14 +395,14 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
540
395
|
// `roomUnitOptions` but exposes every unit instead of collapsing
|
|
541
396
|
// to one primary.
|
|
542
397
|
const roomGroups = React.useMemo(() => {
|
|
543
|
-
|
|
544
|
-
if (travelerUnits.length === 0)
|
|
398
|
+
if (roomUnits.length === 0)
|
|
545
399
|
return [];
|
|
546
400
|
const groups = new Map();
|
|
547
|
-
for (const u of
|
|
401
|
+
for (const u of roomUnits) {
|
|
548
402
|
if (!u.optionId)
|
|
549
403
|
continue;
|
|
550
404
|
const groupKey = u.optionId;
|
|
405
|
+
const isInventory = u.unitType === "room" || u.unitType === "vehicle";
|
|
551
406
|
const isAdultCoded = (u.unitCode ?? "").toUpperCase() === "ADULT";
|
|
552
407
|
const unit = {
|
|
553
408
|
unitId: u.optionUnitId,
|
|
@@ -563,8 +418,12 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
563
418
|
const existing = groups.get(groupKey);
|
|
564
419
|
if (existing) {
|
|
565
420
|
existing.units.push(unit);
|
|
566
|
-
if (
|
|
421
|
+
if (isInventory)
|
|
567
422
|
existing.primaryUnitId = u.optionUnitId;
|
|
423
|
+
else if (isAdultCoded &&
|
|
424
|
+
!existing.units.some((candidate) => candidate.unitType === "room" || candidate.unitType === "vehicle")) {
|
|
425
|
+
existing.primaryUnitId = u.optionUnitId;
|
|
426
|
+
}
|
|
568
427
|
}
|
|
569
428
|
else {
|
|
570
429
|
groups.set(groupKey, {
|
|
@@ -577,12 +436,20 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
577
436
|
}
|
|
578
437
|
return Array.from(groups.values());
|
|
579
438
|
}, [roomUnits]);
|
|
580
|
-
// Apply the same
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
439
|
+
// Apply the same draft resolver we use at submit so live pricing
|
|
440
|
+
// and persisted item lines cannot drift. Person-priced options
|
|
441
|
+
// (excursions) derive line quantities from the traveler list;
|
|
442
|
+
// accommodation options preserve operator-picked stepper quantities.
|
|
443
|
+
const displayDraft = React.useMemo(() => resolveBookingDraft({
|
|
444
|
+
quantities: rooms.quantities,
|
|
445
|
+
travelers: travelers.travelers,
|
|
446
|
+
units: roomUnits,
|
|
447
|
+
}), [rooms.quantities, travelers.travelers, roomUnits]);
|
|
448
|
+
const displayQuantities = displayDraft.quantities;
|
|
449
|
+
const displayExtraLines = React.useMemo(() => resolveBookingExtraLines({
|
|
450
|
+
extraLines,
|
|
451
|
+
travelerCount: travelers.travelers.length,
|
|
452
|
+
}), [extraLines, travelers.travelers.length]);
|
|
586
453
|
// Currency placeholder — used for voucher + payment schedule display.
|
|
587
454
|
// Consumers hooking in real product data should override this by wrapping
|
|
588
455
|
// the component or swapping in their own currency-aware hook.
|
|
@@ -622,6 +489,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
622
489
|
const hasSelectedUnits = React.useMemo(() => Object.values(rooms.quantities).some((qty) => qty > 0), [rooms.quantities]);
|
|
623
490
|
const handleSubmit = async () => {
|
|
624
491
|
setError(null);
|
|
492
|
+
setPayloadMismatchUnitIds([]);
|
|
625
493
|
if (!product.productId) {
|
|
626
494
|
setError(messages.bookingCreateDialog.validation.selectProduct);
|
|
627
495
|
return;
|
|
@@ -683,17 +551,40 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
683
551
|
return;
|
|
684
552
|
}
|
|
685
553
|
const paymentSchedules = paymentScheduleToRows(paymentSchedule, pricingCurrency, confirmedSellAmountCents);
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
554
|
+
// Resolve the draft once, then derive every shape the wire
|
|
555
|
+
// format needs from the result. Person-priced options get
|
|
556
|
+
// per-band quantities (1 adult + 1 child + 1 infant, not
|
|
557
|
+
// "3 x Adult"); accommodation options keep operator-picked
|
|
558
|
+
// room quantities. Server gets `clientLineKey` + `travelerKeys`
|
|
559
|
+
// on each line so it can write `booking_item_travelers` rows.
|
|
689
560
|
const submitUnits = roomUnits.length > 0
|
|
690
|
-
?
|
|
561
|
+
? roomUnits
|
|
691
562
|
: getTravelerAssignableStepperUnits((slotUnitAvailability.data?.data ?? []).map((unit) => ({
|
|
692
563
|
...unit,
|
|
693
564
|
optionId: product.optionId,
|
|
694
565
|
})));
|
|
695
|
-
const redistributed =
|
|
696
|
-
|
|
566
|
+
const redistributed = resolveBookingDraft({
|
|
567
|
+
quantities: rooms.quantities,
|
|
568
|
+
travelers: travelers.travelers,
|
|
569
|
+
units: submitUnits,
|
|
570
|
+
});
|
|
571
|
+
const travelerKeysByUnitId = Object.fromEntries(Object.entries(redistributed.travelerIndexesByUnitId).map(([unitId, indexes]) => [
|
|
572
|
+
unitId,
|
|
573
|
+
indexes.every((index) => Boolean(redistributed.travelers[index]?.clientTravelerKey))
|
|
574
|
+
? indexes
|
|
575
|
+
.map((index) => redistributed.travelers[index]?.clientTravelerKey)
|
|
576
|
+
.filter((key) => Boolean(key))
|
|
577
|
+
: [],
|
|
578
|
+
]));
|
|
579
|
+
const travelerKeys = redistributed.travelers
|
|
580
|
+
.map((traveler) => traveler.clientTravelerKey)
|
|
581
|
+
.filter((key) => Boolean(key));
|
|
582
|
+
const itemLines = itemLinesToRows(redistributed.quantities, submitUnits, pricing, redistributed.travelerIndexesByUnitId, travelerKeysByUnitId);
|
|
583
|
+
const resolvedExtraLines = resolveBookingExtraLines({
|
|
584
|
+
extraLines,
|
|
585
|
+
travelerCount: travelers.travelers.length,
|
|
586
|
+
travelerKeys: travelerKeys.length === redistributed.travelers.length ? travelerKeys : undefined,
|
|
587
|
+
});
|
|
697
588
|
const travelerRows = travelersToRows({ travelers: redistributed.travelers });
|
|
698
589
|
const voucherRedemption = voucher.picked && voucher.picked.remainingAmountCents != null
|
|
699
590
|
? {
|
|
@@ -773,7 +664,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
773
664
|
confirmedSellAmountCents,
|
|
774
665
|
priceOverrideReason: priceOverrideReason || null,
|
|
775
666
|
itemLines: itemLines.length > 0 ? itemLines : undefined,
|
|
776
|
-
extraLines:
|
|
667
|
+
extraLines: resolvedExtraLines.length > 0 ? resolvedExtraLines : undefined,
|
|
777
668
|
travelers: travelerRows.length > 0 ? travelerRows : undefined,
|
|
778
669
|
paymentSchedules: paymentSchedules.length > 0 ? paymentSchedules : undefined,
|
|
779
670
|
voucherRedemption,
|
|
@@ -792,15 +683,26 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
792
683
|
onCreated?.(booking);
|
|
793
684
|
}
|
|
794
685
|
catch (err) {
|
|
686
|
+
if (err instanceof VoyantApiError && isPayloadResolverMismatchBody(err.body)) {
|
|
687
|
+
setPayloadMismatchUnitIds(Array.from(new Set(err.body.mismatches.map((mismatch) => mismatch.optionUnitId))));
|
|
688
|
+
setError(formatPayloadResolverMismatchError(err.body, roomUnitLabels, messages.bookingCreateDialog.validation));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
795
691
|
setError(err instanceof Error ? err.message : messages.bookingCreateDialog.validation.createFailed);
|
|
796
692
|
}
|
|
797
693
|
};
|
|
798
694
|
const isSubmitting = createBookingMutation.isPending;
|
|
799
|
-
return (_jsxs("div", { className: "grid min-h-0 flex-1 gap-6 lg:grid-cols-12", children: [_jsxs("div", { className: "flex min-h-0 min-w-0 flex-col lg:col-span-8", children: [_jsxs("div", { className: "flex flex-1 flex-col gap-4 overflow-y-auto px-1 pb-2", children: [_jsx(ProductPickerSection, { value: product, onChange:
|
|
695
|
+
return (_jsxs("div", { className: "grid min-h-0 flex-1 gap-6 lg:grid-cols-12", children: [_jsxs("div", { className: "flex min-h-0 min-w-0 flex-col lg:col-span-8", children: [_jsxs("div", { className: "flex flex-1 flex-col gap-4 overflow-y-auto px-1 pb-2", children: [_jsx(ProductPickerSection, { value: product, onChange: (next) => {
|
|
696
|
+
setPayloadMismatchUnitIds([]);
|
|
697
|
+
setProduct(next);
|
|
698
|
+
}, enabled: enabled, lockProduct: Boolean(defaultProductId || defaultSlotId), labels: {
|
|
800
699
|
optionNone: messages.bookingCreateDialog.labels.noSpecificOption,
|
|
801
|
-
}, showOptionPicker: false }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsx(AsyncCombobox, { value: slotId, onChange: (v) => setSelectedSlot(v), items: slots, selectedItem: selectedSlot, getKey: (slot) => slot.id, getLabel: (slot) => formatSlotLabel(slot), placeholder: messages.bookingCreateDialog.placeholders.departure, emptyText: messages.bookingCreateDialog.placeholders.departureEmpty, triggerClassName: "w-full", disabled: Boolean(defaultSlotId), clearable: !defaultSlotId })] })) : null, product.productId && slotId ? (_jsx(OptionUnitsStepperSection, { value: rooms, onChange:
|
|
700
|
+
}, showOptionPicker: false }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsx(AsyncCombobox, { value: slotId, onChange: (v) => setSelectedSlot(v), items: slots, selectedItem: selectedSlot, getKey: (slot) => slot.id, getLabel: (slot) => formatSlotLabel(slot), placeholder: messages.bookingCreateDialog.placeholders.departure, emptyText: messages.bookingCreateDialog.placeholders.departureEmpty, triggerClassName: "w-full", disabled: Boolean(defaultSlotId), clearable: !defaultSlotId })] })) : null, product.productId && slotId ? (_jsx(OptionUnitsStepperSection, { value: rooms, onChange: (next) => {
|
|
701
|
+
setPayloadMismatchUnitIds([]);
|
|
702
|
+
setRooms(next);
|
|
703
|
+
}, productId: product.productId, slotId: slotId, optionId: product.optionId, enabled: enabled, onUnitsChange: handleRoomUnitsChange, slotHasFiniteCapacity: Boolean(selectedSlot) &&
|
|
802
704
|
!selectedSlot?.unlimited &&
|
|
803
|
-
typeof selectedSlot?.remainingPax === "number", labels: {
|
|
705
|
+
typeof selectedSlot?.remainingPax === "number", invalidOptionUnitIds: payloadMismatchUnitIds, labels: {
|
|
804
706
|
heading: messages.bookingCreateDialog.labels.roomsHeading,
|
|
805
707
|
noOption: messages.bookingCreateDialog.labels.roomsNoOption,
|
|
806
708
|
noSlot: messages.bookingCreateDialog.labels.roomsNoSlot,
|
|
@@ -825,7 +727,10 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
825
727
|
noGroups: messages.bookingCreateDialog.labels.sharedRoomNoGroups,
|
|
826
728
|
createHint: messages.bookingCreateDialog.labels.sharedRoomCreateHint,
|
|
827
729
|
remove: messages.bookingCreateDialog.labels.sharedRoomRemove,
|
|
828
|
-
} })) : null, product.productId && slotId ? (_jsx(TravelersSection, { value: travelers, onChange:
|
|
730
|
+
} })) : null, product.productId && slotId ? (_jsx(TravelersSection, { value: travelers, onChange: (next) => {
|
|
731
|
+
setPayloadMismatchUnitIds([]);
|
|
732
|
+
setTravelers(next);
|
|
733
|
+
}, roomUnits: roomUnitOptions.length > 0 ? roomUnitOptions : undefined, roomGroups: roomGroups.length > 0 ? roomGroups : undefined, billingPersonId: (person.billTo ?? "person") === "person" ? person.personId : null, labels: {
|
|
829
734
|
heading: messages.bookingCreateDialog.labels.travelerHeading,
|
|
830
735
|
addTraveler: messages.bookingCreateDialog.labels.addTraveler,
|
|
831
736
|
person: messages.bookingCreateDialog.labels.travelerPerson,
|
|
@@ -858,7 +763,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
|
|
|
858
763
|
: 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: (() => {
|
|
859
764
|
const slot = slots.find((s) => s.id === slotId);
|
|
860
765
|
return slot ? formatSlotLabel(slot) : null;
|
|
861
|
-
})(), unitQuantities: displayQuantities, unitLabels: roomUnitLabels, extraLines:
|
|
766
|
+
})(), 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: {
|
|
862
767
|
heading: messages.bookingCreateDialog.labels.voucherHeading,
|
|
863
768
|
codePlaceholder: messages.bookingCreateDialog.labels.voucherCodePlaceholder,
|
|
864
769
|
apply: messages.bookingCreateDialog.labels.voucherApply,
|
|
@@ -42,7 +42,7 @@ export declare function getBookableDepartureSlots<TSlot extends DepartureSlotSea
|
|
|
42
42
|
nowIso: string;
|
|
43
43
|
optionId: string | null;
|
|
44
44
|
}): TSlot[];
|
|
45
|
-
export declare function itemLinesToRows(quantities: Record<string, number>, units: BookingCreateUnitLineRecord[], pricing: BookingCreatePricingRecord | null): BookingCreateItemLineInput[];
|
|
45
|
+
export declare function itemLinesToRows(quantities: Record<string, number>, units: BookingCreateUnitLineRecord[], pricing: BookingCreatePricingRecord | null, travelerIndexesByUnitId?: Record<string, number[]>, travelerKeysByUnitId?: Record<string, string[]>): BookingCreateItemLineInput[];
|
|
46
46
|
export declare function getSelectedSharedRoomUnitId(quantities: Record<string, number>): string | null;
|
|
47
47
|
export {};
|
|
48
48
|
//# 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;CAChC;AAED,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,KAAK,EAAE,8BAA8B,EAAE,CAAA;CACxC;AAED,KAAK,4BAA4B,GAC7B,QAAQ,GACR,OAAO,GACP,MAAM,GACN,SAAS,GACT,SAAS,GACT,OAAO,GACP,IAAI,CAAA;AAER,MAAM,WAAW,yCAAyC;IACxD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,4BAA4B,CAAA;CACxC;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIjG;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,EACrD,KAAK,EAAE,MAAM,GACZ,OAAO,CAKT;AAED,MAAM,MAAM,oCAAoC,GAAG,OAAO,GAAG,iBAAiB,GAAG,eAAe,CAAA;AAEhG,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAI5E;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC3E,oCAAoC,CAOtC;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,SAAS,yCAAyC,EACvD,KAAK,EAAE,SAAS,KAAK,EAAE,GAAG,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -32,20 +32,26 @@ export function validateBillingPersonContact(contact) {
|
|
|
32
32
|
return "valid";
|
|
33
33
|
}
|
|
34
34
|
export function getTravelerAssignableStepperUnits(units) {
|
|
35
|
-
|
|
35
|
+
// "Inventory" units (rooms, vehicles) are containers a traveler is
|
|
36
|
+
// placed into. When an option configures one, person-typed
|
|
37
|
+
// pricing-tier units on the same option are hidden from the
|
|
38
|
+
// stepper since their pricing folds into the container's per-pax
|
|
39
|
+
// accounting at submit time.
|
|
40
|
+
const isInventoryType = (unit) => unit.unitType === "room" || unit.unitType === "vehicle";
|
|
41
|
+
const hasInventoryByOption = new Map();
|
|
36
42
|
for (const unit of units) {
|
|
37
43
|
const optionKey = unit.optionId ?? unit.optionUnitId;
|
|
38
|
-
if (unit
|
|
39
|
-
|
|
40
|
-
else if (!
|
|
41
|
-
|
|
44
|
+
if (isInventoryType(unit))
|
|
45
|
+
hasInventoryByOption.set(optionKey, true);
|
|
46
|
+
else if (!hasInventoryByOption.has(optionKey))
|
|
47
|
+
hasInventoryByOption.set(optionKey, false);
|
|
42
48
|
}
|
|
43
49
|
return units.filter((unit) => {
|
|
44
|
-
if (unit
|
|
50
|
+
if (isInventoryType(unit))
|
|
45
51
|
return true;
|
|
46
52
|
if (unit.unitType !== "person")
|
|
47
53
|
return false;
|
|
48
|
-
return !
|
|
54
|
+
return !hasInventoryByOption.get(unit.optionId ?? unit.optionUnitId);
|
|
49
55
|
});
|
|
50
56
|
}
|
|
51
57
|
export function getBookableDepartureSlots(slots, options) {
|
|
@@ -59,7 +65,7 @@ export function getBookableDepartureSlots(slots, options) {
|
|
|
59
65
|
})
|
|
60
66
|
.sort((left, right) => left.startsAt.localeCompare(right.startsAt));
|
|
61
67
|
}
|
|
62
|
-
export function itemLinesToRows(quantities, units, pricing) {
|
|
68
|
+
export function itemLinesToRows(quantities, units, pricing, travelerIndexesByUnitId = {}, travelerKeysByUnitId = {}) {
|
|
63
69
|
const unitsById = new Map(units.map((unit) => [unit.optionUnitId, unit]));
|
|
64
70
|
const unitNames = new Map(units.map((unit) => [unit.optionUnitId, unit.unitName]));
|
|
65
71
|
const pricedLines = new Map((pricing?.lines ?? []).map((line) => [line.unitId, line]));
|
|
@@ -86,13 +92,25 @@ export function itemLinesToRows(quantities, units, pricing) {
|
|
|
86
92
|
}
|
|
87
93
|
const unitSellAmountCents = pricedLine?.unitAmountCents ??
|
|
88
94
|
(totalSellAmountCents != null ? Math.floor(totalSellAmountCents / quantity) : null);
|
|
95
|
+
const travelerIndexes = travelerIndexesByUnitId[optionUnitId];
|
|
96
|
+
const travelerKeys = travelerKeysByUnitId[optionUnitId];
|
|
97
|
+
const hasTravelerLinks = Boolean(travelerKeys?.length || travelerIndexes?.length);
|
|
89
98
|
return {
|
|
99
|
+
// Server uses `clientLineKey` to look up this item after insert
|
|
100
|
+
// and link it to travelers via `booking_item_travelers`. Only
|
|
101
|
+
// stamp when there's an actual traveler mapping to write.
|
|
102
|
+
clientLineKey: hasTravelerLinks ? `unit:${optionUnitId}` : undefined,
|
|
90
103
|
optionId: unitsById.get(optionUnitId)?.optionId ?? null,
|
|
91
104
|
optionUnitId,
|
|
92
105
|
quantity,
|
|
93
106
|
title: pricedLine?.label ?? unitNames.get(optionUnitId) ?? null,
|
|
94
107
|
unitSellAmountCents,
|
|
95
108
|
totalSellAmountCents,
|
|
109
|
+
...(travelerKeys?.length
|
|
110
|
+
? { travelerKeys }
|
|
111
|
+
: travelerIndexes?.length
|
|
112
|
+
? { travelerIndexes }
|
|
113
|
+
: {}),
|
|
96
114
|
};
|
|
97
115
|
});
|
|
98
116
|
}
|