@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.
Files changed (30) hide show
  1. package/dist/components/booking-create-dialog.d.ts.map +1 -1
  2. package/dist/components/booking-create-dialog.js +63 -10
  3. package/dist/components/booking-create-utils.d.ts +1 -0
  4. package/dist/components/booking-create-utils.d.ts.map +1 -1
  5. package/dist/components/booking-create-utils.js +2 -0
  6. package/dist/components/payment-schedule-section.d.ts +3 -0
  7. package/dist/components/payment-schedule-section.d.ts.map +1 -1
  8. package/dist/components/payment-schedule-section.js +22 -2
  9. package/dist/components/person-picker-section.d.ts.map +1 -1
  10. package/dist/components/person-picker-section.js +14 -10
  11. package/dist/components/price-breakdown-section.d.ts +3 -1
  12. package/dist/components/price-breakdown-section.d.ts.map +1 -1
  13. package/dist/components/price-breakdown-section.js +39 -9
  14. package/dist/components/product-picker-section.d.ts +3 -1
  15. package/dist/components/product-picker-section.d.ts.map +1 -1
  16. package/dist/components/product-picker-section.js +7 -6
  17. package/dist/components/rooms-stepper-section.d.ts +24 -7
  18. package/dist/components/rooms-stepper-section.d.ts.map +1 -1
  19. package/dist/components/rooms-stepper-section.js +64 -8
  20. package/dist/i18n/en.d.ts +8 -0
  21. package/dist/i18n/en.d.ts.map +1 -1
  22. package/dist/i18n/en.js +10 -2
  23. package/dist/i18n/messages.d.ts +8 -0
  24. package/dist/i18n/messages.d.ts.map +1 -1
  25. package/dist/i18n/provider.d.ts +16 -0
  26. package/dist/i18n/provider.d.ts.map +1 -1
  27. package/dist/i18n/ro.d.ts +8 -0
  28. package/dist/i18n/ro.d.ts.map +1 -1
  29. package/dist/i18n/ro.js +10 -2
  30. 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;AAsMjC,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,GACjB,EAAE,wBAAwB,2CAqB1B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CAkgBxB"}
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 resets when product/option changes
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, product.optionId]);
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 slots = React.useMemo(() => {
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
- }, [slotsData?.data, slotsFromIso, product.optionId]);
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, slotUnitAvailability.data?.data ?? [], pricing);
331
+ const itemLines = itemLinesToRows(rooms.quantities, roomUnits.length > 0
332
+ ? roomUnits
333
+ : (slotUnitAvailability.data?.data ?? []).map((unit) => ({
334
+ ...unit,
335
+ optionId: product.optionId,
336
+ })), pricing);
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) => setSlotId(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, slotId ? (_jsx(RoomsStepperSection, { value: rooms, onChange: setRooms, slotId: slotId, enabled: enabled, labels: {
413
+ }, showOptionPicker: false }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsxs(Select, { value: slotId ?? "__none__", onValueChange: (v) => setSelectedSlot(v === "__none__" ? null : (v ?? null)), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: messages.bookingCreateDialog.placeholders.departure }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__none__", children: messages.bookingCreateDialog.placeholders.departureNone }), slots.length === 0 ? (_jsx(SelectItem, { value: "__empty__", disabled: true, children: messages.bookingCreateDialog.placeholders.departureEmpty })) : (slots.map((slot) => (_jsx(SelectItem, { value: slot.id, children: formatSlotLabel(slot) }, slot.id))))] })] })] })) : null, product.productId ? (_jsx(RoomsStepperSection, { value: rooms, onChange: setRooms, productId: product.productId, slotId: slotId ?? undefined, optionId: product.optionId, enabled: enabled, onUnitsChange: handleRoomUnitsChange, labels: {
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,
@@ -8,6 +8,7 @@ export interface DepartureSlotSearchRecord {
8
8
  status?: string;
9
9
  }
10
10
  export interface BookingCreateUnitLineRecord {
11
+ optionId: string | null;
11
12
  optionUnitId: string;
12
13
  unitName: string;
13
14
  }
@@ -1 +1 @@
1
- {"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE7D,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,MAAM,GAAG,cAAc,CAAC,CAAA;AAEpG,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,KAAK,EAAE,8BAA8B,EAAE,CAAA;CACxC;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIjG;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,EACrD,KAAK,EAAE,MAAM,GACZ,OAAO,CAKT;AAED,wBAAgB,yBAAyB,CAAC,KAAK,SAAS,yBAAyB,EAC/E,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GACA,KAAK,EAAE,CAST;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,KAAK,EAAE,2BAA2B,EAAE,EACpC,OAAO,EAAE,0BAA0B,GAAG,IAAI,GACzC,0BAA0B,EAAE,CAuC9B;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAE7F"}
1
+ {"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE7D,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,MAAM,GAAG,cAAc,CAAC,CAAA;AAEpG,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,KAAK,EAAE,8BAA8B,EAAE,CAAA;CACxC;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIjG;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,EACrD,KAAK,EAAE,MAAM,GACZ,OAAO,CAKT;AAED,wBAAgB,yBAAyB,CAAC,KAAK,SAAS,yBAAyB,EAC/E,KAAK,EAAE,SAAS,KAAK,EAAE,EACvB,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GACA,KAAK,EAAE,CAST;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,KAAK,EAAE,2BAA2B,EAAE,EACpC,OAAO,EAAE,0BAA0B,GAAG,IAAI,GACzC,0BAA0B,EAAE,CAyC9B;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAE7F"}
@@ -28,6 +28,7 @@ export function getBookableDepartureSlots(slots, options) {
28
28
  .sort((left, right) => left.startsAt.localeCompare(right.startsAt));
29
29
  }
30
30
  export function itemLinesToRows(quantities, units, pricing) {
31
+ const unitsById = new Map(units.map((unit) => [unit.optionUnitId, unit]));
31
32
  const unitNames = new Map(units.map((unit) => [unit.optionUnitId, unit.unitName]));
32
33
  const pricedLines = new Map((pricing?.lines ?? []).map((line) => [line.unitId, line]));
33
34
  const selectedLines = Object.entries(quantities).filter(([, quantity]) => quantity > 0);
@@ -54,6 +55,7 @@ export function itemLinesToRows(quantities, units, pricing) {
54
55
  const unitSellAmountCents = pricedLine?.unitAmountCents ??
55
56
  (totalSellAmountCents != null ? Math.floor(totalSellAmountCents / quantity) : null);
56
57
  return {
58
+ optionId: unitsById.get(optionUnitId)?.optionId ?? null,
57
59
  optionUnitId,
58
60
  quantity,
59
61
  title: pricedLine?.label ?? unitNames.get(optionUnitId) ?? null,
@@ -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,2CA8L7B"}
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,2CAiQ1B"}
1
+ {"version":3,"file":"person-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/person-picker-section.tsx"],"names":[],"mappings":"AAiCA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,KAAK,CAAA;AACjD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,cAAc,CAAA;AAEzD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,IAAI,EAAE,gBAAgB,CAAA;IACtB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,SAAS,EAAE,cAAc,CAAA;IACzB,yCAAyC;IACzC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,eAAO,MAAM,cAAc,EAAE,cAK5B,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAMpC,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,gBAAuB,EACvB,MAAM,GACP,EAAE,wBAAwB,2CA+Q1B"}
@@ -29,6 +29,8 @@ export const emptyPersonPickerValue = {
29
29
  export function PersonPickerSection({ value, onChange, enabled = true, showOrganization = true, labels, }) {
30
30
  const [personSearch, setPersonSearch] = React.useState("");
31
31
  const [orgSearch, setOrgSearch] = React.useState("");
32
+ const cachedPeopleRef = React.useRef(new Map());
33
+ const cachedOrgsRef = React.useRef(new Map());
32
34
  const [personInputValue, setPersonInputValue] = React.useState("");
33
35
  const [orgInputValue, setOrgInputValue] = React.useState("");
34
36
  const [personSheetOpen, setPersonSheetOpen] = React.useState(false);
@@ -45,11 +47,12 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
45
47
  enabled: enabled && billingTarget === "person" && Boolean(value.personId),
46
48
  });
47
49
  const people = React.useMemo(() => {
48
- const map = new Map();
50
+ const map = new Map(cachedPeopleRef.current);
49
51
  for (const person of peopleData?.data ?? [])
50
52
  map.set(person.id, person);
51
53
  if (selectedPersonQuery.data)
52
54
  map.set(selectedPersonQuery.data.id, selectedPersonQuery.data);
55
+ cachedPeopleRef.current = map;
53
56
  return Array.from(map.values());
54
57
  }, [peopleData?.data, selectedPersonQuery.data]);
55
58
  const peopleMap = React.useMemo(() => new Map(people.map((person) => [person.id, person])), [people]);
@@ -62,19 +65,20 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
62
65
  enabled: enabled && billingTarget === "organization" && Boolean(value.organizationId),
63
66
  });
64
67
  const orgs = React.useMemo(() => {
65
- const map = new Map();
68
+ const map = new Map(cachedOrgsRef.current);
66
69
  for (const org of orgsData?.data ?? [])
67
70
  map.set(org.id, org);
68
71
  if (selectedOrgQuery.data)
69
72
  map.set(selectedOrgQuery.data.id, selectedOrgQuery.data);
73
+ cachedOrgsRef.current = map;
70
74
  return Array.from(map.values());
71
75
  }, [orgsData?.data, selectedOrgQuery.data]);
72
76
  const orgsMap = React.useMemo(() => new Map(orgs.map((org) => [org.id, org])), [orgs]);
73
77
  const setPerson = (patch) => onChange({ ...value, ...patch });
74
- const selectedPersonLabel = value.personId ? formatPerson(peopleMap.get(value.personId)) : "";
75
- const selectedOrgLabel = value.organizationId
76
- ? (orgsMap.get(value.organizationId)?.name ?? "")
77
- : "";
78
+ const resolvePersonLabel = React.useCallback((personId) => formatPerson(peopleMap.get(personId) ?? cachedPeopleRef.current.get(personId)), [peopleMap]);
79
+ const resolveOrgLabel = React.useCallback((organizationId) => orgsMap.get(organizationId)?.name ?? cachedOrgsRef.current.get(organizationId)?.name ?? "", [orgsMap]);
80
+ const selectedPersonLabel = value.personId ? resolvePersonLabel(value.personId) : "";
81
+ const selectedOrgLabel = value.organizationId ? resolveOrgLabel(value.organizationId) : "";
78
82
  React.useEffect(() => {
79
83
  if (selectedPersonLabel)
80
84
  setPersonInputValue(selectedPersonLabel);
@@ -83,7 +87,7 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
83
87
  if (selectedOrgLabel)
84
88
  setOrgInputValue(selectedOrgLabel);
85
89
  }, [selectedOrgLabel]);
86
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.billTo }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs(Button, { type: "button", variant: billingTarget === "person" ? "default" : "outline", onClick: () => setPerson({ billTo: "person", organizationId: null }), disabled: !enabled, children: [_jsx(User, { className: "mr-2 h-4 w-4" }), merged.billToPerson] }), _jsxs(Button, { type: "button", variant: billingTarget === "organization" ? "default" : "outline", onClick: () => setPerson({ billTo: "organization", personId: "" }), disabled: !enabled || !showOrganization, children: [_jsx(Building2, { className: "mr-2 h-4 w-4" }), merged.billToOrganization] })] })] }), billingTarget === "person" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.person, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setPersonSheetOpen(true), disabled: !enabled, children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewPerson] })] }), _jsxs(Combobox, { items: people.map((person) => person.id), value: value.personId || null, inputValue: personInputValue, autoHighlight: true, disabled: !enabled, itemToStringValue: (id) => formatPerson(peopleMap.get(id)), onInputValueChange: (next) => {
90
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.billTo }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs(Button, { type: "button", variant: billingTarget === "person" ? "default" : "outline", onClick: () => setPerson({ billTo: "person", organizationId: null }), disabled: !enabled, children: [_jsx(User, { className: "mr-2 h-4 w-4" }), merged.billToPerson] }), _jsxs(Button, { type: "button", variant: billingTarget === "organization" ? "default" : "outline", onClick: () => setPerson({ billTo: "organization", personId: "" }), disabled: !enabled || !showOrganization, children: [_jsx(Building2, { className: "mr-2 h-4 w-4" }), merged.billToOrganization] })] })] }), billingTarget === "person" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.person, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setPersonSheetOpen(true), disabled: !enabled, children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewPerson] })] }), _jsxs(Combobox, { items: people.map((person) => person.id), value: value.personId || null, inputValue: personInputValue, autoHighlight: true, disabled: !enabled, itemToStringLabel: (id) => resolvePersonLabel(id) || id, itemToStringValue: (id) => id, onInputValueChange: (next) => {
87
91
  setPersonInputValue(next);
88
92
  setPersonSearch(next);
89
93
  if (!next)
@@ -91,13 +95,13 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
91
95
  }, onValueChange: (next) => {
92
96
  const personId = next ?? "";
93
97
  setPerson({ personId });
94
- setPersonInputValue(personId ? formatPerson(peopleMap.get(personId)) : "");
98
+ setPersonInputValue(personId ? resolvePersonLabel(personId) : "");
95
99
  }, children: [_jsx(ComboboxInput, { placeholder: merged.personSearchPlaceholder, showClear: !!value.personId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.personEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
96
100
  const person = peopleMap.get(id);
97
101
  if (!person)
98
102
  return null;
99
103
  return (_jsx(ComboboxItem, { value: person.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: formatPersonName(person) }), person.email ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: person.email })) : null] }) }, person.id));
