@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.
@@ -461,7 +461,10 @@ var SRP$f = {
461
461
  LOADING_ACCOMMODATIONS: "جارٍ تحميل الإقامات...",
462
462
  LOADING_ITINERARY: "جارٍ تحميل خط سير الرحلة...",
463
463
  LOADING_OPTIONS: "جارٍ تحميل الخيارات...",
464
- BOOK_AND_PAY: "احجز وادفع"
464
+ BOOK_AND_PAY: "احجز وادفع",
465
+ PRICE_DETAILS_FAILED: "تعذر حساب السعر.",
466
+ TRY_AGAIN: "حاول مرة أخرى",
467
+ BACK_TO_SERVICES: "العودة إلى الخدمات"
465
468
  };
466
469
  var ITINERARY$f = {
467
470
  DAY: "اليوم",
@@ -968,7 +971,10 @@ var SRP$e = {
968
971
  LOADING_ACCOMMODATIONS: "Indlæser indkvarteringer...",
969
972
  LOADING_ITINERARY: "Indlæser rejseplan...",
970
973
  LOADING_OPTIONS: "Indlæser muligheder...",
971
- BOOK_AND_PAY: "Book og betal"
974
+ BOOK_AND_PAY: "Book og betal",
975
+ PRICE_DETAILS_FAILED: "Prisen kunne ikke beregnes.",
976
+ TRY_AGAIN: "Prøv igen",
977
+ BACK_TO_SERVICES: "Tilbage til ydelser"
972
978
  };
973
979
  var ITINERARY$e = {
974
980
  DAY: "Dag",
@@ -1475,7 +1481,10 @@ var SRP$d = {
1475
1481
  LOADING_ACCOMMODATIONS: "Lade Unterkünfte...",
1476
1482
  LOADING_ITINERARY: "Lade Reiseverlauf...",
1477
1483
  LOADING_OPTIONS: "Lade Optionen...",
1478
- BOOK_AND_PAY: "Buchen und bezahlen"
1484
+ BOOK_AND_PAY: "Buchen und bezahlen",
1485
+ PRICE_DETAILS_FAILED: "Der Preis konnte nicht berechnet werden.",
1486
+ TRY_AGAIN: "Erneut versuchen",
1487
+ BACK_TO_SERVICES: "Zurück zu den Leistungen"
1479
1488
  };
1480
1489
  var ITINERARY$d = {
1481
1490
  DAY: "Tag",
@@ -1986,7 +1995,10 @@ var SRP$c = {
1986
1995
  LOADING_ACCOMMODATIONS: "Loading accommodations...",
1987
1996
  LOADING_ITINERARY: "Loading itinerary...",
1988
1997
  LOADING_OPTIONS: "Loading options...",
1989
- BOOK_AND_PAY: "Book and pay"
1998
+ BOOK_AND_PAY: "Book and pay",
1999
+ PRICE_DETAILS_FAILED: "The price could not be calculated.",
2000
+ TRY_AGAIN: "Try again",
2001
+ BACK_TO_SERVICES: "Back to services"
1990
2002
  };
1991
2003
  var ITINERARY$c = {
1992
2004
  DAY: "Day",
@@ -2493,7 +2505,10 @@ var SRP$b = {
2493
2505
  LOADING_ACCOMMODATIONS: "Cargando alojamientos...",
2494
2506
  LOADING_ITINERARY: "Cargando itinerario...",
2495
2507
  LOADING_OPTIONS: "Cargando opciones...",
2496
- BOOK_AND_PAY: "Reservar y pagar"
2508
+ BOOK_AND_PAY: "Reservar y pagar",
2509
+ PRICE_DETAILS_FAILED: "No se ha podido calcular el precio.",
2510
+ TRY_AGAIN: "Volver a intentarlo",
2511
+ BACK_TO_SERVICES: "Volver a los servicios"
2497
2512
  };
2498
2513
  var ITINERARY$b = {
2499
2514
  DAY: "Día",
@@ -3004,7 +3019,10 @@ var SRP$a = {
3004
3019
  LOADING_ACCOMMODATIONS: "Chargement des hébergements...",
3005
3020
  LOADING_ITINERARY: "Chargement de l’itinéraire...",
3006
3021
  LOADING_OPTIONS: "Chargement des options...",
3007
- BOOK_AND_PAY: "Réserver et payer"
3022
+ BOOK_AND_PAY: "Réserver et payer",
3023
+ PRICE_DETAILS_FAILED: "Le prix n’a pas pu être calculé.",
3024
+ TRY_AGAIN: "Réessayer",
3025
+ BACK_TO_SERVICES: "Retour aux services"
3008
3026
  };
3009
3027
  var ITINERARY$a = {
3010
3028
  DAY: "Jour",
@@ -3511,7 +3529,10 @@ var SRP$9 = {
3511
3529
  LOADING_ACCOMMODATIONS: "Chargement des hébergements...",
3512
3530
  LOADING_ITINERARY: "Chargement de l’itinéraire...",
3513
3531
  LOADING_OPTIONS: "Chargement des options...",
3514
- BOOK_AND_PAY: "Réserver et payer"
3532
+ BOOK_AND_PAY: "Réserver et payer",
3533
+ PRICE_DETAILS_FAILED: "Le prix n’a pas pu être calculé.",
3534
+ TRY_AGAIN: "Réessayer",
3535
+ BACK_TO_SERVICES: "Retour aux services"
3515
3536
  };
3516
3537
  var ITINERARY$9 = {
3517
3538
  DAY: "Jour",
@@ -4018,7 +4039,10 @@ var SRP$8 = {
4018
4039
  LOADING_ACCOMMODATIONS: "Hleðst inn í gistingar...",
4019
4040
  LOADING_ITINERARY: "Hleðst inn í ferðaráætlun...",
4020
4041
  LOADING_OPTIONS: "Hleðst inn í valkosti...",
4021
- BOOK_AND_PAY: "Panta og greiða"
4042
+ BOOK_AND_PAY: "Panta og greiða",
4043
+ PRICE_DETAILS_FAILED: "Ekki tókst að reikna verðið.",
4044
+ TRY_AGAIN: "Reyna aftur",
4045
+ BACK_TO_SERVICES: "Aftur í þjónustu"
4022
4046
  };
4023
4047
  var ITINERARY$8 = {
4024
4048
  DAY: "Dagur",
@@ -4525,7 +4549,10 @@ var SRP$7 = {
4525
4549
  LOADING_ACCOMMODATIONS: "Caricamento degli alloggi...",
4526
4550
  LOADING_ITINERARY: "Caricamento dell'itinerario...",
4527
4551
  LOADING_OPTIONS: "Caricamento delle opzioni...",
4528
- BOOK_AND_PAY: "Prenota e paga"
4552
+ BOOK_AND_PAY: "Prenota e paga",
4553
+ PRICE_DETAILS_FAILED: "Non è stato possibile calcolare il prezzo.",
4554
+ TRY_AGAIN: "Riprova",
4555
+ BACK_TO_SERVICES: "Torna ai servizi"
4529
4556
  };
4530
4557
  var ITINERARY$7 = {
4531
4558
  DAY: "Giorno",
@@ -5036,7 +5063,10 @@ var SRP$6 = {
5036
5063
  LOADING_ACCOMMODATIONS: "Accommodaties laden...",
5037
5064
  LOADING_ITINERARY: "Reisplan laden...",
5038
5065
  LOADING_OPTIONS: "Opties laden...",
5039
- BOOK_AND_PAY: "Boek en betaal"
5066
+ BOOK_AND_PAY: "Boek en betaal",
5067
+ PRICE_DETAILS_FAILED: "De prijs kon niet berekend worden.",
5068
+ TRY_AGAIN: "Opnieuw proberen",
5069
+ BACK_TO_SERVICES: "Terug naar diensten"
5040
5070
  };
5041
5071
  var ITINERARY$6 = {
5042
5072
  DAY: "Dag",
@@ -5543,7 +5573,10 @@ var SRP$5 = {
5543
5573
  LOADING_ACCOMMODATIONS: "Accommodaties laden...",
5544
5574
  LOADING_ITINERARY: "Reisplan laden...",
5545
5575
  LOADING_OPTIONS: "Opties laden...",
5546
- BOOK_AND_PAY: "Boeken en betalen"
5576
+ BOOK_AND_PAY: "Boeken en betalen",
5577
+ PRICE_DETAILS_FAILED: "De prijs kon niet worden berekend.",
5578
+ TRY_AGAIN: "Opnieuw proberen",
5579
+ BACK_TO_SERVICES: "Terug naar diensten"
5547
5580
  };
5548
5581
  var ITINERARY$5 = {
5549
5582
  DAY: "Dag",
@@ -6050,7 +6083,10 @@ var SRP$4 = {
6050
6083
  LOADING_ACCOMMODATIONS: "Laster overnattinger...",
6051
6084
  LOADING_ITINERARY: "Laster reiserute...",
6052
6085
  LOADING_OPTIONS: "Laster alternativer...",
6053
- BOOK_AND_PAY: "Bestill og betal"
6086
+ BOOK_AND_PAY: "Bestill og betal",
6087
+ PRICE_DETAILS_FAILED: "Prisen kunne ikke beregnes.",
6088
+ TRY_AGAIN: "Prøv igjen",
6089
+ BACK_TO_SERVICES: "Tilbake til tjenester"
6054
6090
  };
6055
6091
  var ITINERARY$4 = {
6056
6092
  DAY: "Dag",
@@ -6557,7 +6593,10 @@ var SRP$3 = {
6557
6593
  LOADING_ACCOMMODATIONS: "Ładowanie zakwaterowań...",
6558
6594
  LOADING_ITINERARY: "Ładowanie planu podróży...",
6559
6595
  LOADING_OPTIONS: "Ładowanie opcji...",
6560
- BOOK_AND_PAY: "Zarezerwuj i zapłać"
6596
+ BOOK_AND_PAY: "Zarezerwuj i zapłać",
6597
+ PRICE_DETAILS_FAILED: "Nie udało się obliczyć ceny.",
6598
+ TRY_AGAIN: "Spróbuj ponownie",
6599
+ BACK_TO_SERVICES: "Wróć do usług"
6561
6600
  };
6562
6601
  var ITINERARY$3 = {
6563
6602
  DAY: "Dzień",
@@ -7064,7 +7103,10 @@ var SRP$2 = {
7064
7103
  LOADING_ACCOMMODATIONS: "A carregar alojamentos...",
7065
7104
  LOADING_ITINERARY: "A carregar itinerário...",
7066
7105
  LOADING_OPTIONS: "A carregar opções...",
7067
- BOOK_AND_PAY: "Reservar e pagar"
7106
+ BOOK_AND_PAY: "Reservar e pagar",
7107
+ PRICE_DETAILS_FAILED: "Não foi possível calcular o preço.",
7108
+ TRY_AGAIN: "Tentar novamente",
7109
+ BACK_TO_SERVICES: "Voltar aos serviços"
7068
7110
  };
7069
7111
  var ITINERARY$2 = {
7070
7112
  DAY: "Dia",
@@ -7571,7 +7613,10 @@ var SRP$1 = {
7571
7613
  LOADING_ACCOMMODATIONS: "Laddar boenden...",
7572
7614
  LOADING_ITINERARY: "Laddar resplan...",
7573
7615
  LOADING_OPTIONS: "Laddar alternativ...",
7574
- BOOK_AND_PAY: "Boka och betala"
7616
+ BOOK_AND_PAY: "Boka och betala",
7617
+ PRICE_DETAILS_FAILED: "Priset kunde inte beräknas.",
7618
+ TRY_AGAIN: "Försök igen",
7619
+ BACK_TO_SERVICES: "Tillbaka till tjänster"
7575
7620
  };
7576
7621
  var ITINERARY$1 = {
7577
7622
  DAY: "Dag",
@@ -8078,7 +8123,10 @@ var SRP = {
8078
8123
  LOADING_ACCOMMODATIONS: "宿泊施設を読み込み中...",
8079
8124
  LOADING_ITINERARY: "旅程を読み込み中...",
8080
8125
  LOADING_OPTIONS: "オプションを読み込み中...",
8081
- BOOK_AND_PAY: "予約して支払う"
8126
+ BOOK_AND_PAY: "予約して支払う",
8127
+ PRICE_DETAILS_FAILED: "料金を計算できませんでした。",
8128
+ TRY_AGAIN: "再試行",
8129
+ BACK_TO_SERVICES: "サービスに戻る"
8082
8130
  };
8083
8131
  var ITINERARY = {
8084
8132
  DAY: "日",
@@ -29206,6 +29254,8 @@ const initialState$1 = {
29206
29254
  selectedFlightDetails: null,
29207
29255
  bookingPackageDetails: null,
29208
29256
  priceDetails: null,
29257
+ priceDetailsFailed: false,
29258
+ priceDetailsRequest: 0,
29209
29259
  isLoading: false,
29210
29260
  flightsLoading: false,
29211
29261
  selectedSortType: null,
@@ -29366,6 +29416,12 @@ const searchResultsSlice = createSlice({
29366
29416
  setPriceDetails(state, action) {
29367
29417
  state.priceDetails = action.payload;
29368
29418
  },
29419
+ setPriceDetailsFailed(state, action) {
29420
+ state.priceDetailsFailed = action.payload;
29421
+ },
29422
+ retryPriceDetails(state) {
29423
+ state.priceDetailsRequest += 1;
29424
+ },
29369
29425
  setItinerary(state, action) {
29370
29426
  state.itinerary = action.payload;
29371
29427
  },
@@ -29525,11 +29581,12 @@ const searchResultsSlice = createSlice({
29525
29581
  flight.selectedGuid = null;
29526
29582
  },
29527
29583
  setTrajectTransferLoading(state, action) {
29528
- state.trajectTransferAvailability[action.payload] = { nodeId: action.payload, status: 'loading', result: null };
29584
+ const { nodeId, key } = action.payload;
29585
+ state.trajectTransferAvailability[nodeId] = { nodeId, key, status: 'loading', result: null };
29529
29586
  },
29530
29587
  setTrajectTransferAvailability(state, action) {
29531
- const { nodeId, result } = action.payload;
29532
- state.trajectTransferAvailability[nodeId] = { nodeId, status: result ? 'resolved' : 'unavailable', result };
29588
+ const { nodeId, key, result } = action.payload;
29589
+ state.trajectTransferAvailability[nodeId] = { nodeId, key, status: result ? 'resolved' : 'unavailable', result };
29533
29590
  },
29534
29591
  setTrajectDaySuggestionsLoading(state, action) {
29535
29592
  const dayOrder = action.payload;
@@ -29576,7 +29633,7 @@ const searchResultsSlice = createSlice({
29576
29633
  }
29577
29634
  }
29578
29635
  });
29579
- 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;
29636
+ 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;
29580
29637
  var searchResultsReducer = searchResultsSlice.reducer;
29581
29638
 
29582
29639
  const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
@@ -29741,211 +29798,9 @@ const getPackagingRequestRoomsFromBookingRooms = (rooms) => {
29741
29798
  });
29742
29799
  };
29743
29800
 
29744
- // Administrative service lines (fees, insurances, reductions, ...) live on the entry but say
29745
- // nothing about the journey itself, so the itinerary never draws them.
29746
- const HIDDEN_ITINERARY_SERVICE_TYPES = new Set([
29747
- build.ServiceType.insurance,
29748
- build.ServiceType.extraService,
29749
- build.ServiceType.serviceFee,
29750
- build.ServiceType.gift,
29751
- build.ServiceType.reduction,
29752
- build.ServiceType.fee,
29753
- build.ServiceType.visum,
29754
- build.ServiceType.voucher,
29755
- build.ServiceType.specialServices
29756
- ]);
29757
- // An inactive member of a line group is an alternative the traveller did not pick — not travelled.
29758
- const isMeaningfulItineraryLine = (line) => {
29759
- return isActiveEntryLine(line) && !HIDDEN_ITINERARY_SERVICE_TYPES.has(line.serviceType ?? -1);
29760
- };
29761
- const getFlightLines = (flight) => {
29762
- return flight?.flightInformation?.flightLines ?? [];
29763
- };
29764
- const getDepartureTime = (flight) => {
29765
- const firstLine = first(getFlightLines(flight));
29766
- if (!firstLine?.departureTime)
29767
- return '';
29768
- return firstLine.departureTime.slice(0, 5);
29769
- };
29770
- const getArrivalTime = (flight) => {
29771
- const lastLine = last(getFlightLines(flight));
29772
- if (!lastLine?.arrivalTime)
29773
- return '';
29774
- return lastLine.arrivalTime.slice(0, 5);
29775
- };
29776
- const getDuration = (flight) => {
29777
- const lines = getFlightLines(flight);
29778
- if (!lines.length)
29779
- return '';
29780
- const totalTicks = lines.reduce((sum, line) => sum + (line.durationInTicks ?? 0), 0) || 0;
29781
- if (!totalTicks)
29782
- return '';
29783
- const seconds = totalTicks / 10_000_000;
29784
- const hours = Math.floor(seconds / 3600);
29785
- const minutes = Math.floor((seconds % 3600) / 60);
29786
- return `${hours}h ${minutes.toString().padStart(2, '0')}m`;
29787
- };
29788
- const numberOfNights = (segment) => {
29789
- return differenceInCalendarDays(new Date(segment.to), new Date(segment.from));
29790
- };
29791
- const CHARTER_SERVICE_TYPE = 10;
29792
- const CRUISE_SERVICE_TYPE = 11;
29793
- const FERRY_SERVICE_TYPE = 12;
29794
- const TRANSFER_SERVICE_TYPE$1 = 13;
29795
- const BUS_SERVICE_TYPE = 14;
29796
- const CAR_SERVICE_TYPE = 17;
29797
- const CAMPER_SERVICE_TYPE = 18;
29798
- const TRAIN_SERVICE_TYPE = 20;
29799
- const RENTAL_SERVICE_TYPE = 22;
29800
- const RESTAURANT_SERVICE_TYPE = 23;
29801
- const EVENT_SERVICE_TYPE = 24;
29802
- const TRAINING_SERVICE_TYPE = 25;
29803
- /** Icon per service type, so every node on the itinerary reads at a glance. */
29804
- const getServiceTypeIcon = (serviceType) => {
29805
- switch (serviceType) {
29806
- case ACCOMMODATION_SERVICE_TYPE:
29807
- return 'ui-hotel';
29808
- case EXCURSION_SERVICE_TYPE:
29809
- return 'ui-excursion';
29810
- case FLIGHT_SERVICE_TYPE:
29811
- case CHARTER_SERVICE_TYPE:
29812
- return 'ui-plane';
29813
- case RESTAURANT_SERVICE_TYPE:
29814
- return 'ui-utensils';
29815
- case CRUISE_SERVICE_TYPE:
29816
- case FERRY_SERVICE_TYPE:
29817
- return 'ui-ship';
29818
- case CAR_SERVICE_TYPE:
29819
- case CAMPER_SERVICE_TYPE:
29820
- case RENTAL_SERVICE_TYPE:
29821
- return 'ui-car';
29822
- case BUS_SERVICE_TYPE:
29823
- return 'ui-bus';
29824
- case TRAIN_SERVICE_TYPE:
29825
- return 'ui-train';
29826
- case TRANSFER_SERVICE_TYPE$1:
29827
- return 'ui-transfer';
29828
- case EVENT_SERVICE_TYPE:
29829
- return 'ui-ticket';
29830
- case TRAINING_SERVICE_TYPE:
29831
- return 'ui-group';
29832
- default:
29833
- return 'ui-location';
29834
- }
29835
- };
29836
- const getServiceTypeBadgeModifier = (serviceType) => {
29837
- switch (serviceType) {
29838
- case FLIGHT_SERVICE_TYPE:
29839
- case CHARTER_SERVICE_TYPE:
29840
- case CRUISE_SERVICE_TYPE:
29841
- case FERRY_SERVICE_TYPE:
29842
- return 'flight';
29843
- case ACCOMMODATION_SERVICE_TYPE:
29844
- return 'hotel';
29845
- case EXCURSION_SERVICE_TYPE:
29846
- case EVENT_SERVICE_TYPE:
29847
- return 'excursion';
29848
- case RESTAURANT_SERVICE_TYPE:
29849
- return 'restaurant';
29850
- default:
29851
- return '';
29852
- }
29853
- };
29854
- // DurationType, as the SDK numbers it.
29855
- const DURATION_HOURS = 0;
29856
- const DURATION_DAYS = 1;
29857
- const DURATION_NIGHTS = 2;
29858
- const DURATION_MINUTES = 3;
29859
- /**
29860
- * How long a node takes, in the same shape the flight legs are written in — an hour count only
29861
- * once there is one, so a short drive reads as plain minutes.
29862
- */
29863
- const getTrajectNodeDuration = (node) => {
29864
- const minutes = toMinutes(node);
29865
- if (!minutes)
29866
- return '';
29867
- const hours = Math.floor(minutes / 60);
29868
- const rest = minutes % 60;
29869
- return hours ? `${hours}h ${rest.toString().padStart(2, '0')}m` : `${rest} min`;
29870
- };
29871
- const toMinutes = (node) => {
29872
- const duration = node.duration ?? 0;
29873
- if (duration <= 0)
29874
- return 0;
29875
- switch (node.durationType) {
29876
- case DURATION_MINUTES:
29877
- return duration;
29878
- case DURATION_HOURS:
29879
- return duration * 60;
29880
- case DURATION_DAYS:
29881
- case DURATION_NIGHTS:
29882
- return duration * 24 * 60;
29883
- default:
29884
- return 0;
29885
- }
29886
- };
29887
- // RouteTransportType, as the SDK numbers it. It says how a leg is travelled, which is not the
29888
- // same question as what was sold: cycling and walking have no service type of their own.
29889
- const TRANSPORT_PRIVATE = 1;
29890
- const TRANSPORT_PUBLIC = 2;
29891
- const TRANSPORT_RENTAL_CAR = 3;
29892
- const TRANSPORT_BUS = 4;
29893
- const TRANSPORT_CAMPER = 5;
29894
- const TRANSPORT_CAR = 6;
29895
- const TRANSPORT_CHARTER = 7;
29896
- const TRANSPORT_CRUISE = 8;
29897
- const TRANSPORT_FERRY = 9;
29898
- const TRANSPORT_FLIGHT = 10;
29899
- const TRANSPORT_TAXI = 11;
29900
- const TRANSPORT_TRAIN = 12;
29901
- const TRANSPORT_TRANSFER = 13;
29902
- const TRANSPORT_WALKING = 15;
29903
- const TRANSPORT_CYCLING = 16;
29904
- /** Icon per way of travelling, where the route records one. */
29905
- const getTransportTypeIcon = (transportType) => {
29906
- switch (transportType) {
29907
- case TRANSPORT_WALKING:
29908
- return 'ui-walking';
29909
- case TRANSPORT_CYCLING:
29910
- return 'ui-cycling';
29911
- case TRANSPORT_PRIVATE:
29912
- case TRANSPORT_RENTAL_CAR:
29913
- case TRANSPORT_CAMPER:
29914
- case TRANSPORT_CAR:
29915
- return 'ui-car';
29916
- case TRANSPORT_TAXI:
29917
- case TRANSPORT_TRANSFER:
29918
- return 'ui-transfer';
29919
- case TRANSPORT_PUBLIC:
29920
- case TRANSPORT_BUS:
29921
- return 'ui-bus';
29922
- case TRANSPORT_TRAIN:
29923
- return 'ui-train';
29924
- case TRANSPORT_CRUISE:
29925
- case TRANSPORT_FERRY:
29926
- return 'ui-ship';
29927
- case TRANSPORT_FLIGHT:
29928
- case TRANSPORT_CHARTER:
29929
- return 'ui-plane';
29930
- default:
29931
- return null;
29932
- }
29933
- };
29934
- const POI_TRAJECT_NODE_TYPE = 2;
29935
- /**
29936
- * A point of interest is a place on the map rather than a service, so it gets a marker. Anything
29937
- * else goes by how it is travelled where the route says so — a leg on foot or by bike is sold as
29938
- * nothing, and its service type falls back to a transfer — and by what it is where it does not.
29939
- */
29940
- const getTrajectNodeIcon = (node) => {
29941
- if (node.trajectNodeType === POI_TRAJECT_NODE_TYPE)
29942
- return 'ui-location';
29943
- return getTransportTypeIcon(node.transportType) ?? getServiceTypeIcon(node.serviceType);
29944
- };
29945
- const getTrajectNodeBadgeModifier = (node) => node.trajectNodeType === POI_TRAJECT_NODE_TYPE ? 'poi' : getServiceTypeBadgeModifier(node.serviceType);
29946
-
29947
29801
  const REGULAR_TRAJECT_NODE_TYPE = 0;
29948
29802
  const CONNECTING_TRAJECT_NODE_TYPE = 1;
29803
+ const POI_TRAJECT_NODE_TYPE = 2;
29949
29804
  const isSearchableTrajectNode = (node) => node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE;
29950
29805
  const isTrajectFlightNode = (node) => node.serviceType === FLIGHT_SERVICE_TYPE;
29951
29806
  const getTrajectNodesWithLines = (trajectEntry) => {
@@ -29960,7 +29815,7 @@ const getTrajectNodesWithLines = (trajectEntry) => {
29960
29815
  .filter((x) => !!x.line);
29961
29816
  };
29962
29817
  // Qite.Offer.Enums.ServiceType.Transfer: a traject's connecting node is booked as one.
29963
- const TRANSFER_SERVICE_TYPE = 13;
29818
+ const TRANSFER_SERVICE_TYPE$1 = 13;
29964
29819
  // Qite.Offer.Enums.RequirementType. A Mandatory (0) stop, and one RequiredInGroup (3) — one of the node's
29965
29820
  // alternatives has to be booked — always keep a selection; only these two may be taken off the trip.
29966
29821
  const OPTIONAL_REQUIREMENT_TYPE = 1;
@@ -29988,7 +29843,53 @@ const getDroppedTrajectNodeIds = (trajectEntry, entry) => {
29988
29843
  return dropped;
29989
29844
  };
29990
29845
  const isTrajectTransferNode = (node) => node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE && !!node.pickUp && !!node.dropOff;
29991
- const buildTrajectTransferRequest = (node, context) => ({
29846
+ const getStopLine = (node, lines) => {
29847
+ const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
29848
+ const own = lines.find((line) => guids.includes(line.guid));
29849
+ const groupKey = own ? getLineGroupKey(own) : '';
29850
+ return (groupKey ? getGroupLines(lines, groupKey)[0]?.[0] : undefined) ?? own;
29851
+ };
29852
+ const getMovedStopCoordinate = (node, originalLines, lines) => {
29853
+ if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE || !isSearchableTrajectNode(node))
29854
+ return null;
29855
+ const current = getStopLine(node, lines);
29856
+ if (!current || current.latitude == null || current.longitude == null)
29857
+ return null;
29858
+ if (current.productCode === getStopLine(node, originalLines)?.productCode)
29859
+ return null;
29860
+ return { name: current.productName, latitude: current.latitude, longitude: current.longitude };
29861
+ };
29862
+ const getNightStay = (nodes, lines, night) => nodes.find((node) => {
29863
+ if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE || node.serviceType !== ACCOMMODATION_SERVICE_TYPE)
29864
+ return false;
29865
+ const line = getStopLine(node, lines);
29866
+ const nights = Math.max(differenceInCalendarDays(new Date(line?.to ?? node.to), new Date(line?.from ?? node.from)), 1);
29867
+ return node.dayOrder <= night && night < node.dayOrder + nights;
29868
+ });
29869
+ const getTrajectTransferEndpoints = (trajectEntry, lines, connector) => {
29870
+ if (!isTrajectTransferNode(connector))
29871
+ return null;
29872
+ const routeNodes = trajectEntry.entry.routeNodes ?? [];
29873
+ const onRoute = routeNodes.find((node) => node.nodeId === connector.nodeId);
29874
+ const nodes = onRoute ? routeNodes : trajectEntry.nodes ?? [];
29875
+ const at = onRoute ?? connector;
29876
+ const originalLines = trajectEntry.entry.lines ?? [];
29877
+ const day = nodes.filter((node) => node.dayOrder === at.dayOrder).sort((a, b) => a.order - b.order);
29878
+ const index = day.indexOf(at);
29879
+ const isStop = (node) => node.trajectNodeType !== CONNECTING_TRAJECT_NODE_TYPE;
29880
+ const previous = day.slice(0, index).filter(isStop).pop() ?? getNightStay(nodes, lines, at.dayOrder - 1);
29881
+ const next = day.slice(index + 1).find(isStop) ?? getNightStay(nodes, lines, at.dayOrder);
29882
+ const pickUp = (previous && getMovedStopCoordinate(previous, originalLines, lines)) ?? connector.pickUp;
29883
+ const dropOff = (next && getMovedStopCoordinate(next, originalLines, lines)) ?? connector.dropOff;
29884
+ const place = (coordinate) => `${coordinate.latitude.toFixed(5)},${coordinate.longitude.toFixed(5)}`;
29885
+ return {
29886
+ pickUp,
29887
+ dropOff,
29888
+ key: `${place(pickUp)}|${place(dropOff)}|${toDateOnlyString(connector.from)}`,
29889
+ asDrawn: pickUp === connector.pickUp && dropOff === connector.dropOff
29890
+ };
29891
+ };
29892
+ const buildTrajectTransferRequest = (node, endpoints, context) => ({
29992
29893
  transactionId: context.transactionId,
29993
29894
  officeId: context.officeId,
29994
29895
  agentId: context.agentId ?? null,
@@ -29996,10 +29897,11 @@ const buildTrajectTransferRequest = (node, context) => ({
29996
29897
  searchConfigurationId: context.searchConfigurationId,
29997
29898
  language: context.language,
29998
29899
  date: node.from,
29999
- pickUp: node.pickUp,
30000
- dropOff: node.dropOff,
29900
+ pickUp: endpoints.pickUp,
29901
+ dropOff: endpoints.dropOff,
30001
29902
  travellers: context.rooms.flatMap((room) => room.travellers)
30002
29903
  });
29904
+ const getTrajectTransferLines = (node, lines) => lines.filter((line) => line.trajectNodeId === node.nodeId || (!!node.lineGuids?.length && node.lineGuids.includes(line.guid)));
30003
29905
  const buildTrajectTransferLine = (node, result) => {
30004
29906
  const option = result.rooms?.[0]?.options?.find((x) => x.isSelected) ?? result.rooms?.[0]?.options?.[0];
30005
29907
  if (!option?.guid)
@@ -30013,7 +29915,7 @@ const buildTrajectTransferLine = (node, result) => {
30013
29915
  from: result.fromDate ?? node.from,
30014
29916
  to: result.toDate ?? node.to,
30015
29917
  moment: node.moment ?? '',
30016
- serviceType: TRANSFER_SERVICE_TYPE,
29918
+ serviceType: TRANSFER_SERVICE_TYPE$1,
30017
29919
  productName: result.name,
30018
29920
  productCode: result.code,
30019
29921
  accommodationName: option.accommodationName,
@@ -30388,7 +30290,15 @@ const withCachedEntryLineGuids = (entry, trajectEntry, nodeAvailability, flightA
30388
30290
  const bookedMode = !!trajectEntry.entry.magicLink;
30389
30291
  const editableByGuid = new Map((entry.lines ?? []).map((line) => [line.guid, line]));
30390
30292
  const keepsOwnGuid = (guid) => bookedMode && !editableByGuid.get(guid)?.isChanged;
30293
+ // A swapped line tells which stop of the dossier's route it replaces, so saving links the route to it.
30294
+ const routeNodeIds = new Map();
30295
+ if (bookedMode)
30296
+ (trajectEntry.entry.routeNodes ?? []).forEach((node) => (node.lineGuids ?? []).forEach((guid) => routeNodeIds.set(guid, node.nodeId)));
30297
+ const trajectNodeIds = new Map();
30391
30298
  getTrajectNodesWithLines(trajectEntry).forEach((item) => {
30299
+ const routeNodeId = item.lines.map((line) => routeNodeIds.get(line.guid)).find((id) => id !== undefined);
30300
+ if (routeNodeId !== undefined)
30301
+ item.lines.filter((line) => !keepsOwnGuid(line.guid)).forEach((line) => trajectNodeIds.set(line.guid, routeNodeId));
30392
30302
  if (isTrajectFlightNode(item.node)) {
30393
30303
  if (keepsOwnGuid(item.line.guid))
30394
30304
  return;
@@ -30411,14 +30321,15 @@ const withCachedEntryLineGuids = (entry, trajectEntry, nodeAvailability, flightA
30411
30321
  cachedGuids.set(line.guid, guid);
30412
30322
  });
30413
30323
  });
30414
- if (!cachedGuids.size)
30324
+ if (!cachedGuids.size && !trajectNodeIds.size)
30415
30325
  return entry;
30416
30326
  return {
30417
30327
  ...entry,
30418
30328
  lines: (entry.lines ?? []).map((line) => ({
30419
30329
  ...line,
30420
30330
  guid: cachedGuids.get(line.guid) ?? line.guid,
30421
- parentGuid: (line.parentGuid && cachedGuids.get(line.parentGuid)) ?? line.parentGuid
30331
+ parentGuid: (line.parentGuid && cachedGuids.get(line.parentGuid)) ?? line.parentGuid,
30332
+ trajectNodeId: trajectNodeIds.get(line.guid) ?? line.trajectNodeId
30422
30333
  }))
30423
30334
  };
30424
30335
  };
@@ -30452,222 +30363,227 @@ const applyFlightToLine = (line, flight) => {
30452
30363
  const isExternalResult = (result) => result.vendorId != null || result.vendorConfigurationId != null;
30453
30364
  const isExternalCandidate = (candidate) => candidate.externalVendorId != null || candidate.vendor != null;
30454
30365
 
30455
- const badgeClass = (kind, modifier) => {
30456
- const base = `search__filter__itinerary__${kind}-badge`;
30457
- const fallback = kind === 'segment' ? `${base}--secondary` : `${base}--default`;
30458
- return `${base} ${modifier ? `${base}--${modifier}` : fallback}`;
30366
+ // Administrative service lines (fees, insurances, reductions, ...) live on the entry but say
30367
+ // nothing about the journey itself, so the itinerary never draws them.
30368
+ const HIDDEN_ITINERARY_SERVICE_TYPES = new Set([
30369
+ build.ServiceType.insurance,
30370
+ build.ServiceType.extraService,
30371
+ build.ServiceType.serviceFee,
30372
+ build.ServiceType.gift,
30373
+ build.ServiceType.reduction,
30374
+ build.ServiceType.fee,
30375
+ build.ServiceType.visum,
30376
+ build.ServiceType.voucher,
30377
+ build.ServiceType.specialServices
30378
+ ]);
30379
+ // An inactive member of a line group is an alternative the traveller did not pick — not travelled.
30380
+ const isMeaningfulItineraryLine = (line) => {
30381
+ return isActiveEntryLine(line) && !HIDDEN_ITINERARY_SERVICE_TYPES.has(line.serviceType ?? -1);
30459
30382
  };
30460
- const nightsOf = (node, line) => differenceInCalendarDays(new Date(line?.to ?? node.to), new Date(line?.from ?? node.from));
30461
- const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
30462
- const timeline = useMemo(() => {
30463
- const linesByGuid = new Map((packagingEntry.lines ?? []).map((line) => [line.guid, line]));
30464
- const dropped = getDroppedTrajectNodeIds(trajectEntry, packagingEntry);
30465
- const nodes = [...(trajectEntry.nodes ?? [])]
30466
- .filter((node) => !dropped.has(node.nodeId))
30467
- .sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
30468
- .map((node) => {
30469
- // A connecting node has no line of its own; the transfer found for it carries its node id.
30470
- if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
30471
- return { node, lines: (packagingEntry.lines ?? []).filter((line) => line.trajectNodeId === node.nodeId) };
30472
- }
30473
- const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
30474
- const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
30475
- const groupKey = lines.length ? getLineGroupKey(lines[0]) : '';
30476
- const active = groupKey ? getGroupLines(packagingEntry.lines ?? [], groupKey)[0] : undefined;
30477
- return { node, lines: active ?? lines };
30478
- });
30479
- const claimed = new Set(nodes.flatMap((item) => item.lines.map((line) => line.guid)));
30480
- const extras = (packagingEntry.lines ?? []).filter((line) => !claimed.has(line.guid)).filter(isMeaningfulItineraryLine);
30481
- if (!extras.length)
30482
- return nodes;
30483
- const dayOrderOf = (date) => trajectEntry.traject.days.find((day) => isSameDay(new Date(day.date), new Date(date)))?.order ??
30484
- Math.max(differenceInCalendarDays(new Date(date), new Date(trajectEntry.traject.startDate)), 0);
30485
- // One product bought once, however many option lines it took (an adult and a child ticket).
30486
- const grouped = new Map();
30487
- extras.forEach((line) => {
30488
- const key = `${line.productCode}|${line.from}`;
30489
- grouped.set(key, [...(grouped.get(key) ?? []), line]);
30490
- });
30491
- const byDay = new Map();
30492
- nodes.forEach((item) => byDay.set(item.node.dayOrder, [...(byDay.get(item.node.dayOrder) ?? []), item]));
30493
- [...grouped.values()].forEach((lines, index) => {
30494
- const line = lines[0];
30495
- const dayOrder = dayOrderOf(line.from);
30496
- const item = {
30497
- lines,
30498
- node: {
30499
- nodeId: -(index + 1),
30500
- lineGuid: line.guid,
30501
- lineGuids: lines.map((x) => x.guid),
30502
- dayOrder,
30503
- order: Number.MAX_SAFE_INTEGER,
30504
- name: line.productName,
30505
- serviceType: line.serviceType,
30506
- trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
30507
- requirementType: 0,
30508
- contentSource: 0,
30509
- duration: 0,
30510
- durationType: 0,
30511
- alternatives: [],
30512
- from: line.from,
30513
- to: line.to
30514
- }
30515
- };
30516
- const dayItems = [...(byDay.get(dayOrder) ?? [])];
30517
- let at = dayItems.length;
30518
- while (at > 0 && dayItems[at - 1].node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE)
30519
- at--;
30520
- dayItems.splice(at, 0, item);
30521
- byDay.set(dayOrder, dayItems);
30522
- });
30523
- return [...byDay.keys()].sort((a, b) => a - b).flatMap((dayOrder) => byDay.get(dayOrder));
30524
- }, [trajectEntry, packagingEntry]);
30525
- const stayByDay = useMemo(() => {
30526
- const map = new Map();
30527
- timeline
30528
- .filter(({ node }) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === ACCOMMODATION_SERVICE_TYPE)
30529
- .forEach(({ node, lines }) => {
30530
- const line = lines[0];
30531
- const nights = Math.max(nightsOf(node, line), 1);
30532
- const name = line?.productName || node.preferredAccommodationName || node.name || '';
30533
- for (let night = 0; night < nights; night++)
30534
- map.set(node.dayOrder + night, { name, night, nights });
30535
- });
30536
- return map;
30537
- }, [timeline]);
30538
- const renderDate = (value) => (React__default.createElement("div", { className: "search__filter__itinerary__transport-date" },
30539
- React__default.createElement("p", { className: "search__filter__itinerary__transport-date-date" },
30540
- React__default.createElement("strong", null, format$2(new Date(value), 'd'))),
30541
- React__default.createElement("p", null, format$2(new Date(value), 'MMM'))));
30542
- const renderConnectingNode = ({ node, lines }) => {
30543
- const distance = node.distanceInKm ? `${Math.round(node.distanceInKm)} km` : '';
30544
- // How long the hop takes and how far it is, the way the traject editor puts them.
30545
- const journey = [getTrajectNodeDuration(node), distance].filter(Boolean).join(' · ');
30546
- const title = lines[0]?.productName || node.name;
30547
- return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `connect-${node.nodeId}` },
30548
- React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30549
- React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30550
- React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
30551
- React__default.createElement("div", { className: "search__filter__itinerary__transport-date" }),
30552
- React__default.createElement("div", { className: badgeClass('transport', getTrajectNodeBadgeModifier(node)) },
30553
- React__default.createElement(Icon, { name: getTrajectNodeIcon(node), height: 15 })),
30554
- React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
30555
- React__default.createElement("h6", null, title || journey),
30556
- !!title && !!journey && React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" }, journey)))));
30557
- };
30558
- const renderOvernight = (item, index) => {
30559
- const { dayOrder } = item.node;
30560
- const closesTheDay = timeline[index + 1]?.node.dayOrder !== dayOrder;
30561
- const stay = stayByDay.get(dayOrder);
30562
- if (!closesTheDay || !stay)
30383
+ const getFlightLines = (flight) => {
30384
+ return flight?.flightInformation?.flightLines ?? [];
30385
+ };
30386
+ const getDepartureTime = (flight) => {
30387
+ const firstLine = first(getFlightLines(flight));
30388
+ if (!firstLine?.departureTime)
30389
+ return '';
30390
+ return firstLine.departureTime.slice(0, 5);
30391
+ };
30392
+ const getArrivalTime = (flight) => {
30393
+ const lastLine = last(getFlightLines(flight));
30394
+ if (!lastLine?.arrivalTime)
30395
+ return '';
30396
+ return lastLine.arrivalTime.slice(0, 5);
30397
+ };
30398
+ const getDuration = (flight) => {
30399
+ const lines = getFlightLines(flight);
30400
+ if (!lines.length)
30401
+ return '';
30402
+ const totalTicks = lines.reduce((sum, line) => sum + (line.durationInTicks ?? 0), 0) || 0;
30403
+ if (!totalTicks)
30404
+ return '';
30405
+ const seconds = totalTicks / 10_000_000;
30406
+ const hours = Math.floor(seconds / 3600);
30407
+ const minutes = Math.floor((seconds % 3600) / 60);
30408
+ return `${hours}h ${minutes.toString().padStart(2, '0')}m`;
30409
+ };
30410
+ const numberOfNights = (segment) => {
30411
+ return differenceInCalendarDays(new Date(segment.to), new Date(segment.from));
30412
+ };
30413
+ const CHARTER_SERVICE_TYPE = 10;
30414
+ const CRUISE_SERVICE_TYPE = 11;
30415
+ const FERRY_SERVICE_TYPE = 12;
30416
+ const TRANSFER_SERVICE_TYPE = 13;
30417
+ const BUS_SERVICE_TYPE = 14;
30418
+ const CAR_SERVICE_TYPE = 17;
30419
+ const CAMPER_SERVICE_TYPE = 18;
30420
+ const TRAIN_SERVICE_TYPE = 20;
30421
+ const RENTAL_SERVICE_TYPE = 22;
30422
+ const RESTAURANT_SERVICE_TYPE = 23;
30423
+ const EVENT_SERVICE_TYPE = 24;
30424
+ const TRAINING_SERVICE_TYPE = 25;
30425
+ /** Icon per service type, so every node on the itinerary reads at a glance. */
30426
+ const getServiceTypeIcon = (serviceType) => {
30427
+ switch (serviceType) {
30428
+ case ACCOMMODATION_SERVICE_TYPE:
30429
+ return 'ui-hotel';
30430
+ case EXCURSION_SERVICE_TYPE:
30431
+ return 'ui-excursion';
30432
+ case FLIGHT_SERVICE_TYPE:
30433
+ case CHARTER_SERVICE_TYPE:
30434
+ return 'ui-plane';
30435
+ case RESTAURANT_SERVICE_TYPE:
30436
+ return 'ui-utensils';
30437
+ case CRUISE_SERVICE_TYPE:
30438
+ case FERRY_SERVICE_TYPE:
30439
+ return 'ui-ship';
30440
+ case CAR_SERVICE_TYPE:
30441
+ case CAMPER_SERVICE_TYPE:
30442
+ case RENTAL_SERVICE_TYPE:
30443
+ return 'ui-car';
30444
+ case BUS_SERVICE_TYPE:
30445
+ return 'ui-bus';
30446
+ case TRAIN_SERVICE_TYPE:
30447
+ return 'ui-train';
30448
+ case TRANSFER_SERVICE_TYPE:
30449
+ return 'ui-transfer';
30450
+ case EVENT_SERVICE_TYPE:
30451
+ return 'ui-ticket';
30452
+ case TRAINING_SERVICE_TYPE:
30453
+ return 'ui-group';
30454
+ default:
30455
+ return 'ui-location';
30456
+ }
30457
+ };
30458
+ const getServiceTypeBadgeModifier = (serviceType) => {
30459
+ switch (serviceType) {
30460
+ case FLIGHT_SERVICE_TYPE:
30461
+ case CHARTER_SERVICE_TYPE:
30462
+ case CRUISE_SERVICE_TYPE:
30463
+ case FERRY_SERVICE_TYPE:
30464
+ return 'flight';
30465
+ case ACCOMMODATION_SERVICE_TYPE:
30466
+ return 'hotel';
30467
+ case EXCURSION_SERVICE_TYPE:
30468
+ case EVENT_SERVICE_TYPE:
30469
+ return 'excursion';
30470
+ case RESTAURANT_SERVICE_TYPE:
30471
+ return 'restaurant';
30472
+ default:
30473
+ return '';
30474
+ }
30475
+ };
30476
+ // DurationType, as the SDK numbers it.
30477
+ const DURATION_HOURS = 0;
30478
+ const DURATION_DAYS = 1;
30479
+ const DURATION_NIGHTS = 2;
30480
+ const DURATION_MINUTES = 3;
30481
+ /**
30482
+ * How long a node takes, in the same shape the flight legs are written in — an hour count only
30483
+ * once there is one, so a short drive reads as plain minutes.
30484
+ */
30485
+ const getTrajectNodeDuration = (node) => {
30486
+ const minutes = toMinutes(node);
30487
+ if (!minutes)
30488
+ return '';
30489
+ const hours = Math.floor(minutes / 60);
30490
+ const rest = minutes % 60;
30491
+ return hours ? `${hours}h ${rest.toString().padStart(2, '0')}m` : `${rest} min`;
30492
+ };
30493
+ const toMinutes = (node) => {
30494
+ const duration = node.duration ?? 0;
30495
+ if (duration <= 0)
30496
+ return 0;
30497
+ switch (node.durationType) {
30498
+ case DURATION_MINUTES:
30499
+ return duration;
30500
+ case DURATION_HOURS:
30501
+ return duration * 60;
30502
+ case DURATION_DAYS:
30503
+ case DURATION_NIGHTS:
30504
+ return duration * 24 * 60;
30505
+ default:
30506
+ return 0;
30507
+ }
30508
+ };
30509
+ // RouteTransportType, as the SDK numbers it. It says how a leg is travelled, which is not the
30510
+ // same question as what was sold: cycling and walking have no service type of their own.
30511
+ const TRANSPORT_PRIVATE = 1;
30512
+ const TRANSPORT_PUBLIC = 2;
30513
+ const TRANSPORT_RENTAL_CAR = 3;
30514
+ const TRANSPORT_BUS = 4;
30515
+ const TRANSPORT_CAMPER = 5;
30516
+ const TRANSPORT_CAR = 6;
30517
+ const TRANSPORT_CHARTER = 7;
30518
+ const TRANSPORT_CRUISE = 8;
30519
+ const TRANSPORT_FERRY = 9;
30520
+ const TRANSPORT_FLIGHT = 10;
30521
+ const TRANSPORT_TAXI = 11;
30522
+ const TRANSPORT_TRAIN = 12;
30523
+ const TRANSPORT_TRANSFER = 13;
30524
+ const TRANSPORT_WALKING = 15;
30525
+ const TRANSPORT_CYCLING = 16;
30526
+ /** Icon per way of travelling, where the route records one. */
30527
+ const getTransportTypeIcon = (transportType) => {
30528
+ switch (transportType) {
30529
+ case TRANSPORT_WALKING:
30530
+ return 'ui-walking';
30531
+ case TRANSPORT_CYCLING:
30532
+ return 'ui-cycling';
30533
+ case TRANSPORT_PRIVATE:
30534
+ case TRANSPORT_RENTAL_CAR:
30535
+ case TRANSPORT_CAMPER:
30536
+ case TRANSPORT_CAR:
30537
+ return 'ui-car';
30538
+ case TRANSPORT_TAXI:
30539
+ case TRANSPORT_TRANSFER:
30540
+ return 'ui-transfer';
30541
+ case TRANSPORT_PUBLIC:
30542
+ case TRANSPORT_BUS:
30543
+ return 'ui-bus';
30544
+ case TRANSPORT_TRAIN:
30545
+ return 'ui-train';
30546
+ case TRANSPORT_CRUISE:
30547
+ case TRANSPORT_FERRY:
30548
+ return 'ui-ship';
30549
+ case TRANSPORT_FLIGHT:
30550
+ case TRANSPORT_CHARTER:
30551
+ return 'ui-plane';
30552
+ default:
30563
30553
  return null;
30564
- return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `overnight-${item.node.nodeId}` },
30565
- React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30566
- React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30567
- React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
30568
- renderDate(item.node.from),
30569
- React__default.createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(ACCOMMODATION_SERVICE_TYPE)) },
30570
- React__default.createElement(Icon, { name: "ui-bed", height: 15 })),
30571
- React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
30572
- React__default.createElement("h6", null, stay.name),
30573
- React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" },
30574
- stay.night + 1,
30575
- " ",
30576
- translations.SRP.OF,
30577
- " ",
30578
- stay.nights,
30579
- " ",
30580
- translations.SRP.NIGHTS)))));
30581
- };
30582
- const renderFlightNode = ({ node, lines }) => {
30583
- const line = lines[0];
30584
- return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `flight-${node.nodeId}` },
30585
- React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30586
- React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30587
- React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
30588
- renderDate(line?.from ?? node.from),
30589
- React__default.createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(FLIGHT_SERVICE_TYPE)) },
30590
- React__default.createElement(Icon, { name: "ui-plane", height: 15 })),
30591
- React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
30592
- React__default.createElement("h6", null, line?.productName || node.name),
30593
- !!getDepartureTime(line) && (React__default.createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
30594
- React__default.createElement("span", null,
30595
- React__default.createElement(Icon, { name: "ui-plane-depart", height: 14 }),
30596
- " ",
30597
- React__default.createElement("strong", null, getDepartureTime(line))),
30598
- ' ',
30599
- "-",
30600
- ' ',
30601
- React__default.createElement("span", null,
30602
- React__default.createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
30603
- " ",
30604
- React__default.createElement("strong", null, getArrivalTime(line))))),
30605
- !!getDuration(line) && (React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" },
30606
- React__default.createElement("span", null,
30607
- React__default.createElement(Icon, { name: "ui-clock", height: 20 }),
30608
- " ",
30609
- getDuration(line))))))));
30610
- };
30611
- const renderServiceNode = ({ node, lines }) => {
30612
- const line = lines[0];
30613
- const isStay = node.serviceType === ACCOMMODATION_SERVICE_TYPE;
30614
- const nights = isStay ? nightsOf(node, line) : 0;
30615
- return (React__default.createElement("div", { className: "search__filter__itinerary__segments", key: `node-${node.nodeId}` },
30616
- React__default.createElement("div", { className: "search__filter__itinerary__segments__wrapper" },
30617
- React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline" },
30618
- React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline-line" })),
30619
- React__default.createElement("div", { className: "search__filter__itinerary__segment" },
30620
- React__default.createElement("div", { className: "search__filter__itinerary__segment-item" },
30621
- React__default.createElement("div", { className: `search__filter__itinerary__segment-date ${isStay && nights > 0 ? 'search__filter__itinerary__segment-nights' : ''}` }, isStay && nights > 0 ? (React__default.createElement(React__default.Fragment, null,
30622
- React__default.createElement("p", { className: "search__filter__itinerary__segment-date-date" },
30623
- React__default.createElement("strong", null, nights)),
30624
- React__default.createElement(Icon, { name: "ui-moon", width: 16, height: 16 }))) : (React__default.createElement(React__default.Fragment, null,
30625
- React__default.createElement("p", { className: "search__filter__itinerary__segment-date-date" },
30626
- React__default.createElement("strong", null, format$2(new Date(line?.from ?? node.from), 'd'))),
30627
- React__default.createElement("p", null, format$2(new Date(line?.from ?? node.from), 'MMM'))))),
30628
- React__default.createElement("div", { className: badgeClass('segment', getTrajectNodeBadgeModifier(node)) },
30629
- React__default.createElement(Icon, { name: getTrajectNodeIcon(node), width: 15, height: 15 })),
30630
- React__default.createElement("div", { className: "search__filter__itinerary__segment-details" },
30631
- React__default.createElement("h6", null, line?.productName || node.preferredAccommodationName || node.name),
30632
- isStay &&
30633
- lines.map((roomLine, index) => (React__default.createElement(React__default.Fragment, { key: roomLine.guid },
30634
- lines.length > 1 && (React__default.createElement("strong", null,
30635
- translations.SHARED.ROOM,
30636
- "\u00A0",
30637
- index + 1)),
30638
- roomLine.accommodationName && (React__default.createElement("div", { className: "search__filter__itinerary__segment-details__room" },
30639
- React__default.createElement(Icon, { name: "ui-bed", width: 12, height: 12 }),
30640
- roomLine.accommodationName)),
30641
- roomLine.regimeName && (React__default.createElement("div", { className: "search__filter__itinerary__segment-details__room" },
30642
- React__default.createElement(Icon, { name: "ui-utensils", width: 12, height: 12 }),
30643
- roomLine.regimeName))))),
30644
- !isStay && React__default.createElement("p", { className: "search__filter__itinerary__segment-details-text" }, format$2(new Date(line?.from ?? node.from), 'EEE. d MMM yyyy'))))))));
30645
- };
30646
- return (React__default.createElement(React__default.Fragment, null, timeline.map((item, index) => {
30647
- if (item.node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
30648
- return (React__default.createElement(React__default.Fragment, { key: `connect-${item.node.nodeId}` },
30649
- renderConnectingNode(item),
30650
- renderOvernight(item, index)));
30651
- }
30652
- if (item.node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && item.node.serviceType === FLIGHT_SERVICE_TYPE)
30653
- return renderFlightNode(item);
30654
- return renderServiceNode(item);
30655
- })));
30554
+ }
30656
30555
  };
30556
+ /**
30557
+ * A point of interest is a place on the map rather than a service, so it gets a marker. Anything
30558
+ * else goes by how it is travelled where the route says so — a leg on foot or by bike is sold as
30559
+ * nothing, and its service type falls back to a transfer — and by what it is where it does not.
30560
+ */
30561
+ const getTrajectNodeIcon = (node) => {
30562
+ if (node.trajectNodeType === POI_TRAJECT_NODE_TYPE)
30563
+ return 'ui-location';
30564
+ return getTransportTypeIcon(node.transportType) ?? getServiceTypeIcon(node.serviceType);
30565
+ };
30566
+ const getTrajectNodeBadgeModifier = (node) => node.trajectNodeType === POI_TRAJECT_NODE_TYPE ? 'poi' : getServiceTypeBadgeModifier(node.serviceType);
30657
30567
 
30658
30568
  /**
30659
30569
  * A booked entry opened through its magic link knows nothing about the traject it may once have
30660
30570
  * been composed from — the dossier only keeps the lines. To give it the same day-by-day services
30661
30571
  * screen a traject gets, the traject structure is rebuilt from those lines: every booked service
30662
30572
  * becomes a node whose one "alternative" is the booked product itself, and the days span the trip.
30573
+ * The points of interest on the dossier's own route (entry.routeNodes) are added between them.
30663
30574
  *
30664
30575
  * The synthesized response keeps the entry untouched (magic link included), which is also how the
30665
30576
  * rest of the component tells this mode apart from a real traject: see isBookedEntryTraject.
30666
30577
  */
30667
30578
  const isBookedEntryTraject = (trajectEntry) => !!trajectEntry?.entry?.magicLink;
30668
30579
  const NODE_SERVICE_TYPES = new Set([ACCOMMODATION_SERVICE_TYPE, EXCURSION_SERVICE_TYPE, FLIGHT_SERVICE_TYPE]);
30580
+ const isBookedRoomLineOf = (line, root) => !!line.parentGuid &&
30581
+ (line.parentGuid === root.guid || line.parentGuid === root.parentGuid) &&
30582
+ line.serviceType === root.serviceType &&
30583
+ line.productCode === root.productCode &&
30584
+ isSameDay(new Date(line.from), new Date(root.from));
30669
30585
  /**
30670
- * A room or option line joins its parent's node; a flight line never does — an outward and its
30586
+ * A room or option line joins its service's node; a flight line never does — an outward and its
30671
30587
  * return are separate legs of the trip even though the return points at the outward as parent.
30672
30588
  *
30673
30589
  * Lines that share a group key are one choice (a couple of hotels of which one is picked), so
@@ -30675,16 +30591,13 @@ const NODE_SERVICE_TYPES = new Set([ACCOMMODATION_SERVICE_TYPE, EXCURSION_SERVIC
30675
30591
  */
30676
30592
  const groupNodeLines = (lines) => {
30677
30593
  const groups = [];
30678
- const groupByGuid = new Map();
30679
30594
  lines.forEach((line) => {
30680
- const parentGroup = line.serviceType !== FLIGHT_SERVICE_TYPE && line.parentGuid ? groupByGuid.get(line.parentGuid) : undefined;
30681
- if (parentGroup) {
30682
- parentGroup.push(line);
30595
+ const service = line.serviceType !== FLIGHT_SERVICE_TYPE ? groups.find((group) => isBookedRoomLineOf(line, group[0])) : undefined;
30596
+ if (service) {
30597
+ service.push(line);
30683
30598
  return;
30684
30599
  }
30685
- const group = [line];
30686
- groups.push(group);
30687
- groupByGuid.set(line.guid, group);
30600
+ groups.push([line]);
30688
30601
  });
30689
30602
  const nodes = [];
30690
30603
  const nodeByGroupKey = new Map();
@@ -30734,19 +30647,24 @@ const buildBookedTrajectEntry = (entry) => {
30734
30647
  const tos = lines.map((line) => new Date(line.to).getTime());
30735
30648
  const start = froms.length ? new Date(Math.min(...froms)) : new Date();
30736
30649
  const end = tos.length ? new Date(Math.max(...tos)) : start;
30650
+ // Local midnights, written the way the backend writes a traject's days. A UTC date (toDateOnlyString)
30651
+ // names another day away from Greenwich, and every line looked up by its day would slide onto it.
30737
30652
  const dayCount = Math.max(differenceInCalendarDays(end, start), 0) + 1;
30738
30653
  const days = Array.from({ length: dayCount }, (_, order) => ({
30739
30654
  order,
30740
30655
  name: '',
30741
- date: toDateOnlyString(addDays(start, order))
30656
+ date: format$2(addDays(start, order), "yyyy-MM-dd'T00:00:00'")
30742
30657
  }));
30743
- const dayOrderOf = (line) => Math.max(differenceInCalendarDays(new Date(line.from), start), 0);
30744
- // Services starting the same day are laid out by where they end: the one that ends soonest opens
30745
- // the day (an outward flight on 2 Oct) and the one that spans further follows (the stay until 9 Oct).
30746
- const orderedGroups = groupNodeLines(lines).sort((a, b) => {
30747
- const [lineA, lineB] = [a.lines[0], b.lines[0]];
30748
- return dayOrderOf(lineA) - dayOrderOf(lineB) || new Date(lineA.to).getTime() - new Date(lineB.to).getTime() || lineA.order - lineB.order;
30749
- });
30658
+ const dayOrderOf = (item) => Math.min(Math.max(differenceInCalendarDays(new Date(item.from), start), 0), dayCount - 1);
30659
+ const flights = sortBy(lines.filter((line) => line.serviceType === FLIGHT_SERVICE_TYPE && isActiveEntryLine(line)), (line) => new Date(line.from).getTime(), (line) => line.order);
30660
+ const outward = first(flights);
30661
+ const homeward = flights.length > 1 ? last(flights) : undefined;
30662
+ // The dossier keeps its lines in route order, so a node's order is its line's — except that the
30663
+ // outward flight opens its day and the homeward one closes it, whatever else that day holds.
30664
+ const homewardOrder = Math.max(0, ...(entry.lines ?? []).map((line) => line.order)) + 1;
30665
+ const orderOf = (line) => (line === outward ? -1 : line === homeward ? homewardOrder : line.order);
30666
+ const orderedGroups = groupNodeLines(lines).sort((a, b) => dayOrderOf(a.lines[0]) - dayOrderOf(b.lines[0]) || orderOf(a.lines[0]) - orderOf(b.lines[0]));
30667
+ const idBase = Math.max(0, ...(entry.routeNodes ?? []).map((routeNode) => routeNode.nodeId));
30750
30668
  const nodes = orderedGroups.map(({ lines: groupLines, alternatives }, index) => {
30751
30669
  const line = groupLines[0];
30752
30670
  const flightLines = line.flightInformation?.flightLines ?? [];
@@ -30756,9 +30674,9 @@ const buildBookedTrajectEntry = (entry) => {
30756
30674
  return {
30757
30675
  lineGuid: line.guid,
30758
30676
  lineGuids: groupLines.map((x) => x.guid),
30759
- nodeId: index + 1,
30677
+ nodeId: idBase + index + 1,
30760
30678
  dayOrder: dayOrderOf(line),
30761
- order: index,
30679
+ order: orderOf(line),
30762
30680
  name: (alternatives.length && getLineGroupKey(line)) || line.productName,
30763
30681
  serviceType: line.serviceType,
30764
30682
  trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
@@ -30776,6 +30694,23 @@ const buildBookedTrajectEntry = (entry) => {
30776
30694
  to: line.to
30777
30695
  };
30778
30696
  });
30697
+ const linesByGuid = new Map((entry.lines ?? []).map((line) => [line.guid, line]));
30698
+ let anchorOrder = -2;
30699
+ let step = 0;
30700
+ (entry.routeNodes ?? []).forEach((routeNode) => {
30701
+ const anchor = linesByGuid.get(routeNode.lineGuid);
30702
+ if (anchor) {
30703
+ anchorOrder = orderOf(anchor);
30704
+ step = 0;
30705
+ if (routeNode.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE)
30706
+ nodes.push({ ...routeNode, dayOrder: dayOrderOf(routeNode), order: anchorOrder });
30707
+ return;
30708
+ }
30709
+ if (routeNode.trajectNodeType !== POI_TRAJECT_NODE_TYPE)
30710
+ return;
30711
+ step++;
30712
+ nodes.push({ ...routeNode, nodeId: idBase + nodes.length + 1, dayOrder: dayOrderOf(routeNode), order: anchorOrder + step / 1000 });
30713
+ });
30779
30714
  return {
30780
30715
  entry,
30781
30716
  traject: {
@@ -30897,6 +30832,222 @@ const buildBookedFlightResult = (line) => {
30897
30832
  };
30898
30833
  };
30899
30834
 
30835
+ const badgeClass = (kind, modifier) => {
30836
+ const base = `search__filter__itinerary__${kind}-badge`;
30837
+ const fallback = kind === 'segment' ? `${base}--secondary` : `${base}--default`;
30838
+ return `${base} ${modifier ? `${base}--${modifier}` : fallback}`;
30839
+ };
30840
+ const nightsOf = (node, line) => differenceInCalendarDays(new Date(line?.to ?? node.to), new Date(line?.from ?? node.from));
30841
+ const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
30842
+ const timeline = useMemo(() => {
30843
+ const linesByGuid = new Map((packagingEntry.lines ?? []).map((line) => [line.guid, line]));
30844
+ const dropped = getDroppedTrajectNodeIds(trajectEntry, packagingEntry);
30845
+ const nodes = [...(trajectEntry.nodes ?? [])]
30846
+ .filter((node) => !dropped.has(node.nodeId))
30847
+ .sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
30848
+ .map((node) => {
30849
+ if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
30850
+ return { node, lines: getTrajectTransferLines(node, packagingEntry.lines ?? []) };
30851
+ }
30852
+ const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
30853
+ const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
30854
+ const groupKey = lines.length ? getLineGroupKey(lines[0]) : '';
30855
+ const active = groupKey ? getGroupLines(packagingEntry.lines ?? [], groupKey)[0] : undefined;
30856
+ return { node, lines: active ?? lines };
30857
+ });
30858
+ const claimed = new Set(nodes.flatMap((item) => item.lines.map((line) => line.guid)));
30859
+ const extras = (packagingEntry.lines ?? []).filter((line) => !claimed.has(line.guid)).filter(isMeaningfulItineraryLine);
30860
+ if (!extras.length)
30861
+ return nodes;
30862
+ const dayOrderOf = (date) => trajectEntry.traject.days.find((day) => isSameDay(new Date(day.date), new Date(date)))?.order ??
30863
+ Math.max(differenceInCalendarDays(new Date(date), new Date(trajectEntry.traject.startDate)), 0);
30864
+ // One product bought once, however many option lines it took (an adult and a child ticket).
30865
+ const grouped = new Map();
30866
+ extras.forEach((line) => {
30867
+ const key = `${line.productCode}|${line.from}`;
30868
+ grouped.set(key, [...(grouped.get(key) ?? []), line]);
30869
+ });
30870
+ const byDay = new Map();
30871
+ nodes.forEach((item) => byDay.set(item.node.dayOrder, [...(byDay.get(item.node.dayOrder) ?? []), item]));
30872
+ const flightNodes = nodes.filter(({ node }) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === FLIGHT_SERVICE_TYPE);
30873
+ const homeward = flightNodes.length > 1 ? flightNodes[flightNodes.length - 1] : undefined;
30874
+ // A booked entry's nodes are ordered as the dossier orders its lines (see buildBookedTrajectEntry).
30875
+ const bookedOrders = isBookedEntryTraject(trajectEntry) ? new Map((trajectEntry.entry.lines ?? []).map((line) => [line.guid, line.order])) : undefined;
30876
+ [...grouped.values()].forEach((lines, index) => {
30877
+ const line = lines[0];
30878
+ const dayOrder = dayOrderOf(line.from);
30879
+ const bookedOrder = bookedOrders?.get(line.guid);
30880
+ const item = {
30881
+ lines,
30882
+ node: {
30883
+ nodeId: -(index + 1),
30884
+ lineGuid: line.guid,
30885
+ lineGuids: lines.map((x) => x.guid),
30886
+ dayOrder,
30887
+ order: bookedOrder ?? Number.MAX_SAFE_INTEGER,
30888
+ name: line.productName,
30889
+ serviceType: line.serviceType,
30890
+ trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
30891
+ requirementType: 0,
30892
+ contentSource: 0,
30893
+ duration: 0,
30894
+ durationType: 0,
30895
+ alternatives: [],
30896
+ from: line.from,
30897
+ to: line.to
30898
+ }
30899
+ };
30900
+ const dayItems = [...(byDay.get(dayOrder) ?? [])];
30901
+ // A booked line goes where the dossier's order puts it; one the traveller added closes its
30902
+ // day, but before the hops that leave it and the flight home.
30903
+ let at = dayItems.length;
30904
+ if (bookedOrder !== undefined) {
30905
+ at = dayItems.findIndex((x) => x.node.order > bookedOrder);
30906
+ if (at < 0)
30907
+ at = dayItems.length;
30908
+ }
30909
+ else {
30910
+ while (at > 0 && (dayItems[at - 1].node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE || dayItems[at - 1] === homeward))
30911
+ at--;
30912
+ }
30913
+ dayItems.splice(at, 0, item);
30914
+ byDay.set(dayOrder, dayItems);
30915
+ });
30916
+ return [...byDay.keys()].sort((a, b) => a - b).flatMap((dayOrder) => byDay.get(dayOrder));
30917
+ }, [trajectEntry, packagingEntry]);
30918
+ const stayByDay = useMemo(() => {
30919
+ const map = new Map();
30920
+ timeline
30921
+ .filter(({ node }) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === ACCOMMODATION_SERVICE_TYPE)
30922
+ .forEach(({ node, lines }) => {
30923
+ const line = lines[0];
30924
+ const nights = Math.max(nightsOf(node, line), 1);
30925
+ const name = line?.productName || node.preferredAccommodationName || node.name || '';
30926
+ for (let night = 0; night < nights; night++)
30927
+ map.set(node.dayOrder + night, { name, night, nights });
30928
+ });
30929
+ return map;
30930
+ }, [timeline]);
30931
+ const renderDate = (value) => (React__default.createElement("div", { className: "search__filter__itinerary__transport-date" },
30932
+ React__default.createElement("p", { className: "search__filter__itinerary__transport-date-date" },
30933
+ React__default.createElement("strong", null, format$2(new Date(value), 'd'))),
30934
+ React__default.createElement("p", null, format$2(new Date(value), 'MMM'))));
30935
+ const renderConnectingNode = ({ node, lines }) => {
30936
+ const distance = node.distanceInKm ? `${Math.round(node.distanceInKm)} km` : '';
30937
+ // How long the hop takes and how far it is, the way the traject editor puts them.
30938
+ const journey = [getTrajectNodeDuration(node), distance].filter(Boolean).join(' · ');
30939
+ const title = lines[0]?.productName || node.name;
30940
+ return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `connect-${node.nodeId}` },
30941
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30942
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30943
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
30944
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-date" }),
30945
+ React__default.createElement("div", { className: badgeClass('transport', getTrajectNodeBadgeModifier(node)) },
30946
+ React__default.createElement(Icon, { name: getTrajectNodeIcon(node), height: 15 })),
30947
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
30948
+ React__default.createElement("h6", null, title || journey),
30949
+ !!title && !!journey && React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" }, journey)))));
30950
+ };
30951
+ const renderOvernight = (item, index) => {
30952
+ const { dayOrder } = item.node;
30953
+ const closesTheDay = timeline[index + 1]?.node.dayOrder !== dayOrder;
30954
+ const stay = stayByDay.get(dayOrder);
30955
+ if (!closesTheDay || !stay)
30956
+ return null;
30957
+ return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `overnight-${item.node.nodeId}` },
30958
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30959
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30960
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
30961
+ renderDate(item.node.from),
30962
+ React__default.createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(ACCOMMODATION_SERVICE_TYPE)) },
30963
+ React__default.createElement(Icon, { name: "ui-bed", height: 15 })),
30964
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
30965
+ React__default.createElement("h6", null, stay.name),
30966
+ React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" },
30967
+ stay.night + 1,
30968
+ " ",
30969
+ translations.SRP.OF,
30970
+ " ",
30971
+ stay.nights,
30972
+ " ",
30973
+ translations.SRP.NIGHTS)))));
30974
+ };
30975
+ const renderFlightNode = ({ node, lines }) => {
30976
+ const line = lines[0];
30977
+ return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `flight-${node.nodeId}` },
30978
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30979
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
30980
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
30981
+ renderDate(line?.from ?? node.from),
30982
+ React__default.createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(FLIGHT_SERVICE_TYPE)) },
30983
+ React__default.createElement(Icon, { name: "ui-plane", height: 15 })),
30984
+ React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
30985
+ React__default.createElement("h6", null, line?.productName || node.name),
30986
+ !!getDepartureTime(line) && (React__default.createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
30987
+ React__default.createElement("span", null,
30988
+ React__default.createElement(Icon, { name: "ui-plane-depart", height: 14 }),
30989
+ " ",
30990
+ React__default.createElement("strong", null, getDepartureTime(line))),
30991
+ ' ',
30992
+ "-",
30993
+ ' ',
30994
+ React__default.createElement("span", null,
30995
+ React__default.createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
30996
+ " ",
30997
+ React__default.createElement("strong", null, getArrivalTime(line))))),
30998
+ !!getDuration(line) && (React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" },
30999
+ React__default.createElement("span", null,
31000
+ React__default.createElement(Icon, { name: "ui-clock", height: 20 }),
31001
+ " ",
31002
+ getDuration(line))))))));
31003
+ };
31004
+ const renderServiceNode = ({ node, lines }) => {
31005
+ const line = lines[0];
31006
+ const isStay = node.serviceType === ACCOMMODATION_SERVICE_TYPE;
31007
+ const nights = isStay ? nightsOf(node, line) : 0;
31008
+ return (React__default.createElement("div", { className: "search__filter__itinerary__segments", key: `node-${node.nodeId}` },
31009
+ React__default.createElement("div", { className: "search__filter__itinerary__segments__wrapper" },
31010
+ React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline" },
31011
+ React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline-line" })),
31012
+ React__default.createElement("div", { className: "search__filter__itinerary__segment" },
31013
+ React__default.createElement("div", { className: "search__filter__itinerary__segment-item" },
31014
+ React__default.createElement("div", { className: `search__filter__itinerary__segment-date ${isStay && nights > 0 ? 'search__filter__itinerary__segment-nights' : ''}` }, isStay && nights > 0 ? (React__default.createElement(React__default.Fragment, null,
31015
+ React__default.createElement("p", { className: "search__filter__itinerary__segment-date-date" },
31016
+ React__default.createElement("strong", null, nights)),
31017
+ React__default.createElement(Icon, { name: "ui-moon", width: 16, height: 16 }))) : (React__default.createElement(React__default.Fragment, null,
31018
+ React__default.createElement("p", { className: "search__filter__itinerary__segment-date-date" },
31019
+ React__default.createElement("strong", null, format$2(new Date(line?.from ?? node.from), 'd'))),
31020
+ React__default.createElement("p", null, format$2(new Date(line?.from ?? node.from), 'MMM'))))),
31021
+ React__default.createElement("div", { className: badgeClass('segment', getTrajectNodeBadgeModifier(node)) },
31022
+ React__default.createElement(Icon, { name: getTrajectNodeIcon(node), width: 15, height: 15 })),
31023
+ React__default.createElement("div", { className: "search__filter__itinerary__segment-details" },
31024
+ React__default.createElement("h6", null, line?.productName || node.preferredAccommodationName || node.name),
31025
+ isStay &&
31026
+ lines.map((roomLine, index) => (React__default.createElement(React__default.Fragment, { key: roomLine.guid },
31027
+ lines.length > 1 && (React__default.createElement("strong", null,
31028
+ translations.SHARED.ROOM,
31029
+ "\u00A0",
31030
+ index + 1)),
31031
+ roomLine.accommodationName && (React__default.createElement("div", { className: "search__filter__itinerary__segment-details__room" },
31032
+ React__default.createElement(Icon, { name: "ui-bed", width: 12, height: 12 }),
31033
+ roomLine.accommodationName)),
31034
+ roomLine.regimeName && (React__default.createElement("div", { className: "search__filter__itinerary__segment-details__room" },
31035
+ React__default.createElement(Icon, { name: "ui-utensils", width: 12, height: 12 }),
31036
+ roomLine.regimeName))))),
31037
+ !isStay && React__default.createElement("p", { className: "search__filter__itinerary__segment-details-text" }, format$2(new Date(line?.from ?? node.from), 'EEE. d MMM yyyy'))))))));
31038
+ };
31039
+ return (React__default.createElement(React__default.Fragment, null, timeline.map((item, index) => {
31040
+ if (item.node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
31041
+ return (React__default.createElement(React__default.Fragment, { key: `connect-${item.node.nodeId}` },
31042
+ renderConnectingNode(item),
31043
+ renderOvernight(item, index)));
31044
+ }
31045
+ if (item.node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && item.node.serviceType === FLIGHT_SERVICE_TYPE)
31046
+ return renderFlightNode(item);
31047
+ return renderServiceNode(item);
31048
+ })));
31049
+ };
31050
+
30900
31051
  const getSegmentIcon = (segment) => {
30901
31052
  switch (segment.serviceType) {
30902
31053
  case 3:
@@ -30936,10 +31087,10 @@ const SERVICE_TYPE_PRIORITY = {
30936
31087
  const getServiceTypePriority = (serviceType) => {
30937
31088
  return SERVICE_TYPE_PRIORITY[serviceType ?? -1] ?? 2;
30938
31089
  };
30939
- const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, onSaveEntry }) => {
31090
+ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation }) => {
30940
31091
  const context = useContext(SearchResultsConfigurationContext);
30941
31092
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
30942
- const { editablePackagingEntry, priceDetails, trajectItineraryView } = useSelector((state) => state.searchResults);
31093
+ const { editablePackagingEntry, priceDetails, priceDetailsFailed, trajectItineraryView } = useSelector((state) => state.searchResults);
30943
31094
  const dispatch = useDispatch();
30944
31095
  const packagingEntry = editablePackagingEntry ?? context?.packagingEntry;
30945
31096
  const sortedLines = useMemo(() => {
@@ -31005,7 +31156,9 @@ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, on
31005
31156
  packagingEntry.dossierNumber)),
31006
31157
  React__default.createElement("h4", { className: "search__filter__image__title" }, (location || '') + (location && country ? ' - ' : '') + (country || ''))))),
