@qite/tide-components 1.0.14-preview.5 → 1.0.14-preview.6

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.
@@ -490,7 +490,10 @@ var SRP$f = {
490
490
  LOADING_ACCOMMODATIONS: "جارٍ تحميل الإقامات...",
491
491
  LOADING_ITINERARY: "جارٍ تحميل خط سير الرحلة...",
492
492
  LOADING_OPTIONS: "جارٍ تحميل الخيارات...",
493
- BOOK_AND_PAY: "احجز وادفع"
493
+ BOOK_AND_PAY: "احجز وادفع",
494
+ PRICE_DETAILS_FAILED: "تعذر حساب السعر.",
495
+ TRY_AGAIN: "حاول مرة أخرى",
496
+ BACK_TO_SERVICES: "العودة إلى الخدمات"
494
497
  };
495
498
  var ITINERARY$f = {
496
499
  DAY: "اليوم",
@@ -997,7 +1000,10 @@ var SRP$e = {
997
1000
  LOADING_ACCOMMODATIONS: "Indlæser indkvarteringer...",
998
1001
  LOADING_ITINERARY: "Indlæser rejseplan...",
999
1002
  LOADING_OPTIONS: "Indlæser muligheder...",
1000
- BOOK_AND_PAY: "Book og betal"
1003
+ BOOK_AND_PAY: "Book og betal",
1004
+ PRICE_DETAILS_FAILED: "Prisen kunne ikke beregnes.",
1005
+ TRY_AGAIN: "Prøv igen",
1006
+ BACK_TO_SERVICES: "Tilbage til ydelser"
1001
1007
  };
1002
1008
  var ITINERARY$e = {
1003
1009
  DAY: "Dag",
@@ -1504,7 +1510,10 @@ var SRP$d = {
1504
1510
  LOADING_ACCOMMODATIONS: "Lade Unterkünfte...",
1505
1511
  LOADING_ITINERARY: "Lade Reiseverlauf...",
1506
1512
  LOADING_OPTIONS: "Lade Optionen...",
1507
- BOOK_AND_PAY: "Buchen und bezahlen"
1513
+ BOOK_AND_PAY: "Buchen und bezahlen",
1514
+ PRICE_DETAILS_FAILED: "Der Preis konnte nicht berechnet werden.",
1515
+ TRY_AGAIN: "Erneut versuchen",
1516
+ BACK_TO_SERVICES: "Zurück zu den Leistungen"
1508
1517
  };
1509
1518
  var ITINERARY$d = {
1510
1519
  DAY: "Tag",
@@ -2015,7 +2024,10 @@ var SRP$c = {
2015
2024
  LOADING_ACCOMMODATIONS: "Loading accommodations...",
2016
2025
  LOADING_ITINERARY: "Loading itinerary...",
2017
2026
  LOADING_OPTIONS: "Loading options...",
2018
- BOOK_AND_PAY: "Book and pay"
2027
+ BOOK_AND_PAY: "Book and pay",
2028
+ PRICE_DETAILS_FAILED: "The price could not be calculated.",
2029
+ TRY_AGAIN: "Try again",
2030
+ BACK_TO_SERVICES: "Back to services"
2019
2031
  };
2020
2032
  var ITINERARY$c = {
2021
2033
  DAY: "Day",
@@ -2522,7 +2534,10 @@ var SRP$b = {
2522
2534
  LOADING_ACCOMMODATIONS: "Cargando alojamientos...",
2523
2535
  LOADING_ITINERARY: "Cargando itinerario...",
2524
2536
  LOADING_OPTIONS: "Cargando opciones...",
2525
- BOOK_AND_PAY: "Reservar y pagar"
2537
+ BOOK_AND_PAY: "Reservar y pagar",
2538
+ PRICE_DETAILS_FAILED: "No se ha podido calcular el precio.",
2539
+ TRY_AGAIN: "Volver a intentarlo",
2540
+ BACK_TO_SERVICES: "Volver a los servicios"
2526
2541
  };
2527
2542
  var ITINERARY$b = {
2528
2543
  DAY: "Día",
@@ -3033,7 +3048,10 @@ var SRP$a = {
3033
3048
  LOADING_ACCOMMODATIONS: "Chargement des hébergements...",
3034
3049
  LOADING_ITINERARY: "Chargement de l’itinéraire...",
3035
3050
  LOADING_OPTIONS: "Chargement des options...",
3036
- BOOK_AND_PAY: "Réserver et payer"
3051
+ BOOK_AND_PAY: "Réserver et payer",
3052
+ PRICE_DETAILS_FAILED: "Le prix n’a pas pu être calculé.",
3053
+ TRY_AGAIN: "Réessayer",
3054
+ BACK_TO_SERVICES: "Retour aux services"
3037
3055
  };
3038
3056
  var ITINERARY$a = {
3039
3057
  DAY: "Jour",
@@ -3540,7 +3558,10 @@ var SRP$9 = {
3540
3558
  LOADING_ACCOMMODATIONS: "Chargement des hébergements...",
3541
3559
  LOADING_ITINERARY: "Chargement de l’itinéraire...",
3542
3560
  LOADING_OPTIONS: "Chargement des options...",
3543
- BOOK_AND_PAY: "Réserver et payer"
3561
+ BOOK_AND_PAY: "Réserver et payer",
3562
+ PRICE_DETAILS_FAILED: "Le prix n’a pas pu être calculé.",
3563
+ TRY_AGAIN: "Réessayer",
3564
+ BACK_TO_SERVICES: "Retour aux services"
3544
3565
  };
3545
3566
  var ITINERARY$9 = {
3546
3567
  DAY: "Jour",
@@ -4047,7 +4068,10 @@ var SRP$8 = {
4047
4068
  LOADING_ACCOMMODATIONS: "Hleðst inn í gistingar...",
4048
4069
  LOADING_ITINERARY: "Hleðst inn í ferðaráætlun...",
4049
4070
  LOADING_OPTIONS: "Hleðst inn í valkosti...",
4050
- BOOK_AND_PAY: "Panta og greiða"
4071
+ BOOK_AND_PAY: "Panta og greiða",
4072
+ PRICE_DETAILS_FAILED: "Ekki tókst að reikna verðið.",
4073
+ TRY_AGAIN: "Reyna aftur",
4074
+ BACK_TO_SERVICES: "Aftur í þjónustu"
4051
4075
  };
4052
4076
  var ITINERARY$8 = {
4053
4077
  DAY: "Dagur",
@@ -4554,7 +4578,10 @@ var SRP$7 = {
4554
4578
  LOADING_ACCOMMODATIONS: "Caricamento degli alloggi...",
4555
4579
  LOADING_ITINERARY: "Caricamento dell'itinerario...",
4556
4580
  LOADING_OPTIONS: "Caricamento delle opzioni...",
4557
- BOOK_AND_PAY: "Prenota e paga"
4581
+ BOOK_AND_PAY: "Prenota e paga",
4582
+ PRICE_DETAILS_FAILED: "Non è stato possibile calcolare il prezzo.",
4583
+ TRY_AGAIN: "Riprova",
4584
+ BACK_TO_SERVICES: "Torna ai servizi"
4558
4585
  };
4559
4586
  var ITINERARY$7 = {
4560
4587
  DAY: "Giorno",
@@ -5065,7 +5092,10 @@ var SRP$6 = {
5065
5092
  LOADING_ACCOMMODATIONS: "Accommodaties laden...",
5066
5093
  LOADING_ITINERARY: "Reisplan laden...",
5067
5094
  LOADING_OPTIONS: "Opties laden...",
5068
- BOOK_AND_PAY: "Boek en betaal"
5095
+ BOOK_AND_PAY: "Boek en betaal",
5096
+ PRICE_DETAILS_FAILED: "De prijs kon niet berekend worden.",
5097
+ TRY_AGAIN: "Opnieuw proberen",
5098
+ BACK_TO_SERVICES: "Terug naar diensten"
5069
5099
  };
5070
5100
  var ITINERARY$6 = {
5071
5101
  DAY: "Dag",
@@ -5572,7 +5602,10 @@ var SRP$5 = {
5572
5602
  LOADING_ACCOMMODATIONS: "Accommodaties laden...",
5573
5603
  LOADING_ITINERARY: "Reisplan laden...",
5574
5604
  LOADING_OPTIONS: "Opties laden...",
5575
- BOOK_AND_PAY: "Boeken en betalen"
5605
+ BOOK_AND_PAY: "Boeken en betalen",
5606
+ PRICE_DETAILS_FAILED: "De prijs kon niet worden berekend.",
5607
+ TRY_AGAIN: "Opnieuw proberen",
5608
+ BACK_TO_SERVICES: "Terug naar diensten"
5576
5609
  };
5577
5610
  var ITINERARY$5 = {
5578
5611
  DAY: "Dag",
@@ -6079,7 +6112,10 @@ var SRP$4 = {
6079
6112
  LOADING_ACCOMMODATIONS: "Laster overnattinger...",
6080
6113
  LOADING_ITINERARY: "Laster reiserute...",
6081
6114
  LOADING_OPTIONS: "Laster alternativer...",
6082
- BOOK_AND_PAY: "Bestill og betal"
6115
+ BOOK_AND_PAY: "Bestill og betal",
6116
+ PRICE_DETAILS_FAILED: "Prisen kunne ikke beregnes.",
6117
+ TRY_AGAIN: "Prøv igjen",
6118
+ BACK_TO_SERVICES: "Tilbake til tjenester"
6083
6119
  };
6084
6120
  var ITINERARY$4 = {
6085
6121
  DAY: "Dag",
@@ -6586,7 +6622,10 @@ var SRP$3 = {
6586
6622
  LOADING_ACCOMMODATIONS: "Ładowanie zakwaterowań...",
6587
6623
  LOADING_ITINERARY: "Ładowanie planu podróży...",
6588
6624
  LOADING_OPTIONS: "Ładowanie opcji...",
6589
- BOOK_AND_PAY: "Zarezerwuj i zapłać"
6625
+ BOOK_AND_PAY: "Zarezerwuj i zapłać",
6626
+ PRICE_DETAILS_FAILED: "Nie udało się obliczyć ceny.",
6627
+ TRY_AGAIN: "Spróbuj ponownie",
6628
+ BACK_TO_SERVICES: "Wróć do usług"
6590
6629
  };
6591
6630
  var ITINERARY$3 = {
6592
6631
  DAY: "Dzień",
@@ -7093,7 +7132,10 @@ var SRP$2 = {
7093
7132
  LOADING_ACCOMMODATIONS: "A carregar alojamentos...",
7094
7133
  LOADING_ITINERARY: "A carregar itinerário...",
7095
7134
  LOADING_OPTIONS: "A carregar opções...",
7096
- BOOK_AND_PAY: "Reservar e pagar"
7135
+ BOOK_AND_PAY: "Reservar e pagar",
7136
+ PRICE_DETAILS_FAILED: "Não foi possível calcular o preço.",
7137
+ TRY_AGAIN: "Tentar novamente",
7138
+ BACK_TO_SERVICES: "Voltar aos serviços"
7097
7139
  };
7098
7140
  var ITINERARY$2 = {
7099
7141
  DAY: "Dia",
@@ -7600,7 +7642,10 @@ var SRP$1 = {
7600
7642
  LOADING_ACCOMMODATIONS: "Laddar boenden...",
7601
7643
  LOADING_ITINERARY: "Laddar resplan...",
7602
7644
  LOADING_OPTIONS: "Laddar alternativ...",
7603
- BOOK_AND_PAY: "Boka och betala"
7645
+ BOOK_AND_PAY: "Boka och betala",
7646
+ PRICE_DETAILS_FAILED: "Priset kunde inte beräknas.",
7647
+ TRY_AGAIN: "Försök igen",
7648
+ BACK_TO_SERVICES: "Tillbaka till tjänster"
7604
7649
  };
7605
7650
  var ITINERARY$1 = {
7606
7651
  DAY: "Dag",
@@ -8107,7 +8152,10 @@ var SRP = {
8107
8152
  LOADING_ACCOMMODATIONS: "宿泊施設を読み込み中...",
8108
8153
  LOADING_ITINERARY: "旅程を読み込み中...",
8109
8154
  LOADING_OPTIONS: "オプションを読み込み中...",
8110
- BOOK_AND_PAY: "予約して支払う"
8155
+ BOOK_AND_PAY: "予約して支払う",
8156
+ PRICE_DETAILS_FAILED: "料金を計算できませんでした。",
8157
+ TRY_AGAIN: "再試行",
8158
+ BACK_TO_SERVICES: "サービスに戻る"
8111
8159
  };
8112
8160
  var ITINERARY = {
8113
8161
  DAY: "日",
@@ -29235,6 +29283,8 @@ const initialState$1 = {
29235
29283
  selectedFlightDetails: null,
29236
29284
  bookingPackageDetails: null,
29237
29285
  priceDetails: null,
29286
+ priceDetailsFailed: false,
29287
+ priceDetailsRequest: 0,
29238
29288
  isLoading: false,
29239
29289
  flightsLoading: false,
29240
29290
  selectedSortType: null,
@@ -29395,6 +29445,12 @@ const searchResultsSlice = toolkit.createSlice({
29395
29445
  setPriceDetails(state, action) {
29396
29446
  state.priceDetails = action.payload;
29397
29447
  },
29448
+ setPriceDetailsFailed(state, action) {
29449
+ state.priceDetailsFailed = action.payload;
29450
+ },
29451
+ retryPriceDetails(state) {
29452
+ state.priceDetailsRequest += 1;
29453
+ },
29398
29454
  setItinerary(state, action) {
29399
29455
  state.itinerary = action.payload;
29400
29456
  },
@@ -29554,11 +29610,12 @@ const searchResultsSlice = toolkit.createSlice({
29554
29610
  flight.selectedGuid = null;
29555
29611
  },
29556
29612
  setTrajectTransferLoading(state, action) {
29557
- state.trajectTransferAvailability[action.payload] = { nodeId: action.payload, status: 'loading', result: null };
29613
+ const { nodeId, key } = action.payload;
29614
+ state.trajectTransferAvailability[nodeId] = { nodeId, key, status: 'loading', result: null };
29558
29615
  },
29559
29616
  setTrajectTransferAvailability(state, action) {
29560
- const { nodeId, result } = action.payload;
29561
- state.trajectTransferAvailability[nodeId] = { nodeId, status: result ? 'resolved' : 'unavailable', result };
29617
+ const { nodeId, key, result } = action.payload;
29618
+ state.trajectTransferAvailability[nodeId] = { nodeId, key, status: result ? 'resolved' : 'unavailable', result };
29562
29619
  },
29563
29620
  setTrajectDaySuggestionsLoading(state, action) {
29564
29621
  const dayOrder = action.payload;
@@ -29605,7 +29662,7 @@ const searchResultsSlice = toolkit.createSlice({
29605
29662
  }
29606
29663
  }
29607
29664
  });
29608
- 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, clearTrajectNodeSelection, setTrajectResolving, setTrajectItineraryView, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, clearTrajectFlightSelection, setTrajectTransferLoading, setTrajectTransferAvailability, setTrajectEditLineGuid, setTrajectActivitiesDayOrder, setTrajectDaySuggestionsLoading, setTrajectDaySuggestions, addEditableEntryLines, upsertEditableEntryLines, removeTrajectNode, updateTrajectNodeResult, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
29665
+ 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, setPriceDetailsFailed, retryPriceDetails, setItinerary, setSelectedOutwardKey, setSelectedReturnKey, resetFlightSelection, setExcursionSearchParams, setSelectedExcursionSearchResult, confirmExcursionForDay, removeConfirmedExcursionForDay, clearConfirmedExcursionsForDay, setBookPackagingEntry, setCurrentStep, setBookingNumber, setTrajectNodeLoading, setTrajectNodeAvailability, setTrajectNodeSelection, clearTrajectNodeSelection, setTrajectResolving, setTrajectItineraryView, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, clearTrajectFlightSelection, setTrajectTransferLoading, setTrajectTransferAvailability, setTrajectEditLineGuid, setTrajectActivitiesDayOrder, setTrajectDaySuggestionsLoading, setTrajectDaySuggestions, addEditableEntryLines, upsertEditableEntryLines, removeTrajectNode, updateTrajectNodeResult, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
29609
29666
  var searchResultsReducer = searchResultsSlice.reducer;
29610
29667
 
29611
29668
  const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
@@ -29770,211 +29827,9 @@ const getPackagingRequestRoomsFromBookingRooms = (rooms) => {
29770
29827
  });
29771
29828
  };
29772
29829
 
29773
- // Administrative service lines (fees, insurances, reductions, ...) live on the entry but say
29774
- // nothing about the journey itself, so the itinerary never draws them.
29775
- const HIDDEN_ITINERARY_SERVICE_TYPES = new Set([
29776
- build.ServiceType.insurance,
29777
- build.ServiceType.extraService,
29778
- build.ServiceType.serviceFee,
29779
- build.ServiceType.gift,
29780
- build.ServiceType.reduction,
29781
- build.ServiceType.fee,
29782
- build.ServiceType.visum,
29783
- build.ServiceType.voucher,
29784
- build.ServiceType.specialServices
29785
- ]);
29786
- // An inactive member of a line group is an alternative the traveller did not pick — not travelled.
29787
- const isMeaningfulItineraryLine = (line) => {
29788
- return isActiveEntryLine(line) && !HIDDEN_ITINERARY_SERVICE_TYPES.has(line.serviceType ?? -1);
29789
- };
29790
- const getFlightLines = (flight) => {
29791
- return flight?.flightInformation?.flightLines ?? [];
29792
- };
29793
- const getDepartureTime = (flight) => {
29794
- const firstLine = lodash.first(getFlightLines(flight));
29795
- if (!firstLine?.departureTime)
29796
- return '';
29797
- return firstLine.departureTime.slice(0, 5);
29798
- };
29799
- const getArrivalTime = (flight) => {
29800
- const lastLine = lodash.last(getFlightLines(flight));
29801
- if (!lastLine?.arrivalTime)
29802
- return '';
29803
- return lastLine.arrivalTime.slice(0, 5);
29804
- };
29805
- const getDuration = (flight) => {
29806
- const lines = getFlightLines(flight);
29807
- if (!lines.length)
29808
- return '';
29809
- const totalTicks = lines.reduce((sum, line) => sum + (line.durationInTicks ?? 0), 0) || 0;
29810
- if (!totalTicks)
29811
- return '';
29812
- const seconds = totalTicks / 10_000_000;
29813
- const hours = Math.floor(seconds / 3600);
29814
- const minutes = Math.floor((seconds % 3600) / 60);
29815
- return `${hours}h ${minutes.toString().padStart(2, '0')}m`;
29816
- };
29817
- const numberOfNights = (segment) => {
29818
- return dateFns.differenceInCalendarDays(new Date(segment.to), new Date(segment.from));
29819
- };
29820
- const CHARTER_SERVICE_TYPE = 10;
29821
- const CRUISE_SERVICE_TYPE = 11;
29822
- const FERRY_SERVICE_TYPE = 12;
29823
- const TRANSFER_SERVICE_TYPE$1 = 13;
29824
- const BUS_SERVICE_TYPE = 14;
29825
- const CAR_SERVICE_TYPE = 17;
29826
- const CAMPER_SERVICE_TYPE = 18;
29827
- const TRAIN_SERVICE_TYPE = 20;
29828
- const RENTAL_SERVICE_TYPE = 22;
29829
- const RESTAURANT_SERVICE_TYPE = 23;
29830
- const EVENT_SERVICE_TYPE = 24;
29831
- const TRAINING_SERVICE_TYPE = 25;
29832
- /** Icon per service type, so every node on the itinerary reads at a glance. */
29833
- const getServiceTypeIcon = (serviceType) => {
29834
- switch (serviceType) {
29835
- case ACCOMMODATION_SERVICE_TYPE:
29836
- return 'ui-hotel';
29837
- case EXCURSION_SERVICE_TYPE:
29838
- return 'ui-excursion';
29839
- case FLIGHT_SERVICE_TYPE:
29840
- case CHARTER_SERVICE_TYPE:
29841
- return 'ui-plane';
29842
- case RESTAURANT_SERVICE_TYPE:
29843
- return 'ui-utensils';
29844
- case CRUISE_SERVICE_TYPE:
29845
- case FERRY_SERVICE_TYPE:
29846
- return 'ui-ship';
29847
- case CAR_SERVICE_TYPE:
29848
- case CAMPER_SERVICE_TYPE:
29849
- case RENTAL_SERVICE_TYPE:
29850
- return 'ui-car';
29851
- case BUS_SERVICE_TYPE:
29852
- return 'ui-bus';
29853
- case TRAIN_SERVICE_TYPE:
29854
- return 'ui-train';
29855
- case TRANSFER_SERVICE_TYPE$1:
29856
- return 'ui-transfer';
29857
- case EVENT_SERVICE_TYPE:
29858
- return 'ui-ticket';
29859
- case TRAINING_SERVICE_TYPE:
29860
- return 'ui-group';
29861
- default:
29862
- return 'ui-location';
29863
- }
29864
- };
29865
- const getServiceTypeBadgeModifier = (serviceType) => {
29866
- switch (serviceType) {
29867
- case FLIGHT_SERVICE_TYPE:
29868
- case CHARTER_SERVICE_TYPE:
29869
- case CRUISE_SERVICE_TYPE:
29870
- case FERRY_SERVICE_TYPE:
29871
- return 'flight';
29872
- case ACCOMMODATION_SERVICE_TYPE:
29873
- return 'hotel';
29874
- case EXCURSION_SERVICE_TYPE:
29875
- case EVENT_SERVICE_TYPE:
29876
- return 'excursion';
29877
- case RESTAURANT_SERVICE_TYPE:
29878
- return 'restaurant';
29879
- default:
29880
- return '';
29881
- }
29882
- };
29883
- // DurationType, as the SDK numbers it.
29884
- const DURATION_HOURS = 0;
29885
- const DURATION_DAYS = 1;
29886
- const DURATION_NIGHTS = 2;
29887
- const DURATION_MINUTES = 3;
29888
- /**
29889
- * How long a node takes, in the same shape the flight legs are written in — an hour count only
29890
- * once there is one, so a short drive reads as plain minutes.
29891
- */
29892
- const getTrajectNodeDuration = (node) => {
29893
- const minutes = toMinutes(node);
29894
- if (!minutes)
29895
- return '';
29896
- const hours = Math.floor(minutes / 60);
29897
- const rest = minutes % 60;
29898
- return hours ? `${hours}h ${rest.toString().padStart(2, '0')}m` : `${rest} min`;
29899
- };
29900
- const toMinutes = (node) => {
29901
- const duration = node.duration ?? 0;
29902
- if (duration <= 0)
29903
- return 0;
29904
- switch (node.durationType) {
29905
- case DURATION_MINUTES:
29906
- return duration;
29907
- case DURATION_HOURS:
29908
- return duration * 60;
29909
- case DURATION_DAYS:
29910
- case DURATION_NIGHTS:
29911
- return duration * 24 * 60;
29912
- default:
29913
- return 0;
29914
- }
29915
- };
29916
- // RouteTransportType, as the SDK numbers it. It says how a leg is travelled, which is not the
29917
- // same question as what was sold: cycling and walking have no service type of their own.
29918
- const TRANSPORT_PRIVATE = 1;
29919
- const TRANSPORT_PUBLIC = 2;
29920
- const TRANSPORT_RENTAL_CAR = 3;
29921
- const TRANSPORT_BUS = 4;
29922
- const TRANSPORT_CAMPER = 5;
29923
- const TRANSPORT_CAR = 6;
29924
- const TRANSPORT_CHARTER = 7;
29925
- const TRANSPORT_CRUISE = 8;
29926
- const TRANSPORT_FERRY = 9;
29927
- const TRANSPORT_FLIGHT = 10;
29928
- const TRANSPORT_TAXI = 11;
29929
- const TRANSPORT_TRAIN = 12;
29930
- const TRANSPORT_TRANSFER = 13;
29931
- const TRANSPORT_WALKING = 15;
29932
- const TRANSPORT_CYCLING = 16;
29933
- /** Icon per way of travelling, where the route records one. */
29934
- const getTransportTypeIcon = (transportType) => {
29935
- switch (transportType) {
29936
- case TRANSPORT_WALKING:
29937
- return 'ui-walking';
29938
- case TRANSPORT_CYCLING:
29939
- return 'ui-cycling';
29940
- case TRANSPORT_PRIVATE:
29941
- case TRANSPORT_RENTAL_CAR:
29942
- case TRANSPORT_CAMPER:
29943
- case TRANSPORT_CAR:
29944
- return 'ui-car';
29945
- case TRANSPORT_TAXI:
29946
- case TRANSPORT_TRANSFER:
29947
- return 'ui-transfer';
29948
- case TRANSPORT_PUBLIC:
29949
- case TRANSPORT_BUS:
29950
- return 'ui-bus';
29951
- case TRANSPORT_TRAIN:
29952
- return 'ui-train';
29953
- case TRANSPORT_CRUISE:
29954
- case TRANSPORT_FERRY:
29955
- return 'ui-ship';
29956
- case TRANSPORT_FLIGHT:
29957
- case TRANSPORT_CHARTER:
29958
- return 'ui-plane';
29959
- default:
29960
- return null;
29961
- }
29962
- };
29963
- const POI_TRAJECT_NODE_TYPE = 2;
29964
- /**
29965
- * A point of interest is a place on the map rather than a service, so it gets a marker. Anything
29966
- * else goes by how it is travelled where the route says so — a leg on foot or by bike is sold as
29967
- * nothing, and its service type falls back to a transfer — and by what it is where it does not.
29968
- */
29969
- const getTrajectNodeIcon = (node) => {
29970
- if (node.trajectNodeType === POI_TRAJECT_NODE_TYPE)
29971
- return 'ui-location';
29972
- return getTransportTypeIcon(node.transportType) ?? getServiceTypeIcon(node.serviceType);
29973
- };
29974
- const getTrajectNodeBadgeModifier = (node) => node.trajectNodeType === POI_TRAJECT_NODE_TYPE ? 'poi' : getServiceTypeBadgeModifier(node.serviceType);
29975
-
29976
29830
  const REGULAR_TRAJECT_NODE_TYPE = 0;
29977
29831
  const CONNECTING_TRAJECT_NODE_TYPE = 1;
29832
+ const POI_TRAJECT_NODE_TYPE = 2;
29978
29833
  const isSearchableTrajectNode = (node) => node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE;
29979
29834
  const isTrajectFlightNode = (node) => node.serviceType === FLIGHT_SERVICE_TYPE;
29980
29835
  const getTrajectNodesWithLines = (trajectEntry) => {
@@ -29989,7 +29844,7 @@ const getTrajectNodesWithLines = (trajectEntry) => {
29989
29844
  .filter((x) => !!x.line);
29990
29845
  };
29991
29846
  // Qite.Offer.Enums.ServiceType.Transfer: a traject's connecting node is booked as one.
29992
- const TRANSFER_SERVICE_TYPE = 13;
29847
+ const TRANSFER_SERVICE_TYPE$1 = 13;
29993
29848
  // Qite.Offer.Enums.RequirementType. A Mandatory (0) stop, and one RequiredInGroup (3) — one of the node's
29994
29849
  // alternatives has to be booked — always keep a selection; only these two may be taken off the trip.
29995
29850
  const OPTIONAL_REQUIREMENT_TYPE = 1;
@@ -30017,7 +29872,53 @@ const getDroppedTrajectNodeIds = (trajectEntry, entry) => {
30017
29872
  return dropped;
30018
29873
  };
30019
29874
  const isTrajectTransferNode = (node) => node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE && !!node.pickUp && !!node.dropOff;
30020
- const buildTrajectTransferRequest = (node, context) => ({
29875
+ const getStopLine = (node, lines) => {
29876
+ const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
29877
+ const own = lines.find((line) => guids.includes(line.guid));
29878
+ const groupKey = own ? getLineGroupKey(own) : '';
29879
+ return (groupKey ? getGroupLines(lines, groupKey)[0]?.[0] : undefined) ?? own;
29880
+ };
29881
+ const getMovedStopCoordinate = (node, originalLines, lines) => {
29882
+ if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE || !isSearchableTrajectNode(node))
29883
+ return null;
29884
+ const current = getStopLine(node, lines);
29885
+ if (!current || current.latitude == null || current.longitude == null)
29886
+ return null;
29887
+ if (current.productCode === getStopLine(node, originalLines)?.productCode)
29888
+ return null;
29889
+ return { name: current.productName, latitude: current.latitude, longitude: current.longitude };
29890
+ };
29891
+ const getNightStay = (nodes, lines, night) => nodes.find((node) => {
29892
+ if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE || node.serviceType !== ACCOMMODATION_SERVICE_TYPE)
29893
+ return false;
29894
+ const line = getStopLine(node, lines);
29895
+ const nights = Math.max(dateFns.differenceInCalendarDays(new Date(line?.to ?? node.to), new Date(line?.from ?? node.from)), 1);
29896
+ return node.dayOrder <= night && night < node.dayOrder + nights;
29897
+ });
29898
+ const getTrajectTransferEndpoints = (trajectEntry, lines, connector) => {
29899
+ if (!isTrajectTransferNode(connector))
29900
+ return null;
29901
+ const routeNodes = trajectEntry.entry.routeNodes ?? [];
29902
+ const onRoute = routeNodes.find((node) => node.nodeId === connector.nodeId);
29903
+ const nodes = onRoute ? routeNodes : trajectEntry.nodes ?? [];
29904
+ const at = onRoute ?? connector;
29905
+ const originalLines = trajectEntry.entry.lines ?? [];
29906
+ const day = nodes.filter((node) => node.dayOrder === at.dayOrder).sort((a, b) => a.order - b.order);
29907
+ const index = day.indexOf(at);
29908
+ const isStop = (node) => node.trajectNodeType !== CONNECTING_TRAJECT_NODE_TYPE;
29909
+ const previous = day.slice(0, index).filter(isStop).pop() ?? getNightStay(nodes, lines, at.dayOrder - 1);
29910
+ const next = day.slice(index + 1).find(isStop) ?? getNightStay(nodes, lines, at.dayOrder);
29911
+ const pickUp = (previous && getMovedStopCoordinate(previous, originalLines, lines)) ?? connector.pickUp;
29912
+ const dropOff = (next && getMovedStopCoordinate(next, originalLines, lines)) ?? connector.dropOff;
29913
+ const place = (coordinate) => `${coordinate.latitude.toFixed(5)},${coordinate.longitude.toFixed(5)}`;
29914
+ return {
29915
+ pickUp,
29916
+ dropOff,
29917
+ key: `${place(pickUp)}|${place(dropOff)}|${toDateOnlyString(connector.from)}`,
29918
+ asDrawn: pickUp === connector.pickUp && dropOff === connector.dropOff
29919
+ };
29920
+ };
29921
+ const buildTrajectTransferRequest = (node, endpoints, context) => ({
30021
29922
  transactionId: context.transactionId,
30022
29923
  officeId: context.officeId,
30023
29924
  agentId: context.agentId ?? null,
@@ -30025,10 +29926,11 @@ const buildTrajectTransferRequest = (node, context) => ({
30025
29926
  searchConfigurationId: context.searchConfigurationId,
30026
29927
  language: context.language,
30027
29928
  date: node.from,
30028
- pickUp: node.pickUp,
30029
- dropOff: node.dropOff,
29929
+ pickUp: endpoints.pickUp,
29930
+ dropOff: endpoints.dropOff,
30030
29931
  travellers: context.rooms.flatMap((room) => room.travellers)
30031
29932
  });
29933
+ const getTrajectTransferLines = (node, lines) => lines.filter((line) => line.trajectNodeId === node.nodeId || (!!node.lineGuids?.length && node.lineGuids.includes(line.guid)));
30032
29934
  const buildTrajectTransferLine = (node, result) => {
30033
29935
  const option = result.rooms?.[0]?.options?.find((x) => x.isSelected) ?? result.rooms?.[0]?.options?.[0];
30034
29936
  if (!option?.guid)
@@ -30042,7 +29944,7 @@ const buildTrajectTransferLine = (node, result) => {
30042
29944
  from: result.fromDate ?? node.from,
30043
29945
  to: result.toDate ?? node.to,
30044
29946
  moment: node.moment ?? '',
30045
- serviceType: TRANSFER_SERVICE_TYPE,
29947
+ serviceType: TRANSFER_SERVICE_TYPE$1,
30046
29948
  productName: result.name,
30047
29949
  productCode: result.code,
30048
29950
  accommodationName: option.accommodationName,
@@ -30417,7 +30319,15 @@ const withCachedEntryLineGuids = (entry, trajectEntry, nodeAvailability, flightA
30417
30319
  const bookedMode = !!trajectEntry.entry.magicLink;
30418
30320
  const editableByGuid = new Map((entry.lines ?? []).map((line) => [line.guid, line]));
30419
30321
  const keepsOwnGuid = (guid) => bookedMode && !editableByGuid.get(guid)?.isChanged;
30322
+ // A swapped line tells which stop of the dossier's route it replaces, so saving links the route to it.
30323
+ const routeNodeIds = new Map();
30324
+ if (bookedMode)
30325
+ (trajectEntry.entry.routeNodes ?? []).forEach((node) => (node.lineGuids ?? []).forEach((guid) => routeNodeIds.set(guid, node.nodeId)));
30326
+ const trajectNodeIds = new Map();
30420
30327
  getTrajectNodesWithLines(trajectEntry).forEach((item) => {
30328
+ const routeNodeId = item.lines.map((line) => routeNodeIds.get(line.guid)).find((id) => id !== undefined);
30329
+ if (routeNodeId !== undefined)
30330
+ item.lines.filter((line) => !keepsOwnGuid(line.guid)).forEach((line) => trajectNodeIds.set(line.guid, routeNodeId));
30421
30331
  if (isTrajectFlightNode(item.node)) {
30422
30332
  if (keepsOwnGuid(item.line.guid))
30423
30333
  return;
@@ -30440,14 +30350,15 @@ const withCachedEntryLineGuids = (entry, trajectEntry, nodeAvailability, flightA
30440
30350
  cachedGuids.set(line.guid, guid);
30441
30351
  });
30442
30352
  });
30443
- if (!cachedGuids.size)
30353
+ if (!cachedGuids.size && !trajectNodeIds.size)
30444
30354
  return entry;
30445
30355
  return {
30446
30356
  ...entry,
30447
30357
  lines: (entry.lines ?? []).map((line) => ({
30448
30358
  ...line,
30449
30359
  guid: cachedGuids.get(line.guid) ?? line.guid,
30450
- parentGuid: (line.parentGuid && cachedGuids.get(line.parentGuid)) ?? line.parentGuid
30360
+ parentGuid: (line.parentGuid && cachedGuids.get(line.parentGuid)) ?? line.parentGuid,
30361
+ trajectNodeId: trajectNodeIds.get(line.guid) ?? line.trajectNodeId
30451
30362
  }))
30452
30363
  };
30453
30364
  };
@@ -30481,214 +30392,214 @@ const applyFlightToLine = (line, flight) => {
30481
30392
  const isExternalResult = (result) => result.vendorId != null || result.vendorConfigurationId != null;
30482
30393
  const isExternalCandidate = (candidate) => candidate.externalVendorId != null || candidate.vendor != null;
30483
30394
 
30484
- const badgeClass = (kind, modifier) => {
30485
- const base = `search__filter__itinerary__${kind}-badge`;
30486
- const fallback = kind === 'segment' ? `${base}--secondary` : `${base}--default`;
30487
- return `${base} ${modifier ? `${base}--${modifier}` : fallback}`;
30395
+ // Administrative service lines (fees, insurances, reductions, ...) live on the entry but say
30396
+ // nothing about the journey itself, so the itinerary never draws them.
30397
+ const HIDDEN_ITINERARY_SERVICE_TYPES = new Set([
30398
+ build.ServiceType.insurance,
30399
+ build.ServiceType.extraService,
30400
+ build.ServiceType.serviceFee,
30401
+ build.ServiceType.gift,
30402
+ build.ServiceType.reduction,
30403
+ build.ServiceType.fee,
30404
+ build.ServiceType.visum,
30405
+ build.ServiceType.voucher,
30406
+ build.ServiceType.specialServices
30407
+ ]);
30408
+ // An inactive member of a line group is an alternative the traveller did not pick — not travelled.
30409
+ const isMeaningfulItineraryLine = (line) => {
30410
+ return isActiveEntryLine(line) && !HIDDEN_ITINERARY_SERVICE_TYPES.has(line.serviceType ?? -1);
30488
30411
  };
30489
- const nightsOf = (node, line) => dateFns.differenceInCalendarDays(new Date(line?.to ?? node.to), new Date(line?.from ?? node.from));
30490
- const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
30491
- const timeline = React.useMemo(() => {
30492
- const linesByGuid = new Map((packagingEntry.lines ?? []).map((line) => [line.guid, line]));
30493
- const dropped = getDroppedTrajectNodeIds(trajectEntry, packagingEntry);
30494
- const nodes = [...(trajectEntry.nodes ?? [])]
30495
- .filter((node) => !dropped.has(node.nodeId))
30496
- .sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
30497
- .map((node) => {
30498
- // A connecting node has no line of its own; the transfer found for it carries its node id.
30499
- if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
30500
- return { node, lines: (packagingEntry.lines ?? []).filter((line) => line.trajectNodeId === node.nodeId) };
30501
- }
30502
- const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
30503
- const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
30504
- const groupKey = lines.length ? getLineGroupKey(lines[0]) : '';
30505
- const active = groupKey ? getGroupLines(packagingEntry.lines ?? [], groupKey)[0] : undefined;
30506
- return { node, lines: active ?? lines };
30507
- });
30508
- const claimed = new Set(nodes.flatMap((item) => item.lines.map((line) => line.guid)));
30509
- const extras = (packagingEntry.lines ?? []).filter((line) => !claimed.has(line.guid)).filter(isMeaningfulItineraryLine);
30510
- if (!extras.length)
30511
- return nodes;
30512
- const dayOrderOf = (date) => trajectEntry.traject.days.find((day) => dateFns.isSameDay(new Date(day.date), new Date(date)))?.order ??
30513
- Math.max(dateFns.differenceInCalendarDays(new Date(date), new Date(trajectEntry.traject.startDate)), 0);
30514
- // One product bought once, however many option lines it took (an adult and a child ticket).
30515
- const grouped = new Map();
30516
- extras.forEach((line) => {
30517
- const key = `${line.productCode}|${line.from}`;
30518
- grouped.set(key, [...(grouped.get(key) ?? []), line]);
30519
- });
30520
- const byDay = new Map();
30521
- nodes.forEach((item) => byDay.set(item.node.dayOrder, [...(byDay.get(item.node.dayOrder) ?? []), item]));
30522
- [...grouped.values()].forEach((lines, index) => {
30523
- const line = lines[0];
30524
- const dayOrder = dayOrderOf(line.from);
30525
- const item = {
30526
- lines,
30527
- node: {
30528
- nodeId: -(index + 1),
30529
- lineGuid: line.guid,
30530
- lineGuids: lines.map((x) => x.guid),
30531
- dayOrder,
30532
- order: Number.MAX_SAFE_INTEGER,
30533
- name: line.productName,
30534
- serviceType: line.serviceType,
30535
- trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
30536
- requirementType: 0,
30537
- contentSource: 0,
30538
- duration: 0,
30539
- durationType: 0,
30540
- alternatives: [],
30541
- from: line.from,
30542
- to: line.to
30543
- }
30544
- };
30545
- const dayItems = [...(byDay.get(dayOrder) ?? [])];
30546
- let at = dayItems.length;
30547
- while (at > 0 && dayItems[at - 1].node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE)
30548
- at--;
30549
- dayItems.splice(at, 0, item);
30550
- byDay.set(dayOrder, dayItems);
30551
- });
30552
- return [...byDay.keys()].sort((a, b) => a - b).flatMap((dayOrder) => byDay.get(dayOrder));
30553
- }, [trajectEntry, packagingEntry]);
30554
- const stayByDay = React.useMemo(() => {
30555
- const map = new Map();
30556
- timeline
30557
- .filter(({ node }) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === ACCOMMODATION_SERVICE_TYPE)
30558
- .forEach(({ node, lines }) => {
30559
- const line = lines[0];
30560
- const nights = Math.max(nightsOf(node, line), 1);
30561
- const name = line?.productName || node.preferredAccommodationName || node.name || '';
30562
- for (let night = 0; night < nights; night++)
30563
- map.set(node.dayOrder + night, { name, night, nights });
30564
- });
30565
- return map;
30566
- }, [timeline]);
30567
- const renderDate = (value) => (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" },
30568
- React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-date-date" },
30569
- React__default["default"].createElement("strong", null, dateFns.format(new Date(value), 'd'))),
30570
- React__default["default"].createElement("p", null, dateFns.format(new Date(value), 'MMM'))));
30571
- const renderConnectingNode = ({ node, lines }) => {
30572
- const distance = node.distanceInKm ? `${Math.round(node.distanceInKm)} km` : '';
30573
- // How long the hop takes and how far it is, the way the traject editor puts them.
30574
- const journey = [getTrajectNodeDuration(node), distance].filter(Boolean).join(' · ');
30575
- const title = lines[0]?.productName || node.name;
30576
- return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `connect-${node.nodeId}` },
30577
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30578
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30579
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
30580
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" }),
30581
- React__default["default"].createElement("div", { className: badgeClass('transport', getTrajectNodeBadgeModifier(node)) },
30582
- React__default["default"].createElement(Icon, { name: getTrajectNodeIcon(node), height: 15 })),
30583
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
30584
- React__default["default"].createElement("h6", null, title || journey),
30585
- !!title && !!journey && React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" }, journey)))));
30586
- };
30587
- const renderOvernight = (item, index) => {
30588
- const { dayOrder } = item.node;
30589
- const closesTheDay = timeline[index + 1]?.node.dayOrder !== dayOrder;
30590
- const stay = stayByDay.get(dayOrder);
30591
- if (!closesTheDay || !stay)
30412
+ const getFlightLines = (flight) => {
30413
+ return flight?.flightInformation?.flightLines ?? [];
30414
+ };
30415
+ const getDepartureTime = (flight) => {
30416
+ const firstLine = lodash.first(getFlightLines(flight));
30417
+ if (!firstLine?.departureTime)
30418
+ return '';
30419
+ return firstLine.departureTime.slice(0, 5);
30420
+ };
30421
+ const getArrivalTime = (flight) => {
30422
+ const lastLine = lodash.last(getFlightLines(flight));
30423
+ if (!lastLine?.arrivalTime)
30424
+ return '';
30425
+ return lastLine.arrivalTime.slice(0, 5);
30426
+ };
30427
+ const getDuration = (flight) => {
30428
+ const lines = getFlightLines(flight);
30429
+ if (!lines.length)
30430
+ return '';
30431
+ const totalTicks = lines.reduce((sum, line) => sum + (line.durationInTicks ?? 0), 0) || 0;
30432
+ if (!totalTicks)
30433
+ return '';
30434
+ const seconds = totalTicks / 10_000_000;
30435
+ const hours = Math.floor(seconds / 3600);
30436
+ const minutes = Math.floor((seconds % 3600) / 60);
30437
+ return `${hours}h ${minutes.toString().padStart(2, '0')}m`;
30438
+ };
30439
+ const numberOfNights = (segment) => {
30440
+ return dateFns.differenceInCalendarDays(new Date(segment.to), new Date(segment.from));
30441
+ };
30442
+ const CHARTER_SERVICE_TYPE = 10;
30443
+ const CRUISE_SERVICE_TYPE = 11;
30444
+ const FERRY_SERVICE_TYPE = 12;
30445
+ const TRANSFER_SERVICE_TYPE = 13;
30446
+ const BUS_SERVICE_TYPE = 14;
30447
+ const CAR_SERVICE_TYPE = 17;
30448
+ const CAMPER_SERVICE_TYPE = 18;
30449
+ const TRAIN_SERVICE_TYPE = 20;
30450
+ const RENTAL_SERVICE_TYPE = 22;
30451
+ const RESTAURANT_SERVICE_TYPE = 23;
30452
+ const EVENT_SERVICE_TYPE = 24;
30453
+ const TRAINING_SERVICE_TYPE = 25;
30454
+ /** Icon per service type, so every node on the itinerary reads at a glance. */
30455
+ const getServiceTypeIcon = (serviceType) => {
30456
+ switch (serviceType) {
30457
+ case ACCOMMODATION_SERVICE_TYPE:
30458
+ return 'ui-hotel';
30459
+ case EXCURSION_SERVICE_TYPE:
30460
+ return 'ui-excursion';
30461
+ case FLIGHT_SERVICE_TYPE:
30462
+ case CHARTER_SERVICE_TYPE:
30463
+ return 'ui-plane';
30464
+ case RESTAURANT_SERVICE_TYPE:
30465
+ return 'ui-utensils';
30466
+ case CRUISE_SERVICE_TYPE:
30467
+ case FERRY_SERVICE_TYPE:
30468
+ return 'ui-ship';
30469
+ case CAR_SERVICE_TYPE:
30470
+ case CAMPER_SERVICE_TYPE:
30471
+ case RENTAL_SERVICE_TYPE:
30472
+ return 'ui-car';
30473
+ case BUS_SERVICE_TYPE:
30474
+ return 'ui-bus';
30475
+ case TRAIN_SERVICE_TYPE:
30476
+ return 'ui-train';
30477
+ case TRANSFER_SERVICE_TYPE:
30478
+ return 'ui-transfer';
30479
+ case EVENT_SERVICE_TYPE:
30480
+ return 'ui-ticket';
30481
+ case TRAINING_SERVICE_TYPE:
30482
+ return 'ui-group';
30483
+ default:
30484
+ return 'ui-location';
30485
+ }
30486
+ };
30487
+ const getServiceTypeBadgeModifier = (serviceType) => {
30488
+ switch (serviceType) {
30489
+ case FLIGHT_SERVICE_TYPE:
30490
+ case CHARTER_SERVICE_TYPE:
30491
+ case CRUISE_SERVICE_TYPE:
30492
+ case FERRY_SERVICE_TYPE:
30493
+ return 'flight';
30494
+ case ACCOMMODATION_SERVICE_TYPE:
30495
+ return 'hotel';
30496
+ case EXCURSION_SERVICE_TYPE:
30497
+ case EVENT_SERVICE_TYPE:
30498
+ return 'excursion';
30499
+ case RESTAURANT_SERVICE_TYPE:
30500
+ return 'restaurant';
30501
+ default:
30502
+ return '';
30503
+ }
30504
+ };
30505
+ // DurationType, as the SDK numbers it.
30506
+ const DURATION_HOURS = 0;
30507
+ const DURATION_DAYS = 1;
30508
+ const DURATION_NIGHTS = 2;
30509
+ const DURATION_MINUTES = 3;
30510
+ /**
30511
+ * How long a node takes, in the same shape the flight legs are written in — an hour count only
30512
+ * once there is one, so a short drive reads as plain minutes.
30513
+ */
30514
+ const getTrajectNodeDuration = (node) => {
30515
+ const minutes = toMinutes(node);
30516
+ if (!minutes)
30517
+ return '';
30518
+ const hours = Math.floor(minutes / 60);
30519
+ const rest = minutes % 60;
30520
+ return hours ? `${hours}h ${rest.toString().padStart(2, '0')}m` : `${rest} min`;
30521
+ };
30522
+ const toMinutes = (node) => {
30523
+ const duration = node.duration ?? 0;
30524
+ if (duration <= 0)
30525
+ return 0;
30526
+ switch (node.durationType) {
30527
+ case DURATION_MINUTES:
30528
+ return duration;
30529
+ case DURATION_HOURS:
30530
+ return duration * 60;
30531
+ case DURATION_DAYS:
30532
+ case DURATION_NIGHTS:
30533
+ return duration * 24 * 60;
30534
+ default:
30535
+ return 0;
30536
+ }
30537
+ };
30538
+ // RouteTransportType, as the SDK numbers it. It says how a leg is travelled, which is not the
30539
+ // same question as what was sold: cycling and walking have no service type of their own.
30540
+ const TRANSPORT_PRIVATE = 1;
30541
+ const TRANSPORT_PUBLIC = 2;
30542
+ const TRANSPORT_RENTAL_CAR = 3;
30543
+ const TRANSPORT_BUS = 4;
30544
+ const TRANSPORT_CAMPER = 5;
30545
+ const TRANSPORT_CAR = 6;
30546
+ const TRANSPORT_CHARTER = 7;
30547
+ const TRANSPORT_CRUISE = 8;
30548
+ const TRANSPORT_FERRY = 9;
30549
+ const TRANSPORT_FLIGHT = 10;
30550
+ const TRANSPORT_TAXI = 11;
30551
+ const TRANSPORT_TRAIN = 12;
30552
+ const TRANSPORT_TRANSFER = 13;
30553
+ const TRANSPORT_WALKING = 15;
30554
+ const TRANSPORT_CYCLING = 16;
30555
+ /** Icon per way of travelling, where the route records one. */
30556
+ const getTransportTypeIcon = (transportType) => {
30557
+ switch (transportType) {
30558
+ case TRANSPORT_WALKING:
30559
+ return 'ui-walking';
30560
+ case TRANSPORT_CYCLING:
30561
+ return 'ui-cycling';
30562
+ case TRANSPORT_PRIVATE:
30563
+ case TRANSPORT_RENTAL_CAR:
30564
+ case TRANSPORT_CAMPER:
30565
+ case TRANSPORT_CAR:
30566
+ return 'ui-car';
30567
+ case TRANSPORT_TAXI:
30568
+ case TRANSPORT_TRANSFER:
30569
+ return 'ui-transfer';
30570
+ case TRANSPORT_PUBLIC:
30571
+ case TRANSPORT_BUS:
30572
+ return 'ui-bus';
30573
+ case TRANSPORT_TRAIN:
30574
+ return 'ui-train';
30575
+ case TRANSPORT_CRUISE:
30576
+ case TRANSPORT_FERRY:
30577
+ return 'ui-ship';
30578
+ case TRANSPORT_FLIGHT:
30579
+ case TRANSPORT_CHARTER:
30580
+ return 'ui-plane';
30581
+ default:
30592
30582
  return null;
30593
- return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `overnight-${item.node.nodeId}` },
30594
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30595
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30596
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
30597
- renderDate(item.node.from),
30598
- React__default["default"].createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(ACCOMMODATION_SERVICE_TYPE)) },
30599
- React__default["default"].createElement(Icon, { name: "ui-bed", height: 15 })),
30600
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
30601
- React__default["default"].createElement("h6", null, stay.name),
30602
- React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" },
30603
- stay.night + 1,
30604
- " ",
30605
- translations.SRP.OF,
30606
- " ",
30607
- stay.nights,
30608
- " ",
30609
- translations.SRP.NIGHTS)))));
30610
- };
30611
- const renderFlightNode = ({ node, lines }) => {
30612
- const line = lines[0];
30613
- return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `flight-${node.nodeId}` },
30614
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30615
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30616
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
30617
- renderDate(line?.from ?? node.from),
30618
- React__default["default"].createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(FLIGHT_SERVICE_TYPE)) },
30619
- React__default["default"].createElement(Icon, { name: "ui-plane", height: 15 })),
30620
- React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
30621
- React__default["default"].createElement("h6", null, line?.productName || node.name),
30622
- !!getDepartureTime(line) && (React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
30623
- React__default["default"].createElement("span", null,
30624
- React__default["default"].createElement(Icon, { name: "ui-plane-depart", height: 14 }),
30625
- " ",
30626
- React__default["default"].createElement("strong", null, getDepartureTime(line))),
30627
- ' ',
30628
- "-",
30629
- ' ',
30630
- React__default["default"].createElement("span", null,
30631
- React__default["default"].createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
30632
- " ",
30633
- React__default["default"].createElement("strong", null, getArrivalTime(line))))),
30634
- !!getDuration(line) && (React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" },
30635
- React__default["default"].createElement("span", null,
30636
- React__default["default"].createElement(Icon, { name: "ui-clock", height: 20 }),
30637
- " ",
30638
- getDuration(line))))))));
30639
- };
30640
- const renderServiceNode = ({ node, lines }) => {
30641
- const line = lines[0];
30642
- const isStay = node.serviceType === ACCOMMODATION_SERVICE_TYPE;
30643
- const nights = isStay ? nightsOf(node, line) : 0;
30644
- return (React__default["default"].createElement("div", { className: "search__filter__itinerary__segments", key: `node-${node.nodeId}` },
30645
- React__default["default"].createElement("div", { className: "search__filter__itinerary__segments__wrapper" },
30646
- React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline" },
30647
- React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline-line" })),
30648
- React__default["default"].createElement("div", { className: "search__filter__itinerary__segment" },
30649
- React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-item" },
30650
- React__default["default"].createElement("div", { className: `search__filter__itinerary__segment-date ${isStay && nights > 0 ? 'search__filter__itinerary__segment-nights' : ''}` }, isStay && nights > 0 ? (React__default["default"].createElement(React__default["default"].Fragment, null,
30651
- React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-date-date" },
30652
- React__default["default"].createElement("strong", null, nights)),
30653
- React__default["default"].createElement(Icon, { name: "ui-moon", width: 16, height: 16 }))) : (React__default["default"].createElement(React__default["default"].Fragment, null,
30654
- React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-date-date" },
30655
- React__default["default"].createElement("strong", null, dateFns.format(new Date(line?.from ?? node.from), 'd'))),
30656
- React__default["default"].createElement("p", null, dateFns.format(new Date(line?.from ?? node.from), 'MMM'))))),
30657
- React__default["default"].createElement("div", { className: badgeClass('segment', getTrajectNodeBadgeModifier(node)) },
30658
- React__default["default"].createElement(Icon, { name: getTrajectNodeIcon(node), width: 15, height: 15 })),
30659
- React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details" },
30660
- React__default["default"].createElement("h6", null, line?.productName || node.preferredAccommodationName || node.name),
30661
- isStay &&
30662
- lines.map((roomLine, index) => (React__default["default"].createElement(React__default["default"].Fragment, { key: roomLine.guid },
30663
- lines.length > 1 && (React__default["default"].createElement("strong", null,
30664
- translations.SHARED.ROOM,
30665
- "\u00A0",
30666
- index + 1)),
30667
- roomLine.accommodationName && (React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details__room" },
30668
- React__default["default"].createElement(Icon, { name: "ui-bed", width: 12, height: 12 }),
30669
- roomLine.accommodationName)),
30670
- roomLine.regimeName && (React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details__room" },
30671
- React__default["default"].createElement(Icon, { name: "ui-utensils", width: 12, height: 12 }),
30672
- roomLine.regimeName))))),
30673
- !isStay && React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-details-text" }, dateFns.format(new Date(line?.from ?? node.from), 'EEE. d MMM yyyy'))))))));
30674
- };
30675
- return (React__default["default"].createElement(React__default["default"].Fragment, null, timeline.map((item, index) => {
30676
- if (item.node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
30677
- return (React__default["default"].createElement(React__default["default"].Fragment, { key: `connect-${item.node.nodeId}` },
30678
- renderConnectingNode(item),
30679
- renderOvernight(item, index)));
30680
- }
30681
- if (item.node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && item.node.serviceType === FLIGHT_SERVICE_TYPE)
30682
- return renderFlightNode(item);
30683
- return renderServiceNode(item);
30684
- })));
30583
+ }
30685
30584
  };
30585
+ /**
30586
+ * A point of interest is a place on the map rather than a service, so it gets a marker. Anything
30587
+ * else goes by how it is travelled where the route says so — a leg on foot or by bike is sold as
30588
+ * nothing, and its service type falls back to a transfer — and by what it is where it does not.
30589
+ */
30590
+ const getTrajectNodeIcon = (node) => {
30591
+ if (node.trajectNodeType === POI_TRAJECT_NODE_TYPE)
30592
+ return 'ui-location';
30593
+ return getTransportTypeIcon(node.transportType) ?? getServiceTypeIcon(node.serviceType);
30594
+ };
30595
+ const getTrajectNodeBadgeModifier = (node) => node.trajectNodeType === POI_TRAJECT_NODE_TYPE ? 'poi' : getServiceTypeBadgeModifier(node.serviceType);
30686
30596
 
30687
30597
  /**
30688
30598
  * A booked entry opened through its magic link knows nothing about the traject it may once have
30689
30599
  * been composed from — the dossier only keeps the lines. To give it the same day-by-day services
30690
30600
  * screen a traject gets, the traject structure is rebuilt from those lines: every booked service
30691
30601
  * becomes a node whose one "alternative" is the booked product itself, and the days span the trip.
30602
+ * The points of interest on the dossier's own route (entry.routeNodes) are added between them.
30692
30603
  *
30693
30604
  * The synthesized response keeps the entry untouched (magic link included), which is also how the
30694
30605
  * rest of the component tells this mode apart from a real traject: see isBookedEntryTraject.
@@ -30765,21 +30676,24 @@ const buildBookedTrajectEntry = (entry) => {
30765
30676
  const tos = lines.map((line) => new Date(line.to).getTime());
30766
30677
  const start = froms.length ? new Date(Math.min(...froms)) : new Date();
30767
30678
  const end = tos.length ? new Date(Math.max(...tos)) : start;
30679
+ // Local midnights, written the way the backend writes a traject's days. A UTC date (toDateOnlyString)
30680
+ // names another day away from Greenwich, and every line looked up by its day would slide onto it.
30768
30681
  const dayCount = Math.max(dateFns.differenceInCalendarDays(end, start), 0) + 1;
30769
30682
  const days = Array.from({ length: dayCount }, (_, order) => ({
30770
30683
  order,
30771
30684
  name: '',
30772
- date: toDateOnlyString(dateFns.addDays(start, order))
30685
+ date: dateFns.format(dateFns.addDays(start, order), "yyyy-MM-dd'T00:00:00'")
30773
30686
  }));
30774
- const dayOrderOf = (line) => Math.max(dateFns.differenceInCalendarDays(new Date(line.from), start), 0);
30687
+ const dayOrderOf = (item) => Math.min(Math.max(dateFns.differenceInCalendarDays(new Date(item.from), start), 0), dayCount - 1);
30775
30688
  const flights = lodash.sortBy(lines.filter((line) => line.serviceType === FLIGHT_SERVICE_TYPE && isActiveEntryLine(line)), (line) => new Date(line.from).getTime(), (line) => line.order);
30776
30689
  const outward = lodash.first(flights);
30777
30690
  const homeward = flights.length > 1 ? lodash.last(flights) : undefined;
30778
- const placeInDay = (line) => (line === outward ? -1 : line === homeward ? 1 : 0);
30779
- const orderedGroups = groupNodeLines(lines).sort((a, b) => {
30780
- const [lineA, lineB] = [a.lines[0], b.lines[0]];
30781
- return dayOrderOf(lineA) - dayOrderOf(lineB) || placeInDay(lineA) - placeInDay(lineB) || lineA.order - lineB.order;
30782
- });
30691
+ // The dossier keeps its lines in route order, so a node's order is its line's — except that the
30692
+ // outward flight opens its day and the homeward one closes it, whatever else that day holds.
30693
+ const homewardOrder = Math.max(0, ...(entry.lines ?? []).map((line) => line.order)) + 1;
30694
+ const orderOf = (line) => (line === outward ? -1 : line === homeward ? homewardOrder : line.order);
30695
+ const orderedGroups = groupNodeLines(lines).sort((a, b) => dayOrderOf(a.lines[0]) - dayOrderOf(b.lines[0]) || orderOf(a.lines[0]) - orderOf(b.lines[0]));
30696
+ const idBase = Math.max(0, ...(entry.routeNodes ?? []).map((routeNode) => routeNode.nodeId));
30783
30697
  const nodes = orderedGroups.map(({ lines: groupLines, alternatives }, index) => {
30784
30698
  const line = groupLines[0];
30785
30699
  const flightLines = line.flightInformation?.flightLines ?? [];
@@ -30789,9 +30703,9 @@ const buildBookedTrajectEntry = (entry) => {
30789
30703
  return {
30790
30704
  lineGuid: line.guid,
30791
30705
  lineGuids: groupLines.map((x) => x.guid),
30792
- nodeId: index + 1,
30706
+ nodeId: idBase + index + 1,
30793
30707
  dayOrder: dayOrderOf(line),
30794
- order: index,
30708
+ order: orderOf(line),
30795
30709
  name: (alternatives.length && getLineGroupKey(line)) || line.productName,
30796
30710
  serviceType: line.serviceType,
30797
30711
  trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
@@ -30809,6 +30723,23 @@ const buildBookedTrajectEntry = (entry) => {
30809
30723
  to: line.to
30810
30724
  };
30811
30725
  });
30726
+ const linesByGuid = new Map((entry.lines ?? []).map((line) => [line.guid, line]));
30727
+ let anchorOrder = -2;
30728
+ let step = 0;
30729
+ (entry.routeNodes ?? []).forEach((routeNode) => {
30730
+ const anchor = linesByGuid.get(routeNode.lineGuid);
30731
+ if (anchor) {
30732
+ anchorOrder = orderOf(anchor);
30733
+ step = 0;
30734
+ if (routeNode.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE)
30735
+ nodes.push({ ...routeNode, dayOrder: dayOrderOf(routeNode), order: anchorOrder });
30736
+ return;
30737
+ }
30738
+ if (routeNode.trajectNodeType !== POI_TRAJECT_NODE_TYPE)
30739
+ return;
30740
+ step++;
30741
+ nodes.push({ ...routeNode, nodeId: idBase + nodes.length + 1, dayOrder: dayOrderOf(routeNode), order: anchorOrder + step / 1000 });
30742
+ });
30812
30743
  return {
30813
30744
  entry,
30814
30745
  traject: {
@@ -30930,6 +30861,222 @@ const buildBookedFlightResult = (line) => {
30930
30861
  };
30931
30862
  };
30932
30863
 
30864
+ const badgeClass = (kind, modifier) => {
30865
+ const base = `search__filter__itinerary__${kind}-badge`;
30866
+ const fallback = kind === 'segment' ? `${base}--secondary` : `${base}--default`;
30867
+ return `${base} ${modifier ? `${base}--${modifier}` : fallback}`;
30868
+ };
30869
+ const nightsOf = (node, line) => dateFns.differenceInCalendarDays(new Date(line?.to ?? node.to), new Date(line?.from ?? node.from));
30870
+ const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
30871
+ const timeline = React.useMemo(() => {
30872
+ const linesByGuid = new Map((packagingEntry.lines ?? []).map((line) => [line.guid, line]));
30873
+ const dropped = getDroppedTrajectNodeIds(trajectEntry, packagingEntry);
30874
+ const nodes = [...(trajectEntry.nodes ?? [])]
30875
+ .filter((node) => !dropped.has(node.nodeId))
30876
+ .sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
30877
+ .map((node) => {
30878
+ if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
30879
+ return { node, lines: getTrajectTransferLines(node, packagingEntry.lines ?? []) };
30880
+ }
30881
+ const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
30882
+ const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
30883
+ const groupKey = lines.length ? getLineGroupKey(lines[0]) : '';
30884
+ const active = groupKey ? getGroupLines(packagingEntry.lines ?? [], groupKey)[0] : undefined;
30885
+ return { node, lines: active ?? lines };
30886
+ });
30887
+ const claimed = new Set(nodes.flatMap((item) => item.lines.map((line) => line.guid)));
30888
+ const extras = (packagingEntry.lines ?? []).filter((line) => !claimed.has(line.guid)).filter(isMeaningfulItineraryLine);
30889
+ if (!extras.length)
30890
+ return nodes;
30891
+ const dayOrderOf = (date) => trajectEntry.traject.days.find((day) => dateFns.isSameDay(new Date(day.date), new Date(date)))?.order ??
30892
+ Math.max(dateFns.differenceInCalendarDays(new Date(date), new Date(trajectEntry.traject.startDate)), 0);
30893
+ // One product bought once, however many option lines it took (an adult and a child ticket).
30894
+ const grouped = new Map();
30895
+ extras.forEach((line) => {
30896
+ const key = `${line.productCode}|${line.from}`;
30897
+ grouped.set(key, [...(grouped.get(key) ?? []), line]);
30898
+ });
30899
+ const byDay = new Map();
30900
+ nodes.forEach((item) => byDay.set(item.node.dayOrder, [...(byDay.get(item.node.dayOrder) ?? []), item]));
30901
+ const flightNodes = nodes.filter(({ node }) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === FLIGHT_SERVICE_TYPE);
30902
+ const homeward = flightNodes.length > 1 ? flightNodes[flightNodes.length - 1] : undefined;
30903
+ // A booked entry's nodes are ordered as the dossier orders its lines (see buildBookedTrajectEntry).
30904
+ const bookedOrders = isBookedEntryTraject(trajectEntry) ? new Map((trajectEntry.entry.lines ?? []).map((line) => [line.guid, line.order])) : undefined;
30905
+ [...grouped.values()].forEach((lines, index) => {
30906
+ const line = lines[0];
30907
+ const dayOrder = dayOrderOf(line.from);
30908
+ const bookedOrder = bookedOrders?.get(line.guid);
30909
+ const item = {
30910
+ lines,
30911
+ node: {
30912
+ nodeId: -(index + 1),
30913
+ lineGuid: line.guid,
30914
+ lineGuids: lines.map((x) => x.guid),
30915
+ dayOrder,
30916
+ order: bookedOrder ?? Number.MAX_SAFE_INTEGER,
30917
+ name: line.productName,
30918
+ serviceType: line.serviceType,
30919
+ trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
30920
+ requirementType: 0,
30921
+ contentSource: 0,
30922
+ duration: 0,
30923
+ durationType: 0,
30924
+ alternatives: [],
30925
+ from: line.from,
30926
+ to: line.to
30927
+ }
30928
+ };
30929
+ const dayItems = [...(byDay.get(dayOrder) ?? [])];
30930
+ // A booked line goes where the dossier's order puts it; one the traveller added closes its
30931
+ // day, but before the hops that leave it and the flight home.
30932
+ let at = dayItems.length;
30933
+ if (bookedOrder !== undefined) {
30934
+ at = dayItems.findIndex((x) => x.node.order > bookedOrder);
30935
+ if (at < 0)
30936
+ at = dayItems.length;
30937
+ }
30938
+ else {
30939
+ while (at > 0 && (dayItems[at - 1].node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE || dayItems[at - 1] === homeward))
30940
+ at--;
30941
+ }
30942
+ dayItems.splice(at, 0, item);
30943
+ byDay.set(dayOrder, dayItems);
30944
+ });
30945
+ return [...byDay.keys()].sort((a, b) => a - b).flatMap((dayOrder) => byDay.get(dayOrder));
30946
+ }, [trajectEntry, packagingEntry]);
30947
+ const stayByDay = React.useMemo(() => {
30948
+ const map = new Map();
30949
+ timeline
30950
+ .filter(({ node }) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === ACCOMMODATION_SERVICE_TYPE)
30951
+ .forEach(({ node, lines }) => {
30952
+ const line = lines[0];
30953
+ const nights = Math.max(nightsOf(node, line), 1);
30954
+ const name = line?.productName || node.preferredAccommodationName || node.name || '';
30955
+ for (let night = 0; night < nights; night++)
30956
+ map.set(node.dayOrder + night, { name, night, nights });
30957
+ });
30958
+ return map;
30959
+ }, [timeline]);
30960
+ const renderDate = (value) => (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" },
30961
+ React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-date-date" },
30962
+ React__default["default"].createElement("strong", null, dateFns.format(new Date(value), 'd'))),
30963
+ React__default["default"].createElement("p", null, dateFns.format(new Date(value), 'MMM'))));
30964
+ const renderConnectingNode = ({ node, lines }) => {
30965
+ const distance = node.distanceInKm ? `${Math.round(node.distanceInKm)} km` : '';
30966
+ // How long the hop takes and how far it is, the way the traject editor puts them.
30967
+ const journey = [getTrajectNodeDuration(node), distance].filter(Boolean).join(' · ');
30968
+ const title = lines[0]?.productName || node.name;
30969
+ return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `connect-${node.nodeId}` },
30970
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30971
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30972
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
30973
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" }),
30974
+ React__default["default"].createElement("div", { className: badgeClass('transport', getTrajectNodeBadgeModifier(node)) },
30975
+ React__default["default"].createElement(Icon, { name: getTrajectNodeIcon(node), height: 15 })),
30976
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
30977
+ React__default["default"].createElement("h6", null, title || journey),
30978
+ !!title && !!journey && React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" }, journey)))));
30979
+ };
30980
+ const renderOvernight = (item, index) => {
30981
+ const { dayOrder } = item.node;
30982
+ const closesTheDay = timeline[index + 1]?.node.dayOrder !== dayOrder;
30983
+ const stay = stayByDay.get(dayOrder);
30984
+ if (!closesTheDay || !stay)
30985
+ return null;
30986
+ return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `overnight-${item.node.nodeId}` },
30987
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30988
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30989
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
30990
+ renderDate(item.node.from),
30991
+ React__default["default"].createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(ACCOMMODATION_SERVICE_TYPE)) },
30992
+ React__default["default"].createElement(Icon, { name: "ui-bed", height: 15 })),
30993
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
30994
+ React__default["default"].createElement("h6", null, stay.name),
30995
+ React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" },
30996
+ stay.night + 1,
30997
+ " ",
30998
+ translations.SRP.OF,
30999
+ " ",
31000
+ stay.nights,
31001
+ " ",
31002
+ translations.SRP.NIGHTS)))));
31003
+ };
31004
+ const renderFlightNode = ({ node, lines }) => {
31005
+ const line = lines[0];
31006
+ return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `flight-${node.nodeId}` },
31007
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
31008
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
31009
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
31010
+ renderDate(line?.from ?? node.from),
31011
+ React__default["default"].createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(FLIGHT_SERVICE_TYPE)) },
31012
+ React__default["default"].createElement(Icon, { name: "ui-plane", height: 15 })),
31013
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
31014
+ React__default["default"].createElement("h6", null, line?.productName || node.name),
31015
+ !!getDepartureTime(line) && (React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
31016
+ React__default["default"].createElement("span", null,
31017
+ React__default["default"].createElement(Icon, { name: "ui-plane-depart", height: 14 }),
31018
+ " ",
31019
+ React__default["default"].createElement("strong", null, getDepartureTime(line))),
31020
+ ' ',
31021
+ "-",
31022
+ ' ',
31023
+ React__default["default"].createElement("span", null,
31024
+ React__default["default"].createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
31025
+ " ",
31026
+ React__default["default"].createElement("strong", null, getArrivalTime(line))))),
31027
+ !!getDuration(line) && (React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" },
31028
+ React__default["default"].createElement("span", null,
31029
+ React__default["default"].createElement(Icon, { name: "ui-clock", height: 20 }),
31030
+ " ",
31031
+ getDuration(line))))))));
31032
+ };
31033
+ const renderServiceNode = ({ node, lines }) => {
31034
+ const line = lines[0];
31035
+ const isStay = node.serviceType === ACCOMMODATION_SERVICE_TYPE;
31036
+ const nights = isStay ? nightsOf(node, line) : 0;
31037
+ return (React__default["default"].createElement("div", { className: "search__filter__itinerary__segments", key: `node-${node.nodeId}` },
31038
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__segments__wrapper" },
31039
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline" },
31040
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline-line" })),
31041
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__segment" },
31042
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-item" },
31043
+ React__default["default"].createElement("div", { className: `search__filter__itinerary__segment-date ${isStay && nights > 0 ? 'search__filter__itinerary__segment-nights' : ''}` }, isStay && nights > 0 ? (React__default["default"].createElement(React__default["default"].Fragment, null,
31044
+ React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-date-date" },
31045
+ React__default["default"].createElement("strong", null, nights)),
31046
+ React__default["default"].createElement(Icon, { name: "ui-moon", width: 16, height: 16 }))) : (React__default["default"].createElement(React__default["default"].Fragment, null,
31047
+ React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-date-date" },
31048
+ React__default["default"].createElement("strong", null, dateFns.format(new Date(line?.from ?? node.from), 'd'))),
31049
+ React__default["default"].createElement("p", null, dateFns.format(new Date(line?.from ?? node.from), 'MMM'))))),
31050
+ React__default["default"].createElement("div", { className: badgeClass('segment', getTrajectNodeBadgeModifier(node)) },
31051
+ React__default["default"].createElement(Icon, { name: getTrajectNodeIcon(node), width: 15, height: 15 })),
31052
+ React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details" },
31053
+ React__default["default"].createElement("h6", null, line?.productName || node.preferredAccommodationName || node.name),
31054
+ isStay &&
31055
+ lines.map((roomLine, index) => (React__default["default"].createElement(React__default["default"].Fragment, { key: roomLine.guid },
31056
+ lines.length > 1 && (React__default["default"].createElement("strong", null,
31057
+ translations.SHARED.ROOM,
31058
+ "\u00A0",
31059
+ index + 1)),
31060
+ roomLine.accommodationName && (React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details__room" },
31061
+ React__default["default"].createElement(Icon, { name: "ui-bed", width: 12, height: 12 }),
31062
+ roomLine.accommodationName)),
31063
+ roomLine.regimeName && (React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details__room" },
31064
+ React__default["default"].createElement(Icon, { name: "ui-utensils", width: 12, height: 12 }),
31065
+ roomLine.regimeName))))),
31066
+ !isStay && React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-details-text" }, dateFns.format(new Date(line?.from ?? node.from), 'EEE. d MMM yyyy'))))))));
31067
+ };
31068
+ return (React__default["default"].createElement(React__default["default"].Fragment, null, timeline.map((item, index) => {
31069
+ if (item.node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
31070
+ return (React__default["default"].createElement(React__default["default"].Fragment, { key: `connect-${item.node.nodeId}` },
31071
+ renderConnectingNode(item),
31072
+ renderOvernight(item, index)));
31073
+ }
31074
+ if (item.node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && item.node.serviceType === FLIGHT_SERVICE_TYPE)
31075
+ return renderFlightNode(item);
31076
+ return renderServiceNode(item);
31077
+ })));
31078
+ };
31079
+
30933
31080
  const getSegmentIcon = (segment) => {
30934
31081
  switch (segment.serviceType) {
30935
31082
  case 3:
@@ -30969,10 +31116,10 @@ const SERVICE_TYPE_PRIORITY = {
30969
31116
  const getServiceTypePriority = (serviceType) => {
30970
31117
  return SERVICE_TYPE_PRIORITY[serviceType ?? -1] ?? 2;
30971
31118
  };
30972
- const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, onSaveEntry }) => {
31119
+ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation }) => {
30973
31120
  const context = React.useContext(SearchResultsConfigurationContext);
30974
31121
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
30975
- const { editablePackagingEntry, priceDetails, trajectItineraryView } = reactRedux.useSelector((state) => state.searchResults);
31122
+ const { editablePackagingEntry, priceDetails, priceDetailsFailed, trajectItineraryView } = reactRedux.useSelector((state) => state.searchResults);
30976
31123
  const dispatch = reactRedux.useDispatch();
30977
31124
  const packagingEntry = editablePackagingEntry ?? context?.packagingEntry;
30978
31125
  const sortedLines = React.useMemo(() => {
@@ -31038,7 +31185,9 @@ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, on
31038
31185
  packagingEntry.dossierNumber)),
31039
31186
  React__default["default"].createElement("h4", { className: "search__filter__image__title" }, (location || '') + (location && country ? ' - ' : '') + (country || ''))))),
31040
31187
  React__default["default"].createElement("div", { className: "search__filter__prices" }, isLoading ? (React__default["default"].createElement(Spinner, { label: translations.PRODUCT.LOADING_PRICE })) : (React__default["default"].createElement(React__default["default"].Fragment, null,
31041
- React__default["default"].createElement("div", { className: "search__filter__prices__wrapper" },
31188
+ priceDetailsFailed ? (React__default["default"].createElement("div", { className: "search__filter__prices__wrapper" },
31189
+ React__default["default"].createElement("p", { className: "info-message--error" }, translations.SRP.PRICE_DETAILS_FAILED),
31190
+ React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => dispatch(retryPriceDetails()) }, translations.SRP.TRY_AGAIN))) : (React__default["default"].createElement("div", { className: "search__filter__prices__wrapper" },
31042
31191
  React__default["default"].createElement("h3", { className: "search__filter__prices--amount" }, formatPrice$3(pricePerPerson, 'EUR')),
31043
31192
  React__default["default"].createElement("p", null, translations.SRP.PACKAGE_PRICE_PER_PERSON),
31044
31193
  React__default["default"].createElement("p", null,
@@ -31047,8 +31196,8 @@ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, on
31047
31196
  formatPrice$3(totalPrice, 'EUR'),
31048
31197
  " ",
31049
31198
  translations.SRP.TOTAL,
31050
- ")"))),
31051
- isBookedEntryTraject(context?.trajectEntry) ? (React__default["default"].createElement("button", { className: "cta", onClick: onSaveEntry, disabled: !hasBookedEntryChanges(context?.packagingEntry, packagingEntry) }, translations.SRP.BOOK_AND_PAY)) : isBooked ? (React__default["default"].createElement("span", { className: "cta cta--disabled search__filter__prices--booked", "aria-disabled": "true" }, translations.SHARED.BOOKED)) : (React__default["default"].createElement("button", { className: "cta", onClick: handleConfirm }, translations.SRP.BOOK_AND_PAY)),
31199
+ ")")))),
31200
+ isBookedEntryTraject(context?.trajectEntry) ? (React__default["default"].createElement("button", { className: "cta", onClick: handleConfirm, disabled: priceDetailsFailed || !hasBookedEntryChanges(context?.packagingEntry, packagingEntry) }, translations.SRP.BOOK_AND_PAY)) : isBooked ? (React__default["default"].createElement("span", { className: "cta cta--disabled search__filter__prices--booked", "aria-disabled": "true" }, translations.SHARED.BOOKED)) : (React__default["default"].createElement("button", { className: "cta", onClick: handleConfirm, disabled: priceDetailsFailed }, translations.SRP.BOOK_AND_PAY)),
31052
31201
  !!context?.trajectEntry && (React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleTrajectView(!trajectItineraryView) }, trajectItineraryView ? translations.SRP.SHOW_SERVICES : translations.SRP.VIEW_FULL_ITINERARY))))),
31053
31202
  React__default["default"].createElement("div", { className: "search__filter__itinerary" },
31054
31203
  React__default["default"].createElement("p", null, translations.SRP.DAY_BY_DAY),
@@ -36347,10 +36496,12 @@ const travellersSettings = {
36347
36496
  const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage }) => {
36348
36497
  const context = React.useContext(SearchResultsConfigurationContext);
36349
36498
  const dispatch = reactRedux.useDispatch();
36350
- const { editablePackagingEntry, priceDetails, currentStep, bookingNumber, selectedPackagingAccoResultCode, packagingAccoResults } = reactRedux.useSelector((state) => state.searchResults);
36499
+ const { editablePackagingEntry, priceDetails, priceDetailsFailed, currentStep, bookingNumber, selectedPackagingAccoResultCode, packagingAccoResults } = reactRedux.useSelector((state) => state.searchResults);
36500
+ // A booked entry opened through its magic link is saved onto its own dossier instead of booked again.
36501
+ const bookedMode = isBookedEntryTraject(context?.trajectEntry);
36351
36502
  const [countries, setCountries] = React.useState([]);
36352
36503
  const [userValidated, setUserValidated] = React.useState(true);
36353
- const [remarks, setRemarks] = React.useState('');
36504
+ const [remarks, setRemarks] = React.useState(() => editablePackagingEntry?.remarks ?? '');
36354
36505
  const [isSubmitting, setIsSubmitting] = React.useState(false);
36355
36506
  const [selectedPackagingAccoResult, setSelectedPackagingAccoResult] = React.useState(null);
36356
36507
  const translations = React.useMemo(() => getTranslations(context?.languageCode ?? 'en-GB'), [context?.languageCode]);
@@ -36410,7 +36561,27 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36410
36561
  }, [selectedPackagingAccoResultCode, packagingAccoResults]);
36411
36562
  // What the page holds versus what the backend can look up; they differ for a traject.
36412
36563
  const bookableEntry = useBookableEntry();
36413
- if (!context || !editablePackagingEntry || !priceDetails || !config)
36564
+ // What was already typed stays on the entry, so the form comes back filled in when the traveller returns.
36565
+ const handleBackToServices = () => {
36566
+ if (currentStep === 0 && editablePackagingEntry) {
36567
+ dispatch(setEditablePackagingEntry(applyTravelersFormValuesToEditablePackagingEntry(editablePackagingEntry, formik$1.values)));
36568
+ }
36569
+ dispatch(setCurrentStep(0));
36570
+ dispatch(setBookPackagingEntry(false));
36571
+ if (typeof window !== 'undefined')
36572
+ window.scrollTo(0, 0);
36573
+ };
36574
+ // Nothing can be confirmed without a price for the entry as it is now; the confirmation itself needs none.
36575
+ if (context && priceDetailsFailed && currentStep < 2) {
36576
+ return (React__default["default"].createElement("div", { className: "booking" },
36577
+ React__default["default"].createElement("div", { className: "booking__content" },
36578
+ React__default["default"].createElement("div", { className: "booking__panel" },
36579
+ React__default["default"].createElement("p", { className: "info-message--error" }, translations.SRP.PRICE_DETAILS_FAILED),
36580
+ React__default["default"].createElement("div", { className: "booking__navigator" },
36581
+ React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: handleBackToServices }, translations.SRP.BACK_TO_SERVICES),
36582
+ React__default["default"].createElement("button", { type: "button", className: "cta", onClick: () => dispatch(retryPriceDetails()) }, translations.SRP.TRY_AGAIN))))));
36583
+ }
36584
+ if (!context || !editablePackagingEntry || !config || (!priceDetails && currentStep < 2))
36414
36585
  return React__default["default"].createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING });
36415
36586
  const handleSummarySubmit = async (e) => {
36416
36587
  e.preventDefault();
@@ -36420,7 +36591,7 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36420
36591
  }
36421
36592
  // Held apart from the entry itself so they can be put on both copies of it.
36422
36593
  let bookingFields = { remarks };
36423
- if (context.generatePaymentUrl && typeof window !== 'undefined') {
36594
+ if (!bookedMode && context.generatePaymentUrl && typeof window !== 'undefined') {
36424
36595
  const redirectUrl = new URL(window.location.href);
36425
36596
  redirectUrl.searchParams.set('bookingConfirmation', 'true');
36426
36597
  redirectUrl.searchParams.set('link', '');
@@ -36437,6 +36608,13 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36437
36608
  // The page keeps the entry it works with; the backend gets the one it can look up.
36438
36609
  payload: { ...(bookableEntry ?? editablePackagingEntry), ...bookingFields }
36439
36610
  };
36611
+ if (bookedMode) {
36612
+ const saved = await build.savePackagingEntry(config, request);
36613
+ dispatch(setBookingNumber(saved.dossierNumber ?? editablePackagingEntry.dossierNumber ?? ''));
36614
+ dispatch(setCurrentStep(2));
36615
+ setIsSubmitting(false);
36616
+ return;
36617
+ }
36440
36618
  const bookingResponse = await build.bookPackagingEntry(config, request);
36441
36619
  dispatch(setBookingNumber(bookingResponse.number));
36442
36620
  if (bookingResponse.paymentUrl) {
@@ -36459,8 +36637,8 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36459
36637
  ".\u00A0",
36460
36638
  stepLabels[step])) },
36461
36639
  isConfirmationPage && isLoading && React__default["default"].createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING }),
36462
- currentStep === 0 && (React__default["default"].createElement(SharedTravelersForm, { formik: formik$1, translations: translations, travellersSettings: travellersSettings, countries: countries, travelersFirstStep: false, isUnavailable: false, useCompactForm: false, showAgentSelection: false, paxFields: paxFields })),
36463
- currentStep === 1 && (React__default["default"].createElement(SharedSummary, { translations: translations, travelerFormValues: formik$1.values, isSubmitting: isSubmitting, skipPayment: !context.generatePaymentUrl, userValidated: userValidated, remarks: remarks, enableVoucher: false, allowOption: false, isOffer: false, onUserValidatedChange: setUserValidated, onRemarksChange: setRemarks, onSubmit: handleSummarySubmit, renderOptions: () => renderEditablePackagingEntrySummaryOptions(editablePackagingEntry, priceDetails, translations, selectedPackagingAccoResult?.currencyCode), renderPreviousButton: () => (React__default["default"].createElement("button", { type: "button", title: translations.STEPS.PREVIOUS, onClick: () => dispatch(setCurrentStep(0)), className: "cta cta--secondary" }, translations.STEPS.PREVIOUS)) })),
36640
+ currentStep === 0 && (React__default["default"].createElement(SharedTravelersForm, { formik: formik$1, translations: translations, travellersSettings: travellersSettings, countries: countries, travelersFirstStep: false, isUnavailable: false, useCompactForm: false, showAgentSelection: false, paxFields: paxFields, renderPreviousButton: () => (React__default["default"].createElement("button", { type: "button", title: translations.SRP.BACK_TO_SERVICES, onClick: handleBackToServices, className: "cta cta--secondary" }, translations.SRP.BACK_TO_SERVICES)) })),
36641
+ currentStep === 1 && priceDetails && (React__default["default"].createElement(SharedSummary, { translations: translations, travelerFormValues: formik$1.values, isSubmitting: isSubmitting, skipPayment: bookedMode || !context.generatePaymentUrl, userValidated: userValidated, remarks: remarks, enableVoucher: false, allowOption: false, isOffer: false, onUserValidatedChange: setUserValidated, onRemarksChange: setRemarks, onSubmit: handleSummarySubmit, renderOptions: () => renderEditablePackagingEntrySummaryOptions(editablePackagingEntry, priceDetails, translations, selectedPackagingAccoResult?.currencyCode), renderPreviousButton: () => (React__default["default"].createElement("button", { type: "button", title: translations.STEPS.PREVIOUS, onClick: () => dispatch(setCurrentStep(0)), className: "cta cta--secondary" }, translations.STEPS.PREVIOUS)) })),
36464
36642
  currentStep === 2 && (React__default["default"].createElement(SharedConfirmation, { bookingNumber: bookingNumber ?? editablePackagingEntry?.dossierNumber ?? '', isOption: false, isOffer: false, translations: translations.CONFIRMATION })),
36465
36643
  currentStep === 3 && React__default["default"].createElement("div", null))),
36466
36644
  React__default["default"].createElement("div", { className: "backdrop", id: "backdrop" }),
@@ -36881,6 +37059,16 @@ const TrajectResults = ({ isLoading }) => {
36881
37059
  })));
36882
37060
  };
36883
37061
 
37062
+ const getSearchContext = (context, trajectEntry) => ({
37063
+ transactionId: trajectEntry.entry.transactionId,
37064
+ officeId: context.tideConnection.officeId ?? 1,
37065
+ catalogueId: context.searchConfiguration.defaultCatalogueId ?? lodash.first(context.tideConnection.catalogueIds) ?? 1,
37066
+ searchConfigurationId: context.searchConfiguration.id,
37067
+ portalId: context.portalId ?? null,
37068
+ agentId: context.agentId ?? null,
37069
+ language: context.languageCode ?? 'en-GB',
37070
+ rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
37071
+ });
36884
37072
  const useTrajectAvailability = () => {
36885
37073
  const context = React.useContext(SearchResultsConfigurationContext);
36886
37074
  const dispatch = reactRedux.useDispatch();
@@ -36899,16 +37087,7 @@ const useTrajectAvailability = () => {
36899
37087
  host: context.tideConnection.host,
36900
37088
  apiKey: context.tideConnection.apiKey
36901
37089
  };
36902
- const searchContext = {
36903
- transactionId: trajectEntry.entry.transactionId,
36904
- officeId: context.tideConnection.officeId ?? 1,
36905
- catalogueId: context.searchConfiguration.defaultCatalogueId ?? lodash.first(context.tideConnection.catalogueIds) ?? 1,
36906
- searchConfigurationId: context.searchConfiguration.id,
36907
- portalId: context.portalId ?? null,
36908
- agentId: context.agentId ?? null,
36909
- language: context.languageCode ?? 'en-GB',
36910
- rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
36911
- };
37090
+ const searchContext = getSearchContext(context, trajectEntry);
36912
37091
  // Flight searches take passengers rather than rooms; the API buckets by age.
36913
37092
  // The ids must be the entry's own pax ids: the flight lines this search caches carry them as
36914
37093
  // EntryLinePax.PaxId, and booking maps those onto the entry's travelers (PackagingService.SyncPaxIds).
@@ -37048,30 +37227,9 @@ const useTrajectAvailability = () => {
37048
37227
  return item.lines.map((x) => x.guid);
37049
37228
  }
37050
37229
  };
37051
- const resolveTransferNode = async (node) => {
37052
- dispatch(setTrajectTransferLoading(node.nodeId));
37053
- try {
37054
- const results = (await build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, searchContext), controller.signal)) ?? [];
37055
- if (controller.signal.aborted)
37056
- return [];
37057
- const result = results[0] ?? null;
37058
- dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: result && buildTrajectTransferLine(node, result) ? result : null }));
37059
- }
37060
- catch (error) {
37061
- if (controller.signal.aborted)
37062
- return [];
37063
- console.error('Failed to resolve traject transfer', node.name, error);
37064
- dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: null }));
37065
- }
37066
- return [];
37067
- };
37068
37230
  (async () => {
37069
37231
  const resolvable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node));
37070
- const transfers = bookedMode ? [] : (trajectEntry.nodes ?? []).filter(isTrajectTransferNode);
37071
- const unresolved = (await Promise.all([
37072
- ...resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))),
37073
- ...transfers.map(resolveTransferNode)
37074
- ])).flat();
37232
+ const unresolved = (await Promise.all(resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))))).flat();
37075
37233
  if (controller.signal.aborted)
37076
37234
  return;
37077
37235
  dispatch(setTrajectResolving(false));
@@ -37083,22 +37241,87 @@ const useTrajectAvailability = () => {
37083
37241
  })();
37084
37242
  return () => controller.abort();
37085
37243
  }, [trajectEntry, showApiItems, hideShowMoreResults]);
37244
+ const transferSearches = React.useRef(new Map());
37245
+ React.useEffect(() => {
37246
+ const searches = transferSearches.current;
37247
+ return () => {
37248
+ searches.forEach((search) => search.controller.abort());
37249
+ searches.clear();
37250
+ };
37251
+ }, [trajectEntry]);
37252
+ React.useEffect(() => {
37253
+ if (!context || !trajectEntry || !editablePackagingEntry)
37254
+ return;
37255
+ const bookedMode = isBookedEntryTraject(trajectEntry);
37256
+ const searches = transferSearches.current;
37257
+ const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
37258
+ const lines = editablePackagingEntry.lines ?? [];
37259
+ const config = { host: context.tideConnection.host, apiKey: context.tideConnection.apiKey };
37260
+ const searchContext = getSearchContext(context, trajectEntry);
37261
+ const cancel = (nodeId) => {
37262
+ const search = searches.get(nodeId);
37263
+ if (!search)
37264
+ return;
37265
+ search.controller.abort();
37266
+ searches.delete(nodeId);
37267
+ dispatch(setTrajectTransferAvailability({ nodeId, key: search.key, result: null }));
37268
+ };
37269
+ (trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
37270
+ const endpoints = dropped.has(node.nodeId) ? null : getTrajectTransferEndpoints(trajectEntry, lines, node);
37271
+ if (!endpoints || (bookedMode && endpoints.asDrawn))
37272
+ return cancel(node.nodeId);
37273
+ if (searches.get(node.nodeId)?.key === endpoints.key || trajectTransferAvailability[node.nodeId]?.key === endpoints.key)
37274
+ return;
37275
+ cancel(node.nodeId);
37276
+ const controller = new AbortController();
37277
+ const { key } = endpoints;
37278
+ searches.set(node.nodeId, { key, controller });
37279
+ dispatch(setTrajectTransferLoading({ nodeId: node.nodeId, key }));
37280
+ build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, endpoints, searchContext), controller.signal)
37281
+ .then((results) => {
37282
+ if (controller.signal.aborted)
37283
+ return;
37284
+ const result = results?.[0] ?? null;
37285
+ dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, key, result: result && buildTrajectTransferLine(node, result) ? result : null }));
37286
+ })
37287
+ .catch((error) => {
37288
+ if (controller.signal.aborted)
37289
+ return;
37290
+ console.error('Failed to resolve traject transfer', node.name, error);
37291
+ dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, key, result: null }));
37292
+ })
37293
+ .finally(() => {
37294
+ if (searches.get(node.nodeId)?.controller === controller)
37295
+ searches.delete(node.nodeId);
37296
+ });
37297
+ });
37298
+ }, [trajectEntry, editablePackagingEntry, trajectTransferAvailability]);
37086
37299
  React.useEffect(() => {
37087
- if (!trajectEntry || !editablePackagingEntry || isBookedEntryTraject(trajectEntry))
37300
+ if (!trajectEntry || !editablePackagingEntry)
37088
37301
  return;
37302
+ const bookedMode = isBookedEntryTraject(trajectEntry);
37089
37303
  const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
37090
- const guids = new Set((editablePackagingEntry.lines ?? []).map((line) => line.guid));
37304
+ const lines = editablePackagingEntry.lines ?? [];
37305
+ const guids = new Set(lines.map((line) => line.guid));
37091
37306
  const toAdd = [];
37092
37307
  const toRemove = [];
37093
37308
  (trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
37094
- const result = trajectTransferAvailability[node.nodeId]?.result;
37095
- const line = result ? buildTrajectTransferLine(node, result) : null;
37096
- if (!line)
37097
- return;
37098
- if (dropped.has(node.nodeId) && guids.has(line.guid))
37099
- toRemove.push(line.guid);
37100
- if (!dropped.has(node.nodeId) && !guids.has(line.guid))
37101
- toAdd.push(line);
37309
+ const endpoints = dropped.has(node.nodeId) ? null : getTrajectTransferEndpoints(trajectEntry, lines, node);
37310
+ let wanted = [];
37311
+ if (endpoints && bookedMode && endpoints.asDrawn) {
37312
+ wanted = getTrajectTransferLines(node, trajectEntry.entry.lines ?? []);
37313
+ }
37314
+ else if (endpoints) {
37315
+ const availability = trajectTransferAvailability[node.nodeId];
37316
+ const line = availability?.key === endpoints.key && availability.result ? buildTrajectTransferLine(node, availability.result) : null;
37317
+ if (line)
37318
+ wanted = [line];
37319
+ }
37320
+ const wantedGuids = new Set(wanted.map((line) => line.guid));
37321
+ getTrajectTransferLines(node, lines)
37322
+ .filter((line) => !wantedGuids.has(line.guid))
37323
+ .forEach((line) => toRemove.push(line.guid));
37324
+ wanted.filter((line) => !guids.has(line.guid)).forEach((line) => toAdd.push(line));
37102
37325
  });
37103
37326
  if (toRemove.length)
37104
37327
  dispatch(removeEditableEntryLines(toRemove));
@@ -37112,7 +37335,8 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37112
37335
  const dispatch = reactRedux.useDispatch();
37113
37336
  const context = React.useContext(SearchResultsConfigurationContext);
37114
37337
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
37115
- 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);
37338
+ 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, priceDetailsRequest, trajectTransferAvailability } = reactRedux.useSelector((state) => state.searchResults);
37339
+ const trajectBusy = trajectResolving || Object.values(trajectTransferAvailability).some((transfer) => transfer.status === 'loading');
37116
37340
  const isMobile = useMediaQuery('(max-width: 1200px)');
37117
37341
  // Resolves live availability per traject node; no-op unless a traject entry is configured.
37118
37342
  useTrajectAvailability();
@@ -37124,7 +37348,6 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37124
37348
  const [detailsIsLoading, setDetailsIsLoading] = React.useState(false);
37125
37349
  const [pricesAreLoading, setPricesAreLoading] = React.useState(false);
37126
37350
  const [itineraryIsLoading, setItineraryIsLoading] = React.useState(false);
37127
- const [entrySaving, setEntrySaving] = React.useState(false);
37128
37351
  const [itineraryOpen, setItineraryOpen] = React.useState(false);
37129
37352
  const [isBookingConfirmation, setIsBookingConfirmation] = React.useState(false);
37130
37353
  const [selectedAccommodationSeed, setSelectedAccommodationSeed] = React.useState(null);
@@ -37882,40 +38105,16 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37882
38105
  React.useEffect(() => {
37883
38106
  setInitialFiltersSet(false);
37884
38107
  }, [activeSearchSeed]);
37885
- const handleSaveEntry = async () => {
37886
- if (!context || !bookableEntry || entrySaving)
37887
- return;
37888
- setEntrySaving(true);
37889
- try {
37890
- const config = {
37891
- host: context.tideConnection.host,
37892
- apiKey: context.tideConnection.apiKey
37893
- };
37894
- const request = {
37895
- language: context.languageCode ?? 'en-GB',
37896
- officeId: context.tideConnection.officeId,
37897
- catalogueId: context.searchConfiguration.defaultCatalogueId ?? lodash.first(context?.tideConnection?.catalogueIds) ?? 1,
37898
- agentId: context.agentId,
37899
- payload: bookableEntry
37900
- };
37901
- await build.savePackagingEntry(config, request);
37902
- if (typeof window !== 'undefined')
37903
- window.location.reload();
37904
- }
37905
- catch (err) {
37906
- console.error('Failed to save the entry', err);
37907
- setEntrySaving(false);
37908
- }
37909
- };
37910
38108
  React.useEffect(() => {
37911
38109
  const fetchPriceDetails = async () => {
37912
38110
  if (!context || !editablePackagingEntry || lodash.isEmpty(editablePackagingEntry.lines))
37913
38111
  return;
37914
38112
  // A traject resolves one node at a time. Pricing each step would show a total that climbs
37915
38113
  // as the trip fills in, so the whole trip is priced once, when the last node is in.
37916
- if (context.trajectEntry && trajectResolving)
38114
+ if (context.trajectEntry && trajectBusy)
37917
38115
  return;
37918
38116
  setPricesAreLoading(true);
38117
+ dispatch(setPriceDetailsFailed(false));
37919
38118
  try {
37920
38119
  const config = {
37921
38120
  host: context.tideConnection.host,
@@ -37934,18 +38133,21 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37934
38133
  }
37935
38134
  catch (err) {
37936
38135
  console.error('Error fetching price details', err);
38136
+ // A price from before the change would be wrong for the entry as it is now.
38137
+ dispatch(setPriceDetails(null));
38138
+ dispatch(setPriceDetailsFailed(true));
37937
38139
  setPricesAreLoading(false);
37938
38140
  }
37939
38141
  };
37940
38142
  fetchPriceDetails();
37941
- }, [editablePackagingEntry, bookableEntry, trajectResolving]);
38143
+ }, [editablePackagingEntry, bookableEntry, trajectBusy, priceDetailsRequest]);
37942
38144
  React.useEffect(() => {
37943
38145
  const fetchItinerary = async () => {
37944
38146
  if (!context || !context.packagingEntry || !editablePackagingEntry || lodash.isEmpty(editablePackagingEntry.lines))
37945
38147
  return;
37946
38148
  // While a traject is being composed its own day-by-day column is the itinerary; the generated
37947
38149
  // one is what the traveller reads the finished trip back from, so it waits until then.
37948
- if (context.trajectEntry && (trajectResolving || !trajectItineraryView))
38150
+ if (context.trajectEntry && (trajectBusy || !trajectItineraryView))
37949
38151
  return;
37950
38152
  setItineraryIsLoading(true);
37951
38153
  try {
@@ -37970,7 +38172,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37970
38172
  }
37971
38173
  };
37972
38174
  fetchItinerary();
37973
- }, [editablePackagingEntry, bookableEntry, trajectResolving, trajectItineraryView]);
38175
+ }, [editablePackagingEntry, bookableEntry, trajectBusy, trajectItineraryView]);
37974
38176
  // Flight selection
37975
38177
  // const [selectedOutwardKey, setSelectedOutwardKey] = useState<string | null>(null);
37976
38178
  // const [selectedReturnKey, setSelectedReturnKey] = useState<string | null>(null);
@@ -38348,7 +38550,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
38348
38550
  context.searchConfiguration.qsmType != build.PortalQsmType.AccommodationAndFlight && context.showFilters && (React__default["default"].createElement(Filters, { initialFilters: initialFilters, filters: filters, isOpen: filtersOpen, handleSetIsOpen: () => setFiltersOpen(!filtersOpen),
38349
38551
  // handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
38350
38552
  isLoading: isLoading, setFilters: (filters) => dispatch(setFilters(filters)), resetFilters: (filters) => dispatch(resetFilters(filters)) })),
38351
- context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && (React__default["default"].createElement(Itinerary, { isOpen: itineraryOpen, handleSetIsOpen: () => setItineraryOpen(!itineraryOpen), isLoading: isLoading || pricesAreLoading || flightsLoading || itineraryIsLoading || trajectResolving || entrySaving, onEditAccommodation: handleEditAccommodation, onSaveEntry: handleSaveEntry })),
38553
+ context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && (React__default["default"].createElement(Itinerary, { isOpen: itineraryOpen, handleSetIsOpen: () => setItineraryOpen(!itineraryOpen), isLoading: isLoading || pricesAreLoading || flightsLoading || itineraryIsLoading || trajectBusy, onEditAccommodation: handleEditAccommodation })),
38352
38554
  React__default["default"].createElement("div", { className: "search__results" },
38353
38555
  isMobile && (React__default["default"].createElement("div", { className: "search__result-row" },
38354
38556
  React__default["default"].createElement("div", { className: "search__results__actions" },