Files
web/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/index.tsx
Niclas Edenvin fb44990777 Merged in fix/SW-2501-validation-trigger (pull request #1924)
fix(SW-2501): validation trigger

* fix(SW-2501): validation trigger

On enter details, when submitting we want to trigger the validation for
the details forms for each room. This will display error messages for
the form fields with errors if they are not already displayed, so the
user knows which fields has errors.


Approved-by: Tobias Johansson
2025-05-02 13:59:23 +00:00

198 lines
5.9 KiB
TypeScript

"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useCallback, useEffect, useMemo } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import SpecialRequests from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests"
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import Input from "@/components/TempDesignSystem/Form/Input"
import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import { getMultiroomDetailsSchema } from "./schema"
import styles from "./details.module.css"
import type { MultiroomDetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
const formID = "enter-details"
export default function Details() {
const intl = useIntl()
const { addPreSubmitCallback, rooms } = useEnterDetailsStore((state) => ({
addPreSubmitCallback: state.actions.addPreSubmitCallback,
rooms: state.rooms,
}))
const {
actions: { updateDetails, setIncomplete },
idx,
room,
roomNr,
} = useRoomContext()
const initialData = room.guest
/**
* The data that each room needs from each other to do validations
* across the rooms
*/
const crossValidationData = useMemo(
() =>
rooms
.filter((_, i) => i !== idx)
.map((room) => ({
firstName: room.room.guest.firstName,
lastName: room.room.guest.lastName,
membershipNo: room.room.guest.membershipNo,
})),
[idx, rooms]
)
const methods = useForm<MultiroomDetailsSchema>({
criteriaMode: "all",
mode: "all",
resolver: zodResolver(getMultiroomDetailsSchema(crossValidationData)),
reValidateMode: "onChange",
values: {
countryCode: initialData.countryCode,
email: initialData.email,
firstName: initialData.firstName,
join: initialData.join,
lastName: initialData.lastName,
membershipNo: initialData.membershipNo,
phoneNumber: initialData.phoneNumber,
specialRequest: {
comment: room.specialRequest.comment,
},
},
})
const {
formState: { isValid },
handleSubmit,
trigger,
} = methods
useEffect(() => {
addPreSubmitCallback(`${idx}-details`, trigger)
}, [addPreSubmitCallback, idx, trigger])
const updateDetailsStore = useCallback(() => {
if (isValid) {
handleSubmit(updateDetails)()
} else {
setIncomplete()
}
}, [handleSubmit, isValid, setIncomplete, updateDetails])
useEffect(updateDetailsStore, [methods.formState.isValid, updateDetailsStore])
// Trigger validation of the room manually when another room changes its data.
// Only do it if the field has a value, to avoid error states before the user
// has filled anything in.
useEffect(() => {
const { firstName, lastName, membershipNo } = methods.getValues()
if (firstName) {
methods.trigger("firstName")
}
if (lastName) {
methods.trigger("lastName")
}
if (membershipNo) {
methods.trigger("membershipNo")
}
}, [crossValidationData, methods])
const guestIsGoingToJoin = methods.watch("join")
const guestIsMember = methods.watch("membershipNo")
return (
<FormProvider {...methods}>
<form
className={styles.form}
id={`${formID}-room-${roomNr}`}
onSubmit={methods.handleSubmit(updateDetails)}
>
{guestIsMember ? null : <JoinScandicFriendsCard />}
<div className={styles.container}>
<Footnote
color="uiTextHighContrast"
textTransform="uppercase"
type="label"
className={styles.fullWidth}
>
{intl.formatMessage({
defaultMessage: "Guest information",
})}
</Footnote>
<Input
label={intl.formatMessage({
defaultMessage: "First name",
})}
maxLength={30}
name="firstName"
registerOptions={{
required: true,
deps: "lastName",
onBlur: updateDetailsStore,
}}
/>
<Input
label={intl.formatMessage({
defaultMessage: "Last name",
})}
maxLength={30}
name="lastName"
registerOptions={{
required: true,
deps: "firstName",
onBlur: updateDetailsStore,
}}
/>
<CountrySelect
className={styles.fullWidth}
label={intl.formatMessage({
defaultMessage: "Country",
})}
name="countryCode"
registerOptions={{ required: true, onBlur: updateDetailsStore }}
/>
<Input
className={styles.fullWidth}
label={intl.formatMessage({
defaultMessage: "Email address",
})}
name="email"
registerOptions={{ required: true, onBlur: updateDetailsStore }}
/>
<Phone
className={styles.fullWidth}
label={intl.formatMessage({
defaultMessage: "Phone number",
})}
name="phoneNumber"
registerOptions={{ required: true, onBlur: updateDetailsStore }}
/>
{guestIsGoingToJoin ? null : (
<Input
className={styles.fullWidth}
label={intl.formatMessage({
defaultMessage: "Membership no",
})}
name="membershipNo"
type="tel"
registerOptions={{ onBlur: updateDetailsStore }}
/>
)}
<SpecialRequests registerOptions={{ onBlur: updateDetailsStore }} />
</div>
</form>
</FormProvider>
)
}