@qite/tide-components 1.0.14-preview.3 → 1.0.14-preview.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -33629,7 +33833,7 @@ const TrajectResultsFlyIn = ({ isLoading }) => {
33629
33833
  if (!result)
33630
33834
  return;
33631
33835
  dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
33632
- buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? []).forEach((line) => dispatch(updateEditableEntryLine(line)));
33836
+ dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
33633
33837
  dispatch(setTrajectEditLineGuid(null));
33634
33838
  dispatch(setFlyInIsOpen(false));
33635
33839
  };
@@ -33674,7 +33878,7 @@ const TrajectFlightsFlyIn = ({ isLoading }) => {
33674
33878
  if (!flight)
33675
33879
  return;
33676
33880
  dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
33677
- dispatch(updateEditableEntryLine(applyFlightToLine(item.line, flight)));
33881
+ dispatch(upsertEditableEntryLines([applyFlightToLine(item.line, flight)]));
33678
33882
  dispatch(setTrajectEditLineGuid(null));
33679
33883
  dispatch(setFlyInIsOpen(false));
33680
33884
  };
@@ -33737,10 +33941,16 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
33737
33941
  if (!item)
33738
33942
  return;
33739
33943
  dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code: result.code }));
33740
- buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? []).forEach((line) => dispatch(updateEditableEntryLine(line)));
33944
+ dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
33741
33945
  dispatch(setTrajectActivitiesDayOrder(null));
33742
33946
  dispatch(setFlyInIsOpen(false));
33743
33947
  };
33948
+ const handleRemove = ({ item }) => {
33949
+ if (!item)
33950
+ return;
33951
+ dispatch(clearTrajectNodeSelection(item.line.guid));
33952
+ dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
33953
+ };
33744
33954
  if (isLoading || suggestions?.status === 'loading') {
33745
33955
  return React__default["default"].createElement(React__default["default"].Fragment, null, context.customSpinner ?? React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACTIVITIES }));
33746
33956
  }
@@ -33754,7 +33964,8 @@ const TrajectDayActivitiesFlyIn = ({ isLoading }) => {
33754
33964
  const { result, item } = card;
33755
33965
  const isSelected = item ? trajectNodeAvailability[item.line.guid]?.selectedCode === result.code : getAddedLines(result).length > 0;
33756
33966
  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 }));
33967
+ const canRemove = !!item && isSelected && !bookedMode && canRemoveTrajectNode(item.node);
33968
+ 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
33969
  })))));
33759
33970
  };
33760
33971
 
@@ -36148,16 +36359,18 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36148
36359
  apiKey: context.tideConnection.apiKey
36149
36360
  };
36150
36361
  }, [context]);
36362
+ // The field configuration on the entry is leading for which traveler fields are shown and required.
36363
+ const paxFields = editablePackagingEntry?.paxFields ?? [];
36151
36364
  const initialValues = React.useMemo(() => {
36152
36365
  if (!editablePackagingEntry) {
36153
36366
  return {};
36154
36367
  }
36155
- return createInitialValuesFromEditablePackagingEntry(editablePackagingEntry);
36368
+ return createInitialValuesFromEditablePackagingEntry(editablePackagingEntry, undefined, paxFields);
36156
36369
  }, [editablePackagingEntry?.transactionId]);
36157
36370
  const formik$1 = formik.useFormik({
36158
36371
  initialValues,
36159
36372
  enableReinitialize: true,
36160
- validate: (values) => validateForm(values, false, 'b2c', translations, travellersSettings.formFields, travellersSettings.mainBookerFormFields, undefined, undefined, editablePackagingEntry?.paxFields ?? []),
36373
+ validate: (values) => validateForm(values, false, 'b2c', translations, travellersSettings.formFields, travellersSettings.mainBookerFormFields, undefined, undefined, paxFields),
36161
36374
  onSubmit: (values) => {
36162
36375
  if (!editablePackagingEntry)
36163
36376
  return;
@@ -36242,7 +36455,7 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
36242
36455
  ".\u00A0",
36243
36456
  stepLabels[step])) },
36244
36457
  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 ?? [] })),
36458
+ 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
36459
  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
36460
  currentStep === 2 && (React__default["default"].createElement(SharedConfirmation, { bookingNumber: bookingNumber ?? editablePackagingEntry?.dossierNumber ?? '', isOption: false, isOffer: false, translations: translations.CONFIRMATION })),
36248
36461
  currentStep === 3 && React__default["default"].createElement("div", null))),
@@ -36342,11 +36555,13 @@ const TrajectResults = ({ isLoading }) => {
36342
36555
  });
36343
36556
  return map;
36344
36557
  }, [trajectEntry]);
