@voyantjs/bookings-ui 0.50.7 → 0.51.0

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 +1 @@
1
- {"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;AAsMjC,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;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,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,GACjB,EAAE,wBAAwB,2CAqB1B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CAgiBxB"}
1
+ {"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;AAsNjC,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;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,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,GACjB,EAAE,wBAAwB,2CAqB1B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CAojBxB"}
@@ -115,6 +115,19 @@ function travelersToRows(value) {
115
115
  roomUnitId: traveler.roomUnitId,
116
116
  }));
117
117
  }
118
+ function sameRoomUnits(left, right) {
119
+ if (left.length !== right.length)
120
+ return false;
121
+ return left.every((unit, index) => {
122
+ const other = right[index];
123
+ return (other !== undefined &&
124
+ unit.optionId === other.optionId &&
125
+ unit.optionUnitId === other.optionUnitId &&
126
+ unit.unitName === other.unitName &&
127
+ unit.occupancyMax === other.occupancyMax &&
128
+ unit.remaining === other.remaining);
129
+ });
130
+ }
118
131
  /**
119
132
  * Operator booking-create dialog. Composes the booking-create picker
120
133
  * sections — product, departure, rooms, person, shared-room, travelers,
@@ -140,6 +153,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
140
153
  });
141
154
  const [slotId, setSlotId] = React.useState(null);
142
155
  const [rooms, setRooms] = React.useState(emptyRoomsStepperValue);
156
+ const [roomUnits, setRoomUnits] = React.useState([]);
143
157
  const [person, setPerson] = React.useState(emptyPersonPickerValue);
144
158
  const [sharedRoom, setSharedRoom] = React.useState(emptySharedRoomValue);
145
159
  const [travelers, setTravelers] = React.useState(emptyTravelerListValue);
@@ -165,6 +179,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
165
179
  setProduct({ productId: defaultProductId ?? "", optionId: null });
166
180
  setSlotId(null);
167
181
  setRooms(emptyRoomsStepperValue);
182
+ setRoomUnits([]);
168
183
  setPerson(emptyPersonPickerValue);
169
184
  setSharedRoom(emptySharedRoomValue);
170
185
  setTravelers(emptyTravelerListValue);
@@ -187,6 +202,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
187
202
  React.useEffect(() => {
188
203
  setSlotId(null);
189
204
  setRooms(emptyRoomsStepperValue);
205
+ setRoomUnits([]);
190
206
  setSharedRoom(emptySharedRoomValue);
191
207
  }, [product.productId]);
192
208
  const [slotsFromIso, setSlotsFromIso] = React.useState(() => new Date().toISOString());
@@ -223,6 +239,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
223
239
  }, [allOpenSlots, product.optionId]);
224
240
  React.useEffect(() => {
225
241
  setRooms(emptyRoomsStepperValue);
242
+ setRoomUnits([]);
226
243
  if (!slotId || !product.optionId)
227
244
  return;
228
245
  const selectedSlot = allOpenSlots.find((slot) => slot.id === slotId);
@@ -245,15 +262,18 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
245
262
  slotId: slotId ?? undefined,
246
263
  enabled: enabled && Boolean(slotId),
247
264
  });
265
+ const handleRoomUnitsChange = React.useCallback((units) => {
266
+ setRoomUnits((prev) => (sameRoomUnits(prev, units) ? prev : units));
267
+ }, []);
248
268
  const roomUnitOptions = React.useMemo(() => {
249
- const units = slotUnitAvailability.data?.data ?? [];
269
+ const units = roomUnits.length > 0 ? roomUnits : (slotUnitAvailability.data?.data ?? []);
250
270
  if (units.length === 0)
251
271
  return [];
252
272
  return units
253
273
  .filter((unit) => (rooms.quantities[unit.optionUnitId] ?? 0) > 0)
254
274
  .map((unit) => {
255
275
  const qty = rooms.quantities[unit.optionUnitId] ?? 0;
256
- const occupancyMax = 1;
276
+ const occupancyMax = Math.max(1, unit.occupancyMax ?? 1);
257
277
  const seats = qty * occupancyMax;
258
278
  const assigned = travelers.travelers.filter((traveler) => traveler.roomUnitId === unit.optionUnitId).length;
259
279
  return {
@@ -262,13 +282,14 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
262
282
  remainingCapacity: Math.max(0, seats - assigned),
263
283
  };
264
284
  });
265
- }, [slotUnitAvailability.data, rooms.quantities, travelers.travelers]);
285
+ }, [roomUnits, slotUnitAvailability.data, rooms.quantities, travelers.travelers]);
266
286
  // Currency placeholder — used for voucher + payment schedule display.
267
287
  // Consumers hooking in real product data should override this by wrapping
268
288
  // the component or swapping in their own currency-aware hook.
269
289
  const currency = messages.bookingCreateDialog.labels.currency;
270
290
  const pricingCurrency = pricing?.currency ?? currency;
271
291
  const pricingTotalAmountCents = pricing?.confirmedAmountCents ?? undefined;
292
+ const roomUnitLabels = React.useMemo(() => Object.fromEntries(roomUnits.map((unit) => [unit.optionUnitId, unit.unitName])), [roomUnits]);
272
293
  const createBookingMutation = useBookingCreateMutation();
273
294
  const statusMutation = useBookingStatusByIdMutation();
274
295
  const handleSubmit = async () => {
@@ -307,7 +328,12 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
307
328
  return;
308
329
  }
309
330
  const paymentSchedules = paymentScheduleToRows(paymentSchedule, pricingCurrency, confirmedSellAmountCents);
310
- const itemLines = itemLinesToRows(rooms.quantities, slotUnitAvailability.data?.data ?? [], pricing);
331
+ const itemLines = itemLinesToRows(rooms.quantities, roomUnits.length > 0
332
+ ? roomUnits
333
+ : (slotUnitAvailability.data?.data ?? []).map((unit) => ({
334
+ ...unit,
335
+ optionId: product.optionId,
336
+ })), pricing);
311
337
  const travelerRows = travelersToRows(travelers);
312
338
  const voucherRedemption = voucher.picked && voucher.picked.remainingAmountCents != null
313
339
  ? {
@@ -384,7 +410,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
384
410
  const isSubmitting = createBookingMutation.isPending || statusMutation.isPending;
385
411
  return (_jsxs(_Fragment, { children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductPickerSection, { value: product, onChange: setProduct, enabled: enabled, lockProduct: Boolean(defaultProductId), labels: {
386
412
  optionNone: messages.bookingCreateDialog.labels.noSpecificOption,
387
- } }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsxs(Select, { value: slotId ?? "__none__", onValueChange: (v) => setSelectedSlot(v === "__none__" ? null : (v ?? null)), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: messages.bookingCreateDialog.placeholders.departure }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__none__", children: messages.bookingCreateDialog.placeholders.departureNone }), slots.length === 0 ? (_jsx(SelectItem, { value: "__empty__", disabled: true, children: messages.bookingCreateDialog.placeholders.departureEmpty })) : (slots.map((slot) => (_jsx(SelectItem, { value: slot.id, children: formatSlotLabel(slot) }, slot.id))))] })] })] })) : null, product.optionId ? (_jsx(RoomsStepperSection, { value: rooms, onChange: setRooms, slotId: slotId ?? undefined, optionId: product.optionId, enabled: enabled, labels: {
413
+ }, showOptionPicker: false }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsxs(Select, { value: slotId ?? "__none__", onValueChange: (v) => setSelectedSlot(v === "__none__" ? null : (v ?? null)), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: messages.bookingCreateDialog.placeholders.departure }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__none__", children: messages.bookingCreateDialog.placeholders.departureNone }), slots.length === 0 ? (_jsx(SelectItem, { value: "__empty__", disabled: true, children: messages.bookingCreateDialog.placeholders.departureEmpty })) : (slots.map((slot) => (_jsx(SelectItem, { value: slot.id, children: formatSlotLabel(slot) }, slot.id))))] })] })] })) : null, product.productId ? (_jsx(RoomsStepperSection, { value: rooms, onChange: setRooms, productId: product.productId, slotId: slotId ?? undefined, optionId: product.optionId, enabled: enabled, onUnitsChange: handleRoomUnitsChange, labels: {
388
414
  heading: messages.bookingCreateDialog.labels.roomsHeading,
389
415
  noOption: messages.bookingCreateDialog.labels.roomsNoOption,
390
416
  noSlot: messages.bookingCreateDialog.labels.roomsNoSlot,
@@ -421,7 +447,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
421
447
  noRoom: messages.bookingCreateDialog.labels.travelerNoRoom,
422
448
  remove: messages.bookingCreateDialog.labels.travelerRemove,
423
449
  empty: messages.bookingCreateDialog.labels.travelerEmpty,
424
- } })) : null, product.productId ? (_jsx(PriceBreakdownSection, { productId: product.productId, optionId: product.optionId, unitQuantities: rooms.quantities, labels: {
450
+ } })) : null, product.productId ? (_jsx(PriceBreakdownSection, { productId: product.productId, optionId: product.optionId, unitQuantities: rooms.quantities, unitLabels: roomUnitLabels, labels: {
425
451
  heading: messages.bookingCreateDialog.labels.breakdownHeading,
426
452
  total: messages.bookingCreateDialog.labels.breakdownTotal,
427
453
  onRequest: messages.bookingCreateDialog.labels.breakdownOnRequest,
@@ -8,6 +8,7 @@ export interface DepartureSlotSearchRecord {
8
8
  status?: string;
9
9
  }
10
10
  export interface BookingCreateUnitLineRecord {
11
+ optionId: string | null;
11
12
  optionUnitId: string;
12
13
  unitName: string;
13
14
  }
@@ -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,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,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,wBAAgB,yBAAyB,CAAC,KAAK,SAAS,yBAAyB,EAC/E,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GACA,KAAK,EAAE,CAST;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,KAAK,EAAE,2BAA2B,EAAE,EACpC,OAAO,EAAE,0BAA0B,GAAG,IAAI,GACzC,0BAA0B,EAAE,CAuC9B;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAE7F"}
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,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,wBAAgB,yBAAyB,CAAC,KAAK,SAAS,yBAAyB,EAC/E,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GACA,KAAK,EAAE,CAST;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,KAAK,EAAE,2BAA2B,EAAE,EACpC,OAAO,EAAE,0BAA0B,GAAG,IAAI,GACzC,0BAA0B,EAAE,CAyC9B;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAE7F"}
@@ -28,6 +28,7 @@ export function getBookableDepartureSlots(slots, options) {
28
28
  .sort((left, right) => left.startsAt.localeCompare(right.startsAt));
29
29
  }
30
30
  export function itemLinesToRows(quantities, units, pricing) {
31
+ const unitsById = new Map(units.map((unit) => [unit.optionUnitId, unit]));
31
32
  const unitNames = new Map(units.map((unit) => [unit.optionUnitId, unit.unitName]));
32
33
  const pricedLines = new Map((pricing?.lines ?? []).map((line) => [line.unitId, line]));
33
34
  const selectedLines = Object.entries(quantities).filter(([, quantity]) => quantity > 0);
@@ -54,6 +55,7 @@ export function itemLinesToRows(quantities, units, pricing) {
54
55
  const unitSellAmountCents = pricedLine?.unitAmountCents ??
55
56
  (totalSellAmountCents != null ? Math.floor(totalSellAmountCents / quantity) : null);
56
57
  return {
58
+ optionId: unitsById.get(optionUnitId)?.optionId ?? null,
57
59
  optionUnitId,
58
60
  quantity,
59
61
  title: pricedLine?.label ?? unitNames.get(optionUnitId) ?? null,
@@ -1 +1 @@
1
- {"version":3,"file":"person-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/person-picker-section.tsx"],"names":[],"mappings":"AAiCA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,KAAK,CAAA;AACjD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,cAAc,CAAA;AAEzD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,IAAI,EAAE,gBAAgB,CAAA;IACtB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,SAAS,EAAE,cAAc,CAAA;IACzB,yCAAyC;IACzC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,eAAO,MAAM,cAAc,EAAE,cAK5B,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAMpC,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,gBAAuB,EACvB,MAAM,GACP,EAAE,wBAAwB,2CAiQ1B"}
1
+ {"version":3,"file":"person-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/person-picker-section.tsx"],"names":[],"mappings":"AAiCA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,KAAK,CAAA;AACjD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,cAAc,CAAA;AAEzD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,IAAI,EAAE,gBAAgB,CAAA;IACtB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,SAAS,EAAE,cAAc,CAAA;IACzB,yCAAyC;IACzC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,eAAO,MAAM,cAAc,EAAE,cAK5B,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAMpC,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,gBAAuB,EACvB,MAAM,GACP,EAAE,wBAAwB,2CA+Q1B"}
@@ -29,6 +29,8 @@ export const emptyPersonPickerValue = {
29
29
  export function PersonPickerSection({ value, onChange, enabled = true, showOrganization = true, labels, }) {
30
30
  const [personSearch, setPersonSearch] = React.useState("");
31
31
  const [orgSearch, setOrgSearch] = React.useState("");
32
+ const cachedPeopleRef = React.useRef(new Map());
33
+ const cachedOrgsRef = React.useRef(new Map());
32
34
  const [personInputValue, setPersonInputValue] = React.useState("");
33
35
  const [orgInputValue, setOrgInputValue] = React.useState("");
34
36
  const [personSheetOpen, setPersonSheetOpen] = React.useState(false);
@@ -45,11 +47,12 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
45
47
  enabled: enabled && billingTarget === "person" && Boolean(value.personId),
46
48
  });
47
49
  const people = React.useMemo(() => {
48
- const map = new Map();
50
+ const map = new Map(cachedPeopleRef.current);
49
51
  for (const person of peopleData?.data ?? [])
50
52
  map.set(person.id, person);
51
53
  if (selectedPersonQuery.data)
52
54
  map.set(selectedPersonQuery.data.id, selectedPersonQuery.data);
55
+ cachedPeopleRef.current = map;
53
56
  return Array.from(map.values());
54
57
  }, [peopleData?.data, selectedPersonQuery.data]);
55
58
  const peopleMap = React.useMemo(() => new Map(people.map((person) => [person.id, person])), [people]);
@@ -62,19 +65,20 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
62
65
  enabled: enabled && billingTarget === "organization" && Boolean(value.organizationId),
63
66
  });
64
67
  const orgs = React.useMemo(() => {
65
- const map = new Map();
68
+ const map = new Map(cachedOrgsRef.current);
66
69
  for (const org of orgsData?.data ?? [])
67
70
  map.set(org.id, org);
68
71
  if (selectedOrgQuery.data)
69
72
  map.set(selectedOrgQuery.data.id, selectedOrgQuery.data);
73
+ cachedOrgsRef.current = map;
70
74
  return Array.from(map.values());
71
75
  }, [orgsData?.data, selectedOrgQuery.data]);
72
76
  const orgsMap = React.useMemo(() => new Map(orgs.map((org) => [org.id, org])), [orgs]);
73
77
  const setPerson = (patch) => onChange({ ...value, ...patch });
74
- const selectedPersonLabel = value.personId ? formatPerson(peopleMap.get(value.personId)) : "";
75
- const selectedOrgLabel = value.organizationId
76
- ? (orgsMap.get(value.organizationId)?.name ?? "")
77
- : "";
78
+ const resolvePersonLabel = React.useCallback((personId) => formatPerson(peopleMap.get(personId) ?? cachedPeopleRef.current.get(personId)), [peopleMap]);
79
+ const resolveOrgLabel = React.useCallback((organizationId) => orgsMap.get(organizationId)?.name ?? cachedOrgsRef.current.get(organizationId)?.name ?? "", [orgsMap]);
80
+ const selectedPersonLabel = value.personId ? resolvePersonLabel(value.personId) : "";
81
+ const selectedOrgLabel = value.organizationId ? resolveOrgLabel(value.organizationId) : "";
78
82
  React.useEffect(() => {
79
83
  if (selectedPersonLabel)
80
84
  setPersonInputValue(selectedPersonLabel);
@@ -83,7 +87,7 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
83
87
  if (selectedOrgLabel)
84
88
  setOrgInputValue(selectedOrgLabel);
85
89
  }, [selectedOrgLabel]);
86
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.billTo }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs(Button, { type: "button", variant: billingTarget === "person" ? "default" : "outline", onClick: () => setPerson({ billTo: "person", organizationId: null }), disabled: !enabled, children: [_jsx(User, { className: "mr-2 h-4 w-4" }), merged.billToPerson] }), _jsxs(Button, { type: "button", variant: billingTarget === "organization" ? "default" : "outline", onClick: () => setPerson({ billTo: "organization", personId: "" }), disabled: !enabled || !showOrganization, children: [_jsx(Building2, { className: "mr-2 h-4 w-4" }), merged.billToOrganization] })] })] }), billingTarget === "person" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.person, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setPersonSheetOpen(true), disabled: !enabled, children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewPerson] })] }), _jsxs(Combobox, { items: people.map((person) => person.id), value: value.personId || null, inputValue: personInputValue, autoHighlight: true, disabled: !enabled, itemToStringValue: (id) => formatPerson(peopleMap.get(id)), onInputValueChange: (next) => {
90
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.billTo }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs(Button, { type: "button", variant: billingTarget === "person" ? "default" : "outline", onClick: () => setPerson({ billTo: "person", organizationId: null }), disabled: !enabled, children: [_jsx(User, { className: "mr-2 h-4 w-4" }), merged.billToPerson] }), _jsxs(Button, { type: "button", variant: billingTarget === "organization" ? "default" : "outline", onClick: () => setPerson({ billTo: "organization", personId: "" }), disabled: !enabled || !showOrganization, children: [_jsx(Building2, { className: "mr-2 h-4 w-4" }), merged.billToOrganization] })] })] }), billingTarget === "person" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.person, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setPersonSheetOpen(true), disabled: !enabled, children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewPerson] })] }), _jsxs(Combobox, { items: people.map((person) => person.id), value: value.personId || null, inputValue: personInputValue, autoHighlight: true, disabled: !enabled, itemToStringLabel: (id) => resolvePersonLabel(id) || id, itemToStringValue: (id) => id, onInputValueChange: (next) => {
87
91
  setPersonInputValue(next);
88
92
  setPersonSearch(next);
89
93
  if (!next)
@@ -91,13 +95,13 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
91
95
  }, onValueChange: (next) => {
92
96
  const personId = next ?? "";
93
97
  setPerson({ personId });
94
- setPersonInputValue(personId ? formatPerson(peopleMap.get(personId)) : "");
98
+ setPersonInputValue(personId ? resolvePersonLabel(personId) : "");
95
99
  }, children: [_jsx(ComboboxInput, { placeholder: merged.personSearchPlaceholder, showClear: !!value.personId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.personEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
96
100
  const person = peopleMap.get(id);
97
101
  if (!person)
98
102
  return null;
99
103
  return (_jsx(ComboboxItem, { value: person.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: formatPersonName(person) }), person.email ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: person.email })) : null] }) }, person.id));
100
- } }) })] })] })] })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.organization, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setOrgSheetOpen(true), disabled: !enabled, children: [_jsx(Building2, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewOrganization] })] }), _jsxs(Combobox, { items: orgs.map((org) => org.id), value: value.organizationId ?? null, inputValue: orgInputValue, autoHighlight: true, disabled: !enabled, itemToStringValue: (id) => orgsMap.get(id)?.name ?? "", onInputValueChange: (next) => {
104
+ } }) })] })] })] })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.organization, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setOrgSheetOpen(true), disabled: !enabled, children: [_jsx(Building2, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewOrganization] })] }), _jsxs(Combobox, { items: orgs.map((org) => org.id), value: value.organizationId ?? null, inputValue: orgInputValue, autoHighlight: true, disabled: !enabled, itemToStringLabel: (id) => resolveOrgLabel(id) || id, itemToStringValue: (id) => id, onInputValueChange: (next) => {
101
105
  setOrgInputValue(next);
102
106
  setOrgSearch(next);
103
107
  if (!next)
@@ -105,7 +109,7 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
105
109
  }, onValueChange: (next) => {
106
110
  const organizationId = next ?? null;
107
111
  setPerson({ organizationId });
108
- setOrgInputValue(organizationId ? (orgsMap.get(organizationId)?.name ?? "") : "");
112
+ setOrgInputValue(organizationId ? resolveOrgLabel(organizationId) : "");
109
113
  }, children: [_jsx(ComboboxInput, { placeholder: merged.organizationSearchPlaceholder, showClear: !!value.organizationId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.organizationEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
110
114
  const org = orgsMap.get(id);
111
115
  if (!org)
@@ -27,6 +27,8 @@ export interface PriceBreakdownSectionProps {
27
27
  optionId?: string | null;
28
28
  /** Quantity per option_unit id, typically from RoomsStepperSection. */
