From 8bd43aebf877030953f106699c52a44026765a67 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 5 Nov 2024 10:43:05 +0100 Subject: [PATCH 01/65] fix: summarize prices --- .../(standard)/[step]/@summary/page.tsx | 4 +-- .../EnterDetails/Summary/index.tsx | 26 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index 26b166b78..c145d20b3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -37,7 +37,7 @@ export default async function SummaryPage({ return null } - const prices = user + const prices = user && availability.memberRate ? { local: { price: availability.memberRate?.localPrice.pricePerStay, @@ -61,7 +61,7 @@ export default async function SummaryPage({ return ( () const [chosenBreakfast, setChosenBreakfast] = useState< BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST >() + const [totalPrice, setTotalPrice] = useState({ + local: parseInt(room.localPrice.price ?? "0"), + euro: parseInt(room.euroPrice.price ?? "0"), + }) const intl = useIntl() const lang = useLang() const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore( @@ -51,7 +55,7 @@ export default function Summary({ ) let color: "uiTextHighContrast" | "red" = "uiTextHighContrast" - if (isMember) { + if (showMemberPrice) { color = "red" } @@ -60,8 +64,18 @@ export default function Summary({ if (breakfast) { setChosenBreakfast(breakfast) + if (breakfast !== BreakfastPackageEnum.NO_BREAKFAST) { + setTotalPrice({ + local: + parseInt(room.localPrice.price ?? "0") + + parseInt(breakfast.localPrice.price ?? "0"), + euro: + parseInt(room.euroPrice.price ?? "0") + + parseInt(breakfast.requestedPrice.price ?? "0"), + }) + } } - }, [bedType, breakfast]) + }, [bedType, breakfast, room.localPrice, room.euroPrice]) return (
@@ -178,7 +192,7 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: formatNumber(parseInt(room.localPrice.price ?? "0")), + amount: formatNumber(totalPrice.local), currency: room.localPrice.currency, } )} @@ -188,7 +202,7 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: formatNumber(parseInt(room.euroPrice.price ?? "0")), + amount: formatNumber(totalPrice.euro), currency: room.euroPrice.currency, } )} From fb76c67ceea6ced80438b7e2059d46119bd2ef15 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 5 Nov 2024 13:41:22 +0100 Subject: [PATCH 02/65] fix: calc total price for summary --- .../EnterDetails/Summary/index.tsx | 25 +++++++++++++------ next-env.d.ts | 2 +- server/routers/hotels/query.ts | 16 ++++++------ 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 91bf36e15..f8fe7f218 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -21,6 +21,10 @@ import { RoomsData } from "@/types/components/hotelReservation/enterDetails/book import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast" +function parsePrice(price: string | undefined) { + return price ? parseInt(price) : 0 +} + export default function Summary({ showMemberPrice, room, @@ -33,8 +37,8 @@ export default function Summary({ BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST >() const [totalPrice, setTotalPrice] = useState({ - local: parseInt(room.localPrice.price ?? "0"), - euro: parseInt(room.euroPrice.price ?? "0"), + local: parsePrice(room.localPrice.price), + euro: parsePrice(room.euroPrice.price), }) const intl = useIntl() const lang = useLang() @@ -64,14 +68,19 @@ export default function Summary({ if (breakfast) { setChosenBreakfast(breakfast) - if (breakfast !== BreakfastPackageEnum.NO_BREAKFAST) { + if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { + setTotalPrice({ + local: parsePrice(room.localPrice.price), + euro: parsePrice(room.euroPrice.price), + }) + } else { setTotalPrice({ local: - parseInt(room.localPrice.price ?? "0") + - parseInt(breakfast.localPrice.price ?? "0"), + parsePrice(room.localPrice.price) + + parsePrice(breakfast.localPrice.totalPrice), euro: - parseInt(room.euroPrice.price ?? "0") + - parseInt(breakfast.requestedPrice.price ?? "0"), + parsePrice(room.euroPrice.price) + + parsePrice(breakfast.requestedPrice.totalPrice), }) } } @@ -164,7 +173,7 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: chosenBreakfast.localPrice.price, + amount: chosenBreakfast.localPrice.totalPrice, currency: chosenBreakfast.localPrice.currency, } )} diff --git a/next-env.d.ts b/next-env.d.ts index 40c3d6809..4f11a03dc 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index e08111d07..626ae359d 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -731,13 +731,11 @@ export const hotelQueryRouter = router({ return null } - const memberRate = selectedRoom.products.find( - (rate) => rate.productType.member?.rateCode === rateCode - )?.productType.member - - const publicRate = selectedRoom.products.find( - (rate) => rate.productType.public?.rateCode === rateCode - )?.productType.public + const rateTypes = selectedRoom.products.find( + (rate) => + rate.productType.public?.rateCode === rateCode || + rate.productType.member?.rateCode === rateCode + )?.productType const mustBeGuaranteed = validateAvailabilityData.data.rateDefinitions.filter( @@ -787,8 +785,8 @@ export const hotelQueryRouter = router({ selectedRoom, mustBeGuaranteed, cancellationText, - memberRate, - publicRate, + memberRate: rateTypes?.member, + publicRate: rateTypes?.public, bedTypes, } }), From e4e303ddb95597eb42cd821917fff1ce25038c5c Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Tue, 5 Nov 2024 12:10:48 +0100 Subject: [PATCH 03/65] feat: SW-631 Added hotel alerts for select-rate page --- .../HotelInfoCard/hotelInfoCard.module.css | 6 ++++++ .../SelectRate/HotelInfoCard/index.tsx | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css index aacdf0449..d6fba4bc2 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css +++ b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css @@ -77,6 +77,12 @@ color: var(--UI-Text-Medium-contrast); } +.hotelAlert { + max-width: var(--max-width-navigation); + margin: 0 auto; + padding-top: var(--Spacing-x-one-and-half); +} + @media screen and (min-width: 1367px) { .container { padding: var(--Spacing-x4) var(--Spacing-x5); diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index b48220795..b75e2b4ae 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -3,6 +3,7 @@ import { useIntl } from "react-intl" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" +import Alert from "@/components/TempDesignSystem/Alert" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -94,6 +95,18 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
)} + {hotelAttributes?.meta?.specialAlerts.map((alert) => { + return ( +
+ +
+ ) + })} ) } From 2348fbafd40d57e08f167167f260060806dd197a Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Thu, 7 Nov 2024 12:41:31 +0000 Subject: [PATCH 04/65] fix/SW-451-design-comments (pull request #845) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(SW-451): fix design comments * fix(SW-451): fix design comments * fix(SW-451): fix color styling * fix(SW-451): fix title styling * fix(SW-451): add fix correct gap Approved-by: Pontus Dreij Approved-by: Fredrik Thorsson Approved-by: Matilda Landström --- .../HotelInfoCard/hotelInfoCard.module.css | 22 +++++++---------- .../SelectRate/HotelInfoCard/index.tsx | 24 ++++++++----------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css index d6fba4bc2..e3e5642db 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css +++ b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css @@ -30,9 +30,9 @@ gap: var(--Spacing-x-half); background-color: var(--Base-Surface-Primary-light-Normal); position: absolute; - left: var(--Spacing-x2); - top: var(--Spacing-x2); - padding: 0 var(--Spacing-x1); + left: 8px; + top: 8px; + padding: var(--Spacing-x-quarter) var(--Spacing-x1); border-radius: var(--Corner-radius-Small); } @@ -42,16 +42,16 @@ } .hotelInformation { + display: flex; + flex-direction: column; gap: var(--Spacing-x1); width: min(607px, 100%); } -.title { - margin-bottom: var(--Spacing-x1); -} - -.body { - margin-top: var(--Spacing-x2); +.hotelAddressDescription { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); } .facilities { @@ -73,10 +73,6 @@ gap: var(--Spacing-x1); } -.facilityName { - color: var(--UI-Text-Medium-contrast); -} - .hotelAlert { max-width: var(--max-width-navigation); margin: 0 auto; diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index b75e2b4ae..cd1d11f98 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -49,19 +49,17 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
- + <Title as="h2" textTransform="uppercase"> {hotelAttributes.name} - - {`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${hotelAttributes.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} - - - {hotelAttributes.hotelContent.texts.descriptions.medium} - +
+ + {`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${hotelAttributes.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + + + {hotelAttributes.hotelContent.texts.descriptions.medium} + +
@@ -79,9 +77,7 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { color="grey80" /> )} - - {facility.name} - + {facility.name}
) })} From bf316fe0d08f2b8b552cb57964c69c57c1580824 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 5 Nov 2024 16:38:27 +0100 Subject: [PATCH 05/65] fix: remove signup code --- actions/registerUserBookingFlow.ts | 67 ------------------- .../EnterDetails/Details/index.tsx | 29 -------- 2 files changed, 96 deletions(-) delete mode 100644 actions/registerUserBookingFlow.ts diff --git a/actions/registerUserBookingFlow.ts b/actions/registerUserBookingFlow.ts deleted file mode 100644 index a34cad231..000000000 --- a/actions/registerUserBookingFlow.ts +++ /dev/null @@ -1,67 +0,0 @@ -"use server" - -import { parsePhoneNumber } from "libphonenumber-js" -import { z } from "zod" - -import { serviceServerActionProcedure } from "@/server/trpc" - -import { phoneValidator } from "@/utils/phoneValidator" - -const registerUserPayload = z.object({ - firstName: z.string(), - lastName: z.string(), - dateOfBirth: z.string(), - address: z.object({ - countryCode: z.string(), - zipCode: z.string(), - }), - email: z.string(), - phoneNumber: phoneValidator("Phone is required"), -}) - -export const registerUserBookingFlow = serviceServerActionProcedure - .input(registerUserPayload) - .mutation(async function ({ ctx, input }) { - const payload = { - ...input, - language: ctx.lang, - phoneNumber: parsePhoneNumber(input.phoneNumber) - .formatNational() - .replace(/\s+/g, ""), - } - - // TODO: Consume the API to register the user as soon as passwordless signup is enabled. - // let apiResponse - // try { - // apiResponse = await api.post(api.endpoints.v1.Profile.profile, { - // body: payload, - // headers: { - // Authorization: `Bearer ${ctx.serviceToken}`, - // }, - // }) - // } catch (error) { - // console.error("Unexpected error", error) - // return { success: false, error: "Unexpected error" } - // } - - // if (!apiResponse.ok) { - // const text = await apiResponse.text() - // console.error(text) - // console.error( - // "registerUserBookingFlow api error", - // JSON.stringify({ - // query: input, - // error: { - // status: apiResponse.status, - // statusText: apiResponse.statusText, - // error: text, - // }, - // }) - // ) - // return { success: false, error: "API error" } - // } - // const json = await apiResponse.json() - // console.log("registerUserBookingFlow: json", json) - - return { success: true, data: payload } - }) diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 59085c456..cde67b1a6 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -6,7 +6,6 @@ import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" -import { registerUserBookingFlow } from "@/actions/registerUserBookingFlow" import Button from "@/components/TempDesignSystem/Button" import CountrySelect from "@/components/TempDesignSystem/Form/Country" import Input from "@/components/TempDesignSystem/Form/Input" @@ -59,36 +58,8 @@ export default function Details({ user }: DetailsProps) { const completeStep = useEnterDetailsStore((state) => state.completeStep) - // const errorMessage = intl.formatMessage({ - // id: "An error occurred. Please try again.", - // }) - const onSubmit = useCallback( async function (values: DetailsSchema) { - if (values.join) { - const signupVals = { - firstName: values.firstName, - lastName: values.lastName, - email: values.email, - phoneNumber: values.phoneNumber, - address: { - zipCode: values.zipCode, - countryCode: values.countryCode, - }, - dateOfBirth: values.dateOfBirth, - } - - const res = await registerUserBookingFlow(signupVals) - if (!res.success) { - // if (res.error) { - // toast.error(res.error) - // } else { - // toast.error(errorMessage) - // } - return - } - console.log("Signed up user: ", res) - } completeStep(values) }, From a708eedfd485a5452758aed2978e188dd455e4a2 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 5 Nov 2024 16:40:00 +0100 Subject: [PATCH 06/65] fix: extend bedType to include discription and roomTypeCode --- .../EnterDetails/BedType/index.tsx | 31 ++++++++++++------- .../EnterDetails/BedType/schema.ts | 6 ++-- .../EnterDetails/SectionAccordion/index.tsx | 2 +- .../EnterDetails/Summary/index.tsx | 5 +-- server/routers/hotels/query.ts | 4 +-- stores/enter-details.ts | 8 +++-- .../hotelReservation/enterDetails/bedType.ts | 11 +++++-- types/enums/bedType.ts | 4 --- 8 files changed, 43 insertions(+), 28 deletions(-) delete mode 100644 types/enums/bedType.ts diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index f822024c1..1bf78fee5 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -3,45 +3,52 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useCallback, useEffect } from "react" import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" import { KingBedIcon } from "@/components/Icons" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" -import { bedTypeSchema } from "./schema" +import { bedTypeFormSchema } from "./schema" import styles from "./bedOptions.module.css" import type { + BedTypeFormSchema, BedTypeProps, - BedTypeSchema, } from "@/types/components/hotelReservation/enterDetails/bedType" export default function BedType({ bedTypes }: BedTypeProps) { - const intl = useIntl() const bedType = useEnterDetailsStore((state) => state.userData.bedType) - const methods = useForm({ - defaultValues: bedType + const methods = useForm({ + defaultValues: bedType?.roomTypeCode ? { - bedType, + bedType: bedType.roomTypeCode, } : undefined, criteriaMode: "all", mode: "all", - resolver: zodResolver(bedTypeSchema), + resolver: zodResolver(bedTypeFormSchema), reValidateMode: "onChange", }) const completeStep = useEnterDetailsStore((state) => state.completeStep) const onSubmit = useCallback( - (values: BedTypeSchema) => { - completeStep(values) + (bedTypeRoomCode: BedTypeFormSchema) => { + const matchingRoom = bedTypes.find( + (roomType) => roomType.value === bedTypeRoomCode.bedType + ) + if (matchingRoom) { + const bedType = { + description: matchingRoom.description, + roomTypeCode: matchingRoom.value, + } + completeStep({ bedType }) + } }, - [completeStep] + [completeStep, bedTypes] ) useEffect(() => { @@ -70,7 +77,7 @@ export default function BedType({ bedTypes }: BedTypeProps) { name="bedType" subtitle={width} title={roomType.description} - value={roomType.description} + value={roomType.value} /> ) })} diff --git a/components/HotelReservation/EnterDetails/BedType/schema.ts b/components/HotelReservation/EnterDetails/BedType/schema.ts index 5323f4e02..7433b39da 100644 --- a/components/HotelReservation/EnterDetails/BedType/schema.ts +++ b/components/HotelReservation/EnterDetails/BedType/schema.ts @@ -1,7 +1,9 @@ import { z } from "zod" -import { BedTypeEnum } from "@/types/enums/bedType" - export const bedTypeSchema = z.object({ + description: z.string(), + roomTypeCode: z.string(), +}) +export const bedTypeFormSchema = z.object({ bedType: z.string(), }) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index b809da02a..867a9c392 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -37,7 +37,7 @@ export default function SectionAccordion({ useEffect(() => { if (step === StepEnum.selectBed) { const value = stepData.bedType - value && setTitle(value) + value && setTitle(value.description) } // If breakfast step, check if an option has been selected if (step === StepEnum.breakfast && stepData.breakfast) { diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index f8fe7f218..752f3fd4a 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -17,6 +17,7 @@ import { formatNumber } from "@/utils/format" import styles from "./summary.module.css" +import { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData" import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast" @@ -32,7 +33,7 @@ export default function Summary({ showMemberPrice: boolean room: RoomsData }) { - const [chosenBed, setChosenBed] = useState() + const [chosenBed, setChosenBed] = useState() const [chosenBreakfast, setChosenBreakfast] = useState< BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST >() @@ -136,7 +137,7 @@ export default function Summary({ {chosenBed ? (
- {chosenBed} + {chosenBed.description} {intl.formatMessage({ id: "Based on availability" })} diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 626ae359d..2938616c5 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -57,7 +57,7 @@ import { } from "./utils" import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" -import type { BedType } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { RequestOptionsWithOutBody } from "@/types/fetch" @@ -763,7 +763,7 @@ export const hotelQueryRouter = router({ } } }) - .filter((bed): bed is BedType => Boolean(bed)) + .filter((bed): bed is BedTypeSelection => Boolean(bed)) selectedRoomAvailabilitySuccessCounter.add(1, { hotelId, diff --git a/stores/enter-details.ts b/stores/enter-details.ts index e40100bb3..8b81a2947 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -14,6 +14,7 @@ import { getQueryParamsForEnterDetails, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" import { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" import { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" @@ -24,7 +25,7 @@ const SESSION_STORAGE_KEY = "enterDetails" interface EnterDetailsState { userData: { - bedType: string | undefined + bedType: BedTypeSchema | undefined breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined } & DetailsSchema roomData: BookingData @@ -35,7 +36,10 @@ interface EnterDetailsState { completeStep: (updatedData: Partial) => void navigate: ( step: StepEnum, - updatedData?: Record + updatedData?: Record< + string, + string | boolean | BreakfastPackage | BedTypeSchema + > ) => void setCurrentStep: (step: StepEnum) => void } diff --git a/types/components/hotelReservation/enterDetails/bedType.ts b/types/components/hotelReservation/enterDetails/bedType.ts index 3948fcae3..5553924dd 100644 --- a/types/components/hotelReservation/enterDetails/bedType.ts +++ b/types/components/hotelReservation/enterDetails/bedType.ts @@ -1,8 +1,11 @@ import { z } from "zod" -import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" +import { + bedTypeFormSchema, + bedTypeSchema, +} from "@/components/HotelReservation/EnterDetails/BedType/schema" -export type BedType = { +export type BedTypeSelection = { description: string size: { min: number @@ -11,7 +14,9 @@ export type BedType = { value: string } export type BedTypeProps = { - bedTypes: BedType[] + bedTypes: BedTypeSelection[] } +export interface BedTypeFormSchema extends z.output {} + export interface BedTypeSchema extends z.output {} diff --git a/types/enums/bedType.ts b/types/enums/bedType.ts deleted file mode 100644 index 2feb6d980..000000000 --- a/types/enums/bedType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum BedTypeEnum { - KING = "KING", - QUEEN = "QUEEN", -} From 591cfc7e130f11e7cd431122c8ae3ff3a4b7dccb Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 5 Nov 2024 16:40:42 +0100 Subject: [PATCH 07/65] fix: add packages data from query param --- .../(standard)/[step]/@summary/page.tsx | 40 ++++++++++--------- .../(standard)/[step]/page.tsx | 7 ++-- .../SelectRate/RoomSelection/utils.ts | 22 ++++++---- .../enterDetails/bookingData.ts | 10 +++-- .../hotelReservation/selectRate/selectRate.ts | 1 + 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index c145d20b3..ee900219e 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -17,9 +17,11 @@ export default async function SummaryPage({ searchParams, }: PageArgs>) { const selectRoomParams = new URLSearchParams(searchParams) - const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } = + const { hotel, rooms, fromDate, toDate } = getQueryParamsForEnterDetails(selectRoomParams) + const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms + const availability = await getSelectedRoomAvailability({ hotelId: hotel, adults, @@ -39,25 +41,25 @@ export default async function SummaryPage({ const prices = user && availability.memberRate ? { - local: { - price: availability.memberRate?.localPrice.pricePerStay, - currency: availability.memberRate?.localPrice.currency, - }, - euro: { - price: availability.memberRate?.requestedPrice?.pricePerStay, - currency: availability.memberRate?.requestedPrice?.currency, - }, - } + local: { + price: availability.memberRate?.localPrice.pricePerStay, + currency: availability.memberRate?.localPrice.currency, + }, + euro: { + price: availability.memberRate?.requestedPrice?.pricePerStay, + currency: availability.memberRate?.requestedPrice?.currency, + }, + } : { - local: { - price: availability.publicRate?.localPrice.pricePerStay, - currency: availability.publicRate?.localPrice.currency, - }, - euro: { - price: availability.publicRate?.requestedPrice?.pricePerStay, - currency: availability.publicRate?.requestedPrice?.currency, - }, - } + local: { + price: availability.publicRate?.localPrice.pricePerStay, + currency: availability.publicRate?.localPrice.currency, + }, + euro: { + price: availability.publicRate?.requestedPrice?.pricePerStay, + currency: availability.publicRate?.requestedPrice?.currency, + }, + } return ( ({ + adults: room.adults, // TODO: Handle multiple rooms + child: room.child, // TODO: Handle multiple rooms and children + roomTypeCode: room.roomtype, + rateCode: room.ratecode, + packages: room.packages?.split(",") as RoomPackageCodeEnum[], + })), } } @@ -58,11 +64,11 @@ export function createSelectRateUrl(roomData: BookingData) { const { hotel, fromDate, toDate } = roomData const params = new URLSearchParams({ fromDate, toDate, hotel }) - roomData.room.forEach((room, index) => { + roomData.rooms.forEach((room, index) => { params.set(`room[${index}].adults`, room.adults.toString()) - if (room.child) { - room.child.forEach((child, childIndex) => { + if (room.children) { + room.children.forEach((child, childIndex) => { params.set( `room[${index}].child[${childIndex}].age`, child.age.toString() diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 4a6667211..720dcff3a 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -1,4 +1,5 @@ import { BedTypeEnum } from "../../bookingWidget/enums" +import { RoomPackageCodeEnum } from "../selectRate/roomFilter" interface Child { bed: BedTypeEnum @@ -7,15 +8,16 @@ interface Child { interface Room { adults: number - roomtype?: string - ratecode?: string - child?: Child[] + roomTypeCode: string + rateCode: string + children?: Child[] + packages?: RoomPackageCodeEnum[] } export interface BookingData { hotel: string fromDate: string toDate: string - room: Room[] + rooms: Room[] } type Price = { diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index 24a32d7d5..d1e291459 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -13,6 +13,7 @@ interface Room { ratecode: string counterratecode?: string child?: Child[] + packages?: string } export interface SelectRateSearchParams { From 6d051629d3e7c5a8daaa7fb471f2b27646ee6845 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 5 Nov 2024 16:41:13 +0100 Subject: [PATCH 08/65] fix: send correct values to create booking --- .../EnterDetails/BedType/schema.ts | 3 +- .../EnterDetails/Payment/index.tsx | 92 ++++++++++++------- server/routers/booking/input.ts | 16 ++-- .../hotelReservation/enterDetails/bedType.ts | 2 +- .../enterDetails/bookingData.ts | 7 +- .../hotelReservation/selectRate/section.ts | 1 - 6 files changed, 70 insertions(+), 51 deletions(-) diff --git a/components/HotelReservation/EnterDetails/BedType/schema.ts b/components/HotelReservation/EnterDetails/BedType/schema.ts index 7433b39da..9567d8c7d 100644 --- a/components/HotelReservation/EnterDetails/BedType/schema.ts +++ b/components/HotelReservation/EnterDetails/BedType/schema.ts @@ -1,8 +1,7 @@ import { z } from "zod" export const bedTypeSchema = z.object({ - description: z.string(), - roomTypeCode: z.string(), + bedType: z.object({ description: z.string(), roomTypeCode: z.string() }), }) export const bedTypeFormSchema = z.object({ bedType: z.string(), diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index f4ea2b9f9..d6ac1ac33 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -1,8 +1,9 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" +import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js" import { useRouter, useSearchParams } from "next/navigation" -import { useEffect, useMemo, useState } from "react" +import { useEffect, useState } from "react" import { Label as AriaLabel } from "react-aria-components" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" @@ -36,7 +37,9 @@ import { PaymentFormData, paymentSchema } from "./schema" import styles from "./payment.module.css" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" const maxRetries = 40 const retryInterval = 2000 @@ -55,8 +58,22 @@ export default function Payment({ const lang = useLang() const intl = useIntl() const queryParams = useSearchParams() - const { firstName, lastName, email, phoneNumber, countryCode } = - useEnterDetailsStore((state) => state.userData) + const { userData, roomData } = useEnterDetailsStore((state) => ({ + userData: state.userData, + roomData: state.roomData, + })) + + const { + firstName, + lastName, + email, + phoneNumber, + countryCode, + breakfast, + bedType, + } = userData + const { toDate, fromDate, rooms: rooms, hotel } = roomData + const [confirmationNumber, setConfirmationNumber] = useState("") const methods = useForm({ @@ -114,34 +131,46 @@ export default function Payment({ (card) => card.id === data.paymentMethod ) + let phone: string + let phoneCountryCodePrefix: string | null = null + + if (isValidPhoneNumber(phoneNumber)) { + const parsedPhone = parsePhoneNumber(phoneNumber) + phone = parsedPhone.nationalNumber + phoneCountryCodePrefix = parsedPhone.countryCallingCode + } else { + phone = phoneNumber + } + initiateBooking.mutate({ hotelId: hotelId, - checkInDate: "2024-12-10", - checkOutDate: "2024-12-11", - rooms: [ - { - adults: 1, - childrenAges: [], - rateCode: "SAVEEU", - roomTypeCode: "QC", - guest: { - title: "Mr", // TODO: do we need title? - firstName, - lastName, - email, - phoneCountryCodePrefix: phoneNumber.slice(0, 3), - phoneNumber: phoneNumber.slice(3), - countryCode, - }, - packages: { - breakfast: true, - allergyFriendly: true, - petFriendly: true, - accessibility: true, - }, - smsConfirmationRequested: data.smsConfirmation, + checkInDate: fromDate, + checkOutDate: toDate, + rooms: rooms.map((room) => ({ + adults: room.adults, + children: room.children, + rateCode: room.rateCode, + roomTypeCode: bedType!.roomTypeCode, + guest: { + title: "", // TODO: do we need title? + firstName, + lastName, + email, + phoneCountryCodePrefix, + phoneNumber: phone, + countryCode, }, - ], + packages: { + breakfast: breakfast !== BreakfastPackageEnum.NO_BREAKFAST, + allergyFriendly: + room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? false, + petFriendly: + room.packages?.includes(RoomPackageCodeEnum.PET_ROOM) ?? false, + accessibility: + room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? false, + }, + smsConfirmationRequested: data.smsConfirmation, + })), payment: { paymentMethod, card: savedCreditCard @@ -151,12 +180,7 @@ export default function Payment({ cardType: savedCreditCard.cardType, } : undefined, - cardHolder: { - email: "test.user@scandichotels.com", - name: "Test User", - phoneCountryCode: "", - phoneSubscriber: "", - }, + success: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/success`, error: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/error${allQueryParams}`, cancel: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/cancel${allQueryParams}`, diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index cbda7d3ef..0ea7f4476 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -18,7 +18,7 @@ const roomsSchema = z.array( firstName: z.string(), lastName: z.string(), email: z.string().email(), - phoneCountryCodePrefix: z.string(), + phoneCountryCodePrefix: z.string().nullable(), phoneNumber: z.string(), countryCode: z.string(), membershipNumber: z.string().optional(), @@ -42,12 +42,14 @@ const paymentSchema = z.object({ cardType: z.string(), }) .optional(), - cardHolder: z.object({ - email: z.string().email(), - name: z.string(), - phoneCountryCode: z.string(), - phoneSubscriber: z.string(), - }), + cardHolder: z + .object({ + email: z.string().email(), + name: z.string(), + phoneCountryCode: z.string(), + phoneSubscriber: z.string(), + }) + .optional(), success: z.string(), error: z.string(), cancel: z.string(), diff --git a/types/components/hotelReservation/enterDetails/bedType.ts b/types/components/hotelReservation/enterDetails/bedType.ts index 5553924dd..a222ece2b 100644 --- a/types/components/hotelReservation/enterDetails/bedType.ts +++ b/types/components/hotelReservation/enterDetails/bedType.ts @@ -19,4 +19,4 @@ export type BedTypeProps = { export interface BedTypeFormSchema extends z.output {} -export interface BedTypeSchema extends z.output {} +export type BedTypeSchema = z.output["bedType"] diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 720dcff3a..6aad97085 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -1,10 +1,5 @@ -import { BedTypeEnum } from "../../bookingWidget/enums" import { RoomPackageCodeEnum } from "../selectRate/roomFilter" - -interface Child { - bed: BedTypeEnum - age: number -} +import { Child } from "../selectRate/selectRate" interface Room { adults: number diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index 85dc2a55d..66331816c 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -28,7 +28,6 @@ export interface BreakfastSelectionProps extends SectionProps { export interface DetailsProps extends SectionProps {} export interface PaymentProps { - hotelId: string otherPaymentOptions: string[] savedCreditCards: CreditCard[] | null mustBeGuaranteed: boolean From f4f771ec70ebaf5078110eb2ae637852dcb96c76 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 6 Nov 2024 13:08:15 +0100 Subject: [PATCH 09/65] fix: rename BedTypeEnums --- .../(standard)/[step]/@summary/page.tsx | 44 +++++++++---------- .../(standard)/[step]/page.tsx | 1 - .../GuestsRoomsPicker/AdultSelector/index.tsx | 8 ++-- .../ChildSelector/ChildInfoSelector.tsx | 12 ++--- .../SelectRate/RoomSelection/utils.ts | 30 +++++-------- constants/booking.ts | 2 +- server/routers/booking/input.ts | 4 +- server/routers/booking/output.ts | 11 ++--- server/routers/hotels/output.ts | 4 +- stores/guests-rooms.ts | 18 ++++---- types/components/bookingWidget/enums.ts | 2 +- .../hotelReservation/selectRate/selectRate.ts | 4 +- 12 files changed, 68 insertions(+), 72 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index ee900219e..b39c2622b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -7,7 +7,6 @@ import Summary from "@/components/HotelReservation/EnterDetails/Summary" import { generateChildrenString, getQueryParamsForEnterDetails, - mapChildrenFromString, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" @@ -39,27 +38,28 @@ export default async function SummaryPage({ return null } - const prices = user && availability.memberRate - ? { - local: { - price: availability.memberRate?.localPrice.pricePerStay, - currency: availability.memberRate?.localPrice.currency, - }, - euro: { - price: availability.memberRate?.requestedPrice?.pricePerStay, - currency: availability.memberRate?.requestedPrice?.currency, - }, - } - : { - local: { - price: availability.publicRate?.localPrice.pricePerStay, - currency: availability.publicRate?.localPrice.currency, - }, - euro: { - price: availability.publicRate?.requestedPrice?.pricePerStay, - currency: availability.publicRate?.requestedPrice?.currency, - }, - } + const prices = + user && availability.memberRate + ? { + local: { + price: availability.memberRate?.localPrice.pricePerStay, + currency: availability.memberRate?.localPrice.currency, + }, + euro: { + price: availability.memberRate?.requestedPrice?.pricePerStay, + currency: availability.memberRate?.requestedPrice?.currency, + }, + } + : { + local: { + price: availability.publicRate?.localPrice.pricePerStay, + currency: availability.publicRate?.localPrice.currency, + }, + euro: { + price: availability.publicRate?.requestedPrice?.pricePerStay, + currency: availability.publicRate?.requestedPrice?.currency, + }, + } return ( adults) { const toUpdateIndex = child.findIndex( - (child: Child) => child.bed == BedTypeEnum.IN_ADULTS_BED + (child: Child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED ) if (toUpdateIndex != -1) { setValue( `rooms.${roomIndex}.children.${toUpdateIndex}.bed`, child[toUpdateIndex].age < 3 - ? BedTypeEnum.IN_CRIB - : BedTypeEnum.IN_EXTRA_BED + ? ChildBedMapEnum.IN_CRIB + : ChildBedMapEnum.IN_EXTRA_BED ) } } diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index f219293ab..d8e541eb3 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -11,7 +11,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./child-selector.module.css" -import { BedTypeEnum } from "@/types/components/bookingWidget/enums" +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { ChildBed, ChildInfoSelectorProps, @@ -59,9 +59,9 @@ export default function ChildInfoSelector({ } function updateSelectedBed(bed: number) { - if (bed == BedTypeEnum.IN_ADULTS_BED) { + if (bed == ChildBedMapEnum.IN_ADULTS_BED) { increaseChildInAdultsBed(roomIndex) - } else if (child.bed == BedTypeEnum.IN_ADULTS_BED) { + } else if (child.bed == ChildBedMapEnum.IN_ADULTS_BED) { decreaseChildInAdultsBed(roomIndex) } updateChildBed(bed, roomIndex, index) @@ -71,15 +71,15 @@ export default function ChildInfoSelector({ const allBedTypes: ChildBed[] = [ { label: intl.formatMessage({ id: "In adults bed" }), - value: BedTypeEnum.IN_ADULTS_BED, + value: ChildBedMapEnum.IN_ADULTS_BED, }, { label: intl.formatMessage({ id: "In crib" }), - value: BedTypeEnum.IN_CRIB, + value: ChildBedMapEnum.IN_CRIB, }, { label: intl.formatMessage({ id: "In extra bed" }), - value: BedTypeEnum.IN_EXTRA_BED, + value: ChildBedMapEnum.IN_EXTRA_BED, }, ] diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index 3f02f5f67..aa6ef2810 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -1,6 +1,8 @@ +import { ChildBedTypeEnum } from "@/constants/booking" + import { getFormattedUrlQueryParams } from "@/utils/url" -import { BedTypeEnum } from "@/types/components/bookingWidget/enums" +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { @@ -12,13 +14,14 @@ export function getHotelReservationQueryParams(searchParams: URLSearchParams) { return getFormattedUrlQueryParams(searchParams, { adults: "number", age: "number", + bed: ChildBedMapEnum, }) as SelectRateSearchParams } -const bedTypeMap: Record = { - [BedTypeEnum.IN_ADULTS_BED]: "ParentsBed", - [BedTypeEnum.IN_CRIB]: "Crib", - [BedTypeEnum.IN_EXTRA_BED]: "ExtraBed", +export const bedTypeMap: Record = { + [ChildBedMapEnum.IN_ADULTS_BED]: ChildBedTypeEnum.ParentsBed, + [ChildBedMapEnum.IN_CRIB]: ChildBedTypeEnum.Crib, + [ChildBedMapEnum.IN_EXTRA_BED]: ChildBedTypeEnum.ExtraBed, } export function generateChildrenString(children: Child[]): string { @@ -31,17 +34,6 @@ export function generateChildrenString(children: Child[]): string { .join(",")}]` } -export function mapChildrenFromString(rawChildrenString: string) { - const children = rawChildrenString.split(",") - return children.map((child) => { - const [age, bed] = child.split(":") - return { - age: parseInt(age), - bed: BedTypeEnum[bed as keyof typeof BedTypeEnum], - } - }) -} - export function getQueryParamsForEnterDetails( searchParams: URLSearchParams ): BookingData { @@ -49,10 +41,12 @@ export function getQueryParamsForEnterDetails( const { room } = selectRoomParamsObject return { - ...selectRoomParamsObject, + fromDate: selectRoomParamsObject.fromDate, + toDate: selectRoomParamsObject.toDate, + hotel: selectRoomParamsObject.hotel, rooms: room.map((room) => ({ adults: room.adults, // TODO: Handle multiple rooms - child: room.child, // TODO: Handle multiple rooms and children + children: room.child, // TODO: Handle multiple rooms and children roomTypeCode: room.roomtype, rateCode: room.ratecode, packages: room.packages?.split(",") as RoomPackageCodeEnum[], diff --git a/constants/booking.ts b/constants/booking.ts index da6b30695..c99ca3d0e 100644 --- a/constants/booking.ts +++ b/constants/booking.ts @@ -15,7 +15,7 @@ export enum BookingStatusEnum { PendingPayment = "PendingPayment", } -export enum BedTypeEnum { +export enum ChildBedTypeEnum { Crib = "Crib", ExtraBed = "ExtraBed", ParentsBed = "ParentsBed", diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index 0ea7f4476..61f552f57 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -1,5 +1,7 @@ import { z } from "zod" +import { ChildBedTypeEnum } from "@/constants/booking" + const roomsSchema = z.array( z.object({ adults: z.number().int().nonnegative(), @@ -7,7 +9,7 @@ const roomsSchema = z.array( .array( z.object({ age: z.number().int().nonnegative(), - bedType: z.string(), + bedType: z.nativeEnum(ChildBedTypeEnum), }) ) .default([]), diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index cf4536a0d..e67c8805b 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { BedTypeEnum } from "@/constants/booking" +import { ChildBedTypeEnum } from "@/constants/booking" // MUTATION export const createBookingSchema = z @@ -36,9 +36,9 @@ export const createBookingSchema = z })) // QUERY -const childrenAgesSchema = z.object({ - age: z.number(), - bedType: z.nativeEnum(BedTypeEnum), +const extraBedTypesSchema = z.object({ + quantity: z.number(), + bedType: z.nativeEnum(ChildBedTypeEnum), }) const guestSchema = z.object({ @@ -65,7 +65,8 @@ export const bookingConfirmationSchema = z checkInDate: z.date({ coerce: true }), checkOutDate: z.date({ coerce: true }), createDateTime: z.date({ coerce: true }), - childrenAges: z.array(childrenAgesSchema), + childrenAges: z.array(z.number()), + extraBedTypes: z.array(extraBedTypesSchema), computedReservationStatus: z.string(), confirmationNumber: z.string(), currencyCode: z.string(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index a22e2a033..5ab05e976 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { BedTypeEnum } from "@/constants/booking" +import { ChildBedTypeEnum } from "@/constants/booking" import { dt } from "@/lib/dt" import { toLang } from "@/server/utils" @@ -441,7 +441,7 @@ export const getHotelDataSchema = z.object({ export const childrenSchema = z.object({ age: z.number(), - bedType: z.nativeEnum(BedTypeEnum), + bedType: z.nativeEnum(ChildBedTypeEnum), }) const occupancySchema = z.object({ diff --git a/stores/guests-rooms.ts b/stores/guests-rooms.ts index bc305db61..04cfbc3ec 100644 --- a/stores/guests-rooms.ts +++ b/stores/guests-rooms.ts @@ -4,7 +4,7 @@ import { produce } from "immer" import { createContext, useContext } from "react" import { create, useStore } from "zustand" -import { BedTypeEnum } from "@/types/components/bookingWidget/enums" +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { Child, GuestsRoom, @@ -41,15 +41,15 @@ export function validateBedTypes(data: extendedGuestsRoom[]) { room.child.forEach((child) => { const allowedBedTypes: number[] = [] if (child.age <= 5 && room.adults >= room.childrenInAdultsBed) { - allowedBedTypes.push(BedTypeEnum.IN_ADULTS_BED) + allowedBedTypes.push(ChildBedMapEnum.IN_ADULTS_BED) } else if (child.age <= 5) { room.childrenInAdultsBed = room.childrenInAdultsBed - 1 } if (child.age < 3) { - allowedBedTypes.push(BedTypeEnum.IN_CRIB) + allowedBedTypes.push(ChildBedMapEnum.IN_CRIB) } if (child.age > 2) { - allowedBedTypes.push(BedTypeEnum.IN_EXTRA_BED) + allowedBedTypes.push(ChildBedMapEnum.IN_EXTRA_BED) } if (!allowedBedTypes.includes(child.bed)) { child.bed = allowedBedTypes[0] @@ -84,7 +84,7 @@ export function initGuestsRoomsState(initData?: GuestsRoom[]) { inputData.rooms = initData.map((room) => { const childrenInAdultsBed = room.child ? room.child.reduce((acc, child) => { - acc = acc + (child.bed == BedTypeEnum.IN_ADULTS_BED ? 1 : 0) + acc = acc + (child.bed == ChildBedMapEnum.IN_ADULTS_BED ? 1 : 0) return acc }, 0) : 0 @@ -121,13 +121,13 @@ export function initGuestsRoomsState(initData?: GuestsRoom[]) { state.rooms[roomIndex].adults ) { const toUpdateIndex = state.rooms[roomIndex].child.findIndex( - (child) => child.bed == BedTypeEnum.IN_ADULTS_BED + (child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED ) if (toUpdateIndex != -1) { state.rooms[roomIndex].child[toUpdateIndex].bed = state.rooms[roomIndex].child[toUpdateIndex].age < 3 - ? BedTypeEnum.IN_CRIB - : BedTypeEnum.IN_EXTRA_BED + ? ChildBedMapEnum.IN_CRIB + : ChildBedMapEnum.IN_EXTRA_BED state.rooms[roomIndex].childrenInAdultsBed = state.rooms[roomIndex].adults } @@ -151,7 +151,7 @@ export function initGuestsRoomsState(initData?: GuestsRoom[]) { if ( roomChildren.length && roomChildren[roomChildren.length - 1].bed == - BedTypeEnum.IN_ADULTS_BED + ChildBedMapEnum.IN_ADULTS_BED ) { state.rooms[roomIndex].childrenInAdultsBed = state.rooms[roomIndex].childrenInAdultsBed - 1 diff --git a/types/components/bookingWidget/enums.ts b/types/components/bookingWidget/enums.ts index ae63ad571..381be9b2f 100644 --- a/types/components/bookingWidget/enums.ts +++ b/types/components/bookingWidget/enums.ts @@ -1,4 +1,4 @@ -export enum BedTypeEnum { +export enum ChildBedMapEnum { IN_ADULTS_BED = 0, IN_CRIB = 1, IN_EXTRA_BED = 2, diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index d1e291459..ba8f3f45c 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -1,9 +1,9 @@ import { Product, RoomConfiguration } from "@/server/routers/hotels/output" -import { BedTypeEnum } from "../../bookingWidget/enums" +import { ChildBedMapEnum } from "../../bookingWidget/enums" export interface Child { - bed: BedTypeEnum + bed: ChildBedMapEnum age: number } From 560fb25aee8eec14844ffae62a7920e73839cd5a Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 6 Nov 2024 14:09:18 +0100 Subject: [PATCH 10/65] fix: send totalPrice to create booking --- .../(standard)/[step]/page.tsx | 6 ++++++ .../select-hotel/@modal/(.)map/page.tsx | 6 ++++-- .../(standard)/select-hotel/utils.ts | 18 +----------------- .../EnterDetails/Payment/index.tsx | 15 ++++++++++----- server/routers/booking/input.ts | 1 + .../hotelReservation/selectRate/section.ts | 1 + 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 4dae85539..ae9957e6a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -96,6 +96,11 @@ export default async function StepPage({ id: "Select payment method", }) + const roomPrice = + user && roomAvailability.memberRate + ? roomAvailability.memberRate?.localPrice.pricePerStay + : roomAvailability.publicRate!.localPrice.pricePerStay + return (
@@ -136,6 +141,7 @@ export default async function StepPage({ label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} > = { - [BedTypeEnum.IN_ADULTS_BED]: "ParentsBed", - [BedTypeEnum.IN_CRIB]: "Crib", - [BedTypeEnum.IN_EXTRA_BED]: "ExtraBed", -} - -export function generateChildrenString(children: Child[]): string { - return `[${children - ?.map((child) => { - const age = child.age - const bedType = bedTypeMap[+child.bed] - return `${age}:${bedType}` - }) - .join(",")}]` -} - export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] { // TODO: this is just a quick transformation to get something there. May need rework return hotels.map((hotel) => ({ diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index d6ac1ac33..77821d8a2 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -31,6 +31,7 @@ import { toast } from "@/components/TempDesignSystem/Toasts" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" import useLang from "@/hooks/useLang" +import { bedTypeMap } from "../../SelectRate/RoomSelection/utils" import GuaranteeDetails from "./GuaranteeDetails" import PaymentOption from "./PaymentOption" import { PaymentFormData, paymentSchema } from "./schema" @@ -49,7 +50,7 @@ function isPaymentMethodEnum(value: string): value is PaymentMethodEnum { } export default function Payment({ - hotelId, + roomPrice, otherPaymentOptions, savedCreditCards, mustBeGuaranteed, @@ -143,16 +144,19 @@ export default function Payment({ } initiateBooking.mutate({ - hotelId: hotelId, + hotelId: hotel, checkInDate: fromDate, checkOutDate: toDate, rooms: rooms.map((room) => ({ adults: room.adults, - children: room.children, + childrenAges: room.children?.map((child) => ({ + age: child.age, + bedType: bedTypeMap[parseInt(child.bed.toString())], + })), rateCode: room.rateCode, - roomTypeCode: bedType!.roomTypeCode, + roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step. guest: { - title: "", // TODO: do we need title? + title: "", firstName, lastName, email, @@ -170,6 +174,7 @@ export default function Payment({ room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? false, }, smsConfirmationRequested: data.smsConfirmation, + roomPrice, })), payment: { paymentMethod, diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index 61f552f57..b5bc65a30 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -32,6 +32,7 @@ const roomsSchema = z.array( petFriendly: z.boolean(), accessibility: z.boolean(), }), + roomPrice: z.number().or(z.string().transform((val) => Number(val))), }) ) diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index 66331816c..df9ec7a71 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -28,6 +28,7 @@ export interface BreakfastSelectionProps extends SectionProps { export interface DetailsProps extends SectionProps {} export interface PaymentProps { + roomPrice: string otherPaymentOptions: string[] savedCreditCards: CreditCard[] | null mustBeGuaranteed: boolean From a8558eb499ed918dc1b9cf38637fcb458f0d69a6 Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Wed, 6 Nov 2024 15:22:02 +0100 Subject: [PATCH 11/65] feat/sw-251 Filter hotels on select hotel page --- .../(standard)/select-hotel/utils.ts | 32 +++++++- .../HotelReservation/HotelCard/index.tsx | 7 +- .../HotelCardListing/index.tsx | 23 +++++- .../SelectHotel/HotelFilter/index.tsx | 76 ++++++++++++++++--- .../selectHotel/hotelFilters.ts | 6 +- 5 files changed, 124 insertions(+), 20 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 8fb30757b..527db7fc8 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -6,7 +6,10 @@ import { getLang } from "@/i18n/serverContext" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" -import type { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import type { + CategorizedFilters, + Filter, +} from "@/types/components/hotelReservation/selectHotel/hotelFilters" import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" import { type PointOfInterest, @@ -14,6 +17,15 @@ import { PointOfInterestGroupEnum, } from "@/types/hotel" +const hotelSurroundingsFilterNames = [ + "Hotel surroundings", + "Hotel omgivelser", + "Hotelumgebung", + "Hotellia lähellä", + "Hotellomgivelser", + "Omgivningar", +] + export async function fetchAvailableHotels( input: AvailabilityInput ): Promise { @@ -39,7 +51,7 @@ export async function fetchAvailableHotels( return await Promise.all(hotels) } -export function getFiltersFromHotels(hotels: HotelData[]) { +export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { const filters = hotels.flatMap((hotel) => hotel.hotelData.detailedFacilities) const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))] @@ -47,7 +59,21 @@ export function getFiltersFromHotels(hotels: HotelData[]) { .map((filterId) => filters.find((filter) => filter.id === filterId)) .filter((filter): filter is Filter => filter !== undefined) - return filterList + return filterList.reduce( + (acc, filter) => { + if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) + return { + facilityFilters: acc.facilityFilters, + surroundingsFilters: [...acc.surroundingsFilters, filter], + } + + return { + facilityFilters: [...acc.facilityFilters, filter], + surroundingsFilters: acc.surroundingsFilters, + } + }, + { facilityFilters: [], surroundingsFilters: [] } + ) } export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] { diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 2bea7c895..293530998 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,3 +1,6 @@ +"use client" +import { useIntl } from "react-intl" + import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { PriceTagIcon, ScandicLogoIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" @@ -17,8 +20,8 @@ import styles from "./hotelCard.module.css" import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" -export default async function HotelCard({ hotel }: HotelCardProps) { - const intl = await getIntl() +export default function HotelCard({ hotel }: HotelCardProps) { + const intl = useIntl() const { hotelData } = hotel const { price } = hotel diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index fb17cb2a5..6cecce2d9 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -1,3 +1,7 @@ +"use client" +import { useSearchParams } from "next/navigation" +import { useMemo } from "react" + import Title from "@/components/TempDesignSystem/Text/Title" import HotelCard from "../HotelCard" @@ -7,12 +11,25 @@ import styles from "./hotelCardListing.module.css" import { HotelCardListingProps } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" export default function HotelCardListing({ hotelData }: HotelCardListingProps) { - // TODO: filter with url params + const searchParams = useSearchParams() + + const hotels = useMemo(() => { + const appliedFilters = searchParams.get("filters")?.split(",") + if (!appliedFilters || appliedFilters.length === 0) return hotelData + + return hotelData.filter((hotel) => + appliedFilters.every((appliedFilterId) => + hotel.hotelData.detailedFacilities.some( + (facility) => facility.id.toString() === appliedFilterId + ) + ) + ) + }, [searchParams, hotelData]) return (
- {hotelData && hotelData.length ? ( - hotelData.map((hotel) => ( + {hotels.length ? ( + hotels.map((hotel) => ( )) ) : ( diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index ad32427c0..97096f909 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -1,5 +1,8 @@ "use client" +import { usePathname, useSearchParams } from "next/navigation" +import { useCallback, useEffect } from "react" +import { useForm } from "react-hook-form" import { useIntl } from "react-intl" import styles from "./hotelFilter.module.css" @@ -8,26 +11,77 @@ import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHot export default function HotelFilter({ filters }: HotelFiltersProps) { const intl = useIntl() + const searchParams = useSearchParams() + const pathname = usePathname() - function handleOnChange() { - // TODO: Update URL with selected values - } + const { watch, handleSubmit, getValues, register } = useForm< + Record + >({ + defaultValues: searchParams + ?.get("filters") + ?.split(",") + .reduce((acc, curr) => ({ ...acc, [curr]: true }), {}), + }) + + const submitFilter = useCallback(() => { + const newSearchParams = new URLSearchParams(searchParams) + const values = Object.entries(getValues()) + .filter(([_, value]) => !!value) + .map(([key, _]) => key) + .join(",") + + if (values === "") { + newSearchParams.delete("filters") + } else { + newSearchParams.set("filters", values) + } + + if (values !== searchParams.values.toString()) { + window.history.replaceState( + null, + "", + `${pathname}?${newSearchParams.toString()}` + ) + } + }, [getValues, pathname, searchParams]) + + useEffect(() => { + const subscription = watch(() => handleSubmit(submitFilter)()) + return () => subscription.unsubscribe() + }, [handleSubmit, watch, submitFilter]) return (