@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
|
@@ -9,9 +9,16 @@ import * as React from "react";
|
|
|
9
9
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
10
10
|
import { getDynamicTravelerCategoryButtonState, getStaticTravelerCategoryButtonState, } from "./traveler-category-buttons.js";
|
|
11
11
|
export const emptyTravelerListValue = { travelers: [] };
|
|
12
|
+
function createClientTravelerKey() {
|
|
13
|
+
const random = typeof globalThis.crypto?.randomUUID === "function"
|
|
14
|
+
? globalThis.crypto.randomUUID().replace(/-/g, "")
|
|
15
|
+
: `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
|
|
16
|
+
return `trav:${random}`;
|
|
17
|
+
}
|
|
12
18
|
/** Factory for a blank row — `role` defaults to `adult` unless the list is empty. */
|
|
13
19
|
export function createBlankTraveler(role = "adult") {
|
|
14
20
|
return {
|
|
21
|
+
clientTravelerKey: createClientTravelerKey(),
|
|
15
22
|
personId: null,
|
|
16
23
|
firstName: "",
|
|
17
24
|
lastName: "",
|
|
@@ -20,26 +27,17 @@ export function createBlankTraveler(role = "adult") {
|
|
|
20
27
|
preferredLanguage: "",
|
|
21
28
|
role,
|
|
22
29
|
dateOfBirth: null,
|
|
23
|
-
|
|
30
|
+
pricingUnitId: null,
|
|
31
|
+
inventoryUnitId: null,
|
|
32
|
+
pricingUnitSource: "auto",
|
|
33
|
+
inventoryUnitSource: "auto",
|
|
24
34
|
};
|
|
25
35
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!dob)
|
|
32
|
-
return null;
|
|
33
|
-
const birth = new Date(dob);
|
|
34
|
-
if (Number.isNaN(birth.getTime()))
|
|
35
|
-
return null;
|
|
36
|
-
let age = now.getFullYear() - birth.getFullYear();
|
|
37
|
-
const beforeBirthday = now.getMonth() < birth.getMonth() ||
|
|
38
|
-
(now.getMonth() === birth.getMonth() && now.getDate() < birth.getDate());
|
|
39
|
-
if (beforeBirthday)
|
|
40
|
-
age -= 1;
|
|
41
|
-
return age >= 0 ? age : null;
|
|
42
|
-
}
|
|
36
|
+
// Re-export `computeAgeYears` from the canonical assignment module so
|
|
37
|
+
// existing consumers of `travelers-section`'s public surface keep
|
|
38
|
+
// working. The implementation lives in `@voyantjs/bookings/pricing-assignment`.
|
|
39
|
+
export { computeAgeYears } from "@voyantjs/bookings/pricing-assignment";
|
|
40
|
+
import { computeAgeYears as _computeAgeYears, matchUnitByDob as matchAssignmentUnitByDob, matchUnitByRoleHint as matchAssignmentUnitByRoleHint, } from "@voyantjs/bookings/pricing-assignment";
|
|
43
41
|
/**
|
|
44
42
|
* Derive the age-banded traveler role from DOB. Falls back to `adult`
|
|
45
43
|
* when DOB is missing so partial entries still typecheck downstream.
|
|
@@ -50,7 +48,7 @@ export function computeAgeYears(dob, now = new Date()) {
|
|
|
50
48
|
* - adult: 18+
|
|
51
49
|
*/
|
|
52
50
|
export function deriveTravelerRoleFromDob(dob) {
|
|
53
|
-
const age =
|
|
51
|
+
const age = _computeAgeYears(dob);
|
|
54
52
|
if (age == null)
|
|
55
53
|
return "adult";
|
|
56
54
|
if (age < 2)
|
|
@@ -60,53 +58,32 @@ export function deriveTravelerRoleFromDob(dob) {
|
|
|
60
58
|
return "adult";
|
|
61
59
|
}
|
|
62
60
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
61
|
+
* Adapter from this file's `RoomGroupUnit` shape (UI-side, uses
|
|
62
|
+
* `unitId`) to the canonical `PricingAssignmentUnit` shape (uses
|
|
63
|
+
* `optionUnitId`). Phase 1 of voyantjs/voyant#1267 will collapse these
|
|
64
|
+
* by renaming the UI shape.
|
|
67
65
|
*/
|
|
66
|
+
function roomGroupUnitsAsAssignmentUnits(units) {
|
|
67
|
+
return units.map((u) => ({
|
|
68
|
+
optionId: null,
|
|
69
|
+
optionUnitId: u.unitId,
|
|
70
|
+
unitName: u.unitName,
|
|
71
|
+
unitCode: u.unitCode,
|
|
72
|
+
minAge: u.minAge,
|
|
73
|
+
maxAge: u.maxAge,
|
|
74
|
+
unitType: u.unitType,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
68
77
|
function matchUnitByDob(units, dob) {
|
|
69
|
-
|
|
70
|
-
return null;
|
|
71
|
-
const age = computeAgeYears(dob);
|
|
72
|
-
if (age == null)
|
|
73
|
-
return null;
|
|
74
|
-
const personUnits = units.filter((u) => u.unitType == null || u.unitType === "person");
|
|
75
|
-
const match = personUnits.find((u) => (u.minAge == null || age >= u.minAge) && (u.maxAge == null || age <= u.maxAge));
|
|
76
|
-
return match?.unitId ?? null;
|
|
78
|
+
return matchAssignmentUnitByDob(roomGroupUnitsAsAssignmentUnits(units), dob);
|
|
77
79
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Find the unit matching a role hint when DOB is missing. Maps the
|
|
80
|
-
* role to a representative age and matches against `[minAge, maxAge]`.
|
|
81
|
-
* Returns null when the role doesn't carry an age signal (e.g. `lead`).
|
|
82
|
-
*
|
|
83
|
-
* Routes `infant` to whichever band covers ~1y (e.g. `child_0_5`) and
|
|
84
|
-
* `child` to whichever covers ~8y (e.g. `child_6_12`), regardless of
|
|
85
|
-
* how the product codes the unit names.
|
|
86
|
-
*/
|
|
87
80
|
function matchUnitByRoleHint(units, role) {
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
const HINT_AGE = {
|
|
91
|
-
adult: 30,
|
|
92
|
-
child: 8,
|
|
93
|
-
infant: 1,
|
|
94
|
-
};
|
|
95
|
-
const hintAge = HINT_AGE[role];
|
|
96
|
-
if (hintAge == null)
|
|
97
|
-
return null;
|
|
98
|
-
// Only consider units with explicit age bands — units with null
|
|
99
|
-
// min/max would all spuriously match any hint age.
|
|
100
|
-
const banded = units.filter((u) => (u.unitType == null || u.unitType === "person") && (u.minAge != null || u.maxAge != null));
|
|
101
|
-
const match = banded.find((u) => (u.minAge == null || hintAge >= u.minAge) && (u.maxAge == null || hintAge <= u.maxAge));
|
|
102
|
-
return match?.unitId ?? null;
|
|
81
|
+
return matchAssignmentUnitByRoleHint(roomGroupUnitsAsAssignmentUnits(units), role);
|
|
103
82
|
}
|
|
104
83
|
/**
|
|
105
|
-
* The Room dropdown lists one item per option
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* back to the dropdown's primary key so the Select value matches an
|
|
109
|
-
* existing item — otherwise base-ui falls back to rendering the raw id.
|
|
84
|
+
* The Room dropdown lists one item per inventory option. Map any unit
|
|
85
|
+
* from the same option back to that option's inventory key so the
|
|
86
|
+
* Select value matches an existing item.
|
|
110
87
|
*/
|
|
111
88
|
function mapUnitIdToGroupPrimary(unitId, roomGroups) {
|
|
112
89
|
if (!unitId)
|
|
@@ -116,28 +93,6 @@ function mapUnitIdToGroupPrimary(unitId, roomGroups) {
|
|
|
116
93
|
const group = roomGroups.find((g) => g.primaryUnitId === unitId || g.units.some((u) => u.unitId === unitId));
|
|
117
94
|
return group?.primaryUnitId ?? unitId;
|
|
118
95
|
}
|
|
119
|
-
/**
|
|
120
|
-
* When the operator changes the Room dropdown, preserve the traveler's
|
|
121
|
-
* current category code (Adult/Child/Senior/…) in the destination option
|
|
122
|
-
* if it offers a matching unit. Falls back to the destination's primary
|
|
123
|
-
* unit when no match exists or the previous unit has no code.
|
|
124
|
-
*/
|
|
125
|
-
function pickUnitForRoomChange(currentUnitId, nextRoomPrimaryId, roomGroups) {
|
|
126
|
-
if (!roomGroups)
|
|
127
|
-
return nextRoomPrimaryId;
|
|
128
|
-
const nextGroup = roomGroups.find((g) => g.primaryUnitId === nextRoomPrimaryId);
|
|
129
|
-
if (!nextGroup)
|
|
130
|
-
return nextRoomPrimaryId;
|
|
131
|
-
if (!currentUnitId)
|
|
132
|
-
return nextGroup.primaryUnitId;
|
|
133
|
-
const prevGroup = roomGroups.find((g) => g.primaryUnitId === currentUnitId || g.units.some((u) => u.unitId === currentUnitId));
|
|
134
|
-
const prevUnit = prevGroup?.units.find((u) => u.unitId === currentUnitId);
|
|
135
|
-
const prevCode = (prevUnit?.unitCode ?? "").toLowerCase();
|
|
136
|
-
if (!prevCode)
|
|
137
|
-
return nextGroup.primaryUnitId;
|
|
138
|
-
const sameCategory = nextGroup.units.find((u) => (u.unitCode ?? "").toLowerCase() === prevCode);
|
|
139
|
-
return sameCategory?.unitId ?? nextGroup.primaryUnitId;
|
|
140
|
-
}
|
|
141
96
|
const NO_ROOM = "__unassigned__";
|
|
142
97
|
/**
|
|
143
98
|
* Traveler list for booking-create flows. Each row can point at an existing
|
|
@@ -169,55 +124,42 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
169
124
|
const removeAt = (index) => {
|
|
170
125
|
onChange({ travelers: value.travelers.filter((_, i) => i !== index) });
|
|
171
126
|
};
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// manually via the Room select. Picks the first option (ordering
|
|
175
|
-
// mirrors the upstream `roomUnits` array, which comes from the
|
|
176
|
-
// stepper in catalog order). When `roomGroups` is wired, also
|
|
177
|
-
// pre-pick the matching age-banded unit within that option (DOB
|
|
178
|
-
// first, role hint second) so the Category buttons land on the
|
|
179
|
-
// right row and pricing reflects the operator's intent.
|
|
180
|
-
const pickRoomUnitIdForNewTraveler = React.useCallback((dateOfBirth = null, role = null) => {
|
|
181
|
-
if (!roomUnits || roomUnits.length === 0)
|
|
127
|
+
const pickPricingUnitIdForTraveler = React.useCallback((dateOfBirth = null, role = null, preferredUnitId = null) => {
|
|
128
|
+
if (!roomGroups || roomGroups.length === 0)
|
|
182
129
|
return null;
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
130
|
+
const group = (preferredUnitId
|
|
131
|
+
? roomGroups.find((g) => g.primaryUnitId === preferredUnitId ||
|
|
132
|
+
g.units.some((u) => u.unitId === preferredUnitId))
|
|
133
|
+
: undefined) ?? roomGroups[0];
|
|
187
134
|
if (!group)
|
|
188
|
-
return
|
|
135
|
+
return null;
|
|
189
136
|
return (matchUnitByDob(group.units, dateOfBirth) ??
|
|
190
137
|
matchUnitByRoleHint(group.units, role) ??
|
|
191
|
-
group.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
// "No room" choice from the Room select and left alone. Reset on
|
|
200
|
-
// empty `roomUnits` so the next load (e.g. product change) rehydrates.
|
|
201
|
-
const hasHydratedNullsRef = React.useRef(false);
|
|
202
|
-
React.useEffect(() => {
|
|
138
|
+
group.units.find((unit) => unit.unitType == null || unit.unitType === "person")?.unitId ??
|
|
139
|
+
null);
|
|
140
|
+
}, [roomGroups]);
|
|
141
|
+
// Auto-pick a room with seats available so operators don't have to
|
|
142
|
+
// hunt for the dropdown on every traveler — they can still override
|
|
143
|
+
// manually via the Room select. Pricing is picked from the same
|
|
144
|
+
// option when the product exposes person tiers.
|
|
145
|
+
const pickAssignmentsForNewTraveler = React.useCallback((dateOfBirth = null, role = null) => {
|
|
203
146
|
if (!roomUnits || roomUnits.length === 0) {
|
|
204
|
-
|
|
205
|
-
return;
|
|
147
|
+
return { pricingUnitId: null, inventoryUnitId: null };
|
|
206
148
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
149
|
+
const pickedRoom = roomUnits.find((unit) => unit.remainingCapacity > 0)?.unitId ?? roomUnits[0]?.unitId ?? null;
|
|
150
|
+
if (!pickedRoom || !roomGroups || roomGroups.length === 0) {
|
|
151
|
+
return { pricingUnitId: null, inventoryUnitId: pickedRoom };
|
|
152
|
+
}
|
|
153
|
+
const pricingUnitId = pickPricingUnitIdForTraveler(dateOfBirth, role, pickedRoom);
|
|
154
|
+
return { pricingUnitId, inventoryUnitId: pickedRoom };
|
|
155
|
+
}, [roomUnits, roomGroups, pickPricingUnitIdForTraveler]);
|
|
156
|
+
// Note: there is no hydration effect any more. Travelers attached
|
|
157
|
+
// before the option-units queries resolve get null assignment ids
|
|
158
|
+
// and `*UnitSource: "auto"`; the resolver in
|
|
159
|
+
// `@voyantjs/bookings/pricing-assignment` re-derives them at every
|
|
160
|
+
// preview/submit pass, and respects `"none"` (explicit No room) /
|
|
161
|
+
// `"manual"` (operator click) when set. Operator intent is now
|
|
162
|
+
// declarative on the row, not implicit in a one-shot effect.
|
|
221
163
|
const addRow = () => {
|
|
222
164
|
// First traveler defaults to `lead` so the operator doesn't have to
|
|
223
165
|
// remember to flip the role on the initial row.
|
|
@@ -226,7 +168,12 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
226
168
|
onChange({
|
|
227
169
|
travelers: [
|
|
228
170
|
...value.travelers,
|
|
229
|
-
{
|
|
171
|
+
{
|
|
172
|
+
...blank,
|
|
173
|
+
...pickAssignmentsForNewTraveler(null, role),
|
|
174
|
+
pricingUnitSource: "auto",
|
|
175
|
+
inventoryUnitSource: "auto",
|
|
176
|
+
},
|
|
230
177
|
],
|
|
231
178
|
});
|
|
232
179
|
};
|
|
@@ -238,7 +185,12 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
238
185
|
onChange({
|
|
239
186
|
travelers: [
|
|
240
187
|
...value.travelers,
|
|
241
|
-
{
|
|
188
|
+
{
|
|
189
|
+
...traveler,
|
|
190
|
+
...pickAssignmentsForNewTraveler(traveler.dateOfBirth, role),
|
|
191
|
+
pricingUnitSource: "auto",
|
|
192
|
+
inventoryUnitSource: "auto",
|
|
193
|
+
},
|
|
242
194
|
],
|
|
243
195
|
});
|
|
244
196
|
};
|
|
@@ -248,7 +200,12 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
248
200
|
onChange({
|
|
249
201
|
travelers: [
|
|
250
202
|
...value.travelers,
|
|
251
|
-
{
|
|
203
|
+
{
|
|
204
|
+
...traveler,
|
|
205
|
+
...pickAssignmentsForNewTraveler(traveler.dateOfBirth, role),
|
|
206
|
+
pricingUnitSource: "auto",
|
|
207
|
+
inventoryUnitSource: "auto",
|
|
208
|
+
},
|
|
252
209
|
],
|
|
253
210
|
});
|
|
254
211
|
};
|
|
@@ -289,6 +246,25 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
289
246
|
phone: person.phone ?? "",
|
|
290
247
|
preferredLanguage: person.preferredLanguage ?? "",
|
|
291
248
|
dateOfBirth: person.dateOfBirth ?? null,
|
|
249
|
+
// Re-derive auto-owned unit assignments when the
|
|
250
|
+
// linked CRM person changes. Pricing and inventory
|
|
251
|
+
// stay independent: a manual room does not freeze
|
|
252
|
+
// DOB-driven pricing, and a manual category does
|
|
253
|
+
// not move the room.
|
|
254
|
+
...(traveler.pricingUnitSource === "manual" ||
|
|
255
|
+
traveler.pricingUnitSource === "none"
|
|
256
|
+
? {}
|
|
257
|
+
: {
|
|
258
|
+
pricingUnitId: pickPricingUnitIdForTraveler(person.dateOfBirth ?? null, traveler.role, traveler.inventoryUnitId),
|
|
259
|
+
pricingUnitSource: "auto",
|
|
260
|
+
}),
|
|
261
|
+
...(traveler.inventoryUnitSource === "manual" ||
|
|
262
|
+
traveler.inventoryUnitSource === "none"
|
|
263
|
+
? {}
|
|
264
|
+
: {
|
|
265
|
+
inventoryUnitId: pickAssignmentsForNewTraveler(person.dateOfBirth ?? null, traveler.role).inventoryUnitId,
|
|
266
|
+
inventoryUnitSource: "auto",
|
|
267
|
+
}),
|
|
292
268
|
}), onClear: () => updateAt(index, {
|
|
293
269
|
personId: null,
|
|
294
270
|
firstName: "",
|
|
@@ -297,15 +273,37 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
|
|
|
297
273
|
phone: "",
|
|
298
274
|
preferredLanguage: "",
|
|
299
275
|
dateOfBirth: null,
|
|
276
|
+
...(traveler.pricingUnitSource === "manual" ||
|
|
277
|
+
traveler.pricingUnitSource === "none"
|
|
278
|
+
? {}
|
|
279
|
+
: {
|
|
280
|
+
pricingUnitId: pickPricingUnitIdForTraveler(null, traveler.role, traveler.inventoryUnitId),
|
|
281
|
+
pricingUnitSource: "auto",
|
|
282
|
+
}),
|
|
283
|
+
...(traveler.inventoryUnitSource === "manual" ||
|
|
284
|
+
traveler.inventoryUnitSource === "none"
|
|
285
|
+
? {}
|
|
286
|
+
: {
|
|
287
|
+
inventoryUnitId: pickAssignmentsForNewTraveler(null, traveler.role)
|
|
288
|
+
.inventoryUnitId,
|
|
289
|
+
inventoryUnitSource: "auto",
|
|
290
|
+
}),
|
|
300
291
|
}) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(TravelerCategoryButtons, { traveler: traveler, roomGroups: roomGroups, fallbackLabels: {
|
|
301
292
|
category: merged.category,
|
|
302
293
|
adult: merged.roleAdult,
|
|
303
294
|
child: merged.roleChild,
|
|
304
295
|
infant: merged.roleInfant,
|
|
305
|
-
}, onPickUnit: (unitId, nextRole
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
296
|
+
}, onPickUnit: (unitId, nextRole, source) => updateAt(index, {
|
|
297
|
+
pricingUnitId: unitId,
|
|
298
|
+
role: nextRole,
|
|
299
|
+
// Only freeze as manual when the dynamic button
|
|
300
|
+
// actually picked a unit. Role-only clicks via
|
|
301
|
+
// the static fallback stay `auto` so the
|
|
302
|
+
// resolver can re-derive once real units load.
|
|
303
|
+
pricingUnitSource: source,
|
|
304
|
+
}) }), roomUnits && roomUnits.length > 0 ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.room }), _jsxs(Select, { items: roomSelectItems, value: mapUnitIdToGroupPrimary(traveler.inventoryUnitId, roomGroups) ?? NO_ROOM, onValueChange: (v) => updateAt(index, {
|
|
305
|
+
inventoryUnitId: v === NO_ROOM || !v ? null : v,
|
|
306
|
+
inventoryUnitSource: v === NO_ROOM || !v ? "none" : "manual",
|
|
309
307
|
}), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: NO_ROOM, children: merged.noRoom }), roomUnits.map((unit) => (_jsx(SelectItem, { value: unit.unitId, children: unit.unitName }, unit.unitId)))] })] })] })) : null] }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 text-destructive", onClick: () => removeAt(index), "aria-label": merged.remove, children: [_jsx(Trash2, { className: "mr-1 h-3.5 w-3.5" }), merged.remove] }) })] }, index))) }))] }));
|
|
310
308
|
}
|
|
311
309
|
function TravelerPersonPicker({ personId, labels, pinnedPeople = [], onSelect, onClear, }) {
|
|
@@ -375,6 +373,7 @@ function createTravelerFromPerson(person, role) {
|
|
|
375
373
|
const dateOfBirth = person.dateOfBirth ?? null;
|
|
376
374
|
const effectiveRole = role === "lead" ? "lead" : deriveTravelerRoleFromDob(dateOfBirth);
|
|
377
375
|
return {
|
|
376
|
+
clientTravelerKey: createClientTravelerKey(),
|
|
378
377
|
personId: person.id,
|
|
379
378
|
firstName: person.firstName,
|
|
380
379
|
lastName: person.lastName,
|
|
@@ -383,7 +382,10 @@ function createTravelerFromPerson(person, role) {
|
|
|
383
382
|
preferredLanguage: person.preferredLanguage ?? "",
|
|
384
383
|
role: effectiveRole,
|
|
385
384
|
dateOfBirth,
|
|
386
|
-
|
|
385
|
+
pricingUnitId: null,
|
|
386
|
+
inventoryUnitId: null,
|
|
387
|
+
pricingUnitSource: "auto",
|
|
388
|
+
inventoryUnitSource: "auto",
|
|
387
389
|
};
|
|
388
390
|
}
|
|
389
391
|
function formatPersonName(person) {
|
|
@@ -409,11 +411,13 @@ function formatPerson(person) {
|
|
|
409
411
|
*/
|
|
410
412
|
function TravelerCategoryButtons({ traveler, roomGroups, fallbackLabels, onPickUnit, }) {
|
|
411
413
|
const group = React.useMemo(() => {
|
|
412
|
-
if (!roomGroups
|
|
414
|
+
if (!roomGroups)
|
|
415
|
+
return undefined;
|
|
416
|
+
const assignedUnitId = traveler.inventoryUnitId ?? traveler.pricingUnitId;
|
|
417
|
+
if (!assignedUnitId)
|
|
413
418
|
return undefined;
|
|
414
|
-
return roomGroups.find((g) => g.primaryUnitId ===
|
|
415
|
-
|
|
416
|
-
}, [roomGroups, traveler.roomUnitId]);
|
|
419
|
+
return roomGroups.find((g) => g.primaryUnitId === assignedUnitId || g.units.some((u) => u.unitId === assignedUnitId));
|
|
420
|
+
}, [roomGroups, traveler.inventoryUnitId, traveler.pricingUnitId]);
|
|
417
421
|
// Surface only person-typed units (Adult, Child, Senior, Infant,
|
|
418
422
|
// …). Vehicles / rooms / services aren't categories the operator
|
|
419
423
|
// toggles on a per-traveler basis. If the option has only one
|
|
@@ -440,16 +444,21 @@ function TravelerCategoryButtons({ traveler, roomGroups, fallbackLabels, onPickU
|
|
|
440
444
|
].map(([category, label]) => {
|
|
441
445
|
const { active, nextRole, shouldUpdate } = getStaticTravelerCategoryButtonState(traveler, category);
|
|
442
446
|
return (_jsx(Button, { type: "button", size: "sm", variant: active ? "default" : "outline", className: "h-7 text-xs", onClick: () => {
|
|
447
|
+
// Static fallback: operator chose a role, not a
|
|
448
|
+
// concrete unit. Pass `source: "auto"` so the
|
|
449
|
+
// resolver re-derives the unit instead of freezing
|
|
450
|
+
// a stale auto-assignment as manual.
|
|
443
451
|
if (shouldUpdate)
|
|
444
|
-
onPickUnit(traveler.
|
|
452
|
+
onPickUnit(traveler.pricingUnitId, nextRole, "auto");
|
|
445
453
|
}, children: label }, category));
|
|
446
454
|
}) })] }));
|
|
447
455
|
}
|
|
448
456
|
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: fallbackLabels.category }), _jsx("div", { className: "grid gap-1", style: { gridTemplateColumns: `repeat(${categoryUnits.length}, minmax(0, 1fr))` }, children: categoryUnits.map((unit) => {
|
|
449
457
|
const { active, nextRole, shouldUpdate } = getDynamicTravelerCategoryButtonState(traveler, unit);
|
|
450
458
|
return (_jsx(Button, { type: "button", size: "sm", variant: active ? "default" : "outline", className: "h-7 text-xs", onClick: () => {
|
|
459
|
+
// Dynamic button: real unit pick, freeze as manual.
|
|
451
460
|
if (shouldUpdate)
|
|
452
|
-
onPickUnit(unit.unitId, nextRole);
|
|
461
|
+
onPickUnit(unit.unitId, nextRole, "manual");
|
|
453
462
|
}, title: unit.minAge != null || unit.maxAge != null
|
|
454
463
|
? `${unit.minAge ?? "0"}–${unit.maxAge ?? "∞"}`
|
|
455
464
|
: undefined, children: unit.unitName }, unit.unitId));
|
package/dist/i18n/en.d.ts
CHANGED
|
@@ -487,6 +487,7 @@ export declare const bookingsUiEn: {
|
|
|
487
487
|
fillsSlotCapacity: string;
|
|
488
488
|
decreaseUnitPrefix: string;
|
|
489
489
|
increaseUnitPrefix: string;
|
|
490
|
+
reviewLine: string;
|
|
490
491
|
};
|
|
491
492
|
};
|
|
492
493
|
sharedRoomSection: {
|
|
@@ -1124,6 +1125,9 @@ export declare const bookingsUiEn: {
|
|
|
1124
1125
|
confirmFailedPrefix: string;
|
|
1125
1126
|
confirmFailed: string;
|
|
1126
1127
|
createFailed: string;
|
|
1128
|
+
payloadResolverMismatchDetails: string;
|
|
1129
|
+
payloadResolverMismatchFallback: string;
|
|
1130
|
+
payloadResolverMismatchLine: string;
|
|
1127
1131
|
};
|
|
1128
1132
|
actions: {
|
|
1129
1133
|
createDraftBooking: string;
|
|
@@ -1311,6 +1315,7 @@ export declare const bookingsUiEn: {
|
|
|
1311
1315
|
actions: {
|
|
1312
1316
|
open: string;
|
|
1313
1317
|
view: string;
|
|
1318
|
+
convertToInvoice: string;
|
|
1314
1319
|
edit: string;
|
|
1315
1320
|
delete: string;
|
|
1316
1321
|
};
|
package/dist/i18n/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAm3CK,CAAA"}
|
package/dist/i18n/en.js
CHANGED
|
@@ -487,6 +487,7 @@ export const bookingsUiEn = {
|
|
|
487
487
|
fillsSlotCapacity: "fills slot capacity",
|
|
488
488
|
decreaseUnitPrefix: "Decrease",
|
|
489
489
|
increaseUnitPrefix: "Increase",
|
|
490
|
+
reviewLine: "Review this line",
|
|
490
491
|
},
|
|
491
492
|
},
|
|
492
493
|
sharedRoomSection: {
|
|
@@ -1124,6 +1125,9 @@ export const bookingsUiEn = {
|
|
|
1124
1125
|
confirmFailedPrefix: "Booking created but confirm failed: {message}",
|
|
1125
1126
|
confirmFailed: "Booking created but confirm failed",
|
|
1126
1127
|
createFailed: "Failed to create booking",
|
|
1128
|
+
payloadResolverMismatchDetails: "Booking options are out of sync. Review these lines: {details}.",
|
|
1129
|
+
payloadResolverMismatchFallback: "Booking options are out of sync. Review the selected traveler and option lines.",
|
|
1130
|
+
payloadResolverMismatchLine: "{label}: sent {submittedQuantity}, expected {resolvedQuantity}",
|
|
1127
1131
|
},
|
|
1128
1132
|
actions: {
|
|
1129
1133
|
createDraftBooking: "Create draft booking",
|
|
@@ -1311,6 +1315,7 @@ export const bookingsUiEn = {
|
|
|
1311
1315
|
actions: {
|
|
1312
1316
|
open: "Open payment menu",
|
|
1313
1317
|
view: "View payment",
|
|
1318
|
+
convertToInvoice: "Convert to invoice",
|
|
1314
1319
|
edit: "Edit payment",
|
|
1315
1320
|
delete: "Delete payment",
|
|
1316
1321
|
},
|
package/dist/i18n/messages.d.ts
CHANGED
|
@@ -401,6 +401,7 @@ export type BookingsUiMessages = {
|
|
|
401
401
|
fillsSlotCapacity: string;
|
|
402
402
|
decreaseUnitPrefix: string;
|
|
403
403
|
increaseUnitPrefix: string;
|
|
404
|
+
reviewLine: string;
|
|
404
405
|
};
|
|
405
406
|
};
|
|
406
407
|
sharedRoomSection: {
|
|
@@ -983,6 +984,9 @@ export type BookingsUiMessages = {
|
|
|
983
984
|
confirmFailedPrefix: string;
|
|
984
985
|
confirmFailed: string;
|
|
985
986
|
createFailed: string;
|
|
987
|
+
payloadResolverMismatchDetails: string;
|
|
988
|
+
payloadResolverMismatchFallback: string;
|
|
989
|
+
payloadResolverMismatchLine: string;
|
|
986
990
|
};
|
|
987
991
|
actions: {
|
|
988
992
|
createDraftBooking: string;
|
|
@@ -1164,6 +1168,7 @@ export type BookingsUiMessages = {
|
|
|
1164
1168
|
/** Trigger button screen-reader label. */
|
|
1165
1169
|
open: string;
|
|
1166
1170
|
view: string;
|
|
1171
|
+
convertToInvoice: string;
|
|
1167
1172
|
edit: string;
|
|
1168
1173
|
delete: string;
|
|
1169
1174
|
};
|