react-app/src/components/property/PropertyTable.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
import React, { MutableRefObject, useContext, useMemo, useState } from 'react';
import { CustomListSubheader, CustomMenuItem, FilterSearchDataGrid } from '../table/DataTable';
import { Box, SxProps, Tooltip, lighten, useTheme } from '@mui/material';
import { GridApiCommunity } from '@mui/x-data-grid/internals';
import { GridColDef, GridColumnHeaderTitle, GridEventListener } from '@mui/x-data-grid';
import { dateFormatter, pidFormatter, zeroPadPID } from '@/utilities/formatters';
import { ClassificationInline } from './ClassificationIcon';
import usePimsApi from '@/hooks/usePimsApi';
import { Parcel, ParcelEvaluation, ParcelFiscal } from '@/hooks/api/useParcelsApi';
import { Building, BuildingEvaluation, BuildingFiscal } from '@/hooks/api/useBuildingsApi';
import { propertyTypeMapper, PropertyTypes } from '@/constants/propertyTypes';
import { SnackBarContext } from '@/contexts/snackbarContext';
import { CommonFiltering } from '@/interfaces/ICommonFiltering';
import { LookupContext } from '@/contexts/lookupContext';
import useHistoryAwareNavigate from '@/hooks/useHistoryAwareNavigate';
import { exposedProjectStatuses } from '@/constants/projectStatuses';
import './propertyRowStyle.css';

interface IPropertyTable {
  rowClickHandler: GridEventListener<'rowClick'>;
}

export const useClassificationStyle = () => {
  const theme = useTheme();

  return {
    0: {
      textColor: lighten(theme.palette.success.main, 0.3),
      bgColor: theme.palette.success.light,
    },
    1: { textColor: lighten(theme.palette.blue.main, 0.4), bgColor: theme.palette.blue.light },
    2: { textColor: lighten(theme.palette.info.main, 0.3), bgColor: theme.palette.info.light },
    3: { textColor: lighten(theme.palette.info.main, 0.3), bgColor: theme.palette.info.light },
    4: {
      textColor: lighten(theme.palette.warning.main, 0.2),
      bgColor: theme.palette.warning.light,
    },
    5: {
      textColor: lighten(theme.palette.warning.main, 0.2),
      bgColor: theme.palette.warning.light,
    },
    6: {
      textColor: lighten(theme.palette.warning.main, 0.2),
      bgColor: theme.palette.warning.light,
    },
    undefined: {
      textColor: lighten(theme.palette.warning.main, 0.2),
      bgColor: theme.palette.black.main,
    },
  };
};

