@qite/tide-components 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27370,7 +27370,7 @@ const initialState$1 = {
27370
27370
  currentStep: 0,
27371
27371
  bookingNumber: undefined,
27372
27372
  trajectNodeAvailability: {},
27373
- trajectFlightsResolved: false
27373
+ trajectFlightAvailability: {}
27374
27374
  };
27375
27375
  const searchResultsSlice = toolkit.createSlice({
27376
27376
  name: 'searchResults',
@@ -27592,16 +27592,38 @@ const searchResultsSlice = toolkit.createSlice({
27592
27592
  const guids = new Set(action.payload);
27593
27593
  state.editablePackagingEntry.lines = (state.editablePackagingEntry.lines ?? []).filter((x) => !guids.has(x.guid));
27594
27594
  },
27595
- setTrajectFlightsResolved(state, action) {
27596
- state.trajectFlightsResolved = action.payload;
27595
+ setTrajectFlightLoading(state, action) {
27596
+ const lineGuid = action.payload;
27597
+ state.trajectFlightAvailability[lineGuid] = {
27598
+ lineGuid,
27599
+ status: 'loading',
27600
+ results: state.trajectFlightAvailability[lineGuid]?.results ?? [],
27601
+ selectedGuid: state.trajectFlightAvailability[lineGuid]?.selectedGuid ?? null
27602
+ };
27603
+ },
27604
+ setTrajectFlightAvailability(state, action) {
27605
+ const { lineGuid, results, selectedGuid } = action.payload;
27606
+ state.trajectFlightAvailability[lineGuid] = {
27607
+ lineGuid,
27608
+ status: selectedGuid ? 'resolved' : 'unavailable',
27609
+ results,
27610
+ selectedGuid
27611
+ };
27612
+ },
27613
+ setTrajectFlightSelection(state, action) {
27614
+ const flight = state.trajectFlightAvailability[action.payload.lineGuid];
27615
+ if (!flight)
27616
+ return;
27617
+ flight.selectedGuid = action.payload.guid;
27618
+ flight.status = 'resolved';
27597
27619
  },
27598
27620
  resetTrajectAvailability(state) {
27599
27621
  state.trajectNodeAvailability = {};
27600
- state.trajectFlightsResolved = false;
27622
+ state.trajectFlightAvailability = {};
27601
27623
  }
27602
27624
  }
27603
27625
  });
27604
- 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, setTrajectFlightsResolved, resetTrajectAvailability, updateEditableEntryLine, removeEditableEntryLines } = searchResultsSlice.actions;
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;
27605
27627
  var searchResultsReducer = searchResultsSlice.reducer;
27606
27628
 
27607
27629
  const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
@@ -32967,6 +32989,8 @@ const buildDestination = (line) => {
32967
32989
  return { id: line.region.id, isRegion: true };
32968
32990
  if (line.country?.id)
32969
32991
  return { id: line.country.id, isCountry: true };
32992
+ if (line.latitude && line.longitude)
32993
+ return { latitude: line.latitude, longitude: line.longitude };
32970
32994
  return { id: 0 };
32971
32995
  };
32972
32996
  const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
@@ -32986,10 +33010,27 @@ const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
32986
33010
  rooms: context.rooms,
32987
33011
  tagIds: []
32988
33012
  });
32989
- const getPreferredCodes = (node) => {
32990
- const codes = [node.code, node.preferredAccommodationCode, ...(node.alternativeAccommodationCodes ?? [])];
32991
- return codes.filter((code) => !!code && code.trim().length > 0).filter((code, index, all) => all.indexOf(code) === index);
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
+ ];
32992
33032
  };
33033
+ const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
32993
33034
  const matchesCode = (result, code) => {
32994
33035
  if (result.code === code)
32995
33036
  return true;
@@ -33053,8 +33094,26 @@ const mapFlightSegmentsToFlightLines = (segments) => segments.map((segment) => (
33053
33094
  arrivalAirportDescription: segment.arrivalAirportName,
33054
33095
  durationInTicks: segment.durationInTicks
33055
33096
  }));
33056
- const applyFlightToLine = (line, flight, isOutbound) => {
33057
- const segments = (isOutbound ? flight.outward?.segments : flight.return?.segments) ?? [];
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 ?? [];
33058
33117
  if (!segments.length)
33059
33118
  return line;
33060
33119
  const firstSegment = segments[0];
@@ -33074,14 +33133,6 @@ const applyFlightToLine = (line, flight, isOutbound) => {
33074
33133
  isChanged: true
33075
33134
  };
33076
33135
  };
33077
- const pairTrajectFlights = (nodes) => {
33078
- const flights = nodes.filter((x) => isTrajectFlightNode(x.node));
33079
- return {
33080
- outbound: flights[0] ?? null,
33081
- inbound: flights.length > 1 ? flights[flights.length - 1] : null,
33082
- unsupported: flights.slice(1, Math.max(flights.length - 1, 1))
33083
- };
33084
- };
33085
33136
 
