@qite/tide-components 1.0.14-preview.3 → 1.0.14-preview.4
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 +395 -99
- package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +16 -3
- package/build/build-cjs/src/search-results/utils/traject-utils.d.ts +7 -1
- package/build/build-cjs/src/shared/booking/travelers-form.d.ts +12 -3
- package/build/build-esm/index.js +395 -99
- package/build/build-esm/src/search-results/store/search-results-slice.d.ts +16 -3
- package/build/build-esm/src/search-results/utils/traject-utils.d.ts +7 -1
- package/build/build-esm/src/shared/booking/travelers-form.d.ts +12 -3
- package/package.json +2 -2
package/build/build-esm/index.js
CHANGED
|
@@ -16104,6 +16104,7 @@ var ENDPOINT$1 = "/api/web/packaging";
|
|
|
16104
16104
|
var ENDPOINT_START_TRANSACTION = ENDPOINT$1 + "/start";
|
|
16105
16105
|
var ENDPOINT_ACCOMMODATIONS = ENDPOINT$1 + "/accommodations";
|
|
16106
16106
|
var ENDPOINT_EXCURSIONS = ENDPOINT$1 + "/excursions";
|
|
16107
|
+
var ENDPOINT_TRANSFERS = ENDPOINT$1 + "/transfers";
|
|
16107
16108
|
var ENDPOINT_FLIGHTS = ENDPOINT$1 + "/flights";
|
|
16108
16109
|
var ENDPOINT_PRICE_DETAILS = ENDPOINT$1 + "/price-details";
|
|
16109
16110
|
var ENDPOINT_BOOK = ENDPOINT$1 + "/book";
|
|
@@ -16136,6 +16137,12 @@ var searchPackagingExcursions = function (config, request, signal) {
|
|
|
16136
16137
|
var body = JSON.stringify(request);
|
|
16137
16138
|
return post(url, apiKey, body, config.token, signal, true);
|
|
16138
16139
|
};
|
|
16140
|
+
var searchPackagingTransfers = function (config, request, signal) {
|
|
16141
|
+
var url = "" + config.host + ENDPOINT_TRANSFERS;
|
|
16142
|
+
var apiKey = config.apiKey;
|
|
16143
|
+
var body = JSON.stringify(request);
|
|
16144
|
+
return post(url, apiKey, body, config.token, signal, true);
|
|
16145
|
+
};
|
|
16139
16146
|
var getEntry = function (config, magicLinkCode, signal) {
|
|
16140
16147
|
var apiKey = config.apiKey;
|
|
16141
16148
|
var url = "" + config.host + ENDPOINT_ENTRY(magicLinkCode);
|
|
@@ -16313,6 +16320,7 @@ exports.searchOords = searchOords;
|
|
|
16313
16320
|
exports.searchPackagingAccommodations = searchPackagingAccommodations;
|
|
16314
16321
|
exports.searchPackagingExcursions = searchPackagingExcursions;
|
|
16315
16322
|
exports.searchPackagingFlights = searchPackagingFlights;
|
|
16323
|
+
exports.searchPackagingTransfers = searchPackagingTransfers;
|
|
16316
16324
|
exports.searchRegions = searchRegions;
|
|
16317
16325
|
exports.searchWithErrorMapping = searchWithErrorMapping;
|
|
16318
16326
|
exports.sendContactForm = sendContactForm;
|
|
@@ -19805,10 +19813,30 @@ function formatBirthDate$1(birthDate) {
|
|
|
19805
19813
|
// A field can have separate "main-booker"/other-pax variants sharing the same identity
|
|
19806
19814
|
// (customFieldConfigurationId for custom fields, fieldName for built-in fields).
|
|
19807
19815
|
const MAIN_BOOKER_GROUP = 'main-booker';
|
|
19816
|
+
const MAIN_BOOKER_FIELD_PREFIX = 'mainBooker';
|
|
19808
19817
|
const isCustomPaxField = (field) => field.customFieldConfigurationId != null;
|
|
19818
|
+
// Tide names the built-in main-booker fields with a prefix ('mainBookerGender', 'mainBookerCountryId', ...),
|
|
19819
|
+
// the other-pax ones without ('gender', 'countryId'). Strip it so both are variants of the same field.
|
|
19820
|
+
function normalizeBuiltInFieldName(field) {
|
|
19821
|
+
if (isCustomPaxField(field) || !field.fieldName?.startsWith(MAIN_BOOKER_FIELD_PREFIX))
|
|
19822
|
+
return field;
|
|
19823
|
+
const stripped = field.fieldName.slice(MAIN_BOOKER_FIELD_PREFIX.length);
|
|
19824
|
+
if (!stripped)
|
|
19825
|
+
return field;
|
|
19826
|
+
return { ...field, fieldName: stripped.charAt(0).toLowerCase() + stripped.slice(1) };
|
|
19827
|
+
}
|
|
19828
|
+
// Once Tide sends built-in fields, that configuration decides which built-in fields are shown/required;
|
|
19829
|
+
// the hardcoded formFields / core defaults are only the fallback for when it doesn't.
|
|
19830
|
+
function hasConfiguredBuiltInPaxFields(paxFields = []) {
|
|
19831
|
+
return paxFields.some((f) => !isCustomPaxField(f));
|
|
19832
|
+
}
|
|
19833
|
+
// The main booker's address/contact details are then asked on the main booker's own traveler block.
|
|
19834
|
+
function hasConfiguredMainBookerFields(paxFields = []) {
|
|
19835
|
+
return paxFields.some((f) => !isCustomPaxField(f) && f.group === MAIN_BOOKER_GROUP);
|
|
19836
|
+
}
|
|
19809
19837
|
function resolveApplicablePaxFields(fields, isMainBooker) {
|
|
19810
19838
|
const variantsByKey = new Map();
|
|
19811
|
-
fields.forEach((field) => {
|
|
19839
|
+
fields.map(normalizeBuiltInFieldName).forEach((field) => {
|
|
19812
19840
|
const key = field.customFieldConfigurationId ?? field.fieldName;
|
|
19813
19841
|
const variants = variantsByKey.get(key) ?? [];
|
|
19814
19842
|
variants.push(field);
|
|
@@ -19870,11 +19898,50 @@ const BUILT_IN_FIELD_PATHS = {
|
|
|
19870
19898
|
// The 4 fields that always render/validate regardless of paxFields config (fallback behavior).
|
|
19871
19899
|
const CORE_FIELD_NAMES = new Set(['gender', 'firstName', 'lastName', 'dateOfBirth']);
|
|
19872
19900
|
const ADDRESS_FIELD_NAMES = ['street', 'location', 'countryId', 'postalCode', 'number'];
|
|
19901
|
+
// Built-in fields have no dataType from Tide; their control type and label follow from the fieldName.
|
|
19902
|
+
// gender, hasInsurance, countryId, weight and height have a dedicated control and only need a label here.
|
|
19903
|
+
function getBuiltInFieldControlMeta(translations) {
|
|
19904
|
+
return {
|
|
19905
|
+
gender: { label: translations.TRAVELERS_FORM.GENDER_ID },
|
|
19906
|
+
firstName: { label: translations.TRAVELERS_FORM.FIRST_NAME },
|
|
19907
|
+
lastName: { label: translations.TRAVELERS_FORM.LAST_NAME },
|
|
19908
|
+
dateOfBirth: { type: 'date', label: translations.TRAVELERS_FORM.BIRTHDATE },
|
|
19909
|
+
nickname: { label: translations.TRAVELERS_FORM.NICKNAME },
|
|
19910
|
+
birthplace: { label: translations.TRAVELERS_FORM.BIRTHPLACE },
|
|
19911
|
+
nationality: { label: translations.TRAVELERS_FORM.NATIONALITY },
|
|
19912
|
+
preferredLanguageId: { type: 'number', label: translations.TRAVELERS_FORM.PREFERRED_LANGUAGE },
|
|
19913
|
+
email: { type: 'email', label: translations.TRAVELERS_FORM.TRAVELER_EMAIL },
|
|
19914
|
+
phone: { type: 'tel', label: translations.TRAVELERS_FORM.TRAVELER_PHONE },
|
|
19915
|
+
mobile: { type: 'tel', label: translations.TRAVELERS_FORM.TRAVELER_MOBILE },
|
|
19916
|
+
passportNumber: { label: translations.TRAVELERS_FORM.PASSPORT_NUMBER },
|
|
19917
|
+
passportIssueDate: { type: 'date', label: translations.TRAVELERS_FORM.PASSPORT_ISSUE_DATE },
|
|
19918
|
+
passportIssuedLocation: { label: translations.TRAVELERS_FORM.PASSPORT_ISSUED_LOCATION },
|
|
19919
|
+
passportExpiryDate: { type: 'date', label: translations.TRAVELERS_FORM.PASSPORT_EXPIRY_DATE },
|
|
19920
|
+
emergencyNumber: { type: 'tel', label: translations.TRAVELERS_FORM.EMERGENCY_NUMBER },
|
|
19921
|
+
hasInsurance: { label: translations.TRAVELERS_FORM.HAS_INSURANCE },
|
|
19922
|
+
insuranceName: { label: translations.TRAVELERS_FORM.INSURANCE_NAME },
|
|
19923
|
+
info: { label: translations.TRAVELERS_FORM.INFO },
|
|
19924
|
+
weight: { label: translations.TRAVELERS_FORM.WEIGHT },
|
|
19925
|
+
height: { label: translations.TRAVELERS_FORM.HEIGHT },
|
|
19926
|
+
street: { label: translations.TRAVELERS_FORM.STREET },
|
|
19927
|
+
location: { label: translations.TRAVELERS_FORM.CITY },
|
|
19928
|
+
countryId: { label: translations.TRAVELERS_FORM.COUNTRY },
|
|
19929
|
+
postalCode: { label: translations.TRAVELERS_FORM.ZIPCODE },
|
|
19930
|
+
number: { label: translations.TRAVELERS_FORM.HOUSE_NUMBER }
|
|
19931
|
+
};
|
|
19932
|
+
}
|
|
19933
|
+
// The form's gender radio values ('m'/'f'/'x') <-> Tide's Gender enum.
|
|
19934
|
+
const GENDER_BY_FORM_VALUE = { m: build.Gender.male, f: build.Gender.female, x: build.Gender.other };
|
|
19935
|
+
function formValueFromGender(gender) {
|
|
19936
|
+
return Object.keys(GENDER_BY_FORM_VALUE).find((key) => GENDER_BY_FORM_VALUE[key] === gender) ?? '';
|
|
19937
|
+
}
|
|
19873
19938
|
// Shared by buildPax (new-booking) and applyTravelersFormValuesToEditablePackagingEntry
|
|
19874
19939
|
// (edit-booking), now that PackagingEntryPax and BookingPackagePax have the same shape.
|
|
19875
19940
|
function applyTravelerToPax(pax, traveler, mainBookerId) {
|
|
19876
19941
|
return {
|
|
19877
19942
|
...pax,
|
|
19943
|
+
// Unanswered (optional) gender keeps whatever the pax already had.
|
|
19944
|
+
gender: GENDER_BY_FORM_VALUE[traveler.gender] ?? pax.gender,
|
|
19878
19945
|
firstName: traveler.firstName ?? '',
|
|
19879
19946
|
lastName: traveler.lastName ?? '',
|
|
19880
19947
|
dateOfBirth: traveler.birthDate || null,
|
|
@@ -20034,7 +20101,13 @@ function mapPackagingEntryPaxToTraveler(roomTraveler) {
|
|
|
20034
20101
|
customFields: roomTraveler.customFields ?? []
|
|
20035
20102
|
};
|
|
20036
20103
|
}
|
|
20037
|
-
|
|
20104
|
+
// Keeps the values already on the entry's pax and adds an empty slot for every configured custom field it lacks,
|
|
20105
|
+
// so the form renders/validates the same set as createTraveler does for a new booking.
|
|
20106
|
+
function mergeCustomFields(existing = [], paxFields) {
|
|
20107
|
+
const missing = buildEmptyCustomFields(paxFields).filter((empty) => !existing.some((c) => c.customFieldConfigurationId === empty.customFieldConfigurationId));
|
|
20108
|
+
return [...existing, ...missing];
|
|
20109
|
+
}
|
|
20110
|
+
function createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, agentAdressId, paxFields = editablePackagingEntry?.paxFields ?? []) {
|
|
20038
20111
|
const pax = editablePackagingEntry?.pax ?? [];
|
|
20039
20112
|
const rooms = editablePackagingEntry.rooms.map((room) => {
|
|
20040
20113
|
const roomPax = pax.filter((x) => room.paxIds.includes(x.id));
|
|
@@ -20047,8 +20120,9 @@ function createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, a
|
|
|
20047
20120
|
firstName: roomTraveler.firstName ?? '',
|
|
20048
20121
|
lastName: roomTraveler.lastName ?? '',
|
|
20049
20122
|
birthDate: roomTraveler.dateOfBirth ? format$2(new Date(roomTraveler.dateOfBirth), 'yyyy-MM-dd') : '',
|
|
20050
|
-
gender:
|
|
20051
|
-
...mapPackagingEntryPaxToTraveler(roomTraveler)
|
|
20123
|
+
gender: formValueFromGender(roomTraveler.gender),
|
|
20124
|
+
...mapPackagingEntryPaxToTraveler(roomTraveler),
|
|
20125
|
+
customFields: mergeCustomFields(roomTraveler.customFields, paxFields)
|
|
20052
20126
|
};
|
|
20053
20127
|
}),
|
|
20054
20128
|
children: children.map((roomTraveler) => {
|
|
@@ -20057,9 +20131,10 @@ function createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, a
|
|
|
20057
20131
|
firstName: roomTraveler.firstName ?? '',
|
|
20058
20132
|
lastName: roomTraveler.lastName ?? '',
|
|
20059
20133
|
birthDate: roomTraveler.dateOfBirth ? format$2(new Date(roomTraveler.dateOfBirth), 'yyyy-MM-dd') : '',
|
|
20060
|
-
gender:
|
|
20134
|
+
gender: formValueFromGender(roomTraveler.gender),
|
|
20061
20135
|
isBaby: isBabyAge(roomTraveler.age),
|
|
20062
|
-
...mapPackagingEntryPaxToTraveler(roomTraveler)
|
|
20136
|
+
...mapPackagingEntryPaxToTraveler(roomTraveler),
|
|
20137
|
+
customFields: mergeCustomFields(roomTraveler.customFields, paxFields)
|
|
20063
20138
|
};
|
|
20064
20139
|
})
|
|
20065
20140
|
};
|
|
@@ -20084,7 +20159,8 @@ function applyTravelersFormValuesToEditablePackagingEntry(editablePackagingEntry
|
|
|
20084
20159
|
const travelers = values.rooms.flatMap((room) => [...room.adults, ...room.children]);
|
|
20085
20160
|
// The backend treats the main booker's per-pax address as authoritative, so keep the
|
|
20086
20161
|
// booking-level address in sync with it rather than sending two independent values.
|
|
20087
|
-
const
|
|
20162
|
+
const mainBookerTraveler = travelers.find((x) => x.id === values.mainBookerId);
|
|
20163
|
+
const mainBookerAddress = mainBookerTraveler?.address;
|
|
20088
20164
|
return {
|
|
20089
20165
|
...editablePackagingEntry,
|
|
20090
20166
|
pax: (editablePackagingEntry.pax ?? []).map((pax) => {
|
|
@@ -20100,8 +20176,9 @@ function applyTravelersFormValuesToEditablePackagingEntry(editablePackagingEntry
|
|
|
20100
20176
|
place: mainBookerAddress?.location || values.place,
|
|
20101
20177
|
country: mainBookerAddress?.country || values.country,
|
|
20102
20178
|
travelAgentId: values.travelAgentId,
|
|
20103
|
-
|
|
20104
|
-
|
|
20179
|
+
// When Tide configures the main-booker fields, contact details live on the main booker's traveler block.
|
|
20180
|
+
phone: values.phone || mainBookerTraveler?.mobile || mainBookerTraveler?.phone,
|
|
20181
|
+
email: values.email || mainBookerTraveler?.email
|
|
20105
20182
|
}
|
|
20106
20183
|
};
|
|
20107
20184
|
}
|
|
@@ -20258,30 +20335,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
|
|
|
20258
20335
|
return (React__default.createElement("div", { key: field.customFieldConfigurationId, style: { gridColumn: `span ${field.col || 12}` } }, getCustomFieldControl(field, value, name)));
|
|
20259
20336
|
})));
|
|
20260
20337
|
};
|
|
20261
|
-
|
|
20262
|
-
const BUILT_IN_FIELD_CONTROL_META = {
|
|
20263
|
-
firstName: { label: translations.TRAVELERS_FORM.FIRST_NAME },
|
|
20264
|
-
lastName: { label: translations.TRAVELERS_FORM.LAST_NAME },
|
|
20265
|
-
dateOfBirth: { type: 'date', label: translations.TRAVELERS_FORM.BIRTHDATE },
|
|
20266
|
-
nickname: { label: translations.TRAVELERS_FORM.NICKNAME },
|
|
20267
|
-
birthplace: { label: translations.TRAVELERS_FORM.BIRTHPLACE },
|
|
20268
|
-
nationality: { label: translations.TRAVELERS_FORM.NATIONALITY },
|
|
20269
|
-
preferredLanguageId: { type: 'number', label: translations.TRAVELERS_FORM.PREFERRED_LANGUAGE },
|
|
20270
|
-
email: { type: 'email', label: translations.TRAVELERS_FORM.TRAVELER_EMAIL },
|
|
20271
|
-
phone: { label: translations.TRAVELERS_FORM.TRAVELER_PHONE },
|
|
20272
|
-
mobile: { label: translations.TRAVELERS_FORM.TRAVELER_MOBILE },
|
|
20273
|
-
passportNumber: { label: translations.TRAVELERS_FORM.PASSPORT_NUMBER },
|
|
20274
|
-
passportIssueDate: { type: 'date', label: translations.TRAVELERS_FORM.PASSPORT_ISSUE_DATE },
|
|
20275
|
-
passportIssuedLocation: { label: translations.TRAVELERS_FORM.PASSPORT_ISSUED_LOCATION },
|
|
20276
|
-
passportExpiryDate: { type: 'date', label: translations.TRAVELERS_FORM.PASSPORT_EXPIRY_DATE },
|
|
20277
|
-
emergencyNumber: { label: translations.TRAVELERS_FORM.EMERGENCY_NUMBER },
|
|
20278
|
-
insuranceName: { label: translations.TRAVELERS_FORM.INSURANCE_NAME },
|
|
20279
|
-
info: { label: translations.TRAVELERS_FORM.INFO },
|
|
20280
|
-
street: { label: translations.TRAVELERS_FORM.STREET },
|
|
20281
|
-
location: { label: translations.TRAVELERS_FORM.CITY },
|
|
20282
|
-
postalCode: { label: translations.TRAVELERS_FORM.ZIPCODE },
|
|
20283
|
-
number: { label: translations.TRAVELERS_FORM.HOUSE_NUMBER }
|
|
20284
|
-
};
|
|
20338
|
+
const BUILT_IN_FIELD_CONTROL_META = getBuiltInFieldControlMeta(translations);
|
|
20285
20339
|
const renderBuiltInFieldControl = (fieldName, isRequired, travelerValues, namePrefix) => {
|
|
20286
20340
|
const path = BUILT_IN_FIELD_PATHS[fieldName] ?? fieldName;
|
|
20287
20341
|
const name = `${namePrefix}.${path}`;
|
|
@@ -20316,7 +20370,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
|
|
|
20316
20370
|
const resolveBuiltInField = (fieldName, travelerValues) => {
|
|
20317
20371
|
const isMainBooker = travelerValues.id === formik.values.mainBookerId;
|
|
20318
20372
|
const configured = getBuiltInPaxField(paxFields, fieldName, isMainBooker);
|
|
20319
|
-
const fallback = CORE_FIELD_DEFAULTS[fieldName];
|
|
20373
|
+
const fallback = hasConfiguredBuiltInPaxFields(paxFields) ? undefined : CORE_FIELD_DEFAULTS[fieldName];
|
|
20320
20374
|
if (!configured && !fallback)
|
|
20321
20375
|
return undefined;
|
|
20322
20376
|
return { isRequired: configured?.isRequired ?? fallback.isRequired, col: configured?.col ?? fallback.col };
|
|
@@ -20328,7 +20382,14 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
|
|
|
20328
20382
|
return (React__default.createElement("div", { key: fieldName, style: { gridColumn: `span ${resolved.col || 12}` } }, renderBuiltInFieldControl(fieldName, resolved.isRequired, travelerValues, namePrefix)));
|
|
20329
20383
|
};
|
|
20330
20384
|
const renderTravelerAddressFields = (travelerValues, namePrefix) => {
|
|
20331
|
-
const
|
|
20385
|
+
const isMainBooker = travelerValues.id === formik.values.mainBookerId;
|
|
20386
|
+
// Configured order when Tide sends one, so the configured col widths line up as intended.
|
|
20387
|
+
const fieldNames = hasConfiguredBuiltInPaxFields(paxFields)
|
|
20388
|
+
? getApplicableBuiltInPaxFields(paxFields, isMainBooker)
|
|
20389
|
+
.map((field) => field.fieldName)
|
|
20390
|
+
.filter((fieldName) => ADDRESS_FIELD_NAMES.includes(fieldName))
|
|
20391
|
+
: ADDRESS_FIELD_NAMES;
|
|
20392
|
+
const fields = fieldNames.map((fieldName) => renderBuiltInField(fieldName, travelerValues, namePrefix)).filter(Boolean);
|
|
20332
20393
|
if (!fields.length)
|
|
20333
20394
|
return null;
|
|
20334
20395
|
return React__default.createElement("div", { className: "travelers-form__custom-fields-grid" }, fields);
|
|
@@ -20371,15 +20432,18 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
|
|
|
20371
20432
|
return (React__default.createElement("div", { className: "form__row" },
|
|
20372
20433
|
React__default.createElement(LabeledInput, { hasError: hasVisibleError(`${namePrefix}.age`), extraClassName: "form__group--md-33", label: translations.TRAVELERS_FORM.AGE, required: true, name: `${namePrefix}.age`, onChange: formik.handleChange, onBlur: formik.handleBlur, value: travelerValues.age })));
|
|
20373
20434
|
}
|
|
20374
|
-
// 'birthDate' (the legacy formFields type string) is normalized to the paxFields catalog's
|
|
20375
|
-
// 'dateOfBirth' fieldName for config lookups.
|
|
20376
|
-
const baseFieldNames = (travellersSettings?.formFields?.length ? travellersSettings.formFields.map((field) => field.type) : ['gender', 'firstName', 'lastName', 'dateOfBirth']).map((fieldName) => (fieldName === 'birthDate' ? 'dateOfBirth' : fieldName));
|
|
20377
20435
|
const isMainBooker = travelerValues.id === formik.values.mainBookerId;
|
|
20378
|
-
|
|
20379
|
-
|
|
20380
|
-
|
|
20436
|
+
// With a Tide field configuration, exactly the configured fields render, in their configured order.
|
|
20437
|
+
// Without one, fall back to formFields ('birthDate', the legacy type string, maps to 'dateOfBirth').
|
|
20438
|
+
const fieldNames = hasConfiguredBuiltInPaxFields(paxFields)
|
|
20439
|
+
? getApplicableBuiltInPaxFields(paxFields, isMainBooker)
|
|
20440
|
+
.map((field) => field.fieldName)
|
|
20441
|
+
.filter((fieldName) => !ADDRESS_FIELD_NAMES.includes(fieldName))
|
|
20442
|
+
: (travellersSettings?.formFields?.length
|
|
20443
|
+
? travellersSettings.formFields.map((field) => field.type)
|
|
20444
|
+
: ['gender', 'firstName', 'lastName', 'dateOfBirth']).map((fieldName) => (fieldName === 'birthDate' ? 'dateOfBirth' : fieldName));
|
|
20381
20445
|
return (React__default.createElement(React__default.Fragment, null,
|
|
20382
|
-
React__default.createElement("div", { className: "travelers-form__custom-fields-grid" },
|
|
20446
|
+
React__default.createElement("div", { className: "travelers-form__custom-fields-grid" }, fieldNames.map((fieldName) => renderBuiltInField(fieldName, travelerValues, namePrefix))),
|
|
20383
20447
|
renderTravelerAddressFields(travelerValues, namePrefix),
|
|
20384
20448
|
renderCustomFields(travelerValues, namePrefix),
|
|
20385
20449
|
travelersFirstStep && isAdult && formik.values.rooms[roomIndex].adults.length > 1 && (React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleRemoveTraveler(roomIndex, travelerIndex) }, translations.TRAVELERS_FORM.REMOVE_TRAVELER))));
|
|
@@ -20417,7 +20481,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
|
|
|
20417
20481
|
renderTravelerFields(travelerValues, `rooms[${roomIndex}].children[${index}]`, false, roomIndex, index)))),
|
|
20418
20482
|
!useCompactForm && travelersFirstStep && (React__default.createElement("div", { className: "form__region" },
|
|
20419
20483
|
React__default.createElement("button", { type: "button", className: "cta cta--select", onClick: () => handleAddTraveler(roomIndex) }, translations.TRAVELERS_FORM.ADD_TRAVELER)))))),
|
|
20420
|
-
|
|
20484
|
+
useCompactForm || hasConfiguredMainBookerFields(paxFields) ? null : bookingType !== 'b2b' || travellersSettings?.mainBookerFormFields?.length ? (React__default.createElement("div", { className: "form__region" },
|
|
20421
20485
|
React__default.createElement("div", { className: "form__region-header" },
|
|
20422
20486
|
React__default.createElement("h5", { className: "form__region-heading" }, translations.TRAVELERS_FORM.MAIN_BOOKER),
|
|
20423
20487
|
React__default.createElement("p", { className: "form__region-label" }, compact([compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '), formatBirthDate$1(mainBooker?.birthDate)]).join(', '))),
|
|
@@ -20436,9 +20500,9 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
|
|
|
20436
20500
|
React__default.createElement("div", { className: "form__row" },
|
|
20437
20501
|
React__default.createElement(LabeledInput, { hasError: hasVisibleError('phone'), extraClassName: "form__group--md-33", label: translations.TRAVELERS_FORM.PHONE, required: true, name: "phone", onChange: formik.handleChange, onBlur: formik.handleBlur, value: formik.values.phone }),
|
|
20438
20502
|
React__default.createElement(LabeledInput, { type: "email", hasError: hasVisibleError('email'), extraClassName: "form__group--md-33", label: translations.TRAVELERS_FORM.EMAIL, required: true, name: "email", onChange: formik.handleChange, onBlur: formik.handleBlur, value: formik.values.email }),
|
|
20439
|
-
React__default.createElement(LabeledInput, { type: "email", hasError: hasVisibleError('emailConfirmation'), extraClassName: "form__group--md-33", label: translations.TRAVELERS_FORM.REPEAT_EMAIL, required: true, name: "emailConfirmation", onChange: formik.handleChange, onBlur: formik.handleBlur, value: formik.values.emailConfirmation })))))) :
|
|
20503
|
+
React__default.createElement(LabeledInput, { type: "email", hasError: hasVisibleError('emailConfirmation'), extraClassName: "form__group--md-33", label: translations.TRAVELERS_FORM.REPEAT_EMAIL, required: true, name: "emailConfirmation", onChange: formik.handleChange, onBlur: formik.handleBlur, value: formik.values.emailConfirmation })))))) : (React__default.createElement("div", { className: "form__region" },
|
|
20440
20504
|
React__default.createElement("div", { className: "form__row" },
|
|
20441
|
-
React__default.createElement(LabeledInput, { hasError: hasVisibleError('phone'), extraClassName: "form__group--md-33", label: translations.TRAVELERS_FORM.PHONE, required: true, name: "phone", onChange: formik.handleChange, onBlur: formik.handleBlur, value: formik.values.phone }))))
|
|
20505
|
+
React__default.createElement(LabeledInput, { hasError: hasVisibleError('phone'), extraClassName: "form__group--md-33", label: translations.TRAVELERS_FORM.PHONE, required: true, name: "phone", onChange: formik.handleChange, onBlur: formik.handleBlur, value: formik.values.phone })))),
|
|
20442
20506
|
!useCompactForm && showAgentSelection && (React__default.createElement("div", { className: "form__region" },
|
|
20443
20507
|
React__default.createElement("div", { className: "form__region-header" },
|
|
20444
20508
|
React__default.createElement("h5", { className: "form__region-heading" }, translations.TRAVELERS_FORM.BOOK_WITH_AGENT),
|
|
@@ -21049,8 +21113,9 @@ const selectBookingPackagePax = createSelector(selectTravelersFormValues, (formV
|
|
|
21049
21113
|
formValues?.rooms.forEach((r) => r.adults.forEach((x) => {
|
|
21050
21114
|
const adultPax = buildPax(x, formValues?.mainBookerId);
|
|
21051
21115
|
if (adultPax.isMainBooker) {
|
|
21052
|
-
|
|
21053
|
-
adultPax.
|
|
21116
|
+
// Without the main booker block (Tide configures main-booker fields), keep the traveler's own details.
|
|
21117
|
+
adultPax.mobilePhone = formValues?.phone || adultPax.mobilePhone;
|
|
21118
|
+
adultPax.email = formValues?.email || adultPax.email;
|
|
21054
21119
|
}
|
|
21055
21120
|
pax.push(adultPax);
|
|
21056
21121
|
}));
|
|
@@ -21074,8 +21139,8 @@ const selectBookingAddress = createSelector(selectTravelersFormValues, selectBoo
|
|
|
21074
21139
|
postalCode: mainBookerAddress?.postalCode || formValues?.zipCode,
|
|
21075
21140
|
location: mainBookerAddress?.location || formValues?.place,
|
|
21076
21141
|
country: mainBookerAddress?.country || formValues?.country,
|
|
21077
|
-
mobilePhone:
|
|
21078
|
-
email:
|
|
21142
|
+
mobilePhone: mainBooker.mobilePhone,
|
|
21143
|
+
email: mainBooker.email
|
|
21079
21144
|
};
|
|
21080
21145
|
});
|
|
21081
21146
|
const selectSubFormAnswers = (state) => state.booking.subFormAnswers;
|
|
@@ -26212,6 +26277,10 @@ const Summary = () => {
|
|
|
26212
26277
|
React__default.createElement("button", { title: settings.isOffer ? translations.STEPS.SUBMIT_OFFER : translations.STEPS.SUBMIT_BOOKING, className: buildClassName(['cta', !userValidated && 'cta--disabled']), disabled: !userValidated, name: "default" }, settings.isOffer ? translations.STEPS.SUBMIT_OFFER : translations.STEPS.SUBMIT_BOOKING))))));
|
|
26213
26278
|
};
|
|
26214
26279
|
|
|
26280
|
+
// lodash isEmpty is true for every number, which would flag a filled-in preferredLanguageId as missing.
|
|
26281
|
+
function isBlank(value) {
|
|
26282
|
+
return value === null || value === undefined || (typeof value === 'string' && value.trim() === '');
|
|
26283
|
+
}
|
|
26215
26284
|
function isValidEmail(email) {
|
|
26216
26285
|
return !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,24}$/i.test(email);
|
|
26217
26286
|
}
|
|
@@ -26235,9 +26304,14 @@ function getAge(birthDateText, startDateText) {
|
|
|
26235
26304
|
}
|
|
26236
26305
|
const validateForm = (values, agentRequired, bookingType, translations, formFields, mainBookerFormFields, maxChildAge = CHILD_MAX_AGE, maxBabyAge = BABY_MAX_AGE, paxFields = []) => {
|
|
26237
26306
|
const errors = {};
|
|
26307
|
+
const usesFieldConfiguration = hasConfiguredBuiltInPaxFields(paxFields);
|
|
26308
|
+
const builtInFieldMeta = getBuiltInFieldControlMeta(translations);
|
|
26238
26309
|
const isFormFieldPresent = (type) => {
|
|
26239
26310
|
return formFields?.some((f) => f.type === type) ?? true;
|
|
26240
26311
|
};
|
|
26312
|
+
// The 4 core fields: with a Tide field configuration they're required only when configured so for the
|
|
26313
|
+
// traveler's role (main booker / other pax); without one, whenever formFields lists them (legacy behavior).
|
|
26314
|
+
const isCoreFieldRequired = (traveler, legacyType, fieldName = legacyType) => usesFieldConfiguration ? !!getBuiltInPaxField(paxFields, fieldName, traveler.id === values.mainBookerId)?.isRequired : isFormFieldPresent(legacyType);
|
|
26241
26315
|
const isMainBookerFormFieldPresent = (type) => {
|
|
26242
26316
|
return mainBookerFormFields?.some((f) => f.type === type) ?? true;
|
|
26243
26317
|
};
|
|
@@ -26289,16 +26363,16 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
|
|
|
26289
26363
|
const fieldPath = BUILT_IN_FIELD_PATHS[field.fieldName];
|
|
26290
26364
|
if (!fieldPath)
|
|
26291
26365
|
return;
|
|
26292
|
-
if (
|
|
26293
|
-
set(errors, `${path}.${fieldPath}`, formatTravelerField(room, pax, field.fieldName));
|
|
26366
|
+
if (isBlank(get(traveler, fieldPath))) {
|
|
26367
|
+
set(errors, `${path}.${fieldPath}`, formatTravelerField(room, pax, builtInFieldMeta[field.fieldName]?.label ?? field.fieldName));
|
|
26294
26368
|
}
|
|
26295
26369
|
});
|
|
26296
26370
|
};
|
|
26297
26371
|
values.rooms.forEach((r, rIndex) => r.adults.forEach((adult, index) => {
|
|
26298
|
-
if (isEmpty$1(adult.gender) &&
|
|
26372
|
+
if (isEmpty$1(adult.gender) && isCoreFieldRequired(adult, 'gender')) {
|
|
26299
26373
|
set(errors, `rooms[${rIndex}].adults[${index}].gender`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.GENDER));
|
|
26300
26374
|
}
|
|
26301
|
-
if (
|
|
26375
|
+
if (isCoreFieldRequired(adult, 'firstName') || !isEmpty$1(adult.firstName)) {
|
|
26302
26376
|
if (isEmpty$1(adult.firstName)) {
|
|
26303
26377
|
set(errors, `rooms[${rIndex}].adults[${index}].firstName`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.FIRST_NAME));
|
|
26304
26378
|
}
|
|
@@ -26306,7 +26380,7 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
|
|
|
26306
26380
|
set(errors, `rooms[${rIndex}].adults[${index}].firstName`, formatTravelerFieldInvalidCharacters(rIndex + 1, index + 1, translations.TRAVELERS_FORM.FIRST_NAME));
|
|
26307
26381
|
}
|
|
26308
26382
|
}
|
|
26309
|
-
if (
|
|
26383
|
+
if (isCoreFieldRequired(adult, 'lastName') || !isEmpty$1(adult.lastName)) {
|
|
26310
26384
|
if (isEmpty$1(adult.lastName)) {
|
|
26311
26385
|
set(errors, `rooms[${rIndex}].adults[${index}].lastName`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.LAST_NAME));
|
|
26312
26386
|
}
|
|
@@ -26314,7 +26388,7 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
|
|
|
26314
26388
|
set(errors, `rooms[${rIndex}].adults[${index}].lastName`, formatTravelerFieldInvalidCharacters(rIndex + 1, index + 1, translations.TRAVELERS_FORM.LAST_NAME));
|
|
26315
26389
|
}
|
|
26316
26390
|
}
|
|
26317
|
-
if (
|
|
26391
|
+
if (isCoreFieldRequired(adult, 'birthDate', 'dateOfBirth') || !isEmpty$1(adult.birthDate)) {
|
|
26318
26392
|
if (isEmpty$1(adult.birthDate) || !isValidBirthDate(adult.birthDate)) {
|
|
26319
26393
|
set(errors, `rooms[${rIndex}].adults[${index}].birthDate`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
|
|
26320
26394
|
}
|
|
@@ -26330,10 +26404,10 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
|
|
|
26330
26404
|
validateRequiredBuiltInFields(adult, `rooms[${rIndex}].adults[${index}]`, rIndex + 1, index + 1);
|
|
26331
26405
|
}));
|
|
26332
26406
|
values.rooms.forEach((r, rIndex) => r.children.forEach((child, index) => {
|
|
26333
|
-
if (isEmpty$1(child.gender) &&
|
|
26407
|
+
if (isEmpty$1(child.gender) && isCoreFieldRequired(child, 'gender')) {
|
|
26334
26408
|
set(errors, `rooms[${rIndex}].children[${index}].gender`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.GENDER));
|
|
26335
26409
|
}
|
|
26336
|
-
if (
|
|
26410
|
+
if (isCoreFieldRequired(child, 'firstName') || !isEmpty$1(child.firstName)) {
|
|
26337
26411
|
if (isEmpty$1(child.firstName)) {
|
|
26338
26412
|
set(errors, `rooms[${rIndex}].children[${index}].firstName`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.FIRST_NAME));
|
|
26339
26413
|
}
|
|
@@ -26341,7 +26415,7 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
|
|
|
26341
26415
|
set(errors, `rooms[${rIndex}].children[${index}].firstName`, formatTravelerFieldInvalidCharacters(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.FIRST_NAME));
|
|
26342
26416
|
}
|
|
26343
26417
|
}
|
|
26344
|
-
if (
|
|
26418
|
+
if (isCoreFieldRequired(child, 'lastName') || !isEmpty$1(child.lastName)) {
|
|
26345
26419
|
if (isEmpty$1(child.lastName)) {
|
|
26346
26420
|
set(errors, `rooms[${rIndex}].children[${index}].lastName`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.LAST_NAME));
|
|
26347
26421
|
}
|
|
@@ -26349,7 +26423,7 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
|
|
|
26349
26423
|
set(errors, `rooms[${rIndex}].children[${index}].lastName`, formatTravelerFieldInvalidCharacters(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.LAST_NAME));
|
|
26350
26424
|
}
|
|
26351
26425
|
}
|
|
26352
|
-
if (
|
|
26426
|
+
if (isCoreFieldRequired(child, 'birthDate', 'dateOfBirth') || !isEmpty$1(child.birthDate)) {
|
|
26353
26427
|
if (isEmpty$1(child.birthDate) || !isValidBirthDate(child.birthDate)) {
|
|
26354
26428
|
set(errors, `rooms[${rIndex}].children[${index}].birthDate`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
|
|
26355
26429
|
}
|
|
@@ -26375,7 +26449,8 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
|
|
|
26375
26449
|
if (values.mainBookerId < 0) {
|
|
26376
26450
|
errors.mainBookerId = translations.TRAVELERS_FORM.VALIDATION.NO_MAIN_BOOKER_SELECTED;
|
|
26377
26451
|
}
|
|
26378
|
-
if (
|
|
26452
|
+
if (hasConfiguredMainBookerFields(paxFields)) ;
|
|
26453
|
+
else if (bookingType != 'b2b' || mainBookerFormFields?.length) {
|
|
26379
26454
|
if (isEmpty$1(values.street) && isMainBookerFormFieldPresent('street')) {
|
|
26380
26455
|
errors.street = format$1(translations.TRAVELERS_FORM.VALIDATION.MAIN_BOOKER_FIELD, [translations.TRAVELERS_FORM.STREET]);
|
|
26381
26456
|
}
|
|
@@ -29154,6 +29229,7 @@ const initialState$1 = {
|
|
|
29154
29229
|
bookingNumber: undefined,
|
|
29155
29230
|
trajectNodeAvailability: {},
|
|
29156
29231
|
trajectFlightAvailability: {},
|
|
29232
|
+
trajectTransferAvailability: {},
|
|
29157
29233
|
trajectDaySuggestions: {},
|
|
29158
29234
|
trajectEditLineGuid: null,
|
|
29159
29235
|
trajectActivitiesDayOrder: null,
|
|
@@ -29366,6 +29442,12 @@ const searchResultsSlice = createSlice({
|
|
|
29366
29442
|
node.selectedCode = code;
|
|
29367
29443
|
node.status = 'resolved';
|
|
29368
29444
|
},
|
|
29445
|
+
clearTrajectNodeSelection(state, action) {
|
|
29446
|
+
const node = state.trajectNodeAvailability[action.payload];
|
|
29447
|
+
if (!node)
|
|
29448
|
+
return;
|
|
29449
|
+
node.selectedCode = null;
|
|
29450
|
+
},
|
|
29369
29451
|
updateEditableEntryLine(state, action) {
|
|
29370
29452
|
if (!state.editablePackagingEntry)
|
|
29371
29453
|
return;
|
|
@@ -29379,6 +29461,32 @@ const searchResultsSlice = createSlice({
|
|
|
29379
29461
|
return;
|
|
29380
29462
|
state.editablePackagingEntry.lines = [...(state.editablePackagingEntry.lines ?? []), ...action.payload];
|
|
29381
29463
|
},
|
|
29464
|
+
upsertEditableEntryLines(state, action) {
|
|
29465
|
+
if (!state.editablePackagingEntry)
|
|
29466
|
+
return;
|
|
29467
|
+
const lines = [...(state.editablePackagingEntry.lines ?? [])];
|
|
29468
|
+
action.payload.forEach((line) => {
|
|
29469
|
+
const index = lines.findIndex((x) => x.guid === line.guid);
|
|
29470
|
+
if (index === -1)
|
|
29471
|
+
lines.push(line);
|
|
29472
|
+
else
|
|
29473
|
+
lines[index] = line;
|
|
29474
|
+
});
|
|
29475
|
+
state.editablePackagingEntry.lines = lines;
|
|
29476
|
+
const restored = new Set(action.payload.map((line) => line.trajectNodeId).filter((x) => x != null));
|
|
29477
|
+
if (restored.size && state.editablePackagingEntry.removedTrajectNodeIds?.length) {
|
|
29478
|
+
state.editablePackagingEntry.removedTrajectNodeIds = state.editablePackagingEntry.removedTrajectNodeIds.filter((x) => !restored.has(x));
|
|
29479
|
+
}
|
|
29480
|
+
},
|
|
29481
|
+
removeTrajectNode(state, action) {
|
|
29482
|
+
if (!state.editablePackagingEntry)
|
|
29483
|
+
return;
|
|
29484
|
+
const guids = new Set(action.payload.lineGuids);
|
|
29485
|
+
state.editablePackagingEntry.lines = (state.editablePackagingEntry.lines ?? []).filter((x) => !guids.has(x.guid));
|
|
29486
|
+
const removed = state.editablePackagingEntry.removedTrajectNodeIds ?? [];
|
|
29487
|
+
if (!removed.includes(action.payload.nodeId))
|
|
29488
|
+
state.editablePackagingEntry.removedTrajectNodeIds = [...removed, action.payload.nodeId];
|
|
29489
|
+
},
|
|
29382
29490
|
removeEditableEntryLines(state, action) {
|
|
29383
29491
|
if (!state.editablePackagingEntry)
|
|
29384
29492
|
return;
|
|
@@ -29410,6 +29518,19 @@ const searchResultsSlice = createSlice({
|
|
|
29410
29518
|
flight.selectedGuid = action.payload.guid;
|
|
29411
29519
|
flight.status = 'resolved';
|
|
29412
29520
|
},
|
|
29521
|
+
clearTrajectFlightSelection(state, action) {
|
|
29522
|
+
const flight = state.trajectFlightAvailability[action.payload];
|
|
29523
|
+
if (!flight)
|
|
29524
|
+
return;
|
|
29525
|
+
flight.selectedGuid = null;
|
|
29526
|
+
},
|
|
29527
|
+
setTrajectTransferLoading(state, action) {
|
|
29528
|
+
state.trajectTransferAvailability[action.payload] = { nodeId: action.payload, status: 'loading', result: null };
|
|
29529
|
+
},
|
|
29530
|
+
setTrajectTransferAvailability(state, action) {
|
|
29531
|
+
const { nodeId, result } = action.payload;
|
|
29532
|
+
state.trajectTransferAvailability[nodeId] = { nodeId, status: result ? 'resolved' : 'unavailable', result };
|
|
29533
|
+
},
|
|
29413
29534
|
setTrajectDaySuggestionsLoading(state, action) {
|
|
29414
29535
|
const dayOrder = action.payload;
|
|
29415
29536
|
state.trajectDaySuggestions[dayOrder] = {
|
|
@@ -29446,6 +29567,7 @@ const searchResultsSlice = createSlice({
|
|
|
29446
29567
|
resetTrajectAvailability(state) {
|
|
29447
29568
|
state.trajectNodeAvailability = {};
|
|
29448
29569
|
state.trajectFlightAvailability = {};
|
|
29570
|
+
state.trajectTransferAvailability = {};
|
|
29449
29571
|
state.trajectDaySuggestions = {};
|
|
29450
29572
|
state.trajectEditLineGuid = null;
|
|
29451
29573
|
state.trajectActivitiesDayOrder = null;
|
|
@@ -29454,7 +29576,7 @@ const searchResultsSlice = createSlice({
|
|
|
29454
29576
|
}
|
|
29455
29577
|
}
|
|
29456
29578
|
});
|
|
29457
|
-
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, setTrajectResolving, setTrajectItineraryView, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, setTrajectEditLineGuid, setTrajectActivitiesDayOrder, setTrajectDaySuggestionsLoading, setTrajectDaySuggestions, addEditableEntryLines, updateTrajectNodeResult, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
|
|
29579
|
+
const { setResults, setFilteredResults, setSelectedSearchResult, setPackagingAccoResults, setFilteredPackagingAccoResults, setFilteredPackagingFlightResults, setPackagingAccoSearchDetails, setSelectedPackagingAccoResult, setPackagingFlightResults, setSelectedPackagingFlight, setSelectedFlight, setSelectedFlightDetails, setBookingPackageDetails, selectFlight, setIsLoading, setFlightsLoading, setInitialFilters, setFilters, resetFilters, setInitialFlightFilters, setFlightFilters, resetFlightFilters, setSortType, setFlightSortType, setActiveTab, setCurrentPage, resetSearchState, setFlyInIsOpen, setEditablePackagingEntry, setTransactionId, setFlyInType, setPriceDetails, setItinerary, setSelectedOutwardKey, setSelectedReturnKey, resetFlightSelection, setExcursionSearchParams, setSelectedExcursionSearchResult, confirmExcursionForDay, removeConfirmedExcursionForDay, clearConfirmedExcursionsForDay, setBookPackagingEntry, setCurrentStep, setBookingNumber, setTrajectNodeLoading, setTrajectNodeAvailability, setTrajectNodeSelection, clearTrajectNodeSelection, setTrajectResolving, setTrajectItineraryView, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, clearTrajectFlightSelection, setTrajectTransferLoading, setTrajectTransferAvailability, setTrajectEditLineGuid, setTrajectActivitiesDayOrder, setTrajectDaySuggestionsLoading, setTrajectDaySuggestions, addEditableEntryLines, upsertEditableEntryLines, removeTrajectNode, updateTrajectNodeResult, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
|
|
29458
29580
|
var searchResultsReducer = searchResultsSlice.reducer;
|
|
29459
29581
|
|
|
29460
29582
|
const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
|
|
@@ -29669,7 +29791,7 @@ const numberOfNights = (segment) => {
|
|
|
29669
29791
|
const CHARTER_SERVICE_TYPE = 10;
|
|
29670
29792
|
const CRUISE_SERVICE_TYPE = 11;
|
|
29671
29793
|
const FERRY_SERVICE_TYPE = 12;
|
|
29672
|
-
const TRANSFER_SERVICE_TYPE = 13;
|
|
29794
|
+
const TRANSFER_SERVICE_TYPE$1 = 13;
|
|
29673
29795
|
const BUS_SERVICE_TYPE = 14;
|
|
29674
29796
|
const CAR_SERVICE_TYPE = 17;
|
|
29675
29797
|
const CAMPER_SERVICE_TYPE = 18;
|
|
@@ -29701,7 +29823,7 @@ const getServiceTypeIcon = (serviceType) => {
|
|
|
29701
29823
|
return 'ui-bus';
|
|
29702
29824
|
case TRAIN_SERVICE_TYPE:
|
|
29703
29825
|
return 'ui-train';
|
|
29704
|
-
case TRANSFER_SERVICE_TYPE:
|
|
29826
|
+
case TRANSFER_SERVICE_TYPE$1:
|
|
29705
29827
|
return 'ui-transfer';
|
|
29706
29828
|
case EVENT_SERVICE_TYPE:
|
|
29707
29829
|
return 'ui-ticket';
|
|
@@ -29837,6 +29959,81 @@ const getTrajectNodesWithLines = (trajectEntry) => {
|
|
|
29837
29959
|
})
|
|
29838
29960
|
.filter((x) => !!x.line);
|
|
29839
29961
|
};
|
|
29962
|
+
// Qite.Offer.Enums.ServiceType.Transfer: a traject's connecting node is booked as one.
|
|
29963
|
+
const TRANSFER_SERVICE_TYPE = 13;
|
|
29964
|
+
// Qite.Offer.Enums.RequirementType. A Mandatory (0) stop, and one RequiredInGroup (3) — one of the node's
|
|
29965
|
+
// alternatives has to be booked — always keep a selection; only these two may be taken off the trip.
|
|
29966
|
+
const OPTIONAL_REQUIREMENT_TYPE = 1;
|
|
29967
|
+
const OPTIONAL_IN_GROUP_REQUIREMENT_TYPE = 2;
|
|
29968
|
+
const canRemoveTrajectNode = (node) => node.requirementType === OPTIONAL_REQUIREMENT_TYPE || node.requirementType === OPTIONAL_IN_GROUP_REQUIREMENT_TYPE;
|
|
29969
|
+
const getDroppedTrajectNodeIds = (trajectEntry, entry) => {
|
|
29970
|
+
const dropped = new Set();
|
|
29971
|
+
if (!entry || !!trajectEntry.entry.magicLink)
|
|
29972
|
+
return dropped;
|
|
29973
|
+
const removed = new Set(entry.removedTrajectNodeIds ?? []);
|
|
29974
|
+
if (!removed.size)
|
|
29975
|
+
return dropped;
|
|
29976
|
+
const byDay = new Map();
|
|
29977
|
+
[...(trajectEntry.nodes ?? [])]
|
|
29978
|
+
.sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
|
|
29979
|
+
.forEach((node) => byDay.set(node.dayOrder, [...(byDay.get(node.dayOrder) ?? []), node]));
|
|
29980
|
+
byDay.forEach((nodes) => nodes.forEach((node, index) => {
|
|
29981
|
+
if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE || !canRemoveTrajectNode(node) || !removed.has(node.nodeId))
|
|
29982
|
+
return;
|
|
29983
|
+
dropped.add(node.nodeId);
|
|
29984
|
+
[nodes[index - 1], nodes[index + 1]]
|
|
29985
|
+
.filter((x) => !!x && x.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE)
|
|
29986
|
+
.forEach((x) => dropped.add(x.nodeId));
|
|
29987
|
+
}));
|
|
29988
|
+
return dropped;
|
|
29989
|
+
};
|
|
29990
|
+
const isTrajectTransferNode = (node) => node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE && !!node.pickUp && !!node.dropOff;
|
|
29991
|
+
const buildTrajectTransferRequest = (node, context) => ({
|
|
29992
|
+
transactionId: context.transactionId,
|
|
29993
|
+
officeId: context.officeId,
|
|
29994
|
+
agentId: context.agentId ?? null,
|
|
29995
|
+
catalogueId: context.catalogueId,
|
|
29996
|
+
searchConfigurationId: context.searchConfigurationId,
|
|
29997
|
+
language: context.language,
|
|
29998
|
+
date: node.from,
|
|
29999
|
+
pickUp: node.pickUp,
|
|
30000
|
+
dropOff: node.dropOff,
|
|
30001
|
+
travellers: context.rooms.flatMap((room) => room.travellers)
|
|
30002
|
+
});
|
|
30003
|
+
const buildTrajectTransferLine = (node, result) => {
|
|
30004
|
+
const option = result.rooms?.[0]?.options?.find((x) => x.isSelected) ?? result.rooms?.[0]?.options?.[0];
|
|
30005
|
+
if (!option?.guid)
|
|
30006
|
+
return null;
|
|
30007
|
+
return {
|
|
30008
|
+
guid: option.guid,
|
|
30009
|
+
parentGuid: null,
|
|
30010
|
+
order: 0,
|
|
30011
|
+
trajectNodeId: node.nodeId,
|
|
30012
|
+
isChanged: true,
|
|
30013
|
+
from: result.fromDate ?? node.from,
|
|
30014
|
+
to: result.toDate ?? node.to,
|
|
30015
|
+
moment: node.moment ?? '',
|
|
30016
|
+
serviceType: TRANSFER_SERVICE_TYPE,
|
|
30017
|
+
productName: result.name,
|
|
30018
|
+
productCode: result.code,
|
|
30019
|
+
accommodationName: option.accommodationName,
|
|
30020
|
+
accommodationCode: option.accommodationCode,
|
|
30021
|
+
regimeName: option.regimeName,
|
|
30022
|
+
regimeCode: option.regimeCode,
|
|
30023
|
+
vendor: result.vendorType ?? null,
|
|
30024
|
+
externalVendorId: result.vendorId ?? null,
|
|
30025
|
+
groupKey: '',
|
|
30026
|
+
isActive: true,
|
|
30027
|
+
country: result.countryId ? { id: result.countryId, name: result.countryName, localizations: [] } : null,
|
|
30028
|
+
region: result.regionId ? { id: result.regionId, name: result.regionName, localizations: [] } : null,
|
|
30029
|
+
oord: result.oordId ? { id: result.oordId, name: result.oordName, localizations: [] } : null,
|
|
30030
|
+
location: result.locationId ? { id: result.locationId, name: result.locationName, localizations: [] } : null,
|
|
30031
|
+
longitude: result.longitude ?? null,
|
|
30032
|
+
latitude: result.latitude ?? null,
|
|
30033
|
+
pax: (option.paxIds ?? []).map((paxId, paxIndex) => ({ paxId, room: 0, order: paxIndex })),
|
|
30034
|
+
flightInformation: null
|
|
30035
|
+
};
|
|
30036
|
+
};
|
|
29840
30037
|
const getCoordinates = (line, node, result) => {
|
|
29841
30038
|
const sources = [
|
|
29842
30039
|
...(result ? [result] : []),
|
|
@@ -30264,9 +30461,15 @@ const nightsOf = (node, line) => differenceInCalendarDays(new Date(line?.to ?? n
|
|
|
30264
30461
|
const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
|
|
30265
30462
|
const timeline = useMemo(() => {
|
|
30266
30463
|
const linesByGuid = new Map((packagingEntry.lines ?? []).map((line) => [line.guid, line]));
|
|
30464
|
+
const dropped = getDroppedTrajectNodeIds(trajectEntry, packagingEntry);
|
|
30267
30465
|
const nodes = [...(trajectEntry.nodes ?? [])]
|
|
30466
|
+
.filter((node) => !dropped.has(node.nodeId))
|
|
30268
30467
|
.sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
|
|
30269
30468
|
.map((node) => {
|
|
30469
|
+
// A connecting node has no line of its own; the transfer found for it carries its node id.
|
|
30470
|
+
if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
|
|
30471
|
+
return { node, lines: (packagingEntry.lines ?? []).filter((line) => line.trajectNodeId === node.nodeId) };
|
|
30472
|
+
}
|
|
30270
30473
|
const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
|
|
30271
30474
|
const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
|
|
30272
30475
|
const groupKey = lines.length ? getLineGroupKey(lines[0]) : '';
|
|
@@ -30336,10 +30539,11 @@ const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
|
|
|
30336
30539
|
React__default.createElement("p", { className: "search__filter__itinerary__transport-date-date" },
|
|
30337
30540
|
React__default.createElement("strong", null, format$2(new Date(value), 'd'))),
|
|
30338
30541
|
React__default.createElement("p", null, format$2(new Date(value), 'MMM'))));
|
|
30339
|
-
const renderConnectingNode = ({ node }) => {
|
|
30542
|
+
const renderConnectingNode = ({ node, lines }) => {
|
|
30340
30543
|
const distance = node.distanceInKm ? `${Math.round(node.distanceInKm)} km` : '';
|
|
30341
30544
|
// How long the hop takes and how far it is, the way the traject editor puts them.
|
|
30342
30545
|
const journey = [getTrajectNodeDuration(node), distance].filter(Boolean).join(' · ');
|
|
30546
|
+
const title = lines[0]?.productName || node.name;
|
|
30343
30547
|
return (React__default.createElement("div", { className: "search__filter__itinerary__transport", key: `connect-${node.nodeId}` },
|
|
30344
30548
|
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline" },
|
|
30345
30549
|
React__default.createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
|
|
@@ -30348,8 +30552,8 @@ const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
|
|
|
30348
30552
|
React__default.createElement("div", { className: badgeClass('transport', getTrajectNodeBadgeModifier(node)) },
|
|
30349
30553
|
React__default.createElement(Icon, { name: getTrajectNodeIcon(node), height: 15 })),
|
|
30350
30554
|
React__default.createElement("div", { className: "search__filter__itinerary__transport-details" },
|
|
30351
|
-
React__default.createElement("h6", null,
|
|
30352
|
-
!!
|
|
30555
|
+
React__default.createElement("h6", null, title || journey),
|
|
30556
|
+
!!title && !!journey && React__default.createElement("p", { className: "search__filter__itinerary__transport-details-time" }, journey)))));
|
|
30353
30557
|
};
|
|
30354
30558
|
const renderOvernight = (item, index) => {
|
|
30355
30559
|
const { dayOrder } = item.node;
|
|
@@ -33600,7 +33804,7 @@ const TrajectResultsFlyIn = ({ isLoading }) => {
|
|
|
33600
33804
|
if (!result)
|
|
33601
33805
|
return;
|
|
33602
33806
|
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
|
|
33603
|
-
buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])
|
|
33807
|
+
dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
|
|
33604
33808
|
dispatch(setTrajectEditLineGuid(null));
|
|
33605
33809
|
dispatch(setFlyInIsOpen(false));
|
|
33606
33810
|
};
|
|
@@ -33645,7 +33849,7 @@ const TrajectFlightsFlyIn = ({ isLoading }) => {
|
|
|
33645
33849
|
if (!flight)
|
|
33646
33850
|
return;
|
|
33647
33851
|
dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
|
|
33648
|
-
dispatch(
|
|
33852
|
+
dispatch(upsertEditableEntryLines([applyFlightToLine(item.line, flight)]));
|
|
33649
33853
|
dispatch(setTrajectEditLineGuid(null));
|
|
33650
33854
|
dispatch(setFlyInIsOpen(false));
|
|
33651
33855
|
};
|
|
@@ -33708,10 +33912,16 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
|
|
|
33708
33912
|
if (!item)
|
|
33709
33913
|
return;
|
|
33710
33914
|
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code: result.code }));
|
|
33711
|
-
buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])
|
|
33915
|
+
dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
|
|
33712
33916
|
dispatch(setTrajectActivitiesDayOrder(null));
|
|
33713
33917
|
dispatch(setFlyInIsOpen(false));
|
|
33714
33918
|
};
|
|
33919
|
+
const handleRemove = ({ item }) => {
|
|
33920
|
+
if (!item)
|
|
33921
|
+
return;
|
|
33922
|
+
dispatch(clearTrajectNodeSelection(item.line.guid));
|
|
33923
|
+
dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
|
|
33924
|
+
};
|
|
33715
33925
|
if (isLoading || suggestions?.status === 'loading') {
|
|
33716
33926
|
return React__default.createElement(React__default.Fragment, null, context.customSpinner ?? React__default.createElement(Spinner, { label: translations.SRP.LOADING_ACTIVITIES }));
|
|
33717
33927
|
}
|
|
@@ -33725,7 +33935,8 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
|
|
|
33725
33935
|
const { result, item } = card;
|
|
33726
33936
|
const isSelected = item ? trajectNodeAvailability[item.line.guid]?.selectedCode === result.code : getAddedLines(result).length > 0;
|
|
33727
33937
|
const isBookedCard = !!item && bookedMode && matchesCode(result, item.node.code ?? '');
|
|
33728
|
-
|
|
33938
|
+
const canRemove = !!item && isSelected && !bookedMode && canRemoveTrajectNode(item.node);
|
|
33939
|
+
return (React__default.createElement(TrajectNodeCard, { key: `activity-${result.code}`, node: item?.node, result: result, isSelected: isSelected, languageCode: context.languageCode, translations: translations, onSelect: () => (!item ? handleToggle(result) : canRemove ? handleRemove(card) : handleSelect(card)), selectLabel: item ? undefined : translations.SRP.ADD, selectedLabel: item && !canRemove ? (isBookedCard ? translations.SHARED.BOOKED : undefined) : translations.SRP.REMOVE }));
|
|
33729
33940
|
})))));
|
|
33730
33941
|
};
|
|
33731
33942
|
|
|
@@ -36119,16 +36330,18 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
|
|
|
36119
36330
|
apiKey: context.tideConnection.apiKey
|
|
36120
36331
|
};
|
|
36121
36332
|
}, [context]);
|
|
36333
|
+
// The field configuration on the entry is leading for which traveler fields are shown and required.
|
|
36334
|
+
const paxFields = editablePackagingEntry?.paxFields ?? [];
|
|
36122
36335
|
const initialValues = useMemo(() => {
|
|
36123
36336
|
if (!editablePackagingEntry) {
|
|
36124
36337
|
return {};
|
|
36125
36338
|
}
|
|
36126
|
-
return createInitialValuesFromEditablePackagingEntry(editablePackagingEntry);
|
|
36339
|
+
return createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, undefined, paxFields);
|
|
36127
36340
|
}, [editablePackagingEntry?.transactionId]);
|
|
36128
36341
|
const formik = useFormik({
|
|
36129
36342
|
initialValues,
|
|
36130
36343
|
enableReinitialize: true,
|
|
36131
|
-
validate: (values) => validateForm(values, false, 'b2c', translations, travellersSettings.formFields, travellersSettings.mainBookerFormFields, undefined, undefined,
|
|
36344
|
+
validate: (values) => validateForm(values, false, 'b2c', translations, travellersSettings.formFields, travellersSettings.mainBookerFormFields, undefined, undefined, paxFields),
|
|
36132
36345
|
onSubmit: (values) => {
|
|
36133
36346
|
if (!editablePackagingEntry)
|
|
36134
36347
|
return;
|
|
@@ -36213,7 +36426,7 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
|
|
|
36213
36426
|
".\u00A0",
|
|
36214
36427
|
stepLabels[step])) },
|
|
36215
36428
|
isConfirmationPage && isLoading && React__default.createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING }),
|
|
36216
|
-
currentStep === 0 && (React__default.createElement(SharedTravelersForm, { formik: formik, translations: translations, travellersSettings: travellersSettings, countries: countries, travelersFirstStep: false, isUnavailable: false, useCompactForm: false, showAgentSelection: false, paxFields:
|
|
36429
|
+
currentStep === 0 && (React__default.createElement(SharedTravelersForm, { formik: formik, translations: translations, travellersSettings: travellersSettings, countries: countries, travelersFirstStep: false, isUnavailable: false, useCompactForm: false, showAgentSelection: false, paxFields: paxFields })),
|
|
36217
36430
|
currentStep === 1 && (React__default.createElement(SharedSummary, { translations: translations, travelerFormValues: formik.values, isSubmitting: isSubmitting, skipPayment: !context.generatePaymentUrl, userValidated: userValidated, remarks: remarks, enableVoucher: false, allowOption: false, isOffer: false, onUserValidatedChange: setUserValidated, onRemarksChange: setRemarks, onSubmit: handleSummarySubmit, renderOptions: () => renderEditablePackagingEntrySummaryOptions(editablePackagingEntry, priceDetails, translations, selectedPackagingAccoResult?.currencyCode), renderPreviousButton: () => (React__default.createElement("button", { type: "button", title: translations.STEPS.PREVIOUS, onClick: () => dispatch(setCurrentStep(0)), className: "cta cta--secondary" }, translations.STEPS.PREVIOUS)) })),
|
|
36218
36431
|
currentStep === 2 && (React__default.createElement(SharedConfirmation, { bookingNumber: bookingNumber ?? editablePackagingEntry?.dossierNumber ?? '', isOption: false, isOffer: false, translations: translations.CONFIRMATION })),
|
|
36219
36432
|
currentStep === 3 && React__default.createElement("div", null))),
|
|
@@ -36313,11 +36526,13 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36313
36526
|
});
|
|
36314
36527
|
return map;
|
|
36315
36528
|
}, [trajectEntry]);
|
|
36529
|
+
const droppedNodeIds = trajectEntry ? getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry) : new Set();
|
|
36530
|
+
const droppedKey = [...droppedNodeIds].sort((a, b) => a - b).join(',');
|
|
36316
36531
|
const { stayByDay, stayStartByNodeId } = useMemo(() => {
|
|
36317
36532
|
const byDay = new Map();
|
|
36318
36533
|
const byNodeId = new Map();
|
|
36319
36534
|
nodesWithLines
|
|
36320
|
-
.filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE)
|
|
36535
|
+
.filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE && !droppedNodeIds.has(x.node.nodeId))
|
|
36321
36536
|
.forEach((item) => {
|
|
36322
36537
|
const nights = Math.max(differenceInCalendarDays(new Date(item.line.to), new Date(item.line.from)), 1);
|
|
36323
36538
|
byNodeId.set(item.node.nodeId, { item, night: 0, nights });
|
|
@@ -36326,7 +36541,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36326
36541
|
}
|
|
36327
36542
|
});
|
|
36328
36543
|
return { stayByDay: byDay, stayStartByNodeId: byNodeId };
|
|
36329
|
-
}, [nodesWithLines]);
|
|
36544
|
+
}, [nodesWithLines, droppedKey]);
|
|
36330
36545
|
/** Whether every node of the trip has an answer, one way or the other. */
|
|
36331
36546
|
const tripResolved = useMemo(() => nodesWithLines
|
|
36332
36547
|
.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node))
|
|
@@ -36353,7 +36568,20 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36353
36568
|
if (!result)
|
|
36354
36569
|
return;
|
|
36355
36570
|
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
|
|
36356
|
-
buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])
|
|
36571
|
+
dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
|
|
36572
|
+
};
|
|
36573
|
+
/**
|
|
36574
|
+
* An optional stop can be taken off the trip; a mandatory one, or one of which the group needs a pick,
|
|
36575
|
+
* only swapped. A booked entry's services are never taken off here.
|
|
36576
|
+
*/
|
|
36577
|
+
const canRemove = (item) => !bookedMode && canRemoveTrajectNode(item.node);
|
|
36578
|
+
const handleRemoveNode = (item) => {
|
|
36579
|
+
dispatch(clearTrajectNodeSelection(item.line.guid));
|
|
36580
|
+
dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
|
|
36581
|
+
};
|
|
36582
|
+
const handleRemoveFlight = (item) => {
|
|
36583
|
+
dispatch(clearTrajectFlightSelection(item.line.guid));
|
|
36584
|
+
dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
|
|
36357
36585
|
};
|
|
36358
36586
|
/** The remaining legs for a flight node open in the fly-in, as the regular flight results do. */
|
|
36359
36587
|
const handleShowMoreFlights = (item) => {
|
|
@@ -36421,9 +36649,17 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36421
36649
|
// A hotel row holds the hotels configured on the node; a portal can also let it fill up with
|
|
36422
36650
|
// non-preferred hotels found in the area, the rest staying behind "show more".
|
|
36423
36651
|
const fill = showNonPreferredHotels && node.serviceType === ACCOMMODATION_SERVICE_TYPE ? others.slice(0, Math.max(MAX_HOTEL_RESULTS - 1 - configured.length, 0)) : [];
|
|
36424
|
-
const remaining = others.length - fill.length;
|
|
36425
36652
|
// No entry yet means the node's search has been queued but not dispatched — still loading.
|
|
36426
36653
|
const status = availability?.status ?? 'loading';
|
|
36654
|
+
// A stop taken off the trip keeps its results in view, to pick one back.
|
|
36655
|
+
const isRemoved = status === 'resolved' && !selected && results.length > 0;
|
|
36656
|
+
const removable = canRemove(item);
|
|
36657
|
+
const shown = selected
|
|
36658
|
+
? [selected, ...configured, ...fill]
|
|
36659
|
+
: configured.length || fill.length
|
|
36660
|
+
? [...configured, ...fill]
|
|
36661
|
+
: others.slice(0, MAX_HOTEL_RESULTS);
|
|
36662
|
+
const remaining = results.length - shown.length;
|
|
36427
36663
|
return (React__default.createElement(React__default.Fragment, { key: line.guid },
|
|
36428
36664
|
showHeader && (React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
36429
36665
|
React__default.createElement("div", { className: "search__results__label__date" },
|
|
@@ -36435,13 +36671,18 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36435
36671
|
React__default.createElement("strong", null, headerTitle || node.name))))),
|
|
36436
36672
|
status === 'loading' && React__default.createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }),
|
|
36437
36673
|
status === 'unavailable' && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
36438
|
-
status === 'resolved' && selected && (React__default.createElement(React__default.Fragment, null,
|
|
36439
|
-
availability.preferredUnavailable && node.preferredAccommodationName && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
36674
|
+
status === 'resolved' && (selected || isRemoved) && (React__default.createElement(React__default.Fragment, null,
|
|
36675
|
+
selected && availability.preferredUnavailable && node.preferredAccommodationName && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
36440
36676
|
React__default.createElement("span", null,
|
|
36441
36677
|
node.preferredAccommodationName,
|
|
36442
36678
|
" \u2014 ",
|
|
36443
36679
|
translations.SRP.NO_RESULTS))),
|
|
36444
|
-
React__default.createElement(TrajectCardRow, { selectionKey: selected
|
|
36680
|
+
React__default.createElement(TrajectCardRow, { selectionKey: selected?.code ?? '' }, shown.map((result) => {
|
|
36681
|
+
const isSelected = result.code === selected?.code;
|
|
36682
|
+
return (React__default.createElement(TrajectNodeCard, { key: result.code, node: node, result: result, isSelected: isSelected, languageCode: context.languageCode, translations: translations, onSelect: () => (isSelected && removable ? handleRemoveNode(item) : handleSelect(item, result.code)), onEditOptions: node.serviceType === ACCOMMODATION_SERVICE_TYPE ? () => handleEditOptions(item) : undefined, showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE,
|
|
36683
|
+
// selectedLabel={bookedMode && matchesCode(result, node.code ?? '') ? translations.SHARED.BOOKED : undefined}
|
|
36684
|
+
selectedLabel: removable ? translations.SRP.REMOVE : undefined }));
|
|
36685
|
+
})),
|
|
36445
36686
|
remaining > 0 && !hideShowMoreResults && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
36446
36687
|
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMore(item) },
|
|
36447
36688
|
translations.SRP.SHOW_MORE,
|
|
@@ -36451,7 +36692,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36451
36692
|
};
|
|
36452
36693
|
const handleSelectFlight = (item, flight) => {
|
|
36453
36694
|
dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
|
|
36454
|
-
dispatch(
|
|
36695
|
+
dispatch(upsertEditableEntryLines([buildFlightSelectionLine(item, flight, bookedMode)]));
|
|
36455
36696
|
};
|
|
36456
36697
|
const renderFlightNode = (item) => {
|
|
36457
36698
|
const { node, line } = item;
|
|
@@ -36460,7 +36701,9 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36460
36701
|
const flights = availability?.results ?? [];
|
|
36461
36702
|
const selected = flights.find((x) => x.outwardGuid === availability?.selectedGuid) ?? null;
|
|
36462
36703
|
const others = flights.filter((x) => x.outwardGuid !== availability?.selectedGuid);
|
|
36463
|
-
const
|
|
36704
|
+
const isRemoved = status === 'resolved' && !selected && flights.length > 0;
|
|
36705
|
+
const removable = canRemove(item);
|
|
36706
|
+
const visible = others.slice(0, selected ? 2 : 3);
|
|
36464
36707
|
return (React__default.createElement(React__default.Fragment, { key: line.guid },
|
|
36465
36708
|
React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
36466
36709
|
React__default.createElement("div", { className: "search__results__label__date" },
|
|
@@ -36475,15 +36718,15 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36475
36718
|
node.arrivalAirportCode)))),
|
|
36476
36719
|
status === 'loading' && React__default.createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS }),
|
|
36477
36720
|
status === 'unavailable' && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
36478
|
-
status === 'resolved' && selected && (React__default.createElement(React__default.Fragment, null,
|
|
36721
|
+
status === 'resolved' && (selected || isRemoved) && (React__default.createElement(React__default.Fragment, null,
|
|
36479
36722
|
React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" },
|
|
36480
|
-
React__default.createElement(IndependentFlightOption, { key: `flight-${selected.outwardGuid}`, item: selected.outward, guid: selected.outwardGuid, selectedGuid: selected.outwardGuid, isOutward: true, showSelectedState: true, price: selected.price, selectedLabel: bookedMode && selected.outwardGuid === line.guid ? translations.SHARED.BOOKED : undefined }),
|
|
36481
|
-
visible.map((result) => (React__default.createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: result.outward, guid: result.outwardGuid, isOutward: true, price: result.price, currentSelectedPrice: selected
|
|
36482
|
-
others.length >
|
|
36723
|
+
selected && (React__default.createElement(IndependentFlightOption, { key: `flight-${selected.outwardGuid}`, item: selected.outward, guid: selected.outwardGuid, selectedGuid: selected.outwardGuid, isOutward: true, showSelectedState: true, price: selected.price, onSelect: removable ? () => handleRemoveFlight(item) : undefined, selectedLabel: bookedMode && selected.outwardGuid === line.guid ? translations.SHARED.BOOKED : removable ? translations.SRP.REMOVE : undefined })),
|
|
36724
|
+
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) })))),
|
|
36725
|
+
others.length > visible.length && !hideShowMoreResults && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
36483
36726
|
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreFlights(item) },
|
|
36484
36727
|
translations.SRP.SHOW_MORE,
|
|
36485
36728
|
" (",
|
|
36486
|
-
others.length -
|
|
36729
|
+
others.length - visible.length,
|
|
36487
36730
|
")")))))));
|
|
36488
36731
|
};
|
|
36489
36732
|
/** The lines a suggested activity put on the entry, which is also how we know it was added. */
|
|
@@ -36559,13 +36802,21 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36559
36802
|
")")))));
|
|
36560
36803
|
};
|
|
36561
36804
|
/**
|
|
36562
|
-
* An activity the traject books is swapped for another, the way a hotel is
|
|
36563
|
-
* found for themselves is added to the trip or taken back off it.
|
|
36805
|
+
* An activity the traject books is swapped for another, the way a hotel is, or - when it is optional -
|
|
36806
|
+
* taken off the trip; one the traveller found for themselves is added to the trip or taken back off it.
|
|
36564
36807
|
*/
|
|
36565
36808
|
const renderActivityCard = ({ result, item }) => {
|
|
36566
36809
|
const isSelected = isActivitySelected({ result, item });
|
|
36567
36810
|
const isBookedCard = !!item && bookedMode && matchesCode(result, item.node.code ?? '');
|
|
36568
|
-
|
|
36811
|
+
const removable = !!item && canRemove(item);
|
|
36812
|
+
const handleClick = () => {
|
|
36813
|
+
if (!item)
|
|
36814
|
+
return handleToggleActivity(result);
|
|
36815
|
+
if (isSelected && removable)
|
|
36816
|
+
return handleRemoveNode(item);
|
|
36817
|
+
handleSelect(item, result.code);
|
|
36818
|
+
};
|
|
36819
|
+
return (React__default.createElement(TrajectNodeCard, { key: `activity-${result.code}`, node: item?.node, result: result, isSelected: isSelected, languageCode: context.languageCode, translations: translations, onSelect: handleClick, selectLabel: item ? undefined : translations.SRP.ADD, selectedLabel: item && !removable ? (isBookedCard ? translations.SHARED.BOOKED : undefined) : translations.SRP.REMOVE }));
|
|
36569
36820
|
};
|
|
36570
36821
|
if (isLoading)
|
|
36571
36822
|
return React__default.createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
|
|
@@ -36600,6 +36851,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36600
36851
|
const useTrajectAvailability = () => {
|
|
36601
36852
|
const context = useContext(SearchResultsConfigurationContext);
|
|
36602
36853
|
const dispatch = useDispatch();
|
|
36854
|
+
const { editablePackagingEntry, trajectTransferAvailability } = useSelector((state) => state.searchResults);
|
|
36603
36855
|
const trajectEntry = context?.trajectEntry;
|
|
36604
36856
|
const showApiItems = !!context?.showApiItems;
|
|
36605
36857
|
const hideShowMoreResults = !!context?.hideShowMoreResults;
|
|
@@ -36625,12 +36877,14 @@ const useTrajectAvailability = () => {
|
|
|
36625
36877
|
rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
|
|
36626
36878
|
};
|
|
36627
36879
|
// Flight searches take passengers rather than rooms; the API buckets by age.
|
|
36880
|
+
// The ids must be the entry's own pax ids: the flight lines this search caches carry them as
|
|
36881
|
+
// EntryLinePax.PaxId, and booking maps those onto the entry's travelers (PackagingService.SyncPaxIds).
|
|
36628
36882
|
const pax = trajectEntry.entry.pax ?? [];
|
|
36629
36883
|
const ageOf = (p) => p.age ?? 30;
|
|
36630
|
-
const adults = pax.filter((p) => ageOf(p) >= 12)
|
|
36631
|
-
const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12)
|
|
36632
|
-
const babies = pax.filter((p) => ageOf(p) < 2)
|
|
36633
|
-
const flightPax = concat(
|
|
36884
|
+
const adults = pax.filter((p) => ageOf(p) >= 12);
|
|
36885
|
+
const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12);
|
|
36886
|
+
const babies = pax.filter((p) => ageOf(p) < 2);
|
|
36887
|
+
const flightPax = concat(adults.map((p) => ({ id: p.id, age: 31 })), kids.map((p) => ({ id: p.id, age: 8 })), babies.map((p) => ({ id: p.id, age: 1 })));
|
|
36634
36888
|
const nodesWithLines = getTrajectNodesWithLines(trajectEntry);
|
|
36635
36889
|
// The other members of a line group are alternatives whether or not a search still finds
|
|
36636
36890
|
// them; the ones it does not are drawn from their lines, as the booked service itself is.
|
|
@@ -36761,9 +37015,30 @@ const useTrajectAvailability = () => {
|
|
|
36761
37015
|
return item.lines.map((x) => x.guid);
|
|
36762
37016
|
}
|
|
36763
37017
|
};
|
|
37018
|
+
const resolveTransferNode = async (node) => {
|
|
37019
|
+
dispatch(setTrajectTransferLoading(node.nodeId));
|
|
37020
|
+
try {
|
|
37021
|
+
const results = (await build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, searchContext), controller.signal)) ?? [];
|
|
37022
|
+
if (controller.signal.aborted)
|
|
37023
|
+
return [];
|
|
37024
|
+
const result = results[0] ?? null;
|
|
37025
|
+
dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: result && buildTrajectTransferLine(node, result) ? result : null }));
|
|
37026
|
+
}
|
|
37027
|
+
catch (error) {
|
|
37028
|
+
if (controller.signal.aborted)
|
|
37029
|
+
return [];
|
|
37030
|
+
console.error('Failed to resolve traject transfer', node.name, error);
|
|
37031
|
+
dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: null }));
|
|
37032
|
+
}
|
|
37033
|
+
return [];
|
|
37034
|
+
};
|
|
36764
37035
|
(async () => {
|
|
36765
37036
|
const resolvable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node));
|
|
36766
|
-
const
|
|
37037
|
+
const transfers = bookedMode ? [] : (trajectEntry.nodes ?? []).filter(isTrajectTransferNode);
|
|
37038
|
+
const unresolved = (await Promise.all([
|
|
37039
|
+
...resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))),
|
|
37040
|
+
...transfers.map(resolveTransferNode)
|
|
37041
|
+
])).flat();
|
|
36767
37042
|
if (controller.signal.aborted)
|
|
36768
37043
|
return;
|
|
36769
37044
|
dispatch(setTrajectResolving(false));
|
|
@@ -36775,6 +37050,28 @@ const useTrajectAvailability = () => {
|
|
|
36775
37050
|
})();
|
|
36776
37051
|
return () => controller.abort();
|
|
36777
37052
|
}, [trajectEntry, showApiItems, hideShowMoreResults]);
|
|
37053
|
+
useEffect(() => {
|
|
37054
|
+
if (!trajectEntry || !editablePackagingEntry || isBookedEntryTraject(trajectEntry))
|
|
37055
|
+
return;
|
|
37056
|
+
const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
|
|
37057
|
+
const guids = new Set((editablePackagingEntry.lines ?? []).map((line) => line.guid));
|
|
37058
|
+
const toAdd = [];
|
|
37059
|
+
const toRemove = [];
|
|
37060
|
+
(trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
|
|
37061
|
+
const result = trajectTransferAvailability[node.nodeId]?.result;
|
|
37062
|
+
const line = result ? buildTrajectTransferLine(node, result) : null;
|
|
37063
|
+
if (!line)
|
|
37064
|
+
return;
|
|
37065
|
+
if (dropped.has(node.nodeId) && guids.has(line.guid))
|
|
37066
|
+
toRemove.push(line.guid);
|
|
37067
|
+
if (!dropped.has(node.nodeId) && !guids.has(line.guid))
|
|
37068
|
+
toAdd.push(line);
|
|
37069
|
+
});
|
|
37070
|
+
if (toRemove.length)
|
|
37071
|
+
dispatch(removeEditableEntryLines(toRemove));
|
|
37072
|
+
if (toAdd.length)
|
|
37073
|
+
dispatch(addEditableEntryLines(toAdd));
|
|
37074
|
+
}, [trajectEntry, editablePackagingEntry, trajectTransferAvailability]);
|
|
36778
37075
|
};
|
|
36779
37076
|
|
|
36780
37077
|
const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
@@ -37079,10 +37376,9 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
|
37079
37376
|
const item = getTrajectNodesWithLines(context.trajectEntry).find((x) => x.line.guid === trajectEditLineGuid);
|
|
37080
37377
|
const result = packagingAccoSearchDetails.find((x) => x.code === selectedPackagingAccoResultCode);
|
|
37081
37378
|
if (item && result) {
|
|
37082
|
-
|
|
37083
|
-
|
|
37084
|
-
|
|
37085
|
-
applyResultToLines(currentLines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
37379
|
+
// A stop the traveller took off has no lines left on the entry: the traject's own put it back.
|
|
37380
|
+
const currentLines = item.lines.map((line) => editablePackagingEntry?.lines?.find((x) => x.guid === line.guid) ?? line);
|
|
37381
|
+
dispatch(upsertEditableEntryLines(applyResultToLines(currentLines, result)));
|
|
37086
37382
|
dispatch(updateTrajectNodeResult({ lineGuid: trajectEditLineGuid, result }));
|
|
37087
37383
|
}
|
|
37088
37384
|
dispatch(setTrajectEditLineGuid(null));
|