@qite/tide-components 1.0.4 → 1.0.6

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.
@@ -22890,6 +22890,10 @@ const NAME_CHARACTERS_REGEX = /^[\p{L}\s-]+$/u;
22890
22890
  function hasInvalidNameCharacters(name) {
22891
22891
  return !NAME_CHARACTERS_REGEX.test(name);
22892
22892
  }
22893
+ // Rejects values like a 5-digit year, which the native date input allows while typing
22894
+ function isValidBirthDate(birthDateText) {
22895
+ return dateFns.isValid(dateFns.parse(birthDateText, 'yyyy-MM-dd', new Date()));
22896
+ }
22893
22897
  function getAge(birthDateText, startDateText) {
22894
22898
  var birthDate = new Date(birthDateText);
22895
22899
  var startDate = new Date(startDateText);
@@ -22957,7 +22961,7 @@ const validateForm$1 = (values, agentRequired, bookingType, translations, formFi
22957
22961
  }
22958
22962
  }
22959
22963
  if (isFormFieldPresent('birthDate')) {
22960
- if (lodash.isEmpty(adult.birthDate)) {
22964
+ if (lodash.isEmpty(adult.birthDate) || !isValidBirthDate(adult.birthDate)) {
22961
22965
  lodash.set(errors, `rooms[${rIndex}].adults[${index}].birthDate`, formatTravelerField(rIndex + 1, index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
22962
22966
  }
22963
22967
  else if (values.endDate) {
@@ -22990,7 +22994,7 @@ const validateForm$1 = (values, agentRequired, bookingType, translations, formFi
22990
22994
  }
22991
22995
  }
22992
22996
  if (isFormFieldPresent('birthDate')) {
22993
- if (lodash.isEmpty(child.birthDate)) {
22997
+ if (lodash.isEmpty(child.birthDate) || !isValidBirthDate(child.birthDate)) {
22994
22998
  lodash.set(errors, `rooms[${rIndex}].children[${index}].birthDate`, formatTravelerField(rIndex + 1, r.adults.length + index + 1, translations.TRAVELERS_FORM.BIRTHDATE));
22995
22999
  }
22996
23000
  else if (child.isBaby && values.startDate) {
@@ -23215,6 +23219,13 @@ const TypeAheadInput = ({ name, value, placeholder, options, onChange, onSelect,
23215
23219
  function isBabyAge(age, maxBabyAge = BABY_MAX_AGE) {
23216
23220
  return typeof age === 'number' && age <= maxBabyAge;
23217
23221
  }
23222
+ // The native date input can hold values format() throws on (e.g. a 5+ digit year while typing), so never format an unvalidated birthDate
23223
+ function formatBirthDate$1(birthDate) {
23224
+ if (!birthDate)
23225
+ return '';
23226
+ const parsed = dateFns.parse(birthDate, 'yyyy-MM-dd', new Date());
23227
+ return dateFns.isValid(parsed) ? dateFns.format(parsed, 'dd-MM-yyyy') : '';
23228
+ }
23218
23229
  function createTraveler(traveler, followNumber, personTranslation, isCompact, maxBabyAge) {
23219
23230
  if (isCompact) {
23220
23231
  return {
@@ -23522,10 +23533,7 @@ const SharedTravelersForm = ({ formik, translations, travellersSettings, countri
23522
23533
  !useCompactForm && (bookingType !== 'b2b' || travellersSettings?.mainBookerFormFields?.length) ? (React__default["default"].createElement("div", { className: "form__region" },
23523
23534
  React__default["default"].createElement("div", { className: "form__region-header" },
23524
23535
  React__default["default"].createElement("h5", { className: "form__region-heading" }, translations.TRAVELERS_FORM.MAIN_BOOKER),
23525
- React__default["default"].createElement("p", { className: "form__region-label" }, lodash.compact([
23526
- lodash.compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '),
23527
- mainBooker?.birthDate && dateFns.format(dateFns.parse(mainBooker.birthDate, 'yyyy-MM-dd', new Date()), 'dd-MM-yyyy')
23528
- ]).join(', '))),
23536
+ React__default["default"].createElement("p", { className: "form__region-label" }, lodash.compact([lodash.compact([mainBooker?.firstName, mainBooker?.lastName]).join(' '), formatBirthDate$1(mainBooker?.birthDate)]).join(', '))),
23529
23537
  travellersSettings?.mainBookerFormFields?.length ? (React__default["default"].createElement("div", { className: "main-booker-form__grid" }, travellersSettings.mainBookerFormFields.map((field, index) => (React__default["default"].createElement("div", { key: index, className: `control control--${field.type}` }, getControl(field.type, {}, field.type)))))) : (React__default["default"].createElement(React__default["default"].Fragment, null,
23530
23538
  React__default["default"].createElement("div", { className: "form__twocolumn" },
23531
23539
  React__default["default"].createElement("div", { className: "form__twocolumn-column" },
@@ -27370,7 +27378,8 @@ const initialState$1 = {
27370
27378
  currentStep: 0,
27371
27379
  bookingNumber: undefined,
27372
27380
  trajectNodeAvailability: {},
27373
- trajectFlightAvailability: {}
27381
+ trajectFlightAvailability: {},
27382
+ trajectEditLineGuid: null
27374
27383
  };
27375
27384
  const searchResultsSlice = toolkit.createSlice({
27376
27385
  name: 'searchResults',
@@ -27617,13 +27626,26 @@ const searchResultsSlice = toolkit.createSlice({
27617
27626
  flight.selectedGuid = action.payload.guid;
27618
27627
  flight.status = 'resolved';
27619
27628
  },
27629
+ setTrajectEditLineGuid(state, action) {
27630
+ state.trajectEditLineGuid = action.payload;
27631
+ },
27632
+ updateTrajectNodeResult(state, action) {
27633
+ const { lineGuid, result } = action.payload;
27634
+ const node = state.trajectNodeAvailability[lineGuid];
27635
+ if (!node)
27636
+ return;
27637
+ node.results = node.results.map((x) => (x.code === result.code ? result : x));
27638
+ node.selectedCode = result.code;
27639
+ node.status = 'resolved';
27640
+ },
27620
27641
  resetTrajectAvailability(state) {
27621
27642
  state.trajectNodeAvailability = {};
27622
27643
  state.trajectFlightAvailability = {};
27644
+ state.trajectEditLineGuid = null;
27623
27645
  }
27624
27646
  }
27625
27647
  });
27626
- 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, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
27648
+ 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, setTrajectFlightLoading, setTrajectFlightAvailability, setTrajectFlightSelection, setTrajectEditLineGuid, updateTrajectNodeResult, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
27627
27649
  var searchResultsReducer = searchResultsSlice.reducer;
27628
27650
 
27629
27651
  const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
@@ -30603,6 +30625,293 @@ const ExcursionDetails = () => {
30603
30625
  React__default["default"].createElement("button", { type: "button", className: "cta cta--primary", onClick: handleConfirm }, translations?.QSM.CONFIRM))));
30604
30626
  };
30605
30627
 
30628
+ const REGULAR_TRAJECT_NODE_TYPE = 0;
30629
+ const isSearchableTrajectNode = (node) => node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE;
30630
+ const isTrajectFlightNode = (node) => node.serviceType === FLIGHT_SERVICE_TYPE;
30631
+ const getTrajectNodesWithLines = (trajectEntry) => {
30632
+ const linesByGuid = new Map((trajectEntry.entry.lines ?? []).map((line) => [line.guid, line]));
30633
+ return [...(trajectEntry.nodes ?? [])]
30634
+ .sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
30635
+ .map((node) => {
30636
+ const guids = node.lineGuids?.length ? node.lineGuids : [node.lineGuid];
30637
+ const lines = guids.map((guid) => linesByGuid.get(guid)).filter((x) => !!x);
30638
+ return { node, line: lines[0], lines };
30639
+ })
30640
+ .filter((x) => !!x.line);
30641
+ };
30642
+ const buildDestination = (line) => {
30643
+ if (line.location?.id)
30644
+ return { id: line.location.id, isLocation: true };
30645
+ if (line.oord?.id)
30646
+ return { id: line.oord.id, isOord: true };
30647
+ if (line.region?.id)
30648
+ return { id: line.region.id, isRegion: true };
30649
+ if (line.country?.id)
30650
+ return { id: line.country.id, isCountry: true };
30651
+ if (line.latitude && line.longitude)
30652
+ return { latitude: line.latitude, longitude: line.longitude };
30653
+ return { id: 0 };
30654
+ };
30655
+ const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
30656
+ transactionId: context.transactionId,
30657
+ officeId: context.officeId,
30658
+ agentId: context.agentId ?? null,
30659
+ catalogueId: context.catalogueId,
30660
+ searchConfigurationId: context.searchConfigurationId,
30661
+ portalId: context.portalId ?? null,
30662
+ vendorConfigurationId: node.externalVendorId ?? null,
30663
+ language: context.language,
30664
+ serviceType: node.serviceType,
30665
+ fromDate: toDateOnlyString(line.from),
30666
+ toDate: toDateOnlyString(line.to),
30667
+ destination: buildDestination(line),
30668
+ productCode,
30669
+ rooms: context.rooms,
30670
+ tagIds: []
30671
+ });
30672
+ const getNodeCandidates = (node) => {
30673
+ const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
30674
+ if (configured.length)
30675
+ return configured;
30676
+ const fallbackCode = node.code ?? node.preferredAccommodationCode;
30677
+ if (!fallbackCode || !fallbackCode.trim().length)
30678
+ return [];
30679
+ return [
30680
+ {
30681
+ code: fallbackCode,
30682
+ name: node.name,
30683
+ description: node.description,
30684
+ imageUrl: node.imageUrl,
30685
+ contentSource: node.contentSource,
30686
+ vendor: node.vendor,
30687
+ externalVendorId: node.externalVendorId,
30688
+ isPreferred: true
30689
+ }
30690
+ ];
30691
+ };
30692
+ const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
30693
+ const matchesCode = (result, code) => {
30694
+ if (result.code === code)
30695
+ return true;
30696
+ return (result.rooms ?? []).some((room) => (room.options ?? []).some((option) => option.accommodationCode === code));
30697
+ };
30698
+ const getPackagingRoomsFromEntry = (entry) => {
30699
+ const paxById = new Map((entry.pax ?? []).map((p) => [p.id, p]));
30700
+ const rooms = (entry.rooms ?? []).map((room) => ({
30701
+ travellers: (room.paxIds ?? []).map((paxId) => {
30702
+ const pax = paxById.get(paxId);
30703
+ return {
30704
+ id: paxId,
30705
+ age: pax?.age ?? null,
30706
+ dateOfBirth: pax?.dateOfBirth ?? null
30707
+ };
30708
+ })
30709
+ }));
30710
+ return rooms.filter((room) => room.travellers.length > 0);
30711
+ };
30712
+ const pickCheapest = (results) => [...results].sort((a, b) => a.price - b.price)[0] ?? null;
30713
+ const applyResultToLines = (lines, result) => lines.map((line, index) => applyResultToLine(line, result, index));
30714
+ const getOptionForRoom = (result, roomIndex) => {
30715
+ const room = (result.rooms ?? [])[roomIndex];
30716
+ if (room)
30717
+ return (room.options ?? []).find((x) => x.isSelected) ?? (room.options ?? [])[0];
30718
+ return (result.rooms ?? []).flatMap((x) => x.options ?? []).find((x) => x.isSelected) ?? (result.rooms ?? [])[0]?.options?.[0];
30719
+ };
30720
+ const applyResultToLine = (line, result, roomIndex = 0) => {
30721
+ const option = getOptionForRoom(result, roomIndex);
30722
+ return {
30723
+ ...line,
30724
+ productName: result.name ?? line.productName,
30725
+ productCode: result.code ?? line.productCode,
30726
+ accommodationCode: option?.accommodationCode ?? line.accommodationCode,
30727
+ accommodationName: option?.accommodationName ?? line.accommodationName,
30728
+ regimeCode: option?.regimeCode ?? line.regimeCode,
30729
+ regimeName: option?.regimeName ?? line.regimeName,
30730
+ latitude: result.latitude ?? line.latitude,
30731
+ longitude: result.longitude ?? line.longitude,
30732
+ isChanged: true
30733
+ };
30734
+ };
30735
+ const toDateOnlyUtcString = (value) => {
30736
+ const date = new Date(value);
30737
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())).toISOString();
30738
+ };
30739
+ const toTimeOnlyString = (value) => {
30740
+ const date = new Date(value);
30741
+ const hh = String(date.getUTCHours()).padStart(2, '0');
30742
+ const mm = String(date.getUTCMinutes()).padStart(2, '0');
30743
+ const ss = String(date.getUTCSeconds()).padStart(2, '0');
30744
+ return `${hh}:${mm}:${ss}`;
30745
+ };
30746
+ const mapFlightSegmentsToFlightLines = (segments) => segments.map((segment) => ({
30747
+ airlineCode: segment.marketingAirlineCode,
30748
+ airlineDescription: segment.marketingAirlineName,
30749
+ operatingAirlineCode: segment.operatingAirlineCode,
30750
+ operatingAirlineDescription: segment.operatingAirlineName,
30751
+ flightNumber: segment.flightNumber,
30752
+ operatingFlightNumber: segment.operatingFlightNumber ?? null,
30753
+ departureDate: toDateOnlyUtcString(segment.departureDateTime),
30754
+ departureTime: toTimeOnlyString(segment.departureDateTime),
30755
+ departureAirportCode: segment.departureAirportCode,
30756
+ departureAirportDescription: segment.departureAirportName,
30757
+ arrivalDate: toDateOnlyUtcString(segment.arrivalDateTime),
30758
+ arrivalTime: toTimeOnlyString(segment.arrivalDateTime),
30759
+ arrivalAirportCode: segment.arrivalAirportCode,
30760
+ arrivalAirportDescription: segment.arrivalAirportName,
30761
+ durationInTicks: segment.durationInTicks
30762
+ }));
30763
+ const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
30764
+ transactionId: context.transactionId,
30765
+ officeId: context.officeId,
30766
+ catalogueId: context.catalogueId,
30767
+ agentId: context.agentId ?? null,
30768
+ language: context.language,
30769
+ departureAirportCode: node.departureAirportCode ?? '',
30770
+ arrivalAirportCode: node.arrivalAirportCode ?? '',
30771
+ returnAirportCode: null,
30772
+ luggageIncluded: null,
30773
+ maxStops: null,
30774
+ travelClass: null,
30775
+ vendorConfigurationId: node.externalVendorId ?? null,
30776
+ pax,
30777
+ outward: { date: dateToDateStruct(new Date(line.from)) },
30778
+ return: null
30779
+ });
30780
+ const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
30781
+ const applyFlightToLine = (line, flight) => {
30782
+ const segments = flight.outward?.segments ?? [];
30783
+ if (!segments.length)
30784
+ return line;
30785
+ const firstSegment = segments[0];
30786
+ const lastSegment = segments[segments.length - 1];
30787
+ return {
30788
+ ...line,
30789
+ from: new Date(firstSegment.departureDateTime).toISOString(),
30790
+ to: new Date(lastSegment.arrivalDateTime).toISOString(),
30791
+ productName: `${firstSegment.departureAirportName} - ${lastSegment.arrivalAirportName} (${firstSegment.marketingAirlineName})`,
30792
+ productCode: `${firstSegment.departureAirportCode} ${lastSegment.arrivalAirportCode}/${firstSegment.marketingAirlineCode}`,
30793
+ accommodationName: firstSegment.metaData?.farePriceClassName ?? line.accommodationName,
30794
+ accommodationCode: firstSegment.metaData?.fareCode ?? line.accommodationCode,
30795
+ flightInformation: {
30796
+ pnr: '',
30797
+ flightLines: mapFlightSegmentsToFlightLines(segments)
30798
+ },
30799
+ isChanged: true
30800
+ };
30801
+ };
30802
+
30803
+ const getLocation = (result) => {
30804
+ const place = result.locationName || result.regionName || result.oordName;
30805
+ if (!place)
30806
+ return result.countryName ?? '';
30807
+ return result.countryName ? `${place}, ${result.countryName}` : place;
30808
+ };
30809
+ const toPlainText = (value) => {
30810
+ if (!value)
30811
+ return '';
30812
+ return he
30813
+ .decode(value.replace(/<[^>]*>/g, ' '))
30814
+ .replace(/\s+/g, ' ')
30815
+ .trim();
30816
+ };
30817
+ const TrajectNodeCard = ({ item, result, isSelected, languageCode, translations, onSelect, onEditOptions, showNights }) => {
30818
+ const { node } = item;
30819
+ const selectedPerRoom = (result.rooms ?? []).map((room) => (room.options ?? []).find((x) => x.isSelected) ?? (room.options ?? [])[0]);
30820
+ const isMultiRoom = selectedPerRoom.length > 1;
30821
+ const price = formatPrice$3(result.price, result.currencyCode, languageCode);
30822
+ const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
30823
+ const candidate = findCandidateForResult(node, result);
30824
+ const image = candidate?.imageUrl ?? null;
30825
+ const description = toPlainText(candidate?.description);
30826
+ const title = candidate?.name ?? result.name;
30827
+ const priceBlock = (React__default["default"].createElement("div", { className: "search__result-card__price__wrapper" },
30828
+ React__default["default"].createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
30829
+ React__default["default"].createElement("span", { className: "search__result-card__price" }, price)));
30830
+ const canEditOptions = isSelected && !!onEditOptions && (result.rooms ?? []).some((room) => (room.options ?? []).length > 1);
30831
+ const selectButton = (React__default["default"].createElement(React__default["default"].Fragment, null,
30832
+ React__default["default"].createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT),
30833
+ canEditOptions && (React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: onEditOptions },
30834
+ translations?.SRP.SELECT,
30835
+ " ",
30836
+ translations?.SRP.ACCOMMODATION))));
30837
+ if (result.contents?.length) {
30838
+ return (React__default["default"].createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
30839
+ React__default["default"].createElement("div", { className: "search__result-card__top", dangerouslySetInnerHTML: { __html: he.decode(result.contents) } }),
30840
+ React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton)));
30841
+ }
30842
+ return (React__default["default"].createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
30843
+ image && (React__default["default"].createElement("div", { className: "search__result-card__img-wrapper" },
30844
+ React__default["default"].createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
30845
+ priceBlock)),
30846
+ React__default["default"].createElement("div", { className: "search__result-card__content" },
30847
+ React__default["default"].createElement("div", { className: "search__result-card__content__wrapper" },
30848
+ React__default["default"].createElement("div", { className: "search__result-card__header" },
30849
+ React__default["default"].createElement("div", { className: "search__result-card__header__wrapper" },
30850
+ !!result.stars && (React__default["default"].createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default["default"].createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
30851
+ React__default["default"].createElement("h3", { className: "search__result-card__title" }, title)),
30852
+ !image && priceBlock),
30853
+ React__default["default"].createElement("span", { className: "search__result-card__location" },
30854
+ React__default["default"].createElement(Icon, { name: "ui-location", height: 16 }),
30855
+ getLocation(result)),
30856
+ React__default["default"].createElement("div", { className: "search__result-card__options" },
30857
+ showNights && nights > 0 && (React__default["default"].createElement("div", { className: "search__result-card__option" },
30858
+ React__default["default"].createElement(Icon, { name: "ui-moon", height: 16 }),
30859
+ nights,
30860
+ " ",
30861
+ translations?.SRP.NIGHTS)),
30862
+ selectedPerRoom.map((option, roomIndex) => (React__default["default"].createElement(React__default["default"].Fragment, { key: `room-${roomIndex}` },
30863
+ option?.accommodationName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
30864
+ React__default["default"].createElement(Icon, { name: "ui-bed", height: 16 }),
30865
+ isMultiRoom && `${translations?.SHARED.ROOM} ${roomIndex + 1}: `,
30866
+ option.accommodationName)),
30867
+ option?.regimeName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
30868
+ React__default["default"].createElement(Icon, { name: "ui-utensils", height: 16 }),
30869
+ option.regimeName)))))),
30870
+ description && React__default["default"].createElement("p", { className: "search__result-card__description" }, description)),
30871
+ React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton))));
30872
+ };
30873
+
30874
+ const TrajectResultsFlyIn = ({ isLoading }) => {
30875
+ const context = React.useContext(SearchResultsConfigurationContext);
30876
+ const dispatch = reactRedux.useDispatch();
30877
+ const translations = getTranslations(context?.languageCode ?? 'en-GB');
30878
+ const { trajectNodeAvailability, trajectEditLineGuid } = reactRedux.useSelector((state) => state.searchResults);
30879
+ const trajectEntry = context?.trajectEntry;
30880
+ const item = React.useMemo(() => {
30881
+ if (!trajectEntry || !trajectEditLineGuid)
30882
+ return null;
30883
+ return getTrajectNodesWithLines(trajectEntry).find((x) => x.line.guid === trajectEditLineGuid) ?? null;
30884
+ }, [trajectEntry, trajectEditLineGuid]);
30885
+ if (!context || !item)
30886
+ return null;
30887
+ const availability = trajectNodeAvailability[item.line.guid];
30888
+ const results = availability?.results ?? [];
30889
+ const ordered = [...results].sort((a, b) => {
30890
+ const aConfigured = findCandidateForResult(item.node, a) ? 0 : 1;
30891
+ const bConfigured = findCandidateForResult(item.node, b) ? 0 : 1;
30892
+ return aConfigured - bConfigured || a.price - b.price;
30893
+ });
30894
+ const handleSelect = (code) => {
30895
+ const result = results.find((x) => x.code === code);
30896
+ if (!result)
30897
+ return;
30898
+ dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
30899
+ applyResultToLines(item.lines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
30900
+ dispatch(setTrajectEditLineGuid(null));
30901
+ dispatch(setFlyInIsOpen(false));
30902
+ };
30903
+ if (isLoading) {
30904
+ return React__default["default"].createElement(React__default["default"].Fragment, null, context.customSpinner ?? React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ACCOMMODATIONS }));
30905
+ }
30906
+ return (React__default["default"].createElement("div", { className: "flyin__content" },
30907
+ React__default["default"].createElement("div", { className: "search__result-row" },
30908
+ React__default["default"].createElement("span", { className: "search__result-row-text" },
30909
+ ordered.length,
30910
+ "\u00A0",
30911
+ translations.SRP.TOTAL_RESULTS_LABEL)),
30912
+ ordered.length === 0 ? (React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)) : (React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--compact" }, ordered.map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, item: item, result: result, isSelected: result.code === availability?.selectedCode, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(result.code), showNights: item.node.serviceType === ACCOMMODATION_SERVICE_TYPE })))))));
30913
+ };
30914
+
30606
30915
  const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, detailsLoading, flyInType, isPackageEditFlow, handleConfirm, sortByTypes, activeSearchSeed, toggleFilters, filtersOpen }) => {
30607
30916
  const dispatch = reactRedux.useDispatch();
30608
30917
  const context = React.useContext(SearchResultsConfigurationContext);
@@ -30648,6 +30957,10 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
30648
30957
  }
