fix: trigger memberPrice modal for membership number too

This commit is contained in:
Simon Emanuelsson
2025-05-26 16:09:02 +02:00
parent 32cc0cbe88
commit 39855d3c8a
10 changed files with 105 additions and 73 deletions

View File

@@ -1,5 +1,7 @@
"use client" "use client"
import { useEffect, useState } from "react"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import MagicWandIcon from "@scandic-hotels/design-system/Icons/MagicWandIcon" import MagicWandIcon from "@scandic-hotels/design-system/Icons/MagicWandIcon"
@@ -14,20 +16,35 @@ import { formatPrice } from "@/utils/numberFormatting"
import styles from "./modal.module.css" import styles from "./modal.module.css"
import type { Dispatch, SetStateAction } from "react"
import { CurrencyEnum } from "@/types/enums/currency" import { CurrencyEnum } from "@/types/enums/currency"
export default function MemberPriceModal({ export default function MemberPriceModal() {
isOpen, const {
setIsOpen, actions: { updatePriceForMembershipNo },
}: { room,
isOpen: boolean } = useRoomContext()
setIsOpen: Dispatch<SetStateAction<boolean>>
}) {
const { room } = useRoomContext()
const memberRate = "member" in room.roomRate ? room.roomRate.member : null const memberRate = "member" in room.roomRate ? room.roomRate.member : null
const intl = useIntl() const intl = useIntl()
const [isOpen, setIsOpen] = useState(false)
const { getFieldState, trigger } = useFormContext()
const [join, membershipNo] = useWatch({ name: ["join", "membershipNo"] })
useEffect(() => {
if (join) {
setIsOpen(true)
}
}, [join])
useEffect(() => {
trigger("membershipNo").then((isValid) => {
const { isDirty } = getFieldState("membershipNo")
updatePriceForMembershipNo(membershipNo, isValid)
if (isValid && isDirty) {
setIsOpen(true)
}
})
}, [getFieldState, membershipNo, trigger, updatePriceForMembershipNo])
if (!memberRate) { if (!memberRate) {
return null return null
@@ -36,7 +53,7 @@ export default function MemberPriceModal({
const memberPrice = memberRate?.localPrice ?? memberRate?.requestedPrice const memberPrice = memberRate?.localPrice ?? memberRate?.requestedPrice
return ( return (
<Modal isOpen={isOpen} onToggle={setIsOpen}> <Modal isOpen={isOpen} onToggle={() => setIsOpen(false)}>
<div className={styles.modalContent}> <div className={styles.modalContent}>
<div className={styles.innerModalContent}> <div className={styles.innerModalContent}>
<MagicWandIcon width="265px" /> <MagicWandIcon width="265px" />

View File

@@ -1,6 +1,5 @@
"use client" "use client"
import { useState } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
@@ -10,8 +9,6 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import { formatPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
import MemberPriceModal from "../../MemberPriceModal"
import styles from "./joinScandicFriendsCard.module.css" import styles from "./joinScandicFriendsCard.module.css"
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details" import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
@@ -26,13 +23,9 @@ export default function JoinScandicFriendsCard({
roomNr, roomNr,
actions: { updateJoin }, actions: { updateJoin },
} = useRoomContext() } = useRoomContext()
const [isMemberPriceModalOpen, setIsMemberPriceModalOpen] = useState(false)
function onChange(event: { target: { value: boolean } }) { function onChange(event: { target: { value: boolean } }) {
updateJoin(event.target.value) updateJoin(event.target.value)
if (event.target.value) {
setIsMemberPriceModalOpen(true)
}
} }
if (!("member" in room.roomRate) || !room.roomRate.member) { if (!("member" in room.roomRate) || !room.roomRate.member) {
@@ -106,10 +99,6 @@ export default function JoinScandicFriendsCard({
</Caption> </Caption>
))} ))}
</div> </div>
<MemberPriceModal
isOpen={isMemberPriceModalOpen}
setIsOpen={setIsMemberPriceModalOpen}
/>
</div> </div>
) )
} }

View File

@@ -13,6 +13,7 @@ import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import MemberPriceModal from "../MemberPriceModal"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard" import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import { getMultiroomDetailsSchema } from "./schema" import { getMultiroomDetailsSchema } from "./schema"
@@ -52,11 +53,7 @@ export default function Details() {
) )
const methods = useForm({ const methods = useForm({
criteriaMode: "all", defaultValues: {
mode: "all",
resolver: zodResolver(getMultiroomDetailsSchema(crossValidationData)),
reValidateMode: "onChange",
values: {
countryCode: initialData.countryCode, countryCode: initialData.countryCode,
email: initialData.email, email: initialData.email,
firstName: initialData.firstName, firstName: initialData.firstName,
@@ -68,6 +65,10 @@ export default function Details() {
comment: room.specialRequest.comment, comment: room.specialRequest.comment,
}, },
}, },
criteriaMode: "all",
mode: "all",
resolver: zodResolver(getMultiroomDetailsSchema(crossValidationData)),
reValidateMode: "onChange",
}) })
const { const {
@@ -189,6 +190,7 @@ export default function Details() {
)} )}
<SpecialRequests registerOptions={{ onBlur: updateDetailsStore }} /> <SpecialRequests registerOptions={{ onBlur: updateDetailsStore }} />
</div> </div>
<MemberPriceModal />
</form> </form>
</FormProvider> </FormProvider>
) )