const PropertyTable = (props: IPropertyTable) => {
  const [totalCount, setTotalCount] = useState(0);
  const api = usePimsApi();
  const { navigateAndSetFrom } = useHistoryAwareNavigate();
  const snackbar = useContext(SnackBarContext);
  const lookup = useContext(LookupContext);

  const classification = useClassificationStyle();
  const theme = useTheme();

  const agenciesForFilter = useMemo(() => {
    if (lookup.data) {
      return lookup.data.Agencies.map((a) => a.Name);
    } else {
      return [];
    }
  }, [lookup.data]);

  const classificationForFilter = useMemo(() => {
    if (lookup.data) {
      return lookup.data.Classifications.map((a) => a.Name);
    } else {
      return [];
    }
  }, [lookup.data]);

  const adminAreasForFilter = useMemo(() => {
    if (lookup.data) {
      return lookup.data.AdministrativeAreas.map((a) => a.Name);
    } else {
      return [];
    }
  }, [lookup.data]);

  const projectStatusesForFilter = useMemo(() => {
    if (lookup.data) {
      return lookup.data.ProjectStatuses.map((a) => a.Name);
    } else {
      return [];
    }
  }, [lookup.data]);

  const columns: GridColDef[] = [
    {
      field: 'PropertyType',
      headerName: 'Type',
      flex: 1,
      maxWidth: 130,
      type: 'singleSelect',
      valueOptions: ['Parcel', 'Building'],
    },
    {
      field: 'Classification',
      headerName: 'Classification',
      flex: 1,
      minWidth: 200,
      valueOptions: classificationForFilter,
      type: 'singleSelect',
      renderHeader: (params) => {
        return (
          <Tooltip
            title={
              <Box display={'flex'} flexDirection={'column'} gap={'4px'}>
                <ClassificationInline
                  title="Core operational"
                  color={lighten(theme.palette.success.main, 0.3)}
                  backgroundColor={theme.palette.success.light}
                />
                <ClassificationInline
                  title="Core strategic"
                  color={lighten(theme.palette.blue.main, 0.4)}
                  backgroundColor={theme.palette.blue.light}
                />
                <ClassificationInline
                  title="Surplus"
                  color={lighten(theme.palette.info.main, 0.3)}
                  backgroundColor={theme.palette.info.light}
                />
                <ClassificationInline
                  title="Disposed"
                  color={lighten(theme.palette.warning.main, 0.2)}
                  backgroundColor={theme.palette.warning.light}
                />
              </Box>
            }
          >
            <div>
              <GridColumnHeaderTitle columnWidth={0} label={'Classification'} {...params} />
            </div>
          </Tooltip>
        );
      },
      renderCell: (params) => {
        return (
          <ClassificationInline
            color={classification[params.row.ClassificationId].textColor}
            backgroundColor={classification[params.row.ClassificationId].bgColor}
            title={params.row.Classification ?? ''}
          />
        );
      },
    },
    {
      field: 'ProjectStatus',
      headerName: 'Project Status',
      type: 'singleSelect',
      flex: 1,
      maxWidth: 200,
      valueOptions: projectStatusesForFilter,
    },
    {
      field: 'PID',
      headerName: 'PID',
      flex: 1,
      maxWidth: 150,
      // This odd logic is to allow for search with or without hyphens.
      // It concatinates a non-hyphenated and hyphenated version together for searching, then uses the second for presentation.
      valueGetter: (value: number | null) =>
        value ? `${zeroPadPID(value)},${pidFormatter(zeroPadPID(value))}` : 'N/A',
      renderCell: (params) => (params.value !== 'N/A' ? params.value.split(',').at(1) : 'N/A'),
    },
    {
      field: 'Agency',
      headerName: 'Agency',
      flex: 1,
      type: 'singleSelect',
      valueOptions: agenciesForFilter,
    },
    {
      field: 'Address',
      headerName: 'Main Address',
      flex: 1,
    },
    {
      field: 'AdministrativeArea',
      headerName: 'Administrative Area',
      flex: 1,
      type: 'singleSelect',
      valueOptions: adminAreasForFilter,
    },
    {
      field: 'LandArea',
      headerName: 'Land Area',
      width: 120,
      valueFormatter: (value) => (value ? `${(value as number).toFixed(2)} ha` : ''),
    },
    {
      field: 'UpdatedOn',
      headerName: 'Updated On',
      flex: 1,
      maxWidth: 125,
      valueFormatter: (value) => dateFormatter(value),
      type: 'date',
    },
  ];

  const selectPresetFilter = (value: string, ref: MutableRefObject<GridApiCommunity>) => {
    switch (value) {
      case 'All Properties':
        ref.current.setFilterModel({ items: [] });
        break;
      case 'Building':
      case 'Parcel':
        ref.current.setFilterModel({
          items: [{ value, operator: 'is', field: 'PropertyType' }],
        });
        break;
      case 'Core':
        ref.current.setFilterModel({
          items: [
            {
              value: ['Core Operational', 'Core Strategic'],
              operator: 'isAnyOf',
              field: 'Classification',
            },
          ],
        });
        break;
      case 'Surplus':
        ref.current.setFilterModel({
          items: [
            {
              value: ['Surplus Active', 'Surplus Encumbered'],
              operator: 'isAnyOf',
              field: 'Classification',
            },
          ],
        });
        break;
      case 'Disposed':
        ref.current.setFilterModel({
          items: [{ value, operator: 'is', field: 'Classification' }],
        });
        break;
      case 'Approved for ERP':
        ref.current.setFilterModel({
          items: [{ value: value, operator: 'is', field: 'ProjectStatus' }],
        });
        break;
      default:
        ref.current.setFilterModel({ items: [] });
    }
  };

  const excelDataMap = (data: ((Parcel | Building) & { ProjectNumber: string })[]) => {
    return data.map((property) => {
      return {
        Type: propertyTypeMapper(property.PropertyTypeId),
        Classification: lookup.getLookupValueById('Classifications', property.ClassificationId)
          ?.Name,
        Name: property.Name,
        Description: property.Description,
        Ministry: lookup.getLookupValueById('Agencies', property.AgencyId)?.ParentId
          ? lookup.data.Agencies.find(
              (a) => a.Id === lookup.getLookupValueById('Agencies', property.AgencyId)?.ParentId,
            )?.Name
          : lookup.getLookupValueById('Agencies', property.AgencyId)?.Name,
        Agency: lookup.getLookupValueById('Agencies', property.AgencyId)?.Name,
        Address: property.Address1,
        'Administrative Area': lookup.getLookupValueById(
          'AdministrativeAreas',
          property.AdministrativeAreaId,
        )?.Name,
        Postal: property.Postal,
        PID: pidFormatter(property.PID),
        PIN: property.PIN,
        'Is Sensitive': property.IsSensitive,
        'Assessed Value': property.Evaluations?.length
          ? property.Evaluations.sort(
              (
                a: ParcelEvaluation | BuildingEvaluation,
                b: ParcelEvaluation | BuildingEvaluation,
              ) => b.Year - a.Year,
            ).at(0).Value
          : '',
        'Assessment Year': property.Evaluations?.length
          ? property.Evaluations.sort(
              (
                a: ParcelEvaluation | BuildingEvaluation,
                b: ParcelEvaluation | BuildingEvaluation,
              ) => b.Year - a.Year,
            ).at(0).Year
          : '',
        'Netbook Value': property.Fiscals?.length
          ? property.Fiscals.sort(
              (a: ParcelFiscal | BuildingFiscal, b: ParcelFiscal | BuildingFiscal) =>
                b.FiscalYear - a.FiscalYear,
            ).at(0).Value
          : '',
        'Netbook Year': property.Fiscals?.length
          ? property.Fiscals.sort(
              (a: ParcelFiscal | BuildingFiscal, b: ParcelFiscal | BuildingFiscal) =>
                b.FiscalYear - a.FiscalYear,
            ).at(0).FiscalYear
          : '',
        'Parcel Land Area':
          property.PropertyTypeId === PropertyTypes.LAND ? (property as Parcel).LandArea : '',
        'Building Total Area':
          property.PropertyTypeId === PropertyTypes.BUILDING
            ? (property as Building).TotalArea
            : '',
        'Building Predominate Use':
          property.PropertyTypeId === PropertyTypes.BUILDING
            ? lookup.getLookupValueById(
                'PredominateUses',
                (property as Building).BuildingPredominateUseId,
              )?.Name
            : '',
        'Building Construction Type':
          property.PropertyTypeId === PropertyTypes.BUILDING
            ? lookup.getLookupValueById(
                'ConstructionTypes',
                (property as Building).BuildingConstructionTypeId,
              )?.Name
            : '',
        'Building Tenancy':
          property.PropertyTypeId === PropertyTypes.BUILDING
            ? (property as Building).BuildingTenancy
            : '',
        Latitude: property.Location.y, // This seems backwards, but it's how the database stores it.
        Longitude: property.Location.x,
        'Active Project Number': property.ProjectNumber,
      };
    });
  };

  const handleDataChange = async (filter: CommonFiltering, signal: AbortSignal): Promise<any[]> => {
    try {
      const { data, totalCount } = await api.properties.propertiesDataSource(filter, signal);
      setTotalCount(totalCount);
      return data;
    } catch {
      snackbar.setMessageState({
        open: true,
        text: 'Error loading properties.',
        style: snackbar.styles.warning,
      });
      return [];
    }
  };

  return (
    <Box
      sx={
        {
          padding: '24px',
          height: 'fit-content',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
        } as SxProps
      }
    >
      <Box height={'calc(100vh - 180px)'}>
        <FilterSearchDataGrid
          name="properties"
          dataSource={handleDataChange}
          excelDataSource={api.properties.getPropertiesForExcelExport}
          onPresetFilterChange={selectPresetFilter}
          getRowId={(row) => `${row.Id}_${row.PropertyType}`}
          defaultFilter={'All Properties'}
          tableOperationMode="server"
          onRowClick={props.rowClickHandler}
          onAddButtonClick={() => navigateAndSetFrom('add')}
          presetFilterSelectOptions={[
            <CustomMenuItem key={'All Properties'} value={'All Properties'}>
              All Properties
            </CustomMenuItem>,
            <CustomListSubheader key={'Type'}>Property Type</CustomListSubheader>,
            <CustomMenuItem key={'Building'} value={'Building'}>
              Building
            </CustomMenuItem>,
            <CustomMenuItem key={'Parcel'} value={'Parcel'}>
              Parcel
            </CustomMenuItem>,
            <CustomListSubheader key={'Group'}>Classification Group</CustomListSubheader>,
            <CustomMenuItem key={'Core'} value={'Core'}>
              Core
            </CustomMenuItem>,
            <CustomMenuItem key={'Surplus'} value={'Surplus'}>
              Surplus
            </CustomMenuItem>,
            <CustomMenuItem key={'Disposed'} value={'Disposed'}>
              Disposed
            </CustomMenuItem>,
            <CustomListSubheader key={'Other'}>Other</CustomListSubheader>,
            <CustomMenuItem key={'ERP'} value={'Approved for ERP'}>
              In ERP Project
            </CustomMenuItem>,
          ]}
          tableHeader={'Properties Overview'}
          rowCountProp={totalCount}
          rowCount={totalCount}
          getRowClassName={(params) =>
            exposedProjectStatuses.includes(params.row.ProjectStatusId) ? 'erp-property-row' : ''
          }
          excelTitle={'Properties'}
          customExcelMap={excelDataMap}
          columns={columns}
          addTooltip="Create New Property"
          initialState={{
            columns: {
              columnVisibilityModel: {
                ProjectStatus: false,
              },
            },
          }}
        />
      </Box>
    </Box>
  );
};

export default PropertyTable;