36558
+ const droppedNodeIds = trajectEntry ? getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry) : new Set();
36559
+ const droppedKey = [...droppedNodeIds].sort((a, b) => a - b).join(',');
36345
36560
  const { stayByDay, stayStartByNodeId } = React.useMemo(() => {
36346
36561
  const byDay = new Map();
36347
36562
  const byNodeId = new Map();
36348
36563
  nodesWithLines
36349
- .filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE)
36564
+ .filter((x) => x.node.serviceType === ACCOMMODATION_SERVICE_TYPE && !droppedNodeIds.has(x.node.nodeId))
36350
36565
  .forEach((item) => {
36351
36566
  const nights = Math.max(dateFns.differenceInCalendarDays(new Date(item.line.to), new Date(item.line.from)), 1);
36352
36567
  byNodeId.set(item.node.nodeId, { item, night: 0, nights });
@@ -36355,7 +36570,7 @@ const TrajectResults = ({ isLoading }) => {
36355
36570
  }
36356
36571
  });
36357
36572
  return { stayByDay: byDay, stayStartByNodeId: byNodeId };
36358
- }, [nodesWithLines]);
36573
+ }, [nodesWithLines, droppedKey]);
36359
36574
  /** Whether every node of the trip has an answer, one way or the other. */
36360
36575
  const tripResolved = React.useMemo(() => nodesWithLines
36361
36576
  .filter((x) => isSearchableTrajectNode(x.node) || isTrajectFlightNode(x.node))
@@ -36382,7 +36597,20 @@ const TrajectResults = ({ isLoading }) => {
36382
36597
  if (!result)
36383
36598
  return;
36384
36599
  dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
36385
- buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? []).forEach((line) => dispatch(updateEditableEntryLine(line)));
36600
+ dispatch(upsertEditableEntryLines(buildNodeSelectionLines(item, result, bookedMode, editablePackagingEntry?.lines ?? [])));
36601
+ };
36602
+ /**
36603
+ * An optional stop can be taken off the trip; a mandatory one, or one of which the group needs a pick,
36604
+ * only swapped. A booked entry's services are never taken off here.
36605
+ */
36606
+ const canRemove = (item) => !bookedMode && canRemoveTrajectNode(item.node);
36607
+ const handleRemoveNode = (item) => {
36608
+ dispatch(clearTrajectNodeSelection(item.line.guid));
36609
+ dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
36610
+ };
36611
+ const handleRemoveFlight = (item) => {
36612
+ dispatch(clearTrajectFlightSelection(item.line.guid));
36613
+ dispatch(removeTrajectNode({ nodeId: item.node.nodeId, lineGuids: item.lines.map((line) => line.guid) }));
36386
36614
  };
36387
36615
  /** The remaining legs for a flight node open in the fly-in, as the regular flight results do. */
36388
36616
  const handleShowMoreFlights = (item) => {
@@ -36450,9 +36678,17 @@ const TrajectResults = ({ isLoading }) => {
36450
36678
  // A hotel row holds the hotels configured on the node; a portal can also let it fill up with
36451
36679
  // non-preferred hotels found in the area, the rest staying behind "show more".
36452
36680
  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
36681
  // No entry yet means the node's search has been queued but not dispatched — still loading.
36455
36682
  const status = availability?.status ?? 'loading';
36683
+ // A stop taken off the trip keeps its results in view, to pick one back.
36684
+ const isRemoved = status === 'resolved' && !selected && results.length > 0;
36685
+ const removable = canRemove(item);
36686
+ const shown = selected
36687
+ ? [selected, ...configured, ...fill]
36688
+ : configured.length || fill.length
36689
+ ? [...configured, ...fill]
36690
+ : others.slice(0, MAX_HOTEL_RESULTS);
36691
+ const remaining = results.length - shown.length;
36456
36692
  return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
36457
36693
  showHeader && (React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
36458
36694
  React__default["default"].createElement("div", { className: "search__results__label__date" },
@@ -36464,13 +36700,18 @@ const TrajectResults = ({ isLoading }) => {
36464
36700
  React__default["default"].createElement("strong", null, headerTitle || node.name))))),
36465
36701
  status === 'loading' && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }),
36466
36702
  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" },
36703
+ status === 'resolved' && (selected || isRemoved) && (React__default["default"].createElement(React__default["default"].Fragment, null,
36704
+ selected && availability.preferredUnavailable && node.preferredAccommodationName && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
36469
36705
  React__default["default"].createElement("span", null,
36470
36706
  node.preferredAccommodationName,
36471
36707
  " \u2014 ",
36472
36708
  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 })))),
