@qite/tide-booking-component 1.4.36 → 1.4.37

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.
@@ -9,16 +9,18 @@ import useMediaQuery from '../../../shared/utils/use-media-query-util';
9
9
  import Filters from '../filters/filters';
10
10
  import ItemPicker from '../item-picker';
11
11
 
12
- import { TideClientConfig, detailsWL, search } from '@qite/tide-client';
12
+ import { TideClientConfig, details, detailsWL, getEntryLight, search } from '@qite/tide-client';
13
13
  import {
14
14
  BookingPackageDestination,
15
15
  BookingPackageDetailsRequest,
16
16
  BookingPackagePax,
17
17
  BookingPackageRequest,
18
18
  BookingPackageRequestRoom,
19
- BookingPackageSearchRequest
19
+ BookingPackageSearchRequest,
20
+ EntryLight,
21
+ EntryRoom
20
22
  } from '@qite/tide-client/build/types';
21
- import { getDateFromParams, getNumberFromParams, getRoomsFromParams } from '../../../shared/utils/query-string-util';
23
+ import { getDateFromParams, getNumberFromParams, getRoomsFromParams, getStringFromParams } from '../../../shared/utils/query-string-util';
22
24
  import { range } from 'lodash';
23
25
  import { Room } from '../../../booking-wizard/types';
24
26
  import Icon from '../icon';
@@ -29,17 +31,247 @@ import RoundTripResults from '../round-trip/round-trip-results';
29
31
  import { enrichFiltersWithResults } from '../filters/utility';
30
32
  import FlightResults from '../flight/flight-results';
31
33
  import { getTranslations } from '../../../shared/utils/localization-util';
34
+ import filters from '../filters/filters';
35
+ import FlightAccommodationResults from '../flight/flight-accommodation-results';
32
36
 