View File

@@ -1,6 +1,4 @@
"use client" "use client"
import { useState } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
@@ -16,8 +14,6 @@ import { useRoomContext } from "@/contexts/Details/Room"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
import MemberPriceModal from "../../MemberPriceModal"
import styles from "./joinScandicFriendsCard.module.css" import styles from "./joinScandicFriendsCard.module.css"
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details" import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
@@ -32,13 +28,9 @@ export default function JoinScandicFriendsCard({
room, room,
actions: { updateJoin }, actions: { updateJoin },
} = useRoomContext() } = useRoomContext()
const [isMemberPriceModalOpen, setIsMemberPriceModalOpen] = useState(false)
function onChange(event: { target: { value: boolean } }) { function onChange(event: { target: { value: boolean } }) {
updateJoin(event.target.value) updateJoin(event.target.value)
if (event.target.value) {
setIsMemberPriceModalOpen(true)
}
} }
if (!("member" in room.roomRate) || !room.roomRate.member) { if (!("member" in room.roomRate) || !room.roomRate.member) {
@@ -156,10 +148,6 @@ export default function JoinScandicFriendsCard({
)} )}
</Footnote> </Footnote>
</div> </div>
<MemberPriceModal
isOpen={isMemberPriceModalOpen}
setIsOpen={setIsMemberPriceModalOpen}
/>
</div> </div>
) )
} }

View File