29
29
  unitQuantities: Record<string, number>;
30
+ /** Display labels keyed by option_unit id. */
31
+ unitLabels?: Record<string, string>;
30
32
  /**
31
33
  * Force a specific catalog. Defaults to the public catalog the storefront
32
34
  * uses — matches what a customer would see.
@@ -60,5 +62,5 @@ export interface PriceBreakdownSectionProps {
60
62
  * - `free` / `included` — render 0.00 without an on-request badge.
61
63
  * - `on_request` / anything else — render "On request"; total excludes it.
62
64
  */
63
- export declare function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, onChange, }: PriceBreakdownSectionProps): import("react/jsx-runtime").JSX.Element | null;
65
+ export declare function PriceBreakdownSection({ productId, optionId, unitQuantities, unitLabels, catalogId, labels, onChange, }: PriceBreakdownSectionProps): import("react/jsx-runtime").JSX.Element | null;
64
66
  //# sourceMappingURL=price-breakdown-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;IACvB,KAAK,EAAE,kBAAkB,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAA;KAChC,CAAA;IACD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAChD;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACT,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B,kDAsQ5B"}
1
+ {"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AASA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;IACvB,KAAK,EAAE,kBAAkB,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAA;KAChC,CAAA;IACD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAChD;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B,kDAoS5B"}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { usePricingPreview } from "@voyantjs/bookings-react";
4
+ import { useProduct } from "@voyantjs/products-react";
4
5
  import { Button, Label, Textarea } from "@voyantjs/ui/components";
