Merged in feat/enter-details-multiroom (pull request #1280)
feat(SW-1259): Enter details multiroom * refactor: remove per-step URLs * WIP: map multiroom data * fix: lint errors in details page * fix: made useEnterDetailsStore tests pass * fix: WIP refactor enter details store * fix: WIP enter details store update * fix: added room index to select correct room * fix: added logic for navigating between steps and rooms * fix: update summary to work with store changes * fix: added room and total price calculation * fix: removed unused code and added test for breakfast included * refactor: move store selectors into helpers * refactor: session storage state for multiroom booking * feat: update enter details accordion navigation * fix: added room index to each form component so they select correct room * fix: added unique id to input to handle case when multiple inputs have same name * fix: update payment step with store changes * fix: rebase issues * fix: now you should only be able to go to a step if previous room is completed * refactor: cleanup * fix: if no availability just skip that room for now * fix: add select-rate Summary and adjust typings Approved-by: Arvid Norlin
This commit is contained in:
committed by
Arvid Norlin
parent
f43ee4a0e6
commit
b394d54c3f
@@ -0,0 +1,44 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3) var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.content {
|
||||
width: var(--max-width-page);
|
||||
margin: var(--Spacing-x3) auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.summary {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.container {
|
||||
grid-template-columns: 1fr 340px;
|
||||
grid-template-rows: auto 1fr;
|
||||
width: var(--max-width-page);
|
||||
margin: var(--Spacing-x5) auto 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
margin: var(--Spacing-x3) 0 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
position: static;
|
||||
display: grid;
|
||||
grid-column: 2/3;
|
||||
grid-row: 1/-1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import {
|
||||
getBreakfastPackages,
|
||||
getHotel,
|
||||
getPackages,
|
||||
getProfileSafely,
|
||||
getSelectedRoomAvailability,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||
import Details from "@/components/HotelReservation/EnterDetails/Details"
|
||||
import HotelHeader from "@/components/HotelReservation/EnterDetails/Header"
|
||||
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
|
||||
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||
import { convertSearchParamsToObj } from "@/utils/url"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import { type SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
import type { Packages } from "@/types/requests/packages"
|
||||
|
||||
export interface RoomData {
|
||||
bedTypes?: BedTypeSelection[]
|
||||
mustBeGuaranteed?: boolean
|
||||
breakfastIncluded?: boolean
|
||||
packages: Packages | null
|
||||
cancellationText: string
|
||||
rateDetails: string[]
|
||||
roomType: string
|
||||
roomRate: RoomRate
|
||||
}
|
||||
|
||||
export default async function DetailsPage({
|
||||
params: { lang },
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, SelectRateSearchParams>) {
|
||||
setLang(lang)
|
||||
|
||||
const intl = await getIntl()
|
||||
const selectRoomParams = new URLSearchParams(searchParams)
|
||||
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
||||
|
||||
void getProfileSafely()
|
||||
|
||||
const breakfastInput = {
|
||||
adults: 1,
|
||||
fromDate: booking.fromDate,
|
||||
hotelId: booking.hotelId,
|
||||
toDate: booking.toDate,
|
||||
}
|
||||
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
||||
const roomsData: RoomData[] = []
|
||||
|
||||
for (let room of booking.rooms) {
|
||||
const childrenAsString =
|
||||
room.childrenInRoom && generateChildrenString(room.childrenInRoom)
|
||||
|
||||
const selectedRoomAvailabilityInput = {
|
||||
adults: room.adults,
|
||||
children: childrenAsString,
|
||||
hotelId: booking.hotelId,
|
||||
packageCodes: room.packages,
|
||||
rateCode: room.rateCode,
|
||||
roomStayStartDate: booking.fromDate,
|
||||
roomStayEndDate: booking.toDate,
|
||||
roomTypeCode: room.roomTypeCode,
|
||||
}
|
||||
|
||||
const packages = room.packages
|
||||
? await getPackages({
|
||||
adults: room.adults,
|
||||
children: room.childrenInRoom?.length,
|
||||
endDate: booking.toDate,
|
||||
hotelId: booking.hotelId,
|
||||
packageCodes: room.packages,
|
||||
startDate: booking.fromDate,
|
||||
})
|
||||
: null
|
||||
|
||||
const roomAvailability = await getSelectedRoomAvailability(
|
||||
selectedRoomAvailabilityInput //
|
||||
)
|
||||
|
||||
if (!roomAvailability) {
|
||||
continue // TODO: handle no room availability
|
||||
}
|
||||
|
||||
roomsData.push({
|
||||
bedTypes: roomAvailability.bedTypes,
|
||||
packages,
|
||||
mustBeGuaranteed: roomAvailability.mustBeGuaranteed,
|
||||
breakfastIncluded: roomAvailability.breakfastIncluded,
|
||||
cancellationText: roomAvailability.cancellationText,
|
||||
rateDetails: roomAvailability.rateDetails ?? [],
|
||||
roomType: roomAvailability.selectedRoom.roomType,
|
||||
roomRate: {
|
||||
memberRate: roomAvailability?.memberRate,
|
||||
publicRate: roomAvailability.publicRate,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const isCardOnlyPayment = roomsData.some((room) => room?.mustBeGuaranteed)
|
||||
const hotelData = await getHotel({
|
||||
hotelId: booking.hotelId,
|
||||
isCardOnlyPayment,
|
||||
language: lang,
|
||||
})
|
||||
const user = await getProfileSafely()
|
||||
// const userTrackingData = await getUserTracking()
|
||||
|
||||
if (!hotelData || !roomsData) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
// const arrivalDate = new Date(booking.fromDate)
|
||||
// const departureDate = new Date(booking.toDate)
|
||||
const hotelAttributes = hotelData.hotel
|
||||
|
||||
// TODO: add tracking
|
||||
// const initialHotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
// searchTerm: searchParams.city,
|
||||
// arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
// departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
// noOfAdults: adults,
|
||||
// noOfChildren: childrenInRoom?.length,
|
||||
// ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
|
||||
// childBedPreference: childrenInRoom
|
||||
// ?.map((c) => ChildBedMapEnum[c.bed])
|
||||
// .join("|"),
|
||||
// noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
// duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
// leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
// searchType: "hotel",
|
||||
// bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
// country: hotelAttributes?.address.country,
|
||||
// hotelID: hotelAttributes?.operaId,
|
||||
// region: hotelAttributes?.address.city,
|
||||
// }
|
||||
|
||||
const showBreakfastStep = Boolean(
|
||||
breakfastPackages?.length && !roomsData[0].breakfastIncluded
|
||||
)
|
||||
|
||||
return (
|
||||
<EnterDetailsProvider
|
||||
booking={booking}
|
||||
showBreakfastStep={showBreakfastStep}
|
||||
roomsData={roomsData}
|
||||
searchParamsStr={selectRoomParams.toString()}
|
||||
user={user}
|
||||
vat={hotelAttributes.vat}
|
||||
>
|
||||
<main>
|
||||
<HotelHeader hotelData={hotelData} />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
{roomsData.map((room, idx) => (
|
||||
<section key={idx}>
|
||||
{roomsData.length > 1 && (
|
||||
<header className={styles.header}>
|
||||
<Title level="h2" as="h4">
|
||||
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||
</Title>
|
||||
</header>
|
||||
)}
|
||||
<SelectedRoom
|
||||
hotelId={booking.hotelId}
|
||||
roomType={room.roomType}
|
||||
roomTypeCode={booking.rooms[idx].roomTypeCode}
|
||||
rateDescription={room.cancellationText}
|
||||
/>
|
||||
|
||||
{room.bedTypes ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Select bed" })}
|
||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||
step={StepEnum.selectBed}
|
||||
roomIndex={idx}
|
||||
>
|
||||
<BedType bedTypes={room.bedTypes} roomIndex={idx} />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
{showBreakfastStep ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Food options" })}
|
||||
label={intl.formatMessage({
|
||||
id: "Select breakfast options",
|
||||
})}
|
||||
step={StepEnum.breakfast}
|
||||
roomIndex={idx}
|
||||
>
|
||||
<Breakfast packages={breakfastPackages!} roomIndex={idx} />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Details" })}
|
||||
step={StepEnum.details}
|
||||
label={intl.formatMessage({ id: "Enter your details" })}
|
||||
roomIndex={idx}
|
||||
>
|
||||
<Details
|
||||
user={idx === 0 ? user : null}
|
||||
memberPrice={{
|
||||
currency:
|
||||
room?.roomRate.memberRate?.localPrice.currency ?? "", // TODO: how to handle undefined,
|
||||
price:
|
||||
room?.roomRate.memberRate?.localPrice.pricePerNight ??
|
||||
0, // TODO: how to handle undefined,
|
||||
}}
|
||||
roomIndex={idx}
|
||||
/>
|
||||
</SectionAccordion>
|
||||
</section>
|
||||
))}
|
||||
<Suspense>
|
||||
<Payment
|
||||
user={user}
|
||||
otherPaymentOptions={
|
||||
hotelAttributes.merchantInformationData
|
||||
.alternatePaymentOptions
|
||||
}
|
||||
supportedCards={hotelAttributes.merchantInformationData.cards}
|
||||
mustBeGuaranteed={isCardOnlyPayment}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<aside className={styles.summary}>
|
||||
<MobileSummary
|
||||
isMember={!!user}
|
||||
breakfastIncluded={roomsData[0].breakfastIncluded ?? false}
|
||||
/>
|
||||
<DesktopSummary
|
||||
isMember={!!user}
|
||||
breakfastIncluded={roomsData[0].breakfastIncluded ?? false}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
</EnterDetailsProvider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user