import React, { useContext, useEffect, useMemo, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { QSMRootState } from '../../store/qsm-store'; import QSMConfigurationContext from '../../qsm-configuration-context'; import useMediaQuery from '../../../shared/utils/use-media-query-util'; import MobileFilterModal from '../mobile-filter-modal'; import SearchInputGroup from '../search-input-group'; import DoubleSearchInputGroup from '../double-search-input-group'; import Dates from '../../../booking-product/components/dates'; import TravelInputGroup from '../travel-input-group'; import TravelClassPicker from '../travel-class-picker'; import TravelTypePicker from '../travel-type-picker'; import Icon from '../../../shared/components/icon'; import TravelNationalityPicker from '../travel-nationality-picker'; import { addDays, addMonths, addYears, format } from 'date-fns'; import { DateRange } from '../../../booking-product/types'; import { hydrateQsmState, QSMState, setFromDate, setSelectedQsmType, setToDate, setTripType } from '../../store/qsm-slice'; import { BaseFieldConfig, DoubleFieldConfig } from '../../types'; import { getTranslations } from '../../../shared/utils/localization-util'; import { DateAmountType, PortalQsmType } from '@qite/tide-client'; import { first } from 'lodash'; const QSMContainer: React.FC = () => { const dispatch = useDispatch(); const isMobile = useMediaQuery('(max-width: 768px)'); const qsmState = useSelector((state: QSMRootState) => state.qsm); const { qsmType, mobileFilterType, fromDate, toDate, tripType } = qsmState; const { askTravelers, askRooms, askNationality, askTravelClass, askTravelType, submitIcon, onSubmit, travelTypes, languageCode, departureAirport, destinationAirport, returnAirport, destination, allowOneWay, allowRoundtrip, allowOpenJaw, searchConfigurations } = useContext(QSMConfigurationContext); const translations = getTranslations(languageCode ?? 'en-GB'); useEffect(() => { if (!hasHydratedFromSession.current) return; const stored = sessionStorage.getItem(QSM_SESSION_STORAGE_KEY); const storedQsmType = stored ? JSON.parse(stored).qsmType : undefined; if (storedQsmType) return; const defaultQsmType = first(searchConfigurations)?.qsmType; if (!defaultQsmType) return; handleQsmTypeChange(defaultQsmType); }, [searchConfigurations]); useEffect(() => { if (fromDate || toDate) return; let startDate = addMonths(new Date(), 1); let endDate = addDays(startDate, 7); if (qsmType === PortalQsmType.GroupTour) { startDate = new Date(); endDate = addYears(startDate, 1); } dispatch(setFromDate(startDate.toISOString())); dispatch(setToDate(endDate.toISOString())); }, [fromDate, toDate, dispatch]); const dateRange = useMemo(() => { if (!fromDate || !toDate) return undefined; return { fromDate: new Date(fromDate), toDate: new Date(toDate) }; }, [fromDate, toDate]); const QSM_SESSION_STORAGE_KEY = 'qsm-state'; const hasHydratedFromSession = useRef(false); useEffect(() => { try { const stored = sessionStorage.getItem(QSM_SESSION_STORAGE_KEY); if (stored) { dispatch(hydrateQsmState(JSON.parse(stored))); } } catch { sessionStorage.removeItem(QSM_SESSION_STORAGE_KEY); } finally { hasHydratedFromSession.current = true; } }, [dispatch]); const handleDateChange = (value: DateRange) => { dispatch(setFromDate(value.fromDate ? format(value.fromDate, 'yyyy-MM-dd') : undefined)); dispatch(setToDate(value.toDate ? format(value.toDate, 'yyyy-MM-dd') : undefined)); }; const handleTripTypeChange = (value: 'oneway' | 'roundtrip' | 'openjaw') => { dispatch(setTripType(value)); }; const handleQsmTypeChange = (value: PortalQsmType) => { dispatch(setSelectedQsmType(value)); const now = new Date(); // Default fallback let startDate = addMonths(now, 1); let endDate = addDays(startDate, 7); if (value === PortalQsmType.GroupTour) { startDate = now; endDate = addYears(startDate, 1); } const searchConfig = searchConfigurations.find((config) => config.qsmType === value); if (searchConfig) { const applyAmount = (baseDate: Date, type: DateAmountType, amount?: number) => { if (!amount || type == null) return baseDate; switch (type) { case DateAmountType.days: return addDays(baseDate, amount); case DateAmountType.months: return addMonths(baseDate, amount); default: return baseDate; } }; if (searchConfig.fromDateAmount) { startDate = applyAmount(now, searchConfig.fromDateAmountType, searchConfig.fromDateAmount); } if (searchConfig.toDateAmount) { endDate = applyAmount(startDate, searchConfig.toDateAmountType, searchConfig.toDateAmount); } } handleDateChange({ fromDate: startDate, toDate: endDate }); }; const handleSubmit = () => { if (!onSubmit) return; const { qsmType, fromDate, toDate, selectedTravelClass, selectedTravelType, selectedNationality, rooms, tripType, adults, kids, babies } = qsmState; const selectedTravelTypeValue = travelTypes?.find((t) => t.label === selectedTravelType); const payload = { qsmType, fromDate, toDate, travelClass: selectedTravelClass, travelType: selectedTravelTypeValue, nationality: selectedNationality, tripType } as any; if (askRooms && qsmType !== PortalQsmType.Flight) { payload.rooms = rooms; } else { payload.travelers = { adults, kids, babies }; } // Filter out undefined fields before passing to addSearchFieldsToPayload const searchFields = [departureAirport, destinationAirport, returnAirport, destination].filter((field): field is BaseFieldConfig => field !== undefined); addSearchFieldsToPayload(payload, searchFields, qsmState); if (destination) { const option = destination.options.find((opt) => opt.value === qsmState[destination.fieldKey]); if (option) { payload.destinationType = option?.type; } } onSubmit(payload); console.log('Submitted QSM data:', payload); }; const addSearchFieldsToPayload = (payload: any, fields: BaseFieldConfig[], state: QSMState) => { fields.forEach((field) => { const fieldKey = field.fieldKey; const option = field.options.find((opt) => opt.value === state[fieldKey]); payload[fieldKey] = option?.key ?? state[fieldKey]; }); }; const originDestinationField = useMemo(() => { if (!fromDate || !toDate || !departureAirport || !destinationAirport) return undefined; const departureField: BaseFieldConfig = { ...departureAirport, label: translations.QSM.DEPARTURE, placeholder: translations.QSM.CHOOSE_DEPARTURE }; const destinationField: BaseFieldConfig = { ...destinationAirport, label: translations.QSM.DESTINATION, placeholder: translations.QSM.CHOOSE_DESTINATION }; return { type: 'double', fieldKey: 'locationGroup', showReverse: true, fields: [departureField, destinationField] }; }, [fromDate, toDate, departureAirport, destinationAirport, translations]); const openJawReturnDestinationField = useMemo(() => { if (!fromDate || !toDate || !allowOpenJaw || !departureAirport || !returnAirport) return undefined; const mirroredDepartureField: BaseFieldConfig = { ...departureAirport, label: translations.QSM.DESTINATION, placeholder: translations.QSM.CHOOSE_DESTINATION }; return { type: 'double', fieldKey: 'openjawLocationGroup', showReverse: false, disableReturnField: true, fields: [returnAirport, mirroredDepartureField] }; }, [fromDate, toDate, departureAirport, returnAirport, allowOpenJaw]); const qsmTypeMeta: Record = { [PortalQsmType.Multidestination]: { icon: 'ui-location', label: translations.QSM.MULTIDESTINATION }, [PortalQsmType.Package]: { icon: 'ui-suitcase', label: translations.QSM.PACKAGES }, [PortalQsmType.AccommodationAndFlight]: { icon: ['ui-backforward', 'ui-bed'], label: translations.QSM.TRANSPORT_HOTEL }, [PortalQsmType.Accommodation]: { icon: 'ui-bed', label: translations.QSM.ACCOMMODATION }, [PortalQsmType.Flight]: { icon: 'ui-flight', label: translations.QSM.TRANSPORTS }, [PortalQsmType.GroupTour]: { icon: 'ui-group', label: translations.QSM.GROUP_TOUR }, [PortalQsmType.RoundTrip]: { icon: 'ui-group', label: translations.QSM.ROUNDTRIP }, [PortalQsmType.Ticket]: { icon: 'ui-ticket', label: translations.QSM.TICKET_ONLY }, [PortalQsmType.Car]: { icon: 'ui-car', label: translations.QSM.RENT_A_CAR }, [PortalQsmType.Transfer]: { icon: 'ui-backforward', label: translations.QSM.TRANSFERS }, [PortalQsmType.Cruise]: { icon: 'ui-ship', label: translations.QSM.CRUISES } }; useEffect(() => { if (!hasHydratedFromSession.current) return; const searchFields = [departureAirport, destinationAirport, returnAirport, destination].filter((field): field is BaseFieldConfig => field !== undefined); const dynamicFields = searchFields.reduce>((result, field) => { result[field.fieldKey] = qsmState[field.fieldKey]; return result; }, {}); const stateToPersist: Partial = { qsmType, fromDate, toDate, tripType, selectedTravelClass: qsmState.selectedTravelClass, selectedTravelType: qsmState.selectedTravelType, selectedNationality: qsmState.selectedNationality, adults: qsmState.adults, kids: qsmState.kids, babies: qsmState.babies, rooms: qsmState.rooms, travelers: qsmState.travelers, ...dynamicFields }; sessionStorage.setItem(QSM_SESSION_STORAGE_KEY, JSON.stringify(stateToPersist)); }, [qsmState, qsmType, fromDate, toDate, tripType, departureAirport, destinationAirport, returnAirport, destination]); return (
{searchConfigurations.map((searchConfig, index) => { const meta = qsmTypeMeta[searchConfig.qsmType]; if (!meta) return null; // safety guard return ( ); })} {/* */}
{!isMobile && (
{(qsmType === PortalQsmType.Accommodation || qsmType === PortalQsmType.AccommodationAndFlight || qsmType === PortalQsmType.GroupTour) && (
)} {qsmType === PortalQsmType.Flight && (
{allowOneWay && (
)} {allowRoundtrip && (
)} {allowOpenJaw && (
)}
)}
{qsmType !== PortalQsmType.Accommodation && qsmType !== PortalQsmType.Car && qsmType !== PortalQsmType.Ticket && qsmType !== PortalQsmType.Cruise && qsmType !== PortalQsmType.Transfer && qsmType !== PortalQsmType.GroupTour && askTravelClass && } {qsmType !== PortalQsmType.Multidestination && qsmType !== PortalQsmType.Car && qsmType !== PortalQsmType.Flight && qsmType !== PortalQsmType.Transfer && askTravelType && } {askNationality && }
)}
{/* TODO, determine which fields to show for what type of QSM */} {(qsmType == PortalQsmType.Flight || qsmType == PortalQsmType.AccommodationAndFlight) && originDestinationField && ( )} {qsmType == PortalQsmType.Flight && tripType == 'openjaw' && openJawReturnDestinationField && ( )} {(qsmType == PortalQsmType.Accommodation || qsmType == PortalQsmType.AccommodationAndFlight || qsmType == PortalQsmType.GroupTour) && destination && ( )} {askTravelers && } {isMobile && (
{(qsmType === PortalQsmType.Accommodation || qsmType === PortalQsmType.AccommodationAndFlight || qsmType === PortalQsmType.GroupTour) && (
)} {qsmType === PortalQsmType.Flight && (
{allowOneWay && (
)} {allowRoundtrip && (
)} {allowOpenJaw && (
)}
)}
{qsmType !== PortalQsmType.Accommodation && qsmType !== PortalQsmType.Car && qsmType !== PortalQsmType.Ticket && qsmType !== PortalQsmType.Cruise && qsmType !== PortalQsmType.Transfer && qsmType !== PortalQsmType.GroupTour && askTravelClass && } {qsmType !== PortalQsmType.Multidestination && qsmType !== PortalQsmType.Car && qsmType !== PortalQsmType.Flight && qsmType !== PortalQsmType.Transfer && askTravelType && } {askNationality && }
)}
{isMobile && mobileFilterType && }
); }; export default QSMContainer;