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