@voyantjs/bookings-ui 0.80.16 → 0.80.17

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.
@@ -1,4 +1,22 @@
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;
2
20
  export interface BookingCreateDialogProps {
3
21
  open: boolean;
4
22
  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":"AASA,OAAO,EAML,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;AA+XjC,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,2CAo5BxB"}
1
+ {"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AASA,OAAO,EAML,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;AAiCjC,OAAO,EAGL,KAAK,sBAAsB,EAE5B,MAAM,mCAAmC,CAAA;AA6J1C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,sBAAsB,EAAE,EAC/B,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,QAAQ,GAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,IAAW,GACnD,sBAAsB,GAAG,SAAS,CAoCpC;AA4JD,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,2CAo5BxB"}
@@ -122,30 +122,48 @@ function stripOptionPrefix(name) {
122
122
  * 1. If we have an age (from DOB) and it falls into a unit's
123
123
  * `[minAge, maxAge]` window, use that unit.
124
124
  * 2. Otherwise honor an explicit role hint (Child / Infant / Adult
125
- * buttons) by matching unit code or name.
126
- * 3. Fall back to the ADULT-coded unit, or the first unit when
127
- * nothing else matches.
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.
128
131
  *
129
132
  * `roleHint` covers the common case where the operator knows the
130
133
  * traveler is a child but doesn't have the exact DOB. Without it, a
131
134
  * roleless traveler would silently default to Adult pricing.
132
135
  */
133
- function pickUnitForAge(units, age, roleHint = null) {
136
+ export function pickUnitForAge(units, age, roleHint = null) {
134
137
  if (units.length === 0)
135
138
  return undefined;
136
- const findByCode = (code) => units.find((u) => (u.unitCode ?? "").toUpperCase() === code) ??
137
- units.find((u) => new RegExp(`\\b${code}\\b`, "i").test(u.unitName));
138
- const adult = findByCode("ADULT");
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));
139
143
  if (age != null) {
140
- const match = units.find((u) => (u.minAge == null || age >= u.minAge) && (u.maxAge == null || age <= u.maxAge));
144
+ const match = matchByAge(age);
141
145
  if (match)
142
146
  return match;
143
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));
144
162
  if (roleHint === "child")
145
- return findByCode("CHILD") ?? adult ?? units[0];
163
+ return findByCode("CHILD") ?? sorted[0];
146
164
  if (roleHint === "infant")
147
- return findByCode("INFANT") ?? adult ?? units[0];
148
- return adult ?? units[0];
165
+ return findByCode("INFANT") ?? sorted[0];
166
+ return findByCode("ADULT") ?? sorted[sorted.length - 1] ?? sorted[0];
149
167
  }