36709
+ React__default["default"].createElement(TrajectCardRow, { selectionKey: selected?.code ?? '' }, shown.map((result) => {
36710
+ const isSelected = result.code === selected?.code;
36711
+ 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,
36712
+ // selectedLabel={bookedMode && matchesCode(result, node.code ?? '') ? translations.SHARED.BOOKED : undefined}
36713
+ selectedLabel: removable ? translations.SRP.REMOVE : undefined }));
36714
+ })),
36474
36715
  remaining > 0 && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
36475
36716
  React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMore(item) },
36476
36717
  translations.SRP.SHOW_MORE,
@@ -36480,7 +36721,7 @@ const TrajectResults = ({ isLoading }) => {
36480
36721
  };
36481
36722
  const handleSelectFlight = (item, flight) => {
36482
36723
  dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
36483
- dispatch(updateEditableEntryLine(buildFlightSelectionLine(item, flight, bookedMode)));
36724
+ dispatch(upsertEditableEntryLines([buildFlightSelectionLine(item, flight, bookedMode)]));
36484
36725
  };
36485
36726
  const renderFlightNode = (item) => {
36486
36727
  const { node, line } = item;
@@ -36489,7 +36730,9 @@ const TrajectResults = ({ isLoading }) => {
36489
36730
  const flights = availability?.results ?? [];
36490
36731
  const selected = flights.find((x) => x.outwardGuid === availability?.selectedGuid) ?? null;
36491
36732
  const others = flights.filter((x) => x.outwardGuid !== availability?.selectedGuid);
36492
- const visible = others.slice(0, 2);
36733
+ const isRemoved = status === 'resolved' && !selected && flights.length > 0;
36734
+ const removable = canRemove(item);
36735
+ const visible = others.slice(0, selected ? 2 : 3);
36493
36736
  return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
36494
36737
  React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
36495
36738
  React__default["default"].createElement("div", { className: "search__results__label__date" },
@@ -36504,15 +36747,15 @@ const TrajectResults = ({ isLoading }) => {
36504
36747
  node.arrivalAirportCode)))),
36505
36748
  status === 'loading' && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS }),
36506
36749
  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,
36750
+ status === 'resolved' && (selected || isRemoved) && (React__default["default"].createElement(React__default["default"].Fragment, null,
36508
36751
  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" },
36752
+ 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 })),
36753
+ 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) })))),
36754
+ others.length > visible.length && !hideShowMoreResults && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
36512
36755
  React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMoreFlights(item) },
36513
36756
  translations.SRP.SHOW_MORE,
36514
36757
  " (",
36515
- others.length - 2,
36758
+ others.length - visible.length,
36516
36759
  ")")))))));
36517
36760
  };
36518
36761
  /** The lines a suggested activity put on the entry, which is also how we know it was added. */
@@ -36588,13 +36831,21 @@ const TrajectResults = ({ isLoading }) => {
36588
36831
  ")")))));
36589
36832
  };
36590
36833
  /**
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.
36834
+ * An activity the traject books is swapped for another, the way a hotel is, or - when it is optional -
36835
+ * taken off the trip; one the traveller found for themselves is added to the trip or taken back off it.
36593
36836
  */
36594
36837
  const renderActivityCard = ({ result, item }) => {
36595
36838
  const isSelected = isActivitySelected({ result, item });
36596
36839
  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 }));
36840
+ const removable = !!item && canRemove(item);
36841
+ const handleClick = () => {
36842
+ if (!item)
36843
+ return handleToggleActivity(result);
36844
+ if (isSelected && removable)
36845
+ return handleRemoveNode(item);
36846
+ handleSelect(item, result.code);
36847
+ };
36848
+ 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
36849
  };
36599
36850
  if (isLoading)
36600
36851
  return React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
@@ -36629,6 +36880,7 @@ const TrajectResults = ({ isLoading }) => {
36629
36880
  const useTrajectAvailability = () => {
36630
36881
  const context = React.useContext(SearchResultsConfigurationContext);
36631
36882
  const dispatch = reactRedux.useDispatch();
36883
+ const { editablePackagingEntry, trajectTransferAvailability } = reactRedux.useSelector((state) => state.searchResults);
36632
36884
  const trajectEntry = context?.trajectEntry;
36633
36885
  const showApiItems = !!context?.showApiItems;
36634
36886
  const hideShowMoreResults = !!context?.hideShowMoreResults;
@@ -36654,12 +36906,14 @@ const useTrajectAvailability = () => {
36654
36906
  rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
36655
36907
  };
36656
36908
  // Flight searches take passengers rather than rooms; the API buckets by age.
36909
+ // The ids must be the entry's own pax ids: the flight lines this search caches carry them as
36910
+ // EntryLinePax.PaxId, and booking maps those onto the entry's travelers (PackagingService.SyncPaxIds).
36657
36911
  const pax = trajectEntry.entry.pax ?? [];
36658
36912
  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 })));
