@voyantjs/bookings-ui 0.101.0 → 0.101.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"booking-create-sheet.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-sheet.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AAkPjC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,GACd,EAAE,uBAAuB,2CA8BzB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CA0/BxB"}
1
+ {"version":3,"file":"booking-create-sheet.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-sheet.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKL,KAAK,aAAa,EAMnB,MAAM,0BAA0B,CAAA;AAsWjC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,GACd,EAAE,uBAAuB,2CA8BzB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CAymCxB"}
@@ -3,18 +3,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  import { availabilityQueryKeys, getSlotQueryOptions, useSlots, useSlotUnitAvailability, useVoyantAvailabilityContext, } from "@voyantjs/availability-react";
5
5
  import { resolveBookingDraft, resolveBookingExtraLines, travelersToRows, } from "@voyantjs/bookings/pricing-assignment";
6
- import { useBookingCreateMutation, useBookingTaxPreview, VoyantApiError, } from "@voyantjs/bookings-react";
6
+ import { useBookingCreateMutation, useBookingTaxPreview, usePricingPreview, VoyantApiError, } from "@voyantjs/bookings-react";
7
7
  import { useOrganization, usePerson } from "@voyantjs/crm-react";
8
8
  import { useProductExtras } from "@voyantjs/extras-react";
9
9
  import { useAddresses } from "@voyantjs/identity-react";
10
- import { getExtraPriceRulesQueryOptions, useVoyantPricingContext } from "@voyantjs/pricing-react";
10
+ import { getExtraPriceRulesQueryOptions, useOptionUnitPriceRules, usePricingCategories, useVoyantPricingContext, } from "@voyantjs/pricing-react";
11
11
  import { useProduct, useProductMedia } from "@voyantjs/products-react";
12
12
  import { Button, Checkbox, Label, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Textarea, } from "@voyantjs/ui/components";
13
13
  import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
14
14
  import { ImageIcon, Loader2 } from "lucide-react";
15
15
  import * as React from "react";
16
16
  import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault, } from "../i18n/provider.js";
17
- import { getBookableDepartureSlots, getSelectedSharedRoomUnitId, getTravelerAssignableStepperUnits, isRealBookingEmail, itemLinesToRows, validateBillingPersonContact, } from "./booking-create-utils.js";
17
+ import { getBookableDepartureSlots, getOverCapacityInventoryAssignments, getSelectedSharedRoomUnitId, getTravelerAssignableStepperUnits, isRealBookingEmail, itemLinesToRows, validateBillingPersonContact, } from "./booking-create-utils.js";
18
18
  import { emptyOptionUnitsStepperValue, OptionUnitsStepperSection, } from "./option-units-stepper-section.js";
19
19
  import { emptyPaymentScheduleValue, PaymentScheduleSection, } from "./payment-schedule-section.js";
20
20
  import { emptyPersonPickerValue, PersonPickerSection, } from "./person-picker-section.js";
@@ -116,10 +116,97 @@ function sameRoomUnits(left, right) {
116
116
  unit.optionId === other.optionId &&
117
117
  unit.optionUnitId === other.optionUnitId &&
118
118
  unit.unitName === other.unitName &&
119
+ unit.unitCode === other.unitCode &&
120
+ unit.unitType === other.unitType &&
121
+ unit.minAge === other.minAge &&
122
+ unit.maxAge === other.maxAge &&
119
123
  unit.occupancyMax === other.occupancyMax &&
120
124
  unit.remaining === other.remaining);
121
125
  });
122
126
  }
