diff --git a/.github/workflows/test-containers.yaml b/.github/workflows/test-containers.yaml index 86190a505..2c86da8e3 100644 --- a/.github/workflows/test-containers.yaml +++ b/.github/workflows/test-containers.yaml @@ -81,7 +81,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Self-hosted Renovate - uses: renovatebot/github-action@v40.2.6 + uses: renovatebot/github-action@v40.2.7 with: configurationFile: ./.github/renovate.json token: ${{ secrets.RENOVATE_GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 16acc85c5..2f6e98108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,66 @@ +## [1.186.3](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.186.2...v1.186.3) (2024-08-28) + +### Bug Fixes + +- sa permission for communities source data ([cc1d2e3](https://github.com/bcgov/CONN-CCBC-portal/commit/cc1d2e3e17e82d7807d078d1895cbfdb36958f7c)) + +## [1.186.2](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.186.1...v1.186.2) (2024-08-28) + +### Bug Fixes + +- cbc household count validation ([9c203db](https://github.com/bcgov/CONN-CCBC-portal/commit/9c203db6d2a4e6f2a40d3d785137f2c62a0abc98)) + +## [1.186.1](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.186.0...v1.186.1) (2024-08-27) + +### Bug Fixes + +- analyst status to show in report correctly ([657bf9f](https://github.com/bcgov/CONN-CCBC-portal/commit/657bf9fc1818644ec2c8d409d8283bfa86b8aa2a)) +- filtering for ccbc gcpe report ([dcd2b52](https://github.com/bcgov/CONN-CCBC-portal/commit/dcd2b52ea8f43a82a259cdbfa76cd7e87d05e03e)) + +# [1.186.0](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.185.0...v1.186.0) (2024-08-27) + +### Bug Fixes + +- 500 error when visiting non-locations page ([b3a101c](https://github.com/bcgov/CONN-CCBC-portal/commit/b3a101c68f6ee5351f335e872b8583a8391dbce8)) +- add permissions for executing function ([6defd8f](https://github.com/bcgov/CONN-CCBC-portal/commit/6defd8f09689bdf6f9cfc320294c2953ca70e182)) +- cleanup of [section] to remove dataBySection ([35a85ee](https://github.com/bcgov/CONN-CCBC-portal/commit/35a85ee778573f92f526c8bff124c79abd40b43c)) +- clear button and inputs show correct values at all times ([abd9f20](https://github.com/bcgov/CONN-CCBC-portal/commit/abd9f20a15b2d15564a114f13f18ac4faa8b8542)) +- empty communities are not saved ([9c0c906](https://github.com/bcgov/CONN-CCBC-portal/commit/9c0c906e4b17c9e6e5105903bcfee4a74a7c0dc1)) +- geographic name options are by economic region and regional district ([a1c6927](https://github.com/bcgov/CONN-CCBC-portal/commit/a1c6927b67f89e81683716e0de8a39a4b0fd080d)) +- geographic name options are populated with only economic region ([40b9438](https://github.com/bcgov/CONN-CCBC-portal/commit/40b9438ef9793d5b83c178d9901e0eb587c8771d)) +- update on save instead of after refresh ([56b42f2](https://github.com/bcgov/CONN-CCBC-portal/commit/56b42f246d06b423ad279333199115fcccec035a)) +- user correct rowId ([378dcfc](https://github.com/bcgov/CONN-CCBC-portal/commit/378dcfcebef4b01a48e8da34ae92b03f3c2d3269)) + +### Features + +- add and remove community source behaves as expected ([ce70b29](https://github.com/bcgov/CONN-CCBC-portal/commit/ce70b29e6f100032f9c92b6f8d7023d18d0e10a3)) +- added community sources are read only ([39e3834](https://github.com/bcgov/CONN-CCBC-portal/commit/39e3834d2b75707dd0cdf0ebcc1379b3ede77869)) +- community source data is now displayed properly ([b8e811e](https://github.com/bcgov/CONN-CCBC-portal/commit/b8e811e456700752e0d18c9ca18ea835915c790c)) +- disable previously selected options ([7c810e3](https://github.com/bcgov/CONN-CCBC-portal/commit/7c810e3ace1a19e3da2217f28b9dd2a5dd3c0236)) +- section community source data behaves similar to quick edit ([b9d8e7c](https://github.com/bcgov/CONN-CCBC-portal/commit/b9d8e7c96d348515f99a3b3b8644a3dad618fa6b)) +- style buttons for add, remove, and clear ([35698ff](https://github.com/bcgov/CONN-CCBC-portal/commit/35698ff6ee1164d2a9e90828ce3d1a79193ecf24)) + +# [1.185.0](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.184.2...v1.185.0) (2024-08-26) + +### Features + +- add indication to Template upload ([d880cec](https://github.com/bcgov/CONN-CCBC-portal/commit/d880cecf266e7881f2c37de471a2afbd5816421b)) + +## [1.184.2](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.184.1...v1.184.2) (2024-08-26) + +### Bug Fixes + +- refine communities validation cbc ([33c40d5](https://github.com/bcgov/CONN-CCBC-portal/commit/33c40d575abfa58ca7bd77d7ff7fcc324a6b960b)) + +## [1.184.1](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.184.0...v1.184.1) (2024-08-26) + +# [1.184.0](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.183.0...v1.184.0) (2024-08-26) + +### Features + +- ccbc summary page ([15b83b8](https://github.com/bcgov/CONN-CCBC-portal/commit/15b83b80048b07052e7697011e66fc22491ba7a6)) +- summary highlight communities counts not matching in sow ([450d8b1](https://github.com/bcgov/CONN-CCBC-portal/commit/450d8b1a36d5a6c26be89f2a5f1361d1f1eda951)) + # [1.183.0](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.182.2...v1.183.0) (2024-08-22) ### Features diff --git a/app/backend/lib/reporting/gcpe.ts b/app/backend/lib/reporting/gcpe.ts index 2aa957666..0d98d9049 100644 --- a/app/backend/lib/reporting/gcpe.ts +++ b/app/backend/lib/reporting/gcpe.ts @@ -33,7 +33,7 @@ const getCbcDataQuery = ` const getCcbcQuery = ` query getCcbc { allApplications( - filter: {status: {in: ["conditionally_approved", "approved", "on_hold", "closed", "recommendation", "complete"]}} + filter: {analystStatus: {in: ["conditionally_approved", "approved", "on_hold", "closed", "recommendation", "complete"]}} ) { edges { node { @@ -119,6 +119,7 @@ const getCcbcQuery = ` package projectName status + analystStatus intakeNumber organizationName } @@ -337,7 +338,7 @@ const generateExcelData = async ( // federal funding source { value: 'ISED-UBF Core' }, // status - { value: convertStatus(node?.status) }, + { value: convertStatus(node?.analystStatus) }, // project milestone complete percent { value: diff --git a/app/components/Analyst/CBC/CbcTheme.ts b/app/components/Analyst/CBC/CbcTheme.ts index 292b899c8..59c8ebc5d 100644 --- a/app/components/Analyst/CBC/CbcTheme.ts +++ b/app/components/Analyst/CBC/CbcTheme.ts @@ -11,12 +11,13 @@ import { TextWidget, DatePickerWidget, } from 'lib/theme/widgets'; +import ArrayLocationFieldTemplate from 'lib/theme/fields/ArrayLocationDataField'; +import CommunitySourceWidget from 'lib/theme/widgets/custom/CommunitySourceWidget'; import ArrayBooleanField from '../../Review/fields/ArrayBooleanField'; import ReviewCheckboxField from '../../Review/fields/ReviewCheckboxField'; import ReviewInlineArrayField from '../../Review/fields/ReviewInlineArrayField'; import ReviewObjectFieldTemplate from '../../Review/ReviewObjectFieldTemplate'; import ReviewSectionField from '../../Review/ReviewSectionField'; -import ReviewArrayFieldTemplate from '../../Review/fields/ReviewArrayFieldTemplate'; import ReviewFieldTemplate from '../../Review/fields/ReviewFieldTemplate'; import DefaultWidget from '../../Review/widgets/DefaultWidget'; import BooleanWidget from '../../Review/widgets/BooleanWidget'; @@ -47,12 +48,13 @@ const CbcTheme: ThemeProps = { NumberWidget, NumericStringWidget, ReadOnlyWidget: DefaultWidget, + CommunitySourceWidget, }, templates: { ...templates, ObjectFieldTemplate: ReviewObjectFieldTemplate, FieldTemplate: ReviewFieldTemplate, - ArrayFieldTemplate: ReviewArrayFieldTemplate, + ArrayFieldTemplate: ArrayLocationFieldTemplate, }, }; diff --git a/app/components/Analyst/NavigationSidebar.tsx b/app/components/Analyst/NavigationSidebar.tsx index ffaef5790..c695a7317 100644 --- a/app/components/Analyst/NavigationSidebar.tsx +++ b/app/components/Analyst/NavigationSidebar.tsx @@ -8,6 +8,7 @@ import { faClipboardList, faClockRotateLeft, faEnvelope, + faNoteSticky, } from '@fortawesome/free-solid-svg-icons'; import NavItem from './NavItem'; @@ -47,6 +48,12 @@ const NavigationSidebar = () => { />
+ ` + position: relative; + background-color: ${(props) => props.errorColor}; + padding: 8px; + margin-bottom: 8px; + max-width: calc(100% - 8px) !important; + + .pg-select-wrapper, + .datepicker-widget, + .url-widget-wrapper, + .ccbcid-widget-wrapper { + background-color: white !important; + max-width: calc(340px - 8px) !important; + } + + [class*='StyledMessage']:empty { + display: none; + } +`; + +const StyledHelp = styled(Help)` + color: ${(props) => props.theme.color.primaryBlue}; + cursor: pointer; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); +`; + const ProjectFieldTemplate: React.FC = ({ children, uiSchema, + formContext, + id, }) => { const uiTitle = uiSchema?.['ui:label'] || uiSchema?.['ui:title'] ? `${uiSchema?.['ui:label'] ?? uiSchema?.['ui:title']}` : null; + const fieldName = id?.split('_')?.[1]; const hidden = uiSchema?.['ui:widget'] === 'HiddenWidget' || false; + + const showErrorHint = formContext?.showErrorHint ?? false; + const { errorColor, __errors: formContextErrors } = + formContext?.errors?.[fieldName] || {}; + const hasFormContextError = formContextErrors?.length > 0; + return ( <> {!hidden && ( {uiTitle && {uiTitle}} - {children} + {showErrorHint && hasFormContextError ? ( + +
{children}
+ + + +
+ ) : ( + children + )}
)} diff --git a/app/components/Analyst/RFI/ListFilesWidget.tsx b/app/components/Analyst/RFI/ListFilesWidget.tsx index 221429321..5a6929921 100644 --- a/app/components/Analyst/RFI/ListFilesWidget.tsx +++ b/app/components/Analyst/RFI/ListFilesWidget.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { useRouter } from 'next/router'; import { useFeature } from '@growthbook/growthbook-react'; import { AddButton } from '../Project'; +import TemplateDescription from './TemplateDescription'; const StyledContainer = styled.div` margin-bottom: 24px; @@ -40,10 +41,13 @@ const ListFilesWidget: React.FC = ({ formContext, label, value, + uiSchema, }) => { const showRfiUpload = useFeature('show_analyst_rfi_upload').value; const router = useRouter(); const isFiles = value?.length > 0; + const templateNumber = + (uiSchema['ui:options']?.templateNumber as number) ?? null; const handleDownload = async (uuid: string, fileName: string) => { const url = `/api/s3/download/${uuid}/${fileName}`; @@ -86,6 +90,7 @@ const ListFilesWidget: React.FC = ({ }} /> )} + ); }; diff --git a/app/components/Analyst/RFI/TemplateDescription.tsx b/app/components/Analyst/RFI/TemplateDescription.tsx new file mode 100644 index 000000000..f6d772d3a --- /dev/null +++ b/app/components/Analyst/RFI/TemplateDescription.tsx @@ -0,0 +1,49 @@ +import { Link } from '@button-inc/bcgov-theme'; +import { useRouter } from 'next/router'; +import styled from 'styled-components'; + +const HintText = styled.p` + color: ${(props) => props.theme.color.darkGrey}; + font-style: italic; + font-size: 13px; + a { + color: ${(props) => props.theme.color.links}; + font-size: 13px; + &:hover { + text-decoration: underline; + } + } +`; + +interface Props { + templateNumber: number; +} + +const TemplateDescription: React.FC = ({ templateNumber }) => { + const applicationId = useRouter().query.applicationId as string; + if (!templateNumber) return null; + const link = ( + + {templateNumber === 1 ? 'Benefits' : 'Budget Details'} + + ); + + return ( + (templateNumber === 1 || templateNumber === 2) && ( + + * RFI upload for Template {templateNumber} automatically updates the + data for{' '} + {templateNumber === 1 + ? 'Final Eligible Households and Indigenous' + : 'Total Eligible Costs and Total Project Costs'}{' '} + in the {link} section. Please verify the changes on the application + page.* + + ) + ); +}; + +export default TemplateDescription; diff --git a/app/components/AnalystDashboard/AllDashboard.tsx b/app/components/AnalystDashboard/AllDashboard.tsx index aaf47be27..a60cdbb23 100644 --- a/app/components/AnalystDashboard/AllDashboard.tsx +++ b/app/components/AnalystDashboard/AllDashboard.tsx @@ -121,7 +121,7 @@ const CcbcIdCell = ({ cell }) => { <> {linkCbc ? ( {cell.getValue()} diff --git a/app/components/Review/Accordion.tsx b/app/components/Review/Accordion.tsx index 50cb524f9..c33a5ab02 100644 --- a/app/components/Review/Accordion.tsx +++ b/app/components/Review/Accordion.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { useRouter } from 'next/router'; import { BaseAccordion } from '@button-inc/bcgov-theme/Accordion'; import styled from 'styled-components'; @@ -76,6 +76,7 @@ const Accordion = ({ toggled, title, recordLocked, + focused, ...rest }: any) => { const [isToggled, setIsToggled] = useState( @@ -83,6 +84,8 @@ const Accordion = ({ ); const router = useRouter(); const applicationId = router.query.applicationId as string; + const accordionRef = useRef(null); + const handleToggle = (event) => { setIsToggled((toggle) => !toggle); if (onToggle) onToggle(event); @@ -96,9 +99,21 @@ const Accordion = ({ setIsToggled(getToggledState(toggled, defaultToggled)); }, [toggled, defaultToggled]); + useEffect(() => { + if (focused && accordionRef.current) { + window.scrollTo({ + top: + accordionRef.current.getBoundingClientRect().top + + window.scrollY - + 100, // Adjust offset to account for header + behavior: 'smooth', + }); + } + }, [focused]); + return ( -
+

{title}

{allowAnalystEdit && diff --git a/app/components/Review/Components.tsx b/app/components/Review/Components.tsx index ad34a1376..01af9aa93 100644 --- a/app/components/Review/Components.tsx +++ b/app/components/Review/Components.tsx @@ -35,4 +35,7 @@ interface StyledColErrorProps { export const StyledColError = styled(StyledColRight)` background-color: ${(props) => props?.errorColor ? props.errorColor : props.theme.color.errorBackground}; + .pg-select-wrapper { + background-color: white !important; + } `; diff --git a/app/components/Review/ReviewSectionField.tsx b/app/components/Review/ReviewSectionField.tsx index 72df7deb0..6ffbd8ba7 100644 --- a/app/components/Review/ReviewSectionField.tsx +++ b/app/components/Review/ReviewSectionField.tsx @@ -20,13 +20,15 @@ const ReviewSectionField: React.FC = (props) => { () => Object.keys(errorSchema || {}).length > 0 || !!hasFormContextError, [errorSchema, hasFormContextError] ); + const toggledSection = formContext?.toggledSection === pageName; const allowAnalystEdit = uiOptions?.allowAnalystEdit && (formContext.isEditable ?? true); return ( = ({ const title = (uiSchema?.['ui:options']?.customTitle as JSX.Element) ?? schema.title; + const isExcludeTableFormat = uiSchema?.['ui:options']?.excludeTableFormat; + + if (isExcludeTableFormat) { + return <>{children}; + } + const before = uiSchema?.['ui:before']; const after = uiSchema?.['ui:after']; const fieldName = id?.split('_')?.[2]; diff --git a/app/components/Review/widgets/DateWidget.tsx b/app/components/Review/widgets/DateWidget.tsx index 302289712..62d514bbe 100644 --- a/app/components/Review/widgets/DateWidget.tsx +++ b/app/components/Review/widgets/DateWidget.tsx @@ -1,8 +1,20 @@ import { WidgetProps } from '@rjsf/utils'; +import { StyledSourceSpan } from './DefaultWidget'; -const DateWidget: React.FC = ({ value }) => { +const DateWidget: React.FC = ({ value, formContext, name }) => { const date = value?.toString().split('T')[0]; - return <>{date}; + return ( + <> + {date} + {formContext?.formDataSource?.[name] && + value !== null && + typeof value !== 'undefined' && ( + + {` (${formContext.formDataSource?.[name]})`} + + )} + + ); }; export default DateWidget; diff --git a/app/components/Review/widgets/DefaultWidget.tsx b/app/components/Review/widgets/DefaultWidget.tsx index 2f6c03e4c..70e3946cc 100644 --- a/app/components/Review/widgets/DefaultWidget.tsx +++ b/app/components/Review/widgets/DefaultWidget.tsx @@ -1,7 +1,23 @@ import { WidgetProps } from '@rjsf/utils'; +import styled from 'styled-components'; -const DefaultWidget: React.FC = ({ value }) => ( - <>{value?.toString()} -); +export const StyledSourceSpan = styled('span')` + color: #2e8540; +`; + +const DefaultWidget: React.FC = ({ value, formContext, name }) => { + return ( + <> + {value?.toString()} + {formContext?.formDataSource?.[name] && + value !== null && + typeof value !== 'undefined' && ( + + {` (${formContext.formDataSource?.[name]})`} + + )} + + ); +}; export default DefaultWidget; diff --git a/app/components/Review/widgets/LinkArrayWidget.tsx b/app/components/Review/widgets/LinkArrayWidget.tsx new file mode 100644 index 000000000..ed5f1fa0d --- /dev/null +++ b/app/components/Review/widgets/LinkArrayWidget.tsx @@ -0,0 +1,47 @@ +import { WidgetProps } from '@rjsf/utils'; +import styled from 'styled-components'; +import { StyledSourceSpan } from './DefaultWidget'; + +const LinkArrayWidget: React.FC = ({ + value, + formContext, + name, +}) => { + const StyledLink = styled.a` + color: ${(props) => props.theme.color.links}; + text-decoration-line: underline; + word-break: break-word; + width: fit-content; + :hover { + cursor: pointer; + } + `; + return ( + <> + {value && + Array.isArray(value) && + value.length > 0 && + value.map((item, index) => ( + <> + + {item.name} + + {index < value.length - 1 ? ', ' : ''} + + ))} + {formContext?.formDataSource?.[name] && + value !== null && + typeof value !== 'undefined' && ( + + {` (${formContext.formDataSource?.[name]})`} + + )} + + ); +}; + +export default LinkArrayWidget; diff --git a/app/components/Review/widgets/MoneyWidget.tsx b/app/components/Review/widgets/MoneyWidget.tsx index 01ae256fd..05634afe4 100644 --- a/app/components/Review/widgets/MoneyWidget.tsx +++ b/app/components/Review/widgets/MoneyWidget.tsx @@ -1,8 +1,19 @@ import { WidgetProps } from '@rjsf/utils'; import formatMoney from 'utils/formatMoney'; +import { StyledSourceSpan } from './DefaultWidget'; -const MoneyWidget: React.FC = ({ value }) => ( - <>{formatMoney(value)} +const MoneyWidget: React.FC = ({ value, formContext, name }) => ( + <> + {formatMoney(value)} + {formContext?.formDataSource?.[name] && + value !== null && + typeof value !== 'undefined' && + !!value && ( + + {` (${formContext.formDataSource?.[name]})`} + + )} + ); export default MoneyWidget; diff --git a/app/cypress/e2e/analyst/cbc/[cbcId].cy.js b/app/cypress/e2e/analyst/cbc/[cbcId].cy.js index ef90f32d1..671acc21a 100644 --- a/app/cypress/e2e/analyst/cbc/[cbcId].cy.js +++ b/app/cypress/e2e/analyst/cbc/[cbcId].cy.js @@ -16,7 +16,8 @@ describe('The cbc project view', () => { cy.contains('h1', 'Project 1'); cy.contains('h2', 'Tombstone'); cy.contains('h2', 'Project type'); - cy.contains('h2', 'Locations and counts'); + cy.contains('h2', 'Locations'); + cy.contains('h2', 'Counts'); cy.contains('h2', 'Funding'); cy.contains('h2', 'Events and dates'); cy.contains('h2', 'Miscellaneous'); diff --git a/app/formSchema/analyst/cbc/locations.ts b/app/formSchema/analyst/cbc/locations.ts new file mode 100644 index 000000000..564b62761 --- /dev/null +++ b/app/formSchema/analyst/cbc/locations.ts @@ -0,0 +1,36 @@ +import { RJSFSchema } from '@rjsf/utils'; + +const locations: RJSFSchema = { + title: 'Locations', + description: '', + type: 'object', + required: [], + properties: { + projectLocations: { + type: 'string', + title: 'Project Locations', + }, + geographicNames: { + type: 'string', + title: 'Geographic Names', + }, + regionalDistricts: { + type: 'string', + title: 'Regional Districts', + }, + economicRegions: { + type: 'string', + title: 'Economic Regions', + }, + communitySourceData: { + type: 'array', + default: [], + items: { + type: 'integer', + enum: [], + }, + }, + }, +}; + +export default locations; diff --git a/app/formSchema/analyst/cbc/locationsAndCounts.ts b/app/formSchema/analyst/cbc/locationsAndCounts.ts index 97c94c228..832b5d789 100644 --- a/app/formSchema/analyst/cbc/locationsAndCounts.ts +++ b/app/formSchema/analyst/cbc/locationsAndCounts.ts @@ -1,32 +1,15 @@ import { RJSFSchema } from '@rjsf/utils'; const locationsAndCounts: RJSFSchema = { - title: 'Locations and counts', + title: 'Counts', description: '', type: 'object', required: [ 'projectLocations', 'communitiesAndLocalesCount', 'indigenousCommunities', - 'householdCount', ], properties: { - projectLocations: { - type: 'string', - title: 'Project Locations', - }, - geographicNames: { - type: 'string', - title: 'Geographic Names', - }, - regionalDistricts: { - type: 'string', - title: 'Regional Districts', - }, - economicRegions: { - type: 'string', - title: 'Economic Regions', - }, communitiesAndLocalesCount: { type: 'number', title: 'Communities and locales count', diff --git a/app/formSchema/analyst/cbc/review.ts b/app/formSchema/analyst/cbc/review.ts index 63455e48c..cd2eb8df2 100644 --- a/app/formSchema/analyst/cbc/review.ts +++ b/app/formSchema/analyst/cbc/review.ts @@ -6,6 +6,7 @@ import funding from './funding'; import eventsAndDates from './eventsAndDates'; import miscellaneous from './miscellaneous'; import projectDataReviews from './projectDataReviews'; +import locationsUi from './locations'; const review: RJSFSchema = { type: 'object', @@ -24,6 +25,12 @@ const review: RJSFSchema = { ...projectType.properties, }, }, + locations: { + title: locationsUi.title, + properties: { + ...locationsUi.properties, + }, + }, locationsAndCounts: { required: locationsAndCounts.required, title: locationsAndCounts.title, diff --git a/app/formSchema/analyst/summary/counts.ts b/app/formSchema/analyst/summary/counts.ts new file mode 100644 index 000000000..65f3506b6 --- /dev/null +++ b/app/formSchema/analyst/summary/counts.ts @@ -0,0 +1,43 @@ +import { RJSFSchema } from '@rjsf/utils'; + +const counts: RJSFSchema = { + title: 'Counts', + description: '', + type: 'object', + required: [ + 'communities', + 'benefitingCommunities', + 'indigenousCommunities', + 'benefitingIndigenousCommunities', + 'totalHouseholdsImpacted', + 'numberOfIndigenousHouseholds', + ], + properties: { + communities: { + type: 'number', + title: 'Communities', + }, + benefitingCommunities: { + type: 'number', + title: 'Benefiting Communities', + }, + indigenousCommunities: { + type: 'number', + title: 'Indigenous Communities', + }, + benefitingIndigenousCommunities: { + type: 'number', + title: 'Benefiting Indigenous Communities', + }, + totalHouseholdsImpacted: { + type: 'number', + title: 'Total Households Impacted', + }, + numberOfIndigenousHouseholds: { + type: 'number', + title: 'Number of Indigenous Households', + }, + }, +}; + +export default counts; diff --git a/app/formSchema/analyst/summary/dependency.ts b/app/formSchema/analyst/summary/dependency.ts new file mode 100644 index 000000000..efb0b19a6 --- /dev/null +++ b/app/formSchema/analyst/summary/dependency.ts @@ -0,0 +1,20 @@ +import { RJSFSchema } from '@rjsf/utils'; + +const dependency: RJSFSchema = { + title: 'Dependency', + description: '', + type: 'object', + required: ['connectedCoastNetworkDependent', 'crtcProjectDependent'], + properties: { + connectedCoastNetworkDependent: { + type: 'string', + title: 'Connected Coast Network Dependent', + }, + crtcProjectDependent: { + type: 'string', + title: 'CRTC Project Dependent', + }, + }, +}; + +export default dependency; diff --git a/app/formSchema/analyst/summary/eventsAndDates.ts b/app/formSchema/analyst/summary/eventsAndDates.ts new file mode 100644 index 000000000..ba10152bc --- /dev/null +++ b/app/formSchema/analyst/summary/eventsAndDates.ts @@ -0,0 +1,48 @@ +import { RJSFSchema } from '@rjsf/utils'; + +const eventsAndDates: RJSFSchema = { + title: 'Events and Dates', + description: '', + type: 'object', + required: [ + 'announcedByProvince', + 'dateApplicationReceived', + 'dateConditionallyApproved', + 'dateAgreementSigned', + 'effectiveStartDate', + 'proposedStartDate', + 'proposedCompletionDate', + ], + properties: { + announcedByProvince: { + type: 'string', + title: 'Announced by Province', + }, + dateApplicationReceived: { + type: 'string', + title: 'Date Application Received', + }, + dateConditionallyApproved: { + type: 'string', + title: 'Date Conditionally Approved', + }, + dateAgreementSigned: { + type: 'string', + title: 'Date Agreement Signed', + }, + effectiveStartDate: { + type: 'string', + title: 'Effective Start Date', + }, + proposedStartDate: { + type: 'string', + title: 'Proposed Start Date', + }, + proposedCompletionDate: { + type: 'string', + title: 'Proposed Completion Date', + }, + }, +}; + +export default eventsAndDates; diff --git a/app/formSchema/analyst/summary/funding.ts b/app/formSchema/analyst/summary/funding.ts new file mode 100644 index 000000000..9bfd1b4f9 --- /dev/null +++ b/app/formSchema/analyst/summary/funding.ts @@ -0,0 +1,48 @@ +import { RJSFSchema } from '@rjsf/utils'; + +const funding: RJSFSchema = { + title: 'Funding', + description: '', + type: 'object', + required: [ + 'bcFundingRequested', + 'federalFunding', + 'applicantAmount', + 'cibFunding', + 'fhnaFunding', + 'otherFunding', + 'totalProjectBudget', + ], + properties: { + bcFundingRequested: { + type: 'number', + title: 'BC Funding Requested', + }, + federalFunding: { + type: 'number', + title: 'Federal Funding', + }, + applicantAmount: { + type: 'number', + title: 'Applicant Amount', + }, + cibFunding: { + type: 'number', + title: 'CIB Funding', + }, + fhnaFunding: { + type: 'number', + title: 'FHNA Funding', + }, + otherFunding: { + type: 'number', + title: 'Other Funding', + }, + totalProjectBudget: { + type: 'number', + title: 'Total Project Budget', + }, + }, +}; + +export default funding; diff --git a/app/formSchema/analyst/summary/milestone.ts b/app/formSchema/analyst/summary/milestone.ts new file mode 100644 index 000000000..6b9294e69 --- /dev/null +++ b/app/formSchema/analyst/summary/milestone.ts @@ -0,0 +1,16 @@ +import { RJSFSchema } from '@rjsf/utils'; + +const milestone: RJSFSchema = { + title: 'Milestone', + description: '', + type: 'object', + required: ['percentProjectMilestoneComplete'], + properties: { + percentProjectMilestoneComplete: { + type: 'string', + title: 'Project Milestone Complete', + }, + }, +}; + +export default milestone; diff --git a/app/formSchema/analyst/summary/review.ts b/app/formSchema/analyst/summary/review.ts new file mode 100644 index 000000000..ea431ada1 --- /dev/null +++ b/app/formSchema/analyst/summary/review.ts @@ -0,0 +1,49 @@ +import { RJSFSchema } from '@rjsf/utils'; +import counts from './counts'; +import dependency from './dependency'; +import eventsAndDates from './eventsAndDates'; +import funding from './funding'; +import milestone from './milestone'; + +const review: RJSFSchema = { + type: 'object', + properties: { + dependency: { + required: dependency.required, + title: dependency.title, + properties: { + ...dependency.properties, + }, + }, + counts: { + required: counts.required, + title: counts.title, + properties: { + ...counts.properties, + }, + }, + funding: { + required: funding.required, + title: funding.title, + properties: { + ...funding.properties, + }, + }, + eventsAndDates: { + required: eventsAndDates.required, + title: eventsAndDates.title, + properties: { + ...eventsAndDates.properties, + }, + }, + milestone: { + required: milestone.required, + title: milestone.title, + properties: { + ...milestone.properties, + }, + }, + }, +}; + +export default review; diff --git a/app/formSchema/uiSchema/analyst/rfiUiSchema.ts b/app/formSchema/uiSchema/analyst/rfiUiSchema.ts index 92dd92114..ce6592e90 100644 --- a/app/formSchema/uiSchema/analyst/rfiUiSchema.ts +++ b/app/formSchema/uiSchema/analyst/rfiUiSchema.ts @@ -188,9 +188,15 @@ export const rfiViewUiSchema = { 'ui:field': 'RequestedFilesField', eligibilityAndImpactsCalculator: { 'ui:widget': 'ListFilesWidget', + 'ui:options': { + templateNumber: 1, + }, }, detailedBudget: { 'ui:widget': 'ListFilesWidget', + 'ui:options': { + templateNumber: 2, + }, }, financialForecast: { 'ui:widget': 'ListFilesWidget', @@ -595,6 +601,7 @@ while (stack.length) { currentObj[key]['ui:options'].fileDateTitle = 'Date received'; currentObj[key]['ui:options'].maxDate = new Date(); currentObj[key]['ui:options'].showValidationMessage = true; + currentObj[key]['ui:options'].showTemplateUploadIndication = true; } stack.push(currentObj[key]); } diff --git a/app/formSchema/uiSchema/cbc/editLocationsUiSchema.ts b/app/formSchema/uiSchema/cbc/editLocationsUiSchema.ts new file mode 100644 index 000000000..def9b5520 --- /dev/null +++ b/app/formSchema/uiSchema/cbc/editLocationsUiSchema.ts @@ -0,0 +1,37 @@ +const locationsUiSchema = { + 'ui:field': 'SectionField', + 'ui:options': { + dividers: true, + }, + projectLocations: { + 'ui:widget': 'TextAreaWidget', + 'ui:label': 'Project Locations', + }, + geographicNames: { + 'ui:hidden': true, + 'ui:widget': 'HiddenWidget', + }, + regionalDistricts: { + 'ui:hidden': true, + 'ui:widget': 'HiddenWidget', + }, + economicRegions: { + 'ui:hidden': true, + 'ui:widget': 'HiddenWidget', + }, + communitySourceData: { + 'ui:field': 'ArrayLocationDataField', + 'ui:label': 'Community Location Data', + items: { + 'ui:widget': 'CommunitySourceWidget', + 'ui:options': { + excludeTableFormat: true, + }, + }, + 'ui:options': { + excludeTableFormat: true, + }, + }, +}; + +export default locationsUiSchema; diff --git a/app/formSchema/uiSchema/cbc/editUiSchema.ts b/app/formSchema/uiSchema/cbc/editUiSchema.ts index daa80ad1d..9cfb88acd 100644 --- a/app/formSchema/uiSchema/cbc/editUiSchema.ts +++ b/app/formSchema/uiSchema/cbc/editUiSchema.ts @@ -6,6 +6,7 @@ import fundingUiSchema from './fundingUiSchema'; import eventsAndDatesUiSchema from './eventsAndDatesUiSchema'; import miscellaneousUiSchema from './miscellaneousUiSchema'; import projectDataReviewsUiSchema from './projectDataReviewsUiSchema'; +import editLocationsUiSchema from './editLocationsUiSchema'; const editUiSchema = { 'ui:title': 'CBC Edit', @@ -21,6 +22,10 @@ const editUiSchema = { 'ui:title': 'Locations and Counts', ...locationsAndCountsUiSchema, }, + locations: { + 'ui:title': 'Locations', + ...editLocationsUiSchema, + }, funding: { 'ui:title': 'Funding', ...fundingUiSchema, diff --git a/app/formSchema/uiSchema/cbc/locationsAndCountsUiSchema.ts b/app/formSchema/uiSchema/cbc/locationsAndCountsUiSchema.ts index 67830a66a..ef9dc963f 100644 --- a/app/formSchema/uiSchema/cbc/locationsAndCountsUiSchema.ts +++ b/app/formSchema/uiSchema/cbc/locationsAndCountsUiSchema.ts @@ -4,25 +4,6 @@ const locationsAndCountsUiSchema = { dividers: true, }, 'ui:title': 'Locations and Counts', - projectLocations: { - 'ui:widget': 'TextAreaWidget', - 'ui:label': 'Project Locations', - 'ui:options': { - maxLength: 1000, - }, - }, - geographicNames: { - 'ui:widget': 'ReadOnlyWidget', - 'ui:label': 'Geographic Names', - }, - regionalDistricts: { - 'ui:widget': 'ReadOnlyWidget', - 'ui:label': 'Regional Districts', - }, - economicRegions: { - 'ui:widget': 'ReadOnlyWidget', - 'ui:label': 'Economic Regions', - }, communitiesAndLocalesCount: { 'ui:widget': 'NumberWidget', 'ui:label': 'Communities and Locales Count', diff --git a/app/formSchema/uiSchema/cbc/locationsUiSchema.ts b/app/formSchema/uiSchema/cbc/locationsUiSchema.ts new file mode 100644 index 000000000..ae2b76b2b --- /dev/null +++ b/app/formSchema/uiSchema/cbc/locationsUiSchema.ts @@ -0,0 +1,27 @@ +const locationsUiSchema = { + 'ui:field': 'SectionField', + 'ui:options': { + dividers: true, + }, + projectLocations: { + 'ui:widget': 'TextAreaWidget', + 'ui:label': 'Project Locations', + }, + geographicNames: { + 'ui:widget': 'TextAreaWidget', + 'ui:label': 'Geographic Names', + }, + regionalDistricts: { + 'ui:widget': 'TextAreaWidget', + 'ui:label': 'Regional Districts', + }, + economicRegions: { + 'ui:widget': 'TextAreaWidget', + 'ui:label': 'Economic Regions', + }, + communitySourceData: { + 'ui:hidden': true, + }, +}; + +export default locationsUiSchema; diff --git a/app/formSchema/uiSchema/cbc/projectTypeUiSchema.ts b/app/formSchema/uiSchema/cbc/projectTypeUiSchema.ts index 8a85097a7..1d1f4e4dc 100644 --- a/app/formSchema/uiSchema/cbc/projectTypeUiSchema.ts +++ b/app/formSchema/uiSchema/cbc/projectTypeUiSchema.ts @@ -17,6 +17,7 @@ const projectTypeUiSchema = { highwayProjectType: { 'ui:widget': 'SelectWidget', 'ui:placeholder': 'Highway project type', + 'ui:label': 'Highway Project Type', }, lastMileProjectType: { 'ui:widget': 'SelectWidget', diff --git a/app/formSchema/uiSchema/cbc/reviewUiSchema.ts b/app/formSchema/uiSchema/cbc/reviewUiSchema.ts index 98dda150e..ba0bb41f5 100644 --- a/app/formSchema/uiSchema/cbc/reviewUiSchema.ts +++ b/app/formSchema/uiSchema/cbc/reviewUiSchema.ts @@ -2,10 +2,10 @@ import projectTypeUiSchema from './projectTypeUiSchema'; import tombstoneUiSchema from './tombstoneUiSchema'; import locationsAndCountsUiSchema from './locationsAndCountsUiSchema'; import fundingUiSchema from './fundingUiSchema'; - import eventsAndDatesUiSchema from './eventsAndDatesUiSchema'; import miscellaneousUiSchema from './miscellaneousUiSchema'; import projectDataReviewsUiSchema from './projectDataReviewsUiSchema'; +import locationsUiSchema from './locationsUiSchema'; const reviewUiSchema = { tombstone: { @@ -31,6 +31,10 @@ const reviewUiSchema = { ...projectTypeUiSchema, 'ui:options': { allowAnalystEdit: true }, }, + locations: { + ...locationsUiSchema, + 'ui:options': { allowAnalystEdit: true }, + }, locationsAndCounts: { ...locationsAndCountsUiSchema, 'ui:options': { allowAnalystEdit: true }, diff --git a/app/formSchema/uiSchema/summary/countsUiSchema.ts b/app/formSchema/uiSchema/summary/countsUiSchema.ts new file mode 100644 index 000000000..ba5042759 --- /dev/null +++ b/app/formSchema/uiSchema/summary/countsUiSchema.ts @@ -0,0 +1,32 @@ +const countsUiSchema = { + 'ui:field': 'SectionField', + 'ui:options': { + dividers: true, + }, + 'ui:title': 'Counts', + communities: { + 'ui:widget': 'NumberWidget', + 'ui:label': 'Communities', + }, + benefitingCommunities: { + 'ui:widget': 'LinkArrayWidget', + 'ui:label': 'Benefiting Communities', + }, + indigenousCommunities: { + 'ui:widget': 'NumberWidget', + 'ui:label': 'Indigenous Communities', + }, + benefitingIndigenousCommunities: { + 'ui:widget': 'LinkArrayWidget', + 'ui:label': 'Benefiting Indigenous Communities', + }, + totalHouseholdsImpacted: { + 'ui:widget': 'NumberWidget', + 'ui:label': 'Total Households Impacted', + }, + numberOfIndigenousHouseholds: { + 'ui:widget': 'NumberWidget', + 'ui:label': 'Number of Indigenous Households', + }, +}; +export default countsUiSchema; diff --git a/app/formSchema/uiSchema/summary/dependencyUiSchema.ts b/app/formSchema/uiSchema/summary/dependencyUiSchema.ts new file mode 100644 index 000000000..f297d4c60 --- /dev/null +++ b/app/formSchema/uiSchema/summary/dependencyUiSchema.ts @@ -0,0 +1,16 @@ +const dependencyUiSchema = { + 'ui:field': 'SectionField', + 'ui:options': { + dividers: true, + }, + 'ui:title': 'Dependency', + connectedCoastNetworkDependent: { + 'ui:widget': 'TextWidget', + 'ui:label': 'Connected Coast Network Dependent', + }, + crtcProjectDependent: { + 'ui:widget': 'TextWidget', + 'ui:label': 'CRTC Project Dependent', + }, +}; +export default dependencyUiSchema; diff --git a/app/formSchema/uiSchema/summary/eventsAndDatesUiSchema.ts b/app/formSchema/uiSchema/summary/eventsAndDatesUiSchema.ts new file mode 100644 index 000000000..39d7f6ba7 --- /dev/null +++ b/app/formSchema/uiSchema/summary/eventsAndDatesUiSchema.ts @@ -0,0 +1,37 @@ +const eventsAndDatesUiSchema = { + 'ui:field': 'SectionField', + 'ui:options': { + dividers: true, + }, + 'ui:title': 'Events and Dates', + announcedByProvince: { + 'ui:widget': 'TextWidget', + 'ui:label': 'Announced by Province', + }, + dateApplicationReceived: { + 'ui:widget': 'DateWidget', + 'ui:label': 'Date Application Received', + }, + dateConditionallyApproved: { + 'ui:widget': 'DateWidget', + 'ui:label': 'Date Conditionally Approved', + }, + dateAgreementSigned: { + 'ui:widget': 'DateWidget', + 'ui:label': 'Date Agreement Signed', + }, + effectiveStartDate: { + 'ui:widget': 'DateWidget', + 'ui:label': 'Effective Start Date', + }, + proposedStartDate: { + 'ui:widget': 'DateWidget', + 'ui:label': 'Proposed Start Date', + }, + proposedCompletionDate: { + 'ui:widget': 'DateWidget', + 'ui:label': 'Proposed Completion Date', + }, +}; + +export default eventsAndDatesUiSchema; diff --git a/app/formSchema/uiSchema/summary/fundingUiSchema.ts b/app/formSchema/uiSchema/summary/fundingUiSchema.ts new file mode 100644 index 000000000..bc011e51c --- /dev/null +++ b/app/formSchema/uiSchema/summary/fundingUiSchema.ts @@ -0,0 +1,37 @@ +const fundingUiSchema = { + 'ui:field': 'SectionField', + 'ui:options': { + dividers: true, + }, + 'ui:title': 'Funding', + bcFundingRequested: { + 'ui:widget': 'MoneyWidget', + 'ui:label': 'BC Funding Requested', + }, + federalFunding: { + 'ui:widget': 'MoneyWidget', + 'ui:label': 'Federal Funding', + }, + applicantAmount: { + 'ui:widget': 'MoneyWidget', + 'ui:label': 'Applicant Amount', + }, + cibFunding: { + 'ui:widget': 'MoneyWidget', + 'ui:label': 'CIB Funding', + }, + fhnaFunding: { + 'ui:widget': 'MoneyWidget', + 'ui:label': 'FHNA Funding', + }, + otherFunding: { + 'ui:widget': 'MoneyWidget', + 'ui:label': 'Other Funding', + }, + totalProjectBudget: { + 'ui:widget': 'MoneyWidget', + 'ui:label': 'Total Project Budget', + }, +}; + +export default fundingUiSchema; diff --git a/app/formSchema/uiSchema/summary/milestoneUiSchema.ts b/app/formSchema/uiSchema/summary/milestoneUiSchema.ts new file mode 100644 index 000000000..b6b476885 --- /dev/null +++ b/app/formSchema/uiSchema/summary/milestoneUiSchema.ts @@ -0,0 +1,13 @@ +const milestoneUiSchema = { + 'ui:field': 'SectionField', + 'ui:options': { + dividers: true, + }, + 'ui:title': 'Milestone', + percentProjectMilestoneComplete: { + 'ui:widget': 'TextWidget', + 'ui:label': '% Project Milestone Complete', + }, +}; + +export default milestoneUiSchema; diff --git a/app/formSchema/uiSchema/summary/reviewUiSchema.ts b/app/formSchema/uiSchema/summary/reviewUiSchema.ts new file mode 100644 index 000000000..1d1fa9375 --- /dev/null +++ b/app/formSchema/uiSchema/summary/reviewUiSchema.ts @@ -0,0 +1,14 @@ +import dependencyUiSchema from './dependencyUiSchema'; +import countsUiSchema from './countsUiSchema'; +import fundingUiSchema from './fundingUiSchema'; +import eventsAndDatesUiSchema from './eventsAndDatesUiSchema'; +import milestoneUiSchema from './milestoneUiSchema'; + +const reviewUiSchema = { + dependency: dependencyUiSchema, + counts: countsUiSchema, + funding: fundingUiSchema, + eventsAndDates: eventsAndDatesUiSchema, + milestone: milestoneUiSchema, +}; +export default reviewUiSchema; diff --git a/app/lib/helpers/ccbcSummaryGenerateFormData.tsx b/app/lib/helpers/ccbcSummaryGenerateFormData.tsx new file mode 100644 index 000000000..00642bceb --- /dev/null +++ b/app/lib/helpers/ccbcSummaryGenerateFormData.tsx @@ -0,0 +1,469 @@ +const findScreeningAssessment = (assessments) => { + return assessments.nodes.find( + (assessment) => assessment.assessmentDataType === 'screening' + ); +}; + +const handleApplicationDateReceived = (applicationData, allIntakes) => { + // keep blank for hidden intakes + if (applicationData.intakeNumber === 99) { + return null; + } + // from intake 5 application is immediately submitted, use that date + if (applicationData.intakeNumber === 5) { + return applicationData?.applicationStatusesByApplicationId?.nodes[0] + ?.createdAt; + } + // otherwise find the intake matching and use the close timestamp + const intake = allIntakes.nodes.find( + (i) => i.ccbcIntakeNumber === applicationData.intakeNumber + ); + return intake?.closeTimestamp; +}; + +const handleOtherFundingSourcesApplication = (otherFundingSources) => { + if (!otherFundingSources) { + return null; + } + let otherFundingSourcesTotal = 0; + otherFundingSources?.otherFundingSourcesArray?.forEach((source) => { + otherFundingSourcesTotal += source.totalRequestedFundingPartner; + }); + // Not adding CIB funding to other funding at this time + // if (otherFundingSources?.totalInfrastructureBankFunding) { + // otherFundingSourcesTotal += + // otherFundingSources.totalInfrastructureBankFunding; + // } + return otherFundingSourcesTotal; +}; + +const getConditionalApprovalDate = (conditionalApprovalData) => { + // get the greater of the two dates + const provincialDate = conditionalApprovalData?.decision?.ministerDate; + const isedDate = conditionalApprovalData?.isedDecisionObj?.isedDate; + if (!provincialDate && !isedDate) { + return null; + } + if (!provincialDate) { + if (conditionalApprovalData?.isedDecisionObj?.isedDecision === 'Approved') + return isedDate; + } + if (!isedDate) { + if (conditionalApprovalData?.decision?.ministerDecision === 'Approved') + return provincialDate; + } + + // if provincial date is greater than ised date + if (new Date(provincialDate) > new Date(isedDate)) { + // if provincial date is approved + if (conditionalApprovalData?.decision?.ministerDecision === 'Approved') { + return provincialDate; + } + // otherwise check if ised date is approved + if (conditionalApprovalData?.isedDecisionObj?.isedDecision === 'Approved') { + return isedDate; + } + // otherwise return null as none of them are approved + return null; + } + // otherwise ised date is greater than provincial date + // if ised date is approved + if (conditionalApprovalData?.isedDecisionObj?.isedDecision === 'Approved') { + return isedDate; + } + // otherwise check if provincial date is approved + if (conditionalApprovalData?.decision?.ministerDecision === 'Approved') { + return provincialDate; + } + // otherwise return null as none of them are approved + return null; +}; + +const handleMilestone = (milestonePercent) => { + if (!milestonePercent) { + return null; + } + return `${Math.trunc(milestonePercent * 100)}%`; +}; + +const getCommunities = (communities) => { + if (!communities) { + return null; + } + const benefitingCommunities = []; + let totalBenefitingCommunities = 0; + const benefitingIndigenousCommunities = []; + let totalBenefitingIndigenousCommunities = 0; + communities.forEach((community) => { + if (community?.indigenous?.toUpperCase() === 'N') { + if ( + community?.bcGeoName && + community?.impacted?.toUpperCase() === 'YES' + ) { + totalBenefitingCommunities += 1; + benefitingCommunities.push({ + name: community?.bcGeoName, + link: community?.mapLink, + }); + } + } + if (community?.indigenous?.toUpperCase() === 'Y') { + if ( + community?.bcGeoName && + community?.impacted?.toUpperCase() === 'YES' + ) { + totalBenefitingIndigenousCommunities += 1; + benefitingIndigenousCommunities.push({ + name: community?.bcGeoName, + link: community?.mapLink, + }); + } + } + }); + return { + totalBenefitingCommunities, + benefitingCommunities, + totalBenefitingIndigenousCommunities, + benefitingIndigenousCommunities, + }; +}; + +const getSowErrors = (sowData, communitiesData) => { + // error on benefiting communities + const errors = { + counts: { + benefitingCommunities: {}, + benefitingIndigenousCommunities: {}, + }, + }; + const communitiesNumber = + sowData?.nodes[0]?.sowTab8SBySowId?.nodes[0]?.jsonData?.communitiesNumber; + const indigenousCommunitiesNumber = + sowData?.nodes[0]?.sowTab8SBySowId?.nodes[0]?.jsonData + ?.indigenousCommunitiesNumber; + if (communitiesNumber !== communitiesData?.totalBenefitingCommunities) { + errors.counts.benefitingCommunities = { + __errors: [ + `Communities count mismatch ${communitiesNumber} and ${communitiesData.totalBenefitingCommunities}`, + ], + errorColor: '#f8e78f', + }; + // error on benefiting indigenous communities + } + if ( + indigenousCommunitiesNumber !== + communitiesData?.totalBenefitingIndigenousCommunities + ) { + errors.counts.benefitingIndigenousCommunities = { + __errors: [ + `Indigenous communities count mismatch ${indigenousCommunitiesNumber} and ${communitiesData.totalBenefitingIndigenousCommunities}`, + ], + errorColor: '#f8e78f', + }; + } + return errors; +}; + +const getSowData = (sowData, baseSowData) => { + const communitiesData = getCommunities( + sowData?.nodes[0]?.sowTab8SBySowId?.nodes[0]?.jsonData?.geoNames + ); + const errors = getSowErrors(sowData, communitiesData); + return { + formData: { + counts: { + communities: + sowData?.nodes[0]?.sowTab8SBySowId?.nodes[0]?.jsonData + ?.communitiesNumber, + benefitingCommunities: communitiesData?.benefitingCommunities, + indigenousCommunities: + sowData?.nodes[0]?.sowTab8SBySowId?.nodes[0]?.jsonData + ?.indigenousCommunitiesNumber, + benefitingIndigenousCommunities: + communitiesData?.benefitingIndigenousCommunities, + totalHouseholdsImpacted: + sowData?.nodes[0]?.sowTab1SBySowId?.nodes[0]?.jsonData + ?.numberOfHouseholds, + numberOfIndigenousHouseholds: + sowData?.nodes[0]?.sowTab1SBySowId?.nodes[0]?.jsonData + ?.householdsImpactedIndigenous, + }, + funding: { + bcFundingRequested: + sowData?.nodes[0]?.sowTab7SBySowId?.nodes[0]?.jsonData?.summaryTable + ?.amountRequestedFromProvince, + federalFunding: + sowData?.nodes[0]?.sowTab7SBySowId?.nodes[0]?.jsonData?.summaryTable + ?.amountRequestedFromFederalGovernment, + applicantAmount: + sowData?.nodes[0]?.sowTab7SBySowId?.nodes[0]?.jsonData?.summaryTable + ?.totalApplicantContribution, + cibFunding: + sowData?.nodes[0]?.sowTab7SBySowId?.nodes[0]?.jsonData?.summaryTable + ?.totalInfrastructureBankFunding, + fhnaFunding: null, + otherFunding: + sowData?.nodes[0]?.sowTab7SBySowId?.nodes[0]?.jsonData?.summaryTable + ?.fundingFromAllOtherSources, + totalProjectBudget: + sowData?.nodes[0]?.sowTab7SBySowId?.nodes[0]?.jsonData?.summaryTable + ?.totalProjectCost, + }, + eventsAndDates: { + effectiveStartDate: sowData?.nodes[0]?.jsonData?.effectiveStartDate, + proposedStartDate: sowData?.nodes[0]?.jsonData?.projectStartDate, + proposedCompletionDate: + sowData?.nodes[0]?.jsonData?.projectCompletionDate, + dateAgreementSigned: + baseSowData?.nodes[0]?.jsonData?.dateFundingAgreementSigned, + }, + }, + formDataSource: { + communities: 'SOW', + benefitingCommunities: 'SOW', + indigenousCommunities: 'SOW', + benefitingIndigenousCommunities: 'SOW', + totalHouseholdsImpacted: 'SOW', + numberOfIndigenousHouseholds: 'SOW', + bcFundingRequested: 'SOW', + federalFunding: 'SOW', + applicantAmount: 'SOW', + cibFunding: 'SOW', + otherFunding: 'SOW', + totalProjectBudget: 'SOW', + effectiveStartDate: 'SOW', + proposedStartDate: 'SOW', + proposedCompletionDate: 'SOW', + dateAgreementSigned: 'SOW', + }, + errors, + }; +}; + +const getFormDataNonSow = (applicationData) => { + return { + formData: { + funding: { + bcFundingRequested: + applicationData?.conditionalApproval?.jsonData?.decision + ?.provincialRequested, + federalFunding: + applicationData?.conditionalApproval?.jsonData?.isedDecisionObj + ?.federalRequested, + }, + eventsAndDates: { + dateConditionallyApproved: getConditionalApprovalDate( + applicationData?.conditionalApproval?.jsonData + ), + }, + }, + formDataSource: { + bcFundingRequested: 'Conditional Approval', + federalFunding: 'Conditional Approval', + dateConditionallyApproved: 'Conditional Approval', + }, + }; +}; + +const getFormDataFromApplication = (applicationData, allIntakes) => { + return { + formData: { + counts: { + communities: null, + benefitingCommunities: null, + indigenousCommunities: null, + benefitingIndigenousCommunities: null, + totalHouseholdsImpacted: + applicationData?.formData?.jsonData?.benefits?.numberOfHouseholds, + numberOfIndigenousHouseholds: + applicationData?.formData?.jsonData?.benefits + ?.householdsImpactedIndigenous, + }, + funding: { + bcFundingRequested: null, + federalFunding: null, + applicantAmount: + applicationData?.formData?.jsonData?.projectFunding + ?.totalApplicantContribution, + cibFunding: + applicationData?.formData?.jsonData?.projectFunding?.cibFunding, + fhnaFunding: null, + otherFunding: handleOtherFundingSourcesApplication( + applicationData?.formData?.jsonData?.otherFundingSources + ), + totalProjectBudget: + applicationData?.formData?.jsonData?.budgetDetails?.totalProjectCost, + }, + eventsAndDates: { + dateApplicationReceived: handleApplicationDateReceived( + applicationData, + allIntakes + ), + dateConditionalApproval: null, + dateAgreementSigned: null, + effectiveStartDate: null, + proposedStartDate: + applicationData?.formData?.jsonData?.projectPlan?.projectStartDate, + proposedCompletionDate: + applicationData?.formData?.jsonData?.projectPlan + ?.projectCompletionDate, + }, + }, + formDataSource: { + totalHouseholdsImpacted: 'Application', + numberOfIndigenousHouseholds: 'Application', + applicantAmount: 'Application', + cibFunding: 'Application', + otherFunding: 'Application', + totalProjectBudget: 'Application', + proposedStartDate: 'Application', + proposedCompletionDate: 'Application', + }, + }; +}; + +const generateFormData = (applicationData, sowData, allIntakes) => { + const screeningAssessment = findScreeningAssessment( + applicationData.allAssessments + ); + let formData; + let formDataSource; + let errors = null; + // received, screening, assessment + // not selected, withdrawn will have bare data from application + if ( + applicationData.status === 'received' || + applicationData.status === 'screening' || + applicationData.status === 'assessment' || + applicationData.status === 'recommendation' || + applicationData.status === 'closed' || + applicationData.status === 'analyst_withdrawn' || + applicationData.status === 'withdrawn' + ) { + const applicationFormData = getFormDataFromApplication( + applicationData, + allIntakes + ); + formData = applicationFormData.formData; + formDataSource = applicationFormData.formDataSource; + // conditionally approved (internal), or not selected + // use data from conditionally approved page + // even if conditionally approved page has null values, show null + // applies to BC funding requested and federal funding requested + // rest is from application as above + } else if ( + applicationData.status === 'conditionally_approved' || + applicationData.status === 'applicant_conditionally_approved' || + applicationData.status === 'closed' + ) { + const applicationFormData = getFormDataFromApplication( + applicationData, + allIntakes + ); + formData = applicationFormData.formData; + formDataSource = applicationFormData.formDataSource; + + const conditionalApprovalData = getFormDataNonSow(applicationData); + const conditionalApprovalFormData = conditionalApprovalData.formData; + + formData.funding = { + ...formData.funding, + ...conditionalApprovalFormData.funding, + }; + formData.eventsAndDates = { + ...formData.eventsAndDates, + ...conditionalApprovalFormData.eventsAndDates, + }; + formDataSource = { + ...formDataSource, + ...conditionalApprovalData.formDataSource, + }; + // Agreement signed + } else if ( + applicationData.status === 'approved' || + applicationData.status === 'applicant_approved' + ) { + // first get form data from application + const applicationFormData = getFormDataFromApplication( + applicationData, + allIntakes + ); + formData = applicationFormData.formData; + formDataSource = applicationFormData.formDataSource; + const conditionalApprovalData = getFormDataNonSow(applicationData); + const conditionalApprovalDataFormData = conditionalApprovalData.formData; + formData.eventsAndDates = { + ...formData.eventsAndDates, + ...conditionalApprovalDataFormData.eventsAndDates, + }; + // then, we need the sow data + const sowSummaryData = getSowData( + sowData, + applicationData.projectInformationDataByApplicationId + ); + const sowFormData = sowSummaryData.formData; + const sowFormDataSource = sowSummaryData.formDataSource; + // we overwrite everything except dates with the returned sowFormData + // even if null, as per requirements + formData = { + ...formData, + ...sowFormData, + eventsAndDates: { + ...formData.eventsAndDates, + effectiveStartDate: sowFormData.eventsAndDates.effectiveStartDate, + proposedStartDate: sowFormData.eventsAndDates.proposedStartDate, + proposedCompletionDate: + sowFormData.eventsAndDates.proposedCompletionDate, + dateAgreementSigned: sowFormData.eventsAndDates.dateAgreementSigned, + }, + }; + formDataSource = { + ...formDataSource, + ...sowFormDataSource, + dateConditionallyApproved: 'Conditional Approval', + }; + errors = sowSummaryData.errors; + } + + return { + // dependency is one source + formData: { + dependency: { + connectedCoastNetworkDependent: screeningAssessment?.jsonData + ?.connectedCoastNetworkDependent + ? 'Yes' + : null, + crtcProjectDependent: screeningAssessment?.jsonData + ?.crtcProjectDependent + ? 'Yes' + : null, + }, + counts: { ...formData?.counts }, + funding: { ...formData?.funding }, + eventsAndDates: { + ...formData?.eventsAndDates, + announcedByProvince: applicationData + ?.applicationAnnouncedsByApplicationId?.nodes[0]?.announced + ? 'Yes' + : 'No', + }, + // milestone is one source + milestone: { + percentProjectMilestoneComplete: handleMilestone( + applicationData?.applicationMilestoneExcelDataByApplicationId + ?.nodes[0]?.jsonData?.overallMilestoneProgress + ), + }, + }, + formDataSource: { + ...formDataSource, + connectedCoastNetworkDependent: 'Screening', + crtcProjectDependent: 'Screening', + percentProjectMilestoneComplete: 'Milestone Report', + announcedByProvince: 'Announcements', + }, + errors, + }; +}; + +export default generateFormData; diff --git a/app/lib/theme/components/FileComponent.tsx b/app/lib/theme/components/FileComponent.tsx index 5fe3d0c9e..90ce5205f 100644 --- a/app/lib/theme/components/FileComponent.tsx +++ b/app/lib/theme/components/FileComponent.tsx @@ -13,6 +13,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { DateTime } from 'luxon'; import useModal from 'lib/helpers/useModal'; +import TemplateDescription from 'components/Analyst/RFI/TemplateDescription'; import { LoadingSpinner } from '../../../components'; import { StyledDatePicker, getStyles } from '../widgets/DatePickerWidget'; import GenericModal from '../widgets/GenericModal'; @@ -23,14 +24,25 @@ const StyledContainer = styled.div<{ margin-top: 8px; margin-bottom: 8px; width: 100%; - display: flex; - justify-content: space-between; border: 1px solid rgba(0, 0, 0, 0.16); border-radius: 4px; padding: 16px; +`; + +const StyledInnerContainer = styled.div<{ + wrap?: boolean; +}>` + display: flex; + justify-content: space-between; flex-direction: ${({ wrap }) => (wrap ? 'column-reverse' : 'row')}; `; +const StyledFooterText = styled('div')` + width: 100%; + font-size: 14px; + color: #606060; +`; + const StyledInputContainer = styled.div<{ useFileDate?: boolean }>` ${({ useFileDate }) => useFileDate @@ -152,6 +164,8 @@ interface FileComponentProps { maxDate?: Date; minDate?: Date; allowDragAndDrop?: boolean; + templateNumber?: number; + showTemplateUploadIndication?: boolean; } const ErrorMessage = ({ error, fileName, fileTypes }) => { @@ -230,6 +244,8 @@ const FileComponent: React.FC = ({ maxDate, minDate, allowDragAndDrop, + templateNumber, + showTemplateUploadIndication, }) => { const hiddenFileInput = useRef() as MutableRefObject; const isFiles = value?.length > 0; @@ -318,7 +334,6 @@ const FileComponent: React.FC = ({ {...fileErrorModal} /> = ({ } {...(allowDragAndDrop && dropzoneProps)} > - - {label} - {isFiles && - !hideIfFailed && - value.map((file: File) => ( - <> - - { - e.preventDefault(); - if (handleDownload) { - handleDownload(file.uuid, file.name, onError); + + + {label} + {isFiles && + !hideIfFailed && + value.map((file: File) => ( + <> + + { + e.preventDefault(); + if (handleDownload) { + handleDownload(file.uuid, file.name, onError); + } + }} + > + {file.name} + + ) => { + e.preventDefault(); + if (handleDelete) { + setDropzoneError(null); + handleDelete(file.id); + } + }} + disabled={loading || disabled} + > + + + + {useFileDate && file?.fileDate && ( + + {DateTime.fromISO(file.fileDate).toFormat('MMM dd, yyyy')} + + )} + + ))} + {dropzoneError} + {errors?.map((fileError: any) => ( + + ))} + + {statusLabel} + + {useFileDate && ( +
+

{`${fileDateTitle}`}

+ + { + const originalDate = new Date(d); + if ( + !Number.isNaN(originalDate) && + originalDate.valueOf() >= 0 + ) { + const newDate = originalDate + .toISOString() + .split('T')[0]; + setFileDate(newDate); + } else { + setFileDate(null); } }} - > - {file.name} - - ) => { - e.preventDefault(); - if (handleDelete) { - setDropzoneError(null); - handleDelete(file.id); - } + value={fileDate ? dayjs(fileDate) : null} + defaultValue={null} + slotProps={{ + actionBar: { + actions: ['clear', 'cancel'], + }, + textField: { + inputProps: { + id, + 'data-testid': 'datepicker-widget-input', + }, + }, }} - disabled={loading || disabled} - > - - - - {useFileDate && file?.fileDate && ( - - {DateTime.fromISO(file.fileDate).toFormat('MMM dd, yyyy')} - - )} - - ))} - {dropzoneError} - {errors?.map((fileError: any) => ( - - ))} - - {statusLabel} - - {useFileDate && ( -
-

{`${fileDateTitle}`}

- + +
+ )} + + ) => { + e.preventDefault(); + handleClick(); + }} + variant={buttonVariant} + disabled={loading || disabled || (useFileDate && !fileDate)} > - { - const originalDate = new Date(d); - if ( - !Number.isNaN(originalDate) && - originalDate.valueOf() >= 0 - ) { - const newDate = originalDate.toISOString().split('T')[0]; - setFileDate(newDate); - } else { - setFileDate(null); - } - }} - value={fileDate ? dayjs(fileDate) : null} - defaultValue={null} - slotProps={{ - actionBar: { - actions: ['clear', 'cancel'], - }, - textField: { - inputProps: { - id, - 'data-testid': 'datepicker-widget-input', - }, - }, - }} - slots={{ - openPickerButton: fileDate - ? ClearableIconButton - : undefined, - }} - format="YYYY-MM-DD" - /> -
-
- )} - - ) => { - e.preventDefault(); - handleClick(); - }} - variant={buttonVariant} - disabled={loading || disabled || (useFileDate && !fileDate)} - > - {loading ? ( - - ) : ( - - {buttonLabel()} - {allowDragAndDrop && ( - - - Drop files (or click to upload) - - )} - - )} - - -
- { - onChange(e); - // set target to null to allow for reupload of file with same name - e.currentTarget.value = null; - }} - style={{ display: 'none' }} - type="file" - required={required} - accept={fileTypes?.toString()} - /> + {loading ? ( + + ) : ( + + {buttonLabel()} + {allowDragAndDrop && ( + + + Drop files (or click to upload) + + )} + + )} + + + + { + onChange(e); + // set target to null to allow for reupload of file with same name + e.currentTarget.value = null; + }} + style={{ display: 'none' }} + type="file" + required={required} + accept={fileTypes?.toString()} + /> +
+ {showTemplateUploadIndication && ( + + + + )}
); diff --git a/app/lib/theme/fields/ArrayLocationDataField.tsx b/app/lib/theme/fields/ArrayLocationDataField.tsx new file mode 100644 index 000000000..a827808b1 --- /dev/null +++ b/app/lib/theme/fields/ArrayLocationDataField.tsx @@ -0,0 +1,84 @@ +import { ArrayFieldTemplateProps } from '@rjsf/utils'; +import React from 'react'; +import styled from 'styled-components'; +import Button from '@button-inc/bcgov-theme/Button'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; + +const StyledDiv = styled.div` + display: flex; + flex-direction: row; + gap: 6px; + align-items: center; +`; + +const StyledButton = styled(Button)` + height: 40px; + padding: 5px; + margin: 2px; +`; + +const ArrayLocationFieldTemplate = (props: ArrayFieldTemplateProps) => { + const { items, onAddClick, canAdd, formContext } = props; + + const deleteCommunitySource = formContext?.deleteCommunitySource as Function; + + return ( + <> + {items.map((element, index) => ( +
+ {index === 0 ? ( +
+ + {element.children} + {canAdd && ( + + Add + + )} + + +
+
+ ) : ( + + {element.children} + + + )} +
+ ))} + + ); +}; + +export default ArrayLocationFieldTemplate; diff --git a/app/lib/theme/widgets/FileWidget.tsx b/app/lib/theme/widgets/FileWidget.tsx index 499200714..3c2a691c2 100644 --- a/app/lib/theme/widgets/FileWidget.tsx +++ b/app/lib/theme/widgets/FileWidget.tsx @@ -63,6 +63,8 @@ const FileWidget: React.FC = ({ (uiSchema['ui:options']?.showValidationMessage as boolean) ?? false; const templateNumber = (uiSchema['ui:options']?.templateNumber as number) ?? 0; + const showTemplateUploadIndication = + (uiSchema['ui:options']?.showTemplateUploadIndication as boolean) ?? false; const isFiles = value?.length > 0; const loading = isCreatingAttachment || isDeletingAttachment; // 104857600 bytes = 100mb @@ -268,6 +270,8 @@ const FileWidget: React.FC = ({ maxDate={maxDate} minDate={minDate} allowDragAndDrop={allowDragAndDrop} + templateNumber={templateNumber} + showTemplateUploadIndication={showTemplateUploadIndication} /> ); }; diff --git a/app/lib/theme/widgets/SelectWidget.tsx b/app/lib/theme/widgets/SelectWidget.tsx index 76bbc075a..687782d90 100644 --- a/app/lib/theme/widgets/SelectWidget.tsx +++ b/app/lib/theme/widgets/SelectWidget.tsx @@ -3,8 +3,14 @@ import { Dropdown } from '@button-inc/bcgov-theme'; import styled from 'styled-components'; import Label from 'components/Form/Label'; +interface ObjectOptionProps { + value: string | number; + label: string; +} + interface SelectWidgetProps extends WidgetProps { customOption?: React.ReactNode; + objectOptions?: ObjectOptionProps[]; } interface SelectProps { @@ -64,10 +70,11 @@ const SelectWidget: React.FC = ({ uiSchema, customOption, rawErrors, + objectOptions, }) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const options = schema.enum as Array; + const options = objectOptions ?? (schema.enum as Array); const description = uiSchema ? uiSchema['ui:description'] : null; const isError = rawErrors && rawErrors.length > 0 && !value; @@ -91,11 +98,17 @@ const SelectWidget: React.FC = ({ - {options?.map((opt) => ( - - ))} + {options?.map((opt, index) => { + return ( + + ); + })} {customOption ?? customOption} {description && } diff --git a/app/lib/theme/widgets/custom/CommunitySourceWidget.tsx b/app/lib/theme/widgets/custom/CommunitySourceWidget.tsx new file mode 100644 index 000000000..e1d42d46e --- /dev/null +++ b/app/lib/theme/widgets/custom/CommunitySourceWidget.tsx @@ -0,0 +1,226 @@ +import { WidgetProps } from '@rjsf/utils'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import Autocomplete from '@mui/material/Autocomplete'; +import { TextField } from '@mui/material'; +import styled from 'styled-components'; +import { Button } from '@button-inc/bcgov-theme'; + +interface CommunitySourceWidgetProps extends WidgetProps { + children: React.ReactNode; +} + +const StyledDiv = styled.div` + display: flex; + flex-direction: row; + gap: 4px; + margin-top: 8px; + margin-bottom: 8px; + align-items: center; +`; + +const StyledButton = styled(Button)` + height: 40px; + padding: 5px; + margin: 2px; +`; + +const CommunitySourceWidget: React.FC = (props) => { + const { value, formContext, onChange } = props; + + const { + rowId, + bcGeographicName, + economicRegion, + regionalDistrict, + geographicNameId, + } = value ?? {}; + const [selectedEconomicRegion, setSelectedEconomicRegion] = useState( + economicRegion ?? '' + ); + const [selectedRegionalDistrict, setSelectedRegionalDistrict] = + useState(regionalDistrict ?? ''); + const [selectedGeographicName, setSelectedGeographicName] = useState({ + value: geographicNameId, + label: bcGeographicName, + }); + + useEffect(() => { + setSelectedEconomicRegion(economicRegion); + setSelectedRegionalDistrict(regionalDistrict); + setSelectedGeographicName({ + value: geographicNameId ?? null, + label: bcGeographicName ?? '', + }); + }, [geographicNameId, bcGeographicName, economicRegion, regionalDistrict]); + + const selectedGeographicNameIdList = useMemo(() => { + return [ + ...formContext.cbcCommunitiesData.map( + (community) => + community.communitiesSourceDataByCommunitiesSourceDataId + .geographicNameId + ), + ...formContext.addedCommunities, + ]; + }, [formContext.cbcCommunitiesData, formContext.addedCommunities]); + + const clearWidget = useCallback(() => { + setSelectedEconomicRegion(null); + setSelectedRegionalDistrict(null); + setSelectedGeographicName({ value: null, label: '' }); + // check if the form value has been touched, and if so clear it + if (Object.keys(value).length > 0) { + onChange({}); + } + }, [ + setSelectedEconomicRegion, + setSelectedRegionalDistrict, + setSelectedGeographicName, + value, + onChange, + ]); + + const economicRegionOptions = formContext.economicRegions; + const regionalDistrictOptions = formContext.regionalDistrictsByEconomicRegion; + const geographicNameOptions = formContext.geographicNamesByRegionalDistrict; + + const isGeographicNameOptionDisabled = (option) => { + return selectedGeographicNameIdList.includes(option.value); + }; + + const getGeographicNameOptions = (selectedRegDis, selEcoReg) => { + if (!selectedRegDis && !selEcoReg) { + return []; + } + + if (!selectedRegDis && geographicNameOptions[selEcoReg]['null']) { + return [...geographicNameOptions[selEcoReg]['null']]; + } + + if (geographicNameOptions[selEcoReg][selectedRegDis]) { + return [...geographicNameOptions[selEcoReg][selectedRegDis]]; + } + return []; + }; + + return ( + + { + if (reason === 'clear') { + clearWidget(); + } + if (e) { + setSelectedRegionalDistrict(null); + setSelectedGeographicName({ value: null, label: '' }); + setSelectedEconomicRegion(val); + } + }} + style={{ width: '200px' }} + value={selectedEconomicRegion} + inputValue={selectedEconomicRegion ?? ''} + options={economicRegionOptions} + getOptionLabel={(option) => option} + renderInput={(params) => ( + + )} + /> + + { + if (reason === 'clear') { + setSelectedRegionalDistrict(''); + setSelectedGeographicName({ value: null, label: '' }); + } + if (e) { + setSelectedRegionalDistrict(val); + } + }} + value={selectedRegionalDistrict} + inputValue={selectedRegionalDistrict ?? ''} + options={ + regionalDistrictOptions[selectedEconomicRegion] + ? [...regionalDistrictOptions[selectedEconomicRegion]] + : [] + } + getOptionLabel={(option) => option} + renderInput={(params) => ( + + )} + /> + + ( + + )} + options={getGeographicNameOptions( + selectedRegionalDistrict, + selectedEconomicRegion + )} + isOptionEqualToValue={(option, val) => { + return option.value === val.value; + }} + getOptionDisabled={isGeographicNameOptionDisabled} + getOptionLabel={(option) => { + return option.label ?? ''; + }} + value={selectedGeographicName} + inputValue={selectedGeographicName?.label ?? ''} + onChange={(e, val, reason) => { + if (reason === 'clear') { + setSelectedGeographicName({ value: null, label: '' }); + return; + } + if (e) { + setSelectedGeographicName(val); + onChange({ + bcGeographicName: val.label, + economicRegion: selectedEconomicRegion, + regionalDistrict: selectedRegionalDistrict, + geographicNameId: val.value, + }); + } + }} + /> + {!rowId && ( + { + e.preventDefault(); + clearWidget(); + }} + data-testid="clear-community-button" + > + Clear + + )} + + ); +}; + +export default CommunitySourceWidget; diff --git a/app/package.json b/app/package.json index fd29f5abf..2f281d7ff 100644 --- a/app/package.json +++ b/app/package.json @@ -69,7 +69,7 @@ "dayjs": "^1.11.11", "debug": "^4.1.7", "delay": "^5.0.0", - "dotenv": "^16.3.1", + "dotenv": "^16.4.5", "express": "^4.19.2", "express-rate-limit": "^7.4.0", "express-session": "1.18.0", @@ -116,7 +116,7 @@ "devDependencies": { "@aws-sdk/client-s3": "^3.629.0", "@microsoft/eslint-formatter-sarif": "^3.0.0", - "@testing-library/cypress": "^10.0.1", + "@testing-library/cypress": "^10.0.2", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^14.1.2", "@testing-library/react-hooks": "^8.0.1", @@ -126,7 +126,7 @@ "@types/convict": "^6.1.1", "@types/cookie-parser": "^1.4.3", "@types/debug": "^4.1.12", - "@types/express": "^4.17.13", + "@types/express": "^4.17.21", "@types/jest": "^28.1.6", "@types/json-diff": "^1.0.0", "@types/luxon": "^3.3.2", @@ -155,7 +155,7 @@ "fetch-mock": "9.11.0", "fse": "^4.0.1", "happo-cypress": "^4.2.0", - "happo-e2e": "^2.6.0", + "happo-e2e": "^2.6.1", "happo.io": "^8.3.3", "jest": "^28.1.0", "jest-environment-jsdom": "^28.1.0", diff --git a/app/pages/analyst/application/[applicationId].tsx b/app/pages/analyst/application/[applicationId].tsx index 5b4c03be7..b067ae629 100644 --- a/app/pages/analyst/application/[applicationId].tsx +++ b/app/pages/analyst/application/[applicationId].tsx @@ -11,6 +11,7 @@ import { ApplicationIdQuery } from '__generated__/ApplicationIdQuery.graphql'; import ReviewTheme from 'components/Review/ReviewTheme'; import AnalystLayout from 'components/Analyst/AnalystLayout'; import styled from 'styled-components'; +import { useRouter } from 'next/router'; const StyledButton = styled('button')` color: ${(props) => props.theme.color.links}; @@ -82,6 +83,7 @@ const Application = ({ applicationRfiDataByApplicationId, } = applicationByRowId; const isEditable = status !== 'withdrawn'; + const { section: toggledSection } = useRouter().query; const rfiList = applicationRfiDataByApplicationId?.edges?.map( (edge) => edge.node.rfiDataByRfiDataId @@ -133,6 +135,7 @@ const Application = ({ errors: formErrorSchema, rfiList, toggleOverride, + toggledSection, isEditable, }} formData={jsonData} diff --git a/app/pages/analyst/application/[applicationId]/summary.tsx b/app/pages/analyst/application/[applicationId]/summary.tsx new file mode 100644 index 000000000..4129006f3 --- /dev/null +++ b/app/pages/analyst/application/[applicationId]/summary.tsx @@ -0,0 +1,238 @@ +import defaultRelayOptions from 'lib/relay/withRelayOptions'; +import { graphql } from 'react-relay'; +import { withRelay, RelayProps } from 'relay-nextjs'; +import { usePreloadedQuery } from 'react-relay/hooks'; +import Layout from 'components/Layout'; +import AnalystLayout from 'components/Analyst/AnalystLayout'; +import { summaryQuery } from '__generated__/summaryQuery.graphql'; +import CbcForm from 'components/Analyst/CBC/CbcForm'; +import ReviewTheme from 'components/Review/ReviewTheme'; +import reviewUiSchema from 'formSchema/uiSchema/summary/reviewUiSchema'; +import review from 'formSchema/analyst/summary/review'; +import styled from 'styled-components'; +import { Tooltip } from '@mui/material'; +import { Info } from '@mui/icons-material'; +import { useState } from 'react'; +import generateFormData from 'lib/helpers/ccbcSummaryGenerateFormData'; + +const getSummaryQuery = graphql` + query summaryQuery($rowId: Int!) { + session { + sub + } + applicationByRowId(rowId: $rowId) { + announcements { + totalCount + } + applicationAnnouncedsByApplicationId( + last: 1 + condition: { archivedAt: null } + ) { + nodes { + announced + } + } + applicationStatusesByApplicationId( + filter: { status: { equalTo: "submitted" } } + ) { + nodes { + createdAt + status + } + } + allAssessments { + nodes { + assessmentDataType + jsonData + } + } + formData { + jsonData + } + projectInformationDataByApplicationId(last: 1) { + nodes { + jsonData + } + } + applicationMilestoneExcelDataByApplicationId( + condition: { archivedAt: null } + ) { + nodes { + jsonData + } + } + conditionalApproval { + jsonData + } + changeRequestDataByApplicationId { + edges { + node { + id + } + } + } + status + intakeNumber + } + # Cannot run it inside the above due to conflict of filter with header + allApplicationSowData( + filter: { applicationId: { equalTo: $rowId } } + condition: { archivedAt: null } + last: 1 + ) { + nodes { + rowId + jsonData + sowTab1SBySowId { + nodes { + jsonData + rowId + sowId + } + } + sowTab2SBySowId { + nodes { + rowId + sowId + jsonData + } + } + sowTab7SBySowId { + nodes { + jsonData + rowId + sowId + } + } + sowTab8SBySowId { + nodes { + rowId + jsonData + sowId + } + } + } + } + allIntakes { + nodes { + closeTimestamp + ccbcIntakeNumber + } + } + ...AnalystLayout_query + } +`; + +const StyledInfo = styled(Info)` + color: ${(props) => props.theme.color.primaryBlue}; + float: right; + cursor: pointer; + padding-bottom: 2px; +`; + +const StyledSummaryForm = styled(CbcForm)` + margin-bottom: 0px; +`; + +const StyledButton = styled('button')` + color: ${(props) => props.theme.color.links}; +`; + +const RightAlignText = styled('div')` + padding-top: 20px; + text-align: right; + padding-bottom: 4px; +`; + +const Summary = ({ + preloadedQuery, +}: RelayProps, summaryQuery>) => { + const query = usePreloadedQuery(getSummaryQuery, preloadedQuery); + const { applicationByRowId, allApplicationSowData, allIntakes, session } = + query; + const [toggleOverride, setToggleExpandOrCollapseAll] = useState< + boolean | undefined + >(true); + + const { formData, formDataSource, errors } = generateFormData( + applicationByRowId, + allApplicationSowData, + allIntakes + ); + + return ( + + + + <> + { + setToggleExpandOrCollapseAll(true); + }} + type="button" + > + Expand all + + {' | '} + { + setToggleExpandOrCollapseAll(false); + }} + type="button" + > + Collapse all + + {' | '} + + The fields on this page are read-only and display information + from the application, Conditional Approval, and SOW documents, + based on the application's status. + + } + placement="top" + > + + + + + {}} + isExpanded + isFormAnimated={false} + isFormEditMode={false} + title="Summary" + theme={ReviewTheme} + schema={review} + uiSchema={reviewUiSchema} + resetFormData={() => {}} + onSubmit={() => {}} + setIsFormEditMode={() => {}} + saveBtnText="Save" + /> + + + ); +}; + +export const withRelayOptions = { + ...defaultRelayOptions, + + variablesFromContext: (ctx) => { + return { + rowId: parseInt(ctx.query.applicationId.toString(), 10), + }; + }, +}; + +export default withRelay(Summary, getSummaryQuery, withRelayOptions); diff --git a/app/pages/analyst/cbc/[cbcId].tsx b/app/pages/analyst/cbc/[cbcId].tsx index 3375a1521..fa0073c1c 100644 --- a/app/pages/analyst/cbc/[cbcId].tsx +++ b/app/pages/analyst/cbc/[cbcId].tsx @@ -9,17 +9,23 @@ import CbcForm from 'components/Analyst/CBC/CbcForm'; import { ChangeModal } from 'components/Analyst'; import styled from 'styled-components'; import ReviewTheme from 'components/Review/ReviewTheme'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useUpdateCbcDataAndInsertChangeRequest } from 'schema/mutations/cbc/updateCbcDataAndInsertChangeReason'; import review from 'formSchema/analyst/cbc/review'; import reviewUiSchema from 'formSchema/uiSchema/cbc/reviewUiSchema'; import editUiSchema from 'formSchema/uiSchema/cbc/editUiSchema'; import { useFeature } from '@growthbook/growthbook-react'; import CbcTheme from 'components/Analyst/CBC/CbcTheme'; -import { createCbcSchemaData } from 'utils/schemaUtils'; +import { + createCbcSchemaData, + generateGeographicNamesByRegionalDistrict, + generateRegionalDistrictsByEconomicRegion, + getAllEconomicRegionNames, +} from 'utils/schemaUtils'; import customValidate, { CBC_WARN_COLOR } from 'utils/cbcCustomValidator'; import CbcRecordLock from 'components/Analyst/CBC/CbcRecordLock'; import useModal from 'lib/helpers/useModal'; +import { useUpdateCbcCommunityDataMutationMutation } from 'schema/mutations/cbc/updateCbcCommunityData'; const getCbcQuery = graphql` query CbcIdQuery($rowId: Int!) { @@ -39,7 +45,8 @@ const getCbcQuery = graphql` } } } - cbcProjectCommunitiesByCbcId { + cbcProjectCommunitiesByCbcId(filter: { archivedAt: { isNull: true } }) { + __id nodes { communitiesSourceDataByCommunitiesSourceDataId { economicRegion @@ -47,10 +54,19 @@ const getCbcQuery = graphql` geographicType regionalDistrict bcGeographicName + rowId } } } } + allCommunitiesSourceData { + nodes { + geographicNameId + bcGeographicName + economicRegion + regionalDistrict + } + } session { authRole sub @@ -95,6 +111,60 @@ const Cbc = ({ const { rowId } = query.cbcByRowId; const [formData, setFormData] = useState({} as any); const [baseFormData, setBaseFormData] = useState({} as any); + const [addedCommunities, setAddedCommunities] = useState([]); + const [removedCommunities, setRemovedCommunities] = useState([]); + const [responseCommunityData, setResponseCommunityData] = useState([]); + + const addCommunity = (communityId) => { + setAddedCommunities((prevList) => [...prevList, communityId]); + }; + + const removeCommunity = (communityId) => { + setRemovedCommunities((prevList) => [...prevList, communityId]); + const indexOfRemovedCommunity = + formData.locations.communitySourceData.findIndex( + (community) => community.geographicNameId === communityId + ); + setFormData({ + ...formData, + locations: { + ...formData.locations, + communitySourceData: [ + ...formData.locations.communitySourceData.slice( + 0, + indexOfRemovedCommunity + ), + ...formData.locations.communitySourceData.slice( + indexOfRemovedCommunity + 1 + ), + ], + }, + }); + }; + + const handleAddClick = (formPayload) => { + const communitySourceArray = formPayload.locations + .communitySourceData as Array; + const communitySourceArrayLength = + formPayload.locations.communitySourceData?.length; + if (communitySourceArray[communitySourceArrayLength - 1] === undefined) { + if (communitySourceArray[0]) + addCommunity(communitySourceArray[0].geographicNameId); + return { + ...formPayload, + locations: { + ...formPayload.locations, + communitySourceData: [ + {}, + // done to ensure that the added piece is now readonly + { ...communitySourceArray[0], rowId: true }, + ...communitySourceArray.slice(1, communitySourceArrayLength - 1), + ], + }, + }; + } + return formPayload; + }; const changeModal = useModal(); @@ -102,19 +172,40 @@ const Cbc = ({ const [allowEdit, setAllowEdit] = useState( isCbcAdmin && editFeatureEnabled && !recordLocked ); + + const allCommunitiesSourceData = query.allCommunitiesSourceData.nodes; + + const geographicNamesByRegionalDistrict = useMemo(() => { + return generateGeographicNamesByRegionalDistrict(allCommunitiesSourceData); + }, [allCommunitiesSourceData]); + + const regionalDistrictsByEconomicRegion = useMemo(() => { + return generateRegionalDistrictsByEconomicRegion(allCommunitiesSourceData); + }, [allCommunitiesSourceData]); + + const allEconomicRegions = useMemo(() => { + return getAllEconomicRegionNames(allCommunitiesSourceData); + }, [allCommunitiesSourceData]); + + const cbcCommunitiesData = useMemo(() => { + if (responseCommunityData.length > 0) return responseCommunityData; + return query.cbcByRowId.cbcProjectCommunitiesByCbcId.nodes?.map( + (node) => node.communitiesSourceDataByCommunitiesSourceDataId + ); + }, [query, responseCommunityData]); + useEffect(() => { const { cbcByRowId } = query; - const { cbcDataByCbcId, cbcProjectCommunitiesByCbcId } = cbcByRowId; + const { cbcDataByCbcId } = cbcByRowId; + const { edges } = cbcDataByCbcId; - const cbcCommunitiesData = cbcProjectCommunitiesByCbcId.nodes?.map( - (node) => node.communitiesSourceDataByCommunitiesSourceDataId - ); const cbcData = edges[0].node; const { jsonData } = cbcData; const { tombstone, projectType, + locations, locationsAndCounts, funding, eventsAndDates, @@ -128,6 +219,7 @@ const Cbc = ({ setFormData({ tombstone, projectType, + locations, locationsAndCounts, funding, eventsAndDates, @@ -137,6 +229,7 @@ const Cbc = ({ setBaseFormData({ tombstone, projectType, + locations, locationsAndCounts, funding, eventsAndDates, @@ -147,15 +240,45 @@ const Cbc = ({ setAllowEdit( isCbcAdmin && editFeatureEnabled && !projectDataReviews?.locked ); - }, [query, isCbcAdmin, editFeatureEnabled]); + }, [query, isCbcAdmin, editFeatureEnabled, cbcCommunitiesData]); const [updateFormData] = useUpdateCbcDataAndInsertChangeRequest(); + const [updateCbcCommunitySourceData] = + useUpdateCbcCommunityDataMutationMutation(); const handleChangeRequestModal = () => { setChangeReason(null); changeModal.open(); }; + const handleUpdateCommunitySource = useCallback(() => { + updateCbcCommunitySourceData({ + variables: { + input: { + _projectId: rowId, + _communityIdsToAdd: addedCommunities, + _communityIdsToArchive: removedCommunities, + }, + }, + debounceKey: 'cbc_update_community_source_data', + onCompleted: (response) => { + setAddedCommunities([]); + setRemovedCommunities([]); + setResponseCommunityData( + response.editCbcProjectCommunities.cbcProjectCommunities.map( + (proj) => proj.communitiesSourceDataByCommunitiesSourceDataId + ) + ); + }, + }); + }, [ + addedCommunities, + removedCommunities, + updateCbcCommunitySourceData, + rowId, + setResponseCommunityData, + ]); + const handleSubmit = () => { const { geographicNames, @@ -163,6 +286,7 @@ const Cbc = ({ economicRegions, ...updatedLocationsAndCounts } = formData.locationsAndCounts; + const { projectLocations } = formData.locations; updateFormData({ variables: { inputCbcData: { @@ -171,6 +295,7 @@ const Cbc = ({ jsonData: { ...formData.tombstone, ...formData.projectType, + projectLocations, ...updatedLocationsAndCounts, ...formData.funding, ...formData.eventsAndDates, @@ -191,6 +316,7 @@ const Cbc = ({ onCompleted: () => { setEditMode(false); changeModal.close(); + handleUpdateCommunitySource(); setAllowEdit(isCbcAdmin && editFeatureEnabled); }, }); @@ -312,10 +438,18 @@ const Cbc = ({ errors: formErrors, showErrorHint: true, recordLocked, + geographicNamesByRegionalDistrict, + regionalDistrictsByEconomicRegion, + economicRegions: allEconomicRegions, + cbcCommunitiesData: + query.cbcByRowId.cbcProjectCommunitiesByCbcId.nodes, + addedCommunities, + addCommunitySource: addCommunity, + deleteCommunitySource: removeCommunity, }} formData={formData} handleChange={(e) => { - setFormData({ ...e.formData }); + setFormData(handleAddClick(e.formData)); }} hiddenSubmitRef={hiddenSubmitRef} isExpanded @@ -337,6 +471,8 @@ const Cbc = ({ setChangeReason(null); setFormData(baseFormData); setEditMode(false); + setAddedCommunities([]); + setRemovedCommunities([]); changeModal.close(); }} value={changeReason} diff --git a/app/pages/analyst/cbc/[cbcId]/edit/[section].tsx b/app/pages/analyst/cbc/[cbcId]/edit/[section].tsx index c7718455c..f145d0399 100644 --- a/app/pages/analyst/cbc/[cbcId]/edit/[section].tsx +++ b/app/pages/analyst/cbc/[cbcId]/edit/[section].tsx @@ -7,7 +7,7 @@ import { RelayProps, withRelay } from 'relay-nextjs'; import { graphql } from 'relay-runtime'; import review from 'formSchema/analyst/cbc/review'; import { ProjectTheme } from 'components/Analyst/Project'; -import { useState } from 'react'; +import { useCallback, useMemo, useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import editUiSchema from 'formSchema/uiSchema/cbc/editUiSchema'; import { FormBase } from 'components/Form'; @@ -16,7 +16,15 @@ import { RJSFSchema } from '@rjsf/utils'; import useModal from 'lib/helpers/useModal'; import { ChangeModal } from 'components/Analyst'; import { useUpdateCbcDataAndInsertChangeRequest } from 'schema/mutations/cbc/updateCbcDataAndInsertChangeReason'; -import { createCbcSchemaData } from 'utils/schemaUtils'; +import { + createCbcSchemaData, + generateGeographicNamesByRegionalDistrict, + generateRegionalDistrictsByEconomicRegion, + getAllEconomicRegionNames, +} from 'utils/schemaUtils'; +import ArrayLocationFieldTemplate from 'lib/theme/fields/ArrayLocationDataField'; +import { useUpdateCbcCommunityDataMutationMutation } from 'schema/mutations/cbc/updateCbcCommunityData'; +import customValidate, { CBC_WARN_COLOR } from 'utils/cbcCustomValidator'; const getCbcSectionQuery = graphql` query SectionCbcDataQuery($rowId: Int!) { @@ -36,7 +44,7 @@ const getCbcSectionQuery = graphql` } } } - cbcProjectCommunitiesByCbcId { + cbcProjectCommunitiesByCbcId(filter: { archivedAt: { isNull: true } }) { nodes { communitiesSourceDataByCommunitiesSourceDataId { economicRegion @@ -44,10 +52,19 @@ const getCbcSectionQuery = graphql` geographicType regionalDistrict bcGeographicName + rowId } } } } + allCommunitiesSourceData { + nodes { + geographicNameId + bcGeographicName + economicRegion + regionalDistrict + } + } session { sub } @@ -65,26 +82,129 @@ const EditCbcSection = ({ const [updateFormData] = useUpdateCbcDataAndInsertChangeRequest(); const [changeReason, setChangeReason] = useState(null); const [formData, setFormData] = useState(null); + const [addedCommunities, setAddedCommunities] = useState([]); + const [removedCommunities, setRemovedCommunities] = useState([]); const { cbcDataByCbcId, rowId, cbcProjectCommunitiesByCbcId } = cbcByRowId; const { jsonData, rowId: cbcDataRowId } = cbcDataByCbcId.edges[0].node; - const cbcCommunitiesData = - cbcProjectCommunitiesByCbcId.nodes?.map( - (node) => node.communitiesSourceDataByCommunitiesSourceDataId - ) || []; - const dataBySection = createCbcSchemaData({ - ...jsonData, - cbcCommunitiesData, - }); + useEffect(() => { + const cbcCommunitiesData = + cbcProjectCommunitiesByCbcId.nodes?.map( + (node) => node.communitiesSourceDataByCommunitiesSourceDataId + ) || []; + setFormData( + createCbcSchemaData({ + ...jsonData, + cbcCommunitiesData, + }) + ); + }, [jsonData, cbcProjectCommunitiesByCbcId]); const changeModal = useModal(); + const addCommunity = (communityId) => { + setAddedCommunities((prevList) => [...prevList, communityId]); + }; + + const removeCommunity = (communityId) => { + setRemovedCommunities((prevList) => [...prevList, communityId]); + const indexOfRemovedCommunity = + formData.locations?.communitySourceData?.findIndex( + (community) => community.geographicNameId === communityId + ); + setFormData({ + ...formData, + locations: { + ...formData.locations, + communitySourceData: [ + ...formData.locations.communitySourceData.slice( + 0, + indexOfRemovedCommunity + ), + ...formData.locations.communitySourceData.slice( + indexOfRemovedCommunity + 1 + ), + ], + }, + }); + }; + + const handleAddClick = (formPayload) => { + const communitySourceArray = formPayload.communitySourceData as Array; + const communitySourceArrayLength = formPayload.communitySourceData?.length; + if (communitySourceArray[communitySourceArrayLength - 1] === undefined) { + if (communitySourceArray[0]) + addCommunity(communitySourceArray[0].geographicNameId); + return { + ...formPayload, + communitySourceData: [ + {}, + // setRowId to make widget readonly + { ...communitySourceArray[0], rowId: true }, + ...communitySourceArray.slice(1, communitySourceArrayLength - 1), + ], + }; + } + return formPayload; + }; + + const [updateCbcCommunitySourceData] = + useUpdateCbcCommunityDataMutationMutation(); + + const handleOnChange = (e) => { + if (section === 'locations') { + setFormData({ + ...formData, + [section]: handleAddClick(e.formData), + }); + } else setFormData({ ...formData, [section]: e.formData }); + }; + + const handleUpdateCommunitySource = useCallback(() => { + updateCbcCommunitySourceData({ + variables: { + input: { + _projectId: rowId, + _communityIdsToAdd: addedCommunities, + _communityIdsToArchive: removedCommunities, + }, + }, + debounceKey: 'cbc_update_community_source_data', + onCompleted: () => { + setAddedCommunities([]); + setRemovedCommunities([]); + }, + }); + }, [ + addedCommunities, + removedCommunities, + updateCbcCommunitySourceData, + rowId, + ]); + const handleChangeRequestModal = (e) => { changeModal.open(); - setFormData({ ...dataBySection, [section]: e.formData }); + setFormData({ ...formData, [section]: e.formData }); }; + const allCommunitiesSourceData = query.allCommunitiesSourceData.nodes; + + const geographicNamesByRegionalDistrict = useMemo(() => { + return generateGeographicNamesByRegionalDistrict(allCommunitiesSourceData); + }, [allCommunitiesSourceData]); + + const regionalDistrictsByEconomicRegion = useMemo(() => { + return generateRegionalDistrictsByEconomicRegion(allCommunitiesSourceData); + }, [allCommunitiesSourceData]); + + const allEconomicRegions = useMemo(() => { + return getAllEconomicRegionNames(allCommunitiesSourceData); + }, [allCommunitiesSourceData]); + + const theme = { ...ProjectTheme }; + theme.templates.ArrayFieldTemplate = ArrayLocationFieldTemplate; + const handleSubmit = () => { const { geographicNames, @@ -92,6 +212,7 @@ const EditCbcSection = ({ economicRegions, ...updatedLocationsAndCounts } = formData.locationsAndCounts; + const { projectLocations } = formData.locations; updateFormData({ variables: { inputCbcData: { @@ -100,6 +221,7 @@ const EditCbcSection = ({ jsonData: { ...formData.tombstone, ...formData.projectType, + projectLocations, ...updatedLocationsAndCounts, ...formData.funding, ...formData.eventsAndDates, @@ -117,23 +239,63 @@ const EditCbcSection = ({ }, debounceKey: 'cbc_update_section_data', onCompleted: () => { + handleUpdateCommunitySource(); router.push(`/analyst/cbc/${rowId}`); }, }); }; + const validateSection = useCallback( + (data, schema) => { + const sectionErrors: any = {}; + if (!schema?.properties) return sectionErrors; + + Object.keys(schema.properties).forEach((fieldKey) => { + const fieldErrorList = customValidate(data, section, fieldKey); + if (fieldErrorList.length > 0) { + sectionErrors[fieldKey] = { + __errors: fieldErrorList, + errorColor: CBC_WARN_COLOR, + }; + } + }); + + return sectionErrors; + }, + [section] + ); + + const formErrors = useMemo( + () => validateSection(formData ?? {}, review.properties[section]), + [formData, section, validateSection] + ); + return (