@qite/tide-booking-component
Version:
React Booking wizard & Booking product component for Tide
635 lines (572 loc) • 22.6 kB
text/typescript
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,
searchConfigurationId: bookingAttributes.searchConfigurationId
} as BookingPackageDetailsRequest
} as BookingPackageRequest<BookingPackageDetailsRequest>;
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<boolean>) {
state.hasMounted = action.payload;
},
setIsFetching(state, action: PayloadAction<boolean>) {
state.isFetching = action.payload;
},
setOfficeId(state, action: PayloadAction<number>) {
state.officeId = action.payload;
},
setLanguageCode(state, action: PayloadAction<string>) {
state.languageCode = action.payload;
},
setTranslations(state, action: PayloadAction<any>) {
state.translations = action.payload;
},
setBookingOptions(state, action: PayloadAction<BookingOptions>) {
state.bookingOptions = action.payload;
},
setBookingType(state, action: PayloadAction<string>) {
state.bookingType = action.payload;
},
setProductAttributes(state, action: PayloadAction<ProductAttributes | undefined>) {
state.productAttributes = action.payload;
},
setBookingAttributes(state, action: PayloadAction<BookingAttributes | undefined>) {
state.bookingAttributes = action.payload;
},
setCalculateDeposit(state, action: PayloadAction<boolean>) {
state.calculateDeposit = action.payload;
},
setShowCommission(state, action: PayloadAction<boolean | undefined>) {
state.showCommission = action.payload;
},
setBookingNumber(state, action: PayloadAction<string>) {
state.bookingNumber = action.payload;
},
setIsRetry(state, action: PayloadAction<boolean>) {
state.isRetry = action.payload;
},
setFetchingPackage(state, action: PayloadAction<boolean>) {
state.isBusy = action.payload;
},
setPackage(state, action: PayloadAction<BookingPackage | undefined>) {
state.package = action.payload;
},
setPackageRooms(state, action: PayloadAction<BookingPackageRoom[]>) {
const option = getActiveOption(state);
if (option) option.rooms = action.payload;
},
setPackageOptionPax(state, action: PayloadAction<BookingOptionPax[]>) {
const option = getActiveOption(state);
if (option) option.optionPax = action.payload;
},
setPackageOptionUnits(state, action: PayloadAction<BookingOptionUnit[]>) {
const option = getActiveOption(state);
if (option) option.optionUnits = action.payload;
},
setSkipPayment(state, action: PayloadAction<boolean>) {
state.skipPaymentWithAgent = action.payload;
},
setGeneratePaymentUrl(state, action: PayloadAction<boolean>) {
state.generatePaymentUrl = action.payload;
},
setPackageGroups(state, action: PayloadAction<BookingOptionGroup<PerBookingPackageOption>[]>) {
const option = getActiveOption(state);
if (option) option.groups = action.payload;
},
setPackageAirlineGroups(state, action: PayloadAction<BookingAirlineGroup<AirlineBookingPackageOption>[]>) {
const option = getActiveOption(state);
if (option) option.airlineGroups = action.payload;
},
setPackageAirportGroups(state, action: PayloadAction<BookingAirportGroup<AirportBookingPackageOption>[]>) {
const option = getActiveOption(state);
if (option) option.airportGroups = action.payload;
},
setTagIds(state, action: PayloadAction<number[] | undefined>) {
state.tagIds = action.payload;
},
setAgentAdressId(state, action: PayloadAction<number | undefined>) {
state.agentAdressId = action.payload;
},
setBookingRemarks(state, action: PayloadAction<string>) {
state.remarks = action.payload;
},
setVoucherCodes(state, action: PayloadAction<string[]>) {
state.voucherCodes = action.payload;
},
setCurrentStep(state, action: PayloadAction<BookingStep>) {
document.body.scrollTop = 0; // For Safari
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
state.currentStep = action.payload;
},
setFlights(state, action: PayloadAction<GroupedFlights>) {
if (!state.package) return;
changeOutwardFlight(state.package, action.payload.selectedOutward);
changeReturnFlight(state.package, action.payload.selectedReturn);
changePackageOption(state.package);
},
setAccommodationViewId(state, action: PayloadAction<number>) {
state.accommodationViewId = action.payload;
},
setIsOption(state, action: PayloadAction<boolean>) {
state.isOption = action.payload;
},
setTravelersFirstStep(state, action: PayloadAction<boolean>) {
state.travelersFirstStep = action.payload;
},
setIsUnavailable(state, action: PayloadAction<boolean>) {
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;