@@ -13,6 +13,7 @@ import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import MemberPriceModal from "../MemberPriceModal"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard" import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import { guestDetailsSchema, signedInDetailsSchema } from "./schema" import { guestDetailsSchema, signedInDetailsSchema } from "./schema"
import Signup from "./Signup" import Signup from "./Signup"
@@ -43,25 +44,25 @@ export default function Details({ user }: DetailsProps) {
const memberRate = "member" in room.roomRate ? room.roomRate.member : null const memberRate = "member" in room.roomRate ? room.roomRate.member : null
const methods = useForm({ const methods = useForm({
criteriaMode: "all", defaultValues: {
mode: "all", countryCode: user?.address?.countryCode || initialData.countryCode,
resolver: zodResolver(user ? signedInDetailsSchema : guestDetailsSchema),
reValidateMode: "onChange",
values: {
countryCode: user?.address?.countryCode ?? initialData.countryCode,
dateOfBirth: dateOfBirth:
"dateOfBirth" in initialData ? initialData.dateOfBirth : undefined, "dateOfBirth" in initialData ? initialData.dateOfBirth : undefined,
email: user?.email ?? initialData.email, email: user?.email || initialData.email,
firstName: user?.firstName ?? initialData.firstName, firstName: user?.firstName || initialData.firstName,
join: initialData.join, join: initialData.join,
lastName: user?.lastName ?? initialData.lastName, lastName: user?.lastName || initialData.lastName,
membershipNo: initialData.membershipNo, membershipNo: initialData.membershipNo,
phoneNumber: user?.phoneNumber ?? initialData.phoneNumber, phoneNumber: user?.phoneNumber || initialData.phoneNumber,
zipCode: "zipCode" in initialData ? initialData.zipCode : undefined, zipCode: "zipCode" in initialData ? initialData.zipCode : undefined,
specialRequest: { specialRequest: {
comment: room.specialRequest.comment, comment: room.specialRequest.comment,
}, },
}, },
criteriaMode: "all",
mode: "all",
resolver: zodResolver(user ? signedInDetailsSchema : guestDetailsSchema),
reValidateMode: "onChange",
}) })
const { const {
@@ -168,6 +169,7 @@ export default function Details({ user }: DetailsProps) {
)} )}
<SpecialRequests registerOptions={{ onBlur: updateDetailsStore }} /> <SpecialRequests registerOptions={{ onBlur: updateDetailsStore }} />
</div> </div>
<MemberPriceModal />
</form> </form>
</FormProvider> </FormProvider>
) )

View File

