@qite/tide-components 1.0.14-preview.4 → 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,222 +30392,227 @@ 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.
30695
30606
  */
30696
30607
  const isBookedEntryTraject = (trajectEntry) => !!trajectEntry?.entry?.magicLink;
30697
30608
  const NODE_SERVICE_TYPES = new Set([ACCOMMODATION_SERVICE_TYPE, EXCURSION_SERVICE_TYPE, FLIGHT_SERVICE_TYPE]);
30609
+ const isBookedRoomLineOf = (line, root) => !!line.parentGuid &&
30610
+ (line.parentGuid === root.guid || line.parentGuid === root.parentGuid) &&
30611
+ line.serviceType === root.serviceType &&
30612
+ line.productCode === root.productCode &&
30613
+ dateFns.isSameDay(new Date(line.from), new Date(root.from));
30698
30614
  /**
30699
- * A room or option line joins its parent's node; a flight line never does — an outward and its
30615
+ * A room or option line joins its service's node; a flight line never does — an outward and its
30700
30616
  * return are separate legs of the trip even though the return points at the outward as parent.
30701
30617
  *
30702
30618
  * Lines that share a group key are one choice (a couple of hotels of which one is picked), so
@@ -30704,16 +30620,13 @@ const NODE_SERVICE_TYPES = new Set([ACCOMMODATION_SERVICE_TYPE, EXCURSION_SERVIC
30704
30620
  */
