Merged in feat/sw-1681-add-breakfast (pull request #1635)

Feat/sw-1681 add breakfast
This implements the add breakfast flow

Approved-by: Pontus Dreij
This commit is contained in:
Niclas Edenvin
2025-03-27 12:40:54 +00:00
parent bed490d79a
commit 8eec465afa
15 changed files with 508 additions and 78 deletions

View File

@@ -9,22 +9,24 @@ import PriceRow from "./PriceRow"
import styles from "./priceSummary.module.css"
import type { Ancillary } from "@/types/components/myPages/myStay/ancillaries"
interface PriceSummaryProps {
totalPrice: number | null
totalPoints: number | null
quantityWithPoints: number
quantityWithCard: number
selectedAncillary: NonNullable<Ancillary["ancillaryContent"][number]>
items: {
title: string
totalPrice: number
currency: string
points?: number
quantityWithCard?: number
quantityWithPoints?: number
}[]
}
export default function PriceSummary({
totalPrice,
totalPoints,
quantityWithPoints,
quantityWithCard,
selectedAncillary,
items,
}: PriceSummaryProps) {
const intl = useIntl()
const hasTotalPoints = typeof totalPoints === "number"
@@ -37,27 +39,28 @@ export default function PriceSummary({
</Typography>
<Divider color="subtle" />
{hasTotalPrice && (
<PriceRow
title={selectedAncillary.title}
quantity={quantityWithCard}
label={intl.formatMessage({ id: "Price including VAT" })}
value={formatPrice(
intl,
selectedAncillary.price.totalPrice,
selectedAncillary.price.currency
{items.map((item) => (
<>
{item.quantityWithCard && (
<PriceRow
title={item.title}
quantity={item.quantityWithCard}
label={intl.formatMessage({ id: "Price including VAT" })}
value={formatPrice(intl, item.totalPrice, item.currency)}
/>
)}
/>
)}
{hasTotalPoints && (
<PriceRow
title={selectedAncillary.title}
quantity={quantityWithPoints}
label={intl.formatMessage({ id: "Points" })}
value={`${selectedAncillary.points} ${intl.formatMessage({ id: "points" })}`}
/>
)}
<Divider color="subtle" />
{item.quantityWithPoints && (
<PriceRow
title={item.title}
quantity={item.quantityWithPoints}
label={intl.formatMessage({ id: "Points" })}
value={`${item.points} ${intl.formatMessage({ id: "points" })}`}
/>
)}
<Divider color="subtle" />
</>
))}
<div className={styles.column}>
<Typography variant="Body/Paragraph/mdBold">
<p>
@@ -71,11 +74,7 @@ export default function PriceSummary({
<Typography variant="Body/Paragraph/mdBold">
<p>
{hasTotalPrice &&
formatPrice(
intl,
totalPrice,
selectedAncillary.price.currency
)}
formatPrice(intl, totalPrice, items[0]?.currency)}
{hasTotalPoints && hasTotalPrice && " + "}
{hasTotalPoints &&
`${totalPoints} ${intl.formatMessage({ id: "points" })}`}

View File

@@ -5,6 +5,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import {
AncillaryStepEnum,
type BreakfastData,
useAddAncillaryStore,
} from "@/stores/my-stay/add-ancillary-flow"
@@ -15,6 +16,8 @@ import PriceSummary from "./PriceSummary"
import styles from "./priceDetails.module.css"
import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries"
interface PriceDetailsProps {
isPriceDetailsOpen: boolean
}
@@ -24,18 +27,28 @@ export default function PriceDetails({
}: PriceDetailsProps) {
const intl = useIntl()
const { currentStep, selectedAncillary } = useAddAncillaryStore((state) => ({
currentStep: state.currentStep,
selectedAncillary: state.selectedAncillary,
}))
const { currentStep, selectedAncillary, isBreakfast, breakfastData } =
useAddAncillaryStore((state) => ({
currentStep: state.currentStep,
selectedAncillary: state.selectedAncillary,
isBreakfast: state.isBreakfast,
breakfastData: state.breakfastData,
}))
const quantityWithPoints = useWatch({ name: "quantityWithPoints" })
const quantityWithCard = useWatch({ name: "quantityWithCard" })
if (!selectedAncillary || currentStep !== AncillaryStepEnum.confirmation) {
return null
}
const totalPrice =
quantityWithCard && selectedAncillary
if (isBreakfast && !breakfastData) {
return null
}
const totalPrice = isBreakfast
? breakfastData!.priceAdult * breakfastData!.nrOfAdults +
breakfastData!.priceChild * breakfastData!.nrOfPayingChildren
: quantityWithCard && selectedAncillary
? selectedAncillary.price.totalPrice * quantityWithCard
: null
@@ -47,6 +60,57 @@ export default function PriceDetails({
const hasTotalPoints = typeof totalPoints === "number"
const hasTotalPrice = typeof totalPrice === "number"
function getBreakfastItems(
selectedAncillary: SelectedAncillary,
breakfastData: BreakfastData | null
) {
if (!breakfastData) {
return []
}
const items = [
{
title: `${selectedAncillary.title} / ${intl.formatMessage({ id: "Adult" })}`,
totalPrice: breakfastData.priceAdult,
currency: breakfastData.currency,
quantityWithCard: breakfastData.nrOfAdults,
},
]
if (breakfastData.nrOfPayingChildren > 0) {
items.push({
title: `${selectedAncillary.title} / ${intl.formatMessage({ id: "Children" })} 4-12`,
totalPrice: breakfastData.priceChild,
currency: breakfastData.currency,
quantityWithCard: breakfastData.nrOfPayingChildren,
})
}
if (breakfastData.nrOfFreeChildren > 0) {
items.push({
title: `${selectedAncillary.title} / ${intl.formatMessage({ id: "Children under {age}" }, { age: 4 })}`,
totalPrice: 0,
currency: breakfastData.currency,
quantityWithCard: breakfastData.nrOfFreeChildren,
})
}
return items
}
const items = isBreakfast
? getBreakfastItems(selectedAncillary, breakfastData)
: [
{
title: selectedAncillary.title,
totalPrice: selectedAncillary.price.totalPrice,
currency: selectedAncillary.price.currency,
points: selectedAncillary.points,
quantityWithCard,
quantityWithPoints,
},
]
return (
<>
<div className={styles.totalPrice}>
@@ -94,9 +158,7 @@ export default function PriceDetails({
<PriceSummary
totalPrice={totalPrice}
totalPoints={totalPoints}
quantityWithCard={quantityWithCard ?? 0}
quantityWithPoints={quantityWithPoints ?? 0}
selectedAncillary={selectedAncillary}
items={items}
/>
)}
</>

View File

@@ -1,3 +1,4 @@
import { useEffect } from "react"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
@@ -6,23 +7,30 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
import Alert from "@/components/TempDesignSystem/Alert"
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
import Select from "@/components/TempDesignSystem/Form/Select"
import Body from "@/components/TempDesignSystem/Text/Body"
import styles from "./selectQuantityStep.module.css"
import type { SelectQuantityStepProps } from "@/types/components/myPages/myStay/ancillaries"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
const intl = useIntl()
const selectedAncillary = useAddAncillaryStore(
(state) => state.selectedAncillary
)
const { isBreakfast, selectedAncillary } = useAddAncillaryStore((state) => ({
isBreakfast: state.isBreakfast,
selectedAncillary: state.selectedAncillary,
}))
const {
formState: { errors },
} = useFormContext()
if (isBreakfast) {
return <BreakfastInfo />
}
const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({
label: `${i}`,
value: i,
@@ -47,13 +55,6 @@ export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
? intl.formatMessage({ id: "Insufficient points" })
: intl.formatMessage({ id: "Select quantity" })
// TODO: Remove this when add breakfast is implemented
if (
selectedAncillary?.id === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
) {
return "Breakfast TBI"
}
return (
<div className={styles.selectContainer}>
{selectedAncillary?.points && user && (
@@ -96,3 +97,77 @@ export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
</div>
)
}
function BreakfastInfo() {
const intl = useIntl()
const breakfastData = useAddAncillaryStore((state) => state.breakfastData)
const { setValue } = useFormContext()
setValue("quantityWithCard", 1)
if (!breakfastData) {
return intl.formatMessage({
id: "Can not show breakfast prices.",
})
}
return (
<div className={styles.breakfastContainer}>
<Alert
type={AlertTypeEnum.Info}
text={intl.formatMessage({
id: "Breakfast can only be added for the entire duration of the stayand for all guests.",
})}
/>
{(breakfastData.nrOfPayingChildren > 0 ||
breakfastData.nrOfFreeChildren > 0) && (
<dl className={styles.breakfastPrices}>
<div className={styles.breakfastPriceBox}>
<MaterialIcon icon="check_circle" className={styles.icon} />
<div>
<dt>
{`${breakfastData.nrOfAdults} X ${intl.formatMessage({ id: "Adults" })}`}
</dt>
<dd>
<Body>
{`${breakfastData.priceAdult} ${breakfastData.currency}`}
</Body>
</dd>
</div>
</div>
{breakfastData.nrOfPayingChildren > 0 && (
<div className={styles.breakfastPriceBox}>
<MaterialIcon icon="check_circle" className={styles.icon} />
<div>
<dt>
{`${breakfastData.nrOfPayingChildren} X ${intl.formatMessage({ id: "ages" })} 4-12`}
</dt>
<dd>
<Body>
{`${breakfastData.priceChild} ${breakfastData.currency}`}
</Body>
</dd>
</div>
</div>
)}
{breakfastData.nrOfFreeChildren > 0 && (
<div className={`${styles.breakfastPriceBox} ${styles.free}`}>
<MaterialIcon icon="check_circle" className={styles.icon} />
<div>
<dt>
{`${breakfastData.nrOfFreeChildren} X ${intl.formatMessage({ id: "under" })} 4`}
</dt>
<dd>
<Body>{intl.formatMessage({ id: "Free" })}</Body>
</dd>
</div>
</div>
)}
</dl>
)}
</div>
)
}

View File

@@ -28,3 +28,52 @@
gap: var(--Space-x15);
align-items: center;
}
.breakfastContainer {
display: flex;
flex-direction: column;
gap: var(--Space-x2);
}
.breakfastPrices {
display: flex;
align-items: center;
gap: var(--Spacing-x1);
align-self: stretch;
}
.breakfastPriceBox {
display: flex;
padding: var(--Space-15);
flex: 1 0 0;
border-radius: var(--Corner-radius-md);
border: 1px solid var(--Base-Border-Subtle);
background: var(--Surface-Feedback-Information);
align-items: center;
gap: var(--Spacing-x1);
align-self: stretch;
}
.free {
background: var(--Surface-Feedback-Succes);
}
.breakfastPriceBox dt {
color: var(--Text-Secondary);
font-family: var(--Tag-Font-family);
font-size: var(--Tag-Size);
font-style: normal;
font-weight: var(--Tag-Font-weight);
text-transform: uppercase;
}
.icon {
display: none;
}
@media screen and (min-width: 768px) {
.icon {
display: block;
}
}

View File

@@ -97,3 +97,24 @@
width: 833px;
}
}
.breakfastPriceList {
display: flex;
flex-direction: column;
}
.divider {
display: none;
height: var(--Space-x4);
}
@media screen and (min-width: 768px) {
.breakfastPriceList {
flex-direction: row;
gap: var(--Space-x2);
}
.divider {
display: block;
}
}

View File

@@ -14,6 +14,7 @@ import { env } from "@/env/client"
import { trpc } from "@/lib/trpc/client"
import {
AncillaryStepEnum,
type BreakfastData,
useAddAncillaryStore,
} from "@/stores/my-stay/add-ancillary-flow"
@@ -39,22 +40,34 @@ import Steps from "./Steps"
import styles from "./addAncillaryFlowModal.module.css"
import type { AddAncillaryFlowModalProps } from "@/types/components/myPages/myStay/ancillaries"
import type {
AddAncillaryFlowModalProps,
Packages,
} from "@/types/components/myPages/myStay/ancillaries"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function AddAncillaryFlowModal({
booking,
packages,
user,
savedCreditCards,
refId,
}: AddAncillaryFlowModalProps) {
const { currentStep, selectedAncillary, closeModal } = useAddAncillaryStore(
(state) => ({
currentStep: state.currentStep,
selectedAncillary: state.selectedAncillary,
closeModal: state.closeModal,
})
)
const {
currentStep,
selectedAncillary,
closeModal,
breakfastData,
setBreakfastData,
isBreakfast,
} = useAddAncillaryStore((state) => ({
currentStep: state.currentStep,
selectedAncillary: state.selectedAncillary,
closeModal: state.closeModal,
breakfastData: state.breakfastData,
setBreakfastData: state.setBreakfastData,
isBreakfast: state.isBreakfast,
}))
const intl = useIntl()
const lang = useLang()
const router = useRouter()
@@ -139,17 +152,44 @@ export default function AddAncillaryFlowModal({
selectedAncillary,
})
if (booking.guaranteeInfo) {
const packages = []
const packagesToAdd = []
if (selectedAncillary?.id && data.quantityWithCard) {
packages.push({
code: selectedAncillary.id,
quantity: data.quantityWithCard,
comment: data.optionalText || undefined,
})
if (!isBreakfast) {
packagesToAdd.push({
code: selectedAncillary.id,
quantity: data.quantityWithCard,
comment: data.optionalText || undefined,
})
} else {
if (!breakfastData) {
toast.error(
intl.formatMessage({
id: "Something went wrong!",
})
)
return
}
packagesToAdd.push({
code: BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST,
quantity: breakfastData.nrOfAdults,
comment: data.optionalText || undefined,
})
packagesToAdd.push({
code: BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST,
quantity: breakfastData.nrOfPayingChildren,
comment: data.optionalText || undefined,
})
packagesToAdd.push({
code: BreakfastPackageEnum.FREE_CHILD_BREAKFAST,
quantity: breakfastData.nrOfFreeChildren,
comment: data.optionalText || undefined,
})
}
}
if (selectedAncillary?.loyaltyCode && data.quantityWithPoints) {
packages.push({
packagesToAdd.push({
code: selectedAncillary.loyaltyCode,
quantity: data.quantityWithPoints,
comment: data.optionalText || undefined,
@@ -162,7 +202,7 @@ export default function AddAncillaryFlowModal({
ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime
? data.deliveryTime
: undefined,
packages,
packages: packagesToAdd,
language: lang,
})
} else {
@@ -212,9 +252,26 @@ export default function AddAncillaryFlowModal({
}
}, [searchParams, pathname, formMethods, router])
useEffect(() => {
setBreakfastData(
calculateBreakfastData(
isBreakfast,
packages,
booking.adults,
booking.childrenAges
)
)
}, [
booking.adults,
booking.childrenAges,
isBreakfast,
packages,
setBreakfastData,
])
if (isLoading) {
return (
<div className={styles.loading}>
<div>
<LoadingSpinner />
</div>
)
@@ -251,10 +308,14 @@ export default function AddAncillaryFlowModal({
<div className={styles.price}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{formatPrice(
intl,
selectedAncillary.price.totalPrice,
selectedAncillary.price.currency
{isBreakfast ? (
<BreakfastPriceList />
) : (
formatPrice(
intl,
selectedAncillary.price.totalPrice,
selectedAncillary.price.currency
)
)}
</p>
</Typography>
@@ -292,10 +353,7 @@ export default function AddAncillaryFlowModal({
<Steps user={user} savedCreditCards={savedCreditCards} />
</div>
</form>
{/* TODO: Remove the berakfast check when add breakfast is implemented */}
{currentStep === AncillaryStepEnum.selectAncillary ||
selectedAncillary?.id ===
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST ? null : (
{currentStep === AncillaryStepEnum.selectAncillary ? null : (
<div
className={
currentStep === AncillaryStepEnum.confirmation
@@ -316,3 +374,96 @@ export default function AddAncillaryFlowModal({
</Modal>
)
}
function BreakfastPriceList() {
const intl = useIntl()
const breakfastData = useAddAncillaryStore((state) => state.breakfastData)
if (!breakfastData) {
return intl.formatMessage({
id: "Can not show breakfast prices.",
})
}
return (
<div>
<div className={styles.breakfastPriceList}>
<Typography variant="Body/Paragraph/mdBold">
<span>{`${breakfastData.priceAdult} ${breakfastData.currency} / ${intl.formatMessage({ id: "Adult" })}`}</span>
</Typography>
{breakfastData.nrOfPayingChildren > 0 && (
<>
<div className={styles.divider}>
<Divider variant="vertical" color="baseSurfaceSubtleNormal" />
</div>
<Typography variant="Body/Paragraph/mdBold">
<span>{`${breakfastData.priceChild} ${breakfastData.currency} / ${intl.formatMessage({ id: "Years" })} 4-12`}</span>
</Typography>
</>
)}
{breakfastData.nrOfFreeChildren > 0 && (
<>
<div className={styles.divider}>
<Divider variant="vertical" color="baseSurfaceSubtleNormal" />
</div>
<Typography variant="Body/Paragraph/mdBold">
<span>{`${intl.formatMessage({ id: "Free" })} / ${intl.formatMessage({ id: "Under {age} years" }, { age: 4 })}`}</span>
</Typography>
</>
)}
</div>
</div>
)
}
/**
* This function calculates some breakfast data in the store.
* It is used in various places in the add flow, but only needs
* to be calculated once.
*/
function calculateBreakfastData(
isBreakfast: boolean,
packages: Packages | null,
nrOfAdults: number,
childrenAges: number[]
): BreakfastData | null {
if (!isBreakfast) {
return null
}
const [nrOfPayingChildren, nrOfFreeChildren] = childrenAges.reduce(
(acc, curr) => (curr >= 4 ? [acc[0] + 1, acc[1]] : [acc[0], acc[1] + 1]),
[0, 0]
)
const priceAdult = packages?.find(
(p) => p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
)?.localPrice.price
const priceChild = packages?.find(
(p) => p.code === BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST
)?.localPrice.price
const currency = packages?.find(
(p) => p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
)?.localPrice.currency
if (
typeof priceAdult !== "number" ||
typeof priceChild !== "number" ||
typeof currency !== "string"
) {
return null
} else {
return {
nrOfAdults,
nrOfPayingChildren,
nrOfFreeChildren,
priceAdult,
priceChild,
currency,
}
}
}

View File

@@ -197,6 +197,7 @@ export function Ancillaries({
<AddAncillaryFlowModal
user={user}
booking={booking}
packages={packages}
refId={refId}
savedCreditCards={savedCreditCards}
/>

View File

@@ -37,6 +37,7 @@
"Adding room is not available on the new website yet.": "Tilføjelse af rum er endnu ikke tilgængelig på den nye hjemmeside.",
"Address": "Adresse",
"Address: {address}": "Adresse: {address}",
"Adult": "Voksen",
"Adults": "voksne",
"Age": "Alder",
"Airport": "Lufthavn",
@@ -127,6 +128,7 @@
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Morgenmad ({totalChildren, plural, one {# barn} other {# børn}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Morgenbuffet",
"Breakfast can only be added for the entire duration of the stay and for all guests.": "Morgenmad kan kun tilføjes for hele opholdets varighed og for alle gæster.",
"Breakfast charge": "Morgenmadsgebyr",
"Breakfast deal can be purchased at the hotel.": "Morgenmad kan købes på hotellet.",
"Breakfast excluded": "Morgenmad ikke inkluderet",
@@ -144,6 +146,7 @@
"By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service": "Ved at tilmelde dig accepterer du Scandic Friends <termsAndConditionsLink>vilkår og betingelser</termsAndConditionsLink>. Dit medlemskab er gyldigt indtil videre, og du kan til enhver tid opsige dit medlemskab ved at sende en e-mail til Scandics kundeservice",
"Cabaret seating": "Cabaret seating",
"Campaign": "Kampagne",
"Can not show breakfast prices.": "Kan ikke vise morgenmadspriser.",
"Can't find your stay?": "Kan du ikke finde dit ophold?",
"Cancel": "Afbestille",
"Cancel booking": "Cancel booking",
@@ -303,6 +306,7 @@
"Follow us": "Følg os",
"Food options": "Madvalg",
"Former Scandic Hotel": "Tidligere Scandic Hotel",
"Free": "Gratis",
"Free cancellation": "Gratis afbestilling",
"Free parking": "Gratis parkering",
"Free rebooking": "Gratis ombooking",
@@ -810,6 +814,7 @@
"Type of bed": "Sengtype",
"Type of room": "Værelsestype",
"U-shape": "U-form",
"Under {age} years": "Under {age} år",
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Desværre er et af de værelser, du har valgt, solgt ud. Vælg et andet værelse for at fortsætte.",
"Unlink accounts": "Unlink accounts",
"Unpaid": "Ikke betalt",
@@ -872,6 +877,7 @@
"Windows natural daylight and excellent view": "Windows natural daylight and excellent view",
"Windows with natural daylight": "Vinduer med naturligt dagslys",
"Year": "År",
"Years": "År",
"Yes": "Ja",
"Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.": "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
"Yes, I want to transfer my points": "Yes, I want to transfer my points",
@@ -922,6 +928,7 @@
"Zoo": "Zoo",
"Zoom in": "Zoom ind",
"Zoom out": "Zoom ud",
"ages": "aldre",
"as of today": "pr. dags dato",
"booking.confirmation.text": "Tak fordi du bookede hos os! Vi glæder os til at byde dig velkommen og håber du får et behageligt ophold. Hvis du har spørgsmål eller har brug for at foretage ændringer i din reservation, bedes du <emailLink>kontakte os.</emailLink>",
"booking.confirmation.title": "Booking bekræftelse",
@@ -939,6 +946,7 @@
"sunday": "søndag",
"thursday": "torsdag",
"tuesday": "tirsdag",
"under": "under",
"until": "indtil",
"wednesday": "onsdag",
"{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km til byens centrum",

View File

@@ -37,6 +37,7 @@
"Adding room is not available on the new website yet.": "Zum Hinzufügen eines Zimmers ist derzeit nicht möglich.",
"Address": "Adresse",
"Address: {address}": "Adresse: {address}",
"Adult": "Erwachsener",
"Adults": "Erwachsene",
"Age": "Alter",
"Airport": "Flughafen",
@@ -128,6 +129,7 @@
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Frühstück ({totalChildren, plural, one {# kind} other {# kinder}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Frühstücksbuffet",
"Breakfast can only be added for the entire duration of the stay and for all guests.": "Frühstück kann nur für die gesamte Aufenthaltsdauer und für alle Gäste hinzugefügt werden.",
"Breakfast charge": "Frühstücksgebühr",
"Breakfast deal can be purchased at the hotel.": "Frühstücksangebot kann im Hotel gekauft werden.",
"Breakfast excluded": "Frühstück nicht inbegriffen",
@@ -145,6 +147,7 @@
"By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service": "Mit Ihrer Anmeldung akzeptieren Sie die <termsAndConditionsLink>Allgemeinen Geschäftsbedingungen</termsAndConditionsLink> von Scandic Friends. Ihre Mitgliedschaft ist bis auf Weiteres gültig und Sie können sie jederzeit kündigen, indem Sie eine E-Mail an den Kundenservice von Scandic senden.",
"Cabaret seating": "Cabaret seating",
"Campaign": "Kampagne",
"Can not show breakfast prices.": "Frühstückspreise können nicht angezeigt werden.",
"Can't find your stay?": "Sie können Ihren Aufenthalt nicht finden?",
"Cancel": "Stornieren",
"Cancel booking": "Cancel booking",
@@ -304,6 +307,7 @@
"Follow us": "Folgen Sie uns",
"Food options": "Speisen & Getränke",
"Former Scandic Hotel": "Ehemaliges Scandic Hotel",
"Free": "Kostenlos",
"Free cancellation": "Kostenlose Stornierung",
"Free parking": "Kostenloses Parken",
"Free rebooking": "Kostenlose Umbuchung",
@@ -808,6 +812,7 @@
"Type of bed": "Bettentyp",
"Type of room": "Zimmerart",
"U-shape": "U-shape",
"Under {age} years": "Unter {age} Jahren",
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Leider ist eines der von Ihnen ausgewählten Zimmer verkauft. Bitte wählen Sie ein anderes Zimmer, um fortzufahren.",
"Unlink accounts": "Unlink accounts",
"Unpaid": "Nicht bezahlt",
@@ -870,6 +875,7 @@
"Windows natural daylight and excellent view": "Windows natural daylight and excellent view",
"Windows with natural daylight": "Fenster mit natürlichem Tageslicht",
"Year": "Jahr",
"Years": "Jahre",
"Yes": "Ja",
"Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.": "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
"Yes, I want to transfer my points": "Yes, I want to transfer my points",
@@ -920,6 +926,7 @@
"Zoo": "Zoo",
"Zoom in": "Vergrößern",
"Zoom out": "Verkleinern",
"ages": "Altersgruppen",
"as of today": "Stand heute",
"booking.confirmation.text": "Vielen Dank, dass Sie bei uns gebucht haben! Wir freuen uns, Sie bei uns begrüßen zu dürfen und wünschen Ihnen einen angenehmen Aufenthalt. Wenn Sie Fragen haben oder Änderungen an Ihrer Buchung vornehmen müssen, <emailLink>kontaktieren Sie uns bitte.</emailLink>.",
"booking.confirmation.title": "Buchungsbestätigung",
@@ -937,6 +944,7 @@
"sunday": "sonntag",
"thursday": "donnerstag",
"tuesday": "dienstag",
"under": "unter",
"until": "bis",
"wednesday": "mittwoch",
"{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km bis zum Stadtzentrum",

View File

@@ -37,6 +37,7 @@
"Adding room is not available on the new website yet.": "Adding room is not available on the new website yet.",
"Address": "Address",
"Address: {address}": "Address: {address}",
"Adult": "Adult",
"Adults": "Adults",
"Age": "Age",
"Airport": "Airport",
@@ -126,6 +127,7 @@
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Breakfast buffet",
"Breakfast can only be added for the entire duration of the stayand for all guests.": "Breakfast can only be added for the entire duration of the stayand for all guests.",
"Breakfast charge": "Breakfast charge",
"Breakfast deal can be purchased at the hotel.": "Breakfast deal can be purchased at the hotel.",
"Breakfast excluded": "Breakfast excluded",
@@ -143,6 +145,7 @@
"By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service": "By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service",
"Cabaret seating": "Cabaret seating",
"Campaign": "Campaign",
"Can not show breakfast prices.": "Can not show breakfast prices.",
"Can't find your stay?": "Can't find your stay?",
"Cancel": "Cancel",
"Cancel booking": "Cancel booking",
@@ -302,6 +305,7 @@
"Follow us": "Follow us",
"Food options": "Food options",
"Former Scandic Hotel": "Former Scandic Hotel",
"Free": "Free",
"Free cancellation": "Free cancellation",
"Free parking": "Free parking",
"Free rebooking": "Free rebooking",
@@ -806,6 +810,7 @@
"Type of bed": "Type of bed",
"Type of room": "Type of room",
"U-shape": "U-shape",
"Under {age} years": "Under {age} years",
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.",
"Unlink accounts": "Unlink accounts",
"Unpaid": "Unpaid",
@@ -868,6 +873,7 @@
"Windows natural daylight and excellent view": "Windows natural daylight and excellent view",
"Windows with natural daylight": "Windows with natural daylight",
"Year": "Year",
"Years": "Years",
"Yes": "Yes",
"Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.": "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
"Yes, I want to transfer my points": "Yes, I want to transfer my points",
@@ -918,6 +924,7 @@
"Zoo": "Zoo",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"ages": "ages",
"as of today": "as of today",
"cancelled": "cancelled",
"friday": "friday",
@@ -932,6 +939,7 @@
"sunday": "sunday",
"thursday": "thursday",
"tuesday": "tuesday",
"under": "under",
"until": "until",
"wednesday": "wednesday",
"{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center",

View File

@@ -37,6 +37,7 @@
"Adding room is not available on the new website yet.": "Lisäämään huone on vielä saatavilla uudella verkkosivustolla.",
"Address": "Osoite",
"Address: {address}": "Osoite: {address}",
"Adult": "Aikuinen",
"Adults": "Aikuista",
"Age": "Ikä",
"Airport": "Lentokenttä",
@@ -126,6 +127,7 @@
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Aamiainen ({totalChildren, plural, one {# lapsi} other {# lasta}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Aamiaisbuffet",
"Breakfast can only be added for the entire duration of the stay and for all guests.": "Aamiainen voidaan lisätä vain koko oleskelun ajaksi ja kaikille asiakkaille.",
"Breakfast charge": "Aamiaismaksu",
"Breakfast deal can be purchased at the hotel.": "Aamiaisdeali voidaan ostaa hotellissa.",
"Breakfast excluded": "Aamiainen ei sisälly",
@@ -143,6 +145,7 @@
"By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service": "Rekisteröitymällä hyväksyt Scandic Friendsin <termsAndConditionsLink>käyttöehdot</termsAndConditionsLink>. Jäsenyytesi on voimassa toistaiseksi ja voit lopettaa jäsenyytesi milloin tahansa lähettämällä sähköpostia Scandicin asiakaspalveluun",
"Cabaret seating": "Cabaret seating",
"Campaign": "Kampanja",
"Can not show breakfast prices.": "Aamiainen hintoja ei voida näyttää.",
"Can't find your stay?": "Etkö löydä majoitusta?",
"Cancel": "Peruuttaa",
"Cancel booking": "Cancel booking",
@@ -303,6 +306,7 @@
"Follow us": "Seuraa meitä",
"Food options": "Ruokavalio",
"Former Scandic Hotel": "Entinen Scandic-hotelli",
"Free": "Ilmainen",
"Free cancellation": "Ilmainen peruutus",
"Free parking": "Ilmainen pysäköinti",
"Free rebooking": "Ilmainen uudelleenvaraus",
@@ -808,6 +812,7 @@
"Type of bed": "Vuodetyyppi",
"Type of room": "Huonetyyppi",
"U-shape": "U-muoto",
"Under {age} years": "Alle {age}-vuotiaat",
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Valitettavasti valitsemasi huone on loppuunmyyty. Valitse toinen huone jatkaaksesi.",
"Unlink accounts": "Unlink accounts",
"Unpaid": "Maksettaa",
@@ -870,6 +875,7 @@
"Windows natural daylight and excellent view": "Windows natural daylight and excellent view",
"Windows with natural daylight": "Ikkunat luonnonvalolla",
"Year": "Vuosi",
"Years": "Vuotta",
"Yes": "Kyllä",
"Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.": "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
"Yes, I want to transfer my points": "Yes, I want to transfer my points",
@@ -920,6 +926,7 @@
"Zoo": "Eläintarha",
"Zoom in": "Lähennä",
"Zoom out": "Loitonna",
"ages": "ikäryhmät",
"as of today": "tänään",
"booking.confirmation.text": "Kiitos, että teit varauksen meiltä! Toivotamme sinut tervetulleeksi ja toivomme sinulle miellyttävää oleskelua. Jos sinulla on kysyttävää tai haluat tehdä muutoksia varaukseesi, <emailLink>ota meihin yhteyttä.</emailLink>",
"booking.confirmation.title": "Varausvahvistus",
@@ -937,6 +944,7 @@
"sunday": "sunnuntai",
"thursday": "torstai",
"tuesday": "tiistai",
"under": "alle",
"until": "asti",
"wednesday": "keskiviikko",
"{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km keskustaan",

View File

@@ -37,6 +37,7 @@
"Adding room is not available on the new website yet.": "Legg til rom er enda ikke tilgjengelig på den nye nettsiden.",
"Address": "Adresse",
"Address: {address}": "Adresse: {address}",
"Adult": "Voksen",
"Adults": "Voksne",
"Age": "Alder",
"Airport": "Flyplass",
@@ -126,6 +127,7 @@
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Frokost ({totalChildren, plural, one {# barn} other {# barn}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Breakfast buffet",
"Breakfast can only be added for the entire duration of the stay and for all guests.": "Frokost kan bare legges til for hele oppholdets varighet og for alle gjester.",
"Breakfast charge": "Pris for frokost",
"Breakfast deal can be purchased at the hotel.": "Frokostdeal kan kjøpes på hotellet.",
"Breakfast excluded": "Frokost ekskludert",
@@ -143,6 +145,7 @@
"By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service": "Ved å registrere deg godtar du Scandic Friends <termsAndConditionsLink>vilkår og betingelser</termsAndConditionsLink>. Medlemskapet ditt er gyldig inntil videre, og du kan si opp medlemskapet ditt når som helst ved å sende en e-post til Scandics kundeservice",
"Cabaret seating": "Cabaret seating",
"Campaign": "Kampanje",
"Can not show breakfast prices.": "Kan ikke vise frokostpriser.",
"Can't find your stay?": "Finner du ikke oppholdet ditt?",
"Cancel": "Avbryt",
"Cancel booking": "Cancel booking",
@@ -302,6 +305,7 @@
"Follow us": "Følg oss",
"Food options": "Matvalg",
"Former Scandic Hotel": "Tidligere Scandic-hotell",
"Free": "Gratis",
"Free cancellation": "Gratis avbestilling",
"Free parking": "Gratis parkering",
"Free rebooking": "Gratis ombooking",
@@ -805,6 +809,7 @@
"Type of bed": "Sengtype",
"Type of room": "Romtype",
"U-shape": "U-form",
"Under {age} years": "Under {age} år",
"Unlink accounts": "Unlink accounts",
"Unpaid": "Ikke betalt",
"Until {time}, {date}": "Til {time}, {date}",
@@ -866,6 +871,7 @@
"Windows natural daylight and excellent view": "Windows natural daylight and excellent view",
"Windows with natural daylight": "Vinduer med naturlig dagslys",
"Year": "År",
"Years": "År",
"Yes": "Ja",
"Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.": "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
"Yes, I want to transfer my points": "Yes, I want to transfer my points",
@@ -916,6 +922,7 @@
"Zoo": "Dyrehage",
"Zoom in": "Zoom inn",
"Zoom out": "Zoom ut",
"ages": "aldre",
"as of today": "per i dag",
"booking.confirmation.text": "Takk for at du booket hos oss! Vi ser frem til å ønske deg velkommen og håper du får et hyggelig opphold. Hvis du har spørsmål eller trenger å gjøre endringer i bestillingen din, vennligst <emailLink>kontakt oss.</emailLink>",
"booking.confirmation.title": "Bestillingsbekreftelse",
@@ -933,6 +940,7 @@
"sunday": "søndag",
"thursday": "torsdag",
"tuesday": "tirsdag",
"under": "under",
"until": "til",
"wednesday": "onsdag",
"{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km til sentrum",

View File

@@ -37,6 +37,7 @@
"Adding room is not available on the new website yet.": "Lägg till rum är inte tillgängligt än på den nya webbplatsen.",
"Address": "Adress",
"Address: {address}": "Adress: {address}",
"Adult": "Vuxen",
"Adults": "Vuxna",
"Age": "Ålder",
"Airport": "Flygplats",
@@ -126,6 +127,7 @@
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Frukost ({totalChildren, plural, one {# barn} other {# barn}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Frukostbuffé",
"Breakfast can only be added for the entire duration of the stay and for all guests.": "Frukost kan endast läggas till för hela vistelsens varaktighet och för alla gäster.",
"Breakfast charge": "Frukostpris",
"Breakfast deal can be purchased at the hotel.": "Frukostdeal kan köpas på hotellet.",
"Breakfast excluded": "Frukost ingår ej",
@@ -143,6 +145,7 @@
"By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service": "Genom att registrera dig accepterar du Scandic Friends <termsAndConditionsLink>Användarvillkor</termsAndConditionsLink>. Ditt medlemskap gäller tills vidare och du kan när som helst säga upp ditt medlemskap genom att skicka ett mejl till Scandics kundtjänst",
"Cabaret seating": "Cabaret seating",
"Campaign": "Kampanj",
"Can not show breakfast prices.": "Kan inte visa frukostpriser.",
"Can't find your stay?": "Hittar du inte din vistelse?",
"Cancel": "Avbryt",
"Cancel booking": "Cancel booking",
@@ -302,6 +305,7 @@
"Follow us": "Följ oss",
"Food options": "Matval",
"Former Scandic Hotel": "Tidigare Scandichotell",
"Free": "Gratis",
"Free cancellation": "Fri avbokning",
"Free parking": "Gratis parkering",
"Free rebooking": "Fri ombokning",
@@ -806,6 +810,7 @@
"Type of bed": "Sängtyp",
"Type of room": "Rumstyp",
"U-shape": "U-form",
"Under {age} years": "Under {age} år",
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.": "Tyvärr, ett av de rum du valde är slutsålt. Vänligen välj ett annat rum för att fortsätta.",
"Unlink accounts": "Unlink accounts",
"Unpaid": "Ej betalt",
@@ -868,6 +873,7 @@
"Windows natural daylight and excellent view": "Fönster med naturligt dagsljus och utmärkt utsikt",
"Windows with natural daylight": "Fönster med naturligt dagsljus",
"Year": "År",
"Years": "År",
"Yes": "Ja",
"Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.": "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
"Yes, I want to transfer my points": "Yes, I want to transfer my points",
@@ -918,6 +924,7 @@
"Zoo": "Djurpark",
"Zoom in": "Zooma in",
"Zoom out": "Zooma ut",
"ages": "åldrar",
"as of today": "från och med idag",
"booking.confirmation.text": "Tack för att du bokar hos oss! Vi ser fram emot att välkomna dig och hoppas att du får en trevlig vistelse. Om du har några frågor eller behöver göra ändringar i din bokning, vänligen <emailLink>kontakta oss.</emailLink>",
"booking.confirmation.title": "Bokningsbekräftelse",
@@ -937,6 +944,7 @@
"tuesday": "tisdag",
"type": "typ",
"types": "typer",
"under": "under",
"until": "tills",
"wednesday": "onsdag",
"{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km till stadens centrum",

View File

@@ -10,6 +10,7 @@ import type {
Ancillary,
SelectedAncillary,
} from "@/types/components/myPages/myStay/ancillaries"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export enum AncillaryStepEnum {
@@ -29,6 +30,15 @@ type Steps = {
[AncillaryStepEnum.confirmation]: Step
}
export type BreakfastData = {
nrOfAdults: number
nrOfPayingChildren: number
nrOfFreeChildren: number
priceAdult: number
priceChild: number
currency: string
}
export interface AddAncillaryState {
currentStep: number
steps: Steps
@@ -41,6 +51,9 @@ export interface AddAncillaryState {
openModal: VoidFunction
closeModal: VoidFunction
prevStep: VoidFunction
breakfastData: BreakfastData | null
setBreakfastData: (breakfastData: BreakfastData | null) => void
isBreakfast: boolean
isOpen: boolean
selectedAncillary: SelectedAncillary | null
selectAncillary: (ancillary: SelectedAncillary) => void
@@ -95,6 +108,8 @@ export const createAddAncillaryStore = (
ancillariesBySelectedCategory,
currentStep: AncillaryStepEnum.selectAncillary,
selectedAncillary: null,
breakfastData: null,
isBreakfast: false,
isOpen: false,
steps,
openModal: () =>
@@ -189,6 +204,14 @@ export const createAddAncillaryStore = (
}
state.selectedAncillary = ancillary
state.currentStep = AncillaryStepEnum.selectQuantity
state.isBreakfast =
ancillary.id === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
})
),
setBreakfastData: (breakfastData) =>
set(
produce((state: AddAncillaryState) => {
state.breakfastData = breakfastData
})
),
}))

View File

@@ -40,6 +40,7 @@ export interface AncillaryGridModalProps {
export interface AddAncillaryFlowModalProps
extends Pick<BookingConfirmation, "booking"> {
packages: Packages | null
refId: string
user: User | null
savedCreditCards: CreditCard[] | null