31007
31158
  React__default.createElement("div", { className: "search__filter__prices" }, isLoading ? (React__default.createElement(Spinner, { label: translations.PRODUCT.LOADING_PRICE })) : (React__default.createElement(React__default.Fragment, null,
31008
- React__default.createElement("div", { className: "search__filter__prices__wrapper" },
31159
+ priceDetailsFailed ? (React__default.createElement("div", { className: "search__filter__prices__wrapper" },
31160
+ React__default.createElement("p", { className: "info-message--error" }, translations.SRP.PRICE_DETAILS_FAILED),
31161
+ React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => dispatch(retryPriceDetails()) }, translations.SRP.TRY_AGAIN))) : (React__default.createElement("div", { className: "search__filter__prices__wrapper" },
31009
31162
  React__default.createElement("h3", { className: "search__filter__prices--amount" }, formatPrice$3(pricePerPerson, 'EUR')),
31010
31163
  React__default.createElement("p", null, translations.SRP.PACKAGE_PRICE_PER_PERSON),
31011
31164
  React__default.createElement("p", null,
@@ -31014,8 +31167,8 @@ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation, on
31014
31167
  formatPrice$3(totalPrice, 'EUR'),
31015
31168
  " ",
31016
31169
  translations.SRP.TOTAL,
31017
- ")"))),
31018
- isBookedEntryTraject(context?.trajectEntry) ? (React__default.createElement("button", { className: "cta", onClick: onSaveEntry, disabled: !hasBookedEntryChanges(context?.packagingEntry, packagingEntry) }, translations.SRP.BOOK_AND_PAY)) : isBooked ? (React__default.createElement("span", { className: "cta cta--disabled search__filter__prices--booked", "aria-disabled": "true" }, translations.SHARED.BOOKED)) : (React__default.createElement("button", { className: "cta", onClick: handleConfirm }, translations.SRP.BOOK_AND_PAY)),
31170
+ ")")))),
31171
+ isBookedEntryTraject(context?.trajectEntry) ? (React__default.createElement("button", { className: "cta", onClick: handleConfirm, disabled: priceDetailsFailed || !hasBookedEntryChanges(context?.packagingEntry, packagingEntry) }, translations.SRP.BOOK_AND_PAY)) : isBooked ? (React__default.createElement("span", { className: "cta cta--disabled search__filter__prices--booked", "aria-disabled": "true" }, translations.SHARED.BOOKED)) : (React__default.createElement("button", { className: "cta", onClick: handleConfirm, disabled: priceDetailsFailed }, translations.SRP.BOOK_AND_PAY)),
31019
31172
  !!context?.trajectEntry && (React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleTrajectView(!trajectItineraryView) }, trajectItineraryView ? translations.SRP.SHOW_SERVICES : translations.SRP.VIEW_FULL_ITINERARY))))),
