import React, { useContext, useEffect, useRef, useState } from 'react'; import { concat, first, groupBy, isEmpty, minBy, orderBy } from 'lodash'; import { ExtendedFlightSearchResponseItem, Filters, SortByType } from '../../../types'; import { getArrivalSegment, getDepartureSegment, getNumberOfStops } from '../../../utils/flight-utils'; import { dateToDateStruct, durationInTicksInMinutes, rangeFromDateTimeInMinutes } from '../../../../shared/utils/localization-util'; import { FlightSearchFinishResponse, FlightSearchRequest, FlightSearchResponse } from '@qite/tide-client'; import { v4 as uuidv4 } from 'uuid'; import { getDateAsDateFromParams, getNumberFromParams, getStringFromParams } from '../../../../shared/utils/query-string-util'; interface SearchState { requestId: number | null; results: ExtendedFlightSearchResponseItem[]; sequenceIds: number[]; lastSequenceId: number | null; loading: boolean; } interface FlightFilterContextProps { children: React.ReactNode; tideConnection: any; } interface FlightSearchContext { // Search States isHubReady: boolean; searchRequestId: number; detailesSearchRequestId: number; valuesManuallyChanged: boolean; flightsLoading: boolean; flightSearchDetailsLoading: boolean; numberOfTravellers: number; // Search Results flightSearchResults: ExtendedFlightSearchResponseItem[]; flightDetailsSearchResults: ExtendedFlightSearchResponseItem[]; filteredResults: ExtendedFlightSearchResponseItem[]; // Filter filters: Filters; sortByTypes: SortByType[]; selectedSortByType: SortByType; airlineResults: ExtendedFlightSearchResponseItem[]; numberOfStopsResults: ExtendedFlightSearchResponseItem[]; departureRangeResults: ExtendedFlightSearchResponseItem[]; departureAirportsResults: ExtendedFlightSearchResponseItem[]; arrivalAirportsResults: ExtendedFlightSearchResponseItem[]; travelTimeResults: ExtendedFlightSearchResponseItem[]; priceResults: ExtendedFlightSearchResponseItem[]; onFlightSearch: (selectedFlight?: ExtendedFlightSearchResponseItem) => void; onCancelSearch: () => void; // Filter Setters setFilters: (filters: Filters) => void; setSelectedSortByType: (val: SortByType) => void; resetFilters: () => void; setValuesManuallyChanged: (val: boolean) => void; onFilter: () => void; } const initialState: FlightSearchContext = { isHubReady: false, searchRequestId: 0, detailesSearchRequestId: 0, valuesManuallyChanged: false, flightsLoading: false, flightSearchDetailsLoading: false, numberOfTravellers: 0, flightSearchResults: [], flightDetailsSearchResults: [], filters: { airlines: [], numberOfStops: [], departureRanges: [], departureAirports: [], arrivalAirports: [], travelTimes: [], prices: [] } as Filters, filteredResults: [], sortByTypes: [ { direction: 'asc', label: 'price' } as SortByType, { direction: 'desc', label: 'price' } as SortByType, { direction: 'asc', label: 'departureTime' } as SortByType, { direction: 'desc', label: 'departureTime' } as SortByType, { direction: 'asc', label: 'durationInTicks' } as SortByType, { direction: 'desc', label: 'durationInTicks' } as SortByType ], selectedSortByType: { direction: 'asc', label: 'price' } as SortByType, airlineResults: [], numberOfStopsResults: [], departureRangeResults: [], departureAirportsResults: [], arrivalAirportsResults: [], travelTimeResults: [], priceResults: [], onFlightSearch: () => {}, onCancelSearch: () => {}, setFilters: () => {}, onFilter: () => {}, setSelectedSortByType: () => {}, setValuesManuallyChanged: () => {}, resetFilters: () => {} }; const FlightSearchContext = React.createContext(initialState); const mapItems = (response: FlightSearchResponse) => response.items.map( (item) => ({ ...item, guid: uuidv4(), requestId: response.requestId } as ExtendedFlightSearchResponseItem) ); const checkIfFinished = (state: SearchState) => { console.log('Checking if finished with state:', state); if (state.lastSequenceId === null) return false; for (let i = 0; i <= state.lastSequenceId; i++) { if (!state.sequenceIds.includes(i)) return false; } return true; }; export const FlightSearchProvider: React.FC = ({ children, tideConnection }) => { const [regularSearch, setRegularSearch] = useState({ requestId: null, results: [], sequenceIds: [], lastSequenceId: null, loading: true }); const [detailSearch, setDetailSearch] = useState({ requestId: null, results: [], sequenceIds: [], lastSequenceId: null, loading: true }); const hubProxyRef = useRef(null); const [isHubReady, setIsHubReady] = useState(false); // FILTER States const [filters, setFilters] = useState(initialState.filters); const [groupedResults, setGroupedResults] = useState([]); const [filteredResults, setFilteredResults] = useState(initialState.filteredResults); const [valuesManuallyChanged, setValuesManuallyChanged] = useState(initialState.valuesManuallyChanged); const [airlineResults, setAirlineResults] = useState([]); const [numberOfStopsResults, setNumberOfStopsResults] = useState([]); const [departureRangeResults, setDepartureRangeResults] = useState([]); const [departureAirportsResults, setDepartureAirportsResults] = useState([]); const [arrivalAirportsResults, setArrivalAirportsResults] = useState([]); const [travelTimeResults, setTravelTimeResults] = useState([]); const [priceResults, setPriceResults] = useState([]); const [sortByTypes] = useState(initialState.sortByTypes); const [selectedSortByType, setSelectedSortByType] = useState(initialState.selectedSortByType); const [numberOfTravellers, setNumberOfTravellers] = useState(0); // Connect to SignalR of Tide and set up listeners for results useEffect(() => { if (typeof window === 'undefined') return; console.log('Setting up SignalR connection for flight search results'); // @ts-ignore: SignalR augments jQuery import('signalr-no-jquery').then(({ hubConnection }) => { const connection = hubConnection(tideConnection.host, { useDefaultPath: true }); const hubProxy = connection.createHubProxy('Search'); hubProxy.on('FlightResponse', (response: FlightSearchResponse) => { console.log('FlightResponse received:', response); setRegularSearch((prev) => { if (response.requestId !== prev.requestId) return prev; const updated = { ...prev, results: [...prev.results, ...mapItems(response)], sequenceIds: prev.sequenceIds.includes(response.sequenceId) ? prev.sequenceIds : [...prev.sequenceIds, response.sequenceId] }; return { ...updated, loading: !checkIfFinished(updated) }; }); setDetailSearch((prev) => { if (response.requestId !== prev.requestId) return prev; const updated = { ...prev, results: [...prev.results, ...mapItems(response)], sequenceIds: prev.sequenceIds.includes(response.sequenceId) ? prev.sequenceIds : [...prev.sequenceIds, response.sequenceId] }; return { ...updated, loading: !checkIfFinished(updated) }; }); }); hubProxy.on('FinishResponse', (response: FlightSearchFinishResponse) => { console.log(`FinishResponse received for id ${response.requestId}, response:`, response); setRegularSearch((prev) => { if (response.requestId !== prev.requestId) return prev; const updated = { ...prev, lastSequenceId: response.sequenceId }; return { ...updated, loading: !checkIfFinished(updated) }; }); setDetailSearch((prev) => { if (response.requestId !== prev.requestId) return prev; const updated = { ...prev, lastSequenceId: response.sequenceId }; return { ...updated, loading: !checkIfFinished(updated) }; }); }); connection.start().then(() => { hubProxyRef.current = hubProxy; setTimeout(() => { setIsHubReady(true); }, 1000); // Delay to ensure hub is ready }); return () => { connection.stop(); }; }); }, []); const onFlightSearch = async (selectedFlight?: ExtendedFlightSearchResponseItem) => { const hubProxy = hubProxyRef.current; if (!hubProxy) { console.error('Hub proxy not ready yet'); return; } const params = new URLSearchParams(location.search); let fromDate = getDateAsDateFromParams(params, 'fromDate'); let toDate = getDateAsDateFromParams(params, 'toDate'); let departureAirport = getStringFromParams(params, 'departureAirport'); let destinationAirport = getStringFromParams(params, 'destinationAirport'); let returnAirport = getStringFromParams(params, 'returnAirport'); let onlyDirect = getStringFromParams(params, 'onlyDirect') === 'true' ? true : null; let luggageIncluded = getStringFromParams(params, 'luggageIncluded') === 'true' ? true : null; let travelClass = getStringFromParams(params, 'travelClass'); let adults = getNumberFromParams(params, 'adults'); let kids = getNumberFromParams(params, 'kids'); let babies = getNumberFromParams(params, 'babies'); let request = { officeId: tideConnection?.officeId ?? 0, catalogueId: first(tideConnection?.catalogueIds) ?? 0, departureAirportCode: departureAirport, arrivalAirportCode: destinationAirport, returnAirportCode: returnAirport, luggageIncluded: luggageIncluded, maxStops: onlyDirect ? 0 : null, travelClass: travelClass, pax: concat( Array.from({ length: adults ?? 0 }, (_, index) => ({ id: index, age: 31 })), Array.from({ length: kids ?? 0 }, (_, index) => ({ id: index + (adults ?? 0), age: 8 })), Array.from({ length: babies ?? 0 }, (_, index) => ({ id: index + (adults ?? 0) + (kids ?? 0), age: 1 })) ) } as FlightSearchRequest; if (selectedFlight) { request.outward = { date: fromDate ? dateToDateStruct(fromDate) : dateToDateStruct(getDepartureSegment(selectedFlight.outward)?.departureDateTime), flightCode: selectedFlight.outward.code, flightNumbers: selectedFlight.outward.segments.map((s) => s.flightNumber), airlines: [selectedFlight.airlineCode] }; request.return = { date: toDate ? dateToDateStruct(toDate) : dateToDateStruct(getDepartureSegment(selectedFlight.return)?.departureDateTime), flightCode: selectedFlight.return.code, flightNumbers: selectedFlight.return.segments.map((s) => s.flightNumber), airlines: [selectedFlight.airlineCode] }; request.flightProvider = selectedFlight.provider; request.vendorConfigurationId = selectedFlight.vendorConfigurationId; } else { if (fromDate) { request.outward = { date: dateToDateStruct(fromDate) }; } if (toDate) { request.return = { date: dateToDateStruct(toDate) }; } } setNumberOfTravellers(request.pax.length); try { if (selectedFlight) { if (detailSearch.requestId) { hubProxy.invoke('CancelSearch', detailSearch.requestId); } const requestId = await hubProxy.invoke('SearchFlights', request); console.log(`Invoking detailed search with for id ${requestId} with request:`, request); setDetailSearch({ requestId, results: [], sequenceIds: [], lastSequenceId: null, loading: true }); } else { if (regularSearch.requestId) { hubProxy.invoke('CancelSearch', regularSearch.requestId); } const requestId = await hubProxy.invoke('SearchFlights', request); console.log(`Invoking regular search with for id ${requestId} with request:`, request); setRegularSearch({ requestId, results: [], sequenceIds: [], lastSequenceId: null, loading: true }); } } catch (err) { console.error('Error invoking search:', err); } }; const onCancelSearch = async () => { const hubProxy = hubProxyRef.current; if (detailSearch.requestId) { hubProxy.invoke('CancelSearch', detailSearch.requestId); } }; // useEffect(() => { // console.log('detailSearch search results updated:', detailSearch.results); // if (!isEmpty(detailSearch.results)) { // const grouped = groupBy(detailSearch.results, (x) => x.flightRouteId); // const lowestPerRoute = Object.values(grouped).map((group) => minBy(group, (item) => item.price)); // console.log('Lowest per route:', lowestPerRoute.filter(Boolean) as ExtendedFlightSearchResponseItem[]); // setDetailSearch(prev => { // const updated = { // ...prev, // grouped: lowestPerRoute.filter(Boolean) as ExtendedFlightSearchResponseItem[] // }; // return { // ...updated // }; // }); // } else { // } // }, [detailSearch.results]); useEffect(() => { if (!isEmpty(regularSearch.results)) { onFilter(); } else { setFilteredResults([]); } }, [regularSearch.results]); // Sort the grouped results useEffect(() => { if (groupedResults) { setFilteredResults(sortResults(groupedResults)); } }, [selectedSortByType, groupedResults]); const sortResults = (results: ExtendedFlightSearchResponseItem[]) => { switch (selectedSortByType.label) { case 'departureTime': return orderBy( results, [(result) => getDepartureSegment(result.outward)?.departureDateTime], [selectedSortByType.direction] // or "desc" ); case 'durationInTicks': return orderBy( results, [(result) => durationInTicksInMinutes(result.outward?.durationInTicks ?? 0)], [selectedSortByType.direction] // or "desc" ); default: return orderBy(results, [selectedSortByType.label], [selectedSortByType.direction]); } }; useEffect(() => { if (filters && !isEmpty(filters)) { onFilter(); } }, [filters]); const onFilter = () => { if (!isEmpty(regularSearch.results)) { const filterByAirline = (result: ExtendedFlightSearchResponseItem) => { if (isEmpty(filters.airlines)) { return true; } return filters.airlines.some((airline) => airline.id === result.airlineCode); }; const filterByNumberOfStops = (result: ExtendedFlightSearchResponseItem) => { if (isEmpty(filters.numberOfStops)) { return true; } const outwardStops = getNumberOfStops(result.outward); const returnStops = getNumberOfStops(result.return); return filters.numberOfStops.some((stop) => stop.id === outwardStops && stop.id === returnStops); }; const filterByDepartureRange = (result: ExtendedFlightSearchResponseItem) => { if (isEmpty(filters.departureRanges)) { return true; } const departureRange = rangeFromDateTimeInMinutes(getDepartureSegment(result.outward)?.departureDateTime); return filters.departureRanges.some((range) => range.id === departureRange); }; const filterByDepartureAirport = (result: ExtendedFlightSearchResponseItem) => { if (isEmpty(filters.departureAirports)) { return true; } return filters.departureAirports.some((airport) => airport.id === getDepartureSegment(result.outward)?.departureAirportCode); }; const filterByArrivalAirport = (result: ExtendedFlightSearchResponseItem) => { if (isEmpty(filters.arrivalAirports)) { return true; } return filters.arrivalAirports.some((airport) => airport.id === getArrivalSegment(result.outward)?.arrivalAirportCode); }; const filterByTravelTime = (result: ExtendedFlightSearchResponseItem) => { if (isEmpty(filters.travelTimes) || filters.travelTimes.length !== 2 || result.outward?.durationInTicks === undefined) { return true; } return ( durationInTicksInMinutes(result.outward?.durationInTicks) >= filters.travelTimes[0].lowestPrice && durationInTicksInMinutes(result.outward?.durationInTicks) <= filters.travelTimes[1].lowestPrice ); }; const filterByPrice = (result: ExtendedFlightSearchResponseItem) => { if (isEmpty(filters.prices) || filters.prices.length !== 2 || result.price === undefined) { return true; } return result.price >= filters.prices[0].lowestPrice && result.price <= filters.prices[1].lowestPrice; }; const filtered = regularSearch.results.filter( (result) => filterByAirline(result) && filterByNumberOfStops(result) && filterByDepartureRange(result) && filterByDepartureAirport(result) && filterByArrivalAirport(result) && filterByTravelTime(result) && filterByPrice(result) ); const grouped = groupBy(filtered, (x) => x.flightRouteId); const lowestPerRoute = Object.values(grouped).map((group) => minBy(group, (item) => item.price)); setGroupedResults(lowestPerRoute.filter(Boolean) as ExtendedFlightSearchResponseItem[]); // Set filter option setAirlineResults( regularSearch.results.filter( (result) => filterByNumberOfStops(result) && filterByDepartureRange(result) && filterByDepartureAirport(result) && filterByArrivalAirport(result) && filterByTravelTime(result) && filterByPrice(result) ) ); setNumberOfStopsResults( regularSearch.results.filter( (result) => filterByAirline(result) && filterByDepartureRange(result) && filterByDepartureAirport(result) && filterByArrivalAirport(result) && filterByTravelTime(result) && filterByPrice(result) ) ); setDepartureRangeResults( regularSearch.results.filter( (result) => filterByAirline(result) && filterByNumberOfStops(result) && filterByDepartureAirport(result) && filterByArrivalAirport(result) && filterByTravelTime(result) && filterByPrice(result) ) ); setDepartureAirportsResults( regularSearch.results.filter( (result) => filterByAirline(result) && filterByNumberOfStops(result) && filterByDepartureRange(result) && filterByArrivalAirport(result) && filterByTravelTime(result) && filterByPrice(result) ) ); setArrivalAirportsResults( regularSearch.results.filter( (result) => filterByAirline(result) && filterByNumberOfStops(result) && filterByDepartureRange(result) && filterByDepartureAirport(result) && filterByTravelTime(result) && filterByPrice(result) ) ); setTravelTimeResults( regularSearch.results.filter( (result) => filterByAirline(result) && filterByNumberOfStops(result) && filterByDepartureRange(result) && filterByDepartureAirport(result) && filterByArrivalAirport(result) ) ); setPriceResults( regularSearch.results.filter( (result) => filterByAirline(result) && filterByNumberOfStops(result) && filterByDepartureRange(result) && filterByDepartureAirport(result) && filterByArrivalAirport(result) && filterByTravelTime(result) ) ); } }; const resetFilters = () => { setFilters({ airlines: [], numberOfStops: [], departureRanges: [], departureAirports: [], arrivalAirports: [], travelTimes: [], prices: [] }); }; return ( {children} ); }; export const useFlightSearch = (): FlightSearchContext => { const context = useContext(FlightSearchContext); if (!context) { throw new Error('useFlightSearch must be used within a FlightSearchProvider'); } return context; };