@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.
@@ -27341,7 +27341,7 @@ const initialState$1 = {
27341
27341
  currentStep: 0,
27342
27342
  bookingNumber: undefined,
27343
27343
  trajectNodeAvailability: {},
27344
- trajectFlightsResolved: false
27344
+ trajectFlightAvailability: {}
27345
27345
  };
27346
27346
  const searchResultsSlice = createSlice({
27347
27347
  name: 'searchResults',
@@ -27563,16 +27563,38 @@ const searchResultsSlice = createSlice({
27563
27563
  const guids = new Set(action.payload);
27564
27564
  state.editablePackagingEntry.lines = (state.editablePackagingEntry.lines ?? []).filter((x) => !guids.has(x.guid));
27565
27565
  },
27566
- setTrajectFlightsResolved(state, action) {
27567
- state.trajectFlightsResolved = action.payload;
27566
+ setTrajectFlightLoading(state, action) {
27567
+ const lineGuid = action.payload;
27568
+ state.trajectFlightAvailability[lineGuid] = {
27569
+ lineGuid,
27570
+ status: 'loading',
27571
+ results: state.trajectFlightAvailability[lineGuid]?.results ?? [],
27572
+ selectedGuid: state.trajectFlightAvailability[lineGuid]?.selectedGuid ?? null
27573
+ };
27574
+ },
27575
+ setTrajectFlightAvailability(state, action) {
27576
+ const { lineGuid, results, selectedGuid } = action.payload;
27577
+ state.trajectFlightAvailability[lineGuid] = {
27578
+ lineGuid,
27579
+ status: selectedGuid ? 'resolved' : 'unavailable',
27580
+ results,
27581
+ selectedGuid
27582
+ };
27583
+ },
27584
+ setTrajectFlightSelection(state, action) {
27585
+ const flight = state.trajectFlightAvailability[action.payload.lineGuid];
27586
+ if (!flight)
27587
+ return;
27588
+ flight.selectedGuid = action.payload.guid;
27589
+ flight.status = 'resolved';
27568
27590
  },
27569
27591
  resetTrajectAvailability(state) {
27570
27592
  state.trajectNodeAvailability = {};
27571
- state.trajectFlightsResolved = false;
27593
+ state.trajectFlightAvailability = {};
27572
27594
  }
27573
27595
  }
27574
27596
  });
27575
- 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;
27597
+ 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;
27576
27598
  var searchResultsReducer = searchResultsSlice.reducer;
27577
27599
 
27578
27600
  const ItemPicker = ({ items, selection, selectedSortByType, label, placeholder, classModifier, onPick, valueFormatter }) => {
@@ -32938,6 +32960,8 @@ const buildDestination = (line) => {
32938
32960
  return { id: line.region.id, isRegion: true };
32939
32961
  if (line.country?.id)
32940
32962
  return { id: line.country.id, isCountry: true };
32963
+ if (line.latitude && line.longitude)
32964
+ return { latitude: line.latitude, longitude: line.longitude };
32941
32965
  return { id: 0 };
32942
32966
  };
32943
32967
  const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
@@ -32957,10 +32981,27 @@ const buildTrajectNodeRequest = ({ node, line }, context, productCode) => ({
32957
32981
  rooms: context.rooms,
32958
32982
  tagIds: []
32959
32983
  });
32960
- const getPreferredCodes = (node) => {
32961
- const codes = [node.code, node.preferredAccommodationCode, ...(node.alternativeAccommodationCodes ?? [])];
32962
- return codes.filter((code) => !!code && code.trim().length > 0).filter((code, index, all) => all.indexOf(code) === index);
32984
+ const getNodeCandidates = (node) => {
32985
+ const configured = (node.alternatives ?? []).filter((x) => !!x.code && x.code.trim().length > 0);
32986
+ if (configured.length)
32987
+ return configured;
32988
+ const fallbackCode = node.code ?? node.preferredAccommodationCode;
32989
+ if (!fallbackCode || !fallbackCode.trim().length)
32990
+ return [];
32991
+ return [
32992
+ {
32993
+ code: fallbackCode,
32994
+ name: node.name,
32995
+ description: node.description,
32996
+ imageUrl: node.imageUrl,
32997
+ contentSource: node.contentSource,
32998
+ vendor: node.vendor,
32999
+ externalVendorId: node.externalVendorId,
33000
+ isPreferred: true
33001
+ }
33002
+ ];
32963
33003
  };
33004
+ const findCandidateForResult = (node, result) => getNodeCandidates(node).find((candidate) => matchesCode(result, candidate.code)) ?? null;
32964
33005
  const matchesCode = (result, code) => {
32965
33006
  if (result.code === code)
32966
33007
  return true;
@@ -33024,8 +33065,26 @@ const mapFlightSegmentsToFlightLines = (segments) => segments.map((segment) => (
33024
33065
  arrivalAirportDescription: segment.arrivalAirportName,
33025
33066
  durationInTicks: segment.durationInTicks
33026
33067
  }));
33027
- const applyFlightToLine = (line, flight, isOutbound) => {
33028
- const segments = (isOutbound ? flight.outward?.segments : flight.return?.segments) ?? [];
33068
+ const buildTrajectFlightRequest = ({ node, line }, context, pax) => ({
33069
+ transactionId: context.transactionId,
33070
+ officeId: context.officeId,
33071
+ catalogueId: context.catalogueId,
33072
+ agentId: context.agentId ?? null,
33073
+ language: context.language,
33074
+ departureAirportCode: node.departureAirportCode ?? '',
33075
+ arrivalAirportCode: node.arrivalAirportCode ?? '',
33076
+ returnAirportCode: null,
33077
+ luggageIncluded: null,
33078
+ maxStops: null,
33079
+ travelClass: null,
33080
+ vendorConfigurationId: node.externalVendorId ?? null,
33081
+ pax,
33082
+ outward: { date: dateToDateStruct(new Date(line.from)) },
33083
+ return: null
33084
+ });
33085
+ const pickCheapestFlight = (flights) => [...flights].sort((a, b) => a.price - b.price)[0] ?? null;
33086
+ const applyFlightToLine = (line, flight) => {
33087
+ const segments = flight.outward?.segments ?? [];
33029
33088
  if (!segments.length)
33030
33089
  return line;
33031
33090
  const firstSegment = segments[0];
@@ -33045,14 +33104,6 @@ const applyFlightToLine = (line, flight, isOutbound) => {
33045
33104
  isChanged: true
33046
33105
  };
33047
33106
  };
33048
- const pairTrajectFlights = (nodes) => {
33049
- const flights = nodes.filter((x) => isTrajectFlightNode(x.node));
33050
- return {
33051
- outbound: flights[0] ?? null,
33052
- inbound: flights.length > 1 ? flights[flights.length - 1] : null,
33053
- unsupported: flights.slice(1, Math.max(flights.length - 1, 1))
33054
- };
33055
- };
33056
33107
 
33057
33108
  const getLocation = (result) => {
33058
33109
  const place = result.locationName || result.regionName || result.oordName;
@@ -33060,10 +33111,26 @@ const getLocation = (result) => {
33060
33111
  return result.countryName ?? '';
33061
33112
  return result.countryName ? `${place}, ${result.countryName}` : place;
33062
33113
  };
33063
- const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSelect, showNights }) => {
33114
+ const toPlainText = (value) => {
33115
+ if (!value)
33116
+ return '';
33117
+ return he
33118
+ .decode(value.replace(/<[^>]*>/g, ' '))
33119
+ .replace(/\s+/g, ' ')
33120
+ .trim();
33121
+ };
33122
+ const TrajectNodeCard = ({ item, result, isSelected, languageCode, translations, onSelect, showNights }) => {
33123
+ const { node } = item;
33064
33124
  const selectedOption = first(result.rooms)?.options?.find((x) => x.isSelected) ?? first(result.rooms)?.options?.[0];
33065
33125
  const price = formatPrice$3(result.price, result.currencyCode, languageCode);
33066
33126
  const nights = calculateNights(new Date(result.fromDate), new Date(result.toDate));
33127
+ const candidate = findCandidateForResult(node, result);
33128
+ const image = candidate?.imageUrl ?? null;
33129
+ const description = toPlainText(candidate?.description);
33130
+ const title = candidate?.name ?? result.name;
33131
+ const priceBlock = (React__default.createElement("div", { className: "search__result-card__price__wrapper" },
33132
+ React__default.createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
33133
+ React__default.createElement("span", { className: "search__result-card__price" }, price)));
33067
33134
  const selectButton = (React__default.createElement("button", { type: "button", className: `cta ${isSelected ? 'cta--selected' : 'cta--select'}`, onClick: onSelect }, isSelected ? translations?.SHARED.SELECTED : translations?.SHARED.SELECT));
33068
33135
  if (result.contents?.length) {
33069
33136
  return (React__default.createElement("div", { className: `search__result-card__wrapper search__result-card__wrapper--custom ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
@@ -33071,21 +33138,22 @@ const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSel
33071
33138
  React__default.createElement("div", { className: "search__result-card__footer" }, selectButton)));
33072
33139
  }
33073
33140
  return (React__default.createElement("div", { className: `search__result-card__wrapper ${isSelected ? 'search__result-card__wrapper--selected' : ''}` },
33141
+ image && (React__default.createElement("div", { className: "search__result-card__img-wrapper" },
33142
+ React__default.createElement("img", { src: image, alt: title, className: "search__result-card__img" }),
33143
+ priceBlock)),
33074
33144
  React__default.createElement("div", { className: "search__result-card__content" },
33075
33145
  React__default.createElement("div", { className: "search__result-card__content__wrapper" },
33076
33146
  React__default.createElement("div", { className: "search__result-card__header" },
33077
33147
  React__default.createElement("div", { className: "search__result-card__header__wrapper" },
33078
33148
  !!result.stars && (React__default.createElement("div", { className: "rating" }, [...Array(result.stars)].map((_, index) => (React__default.createElement(Icon, { name: "ui-star", key: `rating-star-${index + 1}`, width: 14, height: 14 }))))),
33079
- React__default.createElement("h3", { className: "search__result-card__title" }, result.name)),
33080
- React__default.createElement("div", { className: "search__result-card__price__wrapper" },
33081
- React__default.createElement("span", { className: "search__result-card__price__label" }, translations?.SHARED.TOTAL_PRICE),
33082
- React__default.createElement("span", { className: "search__result-card__price" }, price))),
33149
+ React__default.createElement("h3", { className: "search__result-card__title" }, title)),
33150
+ !image && priceBlock),
33083
33151
  React__default.createElement("span", { className: "search__result-card__location" },
33084
33152
  React__default.createElement(Icon, { name: "ui-location", height: 16 }),
33085
33153
  getLocation(result)),
33086
33154
  React__default.createElement("div", { className: "search__result-card__options" },
33087
33155
  showNights && nights > 0 && (React__default.createElement("div", { className: "search__result-card__option" },
33088
- React__default.createElement(Icon, { name: "ui-bed", height: 16 }),
33156
+ React__default.createElement(Icon, { name: "ui-moon", height: 16 }),
33089
33157
  nights,
33090
33158
  " ",
33091
33159
  translations?.SRP.NIGHTS)),
@@ -33094,7 +33162,8 @@ const TrajectNodeCard = ({ result, isSelected, languageCode, translations, onSel
33094
33162
  selectedOption.accommodationName)),
33095
33163
  selectedOption?.regimeName && (React__default.createElement("div", { className: "search__result-card__option" },
33096
33164
  React__default.createElement(Icon, { name: "ui-utensils", height: 16 }),
33097
- selectedOption.regimeName)))),
33165
+ selectedOption.regimeName))),
33166
+ description && React__default.createElement("p", { className: "search__result-card__description" }, description)),
33098
33167
  React__default.createElement("div", { className: "search__result-card__footer" }, selectButton))));
33099
33168
  };
33100
33169
 
@@ -33112,15 +33181,10 @@ const TrajectResults = ({ isLoading }) => {
33112
33181
  const dispatch = useDispatch();
33113
33182
  const translations = getTranslations(context?.languageCode ?? 'en-GB');
33114
33183
  const locale = getLocale(context?.languageCode ?? 'en-GB');
33115
- const { trajectNodeAvailability, flightsLoading, trajectFlightsResolved, selectedOutwardKey, selectedReturnKey } = useSelector((state) => state.searchResults);
33116
- const uniqueOutwardFlights = useSelector(selectUniqueOutwardFlights);
33117
- const uniqueReturnFlights = useSelector(selectUniqueReturnFlights);
33118
- const selectedOutward = useSelector(selectSelectedOutward);
33119
- const selectedReturn = useSelector(selectSelectedReturn);
33184
+ const { trajectNodeAvailability, trajectFlightAvailability } = useSelector((state) => state.searchResults);
33120
33185
  const [expandedNodes, setExpandedNodes] = useState({});
33121
33186
  const trajectEntry = context?.trajectEntry;
33122
33187
  const nodesWithLines = useMemo(() => (trajectEntry ? getTrajectNodesWithLines(trajectEntry) : []), [trajectEntry]);
33123
- const flightPairing = useMemo(() => pairTrajectFlights(nodesWithLines), [nodesWithLines]);
33124
33188
  const lineByNodeId = useMemo(() => new Map(nodesWithLines.map((x) => [x.node.nodeId, x])), [nodesWithLines]);
33125
33189
  const nodesByDay = useMemo(() => {
33126
33190
  const map = new Map();
@@ -33145,12 +33209,14 @@ const TrajectResults = ({ isLoading }) => {
33145
33209
  dispatch(updateEditableEntryLine(applyResultToLine(item.line, result)));
33146
33210
  };
33147
33211
  const renderServiceNode = (item) => {
33212
+ console.log('renderServiceNode', item);
33148
33213
  const { node, line } = item;
33149
33214
  const availability = trajectNodeAvailability[line.guid];
33150
33215
  const isExpanded = !!expandedNodes[line.guid];
33151
33216
  const results = availability?.results ?? [];
33152
33217
  const selected = results.find((x) => x.code === availability?.selectedCode) ?? null;
33153
- const alternatives = results.filter((x) => x.code !== availability?.selectedCode);
33218
+ const configured = results.filter((x) => x.code !== selected?.code && !!findCandidateForResult(node, x));
33219
+ const others = results.filter((x) => x.code !== selected?.code && !findCandidateForResult(node, x));
33154
33220
  // No entry yet means the node's search has been queued but not dispatched — still loading.
33155
33221
  const status = availability?.status ?? 'loading';
33156
33222
  return (React__default.createElement(React__default.Fragment, { key: line.guid },
@@ -33159,9 +33225,7 @@ const TrajectResults = ({ isLoading }) => {
33159
33225
  React__default.createElement("p", { className: "search__results__label__date-date" }, format$2(new Date(line.from), 'd', { locale })),
33160
33226
  React__default.createElement("p", null, format$2(new Date(line.from), 'MMM', { locale }))),
33161
33227
  React__default.createElement("div", { className: "search__results__label__text" },
33162
- React__default.createElement(Icon, { name: getNodeIcon(node), height: 16 }),
33163
- React__default.createElement("h3", null,
33164
- React__default.createElement("strong", null, node.name)))),
33228
+ React__default.createElement(Icon, { name: getNodeIcon(node), height: 16 }))),
33165
33229
  status === 'loading' && (React__default.createElement(Spinner, { label: node.serviceType === EXCURSION_SERVICE_TYPE ? translations.SRP.LOADING_EXCURSIONS : translations.SRP.LOADING_ACCOMMODATIONS })),
33166
33230
  status === 'unavailable' && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
33167
33231
  status === 'resolved' && selected && (React__default.createElement(React__default.Fragment, null,
@@ -33170,37 +33234,23 @@ const TrajectResults = ({ isLoading }) => {
33170
33234
  node.preferredAccommodationName,
33171
33235
  " \u2014 ",
33172
33236
  translations.SRP.NO_RESULTS))),
33173
- React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" },
33174
- React__default.createElement(TrajectNodeCard, { result: selected, isSelected: true, languageCode: context.languageCode, translations: translations, onSelect: () => handleSelect(item, selected.code), showNights: node.serviceType === ACCOMMODATION_SERVICE_TYPE }),
33175
- isExpanded &&
33176
- alternatives.map((result) => (React__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 })))),
33177
- alternatives.length > 0 && (React__default.createElement("div", { className: "search__results__cards__actions" },
33178
- React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => toggleExpanded(line.guid) }, isExpanded ? translations.SRP.SHOW_LESS : `${translations.SRP.SHOW_MORE} (${alternatives.length})`)))))));
33237
+ React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" }, [selected, ...configured, ...(isExpanded ? others : [])].map((result) => (React__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 })))),
33238
+ others.length > 0 && (React__default.createElement("div", { className: "search__results__cards__actions" },
33239
+ React__default.createElement("button", { type: "button", className: "cta cta--secondary", onClick: () => toggleExpanded(line.guid) }, isExpanded ? translations.SRP.SHOW_LESS : `${translations.SRP.SHOW_MORE} (${others.length})`)))))));
33240
+ };
33241
+ const handleSelectFlight = (item, flight) => {
33242
+ dispatch(setTrajectFlightSelection({ lineGuid: item.line.guid, guid: flight.outwardGuid }));
33243
+ dispatch(updateEditableEntryLine(applyFlightToLine(item.line, flight)));
33179
33244
  };
33180
33245
  const renderFlightNode = (item) => {
33181
33246
  const { node, line } = item;
33182
- const isOutbound = flightPairing.outbound?.node.nodeId === node.nodeId;
33183
- const isInbound = flightPairing.inbound?.node.nodeId === node.nodeId;
33184
- // A middle leg of a multi-flight traject: the round-trip flight search cannot express it.
33185
- if (!isOutbound && !isInbound) {
33186
- return (React__default.createElement(React__default.Fragment, { key: line.guid },
33187
- React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
33188
- React__default.createElement("div", { className: "search__results__label__date" },
33189
- React__default.createElement("p", { className: "search__results__label__date-date" }, format$2(new Date(line.from), 'd', { locale })),
33190
- React__default.createElement("p", null, format$2(new Date(line.from), 'MMM', { locale }))),
33191
- React__default.createElement("div", { className: "search__results__label__text" },
33192
- React__default.createElement(Icon, { name: "ui-flight", height: 16 }),
33193
- React__default.createElement("h3", null,
33194
- React__default.createElement("strong", null, node.name)))),
33195
- React__default.createElement("div", { className: "no-results" },
33196
- node.departureAirportCode,
33197
- " \u2192 ",
33198
- node.arrivalAirportCode)));
33199
- }
33200
- const flights = isOutbound ? uniqueOutwardFlights : uniqueReturnFlights;
33201
- const selectedFlight = isOutbound ? selectedOutward : selectedReturn;
33202
- const selectedKey = isOutbound ? selectedOutwardKey : selectedReturnKey;
33203
- const visible = flights.filter((x) => getFlightKey(isOutbound ? x.outward.segments : x.return.segments) !== selectedKey);
33247
+ const availability = trajectFlightAvailability[line.guid];
33248
+ const status = availability?.status ?? 'loading';
33249
+ const flights = availability?.results ?? [];
33250
+ const selected = flights.find((x) => x.outwardGuid === availability?.selectedGuid) ?? null;
33251
+ const others = flights.filter((x) => x.outwardGuid !== availability?.selectedGuid);
33252
+ const isExpanded = !!expandedNodes[line.guid];
33253
+ const visible = isExpanded ? others : others.slice(0, 2);
33204
33254
  return (React__default.createElement(React__default.Fragment, { key: line.guid },
33205
33255
  React__default.createElement("div", { className: "search__results__label search__results__label--secondary" },
33206
33256
  React__default.createElement("div", { className: "search__results__label__date" },
@@ -33210,42 +33260,37 @@ const TrajectResults = ({ isLoading }) => {
33210
33260
  React__default.createElement(Icon, { name: "ui-flight", height: 16 }),
33211
33261
  React__default.createElement("h3", null,
33212
33262
  translations.SRP.SELECT,
33213
- " ",
33214
- React__default.createElement("strong", null, isOutbound ? translations.SRP.DEPARTURE : translations.SRP.RETURN)))),
33215
- flightsLoading || !trajectFlightsResolved ? (React__default.createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS })) : flights.length === 0 ? (React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS)) : (React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" },
33216
- selectedFlight && (React__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 })),
33217
- visible.map((result) => (React__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))) })))))));
33218
- };
33219
- /** Connecting markers and points of interest: shown for context, nothing to book. */
33220
- const renderContextNode = (node) => (React__default.createElement("div", { className: "search__results__cards__actions", key: `${node.nodeId}-context` },
33221
- React__default.createElement("span", null,
33222
- React__default.createElement(Icon, { name: "ui-car", height: 14 }),
33223
- " ",
33224
- node.name,
33225
- node.distanceInKm ? ` ${Math.round(node.distanceInKm)} km` : '')));
33263
+ ' ',
33264
+ React__default.createElement("strong", null,
33265
+ node.departureAirportCode,
33266
+ " - ",
33267
+ node.arrivalAirportCode)))),
33268
+ status === 'loading' && React__default.createElement(Spinner, { label: translations.SRP.LOADING_FLIGHTS }),
33269
+ status === 'unavailable' && React__default.createElement("div", { className: "no-results" }, translations.SRP.NO_RESULTS),
33270
+ status === 'resolved' && selected && (React__default.createElement(React__default.Fragment, null,
33271
+ React__default.createElement("div", { className: "search__results__cards search__results__cards--extended" },
33272
+ React__default.createElement(IndependentFlightOption, { key: `flight-${selected.outwardGuid}`, item: selected.outward, guid: selected.outwardGuid, selectedGuid: selected.outwardGuid, isOutward: true, showSelectedState: true, price: selected.price }),
33273
+ visible.map((result) => (React__default.createElement(IndependentFlightOption, { key: `flight-${result.outwardGuid}`, item: result.outward, guid: result.outwardGuid, isOutward: true, price: result.price, currentSelectedPrice: selected.price, onSelect: () => handleSelectFlight(item, result) })))),
33274
+ others.length > 2 && (React__default.createElement("div", { className: "search__results__cards__actions" },
33275
+ React__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})`)))))));
33276
+ };
33226
33277
  if (isLoading)
33227
33278
  return React__default.createElement(Spinner, { label: translations.SRP.LOADING_ITINERARY });
33228
33279
  return (React__default.createElement(React__default.Fragment, null, trajectEntry.traject.days.map((day) => {
33229
33280
  const dayNodes = nodesByDay.get(day.order) ?? [];
33230
33281
  if (!dayNodes.length)
33231
33282
  return null;
33232
- return (React__default.createElement(React__default.Fragment, { key: `day-${day.order}` },
33233
- React__default.createElement("div", { className: "search__results__label" },
33234
- React__default.createElement("div", { className: "search__results__label__text" },
33235
- React__default.createElement("h2", null,
33236
- translations.ITINERARY.DAY,
33237
- " ",
33238
- day.order + 1,
33239
- day.name ? ` — ${day.name}` : ''),
33240
- React__default.createElement("span", null, format$2(new Date(day.date), 'EEEE d MMMM yyyy', { locale })))),
33241
- dayNodes.map((node) => {
33242
- if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
33243
- return renderContextNode(node);
33244
- const item = lineByNodeId.get(node.nodeId);
33245
- if (!item)
33246
- return renderContextNode(node);
33247
- return isTrajectFlightNode(node) ? renderFlightNode(item) : renderServiceNode(item);
33248
- })));
33283
+ return (React__default.createElement(React__default.Fragment, { key: `day-${day.order}` }, dayNodes.map((node) => {
33284
+ // if (node.trajectNodeType === CONNECTING_TRAJECT_NODE_TYPE) return null;
33285
+ // if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE) return renderContextNode(node);
33286
+ if (node.trajectNodeType !== REGULAR_TRAJECT_NODE_TYPE)
33287
+ return null;
33288
+ const item = lineByNodeId.get(node.nodeId);
33289
+ // if (!item) return renderContextNode(node);
33290
+ if (!item)
33291
+ return null;
33292
+ return isTrajectFlightNode(node) ? renderFlightNode(item) : renderServiceNode(item);
33293
+ })));
33249
33294
  })));
33250
33295
  };
