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