import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { BookingAttributes, BookingOptions, GroupedFlights, ProductAttributes } from '../../types'; import { AirlineBookingPackageOption, AirportBookingPackageOption, BookingAirlineGroup, BookingAirportGroup, BookingOptionGroup, BookingOptionPax, BookingOptionUnit, BookingPackage, BookingPackageDetailsRequest, BookingPackageFlight, BookingPackagePax, BookingPackageRequest, BookingPackageRequestRoom, BookingPackageRoom, BookingTravelAgent, CountryItem, GenerateBookingAccommodationRequest, PerBookingPackageOption, SelectedFlight } from '@qite/tide-client/build/types'; import { first, isEmpty, isNil, range } from 'lodash'; import { RootState } from '../../store'; import { selectAgentId } from '../travelers-form/travelers-form-slice'; import packageApi from './api'; import { BookingStep, OPTIONS_FORM_STEP } from './constants'; import { selectAccommodationCodes, selectAgentAdressId, selectBookingAttributes, selectBookingRooms, selectLanguageCode, selectOfficeId, selectProductAttributes, selectProductCode } from './selectors'; export interface BookingState { officeId: number; languageCode: string; productAttributes?: ProductAttributes; bookingAttributes?: BookingAttributes; calculateDeposit: boolean; showCommission?: boolean; bookingNumber?: string; isRetry: boolean; package?: BookingPackage; agents?: BookingTravelAgent[]; isBusy: boolean; skipPaymentWithAgent: boolean; generatePaymentUrl: boolean; isUnavailable?: boolean; tagIds?: number[]; agentAdressId?: number; remarks?: string; voucherCodes?: string[]; bookingOptions: BookingOptions; bookingType: string; currentStep: BookingStep; translations?: { language: string; value: any; }[]; accommodationViewId?: number; accommodationViews?: { [key: string]: string }; isOption?: boolean; travelersFirstStep: boolean; isFetching?: boolean; hasMounted: boolean; countries?: CountryItem[]; } const initialState: BookingState = { officeId: 1, languageCode: 'nl-BE', bookingOptions: { b2b: { tagIds: [], entryStatus: 2, customEntryStatusId: undefined }, b2b2c: { tagIds: [], entryStatus: 2, customEntryStatusId: undefined }, b2c: { tagIds: [], entryStatus: 0, customEntryStatusId: undefined } }, bookingType: 'b2c', productAttributes: undefined, bookingAttributes: undefined, calculateDeposit: false, showCommission: false, bookingNumber: undefined, isRetry: false, package: undefined, isBusy: false, skipPaymentWithAgent: false, generatePaymentUrl: false, tagIds: [], agentAdressId: undefined, currentStep: OPTIONS_FORM_STEP, translations: undefined, travelersFirstStep: false, isFetching: false, hasMounted: false, countries: undefined }; export const fetchPackage = createAsyncThunk('booking/fetchPackage', async (_, { dispatch }) => { dispatch(setFetchingPackage(true)); await dispatch(fetchAgents()); await dispatch(fetchCountries()); await dispatch(fetchPackageDetails()); await dispatch(fetchAccommodationViews()); dispatch(setFetchingPackage(false)); }); export const fetchCountries = createAsyncThunk('booking/countries', async (_, { dispatch, getState, signal }) => { const settings = getState() as RootState; return await packageApi.fetchCountries(signal, settings.apiSettings); }); const fetchAgents = createAsyncThunk('booking/agents', async (_, { dispatch, getState, signal }) => { const settings = getState() as RootState; return await packageApi.fetchAgents(signal, settings.apiSettings); }); const fetchPackageDetails = createAsyncThunk('booking/details', async (_, { dispatch, getState, signal }) => { const state = getState() as RootState; const officeId = selectOfficeId(state); const productAttributes = selectProductAttributes(state); const bookingAttributes = selectBookingAttributes(state); const agentId = selectAgentId(state); const agentAdressId = selectAgentAdressId(state); const rooms = selectBookingRooms(state); const languageCode = selectLanguageCode(state); if (isNil(productAttributes)) { throw Error('productAttributes could not be found'); } if (isNil(bookingAttributes)) { throw Error('bookingAttributes could not be found'); } if (!rooms?.length) { throw Error('rooms could not be found'); } var requestRooms = rooms?.map((x, i) => { var room = { index: i, pax: [] } as BookingPackageRequestRoom; range(0, x.adults).forEach(() => { room.pax.push({ age: 30 } as BookingPackagePax); }); x.childAges.forEach((x) => { room.pax.push({ age: x } as BookingPackagePax); }); return room; }); const isAllotment = bookingAttributes.tourCode || bookingAttributes.allotmentName || (bookingAttributes.allotmentIds && bookingAttributes.allotmentIds.length); let searchType = isAllotment ? 1 // ALLOTMENT : 0; // DEFAULT; let outwardFlight: SelectedFlight | undefined; let returnFlight: SelectedFlight | undefined; if (bookingAttributes.flightRouteId && bookingAttributes.flight) { searchType = 3; // FLIGHT; outwardFlight = { flightCode: bookingAttributes.flight.outwardCode, startDateTime: bookingAttributes.flight.outwardDepartureDate, endDateTime: bookingAttributes.flight.outwardArrivalDate, airlines: bookingAttributes.flight.outwardAirlines, flightNumbers: bookingAttributes.flight.outwardNumbers, fareCode: bookingAttributes.flight.outwardFareCode, marketingName: bookingAttributes.flight.outwardMarketingName, luggageIncluded: bookingAttributes.flight.luggageIncluded } as SelectedFlight; if (bookingAttributes.flight.returnCode) { returnFlight = { flightCode: bookingAttributes.flight.returnCode, startDateTime: bookingAttributes.flight.returnDepartureDate, endDateTime: bookingAttributes.flight.returnArrivalDate, airlines: bookingAttributes.flight.returnAirlines, flightNumbers: bookingAttributes.flight.returnNumbers, fareCode: bookingAttributes.flight.returnFareCode, marketingName: bookingAttributes.flight.returnMarketingName, luggageIncluded: bookingAttributes.flight.luggageIncluded } as SelectedFlight; } } const request = { officeId: officeId, agentId: agentId ?? agentAdressId, payload: { searchType: searchType, catalogueId: bookingAttributes.catalogueId, productCode: productAttributes.productCode, fromDate: bookingAttributes.startDate, toDate: bookingAttributes.endDate, includeFlights: bookingAttributes.includeFlights, allotmentName: bookingAttributes.allotmentName, allotmentIds: bookingAttributes.allotmentIds ?? [], tourCode: bookingAttributes.tourCode, rooms: requestRooms, routeId: bookingAttributes.flightRouteId, outwardFlight: outwardFlight, returnFlight: returnFlight, vendorConfigurationId: bookingAttributes.vendorConfigurationId } as BookingPackageDetailsRequest } as BookingPackageRequest; return await packageApi.fetchDetails(request, signal, languageCode, state.apiSettings); }); const fetchAccommodationViews = createAsyncThunk('booking/accommodationViews', async (_, { dispatch, getState, signal }) => { const state = getState() as RootState; if (!state.booking.accommodationViewId) return Promise.resolve(); const languageCode = selectLanguageCode(state); const accommodationCodes = selectAccommodationCodes(state); const productCode = selectProductCode(state); if (!productCode) { throw Error('No product selected'); } const request = { languageCode: languageCode, productCode: productCode, accommodationCodes: accommodationCodes, contentViewId: state.booking.accommodationViewId } as GenerateBookingAccommodationRequest; return await packageApi.fetchAccommodationViews(request, signal, state.apiSettings); }); const getActiveOption = (state: BookingState) => { if (state.package) return state.package.options.find((x) => x.isSelected); return null; }; const changeOutwardFlight = (state: BookingPackage, flight: BookingPackageFlight) => { const currentOutwardFlight = state.outwardFlights.find((x) => x.isSelected)!; const currentReturnFlight = state.returnFlights.find((x) => x.isSelected)!; if (currentOutwardFlight?.entryLineGuid == flight.entryLineGuid) return; const newFlight = state.outwardFlights.find((x) => x.entryLineGuid == flight.entryLineGuid); if (newFlight) { newFlight.isSelected = true; currentOutwardFlight.isSelected = false; if (newFlight.externalGuid) { if (currentOutwardFlight.externalGuid !== newFlight.externalGuid) { const newReturnFlight = state.returnFlights.find((x) => x.externalGuid === newFlight.externalGuid)!; currentReturnFlight.isSelected = false; newReturnFlight.isSelected = true; } } else if (currentReturnFlight.externalGuid) { const firstInternal = state.returnFlights.find((x) => !x.externalGuid); if (firstInternal) { currentReturnFlight.isSelected = false; firstInternal.isSelected = true; } } } }; const changeReturnFlight = (state: BookingPackage, flight: BookingPackageFlight) => { const currentReturnFlight = state.returnFlights.find((x) => x.isSelected)!; if (currentReturnFlight?.entryLineGuid == flight.entryLineGuid) return; const newFlight = state.outwardFlights.find((x) => x.entryLineGuid == flight.entryLineGuid); if (newFlight) { newFlight.isSelected = true; currentReturnFlight.isSelected = false; } }; const changePackageOption = (state: BookingPackage) => { const selectedOutward = state.outwardFlights.find((x) => x.isSelected)!; const selectedReturn = state.returnFlights.find((x) => x.isSelected)!; const validOptions = selectedOutward.validOptions.filter((x) => selectedReturn.validOptions.some((y) => x === y)); const currentOption = state.options.find((x) => x.isSelected)!; if (validOptions.some((x) => x === currentOption.id)) return; const firstOption = state.options.find((x) => validOptions.some((y) => y === x.id))!; currentOption.isSelected = false; firstOption.isSelected = true; const currentRooms = currentOption.rooms.map((r) => { const selectedOption = r.options.find((o) => o.isSelected)!; return { accommodation: selectedOption?.accommodationCode, regime: selectedOption?.regimeCode }; }); firstOption.rooms.forEach((r, i) => { const currentRoom = currentRooms[i]; const selectedOption = r.options.find((o) => o.isSelected); const selection = r.options.find((x) => x.accommodationCode === currentRoom.accommodation && x.regimeCode === currentRoom.regime); if (selection) { if (selection.entryLineGuid !== selectedOption?.entryLineGuid) { if (selectedOption) selectedOption.isSelected = false; selection.isSelected = true; } } else { const accommodationSelection = r.options.find((x) => x.accommodationCode === currentRoom.accommodation); if (accommodationSelection) { if (accommodationSelection.entryLineGuid !== selectedOption?.entryLineGuid) { if (selectedOption) selectedOption.isSelected = false; accommodationSelection.isSelected = true; } } else { const firstOption = r.options[0]; if (firstOption.entryLineGuid !== selectedOption?.entryLineGuid) { if (selectedOption) selectedOption.isSelected = false; firstOption.isSelected = true; } } } }); }; const bookingSlice = createSlice({ name: 'booking', initialState, reducers: { setHasMounted(state, action: PayloadAction) { state.hasMounted = action.payload; }, setIsFetching(state, action: PayloadAction) { state.isFetching = action.payload; }, setOfficeId(state, action: PayloadAction) { state.officeId = action.payload; }, setLanguageCode(state, action: PayloadAction) { state.languageCode = action.payload; }, setTranslations(state, action: PayloadAction) { state.translations = action.payload; }, setBookingOptions(state, action: PayloadAction) { state.bookingOptions = action.payload; }, setBookingType(state, action: PayloadAction) { state.bookingType = action.payload; }, setProductAttributes(state, action: PayloadAction) { state.productAttributes = action.payload; }, setBookingAttributes(state, action: PayloadAction) { state.bookingAttributes = action.payload; }, setCalculateDeposit(state, action: PayloadAction) { state.calculateDeposit = action.payload; }, setShowCommission(state, action: PayloadAction) { state.showCommission = action.payload; }, setBookingNumber(state, action: PayloadAction) { state.bookingNumber = action.payload; }, setIsRetry(state, action: PayloadAction) { state.isRetry = action.payload; }, setFetchingPackage(state, action: PayloadAction) { state.isBusy = action.payload; }, setPackage(state, action: PayloadAction) { state.package = action.payload; }, setPackageRooms(state, action: PayloadAction) { const option = getActiveOption(state); if (option) option.rooms = action.payload; }, setPackageOptionPax(state, action: PayloadAction) { const option = getActiveOption(state); if (option) option.optionPax = action.payload; }, setPackageOptionUnits(state, action: PayloadAction) { const option = getActiveOption(state); if (option) option.optionUnits = action.payload; }, setSkipPayment(state, action: PayloadAction) { state.skipPaymentWithAgent = action.payload; }, setGeneratePaymentUrl(state, action: PayloadAction) { state.generatePaymentUrl = action.payload; }, setPackageGroups(state, action: PayloadAction[]>) { const option = getActiveOption(state); if (option) option.groups = action.payload; }, setPackageAirlineGroups(state, action: PayloadAction[]>) { const option = getActiveOption(state); if (option) option.airlineGroups = action.payload; }, setPackageAirportGroups(state, action: PayloadAction[]>) { const option = getActiveOption(state); if (option) option.airportGroups = action.payload; }, setTagIds(state, action: PayloadAction) { state.tagIds = action.payload; }, setAgentAdressId(state, action: PayloadAction) { state.agentAdressId = action.payload; }, setBookingRemarks(state, action: PayloadAction) { state.remarks = action.payload; }, setVoucherCodes(state, action: PayloadAction) { state.voucherCodes = action.payload; }, setCurrentStep(state, action: PayloadAction) { document.body.scrollTop = 0; // For Safari document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera state.currentStep = action.payload; }, setFlights(state, action: PayloadAction) { if (!state.package) return; changeOutwardFlight(state.package, action.payload.selectedOutward); changeReturnFlight(state.package, action.payload.selectedReturn); changePackageOption(state.package); }, setAccommodationViewId(state, action: PayloadAction) { state.accommodationViewId = action.payload; }, setIsOption(state, action: PayloadAction) { state.isOption = action.payload; }, setTravelersFirstStep(state, action: PayloadAction) { state.travelersFirstStep = action.payload; }, setIsUnavailable(state, action: PayloadAction) { state.isUnavailable = action.payload; } }, extraReducers: (builder) => { builder.addCase(fetchPackageDetails.fulfilled, (state, action) => { if (action.payload) { if (action.payload.errorCode) { console.error(action.payload.errorCode, action.payload.errorMessage, action.payload.errorDetails); state.isUnavailable = true; return; } if (!action.payload.payload) { state.isUnavailable = true; /* state.package = undefined; */ return; } state.isUnavailable = false; const bookingRooms = state.bookingAttributes?.rooms; const flight = state.bookingAttributes?.flight; const packageDetails = action.payload.payload; let activeOption = packageDetails.options.find((x) => x.isSelected)!; if (flight) { const selectedOutward = packageDetails.outwardFlights.find((x) => x.isSelected); const outwardFlights = packageDetails.outwardFlights.filter( (x) => x.code === flight.outwardCode && x.flightMetaData.flightLines[0]!.flightClass === flight.outwardClass && x.flightMetaData.flightLines.map((y) => y.number).join(',') === flight.outwardNumbers.join(',') ); // ook bij identieke vertrekvluchten eerst kijken als de returnflight kan gekoppeld worden. // op die manier moet het juiste koppel selected worden. // Enkel werkend met externe vluchten let outwardFlight: BookingPackageFlight | undefined = undefined; if (!isEmpty(outwardFlights)) { const returnExternalGuids = packageDetails.returnFlights .filter( (x) => x.code === flight.returnCode && x.flightMetaData.flightLines[0]!.flightClass === flight.returnClass && x.flightMetaData.flightLines.map((y) => y.number).join(',') === flight.returnNumbers.join(',') ) .map((f) => f.externalGuid) .filter((e) => e); outwardFlight = outwardFlights.find((o) => returnExternalGuids.includes(o.externalGuid)); if (!outwardFlight) { outwardFlight = first(outwardFlights); } } if (selectedOutward && outwardFlight) { selectedOutward.isSelected = false; outwardFlight.isSelected = true; } const selectedReturn = packageDetails.returnFlights.find((x) => x.isSelected); const returnFlight = outwardFlight?.externalGuid ? packageDetails.returnFlights.find((x) => x.externalGuid === outwardFlight?.externalGuid) : packageDetails.returnFlights.find( (x) => x.code === flight.returnCode && x.flightMetaData.flightLines[0]!.flightClass === flight.returnClass && x.flightMetaData.flightLines.map((y) => y.number).join(',') === flight.returnNumbers.join(',') ); if (selectedReturn && returnFlight) { selectedReturn.isSelected = false; returnFlight.isSelected = true; } if (outwardFlight && returnFlight) { if (!outwardFlight.validOptions.some((x) => x == activeOption.id)) { activeOption.isSelected = false; activeOption = packageDetails.options.find((x) => outwardFlight?.validOptions.some((y) => y === x.id))!; activeOption.isSelected = true; } } } if (activeOption && bookingRooms?.some((x) => x.accommodationCode || x.regimeCode)) { bookingRooms.forEach((room, i) => { if (room.accommodationCode || room.regimeCode) { activeOption.rooms[i].options = activeOption.rooms[i].options.map((ro) => ({ ...ro, isSelected: ro.accommodationCode == room.accommodationCode && ro.regimeCode == room.regimeCode })); } // Fallback to an option that has the requested accommodation OR regime if the requested option is not available. If no fallback is available, select the first option. if (!activeOption.rooms[i].options.some((x) => x.isSelected)) { const fallbackOption = activeOption.rooms[i].options.find( (x) => x.accommodationCode == room.accommodationCode || x.regimeCode == room.regimeCode ); if (fallbackOption) { fallbackOption.isSelected = true; } else { activeOption.rooms[i].options[0].isSelected = true; } } }); } state.package = packageDetails; } }); builder.addCase(fetchAgents.fulfilled, (state, action) => { if (action.payload) { state.agents = action.payload; } }); builder.addCase(fetchAccommodationViews.fulfilled, (state, action) => { if (action.payload) { state.accommodationViews = action.payload; } }); builder.addCase(fetchCountries.fulfilled, (state, action) => { if (action.payload.items) { state.countries = action.payload.items; } }); } }); export const { setOfficeId, setLanguageCode, setTranslations, setBookingOptions, setBookingType, setProductAttributes, setBookingAttributes, setCalculateDeposit, setShowCommission, setBookingNumber, setIsRetry, setFetchingPackage, setIsFetching, setHasMounted, setPackage, setPackageRooms, setPackageOptionPax, setPackageOptionUnits, setPackageGroups, setSkipPayment, setGeneratePaymentUrl, setTagIds, setAgentAdressId, setBookingRemarks, setVoucherCodes, setCurrentStep, setPackageAirlineGroups, setPackageAirportGroups, setFlights, setAccommodationViewId, setIsOption, setTravelersFirstStep, setIsUnavailable } = bookingSlice.actions; export default bookingSlice.reducer;