33251
33296
 
@@ -33253,7 +33298,6 @@ const useTrajectAvailability = () => {
33253
33298
  const context = useContext(SearchResultsConfigurationContext);
33254
33299
  const dispatch = useDispatch();
33255
33300
  const trajectEntry = context?.trajectEntry;
33256
- const selectedCombinationFlight = useSelector(selectSelectedCombinationFlight);
33257
33301
  useEffect(() => {
33258
33302
  if (!context || !trajectEntry)
33259
33303
  return;
@@ -33273,31 +33317,40 @@ const useTrajectAvailability = () => {
33273
33317
  language: context.languageCode ?? 'en-GB',
33274
33318
  rooms: getPackagingRoomsFromEntry(trajectEntry.entry)
33275
33319
  };
33320
+ // Flight searches take passengers rather than rooms; the API buckets by age.
33321
+ const pax = trajectEntry.entry.pax ?? [];
33322
+ const ageOf = (p) => p.age ?? 30;
33323
+ const adults = pax.filter((p) => ageOf(p) >= 12).length;
33324
+ const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12).length;
33325
+ const babies = pax.filter((p) => ageOf(p) < 2).length;
33326
+ const flightPax = concat(Array.from({ length: adults }, (_, index) => ({ id: index, age: 31 })), Array.from({ length: kids }, (_, index) => ({ id: index + adults, age: 8 })), Array.from({ length: babies }, (_, index) => ({ id: index + adults + kids, age: 1 })));
33276
33327
  const nodesWithLines = getTrajectNodesWithLines(trajectEntry);
33277
33328
  const resolveNode = async (item) => {
33278
33329
  const lineGuid = item.line.guid;
33279
33330
  const search = item.node.serviceType === EXCURSION_SERVICE_TYPE ? build.searchPackagingExcursions : build.searchPackagingAccommodations;
33280
33331
  dispatch(setTrajectNodeLoading(lineGuid));
33281
33332
  try {
33282
- const preferredCodes = getPreferredCodes(item.node);
33283
- let results = [];
33284
- let picked = null;
33285
- // Try the traject's own product/hotel codes first, in configured order.
33286
- for (const code of preferredCodes) {
33287
- const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, code), controller.signal);
33288
- const hit = (attempt ?? []).find((result) => matchesCode(result, code));
33289
- if (hit) {
33290
- results = attempt;
33291
- picked = hit;
33292
- break;
33333
+ const candidates = getNodeCandidates(item.node);
33334
+ const attempts = await Promise.all(candidates.map(async (candidate) => {
33335
+ try {
33336
+ const attempt = await search(config, buildTrajectNodeRequest(item, searchContext, candidate.code), controller.signal);
33337
+ return (attempt ?? []).find((result) => matchesCode(result, candidate.code)) ?? null;
33293
33338
  }
33294
- }
33295
- let preferredUnavailable = false;
33339
+ catch (error) {
33340
+ if (!controller.signal.aborted)
33341
+ console.error('Traject hotel unavailable', candidate.code, error);
33342
+ return null;
33343
+ }
33344
+ }));
33345
+ let results = attempts.filter((x) => !!x).filter((x, i, all) => all.findIndex((y) => y.code === x.code) === i);
33346
+ let picked = results[0] ?? null;
33347
+ const preferredCandidate = candidates.find((x) => x.isPreferred) ?? candidates[0];
33348
+ let preferredUnavailable = !!preferredCandidate && !results.some((x) => matchesCode(x, preferredCandidate.code));
33296
33349
  // Nothing configured was available — fall back to the cheapest result in the destination.
33297
33350
  if (!picked) {
33298
33351
  results = (await search(config, buildTrajectNodeRequest(item, searchContext, ''), controller.signal)) ?? [];
33299
33352
  picked = pickCheapest(results);
33300
- preferredUnavailable = preferredCodes.length > 0;
33353
+ preferredUnavailable = candidates.length > 0;
33301
33354
  }
33302
33355
  if (controller.signal.aborted)
33303
33356
  return null;
@@ -33320,59 +33373,36 @@ const useTrajectAvailability = () => {
33320
33373
  return lineGuid;
33321
33374
  }
33322
33375
  };
33323
- const resolveFlights = async () => {
33324
- const { outbound, inbound } = pairTrajectFlights(nodesWithLines);
33325
- if (!outbound) {
33326
- dispatch(setTrajectFlightsResolved(true));
33327
- return;
33376
+ const resolveFlightNode = async (item) => {
33377
+ const lineGuid = item.line.guid;
33378
+ if (!item.node.departureAirportCode || !item.node.arrivalAirportCode) {
33379
+ dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
33380
+ return lineGuid;
33328
33381
  }
33329
- dispatch(setFlightsLoading(true));
33382
+ dispatch(setTrajectFlightLoading(lineGuid));
33330
33383
  try {
33331
- const pax = trajectEntry.entry.pax ?? [];
33332
- const ageOf = (p) => p.age ?? 30;
33333
- const adults = pax.filter((p) => ageOf(p) >= 12).length;
33334
- const kids = pax.filter((p) => ageOf(p) >= 2 && ageOf(p) < 12).length;
33335
- const babies = pax.filter((p) => ageOf(p) < 2).length;
33336
- const request = {
33337
- transactionId: trajectEntry.entry.transactionId,
33338
- officeId: searchContext.officeId,
33339
- catalogueId: searchContext.catalogueId,
33340
- agentId: searchContext.agentId,
33341
- language: searchContext.language,
33342
- departureAirportCode: outbound.node.departureAirportCode ?? '',
33343
- arrivalAirportCode: outbound.node.arrivalAirportCode ?? '',
33344
- returnAirportCode: inbound?.node.arrivalAirportCode ?? outbound.node.departureAirportCode ?? null,
33345
- luggageIncluded: null,
33346
- maxStops: null,
33347
- travelClass: null,
33348
- pax: 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 }))),
33349
- outward: { date: dateToDateStruct(new Date(outbound.line.from)) },
33350
- return: { date: dateToDateStruct(new Date((inbound ?? outbound).line.from)) }
33351
- };
33352
- const flights = (await build.searchPackagingFlights(config, request, controller.signal)) ?? [];
33384
+ const flights = (await build.searchPackagingFlights(config, buildTrajectFlightRequest(item, searchContext, flightPax), controller.signal)) ?? [];
33353
33385
  if (controller.signal.aborted)
33354
- return;
33355
- dispatch(setPackagingFlightResults(flights));
33356
- const firstFlight = first(flights);
33357
- if (firstFlight) {
33358
- dispatch(setSelectedOutwardKey(getFlightKey(firstFlight.outward.segments)));
33359
- dispatch(setSelectedReturnKey(getFlightKey(firstFlight.return.segments)));
33360
- }
33386
+ return null;
33387
+ const picked = pickCheapestFlight(flights);
33388
+ dispatch(setTrajectFlightAvailability({ lineGuid, results: flights, selectedGuid: picked?.outwardGuid ?? null }));
33389
+ if (!picked)
33390
+ return lineGuid;
33391
+ dispatch(updateEditableEntryLine(applyFlightToLine(item.line, picked)));
33392
+ return null;
33361
33393
  }
33362
33394
  catch (error) {
33363
- if (!controller.signal.aborted)
33364
- console.error('Failed to resolve traject flights', error);
33365
- }
33366
- finally {
33367
- if (!controller.signal.aborted) {
33368
- dispatch(setFlightsLoading(false));
33369
- dispatch(setTrajectFlightsResolved(true));
33370
- }
33395
+ if (controller.signal.aborted)
33396
+ return null;
33397
+ console.error('Failed to resolve traject flight', item.node.name, error);
33398
+ dispatch(setTrajectFlightAvailability({ lineGuid, results: [], selectedGuid: null }));
33399
+ return lineGuid;
33371
33400
  }
33372
33401
  };
