@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.
@@ -458,6 +458,7 @@ var SRP$f = {
458
458
  TRAVEL_GROUP: "مجموعة المسافرين",
459
459
  EXCURSION: "رحلة",
460
460
  LOADING_EXCURSIONS: "جارٍ تحميل الرحلات...",
461
+ LOADING_ACTIVITIES: "جارٍ تحميل الأنشطة...",
461
462
  LOADING_FLIGHTS: "جارٍ تحميل الرحلات الجوية...",
462
463
  LOADING_ACCOMMODATIONS: "جارٍ تحميل الإقامات...",
463
464
  LOADING_ITINERARY: "جارٍ تحميل خط سير الرحلة...",
@@ -902,6 +903,7 @@ var SRP$e = {
902
903
  TRAVEL_GROUP: "Rejseselskab",
903
904
  EXCURSION: "Udflugt",
904
905
  LOADING_EXCURSIONS: "Indlæser udflugter...",
906
+ LOADING_ACTIVITIES: "Indlæser aktiviteter...",
905
907
  LOADING_FLIGHTS: "Indlæser fly...",
906
908
  LOADING_ACCOMMODATIONS: "Indlæser indkvarteringer...",
907
909
  LOADING_ITINERARY: "Indlæser rejseplan...",
@@ -1346,6 +1348,7 @@ var SRP$d = {
1346
1348
  TRAVEL_GROUP: "Reisegruppe",
1347
1349
  EXCURSION: "Ausflug",
1348
1350
  LOADING_EXCURSIONS: "Lade Ausflüge...",
1351
+ LOADING_ACTIVITIES: "Lade Aktivitäten...",
1349
1352
  LOADING_FLIGHTS: "Lade Flüge...",
1350
1353
  LOADING_ACCOMMODATIONS: "Lade Unterkünfte...",
1351
1354
  LOADING_ITINERARY: "Lade Reiseverlauf...",
@@ -1794,6 +1797,7 @@ var SRP$c = {
1794
1797
  TRAVEL_GROUP: "Travel group",
1795
1798
  EXCURSION: "Excursion",
1796
1799
  LOADING_EXCURSIONS: "Loading excursions...",
1800
+ LOADING_ACTIVITIES: "Loading activities...",
1797
1801
  LOADING_FLIGHTS: "Loading flights...",
1798
1802
  LOADING_ACCOMMODATIONS: "Loading accommodations...",
1799
1803
  LOADING_ITINERARY: "Loading itinerary...",
@@ -2238,6 +2242,7 @@ var SRP$b = {
2238
2242
  TRAVEL_GROUP: "Grupo de viaje",
2239
2243
  EXCURSION: "Excursión",
2240
2244
  LOADING_EXCURSIONS: "Cargando excursiones...",
2245
+ LOADING_ACTIVITIES: "Cargando actividades...",
2241
2246
  LOADING_FLIGHTS: "Cargando vuelos...",
2242
2247
  LOADING_ACCOMMODATIONS: "Cargando alojamientos...",
2243
2248
  LOADING_ITINERARY: "Cargando itinerario...",
@@ -2686,6 +2691,7 @@ var SRP$a = {
2686
2691
  TRAVEL_GROUP: "Groupe de voyageurs",
2687
2692
  EXCURSION: "Excursion",
2688
2693
  LOADING_EXCURSIONS: "Chargement des excursions...",
2694
+ LOADING_ACTIVITIES: "Chargement des activités...",
2689
2695
  LOADING_FLIGHTS: "Chargement des vols...",
2690
2696
  LOADING_ACCOMMODATIONS: "Chargement des hébergements...",
2691
2697
  LOADING_ITINERARY: "Chargement de l’itinéraire...",
@@ -3130,6 +3136,7 @@ var SRP$9 = {
3130
3136
  TRAVEL_GROUP: "Groupe de voyageurs",
3131
3137
  EXCURSION: "Excursion",
3132
3138
  LOADING_EXCURSIONS: "Chargement des excursions...",
3139
+ LOADING_ACTIVITIES: "Chargement des activités...",
3133
3140
  LOADING_FLIGHTS: "Chargement des vols...",
3134
3141
  LOADING_ACCOMMODATIONS: "Chargement des hébergements...",
3135
3142
  LOADING_ITINERARY: "Chargement de l’itinéraire...",
@@ -3574,6 +3581,7 @@ var SRP$8 = {
3574
3581
  TRAVEL_GROUP: "Ferðahópur",
3575
3582
  EXCURSION: "Útflutningur",
3576
3583
  LOADING_EXCURSIONS: "Hleðst inn í útflutninga...",
3584
+ LOADING_ACTIVITIES: "Hleð inn afþreyingu...",
3577
3585
  LOADING_FLIGHTS: "Hleðst inn í flug...",
3578
3586
  LOADING_ACCOMMODATIONS: "Hleðst inn í gistingar...",
3579
3587
  LOADING_ITINERARY: "Hleðst inn í ferðaráætlun...",
@@ -4018,6 +4026,7 @@ var SRP$7 = {
4018
4026
  TRAVEL_GROUP: "Gruppo di viaggio",
4019
4027
  EXCURSION: "Escursione",
4020
4028
  LOADING_EXCURSIONS: "Caricamento delle escursioni...",
4029
+ LOADING_ACTIVITIES: "Caricamento delle attività...",
4021
4030
  LOADING_FLIGHTS: "Caricamento dei voli...",
4022
4031
  LOADING_ACCOMMODATIONS: "Caricamento degli alloggi...",
4023
4032
  LOADING_ITINERARY: "Caricamento dell'itinerario...",
@@ -4466,6 +4475,7 @@ var SRP$6 = {
4466
4475
  TRAVEL_GROUP: "Reisgezelschap",
4467
4476
  EXCURSION: "Excursie",
4468
4477
  LOADING_EXCURSIONS: "Excursies laden...",
4478
+ LOADING_ACTIVITIES: "Activiteiten laden...",
4469
4479
  LOADING_FLIGHTS: "Vluchten laden...",
4470
4480
  LOADING_ACCOMMODATIONS: "Accommodaties laden...",
4471
4481
  LOADING_ITINERARY: "Reisplan laden...",
@@ -4910,6 +4920,7 @@ var SRP$5 = {
4910
4920
  TRAVEL_GROUP: "Reisgezelschap",
4911
4921
  EXCURSION: "Excursie",
4912
4922
  LOADING_EXCURSIONS: "Excursies laden...",
4923
+ LOADING_ACTIVITIES: "Activiteiten laden...",
4913
4924
  LOADING_FLIGHTS: "Vluchten laden...",
4914
4925
  LOADING_ACCOMMODATIONS: "Accommodaties laden...",
4915
4926
  LOADING_ITINERARY: "Reisplan laden...",
@@ -5354,6 +5365,7 @@ var SRP$4 = {
5354
5365
  TRAVEL_GROUP: "Reisefølge",
5355
5366
  EXCURSION: "Utflukt",
5356
5367
  LOADING_EXCURSIONS: "Laster utflukter...",
5368
+ LOADING_ACTIVITIES: "Laster aktiviteter...",
5357
5369
  LOADING_FLIGHTS: "Laster flyvninger...",
5358
5370
  LOADING_ACCOMMODATIONS: "Laster overnattinger...",
5359
5371
  LOADING_ITINERARY: "Laster reiserute...",
@@ -5798,6 +5810,7 @@ var SRP$3 = {
5798
5810
  TRAVEL_GROUP: "Grupa podróżnych",
5799
5811
  EXCURSION: "Wycieczka",
5800
5812
  LOADING_EXCURSIONS: "Ładowanie wycieczek...",
5813
+ LOADING_ACTIVITIES: "Ładowanie atrakcji...",
5801
5814
  LOADING_FLIGHTS: "Ładowanie lotów...",
5802
5815
  LOADING_ACCOMMODATIONS: "Ładowanie zakwaterowań...",
5803
5816
  LOADING_ITINERARY: "Ładowanie planu podróży...",
@@ -6242,6 +6255,7 @@ var SRP$2 = {
6242
6255
  TRAVEL_GROUP: "Grupo de viajantes",
6243
6256
  EXCURSION: "Excursão",
6244
6257
  LOADING_EXCURSIONS: "A carregar excursões...",
6258
+ LOADING_ACTIVITIES: "A carregar atividades...",
6245
6259
  LOADING_FLIGHTS: "A carregar voos...",
6246
6260
  LOADING_ACCOMMODATIONS: "A carregar alojamentos...",
6247
6261
  LOADING_ITINERARY: "A carregar itinerário...",
@@ -6686,6 +6700,7 @@ var SRP$1 = {
6686
6700
  TRAVEL_GROUP: "Resesällskap",
6687
6701
  EXCURSION: "Utflykt",
6688
6702
  LOADING_EXCURSIONS: "Laddar utflykter...",
6703
+ LOADING_ACTIVITIES: "Laddar aktiviteter...",
6689
6704
  LOADING_FLIGHTS: "Laddar flyg...",
6690
6705
  LOADING_ACCOMMODATIONS: "Laddar boenden...",
6691
6706
  LOADING_ITINERARY: "Laddar resplan...",
@@ -7130,6 +7145,7 @@ var SRP = {
7130
7145
  TRAVEL_GROUP: "旅行グループ",
7131
7146
  EXCURSION: "エクスカーション",
7132
7147
  LOADING_EXCURSIONS: "エクスカーションを読み込み中...",
7148
+ LOADING_ACTIVITIES: "アクティビティを読み込み中...",
7133
7149
  LOADING_FLIGHTS: "フライトを読み込み中...",
7134
7150
  LOADING_ACCOMMODATIONS: "宿泊施設を読み込み中...",
7135
7151
  LOADING_ITINERARY: "旅程を読み込み中...",
@@ -27471,7 +27487,9 @@ const initialState$1 = {
27471
27487
  trajectFlightAvailability: {},
27472
27488
  trajectDaySuggestions: {},
27473
27489
  trajectEditLineGuid: null,
27474
- trajectActivitiesDayOrder: null
27490
+ trajectActivitiesDayOrder: null,
27491
+ trajectResolving: false,
27492
+ trajectItineraryView: false
27475
27493
  };
27476
27494
  const searchResultsSlice = toolkit.createSlice({
27477
27495
  name: 'searchResults',
@@ -27741,6 +27759,12 @@ const searchResultsSlice = toolkit.createSlice({
27741
27759
  setTrajectActivitiesDayOrder(state, action) {
27742
27760
  state.trajectActivitiesDayOrder = action.payload;
27743
27761
  },
27762
+ setTrajectResolving(state, action) {
27763
+ state.trajectResolving = action.payload;
27764
+ },
27765
+ setTrajectItineraryView(state, action) {
27766
+ state.trajectItineraryView = action.payload;
27767
+ },
27744
27768
  updateTrajectNodeResult(state, action) {
27745
27769
  const { lineGuid, result } = action.payload;
27746
27770
  const node = state.trajectNodeAvailability[lineGuid];
@@ -27756,10 +27780,12 @@ const searchResultsSlice = toolkit.createSlice({
27756
27780
  state.trajectDaySuggestions = {};
27757
27781
  state.trajectEditLineGuid = null;
27758
27782
  state.trajectActivitiesDayOrder = null;
27783
+ state.trajectResolving = false;
27784
+ state.trajectItineraryView = false;
27759
27785
  }
27760
27786
  }
27761
27787
  });
27762
- 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;
27788
+ 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;
27763
27789
  var searchResultsReducer = searchResultsSlice.reducer;
27764
27790
 
27765
27791
  const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
@@ -28049,7 +28075,22 @@ const buildDestination = (line, node, result) => {
28049
28075
  null;
28050
28076
  return { id: 0, ...place, ...coordinates };
28051
28077
  };
28052
- const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
28078
+ const hasDestination = (destination) => !!destination && (!!destination.id || (!!destination.latitude && !!destination.longitude));
28079
+ const buildTrajectClientConfig = (context) => ({
28080
+ host: context.tideConnection.host,
28081
+ apiKey: context.tideConnection.apiKey
28082
+ });
28083
+ const buildTrajectSearchContext = (context, trajectEntry) => ({
28084
+ transactionId: trajectEntry.entry.transactionId,
28085
+ officeId: context.tideConnection.officeId ?? 1,
28086
+ catalogueId: context.searchConfiguration.defaultCatalogueId ?? context.tideConnection.catalogueIds?.[0] ?? 1,
28087
+ searchConfigurationId: context.searchConfiguration.id,
28088
+ portalId: context.portalId ?? null,
28089
+ agentId: context.agentId ?? null,
28090
+ language: context.languageCode ?? 'en-GB',
28091
+ rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
28092
+ });
28093
+ const buildTrajectNodeRequest = ({ node, line }, context, productCode, destination) => ({
28053
28094
  transactionId: context.transactionId,
28054
28095
  officeId: context.officeId,
28055
28096
  agentId: context.agentId ?? null,
@@ -28060,25 +28101,69 @@ const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
28060
28101
  language: context.language,
28061
28102
  serviceType: node.serviceType,
28062
28103
  fromDate: toDateOnlyString(line.from),
28063
- toDate: toDateOnlyString(line.to),
28064
- destination: buildDestination(line, node),
28104
+ // An excursion happens on a day rather than over a span. The traject gives its line an end a day
28105
+ // out, the way a night is counted, which asks the search for a window it does not have.
28106
+ toDate: toDateOnlyString(node.serviceType === EXCURSION_SERVICE_TYPE ? line.from : line.to),
28107
+ destination: destination ?? buildDestination(line, node),
28065
28108
  productCode,
28066
28109
  rooms: context.rooms,
28067
28110
  tagIds: []
28068
28111
  });
28069
- /**
28070
- * Entry lines for an excursion the traveller picked out of the suggestions.
28071
- *
28072
- * A traject node already has its lines and only needs stamping; a suggestion has none, so this
28073
- * builds them — one per selected option, as the regular day-by-day excursion flow does, since an
28074
- * excursion's options are ticket types (adult, child) rather than rooms. Where the search marks no
28075
- * default, the first option of each room stands in.
28076
- */
28077
- const buildTrajectExcursionLines = (result) => {
28078
- const options = (result.rooms ?? []).flatMap((room) => {
28079
- const selected = (room.options ?? []).filter((x) => x.isSelected);
28080
- return selected.length ? selected : (room.options ?? []).slice(0, 1);
28112
+ const coversDay = ({ node, line }, dayOrder) => {
28113
+ const nights = Math.max(dateFns.differenceInCalendarDays(new Date(line.to), new Date(line.from)), 1);
28114
+ return dayOrder >= node.dayOrder && dayOrder < node.dayOrder + nights;
28115
+ };
28116
+ const getDayDestinationItem = (items, dayOrder) => {
28117
+ const knowsWhereItIs = (x) => hasDestination(buildDestination(x.line, x.node));
28118
+ const stay = items.find((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE && coversDay(x, dayOrder) && knowsWhereItIs(x));
28119
+ if (stay)
28120
+ return stay;
28121
+ return ([...items]
28122
+ .filter((x) => x.node.dayOrder <= dayOrder && knowsWhereItIs(x))
28123
+ .sort((a, b) => a.node.dayOrder - b.node.dayOrder || a.node.order - b.node.order)
28124
+ .pop() ?? null);
28125
+ };
28126
+ const getDayDestination = (items, availability, dayOrder) => {
28127
+ const item = getDayDestinationItem(items, dayOrder);
28128
+ if (!item)
28129
+ return null;
28130
+ const resolved = availability[item.line.guid];
28131
+ const result = resolved?.results.find((x) => x.code === resolved.selectedCode) ?? null;
28132
+ const destination = buildDestination(item.line, item.node, result);
28133
+ return hasDestination(destination) ? destination : null;
28134
+ };
28135
+ const buildTrajectDayExcursionRequest = (date, destination, context) => ({
28136
+ transactionId: context.transactionId,
28137
+ officeId: context.officeId,
28138
+ agentId: context.agentId ?? null,
28139
+ catalogueId: context.catalogueId,
28140
+ searchConfigurationId: context.searchConfigurationId,
28141
+ portalId: context.portalId ?? null,
28142
+ vendorConfigurationId: null,
28143
+ language: context.language,
28144
+ serviceType: EXCURSION_SERVICE_TYPE,
28145
+ fromDate: toDateOnlyString(date),
28146
+ toDate: toDateOnlyString(date),
28147
+ destination,
28148
+ productCode: '',
28149
+ rooms: context.rooms,
28150
+ tagIds: []
28151
+ });
28152
+ const getExcursionOptionGroups = (result) => {
28153
+ const groups = new Map();
28154
+ (result.rooms ?? [])
28155
+ .flatMap((room) => room.options ?? [])
28156
+ .forEach((option) => {
28157
+ const key = [...(option.paxIds ?? [])].sort((a, b) => a - b).join(',');
28158
+ groups.set(key, [...(groups.get(key) ?? []), option]);
28081
28159
  });
28160
+ return [...groups.values()];
28161
+ };
28162
+ const excursionNeedsChoice = (result) => getExcursionOptionGroups(result).some((group) => group.length > 1 && !group.some((option) => option.isSelected));
28163
+ const buildTrajectExcursionLines = (result) => {
28164
+ const options = getExcursionOptionGroups(result)
28165
+ .map((group) => group.find((x) => x.isSelected) ?? (group.length === 1 ? group[0] : null))
28166
+ .filter((x) => !!x);
28082
28167
  if (!options.length)
28083
28168
  return [];
28084
28169
  const guids = options.map((option) => option.guid || crypto.randomUUID());
@@ -28107,6 +28192,21 @@ const buildTrajectExcursionLines = (result) => {
28107
28192
  flightInformation: null
28108
28193
  }));
28109
28194
  };
28195
+ const buildExcursionSearchParams = (result) => ({
28196
+ date: result.fromDate,
28197
+ fromDate: result.fromDate,
28198
+ toDate: result.toDate,
28199
+ countryId: result.countryId,
28200
+ regionId: result.regionId,
28201
+ oordId: result.oordId,
28202
+ locationId: result.locationId,
28203
+ locationName: result.locationName,
28204
+ accommodationCode: null,
28205
+ accommodationName: null,
28206
+ hotelCode: result.code,
28207
+ hotelName: result.name,
28208
+ rooms: result.rooms ?? []
28209
+ });
28110
28210
  const buildDayActivityCards = (items, availability, suggestions) => {
28111
28211
  const cards = [];
28112
28212
  const seen = new Set();
@@ -28123,11 +28223,27 @@ const buildDayActivityCards = (items, availability, suggestions) => {
28123
28223
  results.filter((x) => !!findCandidateForResult(item.node, x)).forEach((result) => push(result, item));
28124
28224
  });
28125
28225
  suggestions.forEach((result) => push(result));
28126
- // Whatever the nodes' own destination search found and the day's search did not — normally
28127
- // nothing, as they are the same search, but a node can resolve where a day has no place to look.
28128
- items.forEach((item) => (availability[item.line.guid]?.results ?? []).forEach((result) => push(result, item)));
28226
+ items.forEach((item) => (availability[item.line.guid]?.results ?? []).forEach((result) => push(result)));
28129
28227
  return cards;
28130
28228
  };
28229
+ /**
28230
+ * Where to search for a node.
28231
+ *
28232
+ * Normally the node says so itself, through the product it was configured with. An excursion slot
28233
+ * left empty on the traject says nothing at all — no code, no alternatives, and a line with no
28234
+ * place on it — so it falls back to wherever the party is staying that day, the same anchor the
28235
+ * day's own activity search uses.
28236
+ */
28237
+ const getNodeSearchDestination = (items, item) => {
28238
+ const own = buildDestination(item.line, item.node);
28239
+ if (hasDestination(own))
28240
+ return own;
28241
+ return getDayDestination(items, {}, item.node.dayOrder) ?? own;
28242
+ };
28243
+ const getConfiguredExcursionCodes = (items, dayOrder) => new Set(items
28244
+ .filter((x) => x.node.dayOrder === dayOrder && x.node.serviceType === EXCURSION_SERVICE_TYPE)
28245
+ .flatMap((x) => [x.node.code, ...getNodeCandidates(x.node).map((candidate) => candidate.code)])
28246
+ .filter((code) => !!code));
28131
28247
  const getNodeCandidates = (node) => {
28132
28248
  const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
28133
28249
  if (configured.length)
@@ -28236,6 +28352,51 @@ const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
28236
28352
  outward: { date: dateToDateStruct(new Date(line.from)) },
28237
28353
  return: null
28238
28354
  });
28355
+ /**
28356
+ * The entry as the backend can price it.
28357
+ *
28358
+ * A search caches an EntryLine per option and hands back that line's guid on the option. The
28359
+ * backend then rebuilds the entry by looking each line up in that cache on its guid, so a line
28360
+ * it cannot find is dropped — PackagingEntryBuilder.SyncEntryLines skips it, and an entry whose
28361
+ * lines all came off a traject prices as nothing at all.
28362
+ *
28363
+ * A traject entry is rendered before any search runs, so its lines carry the guids the traject
28364
+ * mapper gave them. This swaps in the guid of whatever each line resolved onto, leaving the
28365
+ * lines the traveller added themselves alone — those were built from a search result and already
28366
+ * carry the right one.
28367
+ */
28368
+ const withCachedEntryLineGuids = (entry, trajectEntry, nodeAvailability, flightAvailability) => {
28369
+ const cachedGuids = new Map();
28370
+ getTrajectNodesWithLines(trajectEntry).forEach((item) => {
28371
+ if (isTrajectFlightNode(item.node)) {
28372
+ // A flight search caches the outward line under the guid it reports as outwardGuid.
28373
+ const guid = flightAvailability[item.line.guid]?.selectedGuid;
28374
+ if (guid)
28375
+ cachedGuids.set(item.line.guid, guid);
28376
+ return;
28377
+ }
28378
+ const availability = nodeAvailability[item.line.guid];
28379
+ const result = availability?.results.find((x) => x.code === availability.selectedCode);
28380
+ if (!result)
28381
+ return;
28382
+ // Room i took option i when the result was stamped on, so it takes option i here too.
28383
+ item.lines.forEach((line, index) => {
28384
+ const guid = getOptionForRoom(result, index)?.guid;
28385
+ if (guid)
28386
+ cachedGuids.set(line.guid, guid);
28387
+ });
28388
+ });
28389
+ if (!cachedGuids.size)
28390
+ return entry;
28391
+ return {
28392
+ ...entry,
28393
+ lines: (entry.lines ?? []).map((line) => ({
28394
+ ...line,
28395
+ guid: cachedGuids.get(line.guid) ?? line.guid,
28396
+ parentGuid: (line.parentGuid && cachedGuids.get(line.parentGuid)) ?? line.parentGuid
28397
+ }))
28398
+ };
28399
+ };
28239
28400
  const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
28240
28401
  const applyFlightToLine = (line, flight) => {
28241
28402
  const segments = flight.outward?.segments ?? [];
@@ -31062,10 +31223,9 @@ const getOptionPaxIds = (option) => {
31062
31223
  const optionAppliesToPax = (option, paxId) => {
31063
31224
  return getOptionPaxIds(option).includes(paxId);
31064
31225
  };
31065
- const optionAppliesToAllTravellers = (option, travellerCount) => {
31226
+ const optionAppliesToAllTravellers = (option, travellerIds) => {
31066
31227
  const paxIds = getOptionPaxIds(option);
31067
- const expected = Array.from({ length: travellerCount }, (_, i) => i);
31068
- return paxIds.length === expected.length && paxIds.every((id, index) => id === expected[index]);
31228
+ return paxIds.length === travellerIds.length && travellerIds.every((id) => paxIds.includes(id));
31069
31229
  };
31070
31230
  const groupOptionsByExcursion = (options) => {
31071
31231
  const groupedMap = new Map();
@@ -31090,26 +31250,26 @@ const ExcursionDetails = () => {
31090
31250
  return null;
31091
31251
  }
31092
31252
  const translations = getTranslations(context.languageCode ?? 'en-GB');
31093
- const travellerCount = editablePackagingEntry.pax.length;
31253
+ const travellerIds = editablePackagingEntry.pax.map((pax) => pax.id);
31094
31254
  const allOptions = React.useMemo(() => {
31095
31255
  return selectedExcursionSearchResult.rooms.flatMap((room) => room.options ?? []);
31096
31256
  }, [selectedExcursionSearchResult]);
31097
31257
  const sharedOptions = React.useMemo(() => {
31098
- return allOptions.filter((option) => optionAppliesToAllTravellers(option, travellerCount));
31099
- }, [allOptions, travellerCount]);
31258
+ return allOptions.filter((option) => optionAppliesToAllTravellers(option, travellerIds));
31259
+ }, [allOptions, travellerIds]);
31100
31260
  const sharedExcursions = React.useMemo(() => {
31101
31261
  return groupOptionsByExcursion(sharedOptions);
31102
31262
  }, [sharedOptions]);
31103
31263
  const paxGroups = React.useMemo(() => {
31104
31264
  return editablePackagingEntry.pax.map((pax) => {
31105
- const paxOptions = allOptions.filter((option) => optionAppliesToPax(option, pax.id) && !optionAppliesToAllTravellers(option, travellerCount));
31265
+ const paxOptions = allOptions.filter((option) => optionAppliesToPax(option, pax.id) && !optionAppliesToAllTravellers(option, travellerIds));
31106
31266
  return {
31107
31267
  pax,
31108
31268
  paxId: pax.id,
31109
31269
  excursions: groupOptionsByExcursion(paxOptions)
31110
31270
  };
31111
31271
  });
31112
- }, [editablePackagingEntry.pax, allOptions, travellerCount]);
31272
+ }, [editablePackagingEntry.pax, allOptions, travellerIds]);
31113
31273
  const getSelectedSharedOption = () => {
31114
31274
  return sharedOptions.find((option) => option.isSelected);
31115
31275
  };
@@ -31117,11 +31277,11 @@ const ExcursionDetails = () => {
31117
31277
  return sharedOptions.find((option) => option.accommodationCode === accommodationCode && option.isSelected);
31118
31278
  };
31119
31279
  const getSelectedOptionForPax = (paxId) => {
31120
- return allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.isSelected);
31280
+ return allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerIds) && option.isSelected);
31121
31281
  };
31122
31282
  const getSelectedOptionForExcursion = (paxId, accommodationCode) => {
31123
31283
  return allOptions.find((option) => optionAppliesToPax(option, paxId) &&
31124
- !optionAppliesToAllTravellers(option, travellerCount) &&
31284
+ !optionAppliesToAllTravellers(option, travellerIds) &&
31125
31285
  option.accommodationCode === accommodationCode &&
31126
31286
  option.isSelected);
31127
31287
  };
@@ -31131,7 +31291,7 @@ const ExcursionDetails = () => {
31131
31291
  rooms: selectedExcursionSearchResult.rooms.map((room) => ({
31132
31292
  ...room,
31133
31293
  options: room.options.map((option) => {
31134
- const isSharedOption = optionAppliesToAllTravellers(option, travellerCount);
31294
+ const isSharedOption = optionAppliesToAllTravellers(option, travellerIds);
31135
31295
  if (paxId === undefined) {
31136
31296
  if (!isSharedOption) {
31137
31297
  return option;
@@ -31178,7 +31338,7 @@ const ExcursionDetails = () => {
31178
31338
  targetPrice = selectedOption.price;
31179
31339
  }
31180
31340
  else {
31181
- const firstOption = allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.accommodationCode === accommodationCode);
31341
+ const firstOption = allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerIds) && option.accommodationCode === accommodationCode);
31182
31342
  targetPrice = firstOption?.price || 0;
31183
31343
  }
31184
31344
  return targetPrice - (currentSelectedPrice || 0);
@@ -31200,6 +31360,13 @@ const ExcursionDetails = () => {
31200
31360
  return 'flyin__acco__price';
31201
31361
  };
31202
31362
  const handleConfirm = () => {
31363
+ if (context.trajectEntry) {
31364
+ const lines = buildTrajectExcursionLines(selectedExcursionSearchResult);
31365
+ if (lines.length)
31366
+ dispatch(addEditableEntryLines(lines));
31367
+ dispatch(setFlyInIsOpen(false));
31368
+ return;
31369
+ }
31203
31370
  const dayKey = getExcursionDayKey(excursionSearchParams.date);
31204
31371
  dispatch(confirmExcursionForDay({
31205
31372
  dayKey,
@@ -31441,6 +31608,12 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
31441
31608
  dispatch(removeEditableEntryLines(added.map((line) => line.guid)));
31442
31609
  return;
31443
31610
  }
31611
+ if (excursionNeedsChoice(result)) {
31612
+ dispatch(setExcursionSearchParams(buildExcursionSearchParams(result)));
31613
+ dispatch(setSelectedExcursionSearchResult(result));
31614
+ dispatch(setFlyInType('excursion-details'));
31615
+ return;
31616
+ }
31444
31617
  const lines = buildTrajectExcursionLines(result);
31445
31618
  if (lines.length)
31446
31619
  dispatch(addEditableEntryLines(lines));
@@ -31455,7 +31628,7 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
31455
31628
  dispatch(setFlyInIsOpen(false));
31456
31629
  };
31457
31630
  if (isLoading || suggestions?.status === 'loading') {
31458
- return React__default["default"].createElement(React__default["default"].Fragment, null, context.customSpinner ?? React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_EXCURSIONS }));
31631
+ return React__default["default"].createElement(React__default["default"].Fragment, null, context.customSpinner ?? React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACTIVITIES }));
31459
31632
  }
31460
31633
  return (React__default["default"].createElement("div", { className: "flyin__content" },
31461
31634
  React__default["default"].createElement("div", { className: "search__result-row" },
@@ -33403,6 +33576,27 @@ const DayByDayExcursions = () => {
33403
33576
  })));
33404
33577
  };
33405
33578
 
33579
+ /**
33580
+ * The entry to send to the backend, as opposed to the one the page works with.
33581
+ *
33582
+ * They differ only for a traject: the page keeps the line guids the traject was rendered with,
33583
+ * because that is what joins its nodes to its lines, while pricing and booking need the guids the
33584
+ * searches cached their lines under. Anything else passes through untouched, so this can be used
33585
+ * wherever the entry leaves the component without checking which flow is running.
33586
+ */
33587
+ const useBookableEntry = () => {
33588
+ const context = React.useContext(SearchResultsConfigurationContext);
33589
+ const { editablePackagingEntry, trajectNodeAvailability, trajectFlightAvailability } = reactRedux.useSelector((state) => state.searchResults);
33590
+ const trajectEntry = context?.trajectEntry;
33591
+ return React.useMemo(() => {
33592
+ if (!editablePackagingEntry)
33593
+ return null;
33594
+ if (!trajectEntry)
33595
+ return editablePackagingEntry;
33596
+ return withCachedEntryLineGuids(editablePackagingEntry, trajectEntry, trajectNodeAvailability, trajectFlightAvailability);
33597
+ }, [editablePackagingEntry, trajectEntry, trajectNodeAvailability, trajectFlightAvailability]);
33598
+ };
33599
+
33406
33600
  const renderEditablePackagingEntrySummaryOptions = (editablePackagingEntry, priceDetails, translations, currencyCode) => {
33407
33601
  const priceDetailsByProduct = lodash.groupBy(priceDetails?.details ?? [], (detail) => `${detail.productCode}|${detail.accommodationCode}`);
33408
33602
  return (editablePackagingEntry?.lines ?? []).map((line) => {
@@ -33787,6 +33981,8 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
33787
33981
  setSelectedPackagingAccoResult(selectedPackagingAccoResult);
33788
33982
  }
33789
33983
  }, [selectedPackagingAccoResultCode, packagingAccoResults]);
33984
+ // What the page holds versus what the backend can look up; they differ for a traject.
33985
+ const bookableEntry = useBookableEntry();
33790
33986
  if (!context || !editablePackagingEntry || !priceDetails || !config)
33791
33987
  return React__default["default"].createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING });
33792
33988
  const handleSummarySubmit = async (e) => {
@@ -33795,20 +33991,15 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
33795
33991
  if (typeof window !== 'undefined') {
33796
33992
  window.scrollTo(0, 0);
33797
33993
  }
33798
- let updatedEditablePackagingEntry = {
33799
- ...editablePackagingEntry,
33800
- remarks
33801
- };
33994
+ // Held apart from the entry itself so they can be put on both copies of it.
33995
+ let bookingFields = { remarks };
33802
33996
  if (context.generatePaymentUrl && typeof window !== 'undefined') {
33803
33997
  const redirectUrl = new URL(window.location.href);
33804
33998
  redirectUrl.searchParams.set('bookingConfirmation', 'true');
33805
33999
  redirectUrl.searchParams.set('link', '');
33806
- updatedEditablePackagingEntry = {
33807
- ...updatedEditablePackagingEntry,
33808
- redirectUrl: redirectUrl.toString(),
33809
- returnPaymentUrl: true
33810
- };
34000
+ bookingFields = { ...bookingFields, redirectUrl: redirectUrl.toString(), returnPaymentUrl: true };
33811
34001
  }
34002
+ const updatedEditablePackagingEntry = { ...editablePackagingEntry, ...bookingFields };
33812
34003
  dispatch(setEditablePackagingEntry(updatedEditablePackagingEntry));
33813
34004
  try {
33814
34005
  const request = {
@@ -33816,7 +34007,8 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
33816
34007
  officeId: context.tideConnection.officeId,
33817
34008
  catalogueId: context.searchConfiguration.defaultCatalogueId ?? 0,
33818
34009
  agentId: context.agentId,
33819
- payload: updatedEditablePackagingEntry
34010
+ // The page keeps the entry it works with; the backend gets the one it can look up.
34011
+ payload: { ...(bookableEntry ?? editablePackagingEntry), ...bookingFields }
33820
34012
  };
33821
34013
  const bookingResponse = await build.bookPackagingEntry(config, request);
33822
34014
  dispatch(setBookingNumber(bookingResponse.number));
@@ -33848,6 +34040,41 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
33848
34040
  React__default["default"].createElement(WLSidebar, { activeSearchSeed: activeSearchSeed, packagingAccoResult: selectedPackagingAccoResult }))));
33849
34041
  };
33850
34042
 
34043
+ const useTrajectDayActivities = () => {
34044
+ const context = React.useContext(SearchResultsConfigurationContext);
34045
+ const dispatch = reactRedux.useDispatch();
34046
+ const { trajectDaySuggestions, trajectNodeAvailability } = reactRedux.useSelector((state) => state.searchResults);
34047
+ const trajectEntry = context?.trajectEntry;
34048
+ const showApiItems = !!context?.showApiItems;
34049
+ const loadDay = React.useCallback(async (day) => {
34050
+ if (!context || !trajectEntry)
34051
+ return;
34052
+ // Already asked for, or already answered: leave it be.
34053
+ if (trajectDaySuggestions[day.order])
34054
+ return;
34055
+ const nodesWithLines = getTrajectNodesWithLines(trajectEntry);
34056
+ const destination = getDayDestination(nodesWithLines, trajectNodeAvailability, day.order);
34057
+ if (!destination)
34058
+ return;
34059
+ dispatch(setTrajectDaySuggestionsLoading(day.order));
34060
+ try {
34061
+ const results = (await build.searchPackagingExcursions(buildTrajectClientConfig(context), buildTrajectDayExcursionRequest(day.date, destination, buildTrajectSearchContext(context, trajectEntry)))) ?? [];
34062
+ // With external items switched off a day only offers what Tide holds itself, and whatever
34063
+ // the traject already books that day has a card of its own further up the block.
34064
+ const configured = getConfiguredExcursionCodes(nodesWithLines, day.order);
34065
+ dispatch(setTrajectDaySuggestions({
34066
+ dayOrder: day.order,
34067
+ results: results.filter((x) => showApiItems || !isExternalResult(x)).filter((x) => !configured.has(x.code))
34068
+ }));
34069
+ }
34070
+ catch (error) {
34071
+ console.error('Failed to load traject day activities', day.order, error);
34072
+ dispatch(setTrajectDaySuggestions({ dayOrder: day.order, results: [] }));
34073
+ }
34074
+ }, [context, trajectEntry, showApiItems, trajectDaySuggestions, trajectNodeAvailability, dispatch]);
34075
+ return loadDay;
34076
+ };
34077
+
33851
34078
  const getNodeIcon = (node) => {
33852
34079
  if (isTrajectFlightNode(node))
33853
34080
  return 'ui-flight';
@@ -33863,9 +34090,10 @@ const MAX_DAY_ACTIVITIES = 6;
33863
34090
  const TrajectResults = ({ isLoading }) => {
33864
34091
  const context = React.useContext(SearchResultsConfigurationContext);
33865
34092
  const dispatch = reactRedux.useDispatch();
34093
+ const loadDay = useTrajectDayActivities();
33866
34094
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
33867
34095
  const locale = getLocale(context?.languageCode ?? 'en-GB');
33868
- const { trajectNodeAvailability, trajectFlightAvailability, trajectDaySuggestions, editablePackagingEntry } = reactRedux.useSelector((state) => state.searchResults);
34096
+ const { trajectNodeAvailability, trajectFlightAvailability, trajectDaySuggestions, editablePackagingEntry, trajectResolving } = reactRedux.useSelector((state) => state.searchResults);
33869
34097
  const trajectEntry = context?.trajectEntry;
33870
34098
  const hideShowMoreResults = !!context?.hideShowMoreResults;
33871
34099
  const nodesWithLines = React.useMemo(() => (trajectEntry ? getTrajectNodesWithLines(trajectEntry) : []), [trajectEntry]);
@@ -33979,7 +34207,7 @@ const TrajectResults = ({ isLoading }) => {
33979
34207
  React__default["default"].createElement(Icon, { name: getNodeIcon(node), height: 16 }),
33980
34208
  React__default["default"].createElement("h3", null,
33981
34209
  React__default["default"].createElement("strong", null, headerTitle || node.name))))),
33982
- status === 'loading' && (React__default["default"].createElement(Spinner, { label: node.serviceType === EXCURSION_SERVICE_TYPE ? translations.SRP.LOADING_EXCURSIONS : translations.SRP.LOADING_ACCOMMODATIONS })),
34210
+ status === 'loading' && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }),
33983
34211
  status === 'unavailable' && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
33984
34212
  status === 'resolved' && selected && (React__default["default"].createElement(React__default["default"].Fragment, null,
33985
34213
  availability.preferredUnavailable && node.preferredAccommodationName && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
@@ -34015,8 +34243,6 @@ const TrajectResults = ({ isLoading }) => {
34015
34243
  React__default["default"].createElement("div", { className: "search__results__label__text" },
34016
34244
  React__default["default"].createElement(Icon, { name: "ui-flight", height: 16 }),
34017
34245
  React__default["default"].createElement("h3", null,
34018
- translations.SRP.SELECT,
34019
- ' ',
34020
34246
  React__default["default"].createElement("strong", null,
34021
34247
  node.departureAirportCode,
34022
34248
  " - ",
@@ -34042,10 +34268,22 @@ const TrajectResults = ({ isLoading }) => {
34042
34268
  dispatch(removeEditableEntryLines(added.map((line) => line.guid)));
34043
34269
  return;
34044
34270
  }
34271
+ if (excursionNeedsChoice(result)) {
34272
+ dispatch(setExcursionSearchParams(buildExcursionSearchParams(result)));
34273
+ dispatch(setSelectedExcursionSearchResult(result));
34274
+ dispatch(setFlyInType('excursion-details'));
34275
+ dispatch(setFlyInIsOpen(true));
34276
+ return;
34277
+ }
34045
34278
  const lines = buildTrajectExcursionLines(result);
34046
34279
  if (lines.length)
34047
34280
  dispatch(addEditableEntryLines(lines));
34048
34281
  };
34282
+ const handleShowItinerary = () => {
34283
+ dispatch(setTrajectItineraryView(true));
34284
+ if (typeof window !== 'undefined')
34285
+ window.scrollTo(0, 0);
34286
+ };
34049
34287
  const handleShowMoreActivities = (dayOrder) => {
34050
34288
  dispatch(setTrajectActivitiesDayOrder(dayOrder));
34051
34289
  dispatch(setFlyInType('traject-day-activities'));
@@ -34068,10 +34306,10 @@ const TrajectResults = ({ isLoading }) => {
34068
34306
  const items = getDayExcursionItems(day.order);
34069
34307
  const suggestions = trajectDaySuggestions[day.order];
34070
34308
  const cards = buildDayActivityCards(items, trajectNodeAvailability, suggestions?.results ?? []);
34071
- const isResolving = items.some((item) => (trajectNodeAvailability[item.line.guid]?.status ?? 'loading') === 'loading') || suggestions?.status === 'loading';
34072
- // A day with nothing configured and nothing found has nothing to say. A day whose own
34073
- // excursion came back empty does, so the gap is not passed off as a day without activities.
34074
- if (!cards.length && !items.length)
34309
+ const nodesResolving = items.some((item) => (trajectNodeAvailability[item.line.guid]?.status ?? 'loading') === 'loading');
34310
+ const isSearching = suggestions?.status === 'loading';
34311
+ const canSearch = !!getDayDestination(nodesWithLines, trajectNodeAvailability, day.order);
34312
+ if (!items.length && !canSearch)
34075
34313
  return null;
34076
34314
  const visible = cards.slice(0, MAX_DAY_ACTIVITIES);
34077
34315
  const hidden = cards.length - visible.length;
@@ -34084,16 +34322,16 @@ const TrajectResults = ({ isLoading }) => {
34084
34322
  React__default["default"].createElement(Icon, { name: "ui-excursion", height: 16 }),
34085
34323
  React__default["default"].createElement("h3", null,
34086
34324
  React__default["default"].createElement("strong", null, translations.SRP.ACTIVITIES)))),
34087
- isResolving && !cards.length && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_EXCURSIONS }),
34088
- !isResolving && !cards.length && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
34089
- !!cards.length && (React__default["default"].createElement(React__default["default"].Fragment, null,
34090
- React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--scroll" }, visible.map(renderActivityCard)),
34091
- hidden > 0 && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
34092
- React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreActivities(day.order) },
34093
- translations.SRP.SHOW_MORE,
34094
- " (",
34095
- hidden,
34096
- ")")))))));
34325
+ (nodesResolving || isSearching) && !cards.length && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACTIVITIES }),
34326
+ !!cards.length && React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--scroll" }, visible.map(renderActivityCard)),
34327
+ !nodesResolving && !isSearching && !cards.length && (!!items.length || !!suggestions) && (React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)),
34328
+ React__default["default"].createElement("div", { className: "search__results__cards__actions" },
34329
+ !suggestions && canSearch && !cards.length && !hideShowMoreResults && (React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => loadDay(day) }, translations.SRP.SHOW_ACTIVITIES)),
34330
+ hidden > 0 && !hideShowMoreResults && (React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreActivities(day.order) },
34331
+ translations.SRP.SHOW_MORE,
34332
+ " (",
34333
+ hidden,
34334
+ ")")))));
34097
34335
  };
