react-app/src/components/users/UserDetail.tsx

Summary

Maintainability
A
35 mins
Test Coverage
import React, { useContext, useEffect, useMemo, useState } from 'react';
import DataCard from '../display/DataCard';
import { Box, Grid, Typography } from '@mui/material';
import { statusChipFormatter } from '@/utilities/formatters';
import ConfirmDialog from '../dialog/ConfirmDialog';
import { FormProvider, useForm } from 'react-hook-form';
import AutocompleteFormField from '@/components/form/AutocompleteFormField';
import usePimsApi from '@/hooks/usePimsApi';
import useDataLoader from '@/hooks/useDataLoader';
import { User } from '@/hooks/api/useUsersApi';
import { UserContext } from '@/contexts/userContext';
import { Agency } from '@/hooks/api/useAgencyApi';
import TextFormField from '../form/TextFormField';
import DetailViewNavigation from '../display/DetailViewNavigation';
import { useGroupedAgenciesApi } from '@/hooks/api/useGroupedAgenciesApi';
import { useParams } from 'react-router-dom';
import useDataSubmitter from '@/hooks/useDataSubmitter';
import { Role, Roles } from '@/constants/roles';
import { LookupContext } from '@/contexts/lookupContext';
import { getProvider, validateEmail } from '@/utilities/helperFunctions';

interface IUserDetail {
  onClose: () => void;
}

interface UserProfile extends User {
  Provider: string;
}