30705
30621
  const groupNodeLines = (lines) => {
30706
30622
  const groups = [];
30707
- const groupByGuid = new Map();
30708
30623
  lines.forEach((line) => {
30709
- const parentGroup = line.serviceType !== FLIGHT_SERVICE_TYPE && line.parentGuid ? groupByGuid.get(line.parentGuid) : undefined;
30710
- if (parentGroup) {
30711
- parentGroup.push(line);
30624
+ const service = line.serviceType !== FLIGHT_SERVICE_TYPE ? groups.find((group) => isBookedRoomLineOf(line, group[0])) : undefined;
30625
+ if (service) {
30626
+ service.push(line);
30712
30627
  return;
30713
30628
  }
30714
- const group = [line];
30715
- groups.push(group);
30716
- groupByGuid.set(line.guid, group);
30629
+ groups.push([line]);
30717
30630
  });
30718
30631
  const nodes = [];
30719
30632
  const nodeByGroupKey = new Map();
@@ -30763,19 +30676,24 @@ const buildBookedTrajectEntry = (entry) => {
30763
30676
  const tos = lines.map((line) => new Date(line.to).getTime());
30764
30677
  const start = froms.length ? new Date(Math.min(...froms)) : new Date();
30765
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.
30766
30681
  const dayCount = Math.max(dateFns.differenceInCalendarDays(end, start), 0) + 1;
30767
30682
  const days = Array.from({ length: dayCount }, (_, order) => ({
30768
30683
  order,
30769
30684
  name: '',
30770
- date: toDateOnlyString(dateFns.addDays(start, order))
30685
+ date: dateFns.format(dateFns.addDays(start, order), "yyyy-MM-dd'T00:00:00'")
30771
30686
  }));
30772
- const dayOrderOf = (line) => Math.max(dateFns.differenceInCalendarDays(new Date(line.from), start), 0);
30773
- // Services starting the same day are laid out by where they end: the one that ends soonest opens
30774
- // the day (an outward flight on 2 Oct) and the one that spans further follows (the stay until 9 Oct).
30775
- const orderedGroups = groupNodeLines(lines).sort((a, b) => {
30776
- const [lineA, lineB] = [a.lines[0], b.lines[0]];
30777
- return dayOrderOf(lineA) - dayOrderOf(lineB) || new Date(lineA.to).getTime() - new Date(lineB.to).getTime() || lineA.order - lineB.order;
30778
- });
30687
+ const dayOrderOf = (item) => Math.min(Math.max(dateFns.differenceInCalendarDays(new Date(item.from), start), 0), dayCount - 1);
30688
+ const flights = lodash.sortBy(lines.filter((line) => line.serviceType === FLIGHT_SERVICE_TYPE && isActiveEntryLine(line)), (line) => new Date(line.from).getTime(), (line) => line.order);
30689
+ const outward = lodash.first(flights);
30690
+ const homeward = flights.length > 1 ? lodash.last(flights) : undefined;
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));
30779
30697
  const nodes = orderedGroups.map(({ lines: groupLines, alternatives }, index) => {
30780
30698
  const line = groupLines[0];
30781
30699
  const flightLines = line.flightInformation?.flightLines ?? [];
@@ -30785,9 +30703,9 @@ const buildBookedTrajectEntry = (entry) => {
30785
30703
  return {
30786
30704
  lineGuid: line.guid,
30787
30705
  lineGuids: groupLines.map((x) => x.guid),
30788
- nodeId: index + 1,
30706
+ nodeId: idBase + index + 1,
30789
30707
  dayOrder: dayOrderOf(line),
30790
- order: index,
30708
+ order: orderOf(line),
30791
30709
  name: (alternatives.length && getLineGroupKey(line)) || line.productName,
30792
30710
  serviceType: line.serviceType,
30793
30711
  trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
@@ -30805,6 +30723,23 @@ const buildBookedTrajectEntry = (entry) => {
30805
30723
  to: line.to
30806
30724
  };
30807
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
+ });
30808
30743
  return {
30809
30744
  entry,
30810
30745
  traject: {
@@ -30926,6 +30861,222 @@ const buildBookedFlightResult = (line) => {
30926
30861
  };
30927
30862
  };
30928
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
+
30929
31080
  const getSegmentIcon = (segment) => {
30930
31081
  switch (segment.serviceType) {
30931
31082
  case 3:
@@ -30965,10 +31116,10 @@ const SERVICE_TYPE_PRIORITY = {
30965
31116
  const getServiceTypePriority = (serviceType) => {
30966
31117
  return SERVICE_TYPE_PRIORITY[serviceType ?? -1] ?? 2;
30967
31118
  };
30968
- const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, onSaveEntry }) => {
31119
+ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation }) => {
30969
31120
  const context = React.useContext(SearchResultsConfigurationContext);
30970
31121
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
30971
- const { editablePackagingEntry, priceDetails, trajectItineraryView } = reactRedux.useSelector((state) => state.searchResults);
31122
+ const { editablePackagingEntry, priceDetails, priceDetailsFailed, trajectItineraryView } = reactRedux.useSelector((state) => state.searchResults);
30972
31123
  const dispatch = reactRedux.useDispatch();
30973
31124
  const packagingEntry = editablePackagingEntry ?? context?.packagingEntry;
30974
31125
  const sortedLines = React.useMemo(() => {
@@ -31034,7 +31185,9 @@ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, on
31034
31185
  packagingEntry.dossierNumber)),
31035
31186
  React__default["default"].createElement("h4", { className: "search__filter__image__title" }, (location || '') + (location && country ? ' - ' : '') + (country || ''))))),
31036
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,
31037
- 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" },
31038
31191
  React__default["default"].createElement("h3", { className: "search__filter__prices--amount" }, formatPrice$3(pricePerPerson, 'EUR')),
31039
31192
  React__default["default"].createElement("p", null, translations.SRP.PACKAGE_PRICE_PER_PERSON),
31040
31193
  React__default["default"].createElement("p", null,
@@ -31043,8 +31196,8 @@ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, on
31043
31196
  formatPrice$3(totalPrice, 'EUR'),
31044
31197
  " ",
31045
31198
  translations.SRP.TOTAL,
31046
- ")"))),
31047
- 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)),
31048
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))))),
31049
31202
  React__default["default"].createElement("div", { className: "search__filter__itinerary" },
31050
31203
  React__default["default"].createElement("p", null, translations.SRP.DAY_BY_DAY),
@@ -36343,10 +36496,12 @@ const travellersSettings = {
36343
36496
  const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage }) => {
36344
36497
  const context = React.useContext(SearchResultsConfigurationContext);
36345
36498
  const dispatch = reactRedux.useDispatch();
36346
- 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);
36347
36502
  const [countries, setCountries] = React.useState([]);
36348
36503
  const [userValidated, setUserValidated] = React.useState(true);
