@qite/tide-booking-component
Version:
React Booking wizard & Booking product component for Tide
604 lines (546 loc) • 20.4 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,
GenerateBookingAccommodationRequest,
PerBookingPackageOption,
} 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,
selectBookingAttributes,
selectBookingRooms,
selectLanguageCode,
selectOfficeId,
selectProductAttributes,
selectProductCode,
} from "./selectors";
export interface BookingState {
officeId: number;
languageCode: string;
productAttributes?: ProductAttributes;
bookingAttributes?: BookingAttributes;
calculateDeposit: 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 };
}
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,
bookingNumber: undefined,
isRetry: false,
package: undefined,
isBusy: false,
skipPaymentWithAgent: false,
generatePaymentUrl: false,
tagIds: [],
agentAdressId: undefined,
currentStep: OPTIONS_FORM_STEP,
translations: undefined,
};
export const fetchPackage = createAsyncThunk(
"booking/fetchPackage",
async (_, { dispatch }) => {
dispatch(setFetchingPackage(true));
await dispatch(fetchAgents());
await dispatch(fetchPackageDetails());
await dispatch(fetchAccommodationViews());
dispatch(setFetchingPackage(false));
}
);
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 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);
const request = {
officeId: officeId,
agentId: agentId,
payload: {
searchType: isAllotment ? 1 : 0,
catalogueId: bookingAttributes.catalog,
productCode: productAttributes.productCode,
fromDate: bookingAttributes.startDate,
toDate: bookingAttributes.endDate,
includeFlights: bookingAttributes.includeFlights,
allotmentName: bookingAttributes.allotmentName,
allotmentIds: bookingAttributes.allotmentIds ?? [],
tourCode: bookingAttributes.tourCode,
rooms: requestRooms,
} 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: {
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>) {
state.productAttributes = action.payload;
},
setBookingAttributes(state, action: PayloadAction<BookingAttributes>) {
state.bookingAttributes = action.payload;
},
setCalculateDeposit(state, action: PayloadAction<boolean>) {
state.calculateDeposit = 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>) {
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;
}
},
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;
return;
}
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;
}
});
},
});
export const {
setOfficeId,
setLanguageCode,
setTranslations,
setBookingOptions,
setBookingType,
setProductAttributes,
setBookingAttributes,
setCalculateDeposit,
setBookingNumber,
setIsRetry,
setFetchingPackage,
setPackage,
setPackageRooms,
setPackageOptionPax,
setPackageOptionUnits,
setPackageGroups,
setSkipPayment,
setGeneratePaymentUrl,
setTagIds,
setAgentAdressId,
setBookingRemarks,
setVoucherCodes,
setCurrentStep,
setPackageAirlineGroups,
setPackageAirportGroups,
setFlights,
setAccommodationViewId
} = bookingSlice.actions;
export default bookingSlice.reducer;