33086
33137
  const getLocation = (result) => {
33087
33138
  const place = result.locationName || result.regionName || result.oordName;
@@ -33089,10 +33140,26 @@ const getLocation = (result) => {
33089
33140
  return result.countryName ?? '';
33090
33141
  return result.countryName ? `${place}, ${result.countryName}` : place;
33091
33142
  };
33092
- const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSelect, showNights }) => {
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;
33093
33153
  const selectedOption = lodash.first(result.rooms)?.options?.find((x) => x.isSelected) ?? lodash.first(result.rooms)?.options?.[0];
33094
33154
  const price = formatPrice$3(result.price, result.currencyCode, languageCode);
33095
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)));
33096
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));
33097
33164
  if (result.contents?.length) {
33098
33165
  return (React__default["default"].createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
@@ -33100,21 +33167,22 @@ const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSel
33100
33167
  React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton)));
33101
33168
  }
33102
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)),
33103
33173
  React__default["default"].createElement("div", { className: "search__result-card__content" },
33104
33174
  React__default["default"].createElement("div", { className: "search__result-card__content__wrapper" },
33105
33175
  React__default["default"].createElement("div", { className: "search__result-card__header" },
33106
33176
  React__default["default"].createElement("div", { className: "search__result-card__header__wrapper" },
33107
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 }))))),
33108
- React__default["default"].createElement("h3", { className: "search__result-card__title" }, result.name)),
33109
- React__default["default"].createElement("div", { className: "search__result-card__price__wrapper" },
33110
- React__default["default"].createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
33111
- React__default["default"].createElement("span", { className: "search__result-card__price" }, price))),
33178
+ React__default["default"].createElement("h3", { className: "search__result-card__title" }, title)),
33179
+ !image && priceBlock),
33112
33180
  React__default["default"].createElement("span", { className: "search__result-card__location" },
33113
33181
  React__default["default"].createElement(Icon, { name: "ui-location", height: 16 }),
33114
33182
  getLocation(result)),
33115
33183
  React__default["default"].createElement("div", { className: "search__result-card__options" },
33116
33184
  showNights && nights > 0 && (React__default["default"].createElement("div", { className: "search__result-card__option" },
33117
- React__default["default"].createElement(Icon, { name: "ui-bed", height: 16 }),
33185
+ React__default["default"].createElement(Icon, { name: "ui-moon", height: 16 }),
33118
33186
  nights,
33119
33187
  " ",
33120
33188
  translations?.SRP.NIGHTS)),
@@ -33123,7 +33191,8 @@ const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSel
33123
33191
  selectedOption.accommodationName)),
33124
33192
  selectedOption?.regimeName && (React__default["default"].createElement("div", { className: "search__result-card__option" },
33125
33193
  React__default["default"].createElement(Icon, { name: "ui-utensils", height: 16 }),
33126
- selectedOption.regimeName)))),
33194
+ selectedOption.regimeName))),
33195
+ description && React__default["default"].createElement("p", { className: "search__result-card__description" }, description)),
33127
33196
  React__default["default"].createElement("div", { className: "search__result-card__footer" }, selectButton))));
33128
33197
  };
33129
33198
 