34098
34336
  /**
34099
34337
  * An activity the traject books is swapped for another, the way a hotel is; one the traveller
@@ -34105,32 +34343,35 @@ const TrajectResults = ({ isLoading }) => {
34105
34343
  };
34106
34344
  if (isLoading)
34107
34345
  return React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
34108
- return (React__default["default"].createElement(React__default["default"].Fragment, null, trajectEntry.traject.days.map((day) => {
34109
- const stay = stayByDay.get(day.order);
34110
- const dayNodes = (nodesByDay.get(day.order) ?? []).filter((node) => isRenderableNode(node) && lineByNodeId.has(node.nodeId));
34111
- const stayLeadsDay = !!stay && (stay.night > 0 || dayNodes[0]?.nodeId === stay.item.node.nodeId);
34112
- // Where the day's activities belong in the order: with the first excursion it carries.
34113
- const firstExcursionNodeId = dayNodes.find((node) => node.serviceType === EXCURSION_SERVICE_TYPE)?.nodeId;
34114
- const showDayHeader = stayLeadsDay || !!day.name?.trim();
34115
- return (React__default["default"].createElement(React__default["default"].Fragment, { key: `day-${day.order}` },
34116
- showDayHeader && renderDayHeader(day, stayLeadsDay ? stay : undefined),
34117
- dayNodes.map((node) => {
34118
- const item = lineByNodeId.get(node.nodeId);
34119
- if (node.serviceType === ACCOMMODATION_SERVICE_TYPE) {
34120
- const stayStart = stayStartByNodeId.get(node.nodeId);
34121
- const titledByDayHeader = stayLeadsDay && stay.item.node.nodeId === node.nodeId;
34122
- return renderServiceNode(item, !titledByDayHeader, stayStart && getStayTitle(stayStart));
34123
- }
34124
- // Every excursion the day holds is drawn once, in one block, where the first of
34125
- // them falls in the order so the activities still sit between the stay and the
34126
- // flight that follow them.
34127
- if (node.serviceType === EXCURSION_SERVICE_TYPE) {
34128
- return node.nodeId === firstExcursionNodeId ? renderDayActivities(day) : null;
34129
- }
34130
- return renderFlightNode(item);
34131
- }),
34132
- firstExcursionNodeId === undefined && renderDayActivities(day)));
34133
- })));
34346
+ return (React__default["default"].createElement(React__default["default"].Fragment, null,
34347
+ trajectEntry.traject.days.map((day) => {
34348
+ const stay = stayByDay.get(day.order);
34349
+ const dayNodes = (nodesByDay.get(day.order) ?? []).filter((node) => isRenderableNode(node) && lineByNodeId.has(node.nodeId));
34350
+ const stayLeadsDay = !!stay && (stay.night > 0 || dayNodes[0]?.nodeId === stay.item.node.nodeId);
34351
+ // Where the day's activities belong in the order: with the first excursion it carries.
34352
+ const firstExcursionNodeId = dayNodes.find((node) => node.serviceType === EXCURSION_SERVICE_TYPE)?.nodeId;
34353
+ const showDayHeader = stayLeadsDay || !!day.name?.trim();
34354
+ return (React__default["default"].createElement(React__default["default"].Fragment, { key: `day-${day.order}` },
34355
+ showDayHeader && renderDayHeader(day, stayLeadsDay ? stay : undefined),
34356
+ dayNodes.map((node) => {
34357
+ const item = lineByNodeId.get(node.nodeId);
34358
+ if (node.serviceType === ACCOMMODATION_SERVICE_TYPE) {
34359
+ const stayStart = stayStartByNodeId.get(node.nodeId);
34360
+ const titledByDayHeader = stayLeadsDay && stay.item.node.nodeId === node.nodeId;
34361
+ return renderServiceNode(item, !titledByDayHeader, stayStart && getStayTitle(stayStart));
34362
+ }
34363
+ // Every excursion the day holds is drawn once, in one block, where the first of
34364
+ // them falls in the order — so the activities still sit between the stay and the
34365
+ // flight that follow them.
34366
+ if (node.serviceType === EXCURSION_SERVICE_TYPE) {
34367
+ return node.nodeId === firstExcursionNodeId ? renderDayActivities(day) : null;
34368
+ }
34369
+ return renderFlightNode(item);
34370
+ }),
34371
+ firstExcursionNodeId === undefined && renderDayActivities(day)));
34372
+ }),
34373
+ React__default["default"].createElement("div", { className: "search__results__cards__actions" },
34374
+ React__default["default"].createElement("button", { type: "button", className: "cta", disabled: trajectResolving, onClick: handleShowItinerary }, translations.STEPS.NEXT))));
34134
34375
  };
34135
34376
 
34136
34377
  const useTrajectAvailability = () => {
@@ -34144,6 +34385,7 @@ const useTrajectAvailability = () => {
34144
34385
  return;
34145
34386
  const controller = new AbortController();
34146
34387
  dispatch(resetTrajectAvailability());
34388
+ dispatch(setTrajectResolving(true));
34147
34389
  const config = {
34148
34390
  host: context.tideConnection.host,
34149
34391
  apiKey: context.tideConnection.apiKey
@@ -34170,42 +34412,51 @@ const useTrajectAvailability = () => {
34170
34412
  const lineGuid = item.line.guid;
34171
34413
  const search = item.node.serviceType === EXCURSION_SERVICE_TYPE ? build.searchPackagingExcursions : build.searchPackagingAccommodations;
34172
34414
  dispatch(setTrajectNodeLoading(lineGuid));
34415
+ const destination = getNodeSearchDestination(nodesWithLines, item);
34416
+ const searchFor = async (productCode) => {
34417
+ try {
34418
+ const results = (await search(config, buildTrajectNodeRequest(item, searchContext, productCode, destination), controller.signal)) ?? [];
34419
+ return results.filter((x) => showApiItems || !isExternalResult(x));
34420
+ }
34421
+ catch (error) {
34422
+ if (!controller.signal.aborted)
34423
+ console.error('Traject product unavailable', item.node.name, productCode, error);
34424
+ return [];
34425
+ }
34426
+ };
34173
34427
  try {
34174
34428
  // With external items switched off the node only looks at products held inside Tide.
34175
34429
  const candidates = getNodeCandidates(item.node).filter((x) => showApiItems || !isExternalCandidate(x));
34176
- const attempts = await Promise.all(candidates.map(async (candidate) => {
34177
- try {
34178
- const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, candidate.code), controller.signal);
34179
- return (attempt ?? []).find((result) => matchesCode(result, candidate.code)) ?? null;
34180
- }
34181
- catch (error) {
34182
- if (!controller.signal.aborted)
34183
- console.error('Traject hotel unavailable', candidate.code, error);
34184
- return null;
34185
- }
34430
+ const areaResults = hideShowMoreResults ? [] : await searchFor('');
34431
+ if (controller.signal.aborted)
34432
+ return [];
34433
+ const configuredResults = [];
34434
+ const found = await Promise.all(candidates.map(async (candidate) => {
34435
+ const known = areaResults.find((x) => matchesCode(x, candidate.code));
34436
+ if (known)
34437
+ return known;
34438
+ return (await searchFor(candidate.code)).find((x) => matchesCode(x, candidate.code)) ?? null;
34186
34439
  }));
34187
- const configuredResults = attempts
34188
- .filter((x) => !!x)
34189
- .filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
34440
+ if (controller.signal.aborted)
34441
+ return [];
34442
+ // Two configured codes can land on the same product — a hotel and one of its room types.
34443
+ found.forEach((result) => {
34444
+ if (!result || configuredResults.some((x) => x.code === result.code))
34445
+ return;
34446
+ configuredResults.push(result);
34447
+ });
34448
+ const configuredCodes = new Set(configuredResults.map((x) => x.code));
34449
+ const destinationResults = areaResults.filter((x) => !configuredCodes.has(x.code));
34190
34450
  const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
34191
- let preferredUnavailable = !!preferredCandidate && !configuredResults.some((x) => matchesCode(x, preferredCandidate.code));
34192
- // The destination search exists only to stock the show-more fly-in. With the button hidden
34193
- // a node is limited to its configured products; if none are available it simply reports so,
34194
- // rather than quietly substituting a hotel nobody configured.
34195
- const destinationResults = hideShowMoreResults
34196
- ? []
34197
- : ((await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [])
34198
- .filter((x) => showApiItems || !isExternalResult(x))
34199
- .filter((x) => !configuredResults.some((configured) => configured.code === x.code));
34451
+ const preferredUnavailable = configuredResults.length
34452
+ ? !!preferredCandidate && !configuredResults.some((x) => matchesCode(x, preferredCandidate.code))
34453
+ : candidates.length > 0;
34200
34454
  const picked = configuredResults[0] ?? pickCheapest(destinationResults);
34201
- const results = [...configuredResults, ...destinationResults];
34202
- if (!configuredResults.length)
34203
- preferredUnavailable = candidates.length > 0;
34204
34455
  if (controller.signal.aborted)
34205
34456
  return [];
34206
34457
  dispatch(setTrajectNodeAvailability({
34207
34458
  lineGuid,
34208
- results,
34459
+ results: [...configuredResults, ...destinationResults],
34209
34460
  selectedCode: picked?.code ?? null,
34210
34461
  preferredUnavailable
34211
34462
  }));
@@ -34249,11 +34500,11 @@ const useTrajectAvailability = () => {
34249
34500
  }
34250
34501
  };
34251
34502
  (async () => {
34252
- const searchable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node));
34253
- const flightNodes = nodesWithLines.filter((x) => isTrajectFlightNode(x.node));
34254
- const unresolved = (await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)])).flat();
34503
+ const resolvable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node));
34504
+ const unresolved = (await Promise.all(resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))))).flat();
34255
34505
  if (controller.signal.aborted)
34256
34506
  return;
34507
+ dispatch(setTrajectResolving(false));
34257
34508
  // A node with no availability anywhere cannot be priced or booked, so its line comes off
34258
34509
  // the entry. The card stays visible and reports the gap.
34259
34510
  const emptyLineGuids = unresolved.filter(Boolean);
@@ -34269,10 +34520,12 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
34269
34520
  const dispatch = reactRedux.useDispatch();
34270
34521
  const context = React.useContext(SearchResultsConfigurationContext);
34271
34522
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
34272
- const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry, trajectEditLineGuid } = reactRedux.useSelector((state) => state.searchResults);
34523
+ 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 } = reactRedux.useSelector((state) => state.searchResults);
34273
34524
  const isMobile = useMediaQuery('(max-width: 1200px)');
34274
34525
  // Resolves live availability per traject node; no-op unless a traject entry is configured.
34275
34526
  useTrajectAvailability();
34527
+ // The entry as the backend can price it: a traject swaps in the guids its searches cached.
34528
+ const bookableEntry = useBookableEntry();
34276
34529
  const [initialFiltersSet, setInitialFiltersSet] = React.useState(false);
34277
34530
  const [initialFlightFiltersSet, setInitialFlightFiltersSet] = React.useState(false);
34278
34531
  const [filtersOpen, setFiltersOpen] = React.useState(false);
@@ -35039,6 +35292,10 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35039
35292
  const fetchPriceDetails = async () => {
35040
35293
  if (!context || !editablePackagingEntry || lodash.isEmpty(editablePackagingEntry.lines))
35041
35294
  return;
35295
+ // A traject resolves one node at a time. Pricing each step would show a total that climbs
35296
+ // as the trip fills in, so the whole trip is priced once, when the last node is in.
35297
+ if (context.trajectEntry && trajectResolving)
35298
+ return;
35042
35299
  setPricesAreLoading(true);
35043
35300
  try {
35044
35301
  const config = {
@@ -35050,7 +35307,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35050
35307
  officeId: context.tideConnection.officeId,
35051
35308
  catalogueId: context.searchConfiguration.defaultCatalogueId ?? lodash.first(context?.tideConnection?.catalogueIds) ?? 1,
35052
35309
  agentId: context.agentId,
35053
- payload: editablePackagingEntry
35310
+ payload: bookableEntry ?? editablePackagingEntry
35054
35311
  };
35055
35312
  const priceDetails = await build.getPriceDetails(config, request);
35056
35313
  dispatch(setPriceDetails(priceDetails));
@@ -35064,8 +35321,9 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35064
35321
  const fetchItinerary = async () => {
35065
35322
  if (!context || !context.packagingEntry || !editablePackagingEntry || lodash.isEmpty(editablePackagingEntry.lines))
35066
35323
  return;
35067
- // The traject view renders its own day-by-day column, so the generated itinerary is unused.
35068
- if (context.trajectEntry)
35324
+ // While a traject is being composed its own day-by-day column is the itinerary; the generated
35325
+ // one is what the traveller reads the finished trip back from, so it waits until then.
35326
+ if (context.trajectEntry && (trajectResolving || !trajectItineraryView))
35069
35327
  return;
35070
35328
  setItineraryIsLoading(true);
35071
35329
  try {
@@ -35078,7 +35336,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35078
35336
  officeId: context.tideConnection.officeId,
35079
35337
  catalogueId: context.searchConfiguration.defaultCatalogueId ?? lodash.first(context?.tideConnection?.catalogueIds) ?? 1,
35080
35338
  agentId: context.agentId,
35081
- payload: editablePackagingEntry
35339
+ payload: bookableEntry ?? editablePackagingEntry
35082
35340
  };
35083
35341
  const itinerary = await build.getItinerary(config, request);
35084
35342
  dispatch(setItinerary(itinerary));
@@ -35091,7 +35349,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35091
35349
  };
35092
35350
  fetchPriceDetails();
35093
35351
  fetchItinerary();
35094
- }, [editablePackagingEntry]);
35352
+ }, [editablePackagingEntry, bookableEntry, trajectResolving, trajectItineraryView]);
35095
35353
  // Flight selection
35096
35354
  // const [selectedOutwardKey, setSelectedOutwardKey] = useState<string | null>(null);
35097
35355
  // const [selectedReturnKey, setSelectedReturnKey] = useState<string | null>(null);
@@ -35440,6 +35698,11 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35440
35698
  address: {}
35441
35699
  };
35442
35700
  };
35701
+ const handleComposeTraject = () => {
35702
+ dispatch(setTrajectItineraryView(false));
35703
+ if (typeof window !== 'undefined')
35704
+ window.scrollTo(0, 0);
35705
+ };
35443
35706
  const handleShowMoreFlights = (flyInType) => {
35444
35707
  dispatch(setFlyInType(flyInType));
35445
35708
  dispatch(setFlyInIsOpen(true));
@@ -35455,7 +35718,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35455
35718
  context.searchConfiguration.qsmType != build.PortalQsmType.AccommodationAndFlight && context.showFilters && (React__default["default"].createElement(Filters, { initialFilters: initialFilters, filters: filters, isOpen: filtersOpen, handleSetIsOpen: () => setFiltersOpen(!filtersOpen),
35456
35719
  // handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
35457
35720
  isLoading: isLoading, setFilters: (filters) => dispatch(setFilters(filters)), resetFilters: (filters) => dispatch(resetFilters(filters)) })),
35458
- context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && (React__default["default"].createElement(Itinerary, { isOpen: itineraryOpen, handleSetIsOpen: () => setItineraryOpen(!itineraryOpen), isLoading: isLoading || pricesAreLoading || flightsLoading || itineraryIsLoading, onEditAccommodation: handleEditAccommodation })),
35721
+ context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && (React__default["default"].createElement(Itinerary, { isOpen: itineraryOpen, handleSetIsOpen: () => setItineraryOpen(!itineraryOpen), isLoading: isLoading || pricesAreLoading || flightsLoading || itineraryIsLoading || trajectResolving, onEditAccommodation: handleEditAccommodation })),
35459
35722
  React__default["default"].createElement("div", { className: "search__results" },
35460
35723
  isMobile && (React__default["default"].createElement("div", { className: "search__result-row" },
35461
35724
  React__default["default"].createElement("div", { className: "search__results__actions" },
@@ -35529,7 +35792,11 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
35529
35792
  React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" },
35530
35793
  selectedReturnKey && selectedReturn && (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${selectedReturnKey}`, item: selectedReturn.return, guid: selectedReturn.outwardGuid, selectedGuid: selectedReturn.outwardGuid, isOutward: false, showSelectedState: true, price: selectedReturn.price })),
35531
35794
  uniqueReturnFlights.map((result) => (React__default["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 })))))),
35532
- context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && context.trajectEntry && (React__default["default"].createElement(TrajectResults, { isLoading: isLoading })),
35795
+ context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && context.trajectEntry && !trajectItineraryView && (React__default["default"].createElement(TrajectResults, { isLoading: isLoading })),
35796
+ context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && context.trajectEntry && trajectItineraryView && (React__default["default"].createElement(React__default["default"].Fragment, null,
35797
+ React__default["default"].createElement("div", { className: "search__results__cards__actions" },
35798
+ React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: handleComposeTraject }, translations.STEPS.PREVIOUS)),
35799
+ React__default["default"].createElement(FullItinerary, { isLoading: itineraryIsLoading }))),
35533
35800
  context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && context.packagingEntry && !context.trajectEntry && (React__default["default"].createElement(FullItinerary, { isLoading: itineraryIsLoading })))),
35534
35801
  React__default["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 })))))))));
35535
35802
  };