5
6
  import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
6
7
  import * as React from "react";
@@ -33,7 +34,7 @@ function matchTier(tiers, qty) {
33
34
  * - `free` / `included` — render 0.00 without an on-request badge.
34
35
  * - `on_request` / anything else — render "On request"; total excludes it.
35
36
  */
36
- export function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, onChange, }) {
37
+ export function PriceBreakdownSection({ productId, optionId, unitQuantities, unitLabels, catalogId, labels, onChange, }) {
37
38
  const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
38
39
  const messages = useBookingsUiMessagesOrDefault();
39
40
  const merged = { ...messages.priceBreakdownSection.labels, ...labels };
@@ -43,16 +44,19 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
43
44
  catalogId: catalogId ?? null,
44
45
  enabled: Boolean(productId),
45
46
  });
47
+ const productQuery = useProduct(productId, { enabled: Boolean(productId) });
46
48
  const quantitiesKey = React.useMemo(() => JSON.stringify(unitQuantities), [unitQuantities]);
47
49
  const [manualAmountCents, setManualAmountCents] = React.useState(null);
48
50
  const [overrideReason, setOverrideReason] = React.useState("");
49
- // biome-ignore lint/correctness/useExhaustiveDependencies: reset manual confirmation when the priced selection changes
51
+ // biome-ignore lint/correctness/useExhaustiveDependencies: #935 reset manual confirmation when the priced selection changes
50
52
  React.useEffect(() => {
51
53
  setManualAmountCents(null);
52
54
  setOverrideReason("");
53
55
  }, [productId, optionId, catalogId, quantitiesKey]);