@@ -57,7 +57,7 @@ export default function Phone({
rules: registerOptions, rules: registerOptions,
}) })
const defaultPhoneNumber = formState.defaultValues?.phoneNumber const defaultPhoneNumber = formState.defaultValues?.phoneNumber ?? ""
// If defaultPhoneNumber exists and is valid, parse it to get the country code, // If defaultPhoneNumber exists and is valid, parse it to get the country code,
// otherwise set the default country from the lang. // otherwise set the default country from the lang.

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import deepmerge from "deepmerge" import deepmerge from "deepmerge"
import { useEffect, useRef } from "react" import { useEffect, useRef, useState } from "react"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { createDetailsStore } from "@/stores/enter-details" import { createDetailsStore } from "@/stores/enter-details"
@@ -14,6 +14,7 @@ import {
import { getMultiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema" import { getMultiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema"
import { guestDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema" import { guestDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema"
import LoadingSpinner from "@/components/LoadingSpinner"
import { DetailsContext } from "@/contexts/Details" import { DetailsContext } from "@/contexts/Details"
import type { DetailsStore } from "@/types/contexts/enter-details" import type { DetailsStore } from "@/types/contexts/enter-details"
@@ -31,6 +32,11 @@ export default function EnterDetailsProvider({
user, user,
vat, vat,
}: DetailsProviderProps) { }: DetailsProviderProps) {
// This state is needed to be able to use defaultValues for
// react-hook-form since values needs to be there on mount
// and since we read from SessionStorage we need to delay
// rendering the form until that has been done.
const [hasInitializedStore, setHasInitializedStore] = useState(false)
const storeRef = useRef<DetailsStore>() const storeRef = useRef<DetailsStore>()
if (!storeRef.current) { if (!storeRef.current) {
const initialData: InitialState = { const initialData: InitialState = {
@@ -74,16 +80,19 @@ export default function EnterDetailsProvider({
useEffect(() => { useEffect(() => {
const storedValues = readFromSessionStorage() const storedValues = readFromSessionStorage()
if (!storedValues) { if (!storedValues) {
setHasInitializedStore(true)
return return
} }
const isSameBooking = checkIsSameBooking(storedValues.booking, booking) const isSameBooking = checkIsSameBooking(storedValues.booking, booking)
if (!isSameBooking) { if (!isSameBooking) {
clearSessionStorage() clearSessionStorage()
setHasInitializedStore(true)
return return
} }
const store = storeRef.current?.getState() const store = storeRef.current?.getState()
if (!store) { if (!store) {
setHasInitializedStore(true)
return return
} }
@@ -209,11 +218,13 @@ export default function EnterDetailsProvider({
rooms: filteredOutMissingRooms, rooms: filteredOutMissingRooms,
totalPrice, totalPrice,
}) })
setHasInitializedStore(true)
}, [booking, rooms, user]) }, [booking, rooms, user])
return ( return (
<DetailsContext.Provider value={storeRef.current}> <DetailsContext.Provider value={storeRef.current}>
{children} {hasInitializedStore ? children : <LoadingSpinner fullPage />}
</DetailsContext.Provider> </DetailsContext.Provider>
) )
} }

View File

@@ -253,24 +253,18 @@ export function createDetailsStore(
}) })
) )
}, },
updateJoin(join) { updatePriceForMembershipNo(membershipNo, isValid) {
return set( return set(
produce((state: DetailsState) => { produce((state: DetailsState) => {
const currentRoom = state.rooms[idx].room const currentRoom = state.rooms[idx].room
currentRoom.guest.join = join currentRoom.guest.join = false
currentRoom.guest.membershipNo = isValid ? membershipNo : ""
if (join) {
currentRoom.guest.membershipNo = undefined
}
const isValidMembershipNo = isValid && !!membershipNo
currentRoom.roomPrice = getRoomPrice( currentRoom.roomPrice = getRoomPrice(
currentRoom.roomRate, currentRoom.roomRate,
Boolean( isValidMembershipNo
join ||
currentRoom.guest.membershipNo ||
(idx === 0 && isMember)
)
) )
const nights = dt(state.booking.toDate).diff( const nights = dt(state.booking.toDate).diff(
@@ -284,6 +278,43 @@ export function createDetailsStore(
isMember, isMember,
nights nights
) )
writeToSessionStorage({
booking: state.booking,
rooms: state.rooms,
})
})
)
},
updateJoin(join) {
return set(
produce((state: DetailsState) => {
const currentRoom = state.rooms[idx].room
currentRoom.guest.join = join
if (join) {
currentRoom.guest.membershipNo = ""
}
currentRoom.roomPrice = getRoomPrice(currentRoom.roomRate, join)
const nights = dt(state.booking.toDate).diff(
state.booking.fromDate,
"days"
)
state.totalPrice = calcTotalPrice(
state.rooms,
state.totalPrice.local.currency,
isMember,
nights
)
writeToSessionStorage({
booking: state.booking,
rooms: state.rooms,
})
}) })
) )
}, },

View File

@@ -1,16 +1,7 @@
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
import type { RoomState } from "@/types/stores/enter-details" import type { RoomState } from "@/types/stores/enter-details"
export interface RoomContextValue { export interface RoomContextValue {
actions: { actions: RoomState["actions"]
setIncomplete: () => void
updateBedType: (data: BedTypeSchema) => void
updateBreakfast: (data: BreakfastPackage | false) => void
updateJoin: (join: boolean) => void
updateDetails: (data: DetailsSchema) => void
}
isComplete: RoomState["isComplete"] isComplete: RoomState["isComplete"]
idx: number idx: number
room: RoomState["room"] room: RoomState["room"]

View File

@@ -65,6 +65,7 @@ export interface RoomState {
updateBedType: (data: BedTypeSchema) => void updateBedType: (data: BedTypeSchema) => void
updateBreakfast: (data: BreakfastPackage | false) => void updateBreakfast: (data: BreakfastPackage | false) => void
updateJoin: (join: boolean) => void updateJoin: (join: boolean) => void
updatePriceForMembershipNo: (membershipNo: string, isValid: boolean) => void
updateDetails: (data: DetailsSchema) => void updateDetails: (data: DetailsSchema) => void
} }
isComplete: boolean isComplete: boolean