33
37
  const SearchResultsContainer: React.FC = () => {
34
- const isMobile = useMediaQuery('(max-width: 1200px)');
35
38
  const dispatch = useDispatch();
39
+
36
40
  const context = useContext(SearchResultsConfigurationContext);
37
41
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
38
- const { results, bookingPackageDetails, isLoading, filters, sortKey, selectedHotelId } = useSelector((state: SearchResultsRootState) => state.searchResults);
42
+
43
+ const { results, bookingPackageDetails, entry, isLoading, filters, sortKey, selectedHotelId } = useSelector(
44
+ (state: SearchResultsRootState) => state.searchResults
45
+ );
46
+
47
+ const isMobile = useMediaQuery('(max-width: 1200px)');
39
48
 
40
49
  const [searchTrigger, setSearchTrigger] = useState(0);
41
50
  const [initialFiltersSet, setInitialFiltersSet] = useState(false);
42
51
  const [initialFilters, setInitialFilters] = useState<Filter[]>([]);
52
+ const [filtersOpen, setFiltersOpen] = useState(false);
53
+ const [itineraryOpen, setItineraryOpen] = useState(false);
54
+
55
+ const sortingOptions: SortingOption[] = [
56
+ { key: 'price-asc', label: translations.SRP.PRICE_ASC },
57
+ { key: 'price-desc', label: translations.SRP.PRICE_DESC },
58
+ { key: 'departure-date', label: translations.SRP.DEPARTURE_ASC }
59
+ ];
60
+
61
+ const handleSortChange = (newSortKey: string) => {
62
+ dispatch(setSortKey(newSortKey));
63
+ };
64
+
65
+ const buildSearchFromEntry = (entry: EntryLight): BookingPackageRequest<BookingPackageSearchRequest> => {
66
+ const from = new Date(Math.min(...entry.items.map((i) => i.startDate.getTime()))).toISOString();
67
+ const to = new Date(Math.max(...entry.items.map((i) => i.endDate.getTime()))).toISOString();
68
+ const rooms = entry.rooms;
69
+
70
+ const hotelItem = entry.items.find((i) => i.productType === 3);
71
+
72
+ let country = hotelItem ? hotelItem.countryId : null;
73
+ let region = hotelItem ? hotelItem.regionId : null;
74
+ let oord = hotelItem ? hotelItem.oordId : null;
75
+ let city = hotelItem ? hotelItem.locationId : null;
76
+ let hotel = hotelItem ? hotelItem.productCode : null;
77
+
78
+ if (typeof window !== 'undefined') {
79
+ window.scrollTo(0, 0);
80
+ }
81
+
82
+ let destinationId: number | null = null;
83
+ let destinationIsCountry = false;
84
+ let destinationIsRegion = false;
85
+ let destinationIsOord = false;
86
+ let destinationIsLocation = false;
87
+
88
+ if (country) {
89
+ destinationId = country;
90
+ destinationIsCountry = true;
91
+ } else if (region) {
92
+ destinationId = region;
93
+ destinationIsRegion = true;
94
+ } else if (oord) {
95
+ destinationId = oord;
96
+ destinationIsOord = true;
97
+ } else if (city) {
98
+ destinationId = city;
99
+ destinationIsLocation = true;
100
+ }
101
+
102
+ var searchRequest = {
103
+ officeId: 1,
104
+ payload: {
105
+ catalogueIds: context!.tideConnection.catalogueIds ?? [],
106
+ serviceType: context!.type === 'hotel' || context!.type === 'hotel-flight' ? 3 : context!.type === 'flight' ? 7 : context!.type === 'roundTrip' ? 1 : 0,
107
+ searchType: 0,
108
+ destination: {
109
+ id: Number(destinationId),
110
+ isCountry: destinationIsCountry,
111
+ isRegion: destinationIsRegion,
112
+ isOord: destinationIsOord,
113
+ isLocation: destinationIsLocation
114
+ } as BookingPackageDestination,
115
+ rooms: getRequestRoomsFromEntry(rooms),
116
+ fromDate: from,
117
+ toDate: to,
118
+ earliestFromOffset: 0,
119
+ latestToOffset: 0,
120
+ includeFlights: true,
121
+ regimeCodes: entry.items.map((i) => i.regimeCode) || [],
122
+ useExactDates: true,
123
+ onlyCachedResults: false,
124
+ includeAllAllotments: true,
125
+ productCodes: hotel ? [hotel] : []
126
+ }
127
+ };
128
+
129
+ return searchRequest;
130
+ };
131
+
132
+ const buildSearchFromQueryParams = (params: URLSearchParams): BookingPackageRequest<BookingPackageSearchRequest> => {
133
+ let from = getDateFromParams(params, 'fromDate');
134
+ let to = getDateFromParams(params, 'toDate');
135
+ const rooms = getRoomsFromParams(params, 'rooms');
136
+ let country = getNumberFromParams(params, 'country');
137
+ let region = getNumberFromParams(params, 'region');
138
+ let oord = getNumberFromParams(params, 'oord');
139
+ let city = getNumberFromParams(params, 'location');
140
+ let hotel = getNumberFromParams(params, 'hotel');
141
+ let tagId = getNumberFromParams(params, 'tagId');
142
+
143
+ // temp hardcoded params
144
+ if (!from || !to) {
145
+ from = '2026-04-07';
146
+ to = '2026-04-13';
147
+ }
148
+ if (!country && !region && !oord && !city) {
149
+ region = 1;
150
+ }
151
+
152
+ if (typeof window !== 'undefined') {
153
+ window.scrollTo(0, 0);
154
+ }
155
+
156
+ let destinationId: number | null = null;
157
+ let destinationIsCountry = false;
158
+ let destinationIsRegion = false;
159
+ let destinationIsOord = false;
160
+ let destinationIsLocation = false;
161
+
162
+ if (country) {
163
+ destinationId = country;
164
+ destinationIsCountry = true;
165
+ } else if (region) {
166
+ destinationId = region;
167
+ destinationIsRegion = true;
168
+ } else if (oord) {
169
+ destinationId = oord;
170
+ destinationIsOord = true;
171
+ } else if (city) {
172
+ destinationId = city;
173
+ destinationIsLocation = true;
174
+ }
175
+
176
+ var searchRequest = {
177
+ officeId: 1,
178
+ payload: {
179
+ catalogueIds: context!.tideConnection.catalogueIds ?? [],
180
+ serviceType: context!.type === 'hotel' || context!.type === 'hotel-flight' ? 3 : context!.type === 'flight' ? 7 : context!.type === 'roundTrip' ? 1 : 0,
181
+ searchType: 0,
182
+ destination: {
183
+ id: Number(destinationId),
184
+ isCountry: destinationIsCountry,
185
+ isRegion: destinationIsRegion,
186
+ isOord: destinationIsOord,
187
+ isLocation: destinationIsLocation
188
+ } as BookingPackageDestination,
189
+ rooms: getRequestRooms(rooms),
190
+ fromDate: from,
191
+ toDate: to,
192
+ earliestFromOffset: 0,
193
+ latestToOffset: 0,
194
+ includeFlights: true,
195
+ regimeCodes:
196
+ filters
197
+ .find((f) => f.property === 'regime')
198
+ ?.options?.filter((o) => o.isChecked)
199
+ .flatMap((o) => o.value.toString()) || [],
200
+ minPrice: filters.find((f) => f.property === 'price')?.selectedMin,
201
+ maxPrice: filters.find((f) => f.property === 'price')?.selectedMax,
202
+ useExactDates: true,
203
+ onlyCachedResults: false,
204
+ includeAllAllotments: true,
205
+ productIds: hotel ? [hotel] : [],
206
+ productTagIds: tagId ? [tagId] : []
207
+ }
208
+ };
209
+
210
+ return searchRequest;
211
+ };
212
+
213
+ const getRequestRoomsFromEntry = (rooms: EntryRoom[] | null) => {
214
+ if (!rooms) {
215
+ // Fall back to 2 adults
216
+ var room = { index: 0, pax: [] } as BookingPackageRequestRoom;
217
+ range(0, 2).forEach(() => {
218
+ room.pax.push({
219
+ age: 30
220
+ } as BookingPackagePax);
221
+ });
222
+ return [room];
223
+ }
224
+
225
+ const requestRooms = rooms?.map((x, i) => {
226
+ var room = { index: i, pax: [] } as BookingPackageRequestRoom;
227
+ x.travellers.forEach((p) => {
228
+ room.pax.push({
229
+ age: p.age,
230
+ dateOfBirth: p.dateOfBirth
231
+ } as BookingPackagePax);
232
+ });
233
+
234
+ return room;
235
+ });
236
+
237
+ return requestRooms;
238
+ };
239
+
240
+ const getRequestRooms = (rooms: Room[] | null) => {
241
+ if (!rooms) {
242
+ // Fall back to 2 adults
243
+ var room = { index: 0, pax: [] } as BookingPackageRequestRoom;
244
+ range(0, 2).forEach(() => {
245
+ room.pax.push({
246
+ age: 30
247
+ } as BookingPackagePax);
248
+ });
249
+ return [room];
250
+ }
251
+
252
+ const requestRooms = rooms?.map((x, i) => {
253
+ var room = { index: i, pax: [] } as BookingPackageRequestRoom;
254
+ range(0, x.adults).forEach(() => {
255
+ room.pax.push({
256
+ age: 30
257
+ } as BookingPackagePax);
258
+ });
259
+ x.childAges.forEach((x) => {
260
+ room.pax.push({
261
+ age: x
262
+ } as BookingPackagePax);
263
+ });
264
+ return room;
265
+ });
266
+
267
+ return requestRooms;
268
+ };
269
+
270
+ useEffect(() => {
271
+ if (typeof document !== 'undefined') {
272
+ document.body.classList.toggle('has-overlay', filtersOpen);
273
+ }
274
+ }, [filtersOpen]);
43
275
 
44
276
  // seperate Search
45
277
  useEffect(() => {
@@ -50,90 +282,27 @@ const SearchResultsContainer: React.FC = () => {
50
282
  return;
51
283
  }
52
284
 
53
- const params = new URLSearchParams(location.search);
54
- let from = getDateFromParams(params, 'fromDate');
55
- let to = getDateFromParams(params, 'toDate');
56
- const rooms = getRoomsFromParams(params, 'rooms');
57
- let country = getNumberFromParams(params, 'country');
58
- let region = getNumberFromParams(params, 'region');
59
- let oord = getNumberFromParams(params, 'oord');
60
- let city = getNumberFromParams(params, 'location');
61
- let hotel = getNumberFromParams(params, 'hotel');
62
- let tagId = getNumberFromParams(params, 'tagId');
63
-
64
- // temp hardcoded params
65
- if (!from || !to) {
66
- from = '2026-04-07';
67
- to = '2026-04-13';
68
- }
69
- if (!country && !region && !oord && !city) {
70
- region = 1;
71
- }
72
-
73
- if (typeof window !== 'undefined') {
74
- window.scrollTo(0, 0);
75
- }
76
-
77
- let destinationId: number | null = null;
78
- let destinationIsCountry = false;
79
- let destinationIsRegion = false;
80
- let destinationIsOord = false;
81
- let destinationIsLocation = false;
82
-
83
- if (country) {
84
- destinationId = country;
85
- destinationIsCountry = true;
86
- } else if (region) {
87
- destinationId = region;
88
- destinationIsRegion = true;
89
- } else if (oord) {
90
- destinationId = oord;
91
- destinationIsOord = true;
92
- } else if (city) {
93
- destinationId = city;
94
- destinationIsLocation = true;
95
- }
96
-
97
- const searchRequest: BookingPackageRequest<BookingPackageSearchRequest> = {
98
- officeId: 1,
99
- payload: {
100
- catalogueIds: context.tideConnection.catalogueIds ?? [],
101
- serviceType:
102
- context?.type === 'hotel' || context?.type === 'hotel-flight' ? 3 : context?.type === 'flight' ? 7 : context?.type === 'roundTrip' ? 1 : 0,
103
- searchType: 0,
104
- destination: {
105
- id: Number(destinationId),
106
- isCountry: destinationIsCountry,
107
- isRegion: destinationIsRegion,
108
- isOord: destinationIsOord,
109
- isLocation: destinationIsLocation
110
- } as BookingPackageDestination,
111
- rooms: getRequestRooms(rooms),
112
- fromDate: from,
113
- toDate: to,
114
- earliestFromOffset: 0,
115
- latestToOffset: 0,
116
- includeFlights: true,
117
- regimeCodes:
118
- filters
119
- .find((f) => f.property === 'regime')
120
- ?.options?.filter((o) => o.isChecked)
121
- .flatMap((o) => o.value.toString()) || [],
122
- minPrice: filters.find((f) => f.property === 'price')?.selectedMin,
123
- maxPrice: filters.find((f) => f.property === 'price')?.selectedMax,
124
- useExactDates: true,
125
- onlyCachedResults: false,
126
- includeAllAllotments: true,
127
- productIds: hotel ? [hotel] : [],
128
- productTagIds: tagId ? [tagId] : []
129
- }
130
- };
131
-
132
285
  const config: TideClientConfig = {
133
286
  host: context.tideConnection.host,
134
287
  apiKey: context.tideConnection.apiKey
135
288
  };
136
289
 
290
+ const params = new URLSearchParams(location.search);
291
+ let entryId = getStringFromParams(params, 'entryId');
292
+ let entryLight: EntryLight | null = null;
293
+ // If entryId is present, we want to fetch the details of that entry and use the details to populate the search results, instead of running a search with the other query params
294
+
295
+ let searchRequest: BookingPackageRequest<BookingPackageSearchRequest>;
296
+
297
+ if (entryId) {
298
+ entryLight = await getEntryLight(config, entryId);
299
+ // populate itinerary store
300
+ dispatch(setEntry({ entry: entryLight }));
301
+ searchRequest = buildSearchFromEntry(entryLight);
302
+ } else {
303
+ searchRequest = buildSearchFromQueryParams(params);
304
+ }
305
+
137
306
  const packageSearchResults = await search(config, searchRequest);
138
307
 
139
308
  console.log('Search results', packageSearchResults);
@@ -147,8 +316,17 @@ const SearchResultsContainer: React.FC = () => {
147
316
 
148
317
  dispatch(setResults({ results: packageSearchResults }));
149
318
  if (packageSearchResults?.length > 0) {
150
- dispatch(setSelectedHotel(packageSearchResults[0].productId));
319
+ if (entryId) {
320
+ const matching = packageSearchResults.find((r) => r.productId === entry?.id);
321
+
322
+ if (matching) {
323
+ dispatch(setSelectedHotel(matching.productId));
324
+ }
325
+ } else {
326
+ dispatch(setSelectedHotel(packageSearchResults[0]?.productId));
327
+ }
151
328
  }
329
+
152
330
  dispatch(setIsLoading(false));
153
331
  } catch (err) {
154
332
  console.error('Search failed', err);
@@ -156,7 +334,9 @@ const SearchResultsContainer: React.FC = () => {
156
334
  }
157
335
  };
158
336
 
159
- runSearch();
337
+ if (!context?.showMockup) {
338
+ runSearch();
339
+ }
160
340
  }, [location.search, searchTrigger]);
161
341
 
162
342
  // Seperate detailsCall
@@ -171,11 +351,21 @@ const SearchResultsContainer: React.FC = () => {
171
351
  };
172
352
 
173
353
  const selectedItem = results.find((r) => r.productId === selectedHotelId);
174
- if (!selectedItem) return;
354
+ if (!selectedItem) {
355
+ // TODO: handle this case better, show an error message to the user
356
+ return;
357
+ }
175
358
 
176
359
  const params = new URLSearchParams(location.search);
177
- const rooms = getRoomsFromParams(params, 'rooms');
178
- const requestRooms = getRequestRooms(rooms);
360
+ let entryId = getStringFromParams(params, 'entryId');
361
+
362
+ let requestRooms;
363
+ if (entry && entryId) {
364
+ requestRooms = getRequestRoomsFromEntry(entry.rooms);
365
+ } else {
366
+ const rooms = getRoomsFromParams(params, 'rooms');
367
+ requestRooms = getRequestRooms(rooms);
368
+ }
179
369
 
180
370
  const detailsRequest: BookingPackageRequest<BookingPackageDetailsRequest> = {
181
371
  officeId: 1,
@@ -197,10 +387,17 @@ const SearchResultsContainer: React.FC = () => {
197
387
  }
198
388
  };
199
389
 
200
- const detailsWLResponse = await detailsWL(config, detailsRequest);
201
- console.log('Details with entryLight:', detailsWLResponse);
202
- dispatch(setBookingPackageDetails({ details: detailsWLResponse?.payload?.bookingPackage }));
203
- dispatch(setEntry({ entry: detailsWLResponse?.payload?.entry }));
390
+ if (entry && entryId) {
391
+ requestRooms = getRequestRoomsFromEntry(entry.rooms);
392
+ const detailsResponse = await details(config, detailsRequest);
393
+ console.log('Details:', detailsResponse);
394
+ dispatch(setBookingPackageDetails({ details: detailsResponse?.payload }));
395
+ } else {
396
+ const detailsWLResponse = await detailsWL(config, detailsRequest);
397
+ console.log('Details with entryLight:', detailsWLResponse);
398
+ dispatch(setBookingPackageDetails({ details: detailsWLResponse?.payload?.bookingPackage }));
399
+ dispatch(setEntry({ entry: detailsWLResponse?.payload?.entry }));
400
+ }
204
401
  } catch (err) {
205
402
  console.error('Failed to fetch package details', err);
206
403
  }
