@voyantjs/bookings-ui 0.50.7 → 0.50.8
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.map +1 -1
- package/dist/components/booking-create-dialog.js +32 -6
- package/dist/components/booking-create-utils.d.ts +1 -0
- package/dist/components/booking-create-utils.d.ts.map +1 -1
- package/dist/components/booking-create-utils.js +2 -0
- package/dist/components/person-picker-section.d.ts.map +1 -1
- package/dist/components/person-picker-section.js +14 -10
- package/dist/components/price-breakdown-section.d.ts +3 -1
- package/dist/components/price-breakdown-section.d.ts.map +1 -1
- package/dist/components/price-breakdown-section.js +39 -9
- package/dist/components/product-picker-section.d.ts +3 -1
- package/dist/components/product-picker-section.d.ts.map +1 -1
- package/dist/components/product-picker-section.js +3 -3
- package/dist/components/rooms-stepper-section.d.ts +15 -3
- package/dist/components/rooms-stepper-section.d.ts.map +1 -1
- package/dist/components/rooms-stepper-section.js +59 -19
- package/dist/i18n/en.js +2 -2
- package/dist/i18n/ro.js +2 -2
- package/package.json +24 -24
|
@@ -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;
|
|
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,
|
|
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.
|
|
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,
|
|
@@ -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,
|
|
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,
|
|
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
|
|
75
|
-
const
|
|
76
|
-
|
|
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,
|
|
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 ?
|
|
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,
|
|
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 ? (
|
|
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":"
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
33
|
-
* so operators can
|
|
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":"
|
|
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 {
|
|
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
|
|
13
|
-
* so operators can
|
|
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
|
|
34
|
-
|
|
36
|
+
const optionsQuery = useProductOptions({
|
|
37
|
+
productId,
|
|
38
|
+
status: "active",
|
|
35
39
|
limit: 100,
|
|
36
|
-
enabled: enabled && !slotId && Boolean(
|
|
40
|
+
enabled: enabled && !slotId && Boolean(productId),
|
|
37
41
|
});
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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.
|
|
3
|
+
"version": "0.50.8",
|
|
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.
|
|
55
|
-
"@voyantjs/bookings-react": "0.50.
|
|
56
|
-
"@voyantjs/catalog": "0.50.
|
|
57
|
-
"@voyantjs/catalog-react": "0.50.
|
|
58
|
-
"@voyantjs/crm-react": "0.50.
|
|
59
|
-
"@voyantjs/crm-ui": "0.50.
|
|
60
|
-
"@voyantjs/finance-react": "0.50.
|
|
61
|
-
"@voyantjs/legal-react": "0.50.
|
|
62
|
-
"@voyantjs/products-react": "0.50.
|
|
63
|
-
"@voyantjs/suppliers-react": "0.50.
|
|
64
|
-
"@voyantjs/ui": "0.50.
|
|
54
|
+
"@voyantjs/availability-react": "0.50.8",
|
|
55
|
+
"@voyantjs/bookings-react": "0.50.8",
|
|
56
|
+
"@voyantjs/catalog": "0.50.8",
|
|
57
|
+
"@voyantjs/catalog-react": "0.50.8",
|
|
58
|
+
"@voyantjs/crm-react": "0.50.8",
|
|
59
|
+
"@voyantjs/crm-ui": "0.50.8",
|
|
60
|
+
"@voyantjs/finance-react": "0.50.8",
|
|
61
|
+
"@voyantjs/legal-react": "0.50.8",
|
|
62
|
+
"@voyantjs/products-react": "0.50.8",
|
|
63
|
+
"@voyantjs/suppliers-react": "0.50.8",
|
|
64
|
+
"@voyantjs/ui": "0.50.8"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@voyantjs/i18n": "0.50.
|
|
67
|
+
"@voyantjs/i18n": "0.50.8"
|
|
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.
|
|
81
|
-
"@voyantjs/bookings-react": "0.50.
|
|
82
|
-
"@voyantjs/catalog": "0.50.
|
|
83
|
-
"@voyantjs/catalog-react": "0.50.
|
|
84
|
-
"@voyantjs/crm-react": "0.50.
|
|
85
|
-
"@voyantjs/crm-ui": "0.50.
|
|
86
|
-
"@voyantjs/finance-react": "0.50.
|
|
87
|
-
"@voyantjs/legal-react": "0.50.
|
|
88
|
-
"@voyantjs/products-react": "0.50.
|
|
89
|
-
"@voyantjs/suppliers-react": "0.50.
|
|
80
|
+
"@voyantjs/availability-react": "0.50.8",
|
|
81
|
+
"@voyantjs/bookings-react": "0.50.8",
|
|
82
|
+
"@voyantjs/catalog": "0.50.8",
|
|
83
|
+
"@voyantjs/catalog-react": "0.50.8",
|
|
84
|
+
"@voyantjs/crm-react": "0.50.8",
|
|
85
|
+
"@voyantjs/crm-ui": "0.50.8",
|
|
86
|
+
"@voyantjs/finance-react": "0.50.8",
|
|
87
|
+
"@voyantjs/legal-react": "0.50.8",
|
|
88
|
+
"@voyantjs/products-react": "0.50.8",
|
|
89
|
+
"@voyantjs/suppliers-react": "0.50.8",
|
|
90
90
|
"@voyantjs/voyant-typescript-config": "0.1.0",
|
|
91
|
-
"@voyantjs/ui": "0.50.
|
|
91
|
+
"@voyantjs/ui": "0.50.8"
|
|
92
92
|
},
|
|
93
93
|
"files": [
|
|
94
94
|
"dist",
|