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