@qite/tide-components 1.0.14-preview.3 → 1.0.14-preview.5
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 +410 -110
- 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 +410 -110
- 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;
|
|
@@ -30462,8 +30666,13 @@ const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
|
|
|
30462
30666
|
*/
|
|
30463
30667
|
const isBookedEntryTraject = (trajectEntry) => !!trajectEntry?.entry?.magicLink;
|
|
30464
30668
|
const NODE_SERVICE_TYPES = new Set([ACCOMMODATION_SERVICE_TYPE, EXCURSION_SERVICE_TYPE, FLIGHT_SERVICE_TYPE]);
|
|
30669
|
+
const isBookedRoomLineOf = (line, root) => !!line.parentGuid &&
|
|
30670
|
+
(line.parentGuid === root.guid || line.parentGuid === root.parentGuid) &&
|
|
30671
|
+
line.serviceType === root.serviceType &&
|
|
30672
|
+
line.productCode === root.productCode &&
|
|
30673
|
+
isSameDay(new Date(line.from), new Date(root.from));
|
|
30465
30674
|
/**
|
|
30466
|
-
* A room or option line joins its
|
|
30675
|
+
* A room or option line joins its service's node; a flight line never does — an outward and its
|
|
30467
30676
|
* return are separate legs of the trip even though the return points at the outward as parent.
|
|
30468
30677
|
*
|
|
30469
30678
|
* Lines that share a group key are one choice (a couple of hotels of which one is picked), so
|
|
@@ -30471,16 +30680,13 @@ const NODE_SERVICE_TYPES = new Set([ACCOMMODATION_SERVICE_TYPE, EXCURSION_SERVIC
|
|
|
30471
30680
|
*/
|
|
30472
30681
|
const groupNodeLines = (lines) => {
|
|
30473
30682
|
const groups = [];
|
|
30474
|
-
const groupByGuid = new Map();
|
|
30475
30683
|
lines.forEach((line) => {
|
|
30476
|
-
const
|
|
30477
|
-
if (
|
|
30478
|
-
|
|
30684
|
+
const service = line.serviceType !== FLIGHT_SERVICE_TYPE ? groups.find((group) => isBookedRoomLineOf(line, group[0])) : undefined;
|
|
30685
|
+
if (service) {
|
|
30686
|
+
service.push(line);
|
|
30479
30687
|
return;
|
|
30480
30688
|
}
|
|
30481
|
-
|
|
30482
|
-
groups.push(group);
|
|
30483
|
-
groupByGuid.set(line.guid, group);
|
|
30689
|
+
groups.push([line]);
|
|
30484
30690
|
});
|
|
30485
30691
|
const nodes = [];
|
|
30486
30692
|
const nodeByGroupKey = new Map();
|
|
@@ -30537,11 +30743,13 @@ const buildBookedTrajectEntry = (entry) => {
|
|
|
30537
30743
|
date: toDateOnlyString(addDays(start, order))
|
|
30538
30744
|
}));
|
|
30539
30745
|
const dayOrderOf = (line) => Math.max(differenceInCalendarDays(new Date(line.from), start), 0);
|
|
30540
|
-
|
|
30541
|
-
|
|
30746
|
+
const flights = sortBy(lines.filter((line) => line.serviceType === FLIGHT_SERVICE_TYPE && isActiveEntryLine(line)), (line) => new Date(line.from).getTime(), (line) => line.order);
|
|
30747
|
+
const outward = first(flights);
|
|
30748
|
+
const homeward = flights.length > 1 ? last(flights) : undefined;
|
|
30749
|
+
const placeInDay = (line) => (line === outward ? -1 : line === homeward ? 1 : 0);
|
|
30542
30750
|
const orderedGroups = groupNodeLines(lines).sort((a, b) => {
|
|
30543
30751
|
const [lineA, lineB] = [a.lines[0], b.lines[0]];
|
|
30544
|
-
return dayOrderOf(lineA) - dayOrderOf(lineB) ||
|
|
30752
|
+
return dayOrderOf(lineA) - dayOrderOf(lineB) || placeInDay(lineA) - placeInDay(lineB) || lineA.order - lineB.order;
|
|
30545
30753
|
});
|
|
30546
30754
|
const nodes = orderedGroups.map(({ lines: groupLines, alternatives }, index) => {
|
|
30547
30755
|
const line = groupLines[0];
|
|
@@ -33600,7 +33808,7 @@ const TrajectResultsFlyIn = ({ isLoading }) => {
|
|
|
33600
33808
|
if (!result)
|
|
33601
33809
|
return;
|
|
33602
33810
|
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
|
|
33603
|
-
buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])
|
|
33811
|
+
dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
|
|
33604
33812
|
dispatch(setTrajectEditLineGuid(null));
|
|
33605
33813
|
dispatch(setFlyInIsOpen(false));
|
|
33606
33814
|
};
|
|
@@ -33645,7 +33853,7 @@ const TrajectFlightsFlyIn = ({ isLoading }) => {
|
|
|
33645
33853
|
if (!flight)
|
|
33646
33854
|
return;
|
|
33647
33855
|
dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
|
|
33648
|
-
dispatch(
|
|
33856
|
+
dispatch(upsertEditableEntryLines([applyFlightToLine(item.line, flight)]));
|
|
33649
33857
|
dispatch(setTrajectEditLineGuid(null));
|
|
33650
33858
|
dispatch(setFlyInIsOpen(false));
|
|
33651
33859
|
};
|
|
@@ -33708,10 +33916,16 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
|
|
|
33708
33916
|
if (!item)
|
|
33709
33917
|
return;
|
|
33710
33918
|
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code: result.code }));
|
|
33711
|
-
buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])
|
|
33919
|
+
dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
|
|
33712
33920
|
dispatch(setTrajectActivitiesDayOrder(null));
|
|
33713
33921
|
dispatch(setFlyInIsOpen(false));
|
|
33714
33922
|
};
|
|
33923
|
+
const handleRemove = ({ item }) => {
|
|
33924
|
+
if (!item)
|
|
33925
|
+
return;
|
|
33926
|
+
dispatch(clearTrajectNodeSelection(item.line.guid));
|
|
33927
|
+
dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
|
|
33928
|
+
};
|
|
33715
33929
|
if (isLoading || suggestions?.status === 'loading') {
|
|
33716
33930
|
return React__default.createElement(React__default.Fragment, null, context.customSpinner ?? React__default.createElement(Spinner, { label: translations.SRP.LOADING_ACTIVITIES }));
|
|
33717
33931
|
}
|
|
@@ -33725,7 +33939,8 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
|
|
|
33725
33939
|
const { result, item } = card;
|
|
33726
33940
|
const isSelected = item ? trajectNodeAvailability[item.line.guid]?.selectedCode === result.code : getAddedLines(result).length > 0;
|
|
33727
33941
|
const isBookedCard = !!item && bookedMode && matchesCode(result, item.node.code ?? '');
|
|
33728
|
-
|
|
33942
|
+
const canRemove = !!item && isSelected && !bookedMode && canRemoveTrajectNode(item.node);
|
|
33943
|
+
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
33944
|
})))));
|
|
33730
33945
|
};
|
|
33731
33946
|
|
|
@@ -36119,16 +36334,18 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
|
|
|
36119
36334
|
apiKey: context.tideConnection.apiKey
|
|
36120
36335
|
};
|
|
36121
36336
|
}, [context]);
|
|
36337
|
+
// The field configuration on the entry is leading for which traveler fields are shown and required.
|
|
36338
|
+
const paxFields = editablePackagingEntry?.paxFields ?? [];
|
|
36122
36339
|
const initialValues = useMemo(() => {
|
|
36123
36340
|
if (!editablePackagingEntry) {
|
|
36124
36341
|
return {};
|
|
36125
36342
|
}
|
|
36126
|
-
return createInitialValuesFromEditablePackagingEntry(editablePackagingEntry);
|
|
36343
|
+
return createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, undefined, paxFields);
|
|
36127
36344
|
}, [editablePackagingEntry?.transactionId]);
|
|
36128
36345
|
const formik = useFormik({
|
|
36129
36346
|
initialValues,
|
|
36130
36347
|
enableReinitialize: true,
|
|
36131
|
-
validate: (values) => validateForm(values, false, 'b2c', translations, travellersSettings.formFields, travellersSettings.mainBookerFormFields, undefined, undefined,
|
|
36348
|
+
validate: (values) => validateForm(values, false, 'b2c', translations, travellersSettings.formFields, travellersSettings.mainBookerFormFields, undefined, undefined, paxFields),
|
|
36132
36349
|
onSubmit: (values) => {
|
|
36133
36350
|
if (!editablePackagingEntry)
|
|
36134
36351
|
return;
|
|
@@ -36213,7 +36430,7 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
|
|
|
36213
36430
|
".\u00A0",
|
|
36214
36431
|
stepLabels[step])) },
|
|
36215
36432
|
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:
|
|
36433
|
+
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
36434
|
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
36435
|
currentStep === 2 && (React__default.createElement(SharedConfirmation, { bookingNumber: bookingNumber ?? editablePackagingEntry?.dossierNumber ?? '', isOption: false, isOffer: false, translations: translations.CONFIRMATION })),
|
|
36219
36436
|
currentStep === 3 && React__default.createElement("div", null))),
|
|
@@ -36313,11 +36530,13 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36313
36530
|
});
|
|
36314
36531
|
return map;
|
|
36315
36532
|
}, [trajectEntry]);
|
|
36533
|
+
const droppedNodeIds = trajectEntry ? getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry) : new Set();
|
|
36534
|
+
const droppedKey = [...droppedNodeIds].sort((a, b) => a - b).join(',');
|
|
36316
36535
|
const { stayByDay, stayStartByNodeId } = useMemo(() => {
|
|
36317
36536
|
const byDay = new Map();
|
|
36318
36537
|
const byNodeId = new Map();
|
|
36319
36538
|
nodesWithLines
|
|
36320
|
-
.filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE)
|
|
36539
|
+
.filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE && !droppedNodeIds.has(x.node.nodeId))
|
|
36321
36540
|
.forEach((item) => {
|
|
36322
36541
|
const nights = Math.max(differenceInCalendarDays(new Date(item.line.to), new Date(item.line.from)), 1);
|
|
36323
36542
|
byNodeId.set(item.node.nodeId, { item, night: 0, nights });
|
|
@@ -36326,7 +36545,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36326
36545
|
}
|
|
36327
36546
|
});
|
|
36328
36547
|
return { stayByDay: byDay, stayStartByNodeId: byNodeId };
|
|
36329
|
-
}, [nodesWithLines]);
|
|
36548
|
+
}, [nodesWithLines, droppedKey]);
|
|
36330
36549
|
/** Whether every node of the trip has an answer, one way or the other. */
|
|
36331
36550
|
const tripResolved = useMemo(() => nodesWithLines
|
|
36332
36551
|
.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node))
|
|
@@ -36353,7 +36572,20 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36353
36572
|
if (!result)
|
|
36354
36573
|
return;
|
|
36355
36574
|
dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
|
|
36356
|
-
buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])
|
|
36575
|
+
dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
|
|
36576
|
+
};
|
|
36577
|
+
/**
|
|
36578
|
+
* An optional stop can be taken off the trip; a mandatory one, or one of which the group needs a pick,
|
|
36579
|
+
* only swapped. A booked entry's services are never taken off here.
|
|
36580
|
+
*/
|
|
36581
|
+
const canRemove = (item) => !bookedMode && canRemoveTrajectNode(item.node);
|
|
36582
|
+
const handleRemoveNode = (item) => {
|
|
36583
|
+
dispatch(clearTrajectNodeSelection(item.line.guid));
|
|
36584
|
+
dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
|
|
36585
|
+
};
|
|
36586
|
+
const handleRemoveFlight = (item) => {
|
|
36587
|
+
dispatch(clearTrajectFlightSelection(item.line.guid));
|
|
36588
|
+
dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
|
|
36357
36589
|
};
|
|
36358
36590
|
/** The remaining legs for a flight node open in the fly-in, as the regular flight results do. */
|
|
36359
36591
|
const handleShowMoreFlights = (item) => {
|
|
@@ -36421,9 +36653,17 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36421
36653
|
// A hotel row holds the hotels configured on the node; a portal can also let it fill up with
|
|
36422
36654
|
// non-preferred hotels found in the area, the rest staying behind "show more".
|
|
36423
36655
|
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
36656
|
// No entry yet means the node's search has been queued but not dispatched — still loading.
|
|
36426
36657
|
const status = availability?.status ?? 'loading';
|
|
36658
|
+
// A stop taken off the trip keeps its results in view, to pick one back.
|
|
36659
|
+
const isRemoved = status === 'resolved' && !selected && results.length > 0;
|
|
36660
|
+
const removable = canRemove(item);
|
|
36661
|
+
const shown = selected
|
|
36662
|
+
? [selected, ...configured, ...fill]
|
|
36663
|
+
: configured.length || fill.length
|
|
36664
|
+
? [...configured, ...fill]
|
|
36665
|
+
: others.slice(0, MAX_HOTEL_RESULTS);
|
|
36666
|
+
const remaining = results.length - shown.length;
|
|
36427
36667
|
return (React__default.createElement(React__default.Fragment, { key: line.guid },
|
|
36428
36668
|
showHeader && (React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
36429
36669
|
React__default.createElement("div", { className: "search__results__label__date" },
|
|
@@ -36435,13 +36675,18 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36435
36675
|
React__default.createElement("strong", null, headerTitle || node.name))))),
|
|
36436
36676
|
status === 'loading' && React__default.createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }),
|
|
36437
36677
|
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" },
|
|
36678
|
+
status === 'resolved' && (selected || isRemoved) && (React__default.createElement(React__default.Fragment, null,
|
|
36679
|
+
selected && availability.preferredUnavailable && node.preferredAccommodationName && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
36440
36680
|
React__default.createElement("span", null,
|
|
36441
36681
|
node.preferredAccommodationName,
|
|
36442
36682
|
" \u2014 ",
|
|
36443
36683
|
translations.SRP.NO_RESULTS))),
|
|
36444
|
-
React__default.createElement(TrajectCardRow, { selectionKey: selected
|
|
36684
|
+
React__default.createElement(TrajectCardRow, { selectionKey: selected?.code ?? '' }, shown.map((result) => {
|
|
36685
|
+
const isSelected = result.code === selected?.code;
|
|
36686
|
+
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,
|
|
36687
|
+
// selectedLabel={bookedMode && matchesCode(result, node.code ?? '') ? translations.SHARED.BOOKED : undefined}
|
|
36688
|
+
selectedLabel: removable ? translations.SRP.REMOVE : undefined }));
|
|
36689
|
+
})),
|
|
36445
36690
|
remaining > 0 && !hideShowMoreResults && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
36446
36691
|
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMore(item) },
|
|
36447
36692
|
translations.SRP.SHOW_MORE,
|
|
@@ -36451,7 +36696,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36451
36696
|
};
|
|
36452
36697
|
const handleSelectFlight = (item, flight) => {
|
|
36453
36698
|
dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
|
|
36454
|
-
dispatch(
|
|
36699
|
+
dispatch(upsertEditableEntryLines([buildFlightSelectionLine(item, flight, bookedMode)]));
|
|
36455
36700
|
};
|
|
36456
36701
|
const renderFlightNode = (item) => {
|
|
36457
36702
|
const { node, line } = item;
|
|
@@ -36460,7 +36705,9 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36460
36705
|
const flights = availability?.results ?? [];
|
|
36461
36706
|
const selected = flights.find((x) => x.outwardGuid === availability?.selectedGuid) ?? null;
|
|
36462
36707
|
const others = flights.filter((x) => x.outwardGuid !== availability?.selectedGuid);
|
|
36463
|
-
const
|
|
36708
|
+
const isRemoved = status === 'resolved' && !selected && flights.length > 0;
|
|
36709
|
+
const removable = canRemove(item);
|
|
36710
|
+
const visible = others.slice(0, selected ? 2 : 3);
|
|
36464
36711
|
return (React__default.createElement(React__default.Fragment, { key: line.guid },
|
|
36465
36712
|
React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
|
|
36466
36713
|
React__default.createElement("div", { className: "search__results__label__date" },
|
|
@@ -36475,15 +36722,15 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36475
36722
|
node.arrivalAirportCode)))),
|
|
36476
36723
|
status === 'loading' && React__default.createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS }),
|
|
36477
36724
|
status === 'unavailable' && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
|
|
36478
|
-
status === 'resolved' && selected && (React__default.createElement(React__default.Fragment, null,
|
|
36725
|
+
status === 'resolved' && (selected || isRemoved) && (React__default.createElement(React__default.Fragment, null,
|
|
36479
36726
|
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 >
|
|
36727
|
+
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 })),
|
|
36728
|
+
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) })))),
|
|
36729
|
+
others.length > visible.length && !hideShowMoreResults && (React__default.createElement("div", { className: "search__results__cards__actions" },
|
|
36483
36730
|
React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreFlights(item) },
|
|
36484
36731
|
translations.SRP.SHOW_MORE,
|
|
36485
36732
|
" (",
|
|
36486
|
-
others.length -
|
|
36733
|
+
others.length - visible.length,
|
|
36487
36734
|
")")))))));
|
|
36488
36735
|
};
|
|
36489
36736
|
/** The lines a suggested activity put on the entry, which is also how we know it was added. */
|
|
@@ -36559,13 +36806,21 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36559
36806
|
")")))));
|
|
36560
36807
|
};
|
|
36561
36808
|
/**
|
|
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.
|
|
36809
|
+
* An activity the traject books is swapped for another, the way a hotel is, or - when it is optional -
|
|
36810
|
+
* taken off the trip; one the traveller found for themselves is added to the trip or taken back off it.
|
|
36564
36811
|
*/
|
|
36565
36812
|
const renderActivityCard = ({ result, item }) => {
|
|
36566
36813
|
const isSelected = isActivitySelected({ result, item });
|
|
36567
36814
|
const isBookedCard = !!item && bookedMode && matchesCode(result, item.node.code ?? '');
|
|
36568
|
-
|
|
36815
|
+
const removable = !!item && canRemove(item);
|
|
36816
|
+
const handleClick = () => {
|
|
36817
|
+
if (!item)
|
|
36818
|
+
return handleToggleActivity(result);
|
|
36819
|
+
if (isSelected && removable)
|
|
36820
|
+
return handleRemoveNode(item);
|
|
36821
|
+
handleSelect(item, result.code);
|
|
36822
|
+
};
|
|
36823
|
+
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
36824
|
};
|
|
36570
36825
|
if (isLoading)
|
|
36571
36826
|
return React__default.createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
|
|
@@ -36600,6 +36855,7 @@ const TrajectResults = ({ isLoading }) => {
|
|
|
36600
36855
|
const useTrajectAvailability = () => {
|
|
36601
36856
|
const context = useContext(SearchResultsConfigurationContext);
|
|
36602
36857
|
const dispatch = useDispatch();
|
|
36858
|
+
const { editablePackagingEntry, trajectTransferAvailability } = useSelector((state) => state.searchResults);
|
|
36603
36859
|
const trajectEntry = context?.trajectEntry;
|
|
36604
36860
|
const showApiItems = !!context?.showApiItems;
|
|
36605
36861
|
const hideShowMoreResults = !!context?.hideShowMoreResults;
|
|
@@ -36625,12 +36881,14 @@ const useTrajectAvailability = () => {
|
|
|
36625
36881
|
rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
|
|
36626
36882
|
};
|
|
36627
36883
|
// Flight searches take passengers rather than rooms; the API buckets by age.
|
|
36884
|
+
// The ids must be the entry's own pax ids: the flight lines this search caches carry them as
|
|
36885
|
+
// EntryLinePax.PaxId, and booking maps those onto the entry's travelers (PackagingService.SyncPaxIds).
|
|
36628
36886
|
const pax = trajectEntry.entry.pax ?? [];
|
|
36629
36887
|
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(
|
|
36888
|
+
const adults = pax.filter((p) => ageOf(p) >= 12);
|
|
36889
|
+
const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12);
|
|
36890
|
+
const babies = pax.filter((p) => ageOf(p) < 2);
|
|
36891
|
+
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
36892
|
const nodesWithLines = getTrajectNodesWithLines(trajectEntry);
|
|
36635
36893
|
// The other members of a line group are alternatives whether or not a search still finds
|
|
36636
36894
|
// them; the ones it does not are drawn from their lines, as the booked service itself is.
|
|
@@ -36761,9 +37019,30 @@ const useTrajectAvailability = () => {
|
|
|
36761
37019
|
return item.lines.map((x) => x.guid);
|
|
36762
37020
|
}
|
|
36763
37021
|
};
|
|
37022
|
+
const resolveTransferNode = async (node) => {
|
|
37023
|
+
dispatch(setTrajectTransferLoading(node.nodeId));
|
|
37024
|
+
try {
|
|
37025
|
+
const results = (await build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, searchContext), controller.signal)) ?? [];
|
|
37026
|
+
if (controller.signal.aborted)
|
|
37027
|
+
return [];
|
|
37028
|
+
const result = results[0] ?? null;
|
|
37029
|
+
dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: result && buildTrajectTransferLine(node, result) ? result : null }));
|
|
37030
|
+
}
|
|
37031
|
+
catch (error) {
|
|
37032
|
+
if (controller.signal.aborted)
|
|
37033
|
+
return [];
|
|
37034
|
+
console.error('Failed to resolve traject transfer', node.name, error);
|
|
37035
|
+
dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: null }));
|
|
37036
|
+
}
|
|
37037
|
+
return [];
|
|
37038
|
+
};
|
|
36764
37039
|
(async () => {
|
|
36765
37040
|
const resolvable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node));
|
|
36766
|
-
const
|
|
37041
|
+
const transfers = bookedMode ? [] : (trajectEntry.nodes ?? []).filter(isTrajectTransferNode);
|
|
37042
|
+
const unresolved = (await Promise.all([
|
|
37043
|
+
...resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))),
|
|
37044
|
+
...transfers.map(resolveTransferNode)
|
|
37045
|
+
])).flat();
|
|
36767
37046
|
if (controller.signal.aborted)
|
|
36768
37047
|
return;
|
|
36769
37048
|
dispatch(setTrajectResolving(false));
|
|
@@ -36775,6 +37054,28 @@ const useTrajectAvailability = () => {
|
|
|
36775
37054
|
})();
|
|
36776
37055
|
return () => controller.abort();
|
|
36777
37056
|
}, [trajectEntry, showApiItems, hideShowMoreResults]);
|
|
37057
|
+
useEffect(() => {
|
|
37058
|
+
if (!trajectEntry || !editablePackagingEntry || isBookedEntryTraject(trajectEntry))
|
|
37059
|
+
return;
|
|
37060
|
+
const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
|
|
37061
|
+
const guids = new Set((editablePackagingEntry.lines ?? []).map((line) => line.guid));
|
|
37062
|
+
const toAdd = [];
|
|
37063
|
+
const toRemove = [];
|
|
37064
|
+
(trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
|
|
37065
|
+
const result = trajectTransferAvailability[node.nodeId]?.result;
|
|
37066
|
+
const line = result ? buildTrajectTransferLine(node, result) : null;
|
|
37067
|
+
if (!line)
|
|
37068
|
+
return;
|
|
37069
|
+
if (dropped.has(node.nodeId) && guids.has(line.guid))
|
|
37070
|
+
toRemove.push(line.guid);
|
|
37071
|
+
if (!dropped.has(node.nodeId) && !guids.has(line.guid))
|
|
37072
|
+
toAdd.push(line);
|
|
37073
|
+
});
|
|
37074
|
+
if (toRemove.length)
|
|
37075
|
+
dispatch(removeEditableEntryLines(toRemove));
|
|
37076
|
+
if (toAdd.length)
|
|
37077
|
+
dispatch(addEditableEntryLines(toAdd));
|
|
37078
|
+
}, [trajectEntry, editablePackagingEntry, trajectTransferAvailability]);
|
|
36778
37079
|
};
|
|
36779
37080
|
|
|
36780
37081
|
const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
@@ -37079,10 +37380,9 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
|
|
|
37079
37380
|
const item = getTrajectNodesWithLines(context.trajectEntry).find((x) => x.line.guid === trajectEditLineGuid);
|
|
37080
37381
|
const result = packagingAccoSearchDetails.find((x) => x.code === selectedPackagingAccoResultCode);
|
|
37081
37382
|
if (item && result) {
|
|
37082
|
-
|
|
37083
|
-
|
|
37084
|
-
|
|
37085
|
-
applyResultToLines(currentLines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
|
|
37383
|
+
// A stop the traveller took off has no lines left on the entry: the traject's own put it back.
|
|
37384
|
+
const currentLines = item.lines.map((line) => editablePackagingEntry?.lines?.find((x) => x.guid === line.guid) ?? line);
|
|
37385
|
+
dispatch(upsertEditableEntryLines(applyResultToLines(currentLines, result)));
|
|
37086
37386
|
dispatch(updateTrajectNodeResult({ lineGuid: trajectEditLineGuid, result }));
|
|
37087
37387
|
}
|
|
37088
37388
|
dispatch(setTrajectEditLineGuid(null));
|