import React, { useContext, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { first } from 'lodash'; import { SearchResultsRootState } from '../../store/search-results-store'; import { getTranslations } from '../../../shared/utils/localization-util'; import SearchResultsConfigurationContext from '../../search-results-configuration-context'; import { PackagingAccommodationResponse } from '@qite/tide-client'; import { confirmExcursionForDay, setFlyInIsOpen, setSelectedExcursionSearchResult } from '../../store/search-results-slice'; import { format, parseISO } from 'date-fns'; type ExcursionOption = PackagingAccommodationResponse['rooms'][number]['options'][number]; type GroupedExcursion = { accommodationCode: string; accommodationName: string; options: ExcursionOption[]; }; const formatPrice = (price?: number, currencyCode?: string | null) => { if (typeof price !== 'number') return ''; return new Intl.NumberFormat('nl-BE', { style: 'currency', currency: currencyCode ?? 'EUR' }).format(price); }; const getExcursionDayKey = (date: string | Date) => { const parsed = typeof date === 'string' ? parseISO(date) : date; return format(parsed, 'yyyy-MM-dd'); }; const getOptionPaxIds = (option: ExcursionOption): number[] => { return Array.isArray(option.paxIds) ? Array.from(new Set(option.paxIds)).sort((a, b) => a - b) : []; }; const optionAppliesToPax = (option: ExcursionOption, paxId: number) => { return getOptionPaxIds(option).includes(paxId); }; const optionAppliesToAllTravellers = (option: ExcursionOption, travellerCount: number) => { const paxIds = getOptionPaxIds(option); const expected = Array.from({ length: travellerCount }, (_, i) => i); return paxIds.length === expected.length && paxIds.every((id, index) => id === expected[index]); }; const groupOptionsByExcursion = (options: ExcursionOption[]): GroupedExcursion[] => { const groupedMap = new Map(); options.forEach((option) => { const key = option.accommodationCode; if (!groupedMap.has(key)) { groupedMap.set(key, { accommodationCode: option.accommodationCode, accommodationName: option.accommodationName, options: [] }); } groupedMap.get(key)!.options.push(option); }); return Array.from(groupedMap.values()); }; const ExcursionDetails: React.FC = () => { const context = useContext(SearchResultsConfigurationContext); const dispatch = useDispatch(); const { selectedExcursionSearchResult, editablePackagingEntry, excursionSearchParams } = useSelector((state: SearchResultsRootState) => state.searchResults); if (!context || !selectedExcursionSearchResult || !editablePackagingEntry || !excursionSearchParams?.date) { return null; } const translations = getTranslations(context.languageCode ?? 'en-GB'); const travellerCount = editablePackagingEntry.pax.length; const allOptions = useMemo(() => { return selectedExcursionSearchResult.rooms.flatMap((room) => room.options ?? []); }, [selectedExcursionSearchResult]); const sharedOptions = useMemo(() => { return allOptions.filter((option) => optionAppliesToAllTravellers(option, travellerCount)); }, [allOptions, travellerCount]); const sharedExcursions = useMemo(() => { return groupOptionsByExcursion(sharedOptions); }, [sharedOptions]); const paxGroups = useMemo(() => { return editablePackagingEntry.pax.map((pax) => { const paxOptions = allOptions.filter((option) => optionAppliesToPax(option, pax.id) && !optionAppliesToAllTravellers(option, travellerCount)); return { pax, paxId: pax.id, excursions: groupOptionsByExcursion(paxOptions) }; }); }, [editablePackagingEntry.pax, allOptions, travellerCount]); const getSelectedSharedOption = () => { return sharedOptions.find((option) => option.isSelected); }; const getSelectedSharedOptionForExcursion = (accommodationCode: string) => { return sharedOptions.find((option) => option.accommodationCode === accommodationCode && option.isSelected); }; const getSelectedOptionForPax = (paxId: number) => { return allOptions.find((option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.isSelected); }; const getSelectedOptionForExcursion = (paxId: number, accommodationCode: string) => { return allOptions.find( (option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.accommodationCode === accommodationCode && option.isSelected ); }; const handlePick = (selectedGuid?: string, paxId?: number) => { const updatedExcursionSearchResult: PackagingAccommodationResponse = { ...selectedExcursionSearchResult, rooms: selectedExcursionSearchResult.rooms.map((room) => ({ ...room, options: room.options.map((option) => { const isSharedOption = optionAppliesToAllTravellers(option, travellerCount); if (paxId === undefined) { if (!isSharedOption) { return option; } return { ...option, isSelected: option.guid === selectedGuid }; } if (isSharedOption || !optionAppliesToPax(option, paxId)) { return option; } return { ...option, isSelected: option.guid === selectedGuid }; }) })) }; dispatch(setSelectedExcursionSearchResult(updatedExcursionSearchResult)); }; const calculateTotalPrice = () => { const selectedOptions = allOptions.filter((option) => option.isSelected); const totalPrice = selectedOptions.reduce((total, option) => total + (option.price || 0), 0); return formatPrice(totalPrice, selectedExcursionSearchResult.currencyCode); }; const getSharedPriceDifference = (accommodationCode: string) => { const currentSelectedShared = getSelectedSharedOption(); let targetPrice = 0; const selectedOption = getSelectedSharedOptionForExcursion(accommodationCode); if (selectedOption?.price) { targetPrice = selectedOption.price; } else { const firstOption = sharedOptions.find((option) => option.accommodationCode === accommodationCode); targetPrice = firstOption?.price || 0; } return targetPrice - (currentSelectedShared?.price || 0); }; const getPriceDifference = (currentSelectedPrice: number | undefined, paxId: number, accommodationCode: string) => { let targetPrice = 0; const selectedOption = getSelectedOptionForExcursion(paxId, accommodationCode); if (selectedOption?.price) { targetPrice = selectedOption.price; } else { const firstOption = allOptions.find( (option) => optionAppliesToPax(option, paxId) && !optionAppliesToAllTravellers(option, travellerCount) && option.accommodationCode === accommodationCode ); targetPrice = firstOption?.price || 0; } return targetPrice - (currentSelectedPrice || 0); }; const formatPriceDifference = (difference: number, currencyCode: string) => { if (difference === 0) { return null; } const formattedAbsoluteValue = formatPrice(Math.abs(difference), currencyCode); return `${difference > 0 ? '+' : '-'} ${formattedAbsoluteValue}`; }; const getPriceDifferenceClassName = (difference: number) => { if (difference < 0) { return 'flyin__acco__price flyin__acco__price--decrease'; } if (difference > 0) { return 'flyin__acco__price flyin__acco__price--increase'; } return 'flyin__acco__price'; }; const handleConfirm = () => { const dayKey = getExcursionDayKey(excursionSearchParams.date); dispatch( confirmExcursionForDay({ dayKey, excursion: selectedExcursionSearchResult }) ); dispatch(setFlyInIsOpen(false)); }; return ( <>
{sharedExcursions.length > 0 && (

{translations.QSM.ALL_TRAVELERS}

{sharedExcursions.map((excursion) => { const selectedOption = getSelectedSharedOptionForExcursion(excursion.accommodationCode); const priceDifference = getSharedPriceDifference(excursion.accommodationCode); return (

{excursion.accommodationName}

{formatPriceDifference(priceDifference, selectedExcursionSearchResult.currencyCode)}
); })}
)} {paxGroups.map(({ pax, paxId, excursions }) => { if (excursions.length === 0) { return null; } const selectedPaxOption = getSelectedOptionForPax(paxId); return (

{translations.SUMMARY.TRAVELER} {pax.id + 1}

{excursions.map((excursion) => { const selectedOption = getSelectedOptionForExcursion(paxId, excursion.accommodationCode); const priceDifference = getPriceDifference(selectedPaxOption?.price, paxId, excursion.accommodationCode); return (

{excursion.accommodationName}

{formatPriceDifference(priceDifference, selectedExcursionSearchResult.currencyCode)}
); })}
); })}
{translations.SHARED.TOTAL_PRICE}: {calculateTotalPrice()}
); }; export default ExcursionDetails;