33373
33402
  (async () => {
33374
33403
  const searchable = nodesWithLines.filter((x) => isSearchableTrajectNode(x.node));
33375
- const [unresolved] = await Promise.all([Promise.all(searchable.map(resolveNode)), resolveFlights()]);
33404
+ const flightNodes = nodesWithLines.filter((x) => isTrajectFlightNode(x.node));
33405
+ const unresolved = await Promise.all([...searchable.map(resolveNode), ...flightNodes.map(resolveFlightNode)]);
33376
33406
  if (controller.signal.aborted)
33377
33407
  return;
33378
33408
  // A node with no availability anywhere cannot be priced or booked, so its line comes off
@@ -33383,16 +33413,6 @@ const useTrajectAvailability = () => {
33383
33413
  })();
33384
33414
  return () => controller.abort();
33385
33415
  }, [trajectEntry]);
33386
- // Keep the traject's own flight lines in step with whatever outward/return the user picks.
33387
- useEffect(() => {
33388
- if (!trajectEntry || !selectedCombinationFlight)
33389
- return;
33390
- const { outbound, inbound } = pairTrajectFlights(getTrajectNodesWithLines(trajectEntry));
33391
- if (outbound)
33392
- dispatch(updateEditableEntryLine(applyFlightToLine(outbound.line, selectedCombinationFlight, true)));
33393
- if (inbound)
33394
- dispatch(updateEditableEntryLine(applyFlightToLine(inbound.line, selectedCombinationFlight, false)));
33395
- }, [trajectEntry, selectedCombinationFlight]);
33396
33416
  };
33397
33417
 
33398
33418
  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;