@qite/tide-booking-component 1.4.102 → 1.4.104

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.
Files changed (55) hide show
  1. package/build/build-cjs/index.js +1773 -877
  2. package/build/build-cjs/src/search-results/components/filters/filters.d.ts +2 -0
  3. package/build/build-cjs/src/search-results/components/flight/flight-selection/independent-flight-option.d.ts +3 -0
  4. package/build/build-cjs/src/search-results/components/hotel/hotel-accommodation-results.d.ts +1 -0
  5. package/build/build-cjs/src/search-results/store/search-results-selectors.d.ts +424 -0
  6. package/build/build-cjs/src/search-results/store/search-results-slice.d.ts +27 -8
  7. package/build/build-cjs/src/search-results/types.d.ts +14 -2
  8. package/build/build-cjs/src/search-results/utils/search-results-utils.d.ts +8 -6
  9. package/build/build-cjs/src/shared/components/flyin/flyin.d.ts +3 -3
  10. package/build/build-cjs/src/shared/components/flyin/packaging-flights-flyin.d.ts +7 -0
  11. package/build/build-cjs/src/shared/utils/localization-util.d.ts +2 -0
  12. package/build/build-esm/index.js +1747 -861
  13. package/build/build-esm/src/search-results/components/filters/filters.d.ts +2 -0
  14. package/build/build-esm/src/search-results/components/flight/flight-selection/independent-flight-option.d.ts +3 -0
  15. package/build/build-esm/src/search-results/components/hotel/hotel-accommodation-results.d.ts +1 -0
  16. package/build/build-esm/src/search-results/store/search-results-selectors.d.ts +424 -0
  17. package/build/build-esm/src/search-results/store/search-results-slice.d.ts +27 -8
  18. package/build/build-esm/src/search-results/types.d.ts +14 -2
  19. package/build/build-esm/src/search-results/utils/search-results-utils.d.ts +8 -6
  20. package/build/build-esm/src/shared/components/flyin/flyin.d.ts +3 -3
  21. package/build/build-esm/src/shared/components/flyin/packaging-flights-flyin.d.ts +7 -0
  22. package/build/build-esm/src/shared/utils/localization-util.d.ts +2 -0
  23. package/package.json +2 -2
  24. package/src/booking-wizard/features/flight-options/index.tsx +6 -2
  25. package/src/search-results/components/filters/filters.tsx +8 -9
  26. package/src/search-results/components/flight/flight-selection/independent-flight-option.tsx +31 -4
  27. package/src/search-results/components/hotel/hotel-accommodation-results.tsx +81 -25
  28. package/src/search-results/components/icon.tsx +1 -1
  29. package/src/search-results/components/search-results-container/search-results-container.tsx +194 -130
  30. package/src/search-results/store/search-results-selectors.ts +73 -0
  31. package/src/search-results/store/search-results-slice.ts +94 -14
  32. package/src/search-results/types.ts +14 -2
  33. package/src/search-results/utils/search-results-utils.ts +310 -58
  34. package/src/shared/components/flyin/flyin.tsx +102 -19
  35. package/src/shared/components/flyin/packaging-flights-flyin.tsx +164 -0
  36. package/src/shared/translations/ar-SA.json +2 -0
  37. package/src/shared/translations/da-DK.json +2 -0
  38. package/src/shared/translations/de-DE.json +2 -0
  39. package/src/shared/translations/en-GB.json +2 -0
  40. package/src/shared/translations/es-ES.json +2 -0
  41. package/src/shared/translations/fr-BE.json +2 -0
  42. package/src/shared/translations/fr-FR.json +2 -0
  43. package/src/shared/translations/is-IS.json +2 -0
  44. package/src/shared/translations/it-IT.json +2 -0
  45. package/src/shared/translations/ja-JP.json +2 -0
  46. package/src/shared/translations/nl-BE.json +2 -0
  47. package/src/shared/translations/nl-NL.json +2 -0
  48. package/src/shared/translations/no-NO.json +2 -0
  49. package/src/shared/translations/pl-PL.json +2 -0
  50. package/src/shared/translations/pt-PT.json +2 -0
  51. package/src/shared/translations/sv-SE.json +2 -0
  52. package/src/shared/utils/localization-util.ts +5 -2
  53. package/styles/components/_flight-option.scss +14 -1
  54. package/styles/components/_flyin.scss +16 -0
  55. package/styles/components/_search.scss +9 -1
