@ticketboothapp/booking 1.2.102 → 1.2.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.102",
3
+ "version": "1.2.104",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -974,6 +974,14 @@ export function AdminChangeBookingFlow({
974
974
  return '';
975
975
  }
976
976
  });
977
+ const selectedDateRangeExtensionKey = useMemo(() => {
978
+ if (!visibleRange || !selectedDate) return '';
979
+ try {
980
+ return isAfter(parseISO(selectedDate), visibleRange.end) ? selectedDate : '';
981
+ } catch {
982
+ return '';
983
+ }
984
+ }, [selectedDate, visibleRange]);
977
985
  const [isItinerarySticky, setIsItinerarySticky] = useState(false);
978
986
  const isItineraryStickyRef = useRef(false);
979
987
  const [isMobile, setIsMobile] = useState(false);
@@ -1303,6 +1311,7 @@ export function AdminChangeBookingFlow({
1303
1311
  activeOptionIdsKey,
1304
1312
  appliedPromoCode,
1305
1313
  pricingProfileIdForAvailabilities,
1314
+ cancellationPolicyProfileIdForAvailabilities,
1306
1315
  )
1307
1316
  : null;
1308
1317
  if (cacheKey && availabilitiesCache) {
@@ -1427,11 +1436,11 @@ export function AdminChangeBookingFlow({
1427
1436
  ? endOfLatestDay
1428
1437
  : visibleRange.end;
1429
1438
 
1430
- // Ensure we include the selected date if it's after the visible range end
1431
- // This handles the case where user selects a date that's not yet in the visible range
1432
- if (selectedDate) {
1439
+ // Include an externally seeded selected date only when it falls outside the current calendar window.
1440
+ // Normal date clicks are already inside visibleRange and should not re-fetch the broad availability range.
1441
+ if (selectedDateRangeExtensionKey) {
1433
1442
  try {
1434
- const selectedDateObj = parseISO(selectedDate);
1443
+ const selectedDateObj = parseISO(selectedDateRangeExtensionKey);
1435
1444
  if (isAfter(selectedDateObj, clampedEnd)) {
1436
1445
  clampedEnd = selectedDateObj;
1437
1446
  }
@@ -1447,6 +1456,7 @@ export function AdminChangeBookingFlow({
1447
1456
  activeOptionIdsKey,
1448
1457
  appliedPromoCode,
1449
1458
  pricingProfileIdForAvailabilities,
1459
+ cancellationPolicyProfileIdForAvailabilities,
1450
1460
  )
1451
1461
  : null;
1452
1462
  const cached = cacheKey ? availabilitiesCache!.get(cacheKey) : undefined;
@@ -1673,7 +1683,7 @@ export function AdminChangeBookingFlow({
1673
1683
  activeOptionIdsKey,
1674
1684
  isPrivateShuttle,
1675
1685
  companyTimezone,
1676
- selectedDate,
1686
+ selectedDateRangeExtensionKey,
1677
1687
  appliedPromoCode,
1678
1688
  pricingProfileIdForAvailabilities,
1679
1689
  cancellationPolicyProfileIdForAvailabilities,
@@ -770,6 +770,14 @@ export function ChangeBookingFlow({
770
770
  return '';
771
771
  }
772
772
  });
773
+ const selectedDateRangeExtensionKey = useMemo(() => {
774
+ if (!visibleRange || !selectedDate) return '';
775
+ try {
776
+ return isAfter(parseISO(selectedDate), visibleRange.end) ? selectedDate : '';
777
+ } catch {
778
+ return '';
779
+ }
780
+ }, [selectedDate, visibleRange]);
773
781
  const [isItinerarySticky, setIsItinerarySticky] = useState(false);
774
782
  const isItineraryStickyRef = useRef(false);
775
783
  const [isMobile, setIsMobile] = useState(false);
@@ -980,6 +988,7 @@ export function ChangeBookingFlow({
980
988
 
981
989
  const availabilityPromises = activeOptions.map(async (option) => {
982
990
  const result = await getAvailabilities(option.optionId, startDateStr, endDateStr, {
991
+ ...(!isPrivateShuttle ? { responseMode: 'booking-day' as const } : {}),
983
992
  promoCode: appliedPromoCode || undefined,
984
993
  ...(pricingProfileIdForAvailabilities
985
994
  ? { pricingProfileId: pricingProfileIdForAvailabilities }
@@ -1054,6 +1063,7 @@ export function ChangeBookingFlow({
1054
1063
  activeOptionIdsKey,
1055
1064
  appliedPromoCode,
1056
1065
  pricingProfileIdForAvailabilities,
1066
+ cancellationPolicyProfileIdForAvailabilities,
1057
1067
  )
1058
1068
  : null;
1059
1069
  if (cacheKey && availabilitiesCache) {
@@ -1178,11 +1188,11 @@ export function ChangeBookingFlow({
1178
1188
  ? endOfLatestDay
1179
1189
  : visibleRange.end;
1180
1190
 
1181
- // Ensure we include the selected date if it's after the visible range end
1182
- // This handles the case where user selects a date that's not yet in the visible range
1183
- if (selectedDate) {
1191
+ // Include an externally seeded selected date only when it falls outside the current calendar window.
1192
+ // Normal date clicks are already inside visibleRange and should not re-fetch the broad availability range.
1193
+ if (selectedDateRangeExtensionKey) {
1184
1194
  try {
1185
- const selectedDateObj = parseISO(selectedDate);
1195
+ const selectedDateObj = parseISO(selectedDateRangeExtensionKey);
1186
1196
  if (isAfter(selectedDateObj, clampedEnd)) {
1187
1197
  clampedEnd = selectedDateObj;
1188
1198
  }
@@ -1198,6 +1208,7 @@ export function ChangeBookingFlow({
1198
1208
  activeOptionIdsKey,
1199
1209
  appliedPromoCode,
1200
1210
  pricingProfileIdForAvailabilities,
1211
+ cancellationPolicyProfileIdForAvailabilities,
1201
1212
  )
1202
1213
  : null;
1203
1214
  const cached = cacheKey ? availabilitiesCache!.get(cacheKey) : undefined;
@@ -1291,6 +1302,7 @@ export function ChangeBookingFlow({
1291
1302
  // Fetch availabilities for all product options in parallel (no currency param - we use precomputedPrices for display)
1292
1303
  const availabilityPromises = activeOptions.map(async (option) => {
1293
1304
  const result = await getAvailabilities(option.optionId, startDateStr, endDateStr, {
1305
+ ...(!isPrivateShuttle ? { responseMode: 'booking-day' as const } : {}),
1294
1306
  promoCode: appliedPromoCode || undefined,
1295
1307
  ...(pricingProfileIdForAvailabilities
1296
1308
  ? { pricingProfileId: pricingProfileIdForAvailabilities }
@@ -1424,7 +1436,7 @@ export function ChangeBookingFlow({
1424
1436
  activeOptionIdsKey,
1425
1437
  isPrivateShuttle,
1426
1438
  companyTimezone,
1427
- selectedDate,
1439
+ selectedDateRangeExtensionKey,
1428
1440
  appliedPromoCode,
1429
1441
  pricingProfileIdForAvailabilities,
1430
1442
  cancellationPolicyProfileIdForAvailabilities,
@@ -493,8 +493,17 @@ export function NewBookingFlow({
493
493
  const inFlightRangeRef = useRef<{ start: Date; end: Date } | null>(null); // Range currently being fetched
494
494
  const fetchedRangesRef = useRef<Array<{ start: Date; end: Date }>>([]); // Track fetched date ranges
495
495
  const pendingRangeRef = useRef<{ start: Date; end: Date } | null>(null); // Range to fetch when current fetch completes (user navigated during fetch)
496
+ const selectedDateHydrationInFlightKeyRef = useRef<string | null>(null);
496
497
  const [visibleRange, setVisibleRange] = useState<{ start: Date; end: Date } | null>(null);
497
498
  const [selectedDate, setSelectedDate] = useState<string>('');
499
+ const selectedDateRangeExtensionKey = useMemo(() => {
500
+ if (!visibleRange || !selectedDate) return '';
501
+ try {
502
+ return isAfter(parseISO(selectedDate), visibleRange.end) ? selectedDate : '';
503
+ } catch {
504
+ return '';
505
+ }
506
+ }, [selectedDate, visibleRange]);
498
507
  const [isItinerarySticky, setIsItinerarySticky] = useState(false);
499
508
  const isItineraryStickyRef = useRef(false);
500
509
  const [isMobile, setIsMobile] = useState(false);
@@ -728,6 +737,7 @@ export function NewBookingFlow({
728
737
  activeOptionIdsKey,
729
738
  appliedPromoCode,
730
739
  pricingProfileIdForAvailabilities,
740
+ cancellationPolicyProfileIdForAvailabilities,
731
741
  )
732
742
  : null;
733
743
  if (cacheKey && availabilitiesCache) {
@@ -830,6 +840,7 @@ export function NewBookingFlow({
830
840
  activeOptionIdsKey,
831
841
  appliedPromoCode,
832
842
  pricingProfileIdForAvailabilities,
843
+ cancellationPolicyProfileIdForAvailabilities,
833
844
  )
834
845
  : null;
835
846
  if (cacheKey && availabilitiesCache) {
@@ -918,11 +929,11 @@ export function NewBookingFlow({
918
929
  ? endOfLatestDay
919
930
  : visibleRange.end;
920
931
 
921
- // Ensure we include the selected date if it's after the visible range end
922
- // This handles the case where user selects a date that's not yet in the visible range
923
- if (selectedDate) {
932
+ // Include an externally seeded selected date only when it falls outside the current calendar window.
933
+ // Normal date clicks are already inside visibleRange and should not re-fetch the broad calendar summary.
934
+ if (selectedDateRangeExtensionKey) {
924
935
  try {
925
- const selectedDateObj = parseISO(selectedDate);
936
+ const selectedDateObj = parseISO(selectedDateRangeExtensionKey);
926
937
  if (isAfter(selectedDateObj, clampedEnd)) {
927
938
  clampedEnd = selectedDateObj;
928
939
  }
@@ -938,6 +949,7 @@ export function NewBookingFlow({
938
949
  activeOptionIdsKey,
939
950
  appliedPromoCode,
940
951
  pricingProfileIdForAvailabilities,
952
+ cancellationPolicyProfileIdForAvailabilities,
941
953
  )
942
954
  : null;
943
955
  const cached = cacheKey ? availabilitiesCache!.get(cacheKey) : undefined;
@@ -1151,7 +1163,7 @@ export function NewBookingFlow({
1151
1163
  activeOptionIdsKey,
1152
1164
  isPrivateShuttle,
1153
1165
  companyTimezone,
1154
- selectedDate,
1166
+ selectedDateRangeExtensionKey,
1155
1167
  appliedPromoCode,
1156
1168
  pricingProfileIdForAvailabilities,
1157
1169
  cancellationPolicyProfileIdForAvailabilities,
@@ -1213,8 +1225,19 @@ export function NewBookingFlow({
1213
1225
 
1214
1226
  useEffect(() => {
1215
1227
  if (!selectedDate || activeOptions.length === 0) return;
1216
- const selectedDateAvailabilities = availabilitiesByDate[selectedDate] ?? [];
1217
- if (!selectedDateAvailabilities.some((availability) => availability.isSummary)) return;
1228
+ if (!selectedAvailability) return;
1229
+ if (!availabilityMatchesSelectedDate(selectedAvailability, selectedDate, companyTimezone)) return;
1230
+ if (selectedAvailability.isSummary !== true) return;
1231
+ const hydrationKey = [
1232
+ product.productId,
1233
+ selectedDate,
1234
+ activeOptionIdsKey,
1235
+ appliedPromoCode ?? '',
1236
+ pricingProfileIdForAvailabilities ?? '',
1237
+ cancellationPolicyProfileIdForAvailabilities ?? '',
1238
+ ].join('|');
1239
+ if (selectedDateHydrationInFlightKeyRef.current === hydrationKey) return;
1240
+ selectedDateHydrationInFlightKeyRef.current = hydrationKey;
1218
1241
 
1219
1242
  let cancelled = false;
1220
1243
  async function hydrateSelectedDateDetails() {
@@ -1235,6 +1258,7 @@ export function NewBookingFlow({
1235
1258
 
1236
1259
  const result = await getAvailabilities(product.productId, startDateStr, endDateStr, {
1237
1260
  allOptions: true,
1261
+ responseMode: 'booking-day',
1238
1262
  promoCode: appliedPromoCode || undefined,
1239
1263
  ...(pricingProfileIdForAvailabilities
1240
1264
  ? { pricingProfileId: pricingProfileIdForAvailabilities }
@@ -1274,6 +1298,16 @@ export function NewBookingFlow({
1274
1298
  mergeAvailabilitiesIntoMap(merged, detailedAvailabilities);
1275
1299
  return Array.from(merged.values());
1276
1300
  });
1301
+ const updatedSelection = findMergedAvailabilityForSelection(detailedAvailabilities, selectedAvailability);
1302
+ if (updatedSelection && selectedAvailability && shouldSyncSelectedAvailability(selectedAvailability, updatedSelection)) {
1303
+ setSelectedAvailability(updatedSelection);
1304
+ if (selectedReturnOption && updatedSelection.returnOptions) {
1305
+ const updatedReturnOption = updatedSelection.returnOptions.find(
1306
+ (option) => option.returnAvailabilityId === selectedReturnOption.returnAvailabilityId
1307
+ );
1308
+ if (updatedReturnOption) setSelectedReturnOption(updatedReturnOption);
1309
+ }
1310
+ }
1277
1311
 
1278
1312
  const cacheKey = availabilitiesCache
1279
1313
  ? buildAvailabilitiesCacheKey(
@@ -1281,6 +1315,7 @@ export function NewBookingFlow({
1281
1315
  activeOptionIdsKey,
1282
1316
  appliedPromoCode,
1283
1317
  pricingProfileIdForAvailabilities,
1318
+ cancellationPolicyProfileIdForAvailabilities,
1284
1319
  )
1285
1320
  : null;
1286
1321
  if (cacheKey && availabilitiesCache) {
@@ -1308,6 +1343,9 @@ export function NewBookingFlow({
1308
1343
  console.error('Error hydrating availability details:', err);
1309
1344
  }
1310
1345
  } finally {
1346
+ if (selectedDateHydrationInFlightKeyRef.current === hydrationKey) {
1347
+ selectedDateHydrationInFlightKeyRef.current = null;
1348
+ }
1311
1349
  if (!cancelled) setIsFetchingMoreAvailabilities(false);
1312
1350
  }
1313
1351
  }
@@ -1318,8 +1356,9 @@ export function NewBookingFlow({
1318
1356
  };
1319
1357
  }, [
1320
1358
  selectedDate,
1321
- availabilitiesByDate,
1322
- activeOptions,
1359
+ selectedAvailability,
1360
+ selectedReturnOption,
1361
+ activeOptions.length,
1323
1362
  activeOptionIdsKey,
1324
1363
  isPrivateShuttle,
1325
1364
  companyTimezone,
@@ -1478,6 +1517,8 @@ export function NewBookingFlow({
1478
1517
  if (!selectedAvailability || !selectedDate) return false;
1479
1518
  return availabilityMatchesSelectedDate(selectedAvailability, selectedDate, companyTimezone);
1480
1519
  }, [selectedAvailability, selectedDate, companyTimezone]);
1520
+ const selectedBookingOptionsHydrating =
1521
+ selectedAvailabilityMatchesDate && selectedAvailability?.isSummary === true;
1481
1522
 
1482
1523
  useEffect(() => {
1483
1524
  if (hasAppliedInitialValuesRef.current || !initialValues) return;
@@ -3478,7 +3519,15 @@ export function NewBookingFlow({
3478
3519
  )}
3479
3520
 
3480
3521
  {/* Select return time — wait for hydrated day details so summary refetches do not flash this block */}
3481
- {selectedAvailabilityMatchesDate &&
3522
+ {selectedBookingOptionsHydrating && (
3523
+ <div className="flex items-center justify-center gap-3 rounded-lg border border-stone-200 bg-white p-4 text-stone-600">
3524
+ <div className="booking-loading-spinner" aria-hidden />
3525
+ <div>{t('booking.loadingTimes')}</div>
3526
+ </div>
3527
+ )}
3528
+
3529
+ {!selectedBookingOptionsHydrating &&
3530
+ selectedAvailabilityMatchesDate &&
3482
3531
  selectedAvailability &&
3483
3532
  !selectedAvailability.isSummary &&
3484
3533
  selectedAvailability.returnOptions &&
@@ -3502,7 +3551,7 @@ export function NewBookingFlow({
3502
3551
  />
3503
3552
  )}
3504
3553
 
3505
- {selectedDate && selectedAvailability && afterItinerary
3554
+ {!selectedBookingOptionsHydrating && selectedDate && selectedAvailability && afterItinerary
3506
3555
  ? typeof afterItinerary === 'function'
3507
3556
  ? afterItinerary({
3508
3557
  selectedDate,
@@ -3513,7 +3562,8 @@ export function NewBookingFlow({
3513
3562
  : null}
3514
3563
 
3515
3564
  {/* Cancellation policy selection - all options from config, sorted by cheapest first. Also show when forced by promo. */}
3516
- {selectedAvailabilityMatchesDate &&
3565
+ {!selectedBookingOptionsHydrating &&
3566
+ selectedAvailabilityMatchesDate &&
3517
3567
  selectedAvailability &&
3518
3568
  ((pricingConfig?.cancellationPolicies?.length ?? 0) > 0 || forcedCancellationPolicy) &&
3519
3569
  (() => {
@@ -3540,7 +3590,7 @@ export function NewBookingFlow({
3540
3590
  })()}
3541
3591
 
3542
3592
  {/* Ticket Selection */}
3543
- {selectedAvailability && (
3593
+ {!selectedBookingOptionsHydrating && selectedAvailability && (
3544
3594
  <TicketSelector
3545
3595
  pricing={pricing}
3546
3596
  quantities={quantities}
@@ -3564,7 +3614,7 @@ export function NewBookingFlow({
3564
3614
  )}
3565
3615
 
3566
3616
  {/* Add-ons — optional extras for the selected product option */}
3567
- {selectedAvailability && totalQuantity > 0 && addOns.length > 0 && (
3617
+ {!selectedBookingOptionsHydrating && selectedAvailability && totalQuantity > 0 && addOns.length > 0 && (
3568
3618
  <AddOnsSection
3569
3619
  addOns={addOns}
3570
3620
  addOnSelections={addOnSelections}
@@ -3576,7 +3626,7 @@ export function NewBookingFlow({
3576
3626
  )}
3577
3627
 
3578
3628
  {/* Total and Checkout — shared PriceSummary component */}
3579
- {selectedAvailability && (
3629
+ {!selectedBookingOptionsHydrating && selectedAvailability && (
3580
3630
  <>
3581
3631
  <CheckoutForm
3582
3632
  priceSummaryLines={displayCheckoutPriceSummaryLines}
@@ -61,10 +61,12 @@ export function buildAvailabilitiesCacheKey(
61
61
  productId: string,
62
62
  optionIdsKey: string,
63
63
  promoCode: string | null,
64
- pricingProfileId?: string | null
64
+ pricingProfileId?: string | null,
65
+ cancellationPolicyProfileId?: string | null,
65
66
  ): string {
66
67
  const pp = (pricingProfileId ?? '').trim();
67
- return `${productId}|${optionIdsKey}|${promoCode ?? ''}|pp:${pp}`;
68
+ const cp = (cancellationPolicyProfileId ?? '').trim();
69
+ return `${productId}|${optionIdsKey}|${promoCode ?? ''}|pp:${pp}|cp:${cp}`;
68
70
  }
69
71
 
70
72
  export function AvailabilitiesCacheProvider({ children }: { children: ReactNode }) {
@@ -1623,6 +1623,7 @@ export interface GetAvailabilitiesOptions {
1623
1623
  promoCode?: string | null;
1624
1624
  allOptions?: boolean;
1625
1625
  summary?: boolean;
1626
+ responseMode?: 'summary' | 'booking-day';
1626
1627
  /**
1627
1628
  * When set, TicketBooth should return rates/precomputed prices for this partner pricing profile.
1628
1629
  * Must match the profile linked on the partner (e.g. capabilities.pricingProfileId).
@@ -1670,6 +1671,7 @@ export async function getAvailabilities(
1670
1671
  if (options?.promoCode?.trim()) params.set('promoCode', options.promoCode.trim());
1671
1672
  if (options?.allOptions === true) params.set('allOptions', 'true');
1672
1673
  if (options?.summary === true) params.set('summary', 'true');
1674
+ if (options?.responseMode) params.set('responseMode', options.responseMode);
1673
1675
  const pricingProfileId = options?.pricingProfileId?.trim();
1674
1676
  if (pricingProfileId) params.set('pricingProfileId', pricingProfileId);
1675
1677
  const cancellationPolicyProfileId = options?.cancellationPolicyProfileId?.trim();