127
+ function inferTravelerPricingCategoryId(traveler, categories) {
128
+ if (traveler.pricingCategoryId)
129
+ return traveler.pricingCategoryId;
130
+ const pool = traveler.inventoryUnitId
131
+ ? categories.filter((category) => category.unitIds.includes(traveler.inventoryUnitId ?? ""))
132
+ : categories;
133
+ if (pool.length === 0)
134
+ return null;
135
+ const roleType = traveler.role === "child" || traveler.role === "infant" ? traveler.role : "adult";
136
+ return (pool.find((category) => category.categoryType === roleType)?.categoryId ??
137
+ pool[0]?.categoryId ??
138
+ null);
139
+ }
140
+ function toStepperUnitType(value) {
141
+ if (value === "person" ||
142
+ value === "group" ||
143
+ value === "room" ||
144
+ value === "vehicle" ||
145
+ value === "service" ||
146
+ value === "other") {
147
+ return value;
148
+ }
149
+ return null;
150
+ }
151
+ function normalizeBookingUnit(unit) {
152
+ return {
153
+ ...unit,
154
+ unitType: unit.unitType ?? (unit.occupancyMax != null ? "room" : null),
155
+ };
156
+ }
157
+ function isBookingInventoryUnit(unit) {
158
+ return unit.unitType === "room" || unit.unitType === "vehicle" || unit.occupancyMax != null;
159
+ }
160
+ function pricingSnapshotRoomUnits(snapshot) {
161
+ if (!snapshot)
162
+ return [];
163
+ const unitsById = new Map();
164
+ for (const unitPrice of snapshot.unitPrices) {
165
+ if (unitPrice.unitType !== "room" && unitPrice.unitType !== "vehicle")
166
+ continue;
167
+ if (unitsById.has(unitPrice.unitId))
168
+ continue;
169
+ unitsById.set(unitPrice.unitId, {
170
+ optionId: unitPrice.optionId,
171
+ optionUnitId: unitPrice.unitId,
172
+ unitName: unitPrice.unitName ?? unitPrice.unitId,
173
+ unitCode: null,
174
+ minAge: null,
175
+ maxAge: null,
176
+ unitType: toStepperUnitType(unitPrice.unitType) ?? "room",
177
+ occupancyMax: unitPrice.occupancyMax,
178
+ initial: null,
179
+ reserved: 0,
180
+ remaining: null,
181
+ });
182
+ }
183
+ return Array.from(unitsById.values());
184
+ }
185
+ function mergePricingRoomMetadata(units, pricingUnits) {
186
+ if (pricingUnits.length === 0)
187
+ return units.map(normalizeBookingUnit);
188
+ const pricingUnitById = new Map(pricingUnits.map((unit) => [unit.optionUnitId, unit]));
189
+ const seen = new Set();
190
+ const merged = units.map((unit) => {
191
+ seen.add(unit.optionUnitId);
192
+ const pricingUnit = pricingUnitById.get(unit.optionUnitId);
193
+ if (!pricingUnit)
194
+ return normalizeBookingUnit(unit);
195
+ return normalizeBookingUnit({
196
+ ...pricingUnit,
197
+ ...unit,
198
+ optionId: unit.optionId ?? pricingUnit.optionId,
199
+ unitName: unit.unitName || pricingUnit.unitName,
200
+ unitType: unit.unitType ?? pricingUnit.unitType,
201
+ occupancyMax: unit.occupancyMax ?? pricingUnit.occupancyMax,
202
+ });
203
+ });
204
+ for (const pricingUnit of pricingUnits) {
205
+ if (!seen.has(pricingUnit.optionUnitId))
206
+ merged.push(pricingUnit);
207
+ }
208
+ return merged;
209
+ }
123
210
  function isPayloadResolverMismatchBody(body) {
124
211
  if (typeof body !== "object" || body === null)
125
212
  return false;
@@ -362,73 +449,65 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
362
449
  slotId: slotId ?? undefined,
363
450
  enabled: enabled && Boolean(slotId),
364
451
  });
452
+ const pricingPreview = usePricingPreview({
453
+ productId: product.productId,
454
+ optionId: product.optionId,
455
+ enabled: enabled && Boolean(product.productId),
456
+ });
457
+ const pricingCategoriesQuery = usePricingCategories({
458
+ active: true,
459
+ limit: 200,
460
+ enabled: enabled && Boolean(product.productId),
461
+ });
462
+ const optionUnitPriceRulesQuery = useOptionUnitPriceRules({
463
+ optionId: product.optionId ?? selectedSlot?.optionId ?? undefined,
464
+ active: true,
465
+ limit: 200,
466
+ enabled: enabled && Boolean(product.productId),
467
+ });
365
468
  const handleRoomUnitsChange = React.useCallback((units) => {
366
469
  setRoomUnits((prev) => (sameRoomUnits(prev, units) ? prev : units));
367
470
  }, []);
471
+ const pricingRoomUnits = React.useMemo(() => pricingSnapshotRoomUnits(pricingPreview.data?.data), [pricingPreview.data]);
472
+ const hasRoomPricingMatrix = pricingRoomUnits.length > 0;
473
+ const bookingUnits = React.useMemo(() => mergePricingRoomMetadata(roomUnits, pricingRoomUnits), [roomUnits, pricingRoomUnits]);
368
474
  // Room choices presented to the traveler row are inventory options
369
475
  // (e.g. "Standard double", "Junior suite upgrade"). The age-band
370
476
  // lives separately on the traveler as a pricing unit.
