Files
web/apps/scandic-web/components/SidePeeks/BookedRoomSidePeekContent/index.tsx
Joakim Jäderberg bf6ed7778e Merged in feat/syncDefaultMessage (pull request #3022)
Sync defaultMessage from lokalise

* Enhance translation sync functionality and tests

- Added logging for found component files during sync.
- Introduced tests for handling complex components with replacements.
- Updated regex in syncIntlFormatMessage to support optional second arguments.
- Removed unused test files.

* feat(syncDefaultMessage): add script for syncing default message with lokalise

* feat(syncDefaultMessage): add script for syncing default message with lokalise


Approved-by: Matilda Landström
2025-10-30 08:38:50 +00:00

476 lines
15 KiB
TypeScript

import { useIntl } from "react-intl"
import { getRoomFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription"
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Accordion from "@scandic-hotels/design-system/Accordion"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import IconChip from "@scandic-hotels/design-system/IconChip"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
import useLang from "@/hooks/useLang"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import RoomDetails from "./RoomDetails"
import styles from "./bookedRoomSidePeekContent.module.css"
import type { BedTypeSchema } from "@scandic-hotels/booking-flow/stores/enter-details/types"
import type { BreakfastPackage } from "@scandic-hotels/trpc/routers/hotels/schemas/packages"
import type { BookingConfirmationSchema } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type { Room as HotelRoom } from "@scandic-hotels/trpc/types/hotel"
import type { Packages } from "@scandic-hotels/trpc/types/packages"
import type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { SafeUser } from "@/types/user"
type PartialHotelRoom = Pick<
HotelRoom,
"descriptions" | "images" | "name" | "roomFacilities" | "roomTypes"
>
type Room = Pick<
BookingConfirmationSchema,
| "adults"
| "bookingCode"
| "cancellationNumber"
| "checkInDate"
| "cheques"
| "confirmationNumber"
| "refId"
| "currencyCode"
| "guest"
| "rateDefinition"
| "totalPoints"
| "totalPrice"
| "vouchers"
> & {
bedType: BedTypeSchema
breakfast: Omit<BreakfastPackage, "requestedPrice"> | false | undefined
childrenInRoom: Child[]
isCancelled: boolean
packages: Packages | null
priceType: PriceTypeEnum
room: PartialHotelRoom | null
roomName: string
roomNumber: number
terms: string | null
}
interface BookedRoomSidepeekContentProps {
room: Room
user: SafeUser
}
export default function BookedRoomSidePeekContent({
room,
user,
}: BookedRoomSidepeekContentProps) {
const intl = useIntl()
const lang = useLang()
const {
adults,
bedType,
bookingCode,
breakfast,
cancellationNumber,
checkInDate,
cheques,
childrenInRoom,
confirmationNumber,
refId,
currencyCode,
guest,
isCancelled,
roomName,
packages,
priceType,
rateDefinition,
room: hotelRoom,
roomNumber,
totalPoints,
terms,
totalPrice,
vouchers,
} = room
let totalRoomPrice = totalPrice
// API returns negative values for totalPrice
// on voucher bookings (╯°□°)╯︵ ┻━┻
if (vouchers && totalRoomPrice < 0) {
const pkgsSum = sumPackages(packages)
totalRoomPrice = pkgsSum.price
}
const fromDate = dt(checkInDate).locale(lang)
const galleryImages = hotelRoom
? mapApiImagesToGalleryImages(hotelRoom.images)
: null
const adultsMsg = intl.formatMessage(
{
id: "booking.numberOfAdults",
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
},
{
adults: adults,
}
)
const childrenMsg = intl.formatMessage(
{
id: "booking.numberOfChildren",
defaultMessage: "{children, plural, one {# child} other {# children}}",
},
{
children: childrenInRoom.length,
}
)
const adultsOnlyMsg = adultsMsg
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
const formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode)
let breakfastPrice = intl.formatMessage({
id: "common.noBreakfast",
defaultMessage: "No breakfast",
})
if (rateDefinition.breakfastIncluded) {
breakfastPrice = intl.formatMessage({
id: "common.included",
defaultMessage: "Included",
})
} else if (breakfast) {
breakfastPrice = formatPrice(
intl,
breakfast.localPrice.totalPrice,
breakfast.localPrice.currency
)
}
const hotelRoomName = hotelRoom?.name || roomName
return (
<div className={styles.wrapper}>
<div className={styles.roomHeader}>
{isCancelled ? (
<IconChip
color={"red"}
icon={
<MaterialIcon
icon="cancel"
size={20}
color="Icon/Feedback/Error"
/>
}
>
<Typography variant="Body/Supporting text (caption)/smBold">
<span>
{intl.formatMessage({
id: "common.cancelled",
defaultMessage: "Cancelled",
})}
</span>
</Typography>
</IconChip>
) : (
<div className={styles.chip}>
<Typography variant="Tag/sm">
<span>
{intl.formatMessage(
{
id: "booking.roomIndex",
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: roomNumber }
)}
</span>
</Typography>
</div>
)}
<div className={styles.reference}>
<Typography variant="Body/Supporting text (caption)/smBold">
{isCancelled ? (
<span>
{intl.formatMessage({
id: "booking.cancellationNo",
defaultMessage: "Cancellation no",
})}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{":"}
</span>
) : (
<span>
{intl.formatMessage({
id: "common.bookingNumber",
defaultMessage: "Booking number",
})}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{":"}
</span>
)}
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
{isCancelled ? (
<span className={styles.cancellationNumber}>
{cancellationNumber}
</span>
) : (
<span>{confirmationNumber}</span>
)}
</Typography>
</div>
</div>
<div className={styles.mainContent}>
<div className={styles.imageContainer}>
{galleryImages ? (
<ImageGallery
height={280}
images={galleryImages}
title={hotelRoomName}
/>
) : null}
</div>
<div className={styles.roomDetails}>
<div className={styles.row}>
<span className={styles.rowTitle}>
<MaterialIcon icon="person" color="Icon/Default" size={20} />
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
id: "booking.guests",
defaultMessage: "Guests",
})}
</p>
</Typography>
</span>
<div className={styles.rowContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{childrenInRoom.length > 0
? adultsAndChildrenMsg
: adultsOnlyMsg}
</p>
</Typography>
</div>
</div>
<div className={styles.row}>
<span className={styles.rowTitle}>
<MaterialIcon icon="contract" color="Icon/Default" size={20} />
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
id: "booking.terms",
defaultMessage: "Terms",
})}
</p>
</Typography>
</span>
<div className={styles.rowContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{terms}</p>
</Typography>
</div>
</div>
{hasModifiableRate(rateDefinition.cancellationRule) && (
<div className={styles.row}>
<span className={styles.rowTitle}>
<MaterialIcon icon="refresh" color="Icon/Default" size={20} />
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
id: "myStay.modifyBy",
defaultMessage: "Modify by",
})}
</p>
</Typography>
</span>
<div className={styles.rowContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage(
{
id: "common.untilWithTimeAndDate",
defaultMessage: "Until {time}, {date}",
},
{
time: "18:00",
date: fromDate.format(changeOrCancelDateFormat[lang]),
}
)}
</p>
</Typography>
</div>
</div>
)}
<div className={styles.row}>
<span className={styles.rowTitle}>
<MaterialIcon icon="coffee" color="Icon/Default" size={20} />
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
id: "common.breakfast",
defaultMessage: "Breakfast",
})}
</p>
</Typography>
</span>
<div className={styles.rowContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{breakfastPrice}</p>
</Typography>
</div>
</div>
{packages?.some((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
) && (
<div className={styles.row}>
<span className={styles.rowTitle}>
<MaterialIcon
icon="meeting_room"
color="Icon/Default"
size={20}
/>
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
id: "booking.roomClassification",
defaultMessage: "Room classification",
})}
</p>
</Typography>
</span>
<div className={styles.rowContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{packages
?.filter((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
)
.map((item) =>
getRoomFeatureDescription(
item.code,
item.description,
intl
)
)
.join(", ")}
</p>
</Typography>
</div>
</div>
)}
<div className={styles.row}>
<span className={styles.rowTitle}>
<MaterialIcon icon="bed" color="Icon/Default" size={20} />
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
id: "booking.bedPreference",
defaultMessage: "Bed preference",
})}
</p>
</Typography>
</span>
<div className={styles.rowContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{bedType?.description}</p>
</Typography>
</div>
</div>
</div>
<div className={styles.bookingInformation}>
<div className={styles.priceDetails}>
<div className={styles.price}>
<Typography variant="Body/Lead text">
<p>
{intl.formatMessage({
id: "booking.roomTotal",
defaultMessage: "Room total",
})}
</p>
</Typography>
<PriceType
cheques={cheques}
formattedTotalPrice={formattedTotalPrice}
isCancelled={isCancelled}
priceType={priceType}
currencyCode={currencyCode}
rateDefinition={rateDefinition}
totalPoints={totalPoints}
totalPrice={totalRoomPrice}
vouchers={vouchers}
/>
</div>
</div>
</div>
{bookingCode && (
<Typography variant="Body/Supporting text (caption)/smBold">
<IconChip
color="blue"
icon={<DiscountIcon color="Icon/Feedback/Information" />}
>
{intl.formatMessage(
{
id: "booking.bookingCodeWithValue",
defaultMessage: "<strong>Booking code</strong>: {value}",
},
{
value: bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)}
</IconChip>
</Typography>
)}
<GuestDetails
refId={refId}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
{hotelRoom ? (
<Accordion type="sidepeek">
<AccordionItem
title={intl.formatMessage({
id: "sidepeek.roomDetails",
defaultMessage: "Room details",
})}
type="sidepeek"
>
<RoomDetails
roomDescription={hotelRoom.descriptions.medium}
roomFacilities={hotelRoom.roomFacilities}
roomTypes={hotelRoom.roomTypes}
/>
</AccordionItem>
</Accordion>
) : null}
</div>
)
}