54
56
  const snapshot = preview.data?.data;
55
- const currency = snapshot?.catalog.currencyCode ?? null;
57
+ const fallbackProduct = productQuery.data;
58
+ const fallbackUnitAmountCents = fallbackProduct?.sellAmountCents ?? null;
59
+ const currency = snapshot?.catalog.currencyCode ?? fallbackProduct?.sellCurrency ?? null;
56
60
  const formatAmount = React.useCallback((cents) => currency
57
61
  ? formatCurrency(cents / 100, currency)
58
62
  : formatNumber(cents / 100, {
@@ -63,8 +67,26 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
63
67
  const out = [];
64
68
  let runningTotal = 0;
65
69
  let anyOnRequest = false;
66
- if (!snapshot)
67
- return { lines: out, total: null };
70
+ if (!snapshot) {
71
+ if (fallbackUnitAmountCents === null)
72
+ return { lines: out, total: null };
73
+ for (const [unitId, quantity] of Object.entries(unitQuantities)) {
74
+ if (quantity <= 0)
75
+ continue;
76
+ const lineTotal = fallbackUnitAmountCents * quantity;
77
+ out.push({
78
+ unitId,
79
+ label: unitLabels?.[unitId] ?? fallbackProduct?.name ?? unitId,
80
+ quantity,
81
+ unitAmountCents: fallbackUnitAmountCents,
82
+ totalAmountCents: lineTotal,
83
+ tierLabel: null,
84
+ isGroupRate: false,
85
+ });
86
+ runningTotal += lineTotal;
87
+ }
88
+ return { lines: out, total: runningTotal };
89
+ }
68
90
  // Pick the default price rule for the resolved option (snapshot already
69
91
  // filters options by the caller's optionId; rules keep isDefault-first
70
92
  // ordering from the server).
@@ -89,7 +111,7 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
89
111
  // operator knows they need to quote manually.
90
112
  out.push({
91
113
  unitId,
92
- label: unitId,
114
+ label: unitLabels?.[unitId] ?? unitId,
93
115
  quantity,
94
116
  unitAmountCents: null,
95
117
  totalAmountCents: null,
@@ -99,7 +121,7 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
99
121
  anyOnRequest = true;
100
122
  continue;
101
123
  }
102
- const label = up.unitName || unitId;
124
+ const label = unitLabels?.[unitId] ?? up.unitName ?? unitId;
103
125
  if (up.pricingMode === "on_request") {
104
126
  out.push({
105
127
  unitId,
@@ -155,7 +177,15 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
155
177
  runningTotal += lineTotal;
156
178
  }
157
179
  return { lines: out, total: anyOnRequest ? null : runningTotal };
158
- }, [snapshot, unitQuantities, merged.onRequest, merged.groupRate]);
180
+ }, [
181
+ snapshot,
182
+ fallbackProduct?.name,
183
+ fallbackUnitAmountCents,
184
+ unitQuantities,
185
+ unitLabels,
186
+ merged.onRequest,
187
+ merged.groupRate,
188
+ ]);
159
189
  const confirmedAmountCents = manualAmountCents ?? total;
160
190
  const isManualOverride = manualAmountCents != null && (total === null || manualAmountCents !== total);
161
191
  const requiresReason = isManualOverride && overrideReason.trim().length === 0;
@@ -183,7 +213,7 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
183
213
  // Empty states
184
214
  if (!productId)
185
215
  return null;
186
- if (preview.isError || (preview.isSuccess && !snapshot)) {
216
+ if ((preview.isError || (preview.isSuccess && !snapshot)) && fallbackUnitAmountCents === null) {
187
217
  return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noPricing }), manualTotalControls] }));