31020
31173
  React__default.createElement("div", { className: "search__filter__itinerary" },
31021
31174
  React__default.createElement("p", null, translations.SRP.DAY_BY_DAY),
@@ -36314,10 +36467,12 @@ const travellersSettings = {
36314
36467
  const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage }) => {
36315
36468
  const context = useContext(SearchResultsConfigurationContext);
36316
36469
  const dispatch = useDispatch();
36317
- const { editablePackagingEntry, priceDetails, currentStep, bookingNumber, selectedPackagingAccoResultCode, packagingAccoResults } = useSelector((state) => state.searchResults);
36470
+ const { editablePackagingEntry, priceDetails, priceDetailsFailed, currentStep, bookingNumber, selectedPackagingAccoResultCode, packagingAccoResults } = useSelector((state) => state.searchResults);
36471
+ // A booked entry opened through its magic link is saved onto its own dossier instead of booked again.
36472
+ const bookedMode = isBookedEntryTraject(context?.trajectEntry);
36318
36473
  const [countries, setCountries] = useState([]);
36319
36474
  const [userValidated, setUserValidated] = useState(true);
36320
- const [remarks, setRemarks] = useState('');
36475
+ const [remarks, setRemarks] = useState(() => editablePackagingEntry?.remarks ?? '');
36321
36476
  const [isSubmitting, setIsSubmitting] = useState(false);
36322
36477
  const [selectedPackagingAccoResult, setSelectedPackagingAccoResult] = useState(null);
36323
36478
  const translations = useMemo(() => getTranslations(context?.languageCode ?? 'en-GB'), [context?.languageCode]);
@@ -36377,7 +36532,27 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36377
36532
  }, [selectedPackagingAccoResultCode, packagingAccoResults]);