150
168
  /**
151
169
  * Take the operator-picked per-option quantities (which are tracked
@@ -1 +1 @@
1
- {"version":3,"file":"travelers-section.d.ts","sourceRoot":"","sources":["../../src/components/travelers-section.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"travelers-section.d.ts","sourceRoot":"","sources":["../../src/components/travelers-section.tsx"],"names":[],"mappings":"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"}
@@ -75,6 +75,32 @@ function matchUnitByDob(units, dob) {
75
75
  const match = personUnits.find((u) => (u.minAge == null || age >= u.minAge) && (u.maxAge == null || age <= u.maxAge));
76
76
  return match?.unitId ?? null;
77
77
  }
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
+ function matchUnitByRoleHint(units, role) {
88
+ if (!role || role === "lead")
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;
103
+ }
78
104
  /**
79
105
  * The Room dropdown lists one item per option (keyed by the option's
80
106
  * primary/ADULT unit id), but a traveler's `roomUnitId` can point at any
@@ -147,10 +173,11 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
147
173
  // hunt for the dropdown on every traveler — they can still override
148
174
  // manually via the Room select. Picks the first option (ordering
149
175
  // mirrors the upstream `roomUnits` array, which comes from the
150
- // stepper in catalog order). When `roomGroups` is wired and the
151
- // traveler has DOB, also pre-pick the matching age-banded unit
152
- // within that option so the Category buttons land on the right row.
153
- const pickRoomUnitIdForNewTraveler = (dateOfBirth = null) => {
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) => {
154
181
  if (!roomUnits || roomUnits.length === 0)
155
182
  return null;
156
183
  const pickedRoom = roomUnits.find((unit) => unit.remainingCapacity > 0)?.unitId ?? roomUnits[0]?.unitId ?? null;
@@ -159,15 +186,39 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
159
186
  const group = roomGroups.find((g) => g.primaryUnitId === pickedRoom || g.units.some((u) => u.unitId === pickedRoom));
160
187
  if (!group)
161
188
  return pickedRoom;
162
- return matchUnitByDob(group.units, dateOfBirth) ?? group.primaryUnitId;
163
- };
189
+ return (matchUnitByDob(group.units, dateOfBirth) ??
190
+ matchUnitByRoleHint(group.units, role) ??
191
+ group.primaryUnitId);
192
+ }, [roomUnits, roomGroups]);
193
+ // Race fix: travelers added before the option-units queries resolve
194
+ // end up with `roomUnitId: null`. Once units arrive, back-fill any
195
+ // missing assignments so the static fallback's role hint (Child /
196
+ // Infant) is honored and `redistributeByAge` doesn't silently price
197
+ // them as adults.
198
+ React.useEffect(() => {
199
+ if (!roomUnits || roomUnits.length === 0)
200
+ return;
201
+ if (!value.travelers.some((t) => !t.roomUnitId))
202
+ return;
203
+ const next = value.travelers.map((t) => t.roomUnitId ? t : { ...t, roomUnitId: pickRoomUnitIdForNewTraveler(t.dateOfBirth, t.role) });
204
+ // Guard against infinite re-runs if hydration can't find a unit
205
+ // (e.g. empty roomGroups): no point dispatching an onChange that
206
+ // doesn't actually change anything.
207
+ const changed = next.some((t, i) => t.roomUnitId !== value.travelers[i]?.roomUnitId);
208
+ if (!changed)
209
+ return;
210
+ onChange({ travelers: next });
211
+ }, [roomUnits, value.travelers, onChange, pickRoomUnitIdForNewTraveler]);
164
212
  const addRow = () => {
165
213
  // First traveler defaults to `lead` so the operator doesn't have to
166
214
  // remember to flip the role on the initial row.
167
215
  const role = value.travelers.length === 0 ? "lead" : "adult";
168
216
  const blank = createBlankTraveler(role);
169
217
  onChange({
170
- travelers: [...value.travelers, { ...blank, roomUnitId: pickRoomUnitIdForNewTraveler(null) }],
218
+ travelers: [
219
+ ...value.travelers,
220
+ { ...blank, roomUnitId: pickRoomUnitIdForNewTraveler(null, role) },
221
+ ],
171
222
  });
172
223
  };
173
224
  const addBillingPerson = () => {
@@ -178,7 +229,7 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
178
229
  onChange({
179
230
  travelers: [
180
231
  ...value.travelers,
181
- { ...traveler, roomUnitId: pickRoomUnitIdForNewTraveler(traveler.dateOfBirth) },
232
+ { ...traveler, roomUnitId: pickRoomUnitIdForNewTraveler(traveler.dateOfBirth, role) },
182
233
  ],
183
234
  });
184
235
  };
@@ -188,7 +239,7 @@ export function TravelersSection({ value, onChange, roomUnits, roomGroups, billi
188
239
  onChange({
189
240
  travelers: [
190
241
  ...value.travelers,
191
- { ...traveler, roomUnitId: pickRoomUnitIdForNewTraveler(traveler.dateOfBirth) },
242
+ { ...traveler, roomUnitId: pickRoomUnitIdForNewTraveler(traveler.dateOfBirth, role) },
192
243
  ],
193
244
  });
194
245
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/bookings-ui",
3
- "version": "0.80.16",
3
+ "version": "0.80.17",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,24 +51,24 @@
51
51
  "react-dom": "^19.0.0",
52
52
  "react-hook-form": "^7.60.0",
53
53
  "zod": "^4.3.6",
54
- "@voyantjs/availability-react": "0.80.16",
55
- "@voyantjs/bookings-react": "0.80.16",
56
- "@voyantjs/catalog": "0.80.16",
57
- "@voyantjs/catalog-react": "0.80.16",
58
- "@voyantjs/crm-react": "0.80.16",
59
- "@voyantjs/crm-ui": "0.80.16",
60
- "@voyantjs/extras-react": "0.80.16",
61
- "@voyantjs/finance-react": "0.80.16",
62
- "@voyantjs/identity-react": "0.80.16",
63
- "@voyantjs/legal-react": "0.80.16",
64
- "@voyantjs/pricing-react": "0.80.16",
65
- "@voyantjs/products-react": "0.80.16",
66
- "@voyantjs/suppliers-react": "0.80.16",
67
- "@voyantjs/ui": "0.80.16"
54
+ "@voyantjs/availability-react": "0.80.17",
55
+ "@voyantjs/bookings-react": "0.80.17",
56
+ "@voyantjs/catalog": "0.80.17",
57
+ "@voyantjs/catalog-react": "0.80.17",
58
+ "@voyantjs/crm-react": "0.80.17",
59
+ "@voyantjs/crm-ui": "0.80.17",
60
+ "@voyantjs/extras-react": "0.80.17",
61
+ "@voyantjs/finance-react": "0.80.17",
62
+ "@voyantjs/identity-react": "0.80.17",
63
+ "@voyantjs/legal-react": "0.80.17",
64
+ "@voyantjs/pricing-react": "0.80.17",
65
+ "@voyantjs/products-react": "0.80.17",
66
+ "@voyantjs/suppliers-react": "0.80.17",
67
+ "@voyantjs/ui": "0.80.17"
68
68
  },
69
69
  "dependencies": {
70
70
  "sonner": "^2.0.7",
71
- "@voyantjs/i18n": "0.80.16"
71
+ "@voyantjs/i18n": "0.80.17"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@tanstack/react-query": "^5.100.11",
@@ -82,20 +82,20 @@
82
82
  "typescript": "^6.0.2",
83
83
  "vitest": "^4.1.2",
84
84
  "zod": "^4.3.6",
85
- "@voyantjs/availability-react": "0.80.16",
86
- "@voyantjs/bookings-react": "0.80.16",
87
- "@voyantjs/catalog": "0.80.16",
88
- "@voyantjs/catalog-react": "0.80.16",
89
- "@voyantjs/crm-react": "0.80.16",
90
- "@voyantjs/crm-ui": "0.80.16",
91
- "@voyantjs/extras-react": "0.80.16",
92
- "@voyantjs/finance-react": "0.80.16",
93
- "@voyantjs/identity-react": "0.80.16",
94
- "@voyantjs/legal-react": "0.80.16",
95
- "@voyantjs/pricing-react": "0.80.16",
96
- "@voyantjs/products-react": "0.80.16",
97
- "@voyantjs/suppliers-react": "0.80.16",
98
- "@voyantjs/ui": "0.80.16",
85
+ "@voyantjs/availability-react": "0.80.17",
86
+ "@voyantjs/bookings-react": "0.80.17",
87
+ "@voyantjs/catalog": "0.80.17",
88
+ "@voyantjs/catalog-react": "0.80.17",
89
+ "@voyantjs/crm-react": "0.80.17",
90
+ "@voyantjs/crm-ui": "0.80.17",
91
+ "@voyantjs/extras-react": "0.80.17",
92
+ "@voyantjs/finance-react": "0.80.17",
93
+ "@voyantjs/identity-react": "0.80.17",
94
+ "@voyantjs/legal-react": "0.80.17",
95
+ "@voyantjs/pricing-react": "0.80.17",
96
+ "@voyantjs/products-react": "0.80.17",
97
+ "@voyantjs/suppliers-react": "0.80.17",
98
+ "@voyantjs/ui": "0.80.17",
99
99
  "@voyantjs/voyant-typescript-config": "0.1.0"
100
100
  },
101
101
  "files": [