@@ -209,55 +406,6 @@ const SearchResultsContainer: React.FC = () => {
209
406
  fetchPackageDetails();
210
407
  }, [selectedHotelId]);
211
408
 
212
- const getRequestRooms = (rooms: Room[] | null) => {
213
- if (!rooms) {
214
- // Fall back to 2 adults
215
- var room = { index: 0, pax: [] } as BookingPackageRequestRoom;
216
- range(0, 2).forEach(() => {
217
- room.pax.push({
218
- age: 30
219
- } as BookingPackagePax);
220
- });
221
- return [room];
222
- }
223
-
224
- const requestRooms = rooms?.map((x, i) => {
225
- var room = { index: i, pax: [] } as BookingPackageRequestRoom;
226
- range(0, x.adults).forEach(() => {
227
- room.pax.push({
228
- age: 30
229
- } as BookingPackagePax);
230
- });
231
- x.childAges.forEach((x) => {
232
- room.pax.push({
233
- age: x
234
- } as BookingPackagePax);
235
- });
236
- return room;
237
- });
238
-
239
- return requestRooms;
240
- };
241
-
242
- const [filtersOpen, setFiltersOpen] = useState(false);
243
- const [itineraryOpen, setItineraryOpen] = useState(false);
244
-
245
- const handleSortChange = (newSortKey: string) => {
246
- dispatch(setSortKey(newSortKey));
247
- };
248
-
249
- useEffect(() => {
250
- if (typeof document !== 'undefined') {
251
- document.body.classList.toggle('has-overlay', filtersOpen);
252
- }
253
- }, [filtersOpen]);
254
-
255
- const sortingOptions: SortingOption[] = [
256
- { key: 'price-asc', label: translations.SRP.PRICE_ASC },
257
- { key: 'price-desc', label: translations.SRP.PRICE_DESC },
258
- { key: 'departure-date', label: translations.SRP.DEPARTURE_ASC }
259
- ];
260
-
261
409
  return (
262
410
  <div id="tide-booking" className="search__bg">
263
411
  {context && (
@@ -329,17 +477,17 @@ const SearchResultsContainer: React.FC = () => {
329
477
  <div className="search__results__wrapper">
330
478
  {context.showTabViews && <TabViews />}
331
479
 
332
- {/* {context.showRoundTripResults && <RoundTripResults />} */}
480
+ {context.showRoundTripResults && context.showMockup && <RoundTripResults />}
333
481
 
334
482
  {context.showFlightResults && bookingPackageDetails?.outwardFlights && (
335
- <FlightResults flights={bookingPackageDetails.outwardFlights} isDeparture={true} />
483
+ <FlightResults flights={bookingPackageDetails?.outwardFlights} isDeparture={true} />
336
484
  )}
337
485
 
338
486
  {context.showHotelAccommodationResults && <HotelAccommodationResults isLoading={isLoading} context={context} />}
339
- {/* {context.showFlightAccommodationResults && <FlightAccommodationResults />} */}
487
+ {context.showFlightAccommodationResults && context.showMockup && <FlightAccommodationResults />}
340
488
 
341
489
  {context.showFlightResults && bookingPackageDetails?.returnFlights && (
342
- <FlightResults flights={bookingPackageDetails.returnFlights} isDeparture={false} />
490
+ <FlightResults flights={bookingPackageDetails?.returnFlights} isDeparture={false} />
343
491
  )}
344
492
  </div>
345
493
  </div>
@@ -27,7 +27,7 @@ export interface SearchResultsConfiguration {
27
27
  showRoundTripResults?: boolean;
28
28
  showCustomCards?: boolean;
29
29
  customCardRenderer?: (result: SearchResult) => ReactNode;
30
-
30
+ showMockup?: boolean;
31
31
  // Map view
32
32
  // not supported for now
33
33
  showMapView?: boolean;