36349
- const [remarks, setRemarks] = React.useState('');
36504
+ const [remarks, setRemarks] = React.useState(() => editablePackagingEntry?.remarks ?? '');
36350
36505
  const [isSubmitting, setIsSubmitting] = React.useState(false);
36351
36506
  const [selectedPackagingAccoResult, setSelectedPackagingAccoResult] = React.useState(null);
36352
36507
  const translations = React.useMemo(() => getTranslations(context?.languageCode ?? 'en-GB'), [context?.languageCode]);
@@ -36406,7 +36561,27 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36406
36561
  }, [selectedPackagingAccoResultCode, packagingAccoResults]);
36407
36562
  // What the page holds versus what the backend can look up; they differ for a traject.
36408
36563
  const bookableEntry = useBookableEntry();
36409
- 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))
36410
36585
  return React__default["default"].createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING });
36411
36586
  const handleSummarySubmit = async (e) => {
36412
36587
  e.preventDefault();
@@ -36416,7 +36591,7 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36416
36591
  }
36417
36592
  // Held apart from the entry itself so they can be put on both copies of it.
36418
36593
  let bookingFields = { remarks };
36419
- if (context.generatePaymentUrl && typeof window !== 'undefined') {
36594
+ if (!bookedMode && context.generatePaymentUrl && typeof window !== 'undefined') {
36420
36595
  const redirectUrl = new URL(window.location.href);
36421
36596
  redirectUrl.searchParams.set('bookingConfirmation', 'true');
36422
36597
  redirectUrl.searchParams.set('link', '');
@@ -36433,6 +36608,13 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36433
36608
  // The page keeps the entry it works with; the backend gets the one it can look up.
36434
36609
  payload: { ...(bookableEntry ?? editablePackagingEntry), ...bookingFields }
36435
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
+ }
36436
36618
  const bookingResponse = await build.bookPackagingEntry(config, request);
36437
36619
  dispatch(setBookingNumber(bookingResponse.number));
36438
36620
  if (bookingResponse.paymentUrl) {
@@ -36455,8 +36637,8 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36455
36637
  ".\u00A0",
36456
36638
  stepLabels[step])) },
36457
36639
  isConfirmationPage && isLoading && React__default["default"].createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING }),
36458
- 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 })),
36459
- 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)) })),
36460
36642
  currentStep === 2 && (React__default["default"].createElement(SharedConfirmation, { bookingNumber: bookingNumber ?? editablePackagingEntry?.dossierNumber ?? '', isOption: false, isOffer: false, translations: translations.CONFIRMATION })),
36461
36643
  currentStep === 3 && React__default["default"].createElement("div", null))),
36462
36644
  React__default["default"].createElement("div", { className: "backdrop", id: "backdrop" }),
@@ -36877,6 +37059,16 @@ const TrajectResults = ({ isLoading }) => {
36877
37059
  })));
36878
37060
  };
36879
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
+ });
36880
37072
  const useTrajectAvailability = () => {
36881
37073
  const context = React.useContext(SearchResultsConfigurationContext);
36882
37074
  const dispatch = reactRedux.useDispatch();
@@ -36895,16 +37087,7 @@ const useTrajectAvailability = () => {
36895
37087
  host: context.tideConnection.host,
36896
37088
  apiKey: context.tideConnection.apiKey
36897
37089
  };
36898
- const searchContext = {
36899
- transactionId: trajectEntry.entry.transactionId,
36900
- officeId: context.tideConnection.officeId ?? 1,
36901
- catalogueId: context.searchConfiguration.defaultCatalogueId ?? lodash.first(context.tideConnection.catalogueIds) ?? 1,
36902
- searchConfigurationId: context.searchConfiguration.id,
36903
- portalId: context.portalId ?? null,
36904
- agentId: context.agentId ?? null,
36905
- language: context.languageCode ?? 'en-GB',
36906
- rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
36907
- };
37090
+ const searchContext = getSearchContext(context, trajectEntry);
36908
37091
  // Flight searches take passengers rather than rooms; the API buckets by age.
36909
37092
  // The ids must be the entry's own pax ids: the flight lines this search caches carry them as
36910
37093
  // EntryLinePax.PaxId, and booking maps those onto the entry's travelers (PackagingService.SyncPaxIds).
@@ -37044,30 +37227,9 @@ const useTrajectAvailability = () => {
37044
37227
  return item.lines.map((x) => x.guid);
37045
37228
  }
37046
37229
  };
