diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx index 8995f0111..fddaffbb1 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx @@ -18,11 +18,14 @@ import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/D import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile" import { generateChildrenString } from "@/components/HotelReservation/utils" import Alert from "@/components/TempDesignSystem/Alert" +import TrackingSDK from "@/components/TrackingSDK" import { getIntl } from "@/i18n" import RoomProvider from "@/providers/Details/RoomProvider" import EnterDetailsProvider from "@/providers/EnterDetailsProvider" import { convertSearchParamsToObj } from "@/utils/url" +import { getTracking } from "./tracking" + import styles from "./page.module.css" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" @@ -57,34 +60,30 @@ export default async function DetailsPage({ const childrenAsString = room.childrenInRoom && generateChildrenString(room.childrenInRoom) - const selectedRoomAvailabilityInput = { + const packages = room.packages + ? await getPackages({ + adults: room.adults, + children: room.childrenInRoom?.length, + endDate: booking.toDate, + hotelId: booking.hotelId, + packageCodes: room.packages, + startDate: booking.fromDate, + lang, + }) + : null + + const roomAvailability = await getSelectedRoomAvailability({ adults: room.adults, + bookingCode: booking.bookingCode, children: childrenAsString, + counterRateCode: room.counterRateCode, hotelId: booking.hotelId, packageCodes: room.packages, rateCode: room.rateCode, - roomStayStartDate: booking.fromDate, roomStayEndDate: booking.toDate, + roomStayStartDate: booking.fromDate, roomTypeCode: room.roomTypeCode, - counterRateCode: room.counterRateCode, - bookingCode: booking.bookingCode, - } - - const packages = room.packages - ? await getPackages({ - adults: room.adults, - children: room.childrenInRoom?.length, - endDate: booking.toDate, - hotelId: booking.hotelId, - packageCodes: room.packages, - startDate: booking.fromDate, - lang, - }) - : null - - const roomAvailability = await getSelectedRoomAvailability( - selectedRoomAvailabilityInput - ) + }) if (!roomAvailability) { // redirect back to select-rate if availability call fails @@ -98,8 +97,11 @@ export default async function DetailsPage({ mustBeGuaranteed: roomAvailability.mustBeGuaranteed, memberMustBeGuaranteed: roomAvailability.memberMustBeGuaranteed, packages, - rateTitle: roomAvailability.rateTitle, + rate: roomAvailability.rate, + rateDefinitionTitle: roomAvailability.rateDefinitionTitle, rateDetails: roomAvailability.rateDetails ?? [], + rateTitle: roomAvailability.rateTitle, + rateType: roomAvailability.rateType, roomType: roomAvailability.selectedRoom.roomType, roomTypeCode: roomAvailability.selectedRoom.roomTypeCode, roomRate: { @@ -122,41 +124,28 @@ export default async function DetailsPage({ language: lang, }) const user = await getProfileSafely() - // const userTrackingData = await getUserTracking() if (!hotelData || !rooms) { return notFound() } - // const arrivalDate = new Date(booking.fromDate) - // const departureDate = new Date(booking.toDate) const { hotel } = hotelData - // TODO: add tracking - // const initialHotelsTrackingData: TrackingSDKHotelInfo = { - // searchTerm: searchParams.city, - // arrivalDate: format(arrivalDate, "yyyy-MM-dd"), - // departureDate: format(departureDate, "yyyy-MM-dd"), - // noOfAdults: adults, - // noOfChildren: childrenInRoom?.length, - // ageOfChildren: childrenInRoom?.map((c) => c.age).join(","), - // childBedPreference: childrenInRoom - // ?.map((c) => ChildBedMapEnum[c.bed]) - // .join("|"), - // noOfRooms: 1, // // TODO: Handle multiple rooms - // duration: differenceInCalendarDays(departureDate, arrivalDate), - // leadTime: differenceInCalendarDays(arrivalDate, new Date()), - // searchType: "hotel", - // bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - // country: hotel?.address.country, - // hotelID: hotel?.operaId, - // region: hotel?.address.city, - // } + const { hotelsTrackingData, pageTrackingData } = getTracking( + booking, + hotel, + rooms, + !!breakfastPackages?.length, + searchParams.city, + !!user, + lang + ) const intl = await getIntl() const firstRoom = rooms[0] const multirooms = rooms.slice(1) + const isRoomNotAvailable = rooms.some((room) => !room.isAvailable) return ( + ) } diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/tracking.ts b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/tracking.ts new file mode 100644 index 000000000..9504e7ff0 --- /dev/null +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/tracking.ts @@ -0,0 +1,101 @@ +import { differenceInCalendarDays, format, isWeekend } from "date-fns" + +import { getSpecialRoomType } from "@/utils/specialRoomType" + +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" +import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { + TrackingChannelEnum, + type TrackingSDKHotelInfo, + type TrackingSDKPageData, +} from "@/types/components/tracking" +import type { Hotel } from "@/types/hotel" +import type { Room } from "@/types/providers/details/room" +import type { Lang } from "@/constants/languages" +import type { SelectHotelParams } from "@/utils/url" + +export function getTracking( + booking: SelectHotelParams, + hotel: Hotel, + rooms: Room[], + offersBreakfast: boolean, + city: string | undefined, + isMember: boolean, + lang: Lang +) { + const arrivalDate = new Date(booking.fromDate) + const departureDate = new Date(booking.toDate) + + const pageTrackingData: TrackingSDKPageData = { + channel: TrackingChannelEnum.hotelreservation, + domainLanguage: lang, + pageId: "details", + pageName: "hotelreservation|details", + pageType: "bookingroomsandratespage", + siteSections: "hotelreservation|details", + siteVersion: "new-web", + } + + const hotelsTrackingData: TrackingSDKHotelInfo = { + ageOfChildren: booking.rooms + .map( + (room) => room.childrenInRoom?.map((kid) => kid.age).join(",") ?? "-" + ) + .join("|"), + analyticsRateCode: rooms.map((room) => room.rate).join("|"), + arrivalDate: format(arrivalDate, "yyyy-MM-dd"), + bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", + breakfastOption: rooms + .map(() => (offersBreakfast ? "breakfast buffet" : "no breakfast")) + .join(","), + childBedPreference: booking.rooms + .map( + (room) => + room.childrenInRoom + ?.map((kid) => ChildBedMapEnum[kid.bed]) + .join(",") ?? "-" + ) + .join("|"), + country: hotel?.address.country, + departureDate: format(departureDate, "yyyy-MM-dd"), + duration: differenceInCalendarDays(departureDate, arrivalDate), + hotelID: hotel?.operaId, + leadTime: differenceInCalendarDays(arrivalDate, new Date()), + noOfAdults: booking.rooms.map((room) => room.adults).join(","), + noOfChildren: booking.rooms + .map((room) => room.childrenInRoom?.length ?? 0) + .join(","), + noOfRooms: booking.rooms.length, + rateCode: rooms + .map((room, idx) => { + if (idx === 0 && isMember && room.roomRate.memberRate) { + return room.roomRate.memberRate?.rateCode + } + return room.roomRate.publicRate?.rateCode + }) + .join("|"), + rateCodeCancellationRule: rooms + .map((room) => room.cancellationText.toLowerCase()) + .join(","), + rateCodeName: rooms.map((room) => room.rateDefinitionTitle).join(","), + rateCodeType: rooms.map((room) => room.rateType.toLowerCase()).join(","), + region: hotel?.address.city, + revenueCurrencyCode: rooms + .map( + (room) => + room.roomRate.publicRate?.localPrice.currency ?? + room.roomRate.memberRate?.localPrice.currency + ) + .join(","), + searchTerm: city, + searchType: "hotel", + specialRoomType: rooms + .map((room) => getSpecialRoomType(room.packages)) + .join(","), + } + + return { + hotelsTrackingData, + pageTrackingData, + } +} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/HotelCardCarousel/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/HotelCardCarousel/index.tsx index 89eb9da49..1625e11b8 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/HotelCardCarousel/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/HotelCardCarousel/index.tsx @@ -21,7 +21,7 @@ export default function HotelCardCarousel({ const { clickedHotel } = useDestinationPageHotelsMapStore() const selectedHotelIdx = visibleHotels.findIndex( - (hotel) => hotel.hotel.operaId === clickedHotel + ({ hotel }) => hotel.operaId === clickedHotel ) return ( diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Tracking/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Tracking/index.tsx new file mode 100644 index 000000000..e1acdcca1 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Tracking/index.tsx @@ -0,0 +1,40 @@ +"use client" + +import { useBookingConfirmationStore } from "@/stores/booking-confirmation" + +import TrackingSDK from "@/components/TrackingSDK" +import useLang from "@/hooks/useLang" + +import { getTracking } from "./tracking" + +import type { Room } from "@/types/stores/booking-confirmation" +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export default function Tracking({ + bookingConfirmation, +}: { + bookingConfirmation: BookingConfirmation +}) { + const lang = useLang() + const bookingRooms = useBookingConfirmationStore((state) => state.rooms) + if (!bookingRooms.every(Boolean)) { + return null + } + + const rooms = bookingRooms.filter((room): room is Room => !!room) + + const { hotelsTrackingData, pageTrackingData, paymentInfo } = getTracking( + lang, + bookingConfirmation.booking, + bookingConfirmation.hotel, + rooms + ) + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Tracking/tracking.ts b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Tracking/tracking.ts new file mode 100644 index 000000000..a1492acf9 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Tracking/tracking.ts @@ -0,0 +1,157 @@ +import { differenceInCalendarDays, format, isWeekend } from "date-fns" + +import { getSpecialRoomType } from "@/utils/specialRoomType" + +import { invertedBedTypeMap } from "../../utils" + +import { + TrackingChannelEnum, + type TrackingSDKHotelInfo, + type TrackingSDKPageData, + type TrackingSDKPaymentInfo, +} from "@/types/components/tracking" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" +import type { Room } from "@/types/stores/booking-confirmation" +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability" +import type { Lang } from "@/constants/languages" + +function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) { + switch (cancellationRule) { + case "CancellableBefore6PM": + return "flex" + case "Changeable": + return "change" + case "NotCancellable": + return "save" + default: + return "-" + } +} + +function findBreakfastPackage( + packages: BookingConfirmation["booking"]["packages"] +) { + return packages.find( + (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST + ) +} + +function mapBreakfastPackage( + breakfastPackage: BookingConfirmation["booking"]["packages"][number], + adults: number, + operaId: string +) { + return { + hotelid: operaId, + productCategory: "", // TODO: Add category + productId: breakfastPackage.code!, // Is not found unless code exists + productName: "BreakfastAdult", + productPoints: 0, + productPrice: +breakfastPackage.unitPrice, + productType: "food", + productUnits: adults, + } +} + +export function getTracking( + lang: Lang, + booking: BookingConfirmation["booking"], + hotel: BookingConfirmation["hotel"], + rooms: Room[] +) { + const arrivalDate = new Date(booking.checkInDate) + const departureDate = new Date(booking.checkOutDate) + + const pageTrackingData: TrackingSDKPageData = { + channel: TrackingChannelEnum.hotelreservation, + domainLanguage: lang, + pageId: "booking-confirmation", + pageName: `hotelreservation|confirmation`, + pageType: "confirmation", + siteSections: `hotelreservation|confirmation`, + siteVersion: "new-web", + } + + const noOfAdults = rooms.map((r) => r.adults).join(",") + const noOfChildren = rooms.map((r) => r.children ?? 0).join(",") + const noOfRooms = rooms.length + + const hotelsTrackingData: TrackingSDKHotelInfo = { + ageOfChildren: rooms.map((r) => r.childrenAges?.join(",") ?? "-").join("|"), + analyticsRateCode: rooms + .map((r) => getRate(r.rateDefinition.cancellationRule)) + .join("|"), + ancillaries: rooms + .filter((r) => findBreakfastPackage(r.packages)) + .map((r) => { + return mapBreakfastPackage( + findBreakfastPackage(r.packages)!, + r.adults, + hotel.operaId + ) + }), + arrivalDate: format(arrivalDate, "yyyy-MM-dd"), + bedType: rooms + .map((r) => r.bedDescription) + .join(",") + .toLowerCase(), + bnr: rooms.map((r) => r.confirmationNumber).join(","), + bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", + breakfastOption: rooms + .map((r) => { + if (r.breakfastIncluded || r.breakfast) { + return "breakfast buffet" + } + return "no breakfast" + }) + .join(","), + childBedPreference: rooms + .map( + (r) => + r.childBedPreferences + .map((cbp) => + Array(cbp.quantity).fill(invertedBedTypeMap[cbp.bedType]) + ) + .join(",") ?? "-" + ) + .join("|"), + country: hotel?.address.country, + departureDate: format(departureDate, "yyyy-MM-dd"), + duration: differenceInCalendarDays(departureDate, arrivalDate), + hotelID: hotel.operaId, + leadTime: differenceInCalendarDays(arrivalDate, new Date()), + noOfAdults, + noOfChildren, + noOfRooms, + rateCode: rooms.map((r) => r.rateDefinition.rateCode).join(","), + rateCodeCancellationRule: rooms + .map((r) => r.rateDefinition.cancellationText) + .join(",") + .toLowerCase(), + rateCodeName: rooms + .map((r) => r.rateDefinition.title) + .join(",") + .toLowerCase(), + //rateCodeType: , //TODO: Add when available in API. "regular, promotion, corporate etx", + region: hotel?.address.city, + revenueCurrencyCode: rooms.map((r) => r.currencyCode).join(","), + roomPrice: rooms.map((r) => r.roomPrice).join(","), + roomTypeCode: rooms.map((r) => r.roomTypeCode ?? "-").join(","), + searchType: "hotel", + specialRoomType: rooms + .map((room) => getSpecialRoomType(room.packages)) + .join(","), + totalPrice: rooms.map((r) => r.totalPrice).join(","), + } + + const paymentInfo: TrackingSDKPaymentInfo = { + paymentStatus: "confirmed", + } + + return { + hotelsTrackingData, + pageTrackingData, + paymentInfo, + } +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/index.tsx index 340a6c821..fa4131eab 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/index.tsx @@ -1,4 +1,3 @@ -import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { notFound } from "next/navigation" import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" @@ -10,30 +9,20 @@ import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt" import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms" import SidePanel from "@/components/HotelReservation/SidePanel" import Divider from "@/components/TempDesignSystem/Divider" -import TrackingSDK from "@/components/TrackingSDK" -import { getLang } from "@/i18n/serverContext" import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider" -import { invertedBedTypeMap } from "../utils" import Alerts from "./Alerts" import Confirmation from "./Confirmation" +import Tracking from "./Tracking" import { mapRoomState } from "./utils" import styles from "./bookingConfirmation.module.css" import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" -import { - TrackingChannelEnum, - type TrackingSDKHotelInfo, - type TrackingSDKPageData, - type TrackingSDKPaymentInfo, -} from "@/types/components/tracking" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" export default async function BookingConfirmation({ confirmationNumber, }: BookingConfirmationProps) { - const lang = getLang() const bookingConfirmation = await getBookingConfirmation(confirmationNumber) if (!bookingConfirmation) { @@ -44,74 +33,6 @@ export default async function BookingConfirmation({ return notFound() } - const arrivalDate = new Date(booking.checkInDate) - const departureDate = new Date(booking.checkOutDate) - - const selectedBreakfast = booking.packages.find( - (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST - ) - - const breakfastAncillary = selectedBreakfast && { - hotelid: hotel.operaId, - productName: "BreakfastAdult", - productCategory: "", // TODO: Add category - productId: selectedBreakfast.code ?? "", - productPrice: +selectedBreakfast.unitPrice, - productUnits: booking.adults, - productPoints: 0, - productType: "food", - } - - const initialPageTrackingData: TrackingSDKPageData = { - pageId: "booking-confirmation", - domainLanguage: lang, - channel: TrackingChannelEnum["hotelreservation"], - pageName: `hotelreservation|confirmation`, - siteSections: `hotelreservation|confirmation`, - pageType: "confirmation", - siteVersion: "new-web", - } - - const initialHotelsTrackingData: TrackingSDKHotelInfo = { - arrivalDate: format(arrivalDate, "yyyy-MM-dd"), - departureDate: format(departureDate, "yyyy-MM-dd"), - noOfAdults: booking.adults, - noOfChildren: booking.childrenAges?.length, - ageOfChildren: booking.childrenAges?.join(","), - childBedPreference: booking?.childBedPreferences - ?.flatMap((c) => Array(c.quantity).fill(invertedBedTypeMap[c.bedType])) - .join("|"), - noOfRooms: 1, // // TODO: Handle multiple rooms - duration: differenceInCalendarDays(departureDate, arrivalDate), - leadTime: differenceInCalendarDays(arrivalDate, new Date()), - searchType: "hotel", - bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - country: hotel?.address.country, - hotelID: hotel.operaId, - region: hotel?.address.city, - rateCode: booking.rateDefinition.rateCode ?? undefined, - //rateCodeType: , //TODO: Add when available in API. "regular, promotion, corporate etx", - rateCodeName: booking.rateDefinition.title ?? undefined, - rateCodeCancellationRule: - booking.rateDefinition?.cancellationText ?? undefined, - revenueCurrencyCode: booking.currencyCode, - breakfastOption: booking.rateDefinition.breakfastIncluded - ? "breakfast buffet" - : "no breakfast", - totalPrice: booking.totalPrice, - //specialRoomType: getSpecialRoomType(booking.packages), TODO: Add - //roomTypeName: booking.roomTypeCode ?? undefined, TODO: Do we get the name? - bedType: room?.bedType.name, - roomTypeCode: booking.roomTypeCode ?? undefined, - roomPrice: booking.roomPrice, - bnr: booking.confirmationNumber ?? undefined, - ancillaries: breakfastAncillary ? [breakfastAncillary] : [], - } - - const paymentInfo: TrackingSDKPaymentInfo = { - paymentStatus: "confirmed", - } - return ( - + ) } diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/utils.ts b/apps/scandic-web/components/HotelReservation/BookingConfirmation/utils.ts index 8b3826610..1fd7eae0e 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/utils.ts +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/utils.ts @@ -19,13 +19,17 @@ export function mapRoomState( breakfast, breakfastIncluded, children: booking.childrenAges.length, + childrenAges: booking.childrenAges, childBedPreferences: booking.childBedPreferences, confirmationNumber: booking.confirmationNumber, + currencyCode: booking.currencyCode, fromDate: booking.checkInDate, name: room.name, + packages: booking.packages, rateDefinition: booking.rateDefinition, roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"), roomPrice: booking.roomPrice, + roomTypeCode: booking.roomTypeCode, toDate: booking.checkOutDate, totalPrice: booking.totalPrice, totalPriceExVat: booking.totalPriceExVat, diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx index 074d14481..5f3a8c331 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/index.tsx @@ -55,15 +55,19 @@ export default function Details({ user }: DetailsProps) { reValidateMode: "onChange", values: { countryCode: user?.address?.countryCode ?? initialData.countryCode, - dateOfBirth: initialData.dateOfBirth, + dateOfBirth: + "dateOfBirth" in initialData ? initialData.dateOfBirth : undefined, email: user?.email ?? initialData.email, firstName: user?.firstName ?? initialData.firstName, join: initialData.join, lastName: user?.lastName ?? initialData.lastName, membershipNo: initialData.membershipNo, phoneNumber: user?.phoneNumber ?? initialData.phoneNumber, - zipCode: initialData.zipCode, - specialRequests: initialData.specialRequests, + zipCode: "zipCode" in initialData ? initialData.zipCode : undefined, + specialRequests: + "specialRequests" in initialData + ? initialData.specialRequests + : undefined, }, }) diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index 9ae43dda7..d5eb28613 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -255,24 +255,24 @@ export default function PaymentClient({ const guarantee = data.guarantee const useSavedCard = savedCreditCard ? { - card: { - alias: savedCreditCard.alias, - expiryDate: savedCreditCard.expirationDate, - cardType: savedCreditCard.cardType, - }, - } + card: { + alias: savedCreditCard.alias, + expiryDate: savedCreditCard.expirationDate, + cardType: savedCreditCard.cardType, + }, + } : {} const shouldUsePayment = !isFlexRate || guarantee const payment = shouldUsePayment ? { - paymentMethod: paymentMethod, - ...useSavedCard, - success: `${paymentRedirectUrl}/success`, - error: `${paymentRedirectUrl}/error`, - cancel: `${paymentRedirectUrl}/cancel`, - } + paymentMethod: paymentMethod, + ...useSavedCard, + success: `${paymentRedirectUrl}/success`, + error: `${paymentRedirectUrl}/error`, + cancel: `${paymentRedirectUrl}/cancel`, + } : undefined trackPaymentEvent({ @@ -285,56 +285,65 @@ export default function PaymentClient({ }) initiateBooking.mutate({ - language: lang, - hotelId, checkInDate: fromDate, checkOutDate: toDate, + hotelId, + language: lang, + payment, rooms: rooms.map(({ room }, idx) => ({ adults: room.adults, childrenAges: room.childrenInRoom?.map((child) => ({ age: child.age, bedType: bedTypeMap[parseInt(child.bed.toString())], })), - rateCode: - (room.guest.join || room.guest.membershipNo) && - booking.rooms[idx].counterRateCode - ? booking.rooms[idx].counterRateCode - : booking.rooms[idx].rateCode, - roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step. guest: { becomeMember: room.guest.join, countryCode: room.guest.countryCode, - dateOfBirth: room.guest.dateOfBirth, email: room.guest.email, firstName: room.guest.firstName, lastName: room.guest.lastName, membershipNumber: room.guest.membershipNo, phoneNumber: room.guest.phoneNumber, - postalCode: room.guest.zipCode, + // Only allowed for room one + ...(idx === 0 && { + dateOfBirth: + "dateOfBirth" in room.guest && room.guest.dateOfBirth + ? room.guest.dateOfBirth + : undefined, + postalCode: + "zipCode" in room.guest && room.guest.zipCode + ? room.guest.zipCode + : undefined, + }), }, packages: { - breakfast: !!(room.breakfast && room.breakfast.code), - allergyFriendly: - room.roomFeatures?.some( - (feature) => feature.code === RoomPackageCodeEnum.ALLERGY_ROOM - ) ?? false, - petFriendly: - room.roomFeatures?.some( - (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM - ) ?? false, accessibility: room.roomFeatures?.some( (feature) => feature.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM ) ?? false, + allergyFriendly: + room.roomFeatures?.some( + (feature) => feature.code === RoomPackageCodeEnum.ALLERGY_ROOM + ) ?? false, + breakfast: !!(room.breakfast && room.breakfast.code), + petFriendly: + room.roomFeatures?.some( + (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM + ) ?? false, }, - smsConfirmationRequested: data.smsConfirmation, + rateCode: + (room.guest.join || room.guest.membershipNo) && + booking.rooms[idx].counterRateCode + ? booking.rooms[idx].counterRateCode + : booking.rooms[idx].rateCode, roomPrice: { memberPrice: room.roomRate.memberRate?.localPrice.pricePerStay, publicPrice: room.roomRate.publicRate?.localPrice.pricePerStay, }, + roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step. + smsConfirmationRequested: data.smsConfirmation, })), - payment, }) }, [ @@ -436,7 +445,7 @@ export default function PaymentClient({ value={paymentMethod} label={ PAYMENT_METHOD_TITLES[ - paymentMethod as PaymentMethodEnum + paymentMethod as PaymentMethodEnum ] } /> diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/PriceChangeDialog/PriceChangeSummary/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/PriceChangeDialog/PriceChangeSummary/index.tsx index 9e45311cc..d37e87656 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/PriceChangeDialog/PriceChangeSummary/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/PriceChangeDialog/PriceChangeSummary/index.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, Fragment } from "react" +import { Fragment, useState } from "react" import { Dialog, DialogTrigger, diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/PriceChangeDialog/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/PriceChangeDialog/index.tsx index a2bdc9cd6..372a8eef6 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/PriceChangeDialog/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/PriceChangeDialog/index.tsx @@ -15,7 +15,8 @@ import { calculateTotalRoomPrice } from "../Payment/helpers" import PriceChangeSummary from "./PriceChangeSummary" import styles from "./priceChangeDialog.module.css" -import { PriceChangeData } from "@/types/components/hotelReservation/enterDetails/payment" + +import type { PriceChangeData } from "@/types/components/hotelReservation/enterDetails/payment" type PriceDetailsState = { newTotalPrice: number diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/index.tsx similarity index 59% rename from apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx rename to apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/index.tsx index bfc5d7024..ab2c019f6 100644 --- a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/index.tsx @@ -13,12 +13,8 @@ import { formatPrice } from "@/utils/numberFormatting" import styles from "./priceDetailsTable.module.css" -import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" -import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details" import type { Price } from "@/types/components/hotelReservation/price" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { Packages } from "@/types/requests/packages" +import type { RoomState } from "@/types/stores/enter-details" function Row({ label, @@ -62,30 +58,37 @@ function TableSectionHeader({ ) } -interface PriceDetailsTableProps { +export type Room = Pick< + RoomState["room"], + | "adults" + | "bedType" + | "breakfast" + | "childrenInRoom" + | "roomFeatures" + | "roomRate" + | "roomType" +> & { + guest?: RoomState["room"]["guest"] +} + +export interface PriceDetailsTableProps { + bookingCode?: string fromDate: string + isMember: boolean + rooms: Room[] toDate: string - rooms: { - adults: number - childrenInRoom: Child[] | undefined - roomType: string - roomPrice: RoomPrice - bedType?: BedTypeSchema - breakfast?: BreakfastPackage | false - roomFeatures?: Packages | null - }[] totalPrice: Price vat: number - bookingCode?: string } export default function PriceDetailsTable({ + bookingCode, fromDate, - toDate, + isMember, rooms, + toDate, totalPrice, vat, - bookingCode, }: PriceDetailsTableProps) { const intl = useIntl() const lang = useLang() @@ -105,112 +108,116 @@ export default function PriceDetailsTable({ ${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})` return ( - {rooms.map((room, idx) => ( - - - {rooms.length > 1 && ( - - {intl.formatMessage( - { id: "Room {roomIndex}" }, - { - roomIndex: idx + 1, - } - )} - - )} - - { + const getMemberRate = + room.guest?.join || + room.guest?.membershipNo || + (idx === 0 && isMember) + const price = + getMemberRate && room.roomRate.memberRate + ? room.roomRate.memberRate + : room.roomRate.publicRate + if (!price) { + return null + } + return ( + + + {rooms.length > 1 && ( + + {intl.formatMessage({ id: "Room" })} {idx + 1} + )} - /> - {room.roomFeatures - ? room.roomFeatures.map((feature) => ( + + + {room.roomFeatures + ? room.roomFeatures.map((feature) => ( )) - : null} - {room.bedType ? ( - - ) : null} - - - - {room.breakfast ? ( - - - {room.childrenInRoom?.length ? ( + : null} + {room.bedType ? ( ) : null} - ) : null} - - ))} + + {room.breakfast ? ( + + + {room.childrenInRoom?.length ? ( + + ) : null} + + + ) : null} + + ) + })} )} -
+ ) } diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/priceDetailsTable.module.css b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/priceDetailsTable.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/priceDetailsTable.module.css rename to apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/priceDetailsTable.module.css diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx index 220bc5ba9..ec9c1a15f 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx @@ -22,6 +22,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" +import PriceDetailsTable from "./PriceDetailsTable" + import styles from "./ui.module.css" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" @@ -62,15 +64,14 @@ export default function SummaryUI({ : null } + const roomOneGuest = rooms[0].room.guest const showSignupPromo = rooms.length === 1 && - rooms - .slice(0, 1) - .some( - (r) => !isMember || !r.room.guest.join || !r.room.guest.membershipNo - ) + !isMember && + !roomOneGuest.membershipNo && + !roomOneGuest.join - const memberPrice = getMemberPrice(rooms[0].room.roomRate) + const roomOneMemberPrice = getMemberPrice(rooms[0].room.roomRate) return (
@@ -120,9 +121,12 @@ export default function SummaryUI({ const memberPrice = getMemberPrice(room.roomRate) const isFirstRoomMember = roomNumber === 1 && isMember - const showMemberPrice = - !!(isFirstRoomMember || room.guest.join || room.guest.membershipNo) && - memberPrice + const isOrWillBecomeMember = !!( + room.guest.join || + room.guest.membershipNo || + isFirstRoomMember + ) + const showMemberPrice = !!(isOrWillBecomeMember && memberPrice) const adultsMsg = intl.formatMessage( { id: "{totalAdults, plural, one {# adult} other {# adults}}" }, @@ -160,11 +164,17 @@ export default function SummaryUI({
{room.roomType} - {formatPrice( - intl, - room.roomPrice.perStay.local.price, - room.roomPrice.perStay.local.currency - )} + {showMemberPrice + ? formatPrice( + intl, + memberPrice.amount, + memberPrice.currency + ) + : formatPrice( + intl, + room.roomPrice.perStay.local.price, + room.roomPrice.perStay.local.currency + )}
@@ -361,22 +371,17 @@ export default function SummaryUI({ { b: (str) => {str} } )} - ({ - adults: r.room.adults, - bedType: r.room.bedType, - breakfast: r.room.breakfast, - childrenInRoom: r.room.childrenInRoom, - roomFeatures: r.room.roomFeatures, - roomPrice: r.room.roomPrice, - roomType: r.room.roomType, - }))} - totalPrice={totalPrice} - vat={vat} - bookingCode={booking.bookingCode} - /> + + r.room)} + toDate={booking.toDate} + totalPrice={totalPrice} + vat={vat} + /> +
@@ -419,8 +424,11 @@ export default function SummaryUI({ )}
- {showSignupPromo && memberPrice && !isMember ? ( - + {showSignupPromo && roomOneMemberPrice && !isMember ? ( + ) : null}
) diff --git a/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx b/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx index b088911cb..d8486a428 100644 --- a/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx @@ -34,7 +34,7 @@ import type { HotelCardProps } from "@/types/components/hotelReservation/selectH import type { Lang } from "@/constants/languages" function HotelCard({ - hotel, + hotelData: { availability, hotel }, isUserLoggedIn, state = "default", type = HotelCardListingTypeEnum.PageListing, @@ -45,36 +45,27 @@ function HotelCard({ const intl = useIntl() const { setActiveHotelPin, setActiveHotelCard } = useHotelsMapStore() - const { hotelData } = hotel - const { price } = hotel - const handleMouseEnter = useCallback(() => { - if (hotelData) { - setActiveHotelPin(hotelData.name) - } - }, [setActiveHotelPin, hotelData]) + setActiveHotelPin(hotel.name) + }, [setActiveHotelPin, hotel]) const handleMouseLeave = useCallback(() => { - if (hotelData) { - setActiveHotelPin(null) - setActiveHotelCard(null) - } - }, [setActiveHotelPin, hotelData, setActiveHotelCard]) + setActiveHotelPin(null) + setActiveHotelCard(null) + }, [setActiveHotelPin, setActiveHotelCard]) - if (!hotel || !hotelData) return null - - const amenities = hotelData.detailedFacilities.slice(0, 5) + const amenities = hotel.detailedFacilities.slice(0, 5) const classNames = hotelCardVariants({ type, state, }) - const addressStr = `${hotelData.address.streetAddress}, ${hotelData.address.city}` - const galleryImages = mapApiImagesToGalleryImages( - hotelData.galleryImages || [] - ) - const fullPrice = hotel.price?.public?.rateType?.toLowerCase() === "regular" + const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}` + const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || []) + const fullPrice = + availability.productType?.public?.rateType?.toLowerCase() === "regular" + const price = availability.productType return (
- - {hotelData.ratings?.tripAdvisor && ( - + + {hotel.ratings?.tripAdvisor && ( + )}
- + - {hotelData.name} + {hotel.name}
@@ -107,7 +95,7 @@ function HotelCard({
- {hotelData.hotelContent.texts.descriptions?.short} + {hotel.hotelContent.texts.descriptions?.short}
{amenities.map((facility) => { @@ -155,13 +143,13 @@ function HotelCard({
- {!price ? ( + {!availability.productType ? ( ) : ( <> @@ -174,18 +162,18 @@ function HotelCard({ )} {(!isUserLoggedIn || - !price.member || + !price?.member || (bookingCode && !fullPrice)) && - price.public && ( + price?.public && ( )} - {price.member && ( + {availability.productType.member && ( )} - {price.redemption && ( + {price?.redemption && (
{intl.formatMessage({ id: "Available rates" })} @@ -210,7 +198,7 @@ function HotelCard({ className={styles.button} > diff --git a/apps/scandic-web/components/HotelReservation/HotelCardDialogListing/index.tsx b/apps/scandic-web/components/HotelReservation/HotelCardDialogListing/index.tsx index e9b07202c..4354841d8 100644 --- a/apps/scandic-web/components/HotelReservation/HotelCardDialogListing/index.tsx +++ b/apps/scandic-web/components/HotelReservation/HotelCardDialogListing/index.tsx @@ -19,7 +19,9 @@ export default function HotelCardDialogListing({ hotels, }: HotelCardDialogListingProps) { const intl = useIntl() - const isRedemption = hotels?.find((hotel) => hotel.price?.redemption) + const isRedemption = hotels?.find( + (hotel) => hotel.availability.productType?.redemption + ) const currencyValue = isRedemption ? intl.formatMessage({ id: "Points" }) : undefined diff --git a/apps/scandic-web/components/HotelReservation/HotelCardDialogListing/utils.ts b/apps/scandic-web/components/HotelReservation/HotelCardDialogListing/utils.ts index 6da69f679..3bcc750c1 100644 --- a/apps/scandic-web/components/HotelReservation/HotelCardDialogListing/utils.ts +++ b/apps/scandic-web/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -1,45 +1,42 @@ -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" +import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" export function getHotelPins( - hotels: HotelData[], + hotels: HotelResponse[], currencyValue?: string ): HotelPin[] { - if (hotels.length === 0) return [] + if (!hotels.length) { + return [] + } - return hotels - .filter((hotel) => hotel.hotelData) - .map((hotel) => ({ + return hotels.map(({ availability, hotel }) => { + const productType = availability.productType + return { coordinates: { - lat: hotel.hotelData.location.latitude, - lng: hotel.hotelData.location.longitude, + lat: hotel.location.latitude, + lng: hotel.location.longitude, }, - name: hotel.hotelData.name, - publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null, - memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null, - redemptionPrice: - hotel.price?.redemption?.localPrice.pointsPerNight ?? null, + name: hotel.name, + publicPrice: productType?.public?.localPrice.pricePerNight ?? null, + memberPrice: productType?.member?.localPrice.pricePerNight ?? null, + redemptionPrice: productType?.redemption?.localPrice.pointsPerNight ?? null, rateType: - hotel.price?.public?.rateType ?? hotel.price?.member?.rateType ?? null, + productType?.public?.rateType ?? productType?.member?.rateType ?? null, currency: - hotel.price?.public?.localPrice.currency || - hotel.price?.member?.localPrice.currency || + productType?.public?.localPrice.currency || + productType?.member?.localPrice.currency || currencyValue || "N/A", - images: [ - hotel.hotelData.hotelContent.images, - ...(hotel.hotelData.gallery?.heroImages ?? []), - ], - amenities: hotel.hotelData.detailedFacilities + images: [hotel.hotelContent.images, ...(hotel.gallery?.heroImages ?? [])], + amenities: hotel.detailedFacilities .map((facility) => ({ ...facility, icon: facility.icon ?? "None", })) .slice(0, 5), - ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, - operaId: hotel.hotelData.operaId, - facilityIds: hotel.hotelData.detailedFacilities.map( - (facility) => facility.id - ), - })) + ratings: hotel.ratings?.tripAdvisor.rating ?? null, + operaId: hotel.operaId, + facilityIds: hotel.detailedFacilities.map((facility) => facility.id), + } + }) } diff --git a/apps/scandic-web/components/HotelReservation/HotelCardListing/index.tsx b/apps/scandic-web/components/HotelReservation/HotelCardListing/index.tsx index 1c75c96d7..339e1ae65 100644 --- a/apps/scandic-web/components/HotelReservation/HotelCardListing/index.tsx +++ b/apps/scandic-web/components/HotelReservation/HotelCardListing/index.tsx @@ -47,61 +47,66 @@ export default function HotelCardListing({ (state) => state.activeCodeFilter ) - const sortedHotels = useMemo(() => { - if (!hotelData) return [] - return getSortedHotels({ hotels: hotelData, sortBy, bookingCode }) - }, [hotelData, sortBy, bookingCode]) - const hotels = useMemo(() => { + const sortedHotels = getSortedHotels({ + hotels: hotelData, + sortBy, + bookingCode, + }) const updatedHotelsList = bookingCode ? sortedHotels.filter( (hotel) => - !hotel.price || + !hotel.availability.productType || activeCodeFilter === BookingCodeFilterEnum.All || (activeCodeFilter === BookingCodeFilterEnum.Discounted && - hotel.price?.public?.rateType !== RateTypeEnum.Regular) || + hotel.availability.productType.public?.rateType !== + RateTypeEnum.Regular) || (activeCodeFilter === BookingCodeFilterEnum.Regular && - hotel.price?.public?.rateType === RateTypeEnum.Regular) + hotel.availability.productType.public?.rateType === + RateTypeEnum.Regular) ) : sortedHotels - if (activeFilters.length === 0) return updatedHotelsList + if (!activeFilters.length) { + return updatedHotelsList + } return updatedHotelsList.filter((hotel) => activeFilters.every((appliedFilterId) => - hotel.hotelData.detailedFacilities.some( + hotel.hotel.detailedFacilities.some( (facility) => facility.id.toString() === appliedFilterId ) ) ) - }, [activeFilters, sortedHotels, bookingCode, activeCodeFilter]) + }, [activeCodeFilter, activeFilters, bookingCode, hotelData, sortBy]) useEffect(() => { - setResultCount(hotels?.length ?? 0) + setResultCount(hotels.length) }, [hotels, setResultCount]) return (
- {hotels?.length ? ( - hotels.map((hotel) => ( -
- ( +
-
- )) - ) : activeFilters ? ( + > + +
+ )) + : null} + {!hotels?.length && activeFilters ? ( - hotel.price?.member?.localPrice?.pricePerNight ?? - hotel.price?.public?.localPrice?.pricePerNight ?? - hotel.price?.redemption?.localPrice?.pointsPerNight ?? - Infinity - const availableHotels = hotels.filter((hotel) => !!hotel?.price) - const unAvailableHotels = hotels.filter((hotel) => !hotel?.price) + const availableHotels = hotels.filter( + (hotel) => !!hotel.availability.productType + ) + const unavailableHotels = hotels.filter( + (hotel) => !hotel.availability.productType + ) const sortingStrategies: Record< string, - (a: HotelData, b: HotelData) => number + (a: HotelResponse, b: HotelResponse) => number > = { - [SortOrder.Name]: (a: HotelData, b: HotelData) => - a.hotelData.name.localeCompare(b.hotelData.name), - [SortOrder.TripAdvisorRating]: (a: HotelData, b: HotelData) => - (b.hotelData.ratings?.tripAdvisor.rating ?? 0) - - (a.hotelData.ratings?.tripAdvisor.rating ?? 0), - [SortOrder.Price]: (a: HotelData, b: HotelData) => + [SortOrder.Name]: (a: HotelResponse, b: HotelResponse) => + a.hotel.name.localeCompare(b.hotel.name), + [SortOrder.TripAdvisorRating]: (a: HotelResponse, b: HotelResponse) => + (b.hotel.ratings?.tripAdvisor.rating ?? 0) - + (a.hotel.ratings?.tripAdvisor.rating ?? 0), + [SortOrder.Price]: (a: HotelResponse, b: HotelResponse) => getPricePerNight(a) - getPricePerNight(b), - [SortOrder.Distance]: (a: HotelData, b: HotelData) => - a.hotelData.location.distanceToCentre - - b.hotelData.location.distanceToCentre, + [SortOrder.Distance]: (a: HotelResponse, b: HotelResponse) => + a.hotel.location.distanceToCentre - b.hotel.location.distanceToCentre, } const sortStrategy = @@ -40,21 +46,25 @@ export function getSortedHotels({ if (bookingCode) { const bookingCodeHotels = hotels.filter( (hotel) => - (hotel?.price?.public?.rateType?.toLowerCase() !== "regular" || - hotel?.price?.member?.rateType?.toLowerCase() !== "regular") && - !!hotel?.price + (hotel.availability.productType?.public?.rateType?.toLowerCase() !== + "regular" || + hotel.availability.productType?.member?.rateType?.toLowerCase() !== + "regular") && + !!hotel.availability.productType ) const regularHotels = hotels.filter( - (hotel) => hotel?.price?.public?.rateType?.toLowerCase() === "regular" + (hotel) => + hotel.availability.productType?.public?.rateType?.toLowerCase() === + "regular" ) - return [...bookingCodeHotels] + return bookingCodeHotels .sort(sortStrategy) - .concat([...regularHotels].sort(sortStrategy)) - .concat([...unAvailableHotels].sort(sortStrategy)) + .concat(regularHotels.sort(sortStrategy)) + .concat(unavailableHotels.sort(sortStrategy)) } - return [...availableHotels] + return availableHotels .sort(sortStrategy) - .concat([...unAvailableHotels].sort(sortStrategy)) + .concat(unavailableHotels.sort(sortStrategy)) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/index.tsx new file mode 100644 index 000000000..1f484fc4c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/index.tsx @@ -0,0 +1,187 @@ +"use client" + +import { Fragment } from "react" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import { PriceTagIcon } from "@/components/Icons" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import useLang from "@/hooks/useLang" +import { formatPrice } from "@/utils/numberFormatting" + +import styles from "./priceDetailsTable.module.css" + +import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details" +import type { Price } from "@/types/components/hotelReservation/price" +import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" + +function Row({ + label, + value, + bold, +}: { + label: string + value: string + bold?: boolean +}) { + return ( + + + {label} + + + {value} + + + ) +} + +function TableSection({ children }: React.PropsWithChildren) { + return {children} +} + +function TableSectionHeader({ + title, + subtitle, +}: { + title: string + subtitle?: string +}) { + return ( + + + {title} + {subtitle ? {subtitle} : null} + + + ) +} + +interface Room { + adults: number + childrenInRoom: Child[] | undefined + roomPrice: RoomPrice + roomType: string +} + +export interface PriceDetailsTableProps { + bookingCode?: string | null + fromDate: string + rooms: Room[] + toDate: string + totalPrice: Price + vat: number +} + +export default function PriceDetailsTable({ + bookingCode, + fromDate, + rooms, + toDate, + totalPrice, + vat, +}: PriceDetailsTableProps) { + const intl = useIntl() + const lang = useLang() + + const diff = dt(toDate).diff(fromDate, "days") + const nights = intl.formatMessage( + { id: "{totalNights, plural, one {# night} other {# nights}}" }, + { totalNights: diff } + ) + const vatPercentage = vat / 100 + const vatAmount = totalPrice.local.price * vatPercentage + + const priceExclVat = totalPrice.local.price - vatAmount + + const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")} + - + ${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})` + return ( + + {rooms.map((room, idx) => { + return ( + + + {rooms.length > 1 && ( + + {intl.formatMessage({ id: "Room" })} {idx + 1} + + )} + + + + + + ) + })} + + + + + + + + + {totalPrice.local.regularPrice && ( + + + + + + )} + {bookingCode && totalPrice.local.regularPrice && ( + + + + + )} + +
+ + {intl.formatMessage({ id: "Price including VAT" })} + + + + {formatPrice( + intl, + totalPrice.local.price, + totalPrice.local.currency + )} + +
+
+ {formatPrice( + intl, + totalPrice.local.regularPrice, + totalPrice.local.currency + )} +
+ + {bookingCode} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/priceDetailsTable.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/priceDetailsTable.module.css new file mode 100644 index 000000000..284e9ac5a --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/priceDetailsTable.module.css @@ -0,0 +1,36 @@ +.priceDetailsTable { + border-collapse: collapse; + width: 100%; +} + +.price { + text-align: end; +} + +.tableSection { + display: flex; + gap: var(--Spacing-x-half); + flex-direction: column; + width: 100%; +} + +.tableSection:has(tr > th) { + padding-top: var(--Spacing-x2); +} + +.tableSection:has(tr > th):not(:first-of-type) { + border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); +} + +.tableSection:not(:last-child) { + padding-bottom: var(--Spacing-x2); +} +.row { + display: flex; + justify-content: space-between; +} +@media screen and (min-width: 768px) { + .priceDetailsTable { + min-width: 512px; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx index a37ea5bd3..98248792e 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx @@ -4,6 +4,7 @@ import { useIntl } from "react-intl" import { dt } from "@/lib/dt" +import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal" import { getIconForFeatureCode } from "@/components/HotelReservation/utils" import { BedDoubleIcon, @@ -22,8 +23,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" -import PriceDetailsModal from "../../PriceDetailsModal" import GuestDetails from "./GuestDetails" +import PriceDetailsTable from "./PriceDetailsTable" import ToggleSidePeek from "./ToggleSidePeek" import styles from "./room.module.css" @@ -287,41 +288,44 @@ export function Room({ booking, room, hotel, user }: RoomProps) {
- + + ]} + toDate={dt(booking.checkOutDate).format("YYYY-MM-DD")} + totalPrice={{ + requested: undefined, + local: { + currency: booking.currencyCode, + price: booking.totalPrice, + }, + }} + vat={booking.vatPercentage} + /> +
diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal.tsx new file mode 100644 index 000000000..02f7bd5bf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal.tsx @@ -0,0 +1,29 @@ +"use client" +import { useIntl } from "react-intl" + +import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall" +import Modal from "@/components/Modal" +import Button from "@/components/TempDesignSystem/Button" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +export default function PriceDetailsModal({ + children, +}: React.PropsWithChildren) { + const intl = useIntl() + + return ( + + + {intl.formatMessage({ id: "Price details" })} + + + + } + > + {children} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/index.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/index.tsx deleted file mode 100644 index d99817f35..000000000 --- a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall" -import Modal from "@/components/Modal" -import Button from "@/components/TempDesignSystem/Button" -import Caption from "@/components/TempDesignSystem/Text/Caption" - -import PriceDetailsTable from "./PriceDetailsTable" - -import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" -import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details" -import type { Price } from "@/types/components/hotelReservation/price" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { Packages } from "@/types/requests/packages" - -interface PriceDetailsModalProps { - fromDate: string - toDate: string - rooms: { - adults: number - childrenInRoom: Child[] | undefined - roomType: string - roomPrice: RoomPrice - bedType?: BedTypeSchema - breakfast?: BreakfastPackage | false - roomFeatures?: Packages | null - }[] - totalPrice: Price - vat: number - bookingCode?: string -} - -export default function PriceDetailsModal({ - fromDate, - toDate, - rooms, - totalPrice, - vat, - bookingCode, -}: PriceDetailsModalProps) { - const intl = useIntl() - - return ( - - - {intl.formatMessage({ id: "Price details" })} - - - - } - > - - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/NoAvailabilityAlert.tsx b/apps/scandic-web/components/HotelReservation/SelectHotel/NoAvailabilityAlert.tsx index 0fed2badb..1db5a49b7 100644 --- a/apps/scandic-web/components/HotelReservation/SelectHotel/NoAvailabilityAlert.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectHotel/NoAvailabilityAlert.tsx @@ -8,9 +8,10 @@ import type { NoAvailabilityAlertProp } from "@/types/components/hotelReservatio import { AlertTypeEnum } from "@/types/enums/alert" export default async function NoAvailabilityAlert({ - hotels, + hotelsLength, isAllUnavailable, isAlternative, + operaId, }: NoAvailabilityAlertProp) { const intl = await getIntl() const lang = getLang() @@ -19,7 +20,7 @@ export default async function NoAvailabilityAlert({ return null } - if (hotels.length === 1 && !isAlternative) { + if (hotelsLength === 1 && !isAlternative && operaId) { return ( diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx index 4e22600ef..a4aa483aa 100644 --- a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx @@ -1,40 +1,26 @@ -import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { notFound } from "next/navigation" import { env } from "@/env/server" import { getCityCoordinates } from "@/lib/trpc/memoizedRequests" -import { - fetchAlternativeHotels, - fetchAvailableHotels, - fetchBookingCodeAvailableHotels, - getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" -import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils" import TrackingSDK from "@/components/TrackingSDK" -import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { getHotelSearchDetails } from "@/utils/hotelSearchDetails" import { safeTry } from "@/utils/safeTry" import { getHotelPins } from "../../HotelCardDialogListing/utils" +import { getFiltersFromHotels, getHotels } from "../helpers" +import { getTracking } from "./tracking" import SelectHotelMap from "." -import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" -import { - TrackingChannelEnum, - type TrackingSDKHotelInfo, - type TrackingSDKPageData, -} from "@/types/components/tracking" export async function SelectHotelMapContainer({ searchParams, isAlternativeHotels, }: SelectHotelMapContainerProps) { const lang = getLang() - const intl = await getIntl() const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY const getHotelSearchDetailsPromise = safeTry( @@ -50,106 +36,58 @@ export async function SelectHotelMapContainer({ const [searchDetails] = await getHotelSearchDetailsPromise - if (!searchDetails) return notFound() + if (!searchDetails) { + return notFound() + } const { - city, - selectHotelParams, adultsInRoom, - childrenInRoom, - childrenInRoomString, - hotel: isAlternativeFor, bookingCode, + childrenInRoom, + city, + hotel: isAlternativeFor, + noOfRooms, redemption, + selectHotelParams, } = searchDetails - if (!city) return notFound() + if (!city) { + return notFound() + } - const fetchAvailableHotelsPromise = isAlternativeFor - ? safeTry( - fetchAlternativeHotels(isAlternativeFor.id, { - roomStayStartDate: selectHotelParams.fromDate, - roomStayEndDate: selectHotelParams.toDate, - adults: adultsInRoom[0], - children: childrenInRoomString, - bookingCode, - redemption, - }) - ) - : bookingCode - ? safeTry( - fetchBookingCodeAvailableHotels({ - cityId: city.id, - roomStayStartDate: selectHotelParams.fromDate, - roomStayEndDate: selectHotelParams.toDate, - adults: adultsInRoom[0], - children: childrenInRoomString, - bookingCode, - }) - ) - : safeTry( - fetchAvailableHotels({ - cityId: city.id, - roomStayStartDate: selectHotelParams.fromDate, - roomStayEndDate: selectHotelParams.toDate, - adults: adultsInRoom[0], - children: childrenInRoomString, - redemption, - }) - ) + const hotels = await getHotels( + selectHotelParams, + isAlternativeFor, + bookingCode, + city, + !!redemption + ) - const [hotels] = await fetchAvailableHotelsPromise - - const validHotels = (hotels?.filter(Boolean) as HotelData[]) || [] - - const currencyValue = redemption - ? intl.formatMessage({ id: "Points" }) - : undefined - const hotelPins = getHotelPins(validHotels, currencyValue) - const filterList = getFiltersFromHotels(validHotels) + const hotelPins = getHotelPins(hotels) + const filterList = getFiltersFromHotels(hotels) const cityCoordinates = await getCityCoordinates({ city: city.name, - hotel: { address: hotels?.[0]?.hotelData?.address.streetAddress }, + hotel: { address: hotels?.[0]?.hotel?.address.streetAddress }, }) const arrivalDate = new Date(selectHotelParams.fromDate) const departureDate = new Date(selectHotelParams.toDate) - const pageTrackingData: TrackingSDKPageData = { - pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel", - domainLanguage: lang, - channel: TrackingChannelEnum["hotelreservation"], - pageName: isAlternativeHotels - ? "hotelreservation|alternative-hotels|mapview" - : "hotelreservation|select-hotel|mapview", - siteSections: isAlternativeHotels - ? "hotelreservation|altervative-hotels|mapview" - : "hotelreservation|select-hotel|mapview", - pageType: "bookinghotelsmapviewpage", - siteVersion: "new-web", - } - - const hotelsTrackingData: TrackingSDKHotelInfo = { - availableResults: validHotels.length, - searchTerm: isAlternativeFor - ? selectHotelParams.hotelId - : (selectHotelParams.city as string), - arrivalDate: format(arrivalDate, "yyyy-MM-dd"), - departureDate: format(departureDate, "yyyy-MM-dd"), - noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms - noOfChildren: childrenInRoom?.length, - ageOfChildren: childrenInRoom?.map((c) => c.age).join(","), - childBedPreference: childrenInRoom - ?.map((c) => ChildBedMapEnum[c.bed]) - .join("|"), - noOfRooms: 1, // // TODO: Handle multiple rooms - duration: differenceInCalendarDays(departureDate, arrivalDate), - leadTime: differenceInCalendarDays(arrivalDate, new Date()), - searchType: "destination", - bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - country: validHotels?.[0]?.hotelData.address.country, - region: validHotels?.[0]?.hotelData.address.city, - } + const { hotelsTrackingData, pageTrackingData } = getTracking( + lang, + !!isAlternativeFor, + !!isAlternativeHotels, + arrivalDate, + departureDate, + adultsInRoom, + childrenInRoom, + hotels.length, + selectHotelParams.hotelId, + noOfRooms, + hotels?.[0]?.hotel.address.country, + hotels?.[0]?.hotel.address.city, + selectHotelParams.city + ) return ( <> @@ -157,7 +95,7 @@ export async function SelectHotelMapContainer({ apiKey={googleMapsApiKey} hotelPins={hotelPins} mapId={googleMapId} - hotels={validHotels} + hotels={hotels} filterList={filterList} cityCoordinates={cityCoordinates} bookingCode={bookingCode ?? ""} diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx index 6e8db972f..49923cd55 100644 --- a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx @@ -26,10 +26,10 @@ import { getVisibleHotels } from "./utils" import styles from "./selectHotelMapContent.module.css" -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map" import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter" import { RateTypeEnum } from "@/types/enums/rateType" +import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" const SKELETON_LOAD_DELAY = 750 @@ -46,7 +46,7 @@ export default function SelectHotelContent({ const map = useMap() const isAboveMobile = useMediaQuery("(min-width: 768px)") - const [visibleHotels, setVisibleHotels] = useState([]) + const [visibleHotels, setVisibleHotels] = useState([]) const [showSkeleton, setShowSkeleton] = useState(true) const listingContainerRef = useRef(null) diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts index 8dcba2169..2ee2d324e 100644 --- a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts @@ -1,5 +1,5 @@ -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" +import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" export function getVisibleHotelPins( map: google.maps.Map | null, @@ -17,13 +17,13 @@ export function getVisibleHotelPins( } export function getVisibleHotels( - hotels: HotelData[], + hotels: HotelResponse[], filteredHotelPins: HotelPin[], map: google.maps.Map | null ) { const visibleHotelPins = getVisibleHotelPins(map, filteredHotelPins) const visibleHotels = hotels.filter((hotel) => - visibleHotelPins.some((pin) => pin.operaId === hotel.hotelData.operaId) + visibleHotelPins.some((pin) => pin.operaId === hotel.hotel.operaId) ) return visibleHotels } diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/tracking.ts b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/tracking.ts new file mode 100644 index 000000000..e955bd77f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/tracking.ts @@ -0,0 +1,67 @@ +import { differenceInCalendarDays, format, isWeekend } from "date-fns" + +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" +import { + TrackingChannelEnum, + type TrackingSDKHotelInfo, + type TrackingSDKPageData, +} from "@/types/components/tracking" +import type { Lang } from "@/constants/languages" +import type { ChildrenInRoom } from "@/utils/hotelSearchDetails" + +export function getTracking( + lang: Lang, + isAlternativeFor: boolean, + isAlternativeHotels: boolean, + arrivalDate: Date, + departureDate: Date, + adultsInRoom: number[], + childrenInRoom: ChildrenInRoom, + hotelsResult: number, + hotelId: string, + noOfRooms: number, + country: string | undefined, + hotelCity: string | undefined, + paramCity: string | undefined +) { + const pageTrackingData: TrackingSDKPageData = { + channel: TrackingChannelEnum["hotelreservation"], + domainLanguage: lang, + pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel", + pageName: isAlternativeHotels + ? "hotelreservation|alternative-hotels|mapview" + : "hotelreservation|select-hotel|mapview", + pageType: "bookinghotelsmapviewpage", + siteSections: isAlternativeHotels + ? "hotelreservation|altervative-hotels|mapview" + : "hotelreservation|select-hotel|mapview", + siteVersion: "new-web", + } + + const hotelsTrackingData: TrackingSDKHotelInfo = { + ageOfChildren: childrenInRoom + ?.map((c) => c?.map((k) => k.age).join(",") ?? "-") + .join("|"), + arrivalDate: format(arrivalDate, "yyyy-MM-dd"), + availableResults: hotelsResult, + bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", + childBedPreference: childrenInRoom + ?.map((c) => c?.map((k) => ChildBedMapEnum[k.bed]).join(",") ?? "-") + .join("|"), + country, + departureDate: format(departureDate, "yyyy-MM-dd"), + duration: differenceInCalendarDays(departureDate, arrivalDate), + leadTime: differenceInCalendarDays(arrivalDate, new Date()), + noOfAdults: adultsInRoom.join(","), + noOfChildren: childrenInRoom?.map((kids) => kids?.length ?? 0).join(","), + noOfRooms, + region: hotelCity, + searchTerm: isAlternativeFor ? hotelId : (paramCity as string), + searchType: "destination", + } + + return { + hotelsTrackingData, + pageTrackingData, + } +} diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts b/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts new file mode 100644 index 000000000..7881b2e51 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts @@ -0,0 +1,270 @@ +import { getHotel } from "@/lib/trpc/memoizedRequests" +import { serverClient } from "@/lib/trpc/server" + +import { getLang } from "@/i18n/serverContext" + +import { generateChildrenString } from "../utils" + +import type { + AlternativeHotelsAvailabilityInput, + AvailabilityInput, +} from "@/types/components/hotelReservation/selectHotel/availabilityInput" +import type { CategorizedFilters } from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" +import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" +import type { DetailedFacility, Hotel } from "@/types/hotel" +import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability" +import type { + HotelLocation, + Location, +} from "@/types/trpc/routers/hotel/locations" + +interface AvailabilityResponse { + availability: HotelsAvailabilityItem[] +} + +export interface HotelResponse { + availability: HotelsAvailabilityItem + hotel: Hotel +} + +type Result = AvailabilityResponse | null +type SettledResult = PromiseSettledResult[] + +async function enhanceHotels(hotels: HotelsAvailabilityItem[]) { + const language = getLang() + return await Promise.allSettled( + hotels.map(async (availability) => { + const hotelData = await getHotel({ + hotelId: availability.hotelId.toString(), + isCardOnlyPayment: false, + language, + }) + + if (!hotelData) { + return null + } + + return { + availability, + hotel: hotelData.hotel, + } + }) + ) +} + +async function fetchAlternativeHotels( + hotelId: string, + input: AlternativeHotelsAvailabilityInput +) { + const alternativeHotelIds = await serverClient().hotel.nearbyHotelIds({ + hotelId, + }) + + if (!alternativeHotelIds) { + return null + } + + return await serverClient().hotel.availability.hotelsByHotelIds({ + ...input, + hotelIds: alternativeHotelIds, + }) +} + +async function fetchAvailableHotels(input: AvailabilityInput) { + return await serverClient().hotel.availability.hotelsByCity(input) +} + +async function fetchBookingCodeAvailableHotels(input: AvailabilityInput) { + return await serverClient().hotel.availability.hotelsByCityWithBookingCode( + input + ) +} + +function getFulfilledResponses(result: PromiseSettledResult[]) { + const fulfilledResponses: NonNullable[] = [] + for (const res of result) { + if (res.status === "fulfilled" && res.value) { + fulfilledResponses.push(res.value) + } + } + return fulfilledResponses +} + +function getHotelAvailabilityItems(hotels: AvailabilityResponse[]) { + return hotels.map((hotel) => hotel.availability) +} + +// Filter out hotels that are unavailable for +// at least one room. +function sortAndFilterHotelsByAvailability( + fulfilledHotels: HotelsAvailabilityItem[][] +) { + const availableHotels = new Map< + HotelsAvailabilityItem["hotelId"], + HotelsAvailabilityItem + >() + const unavailableHotels = new Map< + HotelsAvailabilityItem["hotelId"], + HotelsAvailabilityItem + >() + const unavailableHotelIds = new Set() + + for (const availabilityHotels of fulfilledHotels) { + for (const hotel of availabilityHotels) { + if (hotel.status === AvailabilityEnum.Available) { + if (availableHotels.has(hotel.hotelId)) { + const currentAddedHotel = availableHotels.get(hotel.hotelId) + // Make sure the cheapest version of the room is the one + // we keep so that it matches the cheapest room on select-rate + if ( + (hotel.productType?.public && + currentAddedHotel?.productType?.public && + hotel.productType.public.localPrice.pricePerNight < + currentAddedHotel.productType.public.localPrice + .pricePerNight) || + (hotel.productType?.member && + currentAddedHotel?.productType?.member && + hotel.productType.member.localPrice.pricePerNight < + currentAddedHotel.productType.member.localPrice.pricePerNight) + ) { + availableHotels.set(hotel.hotelId, hotel) + } + } else { + availableHotels.set(hotel.hotelId, hotel) + } + } else { + unavailableHotels.set(hotel.hotelId, hotel) + unavailableHotelIds.add(hotel.hotelId) + } + } + } + + for (const [hotelId] of unavailableHotelIds.entries()) { + if (availableHotels.has(hotelId)) { + availableHotels.delete(hotelId) + } + } + + return [ + Array.from(availableHotels.values()), + Array.from(unavailableHotels.values()), + ].flat() +} + +export async function getHotels( + booking: SelectHotelSearchParams, + isAlternativeFor: HotelLocation | null, + bookingCode: string | undefined, + city: Location, + redemption: boolean +) { + let availableHotelsResponse: SettledResult = [] + if (isAlternativeFor) { + availableHotelsResponse = await Promise.allSettled( + booking.rooms.map(async (room) => { + return fetchAlternativeHotels(isAlternativeFor.id, { + adults: room.adults, + bookingCode, + children: room.childrenInRoom + ? generateChildrenString(room.childrenInRoom) + : undefined, + redemption, + roomStayEndDate: booking.toDate, + roomStayStartDate: booking.fromDate, + }) + }) + ) + } else if (bookingCode) { + availableHotelsResponse = await Promise.allSettled( + booking.rooms.map(async (room) => { + return fetchBookingCodeAvailableHotels({ + adults: room.adults, + bookingCode, + children: room.childrenInRoom + ? generateChildrenString(room.childrenInRoom) + : undefined, + cityId: city.id, + roomStayStartDate: booking.fromDate, + roomStayEndDate: booking.toDate, + }) + }) + ) + } else { + availableHotelsResponse = await Promise.allSettled( + booking.rooms.map( + async (room) => + await fetchAvailableHotels({ + adults: room.adults, + children: room.childrenInRoom + ? generateChildrenString(room.childrenInRoom) + : undefined, + cityId: city.id, + redemption, + roomStayEndDate: booking.toDate, + roomStayStartDate: booking.fromDate, + }) + ) + ) + } + + const fulfilledAvailabilities = getFulfilledResponses( + availableHotelsResponse + ) + const availablilityItems = getHotelAvailabilityItems(fulfilledAvailabilities) + const availableHotels = sortAndFilterHotelsByAvailability(availablilityItems) + if (!availableHotels.length) { + return [] + } + const hotelsResponse = await enhanceHotels(availableHotels) + const hotels = getFulfilledResponses(hotelsResponse) + + return hotels +} + +const hotelSurroundingsFilterNames = [ + "Hotel surroundings", + "Hotel omgivelser", + "Hotelumgebung", + "Hotellia lähellä", + "Hotellomgivelser", + "Omgivningar", +] + +const hotelFacilitiesFilterNames = [ + "Hotel facilities", + "Hotellfaciliteter", + "Hotelfaciliteter", + "Hotel faciliteter", + "Hotel-Infos", + "Hotellin palvelut", +] + +export function getFiltersFromHotels( + hotels: HotelResponse[] +): CategorizedFilters { + const defaultFilters = { facilityFilters: [], surroundingsFilters: [] } + if (!hotels.length) { + return defaultFilters + } + + const filters = hotels.flatMap(({ hotel }) => hotel.detailedFacilities) + + const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))] + const filterList: DetailedFacility[] = uniqueFilterIds + .map((filterId) => filters.find((filter) => filter.id === filterId)) + .filter((filter): filter is DetailedFacility => filter !== undefined) + .sort((a, b) => b.sortOrder - a.sortOrder) + + return filterList.reduce((filters, filter) => { + if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) { + filters.surroundingsFilters.push(filter) + } + + if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) { + filters.facilityFilters.push(filter) + } + + return filters + }, defaultFilters) +} diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/index.tsx b/apps/scandic-web/components/HotelReservation/SelectHotel/index.tsx index 79cfb5867..d9b974039 100644 --- a/apps/scandic-web/components/HotelReservation/SelectHotel/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectHotel/index.tsx @@ -1,4 +1,4 @@ -import { differenceInCalendarDays, format, isWeekend } from "date-fns" +import stringify from "json-stable-stringify-without-jsonify" import { notFound } from "next/navigation" import { Suspense } from "react" @@ -9,13 +9,6 @@ import { selectHotelMap, } from "@/constants/routes/hotelReservation" -import { - fetchAlternativeHotels, - fetchAvailableHotels, - fetchBookingCodeAvailableHotels, - getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" -import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils" import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs" @@ -24,28 +17,23 @@ import Link from "@/components/TempDesignSystem/Link" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import TrackingSDK from "@/components/TrackingSDK" import { getIntl } from "@/i18n" -import { safeTry } from "@/utils/safeTry" +import { getHotelSearchDetails } from "@/utils/hotelSearchDetails" import { convertObjToSearchParams } from "@/utils/url" import HotelCardListing from "../HotelCardListing" import BookingCodeFilter from "./BookingCodeFilter" +import { getFiltersFromHotels, getHotels } from "./helpers" import HotelCount from "./HotelCount" import HotelFilter from "./HotelFilter" import HotelSorter from "./HotelSorter" import MobileMapButtonContainer from "./MobileMapButtonContainer" import NoAvailabilityAlert from "./NoAvailabilityAlert" +import { getTracking } from "./tracking" import styles from "./selectHotel.module.css" -import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { SelectHotelProps } from "@/types/components/hotelReservation/selectHotel/selectHotel" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" -import { - TrackingChannelEnum, - type TrackingSDKHotelInfo, - type TrackingSDKPageData, -} from "@/types/components/tracking" export default async function SelectHotel({ params, @@ -54,78 +42,44 @@ export default async function SelectHotel({ }: SelectHotelProps) { const intl = await getIntl() - const getHotelSearchDetailsPromise = safeTry( - getHotelSearchDetails( - { - searchParams: searchParams as SelectHotelSearchParams & { - [key: string]: string - }, + const searchDetails = await getHotelSearchDetails( + { + searchParams: searchParams as SelectHotelSearchParams & { + [key: string]: string }, - isAlternativeHotels - ) + }, + isAlternativeHotels ) - const [searchDetails] = await getHotelSearchDetailsPromise - if (!searchDetails) return notFound() const { - city, - selectHotelParams, adultsInRoom, - childrenInRoomString, - childrenInRoom, - hotel: isAlternativeFor, bookingCode, + childrenInRoom, + city, + hotel: isAlternativeFor, + noOfRooms, redemption, + selectHotelParams, } = searchDetails if (!city) return notFound() - const hotelsPromise = isAlternativeFor - ? safeTry( - fetchAlternativeHotels(isAlternativeFor.id, { - roomStayStartDate: selectHotelParams.fromDate, - roomStayEndDate: selectHotelParams.toDate, - adults: adultsInRoom[0], - children: childrenInRoomString, - bookingCode, - redemption, - }) - ) - : bookingCode - ? safeTry( - fetchBookingCodeAvailableHotels({ - cityId: city.id, - roomStayStartDate: selectHotelParams.fromDate, - roomStayEndDate: selectHotelParams.toDate, - adults: adultsInRoom[0], - children: childrenInRoomString, - bookingCode, - }) - ) - : safeTry( - fetchAvailableHotels({ - cityId: city.id, - roomStayStartDate: selectHotelParams.fromDate, - roomStayEndDate: selectHotelParams.toDate, - adults: adultsInRoom[0], - children: childrenInRoomString, - redemption, - }) - ) - - const [hotels] = await hotelsPromise + const hotels = await getHotels( + selectHotelParams, + isAlternativeFor, + bookingCode, + city, + !!redemption, + ) const arrivalDate = new Date(selectHotelParams.fromDate) const departureDate = new Date(selectHotelParams.toDate) const isCityWithCountry = (city: any): city is { country: string } => "country" in city - const validHotels = - hotels?.filter((hotel): hotel is HotelData => hotel?.hotelData !== null) || - [] - const filterList = getFiltersFromHotels(validHotels) + const filterList = getFiltersFromHotels(hotels) const convertedSearchParams = convertObjToSearchParams(selectHotelParams) const breadcrumbs = [ @@ -141,65 +95,44 @@ export default async function SelectHotel({ }, isAlternativeFor ? { - title: intl.formatMessage({ id: "Alternative hotels" }), - href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`, - uid: "alternative-hotels", - } + title: intl.formatMessage({ id: "Alternative hotels" }), + href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`, + uid: "alternative-hotels", + } : { - title: intl.formatMessage({ id: "Select hotel" }), - href: `${selectHotel(params.lang)}/?${convertedSearchParams}`, - uid: "select-hotel", - }, + title: intl.formatMessage({ id: "Select hotel" }), + href: `${selectHotel(params.lang)}/?${convertedSearchParams}`, + uid: "select-hotel", + }, isAlternativeFor ? { - title: isAlternativeFor.name, - uid: isAlternativeFor.id, - } + title: isAlternativeFor.name, + uid: isAlternativeFor.id, + } : { - title: city.name, - uid: city.id, - }, + title: city.name, + uid: city.id, + }, ] - const isAllUnavailable = - hotels?.every((hotel) => hotel.price === undefined) || false + const isAllUnavailable = !hotels.length - const pageTrackingData: TrackingSDKPageData = { - pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel", - domainLanguage: params.lang, - channel: TrackingChannelEnum["hotelreservation"], - pageName: isAlternativeFor - ? "hotelreservation|alternative-hotels" - : "hotelreservation|select-hotel", - siteSections: isAlternativeFor - ? "hotelreservation|alternative-hotels" - : "hotelreservation|select-hotel", - pageType: "bookinghotelspage", - siteVersion: "new-web", - } - - const hotelsTrackingData: TrackingSDKHotelInfo = { - availableResults: validHotels.length, - searchTerm: isAlternativeFor - ? selectHotelParams.hotelId - : (selectHotelParams.city as string), - arrivalDate: format(arrivalDate, "yyyy-MM-dd"), - departureDate: format(departureDate, "yyyy-MM-dd"), - noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms, - noOfChildren: childrenInRoom?.length, - ageOfChildren: childrenInRoom?.map((c) => c.age).join(","), - childBedPreference: childrenInRoom - ?.map((c) => ChildBedMapEnum[c.bed]) - .join("|"), - noOfRooms: 1, // // TODO: Handle multiple rooms - duration: differenceInCalendarDays(departureDate, arrivalDate), - leadTime: differenceInCalendarDays(arrivalDate, new Date()), - searchType: "destination", - bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - country: validHotels?.[0]?.hotelData.address.country, - region: validHotels?.[0]?.hotelData.address.city, - } + const { hotelsTrackingData, pageTrackingData } = getTracking( + params.lang, + !!isAlternativeFor, + arrivalDate, + departureDate, + adultsInRoom, + childrenInRoom, + hotels.length, + selectHotelParams.hotelId, + noOfRooms, + hotels?.[0]?.hotel.address.country, + hotels?.[0]?.hotel.address.city, + selectHotelParams.city + ) + const suspenseKey = stringify(searchParams) return ( <>
@@ -229,7 +162,7 @@ export default async function SelectHotel({
{bookingCode ? : null}
- {hotels && hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available + {hotels.length ? (
- +
- + c?.map((k) => k.age).join(",") ?? "-") + .join("|"), + arrivalDate: format(arrivalDate, "yyyy-MM-dd"), + availableResults: hotelsResult, + bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", + childBedPreference: childrenInRoom + ?.map((c) => c?.map((k) => ChildBedMapEnum[k.bed]).join(",") ?? "-") + .join("|"), + country, + departureDate: format(departureDate, "yyyy-MM-dd"), + duration: differenceInCalendarDays(departureDate, arrivalDate), + leadTime: differenceInCalendarDays(arrivalDate, new Date()), + noOfAdults: adultsInRoom.join(","), + noOfChildren: childrenInRoom?.map((kids) => kids?.length ?? 0).join(","), + noOfRooms, + region: hotelCity, + searchTerm: isAlternativeFor ? hotelId : (paramCity as string), + searchType: "destination", + } + + return { + hotelsTrackingData, + pageTrackingData, + } +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/index.tsx new file mode 100644 index 000000000..367b1dab6 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/index.tsx @@ -0,0 +1,189 @@ +"use client" + +import { Fragment } from "react" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import { PriceTagIcon } from "@/components/Icons" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import useLang from "@/hooks/useLang" +import { formatPrice } from "@/utils/numberFormatting" + +import styles from "./priceDetailsTable.module.css" + +import type { Price } from "@/types/components/hotelReservation/price" +import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary" + +function Row({ + label, + value, + bold, +}: { + label: string + value: string + bold?: boolean +}) { + return ( + + + {label} + + + {value} + + + ) +} + +function TableSection({ children }: React.PropsWithChildren) { + return {children} +} + +function TableSectionHeader({ + title, + subtitle, +}: { + title: string + subtitle?: string +}) { + return ( + + + {title} + {subtitle ? {subtitle} : null} + + + ) +} + +export interface PriceDetailsTableProps { + bookingCode?: string + fromDate: string + isMember: boolean + rooms: SelectRateSummaryProps["rooms"] + toDate: string + totalPrice: Price + vat: number +} + +export default function PriceDetailsTable({ + bookingCode, + fromDate, + isMember, + rooms, + toDate, + totalPrice, + vat, +}: PriceDetailsTableProps) { + const intl = useIntl() + const lang = useLang() + + const diff = dt(toDate).diff(fromDate, "days") + const nights = intl.formatMessage( + { id: "{totalNights, plural, one {# night} other {# nights}}" }, + { totalNights: diff } + ) + const vatPercentage = vat / 100 + const vatAmount = totalPrice.local.price * vatPercentage + + const priceExclVat = totalPrice.local.price - vatAmount + + const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")} + - + ${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})` + return ( + + {rooms.map((room, idx) => { + const getMemberRate = idx === 0 && isMember + const price = + getMemberRate && room.roomRate.memberRate + ? room.roomRate.memberRate + : room.roomRate.publicRate + if (!price) { + return null + } + return ( + + + {rooms.length > 1 && ( + + {intl.formatMessage({ id: "Room" })} {idx + 1} + + )} + + + + + + ) + })} + + + + + + + + + {totalPrice.local.regularPrice && ( + + + + + + )} + {bookingCode && totalPrice.local.regularPrice && ( + + + + + )} + +
+ + {intl.formatMessage({ id: "Price including VAT" })} + + + + {formatPrice( + intl, + totalPrice.local.price, + totalPrice.local.currency + )} + +
+
+ {formatPrice( + intl, + totalPrice.local.regularPrice, + totalPrice.local.currency + )} +
+ + {bookingCode} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/priceDetailsTable.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/priceDetailsTable.module.css new file mode 100644 index 000000000..284e9ac5a --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/priceDetailsTable.module.css @@ -0,0 +1,36 @@ +.priceDetailsTable { + border-collapse: collapse; + width: 100%; +} + +.price { + text-align: end; +} + +.tableSection { + display: flex; + gap: var(--Spacing-x-half); + flex-direction: column; + width: 100%; +} + +.tableSection:has(tr > th) { + padding-top: var(--Spacing-x2); +} + +.tableSection:has(tr > th):not(:first-of-type) { + border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); +} + +.tableSection:not(:last-child) { + padding-bottom: var(--Spacing-x2); +} +.row { + display: flex; + justify-content: space-between; +} +@media screen and (min-width: 768px) { + .priceDetailsTable { + min-width: 512px; + } +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx index 756aa4bc1..97576fcf7 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx @@ -20,6 +20,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" +import PriceDetailsTable from "./PriceDetailsTable" + import styles from "./summary.module.css" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" @@ -249,19 +251,17 @@ export default function Summary({ { b: (str) => {str} } )} - ({ - adults: r.adults, - childrenInRoom: r.childrenInRoom, - roomPrice: r.roomPrice, - roomType: r.roomType, - }))} - totalPrice={totalPrice} - vat={vat} - bookingCode={booking.bookingCode} - /> + + +
({ booking: state.booking, bookingRooms: state.booking.rooms, - rateDefinitions: state.roomsAvailability?.rateDefinitions, + roomsAvailability: state.roomsAvailability, rateSummary: state.rateSummary, vat: state.vat, })) @@ -61,10 +62,15 @@ export default function MobileSummary({ } }, [isSummaryOpen]) - if (!rateDefinitions) { + const roomRateDefinitions = roomsAvailability?.find( + (ra): ra is RoomsAvailability => "rateDefinitions" in ra + ) + if (!roomRateDefinitions) { return null } + const rateDefinitions = roomRateDefinitions.rateDefinitions + const rooms = rateSummary.map((room, index) => ({ adults: bookingRooms[index].adults, childrenInRoom: bookingRooms[index].childrenInRoom ?? undefined, diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx index 60b76f754..18fb654ee 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx @@ -28,12 +28,17 @@ import { RateTypeEnum } from "@/types/enums/rateType" export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { const { bookingRooms, + dates, petRoomPackage, rateSummary, roomsAvailability, searchParams, } = useRatesStore((state) => ({ bookingRooms: state.booking.rooms, + dates: { + checkInDate: state.booking.fromDate, + checkOutDate: state.booking.toDate, + }, petRoomPackage: state.petRoomPackage, rateSummary: state.rateSummary, roomsAvailability: state.roomsAvailability, @@ -50,8 +55,8 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { return null } - const checkInDate = new Date(roomsAvailability.checkInDate) - const checkOutDate = new Date(roomsAvailability.checkOutDate) + const checkInDate = new Date(dates.checkInDate) + const checkOutDate = new Date(dates.checkOutDate) const nights = dt(checkOutDate).diff(dt(checkInDate), "days") const bookingCode = params.get("bookingCode") @@ -186,8 +191,15 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) { { - const memberPrice = - room.member?.localPrice.pricePerStay ?? 0 + const memberPrice = room.member?.localPrice.pricePerStay + if (!memberPrice) { + return total + } + const hasSelectedPetRoom = + room.package === RoomPackageCodeEnum.PET_ROOM + if (!hasSelectedPetRoom) { + return total + memberPrice + } const isPetRoom = room.features.find( (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx index 0ca230460..044b2f5fa 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx @@ -20,7 +20,6 @@ export default function SelectedRoomPanel() { const intl = useIntl() const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({ isUserLoggedIn: state.isUserLoggedIn, - rateDefinitions: state.roomsAvailability?.rateDefinitions, roomCategories: state.roomCategories, })) const { diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx index cbf9e2428..625034de6 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx @@ -75,24 +75,18 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { const searchParams = useSearchParams() const bookingCode = searchParams.get("bookingCode") - const { - hotelId, - hotelType, - isUserLoggedIn, - petRoomPackage, - rateDefinitions, - roomCategories, - } = useRatesStore((state) => ({ - hotelId: state.booking.hotelId, - hotelType: state.hotelType, - isUserLoggedIn: state.isUserLoggedIn, - petRoomPackage: state.petRoomPackage, - rateDefinitions: state.roomsAvailability?.rateDefinitions, - roomCategories: state.roomCategories, - })) - const { isMainRoom, roomNr, selectedPackage } = useRoomContext() + const { hotelId, hotelType, isUserLoggedIn, petRoomPackage, roomCategories } = + useRatesStore((state) => ({ + hotelId: state.booking.hotelId, + hotelType: state.hotelType, + isUserLoggedIn: state.isUserLoggedIn, + petRoomPackage: state.petRoomPackage, + roomCategories: state.roomCategories, + })) + const { isMainRoom, roomAvailability, roomNr, selectedPackage } = + useRoomContext() - if (!rateDefinitions) { + if (!roomAvailability || !("rateDefinitions" in roomAvailability)) { return null } @@ -217,16 +211,16 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { {occupancy.max === occupancy.min ? intl.formatMessage( - { id: "{guests, plural, one {# guest} other {# guests}}" }, - { guests: occupancy.max } - ) + { id: "{guests, plural, one {# guest} other {# guests}}" }, + { guests: occupancy.max } + ) : intl.formatMessage( - { id: "{min}-{max} guests" }, - { - min: occupancy.min, - max: occupancy.max, - } - )} + { id: "{min}-{max} guests" }, + { + min: occupancy.min, + max: occupancy.max, + } + )} )} @@ -282,7 +276,10 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { const isAvailable = product.public || (product.member && isUserLoggedIn && isMainRoom) - const rateDefinition = getRateDefinition(product, rateDefinitions) + const rateDefinition = getRateDefinition( + product, + roomAvailability.rateDefinitions + ) return ( r.status === AvailabilityEnum.Available + (room) => room.status === AvailabilityEnum.Available ).length // const tooltipText = intl.formatMessage({ @@ -48,7 +48,7 @@ export default function RoomTypeFilter() { id: "{availableRooms}/{numberOfRooms, plural, one {# room type} other {# room types}} available", }, { - availableRooms: availableRooms, + availableRooms, numberOfRooms: totalRooms, } ) @@ -81,7 +81,7 @@ export default function RoomTypeFilter() { aria-label={option.description} className={styles.radio} id={option.code} - key={option.itemCode} + key={option.code} >
{option.description} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx index 94d6d268d..dd0016518 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/index.tsx @@ -25,19 +25,21 @@ export default function Rooms() { departureDate: state.booking.toDate, hotelId: state.booking.hotelId, rooms: state.rooms, - visibleRooms: state.allRooms, + visibleRooms: state.roomConfigurations, })) useEffect(() => { - const pricesWithCurrencies = visibleRooms.flatMap((room) => - room.products - .filter((product) => product.member || product.public) - .map((product) => ({ - currency: (product.public?.localPrice.currency || - product.member?.localPrice.currency)!, - price: (product.public?.localPrice.pricePerNight || - product.member?.localPrice.pricePerNight)!, - })) + const pricesWithCurrencies = visibleRooms.flatMap((roomConfiguration) => + roomConfiguration.flatMap((room) => + room.products + .filter((product) => product.member || product.public) + .map((product) => ({ + currency: (product.public?.localPrice.currency || + product.member?.localPrice.currency)!, + price: (product.public?.localPrice.pricePerNight || + product.member?.localPrice.pricePerNight)!, + })) + ) ) const lowestPrice = pricesWithCurrencies.reduce( (minPrice, { price }) => Math.min(minPrice, price), diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx index f5288990e..c006006e2 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx @@ -26,11 +26,9 @@ export function RoomsContainer({ const fromDateString = dt(fromDate).format("YYYY-MM-DD") const toDateString = dt(toDate).format("YYYY-MM-DD") - const uniqueAdultsCount = Array.from(new Set(adultArray)) - - const { isPending: isLoadingAvailability, data: roomsAvailability } = + const { data: roomsAvailability, isPending: isLoadingAvailability } = useRoomsAvailability( - uniqueAdultsCount, + adultArray, hotelId, fromDateString, toDateString, diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.test.ts b/apps/scandic-web/components/HotelReservation/SelectRate/getValidDates.test.ts similarity index 100% rename from apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.test.ts rename to apps/scandic-web/components/HotelReservation/SelectRate/getValidDates.test.ts diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts b/apps/scandic-web/components/HotelReservation/SelectRate/getValidDates.ts similarity index 100% rename from apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts rename to apps/scandic-web/components/HotelReservation/SelectRate/getValidDates.ts diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx index d0cd563b0..d7b759ef9 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx @@ -1,12 +1,9 @@ -import { differenceInCalendarDays, format, isWeekend } from "date-fns" import stringify from "json-stable-stringify-without-jsonify" import { notFound } from "next/navigation" import { Suspense } from "react" import { getHotel } from "@/lib/trpc/memoizedRequests" -import { getValidDates } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates" -import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils" import { auth } from "@/auth" import HotelInfoCard, { HotelInfoCardSkeleton, @@ -14,16 +11,14 @@ import HotelInfoCard, { import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer" import TrackingSDK from "@/components/TrackingSDK" import { setLang } from "@/i18n/serverContext" +import { getHotelSearchDetails } from "@/utils/hotelSearchDetails" import { isValidSession } from "@/utils/session" import { convertSearchParamsToObj } from "@/utils/url" -import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" +import { getValidDates } from "./getValidDates" +import { getTracking } from "./tracking" + import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import { - TrackingChannelEnum, - type TrackingSDKHotelInfo, - type TrackingSDKPageData, -} from "@/types/components/tracking" import type { LangParams, PageArgs } from "@/types/params" export default async function SelectRatePage({ @@ -35,7 +30,7 @@ export default async function SelectRatePage({ if (!searchDetails?.hotel) { return notFound() } - const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } = + const { adultsInRoom, childrenInRoom, hotel, noOfRooms, selectHotelParams } = searchDetails const { fromDate, toDate } = getValidDates( @@ -55,41 +50,24 @@ export default async function SelectRatePage({ const arrivalDate = fromDate.toDate() const departureDate = toDate.toDate() - const pageTrackingData: TrackingSDKPageData = { - pageId: "select-rate", - domainLanguage: params.lang, - channel: TrackingChannelEnum["hotelreservation"], - pageName: "hotelreservation|select-rate", - siteSections: "hotelreservation|select-rate", - pageType: "bookingroomsandratespage", - siteVersion: "new-web", - } - - const hotelsTrackingData: TrackingSDKHotelInfo = { - searchTerm: selectHotelParams.city ?? hotel?.name, - arrivalDate: format(arrivalDate, "yyyy-MM-dd"), - departureDate: format(departureDate, "yyyy-MM-dd"), - noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms - noOfChildren: childrenInRoom?.length, - ageOfChildren: childrenInRoom?.map((c) => c.age).join(","), - childBedPreference: childrenInRoom - ?.map((c) => ChildBedMapEnum[c.bed]) - .join("|"), - noOfRooms: 1, // // TODO: Handle multiple rooms - duration: differenceInCalendarDays(departureDate, arrivalDate), - leadTime: differenceInCalendarDays(arrivalDate, new Date()), - searchType: "hotel", - bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - country: hotelData?.hotel.address.country, - hotelID: hotel?.id, - region: hotelData?.hotel.address.city, - } - - const hotelId = +hotel.id + const { hotelsTrackingData, pageTrackingData } = getTracking( + params.lang, + arrivalDate, + departureDate, + adultsInRoom, + childrenInRoom, + hotel.id, + hotel.name, + noOfRooms, + hotelData?.hotel.address.country, + hotelData?.hotel.address.city, + selectHotelParams.city + ) const booking = convertSearchParamsToObj(searchParams) - const suspenseKey = stringify(searchParams) + const hotelId = +hotel.id + const suspenseKey = stringify(searchParams) return ( <> }> diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/tracking.ts b/apps/scandic-web/components/HotelReservation/SelectRate/tracking.ts new file mode 100644 index 000000000..7999fb8eb --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/tracking.ts @@ -0,0 +1,61 @@ +import { differenceInCalendarDays, format, isWeekend } from "date-fns" + +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" +import { + TrackingChannelEnum, + type TrackingSDKHotelInfo, + type TrackingSDKPageData, +} from "@/types/components/tracking" +import type { Lang } from "@/constants/languages" +import type { ChildrenInRoom } from "@/utils/hotelSearchDetails" + +export function getTracking( + lang: Lang, + arrivalDate: Date, + departureDate: Date, + adultsInRoom: number[], + childrenInRoom: ChildrenInRoom, + hotelId: string, + hotelName: string, + noOfRooms: number, + country: string | undefined, + hotelCity: string | undefined, + paramCity: string | undefined +) { + const pageTrackingData: TrackingSDKPageData = { + channel: TrackingChannelEnum.hotelreservation, + domainLanguage: lang, + pageId: "select-rate", + pageName: "hotelreservation|select-rate", + pageType: "bookingroomsandratespage", + siteSections: "hotelreservation|select-rate", + siteVersion: "new-web", + } + + const hotelsTrackingData: TrackingSDKHotelInfo = { + ageOfChildren: childrenInRoom + ?.map((c) => c?.map((k) => k.age).join(",") ?? "none") + .join("|"), + arrivalDate: format(arrivalDate, "yyyy-MM-dd"), + bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", + childBedPreference: childrenInRoom + ?.map((c) => c?.map((k) => ChildBedMapEnum[k.bed]).join(",") ?? "-") + .join("|"), + country, + departureDate: format(departureDate, "yyyy-MM-dd"), + duration: differenceInCalendarDays(departureDate, arrivalDate), + hotelID: hotelId, + leadTime: differenceInCalendarDays(arrivalDate, new Date()), + noOfAdults: adultsInRoom.join(","), + noOfChildren: childrenInRoom?.map((kids) => kids?.length ?? 0).join(","), + noOfRooms, + region: hotelCity, + searchTerm: paramCity ?? hotelName, + searchType: "hotel", + } + + return { + hotelsTrackingData, + pageTrackingData, + } +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts index fc4952c4a..8025d2033 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts @@ -1,73 +1,48 @@ import { trpc } from "@/lib/trpc/client" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" import type { Lang } from "@/constants/languages" - -export function combineRoomAvailabilities( - availabilityResults: PromiseSettledResult[] -) { - return availabilityResults.reduce((acc, result) => { - if (result.status === "fulfilled" && result.value) { - if (acc) { - acc = { - ...acc, - roomConfigurations: [ - ...acc.roomConfigurations, - ...result.value.roomConfigurations, - ], - } - } else { - acc = result.value - } - } - - // Ping monitoring about fail? - if (result.status === "rejected") { - console.info(`RoomAvailability fetch failed`) - console.error(result.reason) - } - - return acc - }, null) -} +import type { ChildrenInRoom } from "@/utils/hotelSearchDetails" export function useRoomsAvailability( - uniqueAdultsCount: number[], + adultsCount: number[], hotelId: number, fromDateString: string, toDateString: string, lang: Lang, - childArray?: Child[], + childArray: ChildrenInRoom, bookingCode?: string ) { - const returnValue = + const roomsAvailability = trpc.hotel.availability.roomsCombinedAvailability.useQuery({ - hotelId, - roomStayStartDate: fromDateString, - roomStayEndDate: toDateString, - uniqueAdultsCount, - childArray, - lang, + adultsCount, bookingCode, + childArray, + hotelId, + lang, + roomStayEndDate: toDateString, + roomStayStartDate: fromDateString, }) - const combinedAvailability = returnValue.data?.length - ? combineRoomAvailabilities( - returnValue.data as PromiseSettledResult[] - ) - : null + const data = roomsAvailability.data?.map((ra) => { + if (ra.status === "fulfilled") { + return ra.value + } + return { + details: ra.reason, + error: "request_failure", + } + }) return { - ...returnValue, - data: combinedAvailability, + ...roomsAvailability, + data, } } export function useHotelPackages( adultArray: number[], - childArray: Child[] | undefined, + childArray: ChildrenInRoom, fromDateString: string, toDateString: string, hotelId: number, @@ -75,7 +50,7 @@ export function useHotelPackages( ) { return trpc.hotel.packages.get.useQuery({ adults: adultArray[0], // Using the first adult count - children: childArray ? childArray.length : undefined, + children: childArray?.[0]?.length, // Using the first children count endDate: toDateString, hotelId: hotelId.toString(), packageCodes: [ diff --git a/apps/scandic-web/providers/EnterDetailsProvider.tsx b/apps/scandic-web/providers/EnterDetailsProvider.tsx index 7ff7493b7..a875e70f6 100644 --- a/apps/scandic-web/providers/EnterDetailsProvider.tsx +++ b/apps/scandic-web/providers/EnterDetailsProvider.tsx @@ -19,7 +19,7 @@ import { DetailsContext } from "@/contexts/Details" import type { DetailsStore } from "@/types/contexts/enter-details" import { StepEnum } from "@/types/enums/step" import type { DetailsProviderProps } from "@/types/providers/enter-details" -import type { InitialState } from "@/types/stores/enter-details" +import type { InitialState, RoomState } from "@/types/stores/enter-details" export default function EnterDetailsProvider({ booking, @@ -50,9 +50,9 @@ export default function EnterDetailsProvider({ bedType: room.bedTypes?.length === 1 ? { - roomTypeCode: room.bedTypes[0].value, - description: room.bedTypes[0].description, - } + roomTypeCode: room.bedTypes[0].value, + description: room.bedTypes[0].description, + } : undefined, mustBeGuaranteed: room.mustBeGuaranteed, isFlexRate: room.isFlexRate, @@ -85,9 +85,13 @@ export default function EnterDetailsProvider({ } const updatedRooms = storedValues.rooms.map((storedRoom, idx) => { + const room = store.rooms[idx] + if (!room) { + return null + } // Need to create a deep new copy // since store is readonly - const currentRoom = deepmerge({}, store.rooms[idx]) + const currentRoom = deepmerge({}, room) if (!currentRoom.room.isAvailable) { return currentRoom @@ -142,27 +146,38 @@ export default function EnterDetailsProvider({ }) const canProceedToPayment = updatedRooms.every( - (room) => room.isComplete && room.room.isAvailable + (room) => room?.isComplete && room?.room.isAvailable + ) + + const filteredOutMissingRooms = updatedRooms.filter( + (room): room is RoomState => !!room ) const nights = dt(booking.toDate).diff(booking.fromDate, "days") - const currency = (updatedRooms[0].room.roomRate.publicRate?.localPrice - .currency || - updatedRooms[0].room.roomRate.memberRate?.localPrice.currency)! - const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights) + const currency = (filteredOutMissingRooms[0].room.roomRate.publicRate + ?.localPrice.currency || + filteredOutMissingRooms[0].room.roomRate.memberRate?.localPrice.currency)! + const totalPrice = calcTotalPrice( + filteredOutMissingRooms, + currency, + !!user, + nights + ) - const activeRoom = updatedRooms.findIndex((room) => !room.isComplete) + const activeRoom = filteredOutMissingRooms.findIndex( + (room) => !room.isComplete + ) writeToSessionStorage({ activeRoom, booking, - rooms: updatedRooms, + rooms: filteredOutMissingRooms, }) storeRef.current?.setState({ activeRoom: storedValues.activeRoom, canProceedToPayment, - rooms: updatedRooms, + rooms: filteredOutMissingRooms, totalPrice, }) }, [booking, rooms, user]) diff --git a/apps/scandic-web/providers/SelectRate/RoomProvider.tsx b/apps/scandic-web/providers/SelectRate/RoomProvider.tsx index 5e1da2997..1c00eab66 100644 --- a/apps/scandic-web/providers/SelectRate/RoomProvider.tsx +++ b/apps/scandic-web/providers/SelectRate/RoomProvider.tsx @@ -14,9 +14,11 @@ export default function RoomProvider({ const activeRoom = useRatesStore((state) => state.activeRoom) const closeSection = useRatesStore((state) => state.actions.closeSection(idx)) const modifyRate = useRatesStore((state) => state.actions.modifyRate(idx)) + const roomAvailability = useRatesStore( + (state) => state.roomsAvailability?.[idx] + ) const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx)) const selectRate = useRatesStore((state) => state.actions.selectRate(idx)) - const totalRooms = useRatesStore((state) => state.allRooms.length) const roomNr = idx + 1 return ( {children} diff --git a/apps/scandic-web/server/routers/booking/input.ts b/apps/scandic-web/server/routers/booking/input.ts index 636860e9e..d398332b4 100644 --- a/apps/scandic-web/server/routers/booking/input.ts +++ b/apps/scandic-web/server/routers/booking/input.ts @@ -3,52 +3,67 @@ import { z } from "zod" import { ChildBedTypeEnum } from "@/constants/booking" import { Lang, langToApiLang } from "@/constants/languages" -const signupSchema = z.discriminatedUnion("becomeMember", [ - z.object({ - dateOfBirth: z.string(), - postalCode: z.string(), - becomeMember: z.literal(true), - }), - z.object({ becomeMember: z.literal(false) }), -]) - -const roomsSchema = z.array( - z.object({ - adults: z.number().int().nonnegative(), - childrenAges: z - .array( - z.object({ - age: z.number().int().nonnegative(), - bedType: z.nativeEnum(ChildBedTypeEnum), - }) - ) - .default([]), - rateCode: z.string(), - roomTypeCode: z.coerce.string(), - guest: z.intersection( - z.object({ +const roomsSchema = z + .array( + z.object({ + adults: z.number().int().nonnegative(), + childrenAges: z + .array( + z.object({ + age: z.number().int().nonnegative(), + bedType: z.nativeEnum(ChildBedTypeEnum), + }) + ) + .default([]), + rateCode: z.string(), + roomTypeCode: z.coerce.string(), + guest: z.object({ + becomeMember: z.boolean(), + countryCode: z.string(), + dateOfBirth: z.string().nullish(), + email: z.string().email(), firstName: z.string(), lastName: z.string(), - email: z.string().email(), + membershipNumber: z.string().nullish(), + postalCode: z.string().nullish(), phoneNumber: z.string(), - countryCode: z.string(), - membershipNumber: z.string().optional(), }), - signupSchema - ), - smsConfirmationRequested: z.boolean(), - packages: z.object({ - breakfast: z.boolean(), - allergyFriendly: z.boolean(), - petFriendly: z.boolean(), - accessibility: z.boolean(), - }), - roomPrice: z.object({ - memberPrice: z.number().nullish(), - publicPrice: z.number().nullish(), - }), + smsConfirmationRequested: z.boolean(), + packages: z.object({ + breakfast: z.boolean(), + allergyFriendly: z.boolean(), + petFriendly: z.boolean(), + accessibility: z.boolean(), + }), + roomPrice: z.object({ + memberPrice: z.number().nullish(), + publicPrice: z.number().nullish(), + }), + }) + ) + .superRefine((data, ctx) => { + data.forEach((room, idx) => { + if (idx === 0 && room.guest.becomeMember) { + if (!room.guest.dateOfBirth) { + ctx.addIssue({ + code: z.ZodIssueCode.invalid_type, + expected: "string", + received: typeof room.guest.dateOfBirth, + path: ["guest", "dateOfBirth"], + }) + } + + if (!room.guest.postalCode) { + ctx.addIssue({ + code: z.ZodIssueCode.invalid_type, + expected: "string", + received: typeof room.guest.postalCode, + path: ["guest", "postalCode"], + }) + } + } + }) }) -) const paymentSchema = z.object({ paymentMethod: z.string(), diff --git a/apps/scandic-web/server/routers/hotels/input.ts b/apps/scandic-web/server/routers/hotels/input.ts index 6c4fb1337..841d5aa97 100644 --- a/apps/scandic-web/server/routers/hotels/input.ts +++ b/apps/scandic-web/server/routers/hotels/input.ts @@ -26,21 +26,25 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({ }) export const roomsCombinedAvailabilityInputSchema = z.object({ - hotelId: z.number(), - roomStayStartDate: z.string(), - roomStayEndDate: z.string(), - uniqueAdultsCount: z.array(z.number()), + adultsCount: z.array(z.number()), + bookingCode: z.string().optional(), childArray: z .array( - z.object({ - bed: z.nativeEnum(ChildBedMapEnum), - age: z.number(), - }) + z + .array( + z.object({ + age: z.number(), + bed: z.nativeEnum(ChildBedMapEnum), + }) + ) + .nullable() ) - .optional(), - bookingCode: z.string().optional(), - rateCode: z.string().optional(), + .nullish(), + hotelId: z.number(), lang: z.nativeEnum(Lang), + rateCode: z.string().optional(), + roomStayEndDate: z.string(), + roomStayStartDate: z.string(), }) export const selectedRoomAvailabilityInputSchema = z.object({ diff --git a/apps/scandic-web/server/routers/hotels/output.ts b/apps/scandic-web/server/routers/hotels/output.ts index e85142318..a90713acb 100644 --- a/apps/scandic-web/server/routers/hotels/output.ts +++ b/apps/scandic-web/server/routers/hotels/output.ts @@ -22,6 +22,7 @@ import { relationshipsSchema } from "./schemas/relationships" import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration" import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition" +import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import type { AdditionalData, City, @@ -137,6 +138,13 @@ const cancellationRules = { NotCancellable: 0, } as const +// Used to ensure `Available` rooms +// are shown before all `NotAvailable` +const statusLookup = { + [AvailabilityEnum.Available]: 1, + [AvailabilityEnum.NotAvailable]: 2, +} + export const roomsAvailabilitySchema = z .object({ data: z.object({ @@ -161,8 +169,8 @@ export const roomsAvailabilitySchema = z return acc }, {}) - attributes.roomConfigurations = attributes.roomConfigurations.map( - (room) => { + const roomConfigurations = attributes.roomConfigurations + .map((room) => { if (room.products.length) { room.breakfastIncludedInAllRatesMember = room.products.every( (product) => @@ -222,10 +230,16 @@ export const roomsAvailabilitySchema = z } return room - } - ) + }) + .sort( + // @ts-expect-error - array indexing + (a, b) => statusLookup[a.status] - statusLookup[b.status] + ) - return attributes + return { + ...attributes, + roomConfigurations, + } }) export const ratesSchema = z.array(rateSchema) diff --git a/apps/scandic-web/server/routers/hotels/query.ts b/apps/scandic-web/server/routers/hotels/query.ts index 54cdf00d8..44e84aac2 100644 --- a/apps/scandic-web/server/routers/hotels/query.ts +++ b/apps/scandic-web/server/routers/hotels/query.ts @@ -499,20 +499,20 @@ export const hotelQueryRouter = router({ const { lang } = input const apiLang = toApiLang(lang) const { - hotelId, - roomStayStartDate, - roomStayEndDate, - uniqueAdultsCount, - childArray, + adultsCount, bookingCode, + childArray, + hotelId, rateCode, + roomStayEndDate, + roomStayStartDate, } = input const metricsData = { hotelId, roomStayStartDate, roomStayEndDate, - uniqueAdultsCount, + adultsCount, childArray: childArray ? JSON.stringify(childArray) : undefined, bookingCode, } @@ -525,15 +525,15 @@ export const hotelQueryRouter = router({ ) const availabilityResponses = await Promise.allSettled( - uniqueAdultsCount.map(async (adultCount: number) => { + adultsCount.map(async (adultCount: number, idx: number) => { + const kids = childArray?.[idx] const params: Record = { roomStayStartDate, roomStayEndDate, adults: adultCount, - ...(childArray && - childArray.length > 0 && { - children: generateChildrenString(childArray), - }), + ...(kids?.length && { + children: generateChildrenString(kids), + }), ...(bookingCode && { bookingCode }), language: apiLang, } @@ -769,9 +769,9 @@ export const hotelQueryRouter = router({ type: matchingRoom.mainBed.type, extraBed: matchingRoom.fixedExtraBed ? { - type: matchingRoom.fixedExtraBed.type, - description: matchingRoom.fixedExtraBed.description, - } + type: matchingRoom.fixedExtraBed.type, + description: matchingRoom.fixedExtraBed.description, + } : undefined, } } @@ -794,23 +794,27 @@ export const hotelQueryRouter = router({ ) return { - selectedRoom, - rateDetails: rateDefinition?.generalTerms, + bedTypes, + breakfastIncluded: !!rateDefinition?.breakfastIncluded, + cancellationRule: rateDefinition?.cancellationRule, cancellationText: rateDefinition?.cancellationText ?? "", isFlexRate: rateDefinition?.cancellationRule === CancellationRuleEnum.CancellableBefore6PM, - mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed, memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed, - breakfastIncluded: !!rateDefinition?.breakfastIncluded, + memberRate: rates?.member, + mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed, + publicRate: rates?.public, + rate: selectedRoom.products[0].rate, + rateDefinitionTitle: rateDefinition?.title ?? "", + rateDetails: rateDefinition?.generalTerms, // Send rate Title when it is a booking code rate rateTitle: rateDefinition?.rateType !== RateTypeEnum.Regular ? rateDefinition?.title : undefined, - memberRate: rates?.member, - publicRate: rates?.public, - bedTypes, + rateType: rateDefinition?.rateType ?? "", + selectedRoom, } }), hotelsByCityWithBookingCode: serviceProcedure @@ -1096,9 +1100,9 @@ export const hotelQueryRouter = router({ return hotelData ? { - ...hotelData, - url: hotelPage?.url ?? null, - } + ...hotelData, + url: hotelPage?.url ?? null, + } : null }) ) diff --git a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts index 2f35b11aa..da0c82c04 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts @@ -38,9 +38,15 @@ export const roomConfigurationSchema = z (product) => !product.public?.rateCode && !product.member?.rateCode ) if (allProductsMissBothRateCodes) { - data.status = AvailabilityEnum.NotAvailable + return { + ...data, + status: AvailabilityEnum.NotAvailable, + } } } - return data + // Creating a new objekt since data is frozen (readony) + // and can cause errors to be thrown if trying to manipulate + // object elsewhere + return { ...data } }) diff --git a/apps/scandic-web/stores/enter-details/helpers.ts b/apps/scandic-web/stores/enter-details/helpers.ts index 4956112c8..85632f0b1 100644 --- a/apps/scandic-web/stores/enter-details/helpers.ts +++ b/apps/scandic-web/stores/enter-details/helpers.ts @@ -211,12 +211,20 @@ export function calcTotalPrice( ? parseInt(room.breakfast.localPrice?.price ?? 0) : 0 - const roomFeaturesTotal = room.roomFeatures?.reduce((total, pkg) => { - if (pkg.requestedPrice.totalPrice) { - total = add(total, pkg.requestedPrice.totalPrice) - } - return total - }, 0) + const roomFeaturesTotal = room.roomFeatures?.reduce( + (total, pkg) => { + if (pkg.requestedPrice.totalPrice) { + total.requestedPrice = add( + total.requestedPrice, + pkg.requestedPrice.totalPrice + ) + } + total.local = add(total.local, pkg.localPrice.totalPrice) + + return total + }, + { local: 0, requestedPrice: 0 } + ) const result: Price = { requested: roomPrice.perStay.requested @@ -235,13 +243,13 @@ export function calcTotalPrice( acc.local.price, roomPrice.perStay.local.price, breakfastLocalPrice * room.adults * nights, - roomFeaturesTotal + roomFeaturesTotal?.local ?? 0 ), regularPrice: add( acc.local.regularPrice, roomPrice.perStay.local.regularPrice, breakfastLocalPrice * room.adults * nights, - roomFeaturesTotal + roomFeaturesTotal?.requestedPrice ?? 0 ), }, } diff --git a/apps/scandic-web/stores/enter-details/index.ts b/apps/scandic-web/stores/enter-details/index.ts index f15e4879c..e3c78bd51 100644 --- a/apps/scandic-web/stores/enter-details/index.ts +++ b/apps/scandic-web/stores/enter-details/index.ts @@ -369,24 +369,33 @@ export function createDetailsStore( return set( produce((state: DetailsState) => { state.rooms[idx].steps[StepEnum.details].isValid = true + const currentRoom = state.rooms[idx].room - state.rooms[idx].room.guest.countryCode = data.countryCode - state.rooms[idx].room.guest.dateOfBirth = data.dateOfBirth - state.rooms[idx].room.guest.email = data.email - state.rooms[idx].room.guest.firstName = data.firstName - state.rooms[idx].room.guest.join = data.join - state.rooms[idx].room.guest.lastName = data.lastName + currentRoom.guest.countryCode = data.countryCode + currentRoom.guest.email = data.email + currentRoom.guest.firstName = data.firstName + currentRoom.guest.join = data.join + currentRoom.guest.lastName = data.lastName if (data.join) { - state.rooms[idx].room.guest.membershipNo = undefined + currentRoom.guest.membershipNo = undefined } else { - state.rooms[idx].room.guest.membershipNo = data.membershipNo + currentRoom.guest.membershipNo = data.membershipNo } - state.rooms[idx].room.guest.phoneNumber = data.phoneNumber - state.rooms[idx].room.guest.zipCode = data.zipCode + currentRoom.guest.phoneNumber = data.phoneNumber - state.rooms[idx].room.roomPrice = getRoomPrice( - state.rooms[idx].room.roomRate, + // Only valid for room 1 + if (idx === 0 && data.join && !isMember) { + if ("dateOfBirth" in currentRoom.guest) { + currentRoom.guest.dateOfBirth = data.dateOfBirth + } + if ("zipCode" in currentRoom.guest) { + currentRoom.guest.zipCode = data.zipCode + } + } + + currentRoom.roomPrice = getRoomPrice( + currentRoom.roomRate, Boolean(data.join || data.membershipNo || isMember) ) diff --git a/apps/scandic-web/stores/select-rate/helper.ts b/apps/scandic-web/stores/select-rate/helper.ts deleted file mode 100644 index 70e886953..000000000 --- a/apps/scandic-web/stores/select-rate/helper.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" -import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" - -/** - * Get the lowest priced room for each room type that appears more than once. - */ - -export function filterDuplicateRoomTypesByLowestPrice( - roomConfigurations: RoomConfiguration[] -): RoomConfiguration[] { - const roomTypeCount = roomConfigurations.reduce>( - (roomTypeTally, currentRoom) => { - const currentRoomType = currentRoom.roomType - const currentCount = roomTypeTally[currentRoomType] || 0 - - return { - ...roomTypeTally, - [currentRoomType]: currentCount + 1, - } - }, - {} - ) - - const duplicateRoomTypes = new Set( - Object.keys(roomTypeCount).filter((roomType) => roomTypeCount[roomType] > 1) - ) - - const roomMap = new Map() - - roomConfigurations.forEach((room) => { - const { roomType, products, status } = room - - if (!duplicateRoomTypes.has(roomType)) { - roomMap.set(roomType, room) - return - } - - const previousRoom = roomMap.get(roomType) - - // Prioritize 'Available' status - if ( - status === AvailabilityEnum.Available && - previousRoom?.status === AvailabilityEnum.NotAvailable - ) { - roomMap.set(roomType, room) - return - } - - if ( - status === AvailabilityEnum.NotAvailable && - previousRoom?.status === AvailabilityEnum.Available - ) { - return - } - - if (previousRoom) { - products.forEach((product) => { - const publicProduct = product?.public || { - requestedPrice: null, - localPrice: null, - } - const memberProduct = product?.member || { - requestedPrice: null, - localPrice: null, - } - - const { - requestedPrice: publicRequestedPrice, - localPrice: publicLocalPrice, - } = publicProduct - const { - requestedPrice: memberRequestedPrice, - localPrice: memberLocalPrice, - } = memberProduct - - const previousLowest = roomMap.get(roomType) - - const currentRequestedPrice = Math.min( - Number(publicRequestedPrice?.pricePerNight) ?? Infinity, - Number(memberRequestedPrice?.pricePerNight) ?? Infinity - ) - const currentLocalPrice = Math.min( - Number(publicLocalPrice?.pricePerNight) ?? Infinity, - Number(memberLocalPrice?.pricePerNight) ?? Infinity - ) - - if ( - !previousLowest || - currentRequestedPrice < - Math.min( - Number( - previousLowest.products[0].public?.requestedPrice?.pricePerNight - ) ?? Infinity, - Number( - previousLowest.products[0].member?.requestedPrice?.pricePerNight - ) ?? Infinity - ) || - (currentRequestedPrice === - Math.min( - Number( - previousLowest.products[0].public?.requestedPrice?.pricePerNight - ) ?? Infinity, - Number( - previousLowest.products[0].member?.requestedPrice?.pricePerNight - ) ?? Infinity - ) && - currentLocalPrice < - Math.min( - Number( - previousLowest.products[0].public?.localPrice?.pricePerNight - ) ?? Infinity, - Number( - previousLowest.products[0].member?.localPrice?.pricePerNight - ) ?? Infinity - )) - ) { - roomMap.set(roomType, room) - } - }) - } else { - roomMap.set(roomType, room) - } - }) - - return Array.from(roomMap.values()) -} diff --git a/apps/scandic-web/stores/select-rate/index.ts b/apps/scandic-web/stores/select-rate/index.ts index cefedd3d5..c950c7c63 100644 --- a/apps/scandic-web/stores/select-rate/index.ts +++ b/apps/scandic-web/stores/select-rate/index.ts @@ -3,26 +3,25 @@ import { ReadonlyURLSearchParams } from "next/navigation" import { useContext } from "react" import { create, useStore } from "zustand" -import { filterDuplicateRoomTypesByLowestPrice } from "@/stores/select-rate/helper" - import { RatesContext } from "@/contexts/Rates" -import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RateTypeEnum } from "@/types/enums/rateType" -import type { InitialState, RatesState } from "@/types/stores/rates" +import type { + AvailabilityError, + InitialState, + RatesState, +} from "@/types/stores/rates" import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" -const statusLookup = { - [AvailabilityEnum.Available]: 1, - [AvailabilityEnum.NotAvailable]: 2, -} - function findSelectedRate( rateCode: string, roomTypeCode: string, - rooms: RoomConfiguration[] + rooms: RoomConfiguration[] | AvailabilityError ) { + if (!Array.isArray(rooms)) { + return null + } return rooms.find( (room) => room.roomTypeCode === roomTypeCode && @@ -70,23 +69,26 @@ export function createRatesStore({ }, ] - let allRooms: RoomConfiguration[] = [] - if (roomsAvailability?.roomConfigurations) { - allRooms = filterDuplicateRoomTypesByLowestPrice( - roomsAvailability.roomConfigurations - ).sort( - // @ts-expect-error - array indexing - (a, b) => statusLookup[a.status] - statusLookup[b.status] - ) + let roomConfigurations: RatesState["roomConfigurations"] = [] + if (roomsAvailability) { + for (const availability of roomsAvailability) { + if ("error" in availability) { + // Availability request failed, default to empty array + roomConfigurations.push([]) + } else { + roomConfigurations.push(availability.roomConfigurations) + } + } } const rateSummary: RatesState["rateSummary"] = [] booking.rooms.forEach((room, idx) => { if (room.rateCode && room.roomTypeCode) { - const selectedRoom = roomsAvailability?.roomConfigurations.find( - (roomConf) => - roomConf.roomTypeCode === room.roomTypeCode && - roomConf.products.find( + const roomConfiguration = roomConfigurations?.[idx] + const selectedRoom = roomConfiguration.find( + (rc) => + rc.roomTypeCode === room.roomTypeCode && + rc.products.find( (product) => product.public?.rateCode === room.rateCode || product.member?.rateCode === room.rateCode @@ -149,31 +151,34 @@ export function createRatesStore({ return set( produce((state: RatesState) => { state.rooms[idx].selectedPackage = code - const searchParams = new URLSearchParams(state.searchParams) - if (code) { - state.rooms[idx].rooms = state.allRooms.filter((room) => - room.features.find((feat) => feat.code === code) + const roomConfiguration = state.roomConfigurations[idx] + if (roomConfiguration) { + const searchParams = new URLSearchParams(state.searchParams) + if (code) { + state.rooms[idx].rooms = roomConfiguration.filter((room) => + room.features.find((feat) => feat.code === code) + ) + searchParams.set(`room[${idx}].packages`, code) + + if (state.rateSummary[idx]) { + state.rateSummary[idx].package = code + } + } else { + state.rooms[idx].rooms = roomConfiguration + searchParams.delete(`room[${idx}].packages`) + + if (state.rateSummary[idx]) { + state.rateSummary[idx].package = undefined + } + } + + state.searchParams = new ReadonlyURLSearchParams(searchParams) + window.history.pushState( + {}, + "", + `${state.pathname}?${searchParams}` ) - searchParams.set(`room[${idx}].packages`, code) - - if (state.rateSummary[idx]) { - state.rateSummary[idx].package = code - } - } else { - state.rooms[idx].rooms = state.allRooms - searchParams.delete(`room[${idx}].packages`) - - if (state.rateSummary[idx]) { - state.rateSummary[idx].package = undefined - } } - - state.searchParams = new ReadonlyURLSearchParams(searchParams) - window.history.pushState( - {}, - "", - `${state.pathname}?${searchParams}` - ) }) ) } @@ -247,7 +252,6 @@ export function createRatesStore({ }, }, activeRoom, - allRooms, booking, filterOptions, hotelType, @@ -258,9 +262,12 @@ export function createRatesStore({ (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM ), rateSummary, - rooms: booking.rooms.map((room) => { + roomConfigurations, + rooms: booking.rooms.map((room, idx) => { + const roomConfiguration = roomConfigurations[idx] const selectedRate = - findSelectedRate(room.rateCode, room.roomTypeCode, allRooms) ?? null + findSelectedRate(room.rateCode, room.roomTypeCode, roomConfiguration) ?? + null const product = selectedRate?.products.find( (prd) => @@ -270,22 +277,25 @@ export function createRatesStore({ const selectedPackage = room.packages?.[0] + let rooms: RoomConfiguration[] = roomConfiguration + if (selectedPackage) { + rooms = roomConfiguration.filter((r) => + r.features.find((f) => f.code === selectedPackage) + ) + } + return { bookingRoom: room, - rooms: selectedPackage - ? allRooms.filter((r) => - r.features.find((f) => f.code === selectedPackage) - ) - : allRooms, + rooms, selectedPackage, selectedRate: selectedRate && product ? { - features: selectedRate.features, - product, - roomType: selectedRate.roomType, - roomTypeCode: selectedRate.roomTypeCode, - } + features: selectedRate.features, + product, + roomType: selectedRate.roomType, + roomTypeCode: selectedRate.roomTypeCode, + } : null, } }), diff --git a/apps/scandic-web/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts b/apps/scandic-web/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts index 24336f390..f32fe58de 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts @@ -1,5 +1,6 @@ import type { Hotel } from "@/types/hotel" import type { ProductType } from "@/types/trpc/routers/hotel/availability" +import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" export enum HotelCardListingTypeEnum { MapListing = "mapListing", @@ -12,7 +13,7 @@ export type HotelData = { } export type HotelCardListingProps = { - hotelData: HotelData[] + hotelData: HotelResponse[] type?: HotelCardListingTypeEnum } diff --git a/apps/scandic-web/types/components/hotelReservation/selectHotel/hotelCardProps.ts b/apps/scandic-web/types/components/hotelReservation/selectHotel/hotelCardProps.ts index c75670354..1c6fe8542 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectHotel/hotelCardProps.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectHotel/hotelCardProps.ts @@ -1,10 +1,8 @@ -import { - type HotelCardListingTypeEnum, - type HotelData, -} from "./hotelCardListingProps" +import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" +import type { HotelCardListingTypeEnum } from "./hotelCardListingProps" export type HotelCardProps = { - hotel: HotelData + hotelData: HotelResponse isUserLoggedIn: boolean type?: HotelCardListingTypeEnum state?: "default" | "active" diff --git a/apps/scandic-web/types/components/hotelReservation/selectHotel/map.ts b/apps/scandic-web/types/components/hotelReservation/selectHotel/map.ts index d1df27696..56d4665b4 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectHotel/map.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectHotel/map.ts @@ -2,8 +2,8 @@ import type { z } from "zod" import type { Coordinates } from "@/types/components/maps/coordinates" import type { Amenities } from "@/types/hotel" +import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" import type { imageSchema } from "@/server/routers/hotels/schemas/image" -import type { HotelData } from "./hotelCardListingProps" import type { CategorizedFilters } from "./hotelFilters" import type { AlternativeHotelsSearchParams, @@ -11,14 +11,14 @@ import type { } from "./selectHotelSearchParams" export interface HotelListingProps { - hotels: HotelData[] + hotels: HotelResponse[] } export interface SelectHotelMapProps { apiKey: string hotelPins: HotelPin[] mapId: string - hotels: HotelData[] + hotels: HotelResponse[] filterList: CategorizedFilters cityCoordinates: Coordinates bookingCode: string | undefined @@ -66,7 +66,7 @@ export interface HotelCardDialogImageProps { } export interface HotelCardDialogListingProps { - hotels: HotelData[] | null + hotels: HotelResponse[] } export type SelectHotelMapContainerProps = { diff --git a/apps/scandic-web/types/components/hotelReservation/selectHotel/noAvailabilityAlert.ts b/apps/scandic-web/types/components/hotelReservation/selectHotel/noAvailabilityAlert.ts index 7405d5281..4dde6c58d 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectHotel/noAvailabilityAlert.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectHotel/noAvailabilityAlert.ts @@ -1,7 +1,8 @@ -import type { HotelData } from "./hotelCardListingProps" +import type { Hotel } from "@/types/hotel" export type NoAvailabilityAlertProp = { + hotelsLength: number isAllUnavailable: boolean isAlternative?: boolean - hotels: HotelData[] + operaId: Hotel["operaId"] } diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/roomsContainer.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/roomsContainer.ts index 2c9bb6059..bf669a881 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectRate/roomsContainer.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectRate/roomsContainer.ts @@ -1,11 +1,12 @@ import type { HotelData } from "@/types/hotel" -import type { Child, SelectRateSearchParams } from "./selectRate" +import type { ChildrenInRoom } from "@/utils/hotelSearchDetails" +import type { SelectRateSearchParams } from "./selectRate" export interface RoomsContainerProps { adultArray: number[] booking: SelectRateSearchParams bookingCode?: string - childArray?: Child[] + childArray: ChildrenInRoom fromDate: Date hotelData: HotelData | null hotelId: number diff --git a/apps/scandic-web/types/components/tracking.ts b/apps/scandic-web/types/components/tracking.ts index c433985cc..7a986eaca 100644 --- a/apps/scandic-web/types/components/tracking.ts +++ b/apps/scandic-web/types/components/tracking.ts @@ -47,46 +47,46 @@ export type TrackingSDKUserData = { } export type TrackingSDKHotelInfo = { - hotelID?: string - arrivalDate?: string - departureDate?: string - noOfAdults?: number - noOfChildren?: number ageOfChildren?: string // "10", "2,5,10" - //rewardNight?: boolean + ancillaries?: Ancillary[] + analyticsRateCode?: "flex" | "change" | "save" | string + arrivalDate?: string + availableResults?: number // Number of hotels to choose from after a city search + bedType?: string + bedTypePosition?: number // Which position the bed type had in the list of available bed types + bnr?: string // Booking number + breakfastOption?: string // "no breakfast" or "breakfast buffet" + //bonuscheque?: boolean //bookingCode?: string //bookingCodeAvailability?: boolean - leadTime?: number // Number of days from booking date until arrivalDate - noOfRooms?: number - //bonuscheque?: boolean - childBedPreference?: string - duration?: number // Number of nights to stay - availableResults?: number // Number of hotels to choose from after a city search bookingTypeofDay?: "weekend" | "weekday" - searchTerm?: string - roomPrice?: number + childBedPreference?: string + country?: string // Country of the hotel + departureDate?: string + discount?: number + duration?: number // Number of nights to stay + hotelID?: string + leadTime?: number // Number of days from booking date until arrivalDate + lowestRoomPrice?: number + //modifyValues?: string // ,roomtype:value>,bed: + noOfAdults?: number | string // multiroom support, "2,1,3" + noOfChildren?: number | string // multiroom support, "2,1,3" + noOfRooms?: number + //rewardNight?: boolean rateCode?: string rateCodeCancellationRule?: string rateCodeName?: string // Scandic Friends - full flex inkl. frukost rateCodeType?: string // regular, promotion etc - revenueCurrencyCode?: string // SEK, DKK, NOK, EUR - roomTypeCode?: string - roomTypePosition?: number // Which position the room had in the list of available rooms - roomTypeName?: string - bedType?: string - bedTypePosition?: number // Which position the bed type had in the list of available bed types - breakfastOption?: string // "no breakfast" or "breakfast buffet" - bnr?: string // Booking number - analyticsrateCode?: "flex" | "change" | "save" | string - specialRoomType?: string // allergy room, pet-friendly, accesibillity room - //modifyValues?: string // ,roomtype:value>,bed: - country?: string // Country of the hotel region?: string // Region of the hotel - discount?: number - totalPrice?: number - lowestRoomPrice?: number + revenueCurrencyCode?: string // SEK, DKK, NOK, EUR + roomPrice?: number | string + roomTypeCode?: string + roomTypeName?: string + roomTypePosition?: number // Which position the room had in the list of available rooms + searchTerm?: string searchType?: "destination" | "hotel" - ancillaries?: Ancillary[] + specialRoomType?: string // allergy room, pet-friendly, accesibillity room + totalPrice?: number | string } export type Ancillary = { diff --git a/apps/scandic-web/types/contexts/select-rate/room.ts b/apps/scandic-web/types/contexts/select-rate/room.ts index 93b96397a..885cf161f 100644 --- a/apps/scandic-web/types/contexts/select-rate/room.ts +++ b/apps/scandic-web/types/contexts/select-rate/room.ts @@ -1,5 +1,9 @@ import type { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { SelectedRate, SelectedRoom } from "@/types/stores/rates" +import type { + RatesState, + SelectedRate, + SelectedRoom, +} from "@/types/stores/rates" export interface RoomContextValue extends SelectedRoom { actions: { @@ -10,6 +14,9 @@ export interface RoomContextValue extends SelectedRoom { } isActiveRoom: boolean isMainRoom: boolean + roomAvailability: + | NonNullable[number] + | undefined roomNr: number totalRooms: number } diff --git a/apps/scandic-web/types/providers/details/room.ts b/apps/scandic-web/types/providers/details/room.ts index b22d14255..713d80ce0 100644 --- a/apps/scandic-web/types/providers/details/room.ts +++ b/apps/scandic-web/types/providers/details/room.ts @@ -10,8 +10,11 @@ export interface Room { mustBeGuaranteed: boolean memberMustBeGuaranteed?: boolean packages: Packages | null + rate: "change" | "flex" | "save" + rateDefinitionTitle: string rateDetails: string[] rateTitle?: string + rateType: string roomRate: RoomRate roomType: string roomTypeCode: string diff --git a/apps/scandic-web/types/providers/rates.ts b/apps/scandic-web/types/providers/rates.ts index 4e1479649..93bd79214 100644 --- a/apps/scandic-web/types/providers/rates.ts +++ b/apps/scandic-web/types/providers/rates.ts @@ -2,6 +2,7 @@ import type { Room } from "@/types/hotel" import type { Packages } from "@/types/requests/packages" import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate" +import type { AvailabilityError } from "../stores/rates" export interface RatesProviderProps extends React.PropsWithChildren { booking: SelectRateSearchParams @@ -9,6 +10,6 @@ export interface RatesProviderProps extends React.PropsWithChildren { isUserLoggedIn: boolean packages: Packages | null roomCategories: Room[] - roomsAvailability: RoomsAvailability | null + roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined vat: number } diff --git a/apps/scandic-web/types/stores/booking-confirmation.ts b/apps/scandic-web/types/stores/booking-confirmation.ts index 75ee07585..4510274db 100644 --- a/apps/scandic-web/types/stores/booking-confirmation.ts +++ b/apps/scandic-web/types/stores/booking-confirmation.ts @@ -16,12 +16,16 @@ export interface Room { breakfastIncluded: boolean children?: number childBedPreferences: ChildBedPreference[] + childrenAges?: number[] confirmationNumber: string + currencyCode: string fromDate: Date name: string + packages: BookingConfirmation["booking"]["packages"] rateDefinition: BookingConfirmation["booking"]["rateDefinition"] roomFeatures?: PackageSchema[] | null roomPrice: number + roomTypeCode: string | null toDate: Date totalPrice: number totalPriceExVat: number diff --git a/apps/scandic-web/types/stores/enter-details.ts b/apps/scandic-web/types/stores/enter-details.ts index ca0b21b6c..e52c64c2d 100644 --- a/apps/scandic-web/types/stores/enter-details.ts +++ b/apps/scandic-web/types/stores/enter-details.ts @@ -22,20 +22,21 @@ import type { import type { Packages } from "../requests/packages" export interface InitialRoomData { - isAvailable: boolean - bedType?: BedTypeSchema // used when there is only one bedtype to preselect it + // used when there is only one bedtype to preselect it + bedType?: BedTypeSchema bedTypes: BedTypeSelection[] breakfastIncluded: boolean cancellationText: string cancellationRule?: string + isAvailable: boolean + isFlexRate: boolean + mustBeGuaranteed: boolean rateDetails: string[] | undefined rateTitle?: string roomFeatures: Packages | null roomRate: RoomRate roomType: string roomTypeCode: string - mustBeGuaranteed: boolean - isFlexRate: boolean } export type RoomStep = { @@ -43,17 +44,19 @@ export type RoomStep = { isValid: boolean } +export interface Room extends InitialRoomData { + adults: number + bedType: BedTypeSchema | undefined + breakfast: BreakfastPackage | false | undefined + childrenInRoom: Child[] | undefined + guest: DetailsSchema | MultiroomDetailsSchema | SignedInDetailsSchema + roomPrice: RoomPrice +} + export interface RoomState { currentStep: StepEnum | null isComplete: boolean - room: InitialRoomData & { - adults: number - bedType: BedTypeSchema | undefined - breakfast: BreakfastPackage | false | undefined - childrenInRoom: Child[] | undefined - guest: DetailsSchema | SignedInDetailsSchema - roomPrice: RoomPrice - } + room: Room steps: { [StepEnum.selectBed]: RoomStep [StepEnum.breakfast]?: RoomStep diff --git a/apps/scandic-web/types/stores/rates.ts b/apps/scandic-web/types/stores/rates.ts index 40a17eb0f..c9ced819a 100644 --- a/apps/scandic-web/types/stores/rates.ts +++ b/apps/scandic-web/types/stores/rates.ts @@ -17,6 +17,11 @@ import type { RoomsAvailability, } from "@/types/trpc/routers/hotel/roomAvailability" +export interface AvailabilityError { + details: string + error: string +} + interface Actions { closeSection: (idx: number) => () => void modifyRate: (idx: number) => () => void @@ -41,7 +46,6 @@ export interface SelectedRoom { export interface RatesState { actions: Actions activeRoom: number - allRooms: RoomConfiguration[] booking: SelectRateSearchParams filterOptions: DefaultFilterOptions[] hotelType: string | undefined @@ -52,7 +56,8 @@ export interface RatesState { rateSummary: Rate[] rooms: SelectedRoom[] roomCategories: Room[] - roomsAvailability: RoomsAvailability | null + roomConfigurations: RoomConfiguration[][] + roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined searchParams: ReadonlyURLSearchParams vat: number } diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts b/apps/scandic-web/utils/hotelSearchDetails.ts similarity index 79% rename from apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts rename to apps/scandic-web/utils/hotelSearchDetails.ts index 78919c291..174c2f00c 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts +++ b/apps/scandic-web/utils/hotelSearchDetails.ts @@ -21,22 +21,26 @@ import { type Location, } from "@/types/trpc/routers/hotel/locations" +export type ChildrenInRoom = (Child[] | null)[] | null +export type ChildrenInRoomString = (string | null)[] | null + interface HotelSearchDetails { + adultsInRoom: number[] + bookingCode?: string + childrenInRoom: ChildrenInRoom + childrenInRoomString: ChildrenInRoomString city: Location | null hotel: HotelLocation | null - selectHotelParams: SelectHotelParams & { city: string | undefined } - adultsInRoom: number[] - childrenInRoomString?: string - childrenInRoom?: Child[] - bookingCode?: string + noOfRooms: number redemption?: boolean + selectHotelParams: SelectHotelParams & { city: string | undefined } } export async function getHotelSearchDetails< T extends - | SelectHotelSearchParams - | SelectRateSearchParams - | AlternativeHotelsSearchParams, + | SelectHotelSearchParams + | SelectRateSearchParams + | AlternativeHotelsSearchParams, >( { searchParams, @@ -85,28 +89,29 @@ export async function getHotelSearchDetails< if (isAlternativeHotels && (!city || !hotel)) return notFound() let adultsInRoom: number[] = [] - let childrenInRoomString: HotelSearchDetails["childrenInRoomString"] = - undefined - let childrenInRoom: HotelSearchDetails["childrenInRoom"] = undefined + let childrenInRoom: ChildrenInRoom = null + let childrenInRoomString: ChildrenInRoomString = null const { rooms } = selectHotelParams - if (rooms && rooms.length > 0) { + if (rooms?.length) { adultsInRoom = rooms.map((room) => room.adults ?? 0) - childrenInRoomString = rooms[0].childrenInRoom - ? generateChildrenString(rooms[0].childrenInRoom) - : undefined // TODO: Handle multiple rooms - childrenInRoom = rooms[0].childrenInRoom // TODO: Handle multiple rooms + + childrenInRoom = rooms.map((room) => room.childrenInRoom ?? null) + childrenInRoomString = rooms.map((room) => + room.childrenInRoom ? generateChildrenString(room.childrenInRoom) : null + ) } return { + adultsInRoom, + bookingCode: selectHotelParams.bookingCode ?? undefined, + childrenInRoom, + childrenInRoomString, city, hotel, - selectHotelParams: { city: cityName, ...selectHotelParams }, - adultsInRoom, - childrenInRoomString, - childrenInRoom, - bookingCode: selectHotelParams.bookingCode ?? undefined, + noOfRooms: rooms?.length ?? 0, redemption: selectHotelParams.searchType === REDEMPTION, + selectHotelParams: { city: cityName, ...selectHotelParams }, } } diff --git a/apps/scandic-web/utils/specialRoomType.ts b/apps/scandic-web/utils/specialRoomType.ts new file mode 100644 index 000000000..ff73fca29 --- /dev/null +++ b/apps/scandic-web/utils/specialRoomType.ts @@ -0,0 +1,20 @@ +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import type { Packages } from "@/types/requests/packages" +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export function getSpecialRoomType( + packages: BookingConfirmation["booking"]["packages"] | Packages | null +) { + const packageCodes = packages + ?.filter((pkg) => pkg.code) + .map((pkg) => pkg.code) + if (packageCodes?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM)) { + return "accesibillity" + } else if (packageCodes?.includes(RoomPackageCodeEnum.ALLERGY_ROOM)) { + return "allergy friendly" + } else if (packageCodes?.includes(RoomPackageCodeEnum.PET_ROOM)) { + return "pet room" + } else { + return "-" + } +}