const UserDetail = ({ onClose }: IUserDetail) => {
  const { id } = useParams();
  const { pimsUser } = useContext(UserContext);
  const { data: lookupData, getLookupValueById } = useContext(LookupContext);
  const api = usePimsApi();

  const [openProfileDialog, setOpenProfileDialog] = useState(false);
  const [openStatusDialog, setOpenStatusDialog] = useState(false);

  const { data, refreshData, isLoading } = useDataLoader(() => api.users.getUserById(id));
  const { submit, submitting } = useDataSubmitter(api.users.updateUser);

  const agencyOptions = useGroupedAgenciesApi().agencyOptions;

  const rolesOptions = useMemo(
    () => lookupData?.Roles?.map((role) => ({ label: role.Name, value: role.Name })) ?? [],
    [lookupData],
  );

  const userStatusData = {
    Status: data?.Status,
    Role: lookupData?.Roles?.find((role) => role.Id === data?.RoleId),
  };

  const provider = useMemo(
    () => getProvider(data?.Username, lookupData?.Config.bcscIdentifier),
    [data],
  );

  const userProfileData = {
    Provider: provider,
    Email: data?.Email,
    FirstName: data?.FirstName,
    LastName: data?.LastName,
    Agency: getLookupValueById('Agencies', data?.AgencyId),
    Position: data?.Position,
    CreatedOn: data?.CreatedOn ? new Date(data?.CreatedOn) : undefined,
    LastLogin: data?.LastLogin ? new Date(data?.LastLogin) : undefined,
  };

  const customFormatterStatus = (key: keyof User, val: any) => {
    if (key === 'Status') {
      return val ? statusChipFormatter(val) : <></>;
    } else if (key === 'Role' && val) {
      return <Typography>{(val as Role).Name}</Typography>;
    }
  };

  const customFormatterProfile = (key: keyof UserProfile, val: any) => {
    if (key === 'Agency' && val) {
      return <Typography>{(val as Agency).Name}</Typography>;
    }
  };

  const profileFormMethods = useForm({
    defaultValues: {
      Provider: '',
      Email: '',
      FirstName: '',
      LastName: '',
      AgencyId: null,
      Position: '',
    },
  });

  const statusFormMethods = useForm({
    defaultValues: {
      Status: '',
      Role: '',
    },
    mode: 'onBlur',
  });

  const canEdit = pimsUser.hasOneOfRoles([Roles.ADMIN]);

  useEffect(() => {
    refreshData();
  }, [id]);

  useEffect(() => {
    profileFormMethods.reset({
      Provider: provider,
      Email: userProfileData.Email,
      FirstName: userProfileData.FirstName,
      LastName: userProfileData.LastName,
      AgencyId: userProfileData.Agency?.Id ?? null,
      Position: userProfileData.Position,
    });
    statusFormMethods.reset({
      Status: userStatusData.Status,
      Role: userStatusData.Role?.Name,
    });
  }, [data]);

  return (
    <Box
      display={'flex'}
      gap={'1rem'}
      mt={'2rem'}
      flexDirection={'column'}
      width={'46rem'}
      marginX={'auto'}
    >
      <DetailViewNavigation
        disableDelete={true}
        navigateBackTitle={'Back to User Overview'}
        deleteTitle={'Delete Account'}
        onBackClick={() => onClose()}
        deleteButtonProps={{ disabled: pimsUser.data?.Id === id }}
      />
      <DataCard
        loading={isLoading}
        customFormatter={customFormatterStatus}
        values={userStatusData}
        title={'User Status'}
        disableEdit={!canEdit}
        onEdit={() => setOpenStatusDialog(true)}
      />
      <DataCard
        loading={isLoading}
        customFormatter={customFormatterProfile}
        values={userProfileData}
        title={'User Profile'}
        disableEdit={!canEdit}
        onEdit={() => setOpenProfileDialog(true)}
      />
      <ConfirmDialog
        title={'Update User Profile'}
        open={openProfileDialog}
        confirmButtonProps={{ loading: submitting }}
        onConfirm={async () => {
          const isValid = await profileFormMethods.trigger();
          if (isValid) {
            const formValues = profileFormMethods.getValues();
            formValues.Provider = undefined;
            submit(id, {
              Id: id,
              ...formValues,
            }).then(() => {
              refreshData();
              setOpenProfileDialog(false);
            });
          }
        }}
        onCancel={async () => setOpenProfileDialog(false)}
      >
        <FormProvider {...profileFormMethods}>
          <Grid mt={'1rem'} spacing={2} container>
            <Grid item xs={6}>
              <TextFormField fullWidth name={'Provider'} label={'Provider'} disabled />
            </Grid>
            <Grid item xs={6}>
              <TextFormField
                required
                fullWidth
                name={'Email'}
                label={'Email'}
                rules={{
                  validate: (value: string) => validateEmail(value) || 'Invalid email.',
                }}
              />
            </Grid>
            <Grid item xs={6}>
              <TextFormField required fullWidth name={'FirstName'} label={'First Name'} />
            </Grid>
            <Grid item xs={6}>
              <TextFormField required fullWidth name={'LastName'} label={'Last Name'} />
            </Grid>
            <Grid item xs={12}>
              <AutocompleteFormField
                allowNestedIndent
                name={'AgencyId'}
                label={'Agency'}
                options={agencyOptions}
                disableClearable={false}
              />
            </Grid>
            <Grid item xs={12}>
              <TextFormField name={'Position'} fullWidth label={'Position'} />
            </Grid>
          </Grid>
        </FormProvider>
      </ConfirmDialog>
      <ConfirmDialog
        title={'Update User Status'}
        open={openStatusDialog}
        confirmButtonProps={{ loading: submitting }}
        onConfirm={async () => {
          const isValid = await statusFormMethods.trigger();
          if (isValid) {
            const formValues = statusFormMethods.getValues();
            submit(id, {
              Id: id,
              Status: formValues.Status,
              Role: lookupData?.Roles.find((role) => role.Name === formValues.Role) as Role,
            }).then(() => {
              refreshData();
              setOpenStatusDialog(false);
            });
          }
        }}
        onCancel={async () => setOpenStatusDialog(false)}
      >
        <FormProvider {...statusFormMethods}>
          <Grid minWidth={'30rem'} mt={1} spacing={2} container>
            <Grid item xs={6}>
              <AutocompleteFormField
                name={'Status'}
                label={'Status'}
                options={[
                  //TODO: Get these through a lookup endpoint.
                  { label: 'Active', value: 'Active' },
                  { label: 'On Hold', value: 'OnHold' },
                  { label: 'Disabled', value: 'Disabled' },
                  { label: 'Denied', value: 'Denied' },
                ]}
              />
            </Grid>
            <Grid item xs={6}>
              <AutocompleteFormField name={'Role'} label={'Role'} options={rolesOptions} />
            </Grid>
          </Grid>
        </FormProvider>
      </ConfirmDialog>
    </Box>
  );
};

export default UserDetail;