-
Andreas Ellewsen authoredAndreas Ellewsen authored
import {
Box,
Divider,
MenuItem,
Select,
SelectChangeEvent,
Stack,
TextField,
Typography,
} from '@mui/material'
import { SubmitHandler, Controller, useForm } from 'react-hook-form'
import React, {
forwardRef,
Ref,
useContext,
useEffect,
useImperativeHandle,
useState,
} from 'react'
import { useTranslation, Trans } from 'react-i18next'
import {
CountryCallingCode,
CountryCode,
getCountries,
getCountryCallingCode,
} from 'libphonenumber-js'
import { getAlpha2Codes, getName } from 'i18n-iso-countries'
import { DatePicker } from '@mui/lab'
import { subYears } from 'date-fns/fp'
import { isValidFnr, isValidMobilePhoneNumber } from 'utils'
import { GuestInviteInformation } from '../guestDataForm'
import { GuestRegisterData } from '../enteredGuestData'
import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods'
import AuthenticationMethod from '../authenticationMethod'
import { FeatureContext } from '../../../../contexts'
interface GuestRegisterProperties {
nextHandler(registerData: GuestRegisterData): void
initialGuestData: GuestInviteInformation
registerData: GuestRegisterData | null
}
/**
* This component is the form where the guest enters missing information about himself and
* where he can see the data the sponsor has entered and the role. The page may also
* be populated with data from a third-party like Feide if the guest logged in using that.
*/
const GuestRegisterStep = forwardRef(
(props: GuestRegisterProperties, ref: Ref<GuestRegisterCallableMethods>) => {
const { i18n, t } = useTranslation(['common'])
const { nextHandler, initialGuestData, registerData } = props
// For select-components it seems to be easier to tie them to a state
// and then handle the updating of the form using this, than to tie the
// components directly to the form-field using useForm
const [countryCode, setCountryCode] = useState<
CountryCallingCode | undefined
>(undefined)
const [passportNationality, setPassportNationality] = useState<
string | undefined
>(undefined)
const [idErrorState, setIdErrorState] = useState<string>('')
const { displayContactAtUnitGuestInput } = useContext(FeatureContext)
console.log('register step registerData', registerData)
const {
register,
handleSubmit,
setValue,
setError,
clearErrors,
control,
trigger,
formState: { errors },
} = useForm<GuestRegisterData>({
defaultValues: registerData ?? {},
})
const submit: SubmitHandler<GuestRegisterData> = async (data) => {
console.log('submit data is', data)
const result = await trigger()
console.log('trigger result is', result)
const tohandler = data
if (
!data.nationalIdNumber &&
!data.passportNumber &&
!data.passportNationality
) {
// The user has not entered a national ID number nor passport information.
// In this case the user should not be allowed to send in the registration
setIdErrorState(t('validation.nationalIdOrPassport'))
return
}
// Users should only input NIN or passport
if (data.passportNumber && data.nationalIdNumber) {
setIdErrorState(t('validation.doubleIdentity'))
return
}
// Reset passportNationality if NIN is set and passport is empty
if (
data.nationalIdNumber &&
!data.passportNumber &&
data.passportNationality
) {
setValue('passportNationality', '')
setPassportNationality('')
tohandler.passportNationality = ''
}
// if one on the passport fields are set, check that both are set
if (
(data.passportNumber && !data.passportNationality) ||
(!data.passportNumber && data.passportNationality)
) {
setIdErrorState(t('validation.passportNationalityAndNumber'))
return
}
setIdErrorState('')
console.log('register submit errors', errors)
if (!Object.keys(errors).length) {
nextHandler(tohandler)
}
}
const onSubmit = handleSubmit<GuestRegisterData>(submit)
const handlePassportNationalityChange = (event: SelectChangeEvent) => {
if (event.target.value) {
const passportValue = event.target.value as string
setValue('passportNationality', passportValue)
setPassportNationality(passportValue)
}
}
const handleCountryCodeChange = (event: SelectChangeEvent) => {
if (event.target.value) {
const countryCodeType = event.target.value as CountryCode
setCountryCode(countryCodeType)
setValue('mobilePhoneCountry', countryCodeType)
} else {
setCountryCode(undefined)
}
}
const handleMobilePhoneChange = (value: any) => {
if (countryCode) {
// The country code and the rest of the mobile number are in two fields, so cannot
// register the field directly in form, but need to have extra logic defined
// to combine the values before writing them to the form handling
const phoneNumberWithCountryCode = `+${getCountryCallingCode(
countryCode as CountryCode
)}${value.target.value}`
const isValidPhoneNumber = isValidMobilePhoneNumber(
phoneNumberWithCountryCode
)
if (isValidPhoneNumber === true) {
setValue('mobilePhone', value.target.value)
clearErrors('mobilePhone')
} else {
setError('mobilePhone', {
type: 'manual',
message: isValidPhoneNumber || undefined,
})
}
}
setValue('mobilePhone', value.target.value)
}
const today = new Date()
const minBirthDate = subYears(100)(today)
const maxBirthDate = subYears(1)(today)
useEffect(() => {
if (registerData?.passportNationality) {
setPassportNationality(registerData.passportNationality)
}
if (registerData?.mobilePhoneCountry) {
setCountryCode(registerData.mobilePhoneCountry)
}
}, [registerData])
register('mobilePhoneCountry')
register('passportNationality')
useImperativeHandle(ref, () => ({ doSubmit: () => onSubmit() }))
const passportCountries = Object.keys(getAlpha2Codes())
.map((countryAlphaCode: string) => {
const countryTuple: [string, string] = [
countryAlphaCode,
getName(countryAlphaCode, i18n.language),
]
return countryTuple
})
.filter(
(countryTuple: [string, string]) =>
// All countries are expected to have a name, this filtering
// is here to make some tests run in an environment where the
// internationalization is not set up
countryTuple[1] !== undefined
)
.sort(
(countryTuple1: [string, string], countryTuple2: [string, string]) =>
countryTuple1[1].localeCompare(countryTuple2[1])
)
return (
<>
<Box sx={{ maxWidth: '30rem' }}>
<Typography
variant="h5"
sx={{
paddingTop: '1rem',
paddingBottom: '1rem',
}}
>
{t('guestRegisterWizardText.yourContactInformation')}
</Typography>
<Typography sx={{ paddingBottom: '2rem' }}>
{t('guestRegisterWizardText.contactInformationDescription')}
<Divider sx={{ border: '1px solid' }} />
</Typography>
<form onSubmit={onSubmit}>
<Stack spacing={2}>
{/* The name is only editable if it is it is not coming from some trusted source */}
{initialGuestData.authentication_method !==
AuthenticationMethod.Invite ? (
<>
<TextField
id="firstName"
label={t('input.firstName')}
// value={initialGuestData.first_name}
disabled
/>
<TextField
id="lastName"
label={t('input.lastName')}
// value={initialGuestData.last_name}
disabled
/>
</>
) : (
<>
<Controller
name="firstName"
control={control}
rules={{
required: t(
'common:validation.firstNameRequired'
).toString(),
}}
render={({ field: { onChange, value } }) => (
<TextField
id="firstName"
label={t('input.firstName')}
value={value}
onChange={onChange}
error={!!errors.firstName}
helperText={
errors.firstName && errors.firstName.message
}
/>
)}
/>
<Controller
name="lastName"
control={control}
rules={{
required: t(
'common:validation.lastNameRequired'
).toString(),
}}
render={({ field: { onChange, value } }) => (
<TextField
id="lastName"
label={t('input.lastName')}
value={value}
onChange={onChange}
error={!!errors.lastName}
helperText={errors.lastName && errors.lastName.message}
/>
)}
/>
</>
)}
<Controller
name="dateOfBirth"
control={control}
rules={{
required: t(
'common:validation.dateOfBirthRequired'
).toString(),
}}
render={({ field }) => (
<DatePicker
mask="____-__-__"
label={t('input.dateOfBirth')}
// If value is set to undefined the birth date is set to today. Using null makes the field blank
value={field.value ?? null}
minDate={minBirthDate}
maxDate={maxBirthDate}
inputFormat="yyyy-MM-dd"
onChange={(value) => {
field.onChange(value)
}}
renderInput={(params) => (
<TextField
data-testid="date-of-birth-input-text"
// FIXME: works, but color is wrong
// error={!!errors.dateOfBirth}
// helperText={errors.dateOfBirth && errors.dateOfBirth.message}
{...params}
/>
)}
/>
)}
/>
{errors.dateOfBirth && (
<Typography color="error">
{errors.dateOfBirth.message}
</Typography>
)}
<TextField
id="email"
label={t('input.email')}
value={initialGuestData.email || ''}
disabled
/>
{/* Only show the Feide ID field if the value is present */}
{initialGuestData.feide_id && (
<TextField
id="feide_id"
label={t('feideId')}
value={initialGuestData.feide_id}
disabled
/>
)}
{/* Box with phone country code and mobile phone */}
<Box
sx={{
display: 'flex',
flexDirection: 'row',
}}
>
<Select
sx={{
maxHeight: '2.5rem',
minWidth: '5rem',
marginRight: '0.5rem',
}}
labelId="phone-number-select"
id="phone-number-select"
displayEmpty
onChange={handleCountryCodeChange}
value={countryCode ? countryCode.toString() : ''}
renderValue={(selected: any) => {
if (!selected) {
return t('input.countryCallingCode')
}
return `${getName(
selected,
i18n.language
)} (${getCountryCallingCode(selected as CountryCode)})`
}}
data-testid="phone-country-code-select"
inputProps={{
'data-testid': 'phone-country-code-select-inner',
}}
>
<MenuItem disabled value="">
{t('input.countryCallingCode')}
</MenuItem>
{getCountries()
.map((country: CountryCode) => {
// Make a tuple including the name to avoid
// having to get it again further down
const countryTuple: [CountryCode, string] = [
country,
getName(country, i18n.language),
]
return countryTuple
})
.filter(
// A few country codes do no have a country name. Assuming
// these are not needed and filtering them out to make the
// list look nicer
(countryTuple: [CountryCode, string]) =>
countryTuple[1] !== undefined
)
.sort(
(
country1: [CountryCode, string],
country2: [CountryCode, string]
) => country1[1].localeCompare(country2[1])
)
.map((country) => (
<MenuItem key={country[0]} value={country[0]}>
{country[1]} ({getCountryCallingCode(country[0])})
</MenuItem>
))}
</Select>
<Controller
name="mobilePhone"
control={control}
rules={{
required: true,
}}
render={({ field }) => (
<TextField
sx={{ flexGrow: 2 }}
label={t('input.mobilePhone')}
error={!!errors.mobilePhone}
value={field.value ?? ''}
helperText={
errors.mobilePhone && errors.mobilePhone.message
}
onChange={handleMobilePhoneChange}
/>
)}
/>
</Box>
{initialGuestData.authentication_method ===
AuthenticationMethod.Invite && (
<>
<Typography variant="h5" sx={{ paddingTop: '1rem' }}>
{t('guestRegisterWizardText.identityHeader')}
</Typography>
<Typography sx={{ paddingBottom: '1rem' }}>
<Trans i18nKey="common:guestRegisterWizardText.identityBody">
Enter national identity number if you have one.{' '}
<strong>Otherwise</strong> use passport information.
</Trans>
<Divider sx={{ border: '1px solid' }} />
</Typography>
{/* The guest should fill in one of national ID number or passport number */}
<Controller
name="nationalIdNumber"
control={control}
rules={{
// It is not required that the national ID number be filled in, the guest may not have
// one, so allow empty values for the validation to pass. Note that both "fødselsnummer" and
// D-number are allowed as input
validate: (value) => isValidFnr(value, true),
}}
render={({ field }) => (
<TextField
id="nationalIdNumber"
label={t('input.nationalIdNumber')}
error={!!errors.nationalIdNumber}
value={field.value}
onChange={field.onChange}
helperText={
errors.nationalIdNumber &&
errors.nationalIdNumber.message
}
/>
)}
/>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
}}
>
<Select
sx={{
maxHeight: '2.5rem',
minWidth: '5rem',
marginRight: '0.5rem',
}}
id="passport-nationality-id"
labelId="passport-nationality-label"
label={t('input.passportNationality')}
displayEmpty
value={passportNationality ?? ''}
onChange={handlePassportNationalityChange}
renderValue={(selected: any) => {
if (!selected || selected.length === 0) {
return t('input.passportNationality')
}
return selected
}}
>
<MenuItem disabled value="">
{t('input.passportNationality')}
</MenuItem>
{passportCountries.map((countryTuple) => (
<MenuItem key={countryTuple[1]} value={countryTuple[0]}>
{`${countryTuple[1]} (${countryTuple[0]})`}
</MenuItem>
))}
</Select>
<Controller
name="passportNumber"
control={control}
render={({ field }) => (
<TextField
id="passportNumber"
data-testid="passport_number_input"
value={field.value}
label={t('input.passportNumber')}
onChange={field.onChange}
/>
)}
/>
</Box>
{idErrorState && (
<Typography color="error">{idErrorState}</Typography>
)}
</>
)}
{initialGuestData.authentication_method ===
AuthenticationMethod.Feide && (
<TextField
id="national_id_number"
data-testid="national_id_number_feide"
label={t('input.nationalIdNumber')}
disabled
/>
)}
<Typography variant="h5" sx={{ paddingTop: '1rem' }}>
{t('guestRegisterWizardText.yourGuestPeriod')}
</Typography>
<Typography sx={{ paddingBottom: '1rem' }}>
{t('guestRegisterWizardText.guestPeriodDescription')}
<Divider sx={{ border: '1px solid' }} />
</Typography>
<TextField
id="ou-unit"
value={
i18n.language === 'en'
? initialGuestData.ou_name_en
: initialGuestData.ou_name_nb
}
label={t('ou')}
disabled
/>
<TextField
id="roleType"
label={t('input.roleType')}
value={
i18n.language === 'en'
? initialGuestData.role_name_en
: initialGuestData.role_name_nb
}
disabled
/>
<TextField
id="rolePeriod"
label={t('period')}
value={`${initialGuestData.role_start} - ${initialGuestData.role_end}`}
disabled
/>
{displayContactAtUnitGuestInput && (
<TextField
id="contactPersonUnit"
label={t('input.contactPersonUnit')}
value={initialGuestData.contact_person_unit}
disabled
/>
)}
</Stack>
</form>
</Box>
</>
)
}
)
export default GuestRegisterStep