36378
36533
  // What the page holds versus what the backend can look up; they differ for a traject.
36379
36534
  const bookableEntry = useBookableEntry();
36380
- if (!context || !editablePackagingEntry || !priceDetails || !config)
36535
+ // What was already typed stays on the entry, so the form comes back filled in when the traveller returns.
36536
+ const handleBackToServices = () => {
36537
+ if (currentStep === 0 && editablePackagingEntry) {
36538
+ dispatch(setEditablePackagingEntry(applyTravelersFormValuesToEditablePackagingEntry(editablePackagingEntry, formik.values)));
36539
+ }
36540
+ dispatch(setCurrentStep(0));
36541
+ dispatch(setBookPackagingEntry(false));
36542
+ if (typeof window !== 'undefined')
36543
+ window.scrollTo(0, 0);
36544
+ };
36545
+ // Nothing can be confirmed without a price for the entry as it is now; the confirmation itself needs none.
36546
+ if (context && priceDetailsFailed && currentStep < 2) {
36547
+ return (React__default.createElement("div", { className: "booking" },
36548
+ React__default.createElement("div", { className: "booking__content" },
36549
+ React__default.createElement("div", { className: "booking__panel" },
36550
+ React__default.createElement("p", { className: "info-message--error" }, translations.SRP.PRICE_DETAILS_FAILED),
36551
+ React__default.createElement("div", { className: "booking__navigator" },
36552
+ React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: handleBackToServices }, translations.SRP.BACK_TO_SERVICES),
36553
+ React__default.createElement("button", { type: "button", className: "cta", onClick: () => dispatch(retryPriceDetails()) }, translations.SRP.TRY_AGAIN))))));
36554
+ }
36555
+ if (!context || !editablePackagingEntry || !config || (!priceDetails && currentStep < 2))
36381
36556
  return React__default.createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING });
36382
36557
  const handleSummarySubmit = async (e) => {
36383
36558
  e.preventDefault();
@@ -36387,7 +36562,7 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36387
36562
  }
36388
36563
  // Held apart from the entry itself so they can be put on both copies of it.
36389
36564
  let bookingFields = { remarks };
36390
- if (context.generatePaymentUrl && typeof window !== 'undefined') {
36565
+ if (!bookedMode && context.generatePaymentUrl && typeof window !== 'undefined') {
36391
36566
  const redirectUrl = new URL(window.location.href);
36392
36567
  redirectUrl.searchParams.set('bookingConfirmation', 'true');
36393
36568
  redirectUrl.searchParams.set('link', '');
@@ -36404,6 +36579,13 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36404
36579
  // The page keeps the entry it works with; the backend gets the one it can look up.
36405
36580
  payload: { ...(bookableEntry ?? editablePackagingEntry), ...bookingFields }
36406
36581
  };
36582
+ if (bookedMode) {
36583
+ const saved = await build.savePackagingEntry(config, request);
36584
+ dispatch(setBookingNumber(saved.dossierNumber ?? editablePackagingEntry.dossierNumber ?? ''));
36585
+ dispatch(setCurrentStep(2));
36586
+ setIsSubmitting(false);
36587
+ return;
36588
+ }
36407
36589
  const bookingResponse = await build.bookPackagingEntry(config, request);
36408
36590
  dispatch(setBookingNumber(bookingResponse.number));
36409
36591
  if (bookingResponse.paymentUrl) {
@@ -36426,8 +36608,8 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36426
36608
  ".\u00A0",
36427
36609
  stepLabels[step])) },
