@qite/tide-components 1.0.7 → 1.0.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.
@@ -429,6 +429,7 @@ var SRP$f = {
429
429
  TRAVEL_GROUP: "مجموعة المسافرين",
430
430
  EXCURSION: "رحلة",
431
431
  LOADING_EXCURSIONS: "جارٍ تحميل الرحلات...",
432
+ LOADING_ACTIVITIES: "جارٍ تحميل الأنشطة...",
432
433
  LOADING_FLIGHTS: "جارٍ تحميل الرحلات الجوية...",
433
434
  LOADING_ACCOMMODATIONS: "جارٍ تحميل الإقامات...",
434
435
  LOADING_ITINERARY: "جارٍ تحميل خط سير الرحلة...",
@@ -873,6 +874,7 @@ var SRP$e = {
873
874
  TRAVEL_GROUP: "Rejseselskab",
874
875
  EXCURSION: "Udflugt",
875
876
  LOADING_EXCURSIONS: "Indlæser udflugter...",
877
+ LOADING_ACTIVITIES: "Indlæser aktiviteter...",
876
878
  LOADING_FLIGHTS: "Indlæser fly...",
877
879
  LOADING_ACCOMMODATIONS: "Indlæser indkvarteringer...",
878
880
  LOADING_ITINERARY: "Indlæser rejseplan...",
@@ -1317,6 +1319,7 @@ var SRP$d = {
1317
1319
  TRAVEL_GROUP: "Reisegruppe",
1318
1320
  EXCURSION: "Ausflug",
1319
1321
  LOADING_EXCURSIONS: "Lade Ausflüge...",
1322
+ LOADING_ACTIVITIES: "Lade Aktivitäten...",
1320
1323
  LOADING_FLIGHTS: "Lade Flüge...",
1321
1324
  LOADING_ACCOMMODATIONS: "Lade Unterkünfte...",
1322
1325
  LOADING_ITINERARY: "Lade Reiseverlauf...",
@@ -1765,6 +1768,7 @@ var SRP$c = {
1765
1768
  TRAVEL_GROUP: "Travel group",
1766
1769
  EXCURSION: "Excursion",
1767
1770
  LOADING_EXCURSIONS: "Loading excursions...",
1771
+ LOADING_ACTIVITIES: "Loading activities...",
1768
1772
  LOADING_FLIGHTS: "Loading flights...",
1769
1773
  LOADING_ACCOMMODATIONS: "Loading accommodations...",
1770
1774
  LOADING_ITINERARY: "Loading itinerary...",
@@ -2209,6 +2213,7 @@ var SRP$b = {
2209
2213
  TRAVEL_GROUP: "Grupo de viaje",
2210
2214
  EXCURSION: "Excursión",
2211
2215
  LOADING_EXCURSIONS: "Cargando excursiones...",
2216
+ LOADING_ACTIVITIES: "Cargando actividades...",
2212
2217
  LOADING_FLIGHTS: "Cargando vuelos...",
2213
2218
  LOADING_ACCOMMODATIONS: "Cargando alojamientos...",
2214
2219
  LOADING_ITINERARY: "Cargando itinerario...",
@@ -2657,6 +2662,7 @@ var SRP$a = {
2657
2662
  TRAVEL_GROUP: "Groupe de voyageurs",
2658
2663
  EXCURSION: "Excursion",
2659
2664
  LOADING_EXCURSIONS: "Chargement des excursions...",
2665
+ LOADING_ACTIVITIES: "Chargement des activités...",
2660
2666
  LOADING_FLIGHTS: "Chargement des vols...",
2661
2667
  LOADING_ACCOMMODATIONS: "Chargement des hébergements...",
2662
2668
  LOADING_ITINERARY: "Chargement de l’itinéraire...",
@@ -3101,6 +3107,7 @@ var SRP$9 = {
3101
3107
  TRAVEL_GROUP: "Groupe de voyageurs",
3102
3108
  EXCURSION: "Excursion",
3103
3109
  LOADING_EXCURSIONS: "Chargement des excursions...",
3110
+ LOADING_ACTIVITIES: "Chargement des activités...",
3104
3111
  LOADING_FLIGHTS: "Chargement des vols...",
3105
3112
  LOADING_ACCOMMODATIONS: "Chargement des hébergements...",
3106
3113
  LOADING_ITINERARY: "Chargement de l’itinéraire...",
@@ -3545,6 +3552,7 @@ var SRP$8 = {
3545
3552
  TRAVEL_GROUP: "Ferðahópur",
3546
3553
  EXCURSION: "Útflutningur",
3547
3554
  LOADING_EXCURSIONS: "Hleðst inn í útflutninga...",
3555
+ LOADING_ACTIVITIES: "Hleð inn afþreyingu...",
3548
3556
  LOADING_FLIGHTS: "Hleðst inn í flug...",
3549
3557
  LOADING_ACCOMMODATIONS: "Hleðst inn í gistingar...",
3550
3558
  LOADING_ITINERARY: "Hleðst inn í ferðaráætlun...",
@@ -3989,6 +3997,7 @@ var SRP$7 = {
3989
3997
  TRAVEL_GROUP: "Gruppo di viaggio",
3990
3998
  EXCURSION: "Escursione",
3991
3999
  LOADING_EXCURSIONS: "Caricamento delle escursioni...",
4000
+ LOADING_ACTIVITIES: "Caricamento delle attività...",
3992
4001
  LOADING_FLIGHTS: "Caricamento dei voli...",
3993
4002
  LOADING_ACCOMMODATIONS: "Caricamento degli alloggi...",
3994
4003
  LOADING_ITINERARY: "Caricamento dell'itinerario...",
@@ -4437,6 +4446,7 @@ var SRP$6 = {
4437
4446
  TRAVEL_GROUP: "Reisgezelschap",
4438
4447
  EXCURSION: "Excursie",
4439
4448
  LOADING_EXCURSIONS: "Excursies laden...",
4449
+ LOADING_ACTIVITIES: "Activiteiten laden...",
4440
4450
  LOADING_FLIGHTS: "Vluchten laden...",
4441
4451
  LOADING_ACCOMMODATIONS: "Accommodaties laden...",
4442
4452
  LOADING_ITINERARY: "Reisplan laden...",
@@ -4881,6 +4891,7 @@ var SRP$5 = {
4881
4891
  TRAVEL_GROUP: "Reisgezelschap",
4882
4892
  EXCURSION: "Excursie",
4883
4893
  LOADING_EXCURSIONS: "Excursies laden...",
4894
+ LOADING_ACTIVITIES: "Activiteiten laden...",
4884
4895
  LOADING_FLIGHTS: "Vluchten laden...",
4885
4896
  LOADING_ACCOMMODATIONS: "Accommodaties laden...",
4886
4897
  LOADING_ITINERARY: "Reisplan laden...",
@@ -5325,6 +5336,7 @@ var SRP$4 = {
5325
5336
  TRAVEL_GROUP: "Reisefølge",
5326
5337
  EXCURSION: "Utflukt",
5327
5338
  LOADING_EXCURSIONS: "Laster utflukter...",
5339
+ LOADING_ACTIVITIES: "Laster aktiviteter...",
5328
5340
  LOADING_FLIGHTS: "Laster flyvninger...",
5329
5341
  LOADING_ACCOMMODATIONS: "Laster overnattinger...",
5330
5342
  LOADING_ITINERARY: "Laster reiserute...",
@@ -5769,6 +5781,7 @@ var SRP$3 = {
5769
5781
  TRAVEL_GROUP: "Grupa podróżnych",
5770
5782
  EXCURSION: "Wycieczka",
5771
5783
  LOADING_EXCURSIONS: "Ładowanie wycieczek...",
5784
+ LOADING_ACTIVITIES: "Ładowanie atrakcji...",
5772
5785
  LOADING_FLIGHTS: "Ładowanie lotów...",
5773
5786
  LOADING_ACCOMMODATIONS: "Ładowanie zakwaterowań...",
5774
5787
  LOADING_ITINERARY: "Ładowanie planu podróży...",
@@ -6213,6 +6226,7 @@ var SRP$2 = {
6213
6226
  TRAVEL_GROUP: "Grupo de viajantes",
6214
6227
  EXCURSION: "Excursão",
6215
6228
  LOADING_EXCURSIONS: "A carregar excursões...",
6229
+ LOADING_ACTIVITIES: "A carregar atividades...",
6216
6230
  LOADING_FLIGHTS: "A carregar voos...",
6217
6231
  LOADING_ACCOMMODATIONS: "A carregar alojamentos...",
6218
6232
  LOADING_ITINERARY: "A carregar itinerário...",
@@ -6657,6 +6671,7 @@ var SRP$1 = {
6657
6671
  TRAVEL_GROUP: "Resesällskap",
6658
6672
  EXCURSION: "Utflykt",
6659
6673
  LOADING_EXCURSIONS: "Laddar utflykter...",
6674
+ LOADING_ACTIVITIES: "Laddar aktiviteter...",
6660
6675
  LOADING_FLIGHTS: "Laddar flyg...",
6661
6676
  LOADING_ACCOMMODATIONS: "Laddar boenden...",
6662
6677
  LOADING_ITINERARY: "Laddar resplan...",
@@ -7101,6 +7116,7 @@ var SRP = {
7101
7116
  TRAVEL_GROUP: "旅行グループ",
7102
7117
  EXCURSION: "エクスカーション",
7103
7118
  LOADING_EXCURSIONS: "エクスカーションを読み込み中...",
7119
+ LOADING_ACTIVITIES: "アクティビティを読み込み中...",
7104
7120
  LOADING_FLIGHTS: "フライトを読み込み中...",
7105
7121
  LOADING_ACCOMMODATIONS: "宿泊施設を読み込み中...",
7106
7122
  LOADING_ITINERARY: "旅程を読み込み中...",
@@ -27442,7 +27458,9 @@ const initialState$1 = {
27442
27458
  trajectFlightAvailability: {},
27443
27459
  trajectDaySuggestions: {},
27444
27460
  trajectEditLineGuid: null,
27445
- trajectActivitiesDayOrder: null
27461
+ trajectActivitiesDayOrder: null,
27462
+ trajectResolving: false,
27463
+ trajectItineraryView: false
27446
27464
  };
27447
27465
  const searchResultsSlice = createSlice({
27448
27466
  name: 'searchResults',
@@ -27712,6 +27730,12 @@ const searchResultsSlice = createSlice({
27712
27730
  setTrajectActivitiesDayOrder(state, action) {
27713
27731
  state.trajectActivitiesDayOrder = action.payload;
27714
27732
  },
27733
+ setTrajectResolving(state, action) {
27734
+ state.trajectResolving = action.payload;
27735
+ },
27736
+ setTrajectItineraryView(state, action) {
27737
+ state.trajectItineraryView = action.payload;
27738
+ },
27715
27739
  updateTrajectNodeResult(state, action) {
27716
27740
  const { lineGuid, result } = action.payload;
27717
27741
  const node = state.trajectNodeAvailability[lineGuid];
@@ -27727,10 +27751,12 @@ const searchResultsSlice = createSlice({
27727
27751
  state.trajectDaySuggestions = {};
27728
27752
  state.trajectEditLineGuid = null;
27729
27753
  state.trajectActivitiesDayOrder = null;
27754
+ state.trajectResolving = false;
27755
+ state.trajectItineraryView = false;
27730
27756
  }
27731
27757
  }
27732
27758
  });
27733
- const { setResults, setFilteredResults, setSelectedSearchResult, setPackagingAccoResults, setFilteredPackagingAccoResults, setFilteredPackagingFlightResults, setPackagingAccoSearchDetails, setSelectedPackagingAccoResult, setPackagingFlightResults, setSelectedPackagingFlight, setSelectedFlight, setSelectedFlightDetails, setBookingPackageDetails, selectFlight, setIsLoading, setFlightsLoading, setInitialFilters, setFilters, resetFilters, setInitialFlightFilters, setFlightFilters, resetFlightFilters, setSortType, setFlightSortType, setActiveTab, setCurrentPage, resetSearchState, setFlyInIsOpen, setEditablePackagingEntry, setTransactionId, setFlyInType, setPriceDetails, setItinerary, setSelectedOutwardKey, setSelectedReturnKey, resetFlightSelection, setExcursionSearchParams, setSelectedExcursionSearchResult, confirmExcursionForDay, removeConfirmedExcursionForDay, clearConfirmedExcursionsForDay, setBookPackagingEntry, setCurrentStep, setBookingNumber, setTrajectNodeLoading, setTrajectNodeAvailability, setTrajectNodeSelection, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, setTrajectEditLineGuid, setTrajectActivitiesDayOrder, setTrajectDaySuggestionsLoading, setTrajectDaySuggestions, addEditableEntryLines, updateTrajectNodeResult, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
27759
+ const { setResults, setFilteredResults, setSelectedSearchResult, setPackagingAccoResults, setFilteredPackagingAccoResults, setFilteredPackagingFlightResults, setPackagingAccoSearchDetails, setSelectedPackagingAccoResult, setPackagingFlightResults, setSelectedPackagingFlight, setSelectedFlight, setSelectedFlightDetails, setBookingPackageDetails, selectFlight, setIsLoading, setFlightsLoading, setInitialFilters, setFilters, resetFilters, setInitialFlightFilters, setFlightFilters, resetFlightFilters, setSortType, setFlightSortType, setActiveTab, setCurrentPage, resetSearchState, setFlyInIsOpen, setEditablePackagingEntry, setTransactionId, setFlyInType, setPriceDetails, setItinerary, setSelectedOutwardKey, setSelectedReturnKey, resetFlightSelection, setExcursionSearchParams, setSelectedExcursionSearchResult, confirmExcursionForDay, removeConfirmedExcursionForDay, clearConfirmedExcursionsForDay, setBookPackagingEntry, setCurrentStep, setBookingNumber, setTrajectNodeLoading, setTrajectNodeAvailability, setTrajectNodeSelection, setTrajectResolving, setTrajectItineraryView, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, setTrajectEditLineGuid, setTrajectActivitiesDayOrder, setTrajectDaySuggestionsLoading, setTrajectDaySuggestions, addEditableEntryLines, updateTrajectNodeResult, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
27734
27760
  var searchResultsReducer = searchResultsSlice.reducer;
27735
27761
 
27736
27762
  const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
@@ -28020,7 +28046,22 @@ const buildDestination = (line, node, result) => {
28020
28046
  null;
28021
28047
  return { id: 0, ...place, ...coordinates };
28022
28048
  };
28023
- const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
28049
+ const hasDestination = (destination) => !!destination && (!!destination.id || (!!destination.latitude && !!destination.longitude));
28050
+ const buildTrajectClientConfig = (context) => ({
28051
+ host: context.tideConnection.host,
28052
+ apiKey: context.tideConnection.apiKey
28053
+ });
28054
+ const buildTrajectSearchContext = (context, trajectEntry) => ({
28055
+ transactionId: trajectEntry.entry.transactionId,
28056
+ officeId: context.tideConnection.officeId ?? 1,
28057
+ catalogueId: context.searchConfiguration.defaultCatalogueId ?? context.tideConnection.catalogueIds?.[0] ?? 1,
28058
+ searchConfigurationId: context.searchConfiguration.id,
28059
+ portalId: context.portalId ?? null,
28060
+ agentId: context.agentId ?? null,
28061
+ language: context.languageCode ?? 'en-GB',
28062
+ rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
28063
+ });
28064
+ const buildTrajectNodeRequest = ({ node, line }, context, productCode, destination) => ({
28024
28065
  transactionId: context.transactionId,
28025
28066
  officeId: context.officeId,
28026
28067
  agentId: context.agentId ?? null,
@@ -28031,25 +28072,69 @@ const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
28031
28072
  language: context.language,
28032
28073
  serviceType: node.serviceType,
28033
28074
  fromDate: toDateOnlyString(line.from),
28034
- toDate: toDateOnlyString(line.to),
28035
- destination: buildDestination(line, node),
28075
+ // An excursion happens on a day rather than over a span. The traject gives its line an end a day
28076
+ // out, the way a night is counted, which asks the search for a window it does not have.
28077
+ toDate: toDateOnlyString(node.serviceType === EXCURSION_SERVICE_TYPE ? line.from : line.to),
28078
+ destination: destination ?? buildDestination(line, node),
28036
28079
  productCode,
28037
28080
  rooms: context.rooms,
28038
28081
  tagIds: []
28039
28082
  });
28040
- /**
28041
- * Entry lines for an excursion the traveller picked out of the suggestions.
28042
- *
28043
- * A traject node already has its lines and only needs stamping; a suggestion has none, so this
28044
- * builds them — one per selected option, as the regular day-by-day excursion flow does, since an
28045
- * excursion's options are ticket types (adult, child) rather than rooms. Where the search marks no
28046
- * default, the first option of each room stands in.
28047
- */
28048
- const buildTrajectExcursionLines = (result) => {
28049
- const options = (result.rooms ?? []).flatMap((room) => {
28050
- const selected = (room.options ?? []).filter((x) => x.isSelected);
28051
- return selected.length ? selected : (room.options ?? []).slice(0, 1);
28083
+ const coversDay = ({ node, line }, dayOrder) => {
28084
+ const nights = Math.max(differenceInCalendarDays(new Date(line.to), new Date(line.from)), 1);
28085
+ return dayOrder >= node.dayOrder && dayOrder < node.dayOrder + nights;
28086
+ };
28087
+ const getDayDestinationItem = (items, dayOrder) => {
28088
+ const knowsWhereItIs = (x) => hasDestination(buildDestination(x.line, x.node));
28089
+ const stay = items.find((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE && coversDay(x, dayOrder) && knowsWhereItIs(x));
28090
+ if (stay)
28091
+ return stay;
28092
+ return ([...items]
28093
+ .filter((x) => x.node.dayOrder <= dayOrder && knowsWhereItIs(x))
28094
+ .sort((a, b) => a.node.dayOrder - b.node.dayOrder || a.node.order - b.node.order)
28095
+ .pop() ?? null);
28096
+ };
28097
+ const getDayDestination = (items, availability, dayOrder) => {
28098
+ const item = getDayDestinationItem(items, dayOrder);
28099
+ if (!item)
28100
+ return null;
28101
+ const resolved = availability[item.line.guid];
28102
+ const result = resolved?.results.find((x) => x.code === resolved.selectedCode) ?? null;
28103
+ const destination = buildDestination(item.line, item.node, result);
28104
+ return hasDestination(destination) ? destination : null;
28105
+ };
28106
+ const buildTrajectDayExcursionRequest = (date, destination, context) => ({
28107
+ transactionId: context.transactionId,
28108
+ officeId: context.officeId,
28109
+ agentId: context.agentId ?? null,
28110
+ catalogueId: context.catalogueId,
28111
+ searchConfigurationId: context.searchConfigurationId,
28112
+ portalId: context.portalId ?? null,
28113
+ vendorConfigurationId: null,
28114
+ language: context.language,
28115
+ serviceType: EXCURSION_SERVICE_TYPE,
28116
+ fromDate: toDateOnlyString(date),
28117
+ toDate: toDateOnlyString(date),
28118
+ destination,
28119
+ productCode: '',
28120
+ rooms: context.rooms,
28121
+ tagIds: []
28122
+ });
28123
+ const getExcursionOptionGroups = (result) => {
28124
+ const groups = new Map();
28125
+ (result.rooms ?? [])
28126
+ .flatMap((room) => room.options ?? [])
28127
+ .forEach((option) => {
28128
+ const key = [...(option.paxIds ?? [])].sort((a, b) => a - b).join(',');
28129
+ groups.set(key, [...(groups.get(key) ?? []), option]);
28052
28130
  });
28131
+ return [...groups.values()];
28132
+ };
28133
+ const excursionNeedsChoice = (result) => getExcursionOptionGroups(result).some((group) => group.length > 1 && !group.some((option) => option.isSelected));
28134
+ const buildTrajectExcursionLines = (result) => {
28135
+ const options = getExcursionOptionGroups(result)
28136
+ .map((group) => group.find((x) => x.isSelected) ?? (group.length === 1 ? group[0] : null))
28137
+ .filter((x) => !!x);
28053
28138
  if (!options.length)
28054
28139
  return [];
28055
28140
  const guids = options.map((option) => option.guid || crypto.randomUUID());
@@ -28078,6 +28163,21 @@ const buildTrajectExcursionLines = (result) => {
28078
28163
  flightInformation: null
28079
28164
  }));
28080
28165
  };
28166
+ const buildExcursionSearchParams = (result) => ({
28167
+ date: result.fromDate,
28168
+ fromDate: result.fromDate,
28169
+ toDate: result.toDate,
28170
+ countryId: result.countryId,
28171
+ regionId: result.regionId,
28172
+ oordId: result.oordId,
28173
+ locationId: result.locationId,
28174
+ locationName: result.locationName,
28175
+ accommodationCode: null,
28176
+ accommodationName: null,
28177
+ hotelCode: result.code,
28178
+ hotelName: result.name,
28179
+ rooms: result.rooms ?? []
28180
+ });
28081
28181
  const buildDayActivityCards = (items, availability, suggestions) => {
28082
28182
  const cards = [];
28083
28183
  const seen = new Set();
@@ -28094,11 +28194,27 @@ const buildDayActivityCards = (items, availability, suggestions) => {
28094
28194
  results.filter((x) => !!findCandidateForResult(item.node, x)).forEach((result) => push(result, item));
28095
28195
  });
28096
28196
  suggestions.forEach((result) => push(result));
28097
- // Whatever the nodes' own destination search found and the day's search did not — normally
28098
- // nothing, as they are the same search, but a node can resolve where a day has no place to look.
28099
- items.forEach((item) => (availability[item.line.guid]?.results ?? []).forEach((result) => push(result, item)));
28197
+ items.forEach((item) => (availability[item.line.guid]?.results ?? []).forEach((result) => push(result)));
28100
28198
  return cards;
28101
28199
  };
28200
+ /**
28201
+ * Where to search for a node.
28202
+ *
28203
+ * Normally the node says so itself, through the product it was configured with. An excursion slot
28204
+ * left empty on the traject says nothing at all — no code, no alternatives, and a line with no
28205
+ * place on it — so it falls back to wherever the party is staying that day, the same anchor the
28206
+ * day's own activity search uses.
28207
+ */
28208
+ const getNodeSearchDestination = (items, item) => {
28209
+ const own = buildDestination(item.line, item.node);
28210
+ if (hasDestination(own))
28211
+ return own;
28212
+ return getDayDestination(items, {}, item.node.dayOrder) ?? own;
28213
+ };
28214
+ const getConfiguredExcursionCodes = (items, dayOrder) => new Set(items
28215
+ .filter((x) => x.node.dayOrder === dayOrder && x.node.serviceType === EXCURSION_SERVICE_TYPE)
28216
+ .flatMap((x) => [x.node.code, ...getNodeCandidates(x.node).map((candidate) => candidate.code)])
28217
+ .filter((code) => !!code));
28102
28218
  const getNodeCandidates = (node) => {
28103
28219
  const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
28104
28220
  if (configured.length)
@@ -28207,6 +28323,51 @@ const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
28207
28323
  outward: { date: dateToDateStruct(new Date(line.from)) },
28208
28324
  return: null
28209
28325
  });
28326
+ /**
28327
+ * The entry as the backend can price it.
28328
+ *
28329
+ * A search caches an EntryLine per option and hands back that line's guid on the option. The
28330
+ * backend then rebuilds the entry by looking each line up in that cache on its guid, so a line
28331
+ * it cannot find is dropped — PackagingEntryBuilder.SyncEntryLines skips it, and an entry whose
28332
+ * lines all came off a traject prices as nothing at all.
28333
+ *
28334
+ * A traject entry is rendered before any search runs, so its lines carry the guids the traject
28335
+ * mapper gave them. This swaps in the guid of whatever each line resolved onto, leaving the
28336
+ * lines the traveller added themselves alone — those were built from a search result and already
28337
+ * carry the right one.
28338
+ */
28339
+ const withCachedEntryLineGuids = (entry, trajectEntry, nodeAvailability, flightAvailability) => {
28340
+ const cachedGuids = new Map();
28341
+ getTrajectNodesWithLines(trajectEntry).forEach((item) => {
28342
+ if (isTrajectFlightNode(item.node)) {
28343
+ // A flight search caches the outward line under the guid it reports as outwardGuid.
28344
+ const guid = flightAvailability[item.line.guid]?.selectedGuid;
28345
+ if (guid)
28346
+ cachedGuids.set(item.line.guid, guid);
28347
+ return;
28348
+ }
28349
+ const availability = nodeAvailability[item.line.guid];
28350
+ const result = availability?.results.find((x) => x.code === availability.selectedCode);
28351
+ if (!result)
28352
+ return;
28353
+ // Room i took option i when the result was stamped on, so it takes option i here too.
28354
+ item.lines.forEach((line, index) => {
28355
+ const guid = getOptionForRoom(result, index)?.guid;
28356
+ if (guid)
28357
+ cachedGuids.set(line.guid, guid);
28358
+ });
28359
+ });
28360
+ if (!cachedGuids.size)
28361
+ return entry;
28362
+ return {
28363
+ ...entry,
28364
+ lines: (entry.lines ?? []).map((line) => ({
28365
+ ...line,
28366
+ guid: cachedGuids.get(line.guid) ?? line.guid,
28367
+ parentGuid: (line.parentGuid && cachedGuids.get(line.parentGuid)) ?? line.parentGuid
28368
+ }))
28369
+ };
28370
+ };
28210
28371
  const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
28211
28372
  const applyFlightToLine = (line, flight) => {
28212
28373
  const segments = flight.outward?.segments ?? [];
@@ -31033,10 +31194,9 @@ const getOptionPaxIds = (option) => {
31033
31194
  const optionAppliesToPax = (option, paxId) => {
31034
31195
  return getOptionPaxIds(option).includes(paxId);
31035
31196
  };
31036
- const optionAppliesToAllTravellers = (option, travellerCount) => {
31197
+ const optionAppliesToAllTravellers = (option, travellerIds) => {
31037
31198
  const paxIds = getOptionPaxIds(option);
31038
- const expected = Array.from({ length: travellerCount }, (_, i) => i);
31039
- return paxIds.length === expected.length && paxIds.every((id, index) => id === expected[index]);
31199
+ return paxIds.length === travellerIds.length && travellerIds.every((id) => paxIds.includes(id));
31040
31200
  };
31041
31201
  const groupOptionsByExcursion = (options) => {
31042
31202
  const groupedMap = new Map();
@@ -31061,26 +31221,26 @@ const ExcursionDetails = () => {
31061
31221
  return null;
31062
31222
  }
31063
31223
  const translations = getTranslations(context.languageCode ?? 'en-GB');
31064
- const travellerCount = editablePackagingEntry.pax.length;
31224
+ const travellerIds = editablePackagingEntry.pax.map((pax) => pax.id);
31065
31225
  const allOptions = useMemo(() => {
31066
31226
  return selectedExcursionSearchResult.rooms.flatMap((room) => room.options ?? []);
31067
31227
  }, [selectedExcursionSearchResult]);
31068
31228
  const sharedOptions = useMemo(() => {
31069
- return allOptions.filter((option) => optionAppliesToAllTravellers(option, travellerCount));
31070
- }, [allOptions, travellerCount]);
31229
+ return allOptions.filter((option) => optionAppliesToAllTravellers(option, travellerIds));
31230
+ }, [allOptions, travellerIds]);
31071
31231
  const sharedExcursions = useMemo(() => {
31072
31232
  return groupOptionsByExcursion(sharedOptions);
31073
31233
  }, [sharedOptions]);
31074
31234
  const paxGroups = useMemo(() => {
31075
31235
  return editablePackagingEntry.pax.map((pax) => {
31076
- const paxOptions = allOptions.filter((option) => optionAppliesToPax(option, pax.id) && !optionAppliesToAllTravellers(option, travellerCount));
31236
+ const paxOptions = allOptions.filter((option) => optionAppliesToPax(option, pax.id) && !optionAppliesToAllTravellers(option, travellerIds));
31077
31237
  return {
31078
31238
  pax,
31079
31239
  paxId: pax.id,
31080
31240
  excursions: groupOptionsByExcursion(paxOptions)
31081
31241
  };
31082
31242
  });
31083
- }, [editablePackagingEntry.pax, allOptions, travellerCount]);
31243
+ }, [editablePackagingEntry.pax, allOptions, travellerIds]);
31084
31244
  const getSelectedSharedOption = () => {
31085
31245
  return sharedOptions.find((option) => option.isSelected);
31086
31246
  };
@@ -31088,11 +31248,11 @@ const ExcursionDetails = () => {
31088
31248
  return sharedOptions.find((option) => option.accommodationCode === accommodationCode && option.isSelected);
31089
31249
  };
31090
31250
  const getSelectedOptionForPax = (paxId) => {
31091
- return allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.isSelected);
31251
+ return allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerIds) && option.isSelected);
31092
31252
  };
31093
31253
  const getSelectedOptionForExcursion = (paxId, accommodationCode) => {
31094
31254
  return allOptions.find((option) => optionAppliesToPax(option, paxId) &&
31095
- !optionAppliesToAllTravellers(option, travellerCount) &&
31255
+ !optionAppliesToAllTravellers(option, travellerIds) &&
31096
31256
  option.accommodationCode === accommodationCode &&
31097
31257
  option.isSelected);
31098
31258
  };
@@ -31102,7 +31262,7 @@ const ExcursionDetails = () => {
31102
31262
  rooms: selectedExcursionSearchResult.rooms.map((room) => ({
31103
31263
  ...room,
31104
31264
  options: room.options.map((option) => {
31105
- const isSharedOption = optionAppliesToAllTravellers(option, travellerCount);
31265
+ const isSharedOption = optionAppliesToAllTravellers(option, travellerIds);
31106
31266
  if (paxId === undefined) {
31107
31267
  if (!isSharedOption) {
31108
31268
  return option;
@@ -31149,7 +31309,7 @@ const ExcursionDetails = () => {
31149
31309
  targetPrice = selectedOption.price;
31150
31310
  }
31151
31311
  else {
31152
- const firstOption = allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.accommodationCode === accommodationCode);
31312
+ const firstOption = allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerIds) && option.accommodationCode === accommodationCode);
31153
31313
  targetPrice = firstOption?.price || 0;
31154
31314
  }
31155
31315
  return targetPrice - (currentSelectedPrice || 0);
@@ -31171,6 +31331,13 @@ const ExcursionDetails = () => {
31171
31331
  return 'flyin__acco__price';
31172
31332
  };
31173
31333
  const handleConfirm = () => {
31334
+ if (context.trajectEntry) {
31335
+ const lines = buildTrajectExcursionLines(selectedExcursionSearchResult);
31336
+ if (lines.length)
31337
+ dispatch(addEditableEntryLines(lines));
31338
+ dispatch(setFlyInIsOpen(false));
31339
+ return;
31340
+ }
31174
31341
  const dayKey = getExcursionDayKey(excursionSearchParams.date);
31175
31342
  dispatch(confirmExcursionForDay({
31176
31343
  dayKey,
@@ -31412,6 +31579,12 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
31412
31579
  dispatch(removeEditableEntryLines(added.map((line) => line.guid)));
31413
31580
  return;
31414
31581
  }
31582
+ if (excursionNeedsChoice(result)) {
31583
+ dispatch(setExcursionSearchParams(buildExcursionSearchParams(result)));
31584
+ dispatch(setSelectedExcursionSearchResult(result));
31585
+ dispatch(setFlyInType('excursion-details'));
31586
+ return;
31587
+ }
31415
31588
  const lines = buildTrajectExcursionLines(result);
31416
31589
  if (lines.length)
31417
31590
  dispatch(addEditableEntryLines(lines));
@@ -31426,7 +31599,7 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
31426
31599
  dispatch(setFlyInIsOpen(false));
31427
31600
  };
31428
31601
  if (isLoading || suggestions?.status === 'loading') {
31429
- return React__default.createElement(React__default.Fragment, null, context.customSpinner ?? React__default.createElement(Spinner, { label: translations.SRP.LOADING_EXCURSIONS }));
31602
+ return React__default.createElement(React__default.Fragment, null, context.customSpinner ?? React__default.createElement(Spinner, { label: translations.SRP.LOADING_ACTIVITIES }));
31430
31603
  }
31431
31604
  return (React__default.createElement("div", { className: "flyin__content" },
31432
31605
  React__default.createElement("div", { className: "search__result-row" },
@@ -33374,6 +33547,27 @@ const DayByDayExcursions = () => {
33374
33547
  })));
33375
33548
  };
33376
33549
 
33550
+ /**
33551
+ * The entry to send to the backend, as opposed to the one the page works with.
33552
+ *
33553
+ * They differ only for a traject: the page keeps the line guids the traject was rendered with,
33554
+ * because that is what joins its nodes to its lines, while pricing and booking need the guids the
33555
+ * searches cached their lines under. Anything else passes through untouched, so this can be used
33556
+ * wherever the entry leaves the component without checking which flow is running.
33557
+ */
33558
+ const useBookableEntry = () => {
33559
+ const context = useContext(SearchResultsConfigurationContext);
33560
+ const { editablePackagingEntry, trajectNodeAvailability, trajectFlightAvailability } = useSelector((state) => state.searchResults);
33561
+ const trajectEntry = context?.trajectEntry;
33562
+ return useMemo(() => {
33563
+ if (!editablePackagingEntry)
33564
+ return null;
33565
+ if (!trajectEntry)
33566
+ return editablePackagingEntry;
33567
+ return withCachedEntryLineGuids(editablePackagingEntry, trajectEntry, trajectNodeAvailability, trajectFlightAvailability);
33568
+ }, [editablePackagingEntry, trajectEntry, trajectNodeAvailability, trajectFlightAvailability]);
33569
+ };
33570
+
33377
33571
  const renderEditablePackagingEntrySummaryOptions = (editablePackagingEntry, priceDetails, translations, currencyCode) => {
33378
33572
  const priceDetailsByProduct = groupBy(priceDetails?.details ?? [], (detail) => `${detail.productCode}|${detail.accommodationCode}`);
33379
33573
  return (editablePackagingEntry?.lines ?? []).map((line) => {
@@ -33758,6 +33952,8 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
33758
33952
  setSelectedPackagingAccoResult(selectedPackagingAccoResult);
33759
33953
  }
33760
33954
  }, [selectedPackagingAccoResultCode, packagingAccoResults]);
33955
+ // What the page holds versus what the backend can look up; they differ for a traject.
33956
+ const bookableEntry = useBookableEntry();
33761
33957
  if (!context || !editablePackagingEntry || !priceDetails || !config)
33762
33958
  return React__default.createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING });
33763
33959
  const handleSummarySubmit = async (e) => {
@@ -33766,20 +33962,15 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
33766
33962
  if (typeof window !== 'undefined') {
33767
33963
  window.scrollTo(0, 0);
33768
33964
  }
33769
- let updatedEditablePackagingEntry = {
33770
- ...editablePackagingEntry,
33771
- remarks
33772
- };
33965
+ // Held apart from the entry itself so they can be put on both copies of it.
33966
+ let bookingFields = { remarks };
33773
33967
  if (context.generatePaymentUrl && typeof window !== 'undefined') {
33774
33968
  const redirectUrl = new URL(window.location.href);
33775
33969
  redirectUrl.searchParams.set('bookingConfirmation', 'true');
33776
33970
  redirectUrl.searchParams.set('link', '');
33777
- updatedEditablePackagingEntry = {
33778
- ...updatedEditablePackagingEntry,
33779
- redirectUrl: redirectUrl.toString(),
33780
- returnPaymentUrl: true
33781
- };
33971
+ bookingFields = { ...bookingFields, redirectUrl: redirectUrl.toString(), returnPaymentUrl: true };
33782
33972
  }
33973
+ const updatedEditablePackagingEntry = { ...editablePackagingEntry, ...bookingFields };
33783
33974
  dispatch(setEditablePackagingEntry(updatedEditablePackagingEntry));
33784
33975
  try {
33785
33976
  const request = {
@@ -33787,7 +33978,8 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
33787
33978
  officeId: context.tideConnection.officeId,
33788
33979
  catalogueId: context.searchConfiguration.defaultCatalogueId ?? 0,
33789
33980
  agentId: context.agentId,
33790
- payload: updatedEditablePackagingEntry
33981
+ // The page keeps the entry it works with; the backend gets the one it can look up.
33982
+ payload: { ...(bookableEntry ?? editablePackagingEntry), ...bookingFields }
33791
33983
  };
33792
33984
  const bookingResponse = await build.bookPackagingEntry(config, request);
33793
33985
  dispatch(setBookingNumber(bookingResponse.number));
@@ -33819,6 +34011,41 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
33819
34011
  React__default.createElement(WLSidebar, { activeSearchSeed: activeSearchSeed, packagingAccoResult: selectedPackagingAccoResult }))));
33820
34012
  };
33821
34013
 
34014
+ const useTrajectDayActivities = () => {
34015
+ const context = useContext(SearchResultsConfigurationContext);
34016
+ const dispatch = useDispatch();
34017
+ const { trajectDaySuggestions, trajectNodeAvailability } = useSelector((state) => state.searchResults);
34018
+ const trajectEntry = context?.trajectEntry;
34019
+ const showApiItems = !!context?.showApiItems;
34020
+ const loadDay = useCallback(async (day) => {
34021
+ if (!context || !trajectEntry)
34022
+ return;
34023
+ // Already asked for, or already answered: leave it be.
34024
+ if (trajectDaySuggestions[day.order])
34025
+ return;
34026
+ const nodesWithLines = getTrajectNodesWithLines(trajectEntry);
34027
+ const destination = getDayDestination(nodesWithLines, trajectNodeAvailability, day.order);
34028
+ if (!destination)
34029
+ return;
34030
+ dispatch(setTrajectDaySuggestionsLoading(day.order));
34031
+ try {
34032
+ const results = (await build.searchPackagingExcursions(buildTrajectClientConfig(context), buildTrajectDayExcursionRequest(day.date, destination, buildTrajectSearchContext(context, trajectEntry)))) ?? [];
34033
+ // With external items switched off a day only offers what Tide holds itself, and whatever
34034
+ // the traject already books that day has a card of its own further up the block.
34035
+ const configured = getConfiguredExcursionCodes(nodesWithLines, day.order);
34036
+ dispatch(setTrajectDaySuggestions({
34037
+ dayOrder: day.order,
34038
+ results: results.filter((x) => showApiItems || !isExternalResult(x)).filter((x) => !configured.has(x.code))
34039
+ }));
34040
+ }
34041
+ catch (error) {
34042
+ console.error('Failed to load traject day activities', day.order, error);
34043
+ dispatch(setTrajectDaySuggestions({ dayOrder: day.order, results: [] }));
34044
+ }
34045
+ }, [context, trajectEntry, showApiItems, trajectDaySuggestions, trajectNodeAvailability, dispatch]);
34046
+ return loadDay;
34047
+ };
34048
+
33822
34049
  const getNodeIcon = (node) => {
33823
34050
  if (isTrajectFlightNode(node))
33824
34051
  return 'ui-flight';
@@ -33834,9 +34061,10 @@ const MAX_DAY_ACTIVITIES = 6;
33834
34061
  const TrajectResults = ({ isLoading }) => {
33835
34062
  const context = useContext(SearchResultsConfigurationContext);
33836
34063
  const dispatch = useDispatch();
34064
+ const loadDay = useTrajectDayActivities();
33837
34065
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
33838
34066
  const locale = getLocale(context?.languageCode ?? 'en-GB');
33839
- const { trajectNodeAvailability, trajectFlightAvailability, trajectDaySuggestions, editablePackagingEntry } = useSelector((state) => state.searchResults);
34067
+ const { trajectNodeAvailability, trajectFlightAvailability, trajectDaySuggestions, editablePackagingEntry, trajectResolving } = useSelector((state) => state.searchResults);
33840
34068
  const trajectEntry = context?.trajectEntry;
33841
34069
  const hideShowMoreResults = !!context?.hideShowMoreResults;
33842
34070
  const nodesWithLines = useMemo(() => (trajectEntry ? getTrajectNodesWithLines(trajectEntry) : []), [trajectEntry]);
@@ -33950,7 +34178,7 @@ const TrajectResults = ({ isLoading }) => {
33950
34178
  React__default.createElement(Icon, { name: getNodeIcon(node), height: 16 }),
33951
34179
  React__default.createElement("h3", null,
33952
34180
  React__default.createElement("strong", null, headerTitle || node.name))))),
33953
- status === 'loading' && (React__default.createElement(Spinner, { label: node.serviceType === EXCURSION_SERVICE_TYPE ? translations.SRP.LOADING_EXCURSIONS : translations.SRP.LOADING_ACCOMMODATIONS })),
34181
+ status === 'loading' && React__default.createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }),
33954
34182
  status === 'unavailable' && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
33955
34183
  status === 'resolved' && selected && (React__default.createElement(React__default.Fragment, null,
33956
34184
  availability.preferredUnavailable && node.preferredAccommodationName && (React__default.createElement("div", { className: "search__results__cards__actions" },
@@ -33986,8 +34214,6 @@ const TrajectResults = ({ isLoading }) => {
33986
34214
  React__default.createElement("div", { className: "search__results__label__text" },
33987
34215
  React__default.createElement(Icon, { name: "ui-flight", height: 16 }),
33988
34216
  React__default.createElement("h3", null,
33989
- translations.SRP.SELECT,
33990
- ' ',
33991
34217
  React__default.createElement("strong", null,
33992
34218
  node.departureAirportCode,
33993
34219
  " - ",
@@ -34013,10 +34239,22 @@ const TrajectResults = ({ isLoading }) => {
34013
34239
  dispatch(removeEditableEntryLines(added.map((line) => line.guid)));
34014
34240
  return;
34015
34241
  }
34242
+ if (excursionNeedsChoice(result)) {
34243
+ dispatch(setExcursionSearchParams(buildExcursionSearchParams(result)));
34244
+ dispatch(setSelectedExcursionSearchResult(result));
34245
+ dispatch(setFlyInType('excursion-details'));
34246
+ dispatch(setFlyInIsOpen(true));
34247
+ return;
34248
+ }
34016
34249
  const lines = buildTrajectExcursionLines(result);
34017
34250
  if (lines.length)
34018
34251
  dispatch(addEditableEntryLines(lines));
34019
34252
  };
34253
+ const handleShowItinerary = () => {
34254
+ dispatch(setTrajectItineraryView(true));
34255
+ if (typeof window !== 'undefined')
34256
+ window.scrollTo(0, 0);
34257
+ };
34020
34258
  const handleShowMoreActivities = (dayOrder) => {
34021
34259
  dispatch(setTrajectActivitiesDayOrder(dayOrder));
34022
34260
  dispatch(setFlyInType('traject-day-activities'));
@@ -34039,10 +34277,10 @@ const TrajectResults = ({ isLoading }) => {
34039
34277
  const items = getDayExcursionItems(day.order);
34040
34278
  const suggestions = trajectDaySuggestions[day.order];
34041
34279
  const cards = buildDayActivityCards(items, trajectNodeAvailability, suggestions?.results ?? []);
34042
- const isResolving = items.some((item) => (trajectNodeAvailability[item.line.guid]?.status ?? 'loading') === 'loading') || suggestions?.status === 'loading';
34043
- // A day with nothing configured and nothing found has nothing to say. A day whose own
34044
- // excursion came back empty does, so the gap is not passed off as a day without activities.
34045
- if (!cards.length && !items.length)
34280
+ const nodesResolving = items.some((item) => (trajectNodeAvailability[item.line.guid]?.status ?? 'loading') === 'loading');
34281
+ const isSearching = suggestions?.status === 'loading';
34282
+ const canSearch = !!getDayDestination(nodesWithLines, trajectNodeAvailability, day.order);
34283
+ if (!items.length && !canSearch)
34046
34284
  return null;
34047
34285
  const visible = cards.slice(0, MAX_DAY_ACTIVITIES);
34048
34286
  const hidden = cards.length - visible.length;
@@ -34055,16 +34293,16 @@ const TrajectResults = ({ isLoading }) => {
34055
34293
  React__default.createElement(Icon, { name: "ui-excursion", height: 16 }),
34056
34294
  React__default.createElement("h3", null,
34057
34295
  React__default.createElement("strong", null, translations.SRP.ACTIVITIES)))),
34058
- isResolving && !cards.length && React__default.createElement(Spinner, { label: translations.SRP.LOADING_EXCURSIONS }),
34059
- !isResolving && !cards.length && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
34060
- !!cards.length && (React__default.createElement(React__default.Fragment, null,
34061
- React__default.createElement("div", { className: "search__results__cards search__results__cards--scroll" }, visible.map(renderActivityCard)),
34062
- hidden > 0 && !hideShowMoreResults && (React__default.createElement("div", { className: "search__results__cards__actions" },
34063
- React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreActivities(day.order) },
34064
- translations.SRP.SHOW_MORE,
34065
- " (",
34066
- hidden,
34067
- ")")))))));
34296
+ (nodesResolving || isSearching) && !cards.length && React__default.createElement(Spinner, { label: translations.SRP.LOADING_ACTIVITIES }),
34297
+ !!cards.length && React__default.createElement("div", { className: "search__results__cards search__results__cards--scroll" }, visible.map(renderActivityCard)),
34298
+ !nodesResolving && !isSearching && !cards.length && (!!items.length || !!suggestions) && (React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)),
34299
+ React__default.createElement("div", { className: "search__results__cards__actions" },
34300
+ !suggestions && canSearch && !cards.length && !hideShowMoreResults && (React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => loadDay(day) }, translations.SRP.SHOW_ACTIVITIES)),
34301
+ hidden > 0 && !hideShowMoreResults && (React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreActivities(day.order) },
34302
+ translations.SRP.SHOW_MORE,
34303
+ " (",
34304
+ hidden,
34305
+ ")")))));
34068
34306
  };
34069
34307
  /**
34070
34308
  * An activity the traject books is swapped for another, the way a hotel is; one the traveller
@@ -34076,32 +34314,35 @@ const TrajectResults = ({ isLoading }) => {
34076
34314
  };
34077
34315
  if (isLoading)
34078
34316
  return React__default.createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
34079
- return (React__default.createElement(React__default.Fragment, null, trajectEntry.traject.days.map((day) => {
34080
- const stay = stayByDay.get(day.order);
34081
- const dayNodes = (nodesByDay.get(day.order) ?? []).filter((node) => isRenderableNode(node) && lineByNodeId.has(node.nodeId));
34082
- const stayLeadsDay = !!stay && (stay.night > 0 || dayNodes[0]?.nodeId === stay.item.node.nodeId);
34083
- // Where the day's activities belong in the order: with the first excursion it carries.
34084
- const firstExcursionNodeId = dayNodes.find((node) => node.serviceType === EXCURSION_SERVICE_TYPE)?.nodeId;
34085
- const showDayHeader = stayLeadsDay || !!day.name?.trim();
34086
- return (React__default.createElement(React__default.Fragment, { key: `day-${day.order}` },
34087
- showDayHeader && renderDayHeader(day, stayLeadsDay ? stay : undefined),
34088
- dayNodes.map((node) => {
34089
- const item = lineByNodeId.get(node.nodeId);
34090
- if (node.serviceType === ACCOMMODATION_SERVICE_TYPE) {
34091
- const stayStart = stayStartByNodeId.get(node.nodeId);
34092
- const titledByDayHeader = stayLeadsDay && stay.item.node.nodeId === node.nodeId;
34093
- return renderServiceNode(item, !titledByDayHeader, stayStart && getStayTitle(stayStart));
34094
- }
34095
- // Every excursion the day holds is drawn once, in one block, where the first of
34096
- // them falls in the order so the activities still sit between the stay and the
34097
- // flight that follow them.
34098
- if (node.serviceType === EXCURSION_SERVICE_TYPE) {
34099
- return node.nodeId === firstExcursionNodeId ? renderDayActivities(day) : null;
34100
- }
34101
- return renderFlightNode(item);
34102
- }),
34103
- firstExcursionNodeId === undefined && renderDayActivities(day)));
34104
- })));
34317
+ return (React__default.createElement(React__default.Fragment, null,
34318
+ trajectEntry.traject.days.map((day) => {
34319
+ const stay = stayByDay.get(day.order);
34320
+ const dayNodes = (nodesByDay.get(day.order) ?? []).filter((node) => isRenderableNode(node) && lineByNodeId.has(node.nodeId));
34321
+ const stayLeadsDay = !!stay && (stay.night > 0 || dayNodes[0]?.nodeId === stay.item.node.nodeId);
34322
+ // Where the day's activities belong in the order: with the first excursion it carries.
34323
+ const firstExcursionNodeId = dayNodes.find((node) => node.serviceType === EXCURSION_SERVICE_TYPE)?.nodeId;
34324
+ const showDayHeader = stayLeadsDay || !!day.name?.trim();
34325
+ return (React__default.createElement(React__default.Fragment, { key: `day-${day.order}` },
34326
+ showDayHeader && renderDayHeader(day, stayLeadsDay ? stay : undefined),
34327
+ dayNodes.map((node) => {
34328
+ const item = lineByNodeId.get(node.nodeId);
34329
+ if (node.serviceType === ACCOMMODATION_SERVICE_TYPE) {
34330
+ const stayStart = stayStartByNodeId.get(node.nodeId);
34331
+ const titledByDayHeader = stayLeadsDay && stay.item.node.nodeId === node.nodeId;
34332
+ return renderServiceNode(item, !titledByDayHeader, stayStart && getStayTitle(stayStart));
34333
+ }
34334
+ // Every excursion the day holds is drawn once, in one block, where the first of
34335
+ // them falls in the order — so the activities still sit between the stay and the
34336
+ // flight that follow them.
34337
+ if (node.serviceType === EXCURSION_SERVICE_TYPE) {
34338
+ return node.nodeId === firstExcursionNodeId ? renderDayActivities(day) : null;
34339
+ }
34340
+ return renderFlightNode(item);
34341
+ }),
34342
+ firstExcursionNodeId === undefined && renderDayActivities(day)));
34343
+ }),
34344
+ React__default.createElement("div", { className: "search__results__cards__actions" },
34345
+ React__default.createElement("button", { type: "button", className: "cta", disabled: trajectResolving, onClick: handleShowItinerary }, translations.STEPS.NEXT))));
34105
34346
  };
34106
34347
 
34107
34348
  const useTrajectAvailability = () => {
@@ -34115,6 +34356,7 @@ const useTrajectAvailability = () => {
34115
34356
  return;
34116
34357
  const controller = new AbortController();
34117
34358
  dispatch(resetTrajectAvailability());
34359
+ dispatch(setTrajectResolving(true));
34118
34360
  const config = {
34119
34361
  host: context.tideConnection.host,
34120
34362
  apiKey: context.tideConnection.apiKey
@@ -34141,42 +34383,51 @@ const useTrajectAvailability = () => {
34141
34383
  const lineGuid = item.line.guid;
34142
34384
  const search = item.node.serviceType === EXCURSION_SERVICE_TYPE ? build.searchPackagingExcursions : build.searchPackagingAccommodations;
34143
34385
  dispatch(setTrajectNodeLoading(lineGuid));
34386
+ const destination = getNodeSearchDestination(nodesWithLines, item);
34387
+ const searchFor = async (productCode) => {
34388
+ try {
34389
+ const results = (await search(config, buildTrajectNodeRequest(item, searchContext, productCode, destination), controller.signal)) ?? [];
34390
+ return results.filter((x) => showApiItems || !isExternalResult(x));
34391
+ }
34392
+ catch (error) {
34393
+ if (!controller.signal.aborted)
34394
+ console.error('Traject product unavailable', item.node.name, productCode, error);
34395
+ return [];
34396
+ }
34397
+ };
34144
34398
  try {
34145
34399
  // With external items switched off the node only looks at products held inside Tide.
34146
34400
  const candidates = getNodeCandidates(item.node).filter((x) => showApiItems || !isExternalCandidate(x));
34147
- const attempts = await Promise.all(candidates.map(async (candidate) => {
34148
- try {
34149
- const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, candidate.code), controller.signal);
34150
- return (attempt ?? []).find((result) => matchesCode(result, candidate.code)) ?? null;
34151
- }
34152
- catch (error) {
34153
- if (!controller.signal.aborted)
34154
- console.error('Traject hotel unavailable', candidate.code, error);
34155
- return null;
34156
- }
34401
+ const areaResults = hideShowMoreResults ? [] : await searchFor('');
34402
+ if (controller.signal.aborted)
34403
+ return [];
34404
+ const configuredResults = [];
34405
+ const found = await Promise.all(candidates.map(async (candidate) => {
34406
+ const known = areaResults.find((x) => matchesCode(x, candidate.code));
34407
+ if (known)
34408
+ return known;
34409
+ return (await searchFor(candidate.code)).find((x) => matchesCode(x, candidate.code)) ?? null;
34157
34410
  }));
34158
- const configuredResults = attempts
34159
- .filter((x) => !!x)
34160
- .filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
34411
+ if (controller.signal.aborted)
34412
+ return [];
34413
+ // Two configured codes can land on the same product — a hotel and one of its room types.
34414
+ found.forEach((result) => {
34415
+ if (!result || configuredResults.some((x) => x.code === result.code))
34416
+ return;
34417
+ configuredResults.push(result);
34418
+ });
34419
+ const configuredCodes = new Set(configuredResults.map((x) => x.code));
34420
+ const destinationResults = areaResults.filter((x) => !configuredCodes.has(x.code));
34161
34421
  const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
34162
- let preferredUnavailable = !!preferredCandidate && !configuredResults.some((x) => matchesCode(x, preferredCandidate.code));
34163
- // The destination search exists only to stock the show-more fly-in. With the button hidden
34164
- // a node is limited to its configured products; if none are available it simply reports so,
34165
- // rather than quietly substituting a hotel nobody configured.
34166
- const destinationResults = hideShowMoreResults
34167
- ? []
34168
- : ((await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [])
34169
- .filter((x) => showApiItems || !isExternalResult(x))
34170
- .filter((x) => !configuredResults.some((configured) => configured.code === x.code));
34422
+ const preferredUnavailable = configuredResults.length
34423
+ ? !!preferredCandidate && !configuredResults.some((x) => matchesCode(x, preferredCandidate.code))
34424
+ : candidates.length > 0;
34171
34425
  const picked = configuredResults[0] ?? pickCheapest(destinationResults);
34172
- const results = [...configuredResults, ...destinationResults];
34173
- if (!configuredResults.length)
34174
- preferredUnavailable = candidates.length > 0;
34175
34426
  if (controller.signal.aborted)
34176
34427
  return [];
34177
34428
  dispatch(setTrajectNodeAvailability({
34178
34429
  lineGuid,
34179
- results,
34430
+ results: [...configuredResults, ...destinationResults],
34180
34431
  selectedCode: picked?.code ?? null,
34181
34432
  preferredUnavailable
34182
34433
  }));
@@ -34220,11 +34471,11 @@ const useTrajectAvailability = () => {
34220
34471
  }
34221
34472
  };
34222
34473
  (async () => {
34223
- const searchable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node));
34224
- const flightNodes = nodesWithLines.filter((x) => isTrajectFlightNode(x.node));
34225
- const unresolved = (await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)])).flat();
34474
+ const resolvable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node));
34475
+ const unresolved = (await Promise.all(resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))))).flat();
34226
34476
  if (controller.signal.aborted)
34227
34477
  return;
34478
+ dispatch(setTrajectResolving(false));
34228
34479
  // A node with no availability anywhere cannot be priced or booked, so its line comes off
34229
34480
  // the entry. The card stays visible and reports the gap.
34230
34481
  const emptyLineGuids = unresolved.filter(Boolean);
@@ -34240,10 +34491,12 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
34240
34491
  const dispatch = useDispatch();
34241
34492
  const context = useContext(SearchResultsConfigurationContext);
34242
34493
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
34243
- const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry, trajectEditLineGuid } = useSelector((state) => state.searchResults);
34494
+ const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry, trajectEditLineGuid, trajectResolving, trajectItineraryView } = useSelector((state) => state.searchResults);
34244
34495
  const isMobile = useMediaQuery('(max-width: 1200px)');
34245
34496
  // Resolves live availability per traject node; no-op unless a traject entry is configured.
34246
34497
  useTrajectAvailability();
34498
+ // The entry as the backend can price it: a traject swaps in the guids its searches cached.
34499
+ const bookableEntry = useBookableEntry();
34247
34500
  const [initialFiltersSet, setInitialFiltersSet] = useState(false);
34248
34501
  const [initialFlightFiltersSet, setInitialFlightFiltersSet] = useState(false);
34249
34502
  const [filtersOpen, setFiltersOpen] = useState(false);
@@ -35010,6 +35263,10 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35010
35263
  const fetchPriceDetails = async () => {
35011
35264
  if (!context || !editablePackagingEntry || isEmpty$1(editablePackagingEntry.lines))
35012
35265
  return;
35266
+ // A traject resolves one node at a time. Pricing each step would show a total that climbs
35267
+ // as the trip fills in, so the whole trip is priced once, when the last node is in.
35268
+ if (context.trajectEntry && trajectResolving)
35269
+ return;
35013
35270
  setPricesAreLoading(true);
35014
35271
  try {
35015
35272
  const config = {
@@ -35021,7 +35278,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35021
35278
  officeId: context.tideConnection.officeId,
35022
35279
  catalogueId: context.searchConfiguration.defaultCatalogueId ?? first(context?.tideConnection?.catalogueIds) ?? 1,
35023
35280
  agentId: context.agentId,
35024
- payload: editablePackagingEntry
35281
+ payload: bookableEntry ?? editablePackagingEntry
35025
35282
  };
35026
35283
  const priceDetails = await build.getPriceDetails(config, request);
35027
35284
  dispatch(setPriceDetails(priceDetails));
@@ -35035,8 +35292,9 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35035
35292
  const fetchItinerary = async () => {
35036
35293
  if (!context || !context.packagingEntry || !editablePackagingEntry || isEmpty$1(editablePackagingEntry.lines))
35037
35294
  return;
35038
- // The traject view renders its own day-by-day column, so the generated itinerary is unused.
35039
- if (context.trajectEntry)
35295
+ // While a traject is being composed its own day-by-day column is the itinerary; the generated
35296
+ // one is what the traveller reads the finished trip back from, so it waits until then.
35297
+ if (context.trajectEntry && (trajectResolving || !trajectItineraryView))
35040
35298
  return;
35041
35299
  setItineraryIsLoading(true);
35042
35300
  try {
@@ -35049,7 +35307,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35049
35307
  officeId: context.tideConnection.officeId,
35050
35308
  catalogueId: context.searchConfiguration.defaultCatalogueId ?? first(context?.tideConnection?.catalogueIds) ?? 1,
35051
35309
  agentId: context.agentId,
35052
- payload: editablePackagingEntry
35310
+ payload: bookableEntry ?? editablePackagingEntry
35053
35311
  };
35054
35312
  const itinerary = await build.getItinerary(config, request);
35055
35313
  dispatch(setItinerary(itinerary));
@@ -35062,7 +35320,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35062
35320
  };
35063
35321
  fetchPriceDetails();
35064
35322
  fetchItinerary();
35065
- }, [editablePackagingEntry]);
35323
+ }, [editablePackagingEntry, bookableEntry, trajectResolving, trajectItineraryView]);
35066
35324
  // Flight selection
35067
35325
  // const [selectedOutwardKey, setSelectedOutwardKey] = useState<string | null>(null);
35068
35326
  // const [selectedReturnKey, setSelectedReturnKey] = useState<string | null>(null);
@@ -35411,6 +35669,11 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35411
35669
  address: {}
35412
35670
  };
35413
35671
  };
35672
+ const handleComposeTraject = () => {
35673
+ dispatch(setTrajectItineraryView(false));
35674
+ if (typeof window !== 'undefined')
35675
+ window.scrollTo(0, 0);
35676
+ };
35414
35677
  const handleShowMoreFlights = (flyInType) => {
35415
35678
  dispatch(setFlyInType(flyInType));
35416
35679
  dispatch(setFlyInIsOpen(true));
@@ -35426,7 +35689,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35426
35689
  context.searchConfiguration.qsmType != build.PortalQsmType.AccommodationAndFlight && context.showFilters && (React__default.createElement(Filters, { initialFilters: initialFilters, filters: filters, isOpen: filtersOpen, handleSetIsOpen: () => setFiltersOpen(!filtersOpen),
35427
35690
  // handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
35428
35691
  isLoading: isLoading, setFilters: (filters) => dispatch(setFilters(filters)), resetFilters: (filters) => dispatch(resetFilters(filters)) })),
35429
- context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && (React__default.createElement(Itinerary, { isOpen: itineraryOpen, handleSetIsOpen: () => setItineraryOpen(!itineraryOpen), isLoading: isLoading || pricesAreLoading || flightsLoading || itineraryIsLoading, onEditAccommodation: handleEditAccommodation })),
35692
+ context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && (React__default.createElement(Itinerary, { isOpen: itineraryOpen, handleSetIsOpen: () => setItineraryOpen(!itineraryOpen), isLoading: isLoading || pricesAreLoading || flightsLoading || itineraryIsLoading || trajectResolving, onEditAccommodation: handleEditAccommodation })),
35430
35693
  React__default.createElement("div", { className: "search__results" },
35431
35694
  isMobile && (React__default.createElement("div", { className: "search__result-row" },
35432
35695
  React__default.createElement("div", { className: "search__results__actions" },
@@ -35500,7 +35763,11 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35500
35763
  React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" },
35501
35764
  selectedReturnKey && selectedReturn && (React__default.createElement(IndependentFlightOption, { key: `flight-${selectedReturnKey}`, item: selectedReturn.return, guid: selectedReturn.outwardGuid, selectedGuid: selectedReturn.outwardGuid, isOutward: false, showSelectedState: true, price: selectedReturn.price })),
35502
35765
  uniqueReturnFlights.map((result) => (React__default.createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: result.return, onSelect: () => dispatch(setSelectedReturnKey(getFlightKey(result.return.segments))), guid: result.outwardGuid, isOutward: false, currentSelectedPrice: selectedReturn?.price, price: result.price })))))),
35503
- context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && context.trajectEntry && (React__default.createElement(TrajectResults, { isLoading: isLoading })),
35766
+ context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && context.trajectEntry && !trajectItineraryView && (React__default.createElement(TrajectResults, { isLoading: isLoading })),
35767
+ context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && context.trajectEntry && trajectItineraryView && (React__default.createElement(React__default.Fragment, null,
35768
+ React__default.createElement("div", { className: "search__results__cards__actions" },
35769
+ React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: handleComposeTraject }, translations.STEPS.PREVIOUS)),
35770
+ React__default.createElement(FullItinerary, { isLoading: itineraryIsLoading }))),
35504
35771
  context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && context.packagingEntry && !context.trajectEntry && (React__default.createElement(FullItinerary, { isLoading: itineraryIsLoading })))),
35505
35772
  React__default.createElement(FlyIn, { srpType: context.searchConfiguration.qsmType, isOpen: flyInIsOpen, setIsOpen: handleFlyInToggle, handleConfirm: () => handleConfirmHotelSwap(), onPanelRef: (el) => (panelRef.current = el), detailsLoading: detailsIsLoading, flyInType: flyInType, isPackageEditFlow: !!context.packagingEntry, sortByTypes: sortByTypes, activeSearchSeed: activeSearchSeed, toggleFilters: () => setFiltersOpen(!filtersOpen), filtersOpen: filtersOpen })))))))));
35506
35773
  };