Merged in feat/SW-966 (pull request #1361)

feat: show both prices for multiroom room 2-4 on all users

Approved-by: Michael Zetterberg
This commit is contained in:
Simon.Emanuelsson
2025-02-25 09:44:21 +00:00
12 changed files with 92 additions and 49 deletions

View File

@@ -102,7 +102,7 @@ export default function Summary({
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED) const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
const memberPrice = getMemberPrice(room.roomRate) const memberPrice = getMemberPrice(room.roomRate)
const showMemberPrice = !!(isMember && memberPrice) const showMemberPrice = !!(isMember && memberPrice && roomNumber === 1)
const adultsMsg = intl.formatMessage( const adultsMsg = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" }, { id: "{totalAdults, plural, one {# adult} other {# adults}}" },

View File

@@ -38,6 +38,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
roomsAvailability: state.roomsAvailability, roomsAvailability: state.roomsAvailability,
searchParams: state.searchParams, searchParams: state.searchParams,
})) }))
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const params = new URLSearchParams(searchParams) const params = new URLSearchParams(searchParams)
@@ -130,24 +131,44 @@ 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}> const isMainRoom = index + 1 === 1
<Subtitle color="uiTextHighContrast"> return (
{intl.formatMessage( <div key={index} className={styles.roomSummary}>
{ id: "Room {roomIndex}" }, {rateSummary.length > 1 ? (
{ roomIndex: index + 1 } <>
<Subtitle color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Room {roomIndex}" },
{ roomIndex: index + 1 }
)}
</Subtitle>
<Body color="uiTextMediumContrast">{room.roomType}</Body>
<Caption color="uiTextMediumContrast">
{getRateDetails(
isUserLoggedIn && room.member && isMainRoom
? room.member?.rateCode
: room.public.rateCode
)}
</Caption>
</>
) : (
<>
<Subtitle color="uiTextHighContrast">
{room.roomType}
</Subtitle>
<Body color="uiTextMediumContrast">
{getRateDetails(
isUserLoggedIn && room.member && isMainRoom
? room.member?.rateCode
: room.public.rateCode
)}
</Body>
</>
)} )}
</Subtitle> </div>
<Body color="uiTextMediumContrast">{room.roomType}</Body> )
<Caption color="uiTextMediumContrast"> })}
{getRateDetails(
isUserLoggedIn && room.member
? room.member?.rateCode
: room.public.rateCode
)}
</Caption>
</div>
))}
{/* Render unselected rooms */} {/* Render unselected rooms */}
{Array.from({ {Array.from({
length: totalRoomsRequired - rateSummary.length, length: totalRoomsRequired - rateSummary.length,

View File

@@ -11,9 +11,11 @@ export const calculateTotalPrice = (
petRoomPackage: RoomPackage | undefined petRoomPackage: RoomPackage | undefined
) => { ) => {
return selectedRateSummary.reduce<Price>( return selectedRateSummary.reduce<Price>(
(total, room) => { (total, room, idx) => {
const priceToUse = const priceToUse =
isUserLoggedIn && room.member ? room.member : room.public isUserLoggedIn && room.member && idx + 1 === 1
? room.member
: room.public
const isPetRoom = room.features.find( const isPetRoom = room.features.find(
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
) )

View File

@@ -26,6 +26,7 @@ export default function SelectedRoomPanel() {
})) }))
const { const {
actions: { modifyRate }, actions: { modifyRate },
isMainRoom,
roomNr, roomNr,
selectedRate, selectedRate,
} = useRoomContext() } = useRoomContext()
@@ -64,10 +65,10 @@ export default function SelectedRoomPanel() {
} }
} }
const rateCode = const rate =
isUserLoggedIn && selectedRate?.product.productType.member isUserLoggedIn && isMainRoom && selectedRate?.product.productType.member
? selectedRate?.product.productType.member?.rateCode ? selectedRate?.product.productType.member
: selectedRate?.product.productType.public.rateCode : selectedRate?.product.productType.public
return ( return (
<div className={styles.selectedRoomPanel}> <div className={styles.selectedRoomPanel}>
@@ -82,11 +83,10 @@ export default function SelectedRoomPanel() {
{selectedRate?.roomType} {selectedRate?.roomType}
</Subtitle> </Subtitle>
<Body color="uiTextMediumContrast"> <Body color="uiTextMediumContrast">
{rateCode ? getRateDetails(rateCode) : null} {rate?.rateCode ? getRateDetails(rate.rateCode) : null}
</Body> </Body>
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
{selectedRate?.product.productType.public.localPrice.pricePerNight}{" "} {rate?.localPrice.pricePerNight} {rate?.localPrice.currency}/
{selectedRate?.product.productType.public.localPrice.currency}/
{intl.formatMessage({ id: "night" })} {intl.formatMessage({ id: "night" })}
</Body> </Body>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
@@ -6,6 +7,8 @@ import { dt } from "@/lib/dt"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room"
import { isValidClientSession } from "@/utils/clientSession"
import { calculatePricesPerNight } from "./utils" import { calculatePricesPerNight } from "./utils"
@@ -14,12 +17,14 @@ import styles from "./priceList.module.css"
import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
export default function PriceList({ export default function PriceList({
isUserLoggedIn,
publicPrice = {}, publicPrice = {},
memberPrice = {}, memberPrice = {},
petRoomPackage, petRoomPackage,
}: PriceListProps) { }: PriceListProps) {
const intl = useIntl() const intl = useIntl()
const { isMainRoom } = useRoomContext()
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } = const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } =
publicPrice publicPrice
@@ -60,7 +65,7 @@ export default function PriceList({
return ( return (
<dl className={styles.priceList}> <dl className={styles.priceList}>
{isUserLoggedIn ? null : ( {isUserLoggedIn && isMainRoom ? null : (
<div className={styles.priceRow}> <div className={styles.priceRow}>
<dt> <dt>
<Caption <Caption

View File

@@ -1,4 +1,5 @@
"use client" "use client"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { CheckIcon, InfoCircleIcon } from "@/components/Icons" import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
@@ -8,6 +9,7 @@ import Label from "@/components/TempDesignSystem/Form/Label"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/Room"
import { isValidClientSession } from "@/utils/clientSession"
import PriceTable from "./PriceList" import PriceTable from "./PriceList"
@@ -18,7 +20,6 @@ import type { FlexibilityOptionProps } from "@/types/components/hotelReservation
export default function FlexibilityOption({ export default function FlexibilityOption({
features, features,
isSelected, isSelected,
isUserLoggedIn,
paymentTerm, paymentTerm,
priceInformation, priceInformation,
petRoomPackage, petRoomPackage,
@@ -28,8 +29,11 @@ export default function FlexibilityOption({
title, title,
}: FlexibilityOptionProps) { }: FlexibilityOptionProps) {
const intl = useIntl() const intl = useIntl()
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const { const {
actions: { selectRate }, actions: { selectRate },
isMainRoom,
} = useRoomContext() } = useRoomContext()
function handleSelect() { function handleSelect() {
@@ -63,15 +67,17 @@ export default function FlexibilityOption({
} }
const { public: publicPrice, member: memberPrice } = product.productType const { public: publicPrice, member: memberPrice } = product.productType
const rate =
isUserLoggedIn && isMainRoom && memberPrice ? memberPrice : publicPrice
return ( return (
<label> <label>
<input <input
checked={isSelected} checked={isSelected}
name={`rateCode-${product.productType.public.rateCode}`} name={`rateCode-${rate.rateCode}`}
onChange={handleSelect} onChange={handleSelect}
type="radio" type="radio"
value={publicPrice?.rateCode} value={rate.rateCode}
/> />
<div className={styles.card}> <div className={styles.card}>
<div className={styles.header}> <div className={styles.header}>
@@ -112,10 +118,9 @@ export default function FlexibilityOption({
</div> </div>
</div> </div>
<PriceTable <PriceTable
isUserLoggedIn={isUserLoggedIn}
publicPrice={publicPrice}
memberPrice={memberPrice} memberPrice={memberPrice}
petRoomPackage={petRoomPackage} petRoomPackage={petRoomPackage}
publicPrice={publicPrice}
/> />
<div className={styles.checkIcon}> <div className={styles.checkIcon}>

View File

@@ -81,7 +81,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
rateDefinitions: state.roomsAvailability?.rateDefinitions, rateDefinitions: state.roomsAvailability?.rateDefinitions,
roomCategories: state.roomCategories, roomCategories: state.roomCategories,
})) }))
const { selectedPackage, selectedRate } = useRoomContext() const { isMainRoom, selectedPackage, selectedRate } = useRoomContext()
const classNames = cardVariants({ const classNames = cardVariants({
availability: availability:
@@ -197,7 +197,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
throw new Error("We should never make it where without rateCodes") throw new Error("We should never make it where without rateCodes")
} }
const key = isUserLoggedIn ? memberRate : publicRate const key = isUserLoggedIn && isMainRoom ? memberRate : publicRate
return getRate(key) return getRate(key)
} }
@@ -299,7 +299,6 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
selectedRate?.roomTypeCode === selectedRate?.roomTypeCode ===
roomConfiguration.roomTypeCode roomConfiguration.roomTypeCode
} }
isUserLoggedIn={isUserLoggedIn}
paymentTerm={rate.isFlex ? payLater : payNow} paymentTerm={rate.isFlex ? payLater : payNow}
petRoomPackage={petRoomPackageSelected} petRoomPackage={petRoomPackageSelected}
product={rate.notAvailable ? undefined : product} product={rate.notAvailable ? undefined : product}

5
package-lock.json generated
View File

@@ -11896,7 +11896,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0", "domelementtype": "^2.3.0",
@@ -11911,7 +11910,6 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -11946,7 +11944,6 @@
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0" "domelementtype": "^2.3.0"
@@ -11963,7 +11960,6 @@
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dev":true,
"dependencies": { "dependencies": {
"dom-serializer": "^2.0.0", "dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0", "domelementtype": "^2.3.0",
@@ -12189,7 +12185,6 @@
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"

View File

@@ -15,6 +15,7 @@ export default function RoomProvider({
const modifyRate = useRatesStore((state) => state.actions.modifyRate(idx)) const modifyRate = useRatesStore((state) => state.actions.modifyRate(idx))
const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx)) const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx))
const selectRate = useRatesStore((state) => state.actions.selectRate(idx)) const selectRate = useRatesStore((state) => state.actions.selectRate(idx))
const roomNr = idx + 1
return ( return (
<RoomContext.Provider <RoomContext.Provider
value={{ value={{
@@ -25,7 +26,8 @@ export default function RoomProvider({
selectRate, selectRate,
}, },
isActiveRoom: activeRoom === idx, isActiveRoom: activeRoom === idx,
roomNr: idx + 1, isMainRoom: roomNr === 1,
roomNr,
}} }}
> >
{children} {children}

View File

@@ -109,6 +109,15 @@ export function createRatesStore({
} }
}) })
let activeRoom = rateSummary.length
if (searchParams.has("modifyRateIndex")) {
activeRoom = Number(searchParams.get("modifyRateIndex"))
} else if (rateSummary.length === booking.rooms.length) {
// Since all rooms has selections, all sections should be
// closed on load
activeRoom = -1
}
return create<RatesState>()((set) => ({ return create<RatesState>()((set) => ({
actions: { actions: {
modifyRate(idx) { modifyRate(idx) {
@@ -168,17 +177,23 @@ export function createRatesStore({
roomTypeCode: selectedRate.roomTypeCode, roomTypeCode: selectedRate.roomTypeCode,
} }
const roomNr = idx + 1
const isMemberRate =
isUserLoggedIn &&
roomNr === 1 &&
selectedRate.product.productType.member
const searchParams = new URLSearchParams(state.searchParams) const searchParams = new URLSearchParams(state.searchParams)
searchParams.set( searchParams.set(
`room[${idx}].counterratecode`, `room[${idx}].counterratecode`,
isUserLoggedIn && selectedRate.product.productType.member isMemberRate
? selectedRate.product.productType.public.rateCode ? selectedRate.product.productType.public.rateCode
: selectedRate.product.productType.member?.rateCode ?? "" : selectedRate.product.productType.member?.rateCode ?? ""
) )
searchParams.set( searchParams.set(
`room[${idx}].ratecode`, `room[${idx}].ratecode`,
isUserLoggedIn && selectedRate.product.productType.member isMemberRate
? selectedRate.product.productType.member.rateCode ? // already checked in isMemberRate
selectedRate.product.productType.member!.rateCode
: selectedRate.product.productType.public.rateCode : selectedRate.product.productType.public.rateCode
) )
searchParams.set( searchParams.set(
@@ -202,7 +217,7 @@ export function createRatesStore({
} }
}, },
}, },
activeRoom: rateSummary.length, activeRoom,
allRooms, allRooms,
booking, booking,
filterOptions, filterOptions,

View File

@@ -16,7 +16,6 @@ export type RoomPriceSchema = z.output<typeof priceSchema>
export type FlexibilityOptionProps = { export type FlexibilityOptionProps = {
features: RoomConfiguration["features"] features: RoomConfiguration["features"]
isSelected: boolean isSelected: boolean
isUserLoggedIn: boolean
paymentTerm: string paymentTerm: string
petRoomPackage: RoomPackage | undefined petRoomPackage: RoomPackage | undefined
priceInformation?: Array<string> priceInformation?: Array<string>
@@ -27,7 +26,6 @@ export type FlexibilityOptionProps = {
} }
export interface PriceListProps { export interface PriceListProps {
isUserLoggedIn: boolean
publicPrice?: ProductPrice | Record<string, never> publicPrice?: ProductPrice | Record<string, never>
memberPrice?: ProductPrice | Record<string, never> memberPrice?: ProductPrice | Record<string, never>
petRoomPackage?: RoomPackage petRoomPackage?: RoomPackage

View File

@@ -8,5 +8,6 @@ export interface RoomContextValue extends SelectedRoom {
selectRate: (rate: SelectedRate) => void selectRate: (rate: SelectedRate) => void
} }
isActiveRoom: boolean isActiveRoom: boolean
isMainRoom: boolean
roomNr: number roomNr: number
} }