30649
30958
  };
30650
30959
  const handleGoBack = () => {
30960
+ if (context?.trajectEntry) {
30961
+ handleClose();
30962
+ return;
30963
+ }
30651
30964
  if (flyInType === 'acco-details') {
30652
30965
  dispatch(setFlyInType('acco-results'));
30653
30966
  }
@@ -30671,7 +30984,7 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
30671
30984
  dispatch(setBookPackagingEntry(true));
30672
30985
  }
30673
30986
  };
30674
- return (React__default["default"].createElement("div", { className: `flyin ${isOpen ? 'flyin--active' : ''} ${className} ${isPackageEditFlow || flyInType === 'acco-results' ? 'flyin--large' : ''} ${flyInType === 'excursion-results' || flyInType === 'excursion-details' ? 'flyin--medium' : ''}
30987
+ return (React__default["default"].createElement("div", { className: `flyin ${isOpen ? 'flyin--active' : ''} ${className} ${isPackageEditFlow || flyInType === 'acco-results' ? 'flyin--large' : ''} ${flyInType === 'excursion-results' || flyInType === 'excursion-details' ? 'flyin--medium' : ''}
30675
30988
  ${flyInType === 'flight-outward-results' || flyInType === 'flight-return-results' ? 'flyin--flight' : ''}` },
30676
30989
  React__default["default"].createElement("div", { className: `flyin__panel ${isOpen ? 'flyin__panel--active' : ''}`, ref: panelRef },
30677
30990
  React__default["default"].createElement("div", { className: "flyin__content" },
@@ -30697,7 +31010,10 @@ const FlyIn = ({ srpType, isOpen, setIsOpen, className = '', onPanelRef, details
30697
31010
  React__default["default"].createElement(Icon, { name: "ui-chevron", width: 14, height: 14, "aria-hidden": "true" }),
30698
31011
  "Go Back")))),
30699
31012
  srpType === build.PortalQsmType.Flight && React__default["default"].createElement(FlightsFlyIn, { isOpen: isOpen, setIsOpen: setIsOpen }),
30700
- (srpType === build.PortalQsmType.Accommodation || srpType === build.PortalQsmType.AccommodationAndFlight) && flyInType === 'acco-results' && (React__default["default"].createElement("div", { className: "flyin__content flyin__content--columns" },
31013
+ context?.trajectEntry && flyInType === 'acco-results' && React__default["default"].createElement(TrajectResultsFlyIn, { isLoading: detailsLoading }),
31014
+ !context?.trajectEntry &&
31015
+ (srpType === build.PortalQsmType.Accommodation || srpType === build.PortalQsmType.AccommodationAndFlight) &&
31016
+ flyInType === 'acco-results' && (React__default["default"].createElement("div", { className: "flyin__content flyin__content--columns" },
30701
31017
  React__default["default"].createElement(Filters, { initialFilters: initialFilters, filters: filters, isOpen: filtersOpen, handleSetIsOpen: () => toggleFilters && toggleFilters(),
30702
31018
  // handleApplyFilters={() => setSearchTrigger((prev) => prev + 1)}
30703
31019
  isLoading: isLoading, setFilters: (filters) => dispatch(setFilters(filters)), resetFilters: (filters) => dispatch(resetFilters(filters)) }),
@@ -32970,232 +33286,6 @@ const BookPackagingEntry = ({ activeSearchSeed, isLoading, isConfirmationPage })
32970
33286
  React__default["default"].createElement(WLSidebar, { activeSearchSeed: activeSearchSeed, packagingAccoResult: selectedPackagingAccoResult }))));
32971
33287
  };
32972
33288
 
32973
- const REGULAR_TRAJECT_NODE_TYPE = 0;
32974
- const isSearchableTrajectNode = (node) => node.serviceType === ACCOMMODATION_SERVICE_TYPE || node.serviceType === EXCURSION_SERVICE_TYPE;
32975
- const isTrajectFlightNode = (node) => node.serviceType === FLIGHT_SERVICE_TYPE;
32976
- const getTrajectNodesWithLines = (trajectEntry) => {
32977
- const linesByGuid = new Map((trajectEntry.entry.lines ?? []).map((line) => [line.guid, line]));
32978
- return [...(trajectEntry.nodes ?? [])]
32979
- .sort((a, b) => a.dayOrder - b.dayOrder || a.order - b.order)
32980
- .map((node) => ({ node, line: linesByGuid.get(node.lineGuid) }))
32981
- .filter((x) => !!x.line);
32982
- };
32983
- const buildDestination = (line) => {
32984
- if (line.location?.id)
32985
- return { id: line.location.id, isLocation: true };
32986
- if (line.oord?.id)
32987
- return { id: line.oord.id, isOord: true };
32988
- if (line.region?.id)
32989
- return { id: line.region.id, isRegion: true };
32990
- if (line.country?.id)
32991
- return { id: line.country.id, isCountry: true };
32992
- if (line.latitude && line.longitude)
32993
- return { latitude: line.latitude, longitude: line.longitude };
32994
- return { id: 0 };
32995
- };
32996
- const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
32997
- transactionId: context.transactionId,
32998
- officeId: context.officeId,
32999
- agentId: context.agentId ?? null,
33000
- catalogueId: context.catalogueId,
33001
- searchConfigurationId: context.searchConfigurationId,
33002
- portalId: context.portalId ?? null,
33003
- vendorConfigurationId: node.externalVendorId ?? null,
33004
- language: context.language,
33005
- serviceType: node.serviceType,
33006
- fromDate: toDateOnlyString(line.from),
33007
- toDate: toDateOnlyString(line.to),
33008
- destination: buildDestination(line),
33009
- productCode,
33010
- rooms: context.rooms,
33011
- tagIds: []
33012
- });
33013
- const getNodeCandidates = (node) => {
33014
- const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
33015
- if (configured.length)
33016
- return configured;
33017
- const fallbackCode = node.code ?? node.preferredAccommodationCode;
33018
- if (!fallbackCode || !fallbackCode.trim().length)
33019
- return [];
33020
- return [
33021
- {
33022
- code: fallbackCode,
33023
- name: node.name,
33024
- description: node.description,
33025
- imageUrl: node.imageUrl,
33026
- contentSource: node.contentSource,
33027
- vendor: node.vendor,
33028
- externalVendorId: node.externalVendorId,
33029
- isPreferred: true
33030
- }
33031
- ];
33032
- };
33033
- const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
33034
- const matchesCode = (result, code) => {
33035
- if (result.code === code)
33036
- return true;
33037
- return (result.rooms ?? []).some((room) => (room.options ?? []).some((option) => option.accommodationCode === code));
33038
- };
33039
- const getPackagingRoomsFromEntry = (entry) => {
33040
- const paxById = new Map((entry.pax ?? []).map((p) => [p.id, p]));
33041
- const rooms = (entry.rooms ?? []).map((room) => ({
33042
- travellers: (room.paxIds ?? []).map((paxId) => {
33043
- const pax = paxById.get(paxId);
33044
- return {
33045
- id: paxId,
33046
- age: pax?.age ?? null,
33047
- dateOfBirth: pax?.dateOfBirth ?? null
33048
- };
33049
- })
33050
- }));
33051
- return rooms.filter((room) => room.travellers.length > 0);
33052
- };
33053
- const pickCheapest = (results) => [...results].sort((a, b) => a.price - b.price)[0] ?? null;
33054
- const applyResultToLine = (line, result) => {
33055
- const option = (result.rooms ?? []).flatMap((room) => room.options ?? []).find((x) => x.isSelected) ?? (result.rooms ?? [])[0]?.options?.[0];
33056
- return {
33057
- ...line,
33058
- productName: result.name ?? line.productName,
33059
- productCode: result.code ?? line.productCode,
33060
- accommodationCode: option?.accommodationCode ?? line.accommodationCode,
33061
- accommodationName: option?.accommodationName ?? line.accommodationName,
33062
- regimeCode: option?.regimeCode ?? line.regimeCode,
33063
- regimeName: option?.regimeName ?? line.regimeName,
33064
- latitude: result.latitude ?? line.latitude,
33065
- longitude: result.longitude ?? line.longitude,
33066
- isChanged: true
33067
- };
33068
- };
33069
- const toDateOnlyUtcString = (value) => {
33070
- const date = new Date(value);
33071
- return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())).toISOString();
33072
- };
33073
- const toTimeOnlyString = (value) => {
33074
- const date = new Date(value);
33075
- const hh = String(date.getUTCHours()).padStart(2, '0');
33076
- const mm = String(date.getUTCMinutes()).padStart(2, '0');
33077
- const ss = String(date.getUTCSeconds()).padStart(2, '0');
33078
- return `${hh}:${mm}:${ss}`;
33079
- };
33080
- const mapFlightSegmentsToFlightLines = (segments) => segments.map((segment) => ({
33081
- airlineCode: segment.marketingAirlineCode,
33082
- airlineDescription: segment.marketingAirlineName,
33083
- operatingAirlineCode: segment.operatingAirlineCode,
33084
- operatingAirlineDescription: segment.operatingAirlineName,
33085
- flightNumber: segment.flightNumber,
33086
- operatingFlightNumber: segment.operatingFlightNumber ?? null,
33087
- departureDate: toDateOnlyUtcString(segment.departureDateTime),
33088
- departureTime: toTimeOnlyString(segment.departureDateTime),
33089
- departureAirportCode: segment.departureAirportCode,
33090
- departureAirportDescription: segment.departureAirportName,
33091
- arrivalDate: toDateOnlyUtcString(segment.arrivalDateTime),
33092
- arrivalTime: toTimeOnlyString(segment.arrivalDateTime),
33093
- arrivalAirportCode: segment.arrivalAirportCode,
33094
- arrivalAirportDescription: segment.arrivalAirportName,
33095
- durationInTicks: segment.durationInTicks
33096
- }));
33097
- const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
33098
- transactionId: context.transactionId,
33099
- officeId: context.officeId,
33100
- catalogueId: context.catalogueId,
33101
- agentId: context.agentId ?? null,
33102
- language: context.language,
33103
- departureAirportCode: node.departureAirportCode ?? '',
33104
- arrivalAirportCode: node.arrivalAirportCode ?? '',
33105
- returnAirportCode: null,
33106
- luggageIncluded: null,
33107
- maxStops: null,
33108
- travelClass: null,
33109
- vendorConfigurationId: node.externalVendorId ?? null,
33110
- pax,
33111
- outward: { date: dateToDateStruct(new Date(line.from)) },
33112
- return: null
33113
- });
33114
- const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
33115
- const applyFlightToLine = (line, flight) => {
33116
- const segments = flight.outward?.segments ?? [];
33117
- if (!segments.length)
33118
- return line;
33119
- const firstSegment = segments[0];
33120
- const lastSegment = segments[segments.length - 1];
33121
- return {
33122
- ...line,
33123
- from: new Date(firstSegment.departureDateTime).toISOString(),
33124
- to: new Date(lastSegment.arrivalDateTime).toISOString(),
33125
- productName: `${firstSegment.departureAirportName} - ${lastSegment.arrivalAirportName} (${firstSegment.marketingAirlineName})`,
33126
- productCode: `${firstSegment.departureAirportCode} ${lastSegment.arrivalAirportCode}/${firstSegment.marketingAirlineCode}`,
33127
- accommodationName: firstSegment.metaData?.farePriceClassName ?? line.accommodationName,
33128
- accommodationCode: firstSegment.metaData?.fareCode ?? line.accommodationCode,
33129
- flightInformation: {
33130
- pnr: '',
33131
- flightLines: mapFlightSegmentsToFlightLines(segments)
33132
- },
33133
- isChanged: true
33134
- };
33135
- };
33136
-
33137
- const getLocation = (result) => {
33138
- const place = result.locationName || result.regionName || result.oordName;
33139
- if (!place)
33140
- return result.countryName ?? '';
33141
- return result.countryName ? `${place}, ${result.countryName}` : place;
33142
- };
33143
- const toPlainText = (value) => {
33144
- if (!value)
33145
- return '';
33146
- return he
33147
- .decode(value.replace(/<[^>]*>/g, ' '))
33148
- .replace(/\s+/g, ' ')
33149
- .trim();
33150
- };
33151
- const TrajectNodeCard = ({ item, result, isSelected, languageCode, translations, onSelect, showNights }) => {
33152
- const { node } = item;
33153
- const selectedOption = lodash.first(result.rooms)?.options?.find((x) => x.isSelected) ?? lodash.first(result.rooms)?.options?.[0];
33154
- const price = formatPrice$3(result.price, result.currencyCode, languageCode);
33155
- const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
33156
- const candidate = findCandidateForResult(node, result);
33157
- const image = candidate?.imageUrl ?? null;
33158
- const description = toPlainText(candidate?.description);
33159
- const title = candidate?.name ?? result.name;
33160
- const priceBlock = (React__default["default"].createElement("div", { className: "search__result-card__price__wrapper" },
33161
- React__default["default"].createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
33162
- React__default["default"].createElement("span", { className: "search__result-card__price" }, price)));
33163
- const selectButton = (React__default["default"].createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT));
33164
- if (result.contents?.length) {
33165
- return (React__default["default"].createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
33166
- React__default["default"].createElement("div", { className: "search__result-card__top", dangerouslySetInnerHTML: { __html: he.decode(result.contents) } }),
33167
- React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton)));
33168
- }
33169
- return (React__default["default"].createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
33170
- image && (React__default["default"].createElement("div", { className: "search__result-card__img-wrapper" },
33171
- React__default["default"].createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
33172
- priceBlock)),
33173
- React__default["default"].createElement("div", { className: "search__result-card__content" },
33174
- React__default["default"].createElement("div", { className: "search__result-card__content__wrapper" },
33175
- React__default["default"].createElement("div", { className: "search__result-card__header" },
33176
- React__default["default"].createElement("div", { className: "search__result-card__header__wrapper" },
33177
- !!result.stars && (React__default["default"].createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default["default"].createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
33178
- React__default["default"].createElement("h3", { className: "search__result-card__title" }, title)),
33179
- !image && priceBlock),
33180
- React__default["default"].createElement("span", { className: "search__result-card__location" },
33181
- React__default["default"].createElement(Icon, { name: "ui-location", height: 16 }),
33182
- getLocation(result)),
33183
- React__default["default"].createElement("div", { className: "search__result-card__options" },
33184
- showNights && nights > 0 && (React__default["default"].createElement("div", { className: "search__result-card__option" },
33185
- React__default["default"].createElement(Icon, { name: "ui-moon", height: 16 }),
33186
- nights,
33187
- " ",
33188
- translations?.SRP.NIGHTS)),
33189
- selectedOption?.accommodationName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
33190
- React__default["default"].createElement(Icon, { name: "ui-bed", height: 16 }),
33191
- selectedOption.accommodationName)),
33192
- selectedOption?.regimeName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
33193
- React__default["default"].createElement(Icon, { name: "ui-utensils", height: 16 }),
33194
- selectedOption.regimeName))),
33195
- description && React__default["default"].createElement("p", { className: "search__result-card__description" }, description)),
33196
- React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton))));
33197
- };
33198
-
33199
33289
  const getNodeIcon = (node) => {
33200
33290
  if (isTrajectFlightNode(node))
33201
33291
  return 'ui-flight';
@@ -33235,13 +33325,28 @@ const TrajectResults = ({ isLoading }) => {
33235
33325
  if (!result)
33236
33326
  return;
33237
33327
  dispatch(setTrajectNodeSelection({ lineGuid: item.line.guid, code }));
33238
- dispatch(updateEditableEntryLine(applyResultToLine(item.line, result)));
33328
+ applyResultToLines(item.lines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
33329
+ };
33330
+ const handleShowMore = (item) => {
33331
+ dispatch(setTrajectEditLineGuid(item.line.guid));
33332
+ dispatch(setFlyInType('acco-results'));
33333
+ dispatch(setFlyInIsOpen(true));
33334
+ };
33335
+ const handleEditOptions = (item) => {
33336
+ const availability = trajectNodeAvailability[item.line.guid];
33337
+ const result = availability?.results.find((x) => x.code === availability?.selectedCode);
33338
+ if (!result)
33339
+ return;
33340
+ dispatch(setPackagingAccoSearchDetails([result]));
33341
+ dispatch(setSelectedPackagingAccoResult(result.code));
33342
+ dispatch(setTrajectEditLineGuid(item.line.guid));
33343
+ dispatch(setFlyInType('acco-details'));
33344
+ dispatch(setFlyInIsOpen(true));
33239
33345
  };
33240
33346
  const renderServiceNode = (item) => {
33241
33347
  console.log('renderServiceNode', item);
33242
33348
  const { node, line } = item;
33243
33349
  const availability = trajectNodeAvailability[line.guid];
33244
- const isExpanded = !!expandedNodes[line.guid];
33245
33350
  const results = availability?.results ?? [];
33246
33351
  const selected = results.find((x) => x.code === availability?.selectedCode) ?? null;
33247
33352
  const configured = results.filter((x) => x.code !== selected?.code && !!findCandidateForResult(node, x));
@@ -33263,9 +33368,13 @@ const TrajectResults = ({ isLoading }) => {
33263
33368
  node.preferredAccommodationName,
33264
33369
  " \u2014 ",
33265
33370
  translations.SRP.NO_RESULTS))),
33266
- React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" }, [selected, ...configured, ...(isExpanded ? others : [])].map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, item: item, result: result, isSelected: result.code === selected.code, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, result.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE })))),
33371
+ React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--compact" }, [selected, ...configured].map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, item: item, 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 })))),
33267
33372
  others.length > 0 && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
33268
- React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => toggleExpanded(line.guid) }, isExpanded ? translations.SRP.SHOW_LESS : `${translations.SRP.SHOW_MORE} (${others.length})`)))))));
33373
+ React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => handleShowMore(item) },
33374
+ translations.SRP.SHOW_MORE,
33375
+ " (",
33376
+ others.length,
33377
+ ")")))))));
33269
33378
  };
33270
33379
  const handleSelectFlight = (item, flight) => {
33271
33380
  dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
@@ -33310,6 +33419,8 @@ const TrajectResults = ({ isLoading }) => {
33310
33419
  if (!dayNodes.length)
33311
33420
  return null;
33312
33421
  return (React__default["default"].createElement(React__default["default"].Fragment, { key: `day-${day.order}` }, dayNodes.map((node) => {
33422
+ if (node.serviceType !== ACCOMMODATION_SERVICE_TYPE && node.serviceType !== EXCURSION_SERVICE_TYPE && node.serviceType !== FLIGHT_SERVICE_TYPE)
33423
+ return null;
33313
33424
  // if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) return null;
33314
33425
  // if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE) return renderContextNode(node);
33315
33426
  if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
@@ -33327,6 +33438,7 @@ const useTrajectAvailability = () => {
33327
33438
  const context = React.useContext(SearchResultsConfigurationContext);
33328
33439
  const dispatch = reactRedux.useDispatch();
33329
33440
  const trajectEntry = context?.trajectEntry;
33441
+ const showNonPreferredItems = !!context?.showNonPreferredItems;
33330
33442
  React.useEffect(() => {
33331
33443
  if (!context || !trajectEntry)
33332
33444
  return;
@@ -33371,18 +33483,20 @@ const useTrajectAvailability = () => {
33371
33483
  return null;
33372
33484
  }
33373
33485
  }));
33374
- let results = attempts.filter((x) => !!x).filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
33375
- let picked = results[0] ?? null;
33486
+ const configuredResults = attempts
33487
+ .filter((x) => !!x)
33488
+ .filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
33376
33489
  const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
33377
- let preferredUnavailable = !!preferredCandidate && !results.some((x) => matchesCode(x, preferredCandidate.code));
33378
- // Nothing configured was available — fall back to the cheapest result in the destination.
33379
- if (!picked) {
33380
- results = (await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [];
33381
- picked = pickCheapest(results);
33490
+ let preferredUnavailable = !!preferredCandidate && !configuredResults.some((x) => matchesCode(x, preferredCandidate.code));
33491
+ const destinationResults = showNonPreferredItems
33492
+ ? ((await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? []).filter((x) => !configuredResults.some((configured) => configured.code === x.code))
33493
+ : [];
33494
+ const results = [...configuredResults, ...destinationResults];
33495
+ const picked = configuredResults[0] ?? pickCheapest(destinationResults);
33496
+ if (!configuredResults.length)
33382
33497
  preferredUnavailable = candidates.length > 0;
33383
- }
33384
33498
  if (controller.signal.aborted)
33385
- return null;
33499
+ return [];
33386
33500
  dispatch(setTrajectNodeAvailability({
33387
33501
  lineGuid,
33388
33502
  results,
@@ -33390,58 +33504,58 @@ const useTrajectAvailability = () => {
33390
33504
  preferredUnavailable
33391
33505
  }));
33392
33506
  if (!picked)
33393
- return lineGuid;
33394
- dispatch(updateEditableEntryLine(applyResultToLine(item.line, picked)));
33395
- return null;
33507
+ return item.lines.map((x) => x.guid);
33508
+ applyResultToLines(item.lines, picked).forEach((line) => dispatch(updateEditableEntryLine(line)));
33509
+ return [];
33396
33510
  }
33397
33511
  catch (error) {
33398
33512
  if (controller.signal.aborted)
33399
- return null;
33513
+ return [];
33400
33514
  console.error('Failed to resolve traject node availability', item.node.name, error);
33401
33515
  dispatch(setTrajectNodeAvailability({ lineGuid, results: [], selectedCode: null, preferredUnavailable: false }));
33402
- return lineGuid;
33516
+ return item.lines.map((x) => x.guid);
33403
33517
  }
33404
33518
  };
33405
33519
  const resolveFlightNode = async (item) => {
33406
33520
  const lineGuid = item.line.guid;
33407
33521
  if (!item.node.departureAirportCode || !item.node.arrivalAirportCode) {
33408
33522
  dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
33409
- return lineGuid;
33523
+ return item.lines.map((x) => x.guid);
33410
33524
  }
33411
33525
  dispatch(setTrajectFlightLoading(lineGuid));
33412
33526
  try {
33413
33527
  const flights = (await build.searchPackagingFlights(config, buildTrajectFlightRequest(item, searchContext, flightPax), controller.signal)) ?? [];
33414
33528
  if (controller.signal.aborted)
33415
- return null;
33529
+ return [];
33416
33530
  const picked = pickCheapestFlight(flights);
33417
33531
  dispatch(setTrajectFlightAvailability({ lineGuid, results: flights, selectedGuid: picked?.outwardGuid ?? null }));
33418
33532
  if (!picked)
33419
- return lineGuid;
33533
+ return item.lines.map((x) => x.guid);
33420
33534
  dispatch(updateEditableEntryLine(applyFlightToLine(item.line, picked)));
33421
- return null;
33535
+ return [];
33422
33536
  }
33423
33537
  catch (error) {
33424
33538
  if (controller.signal.aborted)
33425
- return null;
33539
+ return [];
33426
33540
  console.error('Failed to resolve traject flight', item.node.name, error);
33427
33541
  dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
33428
- return lineGuid;
33542
+ return item.lines.map((x) => x.guid);
33429
33543
  }
33430
33544
  };
33431
33545
  (async () => {
33432
33546
  const searchable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node));
33433
33547
  const flightNodes = nodesWithLines.filter((x) => isTrajectFlightNode(x.node));
33434
- const unresolved = await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)]);
33548
+ const unresolved = (await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)])).flat();
33435
33549
  if (controller.signal.aborted)
33436
33550
  return;
33437
33551
  // A node with no availability anywhere cannot be priced or booked, so its line comes off
33438
33552
  // the entry. The card stays visible and reports the gap.
33439
- const emptyLineGuids = unresolved.filter((guid) => !!guid);
33553
+ const emptyLineGuids = unresolved.filter(Boolean);
33440
33554
  if (emptyLineGuids.length)
33441
33555
  dispatch(removeEditableEntryLines(emptyLineGuids));
33442
33556
  })();
33443
33557
  return () => controller.abort();
33444
- }, [trajectEntry]);
33558
+ }, [trajectEntry, showNonPreferredItems]);
33445
33559
  };
33446
33560
 
33447
33561
  const SearchResultsContainer = ({ onBookingStarted }) => {
@@ -33449,7 +33563,7 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
33449
33563
  const dispatch = reactRedux.useDispatch();
33450
33564
  const context = React.useContext(SearchResultsConfigurationContext);
33451
33565
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
33452
- const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry } = reactRedux.useSelector((state) => state.searchResults);
33566
+ const { results, filteredResults, packagingAccoResults, filteredPackagingAccoResults, isLoading, flightsLoading, initialFilters, filters, flightFilters, selectedSortType, selectedFlightSortType, selectedSearchResult, selectedPackagingAccoResultCode, flyInIsOpen, packagingAccoSearchDetails, editablePackagingEntry, transactionId, flyInType, packagingFlightResults, confirmedExcursionsByDay, bookPackagingEntry, trajectEditLineGuid } = reactRedux.useSelector((state) => state.searchResults);
33453
33567
  const isMobile = useMediaQuery('(max-width: 1200px)');
33454
33568
  // Resolves live availability per traject node; no-op unless a traject entry is configured.
33455
33569
  useTrajectAvailability();
@@ -33738,6 +33852,20 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
33738
33852
  setDetailsIsLoading(false);
33739
33853
  };
33740
33854
  const handleConfirmHotelSwap = () => {
33855
+ if (context?.trajectEntry && trajectEditLineGuid) {
33856
+ const item = getTrajectNodesWithLines(context.trajectEntry).find((x) => x.line.guid === trajectEditLineGuid);
33857
+ const result = packagingAccoSearchDetails.find((x) => x.code === selectedPackagingAccoResultCode);
33858
+ if (item && result) {
33859
+ const currentLines = item.lines
33860
+ .map((line) => editablePackagingEntry?.lines?.find((x) => x.guid === line.guid))
33861
+ .filter((x) => !!x);
33862
+ applyResultToLines(currentLines, result).forEach((line) => dispatch(updateEditableEntryLine(line)));
33863
+ dispatch(updateTrajectNodeResult({ lineGuid: trajectEditLineGuid, result }));
33864
+ }
33865
+ dispatch(setTrajectEditLineGuid(null));
33866
+ handleFlyInToggle(false);
33867
+ return;
33868
+ }
33741
33869
  const updatedEntry = swapHotelInPackagingEntry();
33742
33870
  if (!updatedEntry)
33743
33871
  return;
@@ -34086,6 +34214,10 @@ const SearchResultsContainer = ({ onBookingStarted }) => {
34086
34214
  const fetchPackagingAccoSearchDetails = async () => {
34087
34215
  if (!selectedPackagingAccoResultCode || !context)
34088
34216
  return;
34217
+ // A traject node already resolved its own availability, options per room included, and hands
34218
+ // that result straight to the fly-in.
34219
+ if (context.trajectEntry)
34220
+ return;
34089
34221
  if (skipInitialPackagingAccoDetailsRef.current) {
34090
34222
  skipInitialPackagingAccoDetailsRef.current = false;
34091
34223
  return;
@@ -37600,20 +37732,20 @@ function ItineraryMapView({ destinations: propDestinations, adults = 2, dateFrom
37600
37732
  const isStartEnd = dest.isStart || dest.isEnd;
37601
37733
  let iconHtml;
37602
37734
  if (isStartEnd) {
37603
- iconHtml = `
37604
- <div class="map-view__marker map-view__marker--plane">
37605
- <svg viewBox="0 0 576 512" fill="currentColor" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
37606
- <path d="M482.3 192c34.2 0 93.7 29 93.7 64c0 36-59.5 64-93.7 64l-116.6 0L265.2 495.9c-5.7 10.1-16.3 16.1-27.8 16.1l-56.2 0c-10.6 0-18.3-10.3-15.8-20.6l49-196.3L112 288 68.8 377.6c-3 6.1-9.2 10.4-16 10.4l-2.8 0c-9.2 0-16-8.8-13.3-17.6L80 256L36.7 141.6C34 132.8 40.8 124 50 124l2.8 0c6.8 0 13 4.3 16 10.4L112 224l102.9 0-49-196.3C163.5 17.3 171.2 7 181.8 7l56.2 0c11.5 0 22.1 6 27.8 16.1L365.7 192l116.6 0z"/>
37607
- </svg>
37735
+ iconHtml = `
37736
+ <div class="map-view__marker map-view__marker--plane">
37737
+ <svg viewBox="0 0 576 512" fill="currentColor" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
37738
+ <path d="M482.3 192c34.2 0 93.7 29 93.7 64c0 36-59.5 64-93.7 64l-116.6 0L265.2 495.9c-5.7 10.1-16.3 16.1-27.8 16.1l-56.2 0c-10.6 0-18.3-10.3-15.8-20.6l49-196.3L112 288 68.8 377.6c-3 6.1-9.2 10.4-16 10.4l-2.8 0c-9.2 0-16-8.8-13.3-17.6L80 256L36.7 141.6C34 132.8 40.8 124 50 124l2.8 0c6.8 0 13 4.3 16 10.4L112 224l102.9 0-49-196.3C163.5 17.3 171.2 7 181.8 7l56.2 0c11.5 0 22.1 6 27.8 16.1L365.7 192l116.6 0z"/>
37739
+ </svg>
37608
37740
  </div>`;
37609
37741
  }
37610
37742
  else {
37611
37743
  stopIndex++;
37612
37744
  const imgStyle = dest.imageUrl ? `style="background-image:url('${dest.imageUrl}')"` : '';
37613
- iconHtml = `
37614
- <div class="map-view__marker map-view__marker--stop" ${imgStyle}>
37615
- ${!dest.imageUrl ? `<span class="map-view__marker-number">${stopIndex}</span>` : ''}
37616
- <span class="map-view__marker-badge">${stopIndex}</span>
37745
+ iconHtml = `
37746
+ <div class="map-view__marker map-view__marker--stop" ${imgStyle}>
37747
+ ${!dest.imageUrl ? `<span class="map-view__marker-number">${stopIndex}</span>` : ''}
37748
+ <span class="map-view__marker-badge">${stopIndex}</span>
37617
37749
  </div>`;
37618
37750
  }
37619
37751
  const icon = L.divIcon({