feat: bedtypes is selectable again
This commit is contained in:
committed by
Michael Zetterberg
parent
f62723c6e5
commit
afb37d0cc5
@@ -149,9 +149,13 @@ export default function PriceDetailsTable({
|
||||
<Fragment key={idx}>
|
||||
<TableSection>
|
||||
{rooms.length > 1 && (
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||
</Body>
|
||||
<tr>
|
||||
<th colSpan={2}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||
</Body>
|
||||
</th>
|
||||
</tr>
|
||||
)}
|
||||
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
||||
{price && (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
||||
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" }))
|
||||
return
|
||||
}
|
||||
|
||||
// Update room details with server response data
|
||||
|
||||
const originalCheckIn = dt(bookedRoom.checkInDate)
|
||||
const originalCheckOut = dt(bookedRoom.checkOutDate)
|
||||
|
||||
updateBookedRoom({
|
||||
...bookedRoom,
|
||||
checkInDate: dt(updatedBooking.checkInDate)
|
||||
.hour(originalCheckIn.hour())
|
||||
.minute(originalCheckIn.minute())
|
||||
.second(originalCheckIn.second())
|
||||
.toDate(),
|
||||
checkOutDate: dt(updatedBooking.checkOutDate)
|
||||
.hour(originalCheckOut.hour())
|
||||
.minute(originalCheckOut.minute())
|
||||
.second(originalCheckOut.second())
|
||||
.toDate(),
|
||||
checkInDate: updatedBooking.checkInDate,
|
||||
checkOutDate: updatedBooking.checkOutDate,
|
||||
})
|
||||
|
||||
toast.success(intl.formatMessage({ id: "Your stay was updated" }))
|
||||
@@ -90,22 +76,25 @@ export default function useModifyStay({
|
||||
let totalNewPrice = 0
|
||||
|
||||
try {
|
||||
const data = await utils.hotel.availability.room.fetch({
|
||||
hotelId: bookedRoom.hotelId,
|
||||
roomStayStartDate: formValues.checkInDate,
|
||||
roomStayEndDate: formValues.checkOutDate,
|
||||
adults: bookedRoom.adults,
|
||||
children: bookedRoom.childrenAsString,
|
||||
bookingCode: bookedRoom.bookingCode ?? undefined,
|
||||
rateCode: bookedRoom.rateDefinition.rateCode,
|
||||
roomTypeCode: bookedRoom.roomTypeCode,
|
||||
const data = await utils.hotel.availability.myStay.fetch({
|
||||
booking: {
|
||||
fromDate: formValues.checkInDate,
|
||||
hotelId: bookedRoom.hotelId,
|
||||
room: {
|
||||
adults: bookedRoom.adults,
|
||||
bookingCode: bookedRoom.bookingCode ?? undefined,
|
||||
childrenInRoom: bookedRoom.childrenInRoom,
|
||||
rateCode: bookedRoom.rateDefinition.rateCode,
|
||||
roomTypeCode: bookedRoom.roomTypeCode,
|
||||
},
|
||||
toDate: formValues.checkOutDate,
|
||||
},
|
||||
lang,
|
||||
})
|
||||
|
||||
if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) {
|
||||
return { success: false, noAvailability: true }
|
||||
}
|
||||
|
||||
let roomPrice = 0
|
||||
if (isLoggedIn && "member" in data.product && data.product.member) {
|
||||
roomPrice = data.product.member.localPrice.pricePerStay
|
||||
@@ -123,7 +112,6 @@ export default function useModifyStay({
|
||||
) {
|
||||
roomPrice = data.product.redemption.localPrice.additionalPricePerStay
|
||||
}
|
||||
|
||||
totalNewPrice += roomPrice
|
||||
availabilityResults.push(data)
|
||||
} catch (error) {
|
||||
|
||||
@@ -167,9 +167,7 @@ export default function ModifyStay({ isLoggedIn }: ModifyStayProps) {
|
||||
label: isFirstStep
|
||||
? intl.formatMessage({ id: "Check availability" })
|
||||
: intl.formatMessage({ id: "Confirm" }),
|
||||
onClick: isFirstStep
|
||||
? () => void onCheckAvailability()
|
||||
: () => void handleModifyStay(),
|
||||
onClick: isFirstStep ? onCheckAvailability : handleModifyStay,
|
||||
intent: isFirstStep ? "secondary" : "primary",
|
||||
isLoading: isLoading,
|
||||
disabled: isLoading,
|
||||
|
||||
@@ -94,6 +94,8 @@ export function ReferenceCard({
|
||||
const {
|
||||
confirmationNumber,
|
||||
cancellationNumber,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
isCancelled,
|
||||
bookingCode,
|
||||
rateDefinition,
|
||||
@@ -222,7 +224,7 @@ export function ReferenceCard({
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<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>
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -232,7 +234,7 @@ export function ReferenceCard({
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<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>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
@@ -27,21 +27,14 @@ export default function MobileSummary({
|
||||
const scrollY = useRef(0)
|
||||
const [isSummaryOpen, setIsSummaryOpen] = useState(false)
|
||||
|
||||
const {
|
||||
booking,
|
||||
bookingRooms,
|
||||
roomsAvailability,
|
||||
rateSummary,
|
||||
vat,
|
||||
packages,
|
||||
} = useRatesStore((state) => ({
|
||||
booking: state.booking,
|
||||
bookingRooms: state.booking.rooms,
|
||||
roomsAvailability: state.roomsAvailability,
|
||||
rateSummary: state.rateSummary,
|
||||
vat: state.vat,
|
||||
packages: state.packages,
|
||||
}))
|
||||
const { booking, bookingRooms, roomsAvailability, rateSummary, vat } =
|
||||
useRatesStore((state) => ({
|
||||
booking: state.booking,
|
||||
bookingRooms: state.booking.rooms,
|
||||
roomsAvailability: state.roomsAvailability,
|
||||
rateSummary: state.rateSummary,
|
||||
vat: state.vat,
|
||||
}))
|
||||
|
||||
function toggleSummaryOpen() {
|
||||
setIsSummaryOpen(!isSummaryOpen)
|
||||
@@ -78,7 +71,7 @@ export default function MobileSummary({
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -11,10 +11,6 @@ export function mapRate(
|
||||
bookingRooms: Room[],
|
||||
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 = {
|
||||
adults: bookingRooms[index].adults,
|
||||
cancellationText: room.product.rateDefinition?.cancellationText ?? "",
|
||||
@@ -39,7 +35,7 @@ export function mapRate(
|
||||
},
|
||||
roomRate: room.product,
|
||||
roomType: room.roomType,
|
||||
packages: roomPackages,
|
||||
packages,
|
||||
}
|
||||
|
||||
if ("corporateCheque" in room.product) {
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
bookingCode,
|
||||
bookingRooms,
|
||||
dates,
|
||||
petRoomPackage,
|
||||
isFetchingPackages,
|
||||
rateSummary,
|
||||
roomsAvailability,
|
||||
searchParams,
|
||||
@@ -41,7 +41,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
checkInDate: state.booking.fromDate,
|
||||
checkOutDate: state.booking.toDate,
|
||||
},
|
||||
petRoomPackage: state.petRoomPackage,
|
||||
isFetchingPackages: state.rooms.some((room) => room.isFetchingPackages),
|
||||
rateSummary: state.rateSummary,
|
||||
roomsAvailability: state.roomsAvailability,
|
||||
searchParams: state.searchParams,
|
||||
@@ -123,7 +123,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
})
|
||||
}
|
||||
|
||||
if (!rateSummary.length) {
|
||||
if (!rateSummary.length || isFetchingPackages) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -149,8 +149,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
const totalPriceToShow = getTotalPrice(
|
||||
mainRoomProduct,
|
||||
rateSummary,
|
||||
isUserLoggedIn,
|
||||
petRoomPackage
|
||||
isUserLoggedIn
|
||||
)
|
||||
|
||||
const rateProduct = rateSummary.find((rate) => rate?.product)?.product
|
||||
@@ -248,7 +247,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
return total
|
||||
}
|
||||
|
||||
const { features, packages: roomPackages, product } = rate
|
||||
const { packages: roomPackages, product } = rate
|
||||
|
||||
const memberExists = "member" in product && product.member
|
||||
const publicExists = "public" in product && product.public
|
||||
@@ -266,21 +265,15 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
return total
|
||||
}
|
||||
|
||||
const hasSelectedPetRoom = roomPackages.includes(
|
||||
RoomPackageCodeEnum.PET_ROOM
|
||||
const hasSelectedPetRoom = roomPackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)
|
||||
if (!hasSelectedPetRoom) {
|
||||
return total + price
|
||||
}
|
||||
const isPetRoom = features.find(
|
||||
(feature) =>
|
||||
feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
return (
|
||||
total + price + hasSelectedPetRoom.localPrice.totalPrice
|
||||
)
|
||||
const petRoomPrice =
|
||||
isPetRoom && petRoomPackage
|
||||
? Number(petRoomPackage.localPrice.totalPrice)
|
||||
: 0
|
||||
return total + price + petRoomPrice
|
||||
}, 0),
|
||||
currency: mainRoomCurrency,
|
||||
}}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
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 { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { Packages } from "@/types/requests/packages"
|
||||
import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
export function calculateTotalPrice(
|
||||
selectedRateSummary: Rate[],
|
||||
isUserLoggedIn: boolean,
|
||||
petRoomPackage: RoomPackage | undefined
|
||||
isUserLoggedIn: boolean
|
||||
) {
|
||||
return selectedRateSummary.reduce<Price>(
|
||||
(total, room, idx) => {
|
||||
@@ -32,35 +26,26 @@ export function calculateTotalPrice(
|
||||
return total
|
||||
}
|
||||
|
||||
const isPetRoom = room.features.find(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
const packagesPrice = room.packages.reduce(
|
||||
(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.price =
|
||||
total.local.price + rate.localPrice.pricePerStay + petRoomPriceLocal
|
||||
total.local.price + rate.localPrice.pricePerStay + packagesPrice.local
|
||||
|
||||
if (rate.localPrice.regularPricePerStay) {
|
||||
total.local.regularPrice =
|
||||
(total.local.regularPrice || 0) +
|
||||
rate.localPrice.regularPricePerStay +
|
||||
petRoomPriceLocal
|
||||
packagesPrice.local
|
||||
}
|
||||
|
||||
if (rate.requestedPrice) {
|
||||
@@ -78,13 +63,13 @@ export function calculateTotalPrice(
|
||||
total.requested.price =
|
||||
total.requested.price +
|
||||
rate.requestedPrice.pricePerStay +
|
||||
petRoomPriceRequested
|
||||
packagesPrice.requested
|
||||
|
||||
if (rate.requestedPrice.regularPricePerStay) {
|
||||
total.requested.regularPrice =
|
||||
(total.requested.regularPrice || 0) +
|
||||
rate.requestedPrice.regularPricePerStay +
|
||||
petRoomPriceRequested
|
||||
packagesPrice.requested
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,8 +184,7 @@ export function calculateCorporateChequePrice(selectedRateSummary: Rate[]) {
|
||||
export function getTotalPrice(
|
||||
mainRoomProduct: Rate | null,
|
||||
rateSummary: Array<Rate | null>,
|
||||
isUserLoggedIn: boolean,
|
||||
petRoomPackage: NonNullable<Packages>[number] | undefined
|
||||
isUserLoggedIn: boolean
|
||||
): Price | null {
|
||||
const summaryArray = rateSummary.filter((rate): rate is Rate => rate !== null)
|
||||
|
||||
@@ -209,7 +193,7 @@ export function getTotalPrice(
|
||||
}
|
||||
|
||||
if (!mainRoomProduct) {
|
||||
return calculateTotalPrice(summaryArray, isUserLoggedIn, petRoomPackage)
|
||||
return calculateTotalPrice(summaryArray, isUserLoggedIn)
|
||||
}
|
||||
|
||||
const { product } = mainRoomProduct
|
||||
@@ -222,5 +206,5 @@ export function getTotalPrice(
|
||||
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 { dt } from "@/lib/dt"
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
@@ -15,22 +16,31 @@ import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import styles from "./selectedRoomPanel.module.css"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import { RateEnum } from "@/types/enums/rate"
|
||||
|
||||
export default function SelectedRoomPanel() {
|
||||
const intl = useIntl()
|
||||
const { isUserLoggedIn, roomCategories, rooms } = useRatesStore((state) => ({
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
roomCategories: state.roomCategories,
|
||||
rooms: state.rooms,
|
||||
}))
|
||||
const { dates, isUserLoggedIn, roomCategories, rooms } = useRatesStore(
|
||||
(state) => ({
|
||||
dates: {
|
||||
from: state.booking.fromDate,
|
||||
to: state.booking.toDate,
|
||||
},
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
roomCategories: state.roomCategories,
|
||||
rooms: state.rooms,
|
||||
})
|
||||
)
|
||||
const {
|
||||
actions: { modifyRate },
|
||||
isMainRoom,
|
||||
roomNr,
|
||||
selectedPackages,
|
||||
selectedRate,
|
||||
} = useRoomContext()
|
||||
const nights = dt(dates.to).diff(dt(dates.from), "days")
|
||||
|
||||
const images = roomCategories.find((roomCategory) =>
|
||||
roomCategory.roomTypes.some(
|
||||
@@ -60,8 +70,16 @@ export default function SelectedRoomPanel() {
|
||||
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 isPerNight = true
|
||||
if (
|
||||
isUserLoggedIn &&
|
||||
isMainRoom &&
|
||||
@@ -69,19 +87,17 @@ export default function SelectedRoomPanel() {
|
||||
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) {
|
||||
const { localPrice } = selectedRate.product.public
|
||||
selectedProduct = `${localPrice.pricePerNight} ${localPrice.currency}`
|
||||
selectedProduct = `${localPrice.pricePerNight + petRoomPrice} ${localPrice.currency} / ${night}`
|
||||
} else if ("corporateCheque" in selectedRate.product) {
|
||||
isPerNight = false
|
||||
const { localPrice } = selectedRate.product.corporateCheque
|
||||
selectedProduct = `${localPrice.numberOfCheques} ${CurrencyEnum.CC}`
|
||||
if (localPrice.additionalPricePerStay && localPrice.currency) {
|
||||
selectedProduct = `${selectedProduct} + ${localPrice.additionalPricePerStay} ${localPrice.currency}`
|
||||
}
|
||||
} else if ("voucher" in selectedRate.product) {
|
||||
isPerNight = false
|
||||
selectedProduct = `${selectedRate.product.voucher.numberOfVouchers} ${CurrencyEnum.Voucher}`
|
||||
}
|
||||
|
||||
@@ -109,9 +125,7 @@ export default function SelectedRoomPanel() {
|
||||
<Body color="uiTextMediumContrast">
|
||||
{getRateTitle(selectedRate.product.rate)}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${selectedProduct}${isPerNight ? "/" + intl.formatMessage({ id: "night" }) : ""}`}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{selectedProduct}</Body>
|
||||
</div>
|
||||
<div className={styles.imageContainer}>
|
||||
{images?.[0]?.imageSizes?.tiny ? (
|
||||
|
||||
@@ -17,12 +17,16 @@ export default function NoAvailabilityAlert() {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
|
||||
const { rooms } = useRoomContext()
|
||||
const { isFetchingPackages, rooms } = useRoomContext()
|
||||
|
||||
const noAvailableRooms = rooms.every(
|
||||
(roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable
|
||||
)
|
||||
|
||||
if (isFetchingPackages) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (noAvailableRooms) {
|
||||
const text = intl.formatMessage({
|
||||
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 { trpc } from "@/lib/trpc/client"
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./bookingCodeFilter.module.css"
|
||||
|
||||
@@ -16,12 +18,16 @@ import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
|
||||
export default function BookingCodeFilter() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const utils = trpc.useUtils()
|
||||
const {
|
||||
actions: { selectFilter },
|
||||
selectedFilter,
|
||||
actions: { appendRegularRates, selectFilter },
|
||||
bookingRoom,
|
||||
rooms,
|
||||
selectedFilter,
|
||||
selectedPackages,
|
||||
} = useRoomContext()
|
||||
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
|
||||
const booking = useRatesStore((state) => state.booking)
|
||||
|
||||
const bookingCodeFilterItems = [
|
||||
{
|
||||
@@ -38,25 +44,45 @@ export default function BookingCodeFilter() {
|
||||
},
|
||||
]
|
||||
|
||||
function handleChangeFilter(selectedFilter: Key) {
|
||||
async function handleChangeFilter(selectedFilter: Key) {
|
||||
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) =>
|
||||
room.products.every((product) => {
|
||||
const isRedemption = Array.isArray(product)
|
||||
if (isRedemption) {
|
||||
return true
|
||||
}
|
||||
const isCorporateCheque =
|
||||
product.rateDefinition?.rateType === RateTypeEnum.CorporateCheque
|
||||
const isVoucher =
|
||||
product.rateDefinition?.rateType === RateTypeEnum.Voucher
|
||||
return isCorporateCheque || isVoucher
|
||||
})
|
||||
)
|
||||
const hideFilterDespiteBookingCode =
|
||||
rooms.length &&
|
||||
rooms.every((room) =>
|
||||
room.products.every((product) => {
|
||||
const isRedemption = Array.isArray(product)
|
||||
if (isRedemption) {
|
||||
return true
|
||||
}
|
||||
const isCorporateCheque =
|
||||
product.rateDefinition?.rateType === RateTypeEnum.CorporateCheque
|
||||
const isVoucher =
|
||||
product.rateDefinition?.rateType === RateTypeEnum.Voucher
|
||||
return isCorporateCheque || isVoucher
|
||||
})
|
||||
)
|
||||
|
||||
if ((bookingCode && hideFilterDespiteBookingCode) || !bookingCode) {
|
||||
if (
|
||||
(booking.bookingCode && hideFilterDespiteBookingCode) ||
|
||||
!booking.bookingCode
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -42,8 +15,14 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.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;
|
||||
}
|
||||
@@ -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 type { PackageEnum } from "@/types/requests/packages"
|
||||
|
||||
export function getIconNameByPackageCode(
|
||||
packageCode: RoomPackageCodeEnum
|
||||
): MaterialSymbolProps["icon"] {
|
||||
packageCode: PackageEnum
|
||||
): SymbolCodepoints {
|
||||
switch (packageCode) {
|
||||
case RoomPackageCodeEnum.PET_ROOM:
|
||||
return "pets"
|
||||
@@ -5,15 +5,15 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import BookingCodeFilter from "../BookingCodeFilter"
|
||||
import RoomPackageFilter from "../RoomPackageFilter"
|
||||
import BookingCodeFilter from "./BookingCodeFilter"
|
||||
import RoomPackageFilter from "./RoomPackageFilter"
|
||||
|
||||
import styles from "./roomsHeader.module.css"
|
||||
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
|
||||
export default function RoomsHeader() {
|
||||
const { rooms, totalRooms } = useRoomContext()
|
||||
const { isFetchingPackages, rooms, totalRooms } = useRoomContext()
|
||||
const intl = useIntl()
|
||||
|
||||
const availableRooms = rooms.filter(
|
||||
@@ -42,11 +42,15 @@ export default function RoomsHeader() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Typography variant="Title/Subtitle/md" className={styles.availableRooms}>
|
||||
<p>
|
||||
{availableRooms !== totalRooms
|
||||
? notAllRoomsAvailableText
|
||||
: allRoomsAvailableText}
|
||||
</p>
|
||||
{isFetchingPackages ? (
|
||||
<p></p>
|
||||
) : (
|
||||
<p>
|
||||
{availableRooms !== totalRooms
|
||||
? notAllRoomsAvailableText
|
||||
: allRoomsAvailableText}
|
||||
</p>
|
||||
)}
|
||||
</Typography>
|
||||
<div className={styles.filters}>
|
||||
<RoomPackageFilter />
|
||||
|
||||
@@ -37,24 +37,21 @@ export default function Rates({
|
||||
selectedFilter,
|
||||
selectedPackages,
|
||||
} = useRoomContext()
|
||||
const { nights, petRoomPackage } = useRatesStore((state) => ({
|
||||
nights: dt(state.booking.toDate).diff(state.booking.fromDate, "days"),
|
||||
petRoomPackage: state.petRoomPackage,
|
||||
}))
|
||||
|
||||
const nights = useRatesStore((state) =>
|
||||
dt(state.booking.toDate).diff(state.booking.fromDate, "days")
|
||||
)
|
||||
function handleSelectRate(product: Product) {
|
||||
selectRate({ features, product, roomType, roomTypeCode })
|
||||
}
|
||||
|
||||
const petRoomPackageSelected = selectedPackages.includes(
|
||||
RoomPackageCodeEnum.PET_ROOM
|
||||
const petRoomPackageSelected = selectedPackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)
|
||||
|
||||
const sharedProps = {
|
||||
handleSelectRate,
|
||||
nights,
|
||||
petRoomPackage:
|
||||
petRoomPackageSelected && petRoomPackage ? petRoomPackage : undefined,
|
||||
petRoomPackage: petRoomPackageSelected,
|
||||
roomTypeCode,
|
||||
}
|
||||
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(
|
||||
pricePerNight: number,
|
||||
requestedPricePerNight: number | undefined,
|
||||
nights: number,
|
||||
petRoomPackage?: RoomPackage
|
||||
petRoomPackage?: Package
|
||||
) {
|
||||
const totalPrice = petRoomPackage?.localPrice
|
||||
? 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"
|
||||
|
||||
export default function RoomImage({
|
||||
features,
|
||||
roomPackages,
|
||||
roomsLeft,
|
||||
roomType,
|
||||
roomTypeCode,
|
||||
@@ -44,11 +44,13 @@ export default function RoomImage({
|
||||
</Footnote>
|
||||
</span>
|
||||
) : null}
|
||||
{features
|
||||
.filter((feature) => selectedPackages.includes(feature.code))
|
||||
.map((feature) => (
|
||||
<span className={styles.chip} key={feature.code}>
|
||||
{IconForFeatureCode({ featureCode: feature.code, size: 16 })}
|
||||
{roomPackages
|
||||
.filter((pkg) =>
|
||||
selectedPackages.find((spkg) => spkg.code === pkg.code)
|
||||
)
|
||||
.map((pkg) => (
|
||||
<span className={styles.chip} key={pkg.code}>
|
||||
{IconForFeatureCode({ featureCode: pkg.code, size: 16 })}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import Details from "./Details"
|
||||
import { listItemVariants } from "./listItemVariants"
|
||||
import Rates from "./Rates"
|
||||
@@ -12,6 +14,7 @@ import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHote
|
||||
import type { RoomListItemProps } from "@/types/components/hotelReservation/selectRate/roomListItem"
|
||||
|
||||
export default function RoomListItem({ roomConfiguration }: RoomListItemProps) {
|
||||
const { roomPackages } = useRoomContext()
|
||||
const classNames = listItemVariants({
|
||||
availability:
|
||||
roomConfiguration.status === AvailabilityEnum.NotAvailable
|
||||
@@ -22,7 +25,7 @@ export default function RoomListItem({ roomConfiguration }: RoomListItemProps) {
|
||||
return (
|
||||
<li className={classNames}>
|
||||
<RoomImage
|
||||
features={roomConfiguration.features}
|
||||
roomPackages={roomPackages}
|
||||
roomType={roomConfiguration.roomType}
|
||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||
roomsLeft={roomConfiguration.roomsLeft}
|
||||
|
||||
@@ -8,12 +8,10 @@ import ScrollToList from "./ScrollToList"
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
export default function RoomsList() {
|
||||
const { rooms, isFetchingRoomFeatures } = useRoomContext()
|
||||
|
||||
if (isFetchingRoomFeatures) {
|
||||
const { isFetchingPackages, rooms } = useRoomContext()
|
||||
if (isFetchingPackages) {
|
||||
return <RoomsListSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollToList />
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
import RatesProvider from "@/providers/RatesProvider"
|
||||
|
||||
import { useHotelPackages, useRoomsAvailability } from "../utils"
|
||||
import RateSummary from "./RateSummary"
|
||||
import Rooms from "./Rooms"
|
||||
import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton"
|
||||
@@ -13,57 +12,34 @@ import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton"
|
||||
import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer"
|
||||
|
||||
export function RoomsContainer({
|
||||
adultArray,
|
||||
booking,
|
||||
childArray,
|
||||
fromDate,
|
||||
hotelData,
|
||||
hotelType,
|
||||
isUserLoggedIn,
|
||||
toDate,
|
||||
roomCategories,
|
||||
vat,
|
||||
}: RoomsContainerProps) {
|
||||
const lang = useLang()
|
||||
|
||||
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
||||
const toDateString = dt(toDate).format("YYYY-MM-DD")
|
||||
const roomsAvailability = trpc.hotel.availability.selectRate.rooms.useQuery({
|
||||
booking,
|
||||
lang,
|
||||
})
|
||||
|
||||
const { data: roomsAvailability, isPending: isLoadingAvailability } =
|
||||
useRoomsAvailability(
|
||||
adultArray,
|
||||
hotelData.hotel.id,
|
||||
fromDateString,
|
||||
toDateString,
|
||||
lang,
|
||||
childArray,
|
||||
booking
|
||||
)
|
||||
|
||||
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
|
||||
adultArray,
|
||||
childArray,
|
||||
fromDateString,
|
||||
toDateString,
|
||||
hotelData.hotel.id,
|
||||
lang
|
||||
)
|
||||
|
||||
if (isLoadingAvailability || isLoadingPackages) {
|
||||
if (
|
||||
(roomsAvailability.isFetching || !roomsAvailability.data) &&
|
||||
!roomsAvailability.isFetched
|
||||
) {
|
||||
return <RoomsContainerSkeleton />
|
||||
}
|
||||
|
||||
if (packages === null) {
|
||||
// TODO: Log packages error
|
||||
console.error("[RoomsContainer] unable to fetch packages")
|
||||
}
|
||||
|
||||
return (
|
||||
<RatesProvider
|
||||
booking={booking}
|
||||
hotelType={hotelData.hotel.hotelType}
|
||||
hotelType={hotelType}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
packages={packages}
|
||||
roomCategories={hotelData.roomCategories}
|
||||
roomsAvailability={roomsAvailability}
|
||||
vat={hotelData.hotel.vat}
|
||||
roomCategories={roomCategories}
|
||||
roomsAvailability={roomsAvailability.data}
|
||||
vat={vat}
|
||||
>
|
||||
<Rooms />
|
||||
<RateSummary isUserLoggedIn={isUserLoggedIn} />
|
||||
|
||||
@@ -74,13 +74,11 @@ export default async function SelectRatePage({
|
||||
<HotelInfoCard hotel={hotelData.hotel} />
|
||||
|
||||
<RoomsContainer
|
||||
adultArray={adultsInRoom}
|
||||
booking={booking}
|
||||
childArray={childrenInRoom}
|
||||
fromDate={arrivalDate}
|
||||
hotelData={hotelData}
|
||||
hotelType={hotelData.hotel.hotelType}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
toDate={departureDate}
|
||||
roomCategories={hotelData.roomCategories}
|
||||
vat={hotelData.hotel.vat}
|
||||
/>
|
||||
|
||||
<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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user