Merged in feat/SW-2113-allow-feature-combinations (pull request #1719)
Feat/SW-2113 allow feature combinations * feat(SW-2113): Refactor features data to be fetched on filter room filter change * feat(SW-2113): added loading state * fix: now clear room selection when applying filter and room doesnt exists. And added room features to mobile summary * fix * fix: add package to price details * feat(SW-2113): added buttons to room filter * fix: active room * fix: remove console log * fix: added form and close handler to room package filter * fix: add restriction so you cannot select pet room with allergy room and vice versa * fix: fixes from review feedback * fix * fix: hide modify button if on nextcoming rooms if no selection is made, and adjust filter logic in togglePackage * fix: forgot to use roomFeatureCodes from input.. * fix: naming Approved-by: Simon.Emanuelsson
This commit is contained in:
@@ -100,6 +100,10 @@ export default function PriceDetailsTable({
|
|||||||
const isMainRoom = idx === 0
|
const isMainRoom = idx === 0
|
||||||
const getMemberRate = isMainRoom && isMember
|
const getMemberRate = isMainRoom && isMember
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
let price
|
let price
|
||||||
if (
|
if (
|
||||||
getMemberRate &&
|
getMemberRate &&
|
||||||
@@ -130,6 +134,17 @@ export default function PriceDetailsTable({
|
|||||||
price.localPrice.currency
|
price.localPrice.currency
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{room.packages?.map((pkg) => (
|
||||||
|
<Row
|
||||||
|
key={pkg.code}
|
||||||
|
label={pkg.description}
|
||||||
|
value={formatPrice(
|
||||||
|
intl,
|
||||||
|
+pkg.localPrice.totalPrice,
|
||||||
|
pkg.localPrice.currency
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<Row
|
<Row
|
||||||
bold
|
bold
|
||||||
label={intl.formatMessage({ id: "Room charge" })}
|
label={intl.formatMessage({ id: "Room charge" })}
|
||||||
|
|||||||
@@ -56,10 +56,11 @@ export default function Summary({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const memberPrice = getMemberPrice(rooms[0].roomRate)
|
const memberPrice =
|
||||||
|
rooms.length === 1 && rooms[0] ? getMemberPrice(rooms[0].roomRate) : null
|
||||||
|
|
||||||
const containsBookingCodeRate = rooms.find((r) =>
|
const containsBookingCodeRate = rooms.find(
|
||||||
isBookingCodeRate(r.roomRate)
|
(r) => r && isBookingCodeRate(r.roomRate)
|
||||||
)
|
)
|
||||||
const showDiscounted = containsBookingCodeRate || isMember
|
const showDiscounted = containsBookingCodeRate || isMember
|
||||||
|
|
||||||
@@ -93,6 +94,10 @@ export default function Summary({
|
|||||||
</header>
|
</header>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
{rooms.map((room, idx) => {
|
{rooms.map((room, idx) => {
|
||||||
|
if (!room) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const roomNumber = idx + 1
|
const roomNumber = idx + 1
|
||||||
const adults = room.adults
|
const adults = room.adults
|
||||||
const childrenInRoom = room.childrenInRoom
|
const childrenInRoom = room.childrenInRoom
|
||||||
@@ -137,6 +142,8 @@ export default function Summary({
|
|||||||
guestsParts.push(childrenMsg)
|
guestsParts.push(childrenMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roomPackages = room.packages
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={idx}>
|
<Fragment key={idx}>
|
||||||
<div
|
<div
|
||||||
@@ -245,6 +252,21 @@ export default function Summary({
|
|||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{roomPackages?.map((pkg) => (
|
||||||
|
<div className={styles.entry} key={pkg.code}>
|
||||||
|
<div>
|
||||||
|
<Body color="uiTextHighContrast">{pkg.description}</Body>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
pkg.localPrice.price,
|
||||||
|
pkg.localPrice.currency
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -27,14 +27,21 @@ export default function MobileSummary({
|
|||||||
const scrollY = useRef(0)
|
const scrollY = useRef(0)
|
||||||
const [isSummaryOpen, setIsSummaryOpen] = useState(false)
|
const [isSummaryOpen, setIsSummaryOpen] = useState(false)
|
||||||
|
|
||||||
const { booking, bookingRooms, roomsAvailability, rateSummary, vat } =
|
const {
|
||||||
useRatesStore((state) => ({
|
booking,
|
||||||
booking: state.booking,
|
bookingRooms,
|
||||||
bookingRooms: state.booking.rooms,
|
roomsAvailability,
|
||||||
roomsAvailability: state.roomsAvailability,
|
rateSummary,
|
||||||
rateSummary: state.rateSummary,
|
vat,
|
||||||
vat: state.vat,
|
packages,
|
||||||
}))
|
} = useRatesStore((state) => ({
|
||||||
|
booking: state.booking,
|
||||||
|
bookingRooms: state.booking.rooms,
|
||||||
|
roomsAvailability: state.roomsAvailability,
|
||||||
|
rateSummary: state.rateSummary,
|
||||||
|
vat: state.vat,
|
||||||
|
packages: state.packages,
|
||||||
|
}))
|
||||||
|
|
||||||
function toggleSummaryOpen() {
|
function toggleSummaryOpen() {
|
||||||
setIsSummaryOpen(!isSummaryOpen)
|
setIsSummaryOpen(!isSummaryOpen)
|
||||||
@@ -71,11 +78,11 @@ export default function MobileSummary({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rooms = rateSummary.map((room, index) =>
|
const rooms = rateSummary.map((room, index) =>
|
||||||
mapRate(room, index, bookingRooms)
|
room ? mapRate(room, index, bookingRooms, packages) : null
|
||||||
)
|
)
|
||||||
|
|
||||||
const containsBookingCodeRate = rateSummary.find((r) =>
|
const containsBookingCodeRate = rateSummary.find(
|
||||||
isBookingCodeRate(r.product)
|
(r) => r && isBookingCodeRate(r.product)
|
||||||
)
|
)
|
||||||
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
|
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,18 @@ import type {
|
|||||||
Room,
|
Room,
|
||||||
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
} 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"
|
||||||
|
|
||||||
|
export function mapRate(
|
||||||
|
room: Rate,
|
||||||
|
index: number,
|
||||||
|
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))
|
||||||
|
|
||||||
export function mapRate(room: Rate, index: number, bookingRooms: Room[]) {
|
|
||||||
const rate = {
|
const rate = {
|
||||||
adults: bookingRooms[index].adults,
|
adults: bookingRooms[index].adults,
|
||||||
cancellationText: room.product.rateDefinition?.cancellationText ?? "",
|
cancellationText: room.product.rateDefinition?.cancellationText ?? "",
|
||||||
@@ -29,6 +39,7 @@ export function mapRate(room: Rate, index: number, bookingRooms: Room[]) {
|
|||||||
},
|
},
|
||||||
roomRate: room.product,
|
roomRate: room.product,
|
||||||
roomType: room.roomType,
|
roomType: room.roomType,
|
||||||
|
packages: roomPackages,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("corporateCheque" in room.product) {
|
if ("corporateCheque" in room.product) {
|
||||||
|
|||||||
@@ -16,16 +16,10 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import MobileSummary from "./MobileSummary"
|
import MobileSummary from "./MobileSummary"
|
||||||
import {
|
import { getTotalPrice } from "./utils"
|
||||||
calculateCorporateChequePrice,
|
|
||||||
calculateRedemptionTotalPrice,
|
|
||||||
calculateTotalPrice,
|
|
||||||
calculateVoucherPrice,
|
|
||||||
} from "./utils"
|
|
||||||
|
|
||||||
import styles from "./rateSummary.module.css"
|
import styles from "./rateSummary.module.css"
|
||||||
|
|
||||||
import type { Price } from "@/types/components/hotelReservation/price"
|
|
||||||
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { RateEnum } from "@/types/enums/rate"
|
import { RateEnum } from "@/types/enums/rate"
|
||||||
@@ -96,9 +90,10 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
const summaryPriceText = `${totalNights}, ${totalAdults}${totalChildren}, ${totalRooms}`
|
const summaryPriceText = `${totalNights}, ${totalAdults}${totalChildren}, ${totalRooms}`
|
||||||
|
|
||||||
const totalRoomsRequired = bookingRooms.length
|
const totalRoomsRequired = bookingRooms.length
|
||||||
const isAllRoomsSelected = rateSummary.length === totalRoomsRequired
|
const isAllRoomsSelected =
|
||||||
|
rateSummary.filter((rate) => rate !== null).length === totalRoomsRequired
|
||||||
const hasMemberRates = rateSummary.some(
|
const hasMemberRates = rateSummary.some(
|
||||||
(room) => "member" in room.product && room.product.member
|
(room) => room && "member" in room.product && room.product.member
|
||||||
)
|
)
|
||||||
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
|
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
|
||||||
|
|
||||||
@@ -134,12 +129,15 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
|
|
||||||
const isBookingCodeRate = rateSummary.some(
|
const isBookingCodeRate = rateSummary.some(
|
||||||
(rate) =>
|
(rate) =>
|
||||||
|
rate &&
|
||||||
"public" in rate.product &&
|
"public" in rate.product &&
|
||||||
rate.product.public?.rateType !== RateTypeEnum.Regular
|
rate.product.public?.rateType !== RateTypeEnum.Regular
|
||||||
)
|
)
|
||||||
const isVoucherRate = rateSummary.some((rate) => "voucher" in rate.product)
|
const isVoucherRate = rateSummary.some(
|
||||||
|
(rate) => rate && "voucher" in rate.product
|
||||||
|
)
|
||||||
const isCorporateChequeRate = rateSummary.some(
|
const isCorporateChequeRate = rateSummary.some(
|
||||||
(rate) => "corporateCheque" in rate.product
|
(rate) => rate && "corporateCheque" in rate.product
|
||||||
)
|
)
|
||||||
const showDiscounted =
|
const showDiscounted =
|
||||||
isUserLoggedIn ||
|
isUserLoggedIn ||
|
||||||
@@ -148,37 +146,29 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
isCorporateChequeRate
|
isCorporateChequeRate
|
||||||
|
|
||||||
const mainRoomProduct = rateSummary[0]
|
const mainRoomProduct = rateSummary[0]
|
||||||
let totalPriceToShow: Price
|
const totalPriceToShow = getTotalPrice(
|
||||||
if ("redemption" in mainRoomProduct.product) {
|
mainRoomProduct,
|
||||||
// In case of reward night (redemption) only single room booking is supported by business rules
|
rateSummary,
|
||||||
totalPriceToShow = calculateRedemptionTotalPrice(
|
isUserLoggedIn,
|
||||||
mainRoomProduct.product.redemption
|
petRoomPackage
|
||||||
)
|
)
|
||||||
} else if ("voucher" in mainRoomProduct.product) {
|
|
||||||
totalPriceToShow = calculateVoucherPrice(rateSummary)
|
const rateProduct = rateSummary.find((rate) => rate?.product)?.product
|
||||||
} else if ("corporateCheque" in mainRoomProduct.product) {
|
|
||||||
totalPriceToShow = calculateCorporateChequePrice(rateSummary)
|
if (!totalPriceToShow || !rateProduct) {
|
||||||
} else {
|
return null
|
||||||
totalPriceToShow = calculateTotalPrice(
|
|
||||||
rateSummary,
|
|
||||||
isUserLoggedIn,
|
|
||||||
petRoomPackage
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainRoomCurrency = ""
|
let mainRoomCurrency = ""
|
||||||
if (
|
if ("member" in rateProduct && rateProduct.member?.localPrice) {
|
||||||
"member" in mainRoomProduct.product &&
|
mainRoomCurrency = rateProduct.member.localPrice.currency
|
||||||
mainRoomProduct.product.member?.localPrice
|
|
||||||
) {
|
|
||||||
mainRoomCurrency = mainRoomProduct.product.member.localPrice.currency
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!mainRoomCurrency &&
|
!mainRoomCurrency &&
|
||||||
"public" in mainRoomProduct.product &&
|
"public" in rateProduct &&
|
||||||
mainRoomProduct.product.public?.localPrice
|
rateProduct.public?.localPrice
|
||||||
) {
|
) {
|
||||||
mainRoomCurrency = mainRoomProduct.product.public.localPrice.currency
|
mainRoomCurrency = rateProduct.public.localPrice.currency
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -186,38 +176,56 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
<div className={styles.summary}>
|
<div className={styles.summary}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.summaryText}>
|
<div className={styles.summaryText}>
|
||||||
{rateSummary.map((room, index) => (
|
{rateSummary.map((room, index) => {
|
||||||
<div key={index} className={styles.roomSummary}>
|
if (!room) {
|
||||||
{rateSummary.length > 1 ? (
|
return (
|
||||||
<>
|
<div key={`unselected-${index}`}>
|
||||||
<Subtitle color="uiTextHighContrast">
|
<Subtitle color="uiTextPlaceholder">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "Room {roomIndex}" },
|
{ id: "Room {roomIndex}" },
|
||||||
{ roomIndex: index + 1 }
|
{ roomIndex: index + 1 }
|
||||||
)}
|
)}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="uiTextMediumContrast">{room.roomType}</Body>
|
<Body color="uiTextPlaceholder">
|
||||||
<Caption color="uiTextMediumContrast">
|
{intl.formatMessage({ id: "Select room" })}
|
||||||
{getRateDetails(room.rate)}
|
|
||||||
</Caption>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Subtitle color="uiTextHighContrast">
|
|
||||||
{room.roomType}
|
|
||||||
</Subtitle>
|
|
||||||
<Body color="uiTextMediumContrast">
|
|
||||||
{getRateDetails(room.rate)}
|
|
||||||
</Body>
|
</Body>
|
||||||
</>
|
</div>
|
||||||
)}
|
)
|
||||||
</div>
|
}
|
||||||
))}
|
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
{rateSummary.length > 1 ? (
|
||||||
|
<>
|
||||||
|
<Subtitle color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "Room {roomIndex}" },
|
||||||
|
{ roomIndex: index + 1 }
|
||||||
|
)}
|
||||||
|
</Subtitle>
|
||||||
|
<Body color="uiTextMediumContrast">{room.roomType}</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{getRateDetails(room.rate)}
|
||||||
|
</Caption>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Subtitle color="uiTextHighContrast">
|
||||||
|
{room.roomType}
|
||||||
|
</Subtitle>
|
||||||
|
<Body color="uiTextMediumContrast">
|
||||||
|
{getRateDetails(room.rate)}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
{/* Render unselected rooms */}
|
{/* Render unselected rooms */}
|
||||||
{Array.from({
|
{Array.from({
|
||||||
length: totalRoomsRequired - rateSummary.length,
|
length: totalRoomsRequired - rateSummary.length,
|
||||||
}).map((_, index) => (
|
}).map((_, index) => (
|
||||||
<div key={`unselected-${index}`} className={styles.roomSummary}>
|
<div key={`unselected-${index}`}>
|
||||||
<Subtitle color="uiTextPlaceholder">
|
<Subtitle color="uiTextPlaceholder">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "Room {roomIndex}" },
|
{ id: "Room {roomIndex}" },
|
||||||
@@ -235,47 +243,45 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
<div className={styles.promoContainer}>
|
<div className={styles.promoContainer}>
|
||||||
<SignupPromoDesktop
|
<SignupPromoDesktop
|
||||||
memberPrice={{
|
memberPrice={{
|
||||||
amount: rateSummary.reduce(
|
amount: rateSummary.reduce((total, rate) => {
|
||||||
(
|
if (!rate) {
|
||||||
total,
|
return total
|
||||||
{ features, packages: roomPackages, product }
|
}
|
||||||
) => {
|
|
||||||
const memberExists =
|
|
||||||
"member" in product && product.member
|
|
||||||
const publicExists =
|
|
||||||
"public" in product && product.public
|
|
||||||
if (!memberExists) {
|
|
||||||
if (!publicExists) {
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const price =
|
const { features, packages: roomPackages, product } = rate
|
||||||
product.member?.localPrice.pricePerStay ||
|
|
||||||
product.public?.localPrice.pricePerStay
|
|
||||||
|
|
||||||
if (!price) {
|
const memberExists = "member" in product && product.member
|
||||||
|
const publicExists = "public" in product && product.public
|
||||||
|
if (!memberExists) {
|
||||||
|
if (!publicExists) {
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hasSelectedPetRoom = roomPackages.includes(
|
const price =
|
||||||
RoomPackageCodeEnum.PET_ROOM
|
product.member?.localPrice.pricePerStay ||
|
||||||
)
|
product.public?.localPrice.pricePerStay
|
||||||
if (!hasSelectedPetRoom) {
|
|
||||||
return total + price
|
if (!price) {
|
||||||
}
|
return total
|
||||||
const isPetRoom = features.find(
|
}
|
||||||
(feature) =>
|
|
||||||
feature.code === RoomPackageCodeEnum.PET_ROOM
|
const hasSelectedPetRoom = roomPackages.includes(
|
||||||
)
|
RoomPackageCodeEnum.PET_ROOM
|
||||||
const petRoomPrice =
|
)
|
||||||
isPetRoom && petRoomPackage
|
if (!hasSelectedPetRoom) {
|
||||||
? Number(petRoomPackage.localPrice.totalPrice)
|
return total + price
|
||||||
: 0
|
}
|
||||||
return total + price + petRoomPrice
|
const isPetRoom = features.find(
|
||||||
},
|
(feature) =>
|
||||||
0
|
feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
),
|
)
|
||||||
|
const petRoomPrice =
|
||||||
|
isPetRoom && petRoomPackage
|
||||||
|
? Number(petRoomPackage.localPrice.totalPrice)
|
||||||
|
: 0
|
||||||
|
return total + price + petRoomPrice
|
||||||
|
}, 0),
|
||||||
currency: mainRoomCurrency,
|
currency: mainRoomCurrency,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
} 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(
|
||||||
@@ -194,3 +195,32 @@ export function calculateCorporateChequePrice(selectedRateSummary: Rate[]) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTotalPrice(
|
||||||
|
mainRoomProduct: Rate | null,
|
||||||
|
rateSummary: Array<Rate | null>,
|
||||||
|
isUserLoggedIn: boolean,
|
||||||
|
petRoomPackage: NonNullable<Packages>[number] | undefined
|
||||||
|
): Price | null {
|
||||||
|
const summaryArray = rateSummary.filter((rate): rate is Rate => rate !== null)
|
||||||
|
|
||||||
|
if (summaryArray.some((rate) => "corporateCheque" in rate.product)) {
|
||||||
|
return calculateCorporateChequePrice(summaryArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mainRoomProduct) {
|
||||||
|
return calculateTotalPrice(summaryArray, isUserLoggedIn, petRoomPackage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { product } = mainRoomProduct
|
||||||
|
|
||||||
|
// In case of reward night (redemption) or voucher only single room booking is supported by business rules
|
||||||
|
if ("redemption" in product) {
|
||||||
|
return calculateRedemptionTotalPrice(product.redemption)
|
||||||
|
}
|
||||||
|
if ("voucher" in product) {
|
||||||
|
return calculateVoucherPrice(summaryArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
return calculateTotalPrice(summaryArray, isUserLoggedIn, petRoomPackage)
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ import { RateEnum } from "@/types/enums/rate"
|
|||||||
|
|
||||||
export default function SelectedRoomPanel() {
|
export default function SelectedRoomPanel() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({
|
const { isUserLoggedIn, roomCategories, rooms } = useRatesStore((state) => ({
|
||||||
isUserLoggedIn: state.isUserLoggedIn,
|
isUserLoggedIn: state.isUserLoggedIn,
|
||||||
roomCategories: state.roomCategories,
|
roomCategories: state.roomCategories,
|
||||||
|
rooms: state.rooms,
|
||||||
}))
|
}))
|
||||||
const {
|
const {
|
||||||
actions: { modifyRate },
|
actions: { modifyRate },
|
||||||
@@ -89,6 +90,10 @@ export default function SelectedRoomPanel() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showModifyButton =
|
||||||
|
isMainRoom ||
|
||||||
|
(!isMainRoom && rooms.slice(0, roomNr).every((room) => room.selectedRate))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.selectedRoomPanel}>
|
<div className={styles.selectedRoomPanel}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
@@ -118,14 +123,16 @@ export default function SelectedRoomPanel() {
|
|||||||
width={600}
|
width={600}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={styles.modifyButtonContainer}>
|
{showModifyButton && (
|
||||||
<Button clean onClick={modifyRate}>
|
<div className={styles.modifyButtonContainer}>
|
||||||
<Chip size="small" variant="uiTextHighContrast">
|
<Button clean onClick={modifyRate}>
|
||||||
<MaterialIcon icon="edit_square" />
|
<Chip size="small" variant="uiTextHighContrast">
|
||||||
{intl.formatMessage({ id: "Modify" })}
|
<MaterialIcon icon="edit_square" />
|
||||||
</Chip>
|
{intl.formatMessage({ id: "Modify" })}
|
||||||
</Button>
|
</Chip>
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,6 +32,15 @@
|
|||||||
background-color: var(--UI-Input-Controls-Fill-Selected);
|
background-color: var(--UI-Input-Controls-Fill-Selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkboxWrapper[data-disabled] .checkbox {
|
||||||
|
border-color: var(--UI-Input-Controls-Border-Disabled);
|
||||||
|
background-color: var(--UI-Input-Controls-Surface-Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxWrapper[data-disabled] .text {
|
||||||
|
color: var(--Base-Text-Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.checkboxWrapper:hover {
|
.checkboxWrapper:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface CheckboxProps {
|
|||||||
value: string
|
value: string
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
iconName: MaterialSymbolProps["icon"]
|
iconName: MaterialSymbolProps["icon"]
|
||||||
|
isDisabled: boolean
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,12 +23,14 @@ export default function Checkbox({
|
|||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
iconName,
|
iconName,
|
||||||
|
isDisabled,
|
||||||
onChange,
|
onChange,
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
return (
|
return (
|
||||||
<AriaCheckbox
|
<AriaCheckbox
|
||||||
className={styles.checkboxWrapper}
|
className={styles.checkboxWrapper}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
isDisabled={isDisabled}
|
||||||
onChange={() => onChange(value)}
|
onChange={() => onChange(value)}
|
||||||
>
|
>
|
||||||
{({ isSelected }) => (
|
{({ isSelected }) => (
|
||||||
@@ -35,7 +38,10 @@ export default function Checkbox({
|
|||||||
<span className={styles.checkbox}>
|
<span className={styles.checkbox}>
|
||||||
{isSelected && <MaterialIcon icon="check" color="Icon/Inverted" />}
|
{isSelected && <MaterialIcon icon="check" color="Icon/Inverted" />}
|
||||||
</span>
|
</span>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography
|
||||||
|
variant="Body/Paragraph/mdRegular"
|
||||||
|
className={styles.text}
|
||||||
|
>
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
{iconName ? (
|
{iconName ? (
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { Button, Dialog, DialogTrigger, Popover } from "react-aria-components"
|
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 { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
|
import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
@@ -16,20 +24,46 @@ import { getIconNameByPackageCode } from "./utils"
|
|||||||
|
|
||||||
import styles from "./roomPackageFilter.module.css"
|
import styles from "./roomPackageFilter.module.css"
|
||||||
|
|
||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
selectedPackages: RoomPackageCodeEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
export default function RoomPackageFilter() {
|
export default function RoomPackageFilter() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const packageOptions = useRatesStore((state) => state.packageOptions)
|
const packageOptions = useRatesStore((state) => state.packageOptions)
|
||||||
const {
|
const {
|
||||||
actions: { togglePackage },
|
actions: { togglePackages },
|
||||||
selectedPackages,
|
selectedPackages,
|
||||||
} = useRoomContext()
|
} = useRoomContext()
|
||||||
const intl = useIntl()
|
|
||||||
|
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 (
|
return (
|
||||||
<div className={styles.roomPackageFilter}>
|
<div className={styles.roomPackageFilter}>
|
||||||
{selectedPackages.map((pkg) => (
|
{selectedPackages.map((pkg) => (
|
||||||
<Button
|
<AriaButton
|
||||||
key={pkg}
|
key={pkg}
|
||||||
onPress={() => togglePackage(pkg)}
|
onPress={() => {
|
||||||
|
const packages = selectedPackages.filter((s) => s !== pkg)
|
||||||
|
togglePackages(packages)
|
||||||
|
}}
|
||||||
className={styles.activeFilterButton}
|
className={styles.activeFilterButton}
|
||||||
>
|
>
|
||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
@@ -42,9 +76,9 @@ export default function RoomPackageFilter() {
|
|||||||
size={16}
|
size={16}
|
||||||
color="Icon/Interactive/Default"
|
color="Icon/Interactive/Default"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</AriaButton>
|
||||||
))}
|
))}
|
||||||
<DialogTrigger>
|
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||||
<ChipButton variant="Outlined">
|
<ChipButton variant="Outlined">
|
||||||
{intl.formatMessage({ id: "Room preferences" })}
|
{intl.formatMessage({ id: "Room preferences" })}
|
||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
@@ -55,39 +89,105 @@ export default function RoomPackageFilter() {
|
|||||||
</ChipButton>
|
</ChipButton>
|
||||||
<Popover placement="bottom end">
|
<Popover placement="bottom end">
|
||||||
<Dialog className={styles.dialog}>
|
<Dialog className={styles.dialog}>
|
||||||
<div className={styles.filters}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
{packageOptions.map((option) => (
|
<Controller
|
||||||
<Checkbox
|
control={control}
|
||||||
key={option.code}
|
name="selectedPackages"
|
||||||
name={option.description}
|
render={({ field }) => (
|
||||||
value={option.code}
|
<div>
|
||||||
iconName={getIconNameByPackageCode(option.code)}
|
{packageOptions.map((option) => {
|
||||||
isSelected={selectedPackages.includes(option.code)}
|
const isPetRoom =
|
||||||
onChange={() => togglePackage(option.code)}
|
option.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
/>
|
|
||||||
))}
|
const isAllergyRoom =
|
||||||
</div>
|
option.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||||
<div className={styles.footer}>
|
|
||||||
<Divider color="borderDividerSubtle" />
|
const hasPetRoom = field.value.includes(
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
RoomPackageCodeEnum.PET_ROOM
|
||||||
<p className={styles.additionalInformation}>
|
)
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
const hasAllergyRoom = field.value.includes(
|
||||||
id: "<b>200 SEK/night</b> Important information on pricing and features of pet-friendly rooms.",
|
RoomPackageCodeEnum.ALLERGY_ROOM
|
||||||
},
|
)
|
||||||
{
|
|
||||||
b: (str) => (
|
const isDisabled =
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
(isPetRoom && hasAllergyRoom) ||
|
||||||
<span className={styles.additionalInformationPrice}>
|
(isAllergyRoom && hasPetRoom)
|
||||||
{str}
|
|
||||||
</span>
|
return (
|
||||||
</Typography>
|
<>
|
||||||
),
|
<Checkbox
|
||||||
}
|
key={option.code}
|
||||||
)}
|
name={option.description}
|
||||||
</p>
|
value={option.code}
|
||||||
</Typography>
|
iconName={getIconNameByPackageCode(option.code)}
|
||||||
</div>
|
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>
|
</Dialog>
|
||||||
</Popover>
|
</Popover>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
.additionalInformation {
|
.additionalInformation {
|
||||||
color: var(--Text-Tertiary);
|
color: var(--Text-Tertiary);
|
||||||
|
padding: var(--Space-x1) var(--Space-x15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.additionalInformationPrice {
|
.additionalInformationPrice {
|
||||||
@@ -40,3 +41,9 @@
|
|||||||
border-width: 0;
|
border-width: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { RoomCardSkeleton } from "@/components/HotelReservation/RoomCardSkeleton/RoomCardSkeleton"
|
||||||
|
|
||||||
|
import styles from "./roomsListSkeleton.module.css"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomsListSkeleton({ count = 4 }: Props) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.skeletonContainer}>
|
||||||
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
|
<RoomCardSkeleton key={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,12 +2,18 @@
|
|||||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
|
|
||||||
import RoomListItem from "./RoomListItem"
|
import RoomListItem from "./RoomListItem"
|
||||||
|
import { RoomsListSkeleton } from "./RoomsListSkeleton"
|
||||||
import ScrollToList from "./ScrollToList"
|
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 } = useRoomContext()
|
const { rooms, isFetchingRoomFeatures } = useRoomContext()
|
||||||
|
|
||||||
|
if (isFetchingRoomFeatures) {
|
||||||
|
return <RoomsListSkeleton />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollToList />
|
<ScrollToList />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomList > li {
|
.roomList > li {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.container {
|
||||||
|
max-width: var(--max-width-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonContainer {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
/* used to hide overflowing rows */
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-auto-rows: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { REDEMPTION } from "@/constants/booking"
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
@@ -25,9 +25,6 @@ export function RoomsContainer({
|
|||||||
|
|
||||||
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
||||||
const toDateString = dt(toDate).format("YYYY-MM-DD")
|
const toDateString = dt(toDate).format("YYYY-MM-DD")
|
||||||
const redemption = booking.searchType
|
|
||||||
? booking.searchType === REDEMPTION
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const { data: roomsAvailability, isPending: isLoadingAvailability } =
|
const { data: roomsAvailability, isPending: isLoadingAvailability } =
|
||||||
useRoomsAvailability(
|
useRoomsAvailability(
|
||||||
@@ -37,8 +34,7 @@ export function RoomsContainer({
|
|||||||
toDateString,
|
toDateString,
|
||||||
lang,
|
lang,
|
||||||
childArray,
|
childArray,
|
||||||
booking.bookingCode,
|
booking
|
||||||
redemption
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
|
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { useSearchParams } from "next/navigation"
|
import { REDEMPTION } from "@/constants/booking"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
|
||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url"
|
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
@@ -17,79 +13,30 @@ export function useRoomsAvailability(
|
|||||||
toDateString: string,
|
toDateString: string,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
childArray: ChildrenInRoom,
|
childArray: ChildrenInRoom,
|
||||||
bookingCode?: string,
|
booking: SelectRateSearchParams
|
||||||
redemption?: boolean
|
|
||||||
) {
|
) {
|
||||||
const searchParams = useSearchParams()
|
const redemption = booking.searchType
|
||||||
const searchParamsObj = convertSearchParamsToObj<SelectRateSearchParams>(
|
? booking.searchType === REDEMPTION
|
||||||
searchParamsToRecord(searchParams)
|
: undefined
|
||||||
|
|
||||||
|
const roomFeatureCodesArray = booking.rooms.map(
|
||||||
|
(room) => room.packages ?? null
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasPackagesParam = searchParamsObj.rooms.some((room) => room.packages)
|
const roomsAvailability =
|
||||||
const [hasRoomFeatures, setHasRoomFeatures] = useState(hasPackagesParam)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setHasRoomFeatures(hasPackagesParam)
|
|
||||||
}, [hasPackagesParam, setHasRoomFeatures])
|
|
||||||
|
|
||||||
const { data: roomFeatures, isPending: isRoomFeaturesPending } =
|
|
||||||
trpc.hotel.availability.roomFeatures.useQuery(
|
|
||||||
{
|
|
||||||
hotelId,
|
|
||||||
startDate: fromDateString,
|
|
||||||
endDate: toDateString,
|
|
||||||
adultsCount,
|
|
||||||
childArray,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: hasRoomFeatures,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: roomsAvailability, isPending: isRoomsAvailabiltyPending } =
|
|
||||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
||||||
adultsCount,
|
adultsCount,
|
||||||
bookingCode,
|
|
||||||
childArray,
|
childArray,
|
||||||
hotelId,
|
hotelId,
|
||||||
lang,
|
lang,
|
||||||
redemption,
|
redemption,
|
||||||
roomStayEndDate: toDateString,
|
roomStayEndDate: toDateString,
|
||||||
roomStayStartDate: fromDateString,
|
roomStayStartDate: fromDateString,
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
|
roomFeatureCodesArray,
|
||||||
})
|
})
|
||||||
|
|
||||||
const combinedData = useMemo(() => {
|
return roomsAvailability
|
||||||
if (!roomsAvailability) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
if (!roomFeatures) {
|
|
||||||
return roomsAvailability
|
|
||||||
}
|
|
||||||
|
|
||||||
return roomsAvailability.map((room, idx) => {
|
|
||||||
if ("error" in room) {
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...room,
|
|
||||||
roomConfigurations: room.roomConfigurations.map((config) => ({
|
|
||||||
...config,
|
|
||||||
features:
|
|
||||||
roomFeatures?.[idx]?.find(
|
|
||||||
(r) => r.roomTypeCode === config.roomTypeCode
|
|
||||||
)?.features ?? [],
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [roomFeatures, roomsAvailability])
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: combinedData,
|
|
||||||
isPending: hasRoomFeatures
|
|
||||||
? isRoomsAvailabiltyPending || isRoomFeaturesPending
|
|
||||||
: isRoomsAvailabiltyPending,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useHotelPackages(
|
export function useHotelPackages(
|
||||||
|
|||||||
@@ -186,6 +186,7 @@
|
|||||||
"City pulse": "Byens puls",
|
"City pulse": "Byens puls",
|
||||||
"City/State": "By/Stat",
|
"City/State": "By/Stat",
|
||||||
"Classroom": "Classroom",
|
"Classroom": "Classroom",
|
||||||
|
"Clear": "Ryd",
|
||||||
"Clear all filters": "Ryd alle filtre",
|
"Clear all filters": "Ryd alle filtre",
|
||||||
"Clear searches": "Ryd søgninger",
|
"Clear searches": "Ryd søgninger",
|
||||||
"Click here to log in": "Klik her for at logge ind",
|
"Click here to log in": "Klik her for at logge ind",
|
||||||
|
|||||||
@@ -187,6 +187,7 @@
|
|||||||
"City pulse": "Stadtpuls",
|
"City pulse": "Stadtpuls",
|
||||||
"City/State": "Stadt/Zustand",
|
"City/State": "Stadt/Zustand",
|
||||||
"Classroom": "Classroom",
|
"Classroom": "Classroom",
|
||||||
|
"Clear": "Löschen",
|
||||||
"Clear all filters": "Alle Filter löschen",
|
"Clear all filters": "Alle Filter löschen",
|
||||||
"Clear searches": "Suche löschen",
|
"Clear searches": "Suche löschen",
|
||||||
"Click here to log in": "Klicken Sie hier, um sich einzuloggen",
|
"Click here to log in": "Klicken Sie hier, um sich einzuloggen",
|
||||||
|
|||||||
@@ -187,6 +187,7 @@
|
|||||||
"City pulse": "City pulse",
|
"City pulse": "City pulse",
|
||||||
"City/State": "City/State",
|
"City/State": "City/State",
|
||||||
"Classroom": "Classroom",
|
"Classroom": "Classroom",
|
||||||
|
"Clear": "Clear",
|
||||||
"Clear all filters": "Clear all filters",
|
"Clear all filters": "Clear all filters",
|
||||||
"Clear searches": "Clear searches",
|
"Clear searches": "Clear searches",
|
||||||
"Click here to log in": "Click here to log in",
|
"Click here to log in": "Click here to log in",
|
||||||
|
|||||||
@@ -185,6 +185,7 @@
|
|||||||
"City pulse": "Kaupungin syke",
|
"City pulse": "Kaupungin syke",
|
||||||
"City/State": "Kaupunki/Osavaltio",
|
"City/State": "Kaupunki/Osavaltio",
|
||||||
"Classroom": "Classroom",
|
"Classroom": "Classroom",
|
||||||
|
"Clear": "Tyhjennä",
|
||||||
"Clear all filters": "Tyhjennä kaikki suodattimet",
|
"Clear all filters": "Tyhjennä kaikki suodattimet",
|
||||||
"Clear searches": "Tyhjennä haut",
|
"Clear searches": "Tyhjennä haut",
|
||||||
"Click here to log in": "Napsauta tästä kirjautuaksesi sisään",
|
"Click here to log in": "Napsauta tästä kirjautuaksesi sisään",
|
||||||
|
|||||||
@@ -185,6 +185,7 @@
|
|||||||
"City pulse": "Byens puls",
|
"City pulse": "Byens puls",
|
||||||
"City/State": "By/Stat",
|
"City/State": "By/Stat",
|
||||||
"Classroom": "Classroom",
|
"Classroom": "Classroom",
|
||||||
|
"Clear": "Rens",
|
||||||
"Clear all filters": "Fjern alle filtre",
|
"Clear all filters": "Fjern alle filtre",
|
||||||
"Clear searches": "Tømme søk",
|
"Clear searches": "Tømme søk",
|
||||||
"Click here to log in": "Klikk her for å logge inn",
|
"Click here to log in": "Klikk her for å logge inn",
|
||||||
|
|||||||
@@ -185,6 +185,7 @@
|
|||||||
"City pulse": "Stadspuls",
|
"City pulse": "Stadspuls",
|
||||||
"City/State": "Ort",
|
"City/State": "Ort",
|
||||||
"Classroom": "Klassrum",
|
"Classroom": "Klassrum",
|
||||||
|
"Clear": "Rensa",
|
||||||
"Clear all filters": "Rensa alla filter",
|
"Clear all filters": "Rensa alla filter",
|
||||||
"Clear searches": "Rensa tidigare sökningar",
|
"Clear searches": "Rensa tidigare sökningar",
|
||||||
"Click here to log in": "Klicka här för att logga in",
|
"Click here to log in": "Klicka här för att logga in",
|
||||||
|
|||||||
@@ -24,14 +24,16 @@ export default function RoomProvider({
|
|||||||
roomAvailability,
|
roomAvailability,
|
||||||
searchParams,
|
searchParams,
|
||||||
selectedFilter,
|
selectedFilter,
|
||||||
|
selectedPackages,
|
||||||
} = useRatesStore((state) => ({
|
} = useRatesStore((state) => ({
|
||||||
activeRoom: state.activeRoom,
|
activeRoom: state.activeRoom,
|
||||||
booking: state.booking,
|
booking: state.booking,
|
||||||
roomAvailability: state.roomsAvailability?.[idx],
|
roomAvailability: state.roomsAvailability?.[idx],
|
||||||
searchParams: state.searchParams,
|
searchParams: state.searchParams,
|
||||||
selectedFilter: state.rooms[idx].selectedFilter,
|
selectedFilter: state.rooms[idx].selectedFilter,
|
||||||
|
selectedPackages: state.rooms[idx].selectedPackages,
|
||||||
}))
|
}))
|
||||||
const { appendRegularRates, ...actions } = room.actions
|
const { appendRegularRates, addRoomFeatures, ...actions } = room.actions
|
||||||
const roomNr = idx + 1
|
const roomNr = idx + 1
|
||||||
|
|
||||||
const redemptionSearch = searchParams.has("searchType")
|
const redemptionSearch = searchParams.has("searchType")
|
||||||
@@ -91,6 +93,40 @@ export default function RoomProvider({
|
|||||||
}
|
}
|
||||||
}, [appendRegularRates, data, enabled, isFetched, isFetching])
|
}, [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={{
|
||||||
@@ -98,6 +134,9 @@ export default function RoomProvider({
|
|||||||
actions,
|
actions,
|
||||||
isActiveRoom: activeRoom === idx,
|
isActiveRoom: activeRoom === idx,
|
||||||
isFetchingAdditionalRate: isFetched ? false : isFetching,
|
isFetchingAdditionalRate: isFetched ? false : isFetching,
|
||||||
|
isFetchingRoomFeatures: isRoomFeaturesFetched
|
||||||
|
? false
|
||||||
|
: isRoomFeaturesFetching,
|
||||||
isMainRoom: roomNr === 1,
|
isMainRoom: roomNr === 1,
|
||||||
roomAvailability,
|
roomAvailability,
|
||||||
roomNr,
|
roomNr,
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
|
||||||
|
|
||||||
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 { Country } from "@/types/enums/country"
|
import { Country } from "@/types/enums/country"
|
||||||
@@ -48,6 +46,9 @@ export const roomsCombinedAvailabilityInputSchema = z.object({
|
|||||||
roomStayEndDate: z.string(),
|
roomStayEndDate: z.string(),
|
||||||
roomStayStartDate: z.string(),
|
roomStayStartDate: z.string(),
|
||||||
redemption: z.boolean().optional().default(false),
|
redemption: z.boolean().optional().default(false),
|
||||||
|
roomFeatureCodesArray: z
|
||||||
|
.array(z.array(z.nativeEnum(RoomPackageCodeEnum)).nullable())
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const selectedRoomAvailabilityInputSchema = z.object({
|
export const selectedRoomAvailabilityInputSchema = z.object({
|
||||||
@@ -167,16 +168,17 @@ export const roomFeaturesInputSchema = z.object({
|
|||||||
hotelId: z.string(),
|
hotelId: z.string(),
|
||||||
startDate: z.string(),
|
startDate: z.string(),
|
||||||
endDate: z.string(),
|
endDate: z.string(),
|
||||||
adultsCount: z.array(z.number()),
|
adults: z.number(),
|
||||||
childArray: z
|
childrenInRoom: z
|
||||||
.array(
|
.array(
|
||||||
nullableArrayObjectValidator(
|
z.object({
|
||||||
z.object({
|
age: z.number(),
|
||||||
age: z.number(),
|
bed: z.nativeEnum(ChildBedMapEnum),
|
||||||
bed: z.nativeEnum(ChildBedMapEnum),
|
})
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.nullable(),
|
.optional(),
|
||||||
roomFeatureCode: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
|
roomFeatureCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
|
||||||
|
roomIndex: z.number().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type RoomFeaturesInput = z.input<typeof roomFeaturesInputSchema>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
hotelsAvailabilityInputSchema,
|
hotelsAvailabilityInputSchema,
|
||||||
nearbyHotelIdsInput,
|
nearbyHotelIdsInput,
|
||||||
ratesInputSchema,
|
ratesInputSchema,
|
||||||
|
type RoomFeaturesInput,
|
||||||
roomFeaturesInputSchema,
|
roomFeaturesInputSchema,
|
||||||
roomPackagesInputSchema,
|
roomPackagesInputSchema,
|
||||||
roomsCombinedAvailabilityInputSchema,
|
roomsCombinedAvailabilityInputSchema,
|
||||||
@@ -471,6 +472,75 @@ export const getHotelsAvailabilityByHotelIds = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getRoomFeatures(
|
||||||
|
{
|
||||||
|
hotelId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
adults,
|
||||||
|
childrenInRoom,
|
||||||
|
roomFeatureCodes,
|
||||||
|
}: RoomFeaturesInput,
|
||||||
|
token: string
|
||||||
|
) {
|
||||||
|
const params = {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate: startDate,
|
||||||
|
roomStayEndDate: endDate,
|
||||||
|
adults,
|
||||||
|
...(childrenInRoom?.length && {
|
||||||
|
children: generateChildrenString(childrenInRoom),
|
||||||
|
}),
|
||||||
|
roomFeatureCode: roomFeatureCodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
export const hotelQueryRouter = router({
|
export const hotelQueryRouter = router({
|
||||||
availability: router({
|
availability: router({
|
||||||
hotelsByCity: safeProtectedServiceProcedure
|
hotelsByCity: safeProtectedServiceProcedure
|
||||||
@@ -576,6 +646,7 @@ export const hotelQueryRouter = router({
|
|||||||
redemption,
|
redemption,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
|
roomFeatureCodesArray,
|
||||||
},
|
},
|
||||||
}) => {
|
}) => {
|
||||||
const apiLang = toApiLang(lang)
|
const apiLang = toApiLang(lang)
|
||||||
@@ -643,6 +714,35 @@ export const hotelQueryRouter = router({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roomFeatureCodes = roomFeatureCodesArray?.[idx]
|
||||||
|
if (roomFeatureCodes?.length) {
|
||||||
|
const roomFeaturesResponse = await getRoomFeatures(
|
||||||
|
{
|
||||||
|
hotelId,
|
||||||
|
startDate: roomStayStartDate,
|
||||||
|
endDate: roomStayEndDate,
|
||||||
|
adults: adultCount,
|
||||||
|
childrenInRoom: kids ?? undefined,
|
||||||
|
roomFeatureCodes,
|
||||||
|
},
|
||||||
|
ctx.serviceToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (roomFeaturesResponse) {
|
||||||
|
validateAvailabilityData.data.roomConfigurations.forEach(
|
||||||
|
(room) => {
|
||||||
|
const features = roomFeaturesResponse.find(
|
||||||
|
(feat) => feat.roomTypeCode === room.roomTypeCode
|
||||||
|
)?.features
|
||||||
|
|
||||||
|
if (features) {
|
||||||
|
room.features = features
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (rateCode) {
|
if (rateCode) {
|
||||||
validateAvailabilityData.data.mustBeGuaranteed =
|
validateAvailabilityData.data.mustBeGuaranteed =
|
||||||
validateAvailabilityData.data.rateDefinitions.find(
|
validateAvailabilityData.data.rateDefinitions.find(
|
||||||
@@ -982,73 +1082,7 @@ export const hotelQueryRouter = router({
|
|||||||
roomFeatures: serviceProcedure
|
roomFeatures: serviceProcedure
|
||||||
.input(roomFeaturesInputSchema)
|
.input(roomFeaturesInputSchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { hotelId, startDate, endDate, adultsCount, childArray } = input
|
return await getRoomFeatures(input, ctx.serviceToken)
|
||||||
|
|
||||||
const responses = await Promise.allSettled(
|
|
||||||
adultsCount.map(async (adultCount, index) => {
|
|
||||||
const kids = childArray?.[index]
|
|
||||||
const params = {
|
|
||||||
hotelId,
|
|
||||||
roomStayStartDate: startDate,
|
|
||||||
roomStayEndDate: endDate,
|
|
||||||
adults: adultCount,
|
|
||||||
...(kids?.length && { children: generateChildrenString(kids) }),
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics.roomFeatures.counter.add(1, params)
|
|
||||||
|
|
||||||
const apiResponse = await api.get(
|
|
||||||
api.endpoints.v1.Availability.roomFeatures(hotelId),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return responses.map((features) => {
|
|
||||||
if (features.status === "fulfilled") {
|
|
||||||
return features.value
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
rates: router({
|
rates: router({
|
||||||
|
|||||||
@@ -106,3 +106,16 @@ export function isRoomPackageCode(
|
|||||||
code as RoomPackageCodeEnum
|
code as RoomPackageCodeEnum
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterRoomsBySelectedPackages(
|
||||||
|
selectedPackages: RoomPackageCodeEnum[],
|
||||||
|
rooms: RoomConfiguration[]
|
||||||
|
) {
|
||||||
|
if (!selectedPackages.length) {
|
||||||
|
return rooms
|
||||||
|
}
|
||||||
|
|
||||||
|
return rooms.filter((r) =>
|
||||||
|
selectedPackages.every((pkg) => r.features.find((f) => f.code === pkg))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { create, useStore } from "zustand"
|
|||||||
import { RatesContext } from "@/contexts/Rates"
|
import { RatesContext } from "@/contexts/Rates"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
filterRoomsBySelectedPackages,
|
||||||
findProductInRoom,
|
findProductInRoom,
|
||||||
findSelectedRate,
|
findSelectedRate,
|
||||||
isRoomPackageCode,
|
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
@@ -72,11 +72,18 @@ 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,
|
||||||
roomConfiguration
|
rooms
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!selectedRoom) {
|
if (!selectedRoom) {
|
||||||
@@ -97,6 +104,8 @@ export function createRatesStore({
|
|||||||
roomType: selectedRoom.roomType,
|
roomType: selectedRoom.roomType,
|
||||||
roomTypeCode: selectedRoom.roomTypeCode,
|
roomTypeCode: selectedRoom.roomTypeCode,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
rateSummary[idx] = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,9 +114,10 @@ export function createRatesStore({
|
|||||||
if (searchParams.has("modifyRateIndex")) {
|
if (searchParams.has("modifyRateIndex")) {
|
||||||
activeRoom = Number(searchParams.get("modifyRateIndex"))
|
activeRoom = Number(searchParams.get("modifyRateIndex"))
|
||||||
} else if (rateSummary.length === booking.rooms.length) {
|
} else if (rateSummary.length === booking.rooms.length) {
|
||||||
// Since all rooms has selections, all sections should be
|
// Finds the first unselected room and sets that to active
|
||||||
// closed on load
|
// if no unselected rooms it will return -1 and close all rooms
|
||||||
activeRoom = -1
|
const unselectedRoomIndex = rateSummary.findIndex((rate) => !rate)
|
||||||
|
activeRoom = unselectedRoomIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
return create<RatesState>()((set) => {
|
return create<RatesState>()((set) => {
|
||||||
@@ -126,6 +136,13 @@ export function createRatesStore({
|
|||||||
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 ?? []
|
||||||
|
|
||||||
|
let rooms: RoomConfiguration[] = filterRoomsBySelectedPackages(
|
||||||
|
selectedPackages,
|
||||||
|
roomConfiguration
|
||||||
|
)
|
||||||
|
|
||||||
const selectedRate =
|
const selectedRate =
|
||||||
findSelectedRate(
|
findSelectedRate(
|
||||||
room.rateCode,
|
room.rateCode,
|
||||||
@@ -143,21 +160,6 @@ export function createRatesStore({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since features are fetched async based on query string, we need to read from query string to apply correct filtering
|
|
||||||
const packagesParam = searchParams.get(`room[${idx}].packages`)
|
|
||||||
const selectedPackages = packagesParam
|
|
||||||
? packagesParam.split(",").filter(isRoomPackageCode)
|
|
||||||
: []
|
|
||||||
|
|
||||||
let rooms: RoomConfiguration[] = roomConfiguration
|
|
||||||
if (selectedPackages.length) {
|
|
||||||
rooms = roomConfiguration.filter((r) =>
|
|
||||||
selectedPackages.some((pkg) =>
|
|
||||||
r.features.find((f) => f.code === pkg)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actions: {
|
actions: {
|
||||||
appendRegularRates(roomConfigurations) {
|
appendRegularRates(roomConfigurations) {
|
||||||
@@ -204,6 +206,48 @@ export function createRatesStore({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
closeSection() {
|
closeSection() {
|
||||||
return set(
|
return set(
|
||||||
produce((state: RatesState) => {
|
produce((state: RatesState) => {
|
||||||
@@ -229,44 +273,53 @@ export function createRatesStore({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
togglePackage(code) {
|
togglePackages(selectedPackages) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: RatesState) => {
|
produce((state: RatesState) => {
|
||||||
const isSelected =
|
|
||||||
state.rooms[idx].selectedPackages.includes(code)
|
|
||||||
const selectedPackages = isSelected
|
|
||||||
? state.rooms[idx].selectedPackages.filter(
|
|
||||||
(pkg) => pkg !== code
|
|
||||||
)
|
|
||||||
: [...state.rooms[idx].selectedPackages, code]
|
|
||||||
state.rooms[idx].selectedPackages = selectedPackages
|
state.rooms[idx].selectedPackages = selectedPackages
|
||||||
|
const rateSummaryItem = state.rateSummary[idx]
|
||||||
|
|
||||||
const roomConfiguration = state.roomConfigurations[idx]
|
const roomConfiguration = state.roomConfigurations[idx]
|
||||||
if (roomConfiguration) {
|
if (roomConfiguration) {
|
||||||
const searchParams = new URLSearchParams(state.searchParams)
|
const searchParams = new URLSearchParams(state.searchParams)
|
||||||
if (selectedPackages.length) {
|
if (selectedPackages.length) {
|
||||||
state.rooms[idx].rooms = roomConfiguration.filter(
|
|
||||||
(room) =>
|
|
||||||
selectedPackages.every((pkg) =>
|
|
||||||
room.features.find((feat) => feat.code === pkg)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
searchParams.set(
|
searchParams.set(
|
||||||
`room[${idx}].packages`,
|
`room[${idx}].packages`,
|
||||||
selectedPackages.join(",")
|
selectedPackages.join(",")
|
||||||
)
|
)
|
||||||
|
|
||||||
if (state.rateSummary[idx]) {
|
if (rateSummaryItem) {
|
||||||
state.rateSummary[idx].packages = selectedPackages
|
rateSummaryItem.packages = selectedPackages
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.rooms[idx].rooms = roomConfiguration
|
state.rooms[idx].rooms = roomConfiguration
|
||||||
if (state.rateSummary[idx]) {
|
if (rateSummaryItem) {
|
||||||
state.rateSummary[idx].packages = []
|
rateSummaryItem.packages = []
|
||||||
}
|
}
|
||||||
searchParams.delete(`room[${idx}].packages`)
|
searchParams.delete(`room[${idx}].packages`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we already have the features data 'addRoomFeatures' wont run
|
||||||
|
// so we need to do additional filtering here if thats the case
|
||||||
|
const filteredRooms = filterRoomsBySelectedPackages(
|
||||||
|
selectedPackages,
|
||||||
|
state.roomConfigurations[idx]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filteredRooms.length) {
|
||||||
|
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(
|
state.searchParams = new ReadonlyURLSearchParams(
|
||||||
searchParams
|
searchParams
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,20 +18,21 @@ export interface SummaryProps {
|
|||||||
isMember: boolean
|
isMember: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SummaryUIProps {
|
export interface EnterDetailsSummaryProps {
|
||||||
booking: SelectRateSearchParams
|
booking: SelectRateSearchParams
|
||||||
isMember: boolean
|
isMember: boolean
|
||||||
totalPrice: Price
|
totalPrice: Price
|
||||||
toggleSummaryOpen: () => void
|
|
||||||
vat: number
|
vat: number
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnterDetailsSummaryProps extends SummaryUIProps {
|
|
||||||
rooms: RoomState[]
|
rooms: RoomState[]
|
||||||
|
toggleSummaryOpen: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectRateSummaryProps extends SummaryUIProps {
|
export interface SelectRateSummaryProps {
|
||||||
rooms: {
|
booking: SelectRateSearchParams
|
||||||
|
isMember: boolean
|
||||||
|
totalPrice: Price
|
||||||
|
vat: number
|
||||||
|
rooms: Array<{
|
||||||
adults: number
|
adults: number
|
||||||
childrenInRoom: Child[] | undefined
|
childrenInRoom: Child[] | undefined
|
||||||
roomType: string
|
roomType: string
|
||||||
@@ -39,5 +40,7 @@ export interface SelectRateSummaryProps extends SummaryUIProps {
|
|||||||
roomRate: RoomRate
|
roomRate: RoomRate
|
||||||
rateDetails: string[] | undefined
|
rateDetails: string[] | undefined
|
||||||
cancellationText: string
|
cancellationText: string
|
||||||
}[]
|
packages?: Packages
|
||||||
|
} | null>
|
||||||
|
toggleSummaryOpen: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
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<SelectedRoom["actions"], "appendRegularRates">
|
actions: Omit<
|
||||||
|
SelectedRoom["actions"],
|
||||||
|
"appendRegularRates" | "addRoomFeatures"
|
||||||
|
>
|
||||||
isActiveRoom: boolean
|
isActiveRoom: boolean
|
||||||
isFetchingAdditionalRate: boolean
|
isFetchingAdditionalRate: boolean
|
||||||
|
isFetchingRoomFeatures: boolean
|
||||||
isMainRoom: boolean
|
isMainRoom: boolean
|
||||||
roomAvailability:
|
roomAvailability:
|
||||||
| NonNullable<RatesState["roomsAvailability"]>[number]
|
| NonNullable<RatesState["roomsAvailability"]>[number]
|
||||||
|
|||||||
@@ -25,10 +25,16 @@ export interface AvailabilityError {
|
|||||||
|
|
||||||
interface Actions {
|
interface Actions {
|
||||||
appendRegularRates: (roomConfigurations: RoomConfiguration[]) => void
|
appendRegularRates: (roomConfigurations: RoomConfiguration[]) => void
|
||||||
|
addRoomFeatures: (
|
||||||
|
roomFeatures: {
|
||||||
|
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
||||||
|
features: RoomConfiguration["features"]
|
||||||
|
}[]
|
||||||
|
) => void
|
||||||
closeSection: () => void
|
closeSection: () => void
|
||||||
modifyRate: () => void
|
modifyRate: () => void
|
||||||
selectFilter: (filter: BookingCodeFilterEnum) => void
|
selectFilter: (filter: BookingCodeFilterEnum) => void
|
||||||
togglePackage: (code: RoomPackageCodeEnum) => void
|
togglePackages: (codes: RoomPackageCodeEnum[]) => void
|
||||||
selectRate: (rate: SelectedRate) => void
|
selectRate: (rate: SelectedRate) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +63,7 @@ export interface RatesState {
|
|||||||
packages: NonNullable<Packages>
|
packages: NonNullable<Packages>
|
||||||
pathname: string
|
pathname: string
|
||||||
petRoomPackage: NonNullable<Packages>[number] | undefined
|
petRoomPackage: NonNullable<Packages>[number] | undefined
|
||||||
rateSummary: Rate[]
|
rateSummary: Array<Rate | null>
|
||||||
rooms: SelectedRoom[]
|
rooms: SelectedRoom[]
|
||||||
roomCategories: Room[]
|
roomCategories: Room[]
|
||||||
roomConfigurations: RoomConfiguration[][]
|
roomConfigurations: RoomConfiguration[][]
|
||||||
|
|||||||
Reference in New Issue
Block a user