371
477
  const roomUnitOptions = React.useMemo(() => {
372
- const sourceUnits = roomUnits.length > 0 ? roomUnits : (slotUnitAvailability.data?.data ?? []);
373
- const quantityByOption = new Map();
374
- for (const unit of sourceUnits) {
375
- const key = unit.optionId ?? unit.optionUnitId;
376
- quantityByOption.set(key, (quantityByOption.get(key) ?? 0) + (rooms.quantities[unit.optionUnitId] ?? 0));
377
- }
378
- const units = sourceUnits.filter((unit) => unit.unitType === "room" || unit.unitType === "vehicle");
478
+ const sourceUnits = bookingUnits.length > 0 ? bookingUnits : (slotUnitAvailability.data?.data ?? []);
479
+ const units = sourceUnits.filter(isBookingInventoryUnit);
379
480
  if (units.length === 0)
380
481
  return [];
381
- const optionGroups = new Map();
382
- for (const unit of units) {
383
- const key = unit.optionId ?? unit.optionUnitId;
384
- const existing = optionGroups.get(key);
385
- if (existing) {
386
- existing.units.push(unit);
387
- }
388
- else {
389
- optionGroups.set(key, {
390
- primaryUnitId: unit.optionUnitId,
391
- // Strip the trailing " - Adult" / " - Child" suffix the
392
- // upstream stepper appends when an option has multiple units.
393
- optionName: stripUnitSuffix(unit.unitName),
394
- units: [unit],
395
- });
396
- }
397
- }
398
- return Array.from(optionGroups.values())
399
- .filter((group) => {
400
- const optionKey = group.units[0]?.optionId ?? group.primaryUnitId;
401
- const totalQty = quantityByOption.get(optionKey) ?? 0;
402
- return totalQty > 0;
403
- })
404
- .map((group) => {
405
- const optionKey = group.units[0]?.optionId ?? group.primaryUnitId;
406
- const totalQty = quantityByOption.get(optionKey) ?? 0;
407
- const occupancyMax = Math.max(1, ...group.units.map((u) => u.occupancyMax ?? 1));
482
+ return units
483
+ .filter((unit) => (rooms.quantities[unit.optionUnitId] ?? 0) > 0)
484
+ .map((unit) => {
485
+ const totalQty = rooms.quantities[unit.optionUnitId] ?? 0;
486
+ const occupancyMax = Math.max(1, unit.occupancyMax ?? 1);
408
487
  const seats = totalQty * occupancyMax;
409
- const optionUnitIds = new Set(group.units.map((u) => u.optionUnitId));
410
- const assigned = travelers.travelers.filter((traveler) => traveler.inventoryUnitId && optionUnitIds.has(traveler.inventoryUnitId)).length;
488
+ const assigned = travelers.travelers.filter((traveler) => traveler.inventoryUnitId === unit.optionUnitId).length;
411
489
  return {
412
- unitId: group.primaryUnitId,
413
- unitName: group.optionName,
490
+ unitId: unit.optionUnitId,
491
+ unitName: stripOptionPrefix(unit.unitName),
414
492
  remainingCapacity: Math.max(0, seats - assigned),
415
493
  };
416
494
  });
417
- }, [roomUnits, slotUnitAvailability.data, rooms.quantities, travelers.travelers]);
495
+ }, [bookingUnits, slotUnitAvailability.data, rooms.quantities, travelers.travelers]);
418
496
  // Per-option breakdown of all configured units, with the
419
497
  // attributes the TravelersSection's dynamic category buttons need
420
498
  // (unitCode/min-max/unitType). Mirrors the grouping logic in
421
499
  // `roomUnitOptions` but exposes every unit instead of collapsing
422
500
  // to one primary.
