react-app/src/components/projects/AddProject.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import { Box, Grid, InputAdornment, Typography, useTheme } from '@mui/material';
import { useContext, useEffect, useState } from 'react';
import { NavigateBackButton } from '../display/DetailViewNavigation';
import TextFormField from '../form/TextFormField';
import AutocompleteFormField from '../form/AutocompleteFormField';
import { FormProvider, useForm } from 'react-hook-form';
import SingleSelectBoxFormField from '../form/SingleSelectBoxFormField';
import usePimsApi from '@/hooks/usePimsApi';
import { ProjectPropertyIds } from '@/hooks/api/useProjectsApi';
import DisposalProjectSearch, { PropertyWithType } from './DisposalPropertiesSearchTable';
import React from 'react';
import useDataSubmitter from '@/hooks/useDataSubmitter';
import { LoadingButton } from '@mui/lab';
import { LookupContext } from '@/contexts/lookupContext';
import { ProjectTask } from '@/constants/projectTasks';
import { MonetaryType } from '@/constants/monetaryTypes';
import { NoteTypes } from '@/constants/noteTypes';
import useHistoryAwareNavigate from '@/hooks/useHistoryAwareNavigate';

const AddProject = () => {
  const { goToFromStateOrSetRoute } = useHistoryAwareNavigate();
  const formMethods = useForm({
    defaultValues: {
      Name: '',
      TierLevelId: null,
      Description: '',
      Assessed: 0,
      NetBook: 0,
      Market: 0,
      Appraised: 0,
      ProgramCost: 0,
      SalesCost: 0,
      ExemptionNote: '',
      Approval: false,
      Tasks: [],
    },
  });

  const [showNoPropertiesError, setShowNoPropertiesError] = useState(false);
  const [rows, setRows] = useState([]);
  const theme = useTheme();
  const api = usePimsApi();
  const { data: lookupData } = useContext(LookupContext);
  const [tasksForAddState, setTasksForAddState] = useState([]);
  const { submit, submitting } = useDataSubmitter(api.projects.postProject);

  useEffect(() => {
    if (!lookupData) {
      return;
    } else {
      const defaultState = lookupData?.ProjectStatuses.find(
        (a) => a.Name === 'Required Documentation',
      );
      const addTasks = lookupData?.Tasks.filter((task) => task.StatusId === defaultState.Id);
      addTasks.push({
        Id: ProjectTask.EXEMPTION_REQUESTED,
        Name: 'Apply for Enhanced Referral Process exemption',
        IsOptional: true, //NOTE: If changing this at the data source, this will be out of sync.
      });
      addTasks.forEach((t, idx) => formMethods.setValue(`Tasks.${idx}.TaskId`, t.Id));
      setTasksForAddState(addTasks);
    }
  }, [lookupData, formMethods]);
  const watch = formMethods.watch('Tasks');
  const exemptionWasRequested =
    watch.find((a) => a.TaskId === ProjectTask.EXEMPTION_REQUESTED)?.IsCompleted === true;
  return (
    <Box
      display={'flex'}
      gap={'1rem'}
      mt={'2rem'}
      mb={'2rem'}
      flexDirection={'column'}
      width={'38rem'}
      marginX={'auto'}
      boxShadow={'2em'}
    >
      <Box>
        <NavigateBackButton
          navigateBackTitle={'Back to Disposal Project Overview'}
          onBackClick={() => goToFromStateOrSetRoute('/projects')}
        />
      </Box>
      <Typography variant={'h2'} mb={'2rem'}>
        Create Disposal Project
      </Typography>
      <FormProvider {...formMethods}>
        <Typography variant="h5">Project Information</Typography>
        <Grid container spacing={2}>
          <Grid item xs={12}>
            <TextFormField required fullWidth name={'Name'} label={'Name'} />
          </Grid>
          <Grid item xs={12}>
            <AutocompleteFormField
              required
              name={'TierLevelId'}
              label={'Assign tier'}
              options={
                lookupData?.ProjectTiers?.map((tier) => ({
                  value: tier.Id,
                  label: tier.Name,
                  tooltip: tier.Description,
                })) ?? []
              }
            />
          </Grid>
          <Grid item xs={12}>
            <TextFormField
              multiline
              minRows={3}
              fullWidth
              name={'Description'}
              label={'Description'}
            />
          </Grid>
        </Grid>
        <Typography variant="h5">Disposal Project Properties</Typography>
        <DisposalProjectSearch rows={rows} setRows={setRows} />
        {showNoPropertiesError && (
          <Typography textAlign={'center'} color={theme.palette.error.main}>
            You must include at least one property.
          </Typography>
        )}
        <Typography variant="h5">Financial Information</Typography>
        <Grid container spacing={2}>
          <Grid item xs={6}>
            <TextFormField
              InputProps={{
                startAdornment: <InputAdornment position="start">$</InputAdornment>,
              }}
              fullWidth
              numeric
              name={'Assessed'}
              label={'Assessed value'}
              rules={{
                min: {
                  value: 0.01,
                  message: 'Must be greater than 0.',
                },
              }}
              required
            />
          </Grid>
          <Grid item xs={6}>
            <TextFormField
              InputProps={{
                startAdornment: <InputAdornment position="start">$</InputAdornment>,
              }}
              fullWidth
              numeric
              name={'NetBook'}
              label={'Net Book Value'}
              rules={{
                min: {
                  value: 0.01,
                  message: 'Must be greater than 0.',
                },
              }}
              required
            />
          </Grid>
          <Grid item xs={6}>
            <TextFormField
              InputProps={{
                startAdornment: <InputAdornment position="start">$</InputAdornment>,
              }}
              numeric
              fullWidth
              name={'Market'}
              label={'Estimated market value'}
              rules={{
                min: {
                  value: 0.01,
                  message: 'Must be greater than 0.',
                },
              }}
              required
            />
          </Grid>
          <Grid item xs={6}>
            <TextFormField
              InputProps={{
                startAdornment: <InputAdornment position="start">$</InputAdornment>,
              }}
              numeric
              fullWidth
              name={'Appraised'}
              label={'Appraised value'}
            />
          </Grid>
          <Grid item xs={6}>
            <TextFormField
              InputProps={{
                startAdornment: <InputAdornment position="start">$</InputAdornment>,
              }}
              numeric
              fullWidth
              name={'SalesCost'}
              label={'Estimated sales cost'}
            />
          </Grid>
          <Grid item xs={6}>
            <TextFormField
              InputProps={{
                startAdornment: <InputAdornment position="start">$</InputAdornment>,
              }}
              numeric
              fullWidth
              name={'ProgramCost'}
              label={'Estimated program recovery fees'}
            />
          </Grid>
        </Grid>
        <Typography variant="h5">Documentation</Typography>
        <Grid container spacing={2}>
          {tasksForAddState.map((task, idx) => (
            <Grid key={`${task.Id}-task-grid`} item xs={12}>
              <SingleSelectBoxFormField
                name={`Tasks.${idx}.IsCompleted`}
                label={task.Name}
                required={!task.IsOptional}
              />
            </Grid>
          ))}
          <Grid item xs={12}>
            {exemptionWasRequested && (
              <TextFormField
                fullWidth
                name={'ExemptionNote'}
                label={'Exemption rationale note'}
                required
                multiline
                minRows={2}
                rules={{
                  required: 'Rationale required when requesting exemption.',
                }}
              />
            )}
          </Grid>
        </Grid>
        <Typography variant="h5">Approval</Typography>
        <Grid container spacing={2}>
          <Grid item xs={12}>
            <SingleSelectBoxFormField
              name={'Approval'}
              label={
                'My Ministry/agency has approval/authority to submit the disposal project to SRES for review.'
              }
              required
            />
          </Grid>
        </Grid>
      </FormProvider>
      <LoadingButton
        loading={submitting}
        onClick={async () => {
          const isValid = await formMethods.trigger();
          setShowNoPropertiesError(!rows.length);
          if (isValid && rows.length) {
            const formValues = formMethods.getValues();
            // Construct project properties arrays
            const projectProperties: ProjectPropertyIds = {
              parcels: [],
              buildings: [],
            };
            rows.forEach((row: PropertyWithType) => {
              if (row.Type === 'Parcel') {
                projectProperties.parcels.push(row.Id);
              } else if (row.Type === 'Building') {
                projectProperties.buildings.push(row.Id);
              }
            });
            // Send to API hook
            submit(
              {
                ...formValues,
                ReportedFiscalYear: new Date().getFullYear(),
                ActualFiscalYear: new Date().getFullYear(),
                Monetaries: [
                  { MonetaryTypeId: MonetaryType.PROGRAM_COST, Value: formValues.ProgramCost },
                  { MonetaryTypeId: MonetaryType.SALES_COST, Value: formValues.SalesCost },
                ],
                Tasks: formValues.Tasks.filter((a) => a.IsCompleted),
                Notes: exemptionWasRequested
                  ? [{ NoteTypeId: NoteTypes.EXEMPTION, Note: formValues.ExemptionNote }]
                  : undefined,
              },
              projectProperties,
            ).then((response) => {
              if (response && response.ok) goToFromStateOrSetRoute('/projects');
            });
          }
        }}
        variant="contained"
        color="primary"
        sx={{ padding: '8px', width: '6rem', marginX: 'auto' }}
      >
        Submit
      </LoadingButton>
    </Box>
  );
};

export default AddProject;