@@ -1,5 +1,5 @@
1
1
  import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2
- import { AccommodationFlyInStep, ExtendedFlightSearchResponseItem, Filter, SortByType } from '../types';
2
+ import { ExtendedFlightSearchResponseItem, Filter, FlyInType, SortByType } from '../types';
3
3
  import {
4
4
  BookingPackage,
5
5
  BookingPackageItem,
@@ -14,53 +14,87 @@ export interface SearchResultsState {
14
14
  results: BookingPackageItem[];
15
15
  filteredResults: BookingPackageItem[];
16
16
  selectedSearchResult: BookingPackageItem | null;
17
+
17
18
  packagingAccoResults: PackagingAccommodationResponse[];
18
19
  filteredPackagingAccoResults: PackagingAccommodationResponse[];
19
20
  packagingAccoSearchDetails: PackagingAccommodationResponse[];
20
21
  selectedPackagingAccoResultCode: string | null;
22
+
21
23
  packagingFlightResults: PackagingFlightResponse[];
24
+ filteredPackagingFlightResults: PackagingFlightResponse[];
22
25
  selectedPackagingFlight: PackagingFlightResponse | null;
26
+ selectedOutwardKey: string | null;
27
+ selectedReturnKey: string | null;
28
+
23
29
  selectedFlight: ExtendedFlightSearchResponseItem | null;
24
30
  selectedFlightDetails: ExtendedFlightSearchResponseItem | null;
31
+
25
32
  bookingPackageDetails: BookingPackage | null;
33
+ priceDetails: BookingPriceDetails | null;
34
+
26
35
  isLoading: boolean;
27
36
  flightsLoading: boolean;
28
- filters: Filter[];
37
+
29
38
  selectedSortType: SortByType | null;
39
+ selectedFlightSortType: SortByType | null;
40
+ filters: Filter[];
41
+ initialFilters: Filter[];
42
+ flightFilters: Filter[];
43
+ initialFlightFilters: Filter[];
44
+
30
45
  activeTab: string | null;
31
46
  currentPage: number;
32
- flyInIsOpen: boolean;
33
- editablePackagingEntry: PackagingEntry | null;
47
+
34
48
  transactionId: string | null;
35
- accommodationFlyInStep: AccommodationFlyInStep;
36
- priceDetails: BookingPriceDetails | null;
49
+
50
+ flyInIsOpen: boolean;
51
+ flyInType: FlyInType | null;
52
+
37
53
  itinerary: ClientPortalItinerary | null;
54
+ editablePackagingEntry: PackagingEntry | null;
38
55
  }
39
56
 
40
57
  const initialState: SearchResultsState = {
41
58
  results: [],
42
59
  filteredResults: [],
43
60
  selectedSearchResult: null,
61
+
44
62
  packagingAccoResults: [],
45
63
  filteredPackagingAccoResults: [],
46
64
  packagingAccoSearchDetails: [],
47
65
  selectedPackagingAccoResultCode: null,
66
+
48
67
  packagingFlightResults: [],
68
+ filteredPackagingFlightResults: [],
49
69
  selectedPackagingFlight: null,
70
+ selectedOutwardKey: null,
71
+ selectedReturnKey: null,
72
+
50
73
  selectedFlight: null,
51
74
  selectedFlightDetails: null,
75
+
52
76
  bookingPackageDetails: null,
77
+ priceDetails: null,
78
+
53
79
  isLoading: false,
54
80
  flightsLoading: false,
55
- filters: [],
81
+
56
82
  selectedSortType: null,
83
+ selectedFlightSortType: null,
84
+ initialFilters: [],
85
+ filters: [],
86
+ initialFlightFilters: [],
87
+ flightFilters: [],
88
+
57
89
  activeTab: 'compact',
58
90
  currentPage: 1,
91
+
92
+ transactionId: null,
93
+
59
94
  flyInIsOpen: false,
95
+ flyInType: null,
96
+
60
97
  editablePackagingEntry: null,
61
- transactionId: null,
62
- accommodationFlyInStep: 'details',
63
- priceDetails: null,
64
98
  itinerary: null
65
99
  };
66
100
 
@@ -92,6 +126,9 @@ const searchResultsSlice = createSlice({
92
126
  setPackagingFlightResults(state, action: PayloadAction<PackagingFlightResponse[]>) {
93
127
  state.packagingFlightResults = action.payload;
94
128
  },
129
+ setFilteredPackagingFlightResults(state, action: PayloadAction<PackagingFlightResponse[]>) {
130
+ state.filteredPackagingFlightResults = action.payload;
131
+ },
95
132
  setSelectedPackagingFlight(state, action: PayloadAction<PackagingFlightResponse | null>) {
96
133
  state.selectedPackagingFlight = action.payload;
97
134
  },
@@ -122,6 +159,9 @@ const searchResultsSlice = createSlice({
122
159
  setFlightsLoading(state, action: PayloadAction<boolean>) {
123
160
  state.flightsLoading = action.payload;
124
161
  },
162
+ setInitialFilters(state, action: PayloadAction<Filter[]>) {
163
+ state.initialFilters = action.payload;
164
+ },
125
165
  setFilters(state, action: PayloadAction<Filter[]>) {
126
166
  const updatedFilters = action.payload;
127
167
 
@@ -137,9 +177,30 @@ const searchResultsSlice = createSlice({
137
177
  resetFilters(state, action: PayloadAction<Filter[]>) {
138
178
  state.filters = action.payload;
139
179
  },
180
+ setInitialFlightFilters(state, action: PayloadAction<Filter[]>) {
181
+ state.initialFlightFilters = action.payload;
182
+ },
183
+ setFlightFilters(state, action: PayloadAction<Filter[]>) {
184
+ const updatedFilters = action.payload;
185
+
186
+ updatedFilters.forEach((updatedFilter) => {
187
+ const existingIndex = state.flightFilters.findIndex((f) => f.property === updatedFilter.property);
188
+ if (existingIndex !== -1) {
189
+ state.flightFilters[existingIndex] = updatedFilter;
190
+ } else {
191
+ state.flightFilters.push(updatedFilter); // Optional: Add new filters if not present
192
+ }
193
+ });
194
+ },
195
+ resetFlightFilters(state, action: PayloadAction<Filter[]>) {
196
+ state.flightFilters = action.payload;
197
+ },
140
198
  setSortType(state, action: PayloadAction<SortByType | null>) {
141
199
  state.selectedSortType = action.payload;
142
200
  },
201
+ setFlightSortType(state, action: PayloadAction<SortByType | null>) {
202
+ state.selectedFlightSortType = action.payload;
203
+ },
143
204
  setActiveTab(state, action: PayloadAction<string | null>) {
144
205
  state.activeTab = action.payload;
145
206
  },
@@ -163,14 +224,24 @@ const searchResultsSlice = createSlice({
163
224
  setTransactionId(state, action: PayloadAction<string | null>) {
164
225
  state.transactionId = action.payload;
165
226
  },
166
- setAccommodationFlyInStep(state, action: PayloadAction<AccommodationFlyInStep>) {
167
- state.accommodationFlyInStep = action.payload;
227
+ setFlyInType(state, action: PayloadAction<FlyInType | null>) {
228
+ state.flyInType = action.payload;
168
229
  },
169
230
  setPriceDetails(state, action: PayloadAction<BookingPriceDetails | null>) {
170
231
  state.priceDetails = action.payload;
171
232
  },
172
233
  setItinerary(state, action: PayloadAction<ClientPortalItinerary | null>) {
173
234
  state.itinerary = action.payload;
235
+ },
236
+ setSelectedOutwardKey: (state, action: PayloadAction<string | null>) => {
237
+ state.selectedOutwardKey = action.payload;
238
+ },
239
+ setSelectedReturnKey: (state, action: PayloadAction<string | null>) => {
240
+ state.selectedReturnKey = action.payload;
241
+ },
242
+ resetFlightSelection: (state) => {
243
+ state.selectedOutwardKey = null;
244
+ state.selectedReturnKey = null;
174
245
  }
175
246
  }
176
247
  });
@@ -181,6 +252,7 @@ export const {
181
252
  setSelectedSearchResult,
182
253
  setPackagingAccoResults,
183
254
  setFilteredPackagingAccoResults,
255
+ setFilteredPackagingFlightResults,
184
256
  setPackagingAccoSearchDetails,
185
257
  setSelectedPackagingAccoResult,
186
258
  setPackagingFlightResults,
@@ -191,18 +263,26 @@ export const {
191
263
  selectFlight,
192
264
  setIsLoading,
193
265
  setFlightsLoading,
266
+ setInitialFilters,
194
267
  setFilters,
195
268
  resetFilters,
269
+ setInitialFlightFilters,
270
+ setFlightFilters,
271
+ resetFlightFilters,
196
272
  setSortType,
273
+ setFlightSortType,
197
274
  setActiveTab,
198
275
  setCurrentPage,
199
276
  resetSearchState,
200
277
  setFlyInIsOpen,
201
278
  setEditablePackagingEntry,
202
279
  setTransactionId,
203
- setAccommodationFlyInStep,
280
+ setFlyInType,
204
281
  setPriceDetails,
205
- setItinerary
282
+ setItinerary,
283
+ setSelectedOutwardKey,
284
+ setSelectedReturnKey,
285
+ resetFlightSelection
206
286
  } = searchResultsSlice.actions;
207
287
 
208
288
  export default searchResultsSlice.reducer;
@@ -64,7 +64,19 @@ export interface SearchResultsConfiguration {
64
64
 
65
65
  export type FilterType = 'checkbox' | 'toggle' | 'slider' | 'star-rating';
66
66
 
67
- export type FilterProperty = 'regime' | 'accommodation' | 'max-duration' | 'price' | 'rating' | 'theme';
67
+ export type FilterProperty =
68
+ | 'regime'
69
+ | 'accommodation'
70
+ | 'max-duration'
71
+ | 'price'
72
+ | 'rating'
73
+ | 'theme'
74
+ | 'airline'
75
+ | 'numberOfStops'
76
+ | 'departureRange'
77
+ | 'departureAirport'
78
+ | 'arrivalAirport'
79
+ | 'travelDuration';
68
80
 
69
81
  export interface FilterOption {
70
82
  label: string;
@@ -205,4 +217,4 @@ export type SearchSeed = {
205
217
  rooms: BookingPackageRequestRoom[];
206
218
  };
207
219
 
208
- export type AccommodationFlyInStep = 'results' | 'details';
220
+ export type FlyInType = 'flight-outward-results' | 'flight-return-results' | 'flight-details' | 'acco-results' | 'acco-details';
@@ -1,6 +1,9 @@
1
- import { BookingPackageItem, PackagingAccommodationResponse } from '@qite/tide-client';
1
+ import { BookingPackageItem, PackagingAccommodationResponse, PackagingFlightResponse } from '@qite/tide-client';
2
2
  import { Filter, SortByType, TideTag } from '../types';
3
- import { first, flatMap } from 'lodash';
3
+ import { filter, flatMap, orderBy } from 'lodash';
4
+ import { getArrivalSegment, getDepartureRangeName, getDepartureSegment, getNumberOfStops } from './flight-utils';
5
+ import { DepartureRange } from '../../shared/types';
6
+ import { durationInTicksInMinutes, rangeFromDateTimeInMinutes } from '../../shared/utils/localization-util';
4
7
 
5
8
  export const enrichFiltersWithResults = (results: BookingPackageItem[], filters: Filter[] | undefined, tags: TideTag[]): Filter[] => {
6
9
  if (!results || results.length === 0 || !filters) {
@@ -79,76 +82,237 @@ export const enrichFiltersWithResults = (results: BookingPackageItem[], filters:
79
82
  });
80
83
  };
81
84
 
82
- export const enrichFiltersWithPackageAccoResults = (results: PackagingAccommodationResponse[], filters: Filter[] | undefined, tags: TideTag[]): Filter[] => {
83
- if (!results || results.length === 0 || !filters) {
84
- return filters ?? [];
85
+ export const enrichFiltersWithPackageAccoResults = (results: PackagingAccommodationResponse[], tags: TideTag[]): Filter[] => {
86
+ const filters: Filter[] = [];
87
+ if (!results || results.length === 0) {
88
+ return filters;
85
89
  }
86
90
 
87
- return filters.map((filter) => {
88
- let updatedFilter = { ...filter };
89
-
90
- if (filter.property === 'price' && (filter.min == null || filter.max == null)) {
91
- const prices = results.map((r) => r.price ?? 0).filter((p) => p > 0);
92
- if (prices.length > 0) {
93
- updatedFilter.min = Math.floor(Math.min(...prices));
94
- updatedFilter.max = Math.ceil(Math.max(...prices));
95
- }
91
+ const regimeFilter: Filter = {
92
+ property: 'regime',
93
+ label: 'Regime',
94
+ type: 'checkbox',
95
+ options: [],
96
+ isFrontendFilter: true
97
+ };
98
+ const map = new Map<string, { name?: string; code: string }>();
99
+
100
+ results.forEach((r) => {
101
+ const rooms = flatMap(r.rooms);
102
+ if (rooms) {
103
+ rooms.map((room) => {
104
+ room.options.map((option) => {
105
+ if (option.regimeCode) {
106
+ map.set(option.regimeCode, {
107
+ name: option.regimeName,
108
+ code: option.regimeCode
109
+ });
110
+ }
111
+ });
112
+ });
96
113
  }
114
+ });
97
115
 
98
- if (filter.property === 'accommodation') {
99
- const map = new Map<string, { name?: string; code: string }>();
116
+ regimeFilter.options = Array.from(map.values()).map((regime) => ({
117
+ label: regime.name ?? regime.code,
118
+ value: regime.code,
119
+ isChecked: false
120
+ }));
121
+ filters.push(regimeFilter);
122
+
123
+ const priceFilter: Filter = {
124
+ property: 'price',
125
+ label: 'Prijs',
126
+ type: 'slider',
127
+ isFrontendFilter: true
128
+ };
129
+ const prices = results.map((r) => r.price ?? 0).filter((p) => p > 0);
130
+ priceFilter.min = Math.floor(Math.min(...prices));
131
+ priceFilter.max = Math.ceil(Math.max(...prices));
132
+
133
+ filters.push(priceFilter);
134
+
135
+ return filters;
136
+ };
100
137
 
101
- results.forEach((r) => {
102
- const rooms = flatMap(r.rooms);
103
- if (rooms) {
104
- rooms.map((room) => {
105
- room.options.map((option) => {
106
- if (option.accommodationCode) {
107
- map.set(option.accommodationCode, {
108
- name: option.accommodationName,
109
- code: option.accommodationCode
110
- });
111
- }
112
- });
113
- });
114
- }
138
+ export const enrichFiltersWithPackageFlightResults = (results: PackagingFlightResponse[], tags: TideTag[], translations: any): Filter[] => {
139
+ const filters: Filter[] = [];
140
+ if (!results || results.length === 0) {
141
+ return filters;
142
+ }
143
+
144
+ // Airlines
145
+ const airlinesFilter: Filter = {
146
+ label: 'Airlines',
147
+ property: 'airline',
148
+ type: 'checkbox',
149
+ isFrontendFilter: true,
150
+ options: []
151
+ };
152
+ const airlinesFilterMap = new Map<string, { name?: string; code: string }>();
153
+
154
+ results.map((r) => {
155
+ const airlineCode = r.airlineCode;
156
+ const airlineName = r.airlineName;
157
+ if (airlineCode && airlineName) {
158
+ airlinesFilterMap.set(airlineCode, {
159
+ name: airlineName,
160
+ code: airlineCode
115
161
  });
162
+ }
163
+ });
116
164
 
117
- updatedFilter.options = Array.from(map.values()).map((accommodation) => ({
118
- label: accommodation.name ?? accommodation.code,
119
- value: accommodation.code,
120
- isChecked: false
121
- }));
165
+ airlinesFilter.options = Array.from(airlinesFilterMap.values()).map((airline) => ({
166
+ label: airline.name ?? airline.code,
167
+ value: airline.code,
168
+ isChecked: false
169
+ }));
170
+
171
+ filters.push(airlinesFilter);
172
+
173
+ // Number of stops
174
+ const stopsFilter: Filter = {
175
+ label: 'Number of Stops',
176
+ property: 'numberOfStops',
177
+ type: 'checkbox',
178
+ isFrontendFilter: true,
179
+ options: []
180
+ };
181
+ const stopsMap = new Map<number, { numberOfStops: number }>();
182
+
183
+ results.map((result) => {
184
+ let numberOfStops = getNumberOfStops(result.outward);
185
+ if (!stopsMap.has(numberOfStops)) {
186
+ stopsMap.set(numberOfStops, { numberOfStops });
122
187
  }
188
+ });
123
189
 
124
- if (filter.property === 'regime') {
125
- const map = new Map<string, { name?: string; code: string }>();
190
+ stopsFilter.options = Array.from(stopsMap.values()).map((stop) => ({
191
+ label: `${stop.numberOfStops == 0 ? 'direct' : stop.numberOfStops + ` Stop${stop.numberOfStops !== 1 ? 's' : ''}`}`,
192
+ value: stop.numberOfStops,
193
+ isChecked: false
194
+ }));
195
+
196
+ filters.push(stopsFilter);
197
+
198
+ // Departure range
199
+ const departureRangeFilter: Filter = {
200
+ label: 'Departure Range',
201
+ property: 'departureRange',
202
+ type: 'checkbox',
203
+ isFrontendFilter: true,
204
+ options: []
205
+ };
206
+ const departureRangeMap = new Map<DepartureRange, DepartureRange>();
207
+ results.map((result) => {
208
+ const departureRange = rangeFromDateTimeInMinutes(getDepartureSegment(result.outward)?.departureDateTime);
209
+ if (!departureRangeMap.has(departureRange)) {
210
+ departureRangeMap.set(departureRange, departureRange);
211
+ }
212
+ });
126
213
 
127
- results.forEach((r) => {
128
- const rooms = flatMap(r.rooms);
129
- if (rooms) {
130
- rooms.map((room) => {
131
- room.options.map((option) => {
132
- if (option.regimeCode) {
133
- map.set(option.regimeCode, {
134
- name: option.regimeName,
135
- code: option.regimeCode
136
- });
137
- }
138
- });
139
- });
140
- }
214
+ departureRangeFilter.options = orderBy(Array.from(departureRangeMap.values()), ['id'], ['asc']).map((range) => ({
215
+ label: getDepartureRangeName(translations, range),
216
+ value: range,
217
+ isChecked: false
218
+ }));
219
+
220
+ filters.push(departureRangeFilter);
221
+
222
+ // Departure Airport
223
+ const departureAirportFilter: Filter = {
224
+ label: 'Departure Airport',
225
+ property: 'departureAirport',
226
+ type: 'checkbox',
227
+ isFrontendFilter: true,
228
+ options: []
229
+ };
230
+ const departureAirportsMap = new Map<string, { name?: string; code: string }>();
231
+
232
+ results.map((result) => {
233
+ const departureSegment = getDepartureSegment(result.outward);
234
+ const departureAirport = departureSegment?.departureAirportCode;
235
+ if (departureAirport && !departureAirportsMap.has(departureAirport)) {
236
+ departureAirportsMap.set(departureAirport, {
237
+ name: departureSegment?.departureAirportName + ' (' + departureAirport + ')',
238
+ code: departureAirport
141
239
  });
142
-
143
- updatedFilter.options = Array.from(map.values()).map((regime) => ({
144
- label: regime.name ?? regime.code,
145
- value: regime.code,
146
- isChecked: false
147
- }));
148
240
  }
241
+ });
149
242
 
150
- return updatedFilter;
243
+ departureAirportFilter.options = Array.from(departureAirportsMap.values()).map((airport) => ({
244
+ label: airport.name ?? airport.code,
245
+ value: airport.code,
246
+ isChecked: false
247
+ }));
248
+
249
+ filters.push(departureAirportFilter);
250
+
251
+ // Arrival Airport
252
+ const arrivalAirportFilter: Filter = {
253
+ label: 'Arrival Airport',
254
+ property: 'arrivalAirport',
255
+ type: 'checkbox',
256
+ isFrontendFilter: true,
257
+ options: []
258
+ };
259
+ const arrivalAirportsMap = new Map<string, { name?: string; code: string }>();
260
+
261
+ results.map((result) => {
262
+ const arrivalSegment = getArrivalSegment(result.outward);
263
+ const arrivalAirport = arrivalSegment?.arrivalAirportCode;
264
+ if (arrivalAirport && !arrivalAirportsMap.has(arrivalAirport)) {
265
+ arrivalAirportsMap.set(arrivalAirport, {
266
+ name: arrivalSegment?.arrivalAirportName + ' (' + arrivalAirport + ')',
267
+ code: arrivalAirport
268
+ });
269
+ }
151
270
  });
271
+
272
+ arrivalAirportFilter.options = Array.from(arrivalAirportsMap.values()).map((airport) => ({
273
+ label: airport.name ?? airport.code,
274
+ value: airport.code,
275
+ isChecked: false
276
+ }));
277
+
278
+ filters.push(arrivalAirportFilter);
279
+
280
+ // Price
281
+ const priceFilter: Filter = {
282
+ label: 'Price',
283
+ property: 'price',
284
+ type: 'slider',
285
+ isFrontendFilter: true
286
+ };
287
+
288
+ const prices = results.map((r) => r.price ?? 0).filter((p) => p > 0);
289
+ if (prices.length > 0) {
290
+ priceFilter.min = Math.floor(Math.min(...prices));
291
+ priceFilter.max = Math.ceil(Math.max(...prices));
292
+ }
293
+
294
+ filters.push(priceFilter);
295
+
296
+ // Travel duration
297
+ const travelDurationFilter: Filter = {
298
+ label: 'Travel Duration',
299
+ property: 'travelDuration',
300
+ type: 'slider',
301
+ isFrontendFilter: true
302
+ };
303
+
304
+ const minTravelTimeDuration = Math.min(...results.map((result) => result.outward.durationInTicks));
305
+ const maxTravelTimeDuration = Math.max(...results.map((result) => result.outward.durationInTicks));
306
+ const minTravelTimeValue = durationInTicksInMinutes(minTravelTimeDuration);
307
+ const maxTravelTimeValue = durationInTicksInMinutes(maxTravelTimeDuration);
308
+ if (minTravelTimeValue != null && maxTravelTimeValue != null) {
309
+ travelDurationFilter.min = minTravelTimeValue;
310
+ travelDurationFilter.max = maxTravelTimeValue;
311
+ }
312
+
313
+ filters.push(travelDurationFilter);
314
+
315
+ return filters;
152
316
  };
153
317
 
154
318
  export const applyFilters = (results: BookingPackageItem[], filters: Filter[], sortBy: SortByType | null) => {
@@ -249,3 +413,91 @@ export const applyFiltersToPackageAccoResults = (results: PackagingAccommodation
249
413
  return 0;
250
414
  });
251
415
  };
416
+
417
+ export const applyFiltersToPackageFlightResults = (results: PackagingFlightResponse[], filters: Filter[], sortBy: SortByType | null) => {
418
+ const filtered = results.filter((result) => {
419
+ return filters.every((filter) => {
420
+ if (!filter.isFrontendFilter) return true;
421
+
422
+ // Airline
423
+ if (filter.property === 'airline') {
424
+ const selected = filter.options?.filter((o) => o.isChecked).map((o) => o.value);
425
+ if (!selected || selected.length === 0) return true;
426
+ return selected.includes(result.airlineCode);
427
+ }
428
+
429
+ // Stops
430
+ if (filter.property === 'numberOfStops') {
431
+ const selected = filter.options?.filter((o) => o.isChecked).map((o) => o.value);
432
+ if (!selected || selected.length === 0) return true;
433
+ return selected.includes(getNumberOfStops(result.outward)) && selected.includes(getNumberOfStops(result.return));
434
+ }
435
+
436
+ // Departure range
437
+ if (filter.property === 'departureRange') {
438
+ const selected = filter.options?.filter((o) => o.isChecked).map((o) => o.value);
439
+ if (!selected || selected.length === 0) return true;
440
+ return selected.includes(rangeFromDateTimeInMinutes(getDepartureSegment(result.outward)?.departureDateTime));
441
+ }
442
+
443
+ // Departure airport
444
+ if (filter.property === 'departureAirport') {
445
+ const selected = filter.options?.filter((o) => o.isChecked).map((o) => o.value);
446
+ if (!selected || selected.length === 0) return true;
447
+
448
+ const departureAirportCode = getDepartureSegment(result.outward)?.departureAirportCode;
449
+ if (!departureAirportCode) return false;
450
+
451
+ return selected.includes(departureAirportCode);
452
+ }
453
+
454
+ // Arrival airport
455
+ if (filter.property === 'arrivalAirport') {
456
+ const selected = filter.options?.filter((o) => o.isChecked).map((o) => o.value);
457
+ if (!selected || selected.length === 0) return true;
458
+
459
+ const arrivalAirportCode = getArrivalSegment(result.outward)?.arrivalAirportCode;
460
+ if (!arrivalAirportCode) return false;
461
+
462
+ return selected.includes(arrivalAirportCode);
463
+ }
464
+
465
+ // PRICE
466
+ if (filter.property === 'price') {
467
+ if (filter.selectedMin != null && result.price < filter.selectedMin) return false;
468
+ if (filter.selectedMax != null && result.price > filter.selectedMax) return false;
469
+ return true;
470
+ }
471
+
472
+ // Travel times
473
+ if (filter.property === 'travelDuration') {
474
+ if (filter.selectedMin != null && durationInTicksInMinutes(result.outward?.durationInTicks) < filter.selectedMin) return false;
475
+ if (filter.selectedMax != null && durationInTicksInMinutes(result.outward?.durationInTicks) > filter.selectedMax) return false;
476
+ return true;
477
+ }
478
+
479
+ return true;
480
+ });
481
+ });
482
+
483
+ // SORTING
484
+ if (!sortBy || sortBy.label === 'default') {
485
+ return filtered;
486
+ }
487
+
488
+ if (sortBy.label === 'departureTime') {
489
+ return orderBy(
490
+ results,
491
+ [(result) => getDepartureSegment(result.outward)?.departureDateTime],
492
+ [sortBy.direction] // or "desc"
493
+ );
494
+ } else if (sortBy.label === 'durationInTicks') {
495
+ return orderBy(
496
+ results,
497
+ [(result) => durationInTicksInMinutes(result.outward?.durationInTicks ?? 0)],
498
+ [sortBy.direction] // or "desc"
499
+ );
500
+ } else {
501
+ return orderBy(results, [sortBy.label], [sortBy.direction]);
502
+ }
503
+ };