188
218
  }
189
219
  if (lines.length === 0) {
@@ -10,6 +10,8 @@ export interface ProductPickerSectionProps {
10
10
  enabled?: boolean;
11
11
  /** When true, hide the product picker and fix the productId (e.g., launched from a product page). */
12
12
  lockProduct?: boolean;
13
+ /** When false, product options are selected downstream as quantities instead of a single global choice. */
14
+ showOptionPicker?: boolean;
13
15
  labels?: {
14
16
  product?: string;
15
17
  productSearchPlaceholder?: string;
@@ -23,5 +25,5 @@ export interface ProductPickerSectionProps {
23
25
  * replace the whole section (e.g., with a typeahead against a custom catalog)
24
26
  * without reimplementing the cascade logic, or keep this one and swap labels.
25
27
  */
26
- export declare function ProductPickerSection({ value, onChange, enabled, lockProduct, labels, }: ProductPickerSectionProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function ProductPickerSection({ value, onChange, enabled, lockProduct, showOptionPicker, labels, }: ProductPickerSectionProps): import("react/jsx-runtime").JSX.Element;
27
29
  //# sourceMappingURL=product-picker-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"product-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/product-picker-section.tsx"],"names":[],"mappings":"AA+BA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC7C,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qGAAqG;IACrG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,WAAmB,EACnB,MAAM,GACP,EAAE,yBAAyB,2CAyI3B"}
1
+ {"version":3,"file":"product-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/product-picker-section.tsx"],"names":[],"mappings":"AA+BA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC7C,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qGAAqG;IACrG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2GAA2G;IAC3G,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,WAAmB,EACnB,gBAAuB,EACvB,MAAM,GACP,EAAE,yBAAyB,2CAyI3B"}
@@ -12,7 +12,7 @@ const OPTION_NONE = "__none__";
12
12
  * replace the whole section (e.g., with a typeahead against a custom catalog)
13
13
  * without reimplementing the cascade logic, or keep this one and swap labels.
14
14
  */
15
- export function ProductPickerSection({ value, onChange, enabled = true, lockProduct = false, labels, }) {
15
+ export function ProductPickerSection({ value, onChange, enabled = true, lockProduct = false, showOptionPicker = true, labels, }) {
16
16
  const [productSearch, setProductSearch] = React.useState("");
17
17
  const cachedProductsRef = React.useRef(new Map());
18
18
  const messages = useBookingsUiMessagesOrDefault();
@@ -45,7 +45,7 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
45
45
  const { data: optionsData } = useProductOptions({
46
46
  productId: value.productId || undefined,
47
47
  limit: 50,
48
- enabled: enabled && Boolean(value.productId),
48
+ enabled: enabled && showOptionPicker && Boolean(value.productId),
49
49
  });
50
50
  const options = optionsData?.data ?? [];
51
51
  return (_jsxs(_Fragment, { children: [!lockProduct && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs(Label, { children: [merged.product, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Combobox, { items: products.map((product) => product.id), value: value.productId || null, inputValue: productInputValue, autoHighlight: true, disabled: !enabled, filter: (id, query) => productMatchesPickerSearch(productMap.get(id), query), itemToStringLabel: (id) => resolveProductLabel(id) || id, itemToStringValue: (id) => id, onInputValueChange: (next) => {
@@ -64,7 +64,7 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
64
64
  return (_jsx(ComboboxItem, { value: product.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: product.name }), _jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [product.sellCurrency, product.sellAmountCents != null
65
65
  ? ` · ${product.sellAmountCents / 100}`
66
66
  : ""] })] }) }, product.id));
67
- } }) })] })] })] })), value.productId && options.length > 0 && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.option }), _jsxs(Select, { items: [
67
+ } }) })] })] })] })), showOptionPicker && value.productId && options.length > 0 && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.option }), _jsxs(Select, { items: [
68
68
  { label: merged.optionNone, value: OPTION_NONE },
69
69
  ...options.map((o) => ({ label: o.name, value: o.id })),
70
70
  ], value: value.optionId ?? OPTION_NONE, onValueChange: (v) => onChange({
@@ -3,9 +3,20 @@ export interface RoomsStepperValue {
3
3
  quantities: Record<string, number>;
4
4
  }
5
5
  export declare const emptyRoomsStepperValue: RoomsStepperValue;
6
+ export interface RoomsStepperUnit {
7
+ optionId: string | null;
8
+ optionUnitId: string;
9
+ unitName: string;
10
+ occupancyMax: number | null;
11
+ initial: number | null;
12
+ reserved: number;
13
+ remaining: number | null;
14
+ }
6
15
  export interface RoomsStepperSectionProps {
7
16
  value: RoomsStepperValue;
8
17
  onChange: (value: RoomsStepperValue) => void;
18
+ /** Product whose options become selectable room quantity rows. */
19
+ productId?: string;
9
20
  /**
10
21
  * Departure the operator picked. Departure-specific availability wins
11
22
  * when present; otherwise the section falls back to option-level units.
@@ -17,6 +28,7 @@ export interface RoomsStepperSectionProps {
17
28
  */
18
29
  optionId?: string | null;
19
30
  enabled?: boolean;
31
+ onUnitsChange?: (units: RoomsStepperUnit[]) => void;
20
32
  labels?: {
21
33
  heading?: string;
22
34
  noOption?: string;
@@ -29,8 +41,8 @@ export interface RoomsStepperSectionProps {
29
41
  /**
30
42
  * Rooms / per-unit stepper for booking-create flows. Drives
31
43
  * `GET /v1/availability/slots/:id/unit-availability` from #235 when a
32
- * departure is selected, and option-level units before departure selection,
33
- * so operators can still build "2 double rooms and 1 single" drafts.
44
+ * departure is selected, and product option-level units before departure
45
+ * selection, so operators can build "2 double rooms and 1 single" drafts.
34
46
  *
35
47
  * The section only tracks **intent** (how many of each unit the operator
36
48
  * wants to book). Actual hold/reservation happens when the parent submits
@@ -46,5 +58,5 @@ export interface RoomsStepperSectionProps {
46
58
  * disables the "+" button — we don't let the UI submit a request that
47
59
  * would 409 at insert time.
48
60
  */
49
- export declare function RoomsStepperSection({ value, onChange, slotId, optionId, enabled, labels, }: RoomsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
61
+ export declare function RoomsStepperSection({ value, onChange, productId, slotId, optionId, enabled, onUnitsChange, labels, }: RoomsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
50
62
  //# sourceMappingURL=rooms-stepper-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rooms-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/rooms-stepper-section.tsx"],"names":[],"mappings":"AAQA,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAsC,CAAA;AAE3E,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,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;KACnB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,OAAc,EACd,MAAM,GACP,EAAE,wBAAwB,2CAmG1B"}
1
+ {"version":3,"file":"rooms-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/rooms-stepper-section.tsx"],"names":[],"mappings":"AAgBA,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAsC,CAAA;AAE3E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,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,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,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,gBAAgB,EAAE,KAAK,IAAI,CAAA;IACnD,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;KACnB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAc,EACd,aAAa,EACb,MAAM,GACP,EAAE,wBAAwB,2CAmI1B"}
@@ -1,16 +1,18 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQueries } from "@tanstack/react-query";
3
4
  import { useSlotUnitAvailability } from "@voyantjs/availability-react";
4
- import { useOptionUnits } from "@voyantjs/products-react";
5
+ import { getOptionUnitsQueryOptions, useProductOptions, useVoyantProductsContext, } from "@voyantjs/products-react";
5
6
  import { Button, Label } from "@voyantjs/ui/components";
6
7
  import { Minus, Plus } from "lucide-react";
8
+ import * as React from "react";
7
9
  import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
8
10
  export const emptyRoomsStepperValue = { quantities: {} };
9
11
  /**
10
12
  * Rooms / per-unit stepper for booking-create flows. Drives
11
13
  * `GET /v1/availability/slots/:id/unit-availability` from #235 when a
12
- * departure is selected, and option-level units before departure selection,
13
- * so operators can still build "2 double rooms and 1 single" drafts.
14
+ * departure is selected, and product option-level units before departure
15
+ * selection, so operators can build "2 double rooms and 1 single" drafts.
14
16
  *
15
17
  * The section only tracks **intent** (how many of each unit the operator
16
18
  * wants to book). Actual hold/reservation happens when the parent submits
@@ -26,29 +28,56 @@ export const emptyRoomsStepperValue = { quantities: {} };
26
28
  * disables the "+" button — we don't let the UI submit a request that
27
29
  * would 409 at insert time.
28
30
  */
29
- export function RoomsStepperSection({ value, onChange, slotId, optionId, enabled = true, labels, }) {
31
+ export function RoomsStepperSection({ value, onChange, productId, slotId, optionId, enabled = true, onUnitsChange, labels, }) {
32
+ const productsClient = useVoyantProductsContext();
30
33
  const messages = useBookingsUiMessagesOrDefault();
31
34
  const merged = { ...messages.roomsStepperSection.labels, ...labels };
32
35
  const availability = useSlotUnitAvailability({ slotId, enabled: enabled && Boolean(slotId) });
33
- const optionUnits = useOptionUnits({
34
- optionId: optionId ?? undefined,
36
+ const optionsQuery = useProductOptions({
37
+ productId,
38
+ status: "active",
35
39
  limit: 100,
36
- enabled: enabled && !slotId && Boolean(optionId),
40
+ enabled: enabled && !slotId && Boolean(productId),
37
41
  });
38
- const units = slotId
39
- ? (availability.data?.data ?? [])
40
- : (optionUnits.data?.data ?? []).map((unit) => ({
41
- optionUnitId: unit.id,
42
- unitName: unit.name,
43
- occupancyMax: unit.occupancyMax,
44
- initial: null,
45
- reserved: 0,
46
- remaining: unit.maxQuantity ?? null,
47
- }));
48
- if (!slotId && !optionId) {
42
+ const productOptions = React.useMemo(() => {
43
+ const options = optionsQuery.data?.data ?? [];
44
+ if (!optionId)
45
+ return options;
46
+ const selected = options.find((option) => option.id === optionId);
47
+ const rest = options.filter((option) => option.id !== optionId);
48
+ return selected ? [selected, ...rest] : options;
49
+ }, [optionsQuery.data?.data, optionId]);
50
+ const optionUnitQueries = useQueries({
51
+ queries: productOptions.map((option) => ({
52
+ ...getOptionUnitsQueryOptions(productsClient, {
53
+ optionId: option.id,
54
+ limit: 100,
55
+ }),
56
+ enabled: enabled && !slotId && Boolean(productId),
57
+ })),
58
+ });
59
+ const optionUnitRows = React.useMemo(() => {
60
+ const rows = [];
61
+ productOptions.forEach((option, index) => {
62
+ const units = optionUnitQueries[index]?.data?.data ?? [];
63
+ rows.push(...units.map((unit) => optionUnitToStepperUnit(option, unit, units.length)));
64
+ });
65
+ return rows;
66
+ }, [productOptions, optionUnitQueries]);
67
+ const availabilityUnitRows = React.useMemo(() => (availability.data?.data ?? []).map((unit) => ({
68
+ ...unit,
69
+ optionId: optionId ?? null,
70
+ })), [availability.data?.data, optionId]);
71
+ const units = slotId ? availabilityUnitRows : optionUnitRows;
72
+ React.useEffect(() => {
73
+ onUnitsChange?.(units);
74
+ }, [onUnitsChange, units]);
75
+ if (!slotId && !productId && !optionId) {
49
76
  return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noOption })] }));
50
77
  }
51
- const loaded = slotId ? availability.isSuccess : optionUnits.isSuccess;
78
+ const loaded = slotId
79
+ ? availability.isSuccess
80
+ : optionsQuery.isSuccess && optionUnitQueries.every((query) => query.isSuccess);
52
81
  if (loaded && units.length === 0) {
53
82
  return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noUnits })] }));
54
83
  }
