@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.
@@ -16133,6 +16133,7 @@ var ENDPOINT$1 = "/api/web/packaging";
16133
16133
  var ENDPOINT_START_TRANSACTION = ENDPOINT$1 + "/start";
16134
16134
  var ENDPOINT_ACCOMMODATIONS = ENDPOINT$1 + "/accommodations";
16135
16135
  var ENDPOINT_EXCURSIONS = ENDPOINT$1 + "/excursions";
16136
+ var ENDPOINT_TRANSFERS = ENDPOINT$1 + "/transfers";
16136
16137
  var ENDPOINT_FLIGHTS = ENDPOINT$1 + "/flights";
16137
16138
  var ENDPOINT_PRICE_DETAILS = ENDPOINT$1 + "/price-details";
16138
16139
  var ENDPOINT_BOOK = ENDPOINT$1 + "/book";
@@ -16165,6 +16166,12 @@ var searchPackagingExcursions = function (config, request, signal) {
16165
16166
  var body = JSON.stringify(request);
16166
16167
  return post(url, apiKey, body, config.token, signal, true);
16167
16168
  };
16169
+ var searchPackagingTransfers = function (config, request, signal) {
16170
+ var url = "" + config.host + ENDPOINT_TRANSFERS;
16171
+ var apiKey = config.apiKey;
16172
+ var body = JSON.stringify(request);
16173
+ return post(url, apiKey, body, config.token, signal, true);
16174
+ };
16168
16175
  var getEntry = function (config, magicLinkCode, signal) {
16169
16176
  var apiKey = config.apiKey;
16170
16177
  var url = "" + config.host + ENDPOINT_ENTRY(magicLinkCode);
@@ -16342,6 +16349,7 @@ exports.searchOords = searchOords;
16342
16349
  exports.searchPackagingAccommodations = searchPackagingAccommodations;
16343
16350
  exports.searchPackagingExcursions = searchPackagingExcursions;
16344
16351
  exports.searchPackagingFlights = searchPackagingFlights;
16352
+ exports.searchPackagingTransfers = searchPackagingTransfers;
16345
16353
  exports.searchRegions = searchRegions;
16346
16354
  exports.searchWithErrorMapping = searchWithErrorMapping;
16347
16355
  exports.sendContactForm = sendContactForm;
@@ -19834,10 +19842,30 @@ function formatBirthDate$1(birthDate) {
19834
19842
  // A field can have separate "main-booker"/other-pax variants sharing the same identity
19835
19843
  // (customFieldConfigurationId for custom fields, fieldName for built-in fields).
19836
19844
  const MAIN_BOOKER_GROUP = 'main-booker';
19845
+ const MAIN_BOOKER_FIELD_PREFIX = 'mainBooker';
19837
19846
  const isCustomPaxField = (field) => field.customFieldConfigurationId != null;
19847
+ // Tide names the built-in main-booker fields with a prefix ('mainBookerGender', 'mainBookerCountryId', ...),
19848
+ // the other-pax ones without ('gender', 'countryId'). Strip it so both are variants of the same field.
19849
+ function normalizeBuiltInFieldName(field) {
19850
+ if (isCustomPaxField(field) || !field.fieldName?.startsWith(MAIN_BOOKER_FIELD_PREFIX))
19851
+ return field;
19852
+ const stripped = field.fieldName.slice(MAIN_BOOKER_FIELD_PREFIX.length);
19853
+ if (!stripped)
19854
+ return field;
19855
+ return { ...field, fieldName: stripped.charAt(0).toLowerCase() + stripped.slice(1) };
19856
+ }
19857
+ // Once Tide sends built-in fields, that configuration decides which built-in fields are shown/required;
19858
+ // the hardcoded formFields / core defaults are only the fallback for when it doesn't.
19859
+ function hasConfiguredBuiltInPaxFields(paxFields = []) {
19860
+ return paxFields.some((f) => !isCustomPaxField(f));
19861
+ }
19862
+ // The main booker's address/contact details are then asked on the main booker's own traveler block.
19863
+ function hasConfiguredMainBookerFields(paxFields = []) {
19864
+ return paxFields.some((f) => !isCustomPaxField(f) && f.group === MAIN_BOOKER_GROUP);
19865
+ }
19838
19866
  function resolveApplicablePaxFields(fields, isMainBooker) {
19839
19867
  const variantsByKey = new Map();
19840
- fields.forEach((field) => {
19868
+ fields.map(normalizeBuiltInFieldName).forEach((field) => {
19841
19869
  const key = field.customFieldConfigurationId ?? field.fieldName;
19842
19870
  const variants = variantsByKey.get(key) ?? [];
19843
19871
  variants.push(field);
@@ -19899,11 +19927,50 @@ const BUILT_IN_FIELD_PATHS = {
19899
19927
  // The 4 fields that always render/validate regardless of paxFields config (fallback behavior).
19900
19928
  const CORE_FIELD_NAMES = new Set(['gender', 'firstName', 'lastName', 'dateOfBirth']);
19901
19929
  const ADDRESS_FIELD_NAMES = ['street', 'location', 'countryId', 'postalCode', 'number'];
19930
+ // Built-in fields have no dataType from Tide; their control type and label follow from the fieldName.
19931
+ // gender, hasInsurance, countryId, weight and height have a dedicated control and only need a label here.
19932
+ function getBuiltInFieldControlMeta(translations) {
19933
+ return {
19934
+ gender: { label: translations.TRAVELERS_FORM.GENDER_ID },
19935
+ firstName: { label: translations.TRAVELERS_FORM.FIRST_NAME },
19936
+ lastName: { label: translations.TRAVELERS_FORM.LAST_NAME },
19937
+ dateOfBirth: { type: 'date', label: translations.TRAVELERS_FORM.BIRTHDATE },
19938
+ nickname: { label: translations.TRAVELERS_FORM.NICKNAME },
19939
+ birthplace: { label: translations.TRAVELERS_FORM.BIRTHPLACE },
19940
+ nationality: { label: translations.TRAVELERS_FORM.NATIONALITY },
19941
+ preferredLanguageId: { type: 'number', label: translations.TRAVELERS_FORM.PREFERRED_LANGUAGE },
19942
+ email: { type: 'email', label: translations.TRAVELERS_FORM.TRAVELER_EMAIL },
19943
+ phone: { type: 'tel', label: translations.TRAVELERS_FORM.TRAVELER_PHONE },
19944
+ mobile: { type: 'tel', label: translations.TRAVELERS_FORM.TRAVELER_MOBILE },
19945
+ passportNumber: { label: translations.TRAVELERS_FORM.PASSPORT_NUMBER },
19946
+ passportIssueDate: { type: 'date', label: translations.TRAVELERS_FORM.PASSPORT_ISSUE_DATE },
19947
+ passportIssuedLocation: { label: translations.TRAVELERS_FORM.PASSPORT_ISSUED_LOCATION },
19948
+ passportExpiryDate: { type: 'date', label: translations.TRAVELERS_FORM.PASSPORT_EXPIRY_DATE },
19949
+ emergencyNumber: { type: 'tel', label: translations.TRAVELERS_FORM.EMERGENCY_NUMBER },
19950
+ hasInsurance: { label: translations.TRAVELERS_FORM.HAS_INSURANCE },
19951
+ insuranceName: { label: translations.TRAVELERS_FORM.INSURANCE_NAME },
19952
+ info: { label: translations.TRAVELERS_FORM.INFO },
19953
+ weight: { label: translations.TRAVELERS_FORM.WEIGHT },
19954
+ height: { label: translations.TRAVELERS_FORM.HEIGHT },
19955
+ street: { label: translations.TRAVELERS_FORM.STREET },
19956
+ location: { label: translations.TRAVELERS_FORM.CITY },
19957
+ countryId: { label: translations.TRAVELERS_FORM.COUNTRY },
19958
+ postalCode: { label: translations.TRAVELERS_FORM.ZIPCODE },
19959
+ number: { label: translations.TRAVELERS_FORM.HOUSE_NUMBER }
19960
+ };
19961
+ }
19962
+ // The form's gender radio values ('m'/'f'/'x') <-> Tide's Gender enum.
19963
+ const GENDER_BY_FORM_VALUE = { m: build.Gender.male, f: build.Gender.female, x: build.Gender.other };
19964
+ function formValueFromGender(gender) {
19965
+ return Object.keys(GENDER_BY_FORM_VALUE).find((key) => GENDER_BY_FORM_VALUE[key] === gender) ?? '';
19966
+ }
19902
19967
  // Shared by buildPax (new-booking) and applyTravelersFormValuesToEditablePackagingEntry
19903
19968
  // (edit-booking), now that PackagingEntryPax and BookingPackagePax have the same shape.
19904
19969
  function applyTravelerToPax(pax, traveler, mainBookerId) {
19905
19970
  return {
19906
19971
  ...pax,
19972
+ // Unanswered (optional) gender keeps whatever the pax already had.
19973
+ gender: GENDER_BY_FORM_VALUE[traveler.gender] ?? pax.gender,
19907
19974
  firstName: traveler.firstName ?? '',
19908
19975
  lastName: traveler.lastName ?? '',
19909
19976
  dateOfBirth: traveler.birthDate || null,
@@ -20063,7 +20130,13 @@ function mapPackagingEntryPaxToTraveler(roomTraveler) {
20063
20130
  customFields: roomTraveler.customFields ?? []
20064
20131
  };
20065
20132
  }
20066
- function createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, agentAdressId) {
20133
+ // Keeps the values already on the entry's pax and adds an empty slot for every configured custom field it lacks,
20134
+ // so the form renders/validates the same set as createTraveler does for a new booking.
20135
+ function mergeCustomFields(existing = [], paxFields) {
20136
+ const missing = buildEmptyCustomFields(paxFields).filter((empty) => !existing.some((c) => c.customFieldConfigurationId === empty.customFieldConfigurationId));
20137
+ return [...existing, ...missing];
20138
+ }
20139
+ function createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, agentAdressId, paxFields = editablePackagingEntry?.paxFields ?? []) {
20067
20140
  const pax = editablePackagingEntry?.pax ?? [];
20068
20141
  const rooms = editablePackagingEntry.rooms.map((room) => {
20069
20142
  const roomPax = pax.filter((x) => room.paxIds.includes(x.id));
@@ -20076,8 +20149,9 @@ function createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, a
20076
20149
  firstName: roomTraveler.firstName ?? '',
20077
20150
  lastName: roomTraveler.lastName ?? '',
20078
20151
  birthDate: roomTraveler.dateOfBirth ? dateFns.format(new Date(roomTraveler.dateOfBirth), 'yyyy-MM-dd') : '',
20079
- gender: '',
20080
- ...mapPackagingEntryPaxToTraveler(roomTraveler)
20152
+ gender: formValueFromGender(roomTraveler.gender),
20153
+ ...mapPackagingEntryPaxToTraveler(roomTraveler),
20154
+ customFields: mergeCustomFields(roomTraveler.customFields, paxFields)
20081
20155
  };
20082
20156
  }),
20083
20157
  children: children.map((roomTraveler) => {
@@ -20086,9 +20160,10 @@ function createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, a
20086
20160
  firstName: roomTraveler.firstName ?? '',
20087
20161
  lastName: roomTraveler.lastName ?? '',
20088
20162
  birthDate: roomTraveler.dateOfBirth ? dateFns.format(new Date(roomTraveler.dateOfBirth), 'yyyy-MM-dd') : '',
20089
- gender: '',
20163
+ gender: formValueFromGender(roomTraveler.gender),
20090
20164
  isBaby: isBabyAge(roomTraveler.age),
20091
- ...mapPackagingEntryPaxToTraveler(roomTraveler)
20165
+ ...mapPackagingEntryPaxToTraveler(roomTraveler),
20166
+ customFields: mergeCustomFields(roomTraveler.customFields, paxFields)
20092
20167
  };
20093
20168
  })
20094
20169
  };
@@ -20113,7 +20188,8 @@ function applyTravelersFormValuesToEditablePackagingEntry(editablePackagingEntry
20113
20188
  const travelers = values.rooms.flatMap((room) => [...room.adults, ...room.children]);
20114
20189
  // The backend treats the main booker's per-pax address as authoritative, so keep the
20115
20190
  // booking-level address in sync with it rather than sending two independent values.
20116
- const mainBookerAddress = travelers.find((x) => x.id === values.mainBookerId)?.address;
20191
+ const mainBookerTraveler = travelers.find((x) => x.id === values.mainBookerId);
20192
+ const mainBookerAddress = mainBookerTraveler?.address;
20117
20193
  return {
20118
20194
  ...editablePackagingEntry,
20119
20195
  pax: (editablePackagingEntry.pax ?? []).map((pax) => {
@@ -20129,8 +20205,9 @@ function applyTravelersFormValuesToEditablePackagingEntry(editablePackagingEntry
20129
20205
  place: mainBookerAddress?.location || values.place,
20130
20206
  country: mainBookerAddress?.country || values.country,
20131
20207
  travelAgentId: values.travelAgentId,
20132
- phone: values.phone,
20133
- email: values.email
20208
+ // When Tide configures the main-booker fields, contact details live on the main booker's traveler block.
20209
+ phone: values.phone || mainBookerTraveler?.mobile || mainBookerTraveler?.phone,
20210
+ email: values.email || mainBookerTraveler?.email
20134
20211
  }
20135
20212
  };
20136
20213
  }
@@ -20287,30 +20364,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
20287
20364
  return (React__default["default"].createElement("div", { key: field.customFieldConfigurationId, style: { gridColumn: `span ${field.col || 12}` } }, getCustomFieldControl(field, value, name)));
20288
20365
  })));
20289
20366
  };
20290
- // Fields with a dedicated control (gender, hasInsurance, countryId) aren't listed here.
20291
- const BUILT_IN_FIELD_CONTROL_META = {
20292
- firstName: { label: translations.TRAVELERS_FORM.FIRST_NAME },
20293
- lastName: { label: translations.TRAVELERS_FORM.LAST_NAME },
20294
- dateOfBirth: { type: 'date', label: translations.TRAVELERS_FORM.BIRTHDATE },
20295
- nickname: { label: translations.TRAVELERS_FORM.NICKNAME },
20296
- birthplace: { label: translations.TRAVELERS_FORM.BIRTHPLACE },
20297
- nationality: { label: translations.TRAVELERS_FORM.NATIONALITY },
20298
- preferredLanguageId: { type: 'number', label: translations.TRAVELERS_FORM.PREFERRED_LANGUAGE },
20299
- email: { type: 'email', label: translations.TRAVELERS_FORM.TRAVELER_EMAIL },
20300
- phone: { label: translations.TRAVELERS_FORM.TRAVELER_PHONE },
20301
- mobile: { label: translations.TRAVELERS_FORM.TRAVELER_MOBILE },
20302
- passportNumber: { label: translations.TRAVELERS_FORM.PASSPORT_NUMBER },
20303
- passportIssueDate: { type: 'date', label: translations.TRAVELERS_FORM.PASSPORT_ISSUE_DATE },
20304
- passportIssuedLocation: { label: translations.TRAVELERS_FORM.PASSPORT_ISSUED_LOCATION },
20305
- passportExpiryDate: { type: 'date', label: translations.TRAVELERS_FORM.PASSPORT_EXPIRY_DATE },
20306
- emergencyNumber: { label: translations.TRAVELERS_FORM.EMERGENCY_NUMBER },
20307
- insuranceName: { label: translations.TRAVELERS_FORM.INSURANCE_NAME },
20308
- info: { label: translations.TRAVELERS_FORM.INFO },
20309
- street: { label: translations.TRAVELERS_FORM.STREET },
20310
- location: { label: translations.TRAVELERS_FORM.CITY },
20311
- postalCode: { label: translations.TRAVELERS_FORM.ZIPCODE },
20312
- number: { label: translations.TRAVELERS_FORM.HOUSE_NUMBER }
20313
- };
20367
+ const BUILT_IN_FIELD_CONTROL_META = getBuiltInFieldControlMeta(translations);
20314
20368
  const renderBuiltInFieldControl = (fieldName, isRequired, travelerValues, namePrefix) => {
20315
20369
  const path = BUILT_IN_FIELD_PATHS[fieldName] ?? fieldName;
20316
20370
  const name = `${namePrefix}.${path}`;
@@ -20345,7 +20399,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
20345
20399
  const resolveBuiltInField = (fieldName, travelerValues) => {
20346
20400
  const isMainBooker = travelerValues.id === formik.values.mainBookerId;
20347
20401
  const configured = getBuiltInPaxField(paxFields, fieldName, isMainBooker);
20348
- const fallback = CORE_FIELD_DEFAULTS[fieldName];
20402
+ const fallback = hasConfiguredBuiltInPaxFields(paxFields) ? undefined : CORE_FIELD_DEFAULTS[fieldName];
20349
20403
  if (!configured && !fallback)
20350
20404
  return undefined;
20351
20405
  return { isRequired: configured?.isRequired ?? fallback.isRequired, col: configured?.col ?? fallback.col };
@@ -20357,7 +20411,14 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
20357
20411
  return (React__default["default"].createElement("div", { key: fieldName, style: { gridColumn: `span ${resolved.col || 12}` } }, renderBuiltInFieldControl(fieldName, resolved.isRequired, travelerValues, namePrefix)));
20358
20412
  };
20359
20413
  const renderTravelerAddressFields = (travelerValues, namePrefix) => {
20360
- const fields = ADDRESS_FIELD_NAMES.map((fieldName) => renderBuiltInField(fieldName, travelerValues, namePrefix)).filter(Boolean);
20414
+ const isMainBooker = travelerValues.id === formik.values.mainBookerId;
20415
+ // Configured order when Tide sends one, so the configured col widths line up as intended.
20416
+ const fieldNames = hasConfiguredBuiltInPaxFields(paxFields)
20417
+ ? getApplicableBuiltInPaxFields(paxFields, isMainBooker)
20418
+ .map((field) => field.fieldName)
20419
+ .filter((fieldName) => ADDRESS_FIELD_NAMES.includes(fieldName))
20420
+ : ADDRESS_FIELD_NAMES;
20421
+ const fields = fieldNames.map((fieldName) => renderBuiltInField(fieldName, travelerValues, namePrefix)).filter(Boolean);
20361
20422
  if (!fields.length)
20362
20423
  return null;
20363
20424
  return React__default["default"].createElement("div", { className: "travelers-form__custom-fields-grid" }, fields);
@@ -20400,15 +20461,18 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
20400
20461
  return (React__default["default"].createElement("div", { className: "form__row" },
20401
20462
  React__default["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 })));
20402
20463
  }
20403
- // 'birthDate' (the legacy formFields type string) is normalized to the paxFields catalog's
20404
- // 'dateOfBirth' fieldName for config lookups.
20405
- const baseFieldNames = (travellersSettings?.formFields?.length ? travellersSettings.formFields.map((field) => field.type) : ['gender', 'firstName', 'lastName', 'dateOfBirth']).map((fieldName) => (fieldName === 'birthDate' ? 'dateOfBirth' : fieldName));
20406
20464
  const isMainBooker = travelerValues.id === formik.values.mainBookerId;
20407
- const extraFieldNames = getApplicableBuiltInPaxFields(paxFields, isMainBooker)
20408
- .map((field) => field.fieldName)
20409
- .filter((fieldName) => !baseFieldNames.includes(fieldName) && !ADDRESS_FIELD_NAMES.includes(fieldName));
20465
+ // With a Tide field configuration, exactly the configured fields render, in their configured order.
20466
+ // Without one, fall back to formFields ('birthDate', the legacy type string, maps to 'dateOfBirth').
20467
+ const fieldNames = hasConfiguredBuiltInPaxFields(paxFields)
20468
+ ? getApplicableBuiltInPaxFields(paxFields, isMainBooker)
20469
+ .map((field) => field.fieldName)
20470
+ .filter((fieldName) => !ADDRESS_FIELD_NAMES.includes(fieldName))
20471
+ : (travellersSettings?.formFields?.length
20472
+ ? travellersSettings.formFields.map((field) => field.type)
20473
+ : ['gender', 'firstName', 'lastName', 'dateOfBirth']).map((fieldName) => (fieldName === 'birthDate' ? 'dateOfBirth' : fieldName));
20410
20474
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
20411
- React__default["default"].createElement("div", { className: "travelers-form__custom-fields-grid" }, [...baseFieldNames, ...extraFieldNames].map((fieldName) => renderBuiltInField(fieldName, travelerValues, namePrefix))),
20475
+ React__default["default"].createElement("div", { className: "travelers-form__custom-fields-grid" }, fieldNames.map((fieldName) => renderBuiltInField(fieldName, travelerValues, namePrefix))),
20412
20476
  renderTravelerAddressFields(travelerValues, namePrefix),
20413
20477
  renderCustomFields(travelerValues, namePrefix),
20414
20478
  travelersFirstStep && isAdult && formik.values.rooms[roomIndex].adults.length > 1 && (React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleRemoveTraveler(roomIndex, travelerIndex) }, translations.TRAVELERS_FORM.REMOVE_TRAVELER))));
@@ -20446,7 +20510,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
20446
20510
  renderTravelerFields(travelerValues, `rooms[${roomIndex}].children[${index}]`, false, roomIndex, index)))),
