@qite/tide-components 1.0.4 → 1.0.7
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 +1241 -403
- package/build/build-cjs/src/search-results/components/itinerary/itinerary-utils.d.ts +12 -0
- package/build/build-cjs/src/search-results/components/itinerary/traject-itinerary.d.ts +9 -0
- package/build/build-cjs/src/search-results/components/traject/traject-day-activities-flyin.d.ts +6 -0
- package/build/build-cjs/src/search-results/components/traject/traject-flights-flyin.d.ts +13 -0
- package/build/build-cjs/src/search-results/components/traject/traject-node-card.d.ts +7 -3
- package/build/build-cjs/src/search-results/components/traject/traject-results-flyin.d.ts +6 -0
- package/build/build-cjs/src/search-results/hooks/use-traject-day-activities.d.ts +6 -0
- package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +15 -1
- package/build/build-cjs/src/search-results/types.d.ts +12 -1
- package/build/build-cjs/src/search-results/utils/traject-utils.d.ts +40 -2
- package/build/build-cjs/src/shared/booking/travelers-form.d.ts +1 -0
- package/build/build-esm/index.js +1242 -404
- package/build/build-esm/src/search-results/components/itinerary/itinerary-utils.d.ts +12 -0
- package/build/build-esm/src/search-results/components/itinerary/traject-itinerary.d.ts +9 -0
- package/build/build-esm/src/search-results/components/traject/traject-day-activities-flyin.d.ts +6 -0
- package/build/build-esm/src/search-results/components/traject/traject-flights-flyin.d.ts +13 -0
- package/build/build-esm/src/search-results/components/traject/traject-node-card.d.ts +7 -3
- package/build/build-esm/src/search-results/components/traject/traject-results-flyin.d.ts +6 -0
- package/build/build-esm/src/search-results/hooks/use-traject-day-activities.d.ts +6 -0
- package/build/build-esm/src/search-results/store/search-results-slice.d.ts +15 -1
- package/build/build-esm/src/search-results/types.d.ts +12 -1
- package/build/build-esm/src/search-results/utils/traject-utils.d.ts +40 -2
- package/build/build-esm/src/shared/booking/travelers-form.d.ts +1 -0
- package/package.json +2 -2
- package/styles/booking-search-results-variables.scss +6 -0
- package/styles/components/_search.scss +94 -5
package/build/build-cjs/index.js
CHANGED
|
@@ -438,6 +438,11 @@ var SRP$f = {
|
|
|
438
438
|
ACCOMMODATION: "الإقامة",
|
|
439
439
|
VIEW_DETAILS: "عرض التفاصيل",
|
|
440
440
|
NIGHTS: "ليالٍ",
|
|
441
|
+
OF: "من",
|
|
442
|
+
ADD: "إضافة",
|
|
443
|
+
REMOVE: "إزالة",
|
|
444
|
+
ACTIVITIES: "الأنشطة",
|
|
445
|
+
SHOW_ACTIVITIES: "عرض الأنشطة",
|
|
441
446
|
MORNING_RANGE: "الصباح",
|
|
442
447
|
AFTERNOON_RANGE: "بعد الظهر",
|
|
443
448
|
EVENING_RANGE: "المساء",
|
|
@@ -877,6 +882,11 @@ var SRP$e = {
|
|
|
877
882
|
ACCOMMODATION: "Indkvartering",
|
|
878
883
|
VIEW_DETAILS: "Se detaljer",
|
|
879
884
|
NIGHTS: "nætter",
|
|
885
|
+
OF: "af",
|
|
886
|
+
ADD: "Tilføj",
|
|
887
|
+
REMOVE: "Fjern",
|
|
888
|
+
ACTIVITIES: "Aktiviteter",
|
|
889
|
+
SHOW_ACTIVITIES: "Vis aktiviteter",
|
|
880
890
|
MORNING_RANGE: "Morgen",
|
|
881
891
|
AFTERNOON_RANGE: "Eftermiddag",
|
|
882
892
|
EVENING_RANGE: "Aften",
|
|
@@ -1320,6 +1330,11 @@ var SRP$d = {
|
|
|
1320
1330
|
ACCOMMODATION: "Unterkunft",
|
|
1321
1331
|
VIEW_DETAILS: "Details anzeigen",
|
|
1322
1332
|
NIGHTS: "Nächte",
|
|
1333
|
+
OF: "von",
|
|
1334
|
+
ADD: "Hinzufügen",
|
|
1335
|
+
REMOVE: "Entfernen",
|
|
1336
|
+
ACTIVITIES: "Aktivitäten",
|
|
1337
|
+
SHOW_ACTIVITIES: "Aktivitäten anzeigen",
|
|
1323
1338
|
MORNING_RANGE: "Morgen",
|
|
1324
1339
|
AFTERNOON_RANGE: "Nachmittag",
|
|
1325
1340
|
EVENING_RANGE: "Abend",
|
|
@@ -1763,6 +1778,11 @@ var SRP$c = {
|
|
|
1763
1778
|
ACCOMMODATION: "Accommodation",
|
|
1764
1779
|
VIEW_DETAILS: "View details",
|
|
1765
1780
|
NIGHTS: "nights",
|
|
1781
|
+
OF: "of",
|
|
1782
|
+
ADD: "Add",
|
|
1783
|
+
REMOVE: "Remove",
|
|
1784
|
+
ACTIVITIES: "Activities",
|
|
1785
|
+
SHOW_ACTIVITIES: "Show activities",
|
|
1766
1786
|
PRICE: "Price",
|
|
1767
1787
|
MORNING_RANGE: "Morning",
|
|
1768
1788
|
AFTERNOON_RANGE: "Afternoon",
|
|
@@ -2202,6 +2222,11 @@ var SRP$b = {
|
|
|
2202
2222
|
ACCOMMODATION: "Alojamiento",
|
|
2203
2223
|
VIEW_DETAILS: "Ver detalles",
|
|
2204
2224
|
NIGHTS: "noches",
|
|
2225
|
+
OF: "de",
|
|
2226
|
+
ADD: "Añadir",
|
|
2227
|
+
REMOVE: "Eliminar",
|
|
2228
|
+
ACTIVITIES: "Actividades",
|
|
2229
|
+
SHOW_ACTIVITIES: "Ver actividades",
|
|
2205
2230
|
MORNING_RANGE: "Mañana",
|
|
2206
2231
|
AFTERNOON_RANGE: "Tarde",
|
|
2207
2232
|
EVENING_RANGE: "Noche",
|
|
@@ -2645,6 +2670,11 @@ var SRP$a = {
|
|
|
2645
2670
|
ACCOMMODATION: "Hébergement",
|
|
2646
2671
|
VIEW_DETAILS: "Voir les détails",
|
|
2647
2672
|
NIGHTS: "nuits",
|
|
2673
|
+
OF: "sur",
|
|
2674
|
+
ADD: "Ajouter",
|
|
2675
|
+
REMOVE: "Supprimer",
|
|
2676
|
+
ACTIVITIES: "Activités",
|
|
2677
|
+
SHOW_ACTIVITIES: "Voir les activités",
|
|
2648
2678
|
MORNING_RANGE: "Matin",
|
|
2649
2679
|
AFTERNOON_RANGE: "Après-midi",
|
|
2650
2680
|
EVENING_RANGE: "Soir",
|
|
@@ -3084,6 +3114,11 @@ var SRP$9 = {
|
|
|
3084
3114
|
ACCOMMODATION: "Hébergement",
|
|
3085
3115
|
VIEW_DETAILS: "Voir les détails",
|
|
3086
3116
|
NIGHTS: "nuits",
|
|
3117
|
+
OF: "sur",
|
|
3118
|
+
ADD: "Ajouter",
|
|
3119
|
+
REMOVE: "Supprimer",
|
|
3120
|
+
ACTIVITIES: "Activités",
|
|
3121
|
+
SHOW_ACTIVITIES: "Voir les activités",
|
|
3087
3122
|
MORNING_RANGE: "Matin",
|
|
3088
3123
|
AFTERNOON_RANGE: "Après-midi",
|
|
3089
3124
|
EVENING_RANGE: "Soir",
|
|
@@ -3523,6 +3558,11 @@ var SRP$8 = {
|
|
|
3523
3558
|
ACCOMMODATION: "Gisting",
|
|
3524
3559
|
VIEW_DETAILS: "Skoða upplýsingar",
|
|
3525
3560
|
NIGHTS: "nætur",
|
|
3561
|
+
OF: "af",
|
|
3562
|
+
ADD: "Bæta við",
|
|
3563
|
+
REMOVE: "Fjarlægja",
|
|
3564
|
+
ACTIVITIES: "Afþreying",
|
|
3565
|
+
SHOW_ACTIVITIES: "Sýna afþreyingu",
|
|
3526
3566
|
MORNING_RANGE: "Morgunn",
|
|
3527
3567
|
AFTERNOON_RANGE: "Síðdegi",
|
|
3528
3568
|
EVENING_RANGE: "Kvöld",
|
|
@@ -3962,6 +4002,11 @@ var SRP$7 = {
|
|
|
3962
4002
|
ACCOMMODATION: "Alloggio",
|
|
3963
4003
|
VIEW_DETAILS: "Visualizza dettagli",
|
|
3964
4004
|
NIGHTS: "notti",
|
|
4005
|
+
OF: "di",
|
|
4006
|
+
ADD: "Aggiungi",
|
|
4007
|
+
REMOVE: "Rimuovi",
|
|
4008
|
+
ACTIVITIES: "Attività",
|
|
4009
|
+
SHOW_ACTIVITIES: "Mostra attività",
|
|
3965
4010
|
MORNING_RANGE: "Mattina",
|
|
3966
4011
|
AFTERNOON_RANGE: "Pomeriggio",
|
|
3967
4012
|
EVENING_RANGE: "Sera",
|
|
@@ -4405,6 +4450,11 @@ var SRP$6 = {
|
|
|
4405
4450
|
ACCOMMODATION: "Accommodatie",
|
|
4406
4451
|
VIEW_DETAILS: "Bekijk details",
|
|
4407
4452
|
NIGHTS: "nachten",
|
|
4453
|
+
OF: "van",
|
|
4454
|
+
ADD: "Toevoegen",
|
|
4455
|
+
REMOVE: "Verwijderen",
|
|
4456
|
+
ACTIVITIES: "Activiteiten",
|
|
4457
|
+
SHOW_ACTIVITIES: "Toon activiteiten",
|
|
4408
4458
|
MORNING_RANGE: "Ochtend",
|
|
4409
4459
|
AFTERNOON_RANGE: "Namiddag",
|
|
4410
4460
|
EVENING_RANGE: "Avond",
|
|
@@ -4844,6 +4894,11 @@ var SRP$5 = {
|
|
|
4844
4894
|
ACCOMMODATION: "Accommodatie",
|
|
4845
4895
|
VIEW_DETAILS: "Bekijk details",
|
|
4846
4896
|
NIGHTS: "nachten",
|
|
4897
|
+
OF: "van",
|
|
4898
|
+
ADD: "Toevoegen",
|
|
4899
|
+
REMOVE: "Verwijderen",
|
|
4900
|
+
ACTIVITIES: "Activiteiten",
|
|
4901
|
+
SHOW_ACTIVITIES: "Toon activiteiten",
|
|
4847
4902
|
MORNING_RANGE: "Ochtend",
|
|
4848
4903
|
AFTERNOON_RANGE: "Namiddag",
|
|
4849
4904
|
EVENING_RANGE: "Avond",
|
|
@@ -5283,6 +5338,11 @@ var SRP$4 = {
|
|
|
5283
5338
|
ACCOMMODATION: "Overnatting",
|
|
5284
5339
|
VIEW_DETAILS: "Se detaljer",
|
|
5285
5340
|
NIGHTS: "netter",
|
|
5341
|
+
OF: "av",
|
|
5342
|
+
ADD: "Legg til",
|
|
5343
|
+
REMOVE: "Fjern",
|
|
5344
|
+
ACTIVITIES: "Aktiviteter",
|
|
5345
|
+
SHOW_ACTIVITIES: "Vis aktiviteter",
|
|
5286
5346
|
MORNING_RANGE: "Morgen",
|
|
5287
5347
|
AFTERNOON_RANGE: "Ettermiddag",
|
|
5288
5348
|
EVENING_RANGE: "Kveld",
|
|
@@ -5722,6 +5782,11 @@ var SRP$3 = {
|
|
|
5722
5782
|
ACCOMMODATION: "Zakwaterowanie",
|
|
5723
5783
|
VIEW_DETAILS: "Zobacz szczegóły",
|
|
5724
5784
|
NIGHTS: "noce",
|
|
5785
|
+
OF: "z",
|
|
5786
|
+
ADD: "Dodaj",
|
|
5787
|
+
REMOVE: "Usuń",
|
|
5788
|
+
ACTIVITIES: "Atrakcje",
|
|
5789
|
+
SHOW_ACTIVITIES: "Pokaż atrakcje",
|
|
5725
5790
|
MORNING_RANGE: "Poranek",
|
|
5726
5791
|
AFTERNOON_RANGE: "Popołudnie",
|
|
5727
5792
|
EVENING_RANGE: "Wieczór",
|
|
@@ -6161,6 +6226,11 @@ var SRP$2 = {
|
|
|
6161
6226
|
ACCOMMODATION: "Alojamento",
|
|
6162
6227
|
VIEW_DETAILS: "Ver detalhes",
|
|
6163
6228
|
NIGHTS: "noites",
|
|
6229
|
+
OF: "de",
|
|
6230
|
+
ADD: "Adicionar",
|
|
6231
|
+
REMOVE: "Remover",
|
|
6232
|
+
ACTIVITIES: "Atividades",
|
|
6233
|
+
SHOW_ACTIVITIES: "Ver atividades",
|
|
6164
6234
|
MORNING_RANGE: "Manhã",
|
|
6165
6235
|
AFTERNOON_RANGE: "Tarde",
|
|
6166
6236
|
EVENING_RANGE: "Noite",
|
|
@@ -6600,6 +6670,11 @@ var SRP$1 = {
|
|
|
6600
6670
|
ACCOMMODATION: "Boende",
|
|
6601
6671
|
VIEW_DETAILS: "Visa detaljer",
|
|
6602
6672
|
NIGHTS: "nätter",
|
|
6673
|
+
OF: "av",
|
|
6674
|
+
ADD: "Lägg till",
|
|
6675
|
+
REMOVE: "Ta bort",
|
|
6676
|
+
ACTIVITIES: "Aktiviteter",
|
|
6677
|
+
SHOW_ACTIVITIES: "Visa aktiviteter",
|
|
6603
6678
|
MORNING_RANGE: "Morgon",
|
|
6604
6679
|
AFTERNOON_RANGE: "Eftermiddag",
|
|
6605
6680
|
EVENING_RANGE: "Kväll",
|
|
@@ -7039,6 +7114,11 @@ var SRP = {
|
|
|
7039
7114
|
ACCOMMODATION: "宿泊",
|
|
7040
7115
|
VIEW_DETAILS: "詳細を見る",
|
|
7041
7116
|
NIGHTS: "泊",
|
|
7117
|
+
OF: "/",
|
|
7118
|
+
ADD: "追加",
|
|
7119
|
+
REMOVE: "削除",
|
|
7120
|
+
ACTIVITIES: "アクティビティ",
|
|
7121
|
+
SHOW_ACTIVITIES: "アクティビティを表示",
|
|
7042
7122
|
MORNING_RANGE: "朝",
|
|
7043
7123
|
AFTERNOON_RANGE: "午後",
|
|
7044
7124
|
EVENING_RANGE: "夕方",
|
|
@@ -16532,7 +16612,7 @@ const Icon = ({ name, className, title, width, height, fill }) => {
|
|
|
16532
16612
|
title && React__default["default"].createElement("title", null, title),
|
|
16533
16613
|
React__default["default"].createElement("path", { d: "M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z" })));
|
|
16534
16614
|
case 'ui-hotel':
|
|
16535
|
-
return (React__default["default"].createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !lodash.isEmpty(className)).join(' '), width: width, height: height, viewBox: "0 0
|
|
16615
|
+
return (React__default["default"].createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !lodash.isEmpty(className)).join(' '), width: width, height: height, viewBox: "0 0 512 512", fill: fill ?? 'currentColor' },
|
|
16536
16616
|
React__default["default"].createElement(HTMLComment, { text: "!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc." }),
|
|
16537
16617
|
title && React__default["default"].createElement("title", null, title),
|
|
16538
16618
|
React__default["default"].createElement("path", { d: "M16 24C16 10.7 26.7 0 40 0L472 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-8 0 0 416 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24L40 512c-13.3 0-24-10.7-24-24s10.7-24 24-24l8 0 0-416-8 0C26.7 48 16 37.3 16 24zm208 88l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zM128 96c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zm96 112l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zM352 96c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM112 208l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm240-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM288 384l43.8 0c9.9 0 17.5-9 14-18.2-13.8-36.1-48.8-61.8-89.7-61.8s-75.9 25.7-89.7 61.8c-3.5 9.2 4.1 18.2 14 18.2l43.8 0 0 80 64 0 0-80z", fill: "currentColor" })));
|
|
@@ -16561,6 +16641,16 @@ const Icon = ({ name, className, title, width, height, fill }) => {
|
|
|
16561
16641
|
React__default["default"].createElement(HTMLComment, { text: "!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc." }),
|
|
16562
16642
|
title && React__default["default"].createElement("title", null, title),
|
|
16563
16643
|
React__default["default"].createElement("path", { d: "M135.2 117.4l-26.1 74.6 293.8 0-26.1-74.6C372.3 104.6 360.2 96 346.6 96L165.4 96c-13.6 0-25.7 8.6-30.2 21.4zM39.6 196.8L74.8 96.3C88.3 57.8 124.6 32 165.4 32l181.2 0c40.8 0 77.1 25.8 90.6 64.3l35.2 100.5c23.2 9.6 39.6 32.5 39.6 59.2l0 192c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-32-320 0 0 32c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32L0 256c0-26.7 16.4-49.6 39.6-59.2zM128 304a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm288 32a32 32 0 1 0 0-64 32 32 0 1 0 0 64z", fill: "currentColor" })));
|
|
16644
|
+
case 'ui-bus':
|
|
16645
|
+
return (React__default["default"].createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !lodash.isEmpty(className)).join(' '), width: width, height: height, viewBox: "0 0 512 512", fill: fill ?? 'currentColor' },
|
|
16646
|
+
title && React__default["default"].createElement("title", null, title),
|
|
16647
|
+
React__default["default"].createElement("path", { d: "M112 32h288a64 64 0 0 1 64 64v256a64 64 0 0 1-64 64h-288a64 64 0 0 1-64-64V96a64 64 0 0 1 64-64zM96 112v104h136V112H96zm184 0v104h136V112H280zM160 352a36 36 0 1 0-72 0 36 36 0 1 0 72 0zm264 0a36 36 0 1 0-72 0 36 36 0 1 0 72 0z", fill: "currentColor", fillRule: "evenodd" }),
|
|
16648
|
+
React__default["default"].createElement("path", { d: "M112 416h64v48a24 24 0 0 1-24 24h-16a24 24 0 0 1-24-24v-48zm224 0h64v48a24 24 0 0 1-24 24h-16a24 24 0 0 1-24-24v-48z", fill: "currentColor" })));
|
|
16649
|
+
case 'ui-train':
|
|
16650
|
+
return (React__default["default"].createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !lodash.isEmpty(className)).join(' '), width: width, height: height, viewBox: "0 0 512 512", fill: fill ?? 'currentColor' },
|
|
16651
|
+
title && React__default["default"].createElement("title", null, title),
|
|
16652
|
+
React__default["default"].createElement("path", { d: "M160 32h192a64 64 0 0 1 64 64v224a64 64 0 0 1-64 64h-192a64 64 0 0 1-64-64V96a64 64 0 0 1 64-64zm-24 80v104h240V112H136zm56 168a28 28 0 1 0-56 0 28 28 0 1 0 56 0zm192 0a28 28 0 1 0-56 0 28 28 0 1 0 56 0z", fill: "currentColor", fillRule: "evenodd" }),
|
|
16653
|
+
React__default["default"].createElement("path", { d: "M168 416h72l-48 64h-72l48-64zm104 0h72l48 64h-72l-48-64z", fill: "currentColor" })));
|
|
16564
16654
|
case 'ui-ship':
|
|
16565
16655
|
return (React__default["default"].createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !lodash.isEmpty(className)).join(' '), width: width, height: height, viewBox: "0 0 640 512", fill: fill ?? 'currentColor' },
|
|
16566
16656
|
React__default["default"].createElement(HTMLComment, { text: "!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc." }),
|
|
@@ -22890,6 +22980,10 @@ const NAME_CHARACTERS_REGEX = /^[\p{L}\s-]+$/u;
|
|
|
22890
22980
|
function hasInvalidNameCharacters(name) {
|
|
22891
22981
|
return !NAME_CHARACTERS_REGEX.test(name);
|
|
22892
22982
|
}
|
|
22983
|
+
// Rejects values like a 5-digit year, which the native date input allows while typing
|
|
22984
|
+
function isValidBirthDate(birthDateText) {
|
|
22985
|
+
return dateFns.isValid(dateFns.parse(birthDateText, 'yyyy-MM-dd', new Date()));
|
|
22986
|
+
}
|
|
22893
22987
|
function getAge(birthDateText, startDateText) {
|
|
22894
22988
|
var birthDate = new Date(birthDateText);
|
|
22895
22989
|
var startDate = new Date(startDateText);
|
|
@@ -22957,7 +23051,7 @@ const validateForm$1 = (values, agentRequired, bookingType, translations, formFi
|
|
|
22957
23051
|
}
|
|
22958
23052
|
}
|
|
22959
23053
|
if (isFormFieldPresent('birthDate')) {
|
|
22960
|
-
if (lodash.isEmpty(adult.birthDate)) {
|
|
23054
|
+
if (lodash.isEmpty(adult.birthDate) || !isValidBirthDate(adult.birthDate)) {
|
|
22961
23055
|
lodash.set(errors, `rooms[${rIndex}].adults[${index}].birthDate`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
|
|
22962
23056
|
}
|
|
22963
23057
|
else if (values.endDate) {
|
|
@@ -22990,7 +23084,7 @@ const validateForm$1 = (values, agentRequired, bookingType, translations, formFi
|
|
|
22990
23084
|
}
|
|
22991
23085
|
}
|
|
22992
23086
|
if (isFormFieldPresent('birthDate')) {
|
|
22993
|
-
if (lodash.isEmpty(child.birthDate)) {
|
|
23087
|
+
if (lodash.isEmpty(child.birthDate) || !isValidBirthDate(child.birthDate)) {
|
|
22994
23088
|
lodash.set(errors, `rooms[${rIndex}].children[${index}].birthDate`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
|
|
22995
23089
|
}
|
|
22996
23090
|
else if (child.isBaby && values.startDate) {
|
|
@@ -23215,6 +23309,13 @@ const TypeAheadInput = ({ name, value, placeholder, options, onChange, onSelect,
|
|
|
23215
23309
|
function isBabyAge(age, maxBabyAge = BABY_MAX_AGE) {
|
|
23216
23310
|
return typeof age === 'number' && age <= maxBabyAge;
|
|
23217
23311
|
}
|
|
23312
|
+
// The native date input can hold values format() throws on (e.g. a 5+ digit year while typing), so never format an unvalidated birthDate
|
|
23313
|
+
function formatBirthDate$1(birthDate) {
|
|
23314
|
+
if (!birthDate)
|
|
23315
|
+
return '';
|
|
23316
|
+
const parsed = dateFns.parse(birthDate, 'yyyy-MM-dd', new Date());
|
|
23317
|
+
return dateFns.isValid(parsed) ? dateFns.format(parsed, 'dd-MM-yyyy') : '';
|
|
23318
|
+
}
|
|
23218
23319
|
function createTraveler(traveler, followNumber, personTranslation, isCompact, maxBabyAge) {
|
|
23219
23320
|
if (isCompact) {
|
|
23220
23321
|
return {
|
|
@@ -23522,10 +23623,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
|
|
|
23522
23623
|
!useCompactForm && (bookingType !== 'b2b' || travellersSettings?.mainBookerFormFields?.length) ? (React__default["default"].createElement("div", { className: "form__region" },
|
|
23523
23624
|
React__default["default"].createElement("div", { className: "form__region-header" },
|
|
23524
23625
|
React__default["default"].createElement("h5", { className: "form__region-heading" }, translations.TRAVELERS_FORM.MAIN_BOOKER),
|
|
23525
|
-
React__default["default"].createElement("p", { className: "form__region-label" }, lodash.compact([
|
|
23526
|
-
lodash.compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '),
|
|
23527
|
-
mainBooker?.birthDate && dateFns.format(dateFns.parse(mainBooker.birthDate, 'yyyy-MM-dd', new Date()), 'dd-MM-yyyy')
|
|
23528
|
-
]).join(', '))),
|
|
23626
|
+
React__default["default"].createElement("p", { className: "form__region-label" }, lodash.compact([lodash.compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '), formatBirthDate$1(mainBooker?.birthDate)]).join(', '))),
|
|
23529
23627
|
travellersSettings?.mainBookerFormFields?.length ? (React__default["default"].createElement("div", { className: "main-booker-form__grid" }, travellersSettings.mainBookerFormFields.map((field, index) => (React__default["default"].createElement("div", { key: index, className: `control control--${field.type}` }, getControl(field.type, {}, field.type)))))) : (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
23530
23628
|
React__default["default"].createElement("div", { className: "form__twocolumn" },
|
|
23531
23629
|
React__default["default"].createElement("div", { className: "form__twocolumn-column" },
|
|
@@ -27370,7 +27468,10 @@ const initialState$1 = {
|
|
|
27370
27468
|
currentStep: 0,
|
|
27371
27469
|
bookingNumber: undefined,
|
|
27372
27470
|
trajectNodeAvailability: {},
|
|
27373
|
-
trajectFlightAvailability: {}
|
|
27471
|
+
trajectFlightAvailability: {},
|
|
27472
|
+
trajectDaySuggestions: {},
|
|
27473
|
+
trajectEditLineGuid: null,
|
|
27474
|
+
trajectActivitiesDayOrder: null
|
|
27374
27475
|
};
|
|
27375
27476
|
const searchResultsSlice = toolkit.createSlice({
|
|
27376
27477
|
name: 'searchResults',
|
|
@@ -27586,6 +27687,11 @@ const searchResultsSlice = toolkit.createSlice({
|
|
|
27586
27687
|
return;
|
|
27587
27688
|
state.editablePackagingEntry.lines[index] = action.payload;
|
|
27588
27689
|
},
|
|
27690
|
+
addEditableEntryLines(state, action) {
|
|
27691
|
+
if (!state.editablePackagingEntry)
|
|
27692
|
+
return;
|
|
27693
|
+
state.editablePackagingEntry.lines = [...(state.editablePackagingEntry.lines ?? []), ...action.payload];
|
|
27694
|
+
},
|
|
27589
27695
|
removeEditableEntryLines(state, action) {
|
|
27590
27696
|
if (!state.editablePackagingEntry)
|
|
27591
27697
|
return;
|
|
@@ -27617,13 +27723,43 @@ const searchResultsSlice = toolkit.createSlice({
|
|
|
27617
27723
|
flight.selectedGuid = action.payload.guid;
|
|
27618
27724
|
flight.status = 'resolved';
|
|
27619
27725
|
},
|
|
27726
|
+
setTrajectDaySuggestionsLoading(state, action) {
|
|
27727
|
+
const dayOrder = action.payload;
|
|
27728
|
+
state.trajectDaySuggestions[dayOrder] = {
|
|
27729
|
+
dayOrder,
|
|
27730
|
+
status: 'loading',
|
|
27731
|
+
results: state.trajectDaySuggestions[dayOrder]?.results ?? []
|
|
27732
|
+
};
|
|
27733
|
+
},
|
|
27734
|
+
setTrajectDaySuggestions(state, action) {
|
|
27735
|
+
const { dayOrder, results } = action.payload;
|
|
27736
|
+
state.trajectDaySuggestions[dayOrder] = { dayOrder, status: 'resolved', results };
|
|
27737
|
+
},
|
|
27738
|
+
setTrajectEditLineGuid(state, action) {
|
|
27739
|
+
state.trajectEditLineGuid = action.payload;
|
|
27740
|
+
},
|
|
27741
|
+
setTrajectActivitiesDayOrder(state, action) {
|
|
27742
|
+
state.trajectActivitiesDayOrder = action.payload;
|
|
27743
|
+
},
|
|
27744
|
+
updateTrajectNodeResult(state, action) {
|
|
27745
|
+
const { lineGuid, result } = action.payload;
|
|
27746
|
+
const node = state.trajectNodeAvailability[lineGuid];
|
|
27747
|
+
if (!node)
|
|
27748
|
+
return;
|
|
27749
|
+
node.results = node.results.map((x) => (x.code === result.code ? result : x));
|
|
27750
|
+
node.selectedCode = result.code;
|
|
27751
|
+
node.status = 'resolved';
|
|
27752
|
+
},
|
|
27620
27753
|
resetTrajectAvailability(state) {
|
|
27621
27754
|
state.trajectNodeAvailability = {};
|
|
27622
27755
|
state.trajectFlightAvailability = {};
|
|
27756
|
+
state.trajectDaySuggestions = {};
|
|
27757
|
+
state.trajectEditLineGuid = null;
|
|
27758
|
+
state.trajectActivitiesDayOrder = null;
|
|
27623
27759
|
}
|
|
27624
27760
|
}
|
|
27625
27761
|
});
|
|
27626
|
-
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, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
|
|
27762
|
+
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, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, setTrajectEditLineGuid, setTrajectActivitiesDayOrder, setTrajectDaySuggestionsLoading, setTrajectDaySuggestions, addEditableEntryLines, updateTrajectNodeResult, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
|
|
27627
27763
|
var searchResultsReducer = searchResultsSlice.reducer;
|
|
27628
27764
|
|
|
27629
27765
|
const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
|
|
@@ -27808,6 +27944,518 @@ const getDuration = (flight) => {
|
|
|
27808
27944
|
const numberOfNights = (segment) => {
|
|
27809
27945
|
return dateFns.differenceInCalendarDays(new Date(segment.to), new Date(segment.from));
|
|
27810
27946
|
};
|
|
27947
|
+
const CHARTER_SERVICE_TYPE = 10;
|
|
27948
|
+
const CRUISE_SERVICE_TYPE = 11;
|
|
27949
|
+
const FERRY_SERVICE_TYPE = 12;
|
|
27950
|
+
const TRANSFER_SERVICE_TYPE = 13;
|
|
27951
|
+
const BUS_SERVICE_TYPE = 14;
|
|
27952
|
+
const CAR_SERVICE_TYPE = 17;
|
|
27953
|
+
const CAMPER_SERVICE_TYPE = 18;
|
|
27954
|
+
const TRAIN_SERVICE_TYPE = 20;
|
|
27955
|
+
const RENTAL_SERVICE_TYPE = 22;
|
|
27956
|
+
const RESTAURANT_SERVICE_TYPE = 23;
|
|
27957
|
+
const EVENT_SERVICE_TYPE = 24;
|
|
27958
|
+
const TRAINING_SERVICE_TYPE = 25;
|
|
27959
|
+
/** Icon per service type, so every node on the itinerary reads at a glance. */
|
|
27960
|
+
const getServiceTypeIcon = (serviceType) => {
|
|
27961
|
+
switch (serviceType) {
|
|
27962
|
+
case ACCOMMODATION_SERVICE_TYPE:
|
|
27963
|
+
return 'ui-hotel';
|
|
27964
|
+
case EXCURSION_SERVICE_TYPE:
|
|
27965
|
+
return 'ui-excursion';
|
|
27966
|
+
case FLIGHT_SERVICE_TYPE:
|
|
27967
|
+
case CHARTER_SERVICE_TYPE:
|
|
27968
|
+
return 'ui-plane';
|
|
27969
|
+
case RESTAURANT_SERVICE_TYPE:
|
|
27970
|
+
return 'ui-utensils';
|
|
27971
|
+
case CRUISE_SERVICE_TYPE:
|
|
27972
|
+
case FERRY_SERVICE_TYPE:
|
|
27973
|
+
return 'ui-ship';
|
|
27974
|
+
case CAR_SERVICE_TYPE:
|
|
27975
|
+
case CAMPER_SERVICE_TYPE:
|
|
27976
|
+
case RENTAL_SERVICE_TYPE:
|
|
27977
|
+
return 'ui-car';
|
|
27978
|
+
case BUS_SERVICE_TYPE:
|
|
27979
|
+
return 'ui-bus';
|
|
27980
|
+
case TRAIN_SERVICE_TYPE:
|
|
27981
|
+
return 'ui-train';
|
|
27982
|
+
case TRANSFER_SERVICE_TYPE:
|
|
27983
|
+
return 'ui-transfer';
|
|
27984
|
+
case EVENT_SERVICE_TYPE:
|
|
27985
|
+
return 'ui-ticket';
|
|
27986
|
+
case TRAINING_SERVICE_TYPE:
|
|
27987
|
+
return 'ui-group';
|
|
27988
|
+
default:
|
|
27989
|
+
return 'ui-location';
|
|
27990
|
+
}
|
|
27991
|
+
};
|
|
27992
|
+
const getServiceTypeBadgeModifier = (serviceType) => {
|
|
27993
|
+
switch (serviceType) {
|
|
27994
|
+
case FLIGHT_SERVICE_TYPE:
|
|
27995
|
+
case CHARTER_SERVICE_TYPE:
|
|
27996
|
+
case CRUISE_SERVICE_TYPE:
|
|
27997
|
+
case FERRY_SERVICE_TYPE:
|
|
27998
|
+
return 'flight';
|
|
27999
|
+
case ACCOMMODATION_SERVICE_TYPE:
|
|
28000
|
+
return 'hotel';
|
|
28001
|
+
case EXCURSION_SERVICE_TYPE:
|
|
28002
|
+
case EVENT_SERVICE_TYPE:
|
|
28003
|
+
return 'excursion';
|
|
28004
|
+
case RESTAURANT_SERVICE_TYPE:
|
|
28005
|
+
return 'restaurant';
|
|
28006
|
+
default:
|
|
28007
|
+
return '';
|
|
28008
|
+
}
|
|
28009
|
+
};
|
|
28010
|
+
const POI_TRAJECT_NODE_TYPE = 2;
|
|
28011
|
+
const getTrajectNodeIcon = (node) => node.trajectNodeType === POI_TRAJECT_NODE_TYPE ? 'ui-location' : getServiceTypeIcon(node.serviceType);
|
|
28012
|
+
const getTrajectNodeBadgeModifier = (node) => node.trajectNodeType === POI_TRAJECT_NODE_TYPE ? 'poi' : getServiceTypeBadgeModifier(node.serviceType);
|
|
28013
|
+
|
|
28014
|
+
const REGULAR_TRAJECT_NODE_TYPE = 0;
|
|
28015
|
+
const CONNECTING_TRAJECT_NODE_TYPE = 1;
|
|
28016
|
+
const isSearchableTrajectNode = (node) => node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE;
|
|
28017
|
+
const isTrajectFlightNode = (node) => node.serviceType === FLIGHT_SERVICE_TYPE;
|
|
28018
|
+
const getTrajectNodesWithLines = (trajectEntry) => {
|
|
28019
|
+
const linesByGuid = new Map((trajectEntry.entry.lines ?? []).map((line) => [line.guid, line]));
|
|
28020
|
+
return [...(trajectEntry.nodes ?? [])]
|
|
28021
|
+
.sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
|
|
28022
|
+
.map((node) => {
|
|
28023
|
+
const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
|
|
28024
|
+
const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
|
|
28025
|
+
return { node, line: lines[0], lines };
|
|
28026
|
+
})
|
|
28027
|
+
.filter((x) => !!x.line);
|
|
28028
|
+
};
|
|
28029
|
+
const getCoordinates = (line, node, result) => {
|
|
28030
|
+
const sources = [
|
|
28031
|
+
...(result ? [result] : []),
|
|
28032
|
+
...(line ? [line] : []),
|
|
28033
|
+
...(node?.alternatives ?? []).filter((x) => x.isPreferred),
|
|
28034
|
+
...(node?.alternatives ?? [])
|
|
28035
|
+
];
|
|
28036
|
+
const found = sources.find((x) => !!x.latitude && !!x.longitude);
|
|
28037
|
+
return found ? { latitude: found.latitude, longitude: found.longitude } : null;
|
|
28038
|
+
};
|
|
28039
|
+
const buildDestination = (line, node, result) => {
|
|
28040
|
+
const coordinates = getCoordinates(line, node, result);
|
|
28041
|
+
const place = (result?.locationId && { id: result.locationId, isLocation: true }) ||
|
|
28042
|
+
(result?.oordId && { id: result.oordId, isOord: true }) ||
|
|
28043
|
+
(result?.regionId && { id: result.regionId, isRegion: true }) ||
|
|
28044
|
+
(result?.countryId && { id: result.countryId, isCountry: true }) ||
|
|
28045
|
+
(line.location?.id && { id: line.location.id, isLocation: true }) ||
|
|
28046
|
+
(line.oord?.id && { id: line.oord.id, isOord: true }) ||
|
|
28047
|
+
(line.region?.id && { id: line.region.id, isRegion: true }) ||
|
|
28048
|
+
(line.country?.id && { id: line.country.id, isCountry: true }) ||
|
|
28049
|
+
null;
|
|
28050
|
+
return { id: 0, ...place, ...coordinates };
|
|
28051
|
+
};
|
|
28052
|
+
const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
|
|
28053
|
+
transactionId: context.transactionId,
|
|
28054
|
+
officeId: context.officeId,
|
|
28055
|
+
agentId: context.agentId ?? null,
|
|
28056
|
+
catalogueId: context.catalogueId,
|
|
28057
|
+
searchConfigurationId: context.searchConfigurationId,
|
|
28058
|
+
portalId: context.portalId ?? null,
|
|
28059
|
+
vendorConfigurationId: node.externalVendorId ?? null,
|
|
28060
|
+
language: context.language,
|
|
28061
|
+
serviceType: node.serviceType,
|
|
28062
|
+
fromDate: toDateOnlyString(line.from),
|
|
28063
|
+
toDate: toDateOnlyString(line.to),
|
|
28064
|
+
destination: buildDestination(line, node),
|
|
28065
|
+
productCode,
|
|
28066
|
+
rooms: context.rooms,
|
|
28067
|
+
tagIds: []
|
|
28068
|
+
});
|
|
28069
|
+
/**
|
|
28070
|
+
* Entry lines for an excursion the traveller picked out of the suggestions.
|
|
28071
|
+
*
|
|
28072
|
+
* A traject node already has its lines and only needs stamping; a suggestion has none, so this
|
|
28073
|
+
* builds them — one per selected option, as the regular day-by-day excursion flow does, since an
|
|
28074
|
+
* excursion's options are ticket types (adult, child) rather than rooms. Where the search marks no
|
|
28075
|
+
* default, the first option of each room stands in.
|
|
28076
|
+
*/
|
|
28077
|
+
const buildTrajectExcursionLines = (result) => {
|
|
28078
|
+
const options = (result.rooms ?? []).flatMap((room) => {
|
|
28079
|
+
const selected = (room.options ?? []).filter((x) => x.isSelected);
|
|
28080
|
+
return selected.length ? selected : (room.options ?? []).slice(0, 1);
|
|
28081
|
+
});
|
|
28082
|
+
if (!options.length)
|
|
28083
|
+
return [];
|
|
28084
|
+
const guids = options.map((option) => option.guid || crypto.randomUUID());
|
|
28085
|
+
return options.map((option, index) => ({
|
|
28086
|
+
guid: guids[index],
|
|
28087
|
+
moment: '',
|
|
28088
|
+
parentGuid: index === 0 ? null : guids[0],
|
|
28089
|
+
order: index,
|
|
28090
|
+
isChanged: true,
|
|
28091
|
+
from: result.fromDate,
|
|
28092
|
+
to: result.toDate,
|
|
28093
|
+
serviceType: EXCURSION_SERVICE_TYPE,
|
|
28094
|
+
productName: result.name,
|
|
28095
|
+
productCode: result.code,
|
|
28096
|
+
accommodationName: option.accommodationName,
|
|
28097
|
+
accommodationCode: option.accommodationCode,
|
|
28098
|
+
regimeName: option.regimeName,
|
|
28099
|
+
regimeCode: option.regimeCode,
|
|
28100
|
+
country: result.countryId ? { id: result.countryId, name: result.countryName, localizations: [] } : null,
|
|
28101
|
+
region: result.regionId ? { id: result.regionId, name: result.regionName, localizations: [] } : null,
|
|
28102
|
+
oord: result.oordId ? { id: result.oordId, name: result.oordName, localizations: [] } : null,
|
|
28103
|
+
location: result.locationId ? { id: result.locationId, name: result.locationName, localizations: [] } : null,
|
|
28104
|
+
longitude: result.longitude ?? null,
|
|
28105
|
+
latitude: result.latitude ?? null,
|
|
28106
|
+
pax: (option.paxIds ?? []).map((paxId, paxIndex) => ({ paxId, room: 0, order: paxIndex })),
|
|
28107
|
+
flightInformation: null
|
|
28108
|
+
}));
|
|
28109
|
+
};
|
|
28110
|
+
const buildDayActivityCards = (items, availability, suggestions) => {
|
|
28111
|
+
const cards = [];
|
|
28112
|
+
const seen = new Set();
|
|
28113
|
+
const push = (result, item) => {
|
|
28114
|
+
if (!result || seen.has(result.code))
|
|
28115
|
+
return;
|
|
28116
|
+
seen.add(result.code);
|
|
28117
|
+
cards.push({ result, item });
|
|
28118
|
+
};
|
|
28119
|
+
items.forEach((item) => {
|
|
28120
|
+
const results = availability[item.line.guid]?.results ?? [];
|
|
28121
|
+
const selected = results.find((x) => x.code === availability[item.line.guid]?.selectedCode);
|
|
28122
|
+
push(selected, item);
|
|
28123
|
+
results.filter((x) => !!findCandidateForResult(item.node, x)).forEach((result) => push(result, item));
|
|
28124
|
+
});
|
|
28125
|
+
suggestions.forEach((result) => push(result));
|
|
28126
|
+
// Whatever the nodes' own destination search found and the day's search did not — normally
|
|
28127
|
+
// nothing, as they are the same search, but a node can resolve where a day has no place to look.
|
|
28128
|
+
items.forEach((item) => (availability[item.line.guid]?.results ?? []).forEach((result) => push(result, item)));
|
|
28129
|
+
return cards;
|
|
28130
|
+
};
|
|
28131
|
+
const getNodeCandidates = (node) => {
|
|
28132
|
+
const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
|
|
28133
|
+
if (configured.length)
|
|
28134
|
+
return configured;
|
|
28135
|
+
const fallbackCode = node.code ?? node.preferredAccommodationCode;
|
|
28136
|
+
if (!fallbackCode || !fallbackCode.trim().length)
|
|
28137
|
+
return [];
|
|
28138
|
+
return [
|
|
28139
|
+
{
|
|
28140
|
+
code: fallbackCode,
|
|
28141
|
+
name: node.name,
|
|
28142
|
+
description: node.description,
|
|
28143
|
+
imageUrl: node.imageUrl,
|
|
28144
|
+
contentSource: node.contentSource,
|
|
28145
|
+
vendor: node.vendor,
|
|
28146
|
+
externalVendorId: node.externalVendorId,
|
|
28147
|
+
isPreferred: true
|
|
28148
|
+
}
|
|
28149
|
+
];
|
|
28150
|
+
};
|
|
28151
|
+
const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
|
|
28152
|
+
const matchesCode = (result, code) => {
|
|
28153
|
+
if (result.code === code)
|
|
28154
|
+
return true;
|
|
28155
|
+
return (result.rooms ?? []).some((room) => (room.options ?? []).some((option) => option.accommodationCode === code));
|
|
28156
|
+
};
|
|
28157
|
+
const getPackagingRoomsFromEntry = (entry) => {
|
|
28158
|
+
const paxById = new Map((entry.pax ?? []).map((p) => [p.id, p]));
|
|
28159
|
+
const rooms = (entry.rooms ?? []).map((room) => ({
|
|
28160
|
+
travellers: (room.paxIds ?? []).map((paxId) => {
|
|
28161
|
+
const pax = paxById.get(paxId);
|
|
28162
|
+
return {
|
|
28163
|
+
id: paxId,
|
|
28164
|
+
age: pax?.age ?? null,
|
|
28165
|
+
dateOfBirth: pax?.dateOfBirth ?? null
|
|
28166
|
+
};
|
|
28167
|
+
})
|
|
28168
|
+
}));
|
|
28169
|
+
return rooms.filter((room) => room.travellers.length > 0);
|
|
28170
|
+
};
|
|
28171
|
+
const pickCheapest = (results) => [...results].sort((a, b) => a.price - b.price)[0] ?? null;
|
|
28172
|
+
const applyResultToLines = (lines, result) => lines.map((line, index) => applyResultToLine(line, result, index));
|
|
28173
|
+
const getOptionForRoom = (result, roomIndex) => {
|
|
28174
|
+
const room = (result.rooms ?? [])[roomIndex];
|
|
28175
|
+
if (room)
|
|
28176
|
+
return (room.options ?? []).find((x) => x.isSelected) ?? (room.options ?? [])[0];
|
|
28177
|
+
return (result.rooms ?? []).flatMap((x) => x.options ?? []).find((x) => x.isSelected) ?? (result.rooms ?? [])[0]?.options?.[0];
|
|
28178
|
+
};
|
|
28179
|
+
const applyResultToLine = (line, result, roomIndex = 0) => {
|
|
28180
|
+
const option = getOptionForRoom(result, roomIndex);
|
|
28181
|
+
return {
|
|
28182
|
+
...line,
|
|
28183
|
+
productName: result.name ?? line.productName,
|
|
28184
|
+
productCode: result.code ?? line.productCode,
|
|
28185
|
+
accommodationCode: option?.accommodationCode ?? line.accommodationCode,
|
|
28186
|
+
accommodationName: option?.accommodationName ?? line.accommodationName,
|
|
28187
|
+
regimeCode: option?.regimeCode ?? line.regimeCode,
|
|
28188
|
+
regimeName: option?.regimeName ?? line.regimeName,
|
|
28189
|
+
latitude: result.latitude ?? line.latitude,
|
|
28190
|
+
longitude: result.longitude ?? line.longitude,
|
|
28191
|
+
isChanged: true
|
|
28192
|
+
};
|
|
28193
|
+
};
|
|
28194
|
+
const toDateOnlyUtcString = (value) => {
|
|
28195
|
+
const date = new Date(value);
|
|
28196
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())).toISOString();
|
|
28197
|
+
};
|
|
28198
|
+
const toTimeOnlyString = (value) => {
|
|
28199
|
+
const date = new Date(value);
|
|
28200
|
+
const hh = String(date.getUTCHours()).padStart(2, '0');
|
|
28201
|
+
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
|
28202
|
+
const ss = String(date.getUTCSeconds()).padStart(2, '0');
|
|
28203
|
+
return `${hh}:${mm}:${ss}`;
|
|
28204
|
+
};
|
|
28205
|
+
const mapFlightSegmentsToFlightLines = (segments) => segments.map((segment) => ({
|
|
28206
|
+
airlineCode: segment.marketingAirlineCode,
|
|
28207
|
+
airlineDescription: segment.marketingAirlineName,
|
|
28208
|
+
operatingAirlineCode: segment.operatingAirlineCode,
|
|
28209
|
+
operatingAirlineDescription: segment.operatingAirlineName,
|
|
28210
|
+
flightNumber: segment.flightNumber,
|
|
28211
|
+
operatingFlightNumber: segment.operatingFlightNumber ?? null,
|
|
28212
|
+
departureDate: toDateOnlyUtcString(segment.departureDateTime),
|
|
28213
|
+
departureTime: toTimeOnlyString(segment.departureDateTime),
|
|
28214
|
+
departureAirportCode: segment.departureAirportCode,
|
|
28215
|
+
departureAirportDescription: segment.departureAirportName,
|
|
28216
|
+
arrivalDate: toDateOnlyUtcString(segment.arrivalDateTime),
|
|
28217
|
+
arrivalTime: toTimeOnlyString(segment.arrivalDateTime),
|
|
28218
|
+
arrivalAirportCode: segment.arrivalAirportCode,
|
|
28219
|
+
arrivalAirportDescription: segment.arrivalAirportName,
|
|
28220
|
+
durationInTicks: segment.durationInTicks
|
|
28221
|
+
}));
|
|
28222
|
+
const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
|
|
28223
|
+
transactionId: context.transactionId,
|
|
28224
|
+
officeId: context.officeId,
|
|
28225
|
+
catalogueId: context.catalogueId,
|
|
28226
|
+
agentId: context.agentId ?? null,
|
|
28227
|
+
language: context.language,
|
|
28228
|
+
departureAirportCode: node.departureAirportCode ?? '',
|
|
28229
|
+
arrivalAirportCode: node.arrivalAirportCode ?? '',
|
|
28230
|
+
returnAirportCode: null,
|
|
28231
|
+
luggageIncluded: null,
|
|
28232
|
+
maxStops: null,
|
|
28233
|
+
travelClass: null,
|
|
28234
|
+
vendorConfigurationId: node.externalVendorId ?? null,
|
|
28235
|
+
pax,
|
|
28236
|
+
outward: { date: dateToDateStruct(new Date(line.from)) },
|
|
28237
|
+
return: null
|
|
28238
|
+
});
|
|
28239
|
+
const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
|
|
28240
|
+
const applyFlightToLine = (line, flight) => {
|
|
28241
|
+
const segments = flight.outward?.segments ?? [];
|
|
28242
|
+
if (!segments.length)
|
|
28243
|
+
return line;
|
|
28244
|
+
const firstSegment = segments[0];
|
|
28245
|
+
const lastSegment = segments[segments.length - 1];
|
|
28246
|
+
return {
|
|
28247
|
+
...line,
|
|
28248
|
+
from: new Date(firstSegment.departureDateTime).toISOString(),
|
|
28249
|
+
to: new Date(lastSegment.arrivalDateTime).toISOString(),
|
|
28250
|
+
productName: `${firstSegment.departureAirportName} - ${lastSegment.arrivalAirportName} (${firstSegment.marketingAirlineName})`,
|
|
28251
|
+
productCode: `${firstSegment.departureAirportCode} ${lastSegment.arrivalAirportCode}/${firstSegment.marketingAirlineCode}`,
|
|
28252
|
+
accommodationName: firstSegment.metaData?.farePriceClassName ?? line.accommodationName,
|
|
28253
|
+
accommodationCode: firstSegment.metaData?.fareCode ?? line.accommodationCode,
|
|
28254
|
+
flightInformation: {
|
|
28255
|
+
pnr: '',
|
|
28256
|
+
flightLines: mapFlightSegmentsToFlightLines(segments)
|
|
28257
|
+
},
|
|
28258
|
+
isChanged: true
|
|
28259
|
+
};
|
|
28260
|
+
};
|
|
28261
|
+
/**
|
|
28262
|
+
* Externally sourced products carry a vendor configuration; products held internally in Tide have
|
|
28263
|
+
* none. `vendorType` is not a reliable signal — the packaging service nulls it out when the search
|
|
28264
|
+
* configuration has ExposeProvider off — so the vendor ids are what we go on.
|
|
28265
|
+
*/
|
|
28266
|
+
const isExternalResult = (result) => result.vendorId != null || result.vendorConfigurationId != null;
|
|
28267
|
+
const isExternalCandidate = (candidate) => candidate.externalVendorId != null || candidate.vendor != null;
|
|
28268
|
+
|
|
28269
|
+
const badgeClass = (kind, modifier) => {
|
|
28270
|
+
const base = `search__filter__itinerary__${kind}-badge`;
|
|
28271
|
+
const fallback = kind === 'segment' ? `${base}--secondary` : `${base}--default`;
|
|
28272
|
+
return `${base} ${modifier ? `${base}--${modifier}` : fallback}`;
|
|
28273
|
+
};
|
|
28274
|
+
const nightsOf = (node, line) => dateFns.differenceInCalendarDays(new Date(line?.to ?? node.to), new Date(line?.from ?? node.from));
|
|
28275
|
+
const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
|
|
28276
|
+
const timeline = React.useMemo(() => {
|
|
28277
|
+
const linesByGuid = new Map((packagingEntry.lines ?? []).map((line) => [line.guid, line]));
|
|
28278
|
+
const nodes = [...(trajectEntry.nodes ?? [])]
|
|
28279
|
+
.sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
|
|
28280
|
+
.map((node) => {
|
|
28281
|
+
const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
|
|
28282
|
+
const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
|
|
28283
|
+
return { node, lines };
|
|
28284
|
+
});
|
|
28285
|
+
const claimed = new Set(nodes.flatMap((item) => item.lines.map((line) => line.guid)));
|
|
28286
|
+
const extras = (packagingEntry.lines ?? []).filter((line) => !claimed.has(line.guid));
|
|
28287
|
+
if (!extras.length)
|
|
28288
|
+
return nodes;
|
|
28289
|
+
const dayOrderOf = (date) => trajectEntry.traject.days.find((day) => dateFns.isSameDay(new Date(day.date), new Date(date)))?.order ??
|
|
28290
|
+
Math.max(dateFns.differenceInCalendarDays(new Date(date), new Date(trajectEntry.traject.startDate)), 0);
|
|
28291
|
+
// One product bought once, however many option lines it took (an adult and a child ticket).
|
|
28292
|
+
const grouped = new Map();
|
|
28293
|
+
extras.forEach((line) => {
|
|
28294
|
+
const key = `${line.productCode}|${line.from}`;
|
|
28295
|
+
grouped.set(key, [...(grouped.get(key) ?? []), line]);
|
|
28296
|
+
});
|
|
28297
|
+
const byDay = new Map();
|
|
28298
|
+
nodes.forEach((item) => byDay.set(item.node.dayOrder, [...(byDay.get(item.node.dayOrder) ?? []), item]));
|
|
28299
|
+
[...grouped.values()].forEach((lines, index) => {
|
|
28300
|
+
const line = lines[0];
|
|
28301
|
+
const dayOrder = dayOrderOf(line.from);
|
|
28302
|
+
const item = {
|
|
28303
|
+
lines,
|
|
28304
|
+
node: {
|
|
28305
|
+
nodeId: -(index + 1),
|
|
28306
|
+
lineGuid: line.guid,
|
|
28307
|
+
lineGuids: lines.map((x) => x.guid),
|
|
28308
|
+
dayOrder,
|
|
28309
|
+
order: Number.MAX_SAFE_INTEGER,
|
|
28310
|
+
name: line.productName,
|
|
28311
|
+
serviceType: line.serviceType,
|
|
28312
|
+
trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
|
|
28313
|
+
requirementType: 0,
|
|
28314
|
+
contentSource: 0,
|
|
28315
|
+
alternatives: [],
|
|
28316
|
+
from: line.from,
|
|
28317
|
+
to: line.to
|
|
28318
|
+
}
|
|
28319
|
+
};
|
|
28320
|
+
const dayItems = [...(byDay.get(dayOrder) ?? [])];
|
|
28321
|
+
let at = dayItems.length;
|
|
28322
|
+
while (at > 0 && dayItems[at - 1].node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE)
|
|
28323
|
+
at--;
|
|
28324
|
+
dayItems.splice(at, 0, item);
|
|
28325
|
+
byDay.set(dayOrder, dayItems);
|
|
28326
|
+
});
|
|
28327
|
+
return [...byDay.keys()].sort((a, b) => a - b).flatMap((dayOrder) => byDay.get(dayOrder));
|
|
28328
|
+
}, [trajectEntry, packagingEntry]);
|
|
28329
|
+
const stayByDay = React.useMemo(() => {
|
|
28330
|
+
const map = new Map();
|
|
28331
|
+
timeline
|
|
28332
|
+
.filter(({ node }) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === ACCOMMODATION_SERVICE_TYPE)
|
|
28333
|
+
.forEach(({ node, lines }) => {
|
|
28334
|
+
const line = lines[0];
|
|
28335
|
+
const nights = Math.max(nightsOf(node, line), 1);
|
|
28336
|
+
const name = line?.productName || node.preferredAccommodationName || node.name || '';
|
|
28337
|
+
for (let night = 0; night < nights; night++)
|
|
28338
|
+
map.set(node.dayOrder + night, { name, night, nights });
|
|
28339
|
+
});
|
|
28340
|
+
return map;
|
|
28341
|
+
}, [timeline]);
|
|
28342
|
+
const renderDate = (value) => (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" },
|
|
28343
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
28344
|
+
React__default["default"].createElement("strong", null, dateFns.format(new Date(value), 'd'))),
|
|
28345
|
+
React__default["default"].createElement("p", null, dateFns.format(new Date(value), 'MMM'))));
|
|
28346
|
+
const renderConnectingNode = ({ node }) => {
|
|
28347
|
+
const distance = node.distanceInKm ? `${Math.round(node.distanceInKm)} km` : '';
|
|
28348
|
+
return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `connect-${node.nodeId}` },
|
|
28349
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28350
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28351
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28352
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" }),
|
|
28353
|
+
React__default["default"].createElement("div", { className: badgeClass('transport', getTrajectNodeBadgeModifier(node)) },
|
|
28354
|
+
React__default["default"].createElement(Icon, { name: getTrajectNodeIcon(node), height: 15 })),
|
|
28355
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28356
|
+
React__default["default"].createElement("h6", null, node.name || distance),
|
|
28357
|
+
!!node.name && !!distance && React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" }, distance)))));
|
|
28358
|
+
};
|
|
28359
|
+
const renderOvernight = (item, index) => {
|
|
28360
|
+
const { dayOrder } = item.node;
|
|
28361
|
+
const closesTheDay = timeline[index + 1]?.node.dayOrder !== dayOrder;
|
|
28362
|
+
const stay = stayByDay.get(dayOrder);
|
|
28363
|
+
if (!closesTheDay || !stay)
|
|
28364
|
+
return null;
|
|
28365
|
+
return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `overnight-${item.node.nodeId}` },
|
|
28366
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28367
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28368
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28369
|
+
renderDate(item.node.from),
|
|
28370
|
+
React__default["default"].createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(ACCOMMODATION_SERVICE_TYPE)) },
|
|
28371
|
+
React__default["default"].createElement(Icon, { name: "ui-bed", height: 15 })),
|
|
28372
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28373
|
+
React__default["default"].createElement("h6", null, stay.name),
|
|
28374
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" },
|
|
28375
|
+
stay.night + 1,
|
|
28376
|
+
" ",
|
|
28377
|
+
translations.SRP.OF,
|
|
28378
|
+
" ",
|
|
28379
|
+
stay.nights,
|
|
28380
|
+
" ",
|
|
28381
|
+
translations.SRP.NIGHTS)))));
|
|
28382
|
+
};
|
|
28383
|
+
const renderFlightNode = ({ node, lines }) => {
|
|
28384
|
+
const line = lines[0];
|
|
28385
|
+
return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `flight-${node.nodeId}` },
|
|
28386
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28387
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28388
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28389
|
+
renderDate(line?.from ?? node.from),
|
|
28390
|
+
React__default["default"].createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(FLIGHT_SERVICE_TYPE)) },
|
|
28391
|
+
React__default["default"].createElement(Icon, { name: "ui-plane", height: 15 })),
|
|
28392
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28393
|
+
React__default["default"].createElement("h6", null, line?.productName || node.name),
|
|
28394
|
+
!!getDepartureTime(line) && (React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
|
|
28395
|
+
React__default["default"].createElement("span", null,
|
|
28396
|
+
React__default["default"].createElement(Icon, { name: "ui-plane-depart", height: 14 }),
|
|
28397
|
+
" ",
|
|
28398
|
+
React__default["default"].createElement("strong", null, getDepartureTime(line))),
|
|
28399
|
+
' ',
|
|
28400
|
+
"-",
|
|
28401
|
+
' ',
|
|
28402
|
+
React__default["default"].createElement("span", null,
|
|
28403
|
+
React__default["default"].createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
|
|
28404
|
+
" ",
|
|
28405
|
+
React__default["default"].createElement("strong", null, getArrivalTime(line))))),
|
|
28406
|
+
!!getDuration(line) && (React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" },
|
|
28407
|
+
React__default["default"].createElement("span", null,
|
|
28408
|
+
React__default["default"].createElement(Icon, { name: "ui-clock", height: 20 }),
|
|
28409
|
+
" ",
|
|
28410
|
+
getDuration(line))))))));
|
|
28411
|
+
};
|
|
28412
|
+
const renderServiceNode = ({ node, lines }) => {
|
|
28413
|
+
const line = lines[0];
|
|
28414
|
+
const isStay = node.serviceType === ACCOMMODATION_SERVICE_TYPE;
|
|
28415
|
+
const nights = isStay ? nightsOf(node, line) : 0;
|
|
28416
|
+
return (React__default["default"].createElement("div", { className: "search__filter__itinerary__segments", key: `node-${node.nodeId}` },
|
|
28417
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segments__wrapper" },
|
|
28418
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline" },
|
|
28419
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline-line" })),
|
|
28420
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment" },
|
|
28421
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-item" },
|
|
28422
|
+
React__default["default"].createElement("div", { className: `search__filter__itinerary__segment-date ${isStay && nights > 0 ? 'search__filter__itinerary__segment-nights' : ''}` }, isStay && nights > 0 ? (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
28423
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-date-date" },
|
|
28424
|
+
React__default["default"].createElement("strong", null, nights)),
|
|
28425
|
+
React__default["default"].createElement(Icon, { name: "ui-moon", width: 16, height: 16 }))) : (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
28426
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-date-date" },
|
|
28427
|
+
React__default["default"].createElement("strong", null, dateFns.format(new Date(line?.from ?? node.from), 'd'))),
|
|
28428
|
+
React__default["default"].createElement("p", null, dateFns.format(new Date(line?.from ?? node.from), 'MMM'))))),
|
|
28429
|
+
React__default["default"].createElement("div", { className: badgeClass('segment', getTrajectNodeBadgeModifier(node)) },
|
|
28430
|
+
React__default["default"].createElement(Icon, { name: getTrajectNodeIcon(node), width: 15, height: 15 })),
|
|
28431
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details" },
|
|
28432
|
+
React__default["default"].createElement("h6", null, line?.productName || node.preferredAccommodationName || node.name),
|
|
28433
|
+
isStay &&
|
|
28434
|
+
lines.map((roomLine, index) => (React__default["default"].createElement(React__default["default"].Fragment, { key: roomLine.guid },
|
|
28435
|
+
lines.length > 1 && (React__default["default"].createElement("strong", null,
|
|
28436
|
+
translations.SHARED.ROOM,
|
|
28437
|
+
"\u00A0",
|
|
28438
|
+
index + 1)),
|
|
28439
|
+
roomLine.accommodationName && (React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details__room" },
|
|
28440
|
+
React__default["default"].createElement(Icon, { name: "ui-bed", width: 12, height: 12 }),
|
|
28441
|
+
roomLine.accommodationName)),
|
|
28442
|
+
roomLine.regimeName && (React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details__room" },
|
|
28443
|
+
React__default["default"].createElement(Icon, { name: "ui-utensils", width: 12, height: 12 }),
|
|
28444
|
+
roomLine.regimeName))))),
|
|
28445
|
+
!isStay && React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-details-text" }, dateFns.format(new Date(line?.from ?? node.from), 'EEE. d MMM yyyy'))))))));
|
|
28446
|
+
};
|
|
28447
|
+
return (React__default["default"].createElement(React__default["default"].Fragment, null, timeline.map((item, index) => {
|
|
28448
|
+
if (item.node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
|
|
28449
|
+
return (React__default["default"].createElement(React__default["default"].Fragment, { key: `connect-${item.node.nodeId}` },
|
|
28450
|
+
renderConnectingNode(item),
|
|
28451
|
+
renderOvernight(item, index)));
|
|
28452
|
+
}
|
|
28453
|
+
if (item.node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && item.node.serviceType === FLIGHT_SERVICE_TYPE)
|
|
28454
|
+
return renderFlightNode(item);
|
|
28455
|
+
return renderServiceNode(item);
|
|
28456
|
+
})));
|
|
28457
|
+
};
|
|
28458
|
+
|
|
27811
28459
|
const getSegmentIcon = (segment) => {
|
|
27812
28460
|
switch (segment.serviceType) {
|
|
27813
28461
|
case 3:
|
|
@@ -27931,116 +28579,119 @@ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation })
|
|
|
27931
28579
|
dateFns.format(new Date(firstEntryLine.from), 'EEE. d MMM yyyy'),
|
|
27932
28580
|
" - ",
|
|
27933
28581
|
React__default["default"].createElement("strong", null, translations.SRP.START))))),
|
|
27934
|
-
|
|
27935
|
-
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport
|
|
27936
|
-
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline
|
|
27937
|
-
|
|
27938
|
-
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-
|
|
27939
|
-
React__default["default"].createElement("
|
|
27940
|
-
React__default["default"].createElement("
|
|
27941
|
-
|
|
27942
|
-
|
|
27943
|
-
React__default["default"].createElement(
|
|
27944
|
-
|
|
27945
|
-
React__default["default"].createElement("
|
|
27946
|
-
|
|
27947
|
-
React__default["default"].createElement("
|
|
27948
|
-
React__default["default"].createElement(
|
|
27949
|
-
|
|
27950
|
-
|
|
27951
|
-
|
|
27952
|
-
|
|
27953
|
-
|
|
27954
|
-
|
|
27955
|
-
React__default["default"].createElement(
|
|
27956
|
-
|
|
27957
|
-
|
|
27958
|
-
|
|
27959
|
-
React__default["default"].createElement("
|
|
27960
|
-
React__default["default"].createElement(
|
|
27961
|
-
|
|
27962
|
-
|
|
27963
|
-
|
|
27964
|
-
|
|
27965
|
-
|
|
27966
|
-
|
|
27967
|
-
|
|
27968
|
-
|
|
27969
|
-
React__default["default"].createElement("div", { className: "
|
|
27970
|
-
React__default["default"].createElement("div", { className: "
|
|
27971
|
-
React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline
|
|
27972
|
-
|
|
27973
|
-
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment
|
|
27974
|
-
React__default["default"].createElement("div", { className: "
|
|
27975
|
-
React__default["default"].createElement("
|
|
27976
|
-
React__default["default"].createElement("
|
|
27977
|
-
|
|
27978
|
-
|
|
27979
|
-
React__default["default"].createElement(
|
|
27980
|
-
|
|
27981
|
-
React__default["default"].createElement("
|
|
27982
|
-
|
|
27983
|
-
|
|
27984
|
-
|
|
27985
|
-
|
|
27986
|
-
|
|
27987
|
-
|
|
27988
|
-
|
|
27989
|
-
|
|
27990
|
-
|
|
27991
|
-
|
|
27992
|
-
|
|
27993
|
-
|
|
27994
|
-
|
|
27995
|
-
|
|
27996
|
-
|
|
27997
|
-
|
|
27998
|
-
React__default["default"].createElement(
|
|
27999
|
-
|
|
28000
|
-
|
|
28001
|
-
|
|
28002
|
-
firstSegment
|
|
28003
|
-
|
|
28004
|
-
|
|
28005
|
-
|
|
28006
|
-
|
|
28007
|
-
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
28011
|
-
|
|
28012
|
-
|
|
28013
|
-
|
|
28014
|
-
|
|
28015
|
-
|
|
28016
|
-
|
|
28017
|
-
|
|
28018
|
-
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport
|
|
28019
|
-
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-
|
|
28020
|
-
React__default["default"].createElement("
|
|
28021
|
-
|
|
28022
|
-
React__default["default"].createElement("
|
|
28023
|
-
|
|
28024
|
-
|
|
28025
|
-
|
|
28026
|
-
React__default["default"].createElement("
|
|
28027
|
-
|
|
28028
|
-
|
|
28029
|
-
|
|
28030
|
-
|
|
28031
|
-
React__default["default"].createElement("
|
|
28032
|
-
|
|
28033
|
-
|
|
28034
|
-
|
|
28035
|
-
|
|
28036
|
-
|
|
28037
|
-
|
|
28038
|
-
React__default["default"].createElement("
|
|
28039
|
-
|
|
28040
|
-
|
|
28041
|
-
|
|
28042
|
-
|
|
28043
|
-
|
|
28582
|
+
context?.trajectEntry ? (React__default["default"].createElement(TrajectItinerary, { trajectEntry: context.trajectEntry, packagingEntry: packagingEntry, translations: translations })) : (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
28583
|
+
outboundFlight && (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport" },
|
|
28584
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28585
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28586
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28587
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" },
|
|
28588
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
28589
|
+
React__default["default"].createElement("strong", null, dateFns.format(new Date(outboundFlight.from), 'd'))),
|
|
28590
|
+
React__default["default"].createElement("p", null, dateFns.format(new Date(outboundFlight.from), 'MMM'))),
|
|
28591
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-badge" },
|
|
28592
|
+
React__default["default"].createElement(Icon, { name: "ui-plane", height: 15 })),
|
|
28593
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28594
|
+
React__default["default"].createElement("h6", null, outboundFlight.productName),
|
|
28595
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
|
|
28596
|
+
React__default["default"].createElement("span", null,
|
|
28597
|
+
React__default["default"].createElement(Icon, { name: "ui-plane-depart", height: 14 }),
|
|
28598
|
+
" ",
|
|
28599
|
+
React__default["default"].createElement("strong", null, getDepartureTime(outboundFlight))),
|
|
28600
|
+
' ',
|
|
28601
|
+
"-",
|
|
28602
|
+
' ',
|
|
28603
|
+
React__default["default"].createElement("span", null,
|
|
28604
|
+
React__default["default"].createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
|
|
28605
|
+
" ",
|
|
28606
|
+
React__default["default"].createElement("strong", null, getArrivalTime(outboundFlight)))),
|
|
28607
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" },
|
|
28608
|
+
React__default["default"].createElement("span", null,
|
|
28609
|
+
React__default["default"].createElement(Icon, { name: "ui-clock", height: 20 }),
|
|
28610
|
+
" ",
|
|
28611
|
+
getDuration(outboundFlight))))))),
|
|
28612
|
+
!lodash.isEmpty(groupedOtherSegments) &&
|
|
28613
|
+
groupedOtherSegments.map((group) => {
|
|
28614
|
+
const firstSegment = lodash.first(group.segments);
|
|
28615
|
+
if (!firstSegment)
|
|
28616
|
+
return null;
|
|
28617
|
+
return (React__default["default"].createElement("div", { className: "search__filter__itinerary__segments" },
|
|
28618
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segments__wrapper", key: group.key },
|
|
28619
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline" },
|
|
28620
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segments-timeline-line" })),
|
|
28621
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment" },
|
|
28622
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-item search__filter__itinerary__segment-item--start" },
|
|
28623
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" },
|
|
28624
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
28625
|
+
React__default["default"].createElement("strong", null, dateFns.format(new Date(firstSegment.from), 'd'))),
|
|
28626
|
+
React__default["default"].createElement("p", null, dateFns.format(new Date(firstSegment.from), 'MMM'))),
|
|
28627
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-badge" },
|
|
28628
|
+
React__default["default"].createElement(Icon, { name: "ui-location", width: 15, height: 15 })),
|
|
28629
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details" },
|
|
28630
|
+
React__default["default"].createElement("h6", null,
|
|
28631
|
+
firstSegment.location?.name || firstSegment.oord?.name || firstSegment.region?.name,
|
|
28632
|
+
(firstSegment.location?.name || firstSegment.oord?.name || firstSegment.region?.name) && firstSegment.country?.name
|
|
28633
|
+
? ', '
|
|
28634
|
+
: '',
|
|
28635
|
+
firstSegment.country?.name),
|
|
28636
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-details-text" },
|
|
28637
|
+
dateFns.format(new Date(firstSegment.from), 'EEE. d MMM yyyy'),
|
|
28638
|
+
"> ",
|
|
28639
|
+
dateFns.format(new Date(firstSegment.to), 'EEE. d MMM yyyy'))))),
|
|
28640
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment" },
|
|
28641
|
+
React__default["default"].createElement("div", { className: `search__filter__itinerary__segment-item ${canEdit(firstSegment) ? 'search__filter__itinerary__segment-item--editable' : ''}`, onClick: () => {
|
|
28642
|
+
if (canEdit(firstSegment) && onEditAccommodation) {
|
|
28643
|
+
onEditAccommodation(group.segments);
|
|
28644
|
+
}
|
|
28645
|
+
} },
|
|
28646
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-date search__filter__itinerary__segment-nights" }, firstSegment.serviceType === ACCOMMODATION_SERVICE_TYPE && (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
28647
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__segment-date-date" },
|
|
28648
|
+
React__default["default"].createElement("strong", null, numberOfNights(firstSegment))),
|
|
28649
|
+
React__default["default"].createElement(Icon, { name: "ui-moon", width: 16, height: 16 })))),
|
|
28650
|
+
getSegmentIcon(firstSegment),
|
|
28651
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details" },
|
|
28652
|
+
React__default["default"].createElement("h6", null, getSegmentTitle(firstSegment)),
|
|
28653
|
+
firstSegment.serviceType === ACCOMMODATION_SERVICE_TYPE &&
|
|
28654
|
+
group.segments.map((segment, index) => (React__default["default"].createElement(React__default["default"].Fragment, { key: segment.guid },
|
|
28655
|
+
React__default["default"].createElement("strong", null,
|
|
28656
|
+
translations.SHARED.ROOM,
|
|
28657
|
+
"\u00A0",
|
|
28658
|
+
index + 1),
|
|
28659
|
+
segment.productName && segment.accommodationName && (React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details__room" },
|
|
28660
|
+
React__default["default"].createElement(Icon, { name: "ui-bed", width: 12, height: 12 }),
|
|
28661
|
+
segment.accommodationName)),
|
|
28662
|
+
segment.regimeName && (React__default["default"].createElement("div", { className: "search__filter__itinerary__segment-details__room" },
|
|
28663
|
+
React__default["default"].createElement(Icon, { name: "ui-utensils", width: 12, height: 12 }),
|
|
28664
|
+
segment.regimeName)))))))))));
|
|
28665
|
+
}),
|
|
28666
|
+
returnFlight && returnFlight !== outboundFlight && (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport" },
|
|
28667
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28668
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28669
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28670
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-date" },
|
|
28671
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
28672
|
+
React__default["default"].createElement("strong", null, dateFns.format(new Date(returnFlight.from), 'd'))),
|
|
28673
|
+
React__default["default"].createElement("p", null, dateFns.format(new Date(returnFlight.from), 'MMM'))),
|
|
28674
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-badge" },
|
|
28675
|
+
React__default["default"].createElement(Icon, { name: "ui-plane", height: 15 })),
|
|
28676
|
+
React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28677
|
+
React__default["default"].createElement("h6", null, returnFlight.productName),
|
|
28678
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
|
|
28679
|
+
React__default["default"].createElement("span", null,
|
|
28680
|
+
React__default["default"].createElement(Icon, { name: "ui-plane-depart", height: 14 }),
|
|
28681
|
+
" ",
|
|
28682
|
+
React__default["default"].createElement("strong", null, getDepartureTime(returnFlight))),
|
|
28683
|
+
' ',
|
|
28684
|
+
"-",
|
|
28685
|
+
' ',
|
|
28686
|
+
React__default["default"].createElement("span", null,
|
|
28687
|
+
React__default["default"].createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
|
|
28688
|
+
" ",
|
|
28689
|
+
React__default["default"].createElement("strong", null, getArrivalTime(returnFlight)))),
|
|
28690
|
+
React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" },
|
|
28691
|
+
React__default["default"].createElement("span", null,
|
|
28692
|
+
React__default["default"].createElement(Icon, { name: "ui-clock", height: 20 }),
|
|
28693
|
+
" ",
|
|
28694
|
+
getDuration(returnFlight))))))))),
|
|
28044
28695
|
lastEntryLine && (React__default["default"].createElement("div", { className: "search__filter__itinerary__country" },
|
|
28045
28696
|
React__default["default"].createElement("div", { className: "search__filter__itinerary__country-icon" },
|
|
28046
28697
|
React__default["default"].createElement(Icon, { name: "ui-flag", width: 17.5, height: 20 })),
|
|
@@ -30603,6 +31254,222 @@ const ExcursionDetails = () => {
|
|
|
30603
31254
|
React__default["default"].createElement("button", { type: "button", className: "cta cta--primary", onClick: handleConfirm }, translations?.QSM.CONFIRM))));
|
|
30604
31255
|
};
|
|
30605
31256
|
|
|
31257
|
+
const getLocation = (result) => {
|
|
31258
|
+
const place = result.locationName || result.regionName || result.oordName;
|
|
31259
|
+
if (!place)
|
|
31260
|
+
return result.countryName ?? '';
|
|
31261
|
+
return result.countryName ? `${place}, ${result.countryName}` : place;
|
|
31262
|
+
};
|
|
31263
|
+
const toPlainText = (value) => {
|
|
31264
|
+
if (!value)
|
|
31265
|
+
return '';
|
|
31266
|
+
return he
|
|
31267
|
+
.decode(value.replace(/<[^>]*>/g, ' '))
|
|
31268
|
+
.replace(/\s+/g, ' ')
|
|
31269
|
+
.trim();
|
|
31270
|
+
};
|
|
31271
|
+
const TrajectNodeCard = ({ node, result, isSelected, languageCode, translations, onSelect, onEditOptions, showNights, selectLabel, selectedLabel }) => {
|
|
31272
|
+
const selectedPerRoom = (result.rooms ?? []).map((room) => (room.options ?? []).find((x) => x.isSelected) ?? (room.options ?? [])[0]);
|
|
31273
|
+
const isMultiRoom = selectedPerRoom.length > 1;
|
|
31274
|
+
const price = formatPrice$3(result.price, result.currencyCode, languageCode);
|
|
31275
|
+
const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
|
|
31276
|
+
const candidate = node ? findCandidateForResult(node, result) : null;
|
|
31277
|
+
const image = candidate?.imageUrl ?? null;
|
|
31278
|
+
const description = toPlainText(candidate?.description);
|
|
31279
|
+
const title = candidate?.name ?? result.name;
|
|
31280
|
+
const priceBlock = (React__default["default"].createElement("div", { className: "search__result-card__price__wrapper" },
|
|
31281
|
+
React__default["default"].createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
|
|
31282
|
+
React__default["default"].createElement("span", { className: "search__result-card__price" }, price)));
|
|
31283
|
+
const canEditOptions = isSelected && !!onEditOptions && (result.rooms ?? []).some((room) => (room.options ?? []).length > 1);
|
|
31284
|
+
const selectButton = (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
31285
|
+
React__default["default"].createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? selectedLabel ?? translations?.SHARED.SELECTED : selectLabel ?? translations?.SHARED.SELECT),
|
|
31286
|
+
canEditOptions && (React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: onEditOptions },
|
|
31287
|
+
translations?.SRP.SELECT,
|
|
31288
|
+
" ",
|
|
31289
|
+
translations?.SRP.ACCOMMODATION))));
|
|
31290
|
+
if (result.contents?.length) {
|
|
31291
|
+
return (React__default["default"].createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
31292
|
+
React__default["default"].createElement("div", { className: "search__result-card__top", dangerouslySetInnerHTML: { __html: he.decode(result.contents) } }),
|
|
31293
|
+
React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton)));
|
|
31294
|
+
}
|
|
31295
|
+
return (React__default["default"].createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
31296
|
+
image && (React__default["default"].createElement("div", { className: "search__result-card__img-wrapper" },
|
|
31297
|
+
React__default["default"].createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
|
|
31298
|
+
priceBlock)),
|
|
31299
|
+
React__default["default"].createElement("div", { className: "search__result-card__content" },
|
|
31300
|
+
React__default["default"].createElement("div", { className: "search__result-card__content__wrapper" },
|
|
31301
|
+
React__default["default"].createElement("div", { className: "search__result-card__header" },
|
|
31302
|
+
React__default["default"].createElement("div", { className: "search__result-card__header__wrapper" },
|
|
31303
|
+
!!result.stars && (React__default["default"].createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default["default"].createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
|
|
31304
|
+
React__default["default"].createElement("h3", { className: "search__result-card__title" }, title)),
|
|
31305
|
+
!image && priceBlock),
|
|
31306
|
+
React__default["default"].createElement("span", { className: "search__result-card__location" },
|
|
31307
|
+
React__default["default"].createElement(Icon, { name: "ui-location", height: 16 }),
|
|
31308
|
+
getLocation(result)),
|
|
31309
|
+
React__default["default"].createElement("div", { className: "search__result-card__options" },
|
|
31310
|
+
showNights && nights > 0 && (React__default["default"].createElement("div", { className: "search__result-card__option" },
|
|
31311
|
+
React__default["default"].createElement(Icon, { name: "ui-moon", height: 16 }),
|
|
31312
|
+
nights,
|
|
31313
|
+
" ",
|
|
31314
|
+
translations?.SRP.NIGHTS)),
|
|
31315
|
+
selectedPerRoom.map((option, roomIndex) => (React__default["default"].createElement(React__default["default"].Fragment, { key: `room-${roomIndex}` },
|
|
31316
|
+
option?.accommodationName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
|
|
31317
|
+
React__default["default"].createElement(Icon, { name: "ui-bed", height: 16 }),
|
|
31318
|
+
isMultiRoom && `${translations?.SHARED.ROOM} ${roomIndex + 1}: `,
|
|
31319
|
+
option.accommodationName)),
|
|
31320
|
+
option?.regimeName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
|
|
31321
|
+
React__default["default"].createElement(Icon, { name: "ui-utensils", height: 16 }),
|
|
31322
|
+
option.regimeName)))))),
|
|
31323
|
+
description && React__default["default"].createElement("p", { className: "search__result-card__description" }, description)),
|
|
31324
|
+
React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton))));
|
|
31325
|
+
};
|
|
31326
|
+
|
|
31327
|
+
const TrajectResultsFlyIn = ({ isLoading }) => {
|
|
31328
|
+
const context = React.useContext(SearchResultsConfigurationContext);
|
|
31329
|
+
const dispatch = reactRedux.useDispatch();
|
|
31330
|
+
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
31331
|
+
const { trajectNodeAvailability, trajectEditLineGuid } = reactRedux.useSelector((state) => state.searchResults);
|
|
31332
|
+
const trajectEntry = context?.trajectEntry;
|
|
31333
|
+
const item = React.useMemo(() => {
|
|
31334
|
+
if (!trajectEntry || !trajectEditLineGuid)
|
|
31335
|
+
return null;
|
|
31336
|
+
return getTrajectNodesWithLines(trajectEntry).find((x) => x.line.guid === trajectEditLineGuid) ?? null;
|
|
31337
|
+
}, [trajectEntry, trajectEditLineGuid]);
|
|
31338
|
+
if (!context || !item)
|
|
31339
|
+
return null;
|
|
31340
|
+
const availability = trajectNodeAvailability[item.line.guid];
|
|
31341
|
+
const results = availability?.results ?? [];
|
|
31342
|
+
const ordered = [...results].sort((a, b) => {
|
|
31343
|
+
const aConfigured = findCandidateForResult(item.node, a) ? 0 : 1;
|
|
31344
|
+
const bConfigured = findCandidateForResult(item.node, b) ? 0 : 1;
|
|
31345
|
+
return aConfigured - bConfigured || a.price - b.price;
|
|
31346
|
+
});
|
|
31347
|
+
const handleSelect = (code) => {
|
|
31348
|
+
const result = results.find((x) => x.code === code);
|
|
31349
|
+
if (!result)
|
|
31350
|
+
return;
|
|
31351
|
+
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
|
|
31352
|
+
applyResultToLines(item.lines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
31353
|
+
dispatch(setTrajectEditLineGuid(null));
|
|
31354
|
+
dispatch(setFlyInIsOpen(false));
|
|
31355
|
+
};
|
|
31356
|
+
if (isLoading) {
|
|
31357
|
+
return React__default["default"].createElement(React__default["default"].Fragment, null, context.customSpinner ?? React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }));
|
|
31358
|
+
}
|
|
31359
|
+
return (React__default["default"].createElement("div", { className: "flyin__content" },
|
|
31360
|
+
React__default["default"].createElement("div", { className: "search__result-row" },
|
|
31361
|
+
React__default["default"].createElement("span", { className: "search__result-row-text" },
|
|
31362
|
+
ordered.length,
|
|
31363
|
+
"\u00A0",
|
|
31364
|
+
translations.SRP.TOTAL_RESULTS_LABEL)),
|
|
31365
|
+
ordered.length === 0 ? (React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)) : (React__default["default"].createElement("div", { className: "search__results__cards" }, ordered.map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, node: item.node, result: result, isSelected: result.code === availability?.selectedCode, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(result.code), showNights: item.node.serviceType === ACCOMMODATION_SERVICE_TYPE })))))));
|
|
31366
|
+
};
|
|
31367
|
+
|
|
31368
|
+
/**
|
|
31369
|
+
* Every flight found for one traject leg, shown in the fly-in.
|
|
31370
|
+
*
|
|
31371
|
+
* A one-way search can return far more options than belong in the results column, so the column
|
|
31372
|
+
* keeps three and hands the rest to this list — the same shape the regular flight results use.
|
|
31373
|
+
* Each leg is searched on its own, so the single leg of a result always sits in `outward`.
|
|
31374
|
+
*/
|
|
31375
|
+
const TrajectFlightsFlyIn = ({ isLoading }) => {
|
|
31376
|
+
const context = React.useContext(SearchResultsConfigurationContext);
|
|
31377
|
+
const dispatch = reactRedux.useDispatch();
|
|
31378
|
+
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
31379
|
+
const { trajectFlightAvailability, trajectEditLineGuid, flightsLoading } = reactRedux.useSelector((state) => state.searchResults);
|
|
31380
|
+
const trajectEntry = context?.trajectEntry;
|
|
31381
|
+
const item = React.useMemo(() => {
|
|
31382
|
+
if (!trajectEntry || !trajectEditLineGuid)
|
|
31383
|
+
return null;
|
|
31384
|
+
return getTrajectNodesWithLines(trajectEntry).find((x) => x.line.guid === trajectEditLineGuid) ?? null;
|
|
31385
|
+
}, [trajectEntry, trajectEditLineGuid]);
|
|
31386
|
+
if (!context || !item)
|
|
31387
|
+
return null;
|
|
31388
|
+
const availability = trajectFlightAvailability[item.line.guid];
|
|
31389
|
+
const flights = availability?.results ?? [];
|
|
31390
|
+
const selectedGuid = availability?.selectedGuid;
|
|
31391
|
+
const selected = flights.find((x) => x.outwardGuid === selectedGuid) ?? null;
|
|
31392
|
+
const handleSelect = (outwardGuid) => {
|
|
31393
|
+
const flight = flights.find((x) => x.outwardGuid === outwardGuid);
|
|
31394
|
+
if (!flight)
|
|
31395
|
+
return;
|
|
31396
|
+
dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
|
|
31397
|
+
dispatch(updateEditableEntryLine(applyFlightToLine(item.line, flight)));
|
|
31398
|
+
dispatch(setTrajectEditLineGuid(null));
|
|
31399
|
+
dispatch(setFlyInIsOpen(false));
|
|
31400
|
+
};
|
|
31401
|
+
if (isLoading || flightsLoading) {
|
|
31402
|
+
return React__default["default"].createElement(React__default["default"].Fragment, null, context.customSpinner ?? React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS }));
|
|
31403
|
+
}
|
|
31404
|
+
// The picked flight stays at the top; the rest follow by price.
|
|
31405
|
+
const ordered = [...flights].sort((a, b) => {
|
|
31406
|
+
if (a.outwardGuid === selectedGuid)
|
|
31407
|
+
return -1;
|
|
31408
|
+
if (b.outwardGuid === selectedGuid)
|
|
31409
|
+
return 1;
|
|
31410
|
+
return a.price - b.price;
|
|
31411
|
+
});
|
|
31412
|
+
return (React__default["default"].createElement("div", { className: "flyin__content" },
|
|
31413
|
+
React__default["default"].createElement("div", { className: "search__result-row" },
|
|
31414
|
+
React__default["default"].createElement("span", { className: "search__result-row-text" },
|
|
31415
|
+
ordered.length,
|
|
31416
|
+
"\u00A0",
|
|
31417
|
+
translations.SRP.TOTAL_RESULTS_LABEL)),
|
|
31418
|
+
ordered.length === 0 ? (React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)) : (React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" }, ordered.map((result) => (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: result.outward, guid: result.outwardGuid, selectedGuid: selectedGuid ?? undefined, isOutward: true, showSelectedState: true, price: result.price, currentSelectedPrice: selected?.price, onSelect: result.outwardGuid === selectedGuid ? undefined : () => handleSelect(result.outwardGuid) })))))));
|
|
31419
|
+
};
|
|
31420
|
+
|
|
31421
|
+
const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
|
|
31422
|
+
const context = React.useContext(SearchResultsConfigurationContext);
|
|
31423
|
+
const dispatch = reactRedux.useDispatch();
|
|
31424
|
+
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
31425
|
+
const { trajectNodeAvailability, trajectDaySuggestions, trajectActivitiesDayOrder, editablePackagingEntry } = reactRedux.useSelector((state) => state.searchResults);
|
|
31426
|
+
const trajectEntry = context?.trajectEntry;
|
|
31427
|
+
const items = React.useMemo(() => {
|
|
31428
|
+
if (!trajectEntry || trajectActivitiesDayOrder === null)
|
|
31429
|
+
return [];
|
|
31430
|
+
return getTrajectNodesWithLines(trajectEntry).filter(({ node }) => node.dayOrder === trajectActivitiesDayOrder && node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === EXCURSION_SERVICE_TYPE);
|
|
31431
|
+
}, [trajectEntry, trajectActivitiesDayOrder]);
|
|
31432
|
+
if (!context || trajectActivitiesDayOrder === null)
|
|
31433
|
+
return null;
|
|
31434
|
+
const suggestions = trajectDaySuggestions[trajectActivitiesDayOrder];
|
|
31435
|
+
const cards = buildDayActivityCards(items, trajectNodeAvailability, suggestions?.results ?? []);
|
|
31436
|
+
const getAddedLines = (result) => (editablePackagingEntry?.lines ?? []).filter((line) => line.serviceType === EXCURSION_SERVICE_TYPE && line.productCode === result.code && dateFns.isSameDay(new Date(line.from), new Date(result.fromDate)));
|
|
31437
|
+
/** Adding is not a decision that ends the list — the traveller may want several. */
|
|
31438
|
+
const handleToggle = (result) => {
|
|
31439
|
+
const added = getAddedLines(result);
|
|
31440
|
+
if (added.length) {
|
|
31441
|
+
dispatch(removeEditableEntryLines(added.map((line) => line.guid)));
|
|
31442
|
+
return;
|
|
31443
|
+
}
|
|
31444
|
+
const lines = buildTrajectExcursionLines(result);
|
|
31445
|
+
if (lines.length)
|
|
31446
|
+
dispatch(addEditableEntryLines(lines));
|
|
31447
|
+
};
|
|
31448
|
+
/** Swapping what the traject books is one decision, so the list closes behind it. */
|
|
31449
|
+
const handleSelect = ({ result, item }) => {
|
|
31450
|
+
if (!item)
|
|
31451
|
+
return;
|
|
31452
|
+
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code: result.code }));
|
|
31453
|
+
applyResultToLines(item.lines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
31454
|
+
dispatch(setTrajectActivitiesDayOrder(null));
|
|
31455
|
+
dispatch(setFlyInIsOpen(false));
|
|
31456
|
+
};
|
|
31457
|
+
if (isLoading || suggestions?.status === 'loading') {
|
|
31458
|
+
return React__default["default"].createElement(React__default["default"].Fragment, null, context.customSpinner ?? React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_EXCURSIONS }));
|
|
31459
|
+
}
|
|
31460
|
+
return (React__default["default"].createElement("div", { className: "flyin__content" },
|
|
31461
|
+
React__default["default"].createElement("div", { className: "search__result-row" },
|
|
31462
|
+
React__default["default"].createElement("span", { className: "search__result-row-text" },
|
|
31463
|
+
cards.length,
|
|
31464
|
+
"\u00A0",
|
|
31465
|
+
translations.SRP.TOTAL_RESULTS_LABEL)),
|
|
31466
|
+
cards.length === 0 ? (React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)) : (React__default["default"].createElement("div", { className: "search__results__cards" }, cards.map((card) => {
|
|
31467
|
+
const { result, item } = card;
|
|
31468
|
+
const isSelected = item ? trajectNodeAvailability[item.line.guid]?.selectedCode === result.code : getAddedLines(result).length > 0;
|
|
31469
|
+
return (React__default["default"].createElement(TrajectNodeCard, { key: `activity-${result.code}`, node: item?.node, result: result, isSelected: isSelected, languageCode: context.languageCode, translations: translations, onSelect: () => (item ? handleSelect(card) : handleToggle(result)), selectLabel: item ? undefined : translations.SRP.ADD, selectedLabel: item ? undefined : translations.SRP.REMOVE }));
|
|
31470
|
+
})))));
|
|
31471
|
+
};
|
|
31472
|
+
|
|
30606
31473
|
const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, detailsLoading, flyInType, isPackageEditFlow, handleConfirm, sortByTypes, activeSearchSeed, toggleFilters, filtersOpen }) => {
|
|
30607
31474
|
const dispatch = reactRedux.useDispatch();
|
|
30608
31475
|
const context = React.useContext(SearchResultsConfigurationContext);
|
|
@@ -30648,6 +31515,10 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
|
|
|
30648
31515
|
}
|
|
30649
31516
|
};
|
|
30650
31517
|
const handleGoBack = () => {
|
|
31518
|
+
if (context?.trajectEntry) {
|
|
31519
|
+
handleClose();
|
|
31520
|
+
return;
|
|
31521
|
+
}
|
|
30651
31522
|
if (flyInType === 'acco-details') {
|
|
30652
31523
|
dispatch(setFlyInType('acco-results'));
|
|
30653
31524
|
}
|
|
@@ -30671,8 +31542,8 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
|
|
|
30671
31542
|
dispatch(setBookPackagingEntry(true));
|
|
30672
31543
|
}
|
|
30673
31544
|
};
|
|
30674
|
-
return (React__default["default"].createElement("div", { className: `flyin ${isOpen ? 'flyin--active' : ''} ${className} ${isPackageEditFlow || flyInType === 'acco-results' ? 'flyin--large' : ''} ${flyInType === 'excursion-results' || flyInType === 'excursion-details' ? 'flyin--medium' : ''}
|
|
30675
|
-
${flyInType === 'flight-outward-results' || flyInType === 'flight-return-results' ? 'flyin--flight' : ''}` },
|
|
31545
|
+
return (React__default["default"].createElement("div", { className: `flyin ${isOpen ? 'flyin--active' : ''} ${className} ${isPackageEditFlow || flyInType === 'acco-results' || flyInType === 'traject-day-activities' ? 'flyin--large' : ''} ${flyInType === 'excursion-results' || flyInType === 'excursion-details' ? 'flyin--medium' : ''}
|
|
31546
|
+
${flyInType === 'flight-outward-results' || flyInType === 'flight-return-results' || flyInType === 'traject-flight-results' ? 'flyin--flight' : ''}` },
|
|
30676
31547
|
React__default["default"].createElement("div", { className: `flyin__panel ${isOpen ? 'flyin__panel--active' : ''}`, ref: panelRef },
|
|
30677
31548
|
React__default["default"].createElement("div", { className: "flyin__content" },
|
|
30678
31549
|
React__default["default"].createElement("div", { className: "flyin__content-title-row" },
|
|
@@ -30689,6 +31560,8 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
|
|
|
30689
31560
|
flyInType === 'flight-return-results' &&
|
|
30690
31561
|
`${translations.SRP.SELECT} ${translations.FLIGHTS_FORM.RETURN_FLIGHT}`,
|
|
30691
31562
|
srpType === build.PortalQsmType.AccommodationAndFlight && flyInType === 'flight-details' && 'Select your fare',
|
|
31563
|
+
flyInType === 'traject-flight-results' && `${translations.SRP.SELECT} ${translations.SRP.DEPARTURE}`,
|
|
31564
|
+
flyInType === 'traject-day-activities' && translations.SRP.ACTIVITIES,
|
|
30692
31565
|
flyInType === 'excursion-results' || (flyInType === 'excursion-details' && 'Select excursion')),
|
|
30693
31566
|
React__default["default"].createElement("span", { className: "flyin__close", onClick: () => handleClose() },
|
|
30694
31567
|
React__default["default"].createElement(Icon, { name: "ui-close", width: 30, height: 30, "aria-hidden": "true" }))),
|
|
@@ -30697,7 +31570,12 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
|
|
|
30697
31570
|
React__default["default"].createElement(Icon, { name: "ui-chevron", width: 14, height: 14, "aria-hidden": "true" }),
|
|
30698
31571
|
"Go Back")))),
|
|
30699
31572
|
srpType === build.PortalQsmType.Flight && React__default["default"].createElement(FlightsFlyIn, { isOpen: isOpen, setIsOpen: setIsOpen }),
|
|
30700
|
-
|
|
31573
|
+
context?.trajectEntry && flyInType === 'acco-results' && React__default["default"].createElement(TrajectResultsFlyIn, { isLoading: detailsLoading }),
|
|
31574
|
+
context?.trajectEntry && flyInType === 'traject-flight-results' && React__default["default"].createElement(TrajectFlightsFlyIn, { isLoading: detailsLoading }),
|
|
31575
|
+
context?.trajectEntry && flyInType === 'traject-day-activities' && React__default["default"].createElement(TrajectDayActivitiesFlyIn, { isLoading: detailsLoading }),
|
|
31576
|
+
!context?.trajectEntry &&
|
|
31577
|
+
(srpType === build.PortalQsmType.Accommodation || srpType === build.PortalQsmType.AccommodationAndFlight) &&
|
|
31578
|
+
flyInType === 'acco-results' && (React__default["default"].createElement("div", { className: "flyin__content flyin__content--columns" },
|
|
30701
31579
|
React__default["default"].createElement(Filters, { initialFilters: initialFilters, filters: filters, isOpen: filtersOpen, handleSetIsOpen: () => toggleFilters && toggleFilters(),
|
|
30702
31580
|
// handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
|
|
30703
31581
|
isLoading: isLoading, setFilters: (filters) => dispatch(setFilters(filters)), resetFilters: (filters) => dispatch(resetFilters(filters)) }),
|
|
@@ -32970,232 +33848,6 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
|
|
|
32970
33848
|
React__default["default"].createElement(WLSidebar, { activeSearchSeed: activeSearchSeed, packagingAccoResult: selectedPackagingAccoResult }))));
|
|
32971
33849
|
};
|
|
32972
33850
|
|
|
32973
|
-
const REGULAR_TRAJECT_NODE_TYPE = 0;
|
|
32974
|
-
const isSearchableTrajectNode = (node) => node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE;
|
|
32975
|
-
const isTrajectFlightNode = (node) => node.serviceType === FLIGHT_SERVICE_TYPE;
|
|
32976
|
-
const getTrajectNodesWithLines = (trajectEntry) => {
|
|
32977
|
-
const linesByGuid = new Map((trajectEntry.entry.lines ?? []).map((line) => [line.guid, line]));
|
|
32978
|
-
return [...(trajectEntry.nodes ?? [])]
|
|
32979
|
-
.sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
|
|
32980
|
-
.map((node) => ({ node, line: linesByGuid.get(node.lineGuid) }))
|
|
32981
|
-
.filter((x) => !!x.line);
|
|
32982
|
-
};
|
|
32983
|
-
const buildDestination = (line) => {
|
|
32984
|
-
if (line.location?.id)
|
|
32985
|
-
return { id: line.location.id, isLocation: true };
|
|
32986
|
-
if (line.oord?.id)
|
|
32987
|
-
return { id: line.oord.id, isOord: true };
|
|
32988
|
-
if (line.region?.id)
|
|
32989
|
-
return { id: line.region.id, isRegion: true };
|
|
32990
|
-
if (line.country?.id)
|
|
32991
|
-
return { id: line.country.id, isCountry: true };
|
|
32992
|
-
if (line.latitude && line.longitude)
|
|
32993
|
-
return { latitude: line.latitude, longitude: line.longitude };
|
|
32994
|
-
return { id: 0 };
|
|
32995
|
-
};
|
|
32996
|
-
const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
|
|
32997
|
-
transactionId: context.transactionId,
|
|
32998
|
-
officeId: context.officeId,
|
|
32999
|
-
agentId: context.agentId ?? null,
|
|
33000
|
-
catalogueId: context.catalogueId,
|
|
33001
|
-
searchConfigurationId: context.searchConfigurationId,
|
|
33002
|
-
portalId: context.portalId ?? null,
|
|
33003
|
-
vendorConfigurationId: node.externalVendorId ?? null,
|
|
33004
|
-
language: context.language,
|
|
33005
|
-
serviceType: node.serviceType,
|
|
33006
|
-
fromDate: toDateOnlyString(line.from),
|
|
33007
|
-
toDate: toDateOnlyString(line.to),
|
|
33008
|
-
destination: buildDestination(line),
|
|
33009
|
-
productCode,
|
|
33010
|
-
rooms: context.rooms,
|
|
33011
|
-
tagIds: []
|
|
33012
|
-
});
|
|
33013
|
-
const getNodeCandidates = (node) => {
|
|
33014
|
-
const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
|
|
33015
|
-
if (configured.length)
|
|
33016
|
-
return configured;
|
|
33017
|
-
const fallbackCode = node.code ?? node.preferredAccommodationCode;
|
|
33018
|
-
if (!fallbackCode || !fallbackCode.trim().length)
|
|
33019
|
-
return [];
|
|
33020
|
-
return [
|
|
33021
|
-
{
|
|
33022
|
-
code: fallbackCode,
|
|
33023
|
-
name: node.name,
|
|
33024
|
-
description: node.description,
|
|
33025
|
-
imageUrl: node.imageUrl,
|
|
33026
|
-
contentSource: node.contentSource,
|
|
33027
|
-
vendor: node.vendor,
|
|
33028
|
-
externalVendorId: node.externalVendorId,
|
|
33029
|
-
isPreferred: true
|
|
33030
|
-
}
|
|
33031
|
-
];
|
|
33032
|
-
};
|
|
33033
|
-
const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
|
|
33034
|
-
const matchesCode = (result, code) => {
|
|
33035
|
-
if (result.code === code)
|
|
33036
|
-
return true;
|
|
33037
|
-
return (result.rooms ?? []).some((room) => (room.options ?? []).some((option) => option.accommodationCode === code));
|
|
33038
|
-
};
|
|
33039
|
-
const getPackagingRoomsFromEntry = (entry) => {
|
|
33040
|
-
const paxById = new Map((entry.pax ?? []).map((p) => [p.id, p]));
|
|
33041
|
-
const rooms = (entry.rooms ?? []).map((room) => ({
|
|
33042
|
-
travellers: (room.paxIds ?? []).map((paxId) => {
|
|
33043
|
-
const pax = paxById.get(paxId);
|
|
33044
|
-
return {
|
|
33045
|
-
id: paxId,
|
|
33046
|
-
age: pax?.age ?? null,
|
|
33047
|
-
dateOfBirth: pax?.dateOfBirth ?? null
|
|
33048
|
-
};
|
|
33049
|
-
})
|
|
33050
|
-
}));
|
|
33051
|
-
return rooms.filter((room) => room.travellers.length > 0);
|
|
33052
|
-
};
|
|
33053
|
-
const pickCheapest = (results) => [...results].sort((a, b) => a.price - b.price)[0] ?? null;
|
|
33054
|
-
const applyResultToLine = (line, result) => {
|
|
33055
|
-
const option = (result.rooms ?? []).flatMap((room) => room.options ?? []).find((x) => x.isSelected) ?? (result.rooms ?? [])[0]?.options?.[0];
|
|
33056
|
-
return {
|
|
33057
|
-
...line,
|
|
33058
|
-
productName: result.name ?? line.productName,
|
|
33059
|
-
productCode: result.code ?? line.productCode,
|
|
33060
|
-
accommodationCode: option?.accommodationCode ?? line.accommodationCode,
|
|
33061
|
-
accommodationName: option?.accommodationName ?? line.accommodationName,
|
|
33062
|
-
regimeCode: option?.regimeCode ?? line.regimeCode,
|
|
33063
|
-
regimeName: option?.regimeName ?? line.regimeName,
|
|
33064
|
-
latitude: result.latitude ?? line.latitude,
|
|
33065
|
-
longitude: result.longitude ?? line.longitude,
|
|
33066
|
-
isChanged: true
|
|
33067
|
-
};
|
|
33068
|
-
};
|
|
33069
|
-
const toDateOnlyUtcString = (value) => {
|
|
33070
|
-
const date = new Date(value);
|
|
33071
|
-
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())).toISOString();
|
|
33072
|
-
};
|
|
33073
|
-
const toTimeOnlyString = (value) => {
|
|
33074
|
-
const date = new Date(value);
|
|
33075
|
-
const hh = String(date.getUTCHours()).padStart(2, '0');
|
|
33076
|
-
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
|
33077
|
-
const ss = String(date.getUTCSeconds()).padStart(2, '0');
|
|
33078
|
-
return `${hh}:${mm}:${ss}`;
|
|
33079
|
-
};
|
|
33080
|
-
const mapFlightSegmentsToFlightLines = (segments) => segments.map((segment) => ({
|
|
33081
|
-
airlineCode: segment.marketingAirlineCode,
|
|
33082
|
-
airlineDescription: segment.marketingAirlineName,
|
|
33083
|
-
operatingAirlineCode: segment.operatingAirlineCode,
|
|
33084
|
-
operatingAirlineDescription: segment.operatingAirlineName,
|
|
33085
|
-
flightNumber: segment.flightNumber,
|
|
33086
|
-
operatingFlightNumber: segment.operatingFlightNumber ?? null,
|
|
33087
|
-
departureDate: toDateOnlyUtcString(segment.departureDateTime),
|
|
33088
|
-
departureTime: toTimeOnlyString(segment.departureDateTime),
|
|
33089
|
-
departureAirportCode: segment.departureAirportCode,
|
|
33090
|
-
departureAirportDescription: segment.departureAirportName,
|
|
33091
|
-
arrivalDate: toDateOnlyUtcString(segment.arrivalDateTime),
|
|
33092
|
-
arrivalTime: toTimeOnlyString(segment.arrivalDateTime),
|
|
33093
|
-
arrivalAirportCode: segment.arrivalAirportCode,
|
|
33094
|
-
arrivalAirportDescription: segment.arrivalAirportName,
|
|
33095
|
-
durationInTicks: segment.durationInTicks
|
|
33096
|
-
}));
|
|
33097
|
-
const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
|
|
33098
|
-
transactionId: context.transactionId,
|
|
33099
|
-
officeId: context.officeId,
|
|
33100
|
-
catalogueId: context.catalogueId,
|
|
33101
|
-
agentId: context.agentId ?? null,
|
|
33102
|
-
language: context.language,
|
|
33103
|
-
departureAirportCode: node.departureAirportCode ?? '',
|
|
33104
|
-
arrivalAirportCode: node.arrivalAirportCode ?? '',
|
|
33105
|
-
returnAirportCode: null,
|
|
33106
|
-
luggageIncluded: null,
|
|
33107
|
-
maxStops: null,
|
|
33108
|
-
travelClass: null,
|
|
33109
|
-
vendorConfigurationId: node.externalVendorId ?? null,
|
|
33110
|
-
pax,
|
|
33111
|
-
outward: { date: dateToDateStruct(new Date(line.from)) },
|
|
33112
|
-
return: null
|
|
33113
|
-
});
|
|
33114
|
-
const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
|
|
33115
|
-
const applyFlightToLine = (line, flight) => {
|
|
33116
|
-
const segments = flight.outward?.segments ?? [];
|
|
33117
|
-
if (!segments.length)
|
|
33118
|
-
return line;
|
|
33119
|
-
const firstSegment = segments[0];
|
|
33120
|
-
const lastSegment = segments[segments.length - 1];
|
|
33121
|
-
return {
|
|
33122
|
-
...line,
|
|
33123
|
-
from: new Date(firstSegment.departureDateTime).toISOString(),
|
|
33124
|
-
to: new Date(lastSegment.arrivalDateTime).toISOString(),
|
|
33125
|
-
productName: `${firstSegment.departureAirportName} - ${lastSegment.arrivalAirportName} (${firstSegment.marketingAirlineName})`,
|
|
33126
|
-
productCode: `${firstSegment.departureAirportCode} ${lastSegment.arrivalAirportCode}/${firstSegment.marketingAirlineCode}`,
|
|
33127
|
-
accommodationName: firstSegment.metaData?.farePriceClassName ?? line.accommodationName,
|
|
33128
|
-
accommodationCode: firstSegment.metaData?.fareCode ?? line.accommodationCode,
|
|
33129
|
-
flightInformation: {
|
|
33130
|
-
pnr: '',
|
|
33131
|
-
flightLines: mapFlightSegmentsToFlightLines(segments)
|
|
33132
|
-
},
|
|
33133
|
-
isChanged: true
|
|
33134
|
-
};
|
|
33135
|
-
};
|
|
33136
|
-
|
|
33137
|
-
const getLocation = (result) => {
|
|
33138
|
-
const place = result.locationName || result.regionName || result.oordName;
|
|
33139
|
-
if (!place)
|
|
33140
|
-
return result.countryName ?? '';
|
|
33141
|
-
return result.countryName ? `${place}, ${result.countryName}` : place;
|
|
33142
|
-
};
|
|
33143
|
-
const toPlainText = (value) => {
|
|
33144
|
-
if (!value)
|
|
33145
|
-
return '';
|
|
33146
|
-
return he
|
|
33147
|
-
.decode(value.replace(/<[^>]*>/g, ' '))
|
|
33148
|
-
.replace(/\s+/g, ' ')
|
|
33149
|
-
.trim();
|
|
33150
|
-
};
|
|
33151
|
-
const TrajectNodeCard = ({ item, result, isSelected, languageCode, translations, onSelect, showNights }) => {
|
|
33152
|
-
const { node } = item;
|
|
33153
|
-
const selectedOption = lodash.first(result.rooms)?.options?.find((x) => x.isSelected) ?? lodash.first(result.rooms)?.options?.[0];
|
|
33154
|
-
const price = formatPrice$3(result.price, result.currencyCode, languageCode);
|
|
33155
|
-
const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
|
|
33156
|
-
const candidate = findCandidateForResult(node, result);
|
|
33157
|
-
const image = candidate?.imageUrl ?? null;
|
|
33158
|
-
const description = toPlainText(candidate?.description);
|
|
33159
|
-
const title = candidate?.name ?? result.name;
|
|
33160
|
-
const priceBlock = (React__default["default"].createElement("div", { className: "search__result-card__price__wrapper" },
|
|
33161
|
-
React__default["default"].createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
|
|
33162
|
-
React__default["default"].createElement("span", { className: "search__result-card__price" }, price)));
|
|
33163
|
-
const selectButton = (React__default["default"].createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT));
|
|
33164
|
-
if (result.contents?.length) {
|
|
33165
|
-
return (React__default["default"].createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
33166
|
-
React__default["default"].createElement("div", { className: "search__result-card__top", dangerouslySetInnerHTML: { __html: he.decode(result.contents) } }),
|
|
33167
|
-
React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton)));
|
|
33168
|
-
}
|
|
33169
|
-
return (React__default["default"].createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
33170
|
-
image && (React__default["default"].createElement("div", { className: "search__result-card__img-wrapper" },
|
|
33171
|
-
React__default["default"].createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
|
|
33172
|
-
priceBlock)),
|
|
33173
|
-
React__default["default"].createElement("div", { className: "search__result-card__content" },
|
|
33174
|
-
React__default["default"].createElement("div", { className: "search__result-card__content__wrapper" },
|
|
33175
|
-
React__default["default"].createElement("div", { className: "search__result-card__header" },
|
|
33176
|
-
React__default["default"].createElement("div", { className: "search__result-card__header__wrapper" },
|
|
33177
|
-
!!result.stars && (React__default["default"].createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default["default"].createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
|
|
33178
|
-
React__default["default"].createElement("h3", { className: "search__result-card__title" }, title)),
|
|
33179
|
-
!image && priceBlock),
|
|
33180
|
-
React__default["default"].createElement("span", { className: "search__result-card__location" },
|
|
33181
|
-
React__default["default"].createElement(Icon, { name: "ui-location", height: 16 }),
|
|
33182
|
-
getLocation(result)),
|
|
33183
|
-
React__default["default"].createElement("div", { className: "search__result-card__options" },
|
|
33184
|
-
showNights && nights > 0 && (React__default["default"].createElement("div", { className: "search__result-card__option" },
|
|
33185
|
-
React__default["default"].createElement(Icon, { name: "ui-moon", height: 16 }),
|
|
33186
|
-
nights,
|
|
33187
|
-
" ",
|
|
33188
|
-
translations?.SRP.NIGHTS)),
|
|
33189
|
-
selectedOption?.accommodationName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
|
|
33190
|
-
React__default["default"].createElement(Icon, { name: "ui-bed", height: 16 }),
|
|
33191
|
-
selectedOption.accommodationName)),
|
|
33192
|
-
selectedOption?.regimeName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
|
|
33193
|
-
React__default["default"].createElement(Icon, { name: "ui-utensils", height: 16 }),
|
|
33194
|
-
selectedOption.regimeName))),
|
|
33195
|
-
description && React__default["default"].createElement("p", { className: "search__result-card__description" }, description)),
|
|
33196
|
-
React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton))));
|
|
33197
|
-
};
|
|
33198
|
-
|
|
33199
33851
|
const getNodeIcon = (node) => {
|
|
33200
33852
|
if (isTrajectFlightNode(node))
|
|
33201
33853
|
return 'ui-flight';
|
|
@@ -33205,14 +33857,17 @@ const getNodeIcon = (node) => {
|
|
|
33205
33857
|
return 'ui-bed';
|
|
33206
33858
|
return 'ui-location';
|
|
33207
33859
|
};
|
|
33860
|
+
const isRenderableNode = (node) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE &&
|
|
33861
|
+
(node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE || node.serviceType === FLIGHT_SERVICE_TYPE);
|
|
33862
|
+
const MAX_DAY_ACTIVITIES = 6;
|
|
33208
33863
|
const TrajectResults = ({ isLoading }) => {
|
|
33209
33864
|
const context = React.useContext(SearchResultsConfigurationContext);
|
|
33210
33865
|
const dispatch = reactRedux.useDispatch();
|
|
33211
33866
|
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
33212
33867
|
const locale = getLocale(context?.languageCode ?? 'en-GB');
|
|
33213
|
-
const { trajectNodeAvailability, trajectFlightAvailability } = reactRedux.useSelector((state) => state.searchResults);
|
|
33214
|
-
const [expandedNodes, setExpandedNodes] = React.useState({});
|
|
33868
|
+
const { trajectNodeAvailability, trajectFlightAvailability, trajectDaySuggestions, editablePackagingEntry } = reactRedux.useSelector((state) => state.searchResults);
|
|
33215
33869
|
const trajectEntry = context?.trajectEntry;
|
|
33870
|
+
const hideShowMoreResults = !!context?.hideShowMoreResults;
|
|
33216
33871
|
const nodesWithLines = React.useMemo(() => (trajectEntry ? getTrajectNodesWithLines(trajectEntry) : []), [trajectEntry]);
|
|
33217
33872
|
const lineByNodeId = React.useMemo(() => new Map(nodesWithLines.map((x) => [x.node.nodeId, x])), [nodesWithLines]);
|
|
33218
33873
|
const nodesByDay = React.useMemo(() => {
|
|
@@ -33226,22 +33881,89 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33226
33881
|
});
|
|
33227
33882
|
return map;
|
|
33228
33883
|
}, [trajectEntry]);
|
|
33884
|
+
const { stayByDay, stayStartByNodeId } = React.useMemo(() => {
|
|
33885
|
+
const byDay = new Map();
|
|
33886
|
+
const byNodeId = new Map();
|
|
33887
|
+
nodesWithLines
|
|
33888
|
+
.filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE)
|
|
33889
|
+
.forEach((item) => {
|
|
33890
|
+
const nights = Math.max(dateFns.differenceInCalendarDays(new Date(item.line.to), new Date(item.line.from)), 1);
|
|
33891
|
+
byNodeId.set(item.node.nodeId, { item, night: 0, nights });
|
|
33892
|
+
for (let night = 0; night < nights; night++) {
|
|
33893
|
+
byDay.set(item.node.dayOrder + night, { item, night, nights });
|
|
33894
|
+
}
|
|
33895
|
+
});
|
|
33896
|
+
return { stayByDay: byDay, stayStartByNodeId: byNodeId };
|
|
33897
|
+
}, [nodesWithLines]);
|
|
33229
33898
|
if (!context || !trajectEntry)
|
|
33230
33899
|
return null;
|
|
33231
|
-
const toggleExpanded = (lineGuid) => setExpandedNodes((prev) => ({ ...prev, [lineGuid]: !prev[lineGuid] }));
|
|
33232
33900
|
const handleSelect = (item, code) => {
|
|
33233
33901
|
const availability = trajectNodeAvailability[item.line.guid];
|
|
33234
33902
|
const result = availability?.results.find((x) => x.code === code);
|
|
33235
33903
|
if (!result)
|
|
33236
33904
|
return;
|
|
33237
33905
|
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
|
|
33238
|
-
|
|
33906
|
+
applyResultToLines(item.lines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
33239
33907
|
};
|
|
33240
|
-
|
|
33241
|
-
|
|
33908
|
+
/** The remaining legs for a flight node open in the fly-in, as the regular flight results do. */
|
|
33909
|
+
const handleShowMoreFlights = (item) => {
|
|
33910
|
+
dispatch(setTrajectEditLineGuid(item.line.guid));
|
|
33911
|
+
dispatch(setFlyInType('traject-flight-results'));
|
|
33912
|
+
dispatch(setFlyInIsOpen(true));
|
|
33913
|
+
};
|
|
33914
|
+
const handleShowMore = (item) => {
|
|
33915
|
+
dispatch(setTrajectEditLineGuid(item.line.guid));
|
|
33916
|
+
dispatch(setFlyInType('acco-results'));
|
|
33917
|
+
dispatch(setFlyInIsOpen(true));
|
|
33918
|
+
};
|
|
33919
|
+
const handleEditOptions = (item) => {
|
|
33920
|
+
const availability = trajectNodeAvailability[item.line.guid];
|
|
33921
|
+
const result = availability?.results.find((x) => x.code === availability?.selectedCode);
|
|
33922
|
+
if (!result)
|
|
33923
|
+
return;
|
|
33924
|
+
dispatch(setPackagingAccoSearchDetails([result]));
|
|
33925
|
+
dispatch(setSelectedPackagingAccoResult(result.code));
|
|
33926
|
+
dispatch(setTrajectEditLineGuid(item.line.guid));
|
|
33927
|
+
dispatch(setFlyInType('acco-details'));
|
|
33928
|
+
dispatch(setFlyInIsOpen(true));
|
|
33929
|
+
};
|
|
33930
|
+
const getStayLocation = (item) => {
|
|
33931
|
+
const availability = trajectNodeAvailability[item.line.guid];
|
|
33932
|
+
const result = availability?.results.find((x) => x.code === availability?.selectedCode);
|
|
33933
|
+
return (result?.locationName ||
|
|
33934
|
+
result?.oordName ||
|
|
33935
|
+
result?.regionName ||
|
|
33936
|
+
result?.countryName ||
|
|
33937
|
+
item.line.location?.name ||
|
|
33938
|
+
item.line.oord?.name ||
|
|
33939
|
+
item.line.region?.name ||
|
|
33940
|
+
item.line.country?.name ||
|
|
33941
|
+
item.node.alternatives?.find((x) => x.isPreferred)?.city ||
|
|
33942
|
+
'');
|
|
33943
|
+
};
|
|
33944
|
+
const getStayName = (item) => {
|
|
33945
|
+
const availability = trajectNodeAvailability[item.line.guid];
|
|
33946
|
+
const result = availability?.results.find((x) => x.code === availability?.selectedCode);
|
|
33947
|
+
return result?.name || item.node.preferredAccommodationName || item.node.name || '';
|
|
33948
|
+
};
|
|
33949
|
+
const getStayTitle = (stay) => [getStayLocation(stay.item), getStayName(stay.item), `${stay.night + 1} ${translations.SRP.OF} ${stay.nights} ${translations.SRP.NIGHTS}`]
|
|
33950
|
+
.filter(Boolean)
|
|
33951
|
+
.join(' - ');
|
|
33952
|
+
const renderDayHeader = (day, stay) => {
|
|
33953
|
+
const date = new Date(day.date);
|
|
33954
|
+
const title = stay ? getStayTitle(stay) : day.name;
|
|
33955
|
+
return (React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
33956
|
+
React__default["default"].createElement("div", { className: "search__results__label__date" },
|
|
33957
|
+
React__default["default"].createElement("p", { className: "search__results__label__date-date" }, dateFns.format(date, 'd', { locale })),
|
|
33958
|
+
React__default["default"].createElement("p", null, dateFns.format(date, 'MMM', { locale }))),
|
|
33959
|
+
React__default["default"].createElement("div", { className: "search__results__label__text" },
|
|
33960
|
+
React__default["default"].createElement(Icon, { name: stay ? 'ui-bed' : 'ui-location', height: 16 }),
|
|
33961
|
+
React__default["default"].createElement("h3", null,
|
|
33962
|
+
React__default["default"].createElement("strong", null, title)))));
|
|
33963
|
+
};
|
|
33964
|
+
const renderServiceNode = (item, showHeader = true, headerTitle) => {
|
|
33242
33965
|
const { node, line } = item;
|
|
33243
33966
|
const availability = trajectNodeAvailability[line.guid];
|
|
33244
|
-
const isExpanded = !!expandedNodes[line.guid];
|
|
33245
33967
|
const results = availability?.results ?? [];
|
|
33246
33968
|
const selected = results.find((x) => x.code === availability?.selectedCode) ?? null;
|
|
33247
33969
|
const configured = results.filter((x) => x.code !== selected?.code && !!findCandidateForResult(node, x));
|
|
@@ -33249,12 +33971,14 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33249
33971
|
// No entry yet means the node's search has been queued but not dispatched — still loading.
|
|
33250
33972
|
const status = availability?.status ?? 'loading';
|
|
33251
33973
|
return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
|
|
33252
|
-
React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
33974
|
+
showHeader && (React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
33253
33975
|
React__default["default"].createElement("div", { className: "search__results__label__date" },
|
|
33254
33976
|
React__default["default"].createElement("p", { className: "search__results__label__date-date" }, dateFns.format(new Date(line.from), 'd', { locale })),
|
|
33255
33977
|
React__default["default"].createElement("p", null, dateFns.format(new Date(line.from), 'MMM', { locale }))),
|
|
33256
33978
|
React__default["default"].createElement("div", { className: "search__results__label__text" },
|
|
33257
|
-
React__default["default"].createElement(Icon, { name: getNodeIcon(node), height: 16 })
|
|
33979
|
+
React__default["default"].createElement(Icon, { name: getNodeIcon(node), height: 16 }),
|
|
33980
|
+
React__default["default"].createElement("h3", null,
|
|
33981
|
+
React__default["default"].createElement("strong", null, headerTitle || node.name))))),
|
|
33258
33982
|
status === 'loading' && (React__default["default"].createElement(Spinner, { label: node.serviceType === EXCURSION_SERVICE_TYPE ? translations.SRP.LOADING_EXCURSIONS : translations.SRP.LOADING_ACCOMMODATIONS })),
|
|
33259
33983
|
status === 'unavailable' && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
33260
33984
|
status === 'resolved' && selected && (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
@@ -33263,9 +33987,13 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33263
33987
|
node.preferredAccommodationName,
|
|
33264
33988
|
" \u2014 ",
|
|
33265
33989
|
translations.SRP.NO_RESULTS))),
|
|
33266
|
-
React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--
|
|
33267
|
-
others.length > 0 && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
|
|
33268
|
-
React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () =>
|
|
33990
|
+
React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--scroll" }, [selected, ...configured].map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, node: node, result: result, isSelected: result.code === selected.code, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, result.code), onEditOptions: node.serviceType === ACCOMMODATION_SERVICE_TYPE ? () => handleEditOptions(item) : undefined, showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE })))),
|
|
33991
|
+
others.length > 0 && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
|
|
33992
|
+
React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMore(item) },
|
|
33993
|
+
translations.SRP.SHOW_MORE,
|
|
33994
|
+
" (",
|
|
33995
|
+
others.length,
|
|
33996
|
+
")")))))));
|
|
33269
33997
|
};
|
|
33270
33998
|
const handleSelectFlight = (item, flight) => {
|
|
33271
33999
|
dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
|
|
@@ -33278,8 +34006,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33278
34006
|
const flights = availability?.results ?? [];
|
|
33279
34007
|
const selected = flights.find((x) => x.outwardGuid === availability?.selectedGuid) ?? null;
|
|
33280
34008
|
const others = flights.filter((x) => x.outwardGuid !== availability?.selectedGuid);
|
|
33281
|
-
const
|
|
33282
|
-
const visible = isExpanded ? others : others.slice(0, 2);
|
|
34009
|
+
const visible = others.slice(0, 2);
|
|
33283
34010
|
return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
|
|
33284
34011
|
React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
33285
34012
|
React__default["default"].createElement("div", { className: "search__results__label__date" },
|
|
@@ -33300,26 +34027,109 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33300
34027
|
React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" },
|
|
33301
34028
|
React__default["default"].createElement(IndependentFlightOption, { key: `flight-${selected.outwardGuid}`, item: selected.outward, guid: selected.outwardGuid, selectedGuid: selected.outwardGuid, isOutward: true, showSelectedState: true, price: selected.price }),
|
|
33302
34029
|
visible.map((result) => (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: result.outward, guid: result.outwardGuid, isOutward: true, price: result.price, currentSelectedPrice: selected.price, onSelect: () => handleSelectFlight(item, result) })))),
|
|
33303
|
-
others.length > 2 && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
|
|
33304
|
-
React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () =>
|
|
34030
|
+
others.length > 2 && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
|
|
34031
|
+
React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreFlights(item) },
|
|
34032
|
+
translations.SRP.SHOW_MORE,
|
|
34033
|
+
" (",
|
|
34034
|
+
others.length - 2,
|
|
34035
|
+
")")))))));
|
|
34036
|
+
};
|
|
34037
|
+
/** The lines a suggested activity put on the entry, which is also how we know it was added. */
|
|
34038
|
+
const getAddedExcursionLines = (result) => (editablePackagingEntry?.lines ?? []).filter((line) => line.serviceType === EXCURSION_SERVICE_TYPE && line.productCode === result.code && dateFns.isSameDay(new Date(line.from), new Date(result.fromDate)));
|
|
34039
|
+
const handleToggleActivity = (result) => {
|
|
34040
|
+
const added = getAddedExcursionLines(result);
|
|
34041
|
+
if (added.length) {
|
|
34042
|
+
dispatch(removeEditableEntryLines(added.map((line) => line.guid)));
|
|
34043
|
+
return;
|
|
34044
|
+
}
|
|
34045
|
+
const lines = buildTrajectExcursionLines(result);
|
|
34046
|
+
if (lines.length)
|
|
34047
|
+
dispatch(addEditableEntryLines(lines));
|
|
34048
|
+
};
|
|
34049
|
+
const handleShowMoreActivities = (dayOrder) => {
|
|
34050
|
+
dispatch(setTrajectActivitiesDayOrder(dayOrder));
|
|
34051
|
+
dispatch(setFlyInType('traject-day-activities'));
|
|
34052
|
+
dispatch(setFlyInIsOpen(true));
|
|
34053
|
+
};
|
|
34054
|
+
/** The excursion nodes the traject itself puts on a day. */
|
|
34055
|
+
const getDayExcursionItems = (dayOrder) => (nodesByDay.get(dayOrder) ?? [])
|
|
34056
|
+
.filter((node) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === EXCURSION_SERVICE_TYPE)
|
|
34057
|
+
.map((node) => lineByNodeId.get(node.nodeId))
|
|
34058
|
+
.filter((x) => !!x);
|
|
34059
|
+
/**
|
|
34060
|
+
* One block per day for everything there is to do that day.
|
|
34061
|
+
*
|
|
34062
|
+
* What the traject books sits first and stays selected; the rest of the day follows as things
|
|
34063
|
+
* the traveller may add. They share a row because they answer the same question — an excursion
|
|
34064
|
+
* on the traject and one found for the day are alternatives to each other, and a block apiece
|
|
34065
|
+
* would only hide that.
|
|
34066
|
+
*/
|
|
34067
|
+
const renderDayActivities = (day) => {
|
|
34068
|
+
const items = getDayExcursionItems(day.order);
|
|
34069
|
+
const suggestions = trajectDaySuggestions[day.order];
|
|
34070
|
+
const cards = buildDayActivityCards(items, trajectNodeAvailability, suggestions?.results ?? []);
|
|
34071
|
+
const isResolving = items.some((item) => (trajectNodeAvailability[item.line.guid]?.status ?? 'loading') === 'loading') || suggestions?.status === 'loading';
|
|
34072
|
+
// A day with nothing configured and nothing found has nothing to say. A day whose own
|
|
34073
|
+
// excursion came back empty does, so the gap is not passed off as a day without activities.
|
|
34074
|
+
if (!cards.length && !items.length)
|
|
34075
|
+
return null;
|
|
34076
|
+
const visible = cards.slice(0, MAX_DAY_ACTIVITIES);
|
|
34077
|
+
const hidden = cards.length - visible.length;
|
|
34078
|
+
return (React__default["default"].createElement(React__default["default"].Fragment, { key: `activities-${day.order}` },
|
|
34079
|
+
React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
34080
|
+
React__default["default"].createElement("div", { className: "search__results__label__date" },
|
|
34081
|
+
React__default["default"].createElement("p", { className: "search__results__label__date-date" }, dateFns.format(new Date(day.date), 'd', { locale })),
|
|
34082
|
+
React__default["default"].createElement("p", null, dateFns.format(new Date(day.date), 'MMM', { locale }))),
|
|
34083
|
+
React__default["default"].createElement("div", { className: "search__results__label__text" },
|
|
34084
|
+
React__default["default"].createElement(Icon, { name: "ui-excursion", height: 16 }),
|
|
34085
|
+
React__default["default"].createElement("h3", null,
|
|
34086
|
+
React__default["default"].createElement("strong", null, translations.SRP.ACTIVITIES)))),
|
|
34087
|
+
isResolving && !cards.length && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_EXCURSIONS }),
|
|
34088
|
+
!isResolving && !cards.length && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
34089
|
+
!!cards.length && (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
34090
|
+
React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--scroll" }, visible.map(renderActivityCard)),
|
|
34091
|
+
hidden > 0 && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
|
|
34092
|
+
React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreActivities(day.order) },
|
|
34093
|
+
translations.SRP.SHOW_MORE,
|
|
34094
|
+
" (",
|
|
34095
|
+
hidden,
|
|
34096
|
+
")")))))));
|
|
34097
|
+
};
|
|
34098
|
+
/**
|
|
34099
|
+
* An activity the traject books is swapped for another, the way a hotel is; one the traveller
|
|
34100
|
+
* found for themselves is added to the trip or taken back off it.
|
|
34101
|
+
*/
|
|
34102
|
+
const renderActivityCard = ({ result, item }) => {
|
|
34103
|
+
const isSelected = item ? trajectNodeAvailability[item.line.guid]?.selectedCode === result.code : getAddedExcursionLines(result).length > 0;
|
|
34104
|
+
return (React__default["default"].createElement(TrajectNodeCard, { key: `activity-${result.code}`, node: item?.node, result: result, isSelected: isSelected, languageCode: context.languageCode, translations: translations, onSelect: () => (item ? handleSelect(item, result.code) : handleToggleActivity(result)), selectLabel: item ? undefined : translations.SRP.ADD, selectedLabel: item ? undefined : translations.SRP.REMOVE }));
|
|
33305
34105
|
};
|
|
33306
34106
|
if (isLoading)
|
|
33307
34107
|
return React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
|
|
33308
34108
|
return (React__default["default"].createElement(React__default["default"].Fragment, null, trajectEntry.traject.days.map((day) => {
|
|
33309
|
-
const
|
|
33310
|
-
|
|
33311
|
-
|
|
33312
|
-
|
|
33313
|
-
|
|
33314
|
-
|
|
33315
|
-
|
|
33316
|
-
|
|
33317
|
-
|
|
33318
|
-
|
|
33319
|
-
|
|
33320
|
-
|
|
33321
|
-
|
|
33322
|
-
|
|
34109
|
+
const stay = stayByDay.get(day.order);
|
|
34110
|
+
const dayNodes = (nodesByDay.get(day.order) ?? []).filter((node) => isRenderableNode(node) && lineByNodeId.has(node.nodeId));
|
|
34111
|
+
const stayLeadsDay = !!stay && (stay.night > 0 || dayNodes[0]?.nodeId === stay.item.node.nodeId);
|
|
34112
|
+
// Where the day's activities belong in the order: with the first excursion it carries.
|
|
34113
|
+
const firstExcursionNodeId = dayNodes.find((node) => node.serviceType === EXCURSION_SERVICE_TYPE)?.nodeId;
|
|
34114
|
+
const showDayHeader = stayLeadsDay || !!day.name?.trim();
|
|
34115
|
+
return (React__default["default"].createElement(React__default["default"].Fragment, { key: `day-${day.order}` },
|
|
34116
|
+
showDayHeader && renderDayHeader(day, stayLeadsDay ? stay : undefined),
|
|
34117
|
+
dayNodes.map((node) => {
|
|
34118
|
+
const item = lineByNodeId.get(node.nodeId);
|
|
34119
|
+
if (node.serviceType === ACCOMMODATION_SERVICE_TYPE) {
|
|
34120
|
+
const stayStart = stayStartByNodeId.get(node.nodeId);
|
|
34121
|
+
const titledByDayHeader = stayLeadsDay && stay.item.node.nodeId === node.nodeId;
|
|
34122
|
+
return renderServiceNode(item, !titledByDayHeader, stayStart && getStayTitle(stayStart));
|
|
34123
|
+
}
|
|
34124
|
+
// Every excursion the day holds is drawn once, in one block, where the first of
|
|
34125
|
+
// them falls in the order — so the activities still sit between the stay and the
|
|
34126
|
+
// flight that follow them.
|
|
34127
|
+
if (node.serviceType === EXCURSION_SERVICE_TYPE) {
|
|
34128
|
+
return node.nodeId === firstExcursionNodeId ? renderDayActivities(day) : null;
|
|
34129
|
+
}
|
|
34130
|
+
return renderFlightNode(item);
|
|
34131
|
+
}),
|
|
34132
|
+
firstExcursionNodeId === undefined && renderDayActivities(day)));
|
|
33323
34133
|
})));
|
|
33324
34134
|
};
|
|
33325
34135
|
|
|
@@ -33327,6 +34137,8 @@ const useTrajectAvailability = () => {
|
|
|
33327
34137
|
const context = React.useContext(SearchResultsConfigurationContext);
|
|
33328
34138
|
const dispatch = reactRedux.useDispatch();
|
|
33329
34139
|
const trajectEntry = context?.trajectEntry;
|
|
34140
|
+
const showApiItems = !!context?.showApiItems;
|
|
34141
|
+
const hideShowMoreResults = !!context?.hideShowMoreResults;
|
|
33330
34142
|
React.useEffect(() => {
|
|
33331
34143
|
if (!context || !trajectEntry)
|
|
33332
34144
|
return;
|
|
@@ -33359,7 +34171,8 @@ const useTrajectAvailability = () => {
|
|
|
33359
34171
|
const search = item.node.serviceType === EXCURSION_SERVICE_TYPE ? build.searchPackagingExcursions : build.searchPackagingAccommodations;
|
|
33360
34172
|
dispatch(setTrajectNodeLoading(lineGuid));
|
|
33361
34173
|
try {
|
|
33362
|
-
|
|
34174
|
+
// With external items switched off the node only looks at products held inside Tide.
|
|
34175
|
+
const candidates = getNodeCandidates(item.node).filter((x) => showApiItems || !isExternalCandidate(x));
|
|
33363
34176
|
const attempts = await Promise.all(candidates.map(async (candidate) => {
|
|
33364
34177
|
try {
|
|
33365
34178
|
const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, candidate.code), controller.signal);
|
|
@@ -33371,18 +34184,25 @@ const useTrajectAvailability = () => {
|
|
|
33371
34184
|
return null;
|
|
33372
34185
|
}
|
|
33373
34186
|
}));
|
|
33374
|
-
|
|
33375
|
-
|
|
34187
|
+
const configuredResults = attempts
|
|
34188
|
+
.filter((x) => !!x)
|
|
34189
|
+
.filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
|
|
33376
34190
|
const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
|
|
33377
|
-
let preferredUnavailable = !!preferredCandidate && !
|
|
33378
|
-
//
|
|
33379
|
-
if
|
|
33380
|
-
|
|
33381
|
-
|
|
34191
|
+
let preferredUnavailable = !!preferredCandidate && !configuredResults.some((x) => matchesCode(x, preferredCandidate.code));
|
|
34192
|
+
// The destination search exists only to stock the show-more fly-in. With the button hidden
|
|
34193
|
+
// a node is limited to its configured products; if none are available it simply reports so,
|
|
34194
|
+
// rather than quietly substituting a hotel nobody configured.
|
|
34195
|
+
const destinationResults = hideShowMoreResults
|
|
34196
|
+
? []
|
|
34197
|
+
: ((await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [])
|
|
34198
|
+
.filter((x) => showApiItems || !isExternalResult(x))
|
|
34199
|
+
.filter((x) => !configuredResults.some((configured) => configured.code === x.code));
|
|
34200
|
+
const picked = configuredResults[0] ?? pickCheapest(destinationResults);
|
|
34201
|
+
const results = [...configuredResults, ...destinationResults];
|
|
34202
|
+
if (!configuredResults.length)
|
|
33382
34203
|
preferredUnavailable = candidates.length > 0;
|
|
33383
|
-
}
|
|
33384
34204
|
if (controller.signal.aborted)
|
|
33385
|
-
return
|
|
34205
|
+
return [];
|
|
33386
34206
|
dispatch(setTrajectNodeAvailability({
|
|
33387
34207
|
lineGuid,
|
|
33388
34208
|
results,
|
|
@@ -33390,58 +34210,58 @@ const useTrajectAvailability = () => {
|
|
|
33390
34210
|
preferredUnavailable
|
|
33391
34211
|
}));
|
|
33392
34212
|
if (!picked)
|
|
33393
|
-
return
|
|
33394
|
-
|
|
33395
|
-
return
|
|
34213
|
+
return item.lines.map((x) => x.guid);
|
|
34214
|
+
applyResultToLines(item.lines, picked).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
34215
|
+
return [];
|
|
33396
34216
|
}
|
|
33397
34217
|
catch (error) {
|
|
33398
34218
|
if (controller.signal.aborted)
|
|
33399
|
-
return
|
|
34219
|
+
return [];
|
|
33400
34220
|
console.error('Failed to resolve traject node availability', item.node.name, error);
|
|
33401
34221
|
dispatch(setTrajectNodeAvailability({ lineGuid, results: [], selectedCode: null, preferredUnavailable: false }));
|
|
33402
|
-
return
|
|
34222
|
+
return item.lines.map((x) => x.guid);
|
|
33403
34223
|
}
|
|
33404
34224
|
};
|
|
33405
34225
|
const resolveFlightNode = async (item) => {
|
|
33406
34226
|
const lineGuid = item.line.guid;
|
|
33407
34227
|
if (!item.node.departureAirportCode || !item.node.arrivalAirportCode) {
|
|
33408
34228
|
dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
|
|
33409
|
-
return
|
|
34229
|
+
return item.lines.map((x) => x.guid);
|
|
33410
34230
|
}
|
|
33411
34231
|
dispatch(setTrajectFlightLoading(lineGuid));
|
|
33412
34232
|
try {
|
|
33413
34233
|
const flights = (await build.searchPackagingFlights(config, buildTrajectFlightRequest(item, searchContext, flightPax), controller.signal)) ?? [];
|
|
33414
34234
|
if (controller.signal.aborted)
|
|
33415
|
-
return
|
|
34235
|
+
return [];
|
|
33416
34236
|
const picked = pickCheapestFlight(flights);
|
|
33417
34237
|
dispatch(setTrajectFlightAvailability({ lineGuid, results: flights, selectedGuid: picked?.outwardGuid ?? null }));
|
|
33418
34238
|
if (!picked)
|
|
33419
|
-
return
|
|
34239
|
+
return item.lines.map((x) => x.guid);
|
|
33420
34240
|
dispatch(updateEditableEntryLine(applyFlightToLine(item.line, picked)));
|
|
33421
|
-
return
|
|
34241
|
+
return [];
|
|
33422
34242
|
}
|
|
33423
34243
|
catch (error) {
|
|
33424
34244
|
if (controller.signal.aborted)
|
|
33425
|
-
return
|
|
34245
|
+
return [];
|
|
33426
34246
|
console.error('Failed to resolve traject flight', item.node.name, error);
|
|
33427
34247
|
dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
|
|
33428
|
-
return
|
|
34248
|
+
return item.lines.map((x) => x.guid);
|
|
33429
34249
|
}
|
|
33430
34250
|
};
|
|
33431
34251
|
(async () => {
|
|
33432
34252
|
const searchable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node));
|
|
33433
34253
|
const flightNodes = nodesWithLines.filter((x) => isTrajectFlightNode(x.node));
|
|
33434
|
-
const unresolved = await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)]);
|
|
34254
|
+
const unresolved = (await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)])).flat();
|
|
33435
34255
|
if (controller.signal.aborted)
|
|
33436
34256
|
return;
|
|
33437
34257
|
// A node with no availability anywhere cannot be priced or booked, so its line comes off
|
|
33438
34258
|
// the entry. The card stays visible and reports the gap.
|
|
33439
|
-
const emptyLineGuids = unresolved.filter(
|
|
34259
|
+
const emptyLineGuids = unresolved.filter(Boolean);
|
|
33440
34260
|
if (emptyLineGuids.length)
|
|
33441
34261
|
dispatch(removeEditableEntryLines(emptyLineGuids));
|
|
33442
34262
|
})();
|
|
33443
34263
|
return () => controller.abort();
|
|
33444
|
-
}, [trajectEntry]);
|
|
34264
|
+
}, [trajectEntry, showApiItems, hideShowMoreResults]);
|
|
33445
34265
|
};
|
|
33446
34266
|
|
|
33447
34267
|
const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
@@ -33449,7 +34269,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
|
33449
34269
|
const dispatch = reactRedux.useDispatch();
|
|
33450
34270
|
const context = React.useContext(SearchResultsConfigurationContext);
|
|
33451
34271
|
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
33452
|
-
const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry } = reactRedux.useSelector((state) => state.searchResults);
|
|
34272
|
+
const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry, trajectEditLineGuid } = reactRedux.useSelector((state) => state.searchResults);
|
|
33453
34273
|
const isMobile = useMediaQuery('(max-width: 1200px)');
|
|
33454
34274
|
// Resolves live availability per traject node; no-op unless a traject entry is configured.
|
|
33455
34275
|
useTrajectAvailability();
|
|
@@ -33738,6 +34558,20 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
|
33738
34558
|
setDetailsIsLoading(false);
|
|
33739
34559
|
};
|
|
33740
34560
|
const handleConfirmHotelSwap = () => {
|
|
34561
|
+
if (context?.trajectEntry && trajectEditLineGuid) {
|
|
34562
|
+
const item = getTrajectNodesWithLines(context.trajectEntry).find((x) => x.line.guid === trajectEditLineGuid);
|
|
34563
|
+
const result = packagingAccoSearchDetails.find((x) => x.code === selectedPackagingAccoResultCode);
|
|
34564
|
+
if (item && result) {
|
|
34565
|
+
const currentLines = item.lines
|
|
34566
|
+
.map((line) => editablePackagingEntry?.lines?.find((x) => x.guid === line.guid))
|
|
34567
|
+
.filter((x) => !!x);
|
|
34568
|
+
applyResultToLines(currentLines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
34569
|
+
dispatch(updateTrajectNodeResult({ lineGuid: trajectEditLineGuid, result }));
|
|
34570
|
+
}
|
|
34571
|
+
dispatch(setTrajectEditLineGuid(null));
|
|
34572
|
+
handleFlyInToggle(false);
|
|
34573
|
+
return;
|
|
34574
|
+
}
|
|
33741
34575
|
const updatedEntry = swapHotelInPackagingEntry();
|
|
33742
34576
|
if (!updatedEntry)
|
|
33743
34577
|
return;
|
|
@@ -34086,6 +34920,10 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
|
34086
34920
|
const fetchPackagingAccoSearchDetails = async () => {
|
|
34087
34921
|
if (!selectedPackagingAccoResultCode || !context)
|
|
34088
34922
|
return;
|
|
34923
|
+
// A traject node already resolved its own availability, options per room included, and hands
|
|
34924
|
+
// that result straight to the fly-in.
|
|
34925
|
+
if (context.trajectEntry)
|
|
34926
|
+
return;
|
|
34089
34927
|
if (skipInitialPackagingAccoDetailsRef.current) {
|
|
34090
34928
|
skipInitialPackagingAccoDetailsRef.current = false;
|
|
34091
34929
|
return;
|