36913
+ const adults = pax.filter((p) => ageOf(p) >= 12);
36914
+ const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12);
36915
+ const babies = pax.filter((p) => ageOf(p) < 2);
36916
+ 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
36917
  const nodesWithLines = getTrajectNodesWithLines(trajectEntry);
36664
36918
  // The other members of a line group are alternatives whether or not a search still finds
36665
36919
  // them; the ones it does not are drawn from their lines, as the booked service itself is.
@@ -36790,9 +37044,30 @@ const useTrajectAvailability = () => {
36790
37044
  return item.lines.map((x) => x.guid);
36791
37045
  }
36792
37046
  };
37047
+ const resolveTransferNode = async (node) => {
37048
+ dispatch(setTrajectTransferLoading(node.nodeId));
37049
+ try {
37050
+ const results = (await build.searchPackagingTransfers(config, buildTrajectTransferRequest(node, searchContext), controller.signal)) ?? [];
37051
+ if (controller.signal.aborted)
37052
+ return [];
37053
+ const result = results[0] ?? null;
37054
+ dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: result && buildTrajectTransferLine(node, result) ? result : null }));
37055
+ }
37056
+ catch (error) {
37057
+ if (controller.signal.aborted)
37058
+ return [];
37059
+ console.error('Failed to resolve traject transfer', node.name, error);
37060
+ dispatch(setTrajectTransferAvailability({ nodeId: node.nodeId, result: null }));
37061
+ }
37062
+ return [];
37063
+ };
36793
37064
  (async () => {
36794
37065
  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();
37066
+ const transfers = bookedMode ? [] : (trajectEntry.nodes ?? []).filter(isTrajectTransferNode);
37067
+ const unresolved = (await Promise.all([
37068
+ ...resolvable.map((item) => (isTrajectFlightNode(item.node) ? resolveFlightNode(item) : resolveNode(item))),
37069
+ ...transfers.map(resolveTransferNode)
37070
+ ])).flat();
36796
37071
  if (controller.signal.aborted)
36797
37072
  return;
36798
37073
  dispatch(setTrajectResolving(false));
@@ -36804,6 +37079,28 @@ const useTrajectAvailability = () => {
36804
37079
  })();
36805
37080
  return () => controller.abort();
36806
37081
  }, [trajectEntry, showApiItems, hideShowMoreResults]);
37082
+ React.useEffect(() => {
37083
+ if (!trajectEntry || !editablePackagingEntry || isBookedEntryTraject(trajectEntry))
37084
+ return;
37085
+ const dropped = getDroppedTrajectNodeIds(trajectEntry, editablePackagingEntry);
37086
+ const guids = new Set((editablePackagingEntry.lines ?? []).map((line) => line.guid));
37087
+ const toAdd = [];
37088
+ const toRemove = [];
37089
+ (trajectEntry.nodes ?? []).filter(isTrajectTransferNode).forEach((node) => {
37090
+ const result = trajectTransferAvailability[node.nodeId]?.result;
37091
+ const line = result ? buildTrajectTransferLine(node, result) : null;
37092
+ if (!line)
37093
+ return;
37094
+ if (dropped.has(node.nodeId) && guids.has(line.guid))
37095
+ toRemove.push(line.guid);
37096
+ if (!dropped.has(node.nodeId) && !guids.has(line.guid))
37097
+ toAdd.push(line);
37098
+ });
37099
+ if (toRemove.length)
37100
+ dispatch(removeEditableEntryLines(toRemove));
37101
+ if (toAdd.length)
37102
+ dispatch(addEditableEntryLines(toAdd));
37103
+ }, [trajectEntry, editablePackagingEntry, trajectTransferAvailability]);
36807
37104
  };
36808
37105
 
36809
37106
  const SearchResultsContainer = ({ onBookingStarted }) => {
@@ -37108,10 +37405,9 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
37108
37405
  const item = getTrajectNodesWithLines(context.trajectEntry).find((x) => x.line.guid === trajectEditLineGuid);
37109
37406
  const result = packagingAccoSearchDetails.find((x) => x.code === selectedPackagingAccoResultCode);
37110
37407
  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)));
37408
+ // A stop the traveller took off has no lines left on the entry: the traject's own put it back.
37409
+ const currentLines = item.lines.map((line) => editablePackagingEntry?.lines?.find((x) => x.guid === line.guid) ?? line);
37410
+ dispatch(upsertEditableEntryLines(applyResultToLines(currentLines, result)));
37115
37411
  dispatch(updateTrajectNodeResult({ lineGuid: trajectEditLineGuid, result }));
37116
37412
  }
37117
37413
  dispatch(setTrajectEditLineGuid(null));