@@ -33141,15 +33210,10 @@ const TrajectResults = ({ isLoading }) => {
33141
33210
  const dispatch = reactRedux.useDispatch();
33142
33211
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
33143
33212
  const locale = getLocale(context?.languageCode ?? 'en-GB');
33144
- const { trajectNodeAvailability, flightsLoading, trajectFlightsResolved, selectedOutwardKey, selectedReturnKey } = reactRedux.useSelector((state) => state.searchResults);
33145
- const uniqueOutwardFlights = reactRedux.useSelector(selectUniqueOutwardFlights);
33146
- const uniqueReturnFlights = reactRedux.useSelector(selectUniqueReturnFlights);
33147
- const selectedOutward = reactRedux.useSelector(selectSelectedOutward);
33148
- const selectedReturn = reactRedux.useSelector(selectSelectedReturn);
33213
+ const { trajectNodeAvailability, trajectFlightAvailability } = reactRedux.useSelector((state) => state.searchResults);
33149
33214
  const [expandedNodes, setExpandedNodes] = React.useState({});
33150
33215
  const trajectEntry = context?.trajectEntry;
33151
33216
  const nodesWithLines = React.useMemo(() => (trajectEntry ? getTrajectNodesWithLines(trajectEntry) : []), [trajectEntry]);
33152
- const flightPairing = React.useMemo(() => pairTrajectFlights(nodesWithLines), [nodesWithLines]);
33153
33217
  const lineByNodeId = React.useMemo(() => new Map(nodesWithLines.map((x) => [x.node.nodeId, x])), [nodesWithLines]);
33154
33218
  const nodesByDay = React.useMemo(() => {
33155
33219
  const map = new Map();
@@ -33174,12 +33238,14 @@ const TrajectResults = ({ isLoading }) => {
33174
33238
  dispatch(updateEditableEntryLine(applyResultToLine(item.line, result)));
33175
33239
  };
33176
33240
  const renderServiceNode = (item) => {
33241
+ console.log('renderServiceNode', item);
33177
33242
  const { node, line } = item;
33178
33243
  const availability = trajectNodeAvailability[line.guid];
33179
33244
  const isExpanded = !!expandedNodes[line.guid];
33180
33245
  const results = availability?.results ?? [];
33181
33246
  const selected = results.find((x) => x.code === availability?.selectedCode) ?? null;
33182
- const alternatives = results.filter((x) => x.code !== availability?.selectedCode);
33247
+ const configured = results.filter((x) => x.code !== selected?.code && !!findCandidateForResult(node, x));
33248
+ const others = results.filter((x) => x.code !== selected?.code && !findCandidateForResult(node, x));
33183
33249
  // No entry yet means the node's search has been queued but not dispatched — still loading.
33184
33250
  const status = availability?.status ?? 'loading';
33185
33251
  return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
@@ -33188,9 +33254,7 @@ const TrajectResults = ({ isLoading }) => {
33188
33254
  React__default["default"].createElement("p", { className: "search__results__label__date-date" }, dateFns.format(new Date(line.from), 'd', { locale })),
33189
33255
  React__default["default"].createElement("p", null, dateFns.format(new Date(line.from), 'MMM', { locale }))),
33190
33256
  React__default["default"].createElement("div", { className: "search__results__label__text" },
33191
- React__default["default"].createElement(Icon, { name: getNodeIcon(node), height: 16 }),
33192
- React__default["default"].createElement("h3", null,
33193
- React__default["default"].createElement("strong", null, node.name)))),
33257
+ React__default["default"].createElement(Icon, { name: getNodeIcon(node), height: 16 }))),
33194
33258
  status === 'loading' && (React__default["default"].createElement(Spinner, { label: node.serviceType === EXCURSION_SERVICE_TYPE ? translations.SRP.LOADING_EXCURSIONS : translations.SRP.LOADING_ACCOMMODATIONS })),
33195
33259
  status === 'unavailable' && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
33196
33260
  status === 'resolved' && selected && (React__default["default"].createElement(React__default["default"].Fragment, null,
@@ -33199,37 +33263,23 @@ const TrajectResults = ({ isLoading }) => {
33199
33263
  node.preferredAccommodationName,
33200
33264
  " \u2014 ",
33201
33265
  translations.SRP.NO_RESULTS))),
33202
- React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" },
33203
- React__default["default"].createElement(TrajectNodeCard, { result: selected, isSelected: true, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, selected.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE }),
33204
- isExpanded &&
33205
- alternatives.map((result) => (React__default["default"].createElement(TrajectNodeCard, { key: result.code, result: result, isSelected: false, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, result.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE })))),
33206
- alternatives.length > 0 && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
33207
- React__default["default"].createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => toggleExpanded(line.guid) }, isExpanded ? translations.SRP.SHOW_LESS : `${translations.SRP.SHOW_MORE} (${alternatives.length})`)))))));
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 })))),
33267
+ 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})`)))))));
33269
+ };
33270
+ const handleSelectFlight = (item, flight) => {
33271
+ dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
33272
+ dispatch(updateEditableEntryLine(applyFlightToLine(item.line, flight)));
33208
33273
  };
33209
33274
  const renderFlightNode = (item) => {
33210
33275
  const { node, line } = item;
33211
- const isOutbound = flightPairing.outbound?.node.nodeId === node.nodeId;
33212
- const isInbound = flightPairing.inbound?.node.nodeId === node.nodeId;
33213
- // A middle leg of a multi-flight traject: the round-trip flight search cannot express it.
33214
- if (!isOutbound && !isInbound) {
33215
- return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
33216
- React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
33217
- React__default["default"].createElement("div", { className: "search__results__label__date" },
33218
- React__default["default"].createElement("p", { className: "search__results__label__date-date" }, dateFns.format(new Date(line.from), 'd', { locale })),
33219
- React__default["default"].createElement("p", null, dateFns.format(new Date(line.from), 'MMM', { locale }))),
33220
- React__default["default"].createElement("div", { className: "search__results__label__text" },
33221
- React__default["default"].createElement(Icon, { name: "ui-flight", height: 16 }),
33222
- React__default["default"].createElement("h3", null,
33223
- React__default["default"].createElement("strong", null, node.name)))),
33224
- React__default["default"].createElement("div", { className: "no-results" },
33225
- node.departureAirportCode,
33226
- " \u2192 ",
33227
- node.arrivalAirportCode)));
33228
- }
33229
- const flights = isOutbound ? uniqueOutwardFlights : uniqueReturnFlights;
33230
- const selectedFlight = isOutbound ? selectedOutward : selectedReturn;
33231
- const selectedKey = isOutbound ? selectedOutwardKey : selectedReturnKey;
33232
- const visible = flights.filter((x) => getFlightKey(isOutbound ? x.outward.segments : x.return.segments) !== selectedKey);
33276
+ const availability = trajectFlightAvailability[line.guid];
33277
+ const status = availability?.status ?? 'loading';
33278
+ const flights = availability?.results ?? [];
33279
+ const selected = flights.find((x) => x.outwardGuid === availability?.selectedGuid) ?? null;
33280
+ const others = flights.filter((x) => x.outwardGuid !== availability?.selectedGuid);
33281
+ const isExpanded = !!expandedNodes[line.guid];
33282
+ const visible = isExpanded ? others : others.slice(0, 2);
33233
33283
  return (React__default["default"].createElement(React__default["default"].Fragment, { key: line.guid },
33234
33284
  React__default["default"].createElement("div", { className: "search__results__label search__results__label--secondary" },
33235
33285
  React__default["default"].createElement("div", { className: "search__results__label__date" },
@@ -33239,42 +33289,37 @@ const TrajectResults = ({ isLoading }) => {
33239
33289
  React__default["default"].createElement(Icon, { name: "ui-flight", height: 16 }),
33240
33290
  React__default["default"].createElement("h3", null,
33241
33291
  translations.SRP.SELECT,
33242
- " ",
33243
- React__default["default"].createElement("strong", null, isOutbound ? translations.SRP.DEPARTURE : translations.SRP.RETURN)))),
33244
- flightsLoading || !trajectFlightsResolved ? (React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS })) : flights.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--extended" },
33245
- selectedFlight && (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${selectedKey}`, item: isOutbound ? selectedFlight.outward : selectedFlight.return, guid: selectedFlight.outwardGuid, selectedGuid: selectedFlight.outwardGuid, isOutward: isOutbound, showSelectedState: true, price: selectedFlight.price, onSelect: isOutbound ? () => dispatch(setSelectedOutwardKey(null)) : undefined })),
33246
- visible.map((result) => (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: isOutbound ? result.outward : result.return, guid: result.outwardGuid, isOutward: isOutbound, price: result.price, currentSelectedPrice: selectedFlight?.price, onSelect: () => dispatch(isOutbound ? setSelectedOutwardKey(getFlightKey(result.outward.segments)) : setSelectedReturnKey(getFlightKey(result.return.segments))) })))))));
33247
- };
33248
- /** Connecting markers and points of interest: shown for context, nothing to book. */
33249
- const renderContextNode = (node) => (React__default["default"].createElement("div", { className: "search__results__cards__actions", key: `${node.nodeId}-context` },
33250
- React__default["default"].createElement("span", null,
33251
- React__default["default"].createElement(Icon, { name: "ui-car", height: 14 }),
33252
- " ",
33253
- node.name,
33254
- node.distanceInKm ? ` ${Math.round(node.distanceInKm)} km` : '')));
33292
+ ' ',
33293
+ React__default["default"].createElement("strong", null,
33294
+ node.departureAirportCode,
33295
+ " - ",
33296
+ node.arrivalAirportCode)))),
33297
+ status === 'loading' && React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS }),
33298
+ status === 'unavailable' && React__default["default"].createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
33299
+ status === 'resolved' && selected && (React__default["default"].createElement(React__default["default"].Fragment, null,
33300
+ React__default["default"].createElement("div", { className: "search__results__cards search__results__cards--extended" },
33301
+ React__default["default"].createElement(IndependentFlightOption, { key: `flight-${selected.outwardGuid}`, item: selected.outward, guid: selected.outwardGuid, selectedGuid: selected.outwardGuid, isOutward: true, showSelectedState: true, price: selected.price }),
33302
+ visible.map((result) => (React__default["default"].createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: result.outward, guid: result.outwardGuid, isOutward: true, price: result.price, currentSelectedPrice: selected.price, onSelect: () => handleSelectFlight(item, result) })))),
33303
+ others.length > 2 && (React__default["default"].createElement("div", { className: "search__results__cards__actions" },
33304
+ 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 - 2})`)))))));
33305
+ };
33255
33306
  if (isLoading)
33256
33307
  return React__default["default"].createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
33257
33308
  return (React__default["default"].createElement(React__default["default"].Fragment, null, trajectEntry.traject.days.map((day) => {
33258
33309
  const dayNodes = nodesByDay.get(day.order) ?? [];
33259
33310
  if (!dayNodes.length)
33260
33311
  return null;
33261
- return (React__default["default"].createElement(React__default["default"].Fragment, { key: `day-${day.order}` },
33262
- React__default["default"].createElement("div", { className: "search__results__label" },
33263
- React__default["default"].createElement("div", { className: "search__results__label__text" },
33264
- React__default["default"].createElement("h2", null,
33265
- translations.ITINERARY.DAY,
33266
- " ",
33267
- day.order + 1,
33268
- day.name ? ` — ${day.name}` : ''),
33269
- React__default["default"].createElement("span", null, dateFns.format(new Date(day.date), 'EEEE d MMMM yyyy', { locale })))),
33270
- dayNodes.map((node) => {
33271
- if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
33272
- return renderContextNode(node);
33273
- const item = lineByNodeId.get(node.nodeId);
33274
- if (!item)
33275
- return renderContextNode(node);
33276
- return isTrajectFlightNode(node) ? renderFlightNode(item) : renderServiceNode(item);
33277
- })));
33312
+ return (React__default["default"].createElement(React__default["default"].Fragment, { key: `day-${day.order}` }, dayNodes.map((node) => {
33313
+ // if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) return null;
33314
+ // if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE) return renderContextNode(node);
33315
+ if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
33316
+ return null;
33317
+ const item = lineByNodeId.get(node.nodeId);
33318
+ // if (!item) return renderContextNode(node);
33319
+ if (!item)
33320
+ return null;
33321
+ return isTrajectFlightNode(node) ? renderFlightNode(item) : renderServiceNode(item);
33322
+ })));
33278
33323
  })));
33279
33324
  };
33280
33325
 
@@ -33282,7 +33327,6 @@ const useTrajectAvailability = () => {
33282
33327
  const context = React.useContext(SearchResultsConfigurationContext);
33283
33328
  const dispatch = reactRedux.useDispatch();
33284
33329
  const trajectEntry = context?.trajectEntry;
33285
- const selectedCombinationFlight = reactRedux.useSelector(selectSelectedCombinationFlight);
33286
33330
  React.useEffect(() => {
33287
33331
  if (!context || !trajectEntry)
33288
33332
  return;
@@ -33302,31 +33346,40 @@ const useTrajectAvailability = () => {
33302
33346
  language: context.languageCode ?? 'en-GB',
33303
33347
  rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
33304
33348
  };
33349
+ // Flight searches take passengers rather than rooms; the API buckets by age.
33350
+ const pax = trajectEntry.entry.pax ?? [];
33351
+ const ageOf = (p) => p.age ?? 30;
33352
+ const adults = pax.filter((p) => ageOf(p) >= 12).length;
33353
+ const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12).length;
33354
+ const babies = pax.filter((p) => ageOf(p) < 2).length;
33355
+ const flightPax = lodash.concat(Array.from({ length: adults }, (_, index) => ({ id: index, age: 31 })), Array.from({ length: kids }, (_, index) => ({ id: index + adults, age: 8 })), Array.from({ length: babies }, (_, index) => ({ id: index + adults + kids, age: 1 })));
33305
33356
  const nodesWithLines = getTrajectNodesWithLines(trajectEntry);
33306
33357
  const resolveNode = async (item) => {
33307
33358
  const lineGuid = item.line.guid;
33308
33359
  const search = item.node.serviceType === EXCURSION_SERVICE_TYPE ? build.searchPackagingExcursions : build.searchPackagingAccommodations;
33309
33360
  dispatch(setTrajectNodeLoading(lineGuid));
33310
33361
  try {
33311
- const preferredCodes = getPreferredCodes(item.node);
33312
- let results = [];
33313
- let picked = null;
33314
- // Try the traject's own product/hotel codes first, in configured order.
33315
- for (const code of preferredCodes) {
33316
- const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, code), controller.signal);
33317
- const hit = (attempt ?? []).find((result) => matchesCode(result, code));
33318
- if (hit) {
33319
- results = attempt;
33320
- picked = hit;
33321
- break;
33362
+ const candidates = getNodeCandidates(item.node);
33363
+ const attempts = await Promise.all(candidates.map(async (candidate) => {
33364
+ try {
33365
+ const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, candidate.code), controller.signal);
33366
+ return (attempt ?? []).find((result) => matchesCode(result, candidate.code)) ?? null;
33322
33367
  }
33323
- }
33324
- let preferredUnavailable = false;
33368
+ catch (error) {
33369
+ if (!controller.signal.aborted)
33370
+ console.error('Traject hotel unavailable', candidate.code, error);
33371
+ return null;
33372
+ }
33373
+ }));
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;
33376
+ const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
33377
+ let preferredUnavailable = !!preferredCandidate && !results.some((x) => matchesCode(x, preferredCandidate.code));
33325
33378
  // Nothing configured was available — fall back to the cheapest result in the destination.
33326
33379
  if (!picked) {
33327
33380
  results = (await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [];
33328
33381
  picked = pickCheapest(results);
33329
- preferredUnavailable = preferredCodes.length > 0;
33382
+ preferredUnavailable = candidates.length > 0;
33330
33383
  }
33331
33384
  if (controller.signal.aborted)
33332
33385
  return null;
@@ -33349,59 +33402,36 @@ const useTrajectAvailability = () => {
33349
33402
  return lineGuid;
33350
33403
  }
33351
33404
  };
33352
- const resolveFlights = async () => {
33353
- const { outbound, inbound } = pairTrajectFlights(nodesWithLines);
33354
- if (!outbound) {
33355
- dispatch(setTrajectFlightsResolved(true));
33356
- return;
33405
+ const resolveFlightNode = async (item) => {
33406
+ const lineGuid = item.line.guid;
33407
+ if (!item.node.departureAirportCode || !item.node.arrivalAirportCode) {
33408
+ dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
33409
+ return lineGuid;
33357
33410
  }
33358
- dispatch(setFlightsLoading(true));
33411
+ dispatch(setTrajectFlightLoading(lineGuid));
33359
33412
  try {
33360
- const pax = trajectEntry.entry.pax ?? [];
33361
- const ageOf = (p) => p.age ?? 30;
33362
- const adults = pax.filter((p) => ageOf(p) >= 12).length;
33363
- const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12).length;
33364
- const babies = pax.filter((p) => ageOf(p) < 2).length;
33365
- const request = {
33366
- transactionId: trajectEntry.entry.transactionId,
33367
- officeId: searchContext.officeId,
33368
- catalogueId: searchContext.catalogueId,
33369
- agentId: searchContext.agentId,
33370
- language: searchContext.language,
33371
- departureAirportCode: outbound.node.departureAirportCode ?? '',
33372
- arrivalAirportCode: outbound.node.arrivalAirportCode ?? '',
33373
- returnAirportCode: inbound?.node.arrivalAirportCode ?? outbound.node.departureAirportCode ?? null,
33374
- luggageIncluded: null,
33375
- maxStops: null,
33376
- travelClass: null,
33377
- pax: lodash.concat(Array.from({ length: adults }, (_, index) => ({ id: index, age: 31 })), Array.from({ length: kids }, (_, index) => ({ id: index + adults, age: 8 })), Array.from({ length: babies }, (_, index) => ({ id: index + adults + kids, age: 1 }))),
33378
- outward: { date: dateToDateStruct(new Date(outbound.line.from)) },
33379
- return: { date: dateToDateStruct(new Date((inbound ?? outbound).line.from)) }
33380
- };
33381
- const flights = (await build.searchPackagingFlights(config, request, controller.signal)) ?? [];
33413
+ const flights = (await build.searchPackagingFlights(config, buildTrajectFlightRequest(item, searchContext, flightPax), controller.signal)) ?? [];
33382
33414
  if (controller.signal.aborted)
33383
- return;
33384
- dispatch(setPackagingFlightResults(flights));
33385
- const firstFlight = lodash.first(flights);
33386
- if (firstFlight) {
33387
- dispatch(setSelectedOutwardKey(getFlightKey(firstFlight.outward.segments)));
33388
- dispatch(setSelectedReturnKey(getFlightKey(firstFlight.return.segments)));
33389
- }
33415
+ return null;
33416
+ const picked = pickCheapestFlight(flights);
33417
+ dispatch(setTrajectFlightAvailability({ lineGuid, results: flights, selectedGuid: picked?.outwardGuid ?? null }));
33418
+ if (!picked)
33419
+ return lineGuid;
33420
+ dispatch(updateEditableEntryLine(applyFlightToLine(item.line, picked)));
33421
+ return null;
33390
33422
  }
33391
33423
  catch (error) {
33392
- if (!controller.signal.aborted)
33393
- console.error('Failed to resolve traject flights', error);
33394
- }
33395
- finally {
33396
- if (!controller.signal.aborted) {
33397
- dispatch(setFlightsLoading(false));
33398
- dispatch(setTrajectFlightsResolved(true));
33399
- }
33424
+ if (controller.signal.aborted)
33425
+ return null;
33426
+ console.error('Failed to resolve traject flight', item.node.name, error);
33427
+ dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
33428
+ return lineGuid;
33400
33429
  }
33401
33430
  };
33402
33431
  (async () => {
33403
33432
  const searchable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node));
33404
- const [unresolved] = await Promise.all([Promise.all(searchable.map(resolveNode)), resolveFlights()]);
33433
+ const flightNodes = nodesWithLines.filter((x) => isTrajectFlightNode(x.node));
33434
+ const unresolved = await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)]);
33405
33435
  if (controller.signal.aborted)
33406
33436
  return;
33407
33437
  // A node with no availability anywhere cannot be priced or booked, so its line comes off
@@ -33412,16 +33442,6 @@ const useTrajectAvailability = () => {
33412
33442
  })();
33413
33443
  return () => controller.abort();
33414
33444
  }, [trajectEntry]);
33415
- // Keep the traject's own flight lines in step with whatever outward/return the user picks.
33416
- React.useEffect(() => {
33417
- if (!trajectEntry || !selectedCombinationFlight)
33418
- return;
33419
- const { outbound, inbound } = pairTrajectFlights(getTrajectNodesWithLines(trajectEntry));
33420
- if (outbound)
33421
- dispatch(updateEditableEntryLine(applyFlightToLine(outbound.line, selectedCombinationFlight, true)));
33422
- if (inbound)
33423
- dispatch(updateEditableEntryLine(applyFlightToLine(inbound.line, selectedCombinationFlight, false)));
33424
- }, [trajectEntry, selectedCombinationFlight]);
33425
33445
  };
33426
33446
 
33427
33447
  const SearchResultsContainer = ({ onBookingStarted }) => {
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
2
  import { PackagingAccommodationResponse } from '@qite/tide-client';
3
+ import { TrajectNodeWithLine } from '../../utils/traject-utils';
3
4
  interface TrajectNodeCardProps {
5
+ item: TrajectNodeWithLine;
4
6
  result: PackagingAccommodationResponse;
5
7
  isSelected: boolean;
6
8
  languageCode?: string;
@@ -40,7 +40,8 @@ export interface SearchResultsState {
40
40
  bookingNumber?: string;
41
41
  /** Per-traject-node availability, keyed by the entry line guid the node maps to. */
42
42
  trajectNodeAvailability: Record<string, TrajectNodeAvailability>;
43
- trajectFlightsResolved: boolean;
43
+ /** Per-flight-node one-way search results, keyed by the entry line guid. */
44
+ trajectFlightAvailability: Record<string, TrajectFlightAvailability>;
44
45
  }
45
46
  export type TrajectNodeStatus = 'idle' | 'loading' | 'resolved' | 'unavailable';
46
47
  export interface TrajectNodeAvailability {
@@ -50,6 +51,12 @@ export interface TrajectNodeAvailability {
50
51
  selectedCode: string | null;
51
52
  preferredUnavailable: boolean;
52
53
  }
54
+ export interface TrajectFlightAvailability {
55
+ lineGuid: string;
56
+ status: TrajectNodeStatus;
57
+ results: PackagingFlightResponse[];
58
+ selectedGuid: string | null;
59
+ }
53
60
  export declare const setResults: import("@reduxjs/toolkit").ActionCreatorWithPayload<BookingPackageItem[], "searchResults/setResults">, setFilteredResults: import("@reduxjs/toolkit").ActionCreatorWithPayload<BookingPackageItem[], "searchResults/setFilteredResults">, setSelectedSearchResult: import("@reduxjs/toolkit").ActionCreatorWithPayload<BookingPackageItem | null, "searchResults/setSelectedSearchResult">, setPackagingAccoResults: import("@reduxjs/toolkit").ActionCreatorWithPayload<PackagingAccommodationResponse[], "searchResults/setPackagingAccoResults">, setFilteredPackagingAccoResults: import("@reduxjs/toolkit").ActionCreatorWithPayload<PackagingAccommodationResponse[], "searchResults/setFilteredPackagingAccoResults">, setFilteredPackagingFlightResults: import("@reduxjs/toolkit").ActionCreatorWithPayload<PackagingFlightResponse[], "searchResults/setFilteredPackagingFlightResults">, setPackagingAccoSearchDetails: import("@reduxjs/toolkit").ActionCreatorWithPayload<PackagingAccommodationResponse[], "searchResults/setPackagingAccoSearchDetails">, setSelectedPackagingAccoResult: import("@reduxjs/toolkit").ActionCreatorWithPayload<string | null, "searchResults/setSelectedPackagingAccoResult">, setPackagingFlightResults: import("@reduxjs/toolkit").ActionCreatorWithPayload<PackagingFlightResponse[], "searchResults/setPackagingFlightResults">, setSelectedPackagingFlight: import("@reduxjs/toolkit").ActionCreatorWithPayload<PackagingFlightResponse | null, "searchResults/setSelectedPackagingFlight">, setSelectedFlight: import("@reduxjs/toolkit").ActionCreatorWithPayload<ExtendedFlightSearchResponseItem | null, "searchResults/setSelectedFlight">, setSelectedFlightDetails: import("@reduxjs/toolkit").ActionCreatorWithPayload<ExtendedFlightSearchResponseItem | null, "searchResults/setSelectedFlightDetails">, setBookingPackageDetails: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
54
61
  details: BookingPackage;
55
62
  }, "searchResults/setBookingPackageDetails">, selectFlight: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
@@ -71,6 +78,13 @@ export declare const setResults: import("@reduxjs/toolkit").ActionCreatorWithPay
71
78
  }, "searchResults/setTrajectNodeAvailability">, setTrajectNodeSelection: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
72
79
  lineGuid: string;
73
80
  code: string;
74
- }, "searchResults/setTrajectNodeSelection">, setTrajectFlightsResolved: import("@reduxjs/toolkit").ActionCreatorWithPayload<boolean, "searchResults/setTrajectFlightsResolved">, resetTrajectAvailability: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"searchResults/resetTrajectAvailability">, updateEditableEntryLine: import("@reduxjs/toolkit").ActionCreatorWithPayload<PackagingEntryLine, "searchResults/updateEditableEntryLine">, removeEditableEntryLines: import("@reduxjs/toolkit").ActionCreatorWithPayload<string[], "searchResults/removeEditableEntryLines">;
81
+ }, "searchResults/setTrajectNodeSelection">, setTrajectFlightLoading: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "searchResults/setTrajectFlightLoading">, setTrajectFlightAvailability: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
82
+ lineGuid: string;
83
+ results: PackagingFlightResponse[];
84
+ selectedGuid: string | null;
85
+ }, "searchResults/setTrajectFlightAvailability">, setTrajectFlightSelection: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
86
+ lineGuid: string;
87
+ guid: string;
88
+ }, "searchResults/setTrajectFlightSelection">, resetTrajectAvailability: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"searchResults/resetTrajectAvailability">, updateEditableEntryLine: import("@reduxjs/toolkit").ActionCreatorWithPayload<PackagingEntryLine, "searchResults/updateEditableEntryLine">, removeEditableEntryLines: import("@reduxjs/toolkit").ActionCreatorWithPayload<string[], "searchResults/removeEditableEntryLines">;
75
89
  declare const _default: import("@reduxjs/toolkit").Reducer<SearchResultsState>;
76
90
  export default _default;
@@ -1,5 +1,6 @@
1
- import { PackagingAccommodationRequest, PackagingAccommodationResponse, PackagingEntry, PackagingEntryLine, PackagingFlightResponse, PackagingRoom, PackagingTrajectNode, PackagingTrajectResponse } from '@qite/tide-client';
1
+ import { FlightSearchRequest, PackagingAccommodationRequest, PackagingAccommodationResponse, PackagingEntry, PackagingEntryLine, PackagingFlightResponse, PackagingRoom, PackagingTrajectAlternative, PackagingTrajectNode, PackagingTrajectResponse } from '@qite/tide-client';
2
2
  export declare const REGULAR_TRAJECT_NODE_TYPE = 0;
3
+ export declare const CONNECTING_TRAJECT_NODE_TYPE = 1;
3
4
  export interface TrajectNodeWithLine {
4
5
  node: PackagingTrajectNode;
5
6
  line: PackagingEntryLine;
@@ -18,14 +19,12 @@ export interface TrajectSearchContext {
18
19
  rooms: PackagingAccommodationRequest['rooms'];
19
20
  }
20
21
  export declare const buildTrajectNodeRequest: ({ node, line }: TrajectNodeWithLine, context: TrajectSearchContext, productCode: string) => PackagingAccommodationRequest;
21
- export declare const getPreferredCodes: (node: PackagingTrajectNode) => string[];
22
+ export declare const getNodeCandidates: (node: PackagingTrajectNode) => PackagingTrajectAlternative[];
23
+ export declare const findCandidateForResult: (node: PackagingTrajectNode, result: PackagingAccommodationResponse) => PackagingTrajectAlternative | null;
22
24
  export declare const matchesCode: (result: PackagingAccommodationResponse, code: string) => boolean;
23
25
  export declare const getPackagingRoomsFromEntry: (entry: PackagingEntry) => PackagingRoom[];
24
26
  export declare const pickCheapest: (results: PackagingAccommodationResponse[]) => PackagingAccommodationResponse | null;
25
27
  export declare const applyResultToLine: (line: PackagingEntryLine, result: PackagingAccommodationResponse) => PackagingEntryLine;
26
- export declare const applyFlightToLine: (line: PackagingEntryLine, flight: PackagingFlightResponse, isOutbound: boolean) => PackagingEntryLine;
27
- export declare const pairTrajectFlights: (nodes: TrajectNodeWithLine[]) => {
28
- outbound: TrajectNodeWithLine;
29
- inbound: TrajectNodeWithLine | null;
30
- unsupported: TrajectNodeWithLine[];
31
- };
28
+ export declare const buildTrajectFlightRequest: ({ node, line }: TrajectNodeWithLine, context: TrajectSearchContext, pax: FlightSearchRequest["pax"]) => FlightSearchRequest;
29
+ export declare const pickCheapestFlight: (flights: PackagingFlightResponse[]) => PackagingFlightResponse | null;
30
+ export declare const applyFlightToLine: (line: PackagingEntryLine, flight: PackagingFlightResponse) => PackagingEntryLine;