@voyantjs/bookings-ui 0.50.6 → 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 +63 -10
- 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/payment-schedule-section.d.ts +3 -0
- package/dist/components/payment-schedule-section.d.ts.map +1 -1
- package/dist/components/payment-schedule-section.js +22 -2
- 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 +7 -6
- package/dist/components/rooms-stepper-section.d.ts +24 -7
- package/dist/components/rooms-stepper-section.d.ts.map +1 -1
- package/dist/components/rooms-stepper-section.js +64 -8
- package/dist/i18n/en.d.ts +8 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +10 -2
- package/dist/i18n/messages.d.ts +8 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +16 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +8 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +10 -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);
|
|
@@ -183,12 +198,13 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
183
198
|
: { productId: defaultProductId, optionId: null });
|
|
184
199
|
}
|
|
185
200
|
}, [enabled, defaultProductId]);
|
|
186
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally only
|
|
201
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: booking-create intentionally resets transient departure state only when product id changes; option changes are reconciled against the selected departure below.
|
|
187
202
|
React.useEffect(() => {
|
|
188
203
|
setSlotId(null);
|
|
189
204
|
setRooms(emptyRoomsStepperValue);
|
|
205
|
+
setRoomUnits([]);
|
|
190
206
|
setSharedRoom(emptySharedRoomValue);
|
|
191
|
-
}, [product.productId
|
|
207
|
+
}, [product.productId]);
|
|
192
208
|
const [slotsFromIso, setSlotsFromIso] = React.useState(() => new Date().toISOString());
|
|
193
209
|
React.useEffect(() => {
|
|
194
210
|
if (enabled && product.productId)
|
|
@@ -201,12 +217,36 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
201
217
|
limit: 100,
|
|
202
218
|
enabled: enabled && Boolean(product.productId),
|
|
203
219
|
});
|
|
204
|
-
const
|
|
220
|
+
const allOpenSlots = React.useMemo(() => {
|
|
205
221
|
return getBookableDepartureSlots(slotsData?.data ?? [], {
|
|
222
|
+
nowIso: slotsFromIso,
|
|
223
|
+
optionId: null,
|
|
224
|
+
});
|
|
225
|
+
}, [slotsData?.data, slotsFromIso]);
|
|
226
|
+
const slots = React.useMemo(() => {
|
|
227
|
+
const optionSlots = getBookableDepartureSlots(slotsData?.data ?? [], {
|
|
206
228
|
nowIso: slotsFromIso,
|
|
207
229
|
optionId: product.optionId,
|
|
208
230
|
});
|
|
209
|
-
|
|
231
|
+
return optionSlots.length > 0 ? optionSlots : allOpenSlots;
|
|
232
|
+
}, [slotsData?.data, slotsFromIso, product.optionId, allOpenSlots]);
|
|
233
|
+
const setSelectedSlot = React.useCallback((nextSlotId) => {
|
|
234
|
+
const selectedSlot = nextSlotId ? allOpenSlots.find((slot) => slot.id === nextSlotId) : null;
|
|
235
|
+
if (selectedSlot?.optionId && selectedSlot.optionId !== product.optionId) {
|
|
236
|
+
setProduct((prev) => ({ ...prev, optionId: selectedSlot.optionId }));
|
|
237
|
+
}
|
|
238
|
+
setSlotId(nextSlotId);
|
|
239
|
+
}, [allOpenSlots, product.optionId]);
|
|
240
|
+
React.useEffect(() => {
|
|
241
|
+
setRooms(emptyRoomsStepperValue);
|
|
242
|
+
setRoomUnits([]);
|
|
243
|
+
if (!slotId || !product.optionId)
|
|
244
|
+
return;
|
|
245
|
+
const selectedSlot = allOpenSlots.find((slot) => slot.id === slotId);
|
|
246
|
+
if (selectedSlot?.optionId && selectedSlot.optionId !== product.optionId) {
|
|
247
|
+
setSlotId(null);
|
|
248
|
+
}
|
|
249
|
+
}, [allOpenSlots, product.optionId, slotId]);
|
|
210
250
|
const formatSlotLabel = React.useCallback((slot) => {
|
|
211
251
|
const date = formatDate(slot.startsAt, {
|
|
212
252
|
year: "numeric",
|
|
@@ -222,15 +262,18 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
222
262
|
slotId: slotId ?? undefined,
|
|
223
263
|
enabled: enabled && Boolean(slotId),
|
|
224
264
|
});
|
|
265
|
+
const handleRoomUnitsChange = React.useCallback((units) => {
|
|
266
|
+
setRoomUnits((prev) => (sameRoomUnits(prev, units) ? prev : units));
|
|
267
|
+
}, []);
|
|
225
268
|
const roomUnitOptions = React.useMemo(() => {
|
|
226
|
-
const units = slotUnitAvailability.data?.data ?? [];
|
|
269
|
+
const units = roomUnits.length > 0 ? roomUnits : (slotUnitAvailability.data?.data ?? []);
|
|
227
270
|
if (units.length === 0)
|
|
228
271
|
return [];
|
|
229
272
|
return units
|
|
230
273
|
.filter((unit) => (rooms.quantities[unit.optionUnitId] ?? 0) > 0)
|
|
231
274
|
.map((unit) => {
|
|
232
275
|
const qty = rooms.quantities[unit.optionUnitId] ?? 0;
|
|
233
|
-
const occupancyMax = 1;
|
|
276
|
+
const occupancyMax = Math.max(1, unit.occupancyMax ?? 1);
|
|
234
277
|
const seats = qty * occupancyMax;
|
|
235
278
|
const assigned = travelers.travelers.filter((traveler) => traveler.roomUnitId === unit.optionUnitId).length;
|
|
236
279
|
return {
|
|
@@ -239,13 +282,14 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
239
282
|
remainingCapacity: Math.max(0, seats - assigned),
|
|
240
283
|
};
|
|
241
284
|
});
|
|
242
|
-
}, [slotUnitAvailability.data, rooms.quantities, travelers.travelers]);
|
|
285
|
+
}, [roomUnits, slotUnitAvailability.data, rooms.quantities, travelers.travelers]);
|
|
243
286
|
// Currency placeholder — used for voucher + payment schedule display.
|
|
244
287
|
// Consumers hooking in real product data should override this by wrapping
|
|
245
288
|
// the component or swapping in their own currency-aware hook.
|
|
246
289
|
const currency = messages.bookingCreateDialog.labels.currency;
|
|
247
290
|
const pricingCurrency = pricing?.currency ?? currency;
|
|
248
291
|
const pricingTotalAmountCents = pricing?.confirmedAmountCents ?? undefined;
|
|
292
|
+
const roomUnitLabels = React.useMemo(() => Object.fromEntries(roomUnits.map((unit) => [unit.optionUnitId, unit.unitName])), [roomUnits]);
|
|
249
293
|
const createBookingMutation = useBookingCreateMutation();
|
|
250
294
|
const statusMutation = useBookingStatusByIdMutation();
|
|
251
295
|
const handleSubmit = async () => {
|
|
@@ -284,7 +328,12 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
284
328
|
return;
|
|
285
329
|
}
|
|
286
330
|
const paymentSchedules = paymentScheduleToRows(paymentSchedule, pricingCurrency, confirmedSellAmountCents);
|
|
287
|
-
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);
|
|
288
337
|
const travelerRows = travelersToRows(travelers);
|
|
289
338
|
const voucherRedemption = voucher.picked && voucher.picked.remainingAmountCents != null
|
|
290
339
|
? {
|
|
@@ -361,8 +410,9 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
361
410
|
const isSubmitting = createBookingMutation.isPending || statusMutation.isPending;
|
|
362
411
|
return (_jsxs(_Fragment, { children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductPickerSection, { value: product, onChange: setProduct, enabled: enabled, lockProduct: Boolean(defaultProductId), labels: {
|
|
363
412
|
optionNone: messages.bookingCreateDialog.labels.noSpecificOption,
|
|
364
|
-
} }), 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) =>
|
|
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: {
|
|
365
414
|
heading: messages.bookingCreateDialog.labels.roomsHeading,
|
|
415
|
+
noOption: messages.bookingCreateDialog.labels.roomsNoOption,
|
|
366
416
|
noSlot: messages.bookingCreateDialog.labels.roomsNoSlot,
|
|
367
417
|
noUnits: messages.bookingCreateDialog.labels.roomsNoUnits,
|
|
368
418
|
remaining: messages.bookingCreateDialog.labels.roomsRemaining,
|
|
@@ -397,7 +447,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
397
447
|
noRoom: messages.bookingCreateDialog.labels.travelerNoRoom,
|
|
398
448
|
remove: messages.bookingCreateDialog.labels.travelerRemove,
|
|
399
449
|
empty: messages.bookingCreateDialog.labels.travelerEmpty,
|
|
400
|
-
} })) : 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: {
|
|
401
451
|
heading: messages.bookingCreateDialog.labels.breakdownHeading,
|
|
402
452
|
total: messages.bookingCreateDialog.labels.breakdownTotal,
|
|
403
453
|
onRequest: messages.bookingCreateDialog.labels.breakdownOnRequest,
|
|
@@ -429,6 +479,9 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
429
479
|
secondInstallment: messages.bookingCreateDialog.labels.paymentSecondInstallment,
|
|
430
480
|
preset5050: messages.bookingCreateDialog.labels.paymentPreset5050,
|
|
431
481
|
unpaidHint: messages.bookingCreateDialog.labels.paymentUnpaidHint,
|
|
482
|
+
totalDue: messages.bookingCreateDialog.labels.paymentTotalDue,
|
|
483
|
+
scheduledTotal: messages.bookingCreateDialog.labels.paymentScheduledTotal,
|
|
484
|
+
remaining: messages.bookingCreateDialog.labels.paymentRemaining,
|
|
432
485
|
alreadyPaid: messages.bookingCreateDialog.labels.paymentAlreadyPaid,
|
|
433
486
|
paymentDate: messages.bookingCreateDialog.labels.paymentDate,
|
|
434
487
|
paymentMethod: messages.bookingCreateDialog.labels.paymentMethod,
|
|
@@ -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,
|
|
@@ -52,6 +52,9 @@ export interface PaymentScheduleSectionProps {
|
|
|
52
52
|
secondInstallment?: string;
|
|
53
53
|
preset5050?: string;
|
|
54
54
|
unpaidHint?: string;
|
|
55
|
+
totalDue?: string;
|
|
56
|
+
scheduledTotal?: string;
|
|
57
|
+
remaining?: string;
|
|
55
58
|
alreadyPaid?: string;
|
|
56
59
|
paymentDate?: string;
|
|
57
60
|
paymentMethod?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-schedule-section.d.ts","sourceRoot":"","sources":["../../src/components/payment-schedule-section.tsx"],"names":[],"mappings":"AAiBA,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAEzE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,mBAAmB,CAAA;IACzB,wEAAwE;IACxE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,wEAAwE;IACxE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qDAAqD;IACrD,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,eAAe,EAAE,OAAO,CAAA;IACxB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,iBAAiB,EAAE,MAAM,CAAA;IACzB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,kBAAkB,EAAE,OAAO,CAAA;IAC3B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,qBAAqB,EAAE,OAAO,CAAA;IAC9B,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,uBAAuB,EAAE,MAAM,CAAA;IAC/B,0BAA0B,EAAE,MAAM,CAAA;IAClC,sBAAsB,EAAE,OAAO,CAAA;IAC/B,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,wBAAwB,EAAE,MAAM,CAAA;IAChC,2BAA2B,EAAE,MAAM,CAAA;CACpC;AAED,eAAO,MAAM,yBAAyB,EAAE,oBAyBvC,CAAA;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,oBAAoB,CAAA;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC/C;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,MAAM,GACP,EAAE,2BAA2B,
|
|
1
|
+
{"version":3,"file":"payment-schedule-section.d.ts","sourceRoot":"","sources":["../../src/components/payment-schedule-section.tsx"],"names":[],"mappings":"AAiBA,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAEzE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,mBAAmB,CAAA;IACzB,wEAAwE;IACxE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,wEAAwE;IACxE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qDAAqD;IACrD,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,eAAe,EAAE,OAAO,CAAA;IACxB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,iBAAiB,EAAE,MAAM,CAAA;IACzB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,kBAAkB,EAAE,OAAO,CAAA;IAC3B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,qBAAqB,EAAE,OAAO,CAAA;IAC9B,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,uBAAuB,EAAE,MAAM,CAAA;IAC/B,0BAA0B,EAAE,MAAM,CAAA;IAClC,sBAAsB,EAAE,OAAO,CAAA;IAC/B,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,wBAAwB,EAAE,MAAM,CAAA;IAChC,2BAA2B,EAAE,MAAM,CAAA;CACpC;AAED,eAAO,MAAM,yBAAyB,EAAE,oBAyBvC,CAAA;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,oBAAoB,CAAA;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC/C;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,MAAM,GACP,EAAE,2BAA2B,2CAkO7B"}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Button, Checkbox, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
4
4
|
import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
|
|
5
5
|
import { DatePicker } from "@voyantjs/ui/components/date-picker";
|
|
6
|
-
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
6
|
+
import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
7
7
|
export const emptyPaymentScheduleValue = {
|
|
8
8
|
mode: "unpaid",
|
|
9
9
|
fullDueDate: null,
|
|
@@ -52,6 +52,7 @@ export const emptyPaymentScheduleValue = {
|
|
|
52
52
|
*/
|
|
53
53
|
export function PaymentScheduleSection({ value, onChange, totalAmountCents, currency, labels, }) {
|
|
54
54
|
const messages = useBookingsUiMessagesOrDefault();
|
|
55
|
+
const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
|
|
55
56
|
const merged = { ...messages.paymentScheduleSection.labels, ...labels };
|
|
56
57
|
const set = (patch) => onChange({ ...value, ...patch });
|
|
57
58
|
const modes = [
|
|
@@ -71,6 +72,25 @@ export function PaymentScheduleSection({ value, onChange, totalAmountCents, curr
|
|
|
71
72
|
splitSecondAmountCents: totalAmountCents - half,
|
|
72
73
|
});
|
|
73
74
|
};
|
|
75
|
+
const total = typeof totalAmountCents === "number" ? totalAmountCents : null;
|
|
76
|
+
const scheduledTotal = value.mode === "unpaid"
|
|
77
|
+
? 0
|
|
78
|
+
: value.mode === "full"
|
|
79
|
+
? (total ?? 0)
|
|
80
|
+
: value.mode === "advance"
|
|
81
|
+
? (value.advanceAmountCents ?? 0)
|
|
82
|
+
: (value.splitFirstAmountCents ?? 0) + (value.splitSecondAmountCents ?? 0);
|
|
83
|
+
const remaining = total === null ? null : Math.max(0, total - scheduledTotal);
|
|
84
|
+
const formatAmount = (cents) => {
|
|
85
|
+
if (cents === null)
|
|
86
|
+
return "-";
|
|
87
|
+
return currency
|
|
88
|
+
? formatCurrency(cents / 100, currency)
|
|
89
|
+
: formatNumber(cents / 100, {
|
|
90
|
+
minimumFractionDigits: 2,
|
|
91
|
+
maximumFractionDigits: 2,
|
|
92
|
+
});
|
|
93
|
+
};
|
|
74
94
|
const paymentMethodLabels = messages.bookingPaymentsSummary.paymentMethodLabels;
|
|
75
95
|
const renderPaidFields = (prefix, checked) => {
|
|
76
96
|
const paymentDateKey = `${prefix}PaymentDate`;
|
|
@@ -80,5 +100,5 @@ export function PaymentScheduleSection({ value, onChange, totalAmountCents, curr
|
|
|
80
100
|
const checkboxId = `payment-schedule-${prefix}-already-paid`;
|
|
81
101
|
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border border-dashed p-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: checkboxId, checked: checked, onCheckedChange: (next) => set({ [checkedKey]: next === true }) }), _jsx(Label, { htmlFor: checkboxId, className: "cursor-pointer text-xs", children: merged.alreadyPaid })] }), checked ? (_jsxs("div", { className: "grid gap-2 sm:grid-cols-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentDate }), _jsx(DatePicker, { value: value[paymentDateKey] ?? "", onChange: (nextValue) => set({ [paymentDateKey]: nextValue }) })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentMethod }), _jsxs(Select, { value: value[paymentMethodKey] ?? "bank_transfer", onValueChange: (nextValue) => set({ [paymentMethodKey]: nextValue ?? "bank_transfer" }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ["bank_transfer", "credit_card", "cash", "voucher", "other"].map((method) => (_jsx(SelectItem, { value: method, children: paymentMethodLabels[method === "credit_card" ? "card" : method] }, method))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentReference }), _jsx(Input, { value: value[paymentReferenceKey] ?? "", onChange: (event) => set({ [paymentReferenceKey]: event.target.value }) })] })] })) : null] }));
|
|
82
102
|
};
|
|
83
|
-
return (_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("div", { className: "flex flex-wrap items-center gap-2", children: modes.map((mode) => (_jsx(Button, { type: "button", size: "sm", variant: value.mode === mode.id ? "default" : "ghost", onClick: () => set({ mode: mode.id }), children: mode.label }, mode.id))) }), value.mode === "unpaid" && (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.unpaidHint })), value.mode === "full" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.fullDueDate ?? "", onChange: (nextValue) => set({ fullDueDate: nextValue }) })] }), renderPaidFields("full", value.fullAlreadyPaid)] })), value.mode === "advance" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.amount }), _jsx(CurrencyInput, { value: value.advanceAmountCents, onChange: (next) => set({ advanceAmountCents: next }), currency: currency })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.advanceDueDate ?? "", onChange: (nextValue) => set({ advanceDueDate: nextValue }) })] })] }), renderPaidFields("advance", value.advanceAlreadyPaid)] })), value.mode === "split" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-medium", children: merged.firstInstallment }), totalAmountCents ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handlePreset5050, children: merged.preset5050 })) : null] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitFirstAmountCents, onChange: (next) => set({ splitFirstAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitFirstDueDate ?? "", onChange: (nextValue) => set({ splitFirstDueDate: nextValue }) })] }), renderPaidFields("splitFirst", value.splitFirstAlreadyPaid), _jsx("div", { className: "text-xs font-medium", children: merged.secondInstallment }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitSecondAmountCents, onChange: (next) => set({ splitSecondAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitSecondDueDate ?? "", onChange: (nextValue) => set({ splitSecondDueDate: nextValue }) })] }), renderPaidFields("splitSecond", value.splitSecondAlreadyPaid)] }))] }));
|
|
103
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsxs("div", { className: "grid gap-2 rounded-md bg-muted/40 p-2 text-xs sm:grid-cols-3", children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.totalDue }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(total) })] }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.scheduledTotal }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(scheduledTotal) })] }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.remaining }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(remaining) })] })] }), _jsx("div", { className: "flex flex-wrap items-center gap-2", children: modes.map((mode) => (_jsx(Button, { type: "button", size: "sm", variant: value.mode === mode.id ? "default" : "ghost", onClick: () => set({ mode: mode.id }), children: mode.label }, mode.id))) }), value.mode === "unpaid" && (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.unpaidHint })), value.mode === "full" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.fullDueDate ?? "", onChange: (nextValue) => set({ fullDueDate: nextValue }) })] }), renderPaidFields("full", value.fullAlreadyPaid)] })), value.mode === "advance" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.amount }), _jsx(CurrencyInput, { value: value.advanceAmountCents, onChange: (next) => set({ advanceAmountCents: next }), currency: currency })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.advanceDueDate ?? "", onChange: (nextValue) => set({ advanceDueDate: nextValue }) })] })] }), renderPaidFields("advance", value.advanceAlreadyPaid)] })), value.mode === "split" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-medium", children: merged.firstInstallment }), totalAmountCents ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handlePreset5050, children: merged.preset5050 })) : null] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitFirstAmountCents, onChange: (next) => set({ splitFirstAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitFirstDueDate ?? "", onChange: (nextValue) => set({ splitFirstDueDate: nextValue }) })] }), renderPaidFields("splitFirst", value.splitFirstAlreadyPaid), _jsx("div", { className: "text-xs font-medium", children: merged.secondInstallment }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitSecondAmountCents, onChange: (next) => set({ splitSecondAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitSecondDueDate ?? "", onChange: (nextValue) => set({ splitSecondDueDate: nextValue }) })] }), renderPaidFields("splitSecond", value.splitSecondAlreadyPaid)] }))] }));
|
|
84
104
|
}
|
|
@@ -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,
|
|
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"}
|