@@ -69,3 +98,14 @@ export function RoomsStepperSection({ value, onChange, slotId, optionId, enabled
69
98
  return (_jsxs("div", { className: "flex items-center gap-3 rounded-md border px-3 py-2", children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-sm font-medium", children: unit.unitName }), _jsx("div", { className: "text-xs text-muted-foreground", children: remainingLabel })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => setQuantity(unit.optionUnitId, Math.max(0, qty - 1)), disabled: qty <= 0, "aria-label": `${merged.decreaseUnitPrefix} ${unit.unitName}`, children: _jsx(Minus, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "min-w-[1.5rem] text-center text-sm tabular-nums", children: qty }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => setQuantity(unit.optionUnitId, qty + 1), disabled: atMax, "aria-label": `${merged.increaseUnitPrefix} ${unit.unitName}`, children: _jsx(Plus, { className: "h-3.5 w-3.5" }) })] })] }, unit.optionUnitId));
70
99
  }) })] }));
71
100
  }
101
+ function optionUnitToStepperUnit(option, unit, unitCount) {
102
+ return {
103
+ optionId: option.id,
104
+ optionUnitId: unit.id,
105
+ unitName: unitCount === 1 ? option.name : `${option.name} - ${unit.name}`,
106
+ occupancyMax: unit.occupancyMax,
107
+ initial: null,
108
+ reserved: 0,
109
+ remaining: unit.maxQuantity ?? null,
110
+ };
111
+ }
package/dist/i18n/en.js CHANGED
@@ -455,7 +455,7 @@ export const bookingsUiEn = {
455
455
  onRequest: "On request",
456
456
  groupRate: "group rate",
457
457
  empty: "Pick units above to see the breakdown.",
458
- noPricing: "No pricing catalog available for this product.",
458
+ noPricing: "Pricing preview is unavailable for this selection.",
459
459
  confirmedTotal: "Confirmed total",
460
460
  manualTotal: "Manual total",
461
461
  useCatalogTotal: "Use catalog",
@@ -1087,7 +1087,7 @@ export const bookingsUiEn = {
1087
1087
  breakdownOnRequest: "On request",
1088
1088
  breakdownGroupRate: "group rate",
1089
1089
  breakdownEmpty: "Pick units above to see the breakdown.",
1090
- breakdownNoPricing: "No pricing catalog available for this product.",
1090
+ breakdownNoPricing: "Pricing preview is unavailable for this selection.",
1091
1091
  breakdownConfirmedTotal: "Confirmed total",
1092
1092
  breakdownManualTotal: "Manual total",
1093
1093
  breakdownUseCatalogTotal: "Use catalog",
package/dist/i18n/ro.js CHANGED
@@ -455,7 +455,7 @@ export const bookingsUiRo = {
455
455
  onRequest: "La cerere",
456
456
  groupRate: "tarif de grup",
457
457
  empty: "Alege unitatile de mai sus pentru a vedea descompunerea.",
458
- noPricing: "Nu exista catalog de preturi pentru acest produs.",
458
+ noPricing: "Previzualizarea pretului nu este disponibila pentru aceasta selectie.",
459
459
  confirmedTotal: "Total confirmat",
460
460
  manualTotal: "Total manual",
461
461
  useCatalogTotal: "Foloseste catalogul",
@@ -1087,7 +1087,7 @@ export const bookingsUiRo = {
1087
1087
  breakdownOnRequest: "La cerere",
1088
1088
  breakdownGroupRate: "tarif de grup",
1089
1089
  breakdownEmpty: "Alege unitatile de mai sus pentru a vedea descompunerea.",
1090
- breakdownNoPricing: "Nu exista catalog de preturi pentru acest produs.",
1090
+ breakdownNoPricing: "Previzualizarea pretului nu este disponibila pentru aceasta selectie.",
1091
1091
  breakdownConfirmedTotal: "Total confirmat",
1092
1092
  breakdownManualTotal: "Total manual",
1093
1093
  breakdownUseCatalogTotal: "Foloseste catalogul",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/bookings-ui",
3
- "version": "0.50.7",
3
+ "version": "0.51.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,20 +51,20 @@
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.50.7",
55
- "@voyantjs/bookings-react": "0.50.7",
56
- "@voyantjs/catalog": "0.50.7",
57
- "@voyantjs/catalog-react": "0.50.7",
58
- "@voyantjs/crm-react": "0.50.7",
59
- "@voyantjs/crm-ui": "0.50.7",
60
- "@voyantjs/finance-react": "0.50.7",
61
- "@voyantjs/legal-react": "0.50.7",
62
- "@voyantjs/products-react": "0.50.7",
63
- "@voyantjs/suppliers-react": "0.50.7",
64
- "@voyantjs/ui": "0.50.7"
54
+ "@voyantjs/availability-react": "0.51.0",
55
+ "@voyantjs/bookings-react": "0.51.0",
56
+ "@voyantjs/catalog": "0.51.0",
57
+ "@voyantjs/catalog-react": "0.51.0",
58
+ "@voyantjs/crm-react": "0.51.0",
59
+ "@voyantjs/crm-ui": "0.51.0",
60
+ "@voyantjs/finance-react": "0.51.0",
61
+ "@voyantjs/legal-react": "0.51.0",
62
+ "@voyantjs/products-react": "0.51.0",
63
+ "@voyantjs/suppliers-react": "0.51.0",
64
+ "@voyantjs/ui": "0.51.0"
65
65
  },
66
66
  "dependencies": {
67
- "@voyantjs/i18n": "0.50.7"
67
+ "@voyantjs/i18n": "0.51.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@tanstack/react-query": "^5.96.2",
@@ -77,18 +77,18 @@
77
77
  "typescript": "^6.0.2",
78
78
  "vitest": "^4.1.2",
79
79
  "zod": "^4.3.6",
80
- "@voyantjs/availability-react": "0.50.7",
81
- "@voyantjs/bookings-react": "0.50.7",
82
- "@voyantjs/catalog": "0.50.7",
83
- "@voyantjs/catalog-react": "0.50.7",
84
- "@voyantjs/crm-react": "0.50.7",
85
- "@voyantjs/crm-ui": "0.50.7",
86
- "@voyantjs/finance-react": "0.50.7",
87
- "@voyantjs/legal-react": "0.50.7",
88
- "@voyantjs/products-react": "0.50.7",
89
- "@voyantjs/suppliers-react": "0.50.7",
80
+ "@voyantjs/availability-react": "0.51.0",
81
+ "@voyantjs/bookings-react": "0.51.0",
82
+ "@voyantjs/catalog": "0.51.0",
83
+ "@voyantjs/catalog-react": "0.51.0",
84
+ "@voyantjs/crm-react": "0.51.0",
85
+ "@voyantjs/crm-ui": "0.51.0",
86
+ "@voyantjs/finance-react": "0.51.0",
87
+ "@voyantjs/legal-react": "0.51.0",
88
+ "@voyantjs/products-react": "0.51.0",
89
+ "@voyantjs/suppliers-react": "0.51.0",
90
90
  "@voyantjs/voyant-typescript-config": "0.1.0",
91
- "@voyantjs/ui": "0.50.7"
91
+ "@voyantjs/ui": "0.51.0"
92
92
  },
93
93
  "files": [
94
94
  "dist",