feat: bedtypes is selectable again
This commit is contained in:
committed by
Michael Zetterberg
parent
f62723c6e5
commit
afb37d0cc5
@@ -1,14 +1,12 @@
|
|||||||
import { notFound, redirect } from "next/navigation"
|
import { notFound, redirect } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
import { REDEMPTION } from "@/constants/booking"
|
|
||||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||||
import {
|
import {
|
||||||
getBreakfastPackages,
|
getBreakfastPackages,
|
||||||
getHotel,
|
getHotel,
|
||||||
getPackages,
|
|
||||||
getProfileSafely,
|
getProfileSafely,
|
||||||
getSelectedRoomAvailability,
|
getSelectedRoomsAvailability,
|
||||||
} from "@/lib/trpc/memoizedRequests"
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import HotelHeader from "@/components/HotelReservation/EnterDetails/Header"
|
import HotelHeader from "@/components/HotelReservation/EnterDetails/Header"
|
||||||
@@ -17,7 +15,6 @@ import Multiroom from "@/components/HotelReservation/EnterDetails/Room/Multiroom
|
|||||||
import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
|
import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
|
||||||
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
|
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
|
||||||
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
||||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
@@ -29,7 +26,6 @@ import { getTracking } from "./tracking"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
|
||||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
@@ -40,92 +36,65 @@ export default async function DetailsPage({
|
|||||||
searchParams,
|
searchParams,
|
||||||
}: PageArgs<LangParams, SelectRateSearchParams>) {
|
}: PageArgs<LangParams, SelectRateSearchParams>) {
|
||||||
const selectRoomParams = new URLSearchParams(searchParams)
|
const selectRoomParams = new URLSearchParams(searchParams)
|
||||||
selectRoomParams.delete("modifyRateIndex")
|
|
||||||
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
||||||
if ("modifyRateIndex" in booking) {
|
if ("modifyRateIndex" in booking) {
|
||||||
|
selectRoomParams.delete("modifyRateIndex")
|
||||||
delete booking.modifyRateIndex
|
delete booking.modifyRateIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
void getProfileSafely()
|
|
||||||
|
|
||||||
const breakfastInput = {
|
const breakfastInput = {
|
||||||
adults: 1,
|
adults: 1,
|
||||||
fromDate: booking.fromDate,
|
fromDate: booking.fromDate,
|
||||||
hotelId: booking.hotelId,
|
hotelId: booking.hotelId,
|
||||||
toDate: booking.toDate,
|
toDate: booking.toDate,
|
||||||
}
|
}
|
||||||
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
const hotelInput = {
|
||||||
const rooms: Room[] = []
|
|
||||||
|
|
||||||
for (let room of booking.rooms) {
|
|
||||||
const childrenAsString =
|
|
||||||
room.childrenInRoom && generateChildrenString(room.childrenInRoom)
|
|
||||||
|
|
||||||
const packages = room.packages
|
|
||||||
? await getPackages({
|
|
||||||
adults: room.adults,
|
|
||||||
children: room.childrenInRoom?.length,
|
|
||||||
endDate: booking.toDate,
|
|
||||||
hotelId: booking.hotelId,
|
hotelId: booking.hotelId,
|
||||||
packageCodes: room.packages,
|
// TODO: Remove this from input since it forces
|
||||||
startDate: booking.fromDate,
|
// waterfalls for no other reason than to
|
||||||
|
// set merchantInformationData.alternatePaymentOptions
|
||||||
|
// to an empty array
|
||||||
|
isCardOnlyPayment: false,
|
||||||
|
language: lang,
|
||||||
|
}
|
||||||
|
void getHotel(hotelInput)
|
||||||
|
void getBreakfastPackages(breakfastInput)
|
||||||
|
void getProfileSafely()
|
||||||
|
|
||||||
|
const roomsAvailability = await getSelectedRoomsAvailability({
|
||||||
|
booking,
|
||||||
lang,
|
lang,
|
||||||
})
|
})
|
||||||
: null
|
|
||||||
|
|
||||||
const roomAvailability = await getSelectedRoomAvailability({
|
const rooms: Room[] = []
|
||||||
adults: room.adults,
|
for (let room of roomsAvailability) {
|
||||||
bookingCode: booking.bookingCode,
|
if (!room) {
|
||||||
children: childrenAsString,
|
// TODO: This could be done in the route already.
|
||||||
counterRateCode: room.counterRateCode,
|
// (possibly also add an error case to url?)
|
||||||
hotelId: booking.hotelId,
|
// -------------------------------------------------------
|
||||||
packageCodes: room.packages,
|
|
||||||
rateCode: room.rateCode,
|
|
||||||
roomStayEndDate: booking.toDate,
|
|
||||||
roomStayStartDate: booking.fromDate,
|
|
||||||
roomTypeCode: room.roomTypeCode,
|
|
||||||
redemption: booking.searchType === REDEMPTION,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!roomAvailability) {
|
|
||||||
// redirect back to select-rate if availability call fails
|
// redirect back to select-rate if availability call fails
|
||||||
redirect(`${selectRate(lang)}?${selectRoomParams.toString()}`)
|
redirect(`${selectRate(lang)}?${selectRoomParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
rooms.push({
|
rooms.push(room)
|
||||||
bedTypes: roomAvailability.bedTypes,
|
|
||||||
breakfastIncluded: roomAvailability.breakfastIncluded,
|
|
||||||
cancellationText: roomAvailability.cancellationText,
|
|
||||||
isFlexRate: roomAvailability.isFlexRate,
|
|
||||||
mustBeGuaranteed: roomAvailability.mustBeGuaranteed,
|
|
||||||
memberMustBeGuaranteed: roomAvailability.memberMustBeGuaranteed,
|
|
||||||
packages,
|
|
||||||
rate: roomAvailability.rate,
|
|
||||||
rateDefinitionTitle: roomAvailability.rateDefinitionTitle,
|
|
||||||
rateDetails: roomAvailability.rateDetails ?? [],
|
|
||||||
rateTitle: roomAvailability.rateTitle,
|
|
||||||
rateType: roomAvailability.rateType,
|
|
||||||
roomType: roomAvailability.selectedRoom.roomType,
|
|
||||||
roomTypeCode: roomAvailability.selectedRoom.roomTypeCode,
|
|
||||||
roomRate: roomAvailability.product,
|
|
||||||
isAvailable:
|
|
||||||
roomAvailability.selectedRoom.status === AvailabilityEnum.Available,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const isCardOnlyPayment = rooms.some((room) => room?.mustBeGuaranteed)
|
|
||||||
|
|
||||||
const hotelData = await getHotel({
|
const hotelData = await getHotel(hotelInput)
|
||||||
hotelId: booking.hotelId,
|
|
||||||
isCardOnlyPayment,
|
|
||||||
language: lang,
|
|
||||||
})
|
|
||||||
const user = await getProfileSafely()
|
|
||||||
|
|
||||||
if (!hotelData || !rooms) {
|
if (!hotelData || !rooms.length) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
||||||
|
const user = await getProfileSafely()
|
||||||
|
|
||||||
|
const isCardOnlyPayment = rooms.some((room) => room.mustBeGuaranteed)
|
||||||
const { hotel } = hotelData
|
const { hotel } = hotelData
|
||||||
|
// TODO: Temp fix to avoid waterfall fetch and moving this
|
||||||
|
// logic from the route here for now
|
||||||
|
if (isCardOnlyPayment) {
|
||||||
|
hotel.merchantInformationData.alternatePaymentOptions = []
|
||||||
|
}
|
||||||
|
|
||||||
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||||
booking,
|
booking,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default async function SelectRatePage({
|
|||||||
// If someone tries to update the url with
|
// If someone tries to update the url with
|
||||||
// a bookingCode also, then we need to remove it
|
// a bookingCode also, then we need to remove it
|
||||||
if (isRedemption && searchParams.bookingCode) {
|
if (isRedemption && searchParams.bookingCode) {
|
||||||
searchParams.bookingCode = ""
|
delete searchParams.bookingCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -149,9 +149,13 @@ export default function PriceDetailsTable({
|
|||||||
<Fragment key={idx}>
|
<Fragment key={idx}>
|
||||||
<TableSection>
|
<TableSection>
|
||||||
{rooms.length > 1 && (
|
{rooms.length > 1 && (
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2}>
|
||||||
<Body textTransform="bold">
|
<Body textTransform="bold">
|
||||||
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||||
</Body>
|
</Body>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
)}
|
)}
|
||||||
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
||||||
{price && (
|
{price && (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
||||||
@@ -44,24 +43,11 @@ export default function useModifyStay({
|
|||||||
toast.error(intl.formatMessage({ id: "Failed to update your stay" }))
|
toast.error(intl.formatMessage({ id: "Failed to update your stay" }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update room details with server response data
|
// Update room details with server response data
|
||||||
|
|
||||||
const originalCheckIn = dt(bookedRoom.checkInDate)
|
|
||||||
const originalCheckOut = dt(bookedRoom.checkOutDate)
|
|
||||||
|
|
||||||
updateBookedRoom({
|
updateBookedRoom({
|
||||||
...bookedRoom,
|
...bookedRoom,
|
||||||
checkInDate: dt(updatedBooking.checkInDate)
|
checkInDate: updatedBooking.checkInDate,
|
||||||
.hour(originalCheckIn.hour())
|
checkOutDate: updatedBooking.checkOutDate,
|
||||||
.minute(originalCheckIn.minute())
|
|
||||||
.second(originalCheckIn.second())
|
|
||||||
.toDate(),
|
|
||||||
checkOutDate: dt(updatedBooking.checkOutDate)
|
|
||||||
.hour(originalCheckOut.hour())
|
|
||||||
.minute(originalCheckOut.minute())
|
|
||||||
.second(originalCheckOut.second())
|
|
||||||
.toDate(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.success(intl.formatMessage({ id: "Your stay was updated" }))
|
toast.success(intl.formatMessage({ id: "Your stay was updated" }))
|
||||||
@@ -90,22 +76,25 @@ export default function useModifyStay({
|
|||||||
let totalNewPrice = 0
|
let totalNewPrice = 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await utils.hotel.availability.room.fetch({
|
const data = await utils.hotel.availability.myStay.fetch({
|
||||||
|
booking: {
|
||||||
|
fromDate: formValues.checkInDate,
|
||||||
hotelId: bookedRoom.hotelId,
|
hotelId: bookedRoom.hotelId,
|
||||||
roomStayStartDate: formValues.checkInDate,
|
room: {
|
||||||
roomStayEndDate: formValues.checkOutDate,
|
|
||||||
adults: bookedRoom.adults,
|
adults: bookedRoom.adults,
|
||||||
children: bookedRoom.childrenAsString,
|
|
||||||
bookingCode: bookedRoom.bookingCode ?? undefined,
|
bookingCode: bookedRoom.bookingCode ?? undefined,
|
||||||
|
childrenInRoom: bookedRoom.childrenInRoom,
|
||||||
rateCode: bookedRoom.rateDefinition.rateCode,
|
rateCode: bookedRoom.rateDefinition.rateCode,
|
||||||
roomTypeCode: bookedRoom.roomTypeCode,
|
roomTypeCode: bookedRoom.roomTypeCode,
|
||||||
|
},
|
||||||
|
toDate: formValues.checkOutDate,
|
||||||
|
},
|
||||||
lang,
|
lang,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) {
|
if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) {
|
||||||
return { success: false, noAvailability: true }
|
return { success: false, noAvailability: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomPrice = 0
|
let roomPrice = 0
|
||||||
if (isLoggedIn && "member" in data.product && data.product.member) {
|
if (isLoggedIn && "member" in data.product && data.product.member) {
|
||||||
roomPrice = data.product.member.localPrice.pricePerStay
|
roomPrice = data.product.member.localPrice.pricePerStay
|
||||||
@@ -123,7 +112,6 @@ export default function useModifyStay({
|
|||||||
) {
|
) {
|
||||||
roomPrice = data.product.redemption.localPrice.additionalPricePerStay
|
roomPrice = data.product.redemption.localPrice.additionalPricePerStay
|
||||||
}
|
}
|
||||||
|
|
||||||
totalNewPrice += roomPrice
|
totalNewPrice += roomPrice
|
||||||
availabilityResults.push(data)
|
availabilityResults.push(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -167,9 +167,7 @@ export default function ModifyStay({ isLoggedIn }: ModifyStayProps) {
|
|||||||
label: isFirstStep
|
label: isFirstStep
|
||||||
? intl.formatMessage({ id: "Check availability" })
|
? intl.formatMessage({ id: "Check availability" })
|
||||||
: intl.formatMessage({ id: "Confirm" }),
|
: intl.formatMessage({ id: "Confirm" }),
|
||||||
onClick: isFirstStep
|
onClick: isFirstStep ? onCheckAvailability : handleModifyStay,
|
||||||
? () => void onCheckAvailability()
|
|
||||||
: () => void handleModifyStay(),
|
|
||||||
intent: isFirstStep ? "secondary" : "primary",
|
intent: isFirstStep ? "secondary" : "primary",
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
disabled: isLoading,
|
disabled: isLoading,
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ export function ReferenceCard({
|
|||||||
const {
|
const {
|
||||||
confirmationNumber,
|
confirmationNumber,
|
||||||
cancellationNumber,
|
cancellationNumber,
|
||||||
|
checkInDate,
|
||||||
|
checkOutDate,
|
||||||
isCancelled,
|
isCancelled,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
rateDefinition,
|
rateDefinition,
|
||||||
@@ -222,7 +224,7 @@ export function ReferenceCard({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
<p>
|
<p>
|
||||||
{`${dt(booking.checkInDate).locale(lang).format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${hotel.hotelFacts.checkin.checkInTime}`}
|
{`${dt(checkInDate).locale(lang).format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${hotel.hotelFacts.checkin.checkInTime}`}
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,7 +234,7 @@ export function ReferenceCard({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
<p>
|
<p>
|
||||||
{`${dt(booking.checkOutDate).locale(lang).format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${hotel.hotelFacts.checkin.checkOutTime}`}
|
{`${dt(checkOutDate).locale(lang).format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${hotel.hotelFacts.checkin.checkOutTime}`}
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,20 +27,13 @@ export default function MobileSummary({
|
|||||||
const scrollY = useRef(0)
|
const scrollY = useRef(0)
|
||||||
const [isSummaryOpen, setIsSummaryOpen] = useState(false)
|
const [isSummaryOpen, setIsSummaryOpen] = useState(false)
|
||||||
|
|
||||||
const {
|
const { booking, bookingRooms, roomsAvailability, rateSummary, vat } =
|
||||||
booking,
|
useRatesStore((state) => ({
|
||||||
bookingRooms,
|
|
||||||
roomsAvailability,
|
|
||||||
rateSummary,
|
|
||||||
vat,
|
|
||||||
packages,
|
|
||||||
} = useRatesStore((state) => ({
|
|
||||||
booking: state.booking,
|
booking: state.booking,
|
||||||
bookingRooms: state.booking.rooms,
|
bookingRooms: state.booking.rooms,
|
||||||
roomsAvailability: state.roomsAvailability,
|
roomsAvailability: state.roomsAvailability,
|
||||||
rateSummary: state.rateSummary,
|
rateSummary: state.rateSummary,
|
||||||
vat: state.vat,
|
vat: state.vat,
|
||||||
packages: state.packages,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function toggleSummaryOpen() {
|
function toggleSummaryOpen() {
|
||||||
@@ -78,7 +71,7 @@ export default function MobileSummary({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rooms = rateSummary.map((room, index) =>
|
const rooms = rateSummary.map((room, index) =>
|
||||||
room ? mapRate(room, index, bookingRooms, packages) : null
|
room ? mapRate(room, index, bookingRooms, room.packages) : null
|
||||||
)
|
)
|
||||||
|
|
||||||
const containsBookingCodeRate = rateSummary.find(
|
const containsBookingCodeRate = rateSummary.find(
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ export function mapRate(
|
|||||||
bookingRooms: Room[],
|
bookingRooms: Room[],
|
||||||
packages: NonNullable<Packages>
|
packages: NonNullable<Packages>
|
||||||
) {
|
) {
|
||||||
const roomPackages = room.packages
|
|
||||||
.map((code) => packages.find((pkg) => pkg.code === code))
|
|
||||||
.filter((pkg): pkg is NonNullable<typeof pkg> => Boolean(pkg))
|
|
||||||
|
|
||||||
const rate = {
|
const rate = {
|
||||||
adults: bookingRooms[index].adults,
|
adults: bookingRooms[index].adults,
|
||||||
cancellationText: room.product.rateDefinition?.cancellationText ?? "",
|
cancellationText: room.product.rateDefinition?.cancellationText ?? "",
|
||||||
@@ -39,7 +35,7 @@ export function mapRate(
|
|||||||
},
|
},
|
||||||
roomRate: room.product,
|
roomRate: room.product,
|
||||||
roomType: room.roomType,
|
roomType: room.roomType,
|
||||||
packages: roomPackages,
|
packages,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("corporateCheque" in room.product) {
|
if ("corporateCheque" in room.product) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
bookingCode,
|
bookingCode,
|
||||||
bookingRooms,
|
bookingRooms,
|
||||||
dates,
|
dates,
|
||||||
petRoomPackage,
|
isFetchingPackages,
|
||||||
rateSummary,
|
rateSummary,
|
||||||
roomsAvailability,
|
roomsAvailability,
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -41,7 +41,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
checkInDate: state.booking.fromDate,
|
checkInDate: state.booking.fromDate,
|
||||||
checkOutDate: state.booking.toDate,
|
checkOutDate: state.booking.toDate,
|
||||||
},
|
},
|
||||||
petRoomPackage: state.petRoomPackage,
|
isFetchingPackages: state.rooms.some((room) => room.isFetchingPackages),
|
||||||
rateSummary: state.rateSummary,
|
rateSummary: state.rateSummary,
|
||||||
roomsAvailability: state.roomsAvailability,
|
roomsAvailability: state.roomsAvailability,
|
||||||
searchParams: state.searchParams,
|
searchParams: state.searchParams,
|
||||||
@@ -123,7 +123,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rateSummary.length) {
|
if (!rateSummary.length || isFetchingPackages) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +149,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
const totalPriceToShow = getTotalPrice(
|
const totalPriceToShow = getTotalPrice(
|
||||||
mainRoomProduct,
|
mainRoomProduct,
|
||||||
rateSummary,
|
rateSummary,
|
||||||
isUserLoggedIn,
|
isUserLoggedIn
|
||||||
petRoomPackage
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const rateProduct = rateSummary.find((rate) => rate?.product)?.product
|
const rateProduct = rateSummary.find((rate) => rate?.product)?.product
|
||||||
@@ -248,7 +247,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
const { features, packages: roomPackages, product } = rate
|
const { packages: roomPackages, product } = rate
|
||||||
|
|
||||||
const memberExists = "member" in product && product.member
|
const memberExists = "member" in product && product.member
|
||||||
const publicExists = "public" in product && product.public
|
const publicExists = "public" in product && product.public
|
||||||
@@ -266,21 +265,15 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSelectedPetRoom = roomPackages.includes(
|
const hasSelectedPetRoom = roomPackages.find(
|
||||||
RoomPackageCodeEnum.PET_ROOM
|
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
)
|
)
|
||||||
if (!hasSelectedPetRoom) {
|
if (!hasSelectedPetRoom) {
|
||||||
return total + price
|
return total + price
|
||||||
}
|
}
|
||||||
const isPetRoom = features.find(
|
return (
|
||||||
(feature) =>
|
total + price + hasSelectedPetRoom.localPrice.totalPrice
|
||||||
feature.code === RoomPackageCodeEnum.PET_ROOM
|
|
||||||
)
|
)
|
||||||
const petRoomPrice =
|
|
||||||
isPetRoom && petRoomPackage
|
|
||||||
? Number(petRoomPackage.localPrice.totalPrice)
|
|
||||||
: 0
|
|
||||||
return total + price + petRoomPrice
|
|
||||||
}, 0),
|
}, 0),
|
||||||
currency: mainRoomCurrency,
|
currency: mainRoomCurrency,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import type { Price } from "@/types/components/hotelReservation/price"
|
import type { Price } from "@/types/components/hotelReservation/price"
|
||||||
import {
|
|
||||||
type RoomPackage,
|
|
||||||
RoomPackageCodeEnum,
|
|
||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
|
||||||
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
import type { Packages } from "@/types/requests/packages"
|
|
||||||
import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
export function calculateTotalPrice(
|
export function calculateTotalPrice(
|
||||||
selectedRateSummary: Rate[],
|
selectedRateSummary: Rate[],
|
||||||
isUserLoggedIn: boolean,
|
isUserLoggedIn: boolean
|
||||||
petRoomPackage: RoomPackage | undefined
|
|
||||||
) {
|
) {
|
||||||
return selectedRateSummary.reduce<Price>(
|
return selectedRateSummary.reduce<Price>(
|
||||||
(total, room, idx) => {
|
(total, room, idx) => {
|
||||||
@@ -32,35 +26,26 @@ export function calculateTotalPrice(
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPetRoom = room.features.find(
|
const packagesPrice = room.packages.reduce(
|
||||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
(total, pkg) => {
|
||||||
|
total.local = total.local + pkg.localPrice.totalPrice
|
||||||
|
if (pkg.requestedPrice.totalPrice) {
|
||||||
|
total.requested = total.requested + pkg.requestedPrice.totalPrice
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
{ local: 0, requested: 0 }
|
||||||
)
|
)
|
||||||
let petRoomPriceLocal = 0
|
|
||||||
if (
|
|
||||||
petRoomPackage &&
|
|
||||||
isPetRoom &&
|
|
||||||
room.packages.includes(RoomPackageCodeEnum.PET_ROOM)
|
|
||||||
) {
|
|
||||||
petRoomPriceLocal = Number(petRoomPackage.localPrice.totalPrice)
|
|
||||||
}
|
|
||||||
let petRoomPriceRequested = 0
|
|
||||||
if (
|
|
||||||
petRoomPackage &&
|
|
||||||
isPetRoom &&
|
|
||||||
room.packages.includes(RoomPackageCodeEnum.PET_ROOM)
|
|
||||||
) {
|
|
||||||
petRoomPriceRequested = Number(petRoomPackage.requestedPrice.totalPrice)
|
|
||||||
}
|
|
||||||
|
|
||||||
total.local.currency = rate.localPrice.currency
|
total.local.currency = rate.localPrice.currency
|
||||||
total.local.price =
|
total.local.price =
|
||||||
total.local.price + rate.localPrice.pricePerStay + petRoomPriceLocal
|
total.local.price + rate.localPrice.pricePerStay + packagesPrice.local
|
||||||
|
|
||||||
if (rate.localPrice.regularPricePerStay) {
|
if (rate.localPrice.regularPricePerStay) {
|
||||||
total.local.regularPrice =
|
total.local.regularPrice =
|
||||||
(total.local.regularPrice || 0) +
|
(total.local.regularPrice || 0) +
|
||||||
rate.localPrice.regularPricePerStay +
|
rate.localPrice.regularPricePerStay +
|
||||||
petRoomPriceLocal
|
packagesPrice.local
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rate.requestedPrice) {
|
if (rate.requestedPrice) {
|
||||||
@@ -78,13 +63,13 @@ export function calculateTotalPrice(
|
|||||||
total.requested.price =
|
total.requested.price =
|
||||||
total.requested.price +
|
total.requested.price +
|
||||||
rate.requestedPrice.pricePerStay +
|
rate.requestedPrice.pricePerStay +
|
||||||
petRoomPriceRequested
|
packagesPrice.requested
|
||||||
|
|
||||||
if (rate.requestedPrice.regularPricePerStay) {
|
if (rate.requestedPrice.regularPricePerStay) {
|
||||||
total.requested.regularPrice =
|
total.requested.regularPrice =
|
||||||
(total.requested.regularPrice || 0) +
|
(total.requested.regularPrice || 0) +
|
||||||
rate.requestedPrice.regularPricePerStay +
|
rate.requestedPrice.regularPricePerStay +
|
||||||
petRoomPriceRequested
|
packagesPrice.requested
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,8 +184,7 @@ export function calculateCorporateChequePrice(selectedRateSummary: Rate[]) {
|
|||||||
export function getTotalPrice(
|
export function getTotalPrice(
|
||||||
mainRoomProduct: Rate | null,
|
mainRoomProduct: Rate | null,
|
||||||
rateSummary: Array<Rate | null>,
|
rateSummary: Array<Rate | null>,
|
||||||
isUserLoggedIn: boolean,
|
isUserLoggedIn: boolean
|
||||||
petRoomPackage: NonNullable<Packages>[number] | undefined
|
|
||||||
): Price | null {
|
): Price | null {
|
||||||
const summaryArray = rateSummary.filter((rate): rate is Rate => rate !== null)
|
const summaryArray = rateSummary.filter((rate): rate is Rate => rate !== null)
|
||||||
|
|
||||||
@@ -209,7 +193,7 @@ export function getTotalPrice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!mainRoomProduct) {
|
if (!mainRoomProduct) {
|
||||||
return calculateTotalPrice(summaryArray, isUserLoggedIn, petRoomPackage)
|
return calculateTotalPrice(summaryArray, isUserLoggedIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { product } = mainRoomProduct
|
const { product } = mainRoomProduct
|
||||||
@@ -222,5 +206,5 @@ export function getTotalPrice(
|
|||||||
return calculateVoucherPrice(summaryArray)
|
return calculateVoucherPrice(summaryArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculateTotalPrice(summaryArray, isUserLoggedIn, petRoomPackage)
|
return calculateTotalPrice(summaryArray, isUserLoggedIn)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
import { useRatesStore } from "@/stores/select-rate"
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
@@ -15,22 +16,31 @@ import { useRoomContext } from "@/contexts/SelectRate/Room"
|
|||||||
|
|
||||||
import styles from "./selectedRoomPanel.module.css"
|
import styles from "./selectedRoomPanel.module.css"
|
||||||
|
|
||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
import { RateEnum } from "@/types/enums/rate"
|
import { RateEnum } from "@/types/enums/rate"
|
||||||
|
|
||||||
export default function SelectedRoomPanel() {
|
export default function SelectedRoomPanel() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { isUserLoggedIn, roomCategories, rooms } = useRatesStore((state) => ({
|
const { dates, isUserLoggedIn, roomCategories, rooms } = useRatesStore(
|
||||||
|
(state) => ({
|
||||||
|
dates: {
|
||||||
|
from: state.booking.fromDate,
|
||||||
|
to: state.booking.toDate,
|
||||||
|
},
|
||||||
isUserLoggedIn: state.isUserLoggedIn,
|
isUserLoggedIn: state.isUserLoggedIn,
|
||||||
roomCategories: state.roomCategories,
|
roomCategories: state.roomCategories,
|
||||||
rooms: state.rooms,
|
rooms: state.rooms,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
actions: { modifyRate },
|
actions: { modifyRate },
|
||||||
isMainRoom,
|
isMainRoom,
|
||||||
roomNr,
|
roomNr,
|
||||||
|
selectedPackages,
|
||||||
selectedRate,
|
selectedRate,
|
||||||
} = useRoomContext()
|
} = useRoomContext()
|
||||||
|
const nights = dt(dates.to).diff(dt(dates.from), "days")
|
||||||
|
|
||||||
const images = roomCategories.find((roomCategory) =>
|
const images = roomCategories.find((roomCategory) =>
|
||||||
roomCategory.roomTypes.some(
|
roomCategory.roomTypes.some(
|
||||||
@@ -60,8 +70,16 @@ export default function SelectedRoomPanel() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let petRoomPrice = 0
|
||||||
|
const petRoomPackageSelected = selectedPackages.find(
|
||||||
|
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
)
|
||||||
|
if (petRoomPackageSelected) {
|
||||||
|
petRoomPrice = petRoomPackageSelected.localPrice.totalPrice / nights
|
||||||
|
}
|
||||||
|
|
||||||
|
const night = intl.formatMessage({ id: "night" })
|
||||||
let selectedProduct
|
let selectedProduct
|
||||||
let isPerNight = true
|
|
||||||
if (
|
if (
|
||||||
isUserLoggedIn &&
|
isUserLoggedIn &&
|
||||||
isMainRoom &&
|
isMainRoom &&
|
||||||
@@ -69,19 +87,17 @@ export default function SelectedRoomPanel() {
|
|||||||
selectedRate.product.member
|
selectedRate.product.member
|
||||||
) {
|
) {
|
||||||
const { localPrice } = selectedRate.product.member
|
const { localPrice } = selectedRate.product.member
|
||||||
selectedProduct = `${localPrice.pricePerNight} ${localPrice.currency}`
|
selectedProduct = `${localPrice.pricePerNight + petRoomPrice} ${localPrice.currency} / ${night}`
|
||||||
} else if ("public" in selectedRate.product && selectedRate.product.public) {
|
} else if ("public" in selectedRate.product && selectedRate.product.public) {
|
||||||
const { localPrice } = selectedRate.product.public
|
const { localPrice } = selectedRate.product.public
|
||||||
selectedProduct = `${localPrice.pricePerNight} ${localPrice.currency}`
|
selectedProduct = `${localPrice.pricePerNight + petRoomPrice} ${localPrice.currency} / ${night}`
|
||||||
} else if ("corporateCheque" in selectedRate.product) {
|
} else if ("corporateCheque" in selectedRate.product) {
|
||||||
isPerNight = false
|
|
||||||
const { localPrice } = selectedRate.product.corporateCheque
|
const { localPrice } = selectedRate.product.corporateCheque
|
||||||
selectedProduct = `${localPrice.numberOfCheques} ${CurrencyEnum.CC}`
|
selectedProduct = `${localPrice.numberOfCheques} ${CurrencyEnum.CC}`
|
||||||
if (localPrice.additionalPricePerStay && localPrice.currency) {
|
if (localPrice.additionalPricePerStay && localPrice.currency) {
|
||||||
selectedProduct = `${selectedProduct} + ${localPrice.additionalPricePerStay} ${localPrice.currency}`
|
selectedProduct = `${selectedProduct} + ${localPrice.additionalPricePerStay} ${localPrice.currency}`
|
||||||
}
|
}
|
||||||
} else if ("voucher" in selectedRate.product) {
|
} else if ("voucher" in selectedRate.product) {
|
||||||
isPerNight = false
|
|
||||||
selectedProduct = `${selectedRate.product.voucher.numberOfVouchers} ${CurrencyEnum.Voucher}`
|
selectedProduct = `${selectedRate.product.voucher.numberOfVouchers} ${CurrencyEnum.Voucher}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,9 +125,7 @@ export default function SelectedRoomPanel() {
|
|||||||
<Body color="uiTextMediumContrast">
|
<Body color="uiTextMediumContrast">
|
||||||
{getRateTitle(selectedRate.product.rate)}
|
{getRateTitle(selectedRate.product.rate)}
|
||||||
</Body>
|
</Body>
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">{selectedProduct}</Body>
|
||||||
{`${selectedProduct}${isPerNight ? "/" + intl.formatMessage({ id: "night" }) : ""}`}
|
|
||||||
</Body>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
{images?.[0]?.imageSizes?.tiny ? (
|
{images?.[0]?.imageSizes?.tiny ? (
|
||||||
|
|||||||
@@ -17,12 +17,16 @@ export default function NoAvailabilityAlert() {
|
|||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
|
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
|
||||||
const { rooms } = useRoomContext()
|
const { isFetchingPackages, rooms } = useRoomContext()
|
||||||
|
|
||||||
const noAvailableRooms = rooms.every(
|
const noAvailableRooms = rooms.every(
|
||||||
(roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable
|
(roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isFetchingPackages) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (noAvailableRooms) {
|
if (noAvailableRooms) {
|
||||||
const text = intl.formatMessage({
|
const text = intl.formatMessage({
|
||||||
id: "There are no rooms available that match your request.",
|
id: "There are no rooms available that match your request.",
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Checkbox as AriaCheckbox } from "react-aria-components"
|
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import styles from "./checkbox.module.css"
|
|
||||||
|
|
||||||
import type { MaterialSymbolProps } from "react-material-symbols"
|
|
||||||
|
|
||||||
interface CheckboxProps {
|
|
||||||
name: string
|
|
||||||
value: string
|
|
||||||
isSelected: boolean
|
|
||||||
iconName: MaterialSymbolProps["icon"]
|
|
||||||
isDisabled: boolean
|
|
||||||
onChange: (value: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Checkbox({
|
|
||||||
isSelected,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
iconName,
|
|
||||||
isDisabled,
|
|
||||||
onChange,
|
|
||||||
}: CheckboxProps) {
|
|
||||||
return (
|
|
||||||
<AriaCheckbox
|
|
||||||
className={styles.checkboxWrapper}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onChange={() => onChange(value)}
|
|
||||||
>
|
|
||||||
{({ isSelected }) => (
|
|
||||||
<>
|
|
||||||
<span className={styles.checkbox}>
|
|
||||||
{isSelected && <MaterialIcon icon="check" color="Icon/Inverted" />}
|
|
||||||
</span>
|
|
||||||
<Typography
|
|
||||||
variant="Body/Paragraph/mdRegular"
|
|
||||||
className={styles.text}
|
|
||||||
>
|
|
||||||
<span>{name}</span>
|
|
||||||
</Typography>
|
|
||||||
{iconName ? (
|
|
||||||
<MaterialIcon icon={iconName} color="Icon/Default" />
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AriaCheckbox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import {
|
|
||||||
Button as AriaButton,
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
Popover,
|
|
||||||
} from "react-aria-components"
|
|
||||||
import { Controller, useForm } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { Button } from "@scandic-hotels/design-system/Button"
|
|
||||||
import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { useRatesStore } from "@/stores/select-rate"
|
|
||||||
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
|
||||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
|
||||||
|
|
||||||
import Checkbox from "./Checkbox"
|
|
||||||
import { getIconNameByPackageCode } from "./utils"
|
|
||||||
|
|
||||||
import styles from "./roomPackageFilter.module.css"
|
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
selectedPackages: RoomPackageCodeEnum[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RoomPackageFilter() {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const packageOptions = useRatesStore((state) => state.packageOptions)
|
|
||||||
const {
|
|
||||||
actions: { togglePackages },
|
|
||||||
selectedPackages,
|
|
||||||
} = useRoomContext()
|
|
||||||
|
|
||||||
const { setValue, handleSubmit, control } = useForm<FormValues>({
|
|
||||||
defaultValues: {
|
|
||||||
selectedPackages: selectedPackages,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue("selectedPackages", selectedPackages)
|
|
||||||
}, [selectedPackages, setValue])
|
|
||||||
|
|
||||||
function onSubmit(data: FormValues) {
|
|
||||||
togglePackages(data.selectedPackages)
|
|
||||||
setIsOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.roomPackageFilter}>
|
|
||||||
{selectedPackages.map((pkg) => (
|
|
||||||
<AriaButton
|
|
||||||
key={pkg}
|
|
||||||
onPress={() => {
|
|
||||||
const packages = selectedPackages.filter((s) => s !== pkg)
|
|
||||||
togglePackages(packages)
|
|
||||||
}}
|
|
||||||
className={styles.activeFilterButton}
|
|
||||||
>
|
|
||||||
<MaterialIcon
|
|
||||||
icon={getIconNameByPackageCode(pkg)}
|
|
||||||
size={16}
|
|
||||||
color="Icon/Interactive/Default"
|
|
||||||
/>
|
|
||||||
<MaterialIcon
|
|
||||||
icon="close"
|
|
||||||
size={16}
|
|
||||||
color="Icon/Interactive/Default"
|
|
||||||
/>
|
|
||||||
</AriaButton>
|
|
||||||
))}
|
|
||||||
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<ChipButton variant="Outlined">
|
|
||||||
{intl.formatMessage({ id: "Room preferences" })}
|
|
||||||
<MaterialIcon
|
|
||||||
icon="keyboard_arrow_down"
|
|
||||||
size={20}
|
|
||||||
color="CurrentColor"
|
|
||||||
/>
|
|
||||||
</ChipButton>
|
|
||||||
<Popover placement="bottom end">
|
|
||||||
<Dialog className={styles.dialog}>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="selectedPackages"
|
|
||||||
render={({ field }) => (
|
|
||||||
<div>
|
|
||||||
{packageOptions.map((option) => {
|
|
||||||
const isPetRoom =
|
|
||||||
option.code === RoomPackageCodeEnum.PET_ROOM
|
|
||||||
|
|
||||||
const isAllergyRoom =
|
|
||||||
option.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
|
||||||
|
|
||||||
const hasPetRoom = field.value.includes(
|
|
||||||
RoomPackageCodeEnum.PET_ROOM
|
|
||||||
)
|
|
||||||
|
|
||||||
const hasAllergyRoom = field.value.includes(
|
|
||||||
RoomPackageCodeEnum.ALLERGY_ROOM
|
|
||||||
)
|
|
||||||
|
|
||||||
const isDisabled =
|
|
||||||
(isPetRoom && hasAllergyRoom) ||
|
|
||||||
(isAllergyRoom && hasPetRoom)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Checkbox
|
|
||||||
key={option.code}
|
|
||||||
name={option.description}
|
|
||||||
value={option.code}
|
|
||||||
iconName={getIconNameByPackageCode(option.code)}
|
|
||||||
isSelected={field.value.includes(option.code)}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onChange={() => {
|
|
||||||
const isSelected = field.value.includes(
|
|
||||||
option.code
|
|
||||||
)
|
|
||||||
const newValue = isSelected
|
|
||||||
? field.value.filter(
|
|
||||||
(pkg) => pkg !== option.code
|
|
||||||
)
|
|
||||||
: [...field.value, option.code]
|
|
||||||
field.onChange(newValue)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{option.code === RoomPackageCodeEnum.PET_ROOM && (
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<p className={styles.additionalInformation}>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
b: (str) => (
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
styles.additionalInformationPrice
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className={styles.footer}>
|
|
||||||
<Divider color="borderDividerSubtle" />
|
|
||||||
<div className={styles.buttonContainer}>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
||||||
<Button
|
|
||||||
variant="Text"
|
|
||||||
size="Small"
|
|
||||||
onPress={() => {
|
|
||||||
togglePackages([])
|
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Clear" })}
|
|
||||||
</Button>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
||||||
<Button variant="Tertiary" size="Small" type="submit">
|
|
||||||
{intl.formatMessage({ id: "Apply" })}
|
|
||||||
</Button>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Dialog>
|
|
||||||
</Popover>
|
|
||||||
</DialogTrigger>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
import { useRatesStore } from "@/stores/select-rate"
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
import Select from "@/components/TempDesignSystem/Select"
|
import Select from "@/components/TempDesignSystem/Select"
|
||||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import styles from "./bookingCodeFilter.module.css"
|
import styles from "./bookingCodeFilter.module.css"
|
||||||
|
|
||||||
@@ -16,12 +18,16 @@ import { RateTypeEnum } from "@/types/enums/rateType"
|
|||||||
|
|
||||||
export default function BookingCodeFilter() {
|
export default function BookingCodeFilter() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const utils = trpc.useUtils()
|
||||||
const {
|
const {
|
||||||
actions: { selectFilter },
|
actions: { appendRegularRates, selectFilter },
|
||||||
selectedFilter,
|
bookingRoom,
|
||||||
rooms,
|
rooms,
|
||||||
|
selectedFilter,
|
||||||
|
selectedPackages,
|
||||||
} = useRoomContext()
|
} = useRoomContext()
|
||||||
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
|
const booking = useRatesStore((state) => state.booking)
|
||||||
|
|
||||||
const bookingCodeFilterItems = [
|
const bookingCodeFilterItems = [
|
||||||
{
|
{
|
||||||
@@ -38,11 +44,28 @@ export default function BookingCodeFilter() {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function handleChangeFilter(selectedFilter: Key) {
|
async function handleChangeFilter(selectedFilter: Key) {
|
||||||
selectFilter(selectedFilter as BookingCodeFilterEnum)
|
selectFilter(selectedFilter as BookingCodeFilterEnum)
|
||||||
|
const room = await utils.hotel.availability.selectRate.room.fetch({
|
||||||
|
booking: {
|
||||||
|
...booking,
|
||||||
|
room: {
|
||||||
|
...bookingRoom,
|
||||||
|
bookingCode:
|
||||||
|
selectedFilter === BookingCodeFilterEnum.Discounted
|
||||||
|
? booking.bookingCode
|
||||||
|
: undefined,
|
||||||
|
packages: selectedPackages.map((pkg) => pkg.code),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lang,
|
||||||
|
})
|
||||||
|
appendRegularRates(room?.roomConfigurations)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideFilterDespiteBookingCode = rooms.every((room) =>
|
const hideFilterDespiteBookingCode =
|
||||||
|
rooms.length &&
|
||||||
|
rooms.every((room) =>
|
||||||
room.products.every((product) => {
|
room.products.every((product) => {
|
||||||
const isRedemption = Array.isArray(product)
|
const isRedemption = Array.isArray(product)
|
||||||
if (isRedemption) {
|
if (isRedemption) {
|
||||||
@@ -56,7 +79,10 @@ export default function BookingCodeFilter() {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if ((bookingCode && hideFilterDespiteBookingCode) || !bookingCode) {
|
if (
|
||||||
|
(booking.bookingCode && hideFilterDespiteBookingCode) ||
|
||||||
|
!booking.bookingCode
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import styles from "./petRoom.module.css"
|
||||||
|
|
||||||
|
export default function PetRoomMessage() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const { petRoomPackage } = useRoomContext()
|
||||||
|
if (!petRoomPackage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p className={styles.additionalInformation}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
b: (str) => (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<span className={styles.additionalInformationPrice}>{str}</span>
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
price: formatPrice(
|
||||||
|
intl,
|
||||||
|
petRoomPackage.localPrice.price,
|
||||||
|
petRoomPackage.localPrice.currency
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.additionalInformation {
|
||||||
|
color: var(--Text-Tertiary);
|
||||||
|
padding: var(--Space-x1) var(--Space-x15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.additionalInformationPrice {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"use client"
|
||||||
|
import { Fragment } from "react"
|
||||||
|
import { Checkbox, CheckboxGroup } from "react-aria-components"
|
||||||
|
import { Controller, useFormContext } from "react-hook-form"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
|
import { getIconNameByPackageCode } from "../../utils"
|
||||||
|
import PetRoomMessage from "./PetRoomMessage"
|
||||||
|
import {
|
||||||
|
checkIsAllergyRoom,
|
||||||
|
checkIsPetRoom,
|
||||||
|
includesAllergyRoom,
|
||||||
|
includesPetRoom,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
|
import styles from "./checkbox.module.css"
|
||||||
|
|
||||||
|
import type { FormValues } from "../formValues"
|
||||||
|
|
||||||
|
export default function Checkboxes() {
|
||||||
|
const packageOptions = useRatesStore((state) => state.packageOptions)
|
||||||
|
const { control } = useFormContext<FormValues>()
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="selectedPackages"
|
||||||
|
render={({ field }) => {
|
||||||
|
const allergyRoomSelected = includesAllergyRoom(field.value)
|
||||||
|
const petRoomSelected = includesPetRoom(field.value)
|
||||||
|
return (
|
||||||
|
<CheckboxGroup {...field}>
|
||||||
|
<div>
|
||||||
|
{packageOptions.map((option) => {
|
||||||
|
const isAllergyRoom = checkIsAllergyRoom(option.code)
|
||||||
|
const isPetRoom = checkIsPetRoom(option.code)
|
||||||
|
const isDisabled =
|
||||||
|
(isPetRoom && allergyRoomSelected) ||
|
||||||
|
(isAllergyRoom && petRoomSelected)
|
||||||
|
|
||||||
|
const isSelected = field.value.includes(option.code)
|
||||||
|
const iconName = getIconNameByPackageCode(option.code)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={option.code}>
|
||||||
|
<Checkbox
|
||||||
|
key={option.code}
|
||||||
|
className={styles.checkboxWrapper}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
value={option.code}
|
||||||
|
>
|
||||||
|
<span className={styles.checkbox}>
|
||||||
|
{isSelected ? (
|
||||||
|
<MaterialIcon icon="check" color="Icon/Inverted" />
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
<Typography
|
||||||
|
className={styles.text}
|
||||||
|
variant="Body/Paragraph/mdRegular"
|
||||||
|
>
|
||||||
|
<span>{option.description}</span>
|
||||||
|
</Typography>
|
||||||
|
{iconName ? (
|
||||||
|
<MaterialIcon icon={iconName} color="Icon/Default" />
|
||||||
|
) : null}
|
||||||
|
</Checkbox>
|
||||||
|
{isPetRoom ? <PetRoomMessage /> : null}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CheckboxGroup>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { PackageEnum } from "@/types/requests/packages"
|
||||||
|
|
||||||
|
export function includesAllergyRoom(codes: PackageEnum[]) {
|
||||||
|
return codes.includes(RoomPackageCodeEnum.ALLERGY_ROOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function includesPetRoom(codes: PackageEnum[]) {
|
||||||
|
return codes.includes(RoomPackageCodeEnum.PET_ROOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkIsAllergyRoom(code: PackageEnum) {
|
||||||
|
return code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkIsPetRoom(code: PackageEnum) {
|
||||||
|
return code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.footer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
padding: 0 var(--Space-x15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { PackageEnum } from "@/types/requests/packages"
|
||||||
|
|
||||||
|
export type FormValues = {
|
||||||
|
selectedPackages: PackageEnum[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"use client"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import Checkboxes from "./Checkboxes"
|
||||||
|
|
||||||
|
import styles from "./form.module.css"
|
||||||
|
|
||||||
|
import type { PackageEnum } from "@/types/requests/packages"
|
||||||
|
import type { FormValues } from "./formValues"
|
||||||
|
|
||||||
|
export default function Form({ close }: { close: VoidFunction }) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const utils = trpc.useUtils()
|
||||||
|
|
||||||
|
const {
|
||||||
|
actions: { removeSelectedPackages, selectPackages, updateRooms },
|
||||||
|
bookingRoom,
|
||||||
|
selectedPackages,
|
||||||
|
} = useRoomContext()
|
||||||
|
const booking = useRatesStore((state) => state.booking)
|
||||||
|
|
||||||
|
const methods = useForm<FormValues>({
|
||||||
|
values: {
|
||||||
|
selectedPackages: selectedPackages.map((pkg) => pkg.code),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getFilteredRates(packages: PackageEnum[]) {
|
||||||
|
const filterRates = await utils.hotel.availability.selectRate.room.fetch({
|
||||||
|
booking: {
|
||||||
|
fromDate: booking.fromDate,
|
||||||
|
hotelId: booking.hotelId,
|
||||||
|
searchType: booking.searchType,
|
||||||
|
toDate: booking.toDate,
|
||||||
|
room: {
|
||||||
|
...bookingRoom,
|
||||||
|
bookingCode: bookingRoom.rateCode
|
||||||
|
? bookingRoom.bookingCode
|
||||||
|
: booking.bookingCode,
|
||||||
|
packages,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lang,
|
||||||
|
})
|
||||||
|
updateRooms(filterRates?.roomConfigurations)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectedPackages() {
|
||||||
|
removeSelectedPackages()
|
||||||
|
close()
|
||||||
|
getFilteredRates([])
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit(data: FormValues) {
|
||||||
|
selectPackages(data.selectedPackages)
|
||||||
|
close()
|
||||||
|
getFilteredRates(data.selectedPackages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
|
<Checkboxes />
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<Divider color="borderDividerSubtle" />
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<Button
|
||||||
|
onPress={clearSelectedPackages}
|
||||||
|
size="Small"
|
||||||
|
variant="Text"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Clear" })}
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<Button variant="Tertiary" size="Small" type="submit">
|
||||||
|
{intl.formatMessage({ id: "Apply" })}
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"use client"
|
||||||
|
import { useState } from "react"
|
||||||
|
import {
|
||||||
|
Button as AriaButton,
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
Popover,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import Form from "./Form"
|
||||||
|
import { getIconNameByPackageCode } from "./utils"
|
||||||
|
|
||||||
|
import styles from "./roomPackageFilter.module.css"
|
||||||
|
|
||||||
|
import type { PackageEnum } from "@/types/requests/packages"
|
||||||
|
|
||||||
|
export default function RoomPackageFilter() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const utils = trpc.useUtils()
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
actions: { removeSelectedPackage, updateRooms },
|
||||||
|
bookingRoom,
|
||||||
|
selectedPackages,
|
||||||
|
} = useRoomContext()
|
||||||
|
const booking = useRatesStore((state) => state.booking)
|
||||||
|
|
||||||
|
async function deleteSelectedPackage(code: PackageEnum) {
|
||||||
|
removeSelectedPackage(code)
|
||||||
|
const filterRates = await utils.hotel.availability.selectRate.room.fetch({
|
||||||
|
booking: {
|
||||||
|
fromDate: booking.fromDate,
|
||||||
|
hotelId: booking.hotelId,
|
||||||
|
searchType: booking.searchType,
|
||||||
|
toDate: booking.toDate,
|
||||||
|
room: {
|
||||||
|
...bookingRoom,
|
||||||
|
bookingCode: bookingRoom.rateCode
|
||||||
|
? bookingRoom.bookingCode
|
||||||
|
: booking.bookingCode,
|
||||||
|
packages: selectedPackages
|
||||||
|
.filter((pkg) => pkg.code !== code)
|
||||||
|
.map((pkg) => pkg.code),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lang,
|
||||||
|
})
|
||||||
|
updateRooms(filterRates?.roomConfigurations)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.roomPackageFilter}>
|
||||||
|
{selectedPackages.map((pkg) => (
|
||||||
|
<AriaButton
|
||||||
|
key={pkg.code}
|
||||||
|
className={styles.activeFilterButton}
|
||||||
|
onPress={() => deleteSelectedPackage(pkg.code)}
|
||||||
|
>
|
||||||
|
<MaterialIcon
|
||||||
|
icon={getIconNameByPackageCode(pkg.code)}
|
||||||
|
size={16}
|
||||||
|
color="Icon/Interactive/Default"
|
||||||
|
/>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="close"
|
||||||
|
size={16}
|
||||||
|
color="Icon/Interactive/Default"
|
||||||
|
/>
|
||||||
|
</AriaButton>
|
||||||
|
))}
|
||||||
|
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<ChipButton variant="Outlined">
|
||||||
|
{intl.formatMessage({ id: "Room preferences" })}
|
||||||
|
<MaterialIcon
|
||||||
|
icon="keyboard_arrow_down"
|
||||||
|
size={20}
|
||||||
|
color="CurrentColor"
|
||||||
|
/>
|
||||||
|
</ChipButton>
|
||||||
|
<Popover placement="bottom end">
|
||||||
|
<Dialog className={styles.dialog}>
|
||||||
|
<Form close={() => setIsOpen(false)} />
|
||||||
|
</Dialog>
|
||||||
|
</Popover>
|
||||||
|
</DialogTrigger>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,33 +3,6 @@
|
|||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Space-x1);
|
|
||||||
padding: var(--Space-x2);
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
border-radius: var(--Corner-radius-md);
|
|
||||||
background-color: var(--Surface-Primary-Default);
|
|
||||||
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
max-width: 340px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Space-x1);
|
|
||||||
padding: 0 var(--Space-x15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.additionalInformation {
|
|
||||||
color: var(--Text-Tertiary);
|
|
||||||
padding: var(--Space-x1) var(--Space-x15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.additionalInformationPrice {
|
|
||||||
color: var(--Text-Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
.activeFilterButton {
|
.activeFilterButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -42,8 +15,14 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonContainer {
|
.dialog {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
gap: var(--Space-x1);
|
||||||
align-items: center;
|
padding: var(--Space-x2);
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
background-color: var(--Surface-Primary-Default);
|
||||||
|
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 340px;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { MaterialSymbolProps } from "react-material-symbols"
|
import type { SymbolCodepoints } from "react-material-symbols"
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { PackageEnum } from "@/types/requests/packages"
|
||||||
|
|
||||||
export function getIconNameByPackageCode(
|
export function getIconNameByPackageCode(
|
||||||
packageCode: RoomPackageCodeEnum
|
packageCode: PackageEnum
|
||||||
): MaterialSymbolProps["icon"] {
|
): SymbolCodepoints {
|
||||||
switch (packageCode) {
|
switch (packageCode) {
|
||||||
case RoomPackageCodeEnum.PET_ROOM:
|
case RoomPackageCodeEnum.PET_ROOM:
|
||||||
return "pets"
|
return "pets"
|
||||||
@@ -5,15 +5,15 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
|||||||
|
|
||||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
|
|
||||||
import BookingCodeFilter from "../BookingCodeFilter"
|
import BookingCodeFilter from "./BookingCodeFilter"
|
||||||
import RoomPackageFilter from "../RoomPackageFilter"
|
import RoomPackageFilter from "./RoomPackageFilter"
|
||||||
|
|
||||||
import styles from "./roomsHeader.module.css"
|
import styles from "./roomsHeader.module.css"
|
||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
|
|
||||||
export default function RoomsHeader() {
|
export default function RoomsHeader() {
|
||||||
const { rooms, totalRooms } = useRoomContext()
|
const { isFetchingPackages, rooms, totalRooms } = useRoomContext()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const availableRooms = rooms.filter(
|
const availableRooms = rooms.filter(
|
||||||
@@ -42,11 +42,15 @@ export default function RoomsHeader() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Typography variant="Title/Subtitle/md" className={styles.availableRooms}>
|
<Typography variant="Title/Subtitle/md" className={styles.availableRooms}>
|
||||||
|
{isFetchingPackages ? (
|
||||||
|
<p></p>
|
||||||
|
) : (
|
||||||
<p>
|
<p>
|
||||||
{availableRooms !== totalRooms
|
{availableRooms !== totalRooms
|
||||||
? notAllRoomsAvailableText
|
? notAllRoomsAvailableText
|
||||||
: allRoomsAvailableText}
|
: allRoomsAvailableText}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className={styles.filters}>
|
<div className={styles.filters}>
|
||||||
<RoomPackageFilter />
|
<RoomPackageFilter />
|
||||||
|
|||||||
@@ -37,24 +37,21 @@ export default function Rates({
|
|||||||
selectedFilter,
|
selectedFilter,
|
||||||
selectedPackages,
|
selectedPackages,
|
||||||
} = useRoomContext()
|
} = useRoomContext()
|
||||||
const { nights, petRoomPackage } = useRatesStore((state) => ({
|
const nights = useRatesStore((state) =>
|
||||||
nights: dt(state.booking.toDate).diff(state.booking.fromDate, "days"),
|
dt(state.booking.toDate).diff(state.booking.fromDate, "days")
|
||||||
petRoomPackage: state.petRoomPackage,
|
)
|
||||||
}))
|
|
||||||
|
|
||||||
function handleSelectRate(product: Product) {
|
function handleSelectRate(product: Product) {
|
||||||
selectRate({ features, product, roomType, roomTypeCode })
|
selectRate({ features, product, roomType, roomTypeCode })
|
||||||
}
|
}
|
||||||
|
|
||||||
const petRoomPackageSelected = selectedPackages.includes(
|
const petRoomPackageSelected = selectedPackages.find(
|
||||||
RoomPackageCodeEnum.PET_ROOM
|
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
)
|
)
|
||||||
|
|
||||||
const sharedProps = {
|
const sharedProps = {
|
||||||
handleSelectRate,
|
handleSelectRate,
|
||||||
nights,
|
nights,
|
||||||
petRoomPackage:
|
petRoomPackage: petRoomPackageSelected,
|
||||||
petRoomPackageSelected && petRoomPackage ? petRoomPackage : undefined,
|
|
||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
}
|
}
|
||||||
const showAllRates = selectedFilter === BookingCodeFilterEnum.All
|
const showAllRates = selectedFilter === BookingCodeFilterEnum.All
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { RoomPackage } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import type { Package } from "@/types/requests/packages"
|
||||||
|
|
||||||
export function calculatePricePerNightPriceProduct(
|
export function calculatePricePerNightPriceProduct(
|
||||||
pricePerNight: number,
|
pricePerNight: number,
|
||||||
requestedPricePerNight: number | undefined,
|
requestedPricePerNight: number | undefined,
|
||||||
nights: number,
|
nights: number,
|
||||||
petRoomPackage?: RoomPackage
|
petRoomPackage?: Package
|
||||||
) {
|
) {
|
||||||
const totalPrice = petRoomPackage?.localPrice
|
const totalPrice = petRoomPackage?.localPrice
|
||||||
? Math.floor(pricePerNight + petRoomPackage.localPrice.price / nights)
|
? Math.floor(pricePerNight + petRoomPackage.localPrice.price / nights)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import styles from "./image.module.css"
|
|||||||
import type { RoomListItemImageProps } from "@/types/components/hotelReservation/selectRate/roomListItem"
|
import type { RoomListItemImageProps } from "@/types/components/hotelReservation/selectRate/roomListItem"
|
||||||
|
|
||||||
export default function RoomImage({
|
export default function RoomImage({
|
||||||
features,
|
roomPackages,
|
||||||
roomsLeft,
|
roomsLeft,
|
||||||
roomType,
|
roomType,
|
||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
@@ -44,11 +44,13 @@ export default function RoomImage({
|
|||||||
</Footnote>
|
</Footnote>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{features
|
{roomPackages
|
||||||
.filter((feature) => selectedPackages.includes(feature.code))
|
.filter((pkg) =>
|
||||||
.map((feature) => (
|
selectedPackages.find((spkg) => spkg.code === pkg.code)
|
||||||
<span className={styles.chip} key={feature.code}>
|
)
|
||||||
{IconForFeatureCode({ featureCode: feature.code, size: 16 })}
|
.map((pkg) => (
|
||||||
|
<span className={styles.chip} key={pkg.code}>
|
||||||
|
{IconForFeatureCode({ featureCode: pkg.code, size: 16 })}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
|
|
||||||
import Details from "./Details"
|
import Details from "./Details"
|
||||||
import { listItemVariants } from "./listItemVariants"
|
import { listItemVariants } from "./listItemVariants"
|
||||||
import Rates from "./Rates"
|
import Rates from "./Rates"
|
||||||
@@ -12,6 +14,7 @@ import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHote
|
|||||||
import type { RoomListItemProps } from "@/types/components/hotelReservation/selectRate/roomListItem"
|
import type { RoomListItemProps } from "@/types/components/hotelReservation/selectRate/roomListItem"
|
||||||
|
|
||||||
export default function RoomListItem({ roomConfiguration }: RoomListItemProps) {
|
export default function RoomListItem({ roomConfiguration }: RoomListItemProps) {
|
||||||
|
const { roomPackages } = useRoomContext()
|
||||||
const classNames = listItemVariants({
|
const classNames = listItemVariants({
|
||||||
availability:
|
availability:
|
||||||
roomConfiguration.status === AvailabilityEnum.NotAvailable
|
roomConfiguration.status === AvailabilityEnum.NotAvailable
|
||||||
@@ -22,7 +25,7 @@ export default function RoomListItem({ roomConfiguration }: RoomListItemProps) {
|
|||||||
return (
|
return (
|
||||||
<li className={classNames}>
|
<li className={classNames}>
|
||||||
<RoomImage
|
<RoomImage
|
||||||
features={roomConfiguration.features}
|
roomPackages={roomPackages}
|
||||||
roomType={roomConfiguration.roomType}
|
roomType={roomConfiguration.roomType}
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
roomsLeft={roomConfiguration.roomsLeft}
|
roomsLeft={roomConfiguration.roomsLeft}
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ import ScrollToList from "./ScrollToList"
|
|||||||
import styles from "./rooms.module.css"
|
import styles from "./rooms.module.css"
|
||||||
|
|
||||||
export default function RoomsList() {
|
export default function RoomsList() {
|
||||||
const { rooms, isFetchingRoomFeatures } = useRoomContext()
|
const { isFetchingPackages, rooms } = useRoomContext()
|
||||||
|
if (isFetchingPackages) {
|
||||||
if (isFetchingRoomFeatures) {
|
|
||||||
return <RoomsListSkeleton />
|
return <RoomsListSkeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollToList />
|
<ScrollToList />
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import RatesProvider from "@/providers/RatesProvider"
|
import RatesProvider from "@/providers/RatesProvider"
|
||||||
|
|
||||||
import { useHotelPackages, useRoomsAvailability } from "../utils"
|
|
||||||
import RateSummary from "./RateSummary"
|
import RateSummary from "./RateSummary"
|
||||||
import Rooms from "./Rooms"
|
import Rooms from "./Rooms"
|
||||||
import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton"
|
import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton"
|
||||||
@@ -13,57 +12,34 @@ import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton"
|
|||||||
import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer"
|
import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer"
|
||||||
|
|
||||||
export function RoomsContainer({
|
export function RoomsContainer({
|
||||||
adultArray,
|
|
||||||
booking,
|
booking,
|
||||||
childArray,
|
hotelType,
|
||||||
fromDate,
|
|
||||||
hotelData,
|
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
toDate,
|
roomCategories,
|
||||||
|
vat,
|
||||||
}: RoomsContainerProps) {
|
}: RoomsContainerProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
|
||||||
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
const roomsAvailability = trpc.hotel.availability.selectRate.rooms.useQuery({
|
||||||
const toDateString = dt(toDate).format("YYYY-MM-DD")
|
booking,
|
||||||
|
|
||||||
const { data: roomsAvailability, isPending: isLoadingAvailability } =
|
|
||||||
useRoomsAvailability(
|
|
||||||
adultArray,
|
|
||||||
hotelData.hotel.id,
|
|
||||||
fromDateString,
|
|
||||||
toDateString,
|
|
||||||
lang,
|
lang,
|
||||||
childArray,
|
})
|
||||||
booking
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
|
if (
|
||||||
adultArray,
|
(roomsAvailability.isFetching || !roomsAvailability.data) &&
|
||||||
childArray,
|
!roomsAvailability.isFetched
|
||||||
fromDateString,
|
) {
|
||||||
toDateString,
|
|
||||||
hotelData.hotel.id,
|
|
||||||
lang
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isLoadingAvailability || isLoadingPackages) {
|
|
||||||
return <RoomsContainerSkeleton />
|
return <RoomsContainerSkeleton />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packages === null) {
|
|
||||||
// TODO: Log packages error
|
|
||||||
console.error("[RoomsContainer] unable to fetch packages")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RatesProvider
|
<RatesProvider
|
||||||
booking={booking}
|
booking={booking}
|
||||||
hotelType={hotelData.hotel.hotelType}
|
hotelType={hotelType}
|
||||||
isUserLoggedIn={isUserLoggedIn}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
packages={packages}
|
roomCategories={roomCategories}
|
||||||
roomCategories={hotelData.roomCategories}
|
roomsAvailability={roomsAvailability.data}
|
||||||
roomsAvailability={roomsAvailability}
|
vat={vat}
|
||||||
vat={hotelData.hotel.vat}
|
|
||||||
>
|
>
|
||||||
<Rooms />
|
<Rooms />
|
||||||
<RateSummary isUserLoggedIn={isUserLoggedIn} />
|
<RateSummary isUserLoggedIn={isUserLoggedIn} />
|
||||||
|
|||||||
@@ -74,13 +74,11 @@ export default async function SelectRatePage({
|
|||||||
<HotelInfoCard hotel={hotelData.hotel} />
|
<HotelInfoCard hotel={hotelData.hotel} />
|
||||||
|
|
||||||
<RoomsContainer
|
<RoomsContainer
|
||||||
adultArray={adultsInRoom}
|
|
||||||
booking={booking}
|
booking={booking}
|
||||||
childArray={childrenInRoom}
|
hotelType={hotelData.hotel.hotelType}
|
||||||
fromDate={arrivalDate}
|
|
||||||
hotelData={hotelData}
|
|
||||||
isUserLoggedIn={isUserLoggedIn}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
toDate={departureDate}
|
roomCategories={hotelData.roomCategories}
|
||||||
|
vat={hotelData.hotel.vat}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
|
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { REDEMPTION } from "@/constants/booking"
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
|
||||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
|
||||||
|
|
||||||
export function useRoomsAvailability(
|
|
||||||
adultsCount: number[],
|
|
||||||
hotelId: string,
|
|
||||||
fromDateString: string,
|
|
||||||
toDateString: string,
|
|
||||||
lang: Lang,
|
|
||||||
childArray: ChildrenInRoom,
|
|
||||||
booking: SelectRateSearchParams
|
|
||||||
) {
|
|
||||||
const redemption = booking.searchType
|
|
||||||
? booking.searchType === REDEMPTION
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const roomFeatureCodesArray = booking.rooms.map(
|
|
||||||
(room) => room.packages ?? null
|
|
||||||
)
|
|
||||||
|
|
||||||
const roomsAvailability =
|
|
||||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
|
||||||
adultsCount,
|
|
||||||
childArray,
|
|
||||||
hotelId,
|
|
||||||
lang,
|
|
||||||
redemption,
|
|
||||||
roomStayEndDate: toDateString,
|
|
||||||
roomStayStartDate: fromDateString,
|
|
||||||
bookingCode: booking.bookingCode,
|
|
||||||
roomFeatureCodesArray,
|
|
||||||
})
|
|
||||||
|
|
||||||
return roomsAvailability
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useHotelPackages(
|
|
||||||
adultArray: number[],
|
|
||||||
childArray: ChildrenInRoom,
|
|
||||||
fromDateString: string,
|
|
||||||
toDateString: string,
|
|
||||||
hotelId: string,
|
|
||||||
lang: Lang
|
|
||||||
) {
|
|
||||||
return trpc.hotel.packages.get.useQuery({
|
|
||||||
adults: adultArray[0], // Using the first adult count
|
|
||||||
children: childArray?.[0]?.length, // Using the first children count
|
|
||||||
endDate: toDateString,
|
|
||||||
hotelId,
|
|
||||||
packageCodes: [
|
|
||||||
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
|
||||||
RoomPackageCodeEnum.PET_ROOM,
|
|
||||||
RoomPackageCodeEnum.ALLERGY_ROOM,
|
|
||||||
],
|
|
||||||
startDate: fromDateString,
|
|
||||||
lang: lang,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
"+46 8 517 517 00": "+46 8 517 517 00",
|
"+46 8 517 517 00": "+46 8 517 517 00",
|
||||||
"/night per adult": "/nat per voksen",
|
"/night per adult": "/nat per voksen",
|
||||||
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
||||||
"<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.": "<b>200 SEK/nat</b> Vigtig information om priser og funktioner i kæledyrsvenlige værelser.",
|
|
||||||
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
|
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
|
||||||
"<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)",
|
"<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)",
|
||||||
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
||||||
@@ -610,6 +609,7 @@
|
|||||||
"Pet room charge including VAT": "Gebyr for kæledyrsværelse inkl. moms",
|
"Pet room charge including VAT": "Gebyr for kæledyrsværelse inkl. moms",
|
||||||
"Pet-friendly": "Kæledyrsvenlig",
|
"Pet-friendly": "Kæledyrsvenlig",
|
||||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kæledyrsrum har en ekstra gebyr på 20 EUR per ophold",
|
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kæledyrsrum har en ekstra gebyr på 20 EUR per ophold",
|
||||||
|
"Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>": "Kæledyrsvenlige værelser inkluderer et gebyr på ca. <b>{price}</b>/ophold",
|
||||||
"Phone": "Telefon",
|
"Phone": "Telefon",
|
||||||
"Phone is required": "Telefonnummer er påkrævet",
|
"Phone is required": "Telefonnummer er påkrævet",
|
||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"+46 8 517 517 00": "+46 8 517 517 00",
|
"+46 8 517 517 00": "+46 8 517 517 00",
|
||||||
"/night per adult": "/Nacht pro Erwachsenem",
|
"/night per adult": "/Nacht pro Erwachsenem",
|
||||||
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
||||||
"<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.": "<b>200 SEK/Nacht</b> Wichtige Informationen zu Preisen und Eigenschaften von haustierfreundlichen Zimmern.",
|
|
||||||
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
|
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
|
||||||
"<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)",
|
"<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)",
|
||||||
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
||||||
@@ -609,6 +608,7 @@
|
|||||||
"Pet room charge including VAT": "Haustierzimmergebühr inkl. MwSt.",
|
"Pet room charge including VAT": "Haustierzimmergebühr inkl. MwSt.",
|
||||||
"Pet-friendly": "Haustierfreundlich",
|
"Pet-friendly": "Haustierfreundlich",
|
||||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Haustierzimmer haben einen zusätzlichen Preis von 20 EUR pro Aufenthalt",
|
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Haustierzimmer haben einen zusätzlichen Preis von 20 EUR pro Aufenthalt",
|
||||||
|
"Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>": "Für haustierfreundliche Zimmer fällt eine Gebühr von ca. <b>{price}</b>/Aufenthalt an.",
|
||||||
"Phone": "Telefon",
|
"Phone": "Telefon",
|
||||||
"Phone is required": "Telefon ist erforderlich",
|
"Phone is required": "Telefon ist erforderlich",
|
||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"+46 8 517 517 00": "+46 8 517 517 00",
|
"+46 8 517 517 00": "+46 8 517 517 00",
|
||||||
"/night per adult": "/night per adult",
|
"/night per adult": "/night per adult",
|
||||||
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
||||||
"<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.": "<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.",
|
|
||||||
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)",
|
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)",
|
||||||
"<b>Total price</b> (incl VAT)": "<b>Total price</b> (incl VAT)",
|
"<b>Total price</b> (incl VAT)": "<b>Total price</b> (incl VAT)",
|
||||||
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
||||||
@@ -610,6 +609,7 @@
|
|||||||
"Pet room charge including VAT": "Pet room charge including VAT",
|
"Pet room charge including VAT": "Pet room charge including VAT",
|
||||||
"Pet-friendly": "Pet-friendly",
|
"Pet-friendly": "Pet-friendly",
|
||||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Pet-friendly rooms have an additional fee of 20 EUR per stay",
|
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Pet-friendly rooms have an additional fee of 20 EUR per stay",
|
||||||
|
"Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>": "Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>",
|
||||||
"Phone": "Phone",
|
"Phone": "Phone",
|
||||||
"Phone is required": "Phone is required",
|
"Phone is required": "Phone is required",
|
||||||
"Phone number": "Phone number",
|
"Phone number": "Phone number",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"+46 8 517 517 00": "+46 8 517 517 00",
|
"+46 8 517 517 00": "+46 8 517 517 00",
|
||||||
"/night per adult": "/yötä aikuista kohti",
|
"/night per adult": "/yötä aikuista kohti",
|
||||||
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
||||||
"<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.": "<b>200 SEK/yö</b> Tärkeitä tietoja hinnoista ja lemmikkieläinystävällisten huoneiden ominaisuuksista.",
|
|
||||||
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
|
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
|
||||||
"<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)",
|
"<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)",
|
||||||
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
||||||
@@ -608,6 +607,7 @@
|
|||||||
"Pet room charge including VAT": "Lemmikkihuoneen maksu sis. ALV",
|
"Pet room charge including VAT": "Lemmikkihuoneen maksu sis. ALV",
|
||||||
"Pet-friendly": "Lemmikkiystävällinen",
|
"Pet-friendly": "Lemmikkiystävällinen",
|
||||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Lemmikkihuoneen lisäkustannus on 20 EUR per majoitus",
|
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Lemmikkihuoneen lisäkustannus on 20 EUR per majoitus",
|
||||||
|
"Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>": "Lemmikkiystävälliset huoneet sisältävät n. <b>{price}</b>/yöpyminen",
|
||||||
"Phone": "Puhelin",
|
"Phone": "Puhelin",
|
||||||
"Phone is required": "Puhelin vaaditaan",
|
"Phone is required": "Puhelin vaaditaan",
|
||||||
"Phone number": "Puhelinnumero",
|
"Phone number": "Puhelinnumero",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"+46 8 517 517 00": "+46 8 517 517 00",
|
"+46 8 517 517 00": "+46 8 517 517 00",
|
||||||
"/night per adult": "/natt per voksen",
|
"/night per adult": "/natt per voksen",
|
||||||
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
||||||
"<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.": "<b>200 SEK/natt</b> Viktig informasjon om priser og funksjoner for dyrevennlige rom.",
|
|
||||||
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
|
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
|
||||||
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)",
|
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)",
|
||||||
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
||||||
@@ -607,6 +606,7 @@
|
|||||||
"Pet room charge including VAT": "Kjæledyrromsgebyr inkl. MVA",
|
"Pet room charge including VAT": "Kjæledyrromsgebyr inkl. MVA",
|
||||||
"Pet-friendly": "Dyrevennlig",
|
"Pet-friendly": "Dyrevennlig",
|
||||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kjæledyrsrom har en tilleggsavgift på 20 EUR per opphold",
|
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kjæledyrsrom har en tilleggsavgift på 20 EUR per opphold",
|
||||||
|
"Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>": "Kjæledyrvennlige rom inkluderer en kostnad på ca. <b>{price}</b>/opphold",
|
||||||
"Phone": "Telefon",
|
"Phone": "Telefon",
|
||||||
"Phone is required": "Telefon kreves",
|
"Phone is required": "Telefon kreves",
|
||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"+46 8 517 517 00": "+46 8 517 517 00",
|
"+46 8 517 517 00": "+46 8 517 517 00",
|
||||||
"/night per adult": "/natt per vuxen",
|
"/night per adult": "/natt per vuxen",
|
||||||
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
"1 EuroBonus point = 2 Scandic Friends points": "1 EuroBonus point = 2 Scandic Friends points",
|
||||||
"<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.": "<b>200 SEK/natt</b> Viktig information om prissättning och funktioner för husdjursvänliga rum.",
|
|
||||||
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
|
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
|
||||||
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)",
|
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)",
|
||||||
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
"<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>": "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
||||||
@@ -607,6 +606,7 @@
|
|||||||
"Pet room charge including VAT": "Avgift för husdjursrum inkl. moms",
|
"Pet room charge including VAT": "Avgift för husdjursrum inkl. moms",
|
||||||
"Pet-friendly": "Husdjursvänlig",
|
"Pet-friendly": "Husdjursvänlig",
|
||||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Husdjursrum har en extra avgift på 20 EUR per vistelse",
|
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Husdjursrum har en extra avgift på 20 EUR per vistelse",
|
||||||
|
"Pet-friendly rooms include a charge of approx. <b>{price}/stay</b>": "Husdjursvänliga rum har en avgift på ca. <b>{price}/vistelse",
|
||||||
"Phone": "Telefon",
|
"Phone": "Telefon",
|
||||||
"Phone is required": "Telefonnummer är obligatorisk",
|
"Phone is required": "Telefonnummer är obligatorisk",
|
||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
|
|||||||
@@ -11,15 +11,13 @@ import type {
|
|||||||
BreackfastPackagesInput,
|
BreackfastPackagesInput,
|
||||||
PackagesInput,
|
PackagesInput,
|
||||||
} from "@/types/requests/packages"
|
} from "@/types/requests/packages"
|
||||||
|
import type { RoomsAvailabilityExtendedInputSchema } from "@/types/trpc/routers/hotel/availability"
|
||||||
import type {
|
import type {
|
||||||
CityCoordinatesInput,
|
CityCoordinatesInput,
|
||||||
HotelInput,
|
HotelInput,
|
||||||
} from "@/types/trpc/routers/hotel/hotel"
|
} from "@/types/trpc/routers/hotel/hotel"
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
import type {
|
import type { GetHotelsByCSFilterInput } from "@/server/routers/hotels/input"
|
||||||
GetHotelsByCSFilterInput,
|
|
||||||
GetSelectedRoomAvailabilityInput,
|
|
||||||
} from "@/server/routers/hotels/input"
|
|
||||||
import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input"
|
import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input"
|
||||||
|
|
||||||
export const getLocations = cache(async function getMemoizedLocations() {
|
export const getLocations = cache(async function getMemoizedLocations() {
|
||||||
@@ -92,14 +90,6 @@ export const getHotelPage = cache(async function getMemoizedHotelPage() {
|
|||||||
return serverClient().contentstack.hotelPage.get()
|
return serverClient().contentstack.hotelPage.get()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getSelectedRoomAvailability = cache(
|
|
||||||
function getMemoizedSelectedRoomAvailability(
|
|
||||||
input: GetSelectedRoomAvailabilityInput
|
|
||||||
) {
|
|
||||||
return serverClient().hotel.availability.room(input)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const getFooter = cache(async function getMemoizedFooter() {
|
export const getFooter = cache(async function getMemoizedFooter() {
|
||||||
return serverClient().contentstack.base.footer()
|
return serverClient().contentstack.base.footer()
|
||||||
})
|
})
|
||||||
@@ -352,3 +342,11 @@ export const getJumpToData = cache(async function getMemoizedJumpToData() {
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const getSelectedRoomsAvailability = cache(
|
||||||
|
async function getMemoizedSelectedRoomsAvailability(
|
||||||
|
input: RoomsAvailabilityExtendedInputSchema
|
||||||
|
) {
|
||||||
|
return serverClient().hotel.availability.enterDetails(input)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export default function RatesProvider({
|
|||||||
children,
|
children,
|
||||||
hotelType,
|
hotelType,
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
packages,
|
|
||||||
roomCategories,
|
roomCategories,
|
||||||
roomsAvailability,
|
roomsAvailability,
|
||||||
vat,
|
vat,
|
||||||
@@ -35,11 +34,10 @@ export default function RatesProvider({
|
|||||||
allergyRoom: intl.formatMessage({ id: "Allergy-friendly room" }),
|
allergyRoom: intl.formatMessage({ id: "Allergy-friendly room" }),
|
||||||
petRoom: intl.formatMessage({ id: "Pet room" }),
|
petRoom: intl.formatMessage({ id: "Pet room" }),
|
||||||
},
|
},
|
||||||
packages: packages ?? [],
|
|
||||||
pathname,
|
pathname,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
roomsAvailability,
|
roomsAvailability,
|
||||||
searchParams,
|
searchParams: new URLSearchParams(searchParams),
|
||||||
vat,
|
vat,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useEffect } from "react"
|
|
||||||
|
|
||||||
import { REDEMPTION } from "@/constants/booking"
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
|
||||||
import { useRatesStore } from "@/stores/select-rate"
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
import { RoomContext } from "@/contexts/SelectRate/Room"
|
import { RoomContext } from "@/contexts/SelectRate/Room"
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
|
||||||
import type { RoomProviderProps } from "@/types/providers/select-rate/room"
|
import type { RoomProviderProps } from "@/types/providers/select-rate/room"
|
||||||
|
|
||||||
export default function RoomProvider({
|
export default function RoomProvider({
|
||||||
@@ -17,128 +12,28 @@ export default function RoomProvider({
|
|||||||
idx,
|
idx,
|
||||||
room,
|
room,
|
||||||
}: RoomProviderProps) {
|
}: RoomProviderProps) {
|
||||||
const lang = useLang()
|
const { activeRoom, roomAvailability, roomPackages } = useRatesStore(
|
||||||
const {
|
(state) => ({
|
||||||
activeRoom,
|
|
||||||
booking,
|
|
||||||
roomAvailability,
|
|
||||||
searchParams,
|
|
||||||
selectedFilter,
|
|
||||||
selectedPackages,
|
|
||||||
} = useRatesStore((state) => ({
|
|
||||||
activeRoom: state.activeRoom,
|
activeRoom: state.activeRoom,
|
||||||
booking: state.booking,
|
roomPackages: state.roomsPackages[idx],
|
||||||
roomAvailability: state.roomsAvailability?.[idx],
|
roomAvailability: state.roomsAvailability?.[idx],
|
||||||
searchParams: state.searchParams,
|
|
||||||
selectedFilter: state.rooms[idx].selectedFilter,
|
|
||||||
selectedPackages: state.rooms[idx].selectedPackages,
|
|
||||||
}))
|
|
||||||
const { appendRegularRates, addRoomFeatures, ...actions } = room.actions
|
|
||||||
const roomNr = idx + 1
|
|
||||||
|
|
||||||
const redemptionSearch = searchParams.has("searchType")
|
|
||||||
? searchParams.get("searchType") === REDEMPTION
|
|
||||||
: false
|
|
||||||
const hasRedemptionRates =
|
|
||||||
redemptionSearch || room.rooms.some((room) => room.redemptions.length)
|
|
||||||
const hasCorporateChequeOrVoucherRates = room.rooms.some((room) =>
|
|
||||||
room.code.some((product) => {
|
|
||||||
if ("corporateCheque" in product) {
|
|
||||||
return product.corporateCheque.rateType === RateTypeEnum.CorporateCheque
|
|
||||||
} else if ("voucher" in product) {
|
|
||||||
return product.voucher.rateType === RateTypeEnum.Voucher
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
const roomNr = idx + 1
|
||||||
|
|
||||||
const dontShowRegularRates =
|
const petRoomPackage = roomPackages.find(
|
||||||
hasRedemptionRates || hasCorporateChequeOrVoucherRates
|
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
|
||||||
// Since input would be the same on single room as already
|
|
||||||
// done in useRoomsAvailability hook, data is already present
|
|
||||||
// and thus runs the appendRegularRates updater resulting in
|
|
||||||
// duplicate data
|
|
||||||
const enabled = !!(
|
|
||||||
booking.bookingCode &&
|
|
||||||
selectedFilter !== BookingCodeFilterEnum.Discounted &&
|
|
||||||
!dontShowRegularRates
|
|
||||||
)
|
)
|
||||||
// Extra query needed to fetch regular rates upon user
|
|
||||||
// selecting to view all rates.
|
|
||||||
// TODO: Setup route to handle singular availability call
|
|
||||||
const { data, isFetched, isFetching } =
|
|
||||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery(
|
|
||||||
{
|
|
||||||
adultsCount: [room.bookingRoom.adults],
|
|
||||||
childArray: room.bookingRoom.childrenInRoom
|
|
||||||
? [room.bookingRoom.childrenInRoom]
|
|
||||||
: undefined,
|
|
||||||
hotelId: booking.hotelId,
|
|
||||||
lang,
|
|
||||||
roomStayEndDate: booking.toDate,
|
|
||||||
roomStayStartDate: booking.fromDate,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFetched && !isFetching && data?.length && enabled) {
|
|
||||||
const regularRates = data[0]
|
|
||||||
if ("roomConfigurations" in regularRates) {
|
|
||||||
appendRegularRates(regularRates.roomConfigurations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [appendRegularRates, data, enabled, isFetched, isFetching])
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: roomFeaturesData,
|
|
||||||
isFetched: isRoomFeaturesFetched,
|
|
||||||
isFetching: isRoomFeaturesFetching,
|
|
||||||
} = trpc.hotel.availability.roomFeatures.useQuery(
|
|
||||||
{
|
|
||||||
adults: room.bookingRoom.adults,
|
|
||||||
childrenInRoom: room.bookingRoom.childrenInRoom,
|
|
||||||
hotelId: booking.hotelId,
|
|
||||||
startDate: booking.fromDate,
|
|
||||||
endDate: booking.toDate,
|
|
||||||
roomFeatureCodes: selectedPackages,
|
|
||||||
roomIndex: idx, // Creates a unique query key for each room
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!selectedPackages.length,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
isRoomFeaturesFetched &&
|
|
||||||
!isRoomFeaturesFetching &&
|
|
||||||
roomFeaturesData?.length
|
|
||||||
) {
|
|
||||||
addRoomFeatures(roomFeaturesData)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
addRoomFeatures,
|
|
||||||
roomFeaturesData,
|
|
||||||
isRoomFeaturesFetched,
|
|
||||||
isRoomFeaturesFetching,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider
|
<RoomContext.Provider
|
||||||
value={{
|
value={{
|
||||||
...room,
|
...room,
|
||||||
actions,
|
|
||||||
isActiveRoom: activeRoom === idx,
|
isActiveRoom: activeRoom === idx,
|
||||||
isFetchingAdditionalRate: isFetched ? false : isFetching,
|
|
||||||
isFetchingRoomFeatures: isRoomFeaturesFetched
|
|
||||||
? false
|
|
||||||
: isRoomFeaturesFetching,
|
|
||||||
isMainRoom: roomNr === 1,
|
isMainRoom: roomNr === 1,
|
||||||
|
petRoomPackage,
|
||||||
roomAvailability,
|
roomAvailability,
|
||||||
|
roomPackages,
|
||||||
roomNr,
|
roomNr,
|
||||||
totalRooms: room.rooms.length,
|
totalRooms: room.rooms.length,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
serviceProcedure,
|
serviceProcedure,
|
||||||
} from "@/server/trpc"
|
} from "@/server/trpc"
|
||||||
|
|
||||||
import { getHotel } from "../hotels/query"
|
import { getHotel } from "../hotels/utils"
|
||||||
import { encrypt } from "../utils/encryption"
|
import { encrypt } from "../utils/encryption"
|
||||||
import {
|
import {
|
||||||
bookingConfirmationInput,
|
bookingConfirmationInput,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
|
|||||||
|
|
||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
import { getHotel } from "../../hotels/query"
|
import { getHotel } from "../../hotels/utils"
|
||||||
import { getUrlsOfAllLanguages } from "../languageSwitcher/utils"
|
import { getUrlsOfAllLanguages } from "../languageSwitcher/utils"
|
||||||
import { getMetadataInput } from "./input"
|
import { getMetadataInput } from "./input"
|
||||||
import { getNonContentstackUrls, metadataSchema } from "./output"
|
import { getNonContentstackUrls, metadataSchema } from "./output"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Lang } from "@/constants/languages"
|
|||||||
|
|
||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
import { Country } from "@/types/enums/country"
|
import { Country } from "@/types/enums/country"
|
||||||
|
|
||||||
export const hotelsAvailabilityInputSchema = z.object({
|
export const hotelsAvailabilityInputSchema = z.object({
|
||||||
@@ -25,55 +26,88 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({
|
|||||||
bookingCode: z.string().optional().default(""),
|
bookingCode: z.string().optional().default(""),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const roomsCombinedAvailabilityInputSchema = z.object({
|
const childrenInRoomSchema = z
|
||||||
adultsCount: z.array(z.number()),
|
|
||||||
bookingCode: z.string().optional(),
|
|
||||||
childArray: z
|
|
||||||
.array(
|
|
||||||
z
|
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
age: z.number(),
|
age: z.number(),
|
||||||
bed: z.nativeEnum(ChildBedMapEnum),
|
bed: z.nativeEnum(ChildBedMapEnum),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.nullable()
|
.optional()
|
||||||
)
|
|
||||||
.nullish(),
|
const baseRoomSchema = z.object({
|
||||||
hotelId: z.string(),
|
adults: z.number().int().min(1),
|
||||||
lang: z.nativeEnum(Lang),
|
bookingCode: z.string().optional(),
|
||||||
rateCode: z.string().optional(),
|
childrenInRoom: childrenInRoomSchema,
|
||||||
roomStayEndDate: z.string(),
|
packages: z
|
||||||
roomStayStartDate: z.string(),
|
.array(z.nativeEnum({ ...BreakfastPackageEnum, ...RoomPackageCodeEnum }))
|
||||||
redemption: z.boolean().optional().default(false),
|
|
||||||
roomFeatureCodesArray: z
|
|
||||||
.array(z.array(z.nativeEnum(RoomPackageCodeEnum)).nullable())
|
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const selectedRoomAvailabilityInputSchema = z.object({
|
const selectedRoomSchema = z.object({
|
||||||
hotelId: z.string(),
|
counterRateCode: z.string().optional(),
|
||||||
roomStayStartDate: z.string(),
|
|
||||||
roomStayEndDate: z.string(),
|
|
||||||
adults: z.number(),
|
|
||||||
children: z.string().optional(),
|
|
||||||
bookingCode: z.string().optional(),
|
|
||||||
rateCode: z.string(),
|
rateCode: z.string(),
|
||||||
roomTypeCode: z.string(),
|
roomTypeCode: z.string(),
|
||||||
counterRateCode: z.string().optional(),
|
|
||||||
packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
|
|
||||||
lang: z.nativeEnum(Lang).optional(),
|
|
||||||
redemption: z.boolean().optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type GetSelectedRoomAvailabilityInput = z.input<
|
const baseBookingSchema = z.object({
|
||||||
typeof selectedRoomAvailabilityInputSchema
|
bookingCode: z.string().optional(),
|
||||||
>
|
fromDate: z.string(),
|
||||||
|
|
||||||
export const ratesInputSchema = z.object({
|
|
||||||
hotelId: z.string(),
|
hotelId: z.string(),
|
||||||
|
searchType: z.string().optional(),
|
||||||
|
toDate: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const selectRateRoomsAvailabilityInputSchema = z.object({
|
||||||
|
booking: baseBookingSchema.extend({
|
||||||
|
rooms: z.array(
|
||||||
|
baseRoomSchema.extend({
|
||||||
|
rateCode: z.string().optional(),
|
||||||
|
roomTypeCode: z.string().optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const selectRateRoomAvailabilityInputSchema = z.object({
|
||||||
|
booking: baseBookingSchema.extend({
|
||||||
|
room: baseRoomSchema.extend({
|
||||||
|
rateCode: z.string().optional(),
|
||||||
|
roomTypeCode: z.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const enterDetailsRoomsAvailabilityInputSchema = z.object({
|
||||||
|
booking: baseBookingSchema.extend({
|
||||||
|
rooms: z.array(baseRoomSchema.merge(selectedRoomSchema)),
|
||||||
|
}),
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const myStayRoomAvailabilityInputSchema = z.object({
|
||||||
|
booking: baseBookingSchema.extend({
|
||||||
|
room: baseRoomSchema.merge(selectedRoomSchema),
|
||||||
|
}),
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const roomFeaturesInputSchema = z.object({
|
||||||
|
adults: z.number(),
|
||||||
|
childrenInRoom: childrenInRoomSchema,
|
||||||
|
endDate: z.string(),
|
||||||
|
hotelId: z.string(),
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
roomFeatureCodes: z
|
||||||
|
.array(z.nativeEnum({ ...BreakfastPackageEnum, ...RoomPackageCodeEnum }))
|
||||||
|
.optional(),
|
||||||
|
startDate: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RoomFeaturesInput = z.input<typeof roomFeaturesInputSchema>
|
||||||
|
|
||||||
export const hotelInputSchema = z.object({
|
export const hotelInputSchema = z.object({
|
||||||
hotelId: z.string(),
|
hotelId: z.string(),
|
||||||
isCardOnlyPayment: z.boolean(),
|
isCardOnlyPayment: z.boolean(),
|
||||||
@@ -127,13 +161,13 @@ export const ancillaryPackageInputSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const roomPackagesInputSchema = z.object({
|
export const roomPackagesInputSchema = z.object({
|
||||||
hotelId: z.string(),
|
|
||||||
startDate: z.string(),
|
|
||||||
endDate: z.string(),
|
|
||||||
adults: z.number(),
|
adults: z.number(),
|
||||||
children: z.number().optional().default(0),
|
children: z.number().optional().default(0),
|
||||||
packageCodes: z.array(z.string()).optional().default([]),
|
endDate: z.string(),
|
||||||
|
hotelId: z.string(),
|
||||||
lang: z.nativeEnum(Lang),
|
lang: z.nativeEnum(Lang),
|
||||||
|
packageCodes: z.array(z.string()).optional().default([]),
|
||||||
|
startDate: z.string(),
|
||||||
})
|
})
|
||||||
export const cityCoordinatesInputSchema = z.object({
|
export const cityCoordinatesInputSchema = z.object({
|
||||||
city: z.string(),
|
city: z.string(),
|
||||||
@@ -167,22 +201,3 @@ export const getLocationsInput = z.object({
|
|||||||
export const getLocationsUrlsInput = z.object({
|
export const getLocationsUrlsInput = z.object({
|
||||||
lang: z.nativeEnum(Lang),
|
lang: z.nativeEnum(Lang),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const roomFeaturesInputSchema = z.object({
|
|
||||||
hotelId: z.string(),
|
|
||||||
startDate: z.string(),
|
|
||||||
endDate: z.string(),
|
|
||||||
adults: z.number(),
|
|
||||||
childrenInRoom: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
age: z.number(),
|
|
||||||
bed: z.nativeEnum(ChildBedMapEnum),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
roomFeatureCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
|
|
||||||
roomIndex: z.number().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type RoomFeaturesInput = z.input<typeof roomFeaturesInputSchema>
|
|
||||||
|
|||||||
@@ -70,14 +70,10 @@ export const metrics = {
|
|||||||
fail: meter.createCounter("trpc.hotel.packages.get-fail"),
|
fail: meter.createCounter("trpc.hotel.packages.get-fail"),
|
||||||
success: meter.createCounter("trpc.hotel.packages.get-success"),
|
success: meter.createCounter("trpc.hotel.packages.get-success"),
|
||||||
},
|
},
|
||||||
roomsCombinedAvailability: {
|
roomsAvailability: {
|
||||||
counter: meter.createCounter("trpc.hotel.roomsCombinedAvailability.rooms"),
|
counter: meter.createCounter("trpc.hotel.roomsAvailability.rooms"),
|
||||||
fail: meter.createCounter(
|
fail: meter.createCounter("trpc.hotel.roomsAvailability.rooms-fail"),
|
||||||
"trpc.hotel.roomsCombinedAvailability.rooms-fail"
|
success: meter.createCounter("trpc.hotel.roomsAvailability.rooms-success"),
|
||||||
),
|
|
||||||
success: meter.createCounter(
|
|
||||||
"trpc.hotel.roomsCombinedAvailability.rooms-success"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
selectedRoomAvailability: {
|
selectedRoomAvailability: {
|
||||||
counter: meter.createCounter("trpc.hotel.availability.room"),
|
counter: meter.createCounter("trpc.hotel.availability.room"),
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import {
|
|||||||
breakfastPackageSchema,
|
breakfastPackageSchema,
|
||||||
packageSchema,
|
packageSchema,
|
||||||
} from "./schemas/packages"
|
} from "./schemas/packages"
|
||||||
import { rateSchema } from "./schemas/rate"
|
|
||||||
import { relationshipsSchema } from "./schemas/relationships"
|
import { relationshipsSchema } from "./schemas/relationships"
|
||||||
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
|
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
|
||||||
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
|
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
|
||||||
|
import { sortRoomConfigs } from "./utils"
|
||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
@@ -42,7 +42,6 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
Product,
|
Product,
|
||||||
RateDefinition,
|
RateDefinition,
|
||||||
RoomConfiguration,
|
|
||||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
||||||
@@ -136,18 +135,6 @@ const cancellationRules = {
|
|||||||
NotCancellable: 0,
|
NotCancellable: 0,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// Used to ensure `Available` rooms
|
|
||||||
// are shown before all `NotAvailable`
|
|
||||||
const statusLookup = {
|
|
||||||
[AvailabilityEnum.Available]: 1,
|
|
||||||
[AvailabilityEnum.NotAvailable]: 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
|
|
||||||
// @ts-expect-error - array indexing
|
|
||||||
return statusLookup[a.status] - statusLookup[b.status]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const roomsAvailabilitySchema = z
|
export const roomsAvailabilitySchema = z
|
||||||
.object({
|
.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
@@ -158,49 +145,11 @@ export const roomsAvailabilitySchema = z
|
|||||||
hotelId: z.number(),
|
hotelId: z.number(),
|
||||||
mustBeGuaranteed: z.boolean().optional(),
|
mustBeGuaranteed: z.boolean().optional(),
|
||||||
occupancy: occupancySchema.optional(),
|
occupancy: occupancySchema.optional(),
|
||||||
|
packages: z.array(packageSchema).optional().default([]),
|
||||||
rateDefinitions: z.array(rateDefinitionSchema),
|
rateDefinitions: z.array(rateDefinitionSchema),
|
||||||
roomConfigurations: z
|
roomConfigurations: z.array(roomConfigurationSchema),
|
||||||
.array(roomConfigurationSchema)
|
|
||||||
.transform((data) => {
|
|
||||||
// Initial sort to guarantee if one bed is NotAvailable and whereas
|
|
||||||
// the other is Available to make sure data is added to the correct
|
|
||||||
// roomConfig
|
|
||||||
const configs = data.sort(sortRoomConfigs)
|
|
||||||
const roomConfigs = new Map<string, RoomConfiguration>()
|
|
||||||
for (const roomConfig of configs) {
|
|
||||||
if (roomConfigs.has(roomConfig.roomType)) {
|
|
||||||
const currentRoomConf = roomConfigs.get(roomConfig.roomType)
|
|
||||||
if (currentRoomConf) {
|
|
||||||
currentRoomConf.features = roomConfig.features.reduce(
|
|
||||||
(feats, feature) => {
|
|
||||||
const currentFeatureIndex = feats.findIndex(
|
|
||||||
(f) => f.code === feature.code
|
|
||||||
)
|
|
||||||
if (currentFeatureIndex !== -1) {
|
|
||||||
feats[currentFeatureIndex].inventory =
|
|
||||||
feats[currentFeatureIndex].inventory +
|
|
||||||
feature.inventory
|
|
||||||
} else {
|
|
||||||
feats.push(feature)
|
|
||||||
}
|
|
||||||
return feats
|
|
||||||
},
|
|
||||||
currentRoomConf.features
|
|
||||||
)
|
|
||||||
currentRoomConf.roomsLeft =
|
|
||||||
currentRoomConf.roomsLeft + roomConfig.roomsLeft
|
|
||||||
roomConfigs.set(currentRoomConf.roomType, currentRoomConf)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
roomConfigs.set(roomConfig.roomType, roomConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(roomConfigs.values())
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
relationships: relationshipsSchema.optional(),
|
|
||||||
type: z.string().optional(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.transform(({ data: { attributes } }) => {
|
.transform(({ data: { attributes } }) => {
|
||||||
const rateDefinitions = attributes.rateDefinitions
|
const rateDefinitions = attributes.rateDefinitions
|
||||||
@@ -425,8 +374,6 @@ export const roomsAvailabilitySchema = z
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ratesSchema = z.array(rateSchema)
|
|
||||||
|
|
||||||
export const citiesByCountrySchema = z.object({
|
export const citiesByCountrySchema = z.object({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
citySchema.transform((data) => {
|
citySchema.transform((data) => {
|
||||||
@@ -597,17 +544,6 @@ export const packagesSchema = z
|
|||||||
hotelId: z.number(),
|
hotelId: z.number(),
|
||||||
packages: z.array(packageSchema).default([]),
|
packages: z.array(packageSchema).default([]),
|
||||||
}),
|
}),
|
||||||
relationships: z
|
|
||||||
.object({
|
|
||||||
links: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
url: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
type: z.string(),
|
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
const flexibilityPrice = z.object({
|
|
||||||
member: z.number(),
|
|
||||||
standard: z.number(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const rateSchema = z.object({
|
|
||||||
breakfastIncluded: z.boolean(),
|
|
||||||
description: z.string(),
|
|
||||||
id: z.number(),
|
|
||||||
imageSrc: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
prices: z.object({
|
|
||||||
currency: z.string(),
|
|
||||||
freeCancellation: flexibilityPrice,
|
|
||||||
freeRebooking: flexibilityPrice,
|
|
||||||
nonRefundable: flexibilityPrice,
|
|
||||||
}),
|
|
||||||
size: z.string(),
|
|
||||||
})
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Cabin",
|
|
||||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
|
||||||
"size": "17 - 24 m² (1 - 2 persons)",
|
|
||||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
|
||||||
"breakfastIncluded": false,
|
|
||||||
"prices": {
|
|
||||||
"currency": "SEK",
|
|
||||||
"nonRefundable": {
|
|
||||||
"standard": 2315,
|
|
||||||
"member": 2247
|
|
||||||
},
|
|
||||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
|
||||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "Standard",
|
|
||||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
|
||||||
"size": "19 - 30 m² (1 - 2 persons)",
|
|
||||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
|
||||||
"breakfastIncluded": false,
|
|
||||||
"prices": {
|
|
||||||
"currency": "SEK",
|
|
||||||
"nonRefundable": {
|
|
||||||
"standard": 2315,
|
|
||||||
"member": 2247
|
|
||||||
},
|
|
||||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
|
||||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"name": "Superior",
|
|
||||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
|
||||||
"size": "22 - 40 m² (1 - 3 persons)",
|
|
||||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
|
||||||
"breakfastIncluded": false,
|
|
||||||
"prices": {
|
|
||||||
"currency": "SEK",
|
|
||||||
"nonRefundable": {
|
|
||||||
"standard": 2315,
|
|
||||||
"member": 2247
|
|
||||||
},
|
|
||||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
|
||||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"name": "Superior Family",
|
|
||||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
|
||||||
"size": "29 - 49 m² (3 - 4 persons)",
|
|
||||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
|
||||||
"breakfastIncluded": false,
|
|
||||||
"prices": {
|
|
||||||
"currency": "SEK",
|
|
||||||
"nonRefundable": {
|
|
||||||
"standard": 2315,
|
|
||||||
"member": 2247
|
|
||||||
},
|
|
||||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
|
||||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"name": "Superior PLUS",
|
|
||||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
|
||||||
"size": "21 - 28 m² (2 - 3 persons)",
|
|
||||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
|
||||||
"breakfastIncluded": false,
|
|
||||||
"prices": {
|
|
||||||
"currency": "SEK",
|
|
||||||
"nonRefundable": {
|
|
||||||
"standard": 2315,
|
|
||||||
"member": 2247
|
|
||||||
},
|
|
||||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
|
||||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 6,
|
|
||||||
"name": "Junior Suite",
|
|
||||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
|
||||||
"size": "35 - 43 m² (2 - 4 persons)",
|
|
||||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
|
||||||
"breakfastIncluded": false,
|
|
||||||
"prices": {
|
|
||||||
"currency": "SEK",
|
|
||||||
"nonRefundable": {
|
|
||||||
"standard": 2315,
|
|
||||||
"member": 2247
|
|
||||||
},
|
|
||||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
|
||||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
import deepmerge from "deepmerge"
|
import deepmerge from "deepmerge"
|
||||||
|
import stringify from "json-stable-stringify-without-jsonify"
|
||||||
|
|
||||||
|
import { REDEMPTION } from "@/constants/booking"
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import { badRequestError } from "@/server/errors/trpc"
|
import { badRequestError } from "@/server/errors/trpc"
|
||||||
import { toApiLang } from "@/server/utils"
|
import { toApiLang } from "@/server/utils"
|
||||||
|
|
||||||
|
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||||
import { getCacheClient } from "@/services/dataCache"
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
|
import { cache } from "@/utils/cache"
|
||||||
|
|
||||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||||
|
import { type RoomFeaturesInput, roomPackagesInputSchema } from "./input"
|
||||||
import { metrics } from "./metrics"
|
import { metrics } from "./metrics"
|
||||||
import {
|
import {
|
||||||
type Cities,
|
type Cities,
|
||||||
@@ -16,15 +21,31 @@ import {
|
|||||||
citiesSchema,
|
citiesSchema,
|
||||||
countriesSchema,
|
countriesSchema,
|
||||||
getHotelIdsSchema,
|
getHotelIdsSchema,
|
||||||
|
hotelsAvailabilitySchema,
|
||||||
|
hotelSchema,
|
||||||
locationsSchema,
|
locationsSchema,
|
||||||
|
packagesSchema,
|
||||||
|
roomFeaturesSchema,
|
||||||
roomsAvailabilitySchema,
|
roomsAvailabilitySchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
import { getHotel } from "./query"
|
|
||||||
|
|
||||||
import type { z } from "zod"
|
|
||||||
|
|
||||||
|
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
import type {
|
||||||
|
DestinationPagesHotelData,
|
||||||
|
Room as RoomCategory,
|
||||||
|
} from "@/types/hotel"
|
||||||
|
import type { PackagesInput } from "@/types/requests/packages"
|
||||||
|
import type {
|
||||||
|
HotelsAvailabilityInputSchema,
|
||||||
|
HotelsByHotelIdsAvailabilityInputSchema,
|
||||||
|
RoomsAvailabilityInputRoom,
|
||||||
|
RoomsAvailabilityInputSchema,
|
||||||
|
} from "@/types/trpc/routers/hotel/availability"
|
||||||
|
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
||||||
import type {
|
import type {
|
||||||
CitiesGroupedByCountry,
|
CitiesGroupedByCountry,
|
||||||
CityLocation,
|
CityLocation,
|
||||||
@@ -34,9 +55,9 @@ import type {
|
|||||||
Products,
|
Products,
|
||||||
RateDefinition,
|
RateDefinition,
|
||||||
RedemptionsProduct,
|
RedemptionsProduct,
|
||||||
|
RoomConfiguration,
|
||||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
import type { Endpoint } from "@/lib/api/endpoints"
|
import type { Endpoint } from "@/lib/api/endpoints"
|
||||||
import type { selectedRoomAvailabilityInputSchema } from "./input"
|
|
||||||
|
|
||||||
export function getPoiGroupByCategoryName(category: string | undefined) {
|
export function getPoiGroupByCategoryName(category: string | undefined) {
|
||||||
if (!category) return PointOfInterestGroupEnum.LOCATION
|
if (!category) return PointOfInterestGroupEnum.LOCATION
|
||||||
@@ -608,39 +629,38 @@ function findProduct(product: Products, rateDefinition: RateDefinition) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSelectedRoomAvailability(
|
export const getHotel = cache(
|
||||||
input: z.input<typeof selectedRoomAvailabilityInputSchema>,
|
async (input: HotelInput, serviceToken: string) => {
|
||||||
lang: string,
|
const callable = async function (
|
||||||
serviceToken: string,
|
hotelId: HotelInput["hotelId"],
|
||||||
userPoints?: number
|
language: HotelInput["language"],
|
||||||
|
isCardOnlyPayment?: HotelInput["isCardOnlyPayment"]
|
||||||
) {
|
) {
|
||||||
const {
|
/**
|
||||||
adults,
|
* Since API expects the params appended and not just
|
||||||
bookingCode,
|
* a comma separated string we need to initialize the
|
||||||
children,
|
* SearchParams with a sequence of pairs
|
||||||
|
* (include=City&include=NearbyHotels&include=Restaurants etc.)
|
||||||
|
**/
|
||||||
|
const params = new URLSearchParams([
|
||||||
|
["include", "AdditionalData"],
|
||||||
|
["include", "City"],
|
||||||
|
["include", "NearbyHotels"],
|
||||||
|
["include", "Restaurants"],
|
||||||
|
["include", "RoomCategories"],
|
||||||
|
["language", toApiLang(language)],
|
||||||
|
])
|
||||||
|
metrics.hotel.counter.add(1, {
|
||||||
hotelId,
|
hotelId,
|
||||||
roomStayEndDate,
|
language,
|
||||||
roomStayStartDate,
|
})
|
||||||
redemption,
|
|
||||||
} = input
|
|
||||||
|
|
||||||
const params: Record<string, string | number | undefined> = {
|
|
||||||
roomStayStartDate,
|
|
||||||
roomStayEndDate,
|
|
||||||
adults,
|
|
||||||
...(children && { children }),
|
|
||||||
...(bookingCode && { bookingCode }),
|
|
||||||
...(redemption && { isRedemption: "true" }),
|
|
||||||
language: lang,
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics.selectedRoomAvailability.counter.add(1, input)
|
|
||||||
console.info(
|
console.info(
|
||||||
"api.hotels.selectedRoomAvailability start",
|
"api.hotels.hotelData start",
|
||||||
JSON.stringify({ query: { hotelId: input.hotelId, params } })
|
JSON.stringify({ query: { hotelId, params: params.toString() } })
|
||||||
)
|
)
|
||||||
const apiResponseAvailability = await api.get(
|
|
||||||
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${serviceToken}`,
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
@@ -649,10 +669,144 @@ export async function getSelectedRoomAvailability(
|
|||||||
params
|
params
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!apiResponseAvailability.ok) {
|
if (!apiResponse.ok) {
|
||||||
const text = await apiResponseAvailability.text()
|
const text = await apiResponse.text()
|
||||||
metrics.selectedRoomAvailability.fail.add(1, {
|
metrics.hotel.fail.add(1, {
|
||||||
hotelId,
|
hotelId,
|
||||||
|
language,
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.hotelData error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params: params.toString() },
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validateHotelData = hotelSchema.safeParse(apiJson)
|
||||||
|
|
||||||
|
if (!validateHotelData.success) {
|
||||||
|
metrics.hotel.fail.add(1, {
|
||||||
|
hotelId,
|
||||||
|
language,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validateHotelData.error),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"api.hotels.hotelData validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params: params.toString() },
|
||||||
|
error: validateHotelData.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.hotel.success.add(1, {
|
||||||
|
hotelId,
|
||||||
|
language,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.hotelData success",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params: params.toString() },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const hotelData = validateHotelData.data
|
||||||
|
|
||||||
|
if (isCardOnlyPayment) {
|
||||||
|
hotelData.hotel.merchantInformationData.alternatePaymentOptions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const gallery = hotelData.additionalData?.gallery
|
||||||
|
if (gallery) {
|
||||||
|
const smallerImages = gallery.smallerImages
|
||||||
|
const hotelGalleryImages =
|
||||||
|
hotelData.hotel.hotelType === HotelTypeEnum.Signature
|
||||||
|
? smallerImages.slice(0, 10)
|
||||||
|
: smallerImages.slice(0, 6)
|
||||||
|
hotelData.hotel.galleryImages = hotelGalleryImages
|
||||||
|
}
|
||||||
|
|
||||||
|
return hotelData
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`,
|
||||||
|
async () => {
|
||||||
|
return callable(input.hotelId, input.language, input.isCardOnlyPayment)
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_HOTELS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function getHotelsAvailabilityByCity(
|
||||||
|
input: HotelsAvailabilityInputSchema,
|
||||||
|
apiLang: string,
|
||||||
|
token: string, // Either service token or user access token in case of redemption search
|
||||||
|
userPoints: number = 0
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const params: Record<string, string | number> = {
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
...(children && { children }),
|
||||||
|
...(bookingCode && { bookingCode }),
|
||||||
|
...(redemption ? { isRedemption: "true" } : {}),
|
||||||
|
language: apiLang,
|
||||||
|
}
|
||||||
|
metrics.hotelsAvailability.counter.add(1, {
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.hotelsAvailability start",
|
||||||
|
JSON.stringify({ query: { cityId, params } })
|
||||||
|
)
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.city(cityId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
metrics.hotelsAvailability.fail.add(1, {
|
||||||
|
cityId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
adults,
|
adults,
|
||||||
@@ -660,31 +814,173 @@ export async function getSelectedRoomAvailability(
|
|||||||
bookingCode,
|
bookingCode,
|
||||||
error_type: "http_error",
|
error_type: "http_error",
|
||||||
error: JSON.stringify({
|
error: JSON.stringify({
|
||||||
status: apiResponseAvailability.status,
|
status: apiResponse.status,
|
||||||
statusText: apiResponseAvailability.statusText,
|
statusText: apiResponse.statusText,
|
||||||
text,
|
text,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
console.error(
|
console.error(
|
||||||
"api.hotels.selectedRoomAvailability error",
|
"api.hotels.hotelsAvailability error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { hotelId, params },
|
query: { cityId, params },
|
||||||
error: {
|
error: {
|
||||||
status: apiResponseAvailability.status,
|
status: apiResponse.status,
|
||||||
statusText: apiResponseAvailability.statusText,
|
statusText: apiResponse.statusText,
|
||||||
text,
|
text,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
throw new Error("Failed to fetch selected room availability")
|
throw new Error("Failed to fetch hotels availability by city")
|
||||||
}
|
}
|
||||||
const apiJsonAvailability = await apiResponseAvailability.json()
|
|
||||||
const validateAvailabilityData =
|
const apiJson = await apiResponse.json()
|
||||||
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
|
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
if (!validateAvailabilityData.success) {
|
if (!validateAvailabilityData.success) {
|
||||||
metrics.selectedRoomAvailability.fail.add(1, {
|
metrics.hotelsAvailability.fail.add(1, {
|
||||||
hotelId,
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validateAvailabilityData.error),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.hotelsAvailability validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { cityId, params },
|
||||||
|
error: validateAvailabilityData.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
metrics.hotelsAvailability.success.add(1, {
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.hotelsAvailability success",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { cityId, params: params },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (redemption) {
|
||||||
|
validateAvailabilityData.data.data.forEach((data) => {
|
||||||
|
data.attributes.productType?.redemptions?.forEach((r) => {
|
||||||
|
r.hasEnoughPoints = userPoints >= r.localPrice.pointsPerStay
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
availability: validateAvailabilityData.data.data.flatMap(
|
||||||
|
(hotels) => hotels.attributes
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHotelsAvailabilityByHotelIds(
|
||||||
|
input: HotelsByHotelIdsAvailabilityInputSchema,
|
||||||
|
apiLang: string,
|
||||||
|
serviceToken: string
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
hotelIds,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const params = new URLSearchParams([
|
||||||
|
["roomStayStartDate", roomStayStartDate],
|
||||||
|
["roomStayEndDate", roomStayEndDate],
|
||||||
|
["adults", adults.toString()],
|
||||||
|
["children", children ?? ""],
|
||||||
|
["bookingCode", bookingCode],
|
||||||
|
["language", apiLang],
|
||||||
|
])
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return cacheClient.cacheOrGet(
|
||||||
|
`${apiLang}:hotels:availability:${hotelIds.join(",")}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
|
||||||
|
async () => {
|
||||||
|
/**
|
||||||
|
* Since API expects the params appended and not just
|
||||||
|
* a comma separated string we need to initialize the
|
||||||
|
* SearchParams with a sequence of pairs
|
||||||
|
* (hotelIds=810&hotelIds=879&hotelIds=222 etc.)
|
||||||
|
**/
|
||||||
|
|
||||||
|
hotelIds.forEach((hotelId) =>
|
||||||
|
params.append("hotelIds", hotelId.toString())
|
||||||
|
)
|
||||||
|
metrics.hotelsByHotelIdAvailability.counter.add(1, {
|
||||||
|
hotelIds,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.hotelsByHotelIdAvailability start",
|
||||||
|
JSON.stringify({ query: { params } })
|
||||||
|
)
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.hotels(),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
||||||
|
hotelIds,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.hotelsByHotelIdAvailability error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { params },
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
throw new Error("Failed to fetch hotels availability by hotelIds")
|
||||||
|
}
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validateAvailabilityData =
|
||||||
|
hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
|
if (!validateAvailabilityData.success) {
|
||||||
|
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
||||||
|
hotelIds,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
adults,
|
adults,
|
||||||
@@ -694,27 +990,435 @@ export async function getSelectedRoomAvailability(
|
|||||||
error: JSON.stringify(validateAvailabilityData.error),
|
error: JSON.stringify(validateAvailabilityData.error),
|
||||||
})
|
})
|
||||||
console.error(
|
console.error(
|
||||||
"api.hotels.selectedRoomAvailability validation error",
|
"api.hotels.hotelsByHotelIdAvailability validation error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { hotelId, params },
|
query: { params },
|
||||||
error: validateAvailabilityData.error,
|
error: validateAvailabilityData.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
throw badRequestError()
|
throw badRequestError()
|
||||||
}
|
}
|
||||||
|
metrics.hotelsByHotelIdAvailability.success.add(1, {
|
||||||
const { rateDefinitions, roomConfigurations } = validateAvailabilityData.data
|
hotelIds,
|
||||||
|
roomStayStartDate,
|
||||||
const rateDefinition = rateDefinitions.find(
|
roomStayEndDate,
|
||||||
(rd) => rd.rateCode === input.rateCode
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.hotelsByHotelIdAvailability success",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { params },
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
availability: validateAvailabilityData.data.data.flatMap(
|
||||||
|
(hotels) => hotels.attributes
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_CITY_SEARCH
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRoomFeaturesInventory(
|
||||||
|
input: RoomFeaturesInput,
|
||||||
|
token: string
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
adults,
|
||||||
|
childrenInRoom,
|
||||||
|
endDate,
|
||||||
|
hotelId,
|
||||||
|
roomFeatureCodes,
|
||||||
|
startDate,
|
||||||
|
} = input
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return cacheClient.cacheOrGet(
|
||||||
|
stringify(input),
|
||||||
|
async function () {
|
||||||
|
const params = {
|
||||||
|
adults,
|
||||||
|
hotelId,
|
||||||
|
roomFeatureCode: roomFeatureCodes,
|
||||||
|
roomStayEndDate: endDate,
|
||||||
|
roomStayStartDate: startDate,
|
||||||
|
...(childrenInRoom?.length && {
|
||||||
|
children: generateChildrenString(childrenInRoom),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.roomFeatures.counter.add(1, params)
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.roomFeatures(hotelId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = apiResponse.text()
|
||||||
|
console.error(
|
||||||
|
"api.availability.roomfeature error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params },
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
metrics.roomFeatures.fail.add(1, params)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await apiResponse.json()
|
||||||
|
const validatedRoomFeaturesData = roomFeaturesSchema.safeParse(data)
|
||||||
|
if (!validatedRoomFeaturesData.success) {
|
||||||
|
console.error(
|
||||||
|
"api.availability.roomfeature error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params },
|
||||||
|
error: validatedRoomFeaturesData.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.roomFeatures.success.add(1, params)
|
||||||
|
|
||||||
|
return validatedRoomFeaturesData.data
|
||||||
|
},
|
||||||
|
"5m"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPackages(
|
||||||
|
rawInput: PackagesInput,
|
||||||
|
serviceToken: string
|
||||||
|
) {
|
||||||
|
const parsedInput = roomPackagesInputSchema.safeParse(rawInput)
|
||||||
|
if (!parsedInput.success) {
|
||||||
|
console.info(`Failed to parse input for Get Packages`)
|
||||||
|
console.error(parsedInput.error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const input = parsedInput.data
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return cacheClient.cacheOrGet(
|
||||||
|
stringify(input),
|
||||||
|
async function () {
|
||||||
|
const {
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
endDate,
|
||||||
|
hotelId,
|
||||||
|
lang,
|
||||||
|
packageCodes,
|
||||||
|
startDate,
|
||||||
|
} = input
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
adults: adults.toString(),
|
||||||
|
children: children.toString(),
|
||||||
|
endDate,
|
||||||
|
language: apiLang,
|
||||||
|
startDate,
|
||||||
|
})
|
||||||
|
|
||||||
|
packageCodes.forEach((code) => {
|
||||||
|
searchParams.append("packageCodes", code)
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = searchParams.toString()
|
||||||
|
|
||||||
|
metrics.packages.counter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.packages start",
|
||||||
|
JSON.stringify({ query: { hotelId, params } })
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Package.Packages.hotel(hotelId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searchParams
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
metrics.packages.fail.add(1, {
|
||||||
|
hotelId,
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.packages error",
|
||||||
|
JSON.stringify({ query: { hotelId, params } })
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validatedPackagesData = packagesSchema.safeParse(apiJson)
|
||||||
|
if (!validatedPackagesData.success) {
|
||||||
|
metrics.packages.fail.add(1, {
|
||||||
|
hotelId,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validatedPackagesData.error),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"api.hotels.packages validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params },
|
||||||
|
error: validatedPackagesData.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.packages.success.add(1, {
|
||||||
|
hotelId,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.packages success",
|
||||||
|
JSON.stringify({ query: { hotelId, params: params } })
|
||||||
|
)
|
||||||
|
|
||||||
|
return validatedPackagesData.data
|
||||||
|
},
|
||||||
|
"3h"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRoomsAvailability(
|
||||||
|
input: RoomsAvailabilityInputSchema,
|
||||||
|
token: string,
|
||||||
|
serviceToken: string,
|
||||||
|
userPoints: number | undefined
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
booking: { bookingCode, fromDate, hotelId, rooms, searchType, toDate },
|
||||||
|
lang,
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const redemption = searchType === REDEMPTION
|
||||||
|
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
|
||||||
|
const kids = rooms
|
||||||
|
.map((r) => r.childrenInRoom)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((kid) => JSON.stringify(kid))
|
||||||
|
const metricsData = {
|
||||||
|
adultsCount: rooms.map((r) => r.adults),
|
||||||
|
bookingCode,
|
||||||
|
childArray: kids.length ? kids : undefined,
|
||||||
|
hotelId,
|
||||||
|
roomStayEndDate: toDate,
|
||||||
|
roomStayStartDate: fromDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.roomsAvailability.counter.add(1, metricsData)
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
"api.hotels.roomsAvailability start",
|
||||||
|
JSON.stringify({ query: { hotelId, params: metricsData } })
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseCacheKey = {
|
||||||
|
bookingCode,
|
||||||
|
fromDate,
|
||||||
|
hotelId,
|
||||||
|
lang,
|
||||||
|
searchType,
|
||||||
|
toDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
const availabilityResponses = await Promise.allSettled(
|
||||||
|
rooms.map(async (room: RoomsAvailabilityInputRoom) => {
|
||||||
|
const cacheKey = {
|
||||||
|
...baseCacheKey,
|
||||||
|
room,
|
||||||
|
}
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
stringify(cacheKey),
|
||||||
|
async function () {
|
||||||
|
{
|
||||||
|
const params = {
|
||||||
|
adults: room.adults,
|
||||||
|
language: apiLang,
|
||||||
|
roomStayStartDate: fromDate,
|
||||||
|
roomStayEndDate: toDate,
|
||||||
|
...(room.childrenInRoom?.length && {
|
||||||
|
children: generateChildrenString(room.childrenInRoom),
|
||||||
|
}),
|
||||||
|
...(room.bookingCode && { bookingCode: room.bookingCode }),
|
||||||
|
...(redemption && { isRedemption: "true" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.hotel(hotelId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
revalidate: 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
metrics.roomsAvailability.fail.add(1, metricsData)
|
||||||
|
console.error("Failed API call", { params, text })
|
||||||
|
return { error: "http_error", details: text }
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validateAvailabilityData =
|
||||||
|
roomsAvailabilitySchema.safeParse(apiJson)
|
||||||
|
if (!validateAvailabilityData.success) {
|
||||||
|
console.error("Validation error", {
|
||||||
|
params,
|
||||||
|
error: validateAvailabilityData.error,
|
||||||
|
})
|
||||||
|
metrics.roomsAvailability.fail.add(1, metricsData)
|
||||||
|
return {
|
||||||
|
error: "validation_error",
|
||||||
|
details: validateAvailabilityData.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redemption) {
|
||||||
|
for (const roomConfig of validateAvailabilityData.data
|
||||||
|
.roomConfigurations) {
|
||||||
|
for (const product of roomConfig.redemptions) {
|
||||||
|
if (userPoints) {
|
||||||
|
product.redemption.hasEnoughPoints =
|
||||||
|
userPoints >= product.redemption.localPrice.pointsPerStay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomFeatures = await getPackages(
|
||||||
|
{
|
||||||
|
adults: room.adults,
|
||||||
|
children: room.childrenInRoom?.length,
|
||||||
|
endDate: input.booking.toDate,
|
||||||
|
hotelId: input.booking.hotelId,
|
||||||
|
lang,
|
||||||
|
packageCodes: [
|
||||||
|
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||||
|
RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||||
|
RoomPackageCodeEnum.PET_ROOM,
|
||||||
|
],
|
||||||
|
startDate: input.booking.fromDate,
|
||||||
|
},
|
||||||
|
serviceToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (roomFeatures) {
|
||||||
|
validateAvailabilityData.data.packages = roomFeatures
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch packages
|
||||||
|
if (room.packages?.length) {
|
||||||
|
const roomFeaturesInventory = await getRoomFeaturesInventory(
|
||||||
|
{
|
||||||
|
adults: room.adults,
|
||||||
|
childrenInRoom: room.childrenInRoom,
|
||||||
|
endDate: input.booking.toDate,
|
||||||
|
hotelId: input.booking.hotelId,
|
||||||
|
lang,
|
||||||
|
roomFeatureCodes: room.packages,
|
||||||
|
startDate: input.booking.fromDate,
|
||||||
|
},
|
||||||
|
serviceToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (roomFeaturesInventory) {
|
||||||
|
const features = roomFeaturesInventory.reduce<
|
||||||
|
Record<string, number>
|
||||||
|
>((fts, feat) => {
|
||||||
|
fts[feat.roomTypeCode] = feat.features?.[0]?.inventory ?? 0
|
||||||
|
return fts
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const updatedRoomConfigurations =
|
||||||
|
validateAvailabilityData.data.roomConfigurations
|
||||||
|
// This filter is needed since we can get availability
|
||||||
|
// back from roomFeatures yet the availability call
|
||||||
|
// says there are no rooms left...
|
||||||
|
.filter((rc) => rc.roomsLeft)
|
||||||
|
.filter((rc) => features?.[rc.roomTypeCode])
|
||||||
|
.map((rc) => ({
|
||||||
|
...rc,
|
||||||
|
roomsLeft: features[rc.roomTypeCode],
|
||||||
|
status: AvailabilityEnum.Available,
|
||||||
|
}))
|
||||||
|
|
||||||
|
validateAvailabilityData.data.roomConfigurations =
|
||||||
|
updatedRoomConfigurations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateAvailabilityData.data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1m"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
metrics.roomsAvailability.success.add(1, metricsData)
|
||||||
|
|
||||||
|
const data = availabilityResponses.map((availability) => {
|
||||||
|
if (availability.status === "fulfilled") {
|
||||||
|
return availability.value
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
details: availability.reason,
|
||||||
|
error: "request_failure",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSelectedRoomAvailability(
|
||||||
|
rateCode: string,
|
||||||
|
rateDefinitions: RateDefinition[],
|
||||||
|
roomConfigurations: RoomConfiguration[],
|
||||||
|
roomTypeCode: string,
|
||||||
|
userPoints: number | undefined
|
||||||
|
) {
|
||||||
|
const rateDefinition = rateDefinitions.find((rd) => rd.rateCode === rateCode)
|
||||||
if (!rateDefinition) {
|
if (!rateDefinition) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedRoom = roomConfigurations.find(
|
const selectedRoom = roomConfigurations.find(
|
||||||
(room) =>
|
(room) =>
|
||||||
room.roomTypeCode === input.roomTypeCode &&
|
room.roomTypeCode === roomTypeCode &&
|
||||||
room.products.find((product) => findProduct(product, rateDefinition))
|
room.products.find((product) => findProduct(product, rateDefinition))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -753,3 +1457,90 @@ export async function getSelectedRoomAvailability(
|
|||||||
selectedRoom,
|
selectedRoom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used to ensure `Available` rooms
|
||||||
|
// are shown before all `NotAvailable`
|
||||||
|
const statusLookup = {
|
||||||
|
[AvailabilityEnum.Available]: 1,
|
||||||
|
[AvailabilityEnum.NotAvailable]: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
|
||||||
|
// @ts-expect-error - array indexing
|
||||||
|
return statusLookup[a.status] - statusLookup[b.status]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBedTypes(
|
||||||
|
rooms: RoomConfiguration[],
|
||||||
|
roomType: string,
|
||||||
|
roomCategories?: RoomCategory[]
|
||||||
|
) {
|
||||||
|
if (!roomCategories) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return rooms
|
||||||
|
.filter((room) => room.roomType === roomType)
|
||||||
|
.map((availRoom) => {
|
||||||
|
const matchingRoom = roomCategories
|
||||||
|
?.find((room) =>
|
||||||
|
room.roomTypes
|
||||||
|
.map((roomType) => roomType.code)
|
||||||
|
.includes(availRoom.roomTypeCode)
|
||||||
|
)
|
||||||
|
?.roomTypes.find((roomType) => roomType.code === availRoom.roomTypeCode)
|
||||||
|
|
||||||
|
if (matchingRoom) {
|
||||||
|
return {
|
||||||
|
description: matchingRoom.description,
|
||||||
|
size: matchingRoom.mainBed.widthRange,
|
||||||
|
value: matchingRoom.code,
|
||||||
|
type: matchingRoom.mainBed.type,
|
||||||
|
extraBed: matchingRoom.fixedExtraBed
|
||||||
|
? {
|
||||||
|
type: matchingRoom.fixedExtraBed.type,
|
||||||
|
description: matchingRoom.fixedExtraBed.description,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((bed): bed is BedTypeSelection => Boolean(bed))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeRoomTypes(roomConfigurations: RoomConfiguration[]) {
|
||||||
|
// Initial sort to guarantee if one bed is NotAvailable and whereas
|
||||||
|
// the other is Available to make sure data is added to the correct
|
||||||
|
// roomConfig
|
||||||
|
roomConfigurations.sort(sortRoomConfigs)
|
||||||
|
|
||||||
|
const roomConfigs = new Map<string, RoomConfiguration>()
|
||||||
|
for (const roomConfig of roomConfigurations) {
|
||||||
|
if (roomConfigs.has(roomConfig.roomType)) {
|
||||||
|
const currentRoomConf = roomConfigs.get(roomConfig.roomType)
|
||||||
|
if (currentRoomConf) {
|
||||||
|
currentRoomConf.features = roomConfig.features.reduce(
|
||||||
|
(feats, feature) => {
|
||||||
|
const currentFeatureIndex = feats.findIndex(
|
||||||
|
(f) => f.code === feature.code
|
||||||
|
)
|
||||||
|
if (currentFeatureIndex !== -1) {
|
||||||
|
feats[currentFeatureIndex].inventory =
|
||||||
|
feats[currentFeatureIndex].inventory + feature.inventory
|
||||||
|
} else {
|
||||||
|
feats.push(feature)
|
||||||
|
}
|
||||||
|
return feats
|
||||||
|
},
|
||||||
|
currentRoomConf.features
|
||||||
|
)
|
||||||
|
currentRoomConf.roomsLeft =
|
||||||
|
currentRoomConf.roomsLeft + roomConfig.roomsLeft
|
||||||
|
roomConfigs.set(currentRoomConf.roomType, currentRoomConf)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
roomConfigs.set(roomConfig.roomType, roomConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(roomConfigs.values())
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,15 +107,13 @@ export function isRoomPackageCode(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterRoomsBySelectedPackages(
|
export function clearRoomSelectionFromUrl(
|
||||||
selectedPackages: RoomPackageCodeEnum[],
|
roomIdx: number,
|
||||||
rooms: RoomConfiguration[]
|
searchParams: URLSearchParams
|
||||||
) {
|
) {
|
||||||
if (!selectedPackages.length) {
|
searchParams.delete(`room[${roomIdx}].bookingCode`)
|
||||||
return rooms
|
searchParams.delete(`room[${roomIdx}].counterratecode`)
|
||||||
}
|
searchParams.delete(`room[${roomIdx}].ratecode`)
|
||||||
|
searchParams.delete(`room[${roomIdx}].roomtype`)
|
||||||
return rooms.filter((r) =>
|
return searchParams
|
||||||
selectedPackages.every((pkg) => r.features.find((f) => f.code === pkg))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { produce } from "immer"
|
import { produce } from "immer"
|
||||||
import { ReadonlyURLSearchParams } from "next/navigation"
|
|
||||||
import { useContext } from "react"
|
import { useContext } from "react"
|
||||||
import { create, useStore } from "zustand"
|
import { create, useStore } from "zustand"
|
||||||
|
|
||||||
|
import { REDEMPTION } from "@/constants/booking"
|
||||||
|
|
||||||
import { RatesContext } from "@/contexts/Rates"
|
import { RatesContext } from "@/contexts/Rates"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
filterRoomsBySelectedPackages,
|
clearRoomSelectionFromUrl,
|
||||||
findProductInRoom,
|
findProductInRoom,
|
||||||
findSelectedRate,
|
findSelectedRate,
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
@@ -14,18 +15,15 @@ import {
|
|||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
import type { Package, Packages } from "@/types/requests/packages"
|
||||||
import type { InitialState, RatesState } from "@/types/stores/rates"
|
import type { InitialState, RatesState } from "@/types/stores/rates"
|
||||||
import type {
|
import type { PriceProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
PriceProduct,
|
|
||||||
RoomConfiguration,
|
|
||||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
|
||||||
|
|
||||||
export function createRatesStore({
|
export function createRatesStore({
|
||||||
booking,
|
booking,
|
||||||
hotelType,
|
hotelType,
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
labels,
|
labels,
|
||||||
packages,
|
|
||||||
pathname,
|
pathname,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
roomsAvailability,
|
roomsAvailability,
|
||||||
@@ -36,34 +34,28 @@ export function createRatesStore({
|
|||||||
{
|
{
|
||||||
code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||||
description: labels.accessibilityRoom,
|
description: labels.accessibilityRoom,
|
||||||
itemCode: packages.find(
|
|
||||||
(pkg) => pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
|
|
||||||
)?.itemCode,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: RoomPackageCodeEnum.ALLERGY_ROOM,
|
code: RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||||
description: labels.allergyRoom,
|
description: labels.allergyRoom,
|
||||||
itemCode: packages.find(
|
|
||||||
(pkg) => pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
|
||||||
)?.itemCode,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: RoomPackageCodeEnum.PET_ROOM,
|
code: RoomPackageCodeEnum.PET_ROOM,
|
||||||
description: labels.petRoom,
|
description: labels.petRoom,
|
||||||
itemCode: packages.find(
|
|
||||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
|
||||||
)?.itemCode,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let roomConfigurations: RatesState["roomConfigurations"] = []
|
const roomsPackages: NonNullable<Packages>[] = []
|
||||||
|
const roomConfigurations: RatesState["roomConfigurations"] = []
|
||||||
if (roomsAvailability) {
|
if (roomsAvailability) {
|
||||||
for (const availability of roomsAvailability) {
|
for (const availability of roomsAvailability) {
|
||||||
if ("error" in availability) {
|
if ("error" in availability) {
|
||||||
// Availability request failed, default to empty array
|
// Availability request failed, default to empty array
|
||||||
roomConfigurations.push([])
|
roomConfigurations.push([])
|
||||||
|
roomsPackages.push([])
|
||||||
} else {
|
} else {
|
||||||
roomConfigurations.push(availability.roomConfigurations)
|
roomConfigurations.push(availability.roomConfigurations)
|
||||||
|
roomsPackages.push(availability.packages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,21 +64,22 @@ export function createRatesStore({
|
|||||||
for (const [idx, room] of booking.rooms.entries()) {
|
for (const [idx, room] of booking.rooms.entries()) {
|
||||||
if (room.rateCode && room.roomTypeCode) {
|
if (room.rateCode && room.roomTypeCode) {
|
||||||
const roomConfiguration = roomConfigurations?.[idx]
|
const roomConfiguration = roomConfigurations?.[idx]
|
||||||
const selectedPackages = room.packages ?? []
|
|
||||||
|
|
||||||
let rooms: RoomConfiguration[] = filterRoomsBySelectedPackages(
|
|
||||||
selectedPackages,
|
|
||||||
roomConfiguration
|
|
||||||
)
|
|
||||||
|
|
||||||
const selectedRoom = findSelectedRate(
|
const selectedRoom = findSelectedRate(
|
||||||
room.rateCode,
|
room.rateCode,
|
||||||
room.counterRateCode,
|
room.counterRateCode,
|
||||||
room.roomTypeCode,
|
room.roomTypeCode,
|
||||||
rooms
|
roomConfiguration
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!selectedRoom) {
|
if (!selectedRoom) {
|
||||||
|
const updatedSearchParams = clearRoomSelectionFromUrl(idx, searchParams)
|
||||||
|
searchParams = updatedSearchParams
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`${pathname}?${updatedSearchParams}`
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +92,9 @@ export function createRatesStore({
|
|||||||
rateSummary[idx] = {
|
rateSummary[idx] = {
|
||||||
features: selectedRoom.features,
|
features: selectedRoom.features,
|
||||||
product,
|
product,
|
||||||
packages: room.packages ?? [],
|
packages: roomsPackages[idx].filter((pkg) =>
|
||||||
|
room.packages?.includes(pkg.code)
|
||||||
|
),
|
||||||
rate: product.rate,
|
rate: product.rate,
|
||||||
roomType: selectedRoom.roomType,
|
roomType: selectedRoom.roomType,
|
||||||
roomTypeCode: selectedRoom.roomTypeCode,
|
roomTypeCode: selectedRoom.roomTypeCode,
|
||||||
@@ -126,22 +121,20 @@ export function createRatesStore({
|
|||||||
booking,
|
booking,
|
||||||
packageOptions,
|
packageOptions,
|
||||||
hotelType,
|
hotelType,
|
||||||
|
isRedemptionBooking: searchParams.has("searchType")
|
||||||
|
? searchParams.get("searchType") === REDEMPTION
|
||||||
|
: false,
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
packages,
|
|
||||||
pathname,
|
pathname,
|
||||||
petRoomPackage: packages.find(
|
|
||||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
|
||||||
),
|
|
||||||
rateSummary,
|
rateSummary,
|
||||||
roomConfigurations,
|
roomConfigurations,
|
||||||
rooms: booking.rooms.map((room, idx) => {
|
rooms: booking.rooms.map((room, idx) => {
|
||||||
const roomConfiguration = roomConfigurations[idx]
|
const roomConfiguration = roomConfigurations[idx]
|
||||||
const selectedPackages = room.packages ?? []
|
const roomPackages = roomsPackages[idx]
|
||||||
|
const selectedPackages =
|
||||||
let rooms: RoomConfiguration[] = filterRoomsBySelectedPackages(
|
room.packages
|
||||||
selectedPackages,
|
?.map((code) => roomPackages.find((pkg) => pkg.code === code))
|
||||||
roomConfiguration
|
.filter((pkg): pkg is Package => Boolean(pkg)) ?? []
|
||||||
)
|
|
||||||
|
|
||||||
const selectedRate =
|
const selectedRate =
|
||||||
findSelectedRate(
|
findSelectedRate(
|
||||||
@@ -159,12 +152,23 @@ export function createRatesStore({
|
|||||||
room.counterRateCode
|
room.counterRateCode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
let selectedFilter
|
||||||
|
const bookingCode = room.rateCode
|
||||||
|
? room.bookingCode
|
||||||
|
: booking.bookingCode
|
||||||
|
if (bookingCode) {
|
||||||
|
selectedFilter = BookingCodeFilterEnum.Discounted
|
||||||
|
} else {
|
||||||
|
selectedFilter = BookingCodeFilterEnum.Regular
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actions: {
|
actions: {
|
||||||
appendRegularRates(roomConfigurations) {
|
appendRegularRates(roomConfigurations) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: RatesState) => {
|
produce((state: RatesState) => {
|
||||||
|
state.rooms[idx].isFetchingAdditionalRate = false
|
||||||
|
if (roomConfigurations) {
|
||||||
const rooms = state.rooms[idx].rooms
|
const rooms = state.rooms[idx].rooms
|
||||||
const updatedRooms = rooms.map((currentRoom) => {
|
const updatedRooms = rooms.map((currentRoom) => {
|
||||||
const incomingRoom = roomConfigurations.find(
|
const incomingRoom = roomConfigurations.find(
|
||||||
@@ -203,47 +207,8 @@ export function createRatesStore({
|
|||||||
})
|
})
|
||||||
|
|
||||||
state.rooms[idx].rooms = updatedRooms
|
state.rooms[idx].rooms = updatedRooms
|
||||||
})
|
} else {
|
||||||
)
|
state.rooms[idx].rooms = []
|
||||||
},
|
|
||||||
addRoomFeatures(roomFeatures) {
|
|
||||||
return set(
|
|
||||||
produce((state: RatesState) => {
|
|
||||||
const selectedPackages = state.rooms[idx].selectedPackages
|
|
||||||
const rateSummaryItem = state.rateSummary[idx]
|
|
||||||
|
|
||||||
state.roomConfigurations[idx].forEach((room) => {
|
|
||||||
const features = roomFeatures.find(
|
|
||||||
(feat) => feat.roomTypeCode === room.roomTypeCode
|
|
||||||
)?.features
|
|
||||||
|
|
||||||
if (features) {
|
|
||||||
room.features = features
|
|
||||||
|
|
||||||
if (rateSummaryItem) {
|
|
||||||
rateSummaryItem.packages = selectedPackages
|
|
||||||
rateSummaryItem.features = features
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
state.rateSummary[idx] = rateSummaryItem
|
|
||||||
|
|
||||||
state.rooms[idx].rooms = filterRoomsBySelectedPackages(
|
|
||||||
selectedPackages,
|
|
||||||
state.roomConfigurations[idx]
|
|
||||||
)
|
|
||||||
|
|
||||||
const selectedRate = findSelectedRate(
|
|
||||||
room.rateCode,
|
|
||||||
room.counterRateCode,
|
|
||||||
room.roomTypeCode,
|
|
||||||
state.rooms[idx].rooms
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!selectedRate) {
|
|
||||||
state.rooms[idx].selectedRate = null
|
|
||||||
state.rateSummary[idx] = null
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -266,70 +231,75 @@ export function createRatesStore({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selectFilter(filter) {
|
removeSelectedPackage(code) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: RatesState) => {
|
produce((state: RatesState) => {
|
||||||
state.rooms[idx].selectedFilter = filter
|
state.rooms[idx].isFetchingPackages = true
|
||||||
})
|
const filteredSelectedPackages = state.rooms[
|
||||||
)
|
idx
|
||||||
},
|
].selectedPackages.filter((c) => c.code !== code)
|
||||||
togglePackages(selectedPackages) {
|
state.rooms[idx].selectedPackages = filteredSelectedPackages
|
||||||
return set(
|
|
||||||
produce((state: RatesState) => {
|
|
||||||
state.rooms[idx].selectedPackages = selectedPackages
|
|
||||||
const rateSummaryItem = state.rateSummary[idx]
|
|
||||||
|
|
||||||
const roomConfiguration = state.roomConfigurations[idx]
|
if (
|
||||||
if (roomConfiguration) {
|
state.rooms[idx].selectedRate?.product.bookingCode ||
|
||||||
const searchParams = new URLSearchParams(state.searchParams)
|
state.booking.bookingCode
|
||||||
if (selectedPackages.length) {
|
) {
|
||||||
|
state.rooms[idx].selectedFilter =
|
||||||
|
BookingCodeFilterEnum.Discounted
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = state.searchParams
|
||||||
|
if (filteredSelectedPackages.length) {
|
||||||
searchParams.set(
|
searchParams.set(
|
||||||
`room[${idx}].packages`,
|
`room[${idx}].packages`,
|
||||||
selectedPackages.join(",")
|
filteredSelectedPackages.map((pkg) => pkg.code).join(",")
|
||||||
)
|
)
|
||||||
|
|
||||||
if (rateSummaryItem) {
|
|
||||||
rateSummaryItem.packages = selectedPackages
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
state.rooms[idx].rooms = roomConfiguration
|
|
||||||
if (rateSummaryItem) {
|
|
||||||
rateSummaryItem.packages = []
|
|
||||||
}
|
|
||||||
searchParams.delete(`room[${idx}].packages`)
|
searchParams.delete(`room[${idx}].packages`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we already have the features data 'addRoomFeatures' wont run
|
state.searchParams = searchParams
|
||||||
// so we need to do additional filtering here if thats the case
|
|
||||||
const filteredRooms = filterRoomsBySelectedPackages(
|
|
||||||
selectedPackages,
|
|
||||||
state.roomConfigurations[idx]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (filteredRooms.length) {
|
window.history.replaceState(
|
||||||
const selectedRate = findSelectedRate(
|
|
||||||
room.rateCode,
|
|
||||||
room.counterRateCode,
|
|
||||||
room.roomTypeCode,
|
|
||||||
state.rooms[idx].rooms
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!selectedRate) {
|
|
||||||
state.rooms[idx].selectedRate = null
|
|
||||||
state.rateSummary[idx] = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.searchParams = new ReadonlyURLSearchParams(
|
|
||||||
searchParams
|
|
||||||
)
|
|
||||||
|
|
||||||
window.history.pushState(
|
|
||||||
{},
|
{},
|
||||||
"",
|
"",
|
||||||
`${state.pathname}?${searchParams}`
|
`${state.pathname}?${searchParams}`
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
removeSelectedPackages() {
|
||||||
|
return set(
|
||||||
|
produce((state: RatesState) => {
|
||||||
|
state.rooms[idx].isFetchingPackages = true
|
||||||
|
state.rooms[idx].selectedPackages = []
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.rooms[idx].selectedRate?.product.bookingCode ||
|
||||||
|
state.booking.bookingCode
|
||||||
|
) {
|
||||||
|
state.rooms[idx].selectedFilter =
|
||||||
|
BookingCodeFilterEnum.Discounted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchParams = state.searchParams
|
||||||
|
searchParams.delete(`room[${idx}].packages`)
|
||||||
|
|
||||||
|
state.searchParams = searchParams
|
||||||
|
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`${state.pathname}?${searchParams}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selectFilter(filter) {
|
||||||
|
return set(
|
||||||
|
produce((state: RatesState) => {
|
||||||
|
state.rooms[idx].selectedFilter = filter
|
||||||
|
state.rooms[idx].isFetchingAdditionalRate = true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -393,6 +363,19 @@ export function createRatesStore({
|
|||||||
isMainRoom &&
|
isMainRoom &&
|
||||||
hasMemberRate &&
|
hasMemberRate &&
|
||||||
isRegularRate
|
isRegularRate
|
||||||
|
|
||||||
|
state.rooms[idx].bookingRoom.rateCode = isMemberRate
|
||||||
|
? memberRateCode
|
||||||
|
: productRateCode
|
||||||
|
if (!isMemberRate && hasMemberRate) {
|
||||||
|
state.rooms[idx].bookingRoom.counterRateCode =
|
||||||
|
memberRateCode
|
||||||
|
}
|
||||||
|
state.rooms[idx].bookingRoom.roomTypeCode =
|
||||||
|
selectedRate.roomTypeCode
|
||||||
|
state.rooms[idx].bookingRoom.bookingCode =
|
||||||
|
selectedRate.product.bookingCode
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(state.searchParams)
|
const searchParams = new URLSearchParams(state.searchParams)
|
||||||
const counterratecode = isMemberRate
|
const counterratecode = isMemberRate
|
||||||
? productRateCode
|
? productRateCode
|
||||||
@@ -411,6 +394,17 @@ export function createRatesStore({
|
|||||||
searchParams.set(`room[${idx}].ratecode`, rateCode)
|
searchParams.set(`room[${idx}].ratecode`, rateCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedRate.product.bookingCode) {
|
||||||
|
searchParams.set(
|
||||||
|
`room[${idx}].bookingCode`,
|
||||||
|
selectedRate.product.bookingCode
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (searchParams.has(`room[${idx}].bookingCode`)) {
|
||||||
|
searchParams.delete(`room[${idx}].bookingCode`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
searchParams.set(
|
searchParams.set(
|
||||||
`room[${idx}].roomtype`,
|
`room[${idx}].roomtype`,
|
||||||
selectedRate.roomTypeCode
|
selectedRate.roomTypeCode
|
||||||
@@ -422,8 +416,9 @@ export function createRatesStore({
|
|||||||
state.activeRoom = idx + 1
|
state.activeRoom = idx + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
state.searchParams = new ReadonlyURLSearchParams(searchParams)
|
state.searchParams = searchParams
|
||||||
window.history.pushState(
|
|
||||||
|
window.history.replaceState(
|
||||||
{},
|
{},
|
||||||
"",
|
"",
|
||||||
`${state.pathname}?${searchParams}`
|
`${state.pathname}?${searchParams}`
|
||||||
@@ -431,13 +426,105 @@ export function createRatesStore({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
selectPackages(selectedPackages) {
|
||||||
|
return set(
|
||||||
|
produce((state: RatesState) => {
|
||||||
|
state.rooms[idx].isFetchingPackages = true
|
||||||
|
const pkgs = state.roomsPackages[idx].filter((pkg) =>
|
||||||
|
selectedPackages.includes(pkg.code)
|
||||||
|
)
|
||||||
|
state.rooms[idx].selectedPackages = pkgs
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.rooms[idx].selectedRate?.product.bookingCode ||
|
||||||
|
state.booking.bookingCode
|
||||||
|
) {
|
||||||
|
state.rooms[idx].selectedFilter =
|
||||||
|
BookingCodeFilterEnum.Discounted
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = state.searchParams
|
||||||
|
if (selectedPackages.length) {
|
||||||
|
searchParams.set(
|
||||||
|
`room[${idx}].packages`,
|
||||||
|
selectedPackages.join(",")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
searchParams.delete(`room[${idx}].packages`)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.searchParams = searchParams
|
||||||
|
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`${state.pathname}?${searchParams}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
updateRooms(rooms) {
|
||||||
|
return set(
|
||||||
|
produce((state: RatesState) => {
|
||||||
|
state.rooms[idx].isFetchingPackages = false
|
||||||
|
if (rooms) {
|
||||||
|
state.rooms[idx].rooms = rooms
|
||||||
|
const rateSummaryRoom = state.rateSummary[idx]
|
||||||
|
if (rateSummaryRoom) {
|
||||||
|
const room = state.rooms[idx].bookingRoom
|
||||||
|
const selectedRoom = findSelectedRate(
|
||||||
|
room.rateCode,
|
||||||
|
room.counterRateCode,
|
||||||
|
room.roomTypeCode,
|
||||||
|
rooms
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedRoom) {
|
||||||
|
rateSummaryRoom.packages =
|
||||||
|
state.rooms[idx].selectedPackages
|
||||||
|
} else {
|
||||||
|
const searchParams = clearRoomSelectionFromUrl(
|
||||||
|
idx,
|
||||||
|
state.searchParams
|
||||||
|
)
|
||||||
|
state.searchParams = searchParams
|
||||||
|
state.rateSummary[idx] = null
|
||||||
|
state.rooms[idx].selectedRate = null
|
||||||
|
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`${pathname}?${searchParams}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.rooms[idx].rooms = []
|
||||||
|
if (state.rateSummary[idx]) {
|
||||||
|
const searchParams = clearRoomSelectionFromUrl(
|
||||||
|
idx,
|
||||||
|
state.searchParams
|
||||||
|
)
|
||||||
|
state.searchParams = searchParams
|
||||||
|
state.rateSummary[idx] = null
|
||||||
|
state.rooms[idx].selectedRate = null
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`${pathname}?${searchParams}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
bookingRoom: room,
|
bookingRoom: room,
|
||||||
rooms,
|
isFetchingAdditionalRate: false,
|
||||||
selectedFilter: booking.bookingCode
|
isFetchingPackages: false,
|
||||||
? BookingCodeFilterEnum.Discounted
|
rooms: roomConfiguration,
|
||||||
: BookingCodeFilterEnum.All,
|
selectedFilter,
|
||||||
selectedPackages,
|
selectedPackages,
|
||||||
selectedRate:
|
selectedRate:
|
||||||
selectedRate && product
|
selectedRate && product
|
||||||
@@ -452,6 +539,7 @@ export function createRatesStore({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
roomCategories,
|
roomCategories,
|
||||||
|
roomsPackages,
|
||||||
roomsAvailability,
|
roomsAvailability,
|
||||||
searchParams,
|
searchParams,
|
||||||
vat,
|
vat,
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { z } from "zod"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Product,
|
|
||||||
RoomConfiguration,
|
|
||||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
|
||||||
import type {
|
|
||||||
priceSchema,
|
|
||||||
productTypePriceSchema,
|
|
||||||
} from "@/server/routers/hotels/schemas/productTypePrice"
|
|
||||||
import type { RoomPackage } from "./roomFilter"
|
|
||||||
|
|
||||||
export type ProductPrice = z.output<typeof productTypePriceSchema>
|
|
||||||
export type RoomPriceSchema = z.output<typeof priceSchema>
|
|
||||||
|
|
||||||
export type FlexibilityOptionProps = {
|
|
||||||
features: RoomConfiguration["features"]
|
|
||||||
paymentTerm: string
|
|
||||||
petRoomPackage: RoomPackage | undefined
|
|
||||||
priceInformation?: Array<string>
|
|
||||||
product: Product | undefined
|
|
||||||
roomType: RoomConfiguration["roomType"]
|
|
||||||
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
|
||||||
title: string
|
|
||||||
rateName?: string // Obtained in case of booking code and redemption rates
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FlexibilityOptionVoucherProps
|
|
||||||
extends Omit<FlexibilityOptionProps, "| product"> {
|
|
||||||
product: Product
|
|
||||||
}
|
|
||||||
export type FlexibilityOptionChequeProps = FlexibilityOptionVoucherProps
|
|
||||||
|
|
||||||
export interface PriceListProps {
|
|
||||||
publicPrice: ProductPrice
|
|
||||||
memberPrice: ProductPrice
|
|
||||||
petRoomPackage?: RoomPackage
|
|
||||||
rateName?: string // Obtained in case of booking code and redemption rates
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Packages } from "@/types/requests/packages"
|
import type { Package } from "@/types/requests/packages"
|
||||||
import type {
|
import type {
|
||||||
Product,
|
Product,
|
||||||
RoomConfiguration,
|
RoomConfiguration,
|
||||||
@@ -12,5 +12,5 @@ export interface SharedRateCardProps
|
|||||||
extends Pick<RoomConfiguration, "roomTypeCode"> {
|
extends Pick<RoomConfiguration, "roomTypeCode"> {
|
||||||
handleSelectRate: (product: Product) => void
|
handleSelectRate: (product: Product) => void
|
||||||
nights: number
|
nights: number
|
||||||
petRoomPackage: NonNullable<Packages>[number] | undefined
|
petRoomPackage: Package | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export enum RoomPackageCodeEnum {
|
|||||||
export interface DefaultFilterOptions {
|
export interface DefaultFilterOptions {
|
||||||
code: RoomPackageCodeEnum
|
code: RoomPackageCodeEnum
|
||||||
description: string
|
description: string
|
||||||
itemCode: string | undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterValues = {
|
export type FilterValues = {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { z } from "zod"
|
import type { Package } from "@/types/requests/packages"
|
||||||
|
|
||||||
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
import type { packagePriceSchema } from "@/server/routers/hotels/schemas/packages"
|
|
||||||
import type { RoomPriceSchema } from "./flexibilityOption"
|
|
||||||
|
|
||||||
export type RoomListItemProps = {
|
export type RoomListItemProps = {
|
||||||
roomConfiguration: RoomConfiguration
|
roomConfiguration: RoomConfiguration
|
||||||
@@ -10,19 +7,9 @@ export type RoomListItemProps = {
|
|||||||
|
|
||||||
export type RoomListItemImageProps = Pick<
|
export type RoomListItemImageProps = Pick<
|
||||||
RoomConfiguration,
|
RoomConfiguration,
|
||||||
"features" | "roomType" | "roomTypeCode" | "roomsLeft"
|
"roomType" | "roomTypeCode" | "roomsLeft"
|
||||||
>
|
> & {
|
||||||
|
roomPackages: Package[]
|
||||||
type RoomPackagePriceSchema = z.output<typeof packagePriceSchema>
|
|
||||||
|
|
||||||
export type CalculatePricesPerNightProps = {
|
|
||||||
publicLocalPrice: RoomPriceSchema
|
|
||||||
memberLocalPrice: RoomPriceSchema
|
|
||||||
publicRequestedPrice: RoomPriceSchema | null
|
|
||||||
memberRequestedPrice: RoomPriceSchema | null
|
|
||||||
petRoomLocalPrice?: RoomPackagePriceSchema
|
|
||||||
petRoomRequestedPrice?: RoomPackagePriceSchema
|
|
||||||
nights: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomSizeProps {
|
export interface RoomSizeProps {
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import type { HotelData } from "@/types/hotel"
|
import type { HotelData } from "@/types/hotel"
|
||||||
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
|
||||||
import type { SelectRateSearchParams } from "./selectRate"
|
import type { SelectRateSearchParams } from "./selectRate"
|
||||||
|
|
||||||
export interface RoomsContainerProps {
|
export interface RoomsContainerProps
|
||||||
adultArray: number[]
|
extends Pick<HotelData, "roomCategories">,
|
||||||
|
Pick<HotelData["hotel"], "hotelType" | "vat"> {
|
||||||
booking: SelectRateSearchParams
|
booking: SelectRateSearchParams
|
||||||
bookingCode?: string
|
|
||||||
childArray: ChildrenInRoom
|
|
||||||
fromDate: Date
|
|
||||||
hotelData: HotelData
|
|
||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
toDate: Date
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { RateEnum } from "@/types/enums/rate"
|
import type { RateEnum } from "@/types/enums/rate"
|
||||||
|
import type { PackageEnum, Packages } from "@/types/requests/packages"
|
||||||
import type {
|
import type {
|
||||||
Product,
|
Product,
|
||||||
RoomConfiguration,
|
RoomConfiguration,
|
||||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
import type { ChildBedMapEnum } from "../../bookingWidget/enums"
|
import type { ChildBedMapEnum } from "../../bookingWidget/enums"
|
||||||
import type { RoomPackageCodeEnum } from "./roomFilter"
|
|
||||||
|
|
||||||
export interface Child {
|
export interface Child {
|
||||||
bed: ChildBedMapEnum
|
bed: ChildBedMapEnum
|
||||||
@@ -13,9 +13,10 @@ export interface Child {
|
|||||||
|
|
||||||
export interface Room {
|
export interface Room {
|
||||||
adults: number
|
adults: number
|
||||||
|
bookingCode?: string
|
||||||
childrenInRoom?: Child[]
|
childrenInRoom?: Child[]
|
||||||
counterRateCode: string
|
counterRateCode: string
|
||||||
packages?: RoomPackageCodeEnum[]
|
packages?: PackageEnum[]
|
||||||
rateCode: string
|
rateCode: string
|
||||||
roomTypeCode: string
|
roomTypeCode: string
|
||||||
}
|
}
|
||||||
@@ -32,7 +33,7 @@ export interface SelectRateSearchParams {
|
|||||||
|
|
||||||
export type Rate = {
|
export type Rate = {
|
||||||
features: RoomConfiguration["features"]
|
features: RoomConfiguration["features"]
|
||||||
packages: RoomPackageCodeEnum[]
|
packages: NonNullable<Packages>
|
||||||
priceName?: string
|
priceName?: string
|
||||||
priceTerm?: string
|
priceTerm?: string
|
||||||
product: Product
|
product: Product
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
|
import type { Package } from "@/types/requests/packages"
|
||||||
import type { RatesState, SelectedRoom } from "@/types/stores/rates"
|
import type { RatesState, SelectedRoom } from "@/types/stores/rates"
|
||||||
|
|
||||||
export interface RoomContextValue extends Omit<SelectedRoom, "actions"> {
|
export interface RoomContextValue extends Omit<SelectedRoom, "actions"> {
|
||||||
actions: Omit<
|
actions: SelectedRoom["actions"]
|
||||||
SelectedRoom["actions"],
|
|
||||||
"appendRegularRates" | "addRoomFeatures"
|
|
||||||
>
|
|
||||||
isActiveRoom: boolean
|
isActiveRoom: boolean
|
||||||
isFetchingAdditionalRate: boolean
|
isFetchingAdditionalRate: boolean
|
||||||
isFetchingRoomFeatures: boolean
|
|
||||||
isMainRoom: boolean
|
isMainRoom: boolean
|
||||||
|
petRoomPackage: Package | undefined
|
||||||
roomAvailability:
|
roomAvailability:
|
||||||
| NonNullable<RatesState["roomsAvailability"]>[number]
|
| NonNullable<RatesState["roomsAvailability"]>[number]
|
||||||
| undefined
|
| undefined
|
||||||
|
roomPackages: Package[]
|
||||||
roomNr: number
|
roomNr: number
|
||||||
totalRooms: number
|
totalRooms: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Room } from "@/types/hotel"
|
import type { Room } from "@/types/hotel"
|
||||||
import type { Packages } from "@/types/requests/packages"
|
|
||||||
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate"
|
||||||
import type { AvailabilityError } from "../stores/rates"
|
import type { AvailabilityError } from "../stores/rates"
|
||||||
@@ -8,7 +7,6 @@ export interface RatesProviderProps extends React.PropsWithChildren {
|
|||||||
booking: SelectRateSearchParams
|
booking: SelectRateSearchParams
|
||||||
hotelType: string | undefined
|
hotelType: string | undefined
|
||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
packages: Packages | null
|
|
||||||
roomCategories: Room[]
|
roomCategories: Room[]
|
||||||
roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined
|
roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined
|
||||||
vat: number
|
vat: number
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import type {
|
|||||||
roomPackagesInputSchema,
|
roomPackagesInputSchema,
|
||||||
} from "@/server/routers/hotels/input"
|
} from "@/server/routers/hotels/input"
|
||||||
import type { packagesSchema } from "@/server/routers/hotels/output"
|
import type { packagesSchema } from "@/server/routers/hotels/output"
|
||||||
|
import type { RoomPackageCodeEnum } from "../components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { BreakfastPackageEnum } from "../enums/breakfast"
|
||||||
|
|
||||||
export interface BreackfastPackagesInput
|
export interface BreackfastPackagesInput
|
||||||
extends z.input<typeof breakfastPackageInputSchema> {}
|
extends z.input<typeof breakfastPackageInputSchema> {}
|
||||||
@@ -17,3 +19,6 @@ export interface PackagesInput
|
|||||||
extends z.input<typeof roomPackagesInputSchema> {}
|
extends z.input<typeof roomPackagesInputSchema> {}
|
||||||
|
|
||||||
export type Packages = z.output<typeof packagesSchema>
|
export type Packages = z.output<typeof packagesSchema>
|
||||||
|
export type Package = NonNullable<Packages>[number]
|
||||||
|
|
||||||
|
export type PackageEnum = BreakfastPackageEnum | RoomPackageCodeEnum
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import type { ReadonlyURLSearchParams } from "next/navigation"
|
import type { DefaultFilterOptions } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
|
||||||
import type {
|
|
||||||
DefaultFilterOptions,
|
|
||||||
RoomPackageCodeEnum,
|
|
||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
|
||||||
import type {
|
import type {
|
||||||
Rate,
|
Rate,
|
||||||
Room as RoomBooking,
|
Room as RoomBooking,
|
||||||
SelectRateSearchParams,
|
SelectRateSearchParams,
|
||||||
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type { Room } from "@/types/hotel"
|
import type { Room } from "@/types/hotel"
|
||||||
import type { Packages } from "@/types/requests/packages"
|
import type { Package, PackageEnum } from "@/types/requests/packages"
|
||||||
import type {
|
import type {
|
||||||
Product,
|
Product,
|
||||||
RoomConfiguration,
|
RoomConfiguration,
|
||||||
@@ -24,18 +19,17 @@ export interface AvailabilityError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Actions {
|
interface Actions {
|
||||||
appendRegularRates: (roomConfigurations: RoomConfiguration[]) => void
|
appendRegularRates: (
|
||||||
addRoomFeatures: (
|
roomConfigurations: RoomConfiguration[] | undefined
|
||||||
roomFeatures: {
|
|
||||||
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
|
||||||
features: RoomConfiguration["features"]
|
|
||||||
}[]
|
|
||||||
) => void
|
) => void
|
||||||
closeSection: () => void
|
closeSection: VoidFunction
|
||||||
modifyRate: () => void
|
modifyRate: VoidFunction
|
||||||
|
removeSelectedPackage: (code: PackageEnum) => void
|
||||||
|
removeSelectedPackages: VoidFunction
|
||||||
selectFilter: (filter: BookingCodeFilterEnum) => void
|
selectFilter: (filter: BookingCodeFilterEnum) => void
|
||||||
togglePackages: (codes: RoomPackageCodeEnum[]) => void
|
selectPackages: (codes: PackageEnum[]) => void
|
||||||
selectRate: (rate: SelectedRate) => void
|
selectRate: (rate: SelectedRate) => void
|
||||||
|
updateRooms: (rooms: RoomConfiguration[] | undefined) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectedRate {
|
export interface SelectedRate {
|
||||||
@@ -48,27 +42,29 @@ export interface SelectedRate {
|
|||||||
export interface SelectedRoom {
|
export interface SelectedRoom {
|
||||||
actions: Actions
|
actions: Actions
|
||||||
bookingRoom: RoomBooking
|
bookingRoom: RoomBooking
|
||||||
|
isFetchingAdditionalRate: boolean
|
||||||
|
isFetchingPackages: boolean
|
||||||
rooms: RoomConfiguration[]
|
rooms: RoomConfiguration[]
|
||||||
selectedFilter: BookingCodeFilterEnum | undefined
|
selectedFilter: BookingCodeFilterEnum | undefined
|
||||||
selectedPackages: RoomPackageCodeEnum[]
|
selectedPackages: Package[]
|
||||||
selectedRate: SelectedRate | null
|
selectedRate: SelectedRate | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RatesState {
|
export interface RatesState {
|
||||||
activeRoom: number
|
activeRoom: number
|
||||||
booking: SelectRateSearchParams
|
booking: SelectRateSearchParams
|
||||||
packageOptions: DefaultFilterOptions[]
|
|
||||||
hotelType: string | undefined
|
hotelType: string | undefined
|
||||||
|
isRedemptionBooking: boolean
|
||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
packages: NonNullable<Packages>
|
packageOptions: DefaultFilterOptions[]
|
||||||
pathname: string
|
pathname: string
|
||||||
petRoomPackage: NonNullable<Packages>[number] | undefined
|
|
||||||
rateSummary: Array<Rate | null>
|
rateSummary: Array<Rate | null>
|
||||||
rooms: SelectedRoom[]
|
rooms: SelectedRoom[]
|
||||||
roomCategories: Room[]
|
roomCategories: Room[]
|
||||||
roomConfigurations: RoomConfiguration[][]
|
roomConfigurations: RoomConfiguration[][]
|
||||||
|
roomsPackages: Package[][]
|
||||||
roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined
|
roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined
|
||||||
searchParams: ReadonlyURLSearchParams
|
searchParams: URLSearchParams
|
||||||
vat: number
|
vat: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +74,6 @@ export interface InitialState
|
|||||||
| "booking"
|
| "booking"
|
||||||
| "hotelType"
|
| "hotelType"
|
||||||
| "isUserLoggedIn"
|
| "isUserLoggedIn"
|
||||||
| "packages"
|
|
||||||
| "pathname"
|
| "pathname"
|
||||||
| "roomCategories"
|
| "roomCategories"
|
||||||
| "roomsAvailability"
|
| "roomsAvailability"
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import {
|
|
||||||
type getHotelsByHotelIdsAvailabilityInputSchema,
|
|
||||||
type hotelsAvailabilityInputSchema,
|
|
||||||
type roomsCombinedAvailabilityInputSchema,
|
|
||||||
type selectedRoomAvailabilityInputSchema,
|
|
||||||
} from "@/server/routers/hotels/input"
|
|
||||||
|
|
||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
enterDetailsRoomsAvailabilityInputSchema,
|
||||||
|
getHotelsByHotelIdsAvailabilityInputSchema,
|
||||||
|
hotelsAvailabilityInputSchema,
|
||||||
|
selectRateRoomsAvailabilityInputSchema,
|
||||||
|
} from "@/server/routers/hotels/input"
|
||||||
import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output"
|
import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output"
|
||||||
import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType"
|
import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType"
|
||||||
import type {
|
import type {
|
||||||
@@ -23,12 +22,15 @@ export type HotelsAvailabilityInputSchema = z.output<
|
|||||||
export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
|
export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
|
||||||
typeof getHotelsByHotelIdsAvailabilityInputSchema
|
typeof getHotelsByHotelIdsAvailabilityInputSchema
|
||||||
>
|
>
|
||||||
export type RoomsCombinedAvailabilityInputSchema = z.output<
|
export type RoomsAvailabilityInputSchema = z.input<
|
||||||
typeof roomsCombinedAvailabilityInputSchema
|
typeof selectRateRoomsAvailabilityInputSchema
|
||||||
>
|
>
|
||||||
export type SelectedRoomAvailabilitySchema = z.output<
|
export type RoomsAvailabilityInputRoom =
|
||||||
typeof selectedRoomAvailabilityInputSchema
|
RoomsAvailabilityInputSchema["booking"]["rooms"][number]
|
||||||
|
export type RoomsAvailabilityExtendedInputSchema = z.input<
|
||||||
|
typeof enterDetailsRoomsAvailabilityInputSchema
|
||||||
>
|
>
|
||||||
|
|
||||||
export type ProductType = z.output<typeof productTypeSchema>
|
export type ProductType = z.output<typeof productTypeSchema>
|
||||||
export type ProductTypePrices = z.output<typeof productTypePriceSchema>
|
export type ProductTypePrices = z.output<typeof productTypePriceSchema>
|
||||||
export type ProductTypePoints = z.output<typeof productTypePointsSchema>
|
export type ProductTypePoints = z.output<typeof productTypePointsSchema>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import type { z } from "zod"
|
|
||||||
|
|
||||||
import type { rateSchema } from "@/server/routers/hotels/schemas/rate"
|
|
||||||
|
|
||||||
export type Rate = z.output<typeof rateSchema>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
import type { RouterOutput } from "@/lib/trpc/client"
|
|
||||||
import type { roomsAvailabilitySchema } from "@/server/routers/hotels/output"
|
import type { roomsAvailabilitySchema } from "@/server/routers/hotels/output"
|
||||||
import type { roomConfigurationSchema } from "@/server/routers/hotels/schemas/roomAvailability/configuration"
|
import type { roomConfigurationSchema } from "@/server/routers/hotels/schemas/roomAvailability/configuration"
|
||||||
import type {
|
import type {
|
||||||
@@ -13,10 +12,6 @@ import type {
|
|||||||
} from "@/server/routers/hotels/schemas/roomAvailability/product"
|
} from "@/server/routers/hotels/schemas/roomAvailability/product"
|
||||||
import type { rateDefinitionSchema } from "@/server/routers/hotels/schemas/roomAvailability/rateDefinition"
|
import type { rateDefinitionSchema } from "@/server/routers/hotels/schemas/roomAvailability/rateDefinition"
|
||||||
|
|
||||||
export type RoomAvailability = NonNullable<
|
|
||||||
RouterOutput["hotel"]["availability"]["room"]
|
|
||||||
>
|
|
||||||
|
|
||||||
export type CorporateChequeProduct = z.output<typeof corporateChequeProduct>
|
export type CorporateChequeProduct = z.output<typeof corporateChequeProduct>
|
||||||
export type PriceProduct = z.output<typeof priceProduct>
|
export type PriceProduct = z.output<typeof priceProduct>
|
||||||
export type RedemptionProduct = z.output<typeof redemptionProduct>
|
export type RedemptionProduct = z.output<typeof redemptionProduct>
|
||||||
|
|||||||
Reference in New Issue
Block a user