37047
- const resolveTransferNode = async (node) => {
37048
- dispatch(setTrajectTransferLoading(node.nodeId));
37049
- try {
37050
- const results = (await build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, searchContext), controller.signal)) ?? [];
37051
- if (controller.signal.aborted)
37052
- return [];
37053
- const result = results[0] ?? null;
37054
- dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: result && buildTrajectTransferLine(node, result) ? result : null }));
37055
- }
37056
- catch (error) {
37057
- if (controller.signal.aborted)
37058
- return [];
37059
- console.error('Failed to resolve traject transfer', node.name, error);
37060
- dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: null }));
37061
- }
37062
- return [];
37063
- };
37064
37230
  (async () => {
37065
37231
  const resolvable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node));
37066
- const transfers = bookedMode ? [] : (trajectEntry.nodes ?? []).filter(isTrajectTransferNode);
37067
- const unresolved = (await Promise.all([
37068
- ...resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))),
37069
- ...transfers.map(resolveTransferNode)
37070
- ])).flat();
37232
+ const unresolved = (await Promise.all(resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))))).flat();
37071
37233
  if (controller.signal.aborted)
37072
37234
  return;
37073
37235
  dispatch(setTrajectResolving(false));
@@ -37079,22 +37241,87 @@ const useTrajectAvailability = () => {
37079
37241
  })();
37080
37242
  return () => controller.abort();
37081
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]);
37082
37299
  React.useEffect(() => {
37083
- if (!trajectEntry || !editablePackagingEntry || isBookedEntryTraject(trajectEntry))
37300
+ if (!trajectEntry || !editablePackagingEntry)
37084
37301
  return;
37302
+ const bookedMode = isBookedEntryTraject(trajectEntry);
37085
37303
  const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
37086
- 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));
37087
37306
  const toAdd = [];
37088
37307
  const toRemove = [];
37089
37308
  (trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
37090
- const result = trajectTransferAvailability[node.nodeId]?.result;
37091
- const line = result ? buildTrajectTransferLine(node, result) : null;
37092
- if (!line)
37093
- return;
37094
- if (dropped.has(node.nodeId) && guids.has(line.guid))
37095
- toRemove.push(line.guid);
37096
- if (!dropped.has(node.nodeId) && !guids.has(line.guid))
37097
- 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));
37098
37325
  });
37099
37326
  if (toRemove.length)
37100
37327
  dispatch(removeEditableEntryLines(toRemove));
@@ -37108,7 +37335,8 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37108
37335
  const dispatch = reactRedux.useDispatch();
37109
37336
  const context = React.useContext(SearchResultsConfigurationContext);
37110
37337
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
37111
- 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');
37112
37340
  const isMobile = useMediaQuery('(max-width: 1200px)');
37113
37341
  // Resolves live availability per traject node; no-op unless a traject entry is configured.
37114
37342
  useTrajectAvailability();
@@ -37120,7 +37348,6 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37120
37348
  const [detailsIsLoading, setDetailsIsLoading] = React.useState(false);
37121
37349
  const [pricesAreLoading, setPricesAreLoading] = React.useState(false);
37122
37350
  const [itineraryIsLoading, setItineraryIsLoading] = React.useState(false);
37123
- const [entrySaving, setEntrySaving] = React.useState(false);
37124
37351
  const [itineraryOpen, setItineraryOpen] = React.useState(false);
37125
37352
  const [isBookingConfirmation, setIsBookingConfirmation] = React.useState(false);
37126
37353
  const [selectedAccommodationSeed, setSelectedAccommodationSeed] = React.useState(null);
@@ -37878,40 +38105,16 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37878
38105
  React.useEffect(() => {
37879
38106
  setInitialFiltersSet(false);
37880
38107
  }, [activeSearchSeed]);
