@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-esm/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import React__default, { useLayoutEffect as useLayoutEffect$1, useEffect, useContext, useState, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import JsonURL from '@jsonurl/jsonurl';
|
|
5
|
-
import { format as format$2, isToday, getDate, startOfWeek, startOfMonth, endOfWeek, addWeeks, endOfMonth, eachDayOfInterval, getYear, getMonth, getISOWeek, getISODay, isSameMonth, startOfDay, isAfter, isEqual, isWithinInterval, endOfDay, isBefore, isSameDay, differenceInCalendarDays, addMonths, addDays, formatISO, addYears, differenceInYears, parseISO, differenceInMinutes, parse, startOfToday
|
|
5
|
+
import { format as format$2, isToday, getDate, startOfWeek, startOfMonth, endOfWeek, addWeeks, endOfMonth, eachDayOfInterval, getYear, getMonth, getISOWeek, getISODay, isSameMonth, startOfDay, isAfter, isEqual, isWithinInterval, endOfDay, isBefore, isSameDay, differenceInCalendarDays, addMonths, addDays, formatISO, addYears, differenceInYears, parseISO, differenceInMinutes, isValid, parse, startOfToday } from 'date-fns';
|
|
6
6
|
import { merge, cloneDeep, isNil, isArray as isArray$1, compact, isEmpty as isEmpty$1, range, chunk, isFunction as isFunction$1, clamp, now, omit, isPlainObject as isPlainObject$1, mapValues, pickBy, first, orderBy, uniq, uniqBy, sortBy, sum, last, findIndex as findIndex$1, set, get, isEqual as isEqual$1, groupBy, minBy, concat, flatMap } from 'lodash';
|
|
7
7
|
import { arSA, da, de, enGB, es, fr, is, it, nl, nb, pl, pt, sv, ja, enUS } from 'date-fns/locale';
|
|
8
8
|
import { usePopper } from 'react-popper';
|
|
@@ -409,6 +409,11 @@ var SRP$f = {
|
|
|
409
409
|
ACCOMMODATION: "الإقامة",
|
|
410
410
|
VIEW_DETAILS: "عرض التفاصيل",
|
|
411
411
|
NIGHTS: "ليالٍ",
|
|
412
|
+
OF: "من",
|
|
413
|
+
ADD: "إضافة",
|
|
414
|
+
REMOVE: "إزالة",
|
|
415
|
+
ACTIVITIES: "الأنشطة",
|
|
416
|
+
SHOW_ACTIVITIES: "عرض الأنشطة",
|
|
412
417
|
MORNING_RANGE: "الصباح",
|
|
413
418
|
AFTERNOON_RANGE: "بعد الظهر",
|
|
414
419
|
EVENING_RANGE: "المساء",
|
|
@@ -848,6 +853,11 @@ var SRP$e = {
|
|
|
848
853
|
ACCOMMODATION: "Indkvartering",
|
|
849
854
|
VIEW_DETAILS: "Se detaljer",
|
|
850
855
|
NIGHTS: "nætter",
|
|
856
|
+
OF: "af",
|
|
857
|
+
ADD: "Tilføj",
|
|
858
|
+
REMOVE: "Fjern",
|
|
859
|
+
ACTIVITIES: "Aktiviteter",
|
|
860
|
+
SHOW_ACTIVITIES: "Vis aktiviteter",
|
|
851
861
|
MORNING_RANGE: "Morgen",
|
|
852
862
|
AFTERNOON_RANGE: "Eftermiddag",
|
|
853
863
|
EVENING_RANGE: "Aften",
|
|
@@ -1291,6 +1301,11 @@ var SRP$d = {
|
|
|
1291
1301
|
ACCOMMODATION: "Unterkunft",
|
|
1292
1302
|
VIEW_DETAILS: "Details anzeigen",
|
|
1293
1303
|
NIGHTS: "Nächte",
|
|
1304
|
+
OF: "von",
|
|
1305
|
+
ADD: "Hinzufügen",
|
|
1306
|
+
REMOVE: "Entfernen",
|
|
1307
|
+
ACTIVITIES: "Aktivitäten",
|
|
1308
|
+
SHOW_ACTIVITIES: "Aktivitäten anzeigen",
|
|
1294
1309
|
MORNING_RANGE: "Morgen",
|
|
1295
1310
|
AFTERNOON_RANGE: "Nachmittag",
|
|
1296
1311
|
EVENING_RANGE: "Abend",
|
|
@@ -1734,6 +1749,11 @@ var SRP$c = {
|
|
|
1734
1749
|
ACCOMMODATION: "Accommodation",
|
|
1735
1750
|
VIEW_DETAILS: "View details",
|
|
1736
1751
|
NIGHTS: "nights",
|
|
1752
|
+
OF: "of",
|
|
1753
|
+
ADD: "Add",
|
|
1754
|
+
REMOVE: "Remove",
|
|
1755
|
+
ACTIVITIES: "Activities",
|
|
1756
|
+
SHOW_ACTIVITIES: "Show activities",
|
|
1737
1757
|
PRICE: "Price",
|
|
1738
1758
|
MORNING_RANGE: "Morning",
|
|
1739
1759
|
AFTERNOON_RANGE: "Afternoon",
|
|
@@ -2173,6 +2193,11 @@ var SRP$b = {
|
|
|
2173
2193
|
ACCOMMODATION: "Alojamiento",
|
|
2174
2194
|
VIEW_DETAILS: "Ver detalles",
|
|
2175
2195
|
NIGHTS: "noches",
|
|
2196
|
+
OF: "de",
|
|
2197
|
+
ADD: "Añadir",
|
|
2198
|
+
REMOVE: "Eliminar",
|
|
2199
|
+
ACTIVITIES: "Actividades",
|
|
2200
|
+
SHOW_ACTIVITIES: "Ver actividades",
|
|
2176
2201
|
MORNING_RANGE: "Mañana",
|
|
2177
2202
|
AFTERNOON_RANGE: "Tarde",
|
|
2178
2203
|
EVENING_RANGE: "Noche",
|
|
@@ -2616,6 +2641,11 @@ var SRP$a = {
|
|
|
2616
2641
|
ACCOMMODATION: "Hébergement",
|
|
2617
2642
|
VIEW_DETAILS: "Voir les détails",
|
|
2618
2643
|
NIGHTS: "nuits",
|
|
2644
|
+
OF: "sur",
|
|
2645
|
+
ADD: "Ajouter",
|
|
2646
|
+
REMOVE: "Supprimer",
|
|
2647
|
+
ACTIVITIES: "Activités",
|
|
2648
|
+
SHOW_ACTIVITIES: "Voir les activités",
|
|
2619
2649
|
MORNING_RANGE: "Matin",
|
|
2620
2650
|
AFTERNOON_RANGE: "Après-midi",
|
|
2621
2651
|
EVENING_RANGE: "Soir",
|
|
@@ -3055,6 +3085,11 @@ var SRP$9 = {
|
|
|
3055
3085
|
ACCOMMODATION: "Hébergement",
|
|
3056
3086
|
VIEW_DETAILS: "Voir les détails",
|
|
3057
3087
|
NIGHTS: "nuits",
|
|
3088
|
+
OF: "sur",
|
|
3089
|
+
ADD: "Ajouter",
|
|
3090
|
+
REMOVE: "Supprimer",
|
|
3091
|
+
ACTIVITIES: "Activités",
|
|
3092
|
+
SHOW_ACTIVITIES: "Voir les activités",
|
|
3058
3093
|
MORNING_RANGE: "Matin",
|
|
3059
3094
|
AFTERNOON_RANGE: "Après-midi",
|
|
3060
3095
|
EVENING_RANGE: "Soir",
|
|
@@ -3494,6 +3529,11 @@ var SRP$8 = {
|
|
|
3494
3529
|
ACCOMMODATION: "Gisting",
|
|
3495
3530
|
VIEW_DETAILS: "Skoða upplýsingar",
|
|
3496
3531
|
NIGHTS: "nætur",
|
|
3532
|
+
OF: "af",
|
|
3533
|
+
ADD: "Bæta við",
|
|
3534
|
+
REMOVE: "Fjarlægja",
|
|
3535
|
+
ACTIVITIES: "Afþreying",
|
|
3536
|
+
SHOW_ACTIVITIES: "Sýna afþreyingu",
|
|
3497
3537
|
MORNING_RANGE: "Morgunn",
|
|
3498
3538
|
AFTERNOON_RANGE: "Síðdegi",
|
|
3499
3539
|
EVENING_RANGE: "Kvöld",
|
|
@@ -3933,6 +3973,11 @@ var SRP$7 = {
|
|
|
3933
3973
|
ACCOMMODATION: "Alloggio",
|
|
3934
3974
|
VIEW_DETAILS: "Visualizza dettagli",
|
|
3935
3975
|
NIGHTS: "notti",
|
|
3976
|
+
OF: "di",
|
|
3977
|
+
ADD: "Aggiungi",
|
|
3978
|
+
REMOVE: "Rimuovi",
|
|
3979
|
+
ACTIVITIES: "Attività",
|
|
3980
|
+
SHOW_ACTIVITIES: "Mostra attività",
|
|
3936
3981
|
MORNING_RANGE: "Mattina",
|
|
3937
3982
|
AFTERNOON_RANGE: "Pomeriggio",
|
|
3938
3983
|
EVENING_RANGE: "Sera",
|
|
@@ -4376,6 +4421,11 @@ var SRP$6 = {
|
|
|
4376
4421
|
ACCOMMODATION: "Accommodatie",
|
|
4377
4422
|
VIEW_DETAILS: "Bekijk details",
|
|
4378
4423
|
NIGHTS: "nachten",
|
|
4424
|
+
OF: "van",
|
|
4425
|
+
ADD: "Toevoegen",
|
|
4426
|
+
REMOVE: "Verwijderen",
|
|
4427
|
+
ACTIVITIES: "Activiteiten",
|
|
4428
|
+
SHOW_ACTIVITIES: "Toon activiteiten",
|
|
4379
4429
|
MORNING_RANGE: "Ochtend",
|
|
4380
4430
|
AFTERNOON_RANGE: "Namiddag",
|
|
4381
4431
|
EVENING_RANGE: "Avond",
|
|
@@ -4815,6 +4865,11 @@ var SRP$5 = {
|
|
|
4815
4865
|
ACCOMMODATION: "Accommodatie",
|
|
4816
4866
|
VIEW_DETAILS: "Bekijk details",
|
|
4817
4867
|
NIGHTS: "nachten",
|
|
4868
|
+
OF: "van",
|
|
4869
|
+
ADD: "Toevoegen",
|
|
4870
|
+
REMOVE: "Verwijderen",
|
|
4871
|
+
ACTIVITIES: "Activiteiten",
|
|
4872
|
+
SHOW_ACTIVITIES: "Toon activiteiten",
|
|
4818
4873
|
MORNING_RANGE: "Ochtend",
|
|
4819
4874
|
AFTERNOON_RANGE: "Namiddag",
|
|
4820
4875
|
EVENING_RANGE: "Avond",
|
|
@@ -5254,6 +5309,11 @@ var SRP$4 = {
|
|
|
5254
5309
|
ACCOMMODATION: "Overnatting",
|
|
5255
5310
|
VIEW_DETAILS: "Se detaljer",
|
|
5256
5311
|
NIGHTS: "netter",
|
|
5312
|
+
OF: "av",
|
|
5313
|
+
ADD: "Legg til",
|
|
5314
|
+
REMOVE: "Fjern",
|
|
5315
|
+
ACTIVITIES: "Aktiviteter",
|
|
5316
|
+
SHOW_ACTIVITIES: "Vis aktiviteter",
|
|
5257
5317
|
MORNING_RANGE: "Morgen",
|
|
5258
5318
|
AFTERNOON_RANGE: "Ettermiddag",
|
|
5259
5319
|
EVENING_RANGE: "Kveld",
|
|
@@ -5693,6 +5753,11 @@ var SRP$3 = {
|
|
|
5693
5753
|
ACCOMMODATION: "Zakwaterowanie",
|
|
5694
5754
|
VIEW_DETAILS: "Zobacz szczegóły",
|
|
5695
5755
|
NIGHTS: "noce",
|
|
5756
|
+
OF: "z",
|
|
5757
|
+
ADD: "Dodaj",
|
|
5758
|
+
REMOVE: "Usuń",
|
|
5759
|
+
ACTIVITIES: "Atrakcje",
|
|
5760
|
+
SHOW_ACTIVITIES: "Pokaż atrakcje",
|
|
5696
5761
|
MORNING_RANGE: "Poranek",
|
|
5697
5762
|
AFTERNOON_RANGE: "Popołudnie",
|
|
5698
5763
|
EVENING_RANGE: "Wieczór",
|
|
@@ -6132,6 +6197,11 @@ var SRP$2 = {
|
|
|
6132
6197
|
ACCOMMODATION: "Alojamento",
|
|
6133
6198
|
VIEW_DETAILS: "Ver detalhes",
|
|
6134
6199
|
NIGHTS: "noites",
|
|
6200
|
+
OF: "de",
|
|
6201
|
+
ADD: "Adicionar",
|
|
6202
|
+
REMOVE: "Remover",
|
|
6203
|
+
ACTIVITIES: "Atividades",
|
|
6204
|
+
SHOW_ACTIVITIES: "Ver atividades",
|
|
6135
6205
|
MORNING_RANGE: "Manhã",
|
|
6136
6206
|
AFTERNOON_RANGE: "Tarde",
|
|
6137
6207
|
EVENING_RANGE: "Noite",
|
|
@@ -6571,6 +6641,11 @@ var SRP$1 = {
|
|
|
6571
6641
|
ACCOMMODATION: "Boende",
|
|
6572
6642
|
VIEW_DETAILS: "Visa detaljer",
|
|
6573
6643
|
NIGHTS: "nätter",
|
|
6644
|
+
OF: "av",
|
|
6645
|
+
ADD: "Lägg till",
|
|
6646
|
+
REMOVE: "Ta bort",
|
|
6647
|
+
ACTIVITIES: "Aktiviteter",
|
|
6648
|
+
SHOW_ACTIVITIES: "Visa aktiviteter",
|
|
6574
6649
|
MORNING_RANGE: "Morgon",
|
|
6575
6650
|
AFTERNOON_RANGE: "Eftermiddag",
|
|
6576
6651
|
EVENING_RANGE: "Kväll",
|
|
@@ -7010,6 +7085,11 @@ var SRP = {
|
|
|
7010
7085
|
ACCOMMODATION: "宿泊",
|
|
7011
7086
|
VIEW_DETAILS: "詳細を見る",
|
|
7012
7087
|
NIGHTS: "泊",
|
|
7088
|
+
OF: "/",
|
|
7089
|
+
ADD: "追加",
|
|
7090
|
+
REMOVE: "削除",
|
|
7091
|
+
ACTIVITIES: "アクティビティ",
|
|
7092
|
+
SHOW_ACTIVITIES: "アクティビティを表示",
|
|
7013
7093
|
MORNING_RANGE: "朝",
|
|
7014
7094
|
AFTERNOON_RANGE: "午後",
|
|
7015
7095
|
EVENING_RANGE: "夕方",
|
|
@@ -16503,7 +16583,7 @@ const Icon = ({ name, className, title, width, height, fill }) => {
|
|
|
16503
16583
|
title && React__default.createElement("title", null, title),
|
|
16504
16584
|
React__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" })));
|
|
16505
16585
|
case 'ui-hotel':
|
|
16506
|
-
return (React__default.createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !isEmpty$1(className)).join(' '), width: width, height: height, viewBox: "0 0
|
|
16586
|
+
return (React__default.createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !isEmpty$1(className)).join(' '), width: width, height: height, viewBox: "0 0 512 512", fill: fill ?? 'currentColor' },
|
|
16507
16587
|
React__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." }),
|
|
16508
16588
|
title && React__default.createElement("title", null, title),
|
|
16509
16589
|
React__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" })));
|
|
@@ -16532,6 +16612,16 @@ const Icon = ({ name, className, title, width, height, fill }) => {
|
|
|
16532
16612
|
React__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." }),
|
|
16533
16613
|
title && React__default.createElement("title", null, title),
|
|
16534
16614
|
React__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" })));
|
|
16615
|
+
case 'ui-bus':
|
|
16616
|
+
return (React__default.createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !isEmpty$1(className)).join(' '), width: width, height: height, viewBox: "0 0 512 512", fill: fill ?? 'currentColor' },
|
|
16617
|
+
title && React__default.createElement("title", null, title),
|
|
16618
|
+
React__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" }),
|
|
16619
|
+
React__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" })));
|
|
16620
|
+
case 'ui-train':
|
|
16621
|
+
return (React__default.createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !isEmpty$1(className)).join(' '), width: width, height: height, viewBox: "0 0 512 512", fill: fill ?? 'currentColor' },
|
|
16622
|
+
title && React__default.createElement("title", null, title),
|
|
16623
|
+
React__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" }),
|
|
16624
|
+
React__default.createElement("path", { d: "M168 416h72l-48 64h-72l48-64zm104 0h72l48 64h-72l-48-64z", fill: "currentColor" })));
|
|
16535
16625
|
case 'ui-ship':
|
|
16536
16626
|
return (React__default.createElement("svg", { className: ['icon', `icon--${name}`, className].filter((className) => !isEmpty$1(className)).join(' '), width: width, height: height, viewBox: "0 0 640 512", fill: fill ?? 'currentColor' },
|
|
16537
16627
|
React__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." }),
|
|
@@ -22861,6 +22951,10 @@ const NAME_CHARACTERS_REGEX = /^[\p{L}\s-]+$/u;
|
|
|
22861
22951
|
function hasInvalidNameCharacters(name) {
|
|
22862
22952
|
return !NAME_CHARACTERS_REGEX.test(name);
|
|
22863
22953
|
}
|
|
22954
|
+
// Rejects values like a 5-digit year, which the native date input allows while typing
|
|
22955
|
+
function isValidBirthDate(birthDateText) {
|
|
22956
|
+
return isValid(parse(birthDateText, 'yyyy-MM-dd', new Date()));
|
|
22957
|
+
}
|
|
22864
22958
|
function getAge(birthDateText, startDateText) {
|
|
22865
22959
|
var birthDate = new Date(birthDateText);
|
|
22866
22960
|
var startDate = new Date(startDateText);
|
|
@@ -22928,7 +23022,7 @@ const validateForm$1 = (values, agentRequired, bookingType, translations, formFi
|
|
|
22928
23022
|
}
|
|
22929
23023
|
}
|
|
22930
23024
|
if (isFormFieldPresent('birthDate')) {
|
|
22931
|
-
if (isEmpty$1(adult.birthDate)) {
|
|
23025
|
+
if (isEmpty$1(adult.birthDate) || !isValidBirthDate(adult.birthDate)) {
|
|
22932
23026
|
set(errors, `rooms[${rIndex}].adults[${index}].birthDate`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
|
|
22933
23027
|
}
|
|
22934
23028
|
else if (values.endDate) {
|
|
@@ -22961,7 +23055,7 @@ const validateForm$1 = (values, agentRequired, bookingType, translations, formFi
|
|
|
22961
23055
|
}
|
|
22962
23056
|
}
|
|
22963
23057
|
if (isFormFieldPresent('birthDate')) {
|
|
22964
|
-
if (isEmpty$1(child.birthDate)) {
|
|
23058
|
+
if (isEmpty$1(child.birthDate) || !isValidBirthDate(child.birthDate)) {
|
|
22965
23059
|
set(errors, `rooms[${rIndex}].children[${index}].birthDate`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
|
|
22966
23060
|
}
|
|
22967
23061
|
else if (child.isBaby && values.startDate) {
|
|
@@ -23186,6 +23280,13 @@ const TypeAheadInput = ({ name, value, placeholder, options, onChange, onSelect,
|
|
|
23186
23280
|
function isBabyAge(age, maxBabyAge = BABY_MAX_AGE) {
|
|
23187
23281
|
return typeof age === 'number' && age <= maxBabyAge;
|
|
23188
23282
|
}
|
|
23283
|
+
// The native date input can hold values format() throws on (e.g. a 5+ digit year while typing), so never format an unvalidated birthDate
|
|
23284
|
+
function formatBirthDate$1(birthDate) {
|
|
23285
|
+
if (!birthDate)
|
|
23286
|
+
return '';
|
|
23287
|
+
const parsed = parse(birthDate, 'yyyy-MM-dd', new Date());
|
|
23288
|
+
return isValid(parsed) ? format$2(parsed, 'dd-MM-yyyy') : '';
|
|
23289
|
+
}
|
|
23189
23290
|
function createTraveler(traveler, followNumber, personTranslation, isCompact, maxBabyAge) {
|
|
23190
23291
|
if (isCompact) {
|
|
23191
23292
|
return {
|
|
@@ -23493,10 +23594,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
|
|
|
23493
23594
|
!useCompactForm && (bookingType !== 'b2b' || travellersSettings?.mainBookerFormFields?.length) ? (React__default.createElement("div", { className: "form__region" },
|
|
23494
23595
|
React__default.createElement("div", { className: "form__region-header" },
|
|
23495
23596
|
React__default.createElement("h5", { className: "form__region-heading" }, translations.TRAVELERS_FORM.MAIN_BOOKER),
|
|
23496
|
-
React__default.createElement("p", { className: "form__region-label" }, compact([
|
|
23497
|
-
compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '),
|
|
23498
|
-
mainBooker?.birthDate && format$2(parse(mainBooker.birthDate, 'yyyy-MM-dd', new Date()), 'dd-MM-yyyy')
|
|
23499
|
-
]).join(', '))),
|
|
23597
|
+
React__default.createElement("p", { className: "form__region-label" }, compact([compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '), formatBirthDate$1(mainBooker?.birthDate)]).join(', '))),
|
|
23500
23598
|
travellersSettings?.mainBookerFormFields?.length ? (React__default.createElement("div", { className: "main-booker-form__grid" }, travellersSettings.mainBookerFormFields.map((field, index) => (React__default.createElement("div", { key: index, className: `control control--${field.type}` }, getControl(field.type, {}, field.type)))))) : (React__default.createElement(React__default.Fragment, null,
|
|
23501
23599
|
React__default.createElement("div", { className: "form__twocolumn" },
|
|
23502
23600
|
React__default.createElement("div", { className: "form__twocolumn-column" },
|
|
@@ -27341,7 +27439,10 @@ const initialState$1 = {
|
|
|
27341
27439
|
currentStep: 0,
|
|
27342
27440
|
bookingNumber: undefined,
|
|
27343
27441
|
trajectNodeAvailability: {},
|
|
27344
|
-
trajectFlightAvailability: {}
|
|
27442
|
+
trajectFlightAvailability: {},
|
|
27443
|
+
trajectDaySuggestions: {},
|
|
27444
|
+
trajectEditLineGuid: null,
|
|
27445
|
+
trajectActivitiesDayOrder: null
|
|
27345
27446
|
};
|
|
27346
27447
|
const searchResultsSlice = createSlice({
|
|
27347
27448
|
name: 'searchResults',
|
|
@@ -27557,6 +27658,11 @@ const searchResultsSlice = createSlice({
|
|
|
27557
27658
|
return;
|
|
27558
27659
|
state.editablePackagingEntry.lines[index] = action.payload;
|
|
27559
27660
|
},
|
|
27661
|
+
addEditableEntryLines(state, action) {
|
|
27662
|
+
if (!state.editablePackagingEntry)
|
|
27663
|
+
return;
|
|
27664
|
+
state.editablePackagingEntry.lines = [...(state.editablePackagingEntry.lines ?? []), ...action.payload];
|
|
27665
|
+
},
|
|
27560
27666
|
removeEditableEntryLines(state, action) {
|
|
27561
27667
|
if (!state.editablePackagingEntry)
|
|
27562
27668
|
return;
|
|
@@ -27588,13 +27694,43 @@ const searchResultsSlice = createSlice({
|
|
|
27588
27694
|
flight.selectedGuid = action.payload.guid;
|
|
27589
27695
|
flight.status = 'resolved';
|
|
27590
27696
|
},
|
|
27697
|
+
setTrajectDaySuggestionsLoading(state, action) {
|
|
27698
|
+
const dayOrder = action.payload;
|
|
27699
|
+
state.trajectDaySuggestions[dayOrder] = {
|
|
27700
|
+
dayOrder,
|
|
27701
|
+
status: 'loading',
|
|
27702
|
+
results: state.trajectDaySuggestions[dayOrder]?.results ?? []
|
|
27703
|
+
};
|
|
27704
|
+
},
|
|
27705
|
+
setTrajectDaySuggestions(state, action) {
|
|
27706
|
+
const { dayOrder, results } = action.payload;
|
|
27707
|
+
state.trajectDaySuggestions[dayOrder] = { dayOrder, status: 'resolved', results };
|
|
27708
|
+
},
|
|
27709
|
+
setTrajectEditLineGuid(state, action) {
|
|
27710
|
+
state.trajectEditLineGuid = action.payload;
|
|
27711
|
+
},
|
|
27712
|
+
setTrajectActivitiesDayOrder(state, action) {
|
|
27713
|
+
state.trajectActivitiesDayOrder = action.payload;
|
|
27714
|
+
},
|
|
27715
|
+
updateTrajectNodeResult(state, action) {
|
|
27716
|
+
const { lineGuid, result } = action.payload;
|
|
27717
|
+
const node = state.trajectNodeAvailability[lineGuid];
|
|
27718
|
+
if (!node)
|
|
27719
|
+
return;
|
|
27720
|
+
node.results = node.results.map((x) => (x.code === result.code ? result : x));
|
|
27721
|
+
node.selectedCode = result.code;
|
|
27722
|
+
node.status = 'resolved';
|
|
27723
|
+
},
|
|
27591
27724
|
resetTrajectAvailability(state) {
|
|
27592
27725
|
state.trajectNodeAvailability = {};
|
|
27593
27726
|
state.trajectFlightAvailability = {};
|
|
27727
|
+
state.trajectDaySuggestions = {};
|
|
27728
|
+
state.trajectEditLineGuid = null;
|
|
27729
|
+
state.trajectActivitiesDayOrder = null;
|
|
27594
27730
|
}
|
|
27595
27731
|
}
|
|
27596
27732
|
});
|
|
27597
|
-
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;
|
|
27733
|
+
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;
|
|
27598
27734
|
var searchResultsReducer = searchResultsSlice.reducer;
|
|
27599
27735
|
|
|
27600
27736
|
const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
|
|
@@ -27779,6 +27915,518 @@ const getDuration = (flight) => {
|
|
|
27779
27915
|
const numberOfNights = (segment) => {
|
|
27780
27916
|
return differenceInCalendarDays(new Date(segment.to), new Date(segment.from));
|
|
27781
27917
|
};
|
|
27918
|
+
const CHARTER_SERVICE_TYPE = 10;
|
|
27919
|
+
const CRUISE_SERVICE_TYPE = 11;
|
|
27920
|
+
const FERRY_SERVICE_TYPE = 12;
|
|
27921
|
+
const TRANSFER_SERVICE_TYPE = 13;
|
|
27922
|
+
const BUS_SERVICE_TYPE = 14;
|
|
27923
|
+
const CAR_SERVICE_TYPE = 17;
|
|
27924
|
+
const CAMPER_SERVICE_TYPE = 18;
|
|
27925
|
+
const TRAIN_SERVICE_TYPE = 20;
|
|
27926
|
+
const RENTAL_SERVICE_TYPE = 22;
|
|
27927
|
+
const RESTAURANT_SERVICE_TYPE = 23;
|
|
27928
|
+
const EVENT_SERVICE_TYPE = 24;
|
|
27929
|
+
const TRAINING_SERVICE_TYPE = 25;
|
|
27930
|
+
/** Icon per service type, so every node on the itinerary reads at a glance. */
|
|
27931
|
+
const getServiceTypeIcon = (serviceType) => {
|
|
27932
|
+
switch (serviceType) {
|
|
27933
|
+
case ACCOMMODATION_SERVICE_TYPE:
|
|
27934
|
+
return 'ui-hotel';
|
|
27935
|
+
case EXCURSION_SERVICE_TYPE:
|
|
27936
|
+
return 'ui-excursion';
|
|
27937
|
+
case FLIGHT_SERVICE_TYPE:
|
|
27938
|
+
case CHARTER_SERVICE_TYPE:
|
|
27939
|
+
return 'ui-plane';
|
|
27940
|
+
case RESTAURANT_SERVICE_TYPE:
|
|
27941
|
+
return 'ui-utensils';
|
|
27942
|
+
case CRUISE_SERVICE_TYPE:
|
|
27943
|
+
case FERRY_SERVICE_TYPE:
|
|
27944
|
+
return 'ui-ship';
|
|
27945
|
+
case CAR_SERVICE_TYPE:
|
|
27946
|
+
case CAMPER_SERVICE_TYPE:
|
|
27947
|
+
case RENTAL_SERVICE_TYPE:
|
|
27948
|
+
return 'ui-car';
|
|
27949
|
+
case BUS_SERVICE_TYPE:
|
|
27950
|
+
return 'ui-bus';
|
|
27951
|
+
case TRAIN_SERVICE_TYPE:
|
|
27952
|
+
return 'ui-train';
|
|
27953
|
+
case TRANSFER_SERVICE_TYPE:
|
|
27954
|
+
return 'ui-transfer';
|
|
27955
|
+
case EVENT_SERVICE_TYPE:
|
|
27956
|
+
return 'ui-ticket';
|
|
27957
|
+
case TRAINING_SERVICE_TYPE:
|
|
27958
|
+
return 'ui-group';
|
|
27959
|
+
default:
|
|
27960
|
+
return 'ui-location';
|
|
27961
|
+
}
|
|
27962
|
+
};
|
|
27963
|
+
const getServiceTypeBadgeModifier = (serviceType) => {
|
|
27964
|
+
switch (serviceType) {
|
|
27965
|
+
case FLIGHT_SERVICE_TYPE:
|
|
27966
|
+
case CHARTER_SERVICE_TYPE:
|
|
27967
|
+
case CRUISE_SERVICE_TYPE:
|
|
27968
|
+
case FERRY_SERVICE_TYPE:
|
|
27969
|
+
return 'flight';
|
|
27970
|
+
case ACCOMMODATION_SERVICE_TYPE:
|
|
27971
|
+
return 'hotel';
|
|
27972
|
+
case EXCURSION_SERVICE_TYPE:
|
|
27973
|
+
case EVENT_SERVICE_TYPE:
|
|
27974
|
+
return 'excursion';
|
|
27975
|
+
case RESTAURANT_SERVICE_TYPE:
|
|
27976
|
+
return 'restaurant';
|
|
27977
|
+
default:
|
|
27978
|
+
return '';
|
|
27979
|
+
}
|
|
27980
|
+
};
|
|
27981
|
+
const POI_TRAJECT_NODE_TYPE = 2;
|
|
27982
|
+
const getTrajectNodeIcon = (node) => node.trajectNodeType === POI_TRAJECT_NODE_TYPE ? 'ui-location' : getServiceTypeIcon(node.serviceType);
|
|
27983
|
+
const getTrajectNodeBadgeModifier = (node) => node.trajectNodeType === POI_TRAJECT_NODE_TYPE ? 'poi' : getServiceTypeBadgeModifier(node.serviceType);
|
|
27984
|
+
|
|
27985
|
+
const REGULAR_TRAJECT_NODE_TYPE = 0;
|
|
27986
|
+
const CONNECTING_TRAJECT_NODE_TYPE = 1;
|
|
27987
|
+
const isSearchableTrajectNode = (node) => node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE;
|
|
27988
|
+
const isTrajectFlightNode = (node) => node.serviceType === FLIGHT_SERVICE_TYPE;
|
|
27989
|
+
const getTrajectNodesWithLines = (trajectEntry) => {
|
|
27990
|
+
const linesByGuid = new Map((trajectEntry.entry.lines ?? []).map((line) => [line.guid, line]));
|
|
27991
|
+
return [...(trajectEntry.nodes ?? [])]
|
|
27992
|
+
.sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
|
|
27993
|
+
.map((node) => {
|
|
27994
|
+
const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
|
|
27995
|
+
const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
|
|
27996
|
+
return { node, line: lines[0], lines };
|
|
27997
|
+
})
|
|
27998
|
+
.filter((x) => !!x.line);
|
|
27999
|
+
};
|
|
28000
|
+
const getCoordinates = (line, node, result) => {
|
|
28001
|
+
const sources = [
|
|
28002
|
+
...(result ? [result] : []),
|
|
28003
|
+
...(line ? [line] : []),
|
|
28004
|
+
...(node?.alternatives ?? []).filter((x) => x.isPreferred),
|
|
28005
|
+
...(node?.alternatives ?? [])
|
|
28006
|
+
];
|
|
28007
|
+
const found = sources.find((x) => !!x.latitude && !!x.longitude);
|
|
28008
|
+
return found ? { latitude: found.latitude, longitude: found.longitude } : null;
|
|
28009
|
+
};
|
|
28010
|
+
const buildDestination = (line, node, result) => {
|
|
28011
|
+
const coordinates = getCoordinates(line, node, result);
|
|
28012
|
+
const place = (result?.locationId && { id: result.locationId, isLocation: true }) ||
|
|
28013
|
+
(result?.oordId && { id: result.oordId, isOord: true }) ||
|
|
28014
|
+
(result?.regionId && { id: result.regionId, isRegion: true }) ||
|
|
28015
|
+
(result?.countryId && { id: result.countryId, isCountry: true }) ||
|
|
28016
|
+
(line.location?.id && { id: line.location.id, isLocation: true }) ||
|
|
28017
|
+
(line.oord?.id && { id: line.oord.id, isOord: true }) ||
|
|
28018
|
+
(line.region?.id && { id: line.region.id, isRegion: true }) ||
|
|
28019
|
+
(line.country?.id && { id: line.country.id, isCountry: true }) ||
|
|
28020
|
+
null;
|
|
28021
|
+
return { id: 0, ...place, ...coordinates };
|
|
28022
|
+
};
|
|
28023
|
+
const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
|
|
28024
|
+
transactionId: context.transactionId,
|
|
28025
|
+
officeId: context.officeId,
|
|
28026
|
+
agentId: context.agentId ?? null,
|
|
28027
|
+
catalogueId: context.catalogueId,
|
|
28028
|
+
searchConfigurationId: context.searchConfigurationId,
|
|
28029
|
+
portalId: context.portalId ?? null,
|
|
28030
|
+
vendorConfigurationId: node.externalVendorId ?? null,
|
|
28031
|
+
language: context.language,
|
|
28032
|
+
serviceType: node.serviceType,
|
|
28033
|
+
fromDate: toDateOnlyString(line.from),
|
|
28034
|
+
toDate: toDateOnlyString(line.to),
|
|
28035
|
+
destination: buildDestination(line, node),
|
|
28036
|
+
productCode,
|
|
28037
|
+
rooms: context.rooms,
|
|
28038
|
+
tagIds: []
|
|
28039
|
+
});
|
|
28040
|
+
/**
|
|
28041
|
+
* Entry lines for an excursion the traveller picked out of the suggestions.
|
|
28042
|
+
*
|
|
28043
|
+
* A traject node already has its lines and only needs stamping; a suggestion has none, so this
|
|
28044
|
+
* builds them — one per selected option, as the regular day-by-day excursion flow does, since an
|
|
28045
|
+
* excursion's options are ticket types (adult, child) rather than rooms. Where the search marks no
|
|
28046
|
+
* default, the first option of each room stands in.
|
|
28047
|
+
*/
|
|
28048
|
+
const buildTrajectExcursionLines = (result) => {
|
|
28049
|
+
const options = (result.rooms ?? []).flatMap((room) => {
|
|
28050
|
+
const selected = (room.options ?? []).filter((x) => x.isSelected);
|
|
28051
|
+
return selected.length ? selected : (room.options ?? []).slice(0, 1);
|
|
28052
|
+
});
|
|
28053
|
+
if (!options.length)
|
|
28054
|
+
return [];
|
|
28055
|
+
const guids = options.map((option) => option.guid || crypto.randomUUID());
|
|
28056
|
+
return options.map((option, index) => ({
|
|
28057
|
+
guid: guids[index],
|
|
28058
|
+
moment: '',
|
|
28059
|
+
parentGuid: index === 0 ? null : guids[0],
|
|
28060
|
+
order: index,
|
|
28061
|
+
isChanged: true,
|
|
28062
|
+
from: result.fromDate,
|
|
28063
|
+
to: result.toDate,
|
|
28064
|
+
serviceType: EXCURSION_SERVICE_TYPE,
|
|
28065
|
+
productName: result.name,
|
|
28066
|
+
productCode: result.code,
|
|
28067
|
+
accommodationName: option.accommodationName,
|
|
28068
|
+
accommodationCode: option.accommodationCode,
|
|
28069
|
+
regimeName: option.regimeName,
|
|
28070
|
+
regimeCode: option.regimeCode,
|
|
28071
|
+
country: result.countryId ? { id: result.countryId, name: result.countryName, localizations: [] } : null,
|
|
28072
|
+
region: result.regionId ? { id: result.regionId, name: result.regionName, localizations: [] } : null,
|
|
28073
|
+
oord: result.oordId ? { id: result.oordId, name: result.oordName, localizations: [] } : null,
|
|
28074
|
+
location: result.locationId ? { id: result.locationId, name: result.locationName, localizations: [] } : null,
|
|
28075
|
+
longitude: result.longitude ?? null,
|
|
28076
|
+
latitude: result.latitude ?? null,
|
|
28077
|
+
pax: (option.paxIds ?? []).map((paxId, paxIndex) => ({ paxId, room: 0, order: paxIndex })),
|
|
28078
|
+
flightInformation: null
|
|
28079
|
+
}));
|
|
28080
|
+
};
|
|
28081
|
+
const buildDayActivityCards = (items, availability, suggestions) => {
|
|
28082
|
+
const cards = [];
|
|
28083
|
+
const seen = new Set();
|
|
28084
|
+
const push = (result, item) => {
|
|
28085
|
+
if (!result || seen.has(result.code))
|
|
28086
|
+
return;
|
|
28087
|
+
seen.add(result.code);
|
|
28088
|
+
cards.push({ result, item });
|
|
28089
|
+
};
|
|
28090
|
+
items.forEach((item) => {
|
|
28091
|
+
const results = availability[item.line.guid]?.results ?? [];
|
|
28092
|
+
const selected = results.find((x) => x.code === availability[item.line.guid]?.selectedCode);
|
|
28093
|
+
push(selected, item);
|
|
28094
|
+
results.filter((x) => !!findCandidateForResult(item.node, x)).forEach((result) => push(result, item));
|
|
28095
|
+
});
|
|
28096
|
+
suggestions.forEach((result) => push(result));
|
|
28097
|
+
// Whatever the nodes' own destination search found and the day's search did not — normally
|
|
28098
|
+
// nothing, as they are the same search, but a node can resolve where a day has no place to look.
|
|
28099
|
+
items.forEach((item) => (availability[item.line.guid]?.results ?? []).forEach((result) => push(result, item)));
|
|
28100
|
+
return cards;
|
|
28101
|
+
};
|
|
28102
|
+
const getNodeCandidates = (node) => {
|
|
28103
|
+
const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
|
|
28104
|
+
if (configured.length)
|
|
28105
|
+
return configured;
|
|
28106
|
+
const fallbackCode = node.code ?? node.preferredAccommodationCode;
|
|
28107
|
+
if (!fallbackCode || !fallbackCode.trim().length)
|
|
28108
|
+
return [];
|
|
28109
|
+
return [
|
|
28110
|
+
{
|
|
28111
|
+
code: fallbackCode,
|
|
28112
|
+
name: node.name,
|
|
28113
|
+
description: node.description,
|
|
28114
|
+
imageUrl: node.imageUrl,
|
|
28115
|
+
contentSource: node.contentSource,
|
|
28116
|
+
vendor: node.vendor,
|
|
28117
|
+
externalVendorId: node.externalVendorId,
|
|
28118
|
+
isPreferred: true
|
|
28119
|
+
}
|
|
28120
|
+
];
|
|
28121
|
+
};
|
|
28122
|
+
const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
|
|
28123
|
+
const matchesCode = (result, code) => {
|
|
28124
|
+
if (result.code === code)
|
|
28125
|
+
return true;
|
|
28126
|
+
return (result.rooms ?? []).some((room) => (room.options ?? []).some((option) => option.accommodationCode === code));
|
|
28127
|
+
};
|
|
28128
|
+
const getPackagingRoomsFromEntry = (entry) => {
|
|
28129
|
+
const paxById = new Map((entry.pax ?? []).map((p) => [p.id, p]));
|
|
28130
|
+
const rooms = (entry.rooms ?? []).map((room) => ({
|
|
28131
|
+
travellers: (room.paxIds ?? []).map((paxId) => {
|
|
28132
|
+
const pax = paxById.get(paxId);
|
|
28133
|
+
return {
|
|
28134
|
+
id: paxId,
|
|
28135
|
+
age: pax?.age ?? null,
|
|
28136
|
+
dateOfBirth: pax?.dateOfBirth ?? null
|
|
28137
|
+
};
|
|
28138
|
+
})
|
|
28139
|
+
}));
|
|
28140
|
+
return rooms.filter((room) => room.travellers.length > 0);
|
|
28141
|
+
};
|
|
28142
|
+
const pickCheapest = (results) => [...results].sort((a, b) => a.price - b.price)[0] ?? null;
|
|
28143
|
+
const applyResultToLines = (lines, result) => lines.map((line, index) => applyResultToLine(line, result, index));
|
|
28144
|
+
const getOptionForRoom = (result, roomIndex) => {
|
|
28145
|
+
const room = (result.rooms ?? [])[roomIndex];
|
|
28146
|
+
if (room)
|
|
28147
|
+
return (room.options ?? []).find((x) => x.isSelected) ?? (room.options ?? [])[0];
|
|
28148
|
+
return (result.rooms ?? []).flatMap((x) => x.options ?? []).find((x) => x.isSelected) ?? (result.rooms ?? [])[0]?.options?.[0];
|
|
28149
|
+
};
|
|
28150
|
+
const applyResultToLine = (line, result, roomIndex = 0) => {
|
|
28151
|
+
const option = getOptionForRoom(result, roomIndex);
|
|
28152
|
+
return {
|
|
28153
|
+
...line,
|
|
28154
|
+
productName: result.name ?? line.productName,
|
|
28155
|
+
productCode: result.code ?? line.productCode,
|
|
28156
|
+
accommodationCode: option?.accommodationCode ?? line.accommodationCode,
|
|
28157
|
+
accommodationName: option?.accommodationName ?? line.accommodationName,
|
|
28158
|
+
regimeCode: option?.regimeCode ?? line.regimeCode,
|
|
28159
|
+
regimeName: option?.regimeName ?? line.regimeName,
|
|
28160
|
+
latitude: result.latitude ?? line.latitude,
|
|
28161
|
+
longitude: result.longitude ?? line.longitude,
|
|
28162
|
+
isChanged: true
|
|
28163
|
+
};
|
|
28164
|
+
};
|
|
28165
|
+
const toDateOnlyUtcString = (value) => {
|
|
28166
|
+
const date = new Date(value);
|
|
28167
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())).toISOString();
|
|
28168
|
+
};
|
|
28169
|
+
const toTimeOnlyString = (value) => {
|
|
28170
|
+
const date = new Date(value);
|
|
28171
|
+
const hh = String(date.getUTCHours()).padStart(2, '0');
|
|
28172
|
+
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
|
28173
|
+
const ss = String(date.getUTCSeconds()).padStart(2, '0');
|
|
28174
|
+
return `${hh}:${mm}:${ss}`;
|
|
28175
|
+
};
|
|
28176
|
+
const mapFlightSegmentsToFlightLines = (segments) => segments.map((segment) => ({
|
|
28177
|
+
airlineCode: segment.marketingAirlineCode,
|
|
28178
|
+
airlineDescription: segment.marketingAirlineName,
|
|
28179
|
+
operatingAirlineCode: segment.operatingAirlineCode,
|
|
28180
|
+
operatingAirlineDescription: segment.operatingAirlineName,
|
|
28181
|
+
flightNumber: segment.flightNumber,
|
|
28182
|
+
operatingFlightNumber: segment.operatingFlightNumber ?? null,
|
|
28183
|
+
departureDate: toDateOnlyUtcString(segment.departureDateTime),
|
|
28184
|
+
departureTime: toTimeOnlyString(segment.departureDateTime),
|
|
28185
|
+
departureAirportCode: segment.departureAirportCode,
|
|
28186
|
+
departureAirportDescription: segment.departureAirportName,
|
|
28187
|
+
arrivalDate: toDateOnlyUtcString(segment.arrivalDateTime),
|
|
28188
|
+
arrivalTime: toTimeOnlyString(segment.arrivalDateTime),
|
|
28189
|
+
arrivalAirportCode: segment.arrivalAirportCode,
|
|
28190
|
+
arrivalAirportDescription: segment.arrivalAirportName,
|
|
28191
|
+
durationInTicks: segment.durationInTicks
|
|
28192
|
+
}));
|
|
28193
|
+
const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
|
|
28194
|
+
transactionId: context.transactionId,
|
|
28195
|
+
officeId: context.officeId,
|
|
28196
|
+
catalogueId: context.catalogueId,
|
|
28197
|
+
agentId: context.agentId ?? null,
|
|
28198
|
+
language: context.language,
|
|
28199
|
+
departureAirportCode: node.departureAirportCode ?? '',
|
|
28200
|
+
arrivalAirportCode: node.arrivalAirportCode ?? '',
|
|
28201
|
+
returnAirportCode: null,
|
|
28202
|
+
luggageIncluded: null,
|
|
28203
|
+
maxStops: null,
|
|
28204
|
+
travelClass: null,
|
|
28205
|
+
vendorConfigurationId: node.externalVendorId ?? null,
|
|
28206
|
+
pax,
|
|
28207
|
+
outward: { date: dateToDateStruct(new Date(line.from)) },
|
|
28208
|
+
return: null
|
|
28209
|
+
});
|
|
28210
|
+
const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
|
|
28211
|
+
const applyFlightToLine = (line, flight) => {
|
|
28212
|
+
const segments = flight.outward?.segments ?? [];
|
|
28213
|
+
if (!segments.length)
|
|
28214
|
+
return line;
|
|
28215
|
+
const firstSegment = segments[0];
|
|
28216
|
+
const lastSegment = segments[segments.length - 1];
|
|
28217
|
+
return {
|
|
28218
|
+
...line,
|
|
28219
|
+
from: new Date(firstSegment.departureDateTime).toISOString(),
|
|
28220
|
+
to: new Date(lastSegment.arrivalDateTime).toISOString(),
|
|
28221
|
+
productName: `${firstSegment.departureAirportName} - ${lastSegment.arrivalAirportName} (${firstSegment.marketingAirlineName})`,
|
|
28222
|
+
productCode: `${firstSegment.departureAirportCode} ${lastSegment.arrivalAirportCode}/${firstSegment.marketingAirlineCode}`,
|
|
28223
|
+
accommodationName: firstSegment.metaData?.farePriceClassName ?? line.accommodationName,
|
|
28224
|
+
accommodationCode: firstSegment.metaData?.fareCode ?? line.accommodationCode,
|
|
28225
|
+
flightInformation: {
|
|
28226
|
+
pnr: '',
|
|
28227
|
+
flightLines: mapFlightSegmentsToFlightLines(segments)
|
|
28228
|
+
},
|
|
28229
|
+
isChanged: true
|
|
28230
|
+
};
|
|
28231
|
+
};
|
|
28232
|
+
/**
|
|
28233
|
+
* Externally sourced products carry a vendor configuration; products held internally in Tide have
|
|
28234
|
+
* none. `vendorType` is not a reliable signal — the packaging service nulls it out when the search
|
|
28235
|
+
* configuration has ExposeProvider off — so the vendor ids are what we go on.
|
|
28236
|
+
*/
|
|
28237
|
+
const isExternalResult = (result) => result.vendorId != null || result.vendorConfigurationId != null;
|
|
28238
|
+
const isExternalCandidate = (candidate) => candidate.externalVendorId != null || candidate.vendor != null;
|
|
28239
|
+
|
|
28240
|
+
const badgeClass = (kind, modifier) => {
|
|
28241
|
+
const base = `search__filter__itinerary__${kind}-badge`;
|
|
28242
|
+
const fallback = kind === 'segment' ? `${base}--secondary` : `${base}--default`;
|
|
28243
|
+
return `${base} ${modifier ? `${base}--${modifier}` : fallback}`;
|
|
28244
|
+
};
|
|
28245
|
+
const nightsOf = (node, line) => differenceInCalendarDays(new Date(line?.to ?? node.to), new Date(line?.from ?? node.from));
|
|
28246
|
+
const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
|
|
28247
|
+
const timeline = useMemo(() => {
|
|
28248
|
+
const linesByGuid = new Map((packagingEntry.lines ?? []).map((line) => [line.guid, line]));
|
|
28249
|
+
const nodes = [...(trajectEntry.nodes ?? [])]
|
|
28250
|
+
.sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
|
|
28251
|
+
.map((node) => {
|
|
28252
|
+
const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
|
|
28253
|
+
const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
|
|
28254
|
+
return { node, lines };
|
|
28255
|
+
});
|
|
28256
|
+
const claimed = new Set(nodes.flatMap((item) => item.lines.map((line) => line.guid)));
|
|
28257
|
+
const extras = (packagingEntry.lines ?? []).filter((line) => !claimed.has(line.guid));
|
|
28258
|
+
if (!extras.length)
|
|
28259
|
+
return nodes;
|
|
28260
|
+
const dayOrderOf = (date) => trajectEntry.traject.days.find((day) => isSameDay(new Date(day.date), new Date(date)))?.order ??
|
|
28261
|
+
Math.max(differenceInCalendarDays(new Date(date), new Date(trajectEntry.traject.startDate)), 0);
|
|
28262
|
+
// One product bought once, however many option lines it took (an adult and a child ticket).
|
|
28263
|
+
const grouped = new Map();
|
|
28264
|
+
extras.forEach((line) => {
|
|
28265
|
+
const key = `${line.productCode}|${line.from}`;
|
|
28266
|
+
grouped.set(key, [...(grouped.get(key) ?? []), line]);
|
|
28267
|
+
});
|
|
28268
|
+
const byDay = new Map();
|
|
28269
|
+
nodes.forEach((item) => byDay.set(item.node.dayOrder, [...(byDay.get(item.node.dayOrder) ?? []), item]));
|
|
28270
|
+
[...grouped.values()].forEach((lines, index) => {
|
|
28271
|
+
const line = lines[0];
|
|
28272
|
+
const dayOrder = dayOrderOf(line.from);
|
|
28273
|
+
const item = {
|
|
28274
|
+
lines,
|
|
28275
|
+
node: {
|
|
28276
|
+
nodeId: -(index + 1),
|
|
28277
|
+
lineGuid: line.guid,
|
|
28278
|
+
lineGuids: lines.map((x) => x.guid),
|
|
28279
|
+
dayOrder,
|
|
28280
|
+
order: Number.MAX_SAFE_INTEGER,
|
|
28281
|
+
name: line.productName,
|
|
28282
|
+
serviceType: line.serviceType,
|
|
28283
|
+
trajectNodeType: REGULAR_TRAJECT_NODE_TYPE,
|
|
28284
|
+
requirementType: 0,
|
|
28285
|
+
contentSource: 0,
|
|
28286
|
+
alternatives: [],
|
|
28287
|
+
from: line.from,
|
|
28288
|
+
to: line.to
|
|
28289
|
+
}
|
|
28290
|
+
};
|
|
28291
|
+
const dayItems = [...(byDay.get(dayOrder) ?? [])];
|
|
28292
|
+
let at = dayItems.length;
|
|
28293
|
+
while (at > 0 && dayItems[at - 1].node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE)
|
|
28294
|
+
at--;
|
|
28295
|
+
dayItems.splice(at, 0, item);
|
|
28296
|
+
byDay.set(dayOrder, dayItems);
|
|
28297
|
+
});
|
|
28298
|
+
return [...byDay.keys()].sort((a, b) => a - b).flatMap((dayOrder) => byDay.get(dayOrder));
|
|
28299
|
+
}, [trajectEntry, packagingEntry]);
|
|
28300
|
+
const stayByDay = useMemo(() => {
|
|
28301
|
+
const map = new Map();
|
|
28302
|
+
timeline
|
|
28303
|
+
.filter(({ node }) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === ACCOMMODATION_SERVICE_TYPE)
|
|
28304
|
+
.forEach(({ node, lines }) => {
|
|
28305
|
+
const line = lines[0];
|
|
28306
|
+
const nights = Math.max(nightsOf(node, line), 1);
|
|
28307
|
+
const name = line?.productName || node.preferredAccommodationName || node.name || '';
|
|
28308
|
+
for (let night = 0; night < nights; night++)
|
|
28309
|
+
map.set(node.dayOrder + night, { name, night, nights });
|
|
28310
|
+
});
|
|
28311
|
+
return map;
|
|
28312
|
+
}, [timeline]);
|
|
28313
|
+
const renderDate = (value) => (React__default.createElement("div", { className: "search__filter__itinerary__transport-date" },
|
|
28314
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
28315
|
+
React__default.createElement("strong", null, format$2(new Date(value), 'd'))),
|
|
28316
|
+
React__default.createElement("p", null, format$2(new Date(value), 'MMM'))));
|
|
28317
|
+
const renderConnectingNode = ({ node }) => {
|
|
28318
|
+
const distance = node.distanceInKm ? `${Math.round(node.distanceInKm)} km` : '';
|
|
28319
|
+
return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `connect-${node.nodeId}` },
|
|
28320
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28321
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28322
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28323
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-date" }),
|
|
28324
|
+
React__default.createElement("div", { className: badgeClass('transport', getTrajectNodeBadgeModifier(node)) },
|
|
28325
|
+
React__default.createElement(Icon, { name: getTrajectNodeIcon(node), height: 15 })),
|
|
28326
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28327
|
+
React__default.createElement("h6", null, node.name || distance),
|
|
28328
|
+
!!node.name && !!distance && React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" }, distance)))));
|
|
28329
|
+
};
|
|
28330
|
+
const renderOvernight = (item, index) => {
|
|
28331
|
+
const { dayOrder } = item.node;
|
|
28332
|
+
const closesTheDay = timeline[index + 1]?.node.dayOrder !== dayOrder;
|
|
28333
|
+
const stay = stayByDay.get(dayOrder);
|
|
28334
|
+
if (!closesTheDay || !stay)
|
|
28335
|
+
return null;
|
|
28336
|
+
return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `overnight-${item.node.nodeId}` },
|
|
28337
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28338
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28339
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28340
|
+
renderDate(item.node.from),
|
|
28341
|
+
React__default.createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(ACCOMMODATION_SERVICE_TYPE)) },
|
|
28342
|
+
React__default.createElement(Icon, { name: "ui-bed", height: 15 })),
|
|
28343
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28344
|
+
React__default.createElement("h6", null, stay.name),
|
|
28345
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" },
|
|
28346
|
+
stay.night + 1,
|
|
28347
|
+
" ",
|
|
28348
|
+
translations.SRP.OF,
|
|
28349
|
+
" ",
|
|
28350
|
+
stay.nights,
|
|
28351
|
+
" ",
|
|
28352
|
+
translations.SRP.NIGHTS)))));
|
|
28353
|
+
};
|
|
28354
|
+
const renderFlightNode = ({ node, lines }) => {
|
|
28355
|
+
const line = lines[0];
|
|
28356
|
+
return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `flight-${node.nodeId}` },
|
|
28357
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28358
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28359
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28360
|
+
renderDate(line?.from ?? node.from),
|
|
28361
|
+
React__default.createElement("div", { className: badgeClass('transport', getServiceTypeBadgeModifier(FLIGHT_SERVICE_TYPE)) },
|
|
28362
|
+
React__default.createElement(Icon, { name: "ui-plane", height: 15 })),
|
|
28363
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28364
|
+
React__default.createElement("h6", null, line?.productName || node.name),
|
|
28365
|
+
!!getDepartureTime(line) && (React__default.createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
|
|
28366
|
+
React__default.createElement("span", null,
|
|
28367
|
+
React__default.createElement(Icon, { name: "ui-plane-depart", height: 14 }),
|
|
28368
|
+
" ",
|
|
28369
|
+
React__default.createElement("strong", null, getDepartureTime(line))),
|
|
28370
|
+
' ',
|
|
28371
|
+
"-",
|
|
28372
|
+
' ',
|
|
28373
|
+
React__default.createElement("span", null,
|
|
28374
|
+
React__default.createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
|
|
28375
|
+
" ",
|
|
28376
|
+
React__default.createElement("strong", null, getArrivalTime(line))))),
|
|
28377
|
+
!!getDuration(line) && (React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" },
|
|
28378
|
+
React__default.createElement("span", null,
|
|
28379
|
+
React__default.createElement(Icon, { name: "ui-clock", height: 20 }),
|
|
28380
|
+
" ",
|
|
28381
|
+
getDuration(line))))))));
|
|
28382
|
+
};
|
|
28383
|
+
const renderServiceNode = ({ node, lines }) => {
|
|
28384
|
+
const line = lines[0];
|
|
28385
|
+
const isStay = node.serviceType === ACCOMMODATION_SERVICE_TYPE;
|
|
28386
|
+
const nights = isStay ? nightsOf(node, line) : 0;
|
|
28387
|
+
return (React__default.createElement("div", { className: "search__filter__itinerary__segments", key: `node-${node.nodeId}` },
|
|
28388
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segments__wrapper" },
|
|
28389
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline" },
|
|
28390
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline-line" })),
|
|
28391
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment" },
|
|
28392
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment-item" },
|
|
28393
|
+
React__default.createElement("div", { className: `search__filter__itinerary__segment-date ${isStay && nights > 0 ? 'search__filter__itinerary__segment-nights' : ''}` }, isStay && nights > 0 ? (React__default.createElement(React__default.Fragment, null,
|
|
28394
|
+
React__default.createElement("p", { className: "search__filter__itinerary__segment-date-date" },
|
|
28395
|
+
React__default.createElement("strong", null, nights)),
|
|
28396
|
+
React__default.createElement(Icon, { name: "ui-moon", width: 16, height: 16 }))) : (React__default.createElement(React__default.Fragment, null,
|
|
28397
|
+
React__default.createElement("p", { className: "search__filter__itinerary__segment-date-date" },
|
|
28398
|
+
React__default.createElement("strong", null, format$2(new Date(line?.from ?? node.from), 'd'))),
|
|
28399
|
+
React__default.createElement("p", null, format$2(new Date(line?.from ?? node.from), 'MMM'))))),
|
|
28400
|
+
React__default.createElement("div", { className: badgeClass('segment', getTrajectNodeBadgeModifier(node)) },
|
|
28401
|
+
React__default.createElement(Icon, { name: getTrajectNodeIcon(node), width: 15, height: 15 })),
|
|
28402
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment-details" },
|
|
28403
|
+
React__default.createElement("h6", null, line?.productName || node.preferredAccommodationName || node.name),
|
|
28404
|
+
isStay &&
|
|
28405
|
+
lines.map((roomLine, index) => (React__default.createElement(React__default.Fragment, { key: roomLine.guid },
|
|
28406
|
+
lines.length > 1 && (React__default.createElement("strong", null,
|
|
28407
|
+
translations.SHARED.ROOM,
|
|
28408
|
+
"\u00A0",
|
|
28409
|
+
index + 1)),
|
|
28410
|
+
roomLine.accommodationName && (React__default.createElement("div", { className: "search__filter__itinerary__segment-details__room" },
|
|
28411
|
+
React__default.createElement(Icon, { name: "ui-bed", width: 12, height: 12 }),
|
|
28412
|
+
roomLine.accommodationName)),
|
|
28413
|
+
roomLine.regimeName && (React__default.createElement("div", { className: "search__filter__itinerary__segment-details__room" },
|
|
28414
|
+
React__default.createElement(Icon, { name: "ui-utensils", width: 12, height: 12 }),
|
|
28415
|
+
roomLine.regimeName))))),
|
|
28416
|
+
!isStay && React__default.createElement("p", { className: "search__filter__itinerary__segment-details-text" }, format$2(new Date(line?.from ?? node.from), 'EEE. d MMM yyyy'))))))));
|
|
28417
|
+
};
|
|
28418
|
+
return (React__default.createElement(React__default.Fragment, null, timeline.map((item, index) => {
|
|
28419
|
+
if (item.node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
|
|
28420
|
+
return (React__default.createElement(React__default.Fragment, { key: `connect-${item.node.nodeId}` },
|
|
28421
|
+
renderConnectingNode(item),
|
|
28422
|
+
renderOvernight(item, index)));
|
|
28423
|
+
}
|
|
28424
|
+
if (item.node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && item.node.serviceType === FLIGHT_SERVICE_TYPE)
|
|
28425
|
+
return renderFlightNode(item);
|
|
28426
|
+
return renderServiceNode(item);
|
|
28427
|
+
})));
|
|
28428
|
+
};
|
|
28429
|
+
|
|
27782
28430
|
const getSegmentIcon = (segment) => {
|
|
27783
28431
|
switch (segment.serviceType) {
|
|
27784
28432
|
case 3:
|
|
@@ -27902,116 +28550,119 @@ const Itinerary = ({ isOpen, handleSetIsOpen, isLoading, onEditAccommodation })
|
|
|
27902
28550
|
format$2(new Date(firstEntryLine.from), 'EEE. d MMM yyyy'),
|
|
27903
28551
|
" - ",
|
|
27904
28552
|
React__default.createElement("strong", null, translations.SRP.START))))),
|
|
27905
|
-
|
|
27906
|
-
React__default.createElement("div", { className: "search__filter__itinerary__transport
|
|
27907
|
-
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline
|
|
27908
|
-
|
|
27909
|
-
React__default.createElement("div", { className: "search__filter__itinerary__transport-
|
|
27910
|
-
React__default.createElement("
|
|
27911
|
-
React__default.createElement("
|
|
27912
|
-
|
|
27913
|
-
|
|
27914
|
-
React__default.createElement(
|
|
27915
|
-
|
|
27916
|
-
React__default.createElement("
|
|
27917
|
-
|
|
27918
|
-
React__default.createElement("
|
|
27919
|
-
React__default.createElement(
|
|
27920
|
-
|
|
27921
|
-
|
|
27922
|
-
|
|
27923
|
-
|
|
27924
|
-
|
|
27925
|
-
|
|
27926
|
-
React__default.createElement(
|
|
27927
|
-
|
|
27928
|
-
|
|
27929
|
-
|
|
27930
|
-
React__default.createElement("
|
|
27931
|
-
React__default.createElement(
|
|
27932
|
-
|
|
27933
|
-
|
|
27934
|
-
|
|
27935
|
-
groupedOtherSegments
|
|
27936
|
-
|
|
27937
|
-
|
|
27938
|
-
|
|
27939
|
-
|
|
27940
|
-
React__default.createElement("div", { className: "
|
|
27941
|
-
React__default.createElement("div", { className: "
|
|
27942
|
-
React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline
|
|
27943
|
-
|
|
27944
|
-
React__default.createElement("div", { className: "search__filter__itinerary__segment
|
|
27945
|
-
React__default.createElement("div", { className: "
|
|
27946
|
-
React__default.createElement("
|
|
27947
|
-
React__default.createElement("
|
|
27948
|
-
|
|
27949
|
-
|
|
27950
|
-
React__default.createElement(
|
|
27951
|
-
|
|
27952
|
-
React__default.createElement("
|
|
27953
|
-
|
|
27954
|
-
|
|
27955
|
-
|
|
27956
|
-
|
|
27957
|
-
|
|
27958
|
-
|
|
27959
|
-
|
|
27960
|
-
|
|
27961
|
-
|
|
27962
|
-
|
|
27963
|
-
|
|
27964
|
-
|
|
27965
|
-
|
|
27966
|
-
|
|
27967
|
-
|
|
27968
|
-
|
|
27969
|
-
React__default.createElement(
|
|
27970
|
-
|
|
27971
|
-
|
|
27972
|
-
|
|
27973
|
-
firstSegment
|
|
27974
|
-
|
|
27975
|
-
|
|
27976
|
-
|
|
27977
|
-
|
|
27978
|
-
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
|
|
27983
|
-
|
|
27984
|
-
|
|
27985
|
-
|
|
27986
|
-
|
|
27987
|
-
|
|
27988
|
-
|
|
27989
|
-
React__default.createElement("div", { className: "search__filter__itinerary__transport
|
|
27990
|
-
React__default.createElement("div", { className: "search__filter__itinerary__transport-
|
|
27991
|
-
React__default.createElement("
|
|
27992
|
-
|
|
27993
|
-
React__default.createElement("
|
|
27994
|
-
|
|
27995
|
-
|
|
27996
|
-
|
|
27997
|
-
React__default.createElement("
|
|
27998
|
-
|
|
27999
|
-
|
|
28000
|
-
|
|
28001
|
-
|
|
28002
|
-
React__default.createElement("
|
|
28003
|
-
|
|
28004
|
-
|
|
28005
|
-
|
|
28006
|
-
|
|
28007
|
-
|
|
28008
|
-
|
|
28009
|
-
React__default.createElement("
|
|
28010
|
-
|
|
28011
|
-
|
|
28012
|
-
|
|
28013
|
-
|
|
28014
|
-
|
|
28553
|
+
context?.trajectEntry ? (React__default.createElement(TrajectItinerary, { trajectEntry: context.trajectEntry, packagingEntry: packagingEntry, translations: translations })) : (React__default.createElement(React__default.Fragment, null,
|
|
28554
|
+
outboundFlight && (React__default.createElement("div", { className: "search__filter__itinerary__transport" },
|
|
28555
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28556
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28557
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28558
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-date" },
|
|
28559
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
28560
|
+
React__default.createElement("strong", null, format$2(new Date(outboundFlight.from), 'd'))),
|
|
28561
|
+
React__default.createElement("p", null, format$2(new Date(outboundFlight.from), 'MMM'))),
|
|
28562
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-badge" },
|
|
28563
|
+
React__default.createElement(Icon, { name: "ui-plane", height: 15 })),
|
|
28564
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28565
|
+
React__default.createElement("h6", null, outboundFlight.productName),
|
|
28566
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
|
|
28567
|
+
React__default.createElement("span", null,
|
|
28568
|
+
React__default.createElement(Icon, { name: "ui-plane-depart", height: 14 }),
|
|
28569
|
+
" ",
|
|
28570
|
+
React__default.createElement("strong", null, getDepartureTime(outboundFlight))),
|
|
28571
|
+
' ',
|
|
28572
|
+
"-",
|
|
28573
|
+
' ',
|
|
28574
|
+
React__default.createElement("span", null,
|
|
28575
|
+
React__default.createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
|
|
28576
|
+
" ",
|
|
28577
|
+
React__default.createElement("strong", null, getArrivalTime(outboundFlight)))),
|
|
28578
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" },
|
|
28579
|
+
React__default.createElement("span", null,
|
|
28580
|
+
React__default.createElement(Icon, { name: "ui-clock", height: 20 }),
|
|
28581
|
+
" ",
|
|
28582
|
+
getDuration(outboundFlight))))))),
|
|
28583
|
+
!isEmpty$1(groupedOtherSegments) &&
|
|
28584
|
+
groupedOtherSegments.map((group) => {
|
|
28585
|
+
const firstSegment = first(group.segments);
|
|
28586
|
+
if (!firstSegment)
|
|
28587
|
+
return null;
|
|
28588
|
+
return (React__default.createElement("div", { className: "search__filter__itinerary__segments" },
|
|
28589
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segments__wrapper", key: group.key },
|
|
28590
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline" },
|
|
28591
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segments-timeline-line" })),
|
|
28592
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment" },
|
|
28593
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment-item search__filter__itinerary__segment-item--start" },
|
|
28594
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-date" },
|
|
28595
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
28596
|
+
React__default.createElement("strong", null, format$2(new Date(firstSegment.from), 'd'))),
|
|
28597
|
+
React__default.createElement("p", null, format$2(new Date(firstSegment.from), 'MMM'))),
|
|
28598
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment-badge" },
|
|
28599
|
+
React__default.createElement(Icon, { name: "ui-location", width: 15, height: 15 })),
|
|
28600
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment-details" },
|
|
28601
|
+
React__default.createElement("h6", null,
|
|
28602
|
+
firstSegment.location?.name || firstSegment.oord?.name || firstSegment.region?.name,
|
|
28603
|
+
(firstSegment.location?.name || firstSegment.oord?.name || firstSegment.region?.name) && firstSegment.country?.name
|
|
28604
|
+
? ', '
|
|
28605
|
+
: '',
|
|
28606
|
+
firstSegment.country?.name),
|
|
28607
|
+
React__default.createElement("p", { className: "search__filter__itinerary__segment-details-text" },
|
|
28608
|
+
format$2(new Date(firstSegment.from), 'EEE. d MMM yyyy'),
|
|
28609
|
+
"> ",
|
|
28610
|
+
format$2(new Date(firstSegment.to), 'EEE. d MMM yyyy'))))),
|
|
28611
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment" },
|
|
28612
|
+
React__default.createElement("div", { className: `search__filter__itinerary__segment-item ${canEdit(firstSegment) ? 'search__filter__itinerary__segment-item--editable' : ''}`, onClick: () => {
|
|
28613
|
+
if (canEdit(firstSegment) && onEditAccommodation) {
|
|
28614
|
+
onEditAccommodation(group.segments);
|
|
28615
|
+
}
|
|
28616
|
+
} },
|
|
28617
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment-date search__filter__itinerary__segment-nights" }, firstSegment.serviceType === ACCOMMODATION_SERVICE_TYPE && (React__default.createElement(React__default.Fragment, null,
|
|
28618
|
+
React__default.createElement("p", { className: "search__filter__itinerary__segment-date-date" },
|
|
28619
|
+
React__default.createElement("strong", null, numberOfNights(firstSegment))),
|
|
28620
|
+
React__default.createElement(Icon, { name: "ui-moon", width: 16, height: 16 })))),
|
|
28621
|
+
getSegmentIcon(firstSegment),
|
|
28622
|
+
React__default.createElement("div", { className: "search__filter__itinerary__segment-details" },
|
|
28623
|
+
React__default.createElement("h6", null, getSegmentTitle(firstSegment)),
|
|
28624
|
+
firstSegment.serviceType === ACCOMMODATION_SERVICE_TYPE &&
|
|
28625
|
+
group.segments.map((segment, index) => (React__default.createElement(React__default.Fragment, { key: segment.guid },
|
|
28626
|
+
React__default.createElement("strong", null,
|
|
28627
|
+
translations.SHARED.ROOM,
|
|
28628
|
+
"\u00A0",
|
|
28629
|
+
index + 1),
|
|
28630
|
+
segment.productName && segment.accommodationName && (React__default.createElement("div", { className: "search__filter__itinerary__segment-details__room" },
|
|
28631
|
+
React__default.createElement(Icon, { name: "ui-bed", width: 12, height: 12 }),
|
|
28632
|
+
segment.accommodationName)),
|
|
28633
|
+
segment.regimeName && (React__default.createElement("div", { className: "search__filter__itinerary__segment-details__room" },
|
|
28634
|
+
React__default.createElement(Icon, { name: "ui-utensils", width: 12, height: 12 }),
|
|
28635
|
+
segment.regimeName)))))))))));
|
|
28636
|
+
}),
|
|
28637
|
+
returnFlight && returnFlight !== outboundFlight && (React__default.createElement("div", { className: "search__filter__itinerary__transport" },
|
|
28638
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
28639
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
28640
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-item" },
|
|
28641
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-date" },
|
|
28642
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
28643
|
+
React__default.createElement("strong", null, format$2(new Date(returnFlight.from), 'd'))),
|
|
28644
|
+
React__default.createElement("p", null, format$2(new Date(returnFlight.from), 'MMM'))),
|
|
28645
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-badge" },
|
|
28646
|
+
React__default.createElement(Icon, { name: "ui-plane", height: 15 })),
|
|
28647
|
+
React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
28648
|
+
React__default.createElement("h6", null, returnFlight.productName),
|
|
28649
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-details-plane" },
|
|
28650
|
+
React__default.createElement("span", null,
|
|
28651
|
+
React__default.createElement(Icon, { name: "ui-plane-depart", height: 14 }),
|
|
28652
|
+
" ",
|
|
28653
|
+
React__default.createElement("strong", null, getDepartureTime(returnFlight))),
|
|
28654
|
+
' ',
|
|
28655
|
+
"-",
|
|
28656
|
+
' ',
|
|
28657
|
+
React__default.createElement("span", null,
|
|
28658
|
+
React__default.createElement(Icon, { name: "ui-plane-arrive", height: 14 }),
|
|
28659
|
+
" ",
|
|
28660
|
+
React__default.createElement("strong", null, getArrivalTime(returnFlight)))),
|
|
28661
|
+
React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" },
|
|
28662
|
+
React__default.createElement("span", null,
|
|
28663
|
+
React__default.createElement(Icon, { name: "ui-clock", height: 20 }),
|
|
28664
|
+
" ",
|
|
28665
|
+
getDuration(returnFlight))))))))),
|
|
28015
28666
|
lastEntryLine && (React__default.createElement("div", { className: "search__filter__itinerary__country" },
|
|
28016
28667
|
React__default.createElement("div", { className: "search__filter__itinerary__country-icon" },
|
|
28017
28668
|
React__default.createElement(Icon, { name: "ui-flag", width: 17.5, height: 20 })),
|
|
@@ -30574,6 +31225,222 @@ const ExcursionDetails = () => {
|
|
|
30574
31225
|
React__default.createElement("button", { type: "button", className: "cta cta--primary", onClick: handleConfirm }, translations?.QSM.CONFIRM))));
|
|
30575
31226
|
};
|
|
30576
31227
|
|
|
31228
|
+
const getLocation = (result) => {
|
|
31229
|
+
const place = result.locationName || result.regionName || result.oordName;
|
|
31230
|
+
if (!place)
|
|
31231
|
+
return result.countryName ?? '';
|
|
31232
|
+
return result.countryName ? `${place}, ${result.countryName}` : place;
|
|
31233
|
+
};
|
|
31234
|
+
const toPlainText = (value) => {
|
|
31235
|
+
if (!value)
|
|
31236
|
+
return '';
|
|
31237
|
+
return he
|
|
31238
|
+
.decode(value.replace(/<[^>]*>/g, ' '))
|
|
31239
|
+
.replace(/\s+/g, ' ')
|
|
31240
|
+
.trim();
|
|
31241
|
+
};
|
|
31242
|
+
const TrajectNodeCard = ({ node, result, isSelected, languageCode, translations, onSelect, onEditOptions, showNights, selectLabel, selectedLabel }) => {
|
|
31243
|
+
const selectedPerRoom = (result.rooms ?? []).map((room) => (room.options ?? []).find((x) => x.isSelected) ?? (room.options ?? [])[0]);
|
|
31244
|
+
const isMultiRoom = selectedPerRoom.length > 1;
|
|
31245
|
+
const price = formatPrice$3(result.price, result.currencyCode, languageCode);
|
|
31246
|
+
const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
|
|
31247
|
+
const candidate = node ? findCandidateForResult(node, result) : null;
|
|
31248
|
+
const image = candidate?.imageUrl ?? null;
|
|
31249
|
+
const description = toPlainText(candidate?.description);
|
|
31250
|
+
const title = candidate?.name ?? result.name;
|
|
31251
|
+
const priceBlock = (React__default.createElement("div", { className: "search__result-card__price__wrapper" },
|
|
31252
|
+
React__default.createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
|
|
31253
|
+
React__default.createElement("span", { className: "search__result-card__price" }, price)));
|
|
31254
|
+
const canEditOptions = isSelected && !!onEditOptions && (result.rooms ?? []).some((room) => (room.options ?? []).length > 1);
|
|
31255
|
+
const selectButton = (React__default.createElement(React__default.Fragment, null,
|
|
31256
|
+
React__default.createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? selectedLabel ?? translations?.SHARED.SELECTED : selectLabel ?? translations?.SHARED.SELECT),
|
|
31257
|
+
canEditOptions && (React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: onEditOptions },
|
|
31258
|
+
translations?.SRP.SELECT,
|
|
31259
|
+
" ",
|
|
31260
|
+
translations?.SRP.ACCOMMODATION))));
|
|
31261
|
+
if (result.contents?.length) {
|
|
31262
|
+
return (React__default.createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
31263
|
+
React__default.createElement("div", { className: "search__result-card__top", dangerouslySetInnerHTML: { __html: he.decode(result.contents) } }),
|
|
31264
|
+
React__default.createElement("div", { className: "search__result-card__footer" }, selectButton)));
|
|
31265
|
+
}
|
|
31266
|
+
return (React__default.createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
31267
|
+
image && (React__default.createElement("div", { className: "search__result-card__img-wrapper" },
|
|
31268
|
+
React__default.createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
|
|
31269
|
+
priceBlock)),
|
|
31270
|
+
React__default.createElement("div", { className: "search__result-card__content" },
|
|
31271
|
+
React__default.createElement("div", { className: "search__result-card__content__wrapper" },
|
|
31272
|
+
React__default.createElement("div", { className: "search__result-card__header" },
|
|
31273
|
+
React__default.createElement("div", { className: "search__result-card__header__wrapper" },
|
|
31274
|
+
!!result.stars && (React__default.createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default.createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
|
|
31275
|
+
React__default.createElement("h3", { className: "search__result-card__title" }, title)),
|
|
31276
|
+
!image && priceBlock),
|
|
31277
|
+
React__default.createElement("span", { className: "search__result-card__location" },
|
|
31278
|
+
React__default.createElement(Icon, { name: "ui-location", height: 16 }),
|
|
31279
|
+
getLocation(result)),
|
|
31280
|
+
React__default.createElement("div", { className: "search__result-card__options" },
|
|
31281
|
+
showNights && nights > 0 && (React__default.createElement("div", { className: "search__result-card__option" },
|
|
31282
|
+
React__default.createElement(Icon, { name: "ui-moon", height: 16 }),
|
|
31283
|
+
nights,
|
|
31284
|
+
" ",
|
|
31285
|
+
translations?.SRP.NIGHTS)),
|
|
31286
|
+
selectedPerRoom.map((option, roomIndex) => (React__default.createElement(React__default.Fragment, { key: `room-${roomIndex}` },
|
|
31287
|
+
option?.accommodationName && (React__default.createElement("div", { className: "search__result-card__option" },
|
|
31288
|
+
React__default.createElement(Icon, { name: "ui-bed", height: 16 }),
|
|
31289
|
+
isMultiRoom && `${translations?.SHARED.ROOM} ${roomIndex + 1}: `,
|
|
31290
|
+
option.accommodationName)),
|
|
31291
|
+
option?.regimeName && (React__default.createElement("div", { className: "search__result-card__option" },
|
|
31292
|
+
React__default.createElement(Icon, { name: "ui-utensils", height: 16 }),
|
|
31293
|
+
option.regimeName)))))),
|
|
31294
|
+
description && React__default.createElement("p", { className: "search__result-card__description" }, description)),
|
|
31295
|
+
React__default.createElement("div", { className: "search__result-card__footer" }, selectButton))));
|
|
31296
|
+
};
|
|
31297
|
+
|
|
31298
|
+
const TrajectResultsFlyIn = ({ isLoading }) => {
|
|
31299
|
+
const context = useContext(SearchResultsConfigurationContext);
|
|
31300
|
+
const dispatch = useDispatch();
|
|
31301
|
+
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
31302
|
+
const { trajectNodeAvailability, trajectEditLineGuid } = useSelector((state) => state.searchResults);
|
|
31303
|
+
const trajectEntry = context?.trajectEntry;
|
|
31304
|
+
const item = useMemo(() => {
|
|
31305
|
+
if (!trajectEntry || !trajectEditLineGuid)
|
|
31306
|
+
return null;
|
|
31307
|
+
return getTrajectNodesWithLines(trajectEntry).find((x) => x.line.guid === trajectEditLineGuid) ?? null;
|
|
31308
|
+
}, [trajectEntry, trajectEditLineGuid]);
|
|
31309
|
+
if (!context || !item)
|
|
31310
|
+
return null;
|
|
31311
|
+
const availability = trajectNodeAvailability[item.line.guid];
|
|
31312
|
+
const results = availability?.results ?? [];
|
|
31313
|
+
const ordered = [...results].sort((a, b) => {
|
|
31314
|
+
const aConfigured = findCandidateForResult(item.node, a) ? 0 : 1;
|
|
31315
|
+
const bConfigured = findCandidateForResult(item.node, b) ? 0 : 1;
|
|
31316
|
+
return aConfigured - bConfigured || a.price - b.price;
|
|
31317
|
+
});
|
|
31318
|
+
const handleSelect = (code) => {
|
|
31319
|
+
const result = results.find((x) => x.code === code);
|
|
31320
|
+
if (!result)
|
|
31321
|
+
return;
|
|
31322
|
+
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
|
|
31323
|
+
applyResultToLines(item.lines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
31324
|
+
dispatch(setTrajectEditLineGuid(null));
|
|
31325
|
+
dispatch(setFlyInIsOpen(false));
|
|
31326
|
+
};
|
|
31327
|
+
if (isLoading) {
|
|
31328
|
+
return React__default.createElement(React__default.Fragment, null, context.customSpinner ?? React__default.createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }));
|
|
31329
|
+
}
|
|
31330
|
+
return (React__default.createElement("div", { className: "flyin__content" },
|
|
31331
|
+
React__default.createElement("div", { className: "search__result-row" },
|
|
31332
|
+
React__default.createElement("span", { className: "search__result-row-text" },
|
|
31333
|
+
ordered.length,
|
|
31334
|
+
"\u00A0",
|
|
31335
|
+
translations.SRP.TOTAL_RESULTS_LABEL)),
|
|
31336
|
+
ordered.length === 0 ? (React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)) : (React__default.createElement("div", { className: "search__results__cards" }, ordered.map((result) => (React__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 })))))));
|
|
31337
|
+
};
|
|
31338
|
+
|
|
31339
|
+
/**
|
|
31340
|
+
* Every flight found for one traject leg, shown in the fly-in.
|
|
31341
|
+
*
|
|
31342
|
+
* A one-way search can return far more options than belong in the results column, so the column
|
|
31343
|
+
* keeps three and hands the rest to this list — the same shape the regular flight results use.
|
|
31344
|
+
* Each leg is searched on its own, so the single leg of a result always sits in `outward`.
|
|
31345
|
+
*/
|
|
31346
|
+
const TrajectFlightsFlyIn = ({ isLoading }) => {
|
|
31347
|
+
const context = useContext(SearchResultsConfigurationContext);
|
|
31348
|
+
const dispatch = useDispatch();
|
|
31349
|
+
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
31350
|
+
const { trajectFlightAvailability, trajectEditLineGuid, flightsLoading } = useSelector((state) => state.searchResults);
|
|
31351
|
+
const trajectEntry = context?.trajectEntry;
|
|
31352
|
+
const item = useMemo(() => {
|
|
31353
|
+
if (!trajectEntry || !trajectEditLineGuid)
|
|
31354
|
+
return null;
|
|
31355
|
+
return getTrajectNodesWithLines(trajectEntry).find((x) => x.line.guid === trajectEditLineGuid) ?? null;
|
|
31356
|
+
}, [trajectEntry, trajectEditLineGuid]);
|
|
31357
|
+
if (!context || !item)
|
|
31358
|
+
return null;
|
|
31359
|
+
const availability = trajectFlightAvailability[item.line.guid];
|
|
31360
|
+
const flights = availability?.results ?? [];
|
|
31361
|
+
const selectedGuid = availability?.selectedGuid;
|
|
31362
|
+
const selected = flights.find((x) => x.outwardGuid === selectedGuid) ?? null;
|
|
31363
|
+
const handleSelect = (outwardGuid) => {
|
|
31364
|
+
const flight = flights.find((x) => x.outwardGuid === outwardGuid);
|
|
31365
|
+
if (!flight)
|
|
31366
|
+
return;
|
|
31367
|
+
dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
|
|
31368
|
+
dispatch(updateEditableEntryLine(applyFlightToLine(item.line, flight)));
|
|
31369
|
+
dispatch(setTrajectEditLineGuid(null));
|
|
31370
|
+
dispatch(setFlyInIsOpen(false));
|
|
31371
|
+
};
|
|
31372
|
+
if (isLoading || flightsLoading) {
|
|
31373
|
+
return React__default.createElement(React__default.Fragment, null, context.customSpinner ?? React__default.createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS }));
|
|
31374
|
+
}
|
|
31375
|
+
// The picked flight stays at the top; the rest follow by price.
|
|
31376
|
+
const ordered = [...flights].sort((a, b) => {
|
|
31377
|
+
if (a.outwardGuid === selectedGuid)
|
|
31378
|
+
return -1;
|
|
31379
|
+
if (b.outwardGuid === selectedGuid)
|
|
31380
|
+
return 1;
|
|
31381
|
+
return a.price - b.price;
|
|
31382
|
+
});
|
|
31383
|
+
return (React__default.createElement("div", { className: "flyin__content" },
|
|
31384
|
+
React__default.createElement("div", { className: "search__result-row" },
|
|
31385
|
+
React__default.createElement("span", { className: "search__result-row-text" },
|
|
31386
|
+
ordered.length,
|
|
31387
|
+
"\u00A0",
|
|
31388
|
+
translations.SRP.TOTAL_RESULTS_LABEL)),
|
|
31389
|
+
ordered.length === 0 ? (React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)) : (React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" }, ordered.map((result) => (React__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) })))))));
|
|
31390
|
+
};
|
|
31391
|
+
|
|
31392
|
+
const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
|
|
31393
|
+
const context = useContext(SearchResultsConfigurationContext);
|
|
31394
|
+
const dispatch = useDispatch();
|
|
31395
|
+
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
31396
|
+
const { trajectNodeAvailability, trajectDaySuggestions, trajectActivitiesDayOrder, editablePackagingEntry } = useSelector((state) => state.searchResults);
|
|
31397
|
+
const trajectEntry = context?.trajectEntry;
|
|
31398
|
+
const items = useMemo(() => {
|
|
31399
|
+
if (!trajectEntry || trajectActivitiesDayOrder === null)
|
|
31400
|
+
return [];
|
|
31401
|
+
return getTrajectNodesWithLines(trajectEntry).filter(({ node }) => node.dayOrder === trajectActivitiesDayOrder && node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === EXCURSION_SERVICE_TYPE);
|
|
31402
|
+
}, [trajectEntry, trajectActivitiesDayOrder]);
|
|
31403
|
+
if (!context || trajectActivitiesDayOrder === null)
|
|
31404
|
+
return null;
|
|
31405
|
+
const suggestions = trajectDaySuggestions[trajectActivitiesDayOrder];
|
|
31406
|
+
const cards = buildDayActivityCards(items, trajectNodeAvailability, suggestions?.results ?? []);
|
|
31407
|
+
const getAddedLines = (result) => (editablePackagingEntry?.lines ?? []).filter((line) => line.serviceType === EXCURSION_SERVICE_TYPE && line.productCode === result.code && isSameDay(new Date(line.from), new Date(result.fromDate)));
|
|
31408
|
+
/** Adding is not a decision that ends the list — the traveller may want several. */
|
|
31409
|
+
const handleToggle = (result) => {
|
|
31410
|
+
const added = getAddedLines(result);
|
|
31411
|
+
if (added.length) {
|
|
31412
|
+
dispatch(removeEditableEntryLines(added.map((line) => line.guid)));
|
|
31413
|
+
return;
|
|
31414
|
+
}
|
|
31415
|
+
const lines = buildTrajectExcursionLines(result);
|
|
31416
|
+
if (lines.length)
|
|
31417
|
+
dispatch(addEditableEntryLines(lines));
|
|
31418
|
+
};
|
|
31419
|
+
/** Swapping what the traject books is one decision, so the list closes behind it. */
|
|
31420
|
+
const handleSelect = ({ result, item }) => {
|
|
31421
|
+
if (!item)
|
|
31422
|
+
return;
|
|
31423
|
+
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code: result.code }));
|
|
31424
|
+
applyResultToLines(item.lines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
31425
|
+
dispatch(setTrajectActivitiesDayOrder(null));
|
|
31426
|
+
dispatch(setFlyInIsOpen(false));
|
|
31427
|
+
};
|
|
31428
|
+
if (isLoading || suggestions?.status === 'loading') {
|
|
31429
|
+
return React__default.createElement(React__default.Fragment, null, context.customSpinner ?? React__default.createElement(Spinner, { label: translations.SRP.LOADING_EXCURSIONS }));
|
|
31430
|
+
}
|
|
31431
|
+
return (React__default.createElement("div", { className: "flyin__content" },
|
|
31432
|
+
React__default.createElement("div", { className: "search__result-row" },
|
|
31433
|
+
React__default.createElement("span", { className: "search__result-row-text" },
|
|
31434
|
+
cards.length,
|
|
31435
|
+
"\u00A0",
|
|
31436
|
+
translations.SRP.TOTAL_RESULTS_LABEL)),
|
|
31437
|
+
cards.length === 0 ? (React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)) : (React__default.createElement("div", { className: "search__results__cards" }, cards.map((card) => {
|
|
31438
|
+
const { result, item } = card;
|
|
31439
|
+
const isSelected = item ? trajectNodeAvailability[item.line.guid]?.selectedCode === result.code : getAddedLines(result).length > 0;
|
|
31440
|
+
return (React__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 }));
|
|
31441
|
+
})))));
|
|
31442
|
+
};
|
|
31443
|
+
|
|
30577
31444
|
const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, detailsLoading, flyInType, isPackageEditFlow, handleConfirm, sortByTypes, activeSearchSeed, toggleFilters, filtersOpen }) => {
|
|
30578
31445
|
const dispatch = useDispatch();
|
|
30579
31446
|
const context = useContext(SearchResultsConfigurationContext);
|
|
@@ -30619,6 +31486,10 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
|
|
|
30619
31486
|
}
|
|
30620
31487
|
};
|
|
30621
31488
|
const handleGoBack = () => {
|
|
31489
|
+
if (context?.trajectEntry) {
|
|
31490
|
+
handleClose();
|
|
31491
|
+
return;
|
|
31492
|
+
}
|
|
30622
31493
|
if (flyInType === 'acco-details') {
|
|
30623
31494
|
dispatch(setFlyInType('acco-results'));
|
|
30624
31495
|
}
|
|
@@ -30642,8 +31513,8 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
|
|
|
30642
31513
|
dispatch(setBookPackagingEntry(true));
|
|
30643
31514
|
}
|
|
30644
31515
|
};
|
|
30645
|
-
return (React__default.createElement("div", { className: `flyin ${isOpen ? 'flyin--active' : ''} ${className} ${isPackageEditFlow || flyInType === 'acco-results' ? 'flyin--large' : ''} ${flyInType === 'excursion-results' || flyInType === 'excursion-details' ? 'flyin--medium' : ''}
|
|
30646
|
-
${flyInType === 'flight-outward-results' || flyInType === 'flight-return-results' ? 'flyin--flight' : ''}` },
|
|
31516
|
+
return (React__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' : ''}
|
|
31517
|
+
${flyInType === 'flight-outward-results' || flyInType === 'flight-return-results' || flyInType === 'traject-flight-results' ? 'flyin--flight' : ''}` },
|
|
30647
31518
|
React__default.createElement("div", { className: `flyin__panel ${isOpen ? 'flyin__panel--active' : ''}`, ref: panelRef },
|
|
30648
31519
|
React__default.createElement("div", { className: "flyin__content" },
|
|
30649
31520
|
React__default.createElement("div", { className: "flyin__content-title-row" },
|
|
@@ -30660,6 +31531,8 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
|
|
|
30660
31531
|
flyInType === 'flight-return-results' &&
|
|
30661
31532
|
`${translations.SRP.SELECT} ${translations.FLIGHTS_FORM.RETURN_FLIGHT}`,
|
|
30662
31533
|
srpType === build.PortalQsmType.AccommodationAndFlight && flyInType === 'flight-details' && 'Select your fare',
|
|
31534
|
+
flyInType === 'traject-flight-results' && `${translations.SRP.SELECT} ${translations.SRP.DEPARTURE}`,
|
|
31535
|
+
flyInType === 'traject-day-activities' && translations.SRP.ACTIVITIES,
|
|
30663
31536
|
flyInType === 'excursion-results' || (flyInType === 'excursion-details' && 'Select excursion')),
|
|
30664
31537
|
React__default.createElement("span", { className: "flyin__close", onClick: () => handleClose() },
|
|
30665
31538
|
React__default.createElement(Icon, { name: "ui-close", width: 30, height: 30, "aria-hidden": "true" }))),
|
|
@@ -30668,7 +31541,12 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
|
|
|
30668
31541
|
React__default.createElement(Icon, { name: "ui-chevron", width: 14, height: 14, "aria-hidden": "true" }),
|
|
30669
31542
|
"Go Back")))),
|
|
30670
31543
|
srpType === build.PortalQsmType.Flight && React__default.createElement(FlightsFlyIn, { isOpen: isOpen, setIsOpen: setIsOpen }),
|
|
30671
|
-
|
|
31544
|
+
context?.trajectEntry && flyInType === 'acco-results' && React__default.createElement(TrajectResultsFlyIn, { isLoading: detailsLoading }),
|
|
31545
|
+
context?.trajectEntry && flyInType === 'traject-flight-results' && React__default.createElement(TrajectFlightsFlyIn, { isLoading: detailsLoading }),
|
|
31546
|
+
context?.trajectEntry && flyInType === 'traject-day-activities' && React__default.createElement(TrajectDayActivitiesFlyIn, { isLoading: detailsLoading }),
|
|
31547
|
+
!context?.trajectEntry &&
|
|
31548
|
+
(srpType === build.PortalQsmType.Accommodation || srpType === build.PortalQsmType.AccommodationAndFlight) &&
|
|
31549
|
+
flyInType === 'acco-results' && (React__default.createElement("div", { className: "flyin__content flyin__content--columns" },
|
|
30672
31550
|
React__default.createElement(Filters, { initialFilters: initialFilters, filters: filters, isOpen: filtersOpen, handleSetIsOpen: () => toggleFilters && toggleFilters(),
|
|
30673
31551
|
// handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
|
|
30674
31552
|
isLoading: isLoading, setFilters: (filters) => dispatch(setFilters(filters)), resetFilters: (filters) => dispatch(resetFilters(filters)) }),
|
|
@@ -32941,232 +33819,6 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
|
|
|
32941
33819
|
React__default.createElement(WLSidebar, { activeSearchSeed: activeSearchSeed, packagingAccoResult: selectedPackagingAccoResult }))));
|
|
32942
33820
|
};
|
|
32943
33821
|
|
|
32944
|
-
const REGULAR_TRAJECT_NODE_TYPE = 0;
|
|
32945
|
-
const isSearchableTrajectNode = (node) => node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE;
|
|
32946
|
-
const isTrajectFlightNode = (node) => node.serviceType === FLIGHT_SERVICE_TYPE;
|
|
32947
|
-
const getTrajectNodesWithLines = (trajectEntry) => {
|
|
32948
|
-
const linesByGuid = new Map((trajectEntry.entry.lines ?? []).map((line) => [line.guid, line]));
|
|
32949
|
-
return [...(trajectEntry.nodes ?? [])]
|
|
32950
|
-
.sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
|
|
32951
|
-
.map((node) => ({ node, line: linesByGuid.get(node.lineGuid) }))
|
|
32952
|
-
.filter((x) => !!x.line);
|
|
32953
|
-
};
|
|
32954
|
-
const buildDestination = (line) => {
|
|
32955
|
-
if (line.location?.id)
|
|
32956
|
-
return { id: line.location.id, isLocation: true };
|
|
32957
|
-
if (line.oord?.id)
|
|
32958
|
-
return { id: line.oord.id, isOord: true };
|
|
32959
|
-
if (line.region?.id)
|
|
32960
|
-
return { id: line.region.id, isRegion: true };
|
|
32961
|
-
if (line.country?.id)
|
|
32962
|
-
return { id: line.country.id, isCountry: true };
|
|
32963
|
-
if (line.latitude && line.longitude)
|
|
32964
|
-
return { latitude: line.latitude, longitude: line.longitude };
|
|
32965
|
-
return { id: 0 };
|
|
32966
|
-
};
|
|
32967
|
-
const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
|
|
32968
|
-
transactionId: context.transactionId,
|
|
32969
|
-
officeId: context.officeId,
|
|
32970
|
-
agentId: context.agentId ?? null,
|
|
32971
|
-
catalogueId: context.catalogueId,
|
|
32972
|
-
searchConfigurationId: context.searchConfigurationId,
|
|
32973
|
-
portalId: context.portalId ?? null,
|
|
32974
|
-
vendorConfigurationId: node.externalVendorId ?? null,
|
|
32975
|
-
language: context.language,
|
|
32976
|
-
serviceType: node.serviceType,
|
|
32977
|
-
fromDate: toDateOnlyString(line.from),
|
|
32978
|
-
toDate: toDateOnlyString(line.to),
|
|
32979
|
-
destination: buildDestination(line),
|
|
32980
|
-
productCode,
|
|
32981
|
-
rooms: context.rooms,
|
|
32982
|
-
tagIds: []
|
|
32983
|
-
});
|
|
32984
|
-
const getNodeCandidates = (node) => {
|
|
32985
|
-
const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
|
|
32986
|
-
if (configured.length)
|
|
32987
|
-
return configured;
|
|
32988
|
-
const fallbackCode = node.code ?? node.preferredAccommodationCode;
|
|
32989
|
-
if (!fallbackCode || !fallbackCode.trim().length)
|
|
32990
|
-
return [];
|
|
32991
|
-
return [
|
|
32992
|
-
{
|
|
32993
|
-
code: fallbackCode,
|
|
32994
|
-
name: node.name,
|
|
32995
|
-
description: node.description,
|
|
32996
|
-
imageUrl: node.imageUrl,
|
|
32997
|
-
contentSource: node.contentSource,
|
|
32998
|
-
vendor: node.vendor,
|
|
32999
|
-
externalVendorId: node.externalVendorId,
|
|
33000
|
-
isPreferred: true
|
|
33001
|
-
}
|
|
33002
|
-
];
|
|
33003
|
-
};
|
|
33004
|
-
const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
|
|
33005
|
-
const matchesCode = (result, code) => {
|
|
33006
|
-
if (result.code === code)
|
|
33007
|
-
return true;
|
|
33008
|
-
return (result.rooms ?? []).some((room) => (room.options ?? []).some((option) => option.accommodationCode === code));
|
|
33009
|
-
};
|
|
33010
|
-
const getPackagingRoomsFromEntry = (entry) => {
|
|
33011
|
-
const paxById = new Map((entry.pax ?? []).map((p) => [p.id, p]));
|
|
33012
|
-
const rooms = (entry.rooms ?? []).map((room) => ({
|
|
33013
|
-
travellers: (room.paxIds ?? []).map((paxId) => {
|
|
33014
|
-
const pax = paxById.get(paxId);
|
|
33015
|
-
return {
|
|
33016
|
-
id: paxId,
|
|
33017
|
-
age: pax?.age ?? null,
|
|
33018
|
-
dateOfBirth: pax?.dateOfBirth ?? null
|
|
33019
|
-
};
|
|
33020
|
-
})
|
|
33021
|
-
}));
|
|
33022
|
-
return rooms.filter((room) => room.travellers.length > 0);
|
|
33023
|
-
};
|
|
33024
|
-
const pickCheapest = (results) => [...results].sort((a, b) => a.price - b.price)[0] ?? null;
|
|
33025
|
-
const applyResultToLine = (line, result) => {
|
|
33026
|
-
const option = (result.rooms ?? []).flatMap((room) => room.options ?? []).find((x) => x.isSelected) ?? (result.rooms ?? [])[0]?.options?.[0];
|
|
33027
|
-
return {
|
|
33028
|
-
...line,
|
|
33029
|
-
productName: result.name ?? line.productName,
|
|
33030
|
-
productCode: result.code ?? line.productCode,
|
|
33031
|
-
accommodationCode: option?.accommodationCode ?? line.accommodationCode,
|
|
33032
|
-
accommodationName: option?.accommodationName ?? line.accommodationName,
|
|
33033
|
-
regimeCode: option?.regimeCode ?? line.regimeCode,
|
|
33034
|
-
regimeName: option?.regimeName ?? line.regimeName,
|
|
33035
|
-
latitude: result.latitude ?? line.latitude,
|
|
33036
|
-
longitude: result.longitude ?? line.longitude,
|
|
33037
|
-
isChanged: true
|
|
33038
|
-
};
|
|
33039
|
-
};
|
|
33040
|
-
const toDateOnlyUtcString = (value) => {
|
|
33041
|
-
const date = new Date(value);
|
|
33042
|
-
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())).toISOString();
|
|
33043
|
-
};
|
|
33044
|
-
const toTimeOnlyString = (value) => {
|
|
33045
|
-
const date = new Date(value);
|
|
33046
|
-
const hh = String(date.getUTCHours()).padStart(2, '0');
|
|
33047
|
-
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
|
33048
|
-
const ss = String(date.getUTCSeconds()).padStart(2, '0');
|
|
33049
|
-
return `${hh}:${mm}:${ss}`;
|
|
33050
|
-
};
|
|
33051
|
-
const mapFlightSegmentsToFlightLines = (segments) => segments.map((segment) => ({
|
|
33052
|
-
airlineCode: segment.marketingAirlineCode,
|
|
33053
|
-
airlineDescription: segment.marketingAirlineName,
|
|
33054
|
-
operatingAirlineCode: segment.operatingAirlineCode,
|
|
33055
|
-
operatingAirlineDescription: segment.operatingAirlineName,
|
|
33056
|
-
flightNumber: segment.flightNumber,
|
|
33057
|
-
operatingFlightNumber: segment.operatingFlightNumber ?? null,
|
|
33058
|
-
departureDate: toDateOnlyUtcString(segment.departureDateTime),
|
|
33059
|
-
departureTime: toTimeOnlyString(segment.departureDateTime),
|
|
33060
|
-
departureAirportCode: segment.departureAirportCode,
|
|
33061
|
-
departureAirportDescription: segment.departureAirportName,
|
|
33062
|
-
arrivalDate: toDateOnlyUtcString(segment.arrivalDateTime),
|
|
33063
|
-
arrivalTime: toTimeOnlyString(segment.arrivalDateTime),
|
|
33064
|
-
arrivalAirportCode: segment.arrivalAirportCode,
|
|
33065
|
-
arrivalAirportDescription: segment.arrivalAirportName,
|
|
33066
|
-
durationInTicks: segment.durationInTicks
|
|
33067
|
-
}));
|
|
33068
|
-
const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
|
|
33069
|
-
transactionId: context.transactionId,
|
|
33070
|
-
officeId: context.officeId,
|
|
33071
|
-
catalogueId: context.catalogueId,
|
|
33072
|
-
agentId: context.agentId ?? null,
|
|
33073
|
-
language: context.language,
|
|
33074
|
-
departureAirportCode: node.departureAirportCode ?? '',
|
|
33075
|
-
arrivalAirportCode: node.arrivalAirportCode ?? '',
|
|
33076
|
-
returnAirportCode: null,
|
|
33077
|
-
luggageIncluded: null,
|
|
33078
|
-
maxStops: null,
|
|
33079
|
-
travelClass: null,
|
|
33080
|
-
vendorConfigurationId: node.externalVendorId ?? null,
|
|
33081
|
-
pax,
|
|
33082
|
-
outward: { date: dateToDateStruct(new Date(line.from)) },
|
|
33083
|
-
return: null
|
|
33084
|
-
});
|
|
33085
|
-
const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
|
|
33086
|
-
const applyFlightToLine = (line, flight) => {
|
|
33087
|
-
const segments = flight.outward?.segments ?? [];
|
|
33088
|
-
if (!segments.length)
|
|
33089
|
-
return line;
|
|
33090
|
-
const firstSegment = segments[0];
|
|
33091
|
-
const lastSegment = segments[segments.length - 1];
|
|
33092
|
-
return {
|
|
33093
|
-
...line,
|
|
33094
|
-
from: new Date(firstSegment.departureDateTime).toISOString(),
|
|
33095
|
-
to: new Date(lastSegment.arrivalDateTime).toISOString(),
|
|
33096
|
-
productName: `${firstSegment.departureAirportName} - ${lastSegment.arrivalAirportName} (${firstSegment.marketingAirlineName})`,
|
|
33097
|
-
productCode: `${firstSegment.departureAirportCode} ${lastSegment.arrivalAirportCode}/${firstSegment.marketingAirlineCode}`,
|
|
33098
|
-
accommodationName: firstSegment.metaData?.farePriceClassName ?? line.accommodationName,
|
|
33099
|
-
accommodationCode: firstSegment.metaData?.fareCode ?? line.accommodationCode,
|
|
33100
|
-
flightInformation: {
|
|
33101
|
-
pnr: '',
|
|
33102
|
-
flightLines: mapFlightSegmentsToFlightLines(segments)
|
|
33103
|
-
},
|
|
33104
|
-
isChanged: true
|
|
33105
|
-
};
|
|
33106
|
-
};
|
|
33107
|
-
|
|
33108
|
-
const getLocation = (result) => {
|
|
33109
|
-
const place = result.locationName || result.regionName || result.oordName;
|
|
33110
|
-
if (!place)
|
|
33111
|
-
return result.countryName ?? '';
|
|
33112
|
-
return result.countryName ? `${place}, ${result.countryName}` : place;
|
|
33113
|
-
};
|
|
33114
|
-
const toPlainText = (value) => {
|
|
33115
|
-
if (!value)
|
|
33116
|
-
return '';
|
|
33117
|
-
return he
|
|
33118
|
-
.decode(value.replace(/<[^>]*>/g, ' '))
|
|
33119
|
-
.replace(/\s+/g, ' ')
|
|
33120
|
-
.trim();
|
|
33121
|
-
};
|
|
33122
|
-
const TrajectNodeCard = ({ item, result, isSelected, languageCode, translations, onSelect, showNights }) => {
|
|
33123
|
-
const { node } = item;
|
|
33124
|
-
const selectedOption = first(result.rooms)?.options?.find((x) => x.isSelected) ?? first(result.rooms)?.options?.[0];
|
|
33125
|
-
const price = formatPrice$3(result.price, result.currencyCode, languageCode);
|
|
33126
|
-
const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
|
|
33127
|
-
const candidate = findCandidateForResult(node, result);
|
|
33128
|
-
const image = candidate?.imageUrl ?? null;
|
|
33129
|
-
const description = toPlainText(candidate?.description);
|
|
33130
|
-
const title = candidate?.name ?? result.name;
|
|
33131
|
-
const priceBlock = (React__default.createElement("div", { className: "search__result-card__price__wrapper" },
|
|
33132
|
-
React__default.createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
|
|
33133
|
-
React__default.createElement("span", { className: "search__result-card__price" }, price)));
|
|
33134
|
-
const selectButton = (React__default.createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT));
|
|
33135
|
-
if (result.contents?.length) {
|
|
33136
|
-
return (React__default.createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
33137
|
-
React__default.createElement("div", { className: "search__result-card__top", dangerouslySetInnerHTML: { __html: he.decode(result.contents) } }),
|
|
33138
|
-
React__default.createElement("div", { className: "search__result-card__footer" }, selectButton)));
|
|
33139
|
-
}
|
|
33140
|
-
return (React__default.createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
|
|
33141
|
-
image && (React__default.createElement("div", { className: "search__result-card__img-wrapper" },
|
|
33142
|
-
React__default.createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
|
|
33143
|
-
priceBlock)),
|
|
33144
|
-
React__default.createElement("div", { className: "search__result-card__content" },
|
|
33145
|
-
React__default.createElement("div", { className: "search__result-card__content__wrapper" },
|
|
33146
|
-
React__default.createElement("div", { className: "search__result-card__header" },
|
|
33147
|
-
React__default.createElement("div", { className: "search__result-card__header__wrapper" },
|
|
33148
|
-
!!result.stars && (React__default.createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default.createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
|
|
33149
|
-
React__default.createElement("h3", { className: "search__result-card__title" }, title)),
|
|
33150
|
-
!image && priceBlock),
|
|
33151
|
-
React__default.createElement("span", { className: "search__result-card__location" },
|
|
33152
|
-
React__default.createElement(Icon, { name: "ui-location", height: 16 }),
|
|
33153
|
-
getLocation(result)),
|
|
33154
|
-
React__default.createElement("div", { className: "search__result-card__options" },
|
|
33155
|
-
showNights && nights > 0 && (React__default.createElement("div", { className: "search__result-card__option" },
|
|
33156
|
-
React__default.createElement(Icon, { name: "ui-moon", height: 16 }),
|
|
33157
|
-
nights,
|
|
33158
|
-
" ",
|
|
33159
|
-
translations?.SRP.NIGHTS)),
|
|
33160
|
-
selectedOption?.accommodationName && (React__default.createElement("div", { className: "search__result-card__option" },
|
|
33161
|
-
React__default.createElement(Icon, { name: "ui-bed", height: 16 }),
|
|
33162
|
-
selectedOption.accommodationName)),
|
|
33163
|
-
selectedOption?.regimeName && (React__default.createElement("div", { className: "search__result-card__option" },
|
|
33164
|
-
React__default.createElement(Icon, { name: "ui-utensils", height: 16 }),
|
|
33165
|
-
selectedOption.regimeName))),
|
|
33166
|
-
description && React__default.createElement("p", { className: "search__result-card__description" }, description)),
|
|
33167
|
-
React__default.createElement("div", { className: "search__result-card__footer" }, selectButton))));
|
|
33168
|
-
};
|
|
33169
|
-
|
|
33170
33822
|
const getNodeIcon = (node) => {
|
|
33171
33823
|
if (isTrajectFlightNode(node))
|
|
33172
33824
|
return 'ui-flight';
|
|
@@ -33176,14 +33828,17 @@ const getNodeIcon = (node) => {
|
|
|
33176
33828
|
return 'ui-bed';
|
|
33177
33829
|
return 'ui-location';
|
|
33178
33830
|
};
|
|
33831
|
+
const isRenderableNode = (node) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE &&
|
|
33832
|
+
(node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE || node.serviceType === FLIGHT_SERVICE_TYPE);
|
|
33833
|
+
const MAX_DAY_ACTIVITIES = 6;
|
|
33179
33834
|
const TrajectResults = ({ isLoading }) => {
|
|
33180
33835
|
const context = useContext(SearchResultsConfigurationContext);
|
|
33181
33836
|
const dispatch = useDispatch();
|
|
33182
33837
|
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
33183
33838
|
const locale = getLocale(context?.languageCode ?? 'en-GB');
|
|
33184
|
-
const { trajectNodeAvailability, trajectFlightAvailability } = useSelector((state) => state.searchResults);
|
|
33185
|
-
const [expandedNodes, setExpandedNodes] = useState({});
|
|
33839
|
+
const { trajectNodeAvailability, trajectFlightAvailability, trajectDaySuggestions, editablePackagingEntry } = useSelector((state) => state.searchResults);
|
|
33186
33840
|
const trajectEntry = context?.trajectEntry;
|
|
33841
|
+
const hideShowMoreResults = !!context?.hideShowMoreResults;
|
|
33187
33842
|
const nodesWithLines = useMemo(() => (trajectEntry ? getTrajectNodesWithLines(trajectEntry) : []), [trajectEntry]);
|
|
33188
33843
|
const lineByNodeId = useMemo(() => new Map(nodesWithLines.map((x) => [x.node.nodeId, x])), [nodesWithLines]);
|
|
33189
33844
|
const nodesByDay = useMemo(() => {
|
|
@@ -33197,22 +33852,89 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33197
33852
|
});
|
|
33198
33853
|
return map;
|
|
33199
33854
|
}, [trajectEntry]);
|
|
33855
|
+
const { stayByDay, stayStartByNodeId } = useMemo(() => {
|
|
33856
|
+
const byDay = new Map();
|
|
33857
|
+
const byNodeId = new Map();
|
|
33858
|
+
nodesWithLines
|
|
33859
|
+
.filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE)
|
|
33860
|
+
.forEach((item) => {
|
|
33861
|
+
const nights = Math.max(differenceInCalendarDays(new Date(item.line.to), new Date(item.line.from)), 1);
|
|
33862
|
+
byNodeId.set(item.node.nodeId, { item, night: 0, nights });
|
|
33863
|
+
for (let night = 0; night < nights; night++) {
|
|
33864
|
+
byDay.set(item.node.dayOrder + night, { item, night, nights });
|
|
33865
|
+
}
|
|
33866
|
+
});
|
|
33867
|
+
return { stayByDay: byDay, stayStartByNodeId: byNodeId };
|
|
33868
|
+
}, [nodesWithLines]);
|
|
33200
33869
|
if (!context || !trajectEntry)
|
|
33201
33870
|
return null;
|
|
33202
|
-
const toggleExpanded = (lineGuid) => setExpandedNodes((prev) => ({ ...prev, [lineGuid]: !prev[lineGuid] }));
|
|
33203
33871
|
const handleSelect = (item, code) => {
|
|
33204
33872
|
const availability = trajectNodeAvailability[item.line.guid];
|
|
33205
33873
|
const result = availability?.results.find((x) => x.code === code);
|
|
33206
33874
|
if (!result)
|
|
33207
33875
|
return;
|
|
33208
33876
|
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
|
|
33209
|
-
|
|
33877
|
+
applyResultToLines(item.lines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
33210
33878
|
};
|
|
33211
|
-
|
|
33212
|
-
|
|
33879
|
+
/** The remaining legs for a flight node open in the fly-in, as the regular flight results do. */
|
|
33880
|
+
const handleShowMoreFlights = (item) => {
|
|
33881
|
+
dispatch(setTrajectEditLineGuid(item.line.guid));
|
|
33882
|
+
dispatch(setFlyInType('traject-flight-results'));
|
|
33883
|
+
dispatch(setFlyInIsOpen(true));
|
|
33884
|
+
};
|
|
33885
|
+
const handleShowMore = (item) => {
|
|
33886
|
+
dispatch(setTrajectEditLineGuid(item.line.guid));
|
|
33887
|
+
dispatch(setFlyInType('acco-results'));
|
|
33888
|
+
dispatch(setFlyInIsOpen(true));
|
|
33889
|
+
};
|
|
33890
|
+
const handleEditOptions = (item) => {
|
|
33891
|
+
const availability = trajectNodeAvailability[item.line.guid];
|
|
33892
|
+
const result = availability?.results.find((x) => x.code === availability?.selectedCode);
|
|
33893
|
+
if (!result)
|
|
33894
|
+
return;
|
|
33895
|
+
dispatch(setPackagingAccoSearchDetails([result]));
|
|
33896
|
+
dispatch(setSelectedPackagingAccoResult(result.code));
|
|
33897
|
+
dispatch(setTrajectEditLineGuid(item.line.guid));
|
|
33898
|
+
dispatch(setFlyInType('acco-details'));
|
|
33899
|
+
dispatch(setFlyInIsOpen(true));
|
|
33900
|
+
};
|
|
33901
|
+
const getStayLocation = (item) => {
|
|
33902
|
+
const availability = trajectNodeAvailability[item.line.guid];
|
|
33903
|
+
const result = availability?.results.find((x) => x.code === availability?.selectedCode);
|
|
33904
|
+
return (result?.locationName ||
|
|
33905
|
+
result?.oordName ||
|
|
33906
|
+
result?.regionName ||
|
|
33907
|
+
result?.countryName ||
|
|
33908
|
+
item.line.location?.name ||
|
|
33909
|
+
item.line.oord?.name ||
|
|
33910
|
+
item.line.region?.name ||
|
|
33911
|
+
item.line.country?.name ||
|
|
33912
|
+
item.node.alternatives?.find((x) => x.isPreferred)?.city ||
|
|
33913
|
+
'');
|
|
33914
|
+
};
|
|
33915
|
+
const getStayName = (item) => {
|
|
33916
|
+
const availability = trajectNodeAvailability[item.line.guid];
|
|
33917
|
+
const result = availability?.results.find((x) => x.code === availability?.selectedCode);
|
|
33918
|
+
return result?.name || item.node.preferredAccommodationName || item.node.name || '';
|
|
33919
|
+
};
|
|
33920
|
+
const getStayTitle = (stay) => [getStayLocation(stay.item), getStayName(stay.item), `${stay.night + 1} ${translations.SRP.OF} ${stay.nights} ${translations.SRP.NIGHTS}`]
|
|
33921
|
+
.filter(Boolean)
|
|
33922
|
+
.join(' - ');
|
|
33923
|
+
const renderDayHeader = (day, stay) => {
|
|
33924
|
+
const date = new Date(day.date);
|
|
33925
|
+
const title = stay ? getStayTitle(stay) : day.name;
|
|
33926
|
+
return (React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
33927
|
+
React__default.createElement("div", { className: "search__results__label__date" },
|
|
33928
|
+
React__default.createElement("p", { className: "search__results__label__date-date" }, format$2(date, 'd', { locale })),
|
|
33929
|
+
React__default.createElement("p", null, format$2(date, 'MMM', { locale }))),
|
|
33930
|
+
React__default.createElement("div", { className: "search__results__label__text" },
|
|
33931
|
+
React__default.createElement(Icon, { name: stay ? 'ui-bed' : 'ui-location', height: 16 }),
|
|
33932
|
+
React__default.createElement("h3", null,
|
|
33933
|
+
React__default.createElement("strong", null, title)))));
|
|
33934
|
+
};
|
|
33935
|
+
const renderServiceNode = (item, showHeader = true, headerTitle) => {
|
|
33213
33936
|
const { node, line } = item;
|
|
33214
33937
|
const availability = trajectNodeAvailability[line.guid];
|
|
33215
|
-
const isExpanded = !!expandedNodes[line.guid];
|
|
33216
33938
|
const results = availability?.results ?? [];
|
|
33217
33939
|
const selected = results.find((x) => x.code === availability?.selectedCode) ?? null;
|
|
33218
33940
|
const configured = results.filter((x) => x.code !== selected?.code && !!findCandidateForResult(node, x));
|
|
@@ -33220,12 +33942,14 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33220
33942
|
// No entry yet means the node's search has been queued but not dispatched — still loading.
|
|
33221
33943
|
const status = availability?.status ?? 'loading';
|
|
33222
33944
|
return (React__default.createElement(React__default.Fragment, { key: line.guid },
|
|
33223
|
-
React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
33945
|
+
showHeader && (React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
33224
33946
|
React__default.createElement("div", { className: "search__results__label__date" },
|
|
33225
33947
|
React__default.createElement("p", { className: "search__results__label__date-date" }, format$2(new Date(line.from), 'd', { locale })),
|
|
33226
33948
|
React__default.createElement("p", null, format$2(new Date(line.from), 'MMM', { locale }))),
|
|
33227
33949
|
React__default.createElement("div", { className: "search__results__label__text" },
|
|
33228
|
-
React__default.createElement(Icon, { name: getNodeIcon(node), height: 16 })
|
|
33950
|
+
React__default.createElement(Icon, { name: getNodeIcon(node), height: 16 }),
|
|
33951
|
+
React__default.createElement("h3", null,
|
|
33952
|
+
React__default.createElement("strong", null, headerTitle || node.name))))),
|
|
33229
33953
|
status === 'loading' && (React__default.createElement(Spinner, { label: node.serviceType === EXCURSION_SERVICE_TYPE ? translations.SRP.LOADING_EXCURSIONS : translations.SRP.LOADING_ACCOMMODATIONS })),
|
|
33230
33954
|
status === 'unavailable' && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
33231
33955
|
status === 'resolved' && selected && (React__default.createElement(React__default.Fragment, null,
|
|
@@ -33234,9 +33958,13 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33234
33958
|
node.preferredAccommodationName,
|
|
33235
33959
|
" \u2014 ",
|
|
33236
33960
|
translations.SRP.NO_RESULTS))),
|
|
33237
|
-
React__default.createElement("div", { className: "search__results__cards search__results__cards--
|
|
33238
|
-
others.length > 0 && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
33239
|
-
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () =>
|
|
33961
|
+
React__default.createElement("div", { className: "search__results__cards search__results__cards--scroll" }, [selected, ...configured].map((result) => (React__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 })))),
|
|
33962
|
+
others.length > 0 && !hideShowMoreResults && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
33963
|
+
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMore(item) },
|
|
33964
|
+
translations.SRP.SHOW_MORE,
|
|
33965
|
+
" (",
|
|
33966
|
+
others.length,
|
|
33967
|
+
")")))))));
|
|
33240
33968
|
};
|
|
33241
33969
|
const handleSelectFlight = (item, flight) => {
|
|
33242
33970
|
dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
|
|
@@ -33249,8 +33977,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33249
33977
|
const flights = availability?.results ?? [];
|
|
33250
33978
|
const selected = flights.find((x) => x.outwardGuid === availability?.selectedGuid) ?? null;
|
|
33251
33979
|
const others = flights.filter((x) => x.outwardGuid !== availability?.selectedGuid);
|
|
33252
|
-
const
|
|
33253
|
-
const visible = isExpanded ? others : others.slice(0, 2);
|
|
33980
|
+
const visible = others.slice(0, 2);
|
|
33254
33981
|
return (React__default.createElement(React__default.Fragment, { key: line.guid },
|
|
33255
33982
|
React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
33256
33983
|
React__default.createElement("div", { className: "search__results__label__date" },
|
|
@@ -33271,26 +33998,109 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
33271
33998
|
React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" },
|
|
33272
33999
|
React__default.createElement(IndependentFlightOption, { key: `flight-${selected.outwardGuid}`, item: selected.outward, guid: selected.outwardGuid, selectedGuid: selected.outwardGuid, isOutward: true, showSelectedState: true, price: selected.price }),
|
|
33273
34000
|
visible.map((result) => (React__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) })))),
|
|
33274
|
-
others.length > 2 && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
33275
|
-
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () =>
|
|
34001
|
+
others.length > 2 && !hideShowMoreResults && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
34002
|
+
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreFlights(item) },
|
|
34003
|
+
translations.SRP.SHOW_MORE,
|
|
34004
|
+
" (",
|
|
34005
|
+
others.length - 2,
|
|
34006
|
+
")")))))));
|
|
34007
|
+
};
|
|
34008
|
+
/** The lines a suggested activity put on the entry, which is also how we know it was added. */
|
|
34009
|
+
const getAddedExcursionLines = (result) => (editablePackagingEntry?.lines ?? []).filter((line) => line.serviceType === EXCURSION_SERVICE_TYPE && line.productCode === result.code && isSameDay(new Date(line.from), new Date(result.fromDate)));
|
|
34010
|
+
const handleToggleActivity = (result) => {
|
|
34011
|
+
const added = getAddedExcursionLines(result);
|
|
34012
|
+
if (added.length) {
|
|
34013
|
+
dispatch(removeEditableEntryLines(added.map((line) => line.guid)));
|
|
34014
|
+
return;
|
|
34015
|
+
}
|
|
34016
|
+
const lines = buildTrajectExcursionLines(result);
|
|
34017
|
+
if (lines.length)
|
|
34018
|
+
dispatch(addEditableEntryLines(lines));
|
|
34019
|
+
};
|
|
34020
|
+
const handleShowMoreActivities = (dayOrder) => {
|
|
34021
|
+
dispatch(setTrajectActivitiesDayOrder(dayOrder));
|
|
34022
|
+
dispatch(setFlyInType('traject-day-activities'));
|
|
34023
|
+
dispatch(setFlyInIsOpen(true));
|
|
34024
|
+
};
|
|
34025
|
+
/** The excursion nodes the traject itself puts on a day. */
|
|
34026
|
+
const getDayExcursionItems = (dayOrder) => (nodesByDay.get(dayOrder) ?? [])
|
|
34027
|
+
.filter((node) => node.trajectNodeType === REGULAR_TRAJECT_NODE_TYPE && node.serviceType === EXCURSION_SERVICE_TYPE)
|
|
34028
|
+
.map((node) => lineByNodeId.get(node.nodeId))
|
|
34029
|
+
.filter((x) => !!x);
|
|
34030
|
+
/**
|
|
34031
|
+
* One block per day for everything there is to do that day.
|
|
34032
|
+
*
|
|
34033
|
+
* What the traject books sits first and stays selected; the rest of the day follows as things
|
|
34034
|
+
* the traveller may add. They share a row because they answer the same question — an excursion
|
|
34035
|
+
* on the traject and one found for the day are alternatives to each other, and a block apiece
|
|
34036
|
+
* would only hide that.
|
|
34037
|
+
*/
|
|
34038
|
+
const renderDayActivities = (day) => {
|
|
34039
|
+
const items = getDayExcursionItems(day.order);
|
|
34040
|
+
const suggestions = trajectDaySuggestions[day.order];
|
|
34041
|
+
const cards = buildDayActivityCards(items, trajectNodeAvailability, suggestions?.results ?? []);
|
|
34042
|
+
const isResolving = items.some((item) => (trajectNodeAvailability[item.line.guid]?.status ?? 'loading') === 'loading') || suggestions?.status === 'loading';
|
|
34043
|
+
// A day with nothing configured and nothing found has nothing to say. A day whose own
|
|
34044
|
+
// excursion came back empty does, so the gap is not passed off as a day without activities.
|
|
34045
|
+
if (!cards.length && !items.length)
|
|
34046
|
+
return null;
|
|
34047
|
+
const visible = cards.slice(0, MAX_DAY_ACTIVITIES);
|
|
34048
|
+
const hidden = cards.length - visible.length;
|
|
34049
|
+
return (React__default.createElement(React__default.Fragment, { key: `activities-${day.order}` },
|
|
34050
|
+
React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
34051
|
+
React__default.createElement("div", { className: "search__results__label__date" },
|
|
34052
|
+
React__default.createElement("p", { className: "search__results__label__date-date" }, format$2(new Date(day.date), 'd', { locale })),
|
|
34053
|
+
React__default.createElement("p", null, format$2(new Date(day.date), 'MMM', { locale }))),
|
|
34054
|
+
React__default.createElement("div", { className: "search__results__label__text" },
|
|
34055
|
+
React__default.createElement(Icon, { name: "ui-excursion", height: 16 }),
|
|
34056
|
+
React__default.createElement("h3", null,
|
|
34057
|
+
React__default.createElement("strong", null, translations.SRP.ACTIVITIES)))),
|
|
34058
|
+
isResolving && !cards.length && React__default.createElement(Spinner, { label: translations.SRP.LOADING_EXCURSIONS }),
|
|
34059
|
+
!isResolving && !cards.length && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
34060
|
+
!!cards.length && (React__default.createElement(React__default.Fragment, null,
|
|
34061
|
+
React__default.createElement("div", { className: "search__results__cards search__results__cards--scroll" }, visible.map(renderActivityCard)),
|
|
34062
|
+
hidden > 0 && !hideShowMoreResults && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
34063
|
+
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreActivities(day.order) },
|
|
34064
|
+
translations.SRP.SHOW_MORE,
|
|
34065
|
+
" (",
|
|
34066
|
+
hidden,
|
|
34067
|
+
")")))))));
|
|
34068
|
+
};
|
|
34069
|
+
/**
|
|
34070
|
+
* An activity the traject books is swapped for another, the way a hotel is; one the traveller
|
|
34071
|
+
* found for themselves is added to the trip or taken back off it.
|
|
34072
|
+
*/
|
|
34073
|
+
const renderActivityCard = ({ result, item }) => {
|
|
34074
|
+
const isSelected = item ? trajectNodeAvailability[item.line.guid]?.selectedCode === result.code : getAddedExcursionLines(result).length > 0;
|
|
34075
|
+
return (React__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 }));
|
|
33276
34076
|
};
|
|
33277
34077
|
if (isLoading)
|
|
33278
34078
|
return React__default.createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
|
|
33279
34079
|
return (React__default.createElement(React__default.Fragment, null, trajectEntry.traject.days.map((day) => {
|
|
33280
|
-
const
|
|
33281
|
-
|
|
33282
|
-
|
|
33283
|
-
|
|
33284
|
-
|
|
33285
|
-
|
|
33286
|
-
|
|
33287
|
-
|
|
33288
|
-
|
|
33289
|
-
|
|
33290
|
-
|
|
33291
|
-
|
|
33292
|
-
|
|
33293
|
-
|
|
34080
|
+
const stay = stayByDay.get(day.order);
|
|
34081
|
+
const dayNodes = (nodesByDay.get(day.order) ?? []).filter((node) => isRenderableNode(node) && lineByNodeId.has(node.nodeId));
|
|
34082
|
+
const stayLeadsDay = !!stay && (stay.night > 0 || dayNodes[0]?.nodeId === stay.item.node.nodeId);
|
|
34083
|
+
// Where the day's activities belong in the order: with the first excursion it carries.
|
|
34084
|
+
const firstExcursionNodeId = dayNodes.find((node) => node.serviceType === EXCURSION_SERVICE_TYPE)?.nodeId;
|
|
34085
|
+
const showDayHeader = stayLeadsDay || !!day.name?.trim();
|
|
34086
|
+
return (React__default.createElement(React__default.Fragment, { key: `day-${day.order}` },
|
|
34087
|
+
showDayHeader && renderDayHeader(day, stayLeadsDay ? stay : undefined),
|
|
34088
|
+
dayNodes.map((node) => {
|
|
34089
|
+
const item = lineByNodeId.get(node.nodeId);
|
|
34090
|
+
if (node.serviceType === ACCOMMODATION_SERVICE_TYPE) {
|
|
34091
|
+
const stayStart = stayStartByNodeId.get(node.nodeId);
|
|
34092
|
+
const titledByDayHeader = stayLeadsDay && stay.item.node.nodeId === node.nodeId;
|
|
34093
|
+
return renderServiceNode(item, !titledByDayHeader, stayStart && getStayTitle(stayStart));
|
|
34094
|
+
}
|
|
34095
|
+
// Every excursion the day holds is drawn once, in one block, where the first of
|
|
34096
|
+
// them falls in the order — so the activities still sit between the stay and the
|
|
34097
|
+
// flight that follow them.
|
|
34098
|
+
if (node.serviceType === EXCURSION_SERVICE_TYPE) {
|
|
34099
|
+
return node.nodeId === firstExcursionNodeId ? renderDayActivities(day) : null;
|
|
34100
|
+
}
|
|
34101
|
+
return renderFlightNode(item);
|
|
34102
|
+
}),
|
|
34103
|
+
firstExcursionNodeId === undefined && renderDayActivities(day)));
|
|
33294
34104
|
})));
|
|
33295
34105
|
};
|
|
33296
34106
|
|
|
@@ -33298,6 +34108,8 @@ const useTrajectAvailability = () => {
|
|
|
33298
34108
|
const context = useContext(SearchResultsConfigurationContext);
|
|
33299
34109
|
const dispatch = useDispatch();
|
|
33300
34110
|
const trajectEntry = context?.trajectEntry;
|
|
34111
|
+
const showApiItems = !!context?.showApiItems;
|
|
34112
|
+
const hideShowMoreResults = !!context?.hideShowMoreResults;
|
|
33301
34113
|
useEffect(() => {
|
|
33302
34114
|
if (!context || !trajectEntry)
|
|
33303
34115
|
return;
|
|
@@ -33330,7 +34142,8 @@ const useTrajectAvailability = () => {
|
|
|
33330
34142
|
const search = item.node.serviceType === EXCURSION_SERVICE_TYPE ? build.searchPackagingExcursions : build.searchPackagingAccommodations;
|
|
33331
34143
|
dispatch(setTrajectNodeLoading(lineGuid));
|
|
33332
34144
|
try {
|
|
33333
|
-
|
|
34145
|
+
// With external items switched off the node only looks at products held inside Tide.
|
|
34146
|
+
const candidates = getNodeCandidates(item.node).filter((x) => showApiItems || !isExternalCandidate(x));
|
|
33334
34147
|
const attempts = await Promise.all(candidates.map(async (candidate) => {
|
|
33335
34148
|
try {
|
|
33336
34149
|
const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, candidate.code), controller.signal);
|
|
@@ -33342,18 +34155,25 @@ const useTrajectAvailability = () => {
|
|
|
33342
34155
|
return null;
|
|
33343
34156
|
}
|
|
33344
34157
|
}));
|
|
33345
|
-
|
|
33346
|
-
|
|
34158
|
+
const configuredResults = attempts
|
|
34159
|
+
.filter((x) => !!x)
|
|
34160
|
+
.filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
|
|
33347
34161
|
const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
|
|
33348
|
-
let preferredUnavailable = !!preferredCandidate && !
|
|
33349
|
-
//
|
|
33350
|
-
if
|
|
33351
|
-
|
|
33352
|
-
|
|
34162
|
+
let preferredUnavailable = !!preferredCandidate && !configuredResults.some((x) => matchesCode(x, preferredCandidate.code));
|
|
34163
|
+
// The destination search exists only to stock the show-more fly-in. With the button hidden
|
|
34164
|
+
// a node is limited to its configured products; if none are available it simply reports so,
|
|
34165
|
+
// rather than quietly substituting a hotel nobody configured.
|
|
34166
|
+
const destinationResults = hideShowMoreResults
|
|
34167
|
+
? []
|
|
34168
|
+
: ((await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [])
|
|
34169
|
+
.filter((x) => showApiItems || !isExternalResult(x))
|
|
34170
|
+
.filter((x) => !configuredResults.some((configured) => configured.code === x.code));
|
|
34171
|
+
const picked = configuredResults[0] ?? pickCheapest(destinationResults);
|
|
34172
|
+
const results = [...configuredResults, ...destinationResults];
|
|
34173
|
+
if (!configuredResults.length)
|
|
33353
34174
|
preferredUnavailable = candidates.length > 0;
|
|
33354
|
-
}
|
|
33355
34175
|
if (controller.signal.aborted)
|
|
33356
|
-
return
|
|
34176
|
+
return [];
|
|
33357
34177
|
dispatch(setTrajectNodeAvailability({
|
|
33358
34178
|
lineGuid,
|
|
33359
34179
|
results,
|
|
@@ -33361,58 +34181,58 @@ const useTrajectAvailability = () => {
|
|
|
33361
34181
|
preferredUnavailable
|
|
33362
34182
|
}));
|
|
33363
34183
|
if (!picked)
|
|
33364
|
-
return
|
|
33365
|
-
|
|
33366
|
-
return
|
|
34184
|
+
return item.lines.map((x) => x.guid);
|
|
34185
|
+
applyResultToLines(item.lines, picked).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
34186
|
+
return [];
|
|
33367
34187
|
}
|
|
33368
34188
|
catch (error) {
|
|
33369
34189
|
if (controller.signal.aborted)
|
|
33370
|
-
return
|
|
34190
|
+
return [];
|
|
33371
34191
|
console.error('Failed to resolve traject node availability', item.node.name, error);
|
|
33372
34192
|
dispatch(setTrajectNodeAvailability({ lineGuid, results: [], selectedCode: null, preferredUnavailable: false }));
|
|
33373
|
-
return
|
|
34193
|
+
return item.lines.map((x) => x.guid);
|
|
33374
34194
|
}
|
|
33375
34195
|
};
|
|
33376
34196
|
const resolveFlightNode = async (item) => {
|
|
33377
34197
|
const lineGuid = item.line.guid;
|
|
33378
34198
|
if (!item.node.departureAirportCode || !item.node.arrivalAirportCode) {
|
|
33379
34199
|
dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
|
|
33380
|
-
return
|
|
34200
|
+
return item.lines.map((x) => x.guid);
|
|
33381
34201
|
}
|
|
33382
34202
|
dispatch(setTrajectFlightLoading(lineGuid));
|
|
33383
34203
|
try {
|
|
33384
34204
|
const flights = (await build.searchPackagingFlights(config, buildTrajectFlightRequest(item, searchContext, flightPax), controller.signal)) ?? [];
|
|
33385
34205
|
if (controller.signal.aborted)
|
|
33386
|
-
return
|
|
34206
|
+
return [];
|
|
33387
34207
|
const picked = pickCheapestFlight(flights);
|
|
33388
34208
|
dispatch(setTrajectFlightAvailability({ lineGuid, results: flights, selectedGuid: picked?.outwardGuid ?? null }));
|
|
33389
34209
|
if (!picked)
|
|
33390
|
-
return
|
|
34210
|
+
return item.lines.map((x) => x.guid);
|
|
33391
34211
|
dispatch(updateEditableEntryLine(applyFlightToLine(item.line, picked)));
|
|
33392
|
-
return
|
|
34212
|
+
return [];
|
|
33393
34213
|
}
|
|
33394
34214
|
catch (error) {
|
|
33395
34215
|
if (controller.signal.aborted)
|
|
33396
|
-
return
|
|
34216
|
+
return [];
|
|
33397
34217
|
console.error('Failed to resolve traject flight', item.node.name, error);
|
|
33398
34218
|
dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
|
|
33399
|
-
return
|
|
34219
|
+
return item.lines.map((x) => x.guid);
|
|
33400
34220
|
}
|
|
33401
34221
|
};
|
|
33402
34222
|
(async () => {
|
|
33403
34223
|
const searchable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node));
|
|
33404
34224
|
const flightNodes = nodesWithLines.filter((x) => isTrajectFlightNode(x.node));
|
|
33405
|
-
const unresolved = await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)]);
|
|
34225
|
+
const unresolved = (await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)])).flat();
|
|
33406
34226
|
if (controller.signal.aborted)
|
|
33407
34227
|
return;
|
|
33408
34228
|
// A node with no availability anywhere cannot be priced or booked, so its line comes off
|
|
33409
34229
|
// the entry. The card stays visible and reports the gap.
|
|
33410
|
-
const emptyLineGuids = unresolved.filter(
|
|
34230
|
+
const emptyLineGuids = unresolved.filter(Boolean);
|
|
33411
34231
|
if (emptyLineGuids.length)
|
|
33412
34232
|
dispatch(removeEditableEntryLines(emptyLineGuids));
|
|
33413
34233
|
})();
|
|
33414
34234
|
return () => controller.abort();
|
|
33415
|
-
}, [trajectEntry]);
|
|
34235
|
+
}, [trajectEntry, showApiItems, hideShowMoreResults]);
|
|
33416
34236
|
};
|
|
33417
34237
|
|
|
33418
34238
|
const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
@@ -33420,7 +34240,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
|
33420
34240
|
const dispatch = useDispatch();
|
|
33421
34241
|
const context = useContext(SearchResultsConfigurationContext);
|
|
33422
34242
|
const translations = getTranslations(context?.languageCode ?? 'en-GB');
|
|
33423
|
-
const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry } = useSelector((state) => state.searchResults);
|
|
34243
|
+
const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry, trajectEditLineGuid } = useSelector((state) => state.searchResults);
|
|
33424
34244
|
const isMobile = useMediaQuery('(max-width: 1200px)');
|
|
33425
34245
|
// Resolves live availability per traject node; no-op unless a traject entry is configured.
|
|
33426
34246
|
useTrajectAvailability();
|
|
@@ -33709,6 +34529,20 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
|
33709
34529
|
setDetailsIsLoading(false);
|
|
33710
34530
|
};
|
|
33711
34531
|
const handleConfirmHotelSwap = () => {
|
|
34532
|
+
if (context?.trajectEntry && trajectEditLineGuid) {
|
|
34533
|
+
const item = getTrajectNodesWithLines(context.trajectEntry).find((x) => x.line.guid === trajectEditLineGuid);
|
|
34534
|
+
const result = packagingAccoSearchDetails.find((x) => x.code === selectedPackagingAccoResultCode);
|
|
34535
|
+
if (item && result) {
|
|
34536
|
+
const currentLines = item.lines
|
|
34537
|
+
.map((line) => editablePackagingEntry?.lines?.find((x) => x.guid === line.guid))
|
|
34538
|
+
.filter((x) => !!x);
|
|
34539
|
+
applyResultToLines(currentLines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
34540
|
+
dispatch(updateTrajectNodeResult({ lineGuid: trajectEditLineGuid, result }));
|
|
34541
|
+
}
|
|
34542
|
+
dispatch(setTrajectEditLineGuid(null));
|
|
34543
|
+
handleFlyInToggle(false);
|
|
34544
|
+
return;
|
|
34545
|
+
}
|
|
33712
34546
|
const updatedEntry = swapHotelInPackagingEntry();
|
|
33713
34547
|
if (!updatedEntry)
|
|
33714
34548
|
return;
|
|
@@ -34057,6 +34891,10 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
|
34057
34891
|
const fetchPackagingAccoSearchDetails = async () => {
|
|
34058
34892
|
if (!selectedPackagingAccoResultCode || !context)
|
|
34059
34893
|
return;
|
|
34894
|
+
// A traject node already resolved its own availability, options per room included, and hands
|
|
34895
|
+
// that result straight to the fly-in.
|
|
34896
|
+
if (context.trajectEntry)
|
|
34897
|
+
return;
|
|
34060
34898
|
if (skipInitialPackagingAccoDetailsRef.current) {
|
|
34061
34899
|
skipInitialPackagingAccoDetailsRef.current = false;
|
|
34062
34900
|
return;
|