36428
36610
  isConfirmationPage && isLoading && React__default.createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING }),
36429
- currentStep === 0 && (React__default.createElement(SharedTravelersForm, { formik: formik, translations: translations, travellersSettings: travellersSettings, countries: countries, travelersFirstStep: false, isUnavailable: false, useCompactForm: false, showAgentSelection: false, paxFields: paxFields })),
36430
- currentStep === 1 && (React__default.createElement(SharedSummary, { translations: translations, travelerFormValues: formik.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.createElement("button", { type: "button", title: translations.STEPS.PREVIOUS, onClick: () => dispatch(setCurrentStep(0)), className: "cta cta--secondary" }, translations.STEPS.PREVIOUS)) })),
36611
+ currentStep === 0 && (React__default.createElement(SharedTravelersForm, { formik: formik, translations: translations, travellersSettings: travellersSettings, countries: countries, travelersFirstStep: false, isUnavailable: false, useCompactForm: false, showAgentSelection: false, paxFields: paxFields, renderPreviousButton: () => (React__default.createElement("button", { type: "button", title: translations.SRP.BACK_TO_SERVICES, onClick: handleBackToServices, className: "cta cta--secondary" }, translations.SRP.BACK_TO_SERVICES)) })),
36612
+ currentStep === 1 && priceDetails && (React__default.createElement(SharedSummary, { translations: translations, travelerFormValues: formik.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.createElement("button", { type: "button", title: translations.STEPS.PREVIOUS, onClick: () => dispatch(setCurrentStep(0)), className: "cta cta--secondary" }, translations.STEPS.PREVIOUS)) })),
36431
36613
  currentStep === 2 && (React__default.createElement(SharedConfirmation, { bookingNumber: bookingNumber ?? editablePackagingEntry?.dossierNumber ?? '', isOption: false, isOffer: false, translations: translations.CONFIRMATION })),