20447
20511
  !useCompactForm && travelersFirstStep && (React__default["default"].createElement("div", { className: "form__region" },
20448
20512
  React__default["default"].createElement("button", { type: "button", className: "cta cta--select", onClick: () => handleAddTraveler(roomIndex) }, translations.TRAVELERS_FORM.ADD_TRAVELER)))))),
20449
- !useCompactForm && (bookingType !== 'b2b' || travellersSettings?.mainBookerFormFields?.length) ? (React__default["default"].createElement("div", { className: "form__region" },
20513
+ useCompactForm || hasConfiguredMainBookerFields(paxFields) ? null : bookingType !== 'b2b' || travellersSettings?.mainBookerFormFields?.length ? (React__default["default"].createElement("div", { className: "form__region" },
20450
20514
  React__default["default"].createElement("div", { className: "form__region-header" },
20451
20515
  React__default["default"].createElement("h5", { className: "form__region-heading" }, translations.TRAVELERS_FORM.MAIN_BOOKER),
20452
20516
  React__default["default"].createElement("p", { className: "form__region-label" }, lodash.compact([lodash.compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '), formatBirthDate$1(mainBooker?.birthDate)]).join(', '))),
@@ -20465,9 +20529,9 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
20465
20529
  React__default["default"].createElement("div", { className: "form__row" },
20466
20530
  React__default["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 }),
20467
20531
  React__default["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 }),
20468
- React__default["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 })))))) : !useCompactForm ? (React__default["default"].createElement("div", { className: "form__region" },
20532
+ React__default["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["default"].createElement("div", { className: "form__region" },
20469
20533
  React__default["default"].createElement("div", { className: "form__row" },
20470
- React__default["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 })))) : null,
20534
+ React__default["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 })))),
20471
20535
  !useCompactForm && showAgentSelection && (React__default["default"].createElement("div", { className: "form__region" },
20472
20536
  React__default["default"].createElement("div", { className: "form__region-header" },
20473
20537
  React__default["default"].createElement("h5", { className: "form__region-heading" }, translations.TRAVELERS_FORM.BOOK_WITH_AGENT),
@@ -21078,8 +21142,9 @@ const selectBookingPackagePax = toolkit.createSelector(selectTravelersFormValues
21078
21142
  formValues?.rooms.forEach((r) => r.adults.forEach((x) => {
21079
21143
  const adultPax = buildPax(x, formValues?.mainBookerId);
21080
21144
  if (adultPax.isMainBooker) {
21081
- adultPax.mobilePhone = formValues?.phone;
21082
- adultPax.email = formValues?.email;
21145
+ // Without the main booker block (Tide configures main-booker fields), keep the traveler's own details.
21146
+ adultPax.mobilePhone = formValues?.phone || adultPax.mobilePhone;
21147
+ adultPax.email = formValues?.email || adultPax.email;
21083
21148
  }
21084
21149
  pax.push(adultPax);
21085
21150
  }));
@@ -21103,8 +21168,8 @@ const selectBookingAddress = toolkit.createSelector(selectTravelersFormValues, s
21103
21168
  postalCode: mainBookerAddress?.postalCode || formValues?.zipCode,
21104
21169
  location: mainBookerAddress?.location || formValues?.place,
21105
21170
  country: mainBookerAddress?.country || formValues?.country,
21106
- mobilePhone: formValues?.phone,
21107
- email: formValues?.email
21171
+ mobilePhone: mainBooker.mobilePhone,
21172
+ email: mainBooker.email
21108
21173
  };
21109
21174
  });
21110
21175
  const selectSubFormAnswers = (state) => state.booking.subFormAnswers;
@@ -26241,6 +26306,10 @@ const Summary = () => {
26241
26306
  React__default["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))))));
26242
26307
  };
26243
26308
 
26309
+ // lodash isEmpty is true for every number, which would flag a filled-in preferredLanguageId as missing.
26310
+ function isBlank(value) {
26311
+ return value === null || value === undefined || (typeof value === 'string' && value.trim() === '');
26312
+ }
26244
26313
  function isValidEmail(email) {
26245
26314
  return !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,24}$/i.test(email);
26246
26315
  }
@@ -26264,9 +26333,14 @@ function getAge(birthDateText, startDateText) {
26264
26333
  }
26265
26334
  const validateForm = (values, agentRequired, bookingType, translations, formFields, mainBookerFormFields, maxChildAge = CHILD_MAX_AGE, maxBabyAge = BABY_MAX_AGE, paxFields = []) => {
26266
26335
  const errors = {};
26336
+ const usesFieldConfiguration = hasConfiguredBuiltInPaxFields(paxFields);
26337
+ const builtInFieldMeta = getBuiltInFieldControlMeta(translations);
26267
26338
  const isFormFieldPresent = (type) => {
26268
26339
  return formFields?.some((f) => f.type === type) ?? true;
26269
26340
  };
26341
+ // The 4 core fields: with a Tide field configuration they're required only when configured so for the
26342
+ // traveler's role (main booker / other pax); without one, whenever formFields lists them (legacy behavior).
26343
+ const isCoreFieldRequired = (traveler, legacyType, fieldName = legacyType) => usesFieldConfiguration ? !!getBuiltInPaxField(paxFields, fieldName, traveler.id === values.mainBookerId)?.isRequired : isFormFieldPresent(legacyType);
26270
26344
  const isMainBookerFormFieldPresent = (type) => {
26271
26345
  return mainBookerFormFields?.some((f) => f.type === type) ?? true;
26272
26346
  };
@@ -26318,16 +26392,16 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
26318
26392
  const fieldPath = BUILT_IN_FIELD_PATHS[field.fieldName];
26319
26393
  if (!fieldPath)
26320
26394
  return;
26321
- if (lodash.isEmpty(lodash.get(traveler, fieldPath))) {
26322
- lodash.set(errors, `${path}.${fieldPath}`, formatTravelerField(room, pax, field.fieldName));
26395
+ if (isBlank(lodash.get(traveler, fieldPath))) {
26396
+ lodash.set(errors, `${path}.${fieldPath}`, formatTravelerField(room, pax, builtInFieldMeta[field.fieldName]?.label ?? field.fieldName));
26323
26397
  }
26324
26398
  });
26325
26399
  };
26326
26400
  values.rooms.forEach((r, rIndex) => r.adults.forEach((adult, index) => {
26327
- if (lodash.isEmpty(adult.gender) && isFormFieldPresent('gender')) {
26401
+ if (lodash.isEmpty(adult.gender) && isCoreFieldRequired(adult, 'gender')) {
26328
26402
  lodash.set(errors, `rooms[${rIndex}].adults[${index}].gender`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.GENDER));
26329
26403
  }
26330
- if (isFormFieldPresent('firstName')) {
26404
+ if (isCoreFieldRequired(adult, 'firstName') || !lodash.isEmpty(adult.firstName)) {
26331
26405
  if (lodash.isEmpty(adult.firstName)) {
26332
26406
  lodash.set(errors, `rooms[${rIndex}].adults[${index}].firstName`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.FIRST_NAME));
26333
26407
  }
@@ -26335,7 +26409,7 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
26335
26409
  lodash.set(errors, `rooms[${rIndex}].adults[${index}].firstName`, formatTravelerFieldInvalidCharacters(rIndex + 1, index + 1, translations.TRAVELERS_FORM.FIRST_NAME));
26336
26410
  }
26337
26411
  }
26338
- if (isFormFieldPresent('lastName')) {
26412
+ if (isCoreFieldRequired(adult, 'lastName') || !lodash.isEmpty(adult.lastName)) {
26339
26413
  if (lodash.isEmpty(adult.lastName)) {
26340
26414
  lodash.set(errors, `rooms[${rIndex}].adults[${index}].lastName`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.LAST_NAME));
26341
26415
  }
@@ -26343,7 +26417,7 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
26343
26417
  lodash.set(errors, `rooms[${rIndex}].adults[${index}].lastName`, formatTravelerFieldInvalidCharacters(rIndex + 1, index + 1, translations.TRAVELERS_FORM.LAST_NAME));
26344
26418
  }
26345
26419
  }
26346
- if (isFormFieldPresent('birthDate')) {
26420
+ if (isCoreFieldRequired(adult, 'birthDate', 'dateOfBirth') || !lodash.isEmpty(adult.birthDate)) {
26347
26421
  if (lodash.isEmpty(adult.birthDate) || !isValidBirthDate(adult.birthDate)) {
26348
26422
  lodash.set(errors, `rooms[${rIndex}].adults[${index}].birthDate`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
26349
26423
  }
@@ -26359,10 +26433,10 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
26359
26433
  validateRequiredBuiltInFields(adult, `rooms[${rIndex}].adults[${index}]`, rIndex + 1, index + 1);
26360
26434
  }));
26361
26435
  values.rooms.forEach((r, rIndex) => r.children.forEach((child, index) => {
26362
- if (lodash.isEmpty(child.gender) && isFormFieldPresent('gender')) {
26436
+ if (lodash.isEmpty(child.gender) && isCoreFieldRequired(child, 'gender')) {
26363
26437
  lodash.set(errors, `rooms[${rIndex}].children[${index}].gender`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.GENDER));
26364
26438
  }
26365
- if (isFormFieldPresent('firstName')) {
26439
+ if (isCoreFieldRequired(child, 'firstName') || !lodash.isEmpty(child.firstName)) {
26366
26440
  if (lodash.isEmpty(child.firstName)) {
26367
26441
  lodash.set(errors, `rooms[${rIndex}].children[${index}].firstName`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.FIRST_NAME));
26368
26442
  }
@@ -26370,7 +26444,7 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
26370
26444
  lodash.set(errors, `rooms[${rIndex}].children[${index}].firstName`, formatTravelerFieldInvalidCharacters(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.FIRST_NAME));
26371
26445
  }
26372
26446
  }
26373
- if (isFormFieldPresent('lastName')) {
26447
+ if (isCoreFieldRequired(child, 'lastName') || !lodash.isEmpty(child.lastName)) {
26374
26448
  if (lodash.isEmpty(child.lastName)) {
26375
26449
  lodash.set(errors, `rooms[${rIndex}].children[${index}].lastName`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.LAST_NAME));
26376
26450
  }
@@ -26378,7 +26452,7 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
26378
26452
  lodash.set(errors, `rooms[${rIndex}].children[${index}].lastName`, formatTravelerFieldInvalidCharacters(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.LAST_NAME));
26379
26453
  }
26380
26454
  }
26381
- if (isFormFieldPresent('birthDate')) {
26455
+ if (isCoreFieldRequired(child, 'birthDate', 'dateOfBirth') || !lodash.isEmpty(child.birthDate)) {
26382
26456
  if (lodash.isEmpty(child.birthDate) || !isValidBirthDate(child.birthDate)) {
26383
26457
  lodash.set(errors, `rooms[${rIndex}].children[${index}].birthDate`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
26384
26458
  }
@@ -26404,7 +26478,8 @@ const validateForm = (values, agentRequired, bookingType, translations, formFiel
26404
26478
  if (values.mainBookerId < 0) {
26405
26479
  errors.mainBookerId = translations.TRAVELERS_FORM.VALIDATION.NO_MAIN_BOOKER_SELECTED;
26406
26480
  }
26407
- if (bookingType != 'b2b' || mainBookerFormFields?.length) {
26481
+ if (hasConfiguredMainBookerFields(paxFields)) ;
26482
+ else if (bookingType != 'b2b' || mainBookerFormFields?.length) {
26408
26483
  if (lodash.isEmpty(values.street) && isMainBookerFormFieldPresent('street')) {
26409
26484
  errors.street = format$1(translations.TRAVELERS_FORM.VALIDATION.MAIN_BOOKER_FIELD, [translations.TRAVELERS_FORM.STREET]);
26410
26485
  }
@@ -29183,6 +29258,7 @@ const initialState$1 = {
29183
29258
  bookingNumber: undefined,
29184
29259
  trajectNodeAvailability: {},
29185
29260
  trajectFlightAvailability: {},
29261
+ trajectTransferAvailability: {},
29186
29262
  trajectDaySuggestions: {},
29187
29263
  trajectEditLineGuid: null,
29188
29264
  trajectActivitiesDayOrder: null,
@@ -29395,6 +29471,12 @@ const searchResultsSlice = toolkit.createSlice({
29395
29471
  node.selectedCode = code;
29396
29472
  node.status = 'resolved';
29397
29473
  },
29474
+ clearTrajectNodeSelection(state, action) {
29475
+ const node = state.trajectNodeAvailability[action.payload];
29476
+ if (!node)
29477
+ return;
29478
+ node.selectedCode = null;
29479
+ },
29398
29480
  updateEditableEntryLine(state, action) {
29399
29481
  if (!state.editablePackagingEntry)
29400
29482
  return;
@@ -29408,6 +29490,32 @@ const searchResultsSlice = toolkit.createSlice({
29408
29490
  return;
29409
29491
  state.editablePackagingEntry.lines = [...(state.editablePackagingEntry.lines ?? []), ...action.payload];
29410
29492
  },
29493
+ upsertEditableEntryLines(state, action) {
29494
+ if (!state.editablePackagingEntry)
29495
+ return;
29496
+ const lines = [...(state.editablePackagingEntry.lines ?? [])];
29497
+ action.payload.forEach((line) => {
29498
+ const index = lines.findIndex((x) => x.guid === line.guid);
29499
+ if (index === -1)
29500
+ lines.push(line);
29501
+ else
29502
+ lines[index] = line;
29503
+ });
29504
+ state.editablePackagingEntry.lines = lines;
29505
+ const restored = new Set(action.payload.map((line) => line.trajectNodeId).filter((x) => x != null));
29506
+ if (restored.size && state.editablePackagingEntry.removedTrajectNodeIds?.length) {
29507
+ state.editablePackagingEntry.removedTrajectNodeIds = state.editablePackagingEntry.removedTrajectNodeIds.filter((x) => !restored.has(x));
29508
+ }
29509
+ },
29510
+ removeTrajectNode(state, action) {
29511
+ if (!state.editablePackagingEntry)
29512
+ return;
29513
+ const guids = new Set(action.payload.lineGuids);
29514
+ state.editablePackagingEntry.lines = (state.editablePackagingEntry.lines ?? []).filter((x) => !guids.has(x.guid));
29515
+ const removed = state.editablePackagingEntry.removedTrajectNodeIds ?? [];
29516
+ if (!removed.includes(action.payload.nodeId))
29517
+ state.editablePackagingEntry.removedTrajectNodeIds = [...removed, action.payload.nodeId];
29518
+ },
29411
29519
  removeEditableEntryLines(state, action) {
29412
29520
  if (!state.editablePackagingEntry)
29413
29521
  return;
@@ -29439,6 +29547,19 @@ const searchResultsSlice = toolkit.createSlice({
29439
29547
  flight.selectedGuid = action.payload.guid;
29440
29548
  flight.status = 'resolved';
29441
29549
  },
29550
+ clearTrajectFlightSelection(state, action) {
29551
+ const flight = state.trajectFlightAvailability[action.payload];
29552
+ if (!flight)
29553
+ return;
29554
+ flight.selectedGuid = null;
29555
+ },
29556
+ setTrajectTransferLoading(state, action) {
29557
+ state.trajectTransferAvailability[action.payload] = { nodeId: action.payload, status: 'loading', result: null };
29558
+ },
29559
+ setTrajectTransferAvailability(state, action) {
29560
+ const { nodeId, result } = action.payload;
29561
+ state.trajectTransferAvailability[nodeId] = { nodeId, status: result ? 'resolved' : 'unavailable', result };
29562
+ },
29442
29563
  setTrajectDaySuggestionsLoading(state, action) {
29443
29564
  const dayOrder = action.payload;
29444
29565
  state.trajectDaySuggestions[dayOrder] = {
@@ -29475,6 +29596,7 @@ const searchResultsSlice = toolkit.createSlice({
29475
29596
  resetTrajectAvailability(state) {
29476
29597
  state.trajectNodeAvailability = {};
29477
29598
  state.trajectFlightAvailability = {};
29599
+ state.trajectTransferAvailability = {};
29478
29600
  state.trajectDaySuggestions = {};
29479
29601
  state.trajectEditLineGuid = null;
29480
29602
  state.trajectActivitiesDayOrder = null;
@@ -29483,7 +29605,7 @@ const searchResultsSlice = toolkit.createSlice({
29483
29605
  }
29484
29606
  }
29485
29607
  });
29486
- 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;
29608
+ 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;
29487
29609
  var searchResultsReducer = searchResultsSlice.reducer;
29488
29610
 
29489
29611
  const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
@@ -29698,7 +29820,7 @@ const numberOfNights = (segment) => {
29698
29820
  const CHARTER_SERVICE_TYPE = 10;
29699
29821
  const CRUISE_SERVICE_TYPE = 11;
29700
29822
  const FERRY_SERVICE_TYPE = 12;
29701
- const TRANSFER_SERVICE_TYPE = 13;
29823
+ const TRANSFER_SERVICE_TYPE$1 = 13;
29702
29824
  const BUS_SERVICE_TYPE = 14;
29703
29825
  const CAR_SERVICE_TYPE = 17;
29704
29826
  const CAMPER_SERVICE_TYPE = 18;
@@ -29730,7 +29852,7 @@ const getServiceTypeIcon = (serviceType) => {
29730
29852
  return 'ui-bus';
29731
29853
  case TRAIN_SERVICE_TYPE:
29732
29854
  return 'ui-train';
29733
- case TRANSFER_SERVICE_TYPE:
29855
+ case TRANSFER_SERVICE_TYPE$1:
29734
29856
  return 'ui-transfer';
29735
29857
  case EVENT_SERVICE_TYPE:
29736
29858
  return 'ui-ticket';
@@ -29866,6 +29988,81 @@ const getTrajectNodesWithLines = (trajectEntry) => {
29866
29988
  })
29867
29989
  .filter((x) => !!x.line);
29868
29990
  };
29991
+ // Qite.Offer.Enums.ServiceType.Transfer: a traject's connecting node is booked as one.
29992
+ const TRANSFER_SERVICE_TYPE = 13;
29993
+ // Qite.Offer.Enums.RequirementType. A Mandatory (0) stop, and one RequiredInGroup (3) — one of the node's
29994
+ // alternatives has to be booked — always keep a selection; only these two may be taken off the trip.
29995
+ const OPTIONAL_REQUIREMENT_TYPE = 1;
29996
+ const OPTIONAL_IN_GROUP_REQUIREMENT_TYPE = 2;
29997
+ const canRemoveTrajectNode = (node) => node.requirementType === OPTIONAL_REQUIREMENT_TYPE || node.requirementType === OPTIONAL_IN_GROUP_REQUIREMENT_TYPE;
29998
+ const getDroppedTrajectNodeIds = (trajectEntry, entry) => {
29999
+ const dropped = new Set();
30000
+ if (!entry || !!trajectEntry.entry.magicLink)
30001
+ return dropped;
30002
+ const removed = new Set(entry.removedTrajectNodeIds ?? []);
30003
+ if (!removed.size)
30004
+ return dropped;
30005
+ const byDay = new Map();
30006
+ [...(trajectEntry.nodes ?? [])]
30007
+ .sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
30008
+ .forEach((node) => byDay.set(node.dayOrder, [...(byDay.get(node.dayOrder) ?? []), node]));
30009
+ byDay.forEach((nodes) => nodes.forEach((node, index) => {
30010
+ if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE || !canRemoveTrajectNode(node) || !removed.has(node.nodeId))
30011
+ return;
30012
+ dropped.add(node.nodeId);
30013
+ [nodes[index - 1], nodes[index + 1]]
30014
+ .filter((x) => !!x && x.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE)
30015
+ .forEach((x) => dropped.add(x.nodeId));
30016
+ }));
30017
+ return dropped;
30018
+ };
30019
+ const isTrajectTransferNode = (node) => node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE && !!node.pickUp && !!node.dropOff;
30020
+ const buildTrajectTransferRequest = (node, context) => ({
30021
+ transactionId: context.transactionId,
30022
+ officeId: context.officeId,
30023
+ agentId: context.agentId ?? null,
30024
+ catalogueId: context.catalogueId,
30025
+ searchConfigurationId: context.searchConfigurationId,
30026
+ language: context.language,
30027
+ date: node.from,
30028
+ pickUp: node.pickUp,
30029
+ dropOff: node.dropOff,
30030
+ travellers: context.rooms.flatMap((room) => room.travellers)
30031
+ });
30032
+ const buildTrajectTransferLine = (node, result) => {
30033
+ const option = result.rooms?.[0]?.options?.find((x) => x.isSelected) ?? result.rooms?.[0]?.options?.[0];
30034
+ if (!option?.guid)
30035
+ return null;
30036
+ return {
30037
+ guid: option.guid,
30038
+ parentGuid: null,
30039
+ order: 0,
30040
+ trajectNodeId: node.nodeId,
30041
+ isChanged: true,
30042
+ from: result.fromDate ?? node.from,
30043
+ to: result.toDate ?? node.to,
30044
+ moment: node.moment ?? '',
30045
+ serviceType: TRANSFER_SERVICE_TYPE,
30046
+ productName: result.name,
30047
+ productCode: result.code,
30048
+ accommodationName: option.accommodationName,
30049
+ accommodationCode: option.accommodationCode,
30050
+ regimeName: option.regimeName,
30051
+ regimeCode: option.regimeCode,
30052
+ vendor: result.vendorType ?? null,
30053
+ externalVendorId: result.vendorId ?? null,
30054
+ groupKey: '',
30055
+ isActive: true,
30056
+ country: result.countryId ? { id: result.countryId, name: result.countryName, localizations: [] } : null,
30057
+ region: result.regionId ? { id: result.regionId, name: result.regionName, localizations: [] } : null,
30058
+ oord: result.oordId ? { id: result.oordId, name: result.oordName, localizations: [] } : null,
30059
+ location: result.locationId ? { id: result.locationId, name: result.locationName, localizations: [] } : null,
30060
+ longitude: result.longitude ?? null,
30061
+ latitude: result.latitude ?? null,
30062
+ pax: (option.paxIds ?? []).map((paxId, paxIndex) => ({ paxId, room: 0, order: paxIndex })),
30063
+ flightInformation: null
30064
+ };
30065
+ };
29869
30066
  const getCoordinates = (line, node, result) => {
29870
30067
  const sources = [
29871
30068
  ...(result ? [result] : []),
@@ -30293,9 +30490,15 @@ const nightsOf = (node, line) => dateFns.differenceInCalendarDays(new Date(line?
30293
30490
  const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
30294
30491
  const timeline = React.useMemo(() => {
30295
30492
  const linesByGuid = new Map((packagingEntry.lines ?? []).map((line) => [line.guid, line]));
30493
+ const dropped = getDroppedTrajectNodeIds(trajectEntry, packagingEntry);
30296
30494
  const nodes = [...(trajectEntry.nodes ?? [])]
30495
+ .filter((node) => !dropped.has(node.nodeId))
30297
30496
  .sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
30298
30497
  .map((node) => {
30498
+ // A connecting node has no line of its own; the transfer found for it carries its node id.
30499
+ if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) {
30500
+ return { node, lines: (packagingEntry.lines ?? []).filter((line) => line.trajectNodeId === node.nodeId) };
30501
+ }
30299
30502
  const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
30300
30503
  const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
30301
30504
  const groupKey = lines.length ? getLineGroupKey(lines[0]) : '';
@@ -30365,10 +30568,11 @@ const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
30365
30568
  React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-date-date" },
30366
30569
  React__default["default"].createElement("strong", null, dateFns.format(new Date(value), 'd'))),
30367
30570
  React__default["default"].createElement("p", null, dateFns.format(new Date(value), 'MMM'))));
30368
- const renderConnectingNode = ({ node }) => {
30571
+ const renderConnectingNode = ({ node, lines }) => {
30369
30572
  const distance = node.distanceInKm ? `${Math.round(node.distanceInKm)} km` : '';
30370
30573
  // How long the hop takes and how far it is, the way the traject editor puts them.
30371
30574
  const journey = [getTrajectNodeDuration(node), distance].filter(Boolean).join(' · ');
30575
+ const title = lines[0]?.productName || node.name;
30372
30576
  return (React__default["default"].createElement("div", { className: "search__filter__itinerary__transport", key: `connect-${node.nodeId}` },
30373
30577
  React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline" },
30374
30578
  React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-timeline-line" })),
@@ -30377,8 +30581,8 @@ const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
30377
30581
  React__default["default"].createElement("div", { className: badgeClass('transport', getTrajectNodeBadgeModifier(node)) },
30378
30582
  React__default["default"].createElement(Icon, { name: getTrajectNodeIcon(node), height: 15 })),
30379
30583
  React__default["default"].createElement("div", { className: "search__filter__itinerary__transport-details" },
30380
- React__default["default"].createElement("h6", null, node.name || journey),
30381
- !!node.name && !!journey && React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" }, journey)))));
30584
+ React__default["default"].createElement("h6", null, title || journey),
30585
+ !!title && !!journey && React__default["default"].createElement("p", { className: "search__filter__itinerary__transport-details-time" }, journey)))));
30382
30586
  };
30383
30587
  const renderOvernight = (item, index) => {
30384
30588
  const { dayOrder } = item.node;
@@ -30491,8 +30695,13 @@ const TrajectItinerary = ({ trajectEntry, packagingEntry, translations }) => {
30491
30695
  */
30492
30696
  const isBookedEntryTraject = (trajectEntry) => !!trajectEntry?.entry?.magicLink;
30493
30697
  const NODE_SERVICE_TYPES = new Set([ACCOMMODATION_SERVICE_TYPE, EXCURSION_SERVICE_TYPE, FLIGHT_SERVICE_TYPE]);
30698
+ const isBookedRoomLineOf = (line, root) => !!line.parentGuid &&
30699
+ (line.parentGuid === root.guid || line.parentGuid === root.parentGuid) &&
30700
+ line.serviceType === root.serviceType &&
30701
+ line.productCode === root.productCode &&
30702
+ dateFns.isSameDay(new Date(line.from), new Date(root.from));
30494
30703
  /**
30495
- * A room or option line joins its parent's node; a flight line never does — an outward and its
30704
+ * A room or option line joins its service's node; a flight line never does — an outward and its
30496
30705
  * return are separate legs of the trip even though the return points at the outward as parent.
30497
30706
  *
30498
30707
  * Lines that share a group key are one choice (a couple of hotels of which one is picked), so
@@ -30500,16 +30709,13 @@ const NODE_SERVICE_TYPES = new Set([ACCOMMODATION_SERVICE_TYPE, EXCURSION_SERVIC
30500
30709
  */
30501
30710
  const groupNodeLines = (lines) => {
30502
30711
  const groups = [];
30503
- const groupByGuid = new Map();
30504
30712
  lines.forEach((line) => {
30505
- const parentGroup = line.serviceType !== FLIGHT_SERVICE_TYPE && line.parentGuid ? groupByGuid.get(line.parentGuid) : undefined;
30506
- if (parentGroup) {
30507
- parentGroup.push(line);
30713
+ const service = line.serviceType !== FLIGHT_SERVICE_TYPE ? groups.find((group) => isBookedRoomLineOf(line, group[0])) : undefined;
30714
+ if (service) {
30715
+ service.push(line);
30508
30716
  return;
30509
30717
  }
30510
- const group = [line];
30511
- groups.push(group);
30512
- groupByGuid.set(line.guid, group);
30718
+ groups.push([line]);
30513
30719
  });
30514
30720
  const nodes = [];
30515
30721
  const nodeByGroupKey = new Map();
@@ -30566,11 +30772,13 @@ const buildBookedTrajectEntry = (entry) => {
30566
30772
  date: toDateOnlyString(dateFns.addDays(start, order))
30567
30773
  }));
30568
30774
  const dayOrderOf = (line) => Math.max(dateFns.differenceInCalendarDays(new Date(line.from), start), 0);
30569
- // Services starting the same day are laid out by where they end: the one that ends soonest opens
30570
- // the day (an outward flight on 2 Oct) and the one that spans further follows (the stay until 9 Oct).
30775
+ const flights = lodash.sortBy(lines.filter((line) => line.serviceType === FLIGHT_SERVICE_TYPE && isActiveEntryLine(line)), (line) => new Date(line.from).getTime(), (line) => line.order);
30776
+ const outward = lodash.first(flights);
30777
+ const homeward = flights.length > 1 ? lodash.last(flights) : undefined;
30778
+ const placeInDay = (line) => (line === outward ? -1 : line === homeward ? 1 : 0);
30571
30779
  const orderedGroups = groupNodeLines(lines).sort((a, b) => {
30572
30780
  const [lineA, lineB] = [a.lines[0], b.lines[0]];
30573
- return dayOrderOf(lineA) - dayOrderOf(lineB) || new Date(lineA.to).getTime() - new Date(lineB.to).getTime() || lineA.order - lineB.order;
30781
+ return dayOrderOf(lineA) - dayOrderOf(lineB) || placeInDay(lineA) - placeInDay(lineB) || lineA.order - lineB.order;
30574
30782
  });
30575
30783
  const nodes = orderedGroups.map(({ lines: groupLines, alternatives }, index) => {
30576
30784
  const line = groupLines[0];
@@ -33629,7 +33837,7 @@ const TrajectResultsFlyIn = ({ isLoading }) => {
33629
33837
  if (!result)
33630
33838
  return;
33631
33839
  dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
33632
- buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? []).forEach((line) => dispatch(updateEditableEntryLine(line)));
33840
+ dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
33633
33841
  dispatch(setTrajectEditLineGuid(null));
33634
33842
  dispatch(setFlyInIsOpen(false));
33635
33843
  };
@@ -33674,7 +33882,7 @@ const TrajectFlightsFlyIn = ({ isLoading }) => {
33674
33882
  if (!flight)
33675
33883
  return;
33676
33884
  dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
33677
- dispatch(updateEditableEntryLine(applyFlightToLine(item.line, flight)));
33885
+ dispatch(upsertEditableEntryLines([applyFlightToLine(item.line, flight)]));
33678
33886
  dispatch(setTrajectEditLineGuid(null));
33679
33887
  dispatch(setFlyInIsOpen(false));
33680
33888
  };
@@ -33737,10 +33945,16 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
33737
33945
  if (!item)
33738
33946
  return;
33739
33947
  dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code: result.code }));
33740
- buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? []).forEach((line) => dispatch(updateEditableEntryLine(line)));
33948
+ dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
33741
33949
  dispatch(setTrajectActivitiesDayOrder(null));
33742
33950
  dispatch(setFlyInIsOpen(false));
33743
33951
  };
33952
+ const handleRemove = ({ item }) => {
33953
+ if (!item)
33954
+ return;
33955
+ dispatch(clearTrajectNodeSelection(item.line.guid));
33956
+ dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
33957
+ };
33744
33958
  if (isLoading || suggestions?.status === 'loading') {
33745
33959
  return React__default["default"].createElement(React__default["default"].Fragment, null, context.customSpinner ?? React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACTIVITIES }));
33746
33960
  }
@@ -33754,7 +33968,8 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
33754
33968
  const { result, item } = card;
33755
33969
  const isSelected = item ? trajectNodeAvailability[item.line.guid]?.selectedCode === result.code : getAddedLines(result).length > 0;
33756
33970
  const isBookedCard = !!item && bookedMode && matchesCode(result, item.node.code ?? '');
33757
- return (React__default["default"].createElement(TrajectNodeCard, { key: `activity-${result.code}`, node: item?.node, result: result, isSelected: isSelected, languageCode: context.languageCode, translations: translations, onSelect: () => (item ? handleSelect(card) : handleToggle(result)), selectLabel: item ? undefined : translations.SRP.ADD, selectedLabel: item ? (isBookedCard ? translations.SHARED.BOOKED : undefined) : translations.SRP.REMOVE }));
33971
+ const canRemove = !!item && isSelected && !bookedMode && canRemoveTrajectNode(item.node);
33972
+ return (React__default["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 }));
33758
33973
  })))));
33759
33974
  };
33760
33975
 
@@ -36148,16 +36363,18 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36148
36363
  apiKey: context.tideConnection.apiKey
36149
36364
  };
36150
36365
  }, [context]);
36366
+ // The field configuration on the entry is leading for which traveler fields are shown and required.
36367
+ const paxFields = editablePackagingEntry?.paxFields ?? [];
36151
36368
  const initialValues = React.useMemo(() => {
36152
36369
  if (!editablePackagingEntry) {
36153
36370
  return {};
36154
36371
  }
36155
- return createInitialValuesFromEditablePackagingEntry(editablePackagingEntry);
36372
+ return createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, undefined, paxFields);
36156
36373
  }, [editablePackagingEntry?.transactionId]);
36157
36374
  const formik$1 = formik.useFormik({
36158
36375
  initialValues,
36159
36376
  enableReinitialize: true,
36160
- validate: (values) => validateForm(values, false, 'b2c', translations, travellersSettings.formFields, travellersSettings.mainBookerFormFields, undefined, undefined, editablePackagingEntry?.paxFields ?? []),
36377
+ validate: (values) => validateForm(values, false, 'b2c', translations, travellersSettings.formFields, travellersSettings.mainBookerFormFields, undefined, undefined, paxFields),
36161
36378
  onSubmit: (values) => {
36162
36379
  if (!editablePackagingEntry)
36163
36380
  return;
@@ -36242,7 +36459,7 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36242
36459
  ".\u00A0",
36243
36460
  stepLabels[step])) },
36244
36461
  isConfirmationPage && isLoading && React__default["default"].createElement(Spinner, { label: translations.SUMMARY.PROCESS_BOOKING }),
36245
- currentStep === 0 && (React__default["default"].createElement(SharedTravelersForm, { formik: formik$1, translations: translations, travellersSettings: travellersSettings, countries: countries, travelersFirstStep: false, isUnavailable: false, useCompactForm: false, showAgentSelection: false, paxFields: editablePackagingEntry?.paxFields ?? [] })),
36462
+ currentStep === 0 && (React__default["default"].createElement(SharedTravelersForm, { formik: formik$1, translations: translations, travellersSettings: travellersSettings, countries: countries, travelersFirstStep: false, isUnavailable: false, useCompactForm: false, showAgentSelection: false, paxFields: paxFields })),
36246
36463
  currentStep === 1 && (React__default["default"].createElement(SharedSummary, { translations: translations, travelerFormValues: formik$1.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["default"].createElement("button", { type: "button", title: translations.STEPS.PREVIOUS, onClick: () => dispatch(setCurrentStep(0)), className: "cta cta--secondary" }, translations.STEPS.PREVIOUS)) })),
36247
36464
  currentStep === 2 && (React__default["default"].createElement(SharedConfirmation, { bookingNumber: bookingNumber ?? editablePackagingEntry?.dossierNumber ?? '', isOption: false, isOffer: false, translations: translations.CONFIRMATION })),
36248
36465
  currentStep === 3 && React__default["default"].createElement("div", null))),
@@ -36342,11 +36559,13 @@ const TrajectResults = ({ isLoading }) => {
36342
36559
  });
36343
36560
  return map;
36344
36561
  }, [trajectEntry]);
36562
+ const droppedNodeIds = trajectEntry ? getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry) : new Set();
36563
+ const droppedKey = [...droppedNodeIds].sort((a, b) => a - b).join(',');
36345
36564
  const { stayByDay, stayStartByNodeId } = React.useMemo(() => {
36346
36565
  const byDay = new Map();
36347
36566
  const byNodeId = new Map();
36348
36567
  nodesWithLines
36349
- .filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE)
36568
+ .filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE && !droppedNodeIds.has(x.node.nodeId))
36350
36569
  .forEach((item) => {
36351
36570
  const nights = Math.max(dateFns.differenceInCalendarDays(new Date(item.line.to), new Date(item.line.from)), 1);
36352
36571
  byNodeId.set(item.node.nodeId, { item, night: 0, nights });
@@ -36355,7 +36574,7 @@ const TrajectResults = ({ isLoading }) => {
36355
36574
  }
36356
36575
  });
36357
36576
  return { stayByDay: byDay, stayStartByNodeId: byNodeId };
36358
- }, [nodesWithLines]);
36577
+ }, [nodesWithLines, droppedKey]);
36359
36578
  /** Whether every node of the trip has an answer, one way or the other. */
36360
36579
  const tripResolved = React.useMemo(() => nodesWithLines
36361
36580
  .filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node))
@@ -36382,7 +36601,20 @@ const TrajectResults = ({ isLoading }) => {
36382
36601
  if (!result)
36383
36602
  return;
36384
36603
  dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
36385
- buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? []).forEach((line) => dispatch(updateEditableEntryLine(line)));
36604
+ dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
36605
+ };
36606
+ /**
36607
+ * An optional stop can be taken off the trip; a mandatory one, or one of which the group needs a pick,
36608
+ * only swapped. A booked entry's services are never taken off here.
36609
+ */
36610
+ const canRemove = (item) => !bookedMode && canRemoveTrajectNode(item.node);
36611
+ const handleRemoveNode = (item) => {
36612
+ dispatch(clearTrajectNodeSelection(item.line.guid));
36613
+ dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
36614
+ };
36615
+ const handleRemoveFlight = (item) => {
36616
+ dispatch(clearTrajectFlightSelection(item.line.guid));
36617
+ dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
36386
36618
  };
36387
36619
  /** The remaining legs for a flight node open in the fly-in, as the regular flight results do. */
36388
36620
  const handleShowMoreFlights = (item) => {
@@ -36450,9 +36682,17 @@ const TrajectResults = ({ isLoading }) => {
36450
36682
  // A hotel row holds the hotels configured on the node; a portal can also let it fill up with
36451
36683
  // non-preferred hotels found in the area, the rest staying behind "show more".
36452
36684
  const fill = showNonPreferredHotels && node.serviceType === ACCOMMODATION_SERVICE_TYPE ? others.slice(0, Math.max(MAX_HOTEL_RESULTS - 1 - configured.length, 0)) : [];
36453
- const remaining = others.length - fill.length;
36454
36685
  // No entry yet means the node's search has been queued but not dispatched — still loading.
36455
36686
  const status = availability?.status ?? 'loading';
36687
+ // A stop taken off the trip keeps its results in view, to pick one back.
36688
+ const isRemoved = status === 'resolved' && !selected && results.length > 0;
36689
+ const removable = canRemove(item);
36690
+ const shown = selected
36691
+ ? [selected, ...configured, ...fill]
36692
+ : configured.length || fill.length
36693
+ ? [...configured, ...fill]
36694
+ : others.slice(0, MAX_HOTEL_RESULTS);
36695
+ const remaining = results.length - shown.length;
36456
36696
  return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
36457
36697
  showHeader && (React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
36458
36698
  React__default["default"].createElement("div", { className: "search__results__label__date" },
@@ -36464,13 +36704,18 @@ const TrajectResults = ({ isLoading }) => {
36464
36704
  React__default["default"].createElement("strong", null, headerTitle || node.name))))),
36465
36705
  status === 'loading' && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }),
36466
36706
  status === 'unavailable' && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
36467
- status === 'resolved' && selected && (React__default["default"].createElement(React__default["default"].Fragment, null,
36468
- availability.preferredUnavailable && node.preferredAccommodationName && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
36707
+ status === 'resolved' && (selected || isRemoved) && (React__default["default"].createElement(React__default["default"].Fragment, null,
36708
+ selected && availability.preferredUnavailable && node.preferredAccommodationName && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
36469
36709
  React__default["default"].createElement("span", null,
36470
36710
  node.preferredAccommodationName,
36471
36711
  " \u2014 ",
36472
36712
  translations.SRP.NO_RESULTS))),
36473
- React__default["default"].createElement(TrajectCardRow, { selectionKey: selected.code }, [selected, ...configured, ...fill].map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, node: node, result: result, isSelected: result.code === selected.code, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, result.code), onEditOptions: node.serviceType === ACCOMMODATION_SERVICE_TYPE ? () => handleEditOptions(item) : undefined, showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE })))),
36713
+ React__default["default"].createElement(TrajectCardRow, { selectionKey: selected?.code ?? '' }, shown.map((result) => {
36714
+ const isSelected = result.code === selected?.code;
36715
+ return (React__default["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,
36716
+ // selectedLabel={bookedMode && matchesCode(result, node.code ?? '') ? translations.SHARED.BOOKED : undefined}
36717
+ selectedLabel: removable ? translations.SRP.REMOVE : undefined }));
36718
+ })),
36474
36719
  remaining > 0 && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
36475
36720
  React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMore(item) },
36476
36721
  translations.SRP.SHOW_MORE,
@@ -36480,7 +36725,7 @@ const TrajectResults = ({ isLoading }) => {
36480
36725
  };
36481
36726
  const handleSelectFlight = (item, flight) => {
36482
36727
  dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
36483
- dispatch(updateEditableEntryLine(buildFlightSelectionLine(item, flight, bookedMode)));
36728
+ dispatch(upsertEditableEntryLines([buildFlightSelectionLine(item, flight, bookedMode)]));
36484
36729
  };
36485
36730
  const renderFlightNode = (item) => {
36486
36731
  const { node, line } = item;
@@ -36489,7 +36734,9 @@ const TrajectResults = ({ isLoading }) => {
36489
36734
  const flights = availability?.results ?? [];
36490
36735
  const selected = flights.find((x) => x.outwardGuid === availability?.selectedGuid) ?? null;
36491
36736
  const others = flights.filter((x) => x.outwardGuid !== availability?.selectedGuid);
36492
- const visible = others.slice(0, 2);
36737
+ const isRemoved = status === 'resolved' && !selected && flights.length > 0;
36738
+ const removable = canRemove(item);
36739
+ const visible = others.slice(0, selected ? 2 : 3);
36493
36740
  return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
36494
36741
  React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
36495
36742
  React__default["default"].createElement("div", { className: "search__results__label__date" },
@@ -36504,15 +36751,15 @@ const TrajectResults = ({ isLoading }) => {
36504
36751
  node.arrivalAirportCode)))),
36505
36752
  status === 'loading' && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS }),
36506
36753
  status === 'unavailable' && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
36507
- status === 'resolved' && selected && (React__default["default"].createElement(React__default["default"].Fragment, null,
36754
+ status === 'resolved' && (selected || isRemoved) && (React__default["default"].createElement(React__default["default"].Fragment, null,
36508
36755
  React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" },
36509
- React__default["default"].createElement(IndependentFlightOption, { key: `flight-${selected.outwardGuid}`, item: selected.outward, guid: selected.outwardGuid, selectedGuid: selected.outwardGuid, isOutward: true, showSelectedState: true, price: selected.price, selectedLabel: bookedMode && selected.outwardGuid === line.guid ? translations.SHARED.BOOKED : undefined }),
36510
- visible.map((result) => (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: result.outward, guid: result.outwardGuid, isOutward: true, price: result.price, currentSelectedPrice: selected.price, onSelect: () => handleSelectFlight(item, result) })))),
36511
- others.length > 2 && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
36756
+ selected && (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${selected.outwardGuid}`, item: selected.outward, guid: selected.outwardGuid, selectedGuid: selected.outwardGuid, isOutward: true, showSelectedState: true, price: selected.price, onSelect: removable ? () => handleRemoveFlight(item) : undefined, selectedLabel: bookedMode && selected.outwardGuid === line.guid ? translations.SHARED.BOOKED : removable ? translations.SRP.REMOVE : undefined })),
36757
+ visible.map((result) => (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: result.outward, guid: result.outwardGuid, isOutward: true, price: result.price, currentSelectedPrice: selected?.price, onSelect: () => handleSelectFlight(item, result) })))),
36758
+ others.length > visible.length && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
36512
36759
  React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreFlights(item) },
36513
36760
  translations.SRP.SHOW_MORE,
36514
36761
  " (",
36515
- others.length - 2,
36762
+ others.length - visible.length,
36516
36763
  ")")))))));
36517
36764
  };
36518
36765
  /** The lines a suggested activity put on the entry, which is also how we know it was added. */
@@ -36588,13 +36835,21 @@ const TrajectResults = ({ isLoading }) => {
36588
36835
  ")")))));
36589
36836
  };
36590
36837
  /**
36591
- * An activity the traject books is swapped for another, the way a hotel is; one the traveller
36592
- * found for themselves is added to the trip or taken back off it.
36838
+ * An activity the traject books is swapped for another, the way a hotel is, or - when it is optional -
36839
+ * taken off the trip; one the traveller found for themselves is added to the trip or taken back off it.
36593
36840
  */
36594
36841
  const renderActivityCard = ({ result, item }) => {
36595
36842
  const isSelected = isActivitySelected({ result, item });
36596
36843
  const isBookedCard = !!item && bookedMode && matchesCode(result, item.node.code ?? '');
36597
- return (React__default["default"].createElement(TrajectNodeCard, { key: `activity-${result.code}`, node: item?.node, result: result, isSelected: isSelected, languageCode: context.languageCode, translations: translations, onSelect: () => (item ? handleSelect(item, result.code) : handleToggleActivity(result)), selectLabel: item ? undefined : translations.SRP.ADD, selectedLabel: item ? (isBookedCard ? translations.SHARED.BOOKED : undefined) : translations.SRP.REMOVE }));
36844
+ const removable = !!item && canRemove(item);
36845
+ const handleClick = () => {
36846
+ if (!item)
36847
+ return handleToggleActivity(result);
36848
+ if (isSelected && removable)
36849
+ return handleRemoveNode(item);
36850
+ handleSelect(item, result.code);
36851
+ };
36852
+ return (React__default["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 }));
36598
36853
  };
36599
36854
  if (isLoading)
36600
36855
  return React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
@@ -36629,6 +36884,7 @@ const TrajectResults = ({ isLoading }) => {
36629
36884
  const useTrajectAvailability = () => {
36630
36885
  const context = React.useContext(SearchResultsConfigurationContext);
36631
36886
  const dispatch = reactRedux.useDispatch();
36887
+ const { editablePackagingEntry, trajectTransferAvailability } = reactRedux.useSelector((state) => state.searchResults);
36632
36888
  const trajectEntry = context?.trajectEntry;
36633
36889
  const showApiItems = !!context?.showApiItems;
36634
36890
  const hideShowMoreResults = !!context?.hideShowMoreResults;
@@ -36654,12 +36910,14 @@ const useTrajectAvailability = () => {
36654
36910
  rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
36655
36911
  };
36656
36912
  // Flight searches take passengers rather than rooms; the API buckets by age.
36913
+ // The ids must be the entry's own pax ids: the flight lines this search caches carry them as
36914
+ // EntryLinePax.PaxId, and booking maps those onto the entry's travelers (PackagingService.SyncPaxIds).
36657
36915
  const pax = trajectEntry.entry.pax ?? [];
36658
36916
  const ageOf = (p) => p.age ?? 30;
36659
- const adults = pax.filter((p) => ageOf(p) >= 12).length;
36660
- const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12).length;
36661
- const babies = pax.filter((p) => ageOf(p) < 2).length;
36662
- const flightPax = lodash.concat(Array.from({ length: adults }, (_, index) => ({ id: index, age: 31 })), Array.from({ length: kids }, (_, index) => ({ id: index + adults, age: 8 })), Array.from({ length: babies }, (_, index) => ({ id: index + adults + kids, age: 1 })));
36917
+ const adults = pax.filter((p) => ageOf(p) >= 12);
36918
+ const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12);
36919
+ const babies = pax.filter((p) => ageOf(p) < 2);
36920
+ const flightPax = lodash.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 })));
36663
36921
  const nodesWithLines = getTrajectNodesWithLines(trajectEntry);
36664
36922
  // The other members of a line group are alternatives whether or not a search still finds
36665
36923
  // them; the ones it does not are drawn from their lines, as the booked service itself is.
@@ -36790,9 +37048,30 @@ const useTrajectAvailability = () => {
36790
37048
  return item.lines.map((x) => x.guid);
36791
37049
  }
36792
37050
  };
37051
+ const resolveTransferNode = async (node) => {
37052
+ dispatch(setTrajectTransferLoading(node.nodeId));
37053
+ try {
37054
+ const results = (await build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, searchContext), controller.signal)) ?? [];
37055
+ if (controller.signal.aborted)
37056
+ return [];
37057
+ const result = results[0] ?? null;
37058
+ dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: result && buildTrajectTransferLine(node, result) ? result : null }));
37059
+ }
37060
+ catch (error) {
37061
+ if (controller.signal.aborted)
37062
+ return [];
37063
+ console.error('Failed to resolve traject transfer', node.name, error);
37064
+ dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: null }));
37065
+ }
37066
+ return [];
37067
+ };
36793
37068
  (async () => {
36794
37069
  const resolvable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node));
36795
- const unresolved = (await Promise.all(resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))))).flat();
37070
+ const transfers = bookedMode ? [] : (trajectEntry.nodes ?? []).filter(isTrajectTransferNode);
37071
+ const unresolved = (await Promise.all([
37072
+ ...resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))),
37073
+ ...transfers.map(resolveTransferNode)
37074
+ ])).flat();
36796
37075
  if (controller.signal.aborted)
36797
37076
  return;
36798
37077
  dispatch(setTrajectResolving(false));
@@ -36804,6 +37083,28 @@ const useTrajectAvailability = () => {
36804
37083
  })();
36805
37084
  return () => controller.abort();
36806
37085
  }, [trajectEntry, showApiItems, hideShowMoreResults]);
37086
+ React.useEffect(() => {
37087
+ if (!trajectEntry || !editablePackagingEntry || isBookedEntryTraject(trajectEntry))
37088
+ return;
37089
+ const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
37090
+ const guids = new Set((editablePackagingEntry.lines ?? []).map((line) => line.guid));
37091
+ const toAdd = [];
37092
+ const toRemove = [];
37093
+ (trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
37094
+ const result = trajectTransferAvailability[node.nodeId]?.result;
37095
+ const line = result ? buildTrajectTransferLine(node, result) : null;
37096
+ if (!line)
37097
+ return;
37098
+ if (dropped.has(node.nodeId) && guids.has(line.guid))
37099
+ toRemove.push(line.guid);
37100
+ if (!dropped.has(node.nodeId) && !guids.has(line.guid))
37101
+ toAdd.push(line);
37102
+ });
37103
+ if (toRemove.length)
37104
+ dispatch(removeEditableEntryLines(toRemove));
37105
+ if (toAdd.length)
37106
+ dispatch(addEditableEntryLines(toAdd));
37107
+ }, [trajectEntry, editablePackagingEntry, trajectTransferAvailability]);
36807
37108
  };
36808
37109
 
36809
37110
  const SearchResultsContainer = ({ onBookingStarted }) => {
@@ -37108,10 +37409,9 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37108
37409
  const item = getTrajectNodesWithLines(context.trajectEntry).find((x) => x.line.guid === trajectEditLineGuid);
37109
37410
  const result = packagingAccoSearchDetails.find((x) => x.code === selectedPackagingAccoResultCode);
37110
37411
  if (item && result) {
37111
- const currentLines = item.lines
37112
- .map((line) => editablePackagingEntry?.lines?.find((x) => x.guid === line.guid))
37113
- .filter((x) => !!x);
37114
- applyResultToLines(currentLines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
37412
+ // A stop the traveller took off has no lines left on the entry: the traject's own put it back.
37413
+ const currentLines = item.lines.map((line) => editablePackagingEntry?.lines?.find((x) => x.guid === line.guid) ?? line);
37414
+ dispatch(upsertEditableEntryLines(applyResultToLines(currentLines, result)));
37115
37415
  dispatch(updateTrajectNodeResult({ lineGuid: trajectEditLineGuid, result }));
37116
37416
  }
37117
37417
  dispatch(setTrajectEditLineGuid(null));