@ticketboothapp/booking 1.2.191 → 1.2.193

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.191",
3
+ "version": "1.2.193",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
3
+ import React, { useState, useEffect, useMemo } from 'react';
4
4
  import { formatBookingRefForDisplay } from '../../lib/format-booking-ref';
5
5
  import {
6
6
  fetchProducts,
@@ -11,23 +11,10 @@ import {
11
11
  import { PickupLocationSelector } from './PickupLocationSelector';
12
12
  import { useTranslations } from '../../lib/booking/i18n';
13
13
  import { getPickupLocationDisclaimer } from '../../lib/booking/pickup-location-disclaimer';
14
+ import { useBookingHost } from '../../runtime';
15
+ import { resolvePickupLocationProduct } from './pickup-location-product';
14
16
  import styles from './PickupLocationDialog.module.css';
15
17
 
16
- function findProductWithPickupLocations(
17
- products: Product[],
18
- bookingProductId: string
19
- ): Product | null {
20
- for (const p of products) {
21
- if (p.productId === bookingProductId) return p;
22
- if (p.pickupLocations?.some((loc) => loc.id === bookingProductId)) return p;
23
- const opt = (p as Product & { options?: { optionId: string }[] })?.options?.find(
24
- (o) => o.optionId === bookingProductId
25
- );
26
- if (opt) return p;
27
- }
28
- return null;
29
- }
30
-
31
18
  /** Itinerary rows shown on manage-booking (may include optional label/time from API). */
32
19
  export interface PickupDialogItineraryStep {
33
20
  label?: string | null;
@@ -108,7 +95,8 @@ export function PickupLocationDialog({
108
95
  onSuccess,
109
96
  }: PickupLocationDialogProps) {
110
97
  const { t } = useTranslations();
111
- const [products, setProducts] = useState<Product[]>([]);
98
+ const { catalog } = useBookingHost();
99
+ const [liveProducts, setLiveProducts] = useState<Product[]>([]);
112
100
  const [loading, setLoading] = useState(false);
113
101
  const [error, setError] = useState<string | null>(null);
114
102
  const [selectedLocationId, setSelectedLocationId] = useState<string | null>(
@@ -130,33 +118,53 @@ export function PickupLocationDialog({
130
118
  const lastName = booking.customer?.lastName ?? '';
131
119
  const isPrivateShuttle = booking.productType === 'PRIVATE_SHUTTLE';
132
120
 
121
+ const staticProduct = useMemo(
122
+ () =>
123
+ catalog.getStaticProductByIdOrSlug
124
+ ? (catalog.getStaticProductByIdOrSlug(booking.productId) as Product | null)
125
+ : null,
126
+ [booking.productId, catalog]
127
+ );
133
128
  const product = useMemo(
134
- () => findProductWithPickupLocations(products, booking.productId),
135
- [products, booking.productId]
129
+ () => resolvePickupLocationProduct(staticProduct, liveProducts, booking.productId),
130
+ [staticProduct, liveProducts, booking.productId]
136
131
  );
137
- const pickupLocations = product?.pickupLocations ?? [];
138
- const destinations = product?.destinations ?? [];
132
+ const pickupLocations = useMemo(() => product?.pickupLocations ?? [], [product]);
133
+ const destinations = useMemo(() => product?.destinations ?? [], [product]);
139
134
 
140
- const loadProducts = useCallback(async () => {
141
- if (!companyId?.trim()) {
142
- setError('Missing company information');
135
+ useEffect(() => {
136
+ if (!isOpen) return;
137
+ setError(null);
138
+ if (staticProduct) {
139
+ setLoading(false);
143
140
  return;
144
141
  }
145
- setLoading(true);
146
- setError(null);
147
- try {
148
- const data = await fetchProducts(companyId);
149
- setProducts(data);
150
- } catch (e) {
151
- setError(e instanceof Error ? e.message : 'Failed to load pickup locations');
152
- } finally {
142
+ if (!companyId.trim()) {
153
143
  setLoading(false);
144
+ setError('Missing company information');
145
+ return;
154
146
  }
155
- }, [companyId]);
156
147
 
157
- useEffect(() => {
158
- if (isOpen && companyId) loadProducts();
159
- }, [isOpen, companyId, loadProducts]);
148
+ let cancelled = false;
149
+ setLiveProducts([]);
150
+ setLoading(true);
151
+ fetchProducts(companyId)
152
+ .then((data) => {
153
+ if (!cancelled) setLiveProducts(data);
154
+ })
155
+ .catch((e) => {
156
+ if (!cancelled) {
157
+ setError(e instanceof Error ? e.message : 'Failed to load pickup locations');
158
+ }
159
+ })
160
+ .finally(() => {
161
+ if (!cancelled) setLoading(false);
162
+ });
163
+
164
+ return () => {
165
+ cancelled = true;
166
+ };
167
+ }, [isOpen, companyId, staticProduct]);
160
168
 
161
169
  useEffect(() => {
162
170
  if (isOpen) {
@@ -0,0 +1,24 @@
1
+ import type { Product } from '../../lib/booking-api';
2
+
3
+ export function findProductForPickupLocations(
4
+ products: Product[],
5
+ bookingProductId: string,
6
+ ): Product | null {
7
+ for (const product of products) {
8
+ if (product.productId === bookingProductId) return product;
9
+ if (product.pickupLocations?.some((location) => location.id === bookingProductId)) {
10
+ return product;
11
+ }
12
+ if (product.options?.some((option) => option.optionId === bookingProductId)) return product;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ /** Keep manage-booking pickup choices aligned with the normal booking flow's static catalog. */
18
+ export function resolvePickupLocationProduct(
19
+ staticProduct: Product | null,
20
+ liveProducts: Product[],
21
+ bookingProductId: string,
22
+ ): Product | null {
23
+ return staticProduct ?? findProductForPickupLocations(liveProducts, bookingProductId);
24
+ }
@@ -13,6 +13,8 @@ const SAMSON_MALL_PICKUP_LOCATION_ID = 'loc_afkvmlNRwRqZ';
13
13
  const SAMSON_MALL_PICKUP_LOCATION_NAME = 'samson mall';
14
14
 
15
15
  const LAKE_LOUISE_HOTEL_PICKUP_LOCATIONS = new Map([
16
+ ['loc_Ll05oVACjjAx', 'HI Lake Louise Alpine Centre'],
17
+ ['loc_VAJ0L8amxnC5', 'Fairmont Lake Louise'],
16
18
  ['loc_arPLLVG1xKxw', 'Mountaineer Lodge'],
17
19
  ['loc_9ObwCZ7MxfjC', 'Paradise Lodge and Bungalows'],
18
20
  ['loc_ieW6EfdVeYd4', 'Post Hotel'],
@@ -94,6 +94,7 @@ import {
94
94
  resolveInitialAvailabilityFromBooking,
95
95
  } from '../src/components/booking/change-booking-flow-helpers';
96
96
  import { collapseAdminPromoLines } from '../src/components/booking/admin-change-receipt-lines';
97
+ import { resolvePickupLocationProduct } from '../src/components/booking/pickup-location-product';
97
98
  import {
98
99
  buildAdminChangePaymentChoiceData,
99
100
  buildAdminChangePayNowCheckoutModalData,
@@ -168,6 +169,43 @@ function test(name: string, fn: () => void | Promise<void>): void {
168
169
  }
169
170
  }
170
171
 
172
+ test('manage-booking pickup locations prefer the normal booking static product', () => {
173
+ const staticProduct = {
174
+ productId: 'product_1',
175
+ name: 'Static product',
176
+ description: null,
177
+ status: 'ACTIVE',
178
+ options: [{ optionId: 'option_1' }],
179
+ pickupLocations: [{ id: 'pickup_kept', name: 'Kept pickup' }],
180
+ } as NonNullable<Parameters<typeof resolvePickupLocationProduct>[0]>;
181
+ const liveProduct = {
182
+ ...staticProduct,
183
+ name: 'Live product',
184
+ pickupLocations: [
185
+ { id: 'pickup_kept', name: 'Kept pickup' },
186
+ { id: 'pickup_removed', name: 'Removed pickup' },
187
+ ],
188
+ };
189
+
190
+ const resolved = resolvePickupLocationProduct(staticProduct, [liveProduct], 'option_1');
191
+
192
+ assert.equal(resolved, staticProduct);
193
+ assert.deepEqual(resolved?.pickupLocations?.map((location) => location.id), ['pickup_kept']);
194
+ });
195
+
196
+ test('manage-booking pickup locations fall back to a live product matched by option id', () => {
197
+ const liveProduct = {
198
+ productId: 'product_1',
199
+ name: 'Live product',
200
+ description: null,
201
+ status: 'ACTIVE',
202
+ options: [{ optionId: 'option_1' }],
203
+ pickupLocations: [{ id: 'pickup_1', name: 'Pickup one' }],
204
+ } as NonNullable<Parameters<typeof resolvePickupLocationProduct>[0]>;
205
+
206
+ assert.equal(resolvePickupLocationProduct(null, [liveProduct], 'option_1'), liveProduct);
207
+ });
208
+
171
209
  test('availability cache revalidates only when a covered range is stale', () => {
172
210
  assert.equal(shouldRevalidateAvailabilityCache(true, false), false);
173
211
  assert.equal(shouldRevalidateAvailabilityCache(true, true), true);