36432
36614
  currentStep === 3 && React__default.createElement("div", null))),
36433
36615
  React__default.createElement("div", { className: "backdrop", id: "backdrop" }),
@@ -36848,6 +37030,16 @@ const TrajectResults = ({ isLoading }) => {
36848
37030
  })));
36849
37031
  };
36850
37032
 
37033
+ const getSearchContext = (context, trajectEntry) => ({
37034
+ transactionId: trajectEntry.entry.transactionId,
37035
+ officeId: context.tideConnection.officeId ?? 1,
37036
+ catalogueId: context.searchConfiguration.defaultCatalogueId ?? first(context.tideConnection.catalogueIds) ?? 1,
37037
+ searchConfigurationId: context.searchConfiguration.id,
37038
+ portalId: context.portalId ?? null,
37039
+ agentId: context.agentId ?? null,
37040
+ language: context.languageCode ?? 'en-GB',
37041
+ rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
37042
+ });
36851
37043
  const useTrajectAvailability = () => {
36852
37044
  const context = useContext(SearchResultsConfigurationContext);
36853
37045
  const dispatch = useDispatch();
@@ -36866,16 +37058,7 @@ const useTrajectAvailability = () => {
36866
37058
  host: context.tideConnection.host,
36867
37059
  apiKey: context.tideConnection.apiKey
36868
37060
  };
36869
- const searchContext = {
36870
- transactionId: trajectEntry.entry.transactionId,
36871
- officeId: context.tideConnection.officeId ?? 1,
36872
- catalogueId: context.searchConfiguration.defaultCatalogueId ?? first(context.tideConnection.catalogueIds) ?? 1,
36873
- searchConfigurationId: context.searchConfiguration.id,
36874
- portalId: context.portalId ?? null,
36875
- agentId: context.agentId ?? null,
36876
- language: context.languageCode ?? 'en-GB',
36877
- rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
36878
- };
37061
+ const searchContext = getSearchContext(context, trajectEntry);
36879
37062
  // Flight searches take passengers rather than rooms; the API buckets by age.
36880
37063
  // The ids must be the entry's own pax ids: the flight lines this search caches carry them as
36881
37064
  // EntryLinePax.PaxId, and booking maps those onto the entry's travelers (PackagingService.SyncPaxIds).
@@ -37015,30 +37198,9 @@ const useTrajectAvailability = () => {
37015
37198
  return item.lines.map((x) => x.guid);
37016
37199
  }
37017
37200
  };