37881
- const handleSaveEntry = async () => {
37882
- if (!context || !bookableEntry || entrySaving)
37883
- return;
37884
- setEntrySaving(true);
37885
- try {
37886
- const config = {
37887
- host: context.tideConnection.host,
37888
- apiKey: context.tideConnection.apiKey
37889
- };
37890
- const request = {
37891
- language: context.languageCode ?? 'en-GB',
37892
- officeId: context.tideConnection.officeId,
37893
- catalogueId: context.searchConfiguration.defaultCatalogueId ?? lodash.first(context?.tideConnection?.catalogueIds) ?? 1,
37894
- agentId: context.agentId,
37895
- payload: bookableEntry
37896
- };
37897
- await build.savePackagingEntry(config, request);
37898
- if (typeof window !== 'undefined')
37899
- window.location.reload();
37900
- }
37901
- catch (err) {
37902
- console.error('Failed to save the entry', err);
37903
- setEntrySaving(false);
37904
- }
37905
- };
37906
38108
  React.useEffect(() => {
37907
38109
  const fetchPriceDetails = async () => {
37908
38110
  if (!context || !editablePackagingEntry || lodash.isEmpty(editablePackagingEntry.lines))
37909
38111
  return;
37910
38112
  // A traject resolves one node at a time. Pricing each step would show a total that climbs
37911
38113
  // as the trip fills in, so the whole trip is priced once, when the last node is in.
37912
- if (context.trajectEntry && trajectResolving)
38114
+ if (context.trajectEntry && trajectBusy)
37913
38115
  return;
37914
38116
  setPricesAreLoading(true);
38117
+ dispatch(setPriceDetailsFailed(false));
37915
38118
  try {
37916
38119
  const config = {
37917
38120
  host: context.tideConnection.host,
@@ -37930,18 +38133,21 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37930
38133
  }
37931
38134
  catch (err) {
37932
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));
37933
38139
  setPricesAreLoading(false);
37934
38140
  }
37935
38141
  };
37936
38142
  fetchPriceDetails();
37937
- }, [editablePackagingEntry, bookableEntry, trajectResolving]);
38143
+ }, [editablePackagingEntry, bookableEntry, trajectBusy, priceDetailsRequest]);
37938
38144
  React.useEffect(() => {
37939
38145
  const fetchItinerary = async () => {
37940
38146
  if (!context || !context.packagingEntry || !editablePackagingEntry || lodash.isEmpty(editablePackagingEntry.lines))
37941
38147
  return;
37942
38148
  // While a traject is being composed its own day-by-day column is the itinerary; the generated
37943
38149
  // one is what the traveller reads the finished trip back from, so it waits until then.
37944
- if (context.trajectEntry && (trajectResolving || !trajectItineraryView))
38150
+ if (context.trajectEntry && (trajectBusy || !trajectItineraryView))
37945
38151
  return;
37946
38152
  setItineraryIsLoading(true);
37947
38153
  try {
@@ -37966,7 +38172,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37966
38172
  }
37967
38173
  };
37968
38174
  fetchItinerary();
37969
- }, [editablePackagingEntry, bookableEntry, trajectResolving, trajectItineraryView]);
38175
+ }, [editablePackagingEntry, bookableEntry, trajectBusy, trajectItineraryView]);
37970
38176
  // Flight selection
37971
38177
  // const [selectedOutwardKey, setSelectedOutwardKey] = useState<string | null>(null);
37972
38178
  // const [selectedReturnKey, setSelectedReturnKey] = useState<string | null>(null);
@@ -38344,7 +38550,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
38344
38550
  context.searchConfiguration.qsmType != build.PortalQsmType.AccommodationAndFlight && context.showFilters && (React__default["default"].createElement(Filters, { initialFilters: initialFilters, filters: filters, isOpen: filtersOpen, handleSetIsOpen: () => setFiltersOpen(!filtersOpen),
38345
38551
  // handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
38346
38552
  isLoading: isLoading, setFilters: (filters) => dispatch(setFilters(filters)), resetFilters: (filters) => dispatch(resetFilters(filters)) })),
38347
- 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 })),
38348
38554
  React__default["default"].createElement("div", { className: "search__results" },
38349
38555
  isMobile && (React__default["default"].createElement("div", { className: "search__result-row" },
38350
38556
  React__default["default"].createElement("div", { className: "search__results__actions" },