423
501
  const roomGroups = React.useMemo(() => {
424
- if (roomUnits.length === 0)
502
+ if (bookingUnits.length === 0)
425
503
  return [];
426
504
  const groups = new Map();
427
- for (const u of roomUnits) {
505
+ for (const rawUnit of bookingUnits) {
506
+ const u = normalizeBookingUnit(rawUnit);
428
507
  if (!u.optionId)
429
508
  continue;
430
509
  const groupKey = u.optionId;
431
- const isInventory = u.unitType === "room" || u.unitType === "vehicle";
510
+ const isInventory = isBookingInventoryUnit(u);
432
511
  const isAdultCoded = (u.unitCode ?? "").toUpperCase() === "ADULT";
433
512
  const unit = {
434
513
  unitId: u.optionUnitId,
@@ -461,7 +540,82 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
461
540
  }
462
541
  }
463
542
  return Array.from(groups.values());
464
- }, [roomUnits]);
543
+ }, [bookingUnits]);
544
+ const travelerPricingCategories = React.useMemo(() => {
545
+ const snapshot = pricingPreview.data?.data;
546
+ const categoriesById = new Map();
547
+ const bookingUnitIds = new Set(bookingUnits.map((unit) => unit.optionUnitId));
548
+ for (const category of pricingCategoriesQuery.data?.data ?? []) {
549
+ categoriesById.set(category.id, category);
550
+ }
551
+ for (const category of snapshot?.pricingCategories ?? []) {
552
+ categoriesById.set(category.id, category);
553
+ }
554
+ const unitIdsByCategoryId = new Map();
555
+ for (const unitPrice of snapshot?.unitPrices ?? []) {
556
+ if (!unitPrice.pricingCategoryId)
557
+ continue;
558
+ if (bookingUnitIds.size > 0 && !bookingUnitIds.has(unitPrice.unitId))
559
+ continue;
560
+ const existing = unitIdsByCategoryId.get(unitPrice.pricingCategoryId) ?? new Set();
561
+ existing.add(unitPrice.unitId);
562
+ unitIdsByCategoryId.set(unitPrice.pricingCategoryId, existing);
563
+ }
564
+ for (const unitPriceRule of optionUnitPriceRulesQuery.data?.data ?? []) {
565
+ if (!unitPriceRule.pricingCategoryId)
566
+ continue;
567
+ if (bookingUnitIds.size > 0 && !bookingUnitIds.has(unitPriceRule.unitId))
568
+ continue;
569
+ const existing = unitIdsByCategoryId.get(unitPriceRule.pricingCategoryId) ?? new Set();
570
+ existing.add(unitPriceRule.unitId);
571
+ unitIdsByCategoryId.set(unitPriceRule.pricingCategoryId, existing);
572
+ }
573
+ return Array.from(unitIdsByCategoryId.entries())
574
+ .flatMap(([categoryId, unitIds]) => {
575
+ const category = categoriesById.get(categoryId);
576
+ if (!category)
577
+ return [];
578
+ return [
579
+ {
580
+ categoryId,
581
+ name: category.name,
582
+ code: category.code,
583
+ categoryType: category.categoryType,
584
+ minAge: category.minAge,
585
+ maxAge: category.maxAge,
586
+ unitIds: Array.from(unitIds),
587
+ },
588
+ ];
589
+ })
590
+ .sort((left, right) => {
591
+ const leftSort = categoriesById.get(left.categoryId)?.sortOrder ?? 0;
592
+ const rightSort = categoriesById.get(right.categoryId)?.sortOrder ?? 0;
593
+ return leftSort - rightSort || left.name.localeCompare(right.name);
594
+ });
595
+ }, [
596
+ pricingPreview.data,
597
+ pricingCategoriesQuery.data?.data,
598
+ optionUnitPriceRulesQuery.data?.data,
599
+ bookingUnits,
600
+ ]);
601
+ const travelerPricingCategoryLabels = React.useMemo(() => Object.fromEntries(travelerPricingCategories.map((category) => [category.categoryId, category.name])), [travelerPricingCategories]);
602
+ const travelerPricingCategoryQuantities = React.useMemo(() => {
603
+ const quantities = {};
604
+ if (travelerPricingCategories.length === 0)
605
+ return quantities;
606
+ for (const traveler of travelers.travelers) {
607
+ if (!traveler.inventoryUnitId)
608
+ continue;
609
+ const pricingCategoryId = inferTravelerPricingCategoryId(traveler, travelerPricingCategories);
610
+ if (!pricingCategoryId)
611
+ continue;
612
+ const unitCategoryQuantities = quantities[traveler.inventoryUnitId] ?? {};
613
+ unitCategoryQuantities[pricingCategoryId] =
614
+ (unitCategoryQuantities[pricingCategoryId] ?? 0) + 1;
615
+ quantities[traveler.inventoryUnitId] = unitCategoryQuantities;
616
+ }
617
+ return quantities;
618
+ }, [travelers.travelers, travelerPricingCategories]);
465
619
  // Apply the same draft resolver we use at submit so live pricing
466
620
  // and persisted item lines cannot drift. Person-priced options
467
621
  // (excursions) derive line quantities from the traveler list;
@@ -469,8 +623,8 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
469
623
  const displayDraft = React.useMemo(() => resolveBookingDraft({
470
624
  quantities: rooms.quantities,
471
625
  travelers: travelers.travelers,
472
- units: roomUnits,
473
- }), [rooms.quantities, travelers.travelers, roomUnits]);
626
+ units: bookingUnits,
627
+ }), [rooms.quantities, travelers.travelers, bookingUnits]);
474
628
  const displayQuantities = displayDraft.quantities;
