Skip to content
Snippets Groups Projects
register.tsx 20.97 KiB
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