37018
- const resolveTransferNode = async (node) => {
37019
- dispatch(setTrajectTransferLoading(node.nodeId));
37020
- try {
37021
- const results = (await build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, searchContext), controller.signal)) ?? [];
37022
- if (controller.signal.aborted)
37023
- return [];
37024
- const result = results[0] ?? null;
37025
- dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: result && buildTrajectTransferLine(node, result) ? result : null }));
37026
- }
37027
- catch (error) {
37028
- if (controller.signal.aborted)
37029
- return [];
37030
- console.error('Failed to resolve traject transfer', node.name, error);
37031
- dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: null }));
37032
- }
37033
- return [];
37034
- };
37035
37201
  (async () => {
37036
37202
  const resolvable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node));
37037
- const transfers = bookedMode ? [] : (trajectEntry.nodes ?? []).filter(isTrajectTransferNode);
37038
- const unresolved = (await Promise.all([
37039
- ...resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))),
37040
- ...transfers.map(resolveTransferNode)
37041
- ])).flat();
37203
+ const unresolved = (await Promise.all(resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))))).flat();
37042
37204
  if (controller.signal.aborted)
37043
37205
  return;
37044
37206
  dispatch(setTrajectResolving(false));
@@ -37050,22 +37212,87 @@ const useTrajectAvailability = () => {
37050
37212
  })();
37051
37213
  return () => controller.abort();
37052
37214
  }, [trajectEntry, showApiItems, hideShowMoreResults]);
37215
+ const transferSearches = useRef(new Map());
37216
+ useEffect(() => {
37217
+ const searches = transferSearches.current;
37218
+ return () => {
37219
+ searches.forEach((search) => search.controller.abort());
37220
+ searches.clear();
37221
+ };
37222
+ }, [trajectEntry]);
37223
+ useEffect(() => {
37224
+ if (!context || !trajectEntry || !editablePackagingEntry)
37225
+ return;
37226
+ const bookedMode = isBookedEntryTraject(trajectEntry);
37227
+ const searches = transferSearches.current;
37228
+ const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
37229
+ const lines = editablePackagingEntry.lines ?? [];
37230
+ const config = { host: context.tideConnection.host, apiKey: context.tideConnection.apiKey };
37231
+ const searchContext = getSearchContext(context, trajectEntry);
37232
+ const cancel = (nodeId) => {
37233
+ const search = searches.get(nodeId);
37234
+ if (!search)
37235
+ return;
37236
+ search.controller.abort();
37237
+ searches.delete(nodeId);
37238
+ dispatch(setTrajectTransferAvailability({ nodeId, key: search.key, result: null }));
37239
+ };
37240
+ (trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
37241
+ const endpoints = dropped.has(node.nodeId) ? null : getTrajectTransferEndpoints(trajectEntry, lines, node);
37242
+ if (!endpoints || (bookedMode && endpoints.asDrawn))
37243
+ return cancel(node.nodeId);
37244
+ if (searches.get(node.nodeId)?.key === endpoints.key || trajectTransferAvailability[node.nodeId]?.key === endpoints.key)
37245
+ return;
37246
+ cancel(node.nodeId);
37247
+ const controller = new AbortController();
37248
+ const { key } = endpoints;
37249
+ searches.set(node.nodeId, { key, controller });
37250
+ dispatch(setTrajectTransferLoading({ nodeId: node.nodeId, key }));
37251
+ build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, endpoints, searchContext), controller.signal)
37252
+ .then((results) => {
37253
+ if (controller.signal.aborted)
37254
+ return;
37255
+ const result = results?.[0] ?? null;
37256
+ dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, key, result: result && buildTrajectTransferLine(node, result) ? result : null }));
37257
+ })
37258
+ .catch((error) => {
37259
+ if (controller.signal.aborted)
37260
+ return;
37261
+ console.error('Failed to resolve traject transfer', node.name, error);
37262
+ dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, key, result: null }));
37263
+ })
37264
+ .finally(() => {
37265
+ if (searches.get(node.nodeId)?.controller === controller)
37266
+ searches.delete(node.nodeId);
37267
+ });
37268
+ });
37269
+ }, [trajectEntry, editablePackagingEntry, trajectTransferAvailability]);
37053
37270
  useEffect(() => {
37054
- if (!trajectEntry || !editablePackagingEntry || isBookedEntryTraject(trajectEntry))
37271
+ if (!trajectEntry || !editablePackagingEntry)
37055
37272
  return;
37273
+ const bookedMode = isBookedEntryTraject(trajectEntry);
37056
37274
  const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
37057
- const guids = new Set((editablePackagingEntry.lines ?? []).map((line) => line.guid));
37275
+ const lines = editablePackagingEntry.lines ?? [];
37276
+ const guids = new Set(lines.map((line) => line.guid));
37058
37277
  const toAdd = [];
37059
37278
  const toRemove = [];
37060
37279
  (trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
37061
- const result = trajectTransferAvailability[node.nodeId]?.result;
37062
- const line = result ? buildTrajectTransferLine(node, result) : null;
37063
- if (!line)
37064
- return;
37065
- if (dropped.has(node.nodeId) && guids.has(line.guid))
37066
- toRemove.push(line.guid);
37067
- if (!dropped.has(node.nodeId) && !guids.has(line.guid))
37068
- toAdd.push(line);
37280
+ const endpoints = dropped.has(node.nodeId) ? null : getTrajectTransferEndpoints(trajectEntry, lines, node);
37281
+ let wanted = [];
37282
+ if (endpoints && bookedMode && endpoints.asDrawn) {
37283
+ wanted = getTrajectTransferLines(node, trajectEntry.entry.lines ?? []);
37284
+ }
37285
+ else if (endpoints) {
37286
+ const availability = trajectTransferAvailability[node.nodeId];
37287
+ const line = availability?.key === endpoints.key && availability.result ? buildTrajectTransferLine(node, availability.result) : null;
37288
+ if (line)
37289
+ wanted = [line];
37290
+ }
37291
+ const wantedGuids = new Set(wanted.map((line) => line.guid));
37292
+ getTrajectTransferLines(node, lines)
37293
+ .filter((line) => !wantedGuids.has(line.guid))
37294
+ .forEach((line) => toRemove.push(line.guid));
37295
+ wanted.filter((line) => !guids.has(line.guid)).forEach((line) => toAdd.push(line));
37069
37296
  });
37070
37297
  if (toRemove.length)
37071
37298
  dispatch(removeEditableEntryLines(toRemove));
@@ -37079,7 +37306,8 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37079
37306
  const dispatch = useDispatch();
37080
37307
  const context = useContext(SearchResultsConfigurationContext);
37081
37308
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
37082
- const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry, trajectEditLineGuid, trajectResolving, trajectItineraryView } = useSelector((state) => state.searchResults);
37309
+ 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 } = useSelector((state) => state.searchResults);
37310
+ const trajectBusy = trajectResolving || Object.values(trajectTransferAvailability).some((transfer) => transfer.status === 'loading');
37083
37311
  const isMobile = useMediaQuery('(max-width: 1200px)');
37084
37312
  // Resolves live availability per traject node; no-op unless a traject entry is configured.
37085
37313
  useTrajectAvailability();
@@ -37091,7 +37319,6 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37091
37319
  const [detailsIsLoading, setDetailsIsLoading] = useState(false);
37092
37320
  const [pricesAreLoading, setPricesAreLoading] = useState(false);
37093
37321
  const [itineraryIsLoading, setItineraryIsLoading] = useState(false);
37094
- const [entrySaving, setEntrySaving] = useState(false);
37095
37322
  const [itineraryOpen, setItineraryOpen] = useState(false);
37096
37323
  const [isBookingConfirmation, setIsBookingConfirmation] = useState(false);
37097
37324
  const [selectedAccommodationSeed, setSelectedAccommodationSeed] = useState(null);
@@ -37849,40 +38076,16 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37849
38076
  useEffect(() => {
37850
38077
  setInitialFiltersSet(false);
37851
38078
  }, [activeSearchSeed]);
37852
- const handleSaveEntry = async () => {
37853
- if (!context || !bookableEntry || entrySaving)
37854
- return;
37855
- setEntrySaving(true);
37856
- try {
37857
- const config = {
37858
- host: context.tideConnection.host,
37859
- apiKey: context.tideConnection.apiKey
37860
- };
37861
- const request = {
37862
- language: context.languageCode ?? 'en-GB',
37863
- officeId: context.tideConnection.officeId,
37864
- catalogueId: context.searchConfiguration.defaultCatalogueId ?? first(context?.tideConnection?.catalogueIds) ?? 1,
37865
- agentId: context.agentId,
37866
- payload: bookableEntry
37867
- };
37868
- await build.savePackagingEntry(config, request);
37869
- if (typeof window !== 'undefined')
37870
- window.location.reload();
37871
- }
37872
- catch (err) {
37873
- console.error('Failed to save the entry', err);
37874
- setEntrySaving(false);
37875
- }
37876
- };
37877
38079
  useEffect(() => {
37878
38080
  const fetchPriceDetails = async () => {
37879
38081
  if (!context || !editablePackagingEntry || isEmpty$1(editablePackagingEntry.lines))
37880
38082
  return;
37881
38083
  // A traject resolves one node at a time. Pricing each step would show a total that climbs
37882
38084
  // as the trip fills in, so the whole trip is priced once, when the last node is in.
37883
- if (context.trajectEntry && trajectResolving)
38085
+ if (context.trajectEntry && trajectBusy)
37884
38086
  return;
37885
38087
  setPricesAreLoading(true);
38088
+ dispatch(setPriceDetailsFailed(false));
37886
38089
  try {
37887
38090
  const config = {
37888
38091
  host: context.tideConnection.host,
@@ -37901,18 +38104,21 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37901
38104
  }
37902
38105
  catch (err) {
37903
38106
  console.error('Error fetching price details', err);
38107
+ // A price from before the change would be wrong for the entry as it is now.
38108
+ dispatch(setPriceDetails(null));
38109
+ dispatch(setPriceDetailsFailed(true));
37904
38110
  setPricesAreLoading(false);
37905
38111
  }
37906
38112
  };
37907
38113
  fetchPriceDetails();
37908
- }, [editablePackagingEntry, bookableEntry, trajectResolving]);
38114
+ }, [editablePackagingEntry, bookableEntry, trajectBusy, priceDetailsRequest]);
37909
38115
  useEffect(() => {
37910
38116
  const fetchItinerary = async () => {
37911
38117
  if (!context || !context.packagingEntry || !editablePackagingEntry || isEmpty$1(editablePackagingEntry.lines))
37912
38118
  return;
37913
38119
  // While a traject is being composed its own day-by-day column is the itinerary; the generated
37914
38120
  // one is what the traveller reads the finished trip back from, so it waits until then.
37915
- if (context.trajectEntry && (trajectResolving || !trajectItineraryView))
38121
+ if (context.trajectEntry && (trajectBusy || !trajectItineraryView))
37916
38122
  return;
37917
38123
  setItineraryIsLoading(true);
37918
38124
  try {
@@ -37937,7 +38143,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37937
38143
  }
37938
38144
  };
37939
38145
  fetchItinerary();
37940
- }, [editablePackagingEntry, bookableEntry, trajectResolving, trajectItineraryView]);
38146
+ }, [editablePackagingEntry, bookableEntry, trajectBusy, trajectItineraryView]);
37941
38147
  // Flight selection
37942
38148
  // const [selectedOutwardKey, setSelectedOutwardKey] = useState<string | null>(null);
37943
38149
  // const [selectedReturnKey, setSelectedReturnKey] = useState<string | null>(null);
@@ -38315,7 +38521,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
38315
38521
  context.searchConfiguration.qsmType != build.PortalQsmType.AccommodationAndFlight && context.showFilters && (React__default.createElement(Filters, { initialFilters: initialFilters, filters: filters, isOpen: filtersOpen, handleSetIsOpen: () => setFiltersOpen(!filtersOpen),
38316
38522
  // handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
38317
38523
  isLoading: isLoading, setFilters: (filters) => dispatch(setFilters(filters)), resetFilters: (filters) => dispatch(resetFilters(filters)) })),
38318
- context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && (React__default.createElement(Itinerary, { isOpen: itineraryOpen, handleSetIsOpen: () => setItineraryOpen(!itineraryOpen), isLoading: isLoading || pricesAreLoading || flightsLoading || itineraryIsLoading || trajectResolving || entrySaving, onEditAccommodation: handleEditAccommodation, onSaveEntry: handleSaveEntry })),
38524
+ context.searchConfiguration.qsmType === build.PortalQsmType.AccommodationAndFlight && (React__default.createElement(Itinerary, { isOpen: itineraryOpen, handleSetIsOpen: () => setItineraryOpen(!itineraryOpen), isLoading: isLoading || pricesAreLoading || flightsLoading || itineraryIsLoading || trajectBusy, onEditAccommodation: handleEditAccommodation })),
38319
38525
  React__default.createElement("div", { className: "search__results" },
38320
38526
  isMobile && (React__default.createElement("div", { className: "search__result-row" },
38321
38527
  React__default.createElement("div", { className: "search__results__actions" },