diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetails/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetails/index.tsx new file mode 100644 index 000000000..7d05ebfe2 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetails/index.tsx @@ -0,0 +1,92 @@ +"use client" +import { dt } from "@/lib/dt" +import { useBookingConfirmationStore } from "@/stores/booking-confirmation" + +import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal" + +import { mapToPrice } from "./mapToPrice" + +import type { Price } from "@/types/components/hotelReservation/price" +import { CurrencyEnum } from "@/types/enums/currency" + +export default function PriceDetails() { + const { bookingCode, currency, fromDate, rooms, vat, toDate } = + useBookingConfirmationStore((state) => ({ + bookingCode: state.bookingCode ?? undefined, + currency: state.currencyCode, + fromDate: state.fromDate, + rooms: state.rooms, + toDate: state.toDate, + vat: state.vat, + })) + + if (!rooms[0]) { + return null + } + + const checkInDate = dt(fromDate).format("YYYY-MM-DD") + const checkOutDate = dt(toDate).format("YYYY-MM-DD") + const nights = dt(toDate) + .startOf("day") + .diff(dt(fromDate).startOf("day"), "days") + + const totalPrice = rooms.reduce( + (total, room) => { + if (!room) { + return total + } + + const pkgsSum = + room.roomFeatures?.reduce((total, pkg) => total + pkg.totalPrice, 0) ?? + 0 + + if (room.cheques) { + // CorporateCheque Booking + total.local.currency = CurrencyEnum.CC + total.local.price = total.local.price + room.cheques + } else if (room.roomPoints) { + // Redemption Booking + total.local.currency = CurrencyEnum.POINTS + total.local.price = total.local.price + room.roomPoints + } else if (room.vouchers) { + // Vouchers Booking + total.local.currency = CurrencyEnum.Voucher + total.local.price = total.local.price + room.vouchers + } else { + // Price Booking + total.local.price = total.local.price + room.roomPrice + pkgsSum + } + + if ( + (room.cheques || room.roomPoints || room.vouchers) && + room.roomPrice + ) { + total.local.additionalPrice = + (total.local.additionalPrice || 0) + room.roomPrice + pkgsSum + total.local.additionalPriceCurrency = currency + } + + return total + }, + { + local: { + currency, + price: 0, + }, + requested: undefined, + } + ) + + const mappedRooms = mapToPrice(rooms, nights) + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetails/mapToPrice.ts b/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetails/mapToPrice.ts new file mode 100644 index 000000000..68d231b5d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetails/mapToPrice.ts @@ -0,0 +1,106 @@ +import { + breakfastPackageSchema, + packageSchema, +} from "@/server/routers/hotels/schemas/packages" + +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" +import type { Package } from "@/types/requests/packages" +import type { Room } from "@/types/stores/booking-confirmation" + +export function mapToPrice(rooms: (Room | null)[], nights: number) { + return rooms + .filter((room): room is Room => !!room) + .map((room) => { + let price + if (room.cheques) { + price = { + corporateCheque: { + additionalPricePerStay: room.roomPrice ? room.roomPrice : undefined, + currency: room.roomPrice ? room.currencyCode : undefined, + numberOfCheques: room.cheques, + }, + } + } else if (room.roomPoints) { + price = { + redemption: { + additionalPricePerStay: room.roomPrice ? room.roomPrice : undefined, + currency: room.roomPrice ? room.currencyCode : undefined, + pointsPerNight: room.roomPoints / nights, + pointsPerStay: room.roomPoints, + }, + } + } else if (room.vouchers) { + price = { + voucher: { + numberOfVouchers: room.vouchers, + }, + } + } else { + price = { + regular: { + currency: room.currencyCode, + pricePerNight: room.roomPrice / nights, + pricePerStay: room.roomPrice, + }, + } + } + + const breakfastPackage = breakfastPackageSchema.safeParse({ + code: room.breakfast?.code, + description: room.breakfast?.description, + localPrice: { + currency: room.breakfast?.currency, + price: room.breakfast?.unitPrice, + totalPrice: room.breakfast?.totalPrice, + }, + packageType: room.breakfast?.type, + requestedPrice: { + currency: room.breakfast?.currency, + price: room.breakfast?.unitPrice, + totalPrice: room.breakfast?.totalPrice, + }, + }) + + const packages = room.roomFeatures + ?.map((featPkg) => { + const pkg = packageSchema.safeParse({ + code: featPkg.code, + description: featPkg.description, + inventories: [], + localPrice: { + currency: featPkg.currency, + price: featPkg.unitPrice, + totalPrice: featPkg.totalPrice, + }, + requestedPrice: { + currency: featPkg.currency, + price: featPkg.unitPrice, + totalPrice: featPkg.totalPrice, + }, + }) + if (pkg.success) { + return pkg.data + } + return null + }) + .filter((pkg): pkg is Package => !!pkg) + + return { + ...room, + adults: room.adults, + bedType: { + description: room.bedDescription, + roomTypeCode: room.roomTypeCode || "", + }, + breakfast: breakfastPackage.success ? breakfastPackage.data : undefined, + breakfastIncluded: room.rateDefinition.breakfastIncluded, + childrenInRoom: room.childrenAges?.map((age) => ({ + age, + bed: ChildBedMapEnum.UNKNOWN, + })), + packages, + price, + roomType: room.name, + } + }) +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetailsModal/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetailsModal/index.tsx deleted file mode 100644 index aa776375a..000000000 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetailsModal/index.tsx +++ /dev/null @@ -1,309 +0,0 @@ -"use client" -import React from "react" -import { useIntl } from "react-intl" - -import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { dt } from "@/lib/dt" -import { useBookingConfirmationStore } from "@/stores/booking-confirmation" - -import Modal from "@/components/Modal" -import Button from "@/components/TempDesignSystem/Button" -import IconChip from "@/components/TempDesignSystem/IconChip" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import useLang from "@/hooks/useLang" -import { formatPrice } from "@/utils/numberFormatting" - -import styles from "./priceDetailsModal.module.css" - -function Row({ - label, - value, - bold, -}: { - label: string - value: string - bold?: boolean -}) { - return ( - - - {label} - - - {value} - - - ) -} - -function TableSection({ children }: React.PropsWithChildren) { - return {children} -} - -function TableSectionHeader({ - title, - subtitle, - bold, -}: { - title: string - subtitle?: string - bold?: boolean -}) { - const typographyVariant = bold - ? "Body/Paragraph/mdBold" - : "Body/Paragraph/mdRegular" - return ( - - - -

{title}

-
- {subtitle ? ( - -

{subtitle}

-
- ) : null} - - - ) -} - -export default function PriceDetailsModal() { - const intl = useIntl() - const lang = useLang() - const { - rooms, - currencyCode, - vat, - fromDate, - toDate, - bookingCode, - isVatCurrency, - formattedTotalCost, - } = useBookingConfirmationStore((state) => ({ - rooms: state.rooms, - currencyCode: state.currencyCode, - vat: state.vat, - fromDate: state.fromDate, - toDate: state.toDate, - bookingCode: state.bookingCode, - isVatCurrency: state.isVatCurrency, - formattedTotalCost: state.formattedTotalCost, - })) - - if (!rooms[0]) { - return null - } - - const checkInDate = dt(fromDate).format("YYYY-MM-DD") - const checkOutDate = dt(toDate).format("YYYY-MM-DD") - - const bookingTotal = rooms.reduce( - (acc, room) => { - if (room) { - return { - price: acc.price + room.totalPrice, - priceExVat: acc.priceExVat + room.totalPriceExVat, - vatAmount: acc.vatAmount + room.vatAmount, - } - } - return acc - }, - { price: 0, priceExVat: 0, vatAmount: 0 } - ) - - const diff = dt(checkOutDate).diff(checkInDate, "days") - const nights = intl.formatMessage( - { - defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", - }, - { totalNights: diff } - ) - - const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")} - - - ${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})` - return ( - - - {intl.formatMessage({ - defaultMessage: "Price details", - })} - - - - } - > - - {rooms.map((room, idx) => { - return room ? ( - - - {rooms.length > 1 && ( - - )} - - {room.roomFeatures - ? room.roomFeatures.map((feature) => ( - - )) - : null} - {room.bedDescription ? ( - - ) : null} - - - - {room.breakfast ? ( - - - {room.childrenAges?.length ? ( - - ) : null} - - - ) : null} - - ) : null - })} - - - {isVatCurrency ? ( - <> - - - - ) : null} - - - - - {bookingCode && ( - - - - )} - -
- - - {intl.formatMessage({ - defaultMessage: "Price including VAT", - })} - - - - - {formattedTotalCost} - -
- - } - > - {intl.formatMessage( - { - defaultMessage: - "Booking code: {value}", - }, - { - value: bookingCode, - strong: (text) => ( - - {text} - - ), - } - )} - - -
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetailsModal/priceDetailsModal.module.css b/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetailsModal/priceDetailsModal.module.css deleted file mode 100644 index 284e9ac5a..000000000 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/PriceDetailsModal/priceDetailsModal.module.css +++ /dev/null @@ -1,36 +0,0 @@ -.priceDetailsTable { - border-collapse: collapse; - width: 100%; -} - -.price { - text-align: end; -} - -.tableSection { - display: flex; - gap: var(--Spacing-x-half); - flex-direction: column; - width: 100%; -} - -.tableSection:has(tr > th) { - padding-top: var(--Spacing-x2); -} - -.tableSection:has(tr > th):not(:first-of-type) { - border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); -} - -.tableSection:not(:last-child) { - padding-bottom: var(--Spacing-x2); -} -.row { - display: flex; - justify-content: space-between; -} -@media screen and (min-width: 768px) { - .priceDetailsTable { - min-width: 512px; - } -} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/index.tsx index 8218aadd7..a5cccab40 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/index.tsx @@ -9,7 +9,7 @@ import { useBookingConfirmationStore } from "@/stores/booking-confirmation" import SkeletonShimmer from "@/components/SkeletonShimmer" import Divider from "@/components/TempDesignSystem/Divider" -import PriceDetailsModal from "../../PriceDetailsModal" +import PriceDetails from "../../PriceDetails" import styles from "./totalPrice.module.css" @@ -45,7 +45,7 @@ export default function TotalPrice() { )} {hasAllRoomsLoaded ? ( - + ) : (
diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/priceDetailsTable.module.css b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/priceDetailsTable.module.css deleted file mode 100644 index 284e9ac5a..000000000 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/priceDetailsTable.module.css +++ /dev/null @@ -1,36 +0,0 @@ -.priceDetailsTable { - border-collapse: collapse; - width: 100%; -} - -.price { - text-align: end; -} - -.tableSection { - display: flex; - gap: var(--Spacing-x-half); - flex-direction: column; - width: 100%; -} - -.tableSection:has(tr > th) { - padding-top: var(--Spacing-x2); -} - -.tableSection:has(tr > th):not(:first-of-type) { - border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); -} - -.tableSection:not(:last-child) { - padding-bottom: var(--Spacing-x2); -} -.row { - display: flex; - justify-content: space-between; -} -@media screen and (min-width: 768px) { - .priceDetailsTable { - min-width: 512px; - } -} diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx index 92bb72ed9..d2e850358 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx @@ -21,7 +21,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" -import PriceDetailsTable from "./PriceDetailsTable" +import { mapToPrice } from "./mapToPrice" import styles from "./ui.module.css" @@ -95,6 +95,8 @@ export default function SummaryUI({ ? totalPrice.requested.currency === totalPrice.local.currency : false + const priceDetailsRooms = mapToPrice(rooms, isMember) + return (
@@ -440,17 +442,14 @@ export default function SummaryUI({ { b: (str) => {str} } )} - - r.room)} - toDate={booking.toDate} - totalPrice={totalPrice} - vat={vat} - /> - +
diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/mapToPrice.ts b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/mapToPrice.ts new file mode 100644 index 000000000..d364939fe --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/mapToPrice.ts @@ -0,0 +1,65 @@ +import type { RoomState } from "@/types/stores/enter-details" + +export function mapToPrice(rooms: RoomState[], isMember: boolean) { + return rooms + .filter((room) => room && room.room.roomRate) + .map(({ room }, idx) => { + const isMainRoom = idx === 0 + + if ("corporateCheque" in room.roomRate) { + return { + ...room, + packages: room.roomFeatures, + price: { + corporateCheque: room.roomRate.corporateCheque.localPrice, + }, + } + } + + if ("redemption" in room.roomRate) { + return { + ...room, + packages: room.roomFeatures, + price: { + redemption: room.roomRate.redemption.localPrice, + }, + } + } + + if ("voucher" in room.roomRate) { + return { + ...room, + packages: room.roomFeatures, + price: { + voucher: room.roomRate.voucher, + }, + } + } + + const isMemberRate = !!(room.guest.join || room.guest.membershipNo) + if ((isMember && isMainRoom) || isMemberRate) { + if ("member" in room.roomRate && room.roomRate.member) { + return { + ...room, + packages: room.roomFeatures, + price: { + regular: room.roomRate.member.localPrice, + }, + } + } + } + + if ("public" in room.roomRate && room.roomRate.public) { + return { + ...room, + packages: room.roomFeatures, + price: { + regular: room.roomRate.public.localPrice, + }, + } + } + + console.error(room.roomRate) + throw new Error(`Unknown roomRate`) + }) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/priceDetailsTable.module.css b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/priceDetailsTable.module.css deleted file mode 100644 index 11513f0ad..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/priceDetailsTable.module.css +++ /dev/null @@ -1,37 +0,0 @@ -.priceDetailsTable { - border-collapse: collapse; - width: 100%; -} - -.price { - text-align: end; -} - -.tableSection { - display: flex; - gap: var(--Spacing-x-half); - flex-direction: column; - width: 100%; -} - -.tableSection:has(tr > th) { - padding-top: var(--Spacing-x2); -} - -.tableSection:has(tr > th):not(:first-of-type) { - border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); -} - -.tableSection:not(:last-child) { - padding-bottom: var(--Spacing-x2); -} -.row { - display: flex; - justify-content: space-between; -} - -@media screen and (min-width: 768px) { - .priceDetailsTable { - min-width: 512px; - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx index f56bc688b..a69b9f65b 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx @@ -1,10 +1,10 @@ "use client" import { dt } from "@/lib/dt" import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" -import PriceDetailsModal from "../../PriceDetailsModal" -import PriceDetailsTable from "./PriceDetailsTable" +import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal" + +import { calculateTotalPrice, mapToPrice } from "./mapToPrice" import styles from "./priceDetails.module.css" @@ -13,27 +13,32 @@ export default function PriceDetails() { const linkedReservationRooms = useMyStayRoomDetailsStore( (state) => state.linkedReservationRooms ) - const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode) - const totalPrice = useMyStayTotalPriceStore((state) => state.totalPrice) + const rooms = [bookedRoom, ...linkedReservationRooms] + .filter((room) => !room.isCancelled) + .map((room) => ({ + ...room, + breakfastIncluded: room.rateDefinition.breakfastIncluded, + price: mapToPrice(room), + roomType: room.roomName, + })) + + const bookingCode = + rooms.find((room) => room.bookingCode)?.bookingCode ?? undefined + const totalPrice = calculateTotalPrice(rooms, bookedRoom.currencyCode) + + const fromDate = dt(bookedRoom.checkInDate).format("YYYY-MM-DD") + const toDate = dt(bookedRoom.checkOutDate).format("YYYY-MM-DD") return (
- - - +
) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts new file mode 100644 index 000000000..7dac558dd --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts @@ -0,0 +1,125 @@ +import { dt } from "@/lib/dt" + +import { sumPackages } from "@/components/HotelReservation/utils" + +import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" +import type { Price } from "@/types/components/hotelReservation/price" +import { CurrencyEnum } from "@/types/enums/currency" +import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" + +export function mapToPrice(room: Room) { + switch (room.priceType) { + case PriceTypeEnum.cheque: + return { + corporateCheque: { + additionalPricePerStay: room.roomPrice.perStay.local.price, + currency: room.roomPrice.perStay.local.currency, + numberOfCheques: room.cheques, + }, + } + case PriceTypeEnum.money: + return { + regular: { + currency: room.currencyCode, + pricePerNight: room.roomPrice.perNight, + pricePerStay: room.roomPrice.perStay, + }, + } + case PriceTypeEnum.points: + const nights = dt(room.checkOutDate) + .startOf("day") + .diff(dt(room.checkInDate).startOf("day"), "days") + return { + redemption: { + additionalPricePerStay: room.roomPrice.perStay.local.price, + currency: room.roomPrice.perStay.local.currency, + pointsPerNight: room.roomPoints / nights, + pointsPerStay: room.roomPoints, + }, + } + case PriceTypeEnum.voucher: + return { + voucher: { + numberOfVouchers: room.vouchers, + }, + } + default: + throw new Error(`Unknown payment method!`) + } +} + +export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) { + return rooms.reduce( + (total, room) => { + const pkgsSum = sumPackages(room.packages) + let breakfastPrice = 0 + if (room.breakfast && !room.rateDefinition.breakfastIncluded) { + breakfastPrice = room.breakfast.localPrice.totalPrice + } + switch (room.priceType) { + case PriceTypeEnum.cheque: + { + total.local.currency = CurrencyEnum.CC + total.local.price = total.local.price + room.cheques + } + break + case PriceTypeEnum.money: + { + total.local.price = + total.local.price + + room.roomPrice.perStay.local.price + + pkgsSum.price + + breakfastPrice + + if (!total.local.currency) { + total.local.currency = room.currencyCode + } + } + break + case PriceTypeEnum.points: + { + total.local.currency = CurrencyEnum.POINTS + total.local.price = total.local.price + room.roomPoints + } + break + case PriceTypeEnum.voucher: + total.local.currency = CurrencyEnum.Voucher + total.local.price = total.local.price + room.vouchers + break + } + + switch (room.priceType) { + case PriceTypeEnum.cheque: + case PriceTypeEnum.points: + case PriceTypeEnum.voucher: + { + if (room.roomPrice.perStay.local.price || pkgsSum) { + total.local.additionalPrice = + room.roomPrice.perStay.local.price + + pkgsSum.price + + breakfastPrice + } + + if (!total.local.additionalPriceCurrency) { + if (room.roomPrice.perStay.local.currency) { + total.local.additionalPriceCurrency = + room.roomPrice.perStay.local.currency + } else { + total.local.additionalPriceCurrency = currency + } + } + } + break + } + + return total + }, + { + local: { + currency, + price: 0, + }, + requested: undefined, + } + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal.tsx deleted file mode 100644 index d768f3678..000000000 --- a/apps/scandic-web/components/HotelReservation/PriceDetailsModal.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import { Button } from "@scandic-hotels/design-system/Button" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" - -import Modal from "@/components/Modal" - -export default function PriceDetailsModal({ - children, -}: React.PropsWithChildren) { - const intl = useIntl() - - return ( - - {intl.formatMessage({ - defaultMessage: "Price details", - })} - - - } - > - {children} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx new file mode 100644 index 000000000..6bdc7498c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx @@ -0,0 +1,117 @@ +"use client" +import { useIntl } from "react-intl" + +import { formatPrice } from "@/utils/numberFormatting" + +import BoldRow from "./Row/Bold" +import RegularRow from "./Row/Regular" +import Tbody from "./Tbody" + +import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" +import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" + +interface BreakfastProps { + adults: number + breakfast: BreakfastPackage | false | undefined | null + breakfastIncluded: boolean + childrenInRoom: Child[] | undefined + currency: string + nights: number +} + +export default function Breakfast({ + adults, + breakfast, + breakfastIncluded, + childrenInRoom, + currency, + nights, +}: BreakfastProps) { + const intl = useIntl() + + if (breakfastIncluded) { + const included = intl.formatMessage({ defaultMessage: "Included" }) + return ( + + + {childrenInRoom?.length ? ( + + ) : null} + + + ) + } + + if (!breakfast) { + return null + } + + const breakfastAdultsPricePerNight = formatPrice( + intl, + breakfast.localPrice.price * adults, + breakfast.localPrice.currency + ) + const breakfastAdultsTotalPrice = formatPrice( + intl, + breakfast.localPrice.price * adults * nights, + breakfast.localPrice.currency + ) + + return ( + + + {childrenInRoom?.length ? ( + + ) : null} + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Bold.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Bold.tsx new file mode 100644 index 000000000..be3d431df --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Bold.tsx @@ -0,0 +1,25 @@ +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./row.module.css" + +interface RowProps { + label: string + value: string +} + +export default function BoldRow({ label, value }: RowProps) { + return ( + + + + {label} + + + + + {value} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/BookingCode.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/BookingCode.tsx new file mode 100644 index 000000000..021c833ce --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/BookingCode.tsx @@ -0,0 +1,48 @@ +"use client" +import { useIntl } from "react-intl" + +import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import IconChip from "@/components/TempDesignSystem/IconChip" + +import styles from "./row.module.css" + +interface BookingCodeRowProps { + bookingCode?: string +} + +export default function BookingCodeRow({ bookingCode }: BookingCodeRowProps) { + const intl = useIntl() + + if (!bookingCode) { + return null + } + + const text = intl.formatMessage( + { defaultMessage: "Booking code: {value}" }, + { + value: bookingCode, + strong: (text) => ( + + {text} + + ), + } + ) + + return ( + + + + } + > + {text} + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/DiscountedRegularPrice.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/DiscountedRegularPrice.tsx new file mode 100644 index 000000000..378585eac --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/DiscountedRegularPrice.tsx @@ -0,0 +1,51 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import Caption from "@/components/TempDesignSystem/Text/Caption" +import { formatPrice } from "@/utils/numberFormatting" + +import styles from "./row.module.css" + +import type { CurrencyEnum } from "@/types/enums/currency" +import type { Package } from "@/types/requests/packages" + +interface DiscountedRegularPriceRowProps { + currency: CurrencyEnum + packages: Package[] + regularPrice?: number +} + +export default function DiscountedRegularPriceRow({ + currency, + packages, + regularPrice, +}: DiscountedRegularPriceRowProps) { + const intl = useIntl() + + if (!regularPrice) { + return null + } + + const totalPackagesPrice = packages.reduce( + (total, pkg) => total + pkg.localPrice.totalPrice, + 0 + ) + + const price = formatPrice(intl, regularPrice + totalPackagesPrice, currency) + + return ( + + + + + + {price} + + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Header.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Header.tsx new file mode 100644 index 000000000..fe3c67a65 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Header.tsx @@ -0,0 +1,29 @@ +import { Typography } from "@scandic-hotels/design-system/Typography" + +interface TrProps { + subtitle?: string + title: string +} + +export default function HeaderRow({ subtitle, title }: TrProps) { + return ( + <> + + + + {title} + + + + {subtitle ? ( + + + + {subtitle} + + + + ) : null} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Large.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Large.tsx new file mode 100644 index 000000000..fdfeefc95 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Large.tsx @@ -0,0 +1,25 @@ +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./row.module.css" + +interface RowProps { + label: string + value: string +} + +export default function LargeRow({ label, value }: RowProps) { + return ( + + + + {label} + + + + + {value} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/BedType.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/BedType.tsx new file mode 100644 index 000000000..dd9595082 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/BedType.tsx @@ -0,0 +1,31 @@ +"use client" +import { useIntl } from "react-intl" + +import { formatPrice } from "@/utils/numberFormatting" + +import RegularRow from "../Regular" + +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" + +interface BedTypeRowProps { + bedType: BedTypeSchema | undefined + currency?: string +} + +export default function BedTypeRow({ + bedType, + currency = "", +}: BedTypeRowProps) { + const intl = useIntl() + + if (!bedType) { + return null + } + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/CorporateCheque.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/CorporateCheque.tsx new file mode 100644 index 000000000..b237a6922 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/CorporateCheque.tsx @@ -0,0 +1,83 @@ +"use client" +import { useIntl } from "react-intl" + +import { sumPackages } from "@/components/HotelReservation/utils" +import { formatPrice } from "@/utils/numberFormatting" + +import BoldRow from "../Bold" +import RegularRow from "../Regular" +import BedTypeRow from "./BedType" +import PackagesRow from "./Packages" + +import { CurrencyEnum } from "@/types/enums/currency" +import type { SharedPriceRowProps } from "./price" + +export interface CorporateChequePriceType { + corporateCheque?: { + additionalPricePerStay?: number + currency?: CurrencyEnum + numberOfCheques: number + } +} + +interface CorporateChequePriceProps extends SharedPriceRowProps { + currency: string + nights: number + price: CorporateChequePriceType["corporateCheque"] +} + +export default function CorporateChequePrice({ + bedType, + currency, + nights, + packages, + price, +}: CorporateChequePriceProps) { + const intl = useIntl() + + if (!price) { + return null + } + + const averagePriceTitle = intl.formatMessage({ + defaultMessage: "Average price per night", + }) + + const pkgsSum = sumPackages(packages) + const roomAdditionalPrice = price.additionalPricePerStay + let additionalPricePerStay + if (roomAdditionalPrice) { + additionalPricePerStay = roomAdditionalPrice + pkgsSum.price + } else if (pkgsSum.price) { + additionalPricePerStay = pkgsSum.price + } + + const averageChequesPerNight = price.numberOfCheques / nights + const averageAdditionalPricePerNight = roomAdditionalPrice + ? Math.ceil(roomAdditionalPrice / nights) + : null + + const additionalCurrency = price.currency ?? pkgsSum.currency + let averagePricePerNight = `${averageChequesPerNight} ${CurrencyEnum.CC}` + if (averageAdditionalPricePerNight) { + averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}` + } + + return ( + <> + + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Packages.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Packages.tsx new file mode 100644 index 000000000..9d6ebfdee --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Packages.tsx @@ -0,0 +1,32 @@ +"use client" +import { useIntl } from "react-intl" + +import { formatPrice } from "@/utils/numberFormatting" + +import RegularRow from "../Regular" + +import type { Packages as PackagesType } from "@/types/requests/packages" + +interface PackagesProps { + packages: PackagesType | null +} + +export default function PackagesRow({ packages }: PackagesProps) { + const intl = useIntl() + + if (!packages || !packages.length) { + return null + } + + return packages?.map((pkg) => ( + + )) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Redemption.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Redemption.tsx new file mode 100644 index 000000000..cbeab365e --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Redemption.tsx @@ -0,0 +1,83 @@ +"use client" +import { useIntl } from "react-intl" + +import { sumPackages } from "@/components/HotelReservation/utils" +import { formatPrice } from "@/utils/numberFormatting" + +import BoldRow from "../Bold" +import RegularRow from "../Regular" +import BedTypeRow from "./BedType" +import PackagesRow from "./Packages" + +import { CurrencyEnum } from "@/types/enums/currency" +import type { SharedPriceRowProps } from "./price" + +export interface RedemptionPriceType { + redemption?: { + additionalPricePerStay?: number + currency?: CurrencyEnum + pointsPerNight: number + pointsPerStay: number + } +} + +interface RedemptionPriceProps extends SharedPriceRowProps { + currency: string + nights: number + price: RedemptionPriceType["redemption"] +} + +export default function RedemptionPrice({ + bedType, + currency, + nights, + packages, + price, +}: RedemptionPriceProps) { + const intl = useIntl() + + if (!price) { + return null + } + + const averagePriceTitle = intl.formatMessage({ + defaultMessage: "Average price per night", + }) + const pkgsSum = sumPackages(packages) + + const roomAdditionalPrice = price.additionalPricePerStay + let additionalPricePerStay + if (roomAdditionalPrice) { + additionalPricePerStay = roomAdditionalPrice + pkgsSum.price + } else if (pkgsSum.price) { + additionalPricePerStay = pkgsSum.price + } + + const averageAdditionalPricePerNight = roomAdditionalPrice + ? Math.ceil(roomAdditionalPrice / nights) + : null + + const additionalCurrency = price.currency ?? pkgsSum.currency + let averagePricePerNight = `${price.pointsPerNight} ${CurrencyEnum.POINTS}` + if (averageAdditionalPricePerNight) { + averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}` + } + + return ( + <> + + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Regular.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Regular.tsx new file mode 100644 index 000000000..d27dfa3ed --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Regular.tsx @@ -0,0 +1,67 @@ +"use client" +import { useIntl } from "react-intl" + +import { sumPackages } from "@/components/HotelReservation/utils" +import { formatPrice } from "@/utils/numberFormatting" + +import BoldRow from "../Bold" +import RegularRow from "../Regular" +import BedTypeRow from "./BedType" +import PackagesRow from "./Packages" + +import type { CurrencyEnum } from "@/types/enums/currency" +import type { SharedPriceRowProps } from "./price" + +export interface RegularPriceType { + regular?: { + currency: CurrencyEnum + pricePerNight: number + pricePerStay: number + } +} + +interface RegularPriceProps extends SharedPriceRowProps { + price: RegularPriceType["regular"] +} + +export default function RegularPrice({ + bedType, + packages, + price, +}: RegularPriceProps) { + const intl = useIntl() + + if (!price) { + return null + } + + const averagePriceTitle = intl.formatMessage({ + defaultMessage: "Average price per night", + }) + + const avgeragePricePerNight = formatPrice( + intl, + price.pricePerNight, + price.currency + ) + + const pkgs = sumPackages(packages) + + const roomCharge = formatPrice( + intl, + price.pricePerStay + pkgs.price, + price.currency + ) + + return ( + <> + + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Voucher.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Voucher.tsx new file mode 100644 index 000000000..e914a04ae --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/Voucher.tsx @@ -0,0 +1,76 @@ +"use client" +import { useIntl } from "react-intl" + +import { sumPackages } from "@/components/HotelReservation/utils" +import { formatPrice } from "@/utils/numberFormatting" + +import BoldRow from "../Bold" +import RegularRow from "../Regular" +import BedTypeRow from "./BedType" +import PackagesRow from "./Packages" + +import { CurrencyEnum } from "@/types/enums/currency" +import type { SharedPriceRowProps } from "./price" + +export interface VoucherPriceType { + voucher?: { + numberOfVouchers: number + } +} + +interface VoucherPriceProps extends SharedPriceRowProps { + currency: string + nights: number + price: VoucherPriceType["voucher"] +} + +export default function VoucherPrice({ + bedType, + currency, + nights, + packages, + price, +}: VoucherPriceProps) { + const intl = useIntl() + + if (!price) { + return null + } + + const averagePriceTitle = intl.formatMessage({ + defaultMessage: "Average price per night", + }) + const pkgsSum = sumPackages(packages) + + let additionalPricePerStay + if (pkgsSum.price) { + additionalPricePerStay = pkgsSum.price + } + + const averageAdditionalPricePerNight = additionalPricePerStay + ? Math.ceil(additionalPricePerStay / nights) + : null + + let averagePricePerNight = `${price.numberOfVouchers} ${CurrencyEnum.Voucher}` + if (averageAdditionalPricePerNight) { + averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${pkgsSum.currency}` + } + + return ( + <> + + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/price.ts b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/price.ts new file mode 100644 index 000000000..fe7d9403f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Price/price.ts @@ -0,0 +1,7 @@ +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { Packages } from "@/types/requests/packages" + +export interface SharedPriceRowProps { + bedType: BedTypeSchema | undefined + packages: Packages | null +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Regular.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Regular.tsx new file mode 100644 index 000000000..ecdfddbc0 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Regular.tsx @@ -0,0 +1,25 @@ +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./row.module.css" + +interface RowProps { + label: string + value: string +} + +export default function RegularRow({ label, value }: RowProps) { + return ( + + + + {label} + + + + + {value} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx new file mode 100644 index 000000000..73abbd489 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx @@ -0,0 +1,46 @@ +"use client" +import { useIntl } from "react-intl" + +import { formatPrice } from "@/utils/numberFormatting" + +import RegularRow from "./Regular" + +import type { Price } from "@/types/components/hotelReservation/price" +import { CurrencyEnum } from "@/types/enums/currency" + +interface VatProps { + totalPrice: Price + vat: number +} + +const noVatCurrencies = [ + CurrencyEnum.CC, + CurrencyEnum.POINTS, + CurrencyEnum.Voucher, +] + +export default function VatRow({ totalPrice, vat }: VatProps) { + const intl = useIntl() + + if (noVatCurrencies.includes(totalPrice.local.currency)) { + return null + } + + const vatPercentage = vat / 100 + const vatAmount = totalPrice.local.price * vatPercentage + + const priceExclVat = totalPrice.local.price - vatAmount + + return ( + <> + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/row.module.css b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/row.module.css new file mode 100644 index 000000000..9963233a4 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Row/row.module.css @@ -0,0 +1,8 @@ +.row { + display: flex; + justify-content: space-between; +} + +.price { + text-align: end; +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/index.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/index.tsx new file mode 100644 index 000000000..f55371598 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/index.tsx @@ -0,0 +1,6 @@ +import { type TbodyProps,tbodyVariants } from "./variants" + +export default function Tbody({ border, children }: TbodyProps) { + const classNames = tbodyVariants({ border }) + return {children} +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/tbody.module.css b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/tbody.module.css new file mode 100644 index 000000000..f28542dda --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/tbody.module.css @@ -0,0 +1,23 @@ +.tbody { + display: flex; + gap: var(--Spacing-x-half); + flex-direction: column; + width: 100%; +} + +.tbody:has(tr > th) { + padding-top: var(--Spacing-x2); +} + +.tbody:has(tr > th):not(:first-of-type), +.border { + border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); +} + +.tbody:not(:last-child) { + padding-bottom: var(--Spacing-x2); +} + +.border { + padding-top: var(--Spacing-x2); +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/variants.ts b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/variants.ts new file mode 100644 index 000000000..618f4014b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Tbody/variants.ts @@ -0,0 +1,18 @@ +import { cva, type VariantProps } from "class-variance-authority" + +import styles from "./tbody.module.css" + +import type { PropsWithChildren } from "react" + +export const tbodyVariants = cva(styles.tbody, { + variants: { + border: { + true: styles.border, + }, + }, + defaultVariants: {}, +}) + +export interface TbodyProps + extends PropsWithChildren, + VariantProps {} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx new file mode 100644 index 000000000..b801b14c1 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx @@ -0,0 +1,214 @@ +"use client" +import { Fragment } from "react" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { dt } from "@/lib/dt" + +import useLang from "@/hooks/useLang" +import { formatPrice } from "@/utils/numberFormatting" + +import BookingCodeRow from "./Row/BookingCode" +import DiscountedRegularPriceRow from "./Row/DiscountedRegularPrice" +import HeaderRow from "./Row/Header" +import LargeRow from "./Row/Large" +import CorporateChequePrice, { + type CorporateChequePriceType, +} from "./Row/Price/CorporateCheque" +import RedemptionPrice, { + type RedemptionPriceType, +} from "./Row/Price/Redemption" +import RegularPrice, { type RegularPriceType } from "./Row/Price/Regular" +import VoucherPrice, { type VoucherPriceType } from "./Row/Price/Voucher" +import VatRow from "./Row/Vat" +import Breakfast from "./Breakfast" +import Tbody from "./Tbody" + +import styles from "./priceDetailsTable.module.css" + +import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { Price } from "@/types/components/hotelReservation/price" +import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Package, Packages } from "@/types/requests/packages" + +type RoomPrice = + | CorporateChequePriceType + | RegularPriceType + | RedemptionPriceType + | VoucherPriceType + +export interface Room { + adults: number + bedType: BedTypeSchema | undefined + breakfast: BreakfastPackage | false | undefined | null + breakfastIncluded: boolean + childrenInRoom: Child[] | undefined + packages: Packages | null + price: RoomPrice + roomType: string +} + +export interface PriceDetailsTableProps { + bookingCode?: string + fromDate: string + rooms: Room[] + toDate: string + totalPrice: Price + vat: number +} + +export default function PriceDetailsTable({ + bookingCode, + fromDate, + rooms, + toDate, + totalPrice, + vat, +}: PriceDetailsTableProps) { + const intl = useIntl() + const lang = useLang() + + const diff = dt(toDate).diff(fromDate, "days") + const nights = intl.formatMessage( + { defaultMessage: "{totalNights, plural, one {# night} other {# nights}}" }, + { totalNights: diff } + ) + + const arrival = dt(fromDate).locale(lang).format("ddd, D MMM") + const departue = dt(toDate).locale(lang).format("ddd, D MMM") + const duration = ` ${arrival} - ${departue} (${nights})` + + const allRoomsPackages: Package[] = rooms + .flatMap((r) => r.packages) + .filter((r): r is Package => !!r) + return ( + + {rooms.map((room, idx) => { + let currency = "" + let chequePrice: CorporateChequePriceType["corporateCheque"] | undefined + if ("corporateCheque" in room.price && room.price.corporateCheque) { + chequePrice = room.price.corporateCheque + + if (room.price.corporateCheque.currency) { + currency = room.price.corporateCheque.currency + } + } + + let price: RegularPriceType["regular"] | undefined + if ("regular" in room.price && room.price.regular) { + price = room.price.regular + currency = room.price.regular.currency + } + + let redemptionPrice: RedemptionPriceType["redemption"] | undefined + if ("redemption" in room.price && room.price.redemption) { + redemptionPrice = room.price.redemption + + if (room.price.redemption.currency) { + currency = room.price.redemption.currency + } + } + + let voucherPrice: VoucherPriceType["voucher"] | undefined + if ("voucher" in room.price && room.price.voucher) { + voucherPrice = room.price.voucher + } + + if (!currency) { + if (room.packages?.length) { + currency = room.packages[0].localPrice.currency + } else if (room.breakfast) { + currency = room.breakfast.localPrice.currency + } + } + + if (!price && !voucherPrice && !chequePrice && !redemptionPrice) { + return null + } + + return ( + + + {rooms.length > 1 && ( + + + + )} + + + + + + + + + + ) + })} + + + + + + + + + + + +
+ + + {intl.formatMessage( + { defaultMessage: "Room {roomIndex}" }, + { roomIndex: idx + 1 } + )} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/priceDetailsTable.module.css b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/priceDetailsTable.module.css new file mode 100644 index 000000000..d61dbc140 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/priceDetailsTable.module.css @@ -0,0 +1,10 @@ +.priceDetailsTable { + border-collapse: collapse; + width: 100%; +} + +@media screen and (min-width: 768px) { + .priceDetailsTable { + min-width: 512px; + } +} diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/index.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/index.tsx new file mode 100644 index 000000000..2d029b5d7 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/index.tsx @@ -0,0 +1,34 @@ +"use client" +import { useIntl } from "react-intl" + +import { Button } from "@scandic-hotels/design-system/Button" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" + +import Modal from "@/components/Modal" + +import PriceDetailsTable, { + type PriceDetailsTableProps, +} from "./PriceDetailsTable" + +function Trigger({ title }: { title: string }) { + return ( + + ) +} + +export default function PriceDetailsModal(props: PriceDetailsTableProps) { + const intl = useIntl() + const title = intl.formatMessage({ defaultMessage: "Price details" }) + return ( + }> + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/priceDetailsTable.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/priceDetailsTable.module.css deleted file mode 100644 index 284e9ac5a..000000000 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/PriceDetailsTable/priceDetailsTable.module.css +++ /dev/null @@ -1,36 +0,0 @@ -.priceDetailsTable { - border-collapse: collapse; - width: 100%; -} - -.price { - text-align: end; -} - -.tableSection { - display: flex; - gap: var(--Spacing-x-half); - flex-direction: column; - width: 100%; -} - -.tableSection:has(tr > th) { - padding-top: var(--Spacing-x2); -} - -.tableSection:has(tr > th):not(:first-of-type) { - border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); -} - -.tableSection:not(:last-child) { - padding-bottom: var(--Spacing-x2); -} -.row { - display: flex; - justify-content: space-between; -} -@media screen and (min-width: 768px) { - .priceDetailsTable { - min-width: 512px; - } -} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx index 1fee13712..3490af59c 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx @@ -6,6 +6,7 @@ import { Button } from "@scandic-hotels/design-system/Button" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { dt } from "@/lib/dt" +import { useRatesStore } from "@/stores/select-rate" import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal" import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop" @@ -18,7 +19,7 @@ import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" import { isBookingCodeRate } from "./isBookingCodeRate" -import PriceDetailsTable from "./PriceDetailsTable" +import { mapToPrice } from "./mapToPrice" import styles from "./summary.module.css" @@ -34,6 +35,7 @@ export default function Summary({ vat, toggleSummaryOpen, }: SelectRateSummaryProps) { + const rateSummary = useRatesStore((state) => state.rateSummary) const intl = useIntl() const lang = useLang() @@ -66,6 +68,8 @@ export default function Summary({ ) const showDiscounted = containsBookingCodeRate || isMember + const priceDetailsRooms = mapToPrice(rateSummary, booking, isMember) + return (
@@ -304,17 +308,14 @@ export default function Summary({ { b: (str) => {str} } )} - - - +
{ + if (!room) { + return null + } + + let price = null + if ("corporateCheque" in room.product) { + price = { + corporateCheque: room.product.corporateCheque.localPrice, + } + } else if ("redemption" in room.product) { + price = { + redemption: room.product.redemption.localPrice, + } + } else if ("voucher" in room.product) { + price = { + voucher: room.product.voucher, + } + } else { + const isMainRoom = idx === 0 + const memberRate = room.product.member + const onlyMemberRate = !room.product.public && memberRate + if ((isUserLoggedIn && isMainRoom && memberRate) || onlyMemberRate) { + price = { + regular: memberRate.localPrice, + } + } else if (room.product.public) { + price = { + regular: room.product.public.localPrice, + } + } + } + + const bookingRoom = booking.rooms[idx] + return { + adults: bookingRoom.adults, + bedType: undefined, + breakfast: undefined, + breakfastIncluded: room.product.rateDefinition.breakfastIncluded, + childrenInRoom: bookingRoom.childrenInRoom, + packages: room.packages, + price, + roomType: room.roomType, + } + }) + .filter((r) => !!(r && r.price)) as Room[] +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts index 087e9c320..19ba1c34a 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts @@ -1,6 +1,9 @@ +import { sumPackages } from "@/components/HotelReservation/utils" + import type { Price } from "@/types/components/hotelReservation/price" import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" import { CurrencyEnum } from "@/types/enums/currency" +import type { Packages } from "@/types/requests/packages" import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability" export function calculateTotalPrice( @@ -87,16 +90,28 @@ export function calculateTotalPrice( } export function calculateRedemptionTotalPrice( - redemption: RedemptionProduct["redemption"] + redemption: RedemptionProduct["redemption"], + packages: Packages | null ) { + const pkgsSum = sumPackages(packages) + let additionalPrice + if (redemption.localPrice.additionalPricePerStay) { + additionalPrice = + redemption.localPrice.additionalPricePerStay + pkgsSum.price + } else if (pkgsSum.price) { + additionalPrice = pkgsSum.price + } + + let additionalPriceCurrency + if (redemption.localPrice.currency) { + additionalPriceCurrency = redemption.localPrice.currency + } else if (pkgsSum.currency) { + additionalPriceCurrency = pkgsSum.currency + } return { local: { - additionalPrice: redemption.localPrice.additionalPricePerStay - ? redemption.localPrice.additionalPricePerStay - : undefined, - additionalPriceCurrency: redemption.localPrice.currency - ? redemption.localPrice.currency - : undefined, + additionalPrice, + additionalPriceCurrency, currency: CurrencyEnum.POINTS, price: redemption.localPrice.pointsPerStay, }, @@ -111,13 +126,9 @@ export function calculateVoucherPrice(selectedRateSummary: Rate[]) { } const rate = room.product.voucher - return { - local: { - currency: total.local.currency, - price: total.local.price + rate.numberOfVouchers, - }, - requested: undefined, - } + total.local.price = total.local.price + rate.numberOfVouchers + + return total }, { local: { @@ -136,12 +147,17 @@ export function calculateCorporateChequePrice(selectedRateSummary: Rate[]) { return total } const rate = room.product.corporateCheque + const pkgsSum = sumPackages(room.packages) total.local.price = total.local.price + rate.localPrice.numberOfCheques if (rate.localPrice.additionalPricePerStay) { total.local.additionalPrice = (total.local.additionalPrice || 0) + - rate.localPrice.additionalPricePerStay + rate.localPrice.additionalPricePerStay + + pkgsSum.price + } else if (pkgsSum.price) { + total.local.additionalPrice = + (total.local.additionalPrice || 0) + pkgsSum.price } if (rate.localPrice.currency) { total.local.additionalPriceCurrency = rate.localPrice.currency @@ -196,11 +212,11 @@ export function getTotalPrice( return calculateTotalPrice(summaryArray, isUserLoggedIn) } - const { product } = mainRoomProduct + const { packages, 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) + return calculateRedemptionTotalPrice(product.redemption, packages) } if ("voucher" in product) { return calculateVoucherPrice(summaryArray) diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Campaign.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Campaign.tsx index 3244b20a0..d33c262f3 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Campaign.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Campaign.tsx @@ -4,6 +4,10 @@ import { useIntl } from "react-intl" import CampaignRateCard from "@scandic-hotels/design-system/CampaignRateCard" import NoRateAvailableCard from "@scandic-hotels/design-system/NoRateAvailableCard" +import { + sumPackages, + sumPackagesRequestedPrice, +} from "@/components/HotelReservation/utils" import { useRoomContext } from "@/contexts/SelectRate/Room" import useRateTitles from "@/hooks/booking/useRateTitles" @@ -22,11 +26,15 @@ export default function Campaign({ campaign, handleSelectRate, nights, - petRoomPackage, roomTypeCode, }: CampaignProps) { const intl = useIntl() - const { roomNr, selectedFilter, selectedRate } = useRoomContext() + const { + roomNr, + selectedFilter, + selectedPackages, + selectedRate, + } = useRoomContext() const rateTitles = useRateTitles() const isCampaignRate = campaign.some( @@ -52,6 +60,9 @@ export default function Campaign({ campaign = campaign.filter((product) => !product.bookingCode) } + const pkgsSum = sumPackages(selectedPackages) + const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages) + return campaign.map((product) => { if (!product.public) { return ( @@ -67,21 +78,21 @@ export default function Campaign({ const rateTermDetails = product.rateDefinitionMember ? [ - { - title: product.rateDefinition.title, - terms: product.rateDefinition.generalTerms, - }, - { - title: product.rateDefinitionMember.title, - terms: product.rateDefinition.generalTerms, - }, - ] + { + title: product.rateDefinition.title, + terms: product.rateDefinition.generalTerms, + }, + { + title: product.rateDefinitionMember.title, + terms: product.rateDefinition.generalTerms, + }, + ] : [ - { - title: product.rateDefinition.title, - terms: product.rateDefinition.generalTerms, - }, - ] + { + title: product.rateDefinition.title, + terms: product.rateDefinition.generalTerms, + }, + ] const isSelected = isSelectedPriceProduct( product, @@ -110,16 +121,18 @@ export default function Campaign({ product.public.localPrice.pricePerNight, product.public.requestedPrice?.pricePerNight, nights, - petRoomPackage + pkgsSum.price, + pkgsSumRequested.price ) const pricePerNightMember = product.member ? calculatePricePerNightPriceProduct( - product.member.localPrice.pricePerNight, - product.member.requestedPrice?.pricePerNight, - nights, - petRoomPackage - ) + product.member.localPrice.pricePerNight, + product.member.requestedPrice?.pricePerNight, + nights, + pkgsSum.price, + pkgsSumRequested.price + ) : undefined let approximateRatePrice = undefined @@ -135,12 +148,12 @@ export default function Campaign({ const approximateRate = approximateRatePrice && product.public.requestedPrice ? { - label: intl.formatMessage({ - defaultMessage: "Approx.", - }), - price: approximateRatePrice, - unit: product.public.requestedPrice.currency, - } + label: intl.formatMessage({ + defaultMessage: "Approx.", + }), + price: approximateRatePrice, + unit: product.public.requestedPrice.currency, + } : undefined return ( @@ -154,12 +167,12 @@ export default function Campaign({ memberRate={ pricePerNightMember ? { - label: intl.formatMessage({ - defaultMessage: "Member price", - }), - price: pricePerNightMember.totalPrice, - unit: `${product.member!.localPrice.currency}/${night}`, - } + label: intl.formatMessage({ + defaultMessage: "Member price", + }), + price: pricePerNightMember.totalPrice, + unit: `${product.member!.localPrice.currency}/${night}`, + } : undefined } name={`rateCode-${roomNr}-${product.public.rateCode}`} @@ -173,15 +186,15 @@ export default function Campaign({ omnibusRate={ product.public.localPrice.omnibusPricePerNight ? { - label: intl - .formatMessage({ - defaultMessage: "Lowest price (last 30 days)", - }) - .toUpperCase(), - price: - product.public.localPrice.omnibusPricePerNight.toString(), - unit: product.public.localPrice.currency, - } + label: intl + .formatMessage({ + defaultMessage: "Lowest price (last 30 days)", + }) + .toUpperCase(), + price: + product.public.localPrice.omnibusPricePerNight.toString(), + unit: product.public.localPrice.currency, + } : undefined } rateTermDetails={rateTermDetails} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Code.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Code.tsx index b6261ad2a..b73cbea73 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Code.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Code.tsx @@ -6,6 +6,10 @@ import CodeRateCard from "@scandic-hotels/design-system/CodeRateCard" import { useRatesStore } from "@/stores/select-rate" +import { + sumPackages, + sumPackagesRequestedPrice, +} from "@/components/HotelReservation/utils" import { useRoomContext } from "@/contexts/SelectRate/Room" import useRateTitles from "@/hooks/booking/useRateTitles" @@ -28,11 +32,11 @@ export default function Code({ code, handleSelectRate, nights, - petRoomPackage, roomTypeCode, }: CodeProps) { const intl = useIntl() - const { roomNr, selectedFilter, selectedRate } = useRoomContext() + const { roomNr, selectedFilter, selectedPackages, selectedRate } = + useRoomContext() const bookingCode = useRatesStore((state) => state.booking.bookingCode) const rateTitles = useRateTitles() const night = intl @@ -74,11 +78,16 @@ export default function Code({ }, ] + const pkgsSum = sumPackages(selectedPackages) + const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages) + if ("corporateCheque" in product) { const { localPrice, rateCode } = product.corporateCheque let price = `${localPrice.numberOfCheques} CC` if (localPrice.additionalPricePerStay) { - price = `${price} + ${localPrice.additionalPricePerStay}` + price = `${price} + ${localPrice.additionalPricePerStay + pkgsSum.price}` + } else if (pkgsSum.price) { + price = `${price} + ${pkgsSum.price}` } const isSelected = isSelectedCorporateCheque( @@ -87,6 +96,8 @@ export default function Code({ roomTypeCode ) + const currency = localPrice.currency ?? pkgsSum.currency?.toString() ?? "" + return ( ({ - additionalPrice: - r.redemption.localPrice.additionalPricePerStay && - r.redemption.localPrice.currency - ? { - currency: r.redemption.localPrice.currency, - price: r.redemption.localPrice.additionalPricePerStay.toString(), + const rates = redemptions.map((r) => { + let additionalPrice + if (r.redemption.localPrice.additionalPricePerStay) { + additionalPrice = + r.redemption.localPrice.additionalPricePerStay + pkgsSum.price + } else if (pkgsSum.price) { + additionalPrice = pkgsSum.price + } + let additionalPriceCurrency + if (r.redemption.localPrice.currency) { + additionalPriceCurrency = r.redemption.localPrice.currency + } else if (pkgsSum.currency) { + additionalPriceCurrency = pkgsSum.currency + } + return { + additionalPrice: + additionalPrice && additionalPriceCurrency + ? { + currency: additionalPriceCurrency, + price: additionalPrice.toString(), } - : undefined, - currency: "PTS", - isDisabled: !r.redemption.hasEnoughPoints, - points: r.redemption.localPrice.pointsPerStay.toString(), - rateCode: r.redemption.rateCode, - })) + : undefined, + currency: "PTS", + isDisabled: !r.redemption.hasEnoughPoints, + points: r.redemption.localPrice.pointsPerStay.toString(), + rateCode: r.redemption.rateCode, + } + }) const notEnoughPoints = rates.every((rate) => rate.isDisabled) const firstRedemption = redemptions[0] diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Regular.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Regular.tsx index 4a8515a13..dd0f18e65 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Regular.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Regular.tsx @@ -6,6 +6,10 @@ import RegularRateCard from "@scandic-hotels/design-system/RegularRateCard" import { useRatesStore } from "@/stores/select-rate" +import { + sumPackages, + sumPackagesRequestedPrice, +} from "@/components/HotelReservation/utils" import { useRoomContext } from "@/contexts/SelectRate/Room" import useRateTitles from "@/hooks/booking/useRateTitles" @@ -34,13 +38,13 @@ interface RegularProps extends SharedRateCardProps { export default function Regular({ handleSelectRate, nights, - petRoomPackage, regular, roomTypeCode, }: RegularProps) { const intl = useIntl() const rateTitles = useRateTitles() - const { isMainRoom, roomNr, selectedFilter, selectedRate } = useRoomContext() + const { isMainRoom, roomNr, selectedFilter, selectedPackages, selectedRate } = + useRoomContext() const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn) if (selectedFilter === BookingCodeFilterEnum.Discounted) { @@ -52,6 +56,8 @@ export default function Regular({ defaultMessage: "night", }) .toUpperCase() + const pkgsSum = sumPackages(selectedPackages) + const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages) return regular.map((product) => { const { member, public: standard } = product @@ -81,19 +87,21 @@ export default function Regular({ const memberPricePerNight = member ? calculatePricePerNightPriceProduct( - member.localPrice.pricePerNight, - member.requestedPrice?.pricePerNight, - nights, - petRoomPackage - ) + member.localPrice.pricePerNight, + member.requestedPrice?.pricePerNight, + nights, + pkgsSum.price, + pkgsSumRequested.price + ) : undefined const standardPricePerNight = standard ? calculatePricePerNightPriceProduct( - standard.localPrice.pricePerNight, - standard.requestedPrice?.pricePerNight, - nights, - petRoomPackage - ) + standard.localPrice.pricePerNight, + standard.requestedPrice?.pricePerNight, + nights, + pkgsSum.price, + pkgsSumRequested.price + ) : undefined let approximateMemberRatePrice = null @@ -141,12 +149,12 @@ export default function Regular({ const approximateRate = approximatePrice && requestedCurrency ? { - label: intl.formatMessage({ - defaultMessage: "Approx.", - }), - price: approximatePrice, - unit: requestedCurrency, - } + label: intl.formatMessage({ + defaultMessage: "Approx.", + }), + price: approximatePrice, + unit: requestedCurrency, + } : undefined const isSelected = isSelectedPriceProduct( @@ -157,21 +165,21 @@ export default function Regular({ const rateTermDetails = product.rateDefinitionMember ? [ - { - title: product.rateDefinition.title, - terms: product.rateDefinition.generalTerms, - }, - { - title: product.rateDefinitionMember.title, - terms: product.rateDefinition.generalTerms, - }, - ] + { + title: product.rateDefinition.title, + terms: product.rateDefinition.generalTerms, + }, + { + title: product.rateDefinitionMember.title, + terms: product.rateDefinition.generalTerms, + }, + ] : [ - { - title: product.rateDefinition.title, - terms: product.rateDefinition.generalTerms, - }, - ] + { + title: product.rateDefinition.title, + terms: product.rateDefinition.generalTerms, + }, + ] return ( dt(state.booking.toDate).diff(state.booking.fromDate, "days") @@ -44,14 +42,9 @@ export default function Rates({ selectRate({ features, product, roomType, roomTypeCode }) } - const petRoomPackageSelected = selectedPackages.find( - (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM - ) - const sharedProps = { handleSelectRate, nights, - petRoomPackage: petRoomPackageSelected, roomTypeCode, } const showAllRates = selectedFilter === BookingCodeFilterEnum.All diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/totalPricePerNight.ts b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/totalPricePerNight.ts index 2de166c38..f6b77ebac 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/totalPricePerNight.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/totalPricePerNight.ts @@ -1,20 +1,19 @@ -import type { Package } from "@/types/requests/packages" - export function calculatePricePerNightPriceProduct( pricePerNight: number, requestedPricePerNight: number | undefined, nights: number, - petRoomPackage?: Package + packagesSumLocal: number, + packagesSumRequested: number ) { - const totalPrice = petRoomPackage?.localPrice - ? Math.floor(pricePerNight + petRoomPackage.localPrice.price / nights) + const totalPrice = packagesSumLocal + ? Math.floor(pricePerNight + packagesSumLocal / nights) : Math.floor(pricePerNight) let totalRequestedPrice = undefined if (requestedPricePerNight) { - if (petRoomPackage?.requestedPrice) { + if (packagesSumRequested) { totalRequestedPrice = Math.floor( - requestedPricePerNight + petRoomPackage.requestedPrice.price / nights + requestedPricePerNight + packagesSumRequested / nights ) } else { totalRequestedPrice = Math.floor(requestedPricePerNight) diff --git a/apps/scandic-web/components/HotelReservation/utils.tsx b/apps/scandic-web/components/HotelReservation/utils.tsx index 6b8b7ac84..a25c7b6f5 100644 --- a/apps/scandic-web/components/HotelReservation/utils.tsx +++ b/apps/scandic-web/components/HotelReservation/utils.tsx @@ -11,6 +11,7 @@ import { type RoomPackageCodes, } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Packages } from "@/types/requests/packages" interface IconForFeatureCodeProps { featureCode: RoomPackageCodes @@ -53,3 +54,41 @@ export function generateChildrenString(children: Child[]): string { }) .join(",")}]` } + +export function sumPackages(packages: Packages | null) { + if (!packages || !packages.length) { + return { + currency: undefined, + price: 0, + } + } + return packages.reduce( + (total, pkg) => { + total.price = total.price + pkg.localPrice.totalPrice + return total + }, + { + currency: packages[0].localPrice.currency, + price: 0, + } + ) +} + +export function sumPackagesRequestedPrice(packages: Packages | null) { + if (!packages || !packages.length) { + return { + currency: undefined, + price: 0, + } + } + return packages.reduce( + (total, pkg) => { + total.price = total.price + pkg.requestedPrice.totalPrice + return total + }, + { + currency: packages[0].requestedPrice.currency, + price: 0, + } + ) +} diff --git a/apps/scandic-web/stores/enter-details/helpers.ts b/apps/scandic-web/stores/enter-details/helpers.ts index a1ca92599..618398f28 100644 --- a/apps/scandic-web/stores/enter-details/helpers.ts +++ b/apps/scandic-web/stores/enter-details/helpers.ts @@ -1,5 +1,10 @@ import isEqual from "fast-deep-equal" +import { + sumPackages, + sumPackagesRequestedPrice, +} from "@/components/HotelReservation/utils" + import { detailsStorageName } from "." import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details" @@ -423,6 +428,11 @@ export function calcTotalPrice( return acc } + const isSpecialRate = + "corporateCheque" in room.roomRate || + "redemption" in room.roomRate || + "voucher" in room.roomRate + const breakfastRequestedPrice = room.breakfast ? (room.breakfast.requestedPrice?.price ?? 0) : 0 @@ -430,21 +440,11 @@ export function calcTotalPrice( ? (room.breakfast.localPrice?.price ?? 0) : 0 - const roomFeaturesTotal = (room.roomFeatures || []).reduce( - (total, pkg) => { - if (pkg.requestedPrice.totalPrice) { - total.requestedPrice = add( - total.requestedPrice, - pkg.requestedPrice.totalPrice - ) - } - total.local = add(total.local, pkg.localPrice.totalPrice) - - return total - }, - { local: 0, requestedPrice: 0 } - ) + const pkgsSum = sumPackages(room.roomFeatures) + const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures) + const breakfastRequestedTotalPrice = + breakfastRequestedPrice * room.adults * nights if (roomPrice.perStay.requested) { if (!acc.requested) { acc.requested = { @@ -453,61 +453,84 @@ export function calcTotalPrice( } } - acc.requested.price = add( - acc.requested.price, - roomPrice.perStay.requested.price, - breakfastRequestedPrice * room.adults * nights - ) - - // TODO: Come back and verify on CC, PTS, Voucher - if (roomPrice.perStay.requested.additionalPrice) { - acc.requested.additionalPrice = add( - acc.requested.additionalPrice, - roomPrice.perStay.requested.additionalPrice + if (isSpecialRate) { + acc.requested.price = add( + acc.requested.price, + roomPrice.perStay.requested.price ) - } - if ( - roomPrice.perStay.requested.additionalPriceCurrency && - !acc.requested.additionalPriceCurrency - ) { - acc.requested.additionalPriceCurrency = - roomPrice.perStay.requested.additionalPriceCurrency + acc.requested.additionalPrice = add( + breakfastRequestedTotalPrice, + pkgsSumRequested.price + ) + + if (!acc.requested.additionalPriceCurrency) { + if (roomPrice.perStay.requested.additionalPriceCurrency) { + acc.requested.additionalPriceCurrency = + roomPrice.perStay.requested.additionalPriceCurrency + } else if (room.breakfast) { + acc.requested.additionalPriceCurrency = + room.breakfast.localPrice.currency + } else if (pkgsSumRequested.currency) { + acc.requested.additionalPriceCurrency = pkgsSumRequested.currency + } + } + } else { + acc.requested.price = add( + acc.requested.price, + roomPrice.perStay.requested.price, + breakfastRequestedTotalPrice, + pkgsSumRequested.price + ) } } const breakfastLocalTotalPrice = breakfastLocalPrice * room.adults * nights - acc.local.price = add( - acc.local.price, - roomPrice.perStay.local.price, - breakfastLocalTotalPrice, - roomFeaturesTotal.local - ) + if (isSpecialRate) { + acc.local.price = add(acc.local.price, roomPrice.perStay.local.price) - if (roomPrice.perStay.local.regularPrice) { - acc.local.regularPrice = add( - acc.local.regularPrice, - roomPrice.perStay.local.regularPrice, + if ( + roomPrice.perStay.local.additionalPrice || + breakfastLocalTotalPrice || + pkgsSum.price + ) { + acc.local.additionalPrice = add( + acc.local.additionalPrice, + roomPrice.perStay.local.additionalPrice, + breakfastLocalTotalPrice, + pkgsSum.price + ) + } + + if (!acc.local.additionalPriceCurrency) { + if (roomPrice.perStay.local.additionalPriceCurrency) { + acc.local.additionalPriceCurrency = + roomPrice.perStay.local.additionalPriceCurrency + } else if (room.breakfast) { + acc.local.additionalPriceCurrency = + room.breakfast.localPrice.currency + } else if (pkgsSum.currency) { + acc.local.additionalPriceCurrency = pkgsSum.currency + } + } + } else { + acc.local.price = add( + acc.local.price, + roomPrice.perStay.local.price, breakfastLocalTotalPrice, - roomFeaturesTotal.local + pkgsSum.price ) - } - if (roomPrice.perStay.local.additionalPrice) { - acc.local.additionalPrice = add( - acc.local.additionalPrice, - roomPrice.perStay.local.additionalPrice - ) - } - - if ( - roomPrice.perStay.local.additionalPriceCurrency && - !acc.local.additionalPriceCurrency - ) { - acc.local.additionalPriceCurrency = - roomPrice.perStay.local.additionalPriceCurrency + if (roomPrice.perStay.local.regularPrice) { + acc.local.regularPrice = add( + acc.local.regularPrice, + roomPrice.perStay.local.regularPrice, + breakfastLocalTotalPrice, + pkgsSum.price + ) + } } return acc diff --git a/apps/scandic-web/stores/enter-details/index.ts b/apps/scandic-web/stores/enter-details/index.ts index 05145d39a..648643326 100644 --- a/apps/scandic-web/stores/enter-details/index.ts +++ b/apps/scandic-web/stores/enter-details/index.ts @@ -6,6 +6,10 @@ import { create, useStore } from "zustand" import { REDEMPTION } from "@/constants/booking" import { dt } from "@/lib/dt" +import { + sumPackages, + sumPackagesRequestedPrice, +} from "@/components/HotelReservation/utils" import { DetailsContext } from "@/contexts/Details" import { @@ -64,6 +68,7 @@ export function createDetailsStore( let initialTotalPrice: Price const roomOneRoomRate = initialState.rooms[0].roomRate + const initialRoomRates = initialState.rooms.map((r) => r.roomRate) if (isRedemption && "redemption" in roomOneRoomRate) { initialTotalPrice = { local: { @@ -80,34 +85,56 @@ export function createDetailsStore( roomOneRoomRate.redemption.localPrice.additionalPricePerStay } } else if (isVoucher) { - initialTotalPrice = calculateVoucherPrice( - initialState.rooms.map((r) => r.roomRate) - ) + initialTotalPrice = calculateVoucherPrice(initialRoomRates) } else if (isCorpChq) { - initialTotalPrice = calculateCorporateChequePrice( - initialState.rooms.map((r) => r.roomRate) - ) + initialTotalPrice = calculateCorporateChequePrice(initialRoomRates) } else { - initialTotalPrice = getTotalPrice( - initialState.rooms.map((r) => r.roomRate), - isMember - ) + initialTotalPrice = getTotalPrice(initialRoomRates, isMember) } initialState.rooms.forEach((room) => { if (room.roomFeatures) { - room.roomFeatures.forEach((pkg) => { + const pkgsSum = sumPackages(room.roomFeatures) + const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures) + + if ("corporateCheque" in room.roomRate || "redemption" in room.roomRate) { + initialTotalPrice.local.additionalPrice = add( + initialTotalPrice.local.additionalPrice, + pkgsSum.price + ) + if ( + !initialTotalPrice.local.additionalPriceCurrency && + pkgsSum.currency + ) { + initialTotalPrice.local.additionalPriceCurrency = pkgsSum.currency + } + + if (initialTotalPrice.requested) { + initialTotalPrice.requested.additionalPrice = add( + initialTotalPrice.requested.additionalPrice, + pkgsSumRequested.price + ) + if ( + !initialTotalPrice.requested.additionalPriceCurrency && + pkgsSumRequested.currency + ) { + initialTotalPrice.requested.additionalPriceCurrency = + pkgsSumRequested.currency + } + } + } else if ("public" in room.roomRate) { if (initialTotalPrice.requested) { initialTotalPrice.requested.price = add( initialTotalPrice.requested.price, - pkg.requestedPrice.totalPrice + pkgsSumRequested.price ) } + initialTotalPrice.local.price = add( initialTotalPrice.local.price, - pkg.localPrice.totalPrice + pkgsSum.price ) - }) + } } }) diff --git a/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts b/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts index 1734b913a..533018a1b 100644 --- a/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts +++ b/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts @@ -11,10 +11,12 @@ interface RoomPrice { } interface MyStayTotalPriceState { - rooms: RoomPrice[] - totalPrice: number | null currencyCode: CurrencyEnum + rooms: RoomPrice[] + totalCheques: number totalPoints: number + totalPrice: number | null + totalVouchers: number actions: { // Add a single room price addRoomPrice: (room: RoomPrice) => void diff --git a/apps/scandic-web/stores/select-rate/index.ts b/apps/scandic-web/stores/select-rate/index.ts index b4de379fc..6ac82a208 100644 --- a/apps/scandic-web/stores/select-rate/index.ts +++ b/apps/scandic-web/stores/select-rate/index.ts @@ -89,12 +89,14 @@ export function createRatesStore({ room.counterRateCode ) if (product) { + const roomPackages = roomsPackages[idx].filter((pkg) => + room.packages?.includes(pkg.code) + ) + rateSummary[idx] = { features: selectedRoom.features, product, - packages: roomsPackages[idx].filter((pkg) => - room.packages?.includes(pkg.code) - ), + packages: roomPackages, rate: product.rate, roomType: selectedRoom.roomType, roomTypeCode: selectedRoom.roomTypeCode, diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/myStay.ts b/apps/scandic-web/types/components/hotelReservation/myStay/myStay.ts index 69ea88cd5..09470de0e 100644 --- a/apps/scandic-web/types/components/hotelReservation/myStay/myStay.ts +++ b/apps/scandic-web/types/components/hotelReservation/myStay/myStay.ts @@ -3,4 +3,11 @@ export enum MODAL_STEPS { CONFIRMATION = 2, } -export type PriceType = "points" | "money" | "voucher" | "cheque" +export enum PriceTypeEnum { + cheque = "cheque", + money = "money", + points = "points", + voucher = "voucher", +} + +export type PriceType = keyof typeof PriceTypeEnum diff --git a/apps/scandic-web/types/components/hotelReservation/price.ts b/apps/scandic-web/types/components/hotelReservation/price.ts index bcdc91d15..08a12bef6 100644 --- a/apps/scandic-web/types/components/hotelReservation/price.ts +++ b/apps/scandic-web/types/components/hotelReservation/price.ts @@ -3,11 +3,11 @@ import { z } from "zod" import { CurrencyEnum } from "@/types/enums/currency" interface TPrice { + additionalPrice?: number + additionalPriceCurrency?: CurrencyEnum currency: CurrencyEnum price: number regularPrice?: number - additionalPrice?: number - additionalPriceCurrency?: string } export interface Price { diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/rates.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/rates.ts index 63ebfe78d..b6ac2c147 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectRate/rates.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectRate/rates.ts @@ -1,4 +1,3 @@ -import type { Package } from "@/types/requests/packages" import type { Product, RoomConfiguration, @@ -12,5 +11,4 @@ export interface SharedRateCardProps extends Pick { handleSelectRate: (product: Product) => void nights: number - petRoomPackage: Package | undefined } diff --git a/apps/scandic-web/types/providers/booking-confirmation.ts b/apps/scandic-web/types/providers/booking-confirmation.ts index 12be63a61..5a75f52d7 100644 --- a/apps/scandic-web/types/providers/booking-confirmation.ts +++ b/apps/scandic-web/types/providers/booking-confirmation.ts @@ -1,9 +1,10 @@ +import type { CurrencyEnum } from "../enums/currency" import type { Room } from "../stores/booking-confirmation" export interface BookingConfirmationProviderProps extends React.PropsWithChildren { bookingCode: string | null - currencyCode: string + currencyCode: CurrencyEnum fromDate: Date rooms: (Room | null)[] toDate: Date diff --git a/apps/scandic-web/types/stores/booking-confirmation.ts b/apps/scandic-web/types/stores/booking-confirmation.ts index 91bd8f898..314882049 100644 --- a/apps/scandic-web/types/stores/booking-confirmation.ts +++ b/apps/scandic-web/types/stores/booking-confirmation.ts @@ -1,4 +1,5 @@ import type { ChildBedTypeEnum } from "@/constants/booking" +import type { CurrencyEnum } from "../enums/currency" import type { BookingConfirmation, PackageSchema, @@ -19,7 +20,7 @@ export interface Room { childBedPreferences: ChildBedPreference[] childrenAges?: number[] confirmationNumber: string - currencyCode: string + currencyCode: CurrencyEnum fromDate: Date name: string packages: BookingConfirmation["booking"]["packages"] @@ -41,7 +42,7 @@ export interface InitialState { fromDate: Date rooms: (Room | null)[] toDate: Date - currencyCode: string + currencyCode: CurrencyEnum vat: number isVatCurrency: boolean formattedTotalCost: string