475
629
  const displayExtraLines = React.useMemo(() => resolveBookingExtraLines({
476
630
  extraLines,
@@ -493,7 +647,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
493
647
  const productSellCurrency = bookingProductQuery.data?.sellCurrency ?? null;
494
648
  const pricingCurrency = productSellCurrency ?? pricing?.currency ?? currency;
495
649
  const pricingTotalAmountCents = pricing?.confirmedAmountCents ?? undefined;
496
- const roomUnitLabels = React.useMemo(() => Object.fromEntries(roomUnits.map((unit) => [unit.optionUnitId, unit.unitName])), [roomUnits]);
650
+ const roomUnitLabels = React.useMemo(() => Object.fromEntries(bookingUnits.map((unit) => [unit.optionUnitId, unit.unitName])), [bookingUnits]);
497
651
  const createBookingMutation = useBookingCreateMutation();
498
652
  const queryClient = useQueryClient();
499
653
  // Resolve the billing person/org once at the dialog level so we can
@@ -575,6 +729,15 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
575
729
  setError(messages.bookingCreateDialog.validation.firstAndLastNameRequired);
576
730
  return;
577
731
  }
732
+ const overCapacity = getOverCapacityInventoryAssignments(bookingUnits, rooms.quantities, travelers.travelers)[0];
733
+ if (overCapacity) {
734
+ setError(formatMessage(messages.bookingCreateDialog.validation.roomCapacityExceeded, {
735
+ room: overCapacity.unitName,
736
+ assigned: overCapacity.assignedTravelers,
737
+ capacity: overCapacity.capacity,
738
+ }));
739
+ return;
740
+ }
578
741
  try {
579
742
  if (sharedRoom.enabled && sharedRoom.mode === "join" && !sharedRoom.groupId) {
580
743
  setError(messages.bookingCreateDialog.validation.selectSharedRoomGroup);
@@ -595,8 +758,8 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
595
758
  // "3 x Adult"); accommodation options keep operator-picked
596
759
  // room quantities. Server gets `clientLineKey` + `travelerKeys`
597
760
  // on each line so it can write `booking_item_travelers` rows.
598
- const submitUnits = roomUnits.length > 0
599
- ? roomUnits
761
+ const submitUnits = bookingUnits.length > 0
762
+ ? bookingUnits
600
763
  : getTravelerAssignableStepperUnits((slotUnitAvailability.data?.data ?? []).map((unit) => ({
601
764
  ...unit,
602
765
  optionId: product.optionId,
@@ -614,10 +777,31 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
614
777
  .filter((key) => Boolean(key))
615
778
  : [],
616
779
  ]));
780
+ const travelerIndexesByUnitAndCategoryId = {};
781
+ const travelerKeysByUnitAndCategoryId = {};
782
+ for (const [unitId, indexes] of Object.entries(redistributed.travelerIndexesByUnitId)) {
783
+ for (const index of indexes) {
784
+ const traveler = redistributed.travelers[index];
785
+ if (!traveler)
786
+ continue;
787
+ const pricingCategoryId = inferTravelerPricingCategoryId(traveler, travelerPricingCategories);
788
+ if (!pricingCategoryId)
789
+ continue;
790
+ travelerIndexesByUnitAndCategoryId[unitId] ??= {};
791
+ travelerIndexesByUnitAndCategoryId[unitId][pricingCategoryId] ??= [];
792
+ travelerIndexesByUnitAndCategoryId[unitId][pricingCategoryId].push(index);
793
+ const key = traveler.clientTravelerKey;
794
+ if (key) {
795
+ travelerKeysByUnitAndCategoryId[unitId] ??= {};
796
+ travelerKeysByUnitAndCategoryId[unitId][pricingCategoryId] ??= [];
797
+ travelerKeysByUnitAndCategoryId[unitId][pricingCategoryId].push(key);
798
+ }
799
+ }
800
+ }
617
801
  const travelerKeys = redistributed.travelers
618
802
  .map((traveler) => traveler.clientTravelerKey)
619
803
  .filter((key) => Boolean(key));
620
- const itemLines = itemLinesToRows(redistributed.quantities, submitUnits, pricing, redistributed.travelerIndexesByUnitId, travelerKeysByUnitId);
804
+ const itemLines = itemLinesToRows(redistributed.quantities, submitUnits, pricing, redistributed.travelerIndexesByUnitId, travelerKeysByUnitId, travelerIndexesByUnitAndCategoryId, travelerKeysByUnitAndCategoryId);
621
805
  const resolvedExtraLines = resolveBookingExtraLines({
622
806
  extraLines,
623
807
  travelerCount: travelers.travelers.length,
@@ -785,7 +969,9 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
785
969
  } })) : null, product.productId && slotId ? (_jsx(TravelersSection, { value: travelers, onChange: (next) => {
786
970
  setPayloadMismatchUnitIds([]);
787
971
  setTravelers(next);
788
- }, roomUnits: roomUnitOptions.length > 0 ? roomUnitOptions : undefined, roomGroups: roomGroups.length > 0 ? roomGroups : undefined, billingPersonId: (person.billTo ?? "person") === "person" ? person.personId : null, labels: {
972
+ }, roomUnits: roomUnitOptions.length > 0 ? roomUnitOptions : undefined, roomGroups: roomGroups.length > 0 ? roomGroups : undefined, pricingCategories: hasRoomPricingMatrix || roomUnitOptions.length > 0
973
+ ? travelerPricingCategories
974
+ : undefined, billingPersonId: (person.billTo ?? "person") === "person" ? person.personId : null, labels: {
789
975
  heading: messages.bookingCreateDialog.labels.travelerHeading,
790
976
  addTraveler: messages.bookingCreateDialog.labels.addTraveler,
791
977
  person: messages.bookingCreateDialog.labels.travelerPerson,
@@ -808,7 +994,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, defaultSlotId,
808
994
  : messages.bookingCreateDialog.actions.createAwaitingPaymentBooking] })] })] }), _jsxs("div", { className: "flex flex-col gap-4 lg:col-span-4", children: [_jsx(BookingPreviewCard, { productId: product.productId, optionId: product.optionId, slotId: slotId, slotLabel: (() => {
809
995
  const slot = slots.find((s) => s.id === slotId);
810
996
  return slot ? formatSlotLabel(slot) : null;
811
- })(), unitQuantities: displayQuantities, unitLabels: roomUnitLabels, extraLines: displayExtraLines, travelers: travelers.travelers, messages: messages, onPricingChange: setPricing }), product.productId && slotId ? (_jsx(VoucherPickerSection, { value: voucher, onChange: setVoucher, currency: currency, labels: {
997
+ })(), unitQuantities: displayQuantities, unitLabels: roomUnitLabels, pricingCategoryQuantities: travelerPricingCategoryQuantities, pricingCategoryLabels: travelerPricingCategoryLabels, extraLines: displayExtraLines, travelers: travelers.travelers, messages: messages, onPricingChange: setPricing }), product.productId && slotId ? (_jsx(VoucherPickerSection, { value: voucher, onChange: setVoucher, currency: currency, labels: {
812
998
  heading: messages.bookingCreateDialog.labels.voucherHeading,
813
999
  codePlaceholder: messages.bookingCreateDialog.labels.voucherCodePlaceholder,
814
1000
  apply: messages.bookingCreateDialog.labels.voucherApply,
@@ -918,7 +1104,7 @@ function QuantityButtons({ value, max, onChange, }) {
918
1104
  * confirmed price — so the operator gets a "what am I about to book"
919
1105
  * summary without scrolling back through the form.
920
1106
  */
921
- function BookingPreviewCard({ productId, optionId, slotId, slotLabel, unitQuantities, unitLabels, extraLines, travelers, messages, onPricingChange, }) {
1107
+ function BookingPreviewCard({ productId, optionId, slotId, slotLabel, unitQuantities, unitLabels, pricingCategoryQuantities, pricingCategoryLabels, extraLines, travelers, messages, onPricingChange, }) {
922
1108
  const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
923
1109
  const productQuery = useProduct(productId || undefined, { enabled: Boolean(productId) });
924
1110
  const mediaQuery = useProductMedia(productId, { limit: 1, enabled: Boolean(productId) });
@@ -979,7 +1165,7 @@ function BookingPreviewCard({ productId, optionId, slotId, slotLabel, unitQuanti
979
1165
  .join(" ")
980
1166
  .trim();
981
1167
  return (_jsxs("li", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "truncate text-muted-foreground", children: name || previewMessages.travelerUnnamed }), _jsx("span", { className: "shrink-0 text-xs uppercase tracking-wider text-muted-foreground", children: traveler.role })] }, traveler.personId ?? `traveler-${idx}`));
982
- }) })] })) : null, showPriceBreakdown ? (_jsxs("div", { className: "border-t pt-3", children: [_jsx(PriceBreakdownSection, { flat: true, productId: productId, optionId: optionId, unitQuantities: unitQuantities, unitLabels: unitLabels, labels: {
1168
+ }) })] })) : null, showPriceBreakdown ? (_jsxs("div", { className: "border-t pt-3", children: [_jsx(PriceBreakdownSection, { flat: true, productId: productId, optionId: optionId, unitQuantities: unitQuantities, unitLabels: unitLabels, pricingCategoryQuantities: pricingCategoryQuantities, pricingCategoryLabels: pricingCategoryLabels, labels: {
983
1169
  heading: labels.breakdownHeading,
984
1170
  total: labels.breakdownTotal,
985
1171
  onRequest: labels.breakdownOnRequest,
@@ -14,9 +14,11 @@ export interface BookingCreateUnitLineRecord {
14
14
  }
15
15
  export interface BookingCreatePricingLineRecord {
16
16
  unitId: string;
17
+ pricingCategoryId?: string | null;
17
18
  label: string;
18
19
  unitAmountCents: number | null;
19
20
  totalAmountCents: number | null;
21
+ quantity?: number;
20
22
  }
21
23
  export interface BookingCreatePricingRecord {
22
24
  confirmedAmountCents: number | null;
@@ -28,6 +30,21 @@ export interface BookingCreateTravelerAssignableUnitRecord {
28
30
  optionUnitId: string;
29
31
  unitType?: BookingCreateStepperUnitType;
30
32
  }
33
+ export interface BookingCreateCapacityUnitRecord {
34
+ optionUnitId: string;
35
+ unitName: string;
36
+ unitType?: BookingCreateStepperUnitType;
37
+ occupancyMax?: number | null;
38
+ }
39
+ export interface BookingCreateCapacityTravelerRecord {
40
+ inventoryUnitId?: string | null;
41
+ }
42
+ export interface BookingCreateOverCapacityAssignment {
43
+ optionUnitId: string;
44
+ unitName: string;
45
+ assignedTravelers: number;
46
+ capacity: number;
47
+ }
31
48
  export declare function normalizeBookingSearchText(value: string): string;
32
49
  export declare function matchesBookingSearchText(value: string | null | undefined, query: string): boolean;
33
50
  export declare function productMatchesPickerSearch(product: ProductPickerSearchRecord | null | undefined, query: string): boolean;
@@ -38,11 +55,12 @@ export declare function validateBillingPersonContact(contact: {
38
55
  phone?: string | null;
39
56
  } | null | undefined): BillingPersonContactValidationResult;
40
57
  export declare function getTravelerAssignableStepperUnits<TUnit extends BookingCreateTravelerAssignableUnitRecord>(units: readonly TUnit[]): TUnit[];
58
+ export declare function getOverCapacityInventoryAssignments(units: readonly BookingCreateCapacityUnitRecord[], quantities: Record<string, number>, travelers: readonly BookingCreateCapacityTravelerRecord[]): BookingCreateOverCapacityAssignment[];
41
59
  export declare function getBookableDepartureSlots<TSlot extends DepartureSlotSearchRecord>(slots: readonly TSlot[], options: {
42
60
  nowIso: string;
43
61
  optionId: string | null;
44
62
  }): TSlot[];
45
- export declare function itemLinesToRows(quantities: Record<string, number>, units: BookingCreateUnitLineRecord[], pricing: BookingCreatePricingRecord | null, travelerIndexesByUnitId?: Record<string, number[]>, travelerKeysByUnitId?: Record<string, string[]>): BookingCreateItemLineInput[];
63
+ export declare function itemLinesToRows(quantities: Record<string, number>, units: BookingCreateUnitLineRecord[], pricing: BookingCreatePricingRecord | null, travelerIndexesByUnitId?: Record<string, number[]>, travelerKeysByUnitId?: Record<string, string[]>, travelerIndexesByUnitAndCategoryId?: Record<string, Record<string, number[]>>, travelerKeysByUnitAndCategoryId?: Record<string, Record<string, string[]>>): BookingCreateItemLineInput[];
46
64
  export declare function getSelectedSharedRoomUnitId(quantities: Record<string, number>): string | null;
47
65
  export {};
48
66
  //# sourceMappingURL=booking-create-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"booking-create-utils.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-utils.ts"],"names":[],"mappings":"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"}
@@ -54,6 +54,31 @@ export function getTravelerAssignableStepperUnits(units) {
54
54
  return !hasInventoryByOption.get(unit.optionId ?? unit.optionUnitId);
55
55
  });
56
56
  }
57
+ export function getOverCapacityInventoryAssignments(units, quantities, travelers) {
58
+ const inventoryUnits = units.filter((unit) => unit.unitType === "room" || unit.unitType === "vehicle");
59
+ const assignedByUnitId = new Map();
60
+ for (const traveler of travelers) {
61
+ if (!traveler.inventoryUnitId)
62
+ continue;
63
+ assignedByUnitId.set(traveler.inventoryUnitId, (assignedByUnitId.get(traveler.inventoryUnitId) ?? 0) + 1);
64
+ }
65
+ return inventoryUnits.flatMap((unit) => {
66
+ const quantity = Math.max(0, quantities[unit.optionUnitId] ?? 0);
67
+ const occupancy = Math.max(1, unit.occupancyMax ?? 1);
68
+ const capacity = quantity * occupancy;
69
+ const assignedTravelers = assignedByUnitId.get(unit.optionUnitId) ?? 0;
70
+ if (assignedTravelers <= capacity)
71
+ return [];
72
+ return [
73
+ {
74
+ optionUnitId: unit.optionUnitId,
75
+ unitName: unit.unitName,
76
+ assignedTravelers,
77
+ capacity,
78
+ },
79
+ ];
80
+ });
81
+ }
57
82
  export function getBookableDepartureSlots(slots, options) {
58
83
  return slots
59
84
  .filter((slot) => !slot.status || slot.status === "open")
@@ -65,10 +90,20 @@ export function getBookableDepartureSlots(slots, options) {
65
90
  })
66
91
  .sort((left, right) => left.startsAt.localeCompare(right.startsAt));
67
92
  }
68
- export function itemLinesToRows(quantities, units, pricing, travelerIndexesByUnitId = {}, travelerKeysByUnitId = {}) {
93
+ export function itemLinesToRows(quantities, units, pricing, travelerIndexesByUnitId = {}, travelerKeysByUnitId = {}, travelerIndexesByUnitAndCategoryId = {}, travelerKeysByUnitAndCategoryId = {}) {
69
94
  const unitsById = new Map(units.map((unit) => [unit.optionUnitId, unit]));
70
95
  const unitNames = new Map(units.map((unit) => [unit.optionUnitId, unit.unitName]));
71
- const pricedLines = new Map((pricing?.lines ?? []).map((line) => [line.unitId, line]));
96
+ const pricedLines = new Map((pricing?.lines ?? [])
97
+ .filter((line) => !line.pricingCategoryId)
98
+ .map((line) => [line.unitId, line]));
99
+ const categoryPricedLinesByUnitId = new Map();
100
+ for (const line of pricing?.lines ?? []) {
101
+ if (!line.pricingCategoryId)
102
+ continue;
103
+ const existing = categoryPricedLinesByUnitId.get(line.unitId) ?? [];
104
+ existing.push(line);
105
+ categoryPricedLinesByUnitId.set(line.unitId, existing);
106
+ }
72
107
  const selectedLines = Object.entries(quantities).filter(([, quantity]) => quantity > 0);
73
108
  const pricedTotal = selectedLines.reduce((sum, [optionUnitId]) => {
74
109
  const total = pricedLines.get(optionUnitId)?.totalAmountCents;
@@ -80,7 +115,38 @@ export function itemLinesToRows(quantities, units, pricing, travelerIndexesByUni
80
115
  ? Math.max(0, pricing.confirmedAmountCents - pricedTotal)
81
116
  : null;
82
117
  let allocatedManualTotal = 0;
83
- return selectedLines.map(([optionUnitId, quantity]) => {
118
+ return selectedLines.flatMap(([optionUnitId, quantity]) => {
119
+ const categoryPricedLines = categoryPricedLinesByUnitId.get(optionUnitId) ?? [];
120
+ if (categoryPricedLines.length > 0) {
121
+ return categoryPricedLines.map((pricedLine) => {
122
+ const pricingCategoryId = pricedLine.pricingCategoryId;
123
+ const categoryQuantity = Math.max(1, pricedLine.quantity ?? 1);
124
+ const travelerIndexes = pricingCategoryId
125
+ ? travelerIndexesByUnitAndCategoryId[optionUnitId]?.[pricingCategoryId]
126
+ : undefined;
127
+ const travelerKeys = pricingCategoryId
128
+ ? travelerKeysByUnitAndCategoryId[optionUnitId]?.[pricingCategoryId]
129
+ : undefined;
130
+ const hasTravelerLinks = Boolean(travelerKeys?.length || travelerIndexes?.length);
131
+ return {
132
+ clientLineKey: hasTravelerLinks
133
+ ? `unit:${optionUnitId}:category:${pricingCategoryId ?? "default"}`
134
+ : undefined,
135
+ optionId: unitsById.get(optionUnitId)?.optionId ?? null,
136
+ optionUnitId,
137
+ pricingCategoryId,
138
+ quantity: categoryQuantity,
139
+ title: pricedLine.label ?? unitNames.get(optionUnitId) ?? null,
140
+ unitSellAmountCents: pricedLine.unitAmountCents,
141
+ totalSellAmountCents: pricedLine.totalAmountCents,
142
+ ...(travelerKeys?.length
143
+ ? { travelerKeys }
144
+ : travelerIndexes?.length
145
+ ? { travelerIndexes }
146
+ : {}),
147
+ };
148
+ });
149
+ }
84
150
  const pricedLine = pricedLines.get(optionUnitId);
85
151
  let totalSellAmountCents = pricedLine?.totalAmountCents ?? null;
86
152
  if (totalSellAmountCents == null && manualRemainder != null && unpricedQuantity > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"option-units-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/option-units-stepper-section.tsx"],"names":[],"mappings":"AAgBA,iEAAiE;AACjE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,4BAA4B,EAAE,uBAA4C,CAAA;AAEvF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAAA;IAC/E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,CAAA;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAClD,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,IAAI,CAAA;IACzD,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAc,EACd,aAAa,EACb,MAAM,EACN,qBAA6B,EAC7B,oBAAyB,GAC1B,EAAE,8BAA8B,2CA+OhC;AAED,wBAAgB,2BAA2B,CAAC,EAC1C,cAAc,EACd,KAAK,EACL,qBAAqB,EACrB,SAAS,EACT,SAAS,EACT,iBAAiB,GAClB,EAAE;IACD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,sBAAsB,EAAE,UAAU,CAAC,CAAC,CAAA;IAC9D,qBAAqB,EAAE,OAAO,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,GAAG,MAAM,CAMT;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAC,EAClE,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAAC,WAG1C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,aAAa,CAAC;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EACjD,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3C,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAC9B,MAAM,GAAG,IAAI,CAMf;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAC/C,WAAW,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAClD,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,EAAE,OAAO,GACf,sBAAsB,EAAE,CAM1B"}
1
+ {"version":3,"file":"option-units-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/option-units-stepper-section.tsx"],"names":[],"mappings":"AAgBA,iEAAiE;AACjE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,4BAA4B,EAAE,uBAA4C,CAAA;AAEvF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAAA;IAC/E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,CAAA;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAClD,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,IAAI,CAAA;IACzD,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAc,EACd,aAAa,EACb,MAAM,EACN,qBAA6B,EAC7B,oBAAyB,GAC1B,EAAE,8BAA8B,2CA2PhC;AAED,wBAAgB,2BAA2B,CAAC,EAC1C,cAAc,EACd,KAAK,EACL,qBAAqB,EACrB,SAAS,EACT,SAAS,EACT,iBAAiB,GAClB,EAAE;IACD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,sBAAsB,EAAE,UAAU,CAAC,CAAC,CAAA;IAC9D,qBAAqB,EAAE,OAAO,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,GAAG,MAAM,CAMT;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAC,EAClE,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAAC,WAG1C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,aAAa,CAAC;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EACjD,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3C,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAC9B,MAAM,GAAG,IAAI,CAMf;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAC/C,WAAW,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAClD,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,EAAE,OAAO,GACf,sBAAsB,EAAE,CAM1B"}