100
- } }) })] })] })] })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.organization, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setOrgSheetOpen(true), disabled: !enabled, children: [_jsx(Building2, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewOrganization] })] }), _jsxs(Combobox, { items: orgs.map((org) => org.id), value: value.organizationId ?? null, inputValue: orgInputValue, autoHighlight: true, disabled: !enabled, itemToStringValue: (id) => orgsMap.get(id)?.name ?? "", onInputValueChange: (next) => {
104
+ } }) })] })] })] })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.organization, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setOrgSheetOpen(true), disabled: !enabled, children: [_jsx(Building2, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewOrganization] })] }), _jsxs(Combobox, { items: orgs.map((org) => org.id), value: value.organizationId ?? null, inputValue: orgInputValue, autoHighlight: true, disabled: !enabled, itemToStringLabel: (id) => resolveOrgLabel(id) || id, itemToStringValue: (id) => id, onInputValueChange: (next) => {
101
105
  setOrgInputValue(next);
102
106
  setOrgSearch(next);
103
107
  if (!next)
@@ -105,7 +109,7 @@ export function PersonPickerSection({ value, onChange, enabled = true, showOrgan
105
109
  }, onValueChange: (next) => {
106
110
  const organizationId = next ?? null;
107
111
  setPerson({ organizationId });
108
- setOrgInputValue(organizationId ? (orgsMap.get(organizationId)?.name ?? "") : "");
112
+ setOrgInputValue(organizationId ? resolveOrgLabel(organizationId) : "");
109
113
  }, children: [_jsx(ComboboxInput, { placeholder: merged.organizationSearchPlaceholder, showClear: !!value.organizationId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.organizationEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
110
114
  const org = orgsMap.get(id);
111
115
  if (!org)
@@ -27,6 +27,8 @@ export interface PriceBreakdownSectionProps {
27
27
  optionId?: string | null;
28
28
  /** Quantity per option_unit id, typically from RoomsStepperSection. */
29
29
  unitQuantities: Record<string, number>;
30
+ /** Display labels keyed by option_unit id. */
31
+ unitLabels?: Record<string, string>;
30
32
  /**
31
33
  * Force a specific catalog. Defaults to the public catalog the storefront
32
34
  * uses — matches what a customer would see.
@@ -60,5 +62,5 @@ export interface PriceBreakdownSectionProps {
60
62
  * - `free` / `included` — render 0.00 without an on-request badge.
61
63
  * - `on_request` / anything else — render "On request"; total excludes it.
62
64
  */
63
- export declare function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, onChange, }: PriceBreakdownSectionProps): import("react/jsx-runtime").JSX.Element | null;
65
+ export declare function PriceBreakdownSection({ productId, optionId, unitQuantities, unitLabels, catalogId, labels, onChange, }: PriceBreakdownSectionProps): import("react/jsx-runtime").JSX.Element | null;
64
66
  //# sourceMappingURL=price-breakdown-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;IACvB,KAAK,EAAE,kBAAkB,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAA;KAChC,CAAA;IACD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAChD;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACT,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B,kDAsQ5B"}
1
+ {"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AASA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;IACvB,KAAK,EAAE,kBAAkB,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAA;KAChC,CAAA;IACD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAChD;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B,kDAoS5B"}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { usePricingPreview } from "@voyantjs/bookings-react";
4
+ import { useProduct } from "@voyantjs/products-react";
4
5
  import { Button, Label, Textarea } from "@voyantjs/ui/components";
5
6
  import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
6
7
  import * as React from "react";
@@ -33,7 +34,7 @@ function matchTier(tiers, qty) {
33
34
  * - `free` / `included` — render 0.00 without an on-request badge.
34
35
  * - `on_request` / anything else — render "On request"; total excludes it.
35
36
  */
36
- export function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, onChange, }) {
37
+ export function PriceBreakdownSection({ productId, optionId, unitQuantities, unitLabels, catalogId, labels, onChange, }) {
37
38
  const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
38
39
  const messages = useBookingsUiMessagesOrDefault();
39
40
  const merged = { ...messages.priceBreakdownSection.labels, ...labels };
@@ -43,16 +44,19 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
43
44
  catalogId: catalogId ?? null,
44
45
  enabled: Boolean(productId),
45
46
  });
47
+ const productQuery = useProduct(productId, { enabled: Boolean(productId) });
46
48
  const quantitiesKey = React.useMemo(() => JSON.stringify(unitQuantities), [unitQuantities]);
47
49
  const [manualAmountCents, setManualAmountCents] = React.useState(null);
48
50
  const [overrideReason, setOverrideReason] = React.useState("");
49
- // biome-ignore lint/correctness/useExhaustiveDependencies: reset manual confirmation when the priced selection changes
51
+ // biome-ignore lint/correctness/useExhaustiveDependencies: #935 reset manual confirmation when the priced selection changes
50
52
  React.useEffect(() => {
51
53
  setManualAmountCents(null);
52
54
  setOverrideReason("");
53
55
  }, [productId, optionId, catalogId, quantitiesKey]);
54
56
  const snapshot = preview.data?.data;
55
- const currency = snapshot?.catalog.currencyCode ?? null;
57
+ const fallbackProduct = productQuery.data;
58
+ const fallbackUnitAmountCents = fallbackProduct?.sellAmountCents ?? null;
59
+ const currency = snapshot?.catalog.currencyCode ?? fallbackProduct?.sellCurrency ?? null;
56
60
  const formatAmount = React.useCallback((cents) => currency
57
61
  ? formatCurrency(cents / 100, currency)
58
62
  : formatNumber(cents / 100, {
@@ -63,8 +67,26 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
63
67
  const out = [];
64
68
  let runningTotal = 0;
65
69
  let anyOnRequest = false;
66
- if (!snapshot)
67
- return { lines: out, total: null };
70
+ if (!snapshot) {
71
+ if (fallbackUnitAmountCents === null)
72
+ return { lines: out, total: null };
73
+ for (const [unitId, quantity] of Object.entries(unitQuantities)) {
74
+ if (quantity <= 0)
75
+ continue;
76
+ const lineTotal = fallbackUnitAmountCents * quantity;
77
+ out.push({
78
+ unitId,
79
+ label: unitLabels?.[unitId] ?? fallbackProduct?.name ?? unitId,
80
+ quantity,
81
+ unitAmountCents: fallbackUnitAmountCents,
82
+ totalAmountCents: lineTotal,
83
+ tierLabel: null,
84
+ isGroupRate: false,
85
+ });
86
+ runningTotal += lineTotal;
87
+ }
88
+ return { lines: out, total: runningTotal };
89
+ }
68
90
  // Pick the default price rule for the resolved option (snapshot already
69
91
  // filters options by the caller's optionId; rules keep isDefault-first
70
92
  // ordering from the server).
@@ -89,7 +111,7 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
89
111
  // operator knows they need to quote manually.
90
112
  out.push({
91
113
  unitId,
92
- label: unitId,
114
+ label: unitLabels?.[unitId] ?? unitId,
93
115
  quantity,
94
116
  unitAmountCents: null,
95
117
  totalAmountCents: null,
@@ -99,7 +121,7 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
99
121
  anyOnRequest = true;
100
122
  continue;
101
123
  }
102
- const label = up.unitName || unitId;
124
+ const label = unitLabels?.[unitId] ?? up.unitName ?? unitId;
103
125
  if (up.pricingMode === "on_request") {
104
126
  out.push({
105
127
  unitId,
@@ -155,7 +177,15 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
155
177
  runningTotal += lineTotal;
156
178
  }
157
179
  return { lines: out, total: anyOnRequest ? null : runningTotal };
158
- }, [snapshot, unitQuantities, merged.onRequest, merged.groupRate]);
180
+ }, [
181
+ snapshot,
182
+ fallbackProduct?.name,
183
+ fallbackUnitAmountCents,
184
+ unitQuantities,
185
+ unitLabels,
186
+ merged.onRequest,
187
+ merged.groupRate,
188
+ ]);
159
189
  const confirmedAmountCents = manualAmountCents ?? total;
160
190
  const isManualOverride = manualAmountCents != null && (total === null || manualAmountCents !== total);
161
191
  const requiresReason = isManualOverride && overrideReason.trim().length === 0;
@@ -183,7 +213,7 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
183
213
  // Empty states
184
214
  if (!productId)
185
215
  return null;
186
- if (preview.isError || (preview.isSuccess && !snapshot)) {
216
+ if ((preview.isError || (preview.isSuccess && !snapshot)) && fallbackUnitAmountCents === null) {
187
217
  return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noPricing }), manualTotalControls] }));
188
218
  }
189
219
  if (lines.length === 0) {
@@ -10,6 +10,8 @@ export interface ProductPickerSectionProps {
10
10
  enabled?: boolean;
11
11
  /** When true, hide the product picker and fix the productId (e.g., launched from a product page). */
12
12
  lockProduct?: boolean;
13
+ /** When false, product options are selected downstream as quantities instead of a single global choice. */
14
+ showOptionPicker?: boolean;
13
15
  labels?: {
14
16
  product?: string;
15
17
  productSearchPlaceholder?: string;
@@ -23,5 +25,5 @@ export interface ProductPickerSectionProps {
23
25
  * replace the whole section (e.g., with a typeahead against a custom catalog)
24
26
  * without reimplementing the cascade logic, or keep this one and swap labels.
25
27
  */
26
- export declare function ProductPickerSection({ value, onChange, enabled, lockProduct, labels, }: ProductPickerSectionProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function ProductPickerSection({ value, onChange, enabled, lockProduct, showOptionPicker, labels, }: ProductPickerSectionProps): import("react/jsx-runtime").JSX.Element;
27
29
  //# sourceMappingURL=product-picker-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"product-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/product-picker-section.tsx"],"names":[],"mappings":"AA+BA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC7C,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qGAAqG;IACrG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,WAAmB,EACnB,MAAM,GACP,EAAE,yBAAyB,2CAmI3B"}
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"}