From 9a96d4de33151a4a9342a3153b6e6347b5ee0a01 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 13 Aug 2025 09:27:27 -0700 Subject: [PATCH 01/14] include tooltip in textformfield and autocompleteformfield --- .../components/form/AutocompleteFormField.tsx | 15 +++++++++- .../src/components/form/TextFormField.tsx | 30 ++++++++++++++++--- .../src/components/projects/ProjectForms.tsx | 16 ++++------ 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/react-app/src/components/form/AutocompleteFormField.tsx b/react-app/src/components/form/AutocompleteFormField.tsx index 5d51a1cf1..c98e87175 100644 --- a/react-app/src/components/form/AutocompleteFormField.tsx +++ b/react-app/src/components/form/AutocompleteFormField.tsx @@ -8,13 +8,16 @@ import { FilterOptionsState, ListItemText, AutocompleteProps, + Tooltip, } from '@mui/material'; import { ISelectMenuItem } from './SelectFormField'; import { Controller, useFormContext } from 'react-hook-form'; +import { Help } from '@mui/icons-material'; type AutocompleteFormProps = { name: string; label: string | JSX.Element; + tooltip?: string; required?: boolean; allowNestedIndent?: boolean; disableOptionsFunction?: (option: ISelectMenuItem) => boolean; @@ -39,6 +42,7 @@ const AutocompleteFormField = ( name, options, label, + tooltip, sx, required, allowNestedIndent, @@ -105,7 +109,16 @@ const AutocompleteFormField = ( {...params} error={!!formState.errors?.[name]} required={required} - label={label} + label={ + + {`${label} `} + {tooltip && ( + + + + )} + + } helperText={formState.errors?.[name] ? 'This field is required.' : undefined} /> )} diff --git a/react-app/src/components/form/TextFormField.tsx b/react-app/src/components/form/TextFormField.tsx index 63ae1bdbe..d68cd50c1 100644 --- a/react-app/src/components/form/TextFormField.tsx +++ b/react-app/src/components/form/TextFormField.tsx @@ -1,12 +1,14 @@ import React from 'react'; -import { TextField, TextFieldProps } from '@mui/material'; +import { Box, TextField, TextFieldProps, Tooltip } from '@mui/material'; import { Controller, FieldValues, RegisterOptions, useFormContext } from 'react-hook-form'; import { pidFormatter } from '@/utilities/formatters'; +import { Help } from '@mui/icons-material'; type TextFormFieldProps = { defaultVal?: string; name: string; - label: string; + label: string | JSX.Element; + tooltip?: string; numeric?: boolean; isPid?: boolean; rules?: Omit< @@ -17,7 +19,18 @@ type TextFormFieldProps = { const TextFormField = (props: TextFormFieldProps) => { const { control, setValue } = useFormContext(); - const { name, label, rules, numeric, isPid, defaultVal, disabled, onBlur, ...restProps } = props; + const { + name, + label, + rules, + numeric, + isPid, + defaultVal, + disabled, + onBlur, + tooltip, + ...restProps + } = props; return ( { }} value={value ?? defaultVal} fullWidth - label={label} + label={ + + {`${label} `} + {tooltip && ( + + + + )} + + } type={'text'} error={!!error && !!error.message} helperText={error?.message} diff --git a/react-app/src/components/projects/ProjectForms.tsx b/react-app/src/components/projects/ProjectForms.tsx index 63ba9aea8..ecf4c75ff 100644 --- a/react-app/src/components/projects/ProjectForms.tsx +++ b/react-app/src/components/projects/ProjectForms.tsx @@ -1,4 +1,4 @@ -import { Box, Grid, InputAdornment, Tooltip, Typography } from '@mui/material'; +import { Grid, InputAdornment, Typography } from '@mui/material'; import React, { useContext } from 'react'; import AutocompleteFormField from '../form/AutocompleteFormField'; import { UserContext } from '@/contexts/userContext'; @@ -10,7 +10,6 @@ import { Roles } from '@/constants/roles'; import { useFormContext } from 'react-hook-form'; import { formatFiscalYear } from '@/utilities/formatters'; import { generateNumberList } from '@/utilities/helperFunctions'; -import Help from '@mui/icons-material/Help'; interface IProjectGeneralInfoForm { projectStatuses: ISelectMenuItem[]; @@ -103,14 +102,8 @@ export const ProjectGeneralInfoForm = (props: IProjectGeneralInfoForm) => { - Risk Level - - - - - } + label={'Risk Level'} + tooltip="The risk associated with completion of the sale of a property during the forecasted fiscal year. Risk status on property sales can change through the sales process." required options={ lookupData?.Risks.map((risk) => ({ @@ -214,7 +207,8 @@ export const ProjectFinancialInfoForm = () => { numeric fullWidth name={'ProgramCost'} - label={'Estimated program recovery fees'} + label={'Estimated Program Cost'} + tooltip="1% of Net Proceeds if Tier 2+" /> From d201ab5ed38ffc2496be7632a108804e25768d6f Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 13 Aug 2025 09:28:40 -0700 Subject: [PATCH 02/14] use existing financial form fields component -> add project --- .../src/components/projects/AddProject.tsx | 93 +------------------ 1 file changed, 3 insertions(+), 90 deletions(-) diff --git a/react-app/src/components/projects/AddProject.tsx b/react-app/src/components/projects/AddProject.tsx index 24dd9d409..77f707497 100644 --- a/react-app/src/components/projects/AddProject.tsx +++ b/react-app/src/components/projects/AddProject.tsx @@ -1,4 +1,4 @@ -import { Box, Grid, InputAdornment, Typography, useTheme } from '@mui/material'; +import { Box, Grid, Typography, useTheme } from '@mui/material'; import { useContext, useEffect, useState } from 'react'; import { NavigateBackButton } from '../display/DetailViewNavigation'; import TextFormField from '../form/TextFormField'; @@ -17,6 +17,7 @@ import { MonetaryType } from '@/constants/monetaryTypes'; import { NoteTypes } from '@/constants/noteTypes'; import useHistoryAwareNavigate from '@/hooks/useHistoryAwareNavigate'; import { getFiscalYear } from '@/utilities/helperFunctions'; +import { ProjectFinancialInfoForm } from '@/components/projects/ProjectForms'; const AddProject = () => { const { goToFromStateOrSetRoute } = useHistoryAwareNavigate(); @@ -123,95 +124,7 @@ const AddProject = () => { )} Financial Information - - - $, - }} - fullWidth - numeric - name={'Assessed'} - label={'Assessed value'} - rules={{ - min: { - value: 0.01, - message: 'Must be greater than 0.', - }, - }} - required - /> - - - $, - }} - fullWidth - numeric - name={'NetBook'} - label={'Net Book Value'} - rules={{ - min: { - value: 0.01, - message: 'Must be greater than 0.', - }, - }} - required - /> - - - $, - }} - numeric - fullWidth - name={'Market'} - label={'Estimated market value'} - rules={{ - min: { - value: 0.01, - message: 'Must be greater than 0.', - }, - }} - required - /> - - - $, - }} - numeric - fullWidth - name={'Appraised'} - label={'Appraised value'} - /> - - - $, - }} - numeric - fullWidth - name={'SalesCost'} - label={'Estimated sales cost'} - /> - - - $, - }} - numeric - fullWidth - name={'ProgramCost'} - label={'Estimated program recovery fees'} - /> - - + Documentation {tasksForAddState.map((task, idx) => ( From 0118632380da6eb9644dae9d0c2c7937b0c4ce28 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 13 Aug 2025 11:48:28 -0700 Subject: [PATCH 03/14] consolidate form field label component --- .../src/components/common/FormFieldLabel.tsx | 24 +++++++++++++++++++ .../components/form/AutocompleteFormField.tsx | 16 +++---------- .../src/components/form/TextFormField.tsx | 17 ++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 react-app/src/components/common/FormFieldLabel.tsx diff --git a/react-app/src/components/common/FormFieldLabel.tsx b/react-app/src/components/common/FormFieldLabel.tsx new file mode 100644 index 000000000..57063f601 --- /dev/null +++ b/react-app/src/components/common/FormFieldLabel.tsx @@ -0,0 +1,24 @@ +import Help from '@mui/icons-material/Help'; +import { Box, Tooltip } from '@mui/material'; +import React from 'react'; + +interface FormFieldLabelProps { + label: string; + tooltip?: string; +} + +const FormFieldLabel = (props: FormFieldLabelProps) => { + const { label, tooltip } = props; + return ( + + {`${label} `} + {tooltip && ( + + + + )} + + ); +}; + +export default FormFieldLabel; diff --git a/react-app/src/components/form/AutocompleteFormField.tsx b/react-app/src/components/form/AutocompleteFormField.tsx index c98e87175..e4221d680 100644 --- a/react-app/src/components/form/AutocompleteFormField.tsx +++ b/react-app/src/components/form/AutocompleteFormField.tsx @@ -8,15 +8,14 @@ import { FilterOptionsState, ListItemText, AutocompleteProps, - Tooltip, } from '@mui/material'; import { ISelectMenuItem } from './SelectFormField'; import { Controller, useFormContext } from 'react-hook-form'; -import { Help } from '@mui/icons-material'; +import FormFieldLabel from '@/components/common/FormFieldLabel'; type AutocompleteFormProps = { name: string; - label: string | JSX.Element; + label: string; tooltip?: string; required?: boolean; allowNestedIndent?: boolean; @@ -109,16 +108,7 @@ const AutocompleteFormField = ( {...params} error={!!formState.errors?.[name]} required={required} - label={ - - {`${label} `} - {tooltip && ( - - - - )} - - } + label={} helperText={formState.errors?.[name] ? 'This field is required.' : undefined} /> )} diff --git a/react-app/src/components/form/TextFormField.tsx b/react-app/src/components/form/TextFormField.tsx index d68cd50c1..9b8e9b094 100644 --- a/react-app/src/components/form/TextFormField.tsx +++ b/react-app/src/components/form/TextFormField.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { Box, TextField, TextFieldProps, Tooltip } from '@mui/material'; +import { TextField, TextFieldProps } from '@mui/material'; import { Controller, FieldValues, RegisterOptions, useFormContext } from 'react-hook-form'; import { pidFormatter } from '@/utilities/formatters'; -import { Help } from '@mui/icons-material'; +import FormFieldLabel from '@/components/common/FormFieldLabel'; type TextFormFieldProps = { defaultVal?: string; name: string; - label: string | JSX.Element; + label: string; tooltip?: string; numeric?: boolean; isPid?: boolean; @@ -66,16 +66,7 @@ const TextFormField = (props: TextFormFieldProps) => { }} value={value ?? defaultVal} fullWidth - label={ - - {`${label} `} - {tooltip && ( - - - - )} - - } + label={} type={'text'} error={!!error && !!error.message} helperText={error?.message} From c7bd4c1dfc776ce34e07dada170df53c9ebb0065 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 13 Aug 2025 11:53:36 -0700 Subject: [PATCH 04/14] Remove program cost from main financial dialog --- react-app/src/components/projects/ProjectDialog.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/react-app/src/components/projects/ProjectDialog.tsx b/react-app/src/components/projects/ProjectDialog.tsx index fb7ec2af9..cf67f1a8c 100644 --- a/react-app/src/components/projects/ProjectDialog.tsx +++ b/react-app/src/components/projects/ProjectDialog.tsx @@ -270,6 +270,7 @@ export const ProjectGeneralInfoDialog = (props: IProjectGeneralInfoDialog) => { key={`${mon.Id}-${idx}`} name={`Monetaries.${idx}.Value`} label={columnNameFormatter(mon.Name)} + tooltip={mon.Description} /> ))} @@ -330,7 +331,6 @@ export const ProjectFinancialDialog = (props: IProjectFinancialDialog) => { Market: 0, Appraised: 0, SalesCost: 0, - ProgramCost: 0, }, }); @@ -343,9 +343,6 @@ export const ProjectFinancialDialog = (props: IProjectFinancialDialog) => { SalesCost: initialValues?.Monetaries?.find((a) => a.MonetaryTypeId === MonetaryType.SALES_COST) ?.Value ?? 0, - ProgramCost: - initialValues?.Monetaries?.find((a) => a.MonetaryTypeId === MonetaryType.PROGRAM_COST) - ?.Value ?? 0, }); }, [initialValues, lookupData]); return ( @@ -356,7 +353,7 @@ export const ProjectFinancialDialog = (props: IProjectFinancialDialog) => { onConfirm={async () => { const isValid = await financialFormMethods.trigger(); if (isValid) { - const { Assessed, NetBook, Market, Appraised, ProgramCost, SalesCost } = + const { Assessed, NetBook, Market, Appraised, SalesCost } = financialFormMethods.getValues(); submit(initialValues.Id, { Id: initialValues.Id, @@ -369,10 +366,6 @@ export const ProjectFinancialDialog = (props: IProjectFinancialDialog) => { MonetaryTypeId: MonetaryType.SALES_COST, Value: SalesCost, }, - { - MonetaryTypeId: MonetaryType.PROGRAM_COST, - Value: ProgramCost, - }, ], ProjectProperties: initialValues.ProjectProperties, }).then(() => postSubmit()); From ce4c08789810a00ba536db509a242af11feef84d Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 13 Aug 2025 11:53:50 -0700 Subject: [PATCH 05/14] remove program cost from new project page --- react-app/src/components/projects/AddProject.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/react-app/src/components/projects/AddProject.tsx b/react-app/src/components/projects/AddProject.tsx index 77f707497..c53c96a04 100644 --- a/react-app/src/components/projects/AddProject.tsx +++ b/react-app/src/components/projects/AddProject.tsx @@ -30,7 +30,6 @@ const AddProject = () => { NetBook: 0, Market: 0, Appraised: 0, - ProgramCost: 0, SalesCost: 0, ExemptionNote: '', Approval: false, @@ -191,7 +190,6 @@ const AddProject = () => { ReportedFiscalYear: getFiscalYear(), ActualFiscalYear: getFiscalYear(), Monetaries: [ - { MonetaryTypeId: MonetaryType.PROGRAM_COST, Value: formValues.ProgramCost }, { MonetaryTypeId: MonetaryType.SALES_COST, Value: formValues.SalesCost }, ], Tasks: formValues.Tasks.filter((a) => a.IsCompleted), From 3794a4abf58fea331e3e5af1c19e5c481c88946a Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 13 Aug 2025 11:54:23 -0700 Subject: [PATCH 06/14] remove program cost from project financial form --- .../src/components/projects/ProjectForms.tsx | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/react-app/src/components/projects/ProjectForms.tsx b/react-app/src/components/projects/ProjectForms.tsx index ecf4c75ff..5c83ef450 100644 --- a/react-app/src/components/projects/ProjectForms.tsx +++ b/react-app/src/components/projects/ProjectForms.tsx @@ -177,17 +177,6 @@ export const ProjectFinancialInfoForm = () => { required /> - - $, - }} - numeric - fullWidth - name={'Appraised'} - label={'Appraised value'} - /> - { fullWidth name={'SalesCost'} label={'Estimated sales cost'} + rules={{ + min: { + value: 0, + message: 'Must be 0 or greater.', + }, + }} + required /> @@ -206,9 +202,8 @@ export const ProjectFinancialInfoForm = () => { }} numeric fullWidth - name={'ProgramCost'} - label={'Estimated Program Cost'} - tooltip="1% of Net Proceeds if Tier 2+" + name={'Appraised'} + label={'Appraised value'} /> From e9f169cbfe2df52c4dede3ef8b598b740b899369 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 13 Aug 2025 11:54:59 -0700 Subject: [PATCH 07/14] remove outdated project metadata typing --- .../src/components/projects/ProjectDetail.tsx | 3 -- react-app/src/hooks/api/useProjectsApi.ts | 52 ------------------- 2 files changed, 55 deletions(-) diff --git a/react-app/src/components/projects/ProjectDetail.tsx b/react-app/src/components/projects/ProjectDetail.tsx index 0f53c4494..40d52cc3e 100644 --- a/react-app/src/components/projects/ProjectDetail.tsx +++ b/react-app/src/components/projects/ProjectDetail.tsx @@ -16,7 +16,6 @@ import usePimsApi from '@/hooks/usePimsApi'; import useDataLoader from '@/hooks/useDataLoader'; import { Project, - ProjectMetadata, ProjectMonetary, ProjectNote, ProjectTask, @@ -71,8 +70,6 @@ interface ProjectInfo extends Project { NetBookValue: number; EstimatedMarketValue: number; AppraisedValue: number; - EstimatedSalesCost: ProjectMetadata; - EstimatedProgramRecoveryFees: ProjectMetadata; SurplusDeclaration: boolean; TripleBottom: boolean; 'Fiscal Year of Disposal': number; diff --git a/react-app/src/hooks/api/useProjectsApi.ts b/react-app/src/hooks/api/useProjectsApi.ts index 0313234ca..59e53bc57 100644 --- a/react-app/src/hooks/api/useProjectsApi.ts +++ b/react-app/src/hooks/api/useProjectsApi.ts @@ -68,7 +68,6 @@ export interface Project { CreatedBy?: User; UpdatedOn?: string; UpdatedBy?: User; - Metadata?: ProjectMetadata; Tasks?: ProjectTask[]; Notifications?: ProjectNotification[]; StatusHistory?: ProjectStatusHistory[]; @@ -178,57 +177,6 @@ export interface ProjectAgencyResponse { Note: string | null; } -export interface ProjectMetadata { - // Exemption Fields - exemptionRequested?: boolean; - exemptionApprovedOn?: Date; - // ERP Fields - initialNotificationSentOn?: Date; - thirtyDayNotificationSentOn?: Date; - sixtyDayNotificationSentOn?: Date; - ninetyDayNotificationSentOn?: Date; - onHoldNotificationSentOn?: Date; - interestReceivedOn?: Date; - transferredWithinGreOn?: Date; - clearanceNotificationSentOn?: Date; - // SPL Fields - requestForSplReceivedOn?: Date; - approvedForSplOn?: Date; - marketedOn?: Date; - purchaser?: string; - offerAcceptedOn?: Date; - adjustedOn?: Date; - preliminaryFormSignedOn?: Date; - finalFormSignedOn?: Date; - priorYearAdjustmentOn?: Date; - disposedOn?: Date; - // Removing from SPL - removalFromSplRequestOn?: Date; - removalFromSplApprovedOn?: Date; - // Financials - assessedOn?: Date; - appraisedBy?: string; - appraisedOn?: Date; - salesCost?: number; - netProceeds?: number; - programCost?: number; - gainLoss?: number; - sppCapitalization?: number; - gainBeforeSpl?: number; - ocgFinancialStatement?: number; - interestComponent?: number; - plannedFutureUse?: string; - offerAmount?: number; - saleWithLeaseInPlace?: boolean; - priorYearAdjustment?: boolean; - priorYearAdjustmentAmount?: number; - realtor?: string; - realtorRate?: string; - realtorCommission?: number; - preliminaryFormSignedBy?: string; - finalFormSignedBy?: string; -} - // All values optional because it's not clear what filter will be used. export interface ProjectTask { CompletedOn?: Date; From e10c5db7c066e5ef6d7aa4457db030bbf8ce8b57 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Thu, 14 Aug 2025 11:26:20 -0700 Subject: [PATCH 08/14] add enum for monetary type --- express-api/src/constants/projectMonetaryType.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 express-api/src/constants/projectMonetaryType.ts diff --git a/express-api/src/constants/projectMonetaryType.ts b/express-api/src/constants/projectMonetaryType.ts new file mode 100644 index 000000000..32ceb1877 --- /dev/null +++ b/express-api/src/constants/projectMonetaryType.ts @@ -0,0 +1,11 @@ +export enum ProjectMonetaryType { + GAIN_BEFORE_SPL = 1, + INTEREST_COMPONENT = 2, + NET_PROCEEDS = 3, + OCG_GAIN_LOSS = 4, + OCG_FINANCIAL_STATEMENT = 5, + OFFER_AMOUNT = 6, + PROGRAM_COST = 7, + REALTOR_COMMISION = 8, + SALES_COST = 9, +} From 2f2233f9eb1ce5d9392741eb1ea4b191ab96d2f7 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Thu, 14 Aug 2025 11:28:46 -0700 Subject: [PATCH 09/14] add is calculated column to monetary types --- .../src/typeorm/Entities/MonetaryType.ts | 3 +++ .../1755124066594-IsCalculatedAddition.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 express-api/src/typeorm/Migrations/1755124066594-IsCalculatedAddition.ts diff --git a/express-api/src/typeorm/Entities/MonetaryType.ts b/express-api/src/typeorm/Entities/MonetaryType.ts index 7ff47a51e..edb008b1a 100644 --- a/express-api/src/typeorm/Entities/MonetaryType.ts +++ b/express-api/src/typeorm/Entities/MonetaryType.ts @@ -22,6 +22,9 @@ export class MonetaryType extends BaseEntity { @Column('boolean') IsOptional: boolean; + @Column('boolean', { default: false }) + IsCalculated: boolean; + // Status Relation @Column({ name: 'status_id', type: 'int', nullable: true }) StatusId: number; diff --git a/express-api/src/typeorm/Migrations/1755124066594-IsCalculatedAddition.ts b/express-api/src/typeorm/Migrations/1755124066594-IsCalculatedAddition.ts new file mode 100644 index 000000000..96cd3c097 --- /dev/null +++ b/express-api/src/typeorm/Migrations/1755124066594-IsCalculatedAddition.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class IsCalculatedAddition1755124066594 implements MigrationInterface { + name = 'IsCalculatedAddition1755124066594'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "monetary_type" ADD "is_calculated" boolean NOT NULL DEFAULT false`, + ); + + await queryRunner.query( + "UPDATE monetary_type SET is_calculated = true WHERE name = 'ProgramCost'", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "monetary_type" DROP COLUMN "is_calculated"`); + } +} From 6fa1445bbaacef75b67a9622cb36359549a748db Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Thu, 14 Aug 2025 11:33:52 -0700 Subject: [PATCH 10/14] handle autocalculated value in project monetary --- .../src/services/projects/projectsServices.ts | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/express-api/src/services/projects/projectsServices.ts b/express-api/src/services/projects/projectsServices.ts index a6d627aaa..af74b2874 100644 --- a/express-api/src/services/projects/projectsServices.ts +++ b/express-api/src/services/projects/projectsServices.ts @@ -48,6 +48,8 @@ import { Roles } from '@/constants/roles'; import { PimsRequestUser } from '@/middleware/userAuthCheck'; import { User } from '@/typeorm/Entities/User'; import { ProjectStatusNotification } from '@/typeorm/Entities/ProjectStatusNotification'; +import { ProjectMonetaryType } from '@/constants/projectMonetaryType'; +import { MonetaryType } from '@/typeorm/Entities/MonetaryType'; const projectRepo = AppDataSource.getRepository(Project); @@ -606,7 +608,18 @@ const handleProjectMonetary = async ( queryRunner: QueryRunner, ) => { if (newProject?.Monetaries?.length) { - const saveTimestamps = newProject.Monetaries.map( + // Remove values on incoming project that are pre-calculated fields. We don't save those in case they were manually tampered with. + const preCalculatedFields = ( + await queryRunner.manager.find(MonetaryType, { + where: { IsCalculated: true }, + }) + ).map((m) => m.Id); + const remainingMonetaries = newProject.Monetaries?.filter( + (m) => !preCalculatedFields.includes(m.MonetaryTypeId), + ); + + // Save the remaining monetaries that are not pre-calculated. + const saveMonetaries = remainingMonetaries.map( async (monetary): Promise => { if (monetary.MonetaryTypeId == null) { throw new ErrorWithCode('Provided monetary was missing a required field.', 400); @@ -627,7 +640,35 @@ const handleProjectMonetary = async ( ); }, ); - return Promise.all(saveTimestamps); + + // Some values are autocalculated based on the existing monetaries. + // If the monetaries that trigger these calculations were provided, we must recalculate them. + const calculatedMonetaries: Promise[] = []; + const netProceeds = newProject.Monetaries.find( + (m) => m.MonetaryTypeId === ProjectMonetaryType.NET_PROCEEDS, // Net Proceeds + ); + + if (netProceeds != null) { + const exists = await queryRunner.manager.findOne(ProjectMonetary, { + where: { ProjectId: newProject.Id, MonetaryTypeId: ProjectMonetaryType.PROGRAM_COST }, + }); + // Program Cost = Net Proceeds @ 1% if Project is Tier2+ + const programCost = newProject.TierLevelId >= 2 ? netProceeds.Value * 0.01 : 0; + calculatedMonetaries.push( + queryRunner.manager.upsert( + ProjectMonetary, + { + ProjectId: newProject.Id, + Value: programCost, + MonetaryTypeId: ProjectMonetaryType.PROGRAM_COST, + CreatedById: exists ? exists.CreatedById : newProject.CreatedById, + UpdatedById: exists ? newProject.UpdatedById : undefined, + }, + ['ProjectId', 'MonetaryTypeId'], + ), + ); + } + return Promise.all([...saveMonetaries, ...calculatedMonetaries]); } }; From 60e37a0b349d3bce88b3ec22d6f331937b53da43 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Thu, 14 Aug 2025 11:35:12 -0700 Subject: [PATCH 11/14] disable frontend field if calculated --- express-api/src/controllers/lookup/lookupController.ts | 1 + react-app/src/components/projects/ProjectDialog.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/express-api/src/controllers/lookup/lookupController.ts b/express-api/src/controllers/lookup/lookupController.ts index 26eec7272..134702ed6 100644 --- a/express-api/src/controllers/lookup/lookupController.ts +++ b/express-api/src/controllers/lookup/lookupController.ts @@ -245,6 +245,7 @@ export const lookupAll = async (req: Request, res: Response) => { IsOptional: true, Description: true, StatusId: true, + IsCalculated: true, }, order: { SortOrder: 'asc', diff --git a/react-app/src/components/projects/ProjectDialog.tsx b/react-app/src/components/projects/ProjectDialog.tsx index cf67f1a8c..0d4d90eed 100644 --- a/react-app/src/components/projects/ProjectDialog.tsx +++ b/react-app/src/components/projects/ProjectDialog.tsx @@ -271,6 +271,7 @@ export const ProjectGeneralInfoDialog = (props: IProjectGeneralInfoDialog) => { name={`Monetaries.${idx}.Value`} label={columnNameFormatter(mon.Name)} tooltip={mon.Description} + disabled={mon.IsCalculated} /> ))} From 541f194141fb473e162b808246a0218e5e6cc885 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Thu, 14 Aug 2025 11:37:53 -0700 Subject: [PATCH 12/14] remove cases where checks relied on name, not id, of lookup data --- react-app/src/components/projects/AddProject.tsx | 3 ++- react-app/src/components/projects/ProjectDetail.tsx | 2 +- react-app/src/components/projects/ProjectDialog.tsx | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/react-app/src/components/projects/AddProject.tsx b/react-app/src/components/projects/AddProject.tsx index c53c96a04..481af9e9c 100644 --- a/react-app/src/components/projects/AddProject.tsx +++ b/react-app/src/components/projects/AddProject.tsx @@ -18,6 +18,7 @@ import { NoteTypes } from '@/constants/noteTypes'; import useHistoryAwareNavigate from '@/hooks/useHistoryAwareNavigate'; import { getFiscalYear } from '@/utilities/helperFunctions'; import { ProjectFinancialInfoForm } from '@/components/projects/ProjectForms'; +import { ProjectStatus } from '@/constants/projectStatuses'; const AddProject = () => { const { goToFromStateOrSetRoute } = useHistoryAwareNavigate(); @@ -50,7 +51,7 @@ const AddProject = () => { return; } else { const defaultState = lookupData?.ProjectStatuses.find( - (a) => a.Name === 'Required Documentation', + (a) => a.Id === ProjectStatus.REQUIRED_DOCUMENTATION, ); const addTasks = lookupData?.Tasks.filter((task) => task.StatusId === defaultState.Id); addTasks.push({ diff --git a/react-app/src/components/projects/ProjectDetail.tsx b/react-app/src/components/projects/ProjectDetail.tsx index 40d52cc3e..6d0d911a4 100644 --- a/react-app/src/components/projects/ProjectDetail.tsx +++ b/react-app/src/components/projects/ProjectDetail.tsx @@ -404,7 +404,7 @@ const ProjectDetail = (props: IProjectDetail) => { onEdit={() => setOpenAgencyInterestDialog(true)} disableEdit={!isAdmin} > - {!data?.parsedBody.AgencyResponses?.length ? ( //TODO: Logic will depend on precense of agency responses + {!data?.parsedBody.AgencyResponses?.length ? ( No agencies registered. diff --git a/react-app/src/components/projects/ProjectDialog.tsx b/react-app/src/components/projects/ProjectDialog.tsx index 0d4d90eed..cc7aa35e2 100644 --- a/react-app/src/components/projects/ProjectDialog.tsx +++ b/react-app/src/components/projects/ProjectDialog.tsx @@ -29,6 +29,7 @@ import { MonetaryType } from '@/constants/monetaryTypes'; import BaseDialog from '../dialog/BaseDialog'; import { NotificationQueue } from '@/hooks/api/useProjectNotificationApi'; import useAgencyOptions from '@/hooks/useAgencyOptions'; +import { ProjectStatus } from '@/constants/projectStatuses'; interface IProjectGeneralInfoDialog { initialValues: Project; @@ -186,7 +187,9 @@ export const ProjectGeneralInfoDialog = (props: IProjectGeneralInfoDialog) => { }, [statusTypes, initialValues]); useEffect(() => { - setApprovedStatus(lookupData?.ProjectStatuses?.find((a) => a.Name === 'Approved for ERP')?.Id); + setApprovedStatus( + lookupData?.ProjectStatuses?.find((a) => a.Id === ProjectStatus.APPROVED_FOR_ERP)?.Id, + ); }, [lookupData]); const status = projectFormMethods.watch('StatusId'); const requireNotificationAcknowledge = From f4f2c9a76d417c9c9d2acc2f54e8c887d2ea96cc Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Sat, 20 Sep 2025 21:31:05 -0700 Subject: [PATCH 13/14] show all available monetary values --- .../src/components/projects/ProjectDetail.tsx | 25 ++++++++++--------- .../src/components/projects/ProjectsTable.tsx | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/react-app/src/components/projects/ProjectDetail.tsx b/react-app/src/components/projects/ProjectDetail.tsx index 6d0d911a4..d93e29669 100644 --- a/react-app/src/components/projects/ProjectDetail.tsx +++ b/react-app/src/components/projects/ProjectDetail.tsx @@ -238,19 +238,18 @@ const ProjectDetail = (props: IProjectDetail) => { }; const FinancialInformationData = useMemo(() => { - const salesCostType = lookupData?.MonetaryTypes?.find((a) => a.Name === 'SalesCost'); - const programCostType = lookupData?.MonetaryTypes?.find((a) => a.Name === 'ProgramCost'); + const monetaryEntries = {}; + lookupData?.MonetaryTypes?.forEach((mon) => { + monetaryEntries[mon.Name] = data?.parsedBody.Monetaries?.find( + (a) => a.MonetaryTypeId === mon.Id, + )?.Value; + }); return { AssessedValue: data?.parsedBody.Assessed, NetBookValue: data?.parsedBody.NetBook, EstimatedMarketValue: data?.parsedBody.Market, AppraisedValue: data?.parsedBody.Appraised, - EstimatedSalesCost: data?.parsedBody.Monetaries?.find( - (a) => a.MonetaryTypeId === salesCostType?.Id, - )?.Value, - EstimatedProgramRecoveryFees: data?.parsedBody.Monetaries?.find( - (a) => a.MonetaryTypeId === programCostType?.Id, - )?.Value, + ...monetaryEntries, }; }, [data, lookupData]); @@ -385,10 +384,12 @@ const ProjectDetail = (props: IProjectDetail) => { loading={isLoading} customFormatter={customFormatter} values={Object.fromEntries( - Object.entries(FinancialInformationData).map(([k, v]) => [ - k, - formatMoney(v != null ? v : 0), //This cast spaghetti sucks but hard to avoid when receiving money as a string from the API. - ]), + Object.entries(FinancialInformationData) + .map(([k, v]) => [ + k, + formatMoney(v != null ? v : 0), //This cast spaghetti sucks but hard to avoid when receiving money as a string from the API. + ]) + .sort(([a], [b]) => a.localeCompare(b)), )} title={financialInformation} id={financialInformation} diff --git a/react-app/src/components/projects/ProjectsTable.tsx b/react-app/src/components/projects/ProjectsTable.tsx index 908caf740..872f7ce90 100644 --- a/react-app/src/components/projects/ProjectsTable.tsx +++ b/react-app/src/components/projects/ProjectsTable.tsx @@ -165,7 +165,7 @@ const ProjectsTable = () => { 'Net Proceeds': project.Monetaries.find( (m) => m.MonetaryTypeId === MonetaryType.NET_PROCEEDS, )?.Value, - 'Program Cost': project.Monetaries.find( + 'Estimated Program Cost': project.Monetaries.find( (m) => m.MonetaryTypeId === MonetaryType.PROGRAM_COST, )?.Value, 'Gain Loss': project.Monetaries.find((m) => m.MonetaryTypeId === MonetaryType.OCG_GAIN_LOSS) From 06fb9c813c9288dee22dba6475a355d15ebe1f6f Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Mon, 27 Oct 2025 09:48:56 -0700 Subject: [PATCH 14/14] update tests --- express-api/tests/testUtils/factories.ts | 1 + .../tests/unit/services/projects/projectsServices.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/express-api/tests/testUtils/factories.ts b/express-api/tests/testUtils/factories.ts index 32f18793c..c54850334 100644 --- a/express-api/tests/testUtils/factories.ts +++ b/express-api/tests/testUtils/factories.ts @@ -589,6 +589,7 @@ export const produceMonetaryType = (): MonetaryType => { IsOptional: false, StatusId: faker.number.int(), Status: undefined, + IsCalculated: false, CreatedById: randomUUID(), CreatedBy: undefined, CreatedOn: new Date(), diff --git a/express-api/tests/unit/services/projects/projectsServices.test.ts b/express-api/tests/unit/services/projects/projectsServices.test.ts index 47f9395e9..27a5e5922 100644 --- a/express-api/tests/unit/services/projects/projectsServices.test.ts +++ b/express-api/tests/unit/services/projects/projectsServices.test.ts @@ -701,6 +701,7 @@ describe('UNIT - Project Services', () => { manager: { findOne: async () => monetary, upsert: async () => [monetary], + find: async () => [monetary], }, }; const result = await projectServices.handleProjectMonetary(project, queryRunner);