From 3bcef94a1fc3430325c1ba68be1eb51c3bd1aff2 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 3 Aug 2023 09:06:44 -0700 Subject: [PATCH 001/125] WIP --- api/src/paths/funding-sources.ts | 96 +++++++++++++++++++ .../repositories/funding-source-repository.ts | 3 + api/src/services/funding-source-service.ts | 13 +++ 3 files changed, 112 insertions(+) create mode 100644 api/src/paths/funding-sources.ts create mode 100644 api/src/repositories/funding-source-repository.ts create mode 100644 api/src/services/funding-source-service.ts diff --git a/api/src/paths/funding-sources.ts b/api/src/paths/funding-sources.ts new file mode 100644 index 0000000000..3c0bb0fd81 --- /dev/null +++ b/api/src/paths/funding-sources.ts @@ -0,0 +1,96 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../constants/roles'; +import { getDBConnection } from '../database/db'; +import { authorizeRequestHandler } from '../request-handlers/security/authorization'; +import { DraftService } from '../services/draft-service'; +import { getLogger } from '../utils/logger'; + +const defaultLog = getLogger('paths/funding-sources'); + +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + getFundingSources() +]; + +GET.apiDoc = { + description: 'Get all funding sources.', + tags: ['funding-source'], + security: [ + { + Bearer: [] + } + ], + responses: { + 200: { + description: 'Funding sources response object.', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + required: [], + properties: {} + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get a list of funding sources. + * + * @returns {RequestHandler} + */ +export function getFundingSources(): RequestHandler { + return async (req, res) => { + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + const systemUserId = connection.systemUserId(); + + const draftService = new DraftService(connection); + + const drafts = await draftService.getDraftList(systemUserId); + + await connection.commit(); + + return res.status(200).json(drafts); + } catch (error) { + defaultLog.error({ label: 'getFundingSources', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts new file mode 100644 index 0000000000..d5fdfd63f1 --- /dev/null +++ b/api/src/repositories/funding-source-repository.ts @@ -0,0 +1,3 @@ +import { BaseRepository } from './base-repository'; + +export class FundingSourceRepository extends BaseRepository {} diff --git a/api/src/services/funding-source-service.ts b/api/src/services/funding-source-service.ts new file mode 100644 index 0000000000..d530daeb61 --- /dev/null +++ b/api/src/services/funding-source-service.ts @@ -0,0 +1,13 @@ +import { IDBConnection } from '../database/db'; +import { FundingSourceRepository } from '../repositories/funding-source-repository'; +import { DBService } from './db-service'; + +export class FundingSourceService extends DBService { + fundingSourceRepository: FundingSourceRepository; + + constructor(connection: IDBConnection) { + super(connection); + + this.fundingSourceRepository = new FundingSourceRepository(connection); + } +} From fbffab85e5be37ba5ffe55b3c48f0e493b23e8aa Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 3 Aug 2023 12:36:50 -0700 Subject: [PATCH 002/125] Stub out initial endpoints, service/repo files, and frontend folders --- api/src/paths/funding-source.ts | 103 ++++++ .../paths/funding-source/{fundingSourceId}.ts | 302 ++++++++++++++++++ api/src/paths/funding-sources.ts | 9 +- .../list/FundingSourcesListPage.tsx | 146 +++++++++ .../list/FundingSourcesTable.tsx | 195 +++++++++++ 5 files changed, 748 insertions(+), 7 deletions(-) create mode 100644 api/src/paths/funding-source.ts create mode 100644 api/src/paths/funding-source/{fundingSourceId}.ts create mode 100644 app/src/features/funding-sources/list/FundingSourcesListPage.tsx create mode 100644 app/src/features/funding-sources/list/FundingSourcesTable.tsx diff --git a/api/src/paths/funding-source.ts b/api/src/paths/funding-source.ts new file mode 100644 index 0000000000..0053c619a4 --- /dev/null +++ b/api/src/paths/funding-source.ts @@ -0,0 +1,103 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../constants/roles'; +import { getDBConnection } from '../database/db'; +import { authorizeRequestHandler } from '../request-handlers/security/authorization'; +import { getLogger } from '../utils/logger'; + +const defaultLog = getLogger('paths/funding-source/{fundingSourceId}'); + +export const POST: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + postFundingSource() +]; + +POST.apiDoc = { + description: 'Create a funding source.', + tags: ['funding-source'], + security: [ + { + Bearer: [] + } + ], + requestBody: { + description: 'Funding source post request object.', + content: { + 'application/json': { + schema: { + type: 'object', + required: [], + properties: {} + } + } + } + }, + responses: { + 200: { + description: 'Funding source response object.', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + required: [], + properties: {} + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Create a new funding source. + * + * @returns {RequestHandler} + */ +export function postFundingSource(): RequestHandler { + return async (req, res) => { + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + // TODO + + await connection.commit(); + + return res.status(200).json(); + } catch (error) { + defaultLog.error({ label: 'createFundingSource', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/funding-source/{fundingSourceId}.ts b/api/src/paths/funding-source/{fundingSourceId}.ts new file mode 100644 index 0000000000..58a2f855c6 --- /dev/null +++ b/api/src/paths/funding-source/{fundingSourceId}.ts @@ -0,0 +1,302 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../constants/roles'; +import { getDBConnection } from '../../database/db'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { getLogger } from '../../utils/logger'; + +const defaultLog = getLogger('paths/funding-source/{fundingSourceId}'); + +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + getFundingSource() +]; + +GET.apiDoc = { + description: 'Get a single funding source.', + tags: ['funding-source'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'fundingSourceId', + schema: { + type: 'integer', + minimum: 1 + }, + required: true + } + ], + responses: { + 200: { + description: 'Funding source response object.', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + required: [], + properties: {} + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get a single funding source. + * + * @returns {RequestHandler} + */ +export function getFundingSource(): RequestHandler { + return async (req, res) => { + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + // TODO + + await connection.commit(); + + return res.status(200).json(); + } catch (error) { + defaultLog.error({ label: 'getFundingSource', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} + +export const PUT: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + getFundingSource() +]; + +PUT.apiDoc = { + description: 'Update a single funding source.', + tags: ['funding-source'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'fundingSourceId', + schema: { + type: 'integer', + minimum: 1 + }, + required: true + } + ], + requestBody: { + description: 'Funding source put request object.', + content: { + 'application/json': { + schema: { + type: 'object', + required: [], + properties: {} + } + } + } + }, + responses: { + 200: { + description: 'Funding source response object.', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + required: [], + properties: {} + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Update a single funding source. + * + * @returns {RequestHandler} + */ +export function putFundingSource(): RequestHandler { + return async (req, res) => { + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + // TODO + + await connection.commit(); + + return res.status(200).json(); + } catch (error) { + defaultLog.error({ label: 'putFundingSource', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} + +export const DELETE: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + getFundingSource() +]; + +DELETE.apiDoc = { + description: 'Delete a single funding source.', + tags: ['funding-source'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'fundingSourceId', + schema: { + type: 'integer', + minimum: 1 + }, + required: true + } + ], + responses: { + 200: { + description: 'Funding source response object.', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'object', + required: [], + properties: {} + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Delete a single funding source. + * + * @returns {RequestHandler} + */ +export function deleteFundingSource(): RequestHandler { + return async (req, res) => { + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + // TODO + + await connection.commit(); + + return res.status(200).json(); + } catch (error) { + defaultLog.error({ label: 'deleteFundingSource', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/funding-sources.ts b/api/src/paths/funding-sources.ts index 3c0bb0fd81..fc87943c9c 100644 --- a/api/src/paths/funding-sources.ts +++ b/api/src/paths/funding-sources.ts @@ -3,7 +3,6 @@ import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../constants/roles'; import { getDBConnection } from '../database/db'; import { authorizeRequestHandler } from '../request-handlers/security/authorization'; -import { DraftService } from '../services/draft-service'; import { getLogger } from '../utils/logger'; const defaultLog = getLogger('paths/funding-sources'); @@ -76,15 +75,11 @@ export function getFundingSources(): RequestHandler { try { await connection.open(); - const systemUserId = connection.systemUserId(); - - const draftService = new DraftService(connection); - - const drafts = await draftService.getDraftList(systemUserId); + // TODO await connection.commit(); - return res.status(200).json(drafts); + return res.status(200).json(); } catch (error) { defaultLog.error({ label: 'getFundingSources', message: 'error', error }); await connection.rollback(); diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx new file mode 100644 index 0000000000..b780d7cc2f --- /dev/null +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -0,0 +1,146 @@ +import { mdiFilterOutline, mdiPlus } from '@mdi/js'; +import Icon from '@mdi/react'; +import { Theme } from '@mui/material'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import Container from '@mui/material/Container'; +import Paper from '@mui/material/Paper'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; +import ProjectsSubmissionAlertBar from 'components/publish/ProjectListSubmissionAlertBar'; +import { IProjectAdvancedFilters } from 'components/search-filter/ProjectAdvancedFilters'; +import { SystemRoleGuard } from 'components/security/Guards'; +import { SYSTEM_ROLE } from 'constants/roles'; +import { CodesContext } from 'contexts/codesContext'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; +import React, { useContext, useEffect, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; + +const useStyles = makeStyles((theme: Theme) => ({ + pageTitleContainer: { + maxWidth: '170ch', + overflow: 'hidden', + textOverflow: 'ellipsis' + }, + pageTitle: { + display: '-webkit-box', + '-webkit-line-clamp': 2, + '-webkit-box-orient': 'vertical', + paddingTop: theme.spacing(0.5), + paddingBottom: theme.spacing(0.5), + overflow: 'hidden' + }, + pageTitleActions: { + paddingTop: theme.spacing(0.75), + paddingBottom: theme.spacing(0.75) + }, + actionButton: { + marginLeft: theme.spacing(1), + minWidth: '6rem' + }, + toolbarCount: { + fontWeight: 400 + }, + filtersBox: { + background: '#f7f8fa' + } +})); + +/** + * Page to display a list of funding sources. + * + * @return {*} + */ +const FundingSourcesListPage: React.FC = () => { + const classes = useStyles(); + const biohubApi = useBiohubApi(); + + const [isFiltersOpen, setIsFiltersOpen] = useState(false); + + const codesContext = useContext(CodesContext); + useEffect(() => codesContext.codesDataLoader.load(), [codesContext.codesDataLoader]); + + const projectsDataLoader = useDataLoader((filter?: IProjectAdvancedFilters) => + biohubApi.project.getProjectsList(filter) + ); + projectsDataLoader.load(); + + const draftsDataLoader = useDataLoader(() => biohubApi.draft.getDraftsList()); + draftsDataLoader.load(); + + const handleSubmit = async (filterValues: IProjectAdvancedFilters) => { + projectsDataLoader.refresh(filterValues); + }; + + const handleReset = async () => { + projectsDataLoader.refresh(); + }; + + if (!codesContext.codesDataLoader.data || !projectsDataLoader.data || !draftsDataLoader.data) { + return ; + } + + return ( + <> + + + + + + + Projects + + + + + + + + + + + + + + + + + + + + Records Found ‌ + + ({projectsDataLoader.data?.length || 0}) + + + + + + + + + ); +}; + +export default FundingSourcesListPage; diff --git a/app/src/features/funding-sources/list/FundingSourcesTable.tsx b/app/src/features/funding-sources/list/FundingSourcesTable.tsx new file mode 100644 index 0000000000..38cb8a261b --- /dev/null +++ b/app/src/features/funding-sources/list/FundingSourcesTable.tsx @@ -0,0 +1,195 @@ +import { Theme } from '@mui/material'; +import Chip from '@mui/material/Chip'; +import { grey } from '@mui/material/colors'; +import Link from '@mui/material/Link'; +import Typography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; +import { DataGrid, GridColDef, GridOverlay } from '@mui/x-data-grid'; +import { SubmitStatusChip } from 'components/chips/SubmitStatusChip'; +import { SystemRoleGuard } from 'components/security/Guards'; +import { PublishStatus } from 'constants/attachments'; +import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import { SYSTEM_ROLE } from 'constants/roles'; +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { IGetDraftsListResponse } from 'interfaces/useDraftApi.interface'; +import { IGetProjectsListResponse } from 'interfaces/useProjectApi.interface'; +import { useCallback } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { getFormattedDate } from 'utils/Utils'; + +const useStyles = makeStyles((theme: Theme) => ({ + projectsTable: { + tableLayout: 'fixed' + }, + linkButton: { + textAlign: 'left', + fontWeight: 700 + }, + noDataText: { + fontFamily: 'inherit !important', + fontSize: '0.875rem', + fontWeight: 700 + }, + dataGrid: { + border: 'none !important', + fontFamily: 'inherit !important', + '& .MuiDataGrid-columnHeaderTitle': { + textTransform: 'uppercase', + fontSize: '0.875rem', + fontWeight: 700, + color: grey[600] + }, + '& .MuiDataGrid-cell:focus-within, & .MuiDataGrid-cellCheckbox:focus-within, & .MuiDataGrid-columnHeader:focus-within': + { + outline: 'none !important' + }, + '& .MuiDataGrid-row:hover': { + backgroundColor: 'transparent !important' + } + } +})); + +export interface IProjectsListTableProps { + projects: IGetProjectsListResponse[]; + drafts: IGetDraftsListResponse[]; + codes: IGetAllCodeSetsResponse; +} + +interface IProjectsListTableEntry { + id: number; + isDraft: boolean; + name: string; + status?: PublishStatus; + type?: string; + startDate?: string; + endDate?: string; +} + +const NoRowsOverlay = (props: { className: string }) => ( + + + No funding sources found + + +); + +const FundingSourcesTable = (props: IProjectsListTableProps) => { + const classes = useStyles(); + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + flex: 1, + disableColumnMenu: true, + renderCell: (params) => ( + + ) + }, + { + field: 'program', + headerName: 'Programs', + flex: 1 + }, + { + field: 'regions', + headerName: 'Regions', + flex: 1 + }, + { + field: 'startDate', + headerName: 'Start Date', + minWidth: 150, + valueGetter: ({ value }) => (value ? new Date(value) : undefined), + valueFormatter: ({ value }) => (value ? getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, value) : undefined) + }, + { + field: 'endDate', + headerName: 'End Date', + minWidth: 150, + valueGetter: ({ value }) => (value ? new Date(value) : undefined), + valueFormatter: ({ value }) => (value ? getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, value) : undefined) + }, + { + field: 'status', + headerName: 'Status', + minWidth: 150, + renderCell: (params) => { + if (params.row.isDraft) { + return ; + } + + if (!params.row.status) { + return <>; + } + + //TODO: PRODUCTION_BANDAGE: Remove + return ( + + + + ); + } + } + ]; + + const NoRowsOverlayStyled = useCallback(() => , [classes.noDataText]); + + const getProjectPrograms = (project: IGetProjectsListResponse) => { + return ( + props.codes.program + .filter((code) => project.projectData.project_programs.includes(code.id)) + .map((code) => code.name) + .join(', ') || '' + ); + }; + return ( + ({ + id: draft.webform_draft_id, + name: draft.name, + isDraft: true + })), + ...props.projects.map((project: IGetProjectsListResponse) => ({ + id: project.projectData.id, + name: project.projectData.name, + status: project.projectSupplementaryData.publishStatus, + program: getProjectPrograms(project), + startDate: project.projectData.start_date, + endDate: project.projectData.end_date, + isDraft: false, + regions: project.projectData.regions?.join(', ') + })) + ]} + getRowId={(row) => (row.isDraft ? `draft-${row.id}` : `project-${row.id}`)} + columns={columns} + pageSizeOptions={[5]} + rowSelection={false} + checkboxSelection={false} + hideFooter + disableRowSelectionOnClick + disableColumnSelector + disableColumnFilter + disableColumnMenu + sortingOrder={['asc', 'desc']} + components={{ + NoRowsOverlay: NoRowsOverlayStyled + }} + /> + ); +}; + +export default FundingSourcesTable; From 9ce55d39eed517ef5ecdbd04cb1df66ffbbe3140 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 3 Aug 2023 12:48:26 -0700 Subject: [PATCH 003/125] Initial version of get funding sources endpoint --- api/src/paths/funding-sources.ts | 22 ++++++++++--- .../repositories/funding-source-repository.ts | 31 ++++++++++++++++++- api/src/services/funding-source-service.ts | 12 ++++++- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/api/src/paths/funding-sources.ts b/api/src/paths/funding-sources.ts index fc87943c9c..b83f0dc2e6 100644 --- a/api/src/paths/funding-sources.ts +++ b/api/src/paths/funding-sources.ts @@ -3,6 +3,7 @@ import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../constants/roles'; import { getDBConnection } from '../database/db'; import { authorizeRequestHandler } from '../request-handlers/security/authorization'; +import { FundingSourceService } from '../services/funding-source-service'; import { getLogger } from '../utils/logger'; const defaultLog = getLogger('paths/funding-sources'); @@ -38,8 +39,19 @@ GET.apiDoc = { type: 'array', items: { type: 'object', - required: [], - properties: {} + required: ['funding_source_id', 'name', 'description'], + properties: { + funding_source_id: { + type: 'integer', + minimum: 1 + }, + name: { + type: 'string' + }, + description: { + type: 'string' + } + } } } } @@ -75,11 +87,13 @@ export function getFundingSources(): RequestHandler { try { await connection.open(); - // TODO + const fundingSourceService = new FundingSourceService(connection); + + const response = await fundingSourceService.getFundingSources(); await connection.commit(); - return res.status(200).json(); + return res.status(200).json(response); } catch (error) { defaultLog.error({ label: 'getFundingSources', message: 'error', error }); await connection.rollback(); diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts index d5fdfd63f1..9ea9700507 100644 --- a/api/src/repositories/funding-source-repository.ts +++ b/api/src/repositories/funding-source-repository.ts @@ -1,3 +1,32 @@ +import SQL from 'sql-template-strings'; +import { z } from 'zod'; import { BaseRepository } from './base-repository'; -export class FundingSourceRepository extends BaseRepository {} +const FundingSource = z.object({ + funding_source_id: z.number(), + name: z.string(), + description: z.string() +}); + +export type FundingSource = z.infer; + +export class FundingSourceRepository extends BaseRepository { + /** + * Fetch all funding sources. + * + * @return {*} {Promise} + * @memberof BaseRepository + */ + async getFundingSources(): Promise { + const sqlStatement = SQL` + SELECT + * + FROM + funding_sources; + `; + + const response = await this.connection.sql(sqlStatement, FundingSource); + + return response.rows; + } +} diff --git a/api/src/services/funding-source-service.ts b/api/src/services/funding-source-service.ts index d530daeb61..46a99b5465 100644 --- a/api/src/services/funding-source-service.ts +++ b/api/src/services/funding-source-service.ts @@ -1,5 +1,5 @@ import { IDBConnection } from '../database/db'; -import { FundingSourceRepository } from '../repositories/funding-source-repository'; +import { FundingSource, FundingSourceRepository } from '../repositories/funding-source-repository'; import { DBService } from './db-service'; export class FundingSourceService extends DBService { @@ -10,4 +10,14 @@ export class FundingSourceService extends DBService { this.fundingSourceRepository = new FundingSourceRepository(connection); } + + /** + * Get all funding sources. + * + * @return {*} {Promise} + * @memberof FundingSourceService + */ + async getFundingSources(): Promise { + return this.fundingSourceRepository.getFundingSources(); + } } From 7a3cb2997aeeb1115e643c9c43ee3f7a4040e391 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 3 Aug 2023 13:09:27 -0700 Subject: [PATCH 004/125] Add funding source router and initial list page --- api/src/services/survey-service.ts | 2 +- app/src/AppRouter.tsx | 13 +++ app/src/components/layout/Header.tsx | 5 + .../funding-sources/FundingSourcesLayout.tsx | 35 ++++++ .../funding-sources/FundingSourcesRouter.tsx | 30 +++++ .../list/FundingSourcesListPage.tsx | 82 +++++--------- .../list/FundingSourcesTable.tsx | 107 +++--------------- app/src/hooks/api/useFundingSourceApi.ts | 27 +++++ app/src/hooks/useBioHubApi.ts | 6 +- .../useFundingSourceApi.interface.ts | 5 + 10 files changed, 162 insertions(+), 150 deletions(-) create mode 100644 app/src/features/funding-sources/FundingSourcesLayout.tsx create mode 100644 app/src/features/funding-sources/FundingSourcesRouter.tsx create mode 100644 app/src/hooks/api/useFundingSourceApi.ts create mode 100644 app/src/interfaces/useFundingSourceApi.interface.ts diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index 17b9620fb0..361320b1a9 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -215,7 +215,7 @@ export class SurveyService extends DBService { * Get Occurrence Submission for a given survey id. * * @param {number} surveyId - * @return {*} {(Promise<{ occurrence_submission_id: number | null }>)}F + * @return {*} {(Promise<{ occurrence_submission_id: number | null }>)} * @memberof SurveyService */ async getOccurrenceSubmission(surveyId: number): Promise<{ occurrence_submission_id: number | null }> { diff --git a/app/src/AppRouter.tsx b/app/src/AppRouter.tsx index a1b967036b..a5a8013e99 100644 --- a/app/src/AppRouter.tsx +++ b/app/src/AppRouter.tsx @@ -6,6 +6,7 @@ import { import { SYSTEM_ROLE } from 'constants/roles'; import { CodesContextProvider } from 'contexts/codesContext'; import AdminUsersRouter from 'features/admin/AdminUsersRouter'; +import FundingSourcesRouter from 'features/funding-sources/FundingSourcesRouter'; import ProjectsRouter from 'features/projects/ProjectsRouter'; import ResourcesPage from 'features/resources/ResourcesPage'; import SearchPage from 'features/search/SearchPage'; @@ -87,6 +88,18 @@ const AppRouter: React.FC = () => { + + + + + + + + + + + + diff --git a/app/src/components/layout/Header.tsx b/app/src/components/layout/Header.tsx index 9ac79b3431..10286c39f0 100644 --- a/app/src/components/layout/Header.tsx +++ b/app/src/components/layout/Header.tsx @@ -270,6 +270,11 @@ const Header: React.FC = () => { Manage Users + + + Manage Funding Sources + + diff --git a/app/src/features/funding-sources/FundingSourcesLayout.tsx b/app/src/features/funding-sources/FundingSourcesLayout.tsx new file mode 100644 index 0000000000..c889245c18 --- /dev/null +++ b/app/src/features/funding-sources/FundingSourcesLayout.tsx @@ -0,0 +1,35 @@ +import Box from '@mui/material/Box'; +import { makeStyles } from '@mui/styles'; +import React from 'react'; + +const useStyles = makeStyles(() => ({ + fundingSourcesLayoutRoot: { + width: 'inherit', + height: '100%', + display: 'flex', + flex: '1', + flexDirection: 'column' + }, + fundingSourcesContainer: { + flex: '1', + overflow: 'auto' + } +})); + +/** + * Layout for all admin/funding-sources pages. + * + * @param {*} props + * @return {*} + */ +const FundingSourcesLayout: React.FC = (props) => { + const classes = useStyles(); + + return ( + + {props.children} + + ); +}; + +export default FundingSourcesLayout; diff --git a/app/src/features/funding-sources/FundingSourcesRouter.tsx b/app/src/features/funding-sources/FundingSourcesRouter.tsx new file mode 100644 index 0000000000..0274e44956 --- /dev/null +++ b/app/src/features/funding-sources/FundingSourcesRouter.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Redirect, Route, Switch } from 'react-router'; +import RouteWithTitle from 'utils/RouteWithTitle'; +import { getTitle } from 'utils/Utils'; +import FundingSourcesLayout from './FundingSourcesLayout'; +import FundingSourcesListPage from './list/FundingSourcesListPage'; + +/** + * Router for all `/admin/funding-sources/*` pages. + * + * @return {*} + */ +const FundingSourcesRouter: React.FC = () => { + return ( + + + + + + + + {/* Catch any unknown routes, and re-direct to the not found page */} + + + + + ); +}; + +export default FundingSourcesRouter; diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index b780d7cc2f..779d8648e9 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -1,23 +1,20 @@ -import { mdiFilterOutline, mdiPlus } from '@mdi/js'; +import { mdiPlus } from '@mdi/js'; import Icon from '@mdi/react'; -import { Theme } from '@mui/material'; +import { Skeleton, Theme } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; import Container from '@mui/material/Container'; +import Divider from '@mui/material/Divider'; import Paper from '@mui/material/Paper'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; -import ProjectsSubmissionAlertBar from 'components/publish/ProjectListSubmissionAlertBar'; -import { IProjectAdvancedFilters } from 'components/search-filter/ProjectAdvancedFilters'; -import { SystemRoleGuard } from 'components/security/Guards'; -import { SYSTEM_ROLE } from 'constants/roles'; import { CodesContext } from 'contexts/codesContext'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import FundingSourcesTable from './FundingSourcesTable'; const useStyles = makeStyles((theme: Theme) => ({ pageTitleContainer: { @@ -58,29 +55,19 @@ const FundingSourcesListPage: React.FC = () => { const classes = useStyles(); const biohubApi = useBiohubApi(); - const [isFiltersOpen, setIsFiltersOpen] = useState(false); - const codesContext = useContext(CodesContext); useEffect(() => codesContext.codesDataLoader.load(), [codesContext.codesDataLoader]); - const projectsDataLoader = useDataLoader((filter?: IProjectAdvancedFilters) => - biohubApi.project.getProjectsList(filter) - ); - projectsDataLoader.load(); - - const draftsDataLoader = useDataLoader(() => biohubApi.draft.getDraftsList()); - draftsDataLoader.load(); + const fundingSourceDataLoader = useDataLoader(() => biohubApi.funding.getAllFundingSources()); + fundingSourceDataLoader.load(); - const handleSubmit = async (filterValues: IProjectAdvancedFilters) => { - projectsDataLoader.refresh(filterValues); - }; - - const handleReset = async () => { - projectsDataLoader.refresh(); - }; - - if (!codesContext.codesDataLoader.data || !projectsDataLoader.data || !draftsDataLoader.data) { - return ; + if (!codesContext.codesDataLoader.isReady || !fundingSourceDataLoader.isReady) { + return ( + <> + + + + ); } return ( @@ -91,25 +78,18 @@ const FundingSourcesListPage: React.FC = () => { - Projects + Funding Sources - - - + @@ -117,25 +97,19 @@ const FundingSourcesListPage: React.FC = () => { - - - Records Found ‌ - ({projectsDataLoader.data?.length || 0}) + ({fundingSourceDataLoader.data?.length || 0}) - + + + + diff --git a/app/src/features/funding-sources/list/FundingSourcesTable.tsx b/app/src/features/funding-sources/list/FundingSourcesTable.tsx index 38cb8a261b..21725b205d 100644 --- a/app/src/features/funding-sources/list/FundingSourcesTable.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesTable.tsx @@ -1,21 +1,12 @@ import { Theme } from '@mui/material'; -import Chip from '@mui/material/Chip'; import { grey } from '@mui/material/colors'; import Link from '@mui/material/Link'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; import { DataGrid, GridColDef, GridOverlay } from '@mui/x-data-grid'; -import { SubmitStatusChip } from 'components/chips/SubmitStatusChip'; -import { SystemRoleGuard } from 'components/security/Guards'; -import { PublishStatus } from 'constants/attachments'; -import { DATE_FORMAT } from 'constants/dateTimeFormats'; -import { SYSTEM_ROLE } from 'constants/roles'; -import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; -import { IGetDraftsListResponse } from 'interfaces/useDraftApi.interface'; -import { IGetProjectsListResponse } from 'interfaces/useProjectApi.interface'; +import { IGetFundingSourcesResponse } from 'interfaces/useFundingSourceApi.interface'; import { useCallback } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import { getFormattedDate } from 'utils/Utils'; const useStyles = makeStyles((theme: Theme) => ({ projectsTable: { @@ -49,20 +40,14 @@ const useStyles = makeStyles((theme: Theme) => ({ } })); -export interface IProjectsListTableProps { - projects: IGetProjectsListResponse[]; - drafts: IGetDraftsListResponse[]; - codes: IGetAllCodeSetsResponse; +export interface IFundingSourcesTableTableProps { + fundingSources: IGetFundingSourcesResponse[]; } -interface IProjectsListTableEntry { - id: number; - isDraft: boolean; +interface IFundingSourcesTableEntry { + funding_source_id: number; name: string; - status?: PublishStatus; - type?: string; - startDate?: string; - endDate?: string; + description: string; } const NoRowsOverlay = (props: { className: string }) => ( @@ -73,10 +58,10 @@ const NoRowsOverlay = (props: { className: string }) => ( ); -const FundingSourcesTable = (props: IProjectsListTableProps) => { +const FundingSourcesTable = (props: IFundingSourcesTableTableProps) => { const classes = useStyles(); - const columns: GridColDef[] = [ + const columns: GridColDef[] = [ { field: 'name', headerName: 'Name', @@ -89,92 +74,26 @@ const FundingSourcesTable = (props: IProjectsListTableProps) => { underline="always" title={params.row.name} component={RouterLink} - to={ - params.row.isDraft ? `/admin/projects/create?draftId=${params.row.id}` : `/admin/projects/${params.row.id}` - } + to={`/admin/funding-sources/edit?fundingSourceId=${params.row.funding_source_id}`} children={params.row.name} /> ) }, { - field: 'program', - headerName: 'Programs', + field: 'description', + headerName: 'Description', flex: 1 - }, - { - field: 'regions', - headerName: 'Regions', - flex: 1 - }, - { - field: 'startDate', - headerName: 'Start Date', - minWidth: 150, - valueGetter: ({ value }) => (value ? new Date(value) : undefined), - valueFormatter: ({ value }) => (value ? getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, value) : undefined) - }, - { - field: 'endDate', - headerName: 'End Date', - minWidth: 150, - valueGetter: ({ value }) => (value ? new Date(value) : undefined), - valueFormatter: ({ value }) => (value ? getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, value) : undefined) - }, - { - field: 'status', - headerName: 'Status', - minWidth: 150, - renderCell: (params) => { - if (params.row.isDraft) { - return ; - } - - if (!params.row.status) { - return <>; - } - - //TODO: PRODUCTION_BANDAGE: Remove - return ( - - - - ); - } } ]; const NoRowsOverlayStyled = useCallback(() => , [classes.noDataText]); - const getProjectPrograms = (project: IGetProjectsListResponse) => { - return ( - props.codes.program - .filter((code) => project.projectData.project_programs.includes(code.id)) - .map((code) => code.name) - .join(', ') || '' - ); - }; return ( ({ - id: draft.webform_draft_id, - name: draft.name, - isDraft: true - })), - ...props.projects.map((project: IGetProjectsListResponse) => ({ - id: project.projectData.id, - name: project.projectData.name, - status: project.projectSupplementaryData.publishStatus, - program: getProjectPrograms(project), - startDate: project.projectData.start_date, - endDate: project.projectData.end_date, - isDraft: false, - regions: project.projectData.regions?.join(', ') - })) - ]} - getRowId={(row) => (row.isDraft ? `draft-${row.id}` : `project-${row.id}`)} + rows={props.fundingSources} + getRowId={(row) => `funding-source-${row.funding_source_id}`} columns={columns} pageSizeOptions={[5]} rowSelection={false} diff --git a/app/src/hooks/api/useFundingSourceApi.ts b/app/src/hooks/api/useFundingSourceApi.ts new file mode 100644 index 0000000000..72b30c5894 --- /dev/null +++ b/app/src/hooks/api/useFundingSourceApi.ts @@ -0,0 +1,27 @@ +import { AxiosInstance } from 'axios'; +import { IGetFundingSourcesResponse } from 'interfaces/useFundingSourceApi.interface'; + +/** + * Returns a set of supported api methods for working with funding sources. + * + * @param {AxiosInstance} axios + * @return {*} object whose properties are supported api methods. + */ +const useFundingSourceApi = (axios: AxiosInstance) => { + /** + * Get all funding sources. + * + * @return {*} {Promise} + */ + const getAllFundingSources = async (): Promise => { + const { data } = await axios.get('/api/funding-sources'); + + return data; + }; + + return { + getAllFundingSources + }; +}; + +export default useFundingSourceApi; diff --git a/app/src/hooks/useBioHubApi.ts b/app/src/hooks/useBioHubApi.ts index 0c5a91ae4a..2eeaeafc66 100644 --- a/app/src/hooks/useBioHubApi.ts +++ b/app/src/hooks/useBioHubApi.ts @@ -6,6 +6,7 @@ import useAxios from './api/useAxios'; import useCodesApi from './api/useCodesApi'; import useDraftApi from './api/useDraftApi'; import useExternalApi from './api/useExternalApi'; +import useFundingSourceApi from './api/useFundingSourceApi'; import useObservationApi from './api/useObservationApi'; import useProjectApi from './api/useProjectApi'; import usePublishApi from './api/usePublishApi'; @@ -51,6 +52,8 @@ export const useBiohubApi = () => { const spatial = useSpatialApi(apiAxios); + const funding = useFundingSourceApi(apiAxios); + return { project, search, @@ -64,6 +67,7 @@ export const useBiohubApi = () => { admin, external, publish, - spatial + spatial, + funding }; }; diff --git a/app/src/interfaces/useFundingSourceApi.interface.ts b/app/src/interfaces/useFundingSourceApi.interface.ts new file mode 100644 index 0000000000..48eeb6a65f --- /dev/null +++ b/app/src/interfaces/useFundingSourceApi.interface.ts @@ -0,0 +1,5 @@ +export interface IGetFundingSourcesResponse { + funding_source_id: number; + name: string; + description: string; +} From 1fc3eb0d5ba457e09e4365cfadbf60467fb4c5af Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 3 Aug 2023 15:37:42 -0700 Subject: [PATCH 005/125] wip --- .../repositories/funding-source-repository.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts index 9ea9700507..e4810862a1 100644 --- a/api/src/repositories/funding-source-repository.ts +++ b/api/src/repositories/funding-source-repository.ts @@ -29,4 +29,22 @@ export class FundingSourceRepository extends BaseRepository { return response.rows; } + + async hasFundingSourceNameBeenUsed(name: string): Promise { + const sqlStatement = SQL` + SELECT + * + FROM + funding_sources + WHERE + LOWER(name) = '${name.toLowerCase()}'; + `; + + const response = await this.connection.sql(sqlStatement, FundingSource); + return response.rowCount > 0; + } + + async insertFundingSource(): Promise<{ funding_source_id: number }> { + return { funding_source_id: 1 }; + } } From bb674b4ac15050086d28727b846abd8375739a65 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 3 Aug 2023 17:15:17 -0700 Subject: [PATCH 006/125] wip --- .../components/FundingSourceForm.tsx | 31 +++++++++++++++++++ .../projects/list/ProjectsListPage.tsx | 21 +++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 app/src/features/funding-sources/components/FundingSourceForm.tsx diff --git a/app/src/features/funding-sources/components/FundingSourceForm.tsx b/app/src/features/funding-sources/components/FundingSourceForm.tsx new file mode 100644 index 0000000000..dedfa597ef --- /dev/null +++ b/app/src/features/funding-sources/components/FundingSourceForm.tsx @@ -0,0 +1,31 @@ +import { TextField } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import { Box } from '@mui/system'; +import { useFormikContext } from 'formik'; +import React from 'react'; +import yup from 'utils/YupSchema'; +export const FundingSourceYupSchema = yup.object().shape({ + funding_source_id: yup.number(), + name: yup.string().required('A funding source name is required'), + details: yup.string().max(200).required('A description is required'), + start_date: yup.string().isValidDateString(), + end_date: yup.string().isValidDateString().isEndDateSameOrAfterStartDate('start_date') +}); + +const FundingSourceForm: React.FC = (props) => { + const formikProps = useFormikContext(); + console.log(formikProps); + return ( +
+ + + Name and description + + + + +
+ ); +}; + +export default FundingSourceForm; diff --git a/app/src/features/projects/list/ProjectsListPage.tsx b/app/src/features/projects/list/ProjectsListPage.tsx index ecc1234ec0..a9f3cb86ee 100644 --- a/app/src/features/projects/list/ProjectsListPage.tsx +++ b/app/src/features/projects/list/ProjectsListPage.tsx @@ -10,11 +10,13 @@ import Paper from '@mui/material/Paper'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; +import EditDialog from 'components/dialog/EditDialog'; import ProjectsSubmissionAlertBar from 'components/publish/ProjectListSubmissionAlertBar'; import { IProjectAdvancedFilters } from 'components/search-filter/ProjectAdvancedFilters'; import { SystemRoleGuard } from 'components/security/Guards'; import { SYSTEM_ROLE } from 'constants/roles'; import { CodesContext } from 'contexts/codesContext'; +import FundingSourceForm, { FundingSourceYupSchema } from 'features/funding-sources/components/FundingSourceForm'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import React, { useContext, useEffect, useState } from 'react'; @@ -64,6 +66,7 @@ const ProjectsListPage: React.FC = () => { const biohubApi = useBiohubApi(); const [isFiltersOpen, setIsFiltersOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const codesContext = useContext(CodesContext); useEffect(() => codesContext.codesDataLoader.load(), [codesContext.codesDataLoader]); @@ -120,12 +123,30 @@ const ProjectsListPage: React.FC = () => { to={'/admin/projects/create'}> Create Project +
+ , initialValues: {}, validationSchema: FundingSourceYupSchema }} + onCancel={() => setIsModalOpen(false)} + onSave={(formValues) => { + console.log('WE DOIN IT', formValues); + }} + /> From a97e91693189dfaaf60d5483e9d2c6ea8a98f2c6 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 4 Aug 2023 09:37:28 -0700 Subject: [PATCH 007/125] stubbing out more ui --- .../components/FundingSourceForm.tsx | 18 +++++++++++++----- .../projects/list/ProjectsListPage.tsx | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/features/funding-sources/components/FundingSourceForm.tsx b/app/src/features/funding-sources/components/FundingSourceForm.tsx index dedfa597ef..23241b0e28 100644 --- a/app/src/features/funding-sources/components/FundingSourceForm.tsx +++ b/app/src/features/funding-sources/components/FundingSourceForm.tsx @@ -18,11 +18,19 @@ const FundingSourceForm: React.FC = (props) => { return (
- - Name and description - - - + + + Name and description + + + + + + + Effective Dates + + +
); diff --git a/app/src/features/projects/list/ProjectsListPage.tsx b/app/src/features/projects/list/ProjectsListPage.tsx index a9f3cb86ee..d5db63c253 100644 --- a/app/src/features/projects/list/ProjectsListPage.tsx +++ b/app/src/features/projects/list/ProjectsListPage.tsx @@ -139,7 +139,7 @@ const ProjectsListPage: React.FC = () => {
, initialValues: {}, validationSchema: FundingSourceYupSchema }} onCancel={() => setIsModalOpen(false)} From ba5c3723fe629bbb4954a2aef8b710fd810d732f Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 4 Aug 2023 10:16:43 -0700 Subject: [PATCH 008/125] moved dialog --- .../list/FundingSourcesListPage.tsx | 20 ++++++++++++++---- .../projects/list/ProjectsListPage.tsx | 21 ------------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index 779d8648e9..df16cdba1d 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -9,11 +9,12 @@ import Paper from '@mui/material/Paper'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; +import EditDialog from 'components/dialog/EditDialog'; import { CodesContext } from 'contexts/codesContext'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; -import React, { useContext, useEffect } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; +import React, { useContext, useEffect, useState } from 'react'; +import { FundingSourceForm, FundingSourceYupSchema } from '../components/FundingSourceForm'; import FundingSourcesTable from './FundingSourcesTable'; const useStyles = makeStyles((theme: Theme) => ({ @@ -52,6 +53,7 @@ const useStyles = makeStyles((theme: Theme) => ({ * @return {*} */ const FundingSourcesListPage: React.FC = () => { + const [isModalOpen, setIsModalOpen] = useState(false); const classes = useStyles(); const biohubApi = useBiohubApi(); @@ -86,8 +88,9 @@ const FundingSourcesListPage: React.FC = () => { variant="contained" color="primary" startIcon={} - component={RouterLink} - to={'/admin/funding-sources/create'}> + onClick={() => { + setIsModalOpen(true); + }}> Add Funding Source @@ -95,6 +98,15 @@ const FundingSourcesListPage: React.FC = () => { + , initialValues: {}, validationSchema: FundingSourceYupSchema }} + onCancel={() => setIsModalOpen(false)} + onSave={(formValues) => { + console.log('WE DOIN IT', formValues); + }} + /> diff --git a/app/src/features/projects/list/ProjectsListPage.tsx b/app/src/features/projects/list/ProjectsListPage.tsx index d5db63c253..ecc1234ec0 100644 --- a/app/src/features/projects/list/ProjectsListPage.tsx +++ b/app/src/features/projects/list/ProjectsListPage.tsx @@ -10,13 +10,11 @@ import Paper from '@mui/material/Paper'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; -import EditDialog from 'components/dialog/EditDialog'; import ProjectsSubmissionAlertBar from 'components/publish/ProjectListSubmissionAlertBar'; import { IProjectAdvancedFilters } from 'components/search-filter/ProjectAdvancedFilters'; import { SystemRoleGuard } from 'components/security/Guards'; import { SYSTEM_ROLE } from 'constants/roles'; import { CodesContext } from 'contexts/codesContext'; -import FundingSourceForm, { FundingSourceYupSchema } from 'features/funding-sources/components/FundingSourceForm'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import React, { useContext, useEffect, useState } from 'react'; @@ -66,7 +64,6 @@ const ProjectsListPage: React.FC = () => { const biohubApi = useBiohubApi(); const [isFiltersOpen, setIsFiltersOpen] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); const codesContext = useContext(CodesContext); useEffect(() => codesContext.codesDataLoader.load(), [codesContext.codesDataLoader]); @@ -123,30 +120,12 @@ const ProjectsListPage: React.FC = () => { to={'/admin/projects/create'}> Create Project - - , initialValues: {}, validationSchema: FundingSourceYupSchema }} - onCancel={() => setIsModalOpen(false)} - onSave={(formValues) => { - console.log('WE DOIN IT', formValues); - }} - /> From 62c1c2e73b2fa0cd4d148232a1c24ec463eb0730 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 4 Aug 2023 13:09:38 -0700 Subject: [PATCH 009/125] updating ui --- .../components/FundingSourceForm.tsx | 14 ++++++++++++-- .../list/FundingSourcesListPage.tsx | 3 ++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/features/funding-sources/components/FundingSourceForm.tsx b/app/src/features/funding-sources/components/FundingSourceForm.tsx index 23241b0e28..e00c4aedda 100644 --- a/app/src/features/funding-sources/components/FundingSourceForm.tsx +++ b/app/src/features/funding-sources/components/FundingSourceForm.tsx @@ -1,9 +1,11 @@ import { TextField } from '@mui/material'; import Typography from '@mui/material/Typography'; import { Box } from '@mui/system'; +import StartEndDateFields from 'components/fields/StartEndDateFields'; import { useFormikContext } from 'formik'; import React from 'react'; import yup from 'utils/YupSchema'; + export const FundingSourceYupSchema = yup.object().shape({ funding_source_id: yup.number(), name: yup.string().required('A funding source name is required'), @@ -14,7 +16,7 @@ export const FundingSourceYupSchema = yup.object().shape({ const FundingSourceForm: React.FC = (props) => { const formikProps = useFormikContext(); - console.log(formikProps); + return (
@@ -29,7 +31,15 @@ const FundingSourceForm: React.FC = (props) => { Effective Dates - + + +
diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index df16cdba1d..9f6ff0e032 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -14,7 +14,7 @@ import { CodesContext } from 'contexts/codesContext'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import React, { useContext, useEffect, useState } from 'react'; -import { FundingSourceForm, FundingSourceYupSchema } from '../components/FundingSourceForm'; +import FundingSourceForm, { FundingSourceYupSchema } from '../components/FundingSourceForm'; import FundingSourcesTable from './FundingSourcesTable'; const useStyles = makeStyles((theme: Theme) => ({ @@ -102,6 +102,7 @@ const FundingSourcesListPage: React.FC = () => { dialogTitle="Add New Funding Source" open={isModalOpen} component={{ element: , initialValues: {}, validationSchema: FundingSourceYupSchema }} + dialogSaveButtonLabel="Add" onCancel={() => setIsModalOpen(false)} onSave={(formValues) => { console.log('WE DOIN IT', formValues); From c39d1a5fe29449c3182e9e3667f04a47cdc73889 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 4 Aug 2023 14:54:05 -0700 Subject: [PATCH 010/125] basic form is submitting --- .../components/FundingSourceForm.tsx | 28 ++++++++++--------- .../list/FundingSourcesListPage.tsx | 17 +++++++++-- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/src/features/funding-sources/components/FundingSourceForm.tsx b/app/src/features/funding-sources/components/FundingSourceForm.tsx index e00c4aedda..3c159fbabc 100644 --- a/app/src/features/funding-sources/components/FundingSourceForm.tsx +++ b/app/src/features/funding-sources/components/FundingSourceForm.tsx @@ -1,36 +1,38 @@ -import { TextField } from '@mui/material'; import Typography from '@mui/material/Typography'; import { Box } from '@mui/system'; +import CustomTextField from 'components/fields/CustomTextField'; import StartEndDateFields from 'components/fields/StartEndDateFields'; import { useFormikContext } from 'formik'; import React from 'react'; import yup from 'utils/YupSchema'; export const FundingSourceYupSchema = yup.object().shape({ - funding_source_id: yup.number(), + funding_source_id: yup.number().nullable(), name: yup.string().required('A funding source name is required'), - details: yup.string().max(200).required('A description is required'), + description: yup.string().max(200).required('A description is required'), start_date: yup.string().isValidDateString(), end_date: yup.string().isValidDateString().isEndDateSameOrAfterStartDate('start_date') }); +export type FundingSourceData = yup.InferType; const FundingSourceForm: React.FC = (props) => { - const formikProps = useFormikContext(); + const formikProps = useFormikContext(); + const { handleSubmit } = formikProps; return ( -
+ - - Name and description - - - + Name and description + + - - Effective Dates - + Effective Dates ({ @@ -101,11 +101,22 @@ const FundingSourcesListPage: React.FC = () => { , initialValues: {}, validationSchema: FundingSourceYupSchema }} + component={{ + element: , + initialValues: { + funding_source_id: null, + name: '', + description: '', + start_date: '', + end_date: '' + } as FundingSourceData, + validationSchema: FundingSourceYupSchema + }} dialogSaveButtonLabel="Add" onCancel={() => setIsModalOpen(false)} onSave={(formValues) => { - console.log('WE DOIN IT', formValues); + console.log('SUBMIT THIS FORM PLS'); + console.log(formValues); }} /> From 66d32d3ff580013d369e586a797b6063a4e52a7e Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Fri, 4 Aug 2023 14:55:19 -0700 Subject: [PATCH 011/125] Update funding api tests --- api/src/paths/funding-source.test.ts | 20 ++++++ .../funding-source/{fundingSourceId}.test.ts | 48 +++++++++++++ api/src/paths/funding-sources.test.ts | 72 +++++++++++++++++++ .../funding-source-repository.test.ts | 45 ++++++++++++ .../services/funding-source-service.test.ts | 33 +++++++++ 5 files changed, 218 insertions(+) create mode 100644 api/src/paths/funding-source.test.ts create mode 100644 api/src/paths/funding-source/{fundingSourceId}.test.ts create mode 100644 api/src/paths/funding-sources.test.ts create mode 100644 api/src/repositories/funding-source-repository.test.ts create mode 100644 api/src/services/funding-source-service.test.ts diff --git a/api/src/paths/funding-source.test.ts b/api/src/paths/funding-source.test.ts new file mode 100644 index 0000000000..95f06619c4 --- /dev/null +++ b/api/src/paths/funding-source.test.ts @@ -0,0 +1,20 @@ +import chai from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; + +chai.use(sinonChai); + +describe('postFundingSource', () => { + afterEach(() => { + sinon.restore(); + }); + + it('creates a funding source', async () => { + // TODO + }); + + it('catches and re-throws error', async () => { + // TODO + }); +}); diff --git a/api/src/paths/funding-source/{fundingSourceId}.test.ts b/api/src/paths/funding-source/{fundingSourceId}.test.ts new file mode 100644 index 0000000000..33ba2ed28b --- /dev/null +++ b/api/src/paths/funding-source/{fundingSourceId}.test.ts @@ -0,0 +1,48 @@ +import chai from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; + +chai.use(sinonChai); + +describe('getFundingSource', () => { + afterEach(() => { + sinon.restore(); + }); + + it('gets a funding source', async () => { + // TODO + }); + + it('catches and re-throws error', async () => { + // TODO + }); +}); + +describe('putFundingSource', () => { + afterEach(() => { + sinon.restore(); + }); + + it('updates a funding source', async () => { + // TODO + }); + + it('catches and re-throws error', async () => { + // TODO + }); +}); + +describe('deleteFundingSource', () => { + afterEach(() => { + sinon.restore(); + }); + + it('deletes a funding source', async () => { + // TODO + }); + + it('catches and re-throws error', async () => { + // TODO + }); +}); diff --git a/api/src/paths/funding-sources.test.ts b/api/src/paths/funding-sources.test.ts new file mode 100644 index 0000000000..7a2ecf2e42 --- /dev/null +++ b/api/src/paths/funding-sources.test.ts @@ -0,0 +1,72 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../database/db'; +import { HTTPError } from '../errors/http-error'; +import { FundingSource } from '../repositories/funding-source-repository'; +import { FundingSourceService } from '../services/funding-source-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../__mocks__/db'; +import { getFundingSources } from './funding-sources'; + +chai.use(sinonChai); + +describe('getFundingSources', () => { + afterEach(() => { + sinon.restore(); + }); + + it('returns an array of funding sources', async () => { + const mockFundingSources: FundingSource[] = [ + { + funding_source_id: 1, + name: 'name', + description: 'description' + }, + { + funding_source_id: 2, + name: 'name2', + description: 'description2' + } + ]; + + const mockDBConnection = getMockDBConnection({ open: sinon.stub(), commit: sinon.stub() }); + + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + sinon.stub(FundingSourceService.prototype, 'getFundingSources').resolves(mockFundingSources); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + const requestHandler = getFundingSources(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.jsonValue).to.eql(mockFundingSources); + + expect(mockDBConnection.open).to.have.been.calledOnce; + expect(mockDBConnection.commit).to.have.been.calledOnce; + }); + + it('catches and re-throws error', async () => { + const mockDBConnection = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); + + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + sinon.stub(FundingSourceService.prototype, 'getFundingSources').rejects(new Error('a test error')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + const requestHandler = getFundingSources(); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(mockDBConnection.rollback).to.have.been.calledOnce; + expect(mockDBConnection.release).to.have.been.calledOnce; + + expect((actualError as HTTPError).message).to.equal('a test error'); + } + }); +}); diff --git a/api/src/repositories/funding-source-repository.test.ts b/api/src/repositories/funding-source-repository.test.ts new file mode 100644 index 0000000000..903df746c6 --- /dev/null +++ b/api/src/repositories/funding-source-repository.test.ts @@ -0,0 +1,45 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { getMockDBConnection } from '../__mocks__/db'; +import { FundingSource, FundingSourceRepository } from './funding-source-repository'; + +chai.use(sinonChai); + +describe('FundingSourceRepository', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getFundingSources', () => { + it('returns an empty array of funding source items', async () => { + const mockFundingSources: FundingSource[] = []; + + const mockResponse = ({ rowCount: 0, rows: mockFundingSources } as unknown) as Promise>; + + const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); + + const fundingSourceRepository = new FundingSourceRepository(dbConnection); + + const response = await fundingSourceRepository.getFundingSources(); + + expect(response).to.eql(mockFundingSources); + }); + + it('returns a non empty array of funding source items', async () => { + const mockFundingSources: FundingSource[] = [{ funding_source_id: 1, name: 'name', description: 'description' }]; + + const mockResponse = ({ rowCount: 1, rows: mockFundingSources } as unknown) as Promise>; + + const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); + + const fundingSourceRepository = new FundingSourceRepository(dbConnection); + + const response = await fundingSourceRepository.getFundingSources(); + + expect(response).to.eql(mockFundingSources); + }); + }); +}); diff --git a/api/src/services/funding-source-service.test.ts b/api/src/services/funding-source-service.test.ts new file mode 100644 index 0000000000..6184a65052 --- /dev/null +++ b/api/src/services/funding-source-service.test.ts @@ -0,0 +1,33 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { FundingSourceRepository } from '../repositories/funding-source-repository'; +import { getMockDBConnection } from '../__mocks__/db'; +import { FundingSourceService } from './funding-source-service'; + +chai.use(sinonChai); + +describe('FundingSourceService', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getFundingSources', () => { + it('returns an array of funding source items', async () => { + const dbConnection = getMockDBConnection(); + const fundingSourceService = new FundingSourceService(dbConnection); + + const mockFundingSources = [{ funding_source_id: 1, name: 'name', description: 'description' }]; + + const getFundingSourcesStub = sinon + .stub(FundingSourceRepository.prototype, 'getFundingSources') + .resolves(mockFundingSources); + + const response = await fundingSourceService.getFundingSources(); + + expect(getFundingSourcesStub).to.be.calledOnce; + expect(response).to.eql(mockFundingSources); + }); + }); +}); From 20fda97e98e494c0e4217abba8460527977c52c4 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Fri, 4 Aug 2023 14:57:00 -0700 Subject: [PATCH 012/125] added comment --- .../funding-sources/components/FundingSourceForm.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/features/funding-sources/components/FundingSourceForm.tsx b/app/src/features/funding-sources/components/FundingSourceForm.tsx index 3c159fbabc..787443b11b 100644 --- a/app/src/features/funding-sources/components/FundingSourceForm.tsx +++ b/app/src/features/funding-sources/components/FundingSourceForm.tsx @@ -14,7 +14,13 @@ export const FundingSourceYupSchema = yup.object().shape({ end_date: yup.string().isValidDateString().isEndDateSameOrAfterStartDate('start_date') }); export type FundingSourceData = yup.InferType; - +/* + TODO: + - replace existing StartEndDateFields + - make UI better + - look into fieldset child relationship + - look into fixing project edit/ form validation +*/ const FundingSourceForm: React.FC = (props) => { const formikProps = useFormikContext(); const { handleSubmit } = formikProps; From 6a9b84f8b46595cecf09f0d8189478889a73a379 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Fri, 4 Aug 2023 16:09:17 -0700 Subject: [PATCH 013/125] Add funding source api code --- .../paths/funding-source/{fundingSourceId}.ts | 31 +++- api/src/paths/funding-sources.ts | 6 +- .../funding-source-repository.test.ts | 143 +++++++++++++++++- .../repositories/funding-source-repository.ts | 69 ++++++++- .../services/funding-source-service.test.ts | 53 ++++++- api/src/services/funding-source-service.ts | 22 +++ 6 files changed, 304 insertions(+), 20 deletions(-) diff --git a/api/src/paths/funding-source/{fundingSourceId}.ts b/api/src/paths/funding-source/{fundingSourceId}.ts index 58a2f855c6..f375b6bcac 100644 --- a/api/src/paths/funding-source/{fundingSourceId}.ts +++ b/api/src/paths/funding-source/{fundingSourceId}.ts @@ -3,6 +3,7 @@ import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../../constants/roles'; import { getDBConnection } from '../../database/db'; import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { FundingSourceService } from '../../services/funding-source-service'; import { getLogger } from '../../utils/logger'; const defaultLog = getLogger('paths/funding-source/{fundingSourceId}'); @@ -46,11 +47,23 @@ GET.apiDoc = { content: { 'application/json': { schema: { - type: 'array', - items: { - type: 'object', - required: [], - properties: {} + type: 'object', + required: ['funding_source_id', 'name', 'description', 'revision_count'], + properties: { + funding_source_id: { + type: 'integer', + minimum: 1 + }, + name: { + type: 'string' + }, + description: { + type: 'string' + }, + revision_count: { + type: 'integer', + minimum: 0 + } } } } @@ -83,14 +96,18 @@ export function getFundingSource(): RequestHandler { return async (req, res) => { const connection = getDBConnection(req['keycloak_token']); + const fundingSourceId = Number(req.params.fundingSourceId); + try { await connection.open(); - // TODO + const fundingSourceService = new FundingSourceService(connection); + + const response = await fundingSourceService.getFundingSourceById(fundingSourceId); await connection.commit(); - return res.status(200).json(); + return res.status(200).json(response); } catch (error) { defaultLog.error({ label: 'getFundingSource', message: 'error', error }); await connection.rollback(); diff --git a/api/src/paths/funding-sources.ts b/api/src/paths/funding-sources.ts index b83f0dc2e6..2bd35f1e1b 100644 --- a/api/src/paths/funding-sources.ts +++ b/api/src/paths/funding-sources.ts @@ -39,7 +39,7 @@ GET.apiDoc = { type: 'array', items: { type: 'object', - required: ['funding_source_id', 'name', 'description'], + required: ['funding_source_id', 'name', 'description', 'revision_count'], properties: { funding_source_id: { type: 'integer', @@ -50,6 +50,10 @@ GET.apiDoc = { }, description: { type: 'string' + }, + revision_count: { + type: 'integer', + minimum: 0 } } } diff --git a/api/src/repositories/funding-source-repository.test.ts b/api/src/repositories/funding-source-repository.test.ts index 903df746c6..753e4b6563 100644 --- a/api/src/repositories/funding-source-repository.test.ts +++ b/api/src/repositories/funding-source-repository.test.ts @@ -3,6 +3,7 @@ import { describe } from 'mocha'; import { QueryResult } from 'pg'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; +import { ApiError } from '../errors/api-error'; import { getMockDBConnection } from '../__mocks__/db'; import { FundingSource, FundingSourceRepository } from './funding-source-repository'; @@ -15,9 +16,9 @@ describe('FundingSourceRepository', () => { describe('getFundingSources', () => { it('returns an empty array of funding source items', async () => { - const mockFundingSources: FundingSource[] = []; + const expectedResult: FundingSource[] = []; - const mockResponse = ({ rowCount: 0, rows: mockFundingSources } as unknown) as Promise>; + const mockResponse = ({ rowCount: 1, rows: expectedResult } as unknown) as Promise>; const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); @@ -25,13 +26,13 @@ describe('FundingSourceRepository', () => { const response = await fundingSourceRepository.getFundingSources(); - expect(response).to.eql(mockFundingSources); + expect(response).to.eql(expectedResult); }); it('returns a non empty array of funding source items', async () => { - const mockFundingSources: FundingSource[] = [{ funding_source_id: 1, name: 'name', description: 'description' }]; + const expectedResult: FundingSource[] = [{ funding_source_id: 1, name: 'name', description: 'description' }]; - const mockResponse = ({ rowCount: 1, rows: mockFundingSources } as unknown) as Promise>; + const mockResponse = ({ rowCount: 1, rows: expectedResult } as unknown) as Promise>; const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); @@ -39,7 +40,137 @@ describe('FundingSourceRepository', () => { const response = await fundingSourceRepository.getFundingSources(); - expect(response).to.eql(mockFundingSources); + expect(response).to.eql(expectedResult); + }); + }); + + describe('getFundingSourceById', () => { + it('returns a single funding source', async () => { + const expectedResult: FundingSource = { + funding_source_id: 1, + name: 'name', + description: 'description' + }; + + const mockResponse = ({ rowCount: 1, rows: [expectedResult] } as unknown) as Promise>; + + const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); + + const fundingSourceRepository = new FundingSourceRepository(dbConnection); + + const fundingSourceId = 1; + + const response = await fundingSourceRepository.getFundingSourceById(fundingSourceId); + + expect(response).to.eql(expectedResult); + }); + + it('throws an error if rowCount is 0', async () => { + const mockResponse = ({ rowCount: 0, rows: [] } as unknown) as Promise>; + + const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); + + const fundingSourceRepository = new FundingSourceRepository(dbConnection); + + const fundingSourceId = 1; + + try { + await fundingSourceRepository.getFundingSourceById(fundingSourceId); + + expect.fail(); + } catch (error) { + expect((error as ApiError).message).to.equal('Failed to get funding source'); + } + }); + + it('throws an error if rowCount is greater than 1', async () => { + const mockResponse = ({ rowCount: 2, rows: [] } as unknown) as Promise>; + + const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); + + const fundingSourceRepository = new FundingSourceRepository(dbConnection); + + const fundingSourceId = 1; + + try { + await fundingSourceRepository.getFundingSourceById(fundingSourceId); + + expect.fail(); + } catch (error) { + expect((error as ApiError).message).to.equal('Failed to get funding source'); + } + }); + }); + + describe('putFundingSource', () => { + it('returns a single funding source', async () => { + const fundingSourceId = 1; + const expectedResult = { funding_source_id: fundingSourceId }; + + const mockResponse = ({ rowCount: 1, rows: [expectedResult] } as unknown) as Promise>; + + const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); + + const fundingSourceRepository = new FundingSourceRepository(dbConnection); + + const fundingSource: FundingSource = { + funding_source_id: fundingSourceId, + name: 'name', + description: 'description', + revision_count: 0 + }; + + const response = await fundingSourceRepository.putFundingSource(fundingSource); + + expect(response).to.eql(expectedResult); + }); + + it('throws an error if rowCount is 0', async () => { + const mockResponse = ({ rowCount: 0, rows: [] } as unknown) as Promise>; + + const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); + + const fundingSourceRepository = new FundingSourceRepository(dbConnection); + + const fundingSourceId = 1; + const fundingSource: FundingSource = { + funding_source_id: fundingSourceId, + name: 'name', + description: 'description', + revision_count: 0 + }; + + try { + await fundingSourceRepository.putFundingSource(fundingSource); + + expect.fail(); + } catch (error) { + expect((error as ApiError).message).to.equal('Failed to update funding source'); + } + }); + + it('throws an error if rowCount is greater than 1', async () => { + const mockResponse = ({ rowCount: 2, rows: [] } as unknown) as Promise>; + + const dbConnection = getMockDBConnection({ sql: async () => mockResponse }); + + const fundingSourceRepository = new FundingSourceRepository(dbConnection); + + const fundingSourceId = 1; + const fundingSource: FundingSource = { + funding_source_id: fundingSourceId, + name: 'name', + description: 'description', + revision_count: 0 + }; + + try { + await fundingSourceRepository.putFundingSource(fundingSource); + + expect.fail(); + } catch (error) { + expect((error as ApiError).message).to.equal('Failed to update funding source'); + } }); }); }); diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts index e4810862a1..2efd7cfc62 100644 --- a/api/src/repositories/funding-source-repository.ts +++ b/api/src/repositories/funding-source-repository.ts @@ -1,11 +1,13 @@ import SQL from 'sql-template-strings'; import { z } from 'zod'; +import { ApiExecuteSQLError } from '../errors/api-error'; import { BaseRepository } from './base-repository'; const FundingSource = z.object({ funding_source_id: z.number(), name: z.string(), - description: z.string() + description: z.string(), + revision_count: z.number().optional() }); export type FundingSource = z.infer; @@ -22,7 +24,7 @@ export class FundingSourceRepository extends BaseRepository { SELECT * FROM - funding_sources; + funding_source; `; const response = await this.connection.sql(sqlStatement, FundingSource); @@ -47,4 +49,67 @@ export class FundingSourceRepository extends BaseRepository { async insertFundingSource(): Promise<{ funding_source_id: number }> { return { funding_source_id: 1 }; } + + /** + * Fetch a single funding source by id. + * + * @param {number} fundingSourceId + * @return {*} {Promise} + * @memberof FundingSourceRepository + */ + async getFundingSourceById(fundingSourceId: number): Promise { + const sqlStatement = SQL` + SELECT + * + FROM + funding_source + WHERE + funding_source_id = ${fundingSourceId}; + `; + + const response = await this.connection.sql(sqlStatement, FundingSource); + + if (response.rowCount !== 1) { + throw new ApiExecuteSQLError('Failed to get funding source', [ + 'FundingSourceRepository->getFundingSourceById', + 'rowCount was != 1, expected rowCount = 1' + ]); + } + + return response.rows[0]; + } + + /** + * Fetch a single funding source by id. + * + * @param {FundingSource} fundingSource + * @return {*} {Promise>} + * @memberof FundingSourceRepository + */ + async putFundingSource(fundingSource: FundingSource): Promise> { + const sqlStatement = SQL` + UPDATE + funding_source + SET + name = ${fundingSource.name}, + description = ${fundingSource.description} + WHERE + funding_source_id = ${fundingSource} + AND + revision_count = ${fundingSource.revision_count || 0} + RETURNING + funding_source_id; + `; + + const response = await this.connection.sql(sqlStatement, FundingSource.pick({ funding_source_id: true })); + + if (response.rowCount !== 1) { + throw new ApiExecuteSQLError('Failed to update funding source', [ + 'FundingSourceRepository->putFundingSource', + 'rowCount was != 1, expected rowCount = 1' + ]); + } + + return response.rows[0]; + } } diff --git a/api/src/services/funding-source-service.test.ts b/api/src/services/funding-source-service.test.ts index 6184a65052..4149ac8aa0 100644 --- a/api/src/services/funding-source-service.test.ts +++ b/api/src/services/funding-source-service.test.ts @@ -2,7 +2,7 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import { FundingSourceRepository } from '../repositories/funding-source-repository'; +import { FundingSource, FundingSourceRepository } from '../repositories/funding-source-repository'; import { getMockDBConnection } from '../__mocks__/db'; import { FundingSourceService } from './funding-source-service'; @@ -18,16 +18,61 @@ describe('FundingSourceService', () => { const dbConnection = getMockDBConnection(); const fundingSourceService = new FundingSourceService(dbConnection); - const mockFundingSources = [{ funding_source_id: 1, name: 'name', description: 'description' }]; + const expectedResult = [{ funding_source_id: 1, name: 'name', description: 'description' }]; const getFundingSourcesStub = sinon .stub(FundingSourceRepository.prototype, 'getFundingSources') - .resolves(mockFundingSources); + .resolves(expectedResult); const response = await fundingSourceService.getFundingSources(); expect(getFundingSourcesStub).to.be.calledOnce; - expect(response).to.eql(mockFundingSources); + expect(response).to.eql(expectedResult); + }); + }); + + describe('getFundingSourceById', () => { + it('returns an array of funding source items', async () => { + const dbConnection = getMockDBConnection(); + const fundingSourceService = new FundingSourceService(dbConnection); + + const expectedResult = { funding_source_id: 1, name: 'name', description: 'description' }; + + const getFundingSourceByIdStub = sinon + .stub(FundingSourceRepository.prototype, 'getFundingSourceById') + .resolves(expectedResult); + + const fundingSourceId = 1; + + const response = await fundingSourceService.getFundingSourceById(fundingSourceId); + + expect(getFundingSourceByIdStub).to.be.calledOnce; + expect(response).to.eql(expectedResult); + }); + }); + + describe('putFundingSource', () => { + it('returns an array of funding source items', async () => { + const dbConnection = getMockDBConnection(); + const fundingSourceService = new FundingSourceService(dbConnection); + + const expectedResult = { funding_source_id: 1 }; + + const putFundingSourceStub = sinon + .stub(FundingSourceRepository.prototype, 'putFundingSource') + .resolves(expectedResult); + + const fundingSource: FundingSource = { + funding_source_id: 1, + name: 'name', + description: 'description', + revision_count: 0 + }; + + const response = await fundingSourceService.putFundingSource(fundingSource); + + expect(putFundingSourceStub).to.be.calledOnce; + expect(response).to.eql(expectedResult); }); }); }); diff --git a/api/src/services/funding-source-service.ts b/api/src/services/funding-source-service.ts index 46a99b5465..3b8f698950 100644 --- a/api/src/services/funding-source-service.ts +++ b/api/src/services/funding-source-service.ts @@ -20,4 +20,26 @@ export class FundingSourceService extends DBService { async getFundingSources(): Promise { return this.fundingSourceRepository.getFundingSources(); } + + /** + * Fetch a single funding source by id. + * + * @param {number} fundingSourceId + * @return {*} {Promise} + * @memberof FundingSourceService + */ + async getFundingSourceById(fundingSourceId: number): Promise { + return this.fundingSourceRepository.getFundingSourceById(fundingSourceId); + } + + /** + * Update a single funding source. + * + * @param {FundingSource} fundingSource + * @return {*} {Promise>} + * @memberof FundingSourceService + */ + async putFundingSource(fundingSource: FundingSource): Promise> { + return this.fundingSourceRepository.putFundingSource(fundingSource); + } } From 8d8e7acd1d9e7cbacc6f59441ea14721c068a9ea Mon Sep 17 00:00:00 2001 From: Kjartan Date: Wed, 2 Aug 2023 15:08:58 -0700 Subject: [PATCH 014/125] intial tables setup drop old tables, insert data nuke funding --- api/src/models/project-create.test.ts | 165 -- api/src/models/project-create.ts | 50 - api/src/models/project-update.test.ts | 83 - api/src/models/project-update.ts | 41 - api/src/models/project-view.test.ts | 56 - api/src/models/project-view.ts | 41 - api/src/models/survey-create.test.ts | 40 - api/src/models/survey-create.ts | 11 - api/src/models/survey-update.test.ts | 40 - api/src/models/survey-update.ts | 10 - api/src/models/survey-view.test.ts | 83 - api/src/models/survey-view.ts | 38 - .../schemas/project-funding-source.test.ts | 12 - .../openapi/schemas/project-funding-source.ts | 30 - api/src/openapi/schemas/project.ts | 95 +- .../project/{projectId}/survey/create.ts | 12 - .../survey/funding-sources/list.test.ts | 63 - .../survey/funding-sources/list.ts | 155 -- .../paths/project/{projectId}/survey/list.ts | 44 - .../{projectId}/survey/{surveyId}/update.ts | 13 - .../survey/{surveyId}/update/get.test.ts | 12 +- .../survey/{surveyId}/update/get.ts | 31 +- .../survey/{surveyId}/view.test.ts | 22 - .../{projectId}/survey/{surveyId}/view.ts | 61 +- .../paths/project/{projectId}/update.test.ts | 2 - api/src/paths/project/{projectId}/update.ts | 68 - api/src/paths/project/{projectId}/view.ts | 68 +- .../repositories/project-repository.test.ts | 338 +-- api/src/repositories/project-repository.ts | 294 +-- .../repositories/survey-repository.test.ts | 54 - api/src/repositories/survey-repository.ts | 85 - api/src/services/eml-service.test.ts | 181 -- api/src/services/eml-service.ts | 64 - api/src/services/project-service.test.ts | 17 - api/src/services/project-service.ts | 105 +- api/src/services/survey-service.test.ts | 84 - api/src/services/survey-service.ts | 70 - api/src/utils/shared-api-docs.test.ts | 11 +- api/src/utils/shared-api-docs.ts | 58 - .../funding-source/FundingSource.test.tsx | 64 - .../funding-source/FundingSource.tsx | 98 - .../__snapshots__/FundingSource.test.tsx.snap | 133 -- .../search-filter/ProjectAdvancedFilters.tsx | 26 +- .../components/FundingSourceAutocomplete.tsx | 65 - .../components/ProjectFundingForm.test.tsx | 290 --- .../components/ProjectFundingForm.tsx | 272 --- .../ProjectFundingItemForm.test.tsx | 196 -- .../components/ProjectFundingItemForm.tsx | 285 --- .../ProjectFundingItemForm.test.tsx.snap | 2081 ----------------- .../projects/create/CreateProjectForm.tsx | 27 +- .../create/CreateProjectPage.test.tsx | 10 - .../projects/edit/EditProjectForm.tsx | 22 +- .../projects/edit/EditProjectPage.tsx | 1 - .../projects/list/ProjectsListFilterForm.tsx | 5 - .../features/projects/view/ProjectDetails.tsx | 21 - .../surveys/CreateSurveyPage.test.tsx | 39 +- app/src/features/surveys/CreateSurveyPage.tsx | 25 +- .../GeneralInformationForm.test.tsx | 21 - .../components/GeneralInformationForm.tsx | 19 - .../features/surveys/edit/EditSurveyForm.tsx | 19 +- .../features/surveys/edit/EditSurveyPage.tsx | 7 - .../components/SurveyGeneralInformation.tsx | 17 +- app/src/hooks/api/useProjectApi.test.ts | 2 - app/src/hooks/api/useSurveyApi.ts | 14 - app/src/interfaces/useProjectApi.interface.ts | 43 - app/src/interfaces/useSurveyApi.interface.ts | 24 - app/src/test-helpers/project-helpers.ts | 16 - app/src/test-helpers/survey-helpers.ts | 12 - .../20230802000000_new_funding_table.ts | 172 ++ .../seeds/03_basic_project_survey_setup.ts | 41 +- 70 files changed, 223 insertions(+), 6551 deletions(-) delete mode 100644 api/src/openapi/schemas/project-funding-source.test.ts delete mode 100644 api/src/openapi/schemas/project-funding-source.ts delete mode 100644 api/src/paths/project/{projectId}/survey/funding-sources/list.test.ts delete mode 100644 api/src/paths/project/{projectId}/survey/funding-sources/list.ts delete mode 100644 app/src/components/funding-source/FundingSource.test.tsx delete mode 100644 app/src/components/funding-source/FundingSource.tsx delete mode 100644 app/src/components/funding-source/__snapshots__/FundingSource.test.tsx.snap delete mode 100644 app/src/features/projects/components/FundingSourceAutocomplete.tsx delete mode 100644 app/src/features/projects/components/ProjectFundingForm.test.tsx delete mode 100644 app/src/features/projects/components/ProjectFundingForm.tsx delete mode 100644 app/src/features/projects/components/ProjectFundingItemForm.test.tsx delete mode 100644 app/src/features/projects/components/ProjectFundingItemForm.tsx delete mode 100644 app/src/features/projects/components/__snapshots__/ProjectFundingItemForm.test.tsx.snap create mode 100644 database/src/migrations/20230802000000_new_funding_table.ts diff --git a/api/src/models/project-create.test.ts b/api/src/models/project-create.test.ts index 4e97c37567..3c714c84e5 100644 --- a/api/src/models/project-create.test.ts +++ b/api/src/models/project-create.test.ts @@ -2,8 +2,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { PostCoordinatorData, - PostFundingData, - PostFundingSource, PostIUCNData, PostLocationData, PostObjectivesData, @@ -40,10 +38,6 @@ describe('PostProjectObject', () => { expect(projectPostObject.iucn).to.equal(null); }); - it('sets funding', function () { - expect(projectPostObject.funding).to.equal(null); - }); - it('sets partnerships', function () { expect(projectPostObject.partnerships).to.equal(null); }); @@ -91,18 +85,6 @@ describe('PostProjectObject', () => { } ] }, - funding: { - fundingSources: [ - { - agency_id: 1, - investment_action_category: 1, - agency_project_id: 'agency project id', - funding_amount: 12, - start_date: '2020/04/03', - end_date: '2020/05/05' - } - ] - }, iucn: { classificationDetails: [ { @@ -338,81 +320,6 @@ describe('PostPartnershipsData', () => { }); }); -describe('PostFundingSource', () => { - describe('No values provided', () => { - let projectFundingData: PostFundingSource; - - before(() => { - projectFundingData = new PostFundingSource(null); - }); - - it('sets agency_id', () => { - expect(projectFundingData.agency_id).to.equal(null); - }); - - it('sets investment_action_category', () => { - expect(projectFundingData.investment_action_category).to.equal(null); - }); - - it('sets agency_project_id', () => { - expect(projectFundingData.agency_project_id).to.equal(null); - }); - - it('sets funding_amount', () => { - expect(projectFundingData.funding_amount).to.equal(null); - }); - - it('sets start_date', () => { - expect(projectFundingData.start_date).to.equal(null); - }); - - it('sets end_date', () => { - expect(projectFundingData.end_date).to.equal(null); - }); - }); - - describe('All values provided', () => { - let projectFundingData: PostFundingSource; - - const obj = { - agency_id: 1, - investment_action_category: 1, - agency_project_id: 'agency project id', - funding_amount: 20, - start_date: '2020/04/04', - end_date: '2020/05/05' - }; - - before(() => { - projectFundingData = new PostFundingSource(obj); - }); - - it('sets agency_id', () => { - expect(projectFundingData.agency_id).to.equal(obj.agency_id); - }); - - it('sets investment_action_category', () => { - expect(projectFundingData.investment_action_category).to.equal(obj.investment_action_category); - }); - - it('sets agency_project_id', () => { - expect(projectFundingData.agency_project_id).to.equal(obj.agency_project_id); - }); - - it('sets funding_amount', () => { - expect(projectFundingData.funding_amount).to.equal(obj.funding_amount); - }); - - it('sets start_date', () => { - expect(projectFundingData.start_date).to.equal(obj.start_date); - }); - - it('sets end_date', () => { - expect(projectFundingData.end_date).to.equal(obj.end_date); - }); - }); -}); - describe('PostIUCNData', () => { describe('No values provided', () => { let projectIUCNData: PostIUCNData; @@ -503,75 +410,3 @@ describe('PostLocationData', () => { }); }); }); - -describe('PostFundingData', () => { - describe('No values provided', () => { - let data: PostFundingData; - - before(() => { - data = new PostFundingData(null); - }); - - it('sets fundingSources', () => { - expect(data.fundingSources).to.eql([]); - }); - }); - - describe('Values provided but not valid arrays', () => { - let data: PostFundingData; - - const obj = { - fundingSources: null - }; - - before(() => { - data = new PostFundingData(obj); - }); - - it('sets fundingSources', () => { - expect(data.fundingSources).to.eql([]); - }); - }); - - describe('Values provided but with no length', () => { - let data: PostFundingData; - - const obj = { - fundingSources: [] - }; - - before(() => { - data = new PostFundingData(obj); - }); - - it('sets fundingSources', () => { - expect(data.fundingSources).to.eql([]); - }); - }); - - describe('All values provided', () => { - let data: PostFundingData; - - const obj = { - fundingSources: [ - { - agency_id: 1, - investment_action_category: 1, - agency_project_id: 'agency project id', - funding_amount: 12, - start_date: '2020/04/03', - end_date: '2020/05/05', - first_nations_id: null - } - ] - }; - - before(() => { - data = new PostFundingData(obj); - }); - - it('sets fundingSources', () => { - expect(data.fundingSources).to.eql(obj.fundingSources); - }); - }); -}); diff --git a/api/src/models/project-create.ts b/api/src/models/project-create.ts index 9d9a8563f3..b1a388c739 100644 --- a/api/src/models/project-create.ts +++ b/api/src/models/project-create.ts @@ -15,7 +15,6 @@ export class PostProjectObject { objectives: PostObjectivesData; location: PostLocationData; iucn: PostIUCNData; - funding: PostFundingData; partnerships: PostPartnershipsData; constructor(obj?: any) { @@ -25,7 +24,6 @@ export class PostProjectObject { this.project = (obj?.project && new PostProjectData(obj.project)) || null; this.objectives = (obj?.project && new PostObjectivesData(obj.objectives)) || null; this.location = (obj?.location && new PostLocationData(obj.location)) || null; - this.funding = (obj?.funding && new PostFundingData(obj.funding)) || null; this.iucn = (obj?.iucn && new PostIUCNData(obj.iucn)) || null; this.partnerships = (obj?.partnerships && new PostPartnershipsData(obj.partnerships)) || null; } @@ -155,54 +153,6 @@ export class PostIUCNData { } } -/** - * A single project funding agency. - * - * @See PostFundingData - * - * @export - * @class PostFundingSource - */ -export class PostFundingSource { - id?: number; - agency_id?: number; - investment_action_category: number; - agency_project_id: string; - funding_amount: number; - start_date?: string; - end_date?: string; - first_nations_id?: number; - - constructor(obj?: any) { - defaultLog.debug({ label: 'PostFundingSource', message: 'params', obj }); - - this.agency_id = obj?.agency_id || null; - this.investment_action_category = obj?.investment_action_category || null; - this.agency_project_id = obj?.agency_project_id || null; - this.funding_amount = obj?.funding_amount || null; - this.start_date = obj?.start_date || null; - this.end_date = obj?.end_date || null; - this.first_nations_id = obj?.first_nations_id || null; - } -} - -/** - * Processes POST /project funding data - * - * @export - * @class PostFundingData - */ -export class PostFundingData { - fundingSources: PostFundingSource[]; - - constructor(obj?: any) { - defaultLog.debug({ label: 'PostFundingData', message: 'params', obj }); - - this.fundingSources = - (obj?.fundingSources?.length && obj.fundingSources.map((item: any) => new PostFundingSource(item))) || []; - } -} - /** * Processes POST /project partnerships data * diff --git a/api/src/models/project-update.test.ts b/api/src/models/project-update.test.ts index 6001f4169b..80a6a0da5c 100644 --- a/api/src/models/project-update.test.ts +++ b/api/src/models/project-update.test.ts @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { PutCoordinatorData, - PutFundingSource, PutIUCNData, PutLocationData, PutObjectivesData, @@ -299,88 +298,6 @@ describe('PutIUCNData', () => { }); }); -describe('PutFundingSource', () => { - describe('No values provided', () => { - let data: PutFundingSource; - - before(() => { - data = new PutFundingSource(null); - }); - - it('sets id', () => { - expect(data.id).to.equal(null); - }); - - it('sets investment_action_category', () => { - expect(data.investment_action_category).to.equal(null); - }); - - it('sets agency_project_id', () => { - expect(data.agency_project_id).to.equal(null); - }); - - it('sets funding_amount', () => { - expect(data.funding_amount).to.equal(null); - }); - - it('sets start_date', () => { - expect(data.start_date).to.equal(null); - }); - - it('sets end_date', () => { - expect(data.end_date).to.equal(null); - }); - - it('sets revision_count', () => { - expect(data.revision_count).to.equal(null); - }); - }); - - describe('All values provided', () => { - let data: PutFundingSource; - - before(() => { - data = new PutFundingSource({ - id: 1, - investment_action_category: 1, - agency_project_id: 'agency project id', - funding_amount: 20, - start_date: '2020/04/04', - end_date: '2020/05/05', - revision_count: 1 - }); - }); - - it('sets id', () => { - expect(data.id).to.equal(1); - }); - - it('sets investment_action_category', () => { - expect(data.investment_action_category).to.equal(1); - }); - - it('sets agency_project_id', () => { - expect(data.agency_project_id).to.equal('agency project id'); - }); - - it('sets funding_amount', () => { - expect(data.funding_amount).to.equal(20); - }); - - it('sets start_date', () => { - expect(data.start_date).to.equal('2020/04/04'); - }); - - it('sets end_date', () => { - expect(data.end_date).to.equal('2020/05/05'); - }); - - it('sets revision_count', () => { - expect(data.revision_count).to.equal(1); - }); - }); -}); - describe('PutPartnershipsData', () => { describe('No values provided', () => { let data: PutPartnershipsData; diff --git a/api/src/models/project-update.ts b/api/src/models/project-update.ts index 710df9dda2..f35f5c1c7a 100644 --- a/api/src/models/project-update.ts +++ b/api/src/models/project-update.ts @@ -103,47 +103,6 @@ export class PutIUCNData { } } -export class PutFundingSource { - id?: number; - investment_action_category?: number; - agency_project_id?: string; - funding_amount?: number; - start_date: string; - end_date: string; - revision_count: number; - first_nations_id?: number; - - constructor(obj?: any) { - defaultLog.debug({ label: 'PutFundingSource', message: 'params', obj }); - - this.id = obj?.id || null; - this.investment_action_category = obj?.investment_action_category || null; - this.agency_project_id = obj?.agency_project_id || null; - this.funding_amount = obj?.funding_amount || null; - this.start_date = obj?.start_date || null; - this.end_date = obj?.end_date || null; - this.revision_count = obj?.revision_count ?? null; - this.first_nations_id = obj?.first_nations_id ?? null; - } -} - -/** - * Processes PUT /project funding data - * - * @export - * @class PostFundingData - */ -export class PutFundingData { - fundingSources: PutFundingSource[]; - - constructor(obj?: any) { - defaultLog.debug({ label: 'PostFundingData', message: 'params', obj }); - - this.fundingSources = - (obj?.fundingSources?.length && obj.fundingSources.map((item: any) => new PutFundingSource(item))) || []; - } -} - export class PutPartnershipsData { indigenous_partnerships: number[]; stakeholder_partnerships: string[]; diff --git a/api/src/models/project-view.test.ts b/api/src/models/project-view.test.ts index 44be8f89eb..bcb1fc0ff8 100644 --- a/api/src/models/project-view.test.ts +++ b/api/src/models/project-view.test.ts @@ -3,7 +3,6 @@ import { describe } from 'mocha'; import { GetAttachmentsData, GetCoordinatorData, - GetFundingData, GetIUCNClassificationData, GetLocationData, GetObjectivesData, @@ -346,61 +345,6 @@ describe('GetIUCNClassificationData', () => { }); }); -describe('GetFundingData', () => { - describe('No values provided', () => { - let projectFundingData: GetFundingData; - - before(() => { - projectFundingData = new GetFundingData((null as unknown) as any[]); - }); - - it('sets funding sources', function () { - expect(projectFundingData.fundingSources).to.eql([]); - }); - }); - - describe('Empty array as values provided', () => { - let projectFundingData: GetFundingData; - - before(() => { - projectFundingData = new GetFundingData([]); - }); - - it('sets funding sources', function () { - expect(projectFundingData.fundingSources).to.eql([]); - }); - }); - - describe('All values provided', () => { - let projectFundingData: GetFundingData; - - const fundings = [ - { - id: 1, - agency_id: 2, - investment_action_category: 3, - investment_action_category_name: 'Something', - agency_name: 'fake', - funding_amount: 123456, - start_date: Date.now().toString(), - end_date: Date.now().toString(), - agency_project_id: '12', - revision_count: 1, - first_nations_name: null, - first_nations_id: null - } - ]; - - before(() => { - projectFundingData = new GetFundingData(fundings); - }); - - it('sets funding sources', function () { - expect(projectFundingData.fundingSources).to.eql(fundings); - }); - }); -}); - describe('GetPartnershipsData', () => { describe('No values provided', () => { let data: GetPartnershipsData; diff --git a/api/src/models/project-view.ts b/api/src/models/project-view.ts index c46288070f..e8252fac18 100644 --- a/api/src/models/project-view.ts +++ b/api/src/models/project-view.ts @@ -20,7 +20,6 @@ export interface IGetProject { objectives: GetObjectivesData; location: GetLocationData; iucn: GetIUCNClassificationData; - funding: GetFundingData; partnerships: GetPartnershipsData; } @@ -138,46 +137,6 @@ export class GetIUCNClassificationData { }) ?? []; } } - -interface IGetFundingSource { - id: number; - agency_id?: number; - investment_action_category?: number; - investment_action_category_name?: string; - agency_name: string; - funding_amount?: number; - start_date: string; - end_date: string; - agency_project_id: string; - first_nations_id?: number; - first_nations_name?: string; - revision_count: number; -} - -export class GetFundingData { - fundingSources: IGetFundingSource[]; - - constructor(fundingData?: any[]) { - this.fundingSources = - fundingData?.map((item: any) => { - return { - id: item.id, - agency_id: item.agency_id, - investment_action_category: item.investment_action_category, - investment_action_category_name: item.investment_action_category_name, - agency_name: item.agency_name, - funding_amount: item.funding_amount, - start_date: item.start_date, - end_date: item.end_date, - agency_project_id: item.agency_project_id, - revision_count: item.revision_count, - first_nations_id: item.first_nations_id, - first_nations_name: item.first_nations_name - }; - }) ?? []; - } -} - /** * Pre-processes GET /projects/{id} partnerships data * diff --git a/api/src/models/survey-create.test.ts b/api/src/models/survey-create.test.ts index 0024bd9876..14ab354dbb 100644 --- a/api/src/models/survey-create.test.ts +++ b/api/src/models/survey-create.test.ts @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { PostAgreementsData, - PostFundingData, PostLocationData, PostPermitData, PostProprietorData, @@ -32,10 +31,6 @@ describe('PostSurveyObject', () => { expect(data.permit).to.equal(null); }); - it('sets funding', () => { - expect(data.funding).to.equal(null); - }); - it('sets proprietor', () => { expect(data.proprietor).to.equal(null); }); @@ -60,7 +55,6 @@ describe('PostSurveyObject', () => { survey_details: {}, species: {}, permit: {}, - funding: {}, proprietor: {}, purpose_and_methodology: {}, location: {}, @@ -83,10 +77,6 @@ describe('PostSurveyObject', () => { expect(data.permit).to.instanceOf(PostPermitData); }); - it('sets funding', () => { - expect(data.funding).to.instanceOf(PostFundingData); - }); - it('sets proprietor', () => { expect(data.proprietor).to.instanceOf(PostProprietorData); }); @@ -249,36 +239,6 @@ describe('PostPermitData', () => { }); }); -describe('PostFundingData', () => { - describe('No values provided', () => { - let data: PostFundingData; - - before(() => { - data = new PostFundingData(null); - }); - - it('sets permit_number', () => { - expect(data.funding_sources).to.eql([]); - }); - }); - - describe('All values provided', () => { - let data: PostFundingData; - - const obj = { - funding_sources: [1, 2] - }; - - before(() => { - data = new PostFundingData(obj); - }); - - it('sets funding_sources', () => { - expect(data.funding_sources).to.eql([1, 2]); - }); - }); -}); - describe('PostProprietorData', () => { describe('No values provided', () => { let data: PostProprietorData; diff --git a/api/src/models/survey-create.ts b/api/src/models/survey-create.ts index 6b02e39cea..c593b0420e 100644 --- a/api/src/models/survey-create.ts +++ b/api/src/models/survey-create.ts @@ -4,7 +4,6 @@ export class PostSurveyObject { survey_details: PostSurveyDetailsData; species: PostSpeciesData; permit: PostPermitData; - funding: PostFundingData; proprietor: PostProprietorData; purpose_and_methodology: PostPurposeAndMethodologyData; location: PostLocationData; @@ -14,7 +13,6 @@ export class PostSurveyObject { this.survey_details = (obj?.survey_details && new PostSurveyDetailsData(obj.survey_details)) || null; this.species = (obj?.species && new PostSpeciesData(obj.species)) || null; this.permit = (obj?.permit && new PostPermitData(obj.permit)) || null; - this.funding = (obj?.funding && new PostFundingData(obj.funding)) || null; this.proprietor = (obj?.proprietor && new PostProprietorData(obj.proprietor)) || null; this.purpose_and_methodology = (obj?.purpose_and_methodology && new PostPurposeAndMethodologyData(obj.purpose_and_methodology)) || null; @@ -55,15 +53,6 @@ export class PostPermitData { this.permits = obj?.permits || []; } } - -export class PostFundingData { - funding_sources: number[]; - - constructor(obj?: any) { - this.funding_sources = obj?.funding_sources || []; - } -} - export class PostProprietorData { prt_id: number; fn_id: number; diff --git a/api/src/models/survey-update.test.ts b/api/src/models/survey-update.test.ts index f7cf17c3f9..1b1e001dd9 100644 --- a/api/src/models/survey-update.test.ts +++ b/api/src/models/survey-update.test.ts @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { PutSurveyDetailsData, - PutSurveyFundingData, PutSurveyLocationData, PutSurveyObject, PutSurveyPermitData, @@ -31,10 +30,6 @@ describe('PutSurveyObject', () => { expect(data.permit).to.equal(null); }); - it('sets funding', () => { - expect(data.funding).to.equal(null); - }); - it('sets proprietor', () => { expect(data.proprietor).to.equal(null); }); @@ -55,7 +50,6 @@ describe('PutSurveyObject', () => { survey_details: {}, species: {}, permit: {}, - funding: {}, proprietor: {}, purpose_and_methodology: {}, location: {}, @@ -78,10 +72,6 @@ describe('PutSurveyObject', () => { expect(data.permit).to.instanceOf(PutSurveyPermitData); }); - it('sets funding', () => { - expect(data.funding).to.instanceOf(PutSurveyFundingData); - }); - it('sets proprietor', () => { expect(data.proprietor).to.instanceOf(PutSurveyProprietorData); }); @@ -254,36 +244,6 @@ describe('PutPermitData', () => { }); }); -describe('PutFundingData', () => { - describe('No values provided', () => { - let data: PutSurveyFundingData; - - before(() => { - data = new PutSurveyFundingData(null); - }); - - it('sets permit_number', () => { - expect(data.funding_sources).to.eql([]); - }); - }); - - describe('All values provided', () => { - let data: PutSurveyFundingData; - - const obj = { - funding_sources: [1, 2] - }; - - before(() => { - data = new PutSurveyFundingData(obj); - }); - - it('sets funding_sources', () => { - expect(data.funding_sources).to.eql([1, 2]); - }); - }); -}); - describe('PutProprietorData', () => { describe('No values provided', () => { let data: PutSurveyProprietorData; diff --git a/api/src/models/survey-update.ts b/api/src/models/survey-update.ts index b4c6f7a6c0..8ce9a538b7 100644 --- a/api/src/models/survey-update.ts +++ b/api/src/models/survey-update.ts @@ -4,7 +4,6 @@ export class PutSurveyObject { survey_details: PutSurveyDetailsData; species: PutSurveySpeciesData; permit: PutSurveyPermitData; - funding: PutSurveyFundingData; proprietor: PutSurveyProprietorData; purpose_and_methodology: PutSurveyPurposeAndMethodologyData; location: PutSurveyLocationData; @@ -13,7 +12,6 @@ export class PutSurveyObject { this.survey_details = (obj?.survey_details && new PutSurveyDetailsData(obj.survey_details)) || null; this.species = (obj?.species && new PutSurveySpeciesData(obj.species)) || null; this.permit = (obj?.permit && new PutSurveyPermitData(obj.permit)) || null; - this.funding = (obj?.funding && new PutSurveyFundingData(obj.funding)) || null; this.proprietor = (obj?.proprietor && new PutSurveyProprietorData(obj.proprietor)) || null; this.purpose_and_methodology = (obj?.purpose_and_methodology && new PutSurveyPurposeAndMethodologyData(obj.purpose_and_methodology)) || null; @@ -57,14 +55,6 @@ export class PutSurveyPermitData { } } -export class PutSurveyFundingData { - funding_sources: number[]; - - constructor(obj?: any) { - this.funding_sources = (obj?.funding_sources?.length && obj?.funding_sources) || []; - } -} - export class PutSurveyProprietorData { prt_id: number; fn_id: number; diff --git a/api/src/models/survey-view.test.ts b/api/src/models/survey-view.test.ts index 22cdd8fd48..beb9dac938 100644 --- a/api/src/models/survey-view.test.ts +++ b/api/src/models/survey-view.test.ts @@ -8,7 +8,6 @@ import { GetPermitData, GetReportAttachmentsData, GetSurveyData, - GetSurveyFundingSources, GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData @@ -214,88 +213,6 @@ describe('GetPermitData', () => { }); }); -describe('GetSurveyFundingSources', () => { - describe('No values provided', () => { - let data: GetSurveyFundingSources; - - before(() => { - data = new GetSurveyFundingSources(); - }); - - it('sets funding_sources', () => { - expect(data.funding_sources).to.eql([]); - }); - }); - - describe('All values provided', () => { - let data: GetSurveyFundingSources; - - const obj = [ - { - project_funding_source_id: 1, - funding_amount: 2, - agency_id: 3, - funding_start_date: '2020/04/04', - funding_end_date: '2020/04/05', - investment_action_category_id: 4, - investment_action_category_name: 'name11', - agency_name: 'name1', - funding_source_project_id: '5', - first_nations_id: null, - first_nations_name: null - }, - { - project_funding_source_id: 6, - funding_amount: 7, - agency_id: 8, - funding_start_date: '2020/04/06', - funding_end_date: '2020/04/07', - investment_action_category_id: 9, - investment_action_category_name: 'name22', - agency_name: 'name2', - funding_source_project_id: '10', - first_nations_id: null, - first_nations_name: null - } - ]; - - before(() => { - data = new GetSurveyFundingSources(obj); - }); - - it('sets funding_sources', () => { - expect(data.funding_sources).to.eql([ - { - project_funding_source_id: 1, - funding_amount: 2, - agency_id: 3, - funding_start_date: '2020/04/04', - funding_end_date: '2020/04/05', - investment_action_category_id: 4, - investment_action_category_name: 'name11', - agency_name: 'name1', - funding_source_project_id: '5', - first_nations_id: null, - first_nations_name: null - }, - { - project_funding_source_id: 6, - funding_amount: 7, - agency_id: 8, - funding_start_date: '2020/04/06', - funding_end_date: '2020/04/07', - investment_action_category_id: 9, - investment_action_category_name: 'name22', - agency_name: 'name2', - funding_source_project_id: '10', - first_nations_id: null, - first_nations_name: null - } - ]); - }); - }); -}); - describe('GetSurveyProprietorData', () => { describe('No values provided', () => { let data: GetSurveyProprietorData; diff --git a/api/src/models/survey-view.ts b/api/src/models/survey-view.ts index eccd0e7f2a..db7c594a49 100644 --- a/api/src/models/survey-view.ts +++ b/api/src/models/survey-view.ts @@ -7,7 +7,6 @@ export type SurveyObject = { species: GetFocalSpeciesData & GetAncillarySpeciesData; permit: GetPermitData; purpose_and_methodology: GetSurveyPurposeAndMethodologyData; - funding: GetSurveyFundingSources; proprietor: GetSurveyProprietorData | null; location: GetSurveyLocationData; }; @@ -106,43 +105,6 @@ export class GetSurveyPurposeAndMethodologyData { } } -interface IGetSurveyFundingSource { - project_funding_source_id: number; - funding_amount?: number; - agency_id: number; - funding_start_date: string; - funding_end_date: string; - investment_action_category_id?: number; - investment_action_category_name?: string; - agency_name?: string; - funding_source_project_id: string; - first_nations_id?: number; - first_nations_name?: string; -} - -export class GetSurveyFundingSources { - funding_sources: IGetSurveyFundingSource[]; - - constructor(obj?: any[]) { - this.funding_sources = - obj?.map((item: any) => { - return { - project_funding_source_id: item.project_funding_source_id, - funding_amount: item.funding_amount, - agency_id: item.agency_id, - funding_start_date: item.funding_start_date, - funding_end_date: item.funding_end_date, - investment_action_category_id: item.investment_action_category_id, - investment_action_category_name: item.investment_action_category_name, - agency_name: item.agency_name, - funding_source_project_id: item.funding_source_project_id, - first_nations_id: item.first_nations_id, - first_nations_name: item.first_nations_name - }; - }) ?? []; - } -} - export class GetSurveyProprietorData { proprietor_type_name: string; proprietor_type_id: number; diff --git a/api/src/openapi/schemas/project-funding-source.test.ts b/api/src/openapi/schemas/project-funding-source.test.ts deleted file mode 100644 index cd8e115113..0000000000 --- a/api/src/openapi/schemas/project-funding-source.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Ajv from 'ajv'; -import { expect } from 'chai'; -import { describe } from 'mocha'; -import { projectFundingSourcePostRequestObject } from './project-funding-source'; - -describe('projectFundingSourcePostRequestObject', () => { - const ajv = new Ajv(); - - it('is valid openapi v3 schema', () => { - expect(ajv.validateSchema(projectFundingSourcePostRequestObject)).to.be.true; - }); -}); diff --git a/api/src/openapi/schemas/project-funding-source.ts b/api/src/openapi/schemas/project-funding-source.ts deleted file mode 100644 index 26cc9a16df..0000000000 --- a/api/src/openapi/schemas/project-funding-source.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Request Object for project funding source POST request - */ -export const projectFundingSourcePostRequestObject = { - title: 'Project funding source post request object', - type: 'object', - required: ['agency_id', 'investment_action_category', 'funding_amount', 'start_date', 'end_date'], - properties: { - agency_id: { - type: 'number' - }, - investment_action_category: { - type: 'number' - }, - agency_project_id: { - type: 'string' - }, - funding_amount: { - type: 'number' - }, - start_date: { - type: 'string', - description: 'ISO 8601 date string' - }, - end_date: { - type: 'string', - description: 'ISO 8601 date string' - } - } -}; diff --git a/api/src/openapi/schemas/project.ts b/api/src/openapi/schemas/project.ts index 426ba19bb7..a6e41425c9 100644 --- a/api/src/openapi/schemas/project.ts +++ b/api/src/openapi/schemas/project.ts @@ -1,80 +1,10 @@ -// A funding source object requiring first nations specific data (first_nations_id) -export const projectFundingSourceFirstNations = { - title: 'Project funding source with First Nations data', - type: 'object', - required: ['first_nations_id'], - properties: { - first_nations_id: { - type: 'integer', - minimum: 1 - }, - agency_id: { - type: 'integer', - nullable: true - }, - investment_action_category: { - type: 'integer', - nullable: true - }, - agency_project_id: { - type: 'string', - nullable: true - }, - funding_amount: { - type: 'number', - nullable: true - }, - start_date: { - type: 'string', - description: 'ISO 8601 date string', - nullable: true - }, - end_date: { - type: 'string', - description: 'ISO 8601 date string', - nullable: true - } - } -}; - -// A funding source object requiring agency specific data (agency_id, funding_amount) -export const projectFundingSourceAgency = { - title: 'Project funding source with Agency data', - type: 'object', - required: ['agency_id', 'investment_action_category', 'start_date', 'end_date', 'funding_amount'], - properties: { - agency_id: { - type: 'integer', - minimum: 1 - }, - investment_action_category: { - type: 'number', - nullable: false - }, - agency_project_id: { - type: 'string', - nullable: true - }, - funding_amount: { - type: 'number' - }, - start_date: { - type: 'string', - description: 'ISO 8601 date string' - }, - end_date: { - type: 'string', - description: 'ISO 8601 date string' - } - } -}; /** * Request Object for project create POST request */ export const projectCreatePostRequestObject = { title: 'Project post request object', type: 'object', - required: ['coordinator', 'project', 'location', 'iucn', 'funding'], + required: ['coordinator', 'project', 'location', 'iucn'], properties: { coordinator: { title: 'Project coordinator', @@ -163,18 +93,6 @@ export const projectCreatePostRequestObject = { } } }, - funding: { - title: 'Project funding sources', - type: 'object', - properties: { - fundingSources: { - type: 'array', - items: { - anyOf: [{ ...projectFundingSourceAgency }, { ...projectFundingSourceFirstNations }] - } - } - } - }, partnerships: { title: 'Project partnerships', type: 'object', @@ -234,17 +152,6 @@ const projectUpdateProperties = { } } }, - funding: { - type: 'object', - properties: { - fundingSources: { - type: 'array', - items: { - anyOf: [{ ...projectFundingSourceAgency }, { ...projectFundingSourceFirstNations }] - } - } - } - }, partnerships: { type: 'object', properties: {} } }; diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index 96bf5d37b3..01a6baedcb 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -59,7 +59,6 @@ POST.apiDoc = { 'survey_details', 'species', 'permit', - 'funding', 'proprietor', 'purpose_and_methodology', 'location', @@ -127,17 +126,6 @@ POST.apiDoc = { } } }, - funding: { - type: 'object', - properties: { - funding_sources: { - type: 'array', - items: { - type: 'integer' - } - } - } - }, proprietor: { type: 'object', properties: { diff --git a/api/src/paths/project/{projectId}/survey/funding-sources/list.test.ts b/api/src/paths/project/{projectId}/survey/funding-sources/list.test.ts deleted file mode 100644 index 811ac7314f..0000000000 --- a/api/src/paths/project/{projectId}/survey/funding-sources/list.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as db from '../../../../../database/db'; -import { HTTPError } from '../../../../../errors/http-error'; -import { ProjectService } from '../../../../../services/project-service'; -import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../__mocks__/db'; -import { getSurveyFundingSources } from './list'; - -chai.use(sinonChai); - -describe('getSurveyFundingSources', () => { - afterEach(() => { - sinon.restore(); - }); - - it('fetches survey funding sources', async () => { - const dbConnectionObj = getMockDBConnection(); - - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - sinon.stub(ProjectService.prototype, 'getFundingData').resolves({ fundingSources: [] }); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.params = { - projectId: '1' - }; - - const requestHandler = getSurveyFundingSources(); - - await requestHandler(mockReq, mockRes, mockNext); - - expect(mockRes.statusValue).to.equal(200); - expect(mockRes.jsonValue).to.eql([]); - }); - - it('catches and re-throws error', async () => { - const dbConnectionObj = getMockDBConnection({ release: sinon.stub() }); - - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - sinon.stub(ProjectService.prototype, 'getFundingData').rejects(new Error('a test error')); - - const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - - mockReq.params = { - projectId: '1' - }; - - try { - const requestHandler = getSurveyFundingSources(); - - await requestHandler(mockReq, mockRes, mockNext); - expect.fail(); - } catch (actualError) { - expect(dbConnectionObj.release).to.have.been.called; - - expect((actualError as HTTPError).message).to.equal('a test error'); - } - }); -}); diff --git a/api/src/paths/project/{projectId}/survey/funding-sources/list.ts b/api/src/paths/project/{projectId}/survey/funding-sources/list.ts deleted file mode 100644 index 5c41e7f7d9..0000000000 --- a/api/src/paths/project/{projectId}/survey/funding-sources/list.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../constants/roles'; -import { getDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/http-error'; -import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; -import { ProjectService } from '../../../../../services/project-service'; -import { getLogger } from '../../../../../utils/logger'; - -const defaultLog = getLogger('/api/project/{projectId}/survey/funding-sources/list'); - -export const GET: Operation = [ - authorizeRequestHandler((req) => { - return { - or: [ - { - validProjectPermissions: [ - PROJECT_PERMISSION.COORDINATOR, - PROJECT_PERMISSION.COLLABORATOR, - PROJECT_PERMISSION.OBSERVER - ], - projectId: Number(req.params.projectId), - discriminator: 'ProjectPermission' - }, - { - validSystemRoles: [SYSTEM_ROLE.DATA_ADMINISTRATOR], - discriminator: 'SystemRole' - } - ] - }; - }), - getSurveyFundingSources() -]; - -GET.apiDoc = { - description: 'Fetches a list of project funding sources available for use by a survey.', - tags: ['funding_sources'], - security: [ - { - Bearer: [] - } - ], - parameters: [ - { - in: 'path', - name: 'projectId', - schema: { - type: 'number' - }, - required: true - } - ], - responses: { - 200: { - description: 'Funding sources get response array.', - content: { - 'application/json': { - schema: { - type: 'array', - description: 'Funding sources applicable for the survey', - items: { - type: 'object', - properties: { - id: { - type: 'number' - }, - agency_id: { - type: 'integer', - minimum: 1, - nullable: true - }, - investment_action_category: { - type: 'number', - nullable: true - }, - investment_action_category_name: { - type: 'string', - nullable: true - }, - agency_name: { - type: 'string', - nullable: true - }, - funding_amount: { - type: 'number', - nullable: true - }, - start_date: { - type: 'string', - format: 'date', - description: 'ISO 8601 date string for the funding start date' - }, - end_date: { - type: 'string', - format: 'date', - description: 'ISO 8601 date string for the funding end_date' - }, - agency_project_id: { - type: 'string', - nullable: true - }, - revision_count: { - type: 'number' - }, - first_nations_id: { - type: 'integer', - minimum: 1, - nullable: true - }, - first_nations_name: { - type: 'string', - nullable: true - } - } - } - } - } - } - }, - 401: { - $ref: '#/components/responses/401' - }, - default: { - $ref: '#/components/responses/default' - } - } -}; - -export function getSurveyFundingSources(): RequestHandler { - return async (req, res) => { - defaultLog.debug({ label: 'Get survey funding sources list', message: 'params', req_params: req.params }); - - if (!req.params.projectId) { - throw new HTTP400('Missing required path param `projectId`'); - } - - const connection = getDBConnection(req['keycloak_token']); - - try { - await connection.open(); - - const projectService = new ProjectService(connection); - - const response = await projectService.getFundingData(Number(req.params.projectId)); - - return res.status(200).json(response.fundingSources); - } catch (error) { - defaultLog.error({ label: 'getSurveyFundingSources', message: 'error', error }); - await connection.rollback(); - throw error; - } finally { - connection.release(); - } - }; -} diff --git a/api/src/paths/project/{projectId}/survey/list.ts b/api/src/paths/project/{projectId}/survey/list.ts index d58307651c..145e022342 100644 --- a/api/src/paths/project/{projectId}/survey/list.ts +++ b/api/src/paths/project/{projectId}/survey/list.ts @@ -69,7 +69,6 @@ GET.apiDoc = { 'survey_details', 'species', 'permit', - 'funding', 'proprietor', 'purpose_and_methodology', 'location' @@ -172,49 +171,6 @@ GET.apiDoc = { } } }, - funding: { - description: 'Survey Funding Sources', - type: 'object', - properties: { - funding_sources: { - type: 'array', - items: { - type: 'object', - required: [ - 'project_funding_source_id', - 'agency_name', - 'funding_amount', - 'funding_start_date', - 'funding_end_date' - ], - properties: { - project_funding_source_id: { - type: 'number', - nullable: true - }, - agency_name: { - type: 'string', - nullable: true - }, - funding_amount: { - type: 'number', - nullable: true - }, - funding_start_date: { - type: 'string', - nullable: true, - description: 'ISO 8601 date string' - }, - funding_end_date: { - type: 'string', - nullable: true, - description: 'ISO 8601 date string' - } - } - } - } - } - }, purpose_and_methodology: { description: 'Survey Details', type: 'object', diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts index ea615cdc15..eb1b371d3e 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts @@ -143,19 +143,6 @@ PUT.apiDoc = { } } }, - funding: { - description: 'Survey Funding Sources', - type: 'object', - required: ['funding_sources'], - properties: { - funding_sources: { - type: 'array', - items: { - type: 'integer' - } - } - } - }, proprietor: { type: 'object', required: [ diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.test.ts index 1738faab52..0792995975 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.test.ts @@ -67,8 +67,7 @@ describe('getSurveyForUpdate', () => { const getSurveyByIdStub = sinon.stub(SurveyService.prototype, 'getSurveyById').resolves(({ id: 1, - proprietor: {}, - funding: {} + proprietor: {} } as unknown) as SurveyObject); const expectedResponse = { @@ -84,9 +83,6 @@ describe('getSurveyForUpdate', () => { proprietor_name: '', disa_required: 'false' }, - funding: { - funding_sources: [] - }, agreements: { sedis_procedures_accepted: 'true', foippa_requirements_accepted: 'true' @@ -132,8 +128,7 @@ describe('getSurveyForUpdate', () => { const getSurveyByIdStub = sinon.stub(SurveyService.prototype, 'getSurveyById').resolves(({ id: 1, - proprietor: { proprietor_type_id: 1, first_nations_id: 1, disa_required: true }, - funding: { funding_sources: [{ project_funding_source_id: 1 }] } + proprietor: { proprietor_type_id: 1, first_nations_id: 1, disa_required: true } } as unknown) as SurveyObject); const expectedResponse = { @@ -146,9 +141,6 @@ describe('getSurveyForUpdate', () => { first_nations_id: 1, disa_required: 'true' }, - funding: { - funding_sources: [1] - }, agreements: { sedis_procedures_accepted: 'true', foippa_requirements_accepted: 'true' diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts index 4e3b8b1c27..bc67b7ce28 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update/get.ts @@ -72,15 +72,7 @@ GET.apiDoc = { properties: { surveyData: { type: 'object', - required: [ - 'survey_details', - 'species', - 'permit', - 'funding', - 'proprietor', - 'purpose_and_methodology', - 'location' - ], + required: ['survey_details', 'species', 'permit', 'proprietor', 'purpose_and_methodology', 'location'], properties: { survey_details: { description: 'Survey Details', @@ -174,18 +166,6 @@ GET.apiDoc = { } } }, - funding: { - description: 'Survey Funding Sources', - type: 'object', - properties: { - funding_sources: { - type: 'array', - items: { - type: 'integer' - } - } - } - }, purpose_and_methodology: { description: 'Survey Details', type: 'object', @@ -328,18 +308,9 @@ export function getSurveyForUpdate(): RequestHandler { }; } - let fundingSources: number[] = []; - - if (surveyObject?.funding?.funding_sources) { - fundingSources = surveyObject.funding.funding_sources.map((item) => item.project_funding_source_id); - } - const surveyData = { ...surveyObject, proprietor: proprietor, - funding: { - funding_sources: fundingSources - }, agreements: { sedis_procedures_accepted: 'true', foippa_requirements_accepted: 'true' diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts index 86fb61125d..1d3b848c45 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts @@ -38,17 +38,6 @@ describe('survey/{surveyId}/view', () => { permit_number: '123', permit_type: 'type' }, - funding: { - funding_sources: [ - { - project_funding_source_id: 1, - agency_name: 'name', - funding_amount: 100, - funding_start_date: '2020-04-04', - funding_end_date: '2020-05-05' - } - ] - }, purpose_and_methodology: { field_method_id: 1, additional_details: 'details', @@ -113,17 +102,6 @@ describe('survey/{surveyId}/view', () => { permit_number: null, permit_type: null }, - funding: { - funding_sources: [ - { - project_funding_source_id: null, - agency_name: null, - funding_amount: null, - funding_start_date: null, - funding_end_date: null - } - ] - }, purpose_and_methodology: { field_method_id: 1, additional_details: null, diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts index 764a3b837a..4070e38148 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts @@ -72,15 +72,7 @@ GET.apiDoc = { properties: { surveyData: { type: 'object', - required: [ - 'survey_details', - 'species', - 'permit', - 'funding', - 'proprietor', - 'purpose_and_methodology', - 'location' - ], + required: ['survey_details', 'species', 'permit', 'proprietor', 'purpose_and_methodology', 'location'], properties: { survey_details: { description: 'Survey Details', @@ -174,57 +166,6 @@ GET.apiDoc = { } } }, - funding: { - description: 'Survey Funding Sources', - type: 'object', - properties: { - funding_sources: { - type: 'array', - items: { - type: 'object', - required: [ - 'project_funding_source_id', - 'agency_name', - 'funding_amount', - 'funding_start_date', - 'funding_end_date' - ], - properties: { - project_funding_source_id: { - type: 'number', - nullable: true - }, - agency_name: { - type: 'string', - nullable: true - }, - funding_amount: { - type: 'number', - nullable: true - }, - funding_start_date: { - type: 'string', - nullable: true, - description: 'ISO 8601 date string' - }, - funding_end_date: { - type: 'string', - nullable: true, - description: 'ISO 8601 date string' - }, - first_nations_id: { - type: 'number', - nullable: true - }, - first_nations_name: { - type: 'string', - nullable: true - } - } - } - } - } - }, purpose_and_methodology: { description: 'Survey Details', type: 'object', diff --git a/api/src/paths/project/{projectId}/update.test.ts b/api/src/paths/project/{projectId}/update.test.ts index c74c45bd24..3cd2c1b414 100644 --- a/api/src/paths/project/{projectId}/update.test.ts +++ b/api/src/paths/project/{projectId}/update.test.ts @@ -50,7 +50,6 @@ describe('update', () => { objectives: undefined, location: undefined, iucn: undefined, - funding: undefined, partnerships: undefined }; @@ -147,7 +146,6 @@ describe('update', () => { }, iucn: {}, contact: {}, - funding: {}, partnerships: {}, location: {} }; diff --git a/api/src/paths/project/{projectId}/update.ts b/api/src/paths/project/{projectId}/update.ts index 9d0081513f..42df31ac0c 100644 --- a/api/src/paths/project/{projectId}/update.ts +++ b/api/src/paths/project/{projectId}/update.ts @@ -37,7 +37,6 @@ export enum GET_ENTITIES { objectives = 'objectives', location = 'location', iucn = 'iucn', - funding = 'funding', partnerships = 'partnerships' } @@ -212,72 +211,6 @@ GET.apiDoc = { } } }, - funding: { - description: 'The project funding details', - type: 'object', - required: ['fundingSources'], - nullable: true, - properties: { - fundingSources: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'number' - }, - agency_id: { - type: 'number', - nullable: true - }, - investment_action_category: { - type: 'number', - nullable: true - }, - investment_action_category_name: { - type: 'string', - nullable: true - }, - agency_name: { - type: 'string', - nullable: true - }, - funding_amount: { - type: 'number', - nullable: true - }, - start_date: { - type: 'string', - format: 'date', - nullable: true, - description: 'ISO 8601 date string for the funding start date' - }, - end_date: { - type: 'string', - format: 'date', - nullable: true, - description: 'ISO 8601 date string for the funding end_date' - }, - agency_project_id: { - type: 'string', - nullable: true - }, - revision_count: { - type: 'number' - }, - first_nations_id: { - type: 'number', - nullable: true - }, - first_nations_name: { - type: 'string', - nullable: true - } - } - } - } - } - }, partnerships: { description: 'The project partners', type: 'object', @@ -436,7 +369,6 @@ export interface IUpdateProject { objectives: any | null; location: { geometry: Feature[]; location_description: string } | null; iucn: any | null; - funding: any | null; partnerships: any | null; } diff --git a/api/src/paths/project/{projectId}/view.ts b/api/src/paths/project/{projectId}/view.ts index d8a9dec8b2..ef4cc44816 100644 --- a/api/src/paths/project/{projectId}/view.ts +++ b/api/src/paths/project/{projectId}/view.ts @@ -63,7 +63,7 @@ GET.apiDoc = { properties: { projectData: { type: 'object', - required: ['project', 'coordinator', 'objectives', 'location', 'iucn', 'funding', 'partnerships'], + required: ['project', 'coordinator', 'objectives', 'location', 'iucn', 'partnerships'], properties: { project: { description: 'Basic project metadata', @@ -74,7 +74,6 @@ GET.apiDoc = { 'project_programs', 'project_types', 'start_date', - 'end_date', 'comments' ], properties: { @@ -194,71 +193,6 @@ GET.apiDoc = { } } }, - funding: { - description: 'The project funding details', - type: 'object', - required: ['fundingSources'], - properties: { - fundingSources: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'number' - }, - agency_id: { - type: 'number', - nullable: true - }, - investment_action_category: { - type: 'number', - nullable: true - }, - investment_action_category_name: { - type: 'string', - nullable: true - }, - agency_name: { - type: 'string', - nullable: true - }, - funding_amount: { - type: 'number', - nullable: true - }, - start_date: { - type: 'string', - format: 'date', - description: 'ISO 8601 date string for the funding start date', - nullable: true - }, - end_date: { - type: 'string', - format: 'date', - description: 'ISO 8601 date string for the funding end_date', - nullable: true - }, - agency_project_id: { - type: 'string', - nullable: true - }, - revision_count: { - type: 'number' - }, - first_nations_id: { - type: 'number', - nullable: true - }, - first_nations_name: { - type: 'string', - nullable: true - } - } - } - } - } - }, partnerships: { description: 'The project partners', type: 'object', diff --git a/api/src/repositories/project-repository.test.ts b/api/src/repositories/project-repository.test.ts index bcd5b15608..e20c57c533 100644 --- a/api/src/repositories/project-repository.test.ts +++ b/api/src/repositories/project-repository.test.ts @@ -3,13 +3,11 @@ import { describe } from 'mocha'; import { QueryResult } from 'pg'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import { ApiError, ApiExecuteSQLError } from '../errors/api-error'; -import { PostFundingSource, PostProjectObject } from '../models/project-create'; -import { PutFundingSource } from '../models/project-update'; +import { ApiExecuteSQLError } from '../errors/api-error'; +import { PostProjectObject } from '../models/project-create'; import { GetAttachmentsData, GetCoordinatorData, - GetFundingData, GetIUCNClassificationData, GetLocationData, GetObjectivesData, @@ -21,266 +19,6 @@ import { ProjectRepository } from './project-repository'; chai.use(sinonChai); describe('ProjectRepository', () => { - describe('getProjectFundingSourceIds', () => { - afterEach(() => { - sinon.restore(); - }); - - it('should return an array of project funding source ids', async () => { - const mockQueryResponse = ({ - rowCount: 1, - rows: [{ project_funding_source_id: 2 }] - } as unknown) as QueryResult<{ - project_funding_source_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - const response = await permitRepository.getProjectFundingSourceIds(1); - - expect(response).to.eql([{ project_funding_source_id: 2 }]); - }); - - it('should throw an error if no funding were found', async () => { - const mockQueryResponse = ({} as unknown) as QueryResult<{ - project_funding_source_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - try { - await permitRepository.getProjectFundingSourceIds(1); - expect.fail(); - } catch (error) { - expect((error as ApiError).message).to.equal('Failed to get project funding sources by Id'); - } - }); - }); - - describe('deleteSurveyFundingSourceConnectionToProject', () => { - afterEach(() => { - sinon.restore(); - }); - - it('should delete survey funding source connected to project returning survey_id', async () => { - const mockQueryResponse = ({ - rowCount: 1, - rows: [{ survey_id: 2 }] - } as unknown) as QueryResult<{ - survey_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - const response = await permitRepository.deleteSurveyFundingSourceConnectionToProject(1); - - expect(response).to.eql([{ survey_id: 2 }]); - }); - - it('should throw an error if delete failed', async () => { - const mockQueryResponse = ({} as unknown) as QueryResult<{ - project_funding_source_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - try { - await permitRepository.deleteSurveyFundingSourceConnectionToProject(1); - expect.fail(); - } catch (error) { - expect((error as ApiError).message).to.equal('Failed to delete survey funding source by id'); - } - }); - }); - - describe('deleteProjectFundingSource', () => { - afterEach(() => { - sinon.restore(); - }); - - it('should delete project funding source', async () => { - const mockQueryResponse = ({ - rowCount: 1, - rows: [{ survey_id: 2 }] - } as unknown) as QueryResult<{ - survey_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - const response = await permitRepository.deleteProjectFundingSource(1); - - expect(response).to.eql([{ survey_id: 2 }]); - }); - - it('should throw an error delete failed', async () => { - const mockQueryResponse = ({} as unknown) as QueryResult<{ - survey_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - try { - await permitRepository.deleteProjectFundingSource(1); - expect.fail(); - } catch (error) { - expect((error as ApiError).message).to.equal('Failed to delete project funding source'); - } - }); - }); - - describe('updateProjectFundingSource', () => { - afterEach(() => { - sinon.restore(); - }); - - it('should update project funding source', async () => { - const mockQueryResponse = ({ - rowCount: 1, - rows: [{ project_funding_source_id: 2 }] - } as unknown) as QueryResult<{ - project_funding_source_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const data = new PutFundingSource({ - id: 1, - investment_action_category: 1, - agency_project_id: 'string', - funding_amount: 1, - start_date: 'string', - end_date: 'string', - revision_count: '1' - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - const response = await permitRepository.updateProjectFundingSource(data, 1); - - expect(response).to.eql({ project_funding_source_id: 2 }); - }); - - it('should throw an error update failed', async () => { - const mockQueryResponse = ({} as unknown) as QueryResult<{ - project_funding_source_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const data = new PutFundingSource({ - id: 1, - investment_action_category: 1, - agency_project_id: 'string', - funding_amount: 1, - start_date: 'string', - end_date: 'string', - revision_count: '1' - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - try { - await permitRepository.updateProjectFundingSource(data, 1); - expect.fail(); - } catch (error) { - expect((error as ApiError).message).to.equal('Failed to update project funding source'); - } - }); - }); - - describe('insertProjectFundingSource', () => { - afterEach(() => { - sinon.restore(); - }); - - it('should insert project funding source', async () => { - const mockQueryResponse = ({ - rowCount: 1, - rows: [{ project_funding_source_id: 2 }] - } as unknown) as QueryResult<{ - project_funding_source_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const data = new PutFundingSource({ - id: 1, - investment_action_category: 1, - agency_project_id: 'string', - funding_amount: 1, - start_date: 'string', - end_date: 'string', - revision_count: '1' - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - const response = await permitRepository.insertProjectFundingSource(data, 1); - - expect(response).to.eql({ project_funding_source_id: 2 }); - }); - - it('should throw an error insert failed', async () => { - const mockQueryResponse = ({} as unknown) as QueryResult<{ - project_funding_source_id: number; - }>; - - const mockDBConnection = getMockDBConnection({ - sql: sinon.stub().resolves(mockQueryResponse) - }); - - const data = new PutFundingSource({ - id: 1, - investment_action_category: 1, - agency_project_id: 'string', - funding_amount: 1, - start_date: 'string', - end_date: 'string', - revision_count: '1' - }); - - const permitRepository = new ProjectRepository(mockDBConnection); - - try { - await permitRepository.insertProjectFundingSource(data, 1); - expect.fail(); - } catch (error) { - expect((error as ApiError).message).to.equal('Failed to insert project funding source'); - } - }); - }); - describe('getProjectList', () => { it('should return result', async () => { const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; @@ -497,34 +235,6 @@ describe('ProjectRepository', () => { }); }); - describe('getFundingData', () => { - it('should return result', async () => { - const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; - const dbConnection = getMockDBConnection({ query: () => mockResponse }); - - const repository = new ProjectRepository(dbConnection); - - const response = await repository.getFundingData(1); - - expect(response).to.not.be.null; - expect(response).to.eql(new GetFundingData([{ id: 1 }])); - }); - - it('should throw an error', async () => { - const mockResponse = ({ rows: null, rowCount: 0 } as any) as Promise>; - const dbConnection = getMockDBConnection({ query: () => mockResponse }); - - const repository = new ProjectRepository(dbConnection); - - try { - await repository.getFundingData(1); - expect.fail(); - } catch (error) { - expect((error as Error).message).to.equal('Failed to get project funding data'); - } - }); - }); - describe('getIndigenousPartnershipsRows', () => { it('should return result', async () => { const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; @@ -732,50 +442,6 @@ describe('ProjectRepository', () => { }); }); - describe('insertFundingSource', () => { - it('should return result', async () => { - const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; - const dbConnection = getMockDBConnection({ query: () => mockResponse }); - - const repository = new ProjectRepository(dbConnection); - - const input = ({ - investment_action_category: 1, - agency_project_id: 1, - funding_amount: 123, - start_date: 'start', - end_date: 'end' - } as unknown) as PostFundingSource; - - const response = await repository.insertFundingSource(input, 1); - - expect(response).to.not.be.null; - expect(response).to.eql(1); - }); - - it('should throw an error', async () => { - const mockResponse = ({ rows: null, rowCount: 0 } as any) as Promise>; - const dbConnection = getMockDBConnection({ query: () => mockResponse }); - - const repository = new ProjectRepository(dbConnection); - - const input = ({ - investment_action_category: 1, - agency_project_id: 1, - funding_amount: 123, - start_date: 'start', - end_date: 'end' - } as unknown) as PostFundingSource; - - try { - await repository.insertFundingSource(input, 1); - expect.fail(); - } catch (error) { - expect((error as Error).message).to.equal('Failed to insert project funding data'); - } - }); - }); - describe('insertIndigenousNation', () => { it('should return result', async () => { const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; diff --git a/api/src/repositories/project-repository.ts b/api/src/repositories/project-repository.ts index f711bbffe1..d505d99ec5 100644 --- a/api/src/repositories/project-repository.ts +++ b/api/src/repositories/project-repository.ts @@ -1,18 +1,11 @@ import { isArray } from 'lodash'; import SQL, { SQLStatement } from 'sql-template-strings'; import { ApiExecuteSQLError } from '../errors/api-error'; -import { PostFundingSource, PostProjectObject } from '../models/project-create'; -import { - PutCoordinatorData, - PutFundingSource, - PutLocationData, - PutObjectivesData, - PutProjectData -} from '../models/project-update'; +import { PostProjectObject } from '../models/project-create'; +import { PutCoordinatorData, PutLocationData, PutObjectivesData, PutProjectData } from '../models/project-update'; import { GetAttachmentsData, GetCoordinatorData, - GetFundingData, GetIUCNClassificationData, GetLocationData, GetObjectivesData, @@ -32,157 +25,6 @@ import { BaseRepository } from './base-repository'; * @extends {BaseRepository} */ export class ProjectRepository extends BaseRepository { - async getProjectFundingSourceIds( - projectId: number - ): Promise< - { - project_funding_source_id: number; - }[] - > { - const sqlStatement = SQL` - SELECT - pfs.project_funding_source_id - FROM - project_funding_source pfs - WHERE - pfs.project_id = ${projectId}; - `; - - const response = await this.connection.sql<{ - project_funding_source_id: number; - }>(sqlStatement); - - const result = response?.rows; - - if (!result) { - throw new ApiExecuteSQLError('Failed to get project funding sources by Id', [ - 'ProjectRepository->getProjectFundingSourceIds', - 'rows was null or undefined, expected rows != null' - ]); - } - - return result; - } - - async deleteSurveyFundingSourceConnectionToProject(projectFundingSourceId: number) { - const sqlStatement: SQLStatement = SQL` - DELETE - from survey_funding_source sfs - WHERE - sfs.project_funding_source_id = ${projectFundingSourceId} - RETURNING survey_id;`; - - const response = await this.connection.sql(sqlStatement); - - const result = response?.rows; - - if (!result) { - throw new ApiExecuteSQLError('Failed to delete survey funding source by id', [ - 'ProjectRepository->deleteSurveyFundingSourceConnectionToProject', - 'rows was null or undefined, expected rows != null' - ]); - } - - return result; - } - - async deleteProjectFundingSource(projectFundingSourceId: number) { - const sqlStatement: SQLStatement = SQL` - DELETE - from project_funding_source - WHERE - project_funding_source_id = ${projectFundingSourceId}; - `; - - const response = await this.connection.sql(sqlStatement); - - const result = response?.rows; - - if (!result) { - throw new ApiExecuteSQLError('Failed to delete project funding source', [ - 'ProjectRepository->deleteProjectFundingSource', - 'rows was null or undefined, expected rows != null' - ]); - } - - return result; - } - - async updateProjectFundingSource( - fundingSource: PutFundingSource, - projectId: number - ): Promise<{ project_funding_source_id: number }> { - const sqlStatement: SQLStatement = SQL` - UPDATE - project_funding_source - SET - project_id = ${projectId}, - investment_action_category_id = ${fundingSource.investment_action_category}, - funding_source_project_id = ${fundingSource.agency_project_id}, - funding_amount = ${fundingSource.funding_amount}, - funding_start_date = ${fundingSource.start_date}, - funding_end_date = ${fundingSource.end_date}, - first_nations_id = ${fundingSource.first_nations_id} - WHERE - project_funding_source_id = ${fundingSource.id} - RETURNING - project_funding_source_id; - `; - - const response = await this.connection.sql<{ project_funding_source_id: number }>(sqlStatement); - - const result = response?.rows?.[0]; - - if (!result) { - throw new ApiExecuteSQLError('Failed to update project funding source', [ - 'ProjectRepository->putProjectFundingSource', - 'rows was null or undefined, expected rows != null' - ]); - } - - return result; - } - - async insertProjectFundingSource( - fundingSource: PutFundingSource, - projectId: number - ): Promise<{ project_funding_source_id: number }> { - const sqlStatement: SQLStatement = SQL` - INSERT INTO project_funding_source ( - project_id, - investment_action_category_id, - funding_source_project_id, - funding_amount, - funding_start_date, - funding_end_date, - first_nations_id - ) VALUES ( - ${projectId}, - ${fundingSource.investment_action_category}, - ${fundingSource.agency_project_id}, - ${fundingSource.funding_amount}, - ${fundingSource.start_date}, - ${fundingSource.end_date}, - ${fundingSource.first_nations_id} - ) - RETURNING - project_funding_source_id; - `; - - const response = await this.connection.sql<{ project_funding_source_id: number }>(sqlStatement); - - const result = response?.rows?.[0]; - - if (!result) { - throw new ApiExecuteSQLError('Failed to insert project funding source', [ - 'ProjectRepository->putProjectFundingSource', - 'rows was null or undefined, expected rows != null' - ]); - } - - return result; - } - async getProjectList( isUserAdmin: boolean, systemUserId: number | null, @@ -201,24 +43,18 @@ export class ProjectRepository extends BaseRepository { array_agg(distinct p2.program_id) as project_programs FROM project as p - LEFT JOIN project_program pp - ON p.project_id = pp.project_id - LEFT JOIN program p2 - ON p2.program_id = pp.program_id - LEFT OUTER JOIN project_funding_source as pfs - ON pfs.project_id = p.project_id - LEFT OUTER JOIN investment_action_category as iac - ON pfs.investment_action_category_id = iac.investment_action_category_id + LEFT JOIN project_program pp + ON p.project_id = pp.project_id + LEFT JOIN program p2 + ON p2.program_id = pp.program_id LEFT OUTER JOIN survey as s ON s.project_id = p.project_id LEFT OUTER JOIN study_species as sp ON sp.survey_id = s.survey_id - LEFT JOIN project_region pr + LEFT JOIN project_region pr ON p.project_id = pr.project_id - LEFT JOIN region_lookup rl + LEFT JOIN region_lookup rl ON pr.region_id = rl.region_id - LEFT OUTER JOIN agency as a - ON iac.agency_id = a.agency_id WHERE 1 = 1 `; @@ -258,10 +94,6 @@ export class ProjectRepository extends BaseRepository { sqlStatement.append(SQL` AND p.name = ${filterFields.project_name}`); } - if (filterFields.agency_project_id) { - sqlStatement.append(SQL` AND pfs.funding_source_project_id = ${filterFields.agency_project_id}`); - } - if (filterFields.agency_id) { sqlStatement.append(SQL` AND a.agency_id = ${filterFields.agency_id}`); } @@ -290,9 +122,9 @@ export class ProjectRepository extends BaseRepository { p.revision_count `); - /* + /* this is placed after the `group by` to take advantage of the `HAVING` clause - by placing the filter in the HAVING clause we are able to properly search + by placing the filter in the HAVING clause we are able to properly search on program ids while still returning the full list that is associated to the project */ if (filterFields.project_programs) { @@ -350,19 +182,19 @@ export class ProjectRepository extends BaseRepository { pp.project_programs, pa.project_types FROM - project p + project p LEFT JOIN ( - SELECT array_remove(array_agg(p.program_id), NULL) as project_programs, pp.project_id - FROM program p, project_program pp - WHERE p.program_id = pp.program_id + SELECT array_remove(array_agg(p.program_id), NULL) as project_programs, pp.project_id + FROM program p, project_program pp + WHERE p.program_id = pp.program_id GROUP BY pp.project_id ) as pp on pp.project_id = p.project_id LEFT JOIN ( SELECT array_remove(array_agg(pt.type_id), NULL) as project_types, p.project_id - FROM project p + FROM project p LEFT JOIN project_type pt on p.project_id = pt.project_id GROUP BY p.project_id - ) as pa on pa.project_id = p.project_id + ) as pa on pa.project_id = p.project_id WHERE p.project_id = ${projectId}; `; @@ -503,65 +335,6 @@ export class ProjectRepository extends BaseRepository { return new GetIUCNClassificationData(result); } - async getFundingData(projectId: number): Promise { - const sqlStatement = SQL` - SELECT - pfs.project_funding_source_id as id, - a.agency_id, - pfs.funding_amount::numeric::int, - pfs.funding_start_date as start_date, - pfs.funding_end_date as end_date, - iac.investment_action_category_id as investment_action_category, - iac.name as investment_action_category_name, - a.name as agency_name, - pfs.funding_source_project_id as agency_project_id, - pfs.revision_count as revision_count, - pfs.first_nations_id, - fn.name as first_nations_name - FROM - project_funding_source as pfs - LEFT OUTER JOIN - investment_action_category as iac - ON - pfs.investment_action_category_id = iac.investment_action_category_id - LEFT OUTER JOIN - agency as a - ON - iac.agency_id = a.agency_id - LEFT OUTER JOIN - first_nations as fn - ON - fn.first_nations_id = pfs.first_nations_id - WHERE - pfs.project_id = ${projectId} - GROUP BY - pfs.project_funding_source_id, - a.agency_id, - pfs.funding_source_project_id, - pfs.funding_amount, - pfs.funding_start_date, - pfs.funding_end_date, - iac.investment_action_category_id, - iac.name, - a.name, - pfs.revision_count, - pfs.first_nations_id, - fn.name - `; - const response = await this.connection.query(sqlStatement.text, sqlStatement.values); - - const result = response?.rows; - - if (!result) { - throw new ApiExecuteSQLError('Failed to get project funding data', [ - 'ProjectRepository->getFundingData', - 'rows was null or undefined, expected rows != null' - ]); - } - - return new GetFundingData(result); - } - async getIndigenousPartnershipsRows(projectId: number): Promise { const sqlStatement = SQL` SELECT @@ -746,41 +519,6 @@ export class ProjectRepository extends BaseRepository { return result.id; } - async insertFundingSource(fundingSource: PostFundingSource, project_id: number): Promise { - const sqlStatement = SQL` - INSERT INTO project_funding_source ( - project_id, - investment_action_category_id, - funding_source_project_id, - funding_amount, - funding_start_date, - funding_end_date, - first_nations_id - ) VALUES ( - ${project_id}, - ${fundingSource.investment_action_category}, - ${fundingSource.agency_project_id}, - ${fundingSource.funding_amount}, - ${fundingSource.start_date}, - ${fundingSource.end_date}, - ${fundingSource.first_nations_id} - ) - RETURNING - project_funding_source_id as id; - `; - - const response = await this.connection.query(sqlStatement.text, sqlStatement.values); - - const result = response?.rows?.[0]; - if (!result?.id) { - throw new ApiExecuteSQLError('Failed to insert project funding data', [ - 'ProjectRepository->insertFundingSource', - 'rows was null or undefined, expected rows != null' - ]); - } - return result.id; - } - async insertIndigenousNation(indigenousNationsId: number, project_id: number): Promise { const sqlStatement = SQL` INSERT INTO project_first_nation ( diff --git a/api/src/repositories/survey-repository.test.ts b/api/src/repositories/survey-repository.test.ts index 98bba75101..d1734f1146 100644 --- a/api/src/repositories/survey-repository.test.ts +++ b/api/src/repositories/survey-repository.test.ts @@ -9,7 +9,6 @@ import { PutSurveyObject } from '../models/survey-update'; import { GetAttachmentsData, GetSurveyData, - GetSurveyFundingSources, GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData @@ -148,33 +147,6 @@ describe('SurveyRepository', () => { }); }); - describe('getSurveyFundingSourcesData', () => { - it('should return result', async () => { - const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; - const dbConnection = getMockDBConnection({ sql: () => mockResponse }); - - const repository = new SurveyRepository(dbConnection); - - const response = await repository.getSurveyFundingSourcesData(1); - - expect(response).to.eql(new GetSurveyFundingSources([{ id: 1 }])); - }); - - it('should throw an error', async () => { - const mockResponse = (undefined as any) as Promise>; - const dbConnection = getMockDBConnection({ sql: () => mockResponse }); - - const repository = new SurveyRepository(dbConnection); - - try { - await repository.getSurveyFundingSourcesData(1); - expect.fail(); - } catch (error) { - expect((error as Error).message).to.equal('Failed to get survey funding sources data'); - } - }); - }); - describe('getSurveyProprietorDataForView', () => { it('should return result', async () => { const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; @@ -626,19 +598,6 @@ describe('SurveyRepository', () => { }); }); - describe('insertSurveyFundingSource', () => { - it('should return result', async () => { - const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; - const dbConnection = getMockDBConnection({ sql: () => mockResponse }); - - const repository = new SurveyRepository(dbConnection); - - const response = await repository.insertSurveyFundingSource(1, 1); - - expect(response).to.eql(undefined); - }); - }); - describe('deleteSurveySpeciesData', () => { it('should return result', async () => { const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; @@ -665,19 +624,6 @@ describe('SurveyRepository', () => { }); }); - describe('deleteSurveyFundingSourcesData', () => { - it('should return result', async () => { - const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; - const dbConnection = getMockDBConnection({ sql: () => mockResponse }); - - const repository = new SurveyRepository(dbConnection); - - const response = await repository.deleteSurveyFundingSourcesData(1); - - expect(response).to.eql(undefined); - }); - }); - describe('deleteSurveyProprietorData', () => { it('should return result', async () => { const mockResponse = ({ rows: [{ id: 1 }], rowCount: 1 } as any) as Promise>; diff --git a/api/src/repositories/survey-repository.ts b/api/src/repositories/survey-repository.ts index 402fbe0aae..3e6f812503 100644 --- a/api/src/repositories/survey-repository.ts +++ b/api/src/repositories/survey-repository.ts @@ -8,7 +8,6 @@ import { GetAttachmentsData, GetReportAttachmentsData, GetSurveyData, - GetSurveyFundingSources, GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData @@ -226,51 +225,6 @@ export class SurveyRepository extends BaseRepository { return new GetSurveyPurposeAndMethodologyData(result); } - /** - * Get Survey and Funding Source data for a given surveyId - * - * @param {number} surveyId - * @returns {*} Promise - * @memberof SurveyRepository - */ - async getSurveyFundingSourcesData(surveyId: number): Promise { - const sqlStatement = SQL` - - SELECT - sfs.project_funding_source_id, - a.agency_id, - pfs.funding_source_project_id, - pfs.funding_amount::numeric::int, - pfs.funding_start_date, - pfs.funding_end_date, - iac.investment_action_category_id, - iac.name as investment_action_category_name, - a.name as agency_name, - pfs.first_nations_id as first_nations_id, - fn."name" as first_nations_name - FROM survey_funding_source sfs - LEFT JOIN project_funding_source pfs ON sfs.project_funding_source_id = pfs.project_funding_source_id - LEFT JOIN investment_action_category iac ON pfs.investment_action_category_id = iac.investment_action_category_id - LEFT JOIN agency a ON iac.agency_id = a.agency_id - LEFT JOIN first_nations fn ON pfs.first_nations_id = fn.first_nations_id - WHERE sfs.survey_id = ${surveyId} - ORDER BY pfs.funding_start_date ; - `; - - const response = await this.connection.sql(sqlStatement); - - const result = response?.rows; - - if (!result) { - throw new ApiExecuteSQLError('Failed to get survey funding sources data', [ - 'SurveyRepository->getSurveyFundingSourcesData', - 'response was null or undefined, expected response != null' - ]); - } - - return new GetSurveyFundingSources(result); - } - /** * Get a Survey Proprietor for a given survey ID * @@ -898,27 +852,6 @@ export class SurveyRepository extends BaseRepository { } } - /** - * Links a Survey and a Funding source together - * - * @param {number} project_funding_source_id - * @param {number} surveyId - * @returns {*} Promise - * @memberof SurveyRepository - */ - async insertSurveyFundingSource(project_funding_source_id: number, surveyId: number) { - const sqlStatement = SQL` - INSERT INTO survey_funding_source ( - survey_id, - project_funding_source_id - ) VALUES ( - ${surveyId}, - ${project_funding_source_id} - ); - `; - await this.connection.query(sqlStatement.text, sqlStatement.values); - } - /** * Updates Survey details * @@ -1036,24 +969,6 @@ export class SurveyRepository extends BaseRepository { await this.connection.sql(sqlStatement); } - /** - * Deletes Survey vantage codes for a given survey ID - * - * @param {number} surveyId - * @returns {*} Promise - * @memberof SurveyRepository - */ - async deleteSurveyFundingSourcesData(surveyId: number) { - const sqlStatement = SQL` - DELETE - from survey_funding_source - WHERE - survey_id = ${surveyId}; - `; - - await this.connection.sql(sqlStatement); - } - /** * Deletes Survey proprietor data for a given survey ID * diff --git a/api/src/services/eml-service.test.ts b/api/src/services/eml-service.test.ts index 020d14ee94..5925f75440 100644 --- a/api/src/services/eml-service.test.ts +++ b/api/src/services/eml-service.test.ts @@ -122,24 +122,6 @@ describe('EmlPackage', () => { } ]); - sinon.stub(EmlService.prototype, '_getProjectFundingSources').returns({ - funding: { - section: [ - { - title: 'Agency Name', - para: 'BC Hydro', - section: [ - { title: 'Funding Agency Project ID', para: 'AGENCY PROJECT ID' }, - { title: 'Investment Action/Category', para: 'Not Applicable' }, - { title: 'Funding Amount', para: 123456789 }, - { title: 'Funding Start Date', para: '2023-01-02' }, - { title: 'Funding End Date', para: '2023-01-30' } - ] - } - ] - } - }); - sinon.stub(EmlService.prototype, '_getProjectGeographicCoverage').returns({ geographicCoverage: { geographicDescription: 'Location Description', @@ -191,21 +173,6 @@ describe('EmlPackage', () => { abstract: { section: [{ title: 'Objectives', para: 'Project objectives.' }] }, - funding: { - section: [ - { - title: 'Agency Name', - para: 'BC Hydro', - section: [ - { title: 'Funding Agency Project ID', para: 'AGENCY PROJECT ID' }, - { title: 'Investment Action/Category', para: 'Not Applicable' }, - { title: 'Funding Amount', para: 123456789 }, - { title: 'Funding Start Date', para: '2023-01-02' }, - { title: 'Funding End Date', para: '2023-01-30' } - ] - } - ] - }, studyAreaDescription: { coverage: { geographicCoverage: { @@ -476,21 +443,6 @@ describe('EmlService', () => { abstract: { section: [{ title: 'Objectives', para: 'Objectives' }] }, - funding: { - section: [ - { - title: 'Agency Name', - para: 'BC Hydro', - section: [ - { title: 'Funding Agency Project ID', para: 'AGENCY PROJECT ID' }, - { title: 'Investment Action/Category', para: 'Not Applicable' }, - { title: 'Funding Amount', para: 123456789 }, - { title: 'Funding Start Date', para: '2023-01-02' }, - { title: 'Funding End Date', para: '2023-01-30' } - ] - } - ] - }, studyAreaDescription: { coverage: { geographicCoverage: { @@ -605,36 +557,6 @@ describe('EmlService', () => { } ] }, - funding: { - section: [ - { - title: 'Agency Name', - para: 'BC Hydro', - section: [ - { - title: 'Funding Agency Project ID', - para: 'AGENCY PROJECT ID' - }, - { - title: 'Investment Action/Category', - para: 'Not Applicable' - }, - { - title: 'Funding Amount', - para: 123456789 - }, - { - title: 'Funding Start Date', - para: '2023-01-02' - }, - { - title: 'Funding End Date', - para: '2023-01-30' - } - ] - } - ] - }, studyAreaDescription: { coverage: { geographicCoverage: { @@ -816,36 +738,6 @@ describe('EmlService', () => { } ] }, - funding: { - section: [ - { - title: 'Agency Name', - para: 'BC Hydro', - section: [ - { - title: 'Funding Agency Project ID', - para: 'AGENCY PROJECT ID' - }, - { - title: 'Investment Action/Category', - para: 'Not Applicable' - }, - { - title: 'Funding Amount', - para: 123456789 - }, - { - title: 'Funding Start Date', - para: '2023-01-02' - }, - { - title: 'Funding End Date', - para: '2023-01-30' - } - ] - } - ] - }, studyAreaDescription: { coverage: { geographicCoverage: { @@ -1017,36 +909,6 @@ describe('EmlService', () => { } ] }, - funding: { - section: [ - { - title: 'Agency Name', - para: 'BC Hydro', - section: [ - { - title: 'Funding Agency Project ID', - para: 'AGENCY PROJECT ID' - }, - { - title: 'Investment Action/Category', - para: 'Not Applicable' - }, - { - title: 'Funding Amount', - para: 123456789 - }, - { - title: 'Funding Start Date', - para: '2023-01-02' - }, - { - title: 'Funding End Date', - para: '2023-01-30' - } - ] - } - ] - }, studyAreaDescription: { coverage: { geographicCoverage: { @@ -1365,24 +1227,6 @@ describe('EmlService', () => { } ]); - sinon.stub(EmlService.prototype, '_getProjectFundingSources').returns({ - funding: { - section: [ - { - title: 'Agency Name', - para: 'BC Hydro', - section: [ - { title: 'Funding Agency Project ID', para: 'AGENCY PROJECT ID' }, - { title: 'Investment Action/Category', para: 'Not Applicable' }, - { title: 'Funding Amount', para: 123456789 }, - { title: 'Funding Start Date', para: '2023-01-02' }, - { title: 'Funding End Date', para: '2023-01-30' } - ] - } - ] - } - }); - sinon.stub(EmlService.prototype, '_getProjectGeographicCoverage').returns({ geographicCoverage: { geographicDescription: 'Location Description', @@ -1433,21 +1277,6 @@ describe('EmlService', () => { abstract: { section: [{ title: 'Objectives', para: 'Project objectives.' }] }, - funding: { - section: [ - { - title: 'Agency Name', - para: 'BC Hydro', - section: [ - { title: 'Funding Agency Project ID', para: 'AGENCY PROJECT ID' }, - { title: 'Investment Action/Category', para: 'Not Applicable' }, - { title: 'Funding Amount', para: 123456789 }, - { title: 'Funding Start Date', para: '2023-01-02' }, - { title: 'Funding End Date', para: '2023-01-30' } - ] - } - ] - }, studyAreaDescription: { coverage: { geographicCoverage: { @@ -1508,8 +1337,6 @@ describe('EmlService', () => { } ]); - sinon.stub(EmlService.prototype, '_getProjectFundingSources').returns({}); - sinon.stub(EmlService.prototype, '_getProjectGeographicCoverage').returns({}); sinon.stub(EmlService.prototype, '_getProjectTemporalCoverage').returns({ @@ -1721,14 +1548,6 @@ describe('EmlService', () => { }); }); - describe('_getProjectFundingSources', () => { - // - }); - - describe('_getSurveyFundingSources', () => { - // - }); - describe('_getProjectTemporalCoverage', () => { // }); diff --git a/api/src/services/eml-service.ts b/api/src/services/eml-service.ts index 794834bd94..b8e439af42 100644 --- a/api/src/services/eml-service.ts +++ b/api/src/services/eml-service.ts @@ -502,7 +502,6 @@ export class EmlService extends DBService { abstract: { section: [{ title: 'Objectives', para: projectData.objectives.objectives }] }, - ...this._getProjectFundingSources(projectData), studyAreaDescription: { coverage: { ...this._getProjectGeographicCoverage(projectData), @@ -751,68 +750,6 @@ export class EmlService extends DBService { ]; } - /** - * Creates an object representing all funding sources for the given project. - * - * @param {IGetProject} projectData - * @return {*} {Record} - * @memberof EmlService - */ - _getProjectFundingSources(projectData: IGetProject): Record { - if (!projectData.funding.fundingSources.length) { - return {}; - } - - return { - funding: { - section: projectData.funding.fundingSources.map((fundingSource) => { - return { - title: 'Agency Name', - para: fundingSource.agency_name ?? fundingSource.first_nations_name, - section: [ - { title: 'Funding Agency Project ID', para: fundingSource.agency_project_id }, - { title: 'Investment Action/Category', para: fundingSource.investment_action_category_name }, - { title: 'Funding Amount', para: fundingSource.funding_amount }, - { title: 'Funding Start Date', para: this._makeEmlDateString(fundingSource.start_date) }, - { title: 'Funding End Date', para: this._makeEmlDateString(fundingSource.end_date) } - ] - }; - }) - } - }; - } - - /** - * Creates an object representing all funding sources for the given survey. - * - * @param {SurveyObject} surveyData - * @return {*} {Record} - * @memberof EmlService - */ - _getSurveyFundingSources(surveyData: SurveyObject): Record { - if (!surveyData.funding.funding_sources.length) { - return {}; - } - - return { - funding: { - section: surveyData.funding.funding_sources.map((fundingSource) => { - return { - title: 'Agency Name', - para: fundingSource.agency_name ?? fundingSource.first_nations_name, - section: [ - { title: 'Funding Agency Project ID', para: fundingSource.funding_source_project_id }, - { title: 'Investment Action/Category', para: fundingSource.investment_action_category_name }, - { title: 'Funding Amount', para: fundingSource.funding_amount }, - { title: 'Funding Start Date', para: this._makeEmlDateString(fundingSource.funding_start_date) }, - { title: 'Funding End Date', para: this._makeEmlDateString(fundingSource.funding_end_date) } - ] - }; - }) - } - }; - } - /** * Creates an object representing temporal coverage for the given project * @@ -1088,7 +1025,6 @@ export class EmlService extends DBService { } ] }, - ...this._getSurveyFundingSources(surveyData), studyAreaDescription: { coverage: { ...this._getSurveyGeographicCoverage(surveyData), diff --git a/api/src/services/project-service.test.ts b/api/src/services/project-service.test.ts index a3d95d8ae8..2be732f4b1 100644 --- a/api/src/services/project-service.test.ts +++ b/api/src/services/project-service.test.ts @@ -5,7 +5,6 @@ import sinonChai from 'sinon-chai'; import { PostProjectObject } from '../models/project-create'; import { GetCoordinatorData, - GetFundingData, GetIUCNClassificationData, GetLocationData, GetObjectivesData, @@ -344,22 +343,6 @@ describe('getIUCNClassificationData', () => { }); }); -describe('getFundingData', () => { - it('returns the first row on success', async () => { - const dbConnection = getMockDBConnection(); - const service = new ProjectService(dbConnection); - - const data = new GetFundingData([{ id: 1 }]); - - const repoStub = sinon.stub(ProjectRepository.prototype, 'getFundingData').resolves(data); - - const response = await service.getFundingData(1); - - expect(repoStub).to.be.calledOnce; - expect(response).to.eql(data); - }); -}); - describe('getPartnershipsData', () => { it('returns the first row on success', async () => { const dbConnection = getMockDBConnection(); diff --git a/api/src/services/project-service.ts b/api/src/services/project-service.ts index 198b550ddc..96324223ee 100644 --- a/api/src/services/project-service.ts +++ b/api/src/services/project-service.ts @@ -4,11 +4,10 @@ import { PROJECT_ROLE } from '../constants/roles'; import { COMPLETION_STATUS } from '../constants/status'; import { IDBConnection } from '../database/db'; import { HTTP400 } from '../errors/http-error'; -import { IPostIUCN, PostFundingSource, PostProjectObject } from '../models/project-create'; +import { IPostIUCN, PostProjectObject } from '../models/project-create'; import { IPutIUCN, PutCoordinatorData, - PutFundingData, PutIUCNData, PutLocationData, PutObjectivesData, @@ -18,7 +17,6 @@ import { import { GetAttachmentsData, GetCoordinatorData, - GetFundingData, GetIUCNClassificationData, GetLocationData, GetObjectivesData, @@ -151,21 +149,12 @@ export class ProjectService extends DBService { } async getProjectById(projectId: number): Promise { - const [ - projectData, - objectiveData, - coordinatorData, - locationData, - iucnData, - fundingData, - partnershipsData - ] = await Promise.all([ + const [projectData, objectiveData, coordinatorData, locationData, iucnData, partnershipsData] = await Promise.all([ this.getProjectData(projectId), this.getObjectivesData(projectId), this.getCoordinatorData(projectId), this.getLocationData(projectId), this.getIUCNClassificationData(projectId), - this.getFundingData(projectId), this.getPartnershipsData(projectId) ]); @@ -175,7 +164,6 @@ export class ProjectService extends DBService { coordinator: coordinatorData, location: locationData, iucn: iucnData, - funding: fundingData, partnerships: partnershipsData }; } @@ -200,7 +188,6 @@ export class ProjectService extends DBService { objectives: undefined, location: undefined, iucn: undefined, - funding: undefined, partnerships: undefined }; @@ -253,13 +240,6 @@ export class ProjectService extends DBService { }) ); } - if (entities.includes(GET_ENTITIES.funding)) { - promises.push( - this.getFundingData(projectId).then((value) => { - results.funding = value; - }) - ); - } await Promise.all(promises); @@ -286,10 +266,6 @@ export class ProjectService extends DBService { return this.projectRepository.getIUCNClassificationData(projectId); } - async getFundingData(projectId: number): Promise { - return this.projectRepository.getFundingData(projectId); - } - async getPartnershipsData(projectId: number): Promise { const [indigenousPartnershipsRows, stakegholderPartnershipsRows] = await Promise.all([ this.projectRepository.getIndigenousPartnershipsRows(projectId), @@ -346,15 +322,6 @@ export class ProjectService extends DBService { const promises: Promise[] = []; - // Handle funding sources - promises.push( - Promise.all( - postProjectData.funding.fundingSources.map((fundingSource: PostFundingSource) => - this.insertFundingSource(fundingSource, projectId) - ) - ) - ); - // Handle indigenous partners promises.push( Promise.all( @@ -405,10 +372,6 @@ export class ProjectService extends DBService { return this.projectRepository.insertProject(postProjectData); } - async insertFundingSource(fundingSource: PostFundingSource, project_id: number): Promise { - return this.projectRepository.insertFundingSource(fundingSource, project_id); - } - async insertIndigenousNation(indigenousNationsId: number, project_id: number): Promise { return this.projectRepository.insertIndigenousNation(indigenousNationsId, project_id); } @@ -488,10 +451,6 @@ export class ProjectService extends DBService { promises.push(this.updateIUCNData(projectId, entities)); } - if (entities?.funding) { - promises.push(this.updateFundingData(projectId, entities)); - } - if (entities?.location) { promises.push(this.insertRegion(projectId, entities.location.geometry)); } @@ -576,66 +535,6 @@ export class ProjectService extends DBService { await Promise.all([...insertTypePromises]); } - /** - * Compares incoming project funding data against the existing funding data, if any, and determines which need to be - * deleted, added, or updated. - * - * @param {number} projectId - * @param {IUpdateProject} entities - * @return {*} {Promise} - * @memberof ProjectService - */ - async updateFundingData(projectId: number, entities: IUpdateProject): Promise { - const projectRepository = new ProjectRepository(this.connection); - - const putFundingData = entities?.funding && new PutFundingData(entities.funding); - if (!putFundingData) { - throw new HTTP400('Failed to create funding data object'); - } - // Get any existing funding for this project - const existingProjectFundingSources = await projectRepository.getProjectFundingSourceIds(projectId); - - // Compare the array of existing funding to the array of incoming funding (by project_funding_source_id) and collect any - // existing funding that are not in the incoming funding array. - const existingFundingSourcesToDelete = existingProjectFundingSources.filter((existingFunding) => { - // Find all existing funding (by project_funding_source_id) that have no matching incoming project_funding_source_id - return !putFundingData.fundingSources.find( - (incomingFunding: any) => incomingFunding.id === existingFunding.project_funding_source_id - ); - }); - - // Delete from the database all existing project and survey funding that have been removed - if (existingFundingSourcesToDelete.length) { - const promises: Promise[] = []; - - existingFundingSourcesToDelete.forEach((funding) => { - // Delete funding connection to survey first - promises.push( - projectRepository.deleteSurveyFundingSourceConnectionToProject(funding.project_funding_source_id) - ); - // Delete project funding after - promises.push(projectRepository.deleteProjectFundingSource(funding.project_funding_source_id)); - }); - - await Promise.all(promises); - } - - // The remaining funding are either new, and can be created, or updates to existing funding - const promises: Promise[] = []; - - putFundingData.fundingSources.forEach((funding: any) => { - if (funding.id) { - // Has a project_funding_source_id, indicating this is an update to an existing funding - promises.push(projectRepository.updateProjectFundingSource(funding, projectId)); - } else { - // No project_funding_source_id, indicating this is a new funding which needs to be created - promises.push(projectRepository.insertProjectFundingSource(funding, projectId)); - } - }); - - await Promise.all(promises); - } - async deleteProject(projectId: number): Promise { /** * PART 1 diff --git a/api/src/services/survey-service.test.ts b/api/src/services/survey-service.test.ts index 621f895bb3..70848cc1cb 100644 --- a/api/src/services/survey-service.test.ts +++ b/api/src/services/survey-service.test.ts @@ -13,7 +13,6 @@ import { GetAttachmentsData, GetFocalSpeciesData, GetSurveyData, - GetSurveyFundingSources, GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData, @@ -60,9 +59,6 @@ describe('SurveyService', () => { const getPermitDataStub = sinon .stub(SurveyService.prototype, 'getPermitData') .resolves(({ data: 'permitData' } as unknown) as any); - const getSurveyFundingSourcesDataStub = sinon - .stub(SurveyService.prototype, 'getSurveyFundingSourcesData') - .resolves(({ data: 'fundingData' } as unknown) as any); const getSurveyPurposeAndMethodologyStub = sinon .stub(SurveyService.prototype, 'getSurveyPurposeAndMethodology') .resolves(({ data: 'purposeAndMethodologyData' } as unknown) as any); @@ -78,7 +74,6 @@ describe('SurveyService', () => { expect(getSurveyDataStub).to.be.calledOnce; expect(getSpeciesDataStub).to.be.calledOnce; expect(getPermitDataStub).to.be.calledOnce; - expect(getSurveyFundingSourcesDataStub).to.be.calledOnce; expect(getSurveyPurposeAndMethodologyStub).to.be.calledOnce; expect(getSurveyProprietorDataForViewStub).to.be.calledOnce; expect(getSurveyLocationDataStub).to.be.calledOnce; @@ -88,7 +83,6 @@ describe('SurveyService', () => { species: { data: 'speciesData' }, permit: { data: 'permitData' }, purpose_and_methodology: { data: 'purposeAndMethodologyData' }, - funding: { data: 'fundingData' }, proprietor: { data: 'proprietorData' }, location: { data: 'locationData' } }); @@ -109,7 +103,6 @@ describe('SurveyService', () => { .resolves(); const updateSurveySpeciesDataStub = sinon.stub(SurveyService.prototype, 'updateSurveySpeciesData').resolves(); const updateSurveyPermitDataStub = sinon.stub(SurveyService.prototype, 'updateSurveyPermitData').resolves(); - const updateSurveyFundingDataStub = sinon.stub(SurveyService.prototype, 'updateSurveyFundingData').resolves(); const updateSurveyProprietorDataStub = sinon .stub(SurveyService.prototype, 'updateSurveyProprietorData') .resolves(); @@ -125,7 +118,6 @@ describe('SurveyService', () => { expect(updateSurveyVantageCodesDataStub).not.to.have.been.called; expect(updateSurveySpeciesDataStub).not.to.have.been.called; expect(updateSurveyPermitDataStub).not.to.have.been.called; - expect(updateSurveyFundingDataStub).not.to.have.been.called; expect(updateSurveyProprietorDataStub).not.to.have.been.called; }); @@ -138,7 +130,6 @@ describe('SurveyService', () => { .resolves(); const updateSurveySpeciesDataStub = sinon.stub(SurveyService.prototype, 'updateSurveySpeciesData').resolves(); const updateSurveyPermitDataStub = sinon.stub(SurveyService.prototype, 'updateSurveyPermitData').resolves(); - const updateSurveyFundingDataStub = sinon.stub(SurveyService.prototype, 'updateSurveyFundingData').resolves(); const updateSurveyProprietorDataStub = sinon .stub(SurveyService.prototype, 'updateSurveyProprietorData') .resolves(); @@ -151,7 +142,6 @@ describe('SurveyService', () => { survey_details: {}, species: {}, permit: {}, - funding: {}, proprietor: {}, purpose_and_methodology: {}, location: {} @@ -163,7 +153,6 @@ describe('SurveyService', () => { expect(updateSurveyVantageCodesDataStub).to.have.been.calledOnce; expect(updateSurveySpeciesDataStub).to.have.been.calledOnce; expect(updateSurveyPermitDataStub).to.have.been.calledOnce; - expect(updateSurveyFundingDataStub).to.have.been.calledOnce; expect(updateSurveyProprietorDataStub).to.have.been.calledOnce; expect(updateSurveyRegionStub).to.have.been.calledOnce; }); @@ -331,22 +320,6 @@ describe('SurveyService', () => { }); }); - describe('getSurveyFundingSourcesData', () => { - it('returns the first row on success', async () => { - const dbConnection = getMockDBConnection(); - const service = new SurveyService(dbConnection); - - const data = new GetSurveyFundingSources([{ id: 1 }]); - - const repoStub = sinon.stub(SurveyRepository.prototype, 'getSurveyFundingSourcesData').resolves(data); - - const response = await service.getSurveyFundingSourcesData(1); - - expect(repoStub).to.be.calledOnce; - expect(response).to.eql(data); - }); - }); - describe('getSurveyProprietorDataForView', () => { it('returns the first row on success', async () => { const dbConnection = getMockDBConnection(); @@ -608,20 +581,6 @@ describe('SurveyService', () => { }); }); - describe('insertSurveyFundingSource', () => { - it('returns the first row on success', async () => { - const dbConnection = getMockDBConnection(); - const service = new SurveyService(dbConnection); - - const repoStub = sinon.stub(SurveyRepository.prototype, 'insertSurveyFundingSource').resolves(); - - const response = await service.insertSurveyFundingSource(1, 1); - - expect(repoStub).to.be.calledOnce; - expect(response).to.eql(undefined); - }); - }); - describe('updateSurveyDetailsData', () => { afterEach(() => { sinon.restore(); @@ -795,47 +754,6 @@ describe('SurveyService', () => { }); }); - describe('updateSurveyFundingData', () => { - afterEach(() => { - sinon.restore(); - }); - - it('returns data if response is not null', async () => { - sinon.stub(SurveyService.prototype, 'deleteSurveyFundingSourcesData').resolves(undefined); - sinon.stub(SurveyService.prototype, 'insertSurveyFundingSource').resolves(undefined); - - const mockQueryResponse = (undefined as unknown) as QueryResult; - - const mockDBConnection = getMockDBConnection({ sql: async () => mockQueryResponse }); - const surveyService = new SurveyService(mockDBConnection); - - const response = await surveyService.updateSurveyFundingData(1, ({ - permit: { permit_number: '1', permit_type: 'type' }, - funding: { funding_sources: [1] } - } as unknown) as PutSurveyObject); - - expect(response).to.eql([undefined]); - }); - }); - - describe('deleteSurveyFundingSourcesData', () => { - afterEach(() => { - sinon.restore(); - }); - - it('returns the first row on success', async () => { - const dbConnection = getMockDBConnection(); - const service = new SurveyService(dbConnection); - - const repoStub = sinon.stub(SurveyRepository.prototype, 'deleteSurveyFundingSourcesData').resolves(); - - const response = await service.deleteSurveyFundingSourcesData(1); - - expect(repoStub).to.be.calledOnce; - expect(response).to.eql(undefined); - }); - }); - describe('updateSurveyProprietorData', () => { afterEach(() => { sinon.restore(); @@ -901,7 +819,6 @@ describe('SurveyService', () => { const response = await surveyService.updateSurveyVantageCodesData(1, ({ permit: { permit_number: '1', permit_type: 'type' }, - funding: { funding_sources: [1] }, purpose_and_methodology: { vantage_code_ids: undefined } } as unknown) as PutSurveyObject); @@ -919,7 +836,6 @@ describe('SurveyService', () => { const response = await surveyService.updateSurveyVantageCodesData(1, ({ permit: { permit_number: '1', permit_type: 'type' }, - funding: { funding_sources: [1] }, proprietor: { survey_data_proprietary: 'asd' }, purpose_and_methodology: { vantage_code_ids: [1] } } as unknown) as PutSurveyObject); diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index 361320b1a9..74aea87abd 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -10,7 +10,6 @@ import { GetPermitData, GetReportAttachmentsData, GetSurveyData, - GetSurveyFundingSources, GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData, @@ -82,7 +81,6 @@ export class SurveyService extends DBService { surveyData, speciesData, permitData, - fundingData, purposeAndMethodologyData, proprietorData, locationData @@ -90,7 +88,6 @@ export class SurveyService extends DBService { this.getSurveyData(surveyId), this.getSpeciesData(surveyId), this.getPermitData(surveyId), - this.getSurveyFundingSourcesData(surveyId), this.getSurveyPurposeAndMethodology(surveyId), this.getSurveyProprietorDataForView(surveyId), this.getSurveyLocationData(surveyId) @@ -101,7 +98,6 @@ export class SurveyService extends DBService { species: speciesData, permit: permitData, purpose_and_methodology: purposeAndMethodologyData, - funding: fundingData, proprietor: proprietorData, location: locationData }; @@ -178,17 +174,6 @@ export class SurveyService extends DBService { return this.surveyRepository.getSurveyPurposeAndMethodology(surveyId); } - /** - * Get Survey funding sources for a given survey ID - * - * @param {number} surveyID - * @returns {*} {Promise} - * @memberof SurveyService - */ - async getSurveyFundingSourcesData(surveyId: number): Promise { - return this.surveyRepository.getSurveyFundingSourcesData(surveyId); - } - /** * Gets proprietor data for view or null for a given survey ID * @@ -379,15 +364,6 @@ export class SurveyService extends DBService { ) ); - // Handle inserting any funding sources associated to this survey - promises.push( - Promise.all( - postSurveyData.funding.funding_sources.map((project_funding_source_id: number) => - this.insertSurveyFundingSource(project_funding_source_id, surveyId) - ) - ) - ); - // Handle survey proprietor data postSurveyData.proprietor && promises.push(this.insertSurveyProprietor(postSurveyData.proprietor, surveyId)); @@ -522,18 +498,6 @@ export class SurveyService extends DBService { } } - /** - * Inserts new record and associates funding source to a survey - * - * @param {number} project_funding_source_id - * @param {number} surveyID - * @returns {*} {Promise} - * @memberof SurveyService - */ - async insertSurveyFundingSource(project_funding_source_id: number, surveyId: number) { - return this.surveyRepository.insertSurveyFundingSource(project_funding_source_id, surveyId); - } - /** * Updates provided survey information and submits affected metadata to BioHub * @@ -590,10 +554,6 @@ export class SurveyService extends DBService { promises.push(this.updateSurveyPermitData(surveyId, putSurveyData)); } - if (putSurveyData?.funding) { - promises.push(this.updateSurveyFundingData(surveyId, putSurveyData)); - } - if (putSurveyData?.proprietor) { promises.push(this.updateSurveyProprietorData(surveyId, putSurveyData)); } @@ -713,36 +673,6 @@ export class SurveyService extends DBService { return this.surveyRepository.unassociatePermitFromSurvey(surveyId); } - /** - * Updates a Survey funding source for a given survey ID - * - * @param {number} surveyID - * @returns {*} {Promise} - * @memberof SurveyService - */ - async updateSurveyFundingData(surveyId: number, surveyData: PutSurveyObject) { - await this.deleteSurveyFundingSourcesData(surveyId); - - const promises: Promise[] = []; - - surveyData.funding.funding_sources.forEach((project_funding_source_id: number) => - promises.push(this.insertSurveyFundingSource(project_funding_source_id, surveyId)) - ); - - return Promise.all(promises); - } - - /** - * Breaks link between a funding source and a survey for a given survey ID - * - * @param {number} surveyID - * @returns {*} {Promise} - * @memberof SurveyService - */ - async deleteSurveyFundingSourcesData(surveyId: number): Promise { - return this.surveyRepository.deleteSurveyFundingSourcesData(surveyId); - } - /** * Updates proprietor data on a survey * diff --git a/api/src/utils/shared-api-docs.test.ts b/api/src/utils/shared-api-docs.test.ts index 32cb16f378..7bcb45f39e 100644 --- a/api/src/utils/shared-api-docs.test.ts +++ b/api/src/utils/shared-api-docs.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { addFundingSourceApiDocObject, attachmentApiDocObject } from './shared-api-docs'; +import { attachmentApiDocObject } from './shared-api-docs'; describe('attachmentApiResponseObject', () => { it('returns a valid response object', () => { @@ -10,12 +10,3 @@ describe('attachmentApiResponseObject', () => { expect(result?.description).to.equal('basic'); }); }); - -describe('addFundingSourceApiDocObject', () => { - it('returns a valid response object', () => { - const result = addFundingSourceApiDocObject('basic', 'success'); - - expect(result).to.not.be.null; - expect(result?.description).to.equal('basic'); - }); -}); diff --git a/api/src/utils/shared-api-docs.ts b/api/src/utils/shared-api-docs.ts index f36e440c84..3de10aa287 100644 --- a/api/src/utils/shared-api-docs.ts +++ b/api/src/utils/shared-api-docs.ts @@ -1,5 +1,3 @@ -import { projectFundingSourcePostRequestObject } from '../openapi/schemas/project-funding-source'; - export const attachmentApiDocObject = (basicDescription: string, successDescription: string) => { return { description: basicDescription, @@ -47,59 +45,3 @@ export const attachmentApiDocObject = (basicDescription: string, successDescript } }; }; - -export const addFundingSourceApiDocObject = (basicDescription: string, successDescription: string) => { - return { - description: basicDescription, - tags: ['funding-sources'], - security: [ - { - Bearer: [] - } - ], - parameters: [ - { - in: 'path', - name: 'projectId', - schema: { - type: 'number' - }, - required: true - } - ], - requestBody: { - description: 'Add funding source request object.', - content: { - 'application/json': { - schema: { - ...(projectFundingSourcePostRequestObject as object) - } - } - } - }, - responses: { - 200: { - description: successDescription, - content: { - 'application/json': { - schema: { - type: 'object', - required: ['id'], - properties: { - id: { - type: 'number' - } - } - } - } - } - }, - 401: { - $ref: '#/components/responses/401' - }, - default: { - $ref: '#/components/responses/default' - } - } - }; -}; diff --git a/app/src/components/funding-source/FundingSource.test.tsx b/app/src/components/funding-source/FundingSource.test.tsx deleted file mode 100644 index 8ce0adc265..0000000000 --- a/app/src/components/funding-source/FundingSource.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { IProjectContext, ProjectContext } from 'contexts/projectContext'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { DataLoader } from 'hooks/useDataLoader'; -import { getProjectForViewResponse } from 'test-helpers/project-helpers'; -import { cleanup, render } from 'test-helpers/test-utils'; -import FundingSource, { IFundingSource } from './FundingSource'; - -jest.mock('../../hooks/useBioHubApi'); - -const mockBiohubApi = useBiohubApi as jest.Mock; - -const mockUseApi = { - project: { - updateProject: jest.fn() - } -}; - -const funding_sources: IFundingSource[] = [ - { - id: 1, - start_date: '1900-01-01', - end_date: '2050-01-01', - agency_project_id: 'Project ID', - first_nations_name: 'First Nations' - }, - { - id: 2, - agency_name: 'Agency', - investment_action_category_name: 'Not Applicable', - funding_amount: 500, - start_date: '1900-01-01', - end_date: '1900-02-02', - agency_project_id: '1230' - } -]; - -describe('FundingSource', () => { - beforeEach(() => { - mockBiohubApi.mockImplementation(() => mockUseApi); - mockUseApi.project.updateProject.mockClear(); - }); - - afterEach(() => { - cleanup(); - jest.clearAllMocks(); - }); - - it('renders correctly', () => { - const mockProjectContext: IProjectContext = { - projectDataLoader: { data: getProjectForViewResponse } as DataLoader, - artifactDataLoader: { data: null } as DataLoader, - surveysListDataLoader: { data: null } as DataLoader, - projectId: 1 - }; - - const { asFragment } = render( - - - - ); - - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/app/src/components/funding-source/FundingSource.tsx b/app/src/components/funding-source/FundingSource.tsx deleted file mode 100644 index 2458924976..0000000000 --- a/app/src/components/funding-source/FundingSource.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import Box from '@mui/material/Box'; -import Grid from '@mui/material/Grid'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import Typography from '@mui/material/Typography'; -import { DATE_FORMAT } from 'constants/dateTimeFormats'; -import { getFormattedAmount, getFormattedDateRangeString } from 'utils/Utils'; - -export interface IFundingSource { - id: number; - agency_name?: string; - investment_action_category_name?: string; - funding_amount?: number; - start_date?: string; - end_date?: string; - agency_project_id: string; - first_nations_name?: string; -} - -interface IFundingSourceProps { - funding_sources: IFundingSource[]; -} - -/** - * Funding source content for a project. - * - * @return {*} - */ -const FundingSource = (props: IFundingSourceProps) => { - const hasFundingSources = props.funding_sources.length > 0; - - return ( - <> - - {hasFundingSources && - props.funding_sources.map((item: IFundingSource) => ( - - - - - {item.agency_name ?? item.first_nations_name} - {item.investment_action_category_name && - item.investment_action_category_name !== 'Not Applicable' && ( -  ({item.investment_action_category_name}) - )} - - - - - - - Project ID - - {item.agency_project_id || 'No Agency Project ID'} - - - {item.start_date && item.end_date && ( - <> - - Timeline - - - {getFormattedDateRangeString( - DATE_FORMAT.ShortMediumDateFormat, - item.start_date, - item.end_date - )} - - - )} - - - {item.funding_amount && ( - <> - - Funding Amount - - {getFormattedAmount(item.funding_amount)} - - )} - - - - - - ))} - - {!hasFundingSources && ( - - No Funding Sources - - )} - - - ); -}; - -export default FundingSource; diff --git a/app/src/components/funding-source/__snapshots__/FundingSource.test.tsx.snap b/app/src/components/funding-source/__snapshots__/FundingSource.test.tsx.snap deleted file mode 100644 index 7169397edd..0000000000 --- a/app/src/components/funding-source/__snapshots__/FundingSource.test.tsx.snap +++ /dev/null @@ -1,133 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FundingSource renders correctly 1`] = ` - -
    -
  • -
    -
    - - First Nations - -
    -
    -
    -
    -
    - Project ID -
    -
    - Project ID -
    -
    -
    -
    - Timeline -
    -
    - Jan 1, 1900 - Jan 1, 2050 -
    -
    -
    -
    -
    -
    -
  • -
  • -
    -
    - - Agency - -
    -
    -
    -
    -
    - Project ID -
    -
    - 1230 -
    -
    -
    -
    - Timeline -
    -
    - Jan 1, 1900 - Feb 2, 1900 -
    -
    -
    -
    - Funding Amount -
    -
    - $500 -
    -
    -
    -
    -
    -
  • -
-
-`; diff --git a/app/src/components/search-filter/ProjectAdvancedFilters.tsx b/app/src/components/search-filter/ProjectAdvancedFilters.tsx index 20b78fe45c..429882eeb1 100644 --- a/app/src/components/search-filter/ProjectAdvancedFilters.tsx +++ b/app/src/components/search-filter/ProjectAdvancedFilters.tsx @@ -1,8 +1,5 @@ import FormControl from '@mui/material/FormControl'; import Grid from '@mui/material/Grid'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import assert from 'assert'; import AutocompleteFreeSoloField from 'components/fields/AutocompleteFreeSoloField'; import CustomTextField from 'components/fields/CustomTextField'; @@ -43,7 +40,6 @@ export const ProjectAdvancedFiltersInitialValues: IProjectAdvancedFilters = { }; export interface IProjectAdvancedFiltersProps { - funding_sources: IMultiAutocompleteFieldOption[]; coordinator_agency: string[]; } @@ -57,7 +53,7 @@ const ProjectAdvancedFilters: React.FC = (props) = const biohubApi = useBiohubApi(); - const { handleSubmit, handleChange, values } = formikProps; + const { handleSubmit } = formikProps; const codesContext = useContext(CodesContext); assert(codesContext.codesDataLoader.data); @@ -157,26 +153,6 @@ const ProjectAdvancedFilters: React.FC = (props) = - - - Funding Agency Name - - - diff --git a/app/src/features/projects/components/FundingSourceAutocomplete.tsx b/app/src/features/projects/components/FundingSourceAutocomplete.tsx deleted file mode 100644 index e629aca0d7..0000000000 --- a/app/src/features/projects/components/FundingSourceAutocomplete.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { FundingSourceType } from 'features/projects/components/ProjectFundingItemForm'; -import React, { ChangeEvent, useEffect, useState } from 'react'; -import AutocompleteField from '../../../components/fields/AutocompleteField'; - -/** - * Encapsulates an auto complete option with a funding source type. - * Type is added because the `FundingSourceAutocomplete` component takes both - * Agencies and First Nations groups as options and we need a way to differentiate them - */ -export interface IAutocompleteFieldOptionWithType { - value: T; - label: string; - type: FundingSourceType; -} - -export interface IFundingSourceAutocompleteProps { - id: string; - label: string; - options: IAutocompleteFieldOptionWithType[]; - required?: boolean; - filterLimit?: number; - initialValue?: IAutocompleteFieldOptionWithType; - onChange: (event: ChangeEvent>, option: IAutocompleteFieldOptionWithType | null) => void; -} - -const FundingSourceAutocomplete: React.FC> = < - T extends string | number ->( - props: IFundingSourceAutocompleteProps -) => { - const [name, setName] = useState(''); - - // find the correct 'name' (key) to pull label information out of options - useEffect(() => { - if (props.initialValue) { - if (props.initialValue.type === FundingSourceType.FIRST_NATIONS) { - setName('first_nations_name'); - } else { - setName('agency_name'); - } - } - }, [props.initialValue]); - - return ( - { - if (option?.type === FundingSourceType.FIRST_NATIONS) { - setName('first_nations_name'); - } else { - setName('agency_name'); - } - props.onChange(event as unknown as ChangeEvent>, option); - }} - /> - ); -}; - -export default FundingSourceAutocomplete; diff --git a/app/src/features/projects/components/ProjectFundingForm.test.tsx b/app/src/features/projects/components/ProjectFundingForm.test.tsx deleted file mode 100644 index 9603ca559d..0000000000 --- a/app/src/features/projects/components/ProjectFundingForm.test.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { Formik } from 'formik'; -import { fireEvent, render, waitFor } from 'test-helpers/test-utils'; -import ProjectFundingForm, { - IInvestmentActionCategoryOption, - IProjectFundingForm, - ProjectFundingFormInitialValues, - ProjectFundingFormYupSchema -} from './ProjectFundingForm'; -import { FundingSourceType, IFundingSourceAutocompleteField } from './ProjectFundingItemForm'; - -const funding_sources: IFundingSourceAutocompleteField[] = [ - { - value: 1, - label: 'agency 1', - type: FundingSourceType.FUNDING_SOURCE - }, - { - value: 2, - label: 'agency 2', - type: FundingSourceType.FUNDING_SOURCE - }, - { - value: 3, - label: 'agency 3', - type: FundingSourceType.FUNDING_SOURCE - } -]; - -const first_nations: IFundingSourceAutocompleteField[] = [ - { - value: 1, - label: 'First Nation 1', - type: FundingSourceType.FIRST_NATIONS - }, - { - value: 2, - label: 'First Nation 2', - type: FundingSourceType.FIRST_NATIONS - }, - { - value: 3, - label: 'First Nation 3', - type: FundingSourceType.FIRST_NATIONS - } -]; - -const investment_action_category: IInvestmentActionCategoryOption[] = [ - { - value: 1, - agency_id: 1, - label: 'action 1' - }, - { - value: 2, - agency_id: 2, - label: 'category 1' - }, - { - value: 3, - agency_id: 3, - label: 'not applicable' - } -]; - -describe('ProjectFundingForm', () => { - it('renders correctly with default empty values', async () => { - const { getByTestId } = render( - {}}> - {() => ( - - )} - - ); - - const addButton = getByTestId('funding-form-add-button'); - expect(addButton).toBeInTheDocument(); - }); - - it('renders correctly with existing funding values', async () => { - const existingFormValues: IProjectFundingForm = { - funding: { - fundingSources: [ - { - id: 11, - agency_id: 1, - investment_action_category: 1, - investment_action_category_name: 'Action 23', - agency_project_id: '111', - funding_amount: 222, - start_date: '2021-03-14', - end_date: '2021-04-14', - revision_count: 23 - } - ] - } - }; - - const { getByTestId } = render( - {}}> - {() => ( - - )} - - ); - - const addButton = getByTestId('funding-form-add-button'); - expect(addButton).toBeInTheDocument(); - }); - - it('shows add funding source dialog on add click', async () => { - const existingFormValues: IProjectFundingForm = { - funding: { - fundingSources: [ - { - id: 11, - agency_id: 1, - investment_action_category: 1, - investment_action_category_name: 'action 1', - agency_project_id: '111', - funding_amount: 222, - start_date: '2021-03-14', - end_date: '2021-04-14', - revision_count: 23 - }, - { - id: 12, - agency_id: 2, - investment_action_category: 2, - investment_action_category_name: 'category 1', - agency_project_id: '112', - funding_amount: 223, - start_date: '2021-03-15', - end_date: '2021-04-15', - revision_count: 24 - } - ] - } - }; - - const { getByTestId, findByTestId } = render( - {}}> - {() => ( - - )} - - ); - - const addButton = getByTestId('funding-form-add-button'); - expect(addButton).toBeInTheDocument(); - - fireEvent.click(addButton); - - const editDialog = await findByTestId('edit-dialog'); - expect(editDialog).toBeInTheDocument(); - }); - - it('shows edit funding source dialog on edit click', async () => { - const existingFormValues: IProjectFundingForm = { - funding: { - fundingSources: [ - { - id: 11, - agency_id: 1, - investment_action_category: 1, - investment_action_category_name: 'action 1', - agency_project_id: '111', - funding_amount: 222, - start_date: '2021-03-14', - end_date: '2021-04-14', - revision_count: 23 - } - ] - } - }; - - const { getByTestId, findByTestId, queryByTestId } = render( - {}}> - {() => ( - - )} - - ); - - const editButton = getByTestId('funding-form-edit-button-0'); - expect(editButton).toBeInTheDocument(); - - fireEvent.click(editButton); - - const saveButton = getByTestId('edit-dialog-cancel'); - expect(saveButton).toBeInTheDocument(); - - const editDialog = await findByTestId('edit-dialog'); - expect(editDialog).toBeInTheDocument(); - - const cancelButton = getByTestId('edit-dialog-cancel'); - expect(cancelButton).toBeInTheDocument(); - - fireEvent.click(cancelButton); - - let editDialog2; - await waitFor(() => { - editDialog2 = queryByTestId('edit-dialog'); - }); - expect(editDialog2).not.toBeInTheDocument(); - }); - - it('deletes funding source dialog on delete click', async () => { - const existingFormValues: IProjectFundingForm = { - funding: { - fundingSources: [ - { - id: 11, - agency_id: 1, - investment_action_category: 1, - investment_action_category_name: 'action 1', - agency_project_id: '111', - funding_amount: 222, - start_date: '2021-03-14', - end_date: '2021-04-14', - revision_count: 23 - } - ] - } - }; - - const { getByTestId, queryByTestId } = render( - {}}> - {() => ( - - )} - - ); - - const deleteButton = getByTestId('funding-form-delete-button-0'); - expect(deleteButton).toBeInTheDocument(); - - fireEvent.click(deleteButton); - - let deleteButton2; - await waitFor(() => { - deleteButton2 = queryByTestId('funding-form-delete-button-0'); - }); - expect(deleteButton2).not.toBeInTheDocument(); - }); -}); diff --git a/app/src/features/projects/components/ProjectFundingForm.tsx b/app/src/features/projects/components/ProjectFundingForm.tsx deleted file mode 100644 index b2eedc5d67..0000000000 --- a/app/src/features/projects/components/ProjectFundingForm.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import { mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; -import Icon from '@mdi/react'; -import { Theme } from '@mui/material'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import { grey } from '@mui/material/colors'; -import Divider from '@mui/material/Divider'; -import Grid from '@mui/material/Grid'; -import IconButton from '@mui/material/IconButton'; -import Typography from '@mui/material/Typography'; -import { makeStyles } from '@mui/styles'; -import EditDialog from 'components/dialog/EditDialog'; -import { IMultiAutocompleteFieldOption } from 'components/fields/MultiAutocompleteFieldVariableSize'; -import { DATE_FORMAT } from 'constants/dateTimeFormats'; -import { AddFundingI18N } from 'constants/i18n'; -import { FieldArray, FieldArrayRenderProps, useFormikContext } from 'formik'; -import { ICreateProjectRequest } from 'interfaces/useProjectApi.interface'; -import React, { useState } from 'react'; -import { getFormattedAmount, getFormattedDateRangeString } from 'utils/Utils'; -import yup from 'utils/YupSchema'; -import ProjectFundingItemForm, { - IFundingSourceAutocompleteField, - IProjectFundingFormArrayItem, - ProjectFundingFormArrayItemInitialValues, - ProjectFundingFormArrayItemYupSchema -} from './ProjectFundingItemForm'; - -export interface IProjectFundingForm { - funding: { - fundingSources: IProjectFundingFormArrayItem[]; - }; -} - -export const ProjectFundingFormInitialValues: IProjectFundingForm = { - funding: { - fundingSources: [] - } -}; - -export const ProjectFundingFormYupSchema = yup.object().shape({}); - -export interface IInvestmentActionCategoryOption extends IMultiAutocompleteFieldOption { - agency_id: number; -} - -export interface IProjectFundingFormProps { - funding_sources: IFundingSourceAutocompleteField[]; - investment_action_category: IInvestmentActionCategoryOption[]; - first_nations: IFundingSourceAutocompleteField[]; -} - -const useStyles = makeStyles((theme: Theme) => ({ - title: { - flexGrow: 1, - paddingTop: 0, - paddingBottom: 0, - marginRight: '1rem', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - fontWeight: 700 - }, - titleDesc: { - marginLeft: theme.spacing(1), - fontWeight: 400 - }, - fundingSourceItem: { - marginTop: theme.spacing(2), - borderColor: grey[400], - '& .MuiCardHeader-action': { - margin: '-8px 0' - }, - '& .MuiCardContent-root:last-child': { - paddingBottom: theme.spacing(2) - } - } -})); - -/** - * Create project - Funding section - * - * @return {*} - */ -const ProjectFundingForm: React.FC = (props) => { - const classes = useStyles(); - - const formikProps = useFormikContext(); - const { values, handleSubmit } = formikProps; - - //Tracks information about the current funding source item that is being added/edited - const [currentProjectFundingFormArrayItem, setCurrentProjectFundingFormArrayItem] = useState({ - index: 0, - values: ProjectFundingFormArrayItemInitialValues - }); - - const [isModalOpen, setIsModalOpen] = useState(false); - - return ( - - - - - ( - - - ), - initialValues: currentProjectFundingFormArrayItem.values, - validationSchema: ProjectFundingFormArrayItemYupSchema - }} - onCancel={() => setIsModalOpen(false)} - onSave={(projectFundingItemValues) => { - if (currentProjectFundingFormArrayItem.index < values.funding.fundingSources.length) { - // Update an existing item - arrayHelpers.replace(currentProjectFundingFormArrayItem.index, projectFundingItemValues); - } else { - // Add a new item - arrayHelpers.push(projectFundingItemValues); - } - - // Close the modal - setIsModalOpen(false); - }} - /> - - {values.funding.fundingSources.map((fundingSource, index) => { - const investment_action_category_label = - (fundingSource.agency_id === 1 && 'Investment Action') || - (fundingSource.agency_id === 2 && 'Investment Category') || - null; - const investment_action_category_value = props.investment_action_category.filter( - (item) => item.value === fundingSource.investment_action_category - )?.[0]?.label; - const key = [ - getCodeValueNameByID(props.first_nations, fundingSource.first_nations_id) || - getCodeValueNameByID(props.funding_sources, fundingSource.agency_id), - index - ].join('-'); - return ( - - - {getCodeValueNameByID(props.funding_sources, fundingSource?.agency_id) || - getCodeValueNameByID(props.first_nations, fundingSource?.first_nations_id)} - {investment_action_category_label && ( - ({investment_action_category_value}) - )} - - } - action={ - - { - setCurrentProjectFundingFormArrayItem({ - index: index, - values: values.funding.fundingSources[index] - }); - setIsModalOpen(true); - }}> - - - arrayHelpers.remove(index)}> - - - - }> - - {(fundingSource.agency_project_id || - fundingSource.funding_amount || - (fundingSource.start_date && fundingSource.end_date)) && ( - <> - - - - {fundingSource.agency_project_id && ( - <> - - - Agency Project ID - - {fundingSource.agency_project_id} - - - )} - {fundingSource.funding_amount && ( - <> - - - Funding Amount - - - {getFormattedAmount(fundingSource.funding_amount)} - - - - )} - {fundingSource.start_date && fundingSource.end_date && ( - <> - - - Start / End Date - - - {getFormattedDateRangeString( - DATE_FORMAT.ShortMediumDateFormat, - fundingSource.start_date, - fundingSource.end_date - )} - - - - )} - - - - )} - - ); - })} - - - )} - /> - - - - ); -}; - -export default ProjectFundingForm; - -export const getCodeValueNameByID = (codeSet: IMultiAutocompleteFieldOption[], codeValueId?: number): string => { - if (!codeSet?.length || !codeValueId) { - return ''; - } - return codeSet.find((item) => item.value === codeValueId)?.label ?? ''; -}; diff --git a/app/src/features/projects/components/ProjectFundingItemForm.test.tsx b/app/src/features/projects/components/ProjectFundingItemForm.test.tsx deleted file mode 100644 index aee13589a5..0000000000 --- a/app/src/features/projects/components/ProjectFundingItemForm.test.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { IAutocompleteFieldOptionWithType } from 'features/projects/components/FundingSourceAutocomplete'; -import { Formik } from 'formik'; -import { render, waitFor } from 'test-helpers/test-utils'; -import { IInvestmentActionCategoryOption } from './ProjectFundingForm'; -import ProjectFundingItemForm, { - FundingSourceType, - IProjectFundingFormArrayItem, - ProjectFundingFormArrayItemInitialValues, - ProjectFundingFormArrayItemYupSchema -} from './ProjectFundingItemForm'; - -const funding_sources: IAutocompleteFieldOptionWithType[] = [ - { - value: 1, - label: 'agency 1', - type: FundingSourceType.FUNDING_SOURCE - }, - { - value: 2, - label: 'agency 2', - type: FundingSourceType.FUNDING_SOURCE - }, - { - value: 3, - label: 'agency 3', - type: FundingSourceType.FUNDING_SOURCE - }, - { - value: 4, - label: 'agency 4', - type: FundingSourceType.FIRST_NATIONS - } -]; - -const investment_action_category: IInvestmentActionCategoryOption[] = [ - { - value: 1, - agency_id: 1, - label: 'action 1' - }, - { - value: 2, - agency_id: 2, - label: 'category 1' - }, - { - value: 3, - agency_id: 3, - label: 'not applicable' - } -]; - -describe('ProjectFundingItemForm', () => { - it('renders correctly with default empty values', () => { - const { asFragment } = render( - {}}> - {() => ( - - )} - - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it('renders correctly with agency 1', () => { - const existingFormValues: IProjectFundingFormArrayItem = { - id: 1, - agency_id: 1, - investment_action_category: 1, - investment_action_category_name: 'Some investment action', - agency_project_id: '555', - funding_amount: 666, - start_date: '2021-03-14', - end_date: '2021-04-14', - revision_count: 2 - }; - - const { asFragment } = render( - {}}> - {() => ( - - )} - - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it('renders correctly with agency 2', () => { - const existingFormValues: IProjectFundingFormArrayItem = { - id: 1, - agency_id: 2, - investment_action_category: 2, - investment_action_category_name: 'Some investment category', - agency_project_id: '555', - funding_amount: 666, - start_date: '2021-03-14', - end_date: '2021-04-14', - revision_count: 2 - }; - - const { asFragment } = render( - {}}> - {() => ( - - )} - - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it('renders correctly with any agency other than 1 or 2', () => { - const existingFormValues: IProjectFundingFormArrayItem = { - id: 1, - agency_id: 3, - investment_action_category: 3, - investment_action_category_name: 'Not Applicable', - agency_project_id: '555', - funding_amount: 666, - start_date: '2021-03-14', - end_date: '2021-04-14', - revision_count: 2 - }; - - const { asFragment } = render( - {}}> - {() => ( - - )} - - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - describe('auto selection of investment action category', () => { - const component = ( - {}}> - {() => ( - - )} - - ); - - it('works if an agency_id with a matching NA investment action category is chosen', async () => { - const { asFragment, getByText } = render(component); - - await waitFor(() => { - expect(getByText('Agency Details')).toBeInTheDocument(); - }); - - await waitFor(() => { - expect(asFragment()).toMatchSnapshot(); - }); - }); - - it('works if an agency_id with a non-matching investment action category is chosen', async () => { - const { asFragment, getByText } = render(component); - - await waitFor(() => { - expect(getByText('Agency Details')).toBeInTheDocument(); - }); - - await waitFor(() => { - expect(asFragment()).toMatchSnapshot(); - }); - }); - }); -}); diff --git a/app/src/features/projects/components/ProjectFundingItemForm.tsx b/app/src/features/projects/components/ProjectFundingItemForm.tsx deleted file mode 100644 index 88a21cc040..0000000000 --- a/app/src/features/projects/components/ProjectFundingItemForm.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import Box from '@mui/material/Box'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormHelperText from '@mui/material/FormHelperText'; -import Grid from '@mui/material/Grid'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; -import Typography from '@mui/material/Typography'; -import CustomTextField from 'components/fields/CustomTextField'; -import DollarAmountField from 'components/fields/DollarAmountField'; -import StartEndDateFields from 'components/fields/StartEndDateFields'; -import FundingSourceAutocomplete, { - IAutocompleteFieldOptionWithType -} from 'features/projects/components/FundingSourceAutocomplete'; -import { useFormikContext } from 'formik'; -import React from 'react'; -import yup from 'utils/YupSchema'; -import { IInvestmentActionCategoryOption } from './ProjectFundingForm'; - -export interface IProjectFundingFormArrayItem { - id: number; - agency_id?: number; - investment_action_category: number; - investment_action_category_name: string; - agency_project_id: string; - funding_amount?: number; - start_date?: string; - end_date?: string; - revision_count: number; - first_nations_id?: number; -} - -export const ProjectFundingFormArrayItemInitialValues: IProjectFundingFormArrayItem = { - id: 0, - agency_id: '' as unknown as number, - investment_action_category: '' as unknown as number, - investment_action_category_name: '', - agency_project_id: '', - funding_amount: undefined, - start_date: undefined, - end_date: undefined, - revision_count: 0, - first_nations_id: undefined -}; - -export const ProjectFundingFormArrayItemYupSchema = yup.object().shape( - { - // if agency_id is present, first_nations_id is no longer required - agency_id: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - .when('first_nations_id', { - is: (first_nations_id: number) => !first_nations_id, - then: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .required('Required'), - otherwise: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - }), - // if first_nations_id is present, agency_id is no longer required - first_nations_id: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - .when('agency_id', { - is: (agency_id: number) => !agency_id, - then: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .required('Required'), - otherwise: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - }), - investment_action_category: yup.number().nullable(true), - agency_project_id: yup.string().max(50, 'Cannot exceed 50 characters').nullable(true), - // funding amount is not required when a first nation is selected as a funding source - funding_amount: yup - .number() - .transform((value) => (isNaN(value) && null) || value) - .typeError('Must be a number') - .min(0, 'Must be a positive number') - .max(9999999999, 'Must be less than $9,999,999,999') - .when('first_nations_id', (val: any) => { - const rules = yup - .number() - .transform((value) => (isNaN(value) && null) || value) - .typeError('Must be a number') - .min(0, 'Must be a positive number') - .max(9999999999, 'Must be less than $9,999,999,999'); - if (!val) { - return rules.required('Required'); - } - - return rules.nullable(true); - }), - start_date: yup.string().when('first_nations_id', (val: any) => { - const rules = yup.string().isValidDateString(); - if (!val) { - return rules.required('Required'); - } - return rules.nullable(true); - }), - end_date: yup.string().when('first_nations_id', (val: any) => { - const rules = yup.string().isValidDateString().isEndDateAfterStartDate('start_date'); - if (!val) { - return rules.required('Required'); - } - return rules.nullable(true); - }) - }, - [['agency_id', 'first_nations_id']] // this prevents a cyclical dependency -); - -export enum FundingSourceType { - FUNDING_SOURCE, - FIRST_NATIONS -} -export interface IFundingSourceAutocompleteField { - value: number; - label: string; - type: FundingSourceType; -} -export interface IProjectFundingItemFormProps { - sources: IFundingSourceAutocompleteField[]; - investment_action_category: IInvestmentActionCategoryOption[]; -} - -/** - * A modal form for a single item of the project funding sources array. - * - * @See ProjectFundingForm.tsx - * - * @param {*} props - * @return {*} - */ -const ProjectFundingItemForm: React.FC = (props) => { - const formikProps = useFormikContext(); - const { values, touched, errors, handleChange, handleSubmit, setFieldValue } = formikProps; - // Only show investment_action_category if certain agency_id values are selected - // Toggle investment_action_category label and dropdown values based on chosen agency_id - const investment_action_category_label = - (values.agency_id === 1 && 'Investment Action') || (values.agency_id === 2 && 'Investment Category') || null; - - const findItemLabel = (id: number, type: FundingSourceType) => { - return props.sources.find((item) => item.value === id && item.type === type)?.label; - }; - - // find label for initial value - const mapInitialValue = ( - formValues?: IProjectFundingFormArrayItem - ): IAutocompleteFieldOptionWithType | undefined => { - if (formValues) { - const id = formValues.agency_id ?? formValues.first_nations_id ?? 0; - const type = formValues.agency_id ? FundingSourceType.FUNDING_SOURCE : FundingSourceType.FIRST_NATIONS; - - const initialValue = { - value: id, - type: type, - label: String(findItemLabel(id, type)) - } as IAutocompleteFieldOptionWithType; - - return initialValue; - } - }; - - return ( -
- - - Agency Details - - - - - - { - // investment_action_category is dependent on agency_id, so reset it if agency_id changes - setFieldValue( - 'investment_action_category', - ProjectFundingFormArrayItemInitialValues.investment_action_category - ); - // first_nations_id AND agency_id cannot be present on the same funding source - // reset values when a change occurs to prevent that from happening - setFieldValue('first_nations_id', null); - setFieldValue('agency_id', null); - setFieldValue('first_nations_name', null); - setFieldValue('agency_name', null); - - if (options?.type === FundingSourceType.FIRST_NATIONS) { - setFieldValue('first_nations_id', options?.value); - setFieldValue('first_nations_name', options?.label); - setFieldValue('investment_action_category', 0); // first nations do not have an investment category - } else { - setFieldValue('agency_id', options?.value); - setFieldValue('agency_name', options?.label); - // If an agency_id with a `Not Applicable` investment_action_category is chosen, auto select - // it for the user. - if (event.target.value !== 1 && event.target.value !== 2) { - setFieldValue( - 'investment_action_category', - props.investment_action_category.find((item) => item.agency_id === options?.value)?.value - ); - } - } - }} - /> - - {errors.agency_id} - - - {investment_action_category_label && ( - - - {investment_action_category_label} - - {errors.investment_action_category} - - - )} - - - - - - - Funding Details - - - - - - - - - - -
- ); -}; - -export default ProjectFundingItemForm; diff --git a/app/src/features/projects/components/__snapshots__/ProjectFundingItemForm.test.tsx.snap b/app/src/features/projects/components/__snapshots__/ProjectFundingItemForm.test.tsx.snap deleted file mode 100644 index 057673f3c5..0000000000 --- a/app/src/features/projects/components/__snapshots__/ProjectFundingItemForm.test.tsx.snap +++ /dev/null @@ -1,2081 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ProjectFundingItemForm auto selection of investment action category works if an agency_id with a matching NA investment action category is chosen 1`] = ` - -
-
- - Agency Details - -
-
-
-
-
-
- -
- -
- -
- -
-
-
-
-

-

-
-
-
- -
- - -
-
-
-
-
-
- - Funding Details - -
-
-
- -
- - -
-
-
-
-
-
- -
- - -
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`ProjectFundingItemForm auto selection of investment action category works if an agency_id with a non-matching investment action category is chosen 1`] = ` - -
-
- - Agency Details - -
-
-
-
-
-
- -
- -
- -
- -
-
-
-
-

-

-
-
-
- -
- - -
-
-
-
-
-
- - Funding Details - -
-
-
- -
- - -
-
-
-
-
-
- -
- - -
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`ProjectFundingItemForm renders correctly with agency 1 1`] = ` - -
-
- - Agency Details - -
-
-
-
-
-
- -
- -
- -
- -
-
-
-
-

-

-
-
-
- -
- - - - -
-

-

-
-
-
- -
- - -
-
-
-
-
-
- - Funding Details - -
-
-
- -
- - -
-
-
-
-
-
- -
- - -
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`ProjectFundingItemForm renders correctly with agency 2 1`] = ` - -
-
- - Agency Details - -
-
-
-
-
-
- -
- -
- -
- -
-
-
-
-

-

-
-
-
- -
- - - - -
-

-

-
-
-
- -
- - -
-
-
-
-
-
- - Funding Details - -
-
-
- -
- - -
-
-
-
-
-
- -
- - -
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`ProjectFundingItemForm renders correctly with any agency other than 1 or 2 1`] = ` - -
-
- - Agency Details - -
-
-
-
-
-
- -
- -
- -
- -
-
-
-
-

-

-
-
-
- -
- - -
-
-
-
-
-
- - Funding Details - -
-
-
- -
- - -
-
-
-
-
-
- -
- - -
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
-
-`; - -exports[`ProjectFundingItemForm renders correctly with default empty values 1`] = ` - -
-
- - Agency Details - -
-
-
-
-
-
- -
- -
- -
- -
-
-
-
-

-

-
-
-
- -
- - -
-
-
-
-
-
- - Funding Details - -
-
-
- -
- - -
-
-
-
-
-
- -
- - -
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
-
-`; diff --git a/app/src/features/projects/create/CreateProjectForm.tsx b/app/src/features/projects/create/CreateProjectForm.tsx index e4d87a4fbb..fe126e482a 100644 --- a/app/src/features/projects/create/CreateProjectForm.tsx +++ b/app/src/features/projects/create/CreateProjectForm.tsx @@ -18,11 +18,6 @@ import ProjectDetailsForm, { ProjectDetailsFormInitialValues, ProjectDetailsFormYupSchema } from '../components/ProjectDetailsForm'; -import ProjectFundingForm, { - ProjectFundingFormInitialValues, - ProjectFundingFormYupSchema -} from '../components/ProjectFundingForm'; -import { FundingSourceType } from '../components/ProjectFundingItemForm'; import ProjectIUCNForm, { ProjectIUCNFormInitialValues, ProjectIUCNFormYupSchema } from '../components/ProjectIUCNForm'; import ProjectLocationForm, { ProjectLocationFormInitialValues, @@ -58,7 +53,6 @@ export const initialProjectFieldData: ICreateProjectRequest = { ...ProjectCoordinatorInitialValues, ...ProjectLocationFormInitialValues, ...ProjectIUCNFormInitialValues, - ...ProjectFundingFormInitialValues, ...ProjectPartnershipsFormInitialValues }; @@ -66,7 +60,6 @@ export const validationProjectYupSchema = ProjectCoordinatorYupSchema.concat(Pro .concat(ProjectObjectivesFormYupSchema) .concat(ProjectLocationFormYupSchema) .concat(ProjectIUCNFormYupSchema) - .concat(ProjectFundingFormYupSchema) .concat(ProjectPartnershipsFormYupSchema); //Function to get the list of coordinator agencies from the code set @@ -177,23 +170,9 @@ const CreateProjectForm: React.FC = (props) => { exact, please round to the nearest 100. - { - return { value: item.id, label: item.name, type: FundingSourceType.FUNDING_SOURCE }; - }) || [] - } - investment_action_category={ - codes?.investment_action_category?.map((item) => { - return { value: item.id, agency_id: item.agency_id, label: item.name }; - }) || [] - } - first_nations={ - codes.first_nations?.map((item) => { - return { value: item.id, label: item.name, type: FundingSourceType.FIRST_NATIONS }; - }) || [] - } - /> + { + //funding + }
diff --git a/app/src/features/projects/create/CreateProjectPage.test.tsx b/app/src/features/projects/create/CreateProjectPage.test.tsx index 520a0014e1..e1cc713518 100644 --- a/app/src/features/projects/create/CreateProjectPage.test.tsx +++ b/app/src/features/projects/create/CreateProjectPage.test.tsx @@ -1,7 +1,6 @@ import { CodesContext, ICodesContext } from 'contexts/codesContext'; import { DialogContextProvider } from 'contexts/dialogContext'; import { ProjectDetailsFormInitialValues } from 'features/projects/components/ProjectDetailsForm'; -import { ProjectFundingFormInitialValues } from 'features/projects/components/ProjectFundingForm'; import { ProjectIUCNFormInitialValues } from 'features/projects/components/ProjectIUCNForm'; import { ProjectLocationFormInitialValues } from 'features/projects/components/ProjectLocationForm'; import { ProjectObjectivesFormInitialValues } from 'features/projects/components/ProjectObjectivesForm'; @@ -174,7 +173,6 @@ describe('CreateProjectPage', () => { objectives: ProjectObjectivesFormInitialValues.objectives, location: ProjectLocationFormInitialValues.location, iucn: ProjectIUCNFormInitialValues.iucn, - funding: ProjectFundingFormInitialValues.funding, partnerships: ProjectPartnershipsFormInitialValues.partnerships } }); @@ -208,7 +206,6 @@ describe('CreateProjectPage', () => { objectives: ProjectObjectivesFormInitialValues.objectives, location: ProjectLocationFormInitialValues.location, iucn: ProjectIUCNFormInitialValues.iucn, - funding: ProjectFundingFormInitialValues.funding, partnerships: ProjectPartnershipsFormInitialValues.partnerships } }); @@ -250,7 +247,6 @@ describe('CreateProjectPage', () => { objectives: ProjectObjectivesFormInitialValues.objectives, location: ProjectLocationFormInitialValues.location, iucn: ProjectIUCNFormInitialValues.iucn, - funding: ProjectFundingFormInitialValues.funding, partnerships: ProjectPartnershipsFormInitialValues.partnerships } }); @@ -299,7 +295,6 @@ describe('CreateProjectPage', () => { objectives: ProjectObjectivesFormInitialValues.objectives, location: ProjectLocationFormInitialValues.location, iucn: ProjectIUCNFormInitialValues.iucn, - funding: ProjectFundingFormInitialValues.funding, partnerships: ProjectPartnershipsFormInitialValues.partnerships } }); @@ -347,7 +342,6 @@ describe('CreateProjectPage', () => { objectives: ProjectObjectivesFormInitialValues.objectives, location: ProjectLocationFormInitialValues.location, iucn: ProjectIUCNFormInitialValues.iucn, - funding: ProjectFundingFormInitialValues.funding, partnerships: ProjectPartnershipsFormInitialValues.partnerships } }); @@ -446,7 +440,6 @@ describe('CreateProjectPage', () => { objectives: ProjectObjectivesFormInitialValues.objectives, location: ProjectLocationFormInitialValues.location, iucn: ProjectIUCNFormInitialValues.iucn, - funding: ProjectFundingFormInitialValues.funding, partnerships: ProjectPartnershipsFormInitialValues.partnerships } }); @@ -535,7 +528,6 @@ describe('CreateProjectPage', () => { objectives: { objectives: '' }, location: { location_description: '', geometry: [] }, iucn: { classificationDetails: [] }, - funding: { fundingSources: [] }, partnerships: { indigenous_partnerships: [], stakeholder_partnerships: [] } }); @@ -561,7 +553,6 @@ describe('CreateProjectPage', () => { objectives: ProjectObjectivesFormInitialValues.objectives, location: ProjectLocationFormInitialValues.location, iucn: ProjectIUCNFormInitialValues.iucn, - funding: ProjectFundingFormInitialValues.funding, partnerships: ProjectPartnershipsFormInitialValues.partnerships } }); @@ -613,7 +604,6 @@ describe('CreateProjectPage', () => { objectives: { objectives: '' }, location: { location_description: '', geometry: [] }, iucn: { classificationDetails: [] }, - funding: { fundingSources: [] }, partnerships: { indigenous_partnerships: [], stakeholder_partnerships: [] } }); diff --git a/app/src/features/projects/edit/EditProjectForm.tsx b/app/src/features/projects/edit/EditProjectForm.tsx index f2abefc853..1180d14f8d 100644 --- a/app/src/features/projects/edit/EditProjectForm.tsx +++ b/app/src/features/projects/edit/EditProjectForm.tsx @@ -11,8 +11,6 @@ import { IUpdateProjectRequest } from 'interfaces/useProjectApi.interface'; import React from 'react'; import ProjectCoordinatorForm from '../components/ProjectCoordinatorForm'; import ProjectDetailsForm from '../components/ProjectDetailsForm'; -import ProjectFundingForm from '../components/ProjectFundingForm'; -import { FundingSourceType } from '../components/ProjectFundingItemForm'; import ProjectIUCNForm from '../components/ProjectIUCNForm'; import ProjectLocationForm from '../components/ProjectLocationForm'; import ProjectObjectivesForm from '../components/ProjectObjectivesForm'; @@ -151,25 +149,7 @@ const EditProjectForm: React.FC = (props) => { Specify funding sources for the project. Note: Dollar amounts are not intended to be exact, please round to the nearest 100. - - { - return { value: item.id, label: item.name, type: FundingSourceType.FUNDING_SOURCE }; - }) || [] - } - investment_action_category={ - codes?.investment_action_category?.map((item) => { - return { value: item.id, agency_id: item.agency_id, label: item.name }; - }) || [] - } - first_nations={ - codes?.first_nations.map((item) => { - return { value: item.id, label: item.name, type: FundingSourceType.FIRST_NATIONS }; - }) || [] - } - /> - + diff --git a/app/src/features/projects/edit/EditProjectPage.tsx b/app/src/features/projects/edit/EditProjectPage.tsx index fa75f9889b..adab37ad67 100644 --- a/app/src/features/projects/edit/EditProjectPage.tsx +++ b/app/src/features/projects/edit/EditProjectPage.tsx @@ -76,7 +76,6 @@ const EditProjectPage: React.FC = (props) => { UPDATE_GET_ENTITIES.objectives, UPDATE_GET_ENTITIES.location, UPDATE_GET_ENTITIES.iucn, - UPDATE_GET_ENTITIES.funding, UPDATE_GET_ENTITIES.partnerships ]) ); diff --git a/app/src/features/projects/list/ProjectsListFilterForm.tsx b/app/src/features/projects/list/ProjectsListFilterForm.tsx index 02bcb0c161..155b47c005 100644 --- a/app/src/features/projects/list/ProjectsListFilterForm.tsx +++ b/app/src/features/projects/list/ProjectsListFilterForm.tsx @@ -45,11 +45,6 @@ const ProjectsListFilterForm: React.FC = (props) = return item.name; }) || [] } - funding_sources={ - codesContext.codesDataLoader.data.agency?.map((item) => { - return { value: item.id, label: item.name }; - }) || [] - } /> + diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index 490921b2bc..7342572fdd 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -56,6 +56,7 @@ const useStyles = makeStyles((theme: Theme) => ({ */ const FundingSourcesListPage: React.FC = () => { const [isModalOpen, setIsModalOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const classes = useStyles(); const biohubApi = useBiohubApi(); const dialogContext = useContext(DialogContext); @@ -100,6 +101,7 @@ const FundingSourcesListPage: React.FC = () => { // }; const handleSubmitDraft = async (values: IFundingSourceData) => { + setIsSubmitting(true); try { if (values.funding_source_id) { // edit the funding source @@ -107,7 +109,7 @@ const FundingSourcesListPage: React.FC = () => { } else { await biohubApi.funding.postFundingSource(values); } - + setIsSubmitting(false); setIsModalOpen(false); fundingSourceDataLoader.refresh(); @@ -166,6 +168,7 @@ const FundingSourcesListPage: React.FC = () => { , initialValues: { From 77fde912bba84c695f489cfb6f3174e1742ab771 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 8 Aug 2023 15:35:42 -0700 Subject: [PATCH 027/125] added error dialog for API --- .../list/FundingSourcesListPage.tsx | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index 7342572fdd..e9a0f8cddc 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -10,8 +10,11 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; import EditDialog from 'components/dialog/EditDialog'; +import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { CreateFundingSourceI18N } from 'constants/i18n'; import { CodesContext } from 'contexts/codesContext'; import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import React, { useContext, useEffect, useState } from 'react'; @@ -90,15 +93,16 @@ const FundingSourcesListPage: React.FC = () => { const fundingSourceDataLoader = useDataLoader(() => biohubApi.funding.getAllFundingSources()); fundingSourceDataLoader.load(); - // const showCreateErrorDialog = (textDialogProps?: Partial) => { - // dialogContext.setErrorDialog({ - // dialogTitle: CreateFundingSourceI18N.createErrorTitle, - // dialogText: CreateFundingSourceI18N.createErrorText, - // ...defaultErrorDialogProps, - // ...textDialogProps, - // open: true - // }); - // }; + const showCreateErrorDialog = (textDialogProps?: Partial) => { + dialogContext.setErrorDialog({ + dialogTitle: CreateFundingSourceI18N.createErrorTitle, + dialogText: CreateFundingSourceI18N.createErrorText, + onClose: () => dialogContext.setErrorDialog({ open: false }), + onOk: () => dialogContext.setErrorDialog({ open: false }), + ...textDialogProps, + open: true + }); + }; const handleSubmitDraft = async (values: IFundingSourceData) => { setIsSubmitting(true); @@ -109,7 +113,7 @@ const FundingSourcesListPage: React.FC = () => { } else { await biohubApi.funding.postFundingSource(values); } - setIsSubmitting(false); + setIsModalOpen(false); fundingSourceDataLoader.refresh(); @@ -125,8 +129,13 @@ const FundingSourcesListPage: React.FC = () => { open: true }); // refresh the list - } catch (error) { - console.log('Show an error dialog'); + } catch (error: any) { + console.log(error); + showCreateErrorDialog({ + dialogError: (error as APIError).message, + dialogErrorDetails: (error as APIError).errors + }); + setIsSubmitting(false); } }; From 8c3f8c9cef9ac7a4b9804bec823287b921dd1ce3 Mon Sep 17 00:00:00 2001 From: Kjartan Date: Tue, 8 Aug 2023 16:04:13 -0700 Subject: [PATCH 028/125] update openapi schema, put has revision_count, get --- api/src/models/survey-update.ts | 2 + api/src/models/survey-view.ts | 21 +++++++++ .../project/{projectId}/survey/create.ts | 16 +++++++ .../{projectId}/survey/{surveyId}/update.ts | 19 ++++++++ .../{projectId}/survey/{surveyId}/view.ts | 44 +++++++++++++++++ .../repositories/funding-source-repository.ts | 43 ++++++++++++++++- api/src/services/funding-source-service.ts | 25 +++++++++- api/src/services/survey-service.ts | 47 +++++++++++++++++-- 8 files changed, 208 insertions(+), 9 deletions(-) diff --git a/api/src/models/survey-update.ts b/api/src/models/survey-update.ts index 40df88c922..df5acb4906 100644 --- a/api/src/models/survey-update.ts +++ b/api/src/models/survey-update.ts @@ -25,10 +25,12 @@ export class PutSurveyObject { export class PutFundingSourceData { funding_source_id: number; amount: number; + revision_count: number; constructor(obj?: any) { this.funding_source_id = obj?.funding_source_id || null; this.amount = obj?.amount || null; + this.revision_count = obj?.revision_count || null; } } diff --git a/api/src/models/survey-view.ts b/api/src/models/survey-view.ts index db7c594a49..d75079b257 100644 --- a/api/src/models/survey-view.ts +++ b/api/src/models/survey-view.ts @@ -6,6 +6,7 @@ export type SurveyObject = { survey_details: GetSurveyData; species: GetFocalSpeciesData & GetAncillarySpeciesData; permit: GetPermitData; + funding_sources: GetSurveyFundingSourceData[]; purpose_and_methodology: GetSurveyPurposeAndMethodologyData; proprietor: GetSurveyProprietorData | null; location: GetSurveyLocationData; @@ -39,6 +40,26 @@ export class GetSurveyData { } } +export class GetSurveyFundingSourceData { + survey_funding_source_id: number; + survey_id: number; + funding_source_id: number; + amount: number; + start_date: string; + end_date: string; + revision_count?: number; + + constructor(obj?: any) { + this.survey_funding_source_id = obj?.survey_funding_source_id || null; + this.funding_source_id = obj?.funding_source_id || null; + this.survey_id = obj?.survey_id || null; + this.amount = obj?.amount || null; + this.start_date = obj?.start_date || null; + this.end_date = obj?.end_date || null; + this.revision_count = obj?.revision_count || 0; + } +} + export class GetFocalSpeciesData { focal_species: number[]; focal_species_names: string[]; diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index 01a6baedcb..59f225c0bb 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -126,6 +126,22 @@ POST.apiDoc = { } } }, + funding_sources: { + type: 'array', + items: { + type: 'object', + required: ['funding_source_id', 'amount'], + properties: { + funding_source_id: { + type: 'number', + minimum: 1 + }, + amount: { + type: 'number' + } + } + } + }, proprietor: { type: 'object', properties: { diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts index eb1b371d3e..3e9ec1ae4a 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts @@ -143,6 +143,25 @@ PUT.apiDoc = { } } }, + funding_sources: { + type: 'array', + items: { + type: 'object', + required: ['funding_source_id', 'amount', 'revision_count'], + properties: { + funding_source_id: { + type: 'number', + minimum: 1 + }, + amount: { + type: 'number' + }, + revision_count: { + type: 'number' + } + } + } + }, proprietor: { type: 'object', required: [ diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts index 4070e38148..535eb79247 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts @@ -166,6 +166,50 @@ GET.apiDoc = { } } }, + funding_sources: { + type: 'array', + items: { + type: 'object', + required: [ + 'survey_funding_source_id', + 'survey_id', + 'funding_source_id', + 'amount', + 'start_date', + 'end_date', + 'revision_count' + ], + properties: { + survey_funding_source_id: { + type: 'number', + minimum: 1 + }, + survey_id: { + type: 'number', + minimum: 1 + }, + funding_source_id: { + type: 'number', + minimum: 1 + }, + amount: { + type: 'number' + }, + start_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + description: 'ISO 8601 date string for the funding start_date' + }, + end_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + nullable: true, + description: 'ISO 8601 date string for the funding end_date' + }, + revision_count: { + type: 'number' + } + } + } + }, purpose_and_methodology: { description: 'Survey Details', type: 'object', diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts index b4b6fee5bd..9d6bf9ee9e 100644 --- a/api/src/repositories/funding-source-repository.ts +++ b/api/src/repositories/funding-source-repository.ts @@ -190,6 +190,38 @@ export class FundingSourceRepository extends BaseRepository { * SURVEY FUNDING FUNCTIONS */ + /** + * Fetch a single survey funding source by survey id and funding source id. + * + * @param {number} surveyId + * @param {number} fundingSourceId + * @return {*} {Promise} + * @memberof FundingSourceRepository + */ + async getSurveyFundingSourceByFundingSourceId( + surveyId: number, + fundingSourceId: number + ): Promise { + const sqlStatement = SQL` + SELECT + * + FROM + survey_funding_source + WHERE + survey_id = ${surveyId} + AND + funding_source_id = ${fundingSourceId}; + `; + const response = await this.connection.sql(sqlStatement, SurveyFundingSource); + if (response.rowCount !== 1) { + throw new ApiExecuteSQLError('Failed to get survey funding source', [ + 'FundingSourceRepository->getSurveyFundingSourceByFundingSourceId', + 'rowCount was != 1, expected rowCount = 1' + ]); + } + return response.rows[0]; + } + /** * Fetch all survey funding sources by survey id. * @@ -248,15 +280,22 @@ export class FundingSourceRepository extends BaseRepository { * @param {number} surveyId * @param {number} fundingSourceId * @param {number} amount + * @param {number} revision_count * @return {*} {Promise} * @memberof FundingSourceRepository */ - async putSurveyFundingSource(surveyId: number, fundingSourceId: number, amount: number): Promise { + async putSurveyFundingSource( + surveyId: number, + fundingSourceId: number, + amount: number, + revision_count: number + ): Promise { const sqlStatement = SQL` UPDATE survey_funding_source SET - amount = ${amount} + amount = ${amount}, + revision_count = ${revision_count} WHERE survey_id = ${surveyId} AND diff --git a/api/src/services/funding-source-service.ts b/api/src/services/funding-source-service.ts index b771f02bf8..8ee2efad58 100644 --- a/api/src/services/funding-source-service.ts +++ b/api/src/services/funding-source-service.ts @@ -79,6 +79,21 @@ export class FundingSourceService extends DBService { * SURVEY FUNDING SOURCE FUNCTIONS */ + /** + * Fetch a single survey funding source by survey id and funding source id. + * + * @param {number} surveyId + * @param {number} fundingSourceId + * @return {*} {Promise} + * @memberof FundingSourceService + */ + async getSurveyFundingSourceByFundingSourceId( + surveyId: number, + fundingSourceId: number + ): Promise { + return this.fundingSourceRepository.getSurveyFundingSourceByFundingSourceId(surveyId, fundingSourceId); + } + /** * Fetch all survey funding sources by survey id. * @@ -109,11 +124,17 @@ export class FundingSourceService extends DBService { * @param {number} surveyId * @param {number} fundingSourceId * @param {number} amount + * @param {number} revision_count * @return {*} {Promise} * @memberof FundingSourceService */ - async putSurveyFundingSource(surveyId: number, fundingSourceId: number, amount: number): Promise { - return this.fundingSourceRepository.putSurveyFundingSource(surveyId, fundingSourceId, amount); + async putSurveyFundingSource( + surveyId: number, + fundingSourceId: number, + amount: number, + revision_count: number + ): Promise { + return this.fundingSourceRepository.putSurveyFundingSource(surveyId, fundingSourceId, amount, revision_count); } /** diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index 779cad8fa0..434f963e2d 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -10,6 +10,7 @@ import { GetPermitData, GetReportAttachmentsData, GetSurveyData, + GetSurveyFundingSourceData, GetSurveyLocationData, GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData, @@ -84,6 +85,7 @@ export class SurveyService extends DBService { surveyData, speciesData, permitData, + fundingData, purposeAndMethodologyData, proprietorData, locationData @@ -91,6 +93,7 @@ export class SurveyService extends DBService { this.getSurveyData(surveyId), this.getSpeciesData(surveyId), this.getPermitData(surveyId), + this.getSurveyFundingData(surveyId), this.getSurveyPurposeAndMethodology(surveyId), this.getSurveyProprietorDataForView(surveyId), this.getSurveyLocationData(surveyId) @@ -100,12 +103,24 @@ export class SurveyService extends DBService { survey_details: surveyData, species: speciesData, permit: permitData, + funding_sources: fundingData, purpose_and_methodology: purposeAndMethodologyData, proprietor: proprietorData, location: locationData }; } + /** + * Get Survey funding data for a given survey ID + * + * @param {number} surveyId + * @return {*} {Promise} + * @memberof SurveyService + */ + async getSurveyFundingData(surveyId: number): Promise { + return await this.fundingSourceService.getSurveyFundingSources(surveyId); + } + /** * Get Survey supplementary data for a given survey ID * @@ -571,7 +586,7 @@ export class SurveyService extends DBService { } if (putSurveyData?.funding_sources) { - promises.push(this.updateSurveyFundingSourceData(surveyId, putSurveyData)); + promises.push(this.upsertSurveyFundingSourceData(surveyId, putSurveyData)); } if (putSurveyData?.proprietor) { @@ -690,10 +705,32 @@ export class SurveyService extends DBService { * @return {*} * @memberof SurveyService */ - async updateSurveyFundingSourceData(surveyId: number, surveyData: PutSurveyObject) { - return surveyData.funding_sources.map((fundingSource) => - this.fundingSourceService.putSurveyFundingSource(surveyId, fundingSource.funding_source_id, fundingSource.amount) - ); + async upsertSurveyFundingSourceData(surveyId: number, surveyData: PutSurveyObject) { + //loop for all funding source data + surveyData.funding_sources.forEach(async (fundingSource) => { + //check if funding exists + const checkFunding = await this.fundingSourceService.getSurveyFundingSourceByFundingSourceId( + surveyId, + fundingSource.funding_source_id + ); + + if (!checkFunding) { + //create funding source + return this.fundingSourceService.postSurveyFundingSource( + surveyId, + fundingSource.funding_source_id, + fundingSource.amount + ); + } else { + //update funding source + return this.fundingSourceService.putSurveyFundingSource( + surveyId, + fundingSource.funding_source_id, + fundingSource.amount, + fundingSource.revision_count + ); + } + }); } /** From 18ef9566d657a4a2911ea30eb18fa82410f9d101 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 8 Aug 2023 16:19:26 -0700 Subject: [PATCH 029/125] added UI styling --- app/src/components/dialog/EditDialog.tsx | 15 ++++++++- .../components/FundingSourceForm.tsx | 32 +++++++++++++------ .../list/FundingSourcesListPage.tsx | 1 + 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/src/components/dialog/EditDialog.tsx b/app/src/components/dialog/EditDialog.tsx index c7e6cb8929..ba25ff51ca 100644 --- a/app/src/components/dialog/EditDialog.tsx +++ b/app/src/components/dialog/EditDialog.tsx @@ -2,6 +2,7 @@ import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; import useTheme from '@mui/material/styles/useTheme'; import useMediaQuery from '@mui/material/useMediaQuery'; @@ -24,6 +25,14 @@ export interface IEditDialogProps { */ dialogTitle: string; + /** + * The dialog window content text. + * + * @type {string} + * @memberof IEditDialogProps + */ + dialogText?: string; + /** * The label of the `onSave` button. * @@ -107,9 +116,13 @@ export const EditDialog = (props: PropsWithChildren {props.dialogTitle} - {props.component.element} + + {props.dialogText} + {props.component.element} + { return (
- + - Name and description - - + + Name and description + + + + + - - Effective Dates + + + + Effective Dates + + + Effective date description + + { /> +
); diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index e9a0f8cddc..2b09af3edc 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -176,6 +176,7 @@ const FundingSourcesListPage: React.FC = () => { Date: Tue, 8 Aug 2023 16:22:03 -0700 Subject: [PATCH 030/125] clean up --- .../funding-sources/components/FundingSourceForm.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/features/funding-sources/components/FundingSourceForm.tsx b/app/src/features/funding-sources/components/FundingSourceForm.tsx index a05aab7982..3aca79e34c 100644 --- a/app/src/features/funding-sources/components/FundingSourceForm.tsx +++ b/app/src/features/funding-sources/components/FundingSourceForm.tsx @@ -12,13 +12,7 @@ export interface IFundingSourceData { start_date: string | null; end_date: string | null; } -/* - TODO: - - replace existing StartEndDateFields, not sure if this is possible - - make UI better - - look into fieldset child relationship - - display errors from api -*/ + const FundingSourceForm: React.FC = (props) => { const formikProps = useFormikContext(); const { handleSubmit } = formikProps; From 6b53c3b62d969c8fd119f2f908cf705a0471f3d3 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 8 Aug 2023 17:21:25 -0700 Subject: [PATCH 031/125] reorganized create funding component --- .../components/CreateFundingSource.tsx | 116 ++++++++++++++++++ .../list/FundingSourcesListPage.tsx | 108 ++-------------- 2 files changed, 124 insertions(+), 100 deletions(-) create mode 100644 app/src/features/funding-sources/components/CreateFundingSource.tsx diff --git a/app/src/features/funding-sources/components/CreateFundingSource.tsx b/app/src/features/funding-sources/components/CreateFundingSource.tsx new file mode 100644 index 0000000000..926dbf858f --- /dev/null +++ b/app/src/features/funding-sources/components/CreateFundingSource.tsx @@ -0,0 +1,116 @@ +import Typography from '@mui/material/Typography'; +import EditDialog from 'components/dialog/EditDialog'; +import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { CreateFundingSourceI18N } from 'constants/i18n'; +import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; +import { APIError } from 'hooks/api/useAxios'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { useContext, useState } from 'react'; +import yup from 'utils/YupSchema'; +import FundingSourceForm, { IFundingSourceData } from './FundingSourceForm'; + +interface ICreateFundingSourceProps { + isModalOpen: boolean; + closeModal: (refresh?: boolean) => void; +} + +const CreateFundingSource: React.FC = (props) => { + const [isSubmitting, setIsSubmitting] = useState(false); + const dialogContext = useContext(DialogContext); + + const biohubApi = useBiohubApi(); + + // This is placed inside the `FundingSourcesListPage` to make use of an API call to check for used names + // The API call would violate the rules of react hooks if placed in an object outside of the component + // Reference: https://react.dev/warnings/invalid-hook-call-warning + const FundingSourceYupSchema = yup.object().shape({ + name: yup + .string() + .required('A funding source name is required') + .test('nameUsed', 'This name has already been used', async (val) => { + let hasBeenUsed = false; + if (val) { + hasBeenUsed = await biohubApi.funding.hasFundingSourceNameBeenUsed(val); + } + return !hasBeenUsed; + }), + description: yup.string().max(200).required('A description is required'), + start_date: yup.string().isValidDateString().nullable(), + end_date: yup.string().isValidDateString().isEndDateSameOrAfterStartDate('start_date').nullable() + }); + + const showSnackBar = (textDialogProps?: Partial) => { + dialogContext.setSnackbar({ ...textDialogProps, open: true }); + }; + const showCreateErrorDialog = (textDialogProps?: Partial) => { + dialogContext.setErrorDialog({ + dialogTitle: CreateFundingSourceI18N.createErrorTitle, + dialogText: CreateFundingSourceI18N.createErrorText, + onClose: () => dialogContext.setErrorDialog({ open: false }), + onOk: () => dialogContext.setErrorDialog({ open: false }), + ...textDialogProps, + open: true + }); + }; + + const handleSubmitFundingService = async (values: IFundingSourceData) => { + setIsSubmitting(true); + try { + if (values.funding_source_id) { + // edit the funding source + await biohubApi.funding.putFundingSource(values); + } else { + await biohubApi.funding.postFundingSource(values); + } + + // callback to parent to update + props.closeModal(true); + + showSnackBar({ + snackbarMessage: ( + <> + + Funding Source: {values.name} has been created. + + + ), + open: true + }); + } catch (error: any) { + showCreateErrorDialog({ + dialogError: (error as APIError).message, + dialogErrorDetails: (error as APIError).errors + }); + setIsSubmitting(false); + } + }; + + return ( + <> + , + initialValues: { + funding_source_id: null, + name: '', + description: '', + start_date: null, + end_date: null + }, + validationSchema: FundingSourceYupSchema + }} + dialogSaveButtonLabel="Add" + onCancel={() => props.closeModal()} + onSave={(formValues) => { + handleSubmitFundingService(formValues); + }} + /> + + ); +}; + +export default CreateFundingSource; diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index 2b09af3edc..12baf3c772 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -9,17 +9,11 @@ import Paper from '@mui/material/Paper'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; -import EditDialog from 'components/dialog/EditDialog'; -import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; -import { CreateFundingSourceI18N } from 'constants/i18n'; import { CodesContext } from 'contexts/codesContext'; -import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; -import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import React, { useContext, useEffect, useState } from 'react'; -import yup from 'utils/YupSchema'; -import FundingSourceForm, { IFundingSourceData } from '../components/FundingSourceForm'; +import CreateFundingSource from '../components/CreateFundingSource'; import FundingSourcesTable from './FundingSourcesTable'; const useStyles = makeStyles((theme: Theme) => ({ @@ -58,85 +52,22 @@ const useStyles = makeStyles((theme: Theme) => ({ * @return {*} */ const FundingSourcesListPage: React.FC = () => { - const [isModalOpen, setIsModalOpen] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); + const [isCreateModelOpen, setIsCreateModalOpen] = useState(false); + const classes = useStyles(); const biohubApi = useBiohubApi(); - const dialogContext = useContext(DialogContext); - - // This is placed inside the `FundingSourcesListPage` to make use of an API call to check for used names - // The API call would violate the rules of react hooks if placed in an object outside of the component - // Reference: https://react.dev/warnings/invalid-hook-call-warning - const FundingSourceYupSchema = yup.object().shape({ - funding_source_id: yup.number().nullable(), - name: yup - .string() - .required('A funding source name is required') - .test('nameUsed', 'This name has already been used', async (val) => { - let hasBeenUsed = false; - if (val) { - hasBeenUsed = await biohubApi.funding.hasFundingSourceNameBeenUsed(val); - } - return !hasBeenUsed; - }), - description: yup.string().max(200).required('A description is required'), - start_date: yup.string().isValidDateString().nullable(), - end_date: yup.string().isValidDateString().isEndDateSameOrAfterStartDate('start_date').nullable() - }); - const showSnackBar = (textDialogProps?: Partial) => { - dialogContext.setSnackbar({ ...textDialogProps, open: true }); - }; const codesContext = useContext(CodesContext); useEffect(() => codesContext.codesDataLoader.load(), [codesContext.codesDataLoader]); const fundingSourceDataLoader = useDataLoader(() => biohubApi.funding.getAllFundingSources()); fundingSourceDataLoader.load(); - const showCreateErrorDialog = (textDialogProps?: Partial) => { - dialogContext.setErrorDialog({ - dialogTitle: CreateFundingSourceI18N.createErrorTitle, - dialogText: CreateFundingSourceI18N.createErrorText, - onClose: () => dialogContext.setErrorDialog({ open: false }), - onOk: () => dialogContext.setErrorDialog({ open: false }), - ...textDialogProps, - open: true - }); - }; - - const handleSubmitDraft = async (values: IFundingSourceData) => { - setIsSubmitting(true); - try { - if (values.funding_source_id) { - // edit the funding source - await biohubApi.funding.putFundingSource(values); - } else { - await biohubApi.funding.postFundingSource(values); - } - - setIsModalOpen(false); - + const closeCreateModal = (refresh?: boolean) => { + if (refresh) { fundingSourceDataLoader.refresh(); - - showSnackBar({ - snackbarMessage: ( - <> - - Funding Source: {values.name} has been created. - - - ), - open: true - }); - // refresh the list - } catch (error: any) { - console.log(error); - showCreateErrorDialog({ - dialogError: (error as APIError).message, - dialogErrorDetails: (error as APIError).errors - }); - setIsSubmitting(false); } + setIsCreateModalOpen(false); }; if (!codesContext.codesDataLoader.isReady || !fundingSourceDataLoader.isReady) { @@ -164,9 +95,7 @@ const FundingSourcesListPage: React.FC = () => { variant="contained" color="primary" startIcon={} - onClick={() => { - setIsModalOpen(true); - }}> + onClick={() => setIsCreateModalOpen(true)}> Add Funding Source
@@ -174,28 +103,7 @@ const FundingSourcesListPage: React.FC = () => {
- , - initialValues: { - funding_source_id: null, - name: '', - description: '', - start_date: null, - end_date: null - }, - validationSchema: FundingSourceYupSchema - }} - dialogSaveButtonLabel="Add" - onCancel={() => setIsModalOpen(false)} - onSave={(formValues) => { - handleSubmitDraft(formValues); - }} - /> + From f766972ac3fe722d90796f9cb46de8a9816de5d1 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 8 Aug 2023 17:25:28 -0700 Subject: [PATCH 032/125] clean up --- api/src/repositories/funding-source-repository.ts | 2 +- .../funding-sources/components/CreateFundingSource.tsx | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts index 9d6bf9ee9e..3963f6ece6 100644 --- a/api/src/repositories/funding-source-repository.ts +++ b/api/src/repositories/funding-source-repository.ts @@ -66,7 +66,7 @@ export class FundingSourceRepository extends BaseRepository { async postFundingSource(data: ICreateFundingSource): Promise> { const sql = SQL` - INSERT INTO funding_source ( + INSERT INTO funding_sources ( name, description, start_date, diff --git a/app/src/features/funding-sources/components/CreateFundingSource.tsx b/app/src/features/funding-sources/components/CreateFundingSource.tsx index 926dbf858f..9725d3d9e8 100644 --- a/app/src/features/funding-sources/components/CreateFundingSource.tsx +++ b/app/src/features/funding-sources/components/CreateFundingSource.tsx @@ -56,14 +56,9 @@ const CreateFundingSource: React.FC = (props) => { const handleSubmitFundingService = async (values: IFundingSourceData) => { setIsSubmitting(true); try { - if (values.funding_source_id) { - // edit the funding source - await biohubApi.funding.putFundingSource(values); - } else { - await biohubApi.funding.postFundingSource(values); - } + await biohubApi.funding.postFundingSource(values); - // callback to parent to update + // creation was a success, tell parent to refresh props.closeModal(true); showSnackBar({ From 972cc47c237b71c46125c41bd5ea97bc98ba3117 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 9 Aug 2023 09:07:00 -0700 Subject: [PATCH 033/125] fixed db table --- api/src/repositories/funding-source-repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts index 3963f6ece6..9d6bf9ee9e 100644 --- a/api/src/repositories/funding-source-repository.ts +++ b/api/src/repositories/funding-source-repository.ts @@ -66,7 +66,7 @@ export class FundingSourceRepository extends BaseRepository { async postFundingSource(data: ICreateFundingSource): Promise> { const sql = SQL` - INSERT INTO funding_sources ( + INSERT INTO funding_source ( name, description, start_date, From 945b2fe0dd9e9752f64e0cc1a00756973a9236c1 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 9 Aug 2023 10:59:18 -0700 Subject: [PATCH 034/125] comment --- .../features/funding-sources/components/CreateFundingSource.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/features/funding-sources/components/CreateFundingSource.tsx b/app/src/features/funding-sources/components/CreateFundingSource.tsx index 9725d3d9e8..ffcf668d60 100644 --- a/app/src/features/funding-sources/components/CreateFundingSource.tsx +++ b/app/src/features/funding-sources/components/CreateFundingSource.tsx @@ -20,7 +20,7 @@ const CreateFundingSource: React.FC = (props) => { const biohubApi = useBiohubApi(); - // This is placed inside the `FundingSourcesListPage` to make use of an API call to check for used names + // This is placed inside the `CreateFundingSource` component to make use of an API call to check for used names // The API call would violate the rules of react hooks if placed in an object outside of the component // Reference: https://react.dev/warnings/invalid-hook-call-warning const FundingSourceYupSchema = yup.object().shape({ From ebaff8c6bb42dab16358cf456d8522c15471efe5 Mon Sep 17 00:00:00 2001 From: Kjartan Date: Wed, 9 Aug 2023 11:05:31 -0700 Subject: [PATCH 035/125] zod fix for survey funding --- api/src/models/survey-view.ts | 6 +----- api/src/repositories/funding-source-repository.ts | 10 ++++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/api/src/models/survey-view.ts b/api/src/models/survey-view.ts index d75079b257..db79561ca8 100644 --- a/api/src/models/survey-view.ts +++ b/api/src/models/survey-view.ts @@ -44,9 +44,7 @@ export class GetSurveyFundingSourceData { survey_funding_source_id: number; survey_id: number; funding_source_id: number; - amount: number; - start_date: string; - end_date: string; + amount: string; revision_count?: number; constructor(obj?: any) { @@ -54,8 +52,6 @@ export class GetSurveyFundingSourceData { this.funding_source_id = obj?.funding_source_id || null; this.survey_id = obj?.survey_id || null; this.amount = obj?.amount || null; - this.start_date = obj?.start_date || null; - this.end_date = obj?.end_date || null; this.revision_count = obj?.revision_count || 0; } } diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts index 9d6bf9ee9e..9b46822084 100644 --- a/api/src/repositories/funding-source-repository.ts +++ b/api/src/repositories/funding-source-repository.ts @@ -18,9 +18,7 @@ const SurveyFundingSource = z.object({ survey_funding_source_id: z.number(), survey_id: z.number(), funding_source_id: z.number(), - amount: z.number(), - start_date: z.string(), - end_date: z.string(), + amount: z.string(), revision_count: z.number().optional() }); @@ -67,9 +65,9 @@ export class FundingSourceRepository extends BaseRepository { async postFundingSource(data: ICreateFundingSource): Promise> { const sql = SQL` INSERT INTO funding_source ( - name, - description, - start_date, + name, + description, + start_date, end_date, record_effective_date ) VALUES ( From f2a8cb00ef2ed18bbc9b486005805f63e005fc08 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Wed, 9 Aug 2023 11:27:28 -0700 Subject: [PATCH 036/125] SIMSBIOHUB-172: Added funding sources UI to survey --- app/src/features/surveys/CreateSurveyPage.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/app/src/features/surveys/CreateSurveyPage.tsx b/app/src/features/surveys/CreateSurveyPage.tsx index 66b4ab7517..a35e51dfb7 100644 --- a/app/src/features/surveys/CreateSurveyPage.tsx +++ b/app/src/features/surveys/CreateSurveyPage.tsx @@ -39,6 +39,8 @@ import PurposeAndMethodologyForm, { PurposeAndMethodologyYupSchema } from './components/PurposeAndMethodologyForm'; import StudyAreaForm, { StudyAreaInitialValues, StudyAreaYupSchema } from './components/StudyAreaForm'; +import { mdiPlus } from '@mdi/js'; +import Icon from '@mdi/react'; const useStyles = makeStyles((theme: Theme) => ({ actionButton: { @@ -326,6 +328,47 @@ const CreateSurveyPage = () => { + + + + Funding Sources + + + Specify funding sources for the project. Note: Dollar amounts are not intended to + be exact, please round to the nearest 100. + + + + + + + + Partnerships + + + Additional partnerships that have not been previously identified as a funding sources. + + +

Hello world

+
+
+ + }>
+ + + Date: Wed, 9 Aug 2023 11:53:23 -0700 Subject: [PATCH 037/125] SIMSBIOHUB-172: Added autocomplete; Refactored DollarAmountField --- .../fields/DollarAmountField.test.tsx | 31 -- .../components/fields/DollarAmountField.tsx | 13 +- app/src/features/surveys/CreateSurveyPage.tsx | 72 ++++- .../surveys/components/FundingSourceForm.tsx | 272 +++++++++++++++++ .../components/ProjectFundingItemForm.tsx | 285 ++++++++++++++++++ 5 files changed, 624 insertions(+), 49 deletions(-) delete mode 100644 app/src/components/fields/DollarAmountField.test.tsx create mode 100644 app/src/features/surveys/components/FundingSourceForm.tsx create mode 100644 app/src/features/surveys/components/ProjectFundingItemForm.tsx diff --git a/app/src/components/fields/DollarAmountField.test.tsx b/app/src/components/fields/DollarAmountField.test.tsx deleted file mode 100644 index 2925843859..0000000000 --- a/app/src/components/fields/DollarAmountField.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Formik } from 'formik'; -import { render } from 'test-helpers/test-utils'; -import DollarAmountField from './DollarAmountField'; - -describe('DollarAmountField', () => { - it('matches the snapshot without error', () => { - const { asFragment } = render( - {}}> - {() => } - - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - it('matches the snapshot with error', () => { - const { asFragment } = render( - {}} - initialErrors={{ amount: 'error is here' }} - initialTouched={{ amount: true }}> - {() => } - - ); - - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/app/src/components/fields/DollarAmountField.tsx b/app/src/components/fields/DollarAmountField.tsx index b5827a5e28..6754452533 100644 --- a/app/src/components/fields/DollarAmountField.tsx +++ b/app/src/components/fields/DollarAmountField.tsx @@ -1,15 +1,15 @@ -import TextField from '@mui/material/TextField'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; import { useFormikContext } from 'formik'; import get from 'lodash-es/get'; import React from 'react'; import NumberFormat, { NumberFormatProps } from 'react-number-format'; -export interface IDollarAmountFieldProps { +export type IDollarAmountFieldProps = (TextFieldProps & { required?: boolean; id: string; label: string; name: string; -} +}); interface NumberFormatCustomProps { onChange: (event: { target: { name: string; value: number } }) => void; @@ -44,15 +44,12 @@ const NumberFormatCustom = React.forwardRef = (props) => { const { values, handleChange, touched, errors } = useFormikContext(); - const { required, id, name, label } = props; + const { name, ...rest } = props; return ( ({ actionButton: { @@ -86,6 +87,8 @@ const CreateSurveyPage = () => { const biohubApi = useBiohubApi(); const history = useHistory(); + const [loadingFundingSources, setLoadingFundingSources] = useState(true); + const codesContext = useContext(CodesContext); useEffect(() => codesContext.codesDataLoader.load(), [codesContext.codesDataLoader]); const codes = codesContext.codesDataLoader.data; @@ -248,6 +251,11 @@ const CreateSurveyPage = () => { return ; } + const _tempFundingSources = [ + 0, + 1 + ] + return ( <> @@ -342,15 +350,59 @@ const CreateSurveyPage = () => { be exact, please round to the nearest 100. - + {_tempFundingSources.map((fundingSource) => { + return ( + + option.title === value.title} + //getOptionLabel={(option) => option.title} + options={[]} + loading={loadingFundingSources} + renderInput={(params) => ( + + {loadingFundingSources ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + ) + })} +
diff --git a/app/src/features/surveys/components/FundingSourceForm.tsx b/app/src/features/surveys/components/FundingSourceForm.tsx new file mode 100644 index 0000000000..4cfbf96de6 --- /dev/null +++ b/app/src/features/surveys/components/FundingSourceForm.tsx @@ -0,0 +1,272 @@ +import { mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import { Theme } from '@mui/material'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import { grey } from '@mui/material/colors'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; +import EditDialog from 'components/dialog/EditDialog'; +import { IMultiAutocompleteFieldOption } from 'components/fields/MultiAutocompleteFieldVariableSize'; +import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import { AddFundingI18N } from 'constants/i18n'; +import { FieldArray, FieldArrayRenderProps, useFormikContext } from 'formik'; +import { ICreateProjectRequest } from 'interfaces/useProjectApi.interface'; +import React, { useState } from 'react'; +import { getFormattedAmount, getFormattedDateRangeString } from 'utils/Utils'; +import yup from 'utils/YupSchema'; +import FundingSourceItemForm, { + IFundingSourceAutocompleteField, + IFundingSourceFormArrayItem, + FundingSourceFormArrayItemInitialValues, + FundingSourceFormArrayItemYupSchema +} from './FundingSourceItemForm'; + +export interface IFundingSourceForm { + funding: { + fundingSources: IFundingSourceFormArrayItem[]; + }; +} + +export const FundingSourceFormInitialValues: IFundingSourceForm = { + funding: { + fundingSources: [] + } +}; + +export const FundingSourceFormYupSchema = yup.object().shape({}); + +export interface IInvestmentActionCategoryOption extends IMultiAutocompleteFieldOption { + agency_id: number; +} + +export interface IFundingSourceFormProps { + funding_sources: IFundingSourceAutocompleteField[]; + investment_action_category: IInvestmentActionCategoryOption[]; + first_nations: IFundingSourceAutocompleteField[]; +} + +const useStyles = makeStyles((theme: Theme) => ({ + title: { + flexGrow: 1, + paddingTop: 0, + paddingBottom: 0, + marginRight: '1rem', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + fontWeight: 700 + }, + titleDesc: { + marginLeft: theme.spacing(1), + fontWeight: 400 + }, + fundingSourceItem: { + marginTop: theme.spacing(2), + borderColor: grey[400], + '& .MuiCardHeader-action': { + margin: '-8px 0' + }, + '& .MuiCardContent-root:last-child': { + paddingBottom: theme.spacing(2) + } + } +})); + +/** + * Create project - Funding section + * + * @return {*} + */ +const FundingSourceForm: React.FC = (props) => { + const classes = useStyles(); + + const formikProps = useFormikContext(); + const { values, handleSubmit } = formikProps; + + //Tracks information about the current funding source item that is being added/edited + const [currentFundingSourceFormArrayItem, setCurrentFundingSourceFormArrayItem] = useState({ + index: 0, + values: FundingSourceFormArrayItemInitialValues + }); + + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( +
+ + + + ( + + + ), + initialValues: currentFundingSourceFormArrayItem.values, + validationSchema: FundingSourceFormArrayItemYupSchema + }} + onCancel={() => setIsModalOpen(false)} + onSave={(projectFundingItemValues) => { + if (currentFundingSourceFormArrayItem.index < values.funding.fundingSources.length) { + // Update an existing item + arrayHelpers.replace(currentFundingSourceFormArrayItem.index, projectFundingItemValues); + } else { + // Add a new item + arrayHelpers.push(projectFundingItemValues); + } + + // Close the modal + setIsModalOpen(false); + }} + /> + + {values.funding.fundingSources.map((fundingSource, index) => { + const investment_action_category_label = + (fundingSource.agency_id === 1 && 'Investment Action') || + (fundingSource.agency_id === 2 && 'Investment Category') || + null; + const investment_action_category_value = props.investment_action_category.filter( + (item) => item.value === fundingSource.investment_action_category + )?.[0]?.label; + const key = [ + getCodeValueNameByID(props.first_nations, fundingSource.first_nations_id) || + getCodeValueNameByID(props.funding_sources, fundingSource.agency_id), + index + ].join('-'); + return ( + + + {getCodeValueNameByID(props.funding_sources, fundingSource?.agency_id) || + getCodeValueNameByID(props.first_nations, fundingSource?.first_nations_id)} + {investment_action_category_label && ( + ({investment_action_category_value}) + )} + + } + action={ + + { + setCurrentFundingSourceFormArrayItem({ + index: index, + values: values.funding.fundingSources[index] + }); + setIsModalOpen(true); + }}> + + + arrayHelpers.remove(index)}> + + + + }> + + {(fundingSource.agency_project_id || + fundingSource.funding_amount || + (fundingSource.start_date && fundingSource.end_date)) && ( + <> + + + + {fundingSource.agency_project_id && ( + <> + + + Agency Project ID + + {fundingSource.agency_project_id} + + + )} + {fundingSource.funding_amount && ( + <> + + + Funding Amount + + + {getFormattedAmount(fundingSource.funding_amount)} + + + + )} + {fundingSource.start_date && fundingSource.end_date && ( + <> + + + Start / End Date + + + {getFormattedDateRangeString( + DATE_FORMAT.ShortMediumDateFormat, + fundingSource.start_date, + fundingSource.end_date + )} + + + + )} + + + + )} + + ); + })} + + + )} + /> + + +
+ ); +}; + +export default FundingSourceForm; + +export const getCodeValueNameByID = (codeSet: IMultiAutocompleteFieldOption[], codeValueId?: number): string => { + if (!codeSet?.length || !codeValueId) { + return ''; + } + return codeSet.find((item) => item.value === codeValueId)?.label ?? ''; +}; diff --git a/app/src/features/surveys/components/ProjectFundingItemForm.tsx b/app/src/features/surveys/components/ProjectFundingItemForm.tsx new file mode 100644 index 0000000000..5252db5113 --- /dev/null +++ b/app/src/features/surveys/components/ProjectFundingItemForm.tsx @@ -0,0 +1,285 @@ +import Box from '@mui/material/Box'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import Grid from '@mui/material/Grid'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import Typography from '@mui/material/Typography'; +import CustomTextField from 'components/fields/CustomTextField'; +import DollarAmountField from 'components/fields/DollarAmountField'; +import StartEndDateFields from 'components/fields/StartEndDateFields'; +import FundingSourceAutocomplete, { + IAutocompleteFieldOptionWithType +} from 'features/projects/components/FundingSourceAutocomplete'; +import { useFormikContext } from 'formik'; +import React from 'react'; +import yup from 'utils/YupSchema'; +import { IInvestmentActionCategoryOption } from './FundingSourceForm'; + +export interface IProjectFundingFormArrayItem { + id: number; + agency_id?: number; + investment_action_category: number; + investment_action_category_name: string; + agency_project_id: string; + funding_amount?: number; + start_date?: string; + end_date?: string; + revision_count: number; + first_nations_id?: number; +} + +export const ProjectFundingFormArrayItemInitialValues: IProjectFundingFormArrayItem = { + id: 0, + agency_id: '' as unknown as number, + investment_action_category: '' as unknown as number, + investment_action_category_name: '', + agency_project_id: '', + funding_amount: undefined, + start_date: undefined, + end_date: undefined, + revision_count: 0, + first_nations_id: undefined +}; + +export const ProjectFundingFormArrayItemYupSchema = yup.object().shape( + { + // if agency_id is present, first_nations_id is no longer required + agency_id: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .nullable(true) + .when('first_nations_id', { + is: (first_nations_id: number) => !first_nations_id, + then: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .required('Required'), + otherwise: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .nullable(true) + }), + // if first_nations_id is present, agency_id is no longer required + first_nations_id: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .nullable(true) + .when('agency_id', { + is: (agency_id: number) => !agency_id, + then: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .required('Required'), + otherwise: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .nullable(true) + }), + investment_action_category: yup.number().nullable(true), + agency_project_id: yup.string().max(50, 'Cannot exceed 50 characters').nullable(true), + // funding amount is not required when a first nation is selected as a funding source + funding_amount: yup + .number() + .transform((value) => (isNaN(value) && null) || value) + .typeError('Must be a number') + .min(0, 'Must be a positive number') + .max(9999999999, 'Must be less than $9,999,999,999') + .when('first_nations_id', (val: any) => { + const rules = yup + .number() + .transform((value) => (isNaN(value) && null) || value) + .typeError('Must be a number') + .min(0, 'Must be a positive number') + .max(9999999999, 'Must be less than $9,999,999,999'); + if (!val) { + return rules.required('Required'); + } + + return rules.nullable(true); + }), + start_date: yup.string().when('first_nations_id', (val: any) => { + const rules = yup.string().isValidDateString(); + if (!val) { + return rules.required('Required'); + } + return rules.nullable(true); + }), + end_date: yup.string().when('first_nations_id', (val: any) => { + const rules = yup.string().isValidDateString().isEndDateAfterStartDate('start_date'); + if (!val) { + return rules.required('Required'); + } + return rules.nullable(true); + }) + }, + [['agency_id', 'first_nations_id']] // this prevents a cyclical dependency +); + +export enum FundingSourceType { + FUNDING_SOURCE, + FIRST_NATIONS +} +export interface IFundingSourceAutocompleteField { + value: number; + label: string; + type: FundingSourceType; +} +export interface IProjectFundingItemFormProps { + sources: IFundingSourceAutocompleteField[]; + investment_action_category: IInvestmentActionCategoryOption[]; +} + +/** + * A modal form for a single item of the project funding sources array. + * + * @See ProjectFundingForm.tsx + * + * @param {*} props + * @return {*} + */ +const ProjectFundingItemForm: React.FC = (props) => { + const formikProps = useFormikContext(); + const { values, touched, errors, handleChange, handleSubmit, setFieldValue } = formikProps; + // Only show investment_action_category if certain agency_id values are selected + // Toggle investment_action_category label and dropdown values based on chosen agency_id + const investment_action_category_label = + (values.agency_id === 1 && 'Investment Action') || (values.agency_id === 2 && 'Investment Category') || null; + + const findItemLabel = (id: number, type: FundingSourceType) => { + return props.sources.find((item) => item.value === id && item.type === type)?.label; + }; + + // find label for initial value + const mapInitialValue = ( + formValues?: IProjectFundingFormArrayItem + ): IAutocompleteFieldOptionWithType | undefined => { + if (formValues) { + const id = formValues.agency_id ?? formValues.first_nations_id ?? 0; + const type = formValues.agency_id ? FundingSourceType.FUNDING_SOURCE : FundingSourceType.FIRST_NATIONS; + + const initialValue = { + value: id, + type: type, + label: String(findItemLabel(id, type)) + } as IAutocompleteFieldOptionWithType; + + return initialValue; + } + }; + + return ( +
+ + + Agency Details + + + + + + { + // investment_action_category is dependent on agency_id, so reset it if agency_id changes + setFieldValue( + 'investment_action_category', + ProjectFundingFormArrayItemInitialValues.investment_action_category + ); + // first_nations_id AND agency_id cannot be present on the same funding source + // reset values when a change occurs to prevent that from happening + setFieldValue('first_nations_id', null); + setFieldValue('agency_id', null); + setFieldValue('first_nations_name', null); + setFieldValue('agency_name', null); + + if (options?.type === FundingSourceType.FIRST_NATIONS) { + setFieldValue('first_nations_id', options?.value); + setFieldValue('first_nations_name', options?.label); + setFieldValue('investment_action_category', 0); // first nations do not have an investment category + } else { + setFieldValue('agency_id', options?.value); + setFieldValue('agency_name', options?.label); + // If an agency_id with a `Not Applicable` investment_action_category is chosen, auto select + // it for the user. + if (event.target.value !== 1 && event.target.value !== 2) { + setFieldValue( + 'investment_action_category', + props.investment_action_category.find((item) => item.agency_id === options?.value)?.value + ); + } + } + }} + /> + + {errors.agency_id} + + + {investment_action_category_label && ( + + + {investment_action_category_label} + + {errors.investment_action_category} + + + )} + + + + + + + Funding Details + + + + + + + + + + +
+ ); +}; + +export default ProjectFundingItemForm; From f151a847322372f920dc617daebd06a5288d828f Mon Sep 17 00:00:00 2001 From: Kjartan Date: Wed, 9 Aug 2023 11:56:25 -0700 Subject: [PATCH 038/125] fix survey funding source interfaces --- api/src/paths/project/{projectId}/survey/create.ts | 2 +- .../project/{projectId}/survey/{surveyId}/update.ts | 2 +- .../project/{projectId}/survey/{surveyId}/view.ts | 13 +------------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index 59f225c0bb..33625fd177 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -137,7 +137,7 @@ POST.apiDoc = { minimum: 1 }, amount: { - type: 'number' + type: 'string' } } } diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts index 3e9ec1ae4a..054697bd26 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts @@ -154,7 +154,7 @@ PUT.apiDoc = { minimum: 1 }, amount: { - type: 'number' + type: 'string' }, revision_count: { type: 'number' diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts index 535eb79247..b71e4e71d1 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts @@ -175,8 +175,6 @@ GET.apiDoc = { 'survey_id', 'funding_source_id', 'amount', - 'start_date', - 'end_date', 'revision_count' ], properties: { @@ -193,16 +191,7 @@ GET.apiDoc = { minimum: 1 }, amount: { - type: 'number' - }, - start_date: { - oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], - description: 'ISO 8601 date string for the funding start_date' - }, - end_date: { - oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], - nullable: true, - description: 'ISO 8601 date string for the funding end_date' + type: 'string' }, revision_count: { type: 'number' From 736302d154efa85870b237ec377f812251c643bb Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 9 Aug 2023 13:44:28 -0700 Subject: [PATCH 039/125] some UI love --- .../components/CreateFundingSource.tsx | 6 +- .../components/FundingSourceForm.tsx | 55 +++++++------------ .../list/FundingSourcesListPage.tsx | 3 +- app/src/styles.scss | 2 +- 4 files changed, 27 insertions(+), 39 deletions(-) diff --git a/app/src/features/funding-sources/components/CreateFundingSource.tsx b/app/src/features/funding-sources/components/CreateFundingSource.tsx index ffcf668d60..fc47cb27b1 100644 --- a/app/src/features/funding-sources/components/CreateFundingSource.tsx +++ b/app/src/features/funding-sources/components/CreateFundingSource.tsx @@ -26,15 +26,15 @@ const CreateFundingSource: React.FC = (props) => { const FundingSourceYupSchema = yup.object().shape({ name: yup .string() - .required('A funding source name is required') - .test('nameUsed', 'This name has already been used', async (val) => { + .required('Name is required') + .test('nameUsed', 'Name has already been used', async (val) => { let hasBeenUsed = false; if (val) { hasBeenUsed = await biohubApi.funding.hasFundingSourceNameBeenUsed(val); } return !hasBeenUsed; }), - description: yup.string().max(200).required('A description is required'), + description: yup.string().max(200).required('Description is required'), start_date: yup.string().isValidDateString().nullable(), end_date: yup.string().isValidDateString().isEndDateSameOrAfterStartDate('start_date').nullable() }); diff --git a/app/src/features/funding-sources/components/FundingSourceForm.tsx b/app/src/features/funding-sources/components/FundingSourceForm.tsx index a05aab7982..8091665421 100644 --- a/app/src/features/funding-sources/components/FundingSourceForm.tsx +++ b/app/src/features/funding-sources/components/FundingSourceForm.tsx @@ -12,53 +12,40 @@ export interface IFundingSourceData { start_date: string | null; end_date: string | null; } -/* - TODO: - - replace existing StartEndDateFields, not sure if this is possible - - make UI better - - look into fieldset child relationship - - display errors from api -*/ const FundingSourceForm: React.FC = (props) => { const formikProps = useFormikContext(); const { handleSubmit } = formikProps; return (
- - - - Name and description + + + Name and description + - - - + - - - - Effective Dates - + + + Effective Dates + - Effective date description + Lorem ipsum dolor sit amet, consectetur adipiscing elit. - - - + -
); diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index 12baf3c772..f3fc693dba 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -94,9 +94,10 @@ const FundingSourcesListPage: React.FC = () => {
diff --git a/app/src/styles.scss b/app/src/styles.scss index 4ad9607aaf..a9d71406d1 100644 --- a/app/src/styles.scss +++ b/app/src/styles.scss @@ -12,7 +12,7 @@ fieldset { } legend.MuiTypography-root { - margin-bottom: 20px; + margin-bottom: 15px; padding: 0; font-size: 16px; font-weight: 700; From 3bc41809192e85273b03cb4a07b6b5e6fcd9f958 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 9 Aug 2023 13:55:35 -0700 Subject: [PATCH 040/125] updated endpoints and comments --- api/src/paths/funding-sources/index.ts | 6 +----- .../paths/funding-sources/{fundingSourceId}.ts | 4 ++-- app/src/constants/i18n.ts | 15 +++++++++++++-- .../components/CreateFundingSource.tsx | 10 +++++----- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/api/src/paths/funding-sources/index.ts b/api/src/paths/funding-sources/index.ts index 791669445f..b1ed8ce604 100644 --- a/api/src/paths/funding-sources/index.ts +++ b/api/src/paths/funding-sources/index.ts @@ -204,11 +204,7 @@ export function postFundingSource(): RequestHandler { const data = req.body; try { await connection.open(); - console.log('_________________'); - console.log('_________________'); - console.log('_________________'); - console.log('_________________'); - console.log(data); + const response = await service.postFundingSource(data); await connection.commit(); diff --git a/api/src/paths/funding-sources/{fundingSourceId}.ts b/api/src/paths/funding-sources/{fundingSourceId}.ts index 99b8118520..ecfaff6315 100644 --- a/api/src/paths/funding-sources/{fundingSourceId}.ts +++ b/api/src/paths/funding-sources/{fundingSourceId}.ts @@ -129,7 +129,7 @@ export const PUT: Operation = [ ] }; }), - getFundingSource() + putFundingSource() ]; PUT.apiDoc = { @@ -257,7 +257,7 @@ export const DELETE: Operation = [ ] }; }), - getFundingSource() + deleteFundingSource() ]; DELETE.apiDoc = { diff --git a/app/src/constants/i18n.ts b/app/src/constants/i18n.ts index 01d52922e1..17a34136cf 100644 --- a/app/src/constants/i18n.ts +++ b/app/src/constants/i18n.ts @@ -297,10 +297,21 @@ export const SubmitSurveyBiohubI18N = { submitSurveyBiohubNoSubmissionDataDialogText: 'No new data or information has been added to this survey to submit.' }; -export const CreateFundingSourceI18N = { +export const FundingSourceI18N = { cancelTitle: 'Discard changes and exit?', cancelText: 'Any changes you have made will not be saved. Do you want to proceed?', + // CREATE FUNDING SOURCE + createFundingSourceDialogTitle: 'Add New Funding Source', + createFundingSourceDialogText: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor sem. Aliquam erat volutpat. Donec placerat nisl magna, et faucibus arcu condimentum sed.', createErrorTitle: 'Error Creating Funding Source', createErrorText: - 'An error has occurred while attempting to create your funding source, please try again. If the error persists, please contact your system administrator.' + 'An error has occurred while attempting to create your funding source, please try again. If the error persists, please contact your system administrator.', + // UPDATE FUNDING SOURCE + updateFundingSourceDialogTitle: 'Update Funding Source', + updateFundingSourceDialogText: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor sem. Aliquam erat volutpat. Donec placerat nisl magna, et faucibus arcu condimentum sed.', + updateErrorTitle: 'Error Creating Funding Source', + updateErrorText: + 'An error has occurred while attempting to update your Funding Source, please try again. If the error persists, please contact your system administrator.' }; diff --git a/app/src/features/funding-sources/components/CreateFundingSource.tsx b/app/src/features/funding-sources/components/CreateFundingSource.tsx index fc47cb27b1..5b44d976b0 100644 --- a/app/src/features/funding-sources/components/CreateFundingSource.tsx +++ b/app/src/features/funding-sources/components/CreateFundingSource.tsx @@ -1,7 +1,7 @@ import Typography from '@mui/material/Typography'; import EditDialog from 'components/dialog/EditDialog'; import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; -import { CreateFundingSourceI18N } from 'constants/i18n'; +import { FundingSourceI18N } from 'constants/i18n'; import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; @@ -44,8 +44,8 @@ const CreateFundingSource: React.FC = (props) => { }; const showCreateErrorDialog = (textDialogProps?: Partial) => { dialogContext.setErrorDialog({ - dialogTitle: CreateFundingSourceI18N.createErrorTitle, - dialogText: CreateFundingSourceI18N.createErrorText, + dialogTitle: FundingSourceI18N.createErrorTitle, + dialogText: FundingSourceI18N.createErrorText, onClose: () => dialogContext.setErrorDialog({ open: false }), onOk: () => dialogContext.setErrorDialog({ open: false }), ...textDialogProps, @@ -83,8 +83,8 @@ const CreateFundingSource: React.FC = (props) => { return ( <> Date: Wed, 9 Aug 2023 14:12:42 -0700 Subject: [PATCH 041/125] SIMSBIOHUB-172: More refactoring --- app/src/features/surveys/CreateSurveyPage.tsx | 100 +---- .../surveys/components/FundingSourceForm.tsx | 407 +++++++++--------- 2 files changed, 205 insertions(+), 302 deletions(-) diff --git a/app/src/features/surveys/CreateSurveyPage.tsx b/app/src/features/surveys/CreateSurveyPage.tsx index 78cfb5feb9..7929ebda90 100644 --- a/app/src/features/surveys/CreateSurveyPage.tsx +++ b/app/src/features/surveys/CreateSurveyPage.tsx @@ -1,4 +1,4 @@ -import { Autocomplete, TextField, Theme } from '@mui/material'; +import { Theme } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; @@ -39,9 +39,7 @@ import PurposeAndMethodologyForm, { PurposeAndMethodologyYupSchema } from './components/PurposeAndMethodologyForm'; import StudyAreaForm, { StudyAreaInitialValues, StudyAreaYupSchema } from './components/StudyAreaForm'; -import { mdiPlus } from '@mdi/js'; -import Icon from '@mdi/react'; -import DollarAmountField from 'components/fields/DollarAmountField'; +import FundingSourceForm from './components/FundingSourceForm'; const useStyles = makeStyles((theme: Theme) => ({ actionButton: { @@ -87,8 +85,6 @@ const CreateSurveyPage = () => { const biohubApi = useBiohubApi(); const history = useHistory(); - const [loadingFundingSources, setLoadingFundingSources] = useState(true); - const codesContext = useContext(CodesContext); useEffect(() => codesContext.codesDataLoader.load(), [codesContext.codesDataLoader]); const codes = codesContext.codesDataLoader.data; @@ -251,11 +247,6 @@ const CreateSurveyPage = () => { return ; } - const _tempFundingSources = [ - 0, - 1 - ] - return ( <> @@ -340,83 +331,18 @@ const CreateSurveyPage = () => { title="Funding and Partnerships" summary="Specify project funding sources and additional partnerships." component={ - <> - - - Funding Sources - - - Specify funding sources for the project. Note: Dollar amounts are not intended to - be exact, please round to the nearest 100. - - - {_tempFundingSources.map((fundingSource) => { - return ( - - option.title === value.title} - //getOptionLabel={(option) => option.title} - options={[]} - loading={loadingFundingSources} - renderInput={(params) => ( - - {loadingFundingSources ? : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - /> - - - ) - })} - - - - - - Partnerships - - - Additional partnerships that have not been previously identified as a funding sources. - - -

Hello world

-
+ + + Add Funding Sources + + + Specify funding sources for the project. Note: Dollar amounts are not intended to + be exact, please round to the nearest 100. + + + - + }> diff --git a/app/src/features/surveys/components/FundingSourceForm.tsx b/app/src/features/surveys/components/FundingSourceForm.tsx index 4cfbf96de6..60ec7ac168 100644 --- a/app/src/features/surveys/components/FundingSourceForm.tsx +++ b/app/src/features/surveys/components/FundingSourceForm.tsx @@ -1,6 +1,6 @@ import { mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; -import { Theme } from '@mui/material'; +import { Autocomplete, CircularProgress, TextField, Theme } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; @@ -13,6 +13,7 @@ import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; import EditDialog from 'components/dialog/EditDialog'; +import DollarAmountField from 'components/fields/DollarAmountField'; import { IMultiAutocompleteFieldOption } from 'components/fields/MultiAutocompleteFieldVariableSize'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import { AddFundingI18N } from 'constants/i18n'; @@ -21,16 +22,125 @@ import { ICreateProjectRequest } from 'interfaces/useProjectApi.interface'; import React, { useState } from 'react'; import { getFormattedAmount, getFormattedDateRangeString } from 'utils/Utils'; import yup from 'utils/YupSchema'; -import FundingSourceItemForm, { - IFundingSourceAutocompleteField, - IFundingSourceFormArrayItem, - FundingSourceFormArrayItemInitialValues, - FundingSourceFormArrayItemYupSchema -} from './FundingSourceItemForm'; + +export interface IProjectFundingFormArrayItem { + id: number; + agency_id?: number; + investment_action_category: number; + investment_action_category_name: string; + agency_project_id: string; + funding_amount?: number; + start_date?: string; + end_date?: string; + revision_count: number; + first_nations_id?: number; +} + +export const ProjectFundingFormArrayItemInitialValues: IProjectFundingFormArrayItem = { + id: 0, + agency_id: '' as unknown as number, + investment_action_category: '' as unknown as number, + investment_action_category_name: '', + agency_project_id: '', + funding_amount: undefined, + start_date: undefined, + end_date: undefined, + revision_count: 0, + first_nations_id: undefined +}; + +export const ProjectFundingFormArrayItemYupSchema = yup.object().shape( + { + // if agency_id is present, first_nations_id is no longer required + agency_id: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .nullable(true) + .when('first_nations_id', { + is: (first_nations_id: number) => !first_nations_id, + then: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .required('Required'), + otherwise: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .nullable(true) + }), + // if first_nations_id is present, agency_id is no longer required + first_nations_id: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .nullable(true) + .when('agency_id', { + is: (agency_id: number) => !agency_id, + then: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .required('Required'), + otherwise: yup + .number() + .transform((value) => (isNaN(value) ? undefined : value)) + .nullable(true) + }), + investment_action_category: yup.number().nullable(true), + agency_project_id: yup.string().max(50, 'Cannot exceed 50 characters').nullable(true), + // funding amount is not required when a first nation is selected as a funding source + funding_amount: yup + .number() + .transform((value) => (isNaN(value) && null) || value) + .typeError('Must be a number') + .min(0, 'Must be a positive number') + .max(9999999999, 'Must be less than $9,999,999,999') + .when('first_nations_id', (val: any) => { + const rules = yup + .number() + .transform((value) => (isNaN(value) && null) || value) + .typeError('Must be a number') + .min(0, 'Must be a positive number') + .max(9999999999, 'Must be less than $9,999,999,999'); + if (!val) { + return rules.required('Required'); + } + + return rules.nullable(true); + }), + start_date: yup.string().when('first_nations_id', (val: any) => { + const rules = yup.string().isValidDateString(); + if (!val) { + return rules.required('Required'); + } + return rules.nullable(true); + }), + end_date: yup.string().when('first_nations_id', (val: any) => { + const rules = yup.string().isValidDateString().isEndDateAfterStartDate('start_date'); + if (!val) { + return rules.required('Required'); + } + return rules.nullable(true); + }) + }, + [['agency_id', 'first_nations_id']] // this prevents a cyclical dependency +); + +export enum FundingSourceType { + FUNDING_SOURCE, + FIRST_NATIONS +} +export interface IFundingSourceAutocompleteField { + value: number; + label: string; + type: FundingSourceType; +} +export interface IProjectFundingItemFormProps { + sources: IFundingSourceAutocompleteField[]; + investment_action_category: IInvestmentActionCategoryOption[]; +} + export interface IFundingSourceForm { funding: { - fundingSources: IFundingSourceFormArrayItem[]; + fundingSources: any[]; // TODO }; } @@ -46,227 +156,94 @@ export interface IInvestmentActionCategoryOption extends IMultiAutocompleteField agency_id: number; } -export interface IFundingSourceFormProps { - funding_sources: IFundingSourceAutocompleteField[]; - investment_action_category: IInvestmentActionCategoryOption[]; - first_nations: IFundingSourceAutocompleteField[]; -} - -const useStyles = makeStyles((theme: Theme) => ({ - title: { - flexGrow: 1, - paddingTop: 0, - paddingBottom: 0, - marginRight: '1rem', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - fontWeight: 700 - }, - titleDesc: { - marginLeft: theme.spacing(1), - fontWeight: 400 - }, - fundingSourceItem: { - marginTop: theme.spacing(2), - borderColor: grey[400], - '& .MuiCardHeader-action': { - margin: '-8px 0' - }, - '& .MuiCardContent-root:last-child': { - paddingBottom: theme.spacing(2) - } - } -})); - /** - * Create project - Funding section + * Create/edit survey - Funding section * * @return {*} */ -const FundingSourceForm: React.FC = (props) => { - const classes = useStyles(); - +const FundingSourceForm = () => { const formikProps = useFormikContext(); const { values, handleSubmit } = formikProps; + const [loadingFundingSources, setLoadingFundingSources] = useState(true); - //Tracks information about the current funding source item that is being added/edited - const [currentFundingSourceFormArrayItem, setCurrentFundingSourceFormArrayItem] = useState({ - index: 0, - values: FundingSourceFormArrayItemInitialValues - }); - - const [isModalOpen, setIsModalOpen] = useState(false); + const _tempFundingSources = [0, 1] return (
- - - - ( - - ( + + {_tempFundingSources.map((fundingSource, index) => { + return ( + + option.title === value.title} + //getOptionLabel={(option) => option.title} + options={[]} + loading={loadingFundingSources} + renderInput={(params) => ( + + {loadingFundingSources ? : null} + {params.InputProps.endAdornment} + + ), + }} /> - ), - initialValues: currentFundingSourceFormArrayItem.values, - validationSchema: FundingSourceFormArrayItemYupSchema - }} - onCancel={() => setIsModalOpen(false)} - onSave={(projectFundingItemValues) => { - if (currentFundingSourceFormArrayItem.index < values.funding.fundingSources.length) { - // Update an existing item - arrayHelpers.replace(currentFundingSourceFormArrayItem.index, projectFundingItemValues); - } else { - // Add a new item - arrayHelpers.push(projectFundingItemValues); - } - - // Close the modal - setIsModalOpen(false); - }} - /> - - {values.funding.fundingSources.map((fundingSource, index) => { - const investment_action_category_label = - (fundingSource.agency_id === 1 && 'Investment Action') || - (fundingSource.agency_id === 2 && 'Investment Category') || - null; - const investment_action_category_value = props.investment_action_category.filter( - (item) => item.value === fundingSource.investment_action_category - )?.[0]?.label; - const key = [ - getCodeValueNameByID(props.first_nations, fundingSource.first_nations_id) || - getCodeValueNameByID(props.funding_sources, fundingSource.agency_id), - index - ].join('-'); - return ( - - - {getCodeValueNameByID(props.funding_sources, fundingSource?.agency_id) || - getCodeValueNameByID(props.first_nations, fundingSource?.first_nations_id)} - {investment_action_category_label && ( - ({investment_action_category_value}) - )} - - } - action={ - - { - setCurrentFundingSourceFormArrayItem({ - index: index, - values: values.funding.fundingSources[index] - }); - setIsModalOpen(true); - }}> - - - arrayHelpers.remove(index)}> - - - - }> - - {(fundingSource.agency_project_id || - fundingSource.funding_amount || - (fundingSource.start_date && fundingSource.end_date)) && ( - <> - - - - {fundingSource.agency_project_id && ( - <> - - - Agency Project ID - - {fundingSource.agency_project_id} - - - )} - {fundingSource.funding_amount && ( - <> - - - Funding Amount - - - {getFormattedAmount(fundingSource.funding_amount)} - - - - )} - {fundingSource.start_date && fundingSource.end_date && ( - <> - - - Start / End Date - - - {getFormattedDateRangeString( - DATE_FORMAT.ShortMediumDateFormat, - fundingSource.start_date, - fundingSource.end_date - )} - - - - )} - - - - )} - - ); - })} + )} + /> + + + arrayHelpers.remove(index)} + sx={{ ml: -1 }} + > + + + - - )} - /> - - + ) + })} + + + )} + /> ); }; export default FundingSourceForm; - -export const getCodeValueNameByID = (codeSet: IMultiAutocompleteFieldOption[], codeValueId?: number): string => { - if (!codeSet?.length || !codeValueId) { - return ''; - } - return codeSet.find((item) => item.value === codeValueId)?.label ?? ''; -}; From 3c08b54fbe40306af4e9d67e9899397fca847d56 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Wed, 9 Aug 2023 14:57:21 -0700 Subject: [PATCH 042/125] SIMSBIOHUB-172: Updated interfaces --- .../surveys/components/FundingSourceForm.tsx | 176 ++++-------------- .../features/surveys/edit/EditSurveyForm.tsx | 23 ++- app/src/interfaces/useSurveyApi.interface.ts | 14 +- 3 files changed, 67 insertions(+), 146 deletions(-) diff --git a/app/src/features/surveys/components/FundingSourceForm.tsx b/app/src/features/surveys/components/FundingSourceForm.tsx index 60ec7ac168..28a867ebc9 100644 --- a/app/src/features/surveys/components/FundingSourceForm.tsx +++ b/app/src/features/surveys/components/FundingSourceForm.tsx @@ -1,160 +1,60 @@ -import { mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import { mdiPlus, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; -import { Autocomplete, CircularProgress, TextField, Theme } from '@mui/material'; +import { Autocomplete, CircularProgress, TextField } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import { grey } from '@mui/material/colors'; -import Divider from '@mui/material/Divider'; -import Grid from '@mui/material/Grid'; import IconButton from '@mui/material/IconButton'; -import Typography from '@mui/material/Typography'; -import { makeStyles } from '@mui/styles'; -import EditDialog from 'components/dialog/EditDialog'; import DollarAmountField from 'components/fields/DollarAmountField'; import { IMultiAutocompleteFieldOption } from 'components/fields/MultiAutocompleteFieldVariableSize'; -import { DATE_FORMAT } from 'constants/dateTimeFormats'; -import { AddFundingI18N } from 'constants/i18n'; import { FieldArray, FieldArrayRenderProps, useFormikContext } from 'formik'; import { ICreateProjectRequest } from 'interfaces/useProjectApi.interface'; -import React, { useState } from 'react'; -import { getFormattedAmount, getFormattedDateRangeString } from 'utils/Utils'; +import { IEditSurveyRequest } from 'interfaces/useSurveyApi.interface'; +import { useState } from 'react'; import yup from 'utils/YupSchema'; -export interface IProjectFundingFormArrayItem { - id: number; - agency_id?: number; - investment_action_category: number; - investment_action_category_name: string; - agency_project_id: string; - funding_amount?: number; - start_date?: string; - end_date?: string; +export interface ISurveyFundingSource { + funding_source_id: number; + amount: string | undefined; revision_count: number; - first_nations_id?: number; + survey_funding_source_id: number; + survey_id: number; } -export const ProjectFundingFormArrayItemInitialValues: IProjectFundingFormArrayItem = { - id: 0, - agency_id: '' as unknown as number, - investment_action_category: '' as unknown as number, - investment_action_category_name: '', - agency_project_id: '', - funding_amount: undefined, - start_date: undefined, - end_date: undefined, +export interface ISurveyFundingSourceForm { + funding_sources: ISurveyFundingSource[] +} + +export const FundingSourceFormInitialValues: ISurveyFundingSourceForm = { + funding_sources: [] +}; + +export const FundingSourceFormYupSchema = yup.object().shape({ + funding_sources: yup.array() +}); + + +export const FundingSourceInitialValues: ISurveyFundingSource = { + funding_source_id: 0, + amount: undefined, revision_count: 0, - first_nations_id: undefined + survey_funding_source_id: 0, + survey_id: 0 }; -export const ProjectFundingFormArrayItemYupSchema = yup.object().shape( +export const FundingSourceYupSchema = yup.object().shape( { - // if agency_id is present, first_nations_id is no longer required - agency_id: yup + funding_source_id: yup .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - .when('first_nations_id', { - is: (first_nations_id: number) => !first_nations_id, - then: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .required('Required'), - otherwise: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - }), - // if first_nations_id is present, agency_id is no longer required - first_nations_id: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - .when('agency_id', { - is: (agency_id: number) => !agency_id, - then: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .required('Required'), - otherwise: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - }), - investment_action_category: yup.number().nullable(true), - agency_project_id: yup.string().max(50, 'Cannot exceed 50 characters').nullable(true), - // funding amount is not required when a first nation is selected as a funding source + .required('Must select a Funding Source') + .min(1, 'Must select a valid Funding Source'), // TODO confirm that this is not triggered when the autocomplete is empty. funding_amount: yup .number() .transform((value) => (isNaN(value) && null) || value) .typeError('Must be a number') .min(0, 'Must be a positive number') - .max(9999999999, 'Must be less than $9,999,999,999') - .when('first_nations_id', (val: any) => { - const rules = yup - .number() - .transform((value) => (isNaN(value) && null) || value) - .typeError('Must be a number') - .min(0, 'Must be a positive number') - .max(9999999999, 'Must be less than $9,999,999,999'); - if (!val) { - return rules.required('Required'); - } - - return rules.nullable(true); - }), - start_date: yup.string().when('first_nations_id', (val: any) => { - const rules = yup.string().isValidDateString(); - if (!val) { - return rules.required('Required'); - } - return rules.nullable(true); - }), - end_date: yup.string().when('first_nations_id', (val: any) => { - const rules = yup.string().isValidDateString().isEndDateAfterStartDate('start_date'); - if (!val) { - return rules.required('Required'); - } - return rules.nullable(true); - }) - }, - [['agency_id', 'first_nations_id']] // this prevents a cyclical dependency -); - -export enum FundingSourceType { - FUNDING_SOURCE, - FIRST_NATIONS -} -export interface IFundingSourceAutocompleteField { - value: number; - label: string; - type: FundingSourceType; -} -export interface IProjectFundingItemFormProps { - sources: IFundingSourceAutocompleteField[]; - investment_action_category: IInvestmentActionCategoryOption[]; -} - - -export interface IFundingSourceForm { - funding: { - fundingSources: any[]; // TODO - }; -} - -export const FundingSourceFormInitialValues: IFundingSourceForm = { - funding: { - fundingSources: [] + .max(9999999999, 'Cannot exceed $9,999,999,999'), } -}; - -export const FundingSourceFormYupSchema = yup.object().shape({}); - -export interface IInvestmentActionCategoryOption extends IMultiAutocompleteFieldOption { - agency_id: number; -} +); /** * Create/edit survey - Funding section @@ -162,12 +62,10 @@ export interface IInvestmentActionCategoryOption extends IMultiAutocompleteField * @return {*} */ const FundingSourceForm = () => { - const formikProps = useFormikContext(); + const formikProps = useFormikContext(); const { values, handleSubmit } = formikProps; const [loadingFundingSources, setLoadingFundingSources] = useState(true); - const _tempFundingSources = [0, 1] - return (
@@ -175,11 +73,11 @@ const FundingSourceForm = () => { name="funding.fundingSources" render={(arrayHelpers: FieldArrayRenderProps) => ( - {_tempFundingSources.map((fundingSource, index) => { + {values.funding_sources.map((surveyFundingSource, index) => { return ( option.title === value.title} //getOptionLabel={(option) => option.title} @@ -202,7 +100,7 @@ const FundingSourceForm = () => { )} /> ({ actionButton: { @@ -181,6 +182,26 @@ const EditSurveyForm: React.FC = (props) => { + + + Add Funding Sources + + + Specify funding sources for the project. Note: Dollar amounts are not intended to + be exact, please round to the nearest 100. + + + + + + }> + + + Date: Wed, 9 Aug 2023 15:02:44 -0700 Subject: [PATCH 043/125] Added a todo comment --- app/src/interfaces/useFundingSourceApi.interface.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/interfaces/useFundingSourceApi.interface.ts b/app/src/interfaces/useFundingSourceApi.interface.ts index 48eeb6a65f..ef6e3873de 100644 --- a/app/src/interfaces/useFundingSourceApi.interface.ts +++ b/app/src/interfaces/useFundingSourceApi.interface.ts @@ -1,3 +1,4 @@ +// TODO is this supposed to have start and end date? export interface IGetFundingSourcesResponse { funding_source_id: number; name: string; From 9a013fdf21d00cb42f004e268d14a5739f79975d Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 9 Aug 2023 17:09:40 -0700 Subject: [PATCH 044/125] updated start end date fields to new date picker --- app/package.json | 3 +- .../components/fields/StartEndDateFields.tsx | 132 +++++++++--------- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/app/package.json b/app/package.json index 2926afd4fb..dbb33e037c 100644 --- a/app/package.json +++ b/app/package.json @@ -36,6 +36,7 @@ "@mui/styles": "^5.9.3", "@mui/system": "^5.12.3", "@mui/x-data-grid": "^6.3.1", + "@mui/x-date-pickers": "^6.11.0", "@react-keycloak/web": "^3.4.0", "@react-leaflet/core": "~1.0.2", "@tmcw/togeojson": "~4.2.0", @@ -54,7 +55,7 @@ "leaflet-fullscreen": "~1.0.2", "leaflet.locatecontrol": "~0.76.0", "lodash-es": "~4.17.21", - "moment": "~2.29.2", + "moment": "~2.29.4", "node-sass": "~4.14.1", "proj4": "^2.9.0", "qs": "~6.9.4", diff --git a/app/src/components/fields/StartEndDateFields.tsx b/app/src/components/fields/StartEndDateFields.tsx index 602928173d..69b9158aec 100644 --- a/app/src/components/fields/StartEndDateFields.tsx +++ b/app/src/components/fields/StartEndDateFields.tsx @@ -1,5 +1,7 @@ import Grid from '@mui/material/Grid'; -import TextField from '@mui/material/TextField'; +import { DatePicker } from '@mui/x-date-pickers'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DATE_FORMAT, DATE_LIMIT } from 'constants/dateTimeFormats'; import get from 'lodash-es/get'; import moment from 'moment'; @@ -21,7 +23,7 @@ interface IStartEndDateFieldsProps { */ const StartEndDateFields: React.FC = (props) => { const { - formikProps: { values, handleChange, errors, touched }, + formikProps: { values, errors, touched, setFieldValue }, startName, endName, startRequired, @@ -34,76 +36,72 @@ const StartEndDateFields: React.FC = (props) => { const rawEndDateValue = get(values, endName); const formattedStartDateValue = - (rawStartDateValue && - moment(rawStartDateValue).isValid() && - moment(rawStartDateValue).format(DATE_FORMAT.ShortDateFormat)) || - ''; + (rawStartDateValue && moment(rawStartDateValue).isValid() && moment(rawStartDateValue)) || null; const formattedEndDateValue = - (rawEndDateValue && - moment(rawEndDateValue).isValid() && - moment(rawEndDateValue).format(DATE_FORMAT.ShortDateFormat)) || - ''; + (rawEndDateValue && moment(rawEndDateValue).isValid() && moment(rawEndDateValue)) || null; return ( - - - + + + + { + setFieldValue('start)date', moment(value).format(DATE_FORMAT.ShortDateFormat)); + }} + /> + + + { + setFieldValue('end_date', moment(value).format(DATE_FORMAT.ShortDateFormat)); + }} + /> + - - - - + ); }; From 3e6affd115ec194482153f7a8fd7f89ba14d75e9 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Wed, 9 Aug 2023 17:11:20 -0700 Subject: [PATCH 045/125] clean up --- .../components/fields/StartEndDateFields.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/components/fields/StartEndDateFields.tsx b/app/src/components/fields/StartEndDateFields.tsx index 69b9158aec..13f572bee0 100644 --- a/app/src/components/fields/StartEndDateFields.tsx +++ b/app/src/components/fields/StartEndDateFields.tsx @@ -48,27 +48,27 @@ const StartEndDateFields: React.FC = (props) => { { - setFieldValue('start)date', moment(value).format(DATE_FORMAT.ShortDateFormat)); + setFieldValue('start_date', moment(value).format(DATE_FORMAT.ShortDateFormat)); }} /> @@ -76,15 +76,15 @@ const StartEndDateFields: React.FC = (props) => { Date: Wed, 9 Aug 2023 17:48:36 -0700 Subject: [PATCH 046/125] Update funding list page --- api/src/models/survey-view.ts | 2 +- api/src/paths/funding-sources/index.ts | 27 +++++++- .../project/{projectId}/survey/create.ts | 2 +- .../{projectId}/survey/{surveyId}/update.ts | 2 +- .../{projectId}/survey/{surveyId}/view.ts | 2 +- .../repositories/funding-source-repository.ts | 45 +++++++++++++- api/src/services/funding-source-service.ts | 24 +++++-- api/src/services/survey-service.ts | 2 +- .../list/FundingSourcesTable.tsx | 62 +++++++++++++++++-- app/src/hooks/api/useFundingSourceApi.test.ts | 7 ++- app/src/hooks/api/useFundingSourceApi.ts | 7 ++- .../useFundingSourceApi.interface.ts | 14 +++++ app/src/utils/Utils.ts | 19 ++++-- 13 files changed, 188 insertions(+), 27 deletions(-) diff --git a/api/src/models/survey-view.ts b/api/src/models/survey-view.ts index db79561ca8..6173f0fcb2 100644 --- a/api/src/models/survey-view.ts +++ b/api/src/models/survey-view.ts @@ -44,7 +44,7 @@ export class GetSurveyFundingSourceData { survey_funding_source_id: number; survey_id: number; funding_source_id: number; - amount: string; + amount: number; revision_count?: number; constructor(obj?: any) { diff --git a/api/src/paths/funding-sources/index.ts b/api/src/paths/funding-sources/index.ts index 791669445f..181165528e 100644 --- a/api/src/paths/funding-sources/index.ts +++ b/api/src/paths/funding-sources/index.ts @@ -39,7 +39,14 @@ GET.apiDoc = { type: 'array', items: { type: 'object', - required: ['funding_source_id', 'name', 'description', 'revision_count'], + required: [ + 'funding_source_id', + 'name', + 'description', + 'revision_count', + 'survey_reference_count', + 'survey_reference_amount_total' + ], properties: { funding_source_id: { type: 'integer', @@ -51,9 +58,27 @@ GET.apiDoc = { description: { type: 'string' }, + start_date: { + type: 'string', + nullable: true + }, + end_date: { + type: 'string', + nullable: true + }, revision_count: { type: 'integer', minimum: 0 + }, + survey_reference_count: { + type: 'number', + minimum: 0, + description: 'The number of surveys that reference this funding source.' + }, + survey_reference_amount_total: { + type: 'number', + minimum: 0, + description: 'The total amount from all references to this funding source by all surveys.' } } } diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index 33625fd177..59f225c0bb 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -137,7 +137,7 @@ POST.apiDoc = { minimum: 1 }, amount: { - type: 'string' + type: 'number' } } } diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts index 054697bd26..3e9ec1ae4a 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts @@ -154,7 +154,7 @@ PUT.apiDoc = { minimum: 1 }, amount: { - type: 'string' + type: 'number' }, revision_count: { type: 'number' diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts index b71e4e71d1..7327fded2d 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts @@ -191,7 +191,7 @@ GET.apiDoc = { minimum: 1 }, amount: { - type: 'string' + type: 'number' }, revision_count: { type: 'number' diff --git a/api/src/repositories/funding-source-repository.ts b/api/src/repositories/funding-source-repository.ts index 9b46822084..2596b2eb82 100644 --- a/api/src/repositories/funding-source-repository.ts +++ b/api/src/repositories/funding-source-repository.ts @@ -9,16 +9,25 @@ const FundingSource = z.object({ funding_source_id: z.number(), name: z.string(), description: z.string(), + start_date: z.string().nullable(), + end_date: z.string().nullable(), revision_count: z.number().optional() }); export type FundingSource = z.infer; +const FundingSourceBasicSupplementaryData = z.object({ + survey_reference_count: z.number(), + survey_reference_amount_total: z.number() +}); + +export type FundingSourceBasicSupplementaryData = z.infer; + const SurveyFundingSource = z.object({ survey_funding_source_id: z.number(), survey_id: z.number(), funding_source_id: z.number(), - amount: z.string(), + amount: z.number(), revision_count: z.number().optional() }); @@ -184,6 +193,40 @@ export class FundingSourceRepository extends BaseRepository { return response.rows[0]; } + /** + * Fetch basic supplementary data for a single funding source. + * + * @param {number} fundingSourceId + * @return {*} {Promise} + * @memberof FundingSourceRepository + */ + async getFundingSourceBasicSupplementaryData(fundingSourceId: number): Promise { + const sqlStatement = SQL` + SELECT + COUNT(survey_funding_source.funding_source_id)::int as survey_reference_count, + SUM(survey_funding_source.amount)::numeric::int as survey_reference_amount_total + FROM + funding_source + LEFT JOIN + survey_funding_source + ON + funding_source.funding_source_id = survey_funding_source.funding_source_id + WHERE + funding_source.funding_source_id = ${fundingSourceId}; + `; + + const response = await this.connection.sql(sqlStatement, FundingSourceBasicSupplementaryData); + + if (response.rowCount !== 1) { + throw new ApiExecuteSQLError('Failed to get funding source basic supplementary data', [ + 'FundingSourceRepository->getFundingSourceBasicSupplementaryData', + 'rowCount was != 1, expected rowCount = 1' + ]); + } + + return response.rows[0]; + } + /* * SURVEY FUNDING FUNCTIONS */ diff --git a/api/src/services/funding-source-service.ts b/api/src/services/funding-source-service.ts index 8ee2efad58..09ed7fb59d 100644 --- a/api/src/services/funding-source-service.ts +++ b/api/src/services/funding-source-service.ts @@ -1,5 +1,10 @@ import { IDBConnection } from '../database/db'; -import { FundingSource, FundingSourceRepository, SurveyFundingSource } from '../repositories/funding-source-repository'; +import { + FundingSource, + FundingSourceBasicSupplementaryData, + FundingSourceRepository, + SurveyFundingSource +} from '../repositories/funding-source-repository'; import { DBService } from './db-service'; export interface IFundingSourceSearchParams { @@ -25,11 +30,22 @@ export class FundingSourceService extends DBService { /** * Get all funding sources. * - * @return {*} {Promise} + * @return {*} {(Promise<(FundingSource | FundingSourceBasicSupplementaryData)[]>)} * @memberof FundingSourceService */ - async getFundingSources(searchParams: IFundingSourceSearchParams): Promise { - return this.fundingSourceRepository.getFundingSources(searchParams); + async getFundingSources( + searchParams: IFundingSourceSearchParams + ): Promise<(FundingSource | FundingSourceBasicSupplementaryData)[]> { + const fundingSources = await this.fundingSourceRepository.getFundingSources(searchParams); + + return Promise.all( + fundingSources.map(async (fundingSource) => { + const basicSupplementalData = await this.fundingSourceRepository.getFundingSourceBasicSupplementaryData( + fundingSource.funding_source_id + ); + return { ...fundingSource, ...basicSupplementalData }; + }) + ); } /** diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts index 434f963e2d..68a5ef337c 100644 --- a/api/src/services/survey-service.ts +++ b/api/src/services/survey-service.ts @@ -118,7 +118,7 @@ export class SurveyService extends DBService { * @memberof SurveyService */ async getSurveyFundingData(surveyId: number): Promise { - return await this.fundingSourceService.getSurveyFundingSources(surveyId); + return this.fundingSourceService.getSurveyFundingSources(surveyId); } /** diff --git a/app/src/features/funding-sources/list/FundingSourcesTable.tsx b/app/src/features/funding-sources/list/FundingSourcesTable.tsx index 21725b205d..68edfc4593 100644 --- a/app/src/features/funding-sources/list/FundingSourcesTable.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesTable.tsx @@ -1,12 +1,15 @@ +import { mdiInformationOutline, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; import { Theme } from '@mui/material'; import { grey } from '@mui/material/colors'; import Link from '@mui/material/Link'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; -import { DataGrid, GridColDef, GridOverlay } from '@mui/x-data-grid'; +import { DataGrid, GridActionsCellItem, GridColDef, GridOverlay } from '@mui/x-data-grid'; import { IGetFundingSourcesResponse } from 'interfaces/useFundingSourceApi.interface'; import { useCallback } from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import { getFormattedAmount } from 'utils/Utils'; const useStyles = makeStyles((theme: Theme) => ({ projectsTable: { @@ -47,7 +50,8 @@ export interface IFundingSourcesTableTableProps { interface IFundingSourcesTableEntry { funding_source_id: number; name: string; - description: string; + survey_reference_count: number; + survey_reference_amount_total: number; } const NoRowsOverlay = (props: { className: string }) => ( @@ -61,6 +65,18 @@ const NoRowsOverlay = (props: { className: string }) => ( const FundingSourcesTable = (props: IFundingSourcesTableTableProps) => { const classes = useStyles(); + const handleViewDetails = (row: IFundingSourcesTableEntry) => { + // TOOD + }; + + const handleEdit = (row: IFundingSourcesTableEntry) => { + // TOOD + }; + + const handleDelete = (row: IFundingSourcesTableEntry) => { + // TOOD + }; + const columns: GridColDef[] = [ { field: 'name', @@ -80,9 +96,43 @@ const FundingSourcesTable = (props: IFundingSourcesTableTableProps) => { ) }, { - field: 'description', - headerName: 'Description', + field: 'survey_reference_amount_total', + headerName: 'Amount Distributed', + flex: 1, + valueGetter: (params) => { + return getFormattedAmount(params.value, { maximumFractionDigits: 2 }); + } + }, + { + field: 'survey_reference_count', + headerName: 'Surveys', flex: 1 + }, + { + field: 'actions', + type: 'actions', + getActions: (params) => { + return [ + } + label="View Details" + onClick={() => handleViewDetails(params.row)} + showInMenu + />, + } + label="Edit" + onClick={() => handleEdit(params.row)} + showInMenu + />, + } + label="Delete" + onClick={() => handleDelete(params.row)} + showInMenu + /> + ]; + } } ]; @@ -104,8 +154,8 @@ const FundingSourcesTable = (props: IFundingSourcesTableTableProps) => { disableColumnFilter disableColumnMenu sortingOrder={['asc', 'desc']} - components={{ - NoRowsOverlay: NoRowsOverlayStyled + slots={{ + noRowsOverlay: NoRowsOverlayStyled }} /> ); diff --git a/app/src/hooks/api/useFundingSourceApi.test.ts b/app/src/hooks/api/useFundingSourceApi.test.ts index 3081a67d07..20d41d2f96 100644 --- a/app/src/hooks/api/useFundingSourceApi.test.ts +++ b/app/src/hooks/api/useFundingSourceApi.test.ts @@ -20,7 +20,12 @@ describe('useFundingSourceApi', () => { { funding_source_id: 1, name: 'name', - description: 'description' + description: 'description', + start_date: null, + end_date: null, + revision_count: 0, + survey_reference_count: 1, + survey_reference_amount_total: 1000 } ]; diff --git a/app/src/hooks/api/useFundingSourceApi.ts b/app/src/hooks/api/useFundingSourceApi.ts index d2ab43979c..d18e08b47c 100644 --- a/app/src/hooks/api/useFundingSourceApi.ts +++ b/app/src/hooks/api/useFundingSourceApi.ts @@ -78,11 +78,12 @@ const useFundingSourceApi = (axios: AxiosInstance) => { /** * Update a single funding source. * - * * @param {number} fundingSourceId - * @return {*} {Promise} + * @return {*} {Promise>} */ - const putFundingSource = async (fundingSource: IFundingSourceData): Promise => { + const putFundingSource = async ( + fundingSource: IFundingSourceData + ): Promise> => { const { data } = await axios.put(`/api/funding-sources/${fundingSource.funding_source_id}`, fundingSource); return data; diff --git a/app/src/interfaces/useFundingSourceApi.interface.ts b/app/src/interfaces/useFundingSourceApi.interface.ts index 48eeb6a65f..f2854ec0b1 100644 --- a/app/src/interfaces/useFundingSourceApi.interface.ts +++ b/app/src/interfaces/useFundingSourceApi.interface.ts @@ -2,4 +2,18 @@ export interface IGetFundingSourcesResponse { funding_source_id: number; name: string; description: string; + start_date: string | null; + end_date: string | null; + revision_count: number; + survey_reference_count: number; + survey_reference_amount_total: number; +} + +export interface IGetFundingSourceResponse { + funding_source_id: number; + name: string; + description: string; + start_date: string | null; + end_date: string | null; + revision_count: number; } diff --git a/app/src/utils/Utils.ts b/app/src/utils/Utils.ts index 7711923835..035d47e92c 100644 --- a/app/src/utils/Utils.ts +++ b/app/src/utils/Utils.ts @@ -127,20 +127,27 @@ export const getFormattedTime = (timeFormat: TIME_FORMAT, date: string): string /** * Get a formatted amount string. * - * @param {number} amount + * @param {number} [amount] + * @param {{ minimumFractionDigits: number; maximumFractionDigits: number }} [options] * @return {string} formatted amount string (rounded to the nearest integer), or an empty string if unable to parse the amount */ -export const getFormattedAmount = (amount?: number): string => { +export const getFormattedAmount = ( + amount?: number, + options?: { + minimumFractionDigits?: number; + maximumFractionDigits?: number; + } +): string => { if (!amount && amount !== 0) { //amount was invalid return ''; } - const formatter = new Intl.NumberFormat('en-US', { + const formatter = new Intl.NumberFormat('en-CA', { style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - maximumFractionDigits: 0 + currency: 'CAD', + minimumFractionDigits: options?.minimumFractionDigits ?? 0, + maximumFractionDigits: options?.maximumFractionDigits ?? 0 }); return formatter.format(amount); From 1d0cb83beefe4126c8675523de88b17a4c61295d Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Wed, 9 Aug 2023 19:39:19 -0700 Subject: [PATCH 047/125] SIMSBIOHUB-172: Amend Yup schema values --- .../surveys/components/FundingSourceForm.tsx | 76 +++++++++++-------- .../features/surveys/edit/EditSurveyForm.tsx | 3 +- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/app/src/features/surveys/components/FundingSourceForm.tsx b/app/src/features/surveys/components/FundingSourceForm.tsx index 28a867ebc9..377f63a1ed 100644 --- a/app/src/features/surveys/components/FundingSourceForm.tsx +++ b/app/src/features/surveys/components/FundingSourceForm.tsx @@ -5,11 +5,12 @@ import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import DollarAmountField from 'components/fields/DollarAmountField'; -import { IMultiAutocompleteFieldOption } from 'components/fields/MultiAutocompleteFieldVariableSize'; +import { IFundingSourceData } from 'features/funding-sources/components/FundingSourceForm'; import { FieldArray, FieldArrayRenderProps, useFormikContext } from 'formik'; -import { ICreateProjectRequest } from 'interfaces/useProjectApi.interface'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; +import { IGetFundingSourcesResponse } from 'interfaces/useFundingSourceApi.interface'; import { IEditSurveyRequest } from 'interfaces/useSurveyApi.interface'; -import { useState } from 'react'; import yup from 'utils/YupSchema'; export interface ISurveyFundingSource { @@ -24,24 +25,23 @@ export interface ISurveyFundingSourceForm { funding_sources: ISurveyFundingSource[] } -export const FundingSourceFormInitialValues: ISurveyFundingSourceForm = { +export const SurveyFundingSourceFormInitialValues: ISurveyFundingSourceForm = { funding_sources: [] }; -export const FundingSourceFormYupSchema = yup.object().shape({ +export const SurveyFundingSourceFormYupSchema = yup.object().shape({ funding_sources: yup.array() }); - -export const FundingSourceInitialValues: ISurveyFundingSource = { - funding_source_id: 0, +export const SurveyFundingSourceInitialValues: ISurveyFundingSource = { + funding_source_id: undefined as unknown as number, amount: undefined, revision_count: 0, survey_funding_source_id: 0, survey_id: 0 }; -export const FundingSourceYupSchema = yup.object().shape( +export const SurveyFundingSourceYupSchema = yup.object().shape( { funding_source_id: yup .number() @@ -63,35 +63,57 @@ export const FundingSourceYupSchema = yup.object().shape( */ const FundingSourceForm = () => { const formikProps = useFormikContext(); - const { values, handleSubmit } = formikProps; - const [loadingFundingSources, setLoadingFundingSources] = useState(true); + const { values, handleChange, setFieldValue, handleSubmit } = formikProps; + + const biohubApi = useBiohubApi(); + const fundingSourcesDataLoader = useDataLoader(() => biohubApi.funding.getAllFundingSources()); + fundingSourcesDataLoader.load(); + + const fundingSources = fundingSourcesDataLoader.data ?? []; + + console.log({ values, fundingSources }) return ( ( {values.funding_sources.map((surveyFundingSource, index) => { + const value = fundingSources.find((fundingSource) => fundingSource.funding_source_id === surveyFundingSource.funding_source_id) + console.log('value=', value) return ( - + id={`funding_sources.[${index}].funding_source_id`} + value={value} + onChange={(_event, option) => { + setFieldValue(`funding_sources.[${index}].funding_source_id`, option?.funding_source_id) + }} sx={{ flex: 6 }} - //isOptionEqualToValue={(option, value) => option.title === value.title} - //getOptionLabel={(option) => option.title} - options={[]} - loading={loadingFundingSources} + isOptionEqualToValue={(option, value) => option.funding_source_id === value.funding_source_id} + getOptionLabel={(option) => option.name} + + options={fundingSources} + + /* + options={fundingSources.map((fundingSource) => ({ + value: fundingSource.funding_source_id, + label: fundingSource.name + }))} + */ + loading={fundingSourcesDataLoader.isLoading} renderInput={(params) => ( - {loadingFundingSources ? : null} + {fundingSourcesDataLoader.isLoading ? : null} {params.InputProps.endAdornment} ), @@ -100,9 +122,11 @@ const FundingSourceForm = () => { )} /> @@ -126,15 +150,7 @@ const FundingSourceForm = () => { title="Add Funding Source" aria-label="Add Funding Source" startIcon={} - onClick={() => { - /* - setCurrentProjectFundingFormArrayItem({ - index: values.funding.fundingSources.length, - values: ProjectFundingFormArrayItemInitialValues - }); - setIsModalOpen(true); - */ - }}> + onClick={() => arrayHelpers.push(SurveyFundingSourceInitialValues)}> Add Funding Source diff --git a/app/src/features/surveys/edit/EditSurveyForm.tsx b/app/src/features/surveys/edit/EditSurveyForm.tsx index f96b47b135..3b3de61954 100644 --- a/app/src/features/surveys/edit/EditSurveyForm.tsx +++ b/app/src/features/surveys/edit/EditSurveyForm.tsx @@ -24,7 +24,7 @@ import GeneralInformationForm, { import ProprietaryDataForm, { ProprietaryDataYupSchema } from '../components/ProprietaryDataForm'; import PurposeAndMethodologyForm, { PurposeAndMethodologyYupSchema } from '../components/PurposeAndMethodologyForm'; import StudyAreaForm, { StudyAreaInitialValues, StudyAreaYupSchema } from '../components/StudyAreaForm'; -import FundingSourceForm from '../components/FundingSourceForm'; +import FundingSourceForm, { SurveyFundingSourceFormInitialValues } from '../components/FundingSourceForm'; const useStyles = makeStyles((theme: Theme) => ({ actionButton: { @@ -69,6 +69,7 @@ const EditSurveyForm: React.FC = (props) => { } }, ...StudyAreaInitialValues, + ...SurveyFundingSourceFormInitialValues, ...{ proprietor: { survey_data_proprietary: '' as unknown as StringBoolean, From 7d26ab2145f70d8bd16beaeba9f675676eeb6e5a Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 10 Aug 2023 08:53:31 -0700 Subject: [PATCH 048/125] fixed project creation issue --- app/src/components/fields/StartEndDateFields.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/components/fields/StartEndDateFields.tsx b/app/src/components/fields/StartEndDateFields.tsx index 13f572bee0..484becc6ac 100644 --- a/app/src/components/fields/StartEndDateFields.tsx +++ b/app/src/components/fields/StartEndDateFields.tsx @@ -68,7 +68,7 @@ const StartEndDateFields: React.FC = (props) => { maxDate={moment(DATE_LIMIT.max)} value={formattedStartDateValue} onChange={(value) => { - setFieldValue('start_date', moment(value).format(DATE_FORMAT.ShortDateFormat)); + setFieldValue(startName, moment(value).format(DATE_FORMAT.ShortDateFormat)); }} /> @@ -96,7 +96,7 @@ const StartEndDateFields: React.FC = (props) => { maxDate={moment(DATE_LIMIT.max)} value={formattedEndDateValue} onChange={(value) => { - setFieldValue('end_date', moment(value).format(DATE_FORMAT.ShortDateFormat)); + setFieldValue(endName, moment(value).format(DATE_FORMAT.ShortDateFormat)); }} /> From 7bf52ff65b69e5e740224656c363c51ec0a55a93 Mon Sep 17 00:00:00 2001 From: Curtis Upshall Date: Thu, 10 Aug 2023 10:56:01 -0700 Subject: [PATCH 049/125] SIMSBIOHUB-172: Use AutocompleteField component --- .../components/fields/AutocompleteField.tsx | 15 + .../surveys/components/FundingSourceForm.tsx | 44 +-- .../components/ProjectFundingItemForm.tsx | 285 ------------------ 3 files changed, 23 insertions(+), 321 deletions(-) delete mode 100644 app/src/features/surveys/components/ProjectFundingItemForm.tsx diff --git a/app/src/components/fields/AutocompleteField.tsx b/app/src/components/fields/AutocompleteField.tsx index 9bbf2f70d1..769b246ccb 100644 --- a/app/src/components/fields/AutocompleteField.tsx +++ b/app/src/components/fields/AutocompleteField.tsx @@ -1,5 +1,7 @@ +import { CircularProgress } from '@mui/material'; import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; import TextField from '@mui/material/TextField'; +import { SxProps } from '@mui/system'; import { useFormikContext } from 'formik'; import get from 'lodash-es/get'; import { SyntheticEvent } from 'react'; @@ -14,6 +16,8 @@ export interface IAutocompleteField { label: string; name: string; options: IAutocompleteFieldOption[]; + loading?: boolean; + sx?: SxProps; required?: boolean; filterLimit?: number; optionFilter?: 'value' | 'label'; // used to filter existing/ set data for the AutocompleteField, defaults to value in getExistingValue function @@ -62,6 +66,8 @@ const AutocompleteField: React.FC option.label} isOptionEqualToValue={handleGetOptionSelected} filterOptions={createFilterOptions({ limit: props.filterLimit })} + sx={props.sx} + loading={props.loading} onChange={(event, option) => { if (props.onChange) { props.onChange(event, option); @@ -79,6 +85,15 @@ const AutocompleteField: React.FC + {props.loading ? : null} + {params.InputProps.endAdornment} + + ), + }} /> )} /> diff --git a/app/src/features/surveys/components/FundingSourceForm.tsx b/app/src/features/surveys/components/FundingSourceForm.tsx index 377f63a1ed..185aeff513 100644 --- a/app/src/features/surveys/components/FundingSourceForm.tsx +++ b/app/src/features/surveys/components/FundingSourceForm.tsx @@ -1,15 +1,13 @@ import { mdiPlus, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; -import { Autocomplete, CircularProgress, TextField } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; +import AutocompleteField from 'components/fields/AutocompleteField'; import DollarAmountField from 'components/fields/DollarAmountField'; -import { IFundingSourceData } from 'features/funding-sources/components/FundingSourceForm'; import { FieldArray, FieldArrayRenderProps, useFormikContext } from 'formik'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; -import { IGetFundingSourcesResponse } from 'interfaces/useFundingSourceApi.interface'; import { IEditSurveyRequest } from 'interfaces/useSurveyApi.interface'; import yup from 'utils/YupSchema'; @@ -63,7 +61,7 @@ export const SurveyFundingSourceYupSchema = yup.object().shape( */ const FundingSourceForm = () => { const formikProps = useFormikContext(); - const { values, handleChange, setFieldValue, handleSubmit } = formikProps; + const { values, handleChange, handleSubmit } = formikProps; const biohubApi = useBiohubApi(); const fundingSourcesDataLoader = useDataLoader(() => biohubApi.funding.getAllFundingSources()); @@ -75,51 +73,25 @@ const FundingSourceForm = () => { return ( - ( {values.funding_sources.map((surveyFundingSource, index) => { - const value = fundingSources.find((fundingSource) => fundingSource.funding_source_id === surveyFundingSource.funding_source_id) - console.log('value=', value) return ( - - + + id={`funding_sources.[${index}].funding_source_id`} - value={value} - onChange={(_event, option) => { - setFieldValue(`funding_sources.[${index}].funding_source_id`, option?.funding_source_id) - }} + name={`funding_sources.[${index}].funding_source_id`} + label='Funding Source' sx={{ flex: 6 }} - isOptionEqualToValue={(option, value) => option.funding_source_id === value.funding_source_id} - getOptionLabel={(option) => option.name} - - options={fundingSources} - - /* options={fundingSources.map((fundingSource) => ({ value: fundingSource.funding_source_id, label: fundingSource.name }))} - */ + loading={fundingSourcesDataLoader.isLoading} - renderInput={(params) => ( - - {fundingSourcesDataLoader.isLoading ? : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} + required /> (isNaN(value) ? undefined : value)) - .nullable(true) - .when('first_nations_id', { - is: (first_nations_id: number) => !first_nations_id, - then: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .required('Required'), - otherwise: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - }), - // if first_nations_id is present, agency_id is no longer required - first_nations_id: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - .when('agency_id', { - is: (agency_id: number) => !agency_id, - then: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .required('Required'), - otherwise: yup - .number() - .transform((value) => (isNaN(value) ? undefined : value)) - .nullable(true) - }), - investment_action_category: yup.number().nullable(true), - agency_project_id: yup.string().max(50, 'Cannot exceed 50 characters').nullable(true), - // funding amount is not required when a first nation is selected as a funding source - funding_amount: yup - .number() - .transform((value) => (isNaN(value) && null) || value) - .typeError('Must be a number') - .min(0, 'Must be a positive number') - .max(9999999999, 'Must be less than $9,999,999,999') - .when('first_nations_id', (val: any) => { - const rules = yup - .number() - .transform((value) => (isNaN(value) && null) || value) - .typeError('Must be a number') - .min(0, 'Must be a positive number') - .max(9999999999, 'Must be less than $9,999,999,999'); - if (!val) { - return rules.required('Required'); - } - - return rules.nullable(true); - }), - start_date: yup.string().when('first_nations_id', (val: any) => { - const rules = yup.string().isValidDateString(); - if (!val) { - return rules.required('Required'); - } - return rules.nullable(true); - }), - end_date: yup.string().when('first_nations_id', (val: any) => { - const rules = yup.string().isValidDateString().isEndDateAfterStartDate('start_date'); - if (!val) { - return rules.required('Required'); - } - return rules.nullable(true); - }) - }, - [['agency_id', 'first_nations_id']] // this prevents a cyclical dependency -); - -export enum FundingSourceType { - FUNDING_SOURCE, - FIRST_NATIONS -} -export interface IFundingSourceAutocompleteField { - value: number; - label: string; - type: FundingSourceType; -} -export interface IProjectFundingItemFormProps { - sources: IFundingSourceAutocompleteField[]; - investment_action_category: IInvestmentActionCategoryOption[]; -} - -/** - * A modal form for a single item of the project funding sources array. - * - * @See ProjectFundingForm.tsx - * - * @param {*} props - * @return {*} - */ -const ProjectFundingItemForm: React.FC = (props) => { - const formikProps = useFormikContext(); - const { values, touched, errors, handleChange, handleSubmit, setFieldValue } = formikProps; - // Only show investment_action_category if certain agency_id values are selected - // Toggle investment_action_category label and dropdown values based on chosen agency_id - const investment_action_category_label = - (values.agency_id === 1 && 'Investment Action') || (values.agency_id === 2 && 'Investment Category') || null; - - const findItemLabel = (id: number, type: FundingSourceType) => { - return props.sources.find((item) => item.value === id && item.type === type)?.label; - }; - - // find label for initial value - const mapInitialValue = ( - formValues?: IProjectFundingFormArrayItem - ): IAutocompleteFieldOptionWithType | undefined => { - if (formValues) { - const id = formValues.agency_id ?? formValues.first_nations_id ?? 0; - const type = formValues.agency_id ? FundingSourceType.FUNDING_SOURCE : FundingSourceType.FIRST_NATIONS; - - const initialValue = { - value: id, - type: type, - label: String(findItemLabel(id, type)) - } as IAutocompleteFieldOptionWithType; - - return initialValue; - } - }; - - return ( - - - - Agency Details - - - - - - { - // investment_action_category is dependent on agency_id, so reset it if agency_id changes - setFieldValue( - 'investment_action_category', - ProjectFundingFormArrayItemInitialValues.investment_action_category - ); - // first_nations_id AND agency_id cannot be present on the same funding source - // reset values when a change occurs to prevent that from happening - setFieldValue('first_nations_id', null); - setFieldValue('agency_id', null); - setFieldValue('first_nations_name', null); - setFieldValue('agency_name', null); - - if (options?.type === FundingSourceType.FIRST_NATIONS) { - setFieldValue('first_nations_id', options?.value); - setFieldValue('first_nations_name', options?.label); - setFieldValue('investment_action_category', 0); // first nations do not have an investment category - } else { - setFieldValue('agency_id', options?.value); - setFieldValue('agency_name', options?.label); - // If an agency_id with a `Not Applicable` investment_action_category is chosen, auto select - // it for the user. - if (event.target.value !== 1 && event.target.value !== 2) { - setFieldValue( - 'investment_action_category', - props.investment_action_category.find((item) => item.agency_id === options?.value)?.value - ); - } - } - }} - /> - - {errors.agency_id} - - - {investment_action_category_label && ( - - - {investment_action_category_label} - - {errors.investment_action_category} - - - )} - - - - - - - Funding Details - - - - - - - - - - - - ); -}; - -export default ProjectFundingItemForm; From 707c448f92b7d727cdb0bc0a8f2752d73d197176 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 10 Aug 2023 11:01:45 -0700 Subject: [PATCH 050/125] added mui loading button and removed old --- app/package.json | 1 + app/src/components/buttons/LoadingButton.tsx | 43 ------------------- .../components/dialog/SubmitBiohubDialog.tsx | 2 +- .../attachments/EditFileWithMetaDialog.tsx | 2 +- .../attachments/FileUploadWithMetaDialog.tsx | 2 +- 5 files changed, 4 insertions(+), 46 deletions(-) delete mode 100644 app/src/components/buttons/LoadingButton.tsx diff --git a/app/package.json b/app/package.json index dbb33e037c..b064f3b146 100644 --- a/app/package.json +++ b/app/package.json @@ -32,6 +32,7 @@ "@mdi/js": "~6.9.96", "@mdi/react": "~1.4.0", "@mui/icons-material": "^5.8.4", + "@mui/lab": "^5.0.0-alpha.139", "@mui/material": "^5.13.0", "@mui/styles": "^5.9.3", "@mui/system": "^5.12.3", diff --git a/app/src/components/buttons/LoadingButton.tsx b/app/src/components/buttons/LoadingButton.tsx deleted file mode 100644 index e3b77fa4fb..0000000000 --- a/app/src/components/buttons/LoadingButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Theme } from '@mui/material'; -import Box from '@mui/material/Box'; -import Button, { ButtonProps } from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme: Theme) => ({ - wrapper: { - position: 'relative' - }, - buttonProgress: { - color: theme.palette.primary.main, - position: 'absolute', - top: '50%', - left: '50%', - marginTop: -12, - marginLeft: -12 - } -})); - -export type LoadingButtonProps = ButtonProps & { loading: boolean }; - -/** - * An MUI `Button` component with an added `loading` prop, which displays a spinner and disables the button until `loading` - * becomes false. Notably, this kind of component is already available in MUI Lab, but only in MUI v5. See: - * https://mui.com/material-ui/api/loading-button/ - * - * @param {LoadingButtonProps} props - * @return {*} - */ -const LoadingButton = (props: LoadingButtonProps) => { - const { disabled, loading, ...rest } = props; - const classes = useStyles(); - - return ( - - @@ -105,6 +116,7 @@ const FundingSourcesListPage: React.FC = () => {
+ diff --git a/app/src/hooks/api/useFundingSourceApi.ts b/app/src/hooks/api/useFundingSourceApi.ts index d18e08b47c..fdc980e2b1 100644 --- a/app/src/hooks/api/useFundingSourceApi.ts +++ b/app/src/hooks/api/useFundingSourceApi.ts @@ -36,6 +36,16 @@ const useFundingSourceApi = (axios: AxiosInstance) => { return data.length > 0; }; + const getFundingSources = async (name: string): Promise => { + const { data } = await axios.get('/api/funding-sources', { + params: { + name + } + }); + + return data; + }; + /** * Get a single funding source. * @@ -95,7 +105,8 @@ const useFundingSourceApi = (axios: AxiosInstance) => { getFundingSource, deleteFundingSourceById, putFundingSource, - postFundingSource + postFundingSource, + getFundingSources }; }; diff --git a/app/src/pages/access/AccessRequestPage.tsx b/app/src/pages/access/AccessRequestPage.tsx index 764415cc97..df1faaec7b 100644 --- a/app/src/pages/access/AccessRequestPage.tsx +++ b/app/src/pages/access/AccessRequestPage.tsx @@ -1,3 +1,4 @@ +import { LoadingButton } from '@mui/lab'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; @@ -5,7 +6,6 @@ import Container from '@mui/material/Container'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; -import LoadingButton from 'components/buttons/LoadingButton'; import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; import { AccessRequestI18N } from 'constants/i18n'; import { AuthStateContext } from 'contexts/authStateContext'; From e7c33dba7ddd1e1eb0e00e9002d82f83dfee10be Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Thu, 10 Aug 2023 13:40:47 -0700 Subject: [PATCH 056/125] edit funding source wired up --- .../list/FundingSourcesListPage.tsx | 18 +++++++++++++++--- .../list/FundingSourcesTable.tsx | 3 ++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx index d6a8f3b20c..424bcb7548 100644 --- a/app/src/features/funding-sources/list/FundingSourcesListPage.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesListPage.tsx @@ -55,6 +55,7 @@ const useStyles = makeStyles((theme: Theme) => ({ const FundingSourcesListPage: React.FC = () => { const [isCreateModelOpen, setIsCreateModalOpen] = useState(false); const [isEditModelOpen, setIsEditModalOpen] = useState(false); + const [editFundingSourceId, setEditFundingSourceId] = useState(null); const classes = useStyles(); const biohubApi = useBiohubApi(); @@ -81,6 +82,11 @@ const FundingSourcesListPage: React.FC = () => { setIsEditModalOpen(false); }; + const openEditModal = (fundingSourceId: number) => { + setIsEditModalOpen(true); + setEditFundingSourceId(fundingSourceId); + }; + if (!codesContext.codesDataLoader.isReady || !fundingSourceDataLoader.isReady) { return ( <> @@ -107,7 +113,7 @@ const FundingSourcesListPage: React.FC = () => { color="primary" aria-label="Add Funding Source" startIcon={} - onClick={() => setIsEditModalOpen(true)}> + onClick={() => setIsCreateModalOpen(true)}> Add @@ -116,7 +122,13 @@ const FundingSourcesListPage: React.FC = () => { - + {editFundingSourceId && ( + + )} @@ -130,7 +142,7 @@ const FundingSourcesListPage: React.FC = () => { - + diff --git a/app/src/features/funding-sources/list/FundingSourcesTable.tsx b/app/src/features/funding-sources/list/FundingSourcesTable.tsx index 68edfc4593..e79587187a 100644 --- a/app/src/features/funding-sources/list/FundingSourcesTable.tsx +++ b/app/src/features/funding-sources/list/FundingSourcesTable.tsx @@ -45,6 +45,7 @@ const useStyles = makeStyles((theme: Theme) => ({ export interface IFundingSourcesTableTableProps { fundingSources: IGetFundingSourcesResponse[]; + openEditModal: (fundingSourceId: number) => void; } interface IFundingSourcesTableEntry { @@ -70,7 +71,7 @@ const FundingSourcesTable = (props: IFundingSourcesTableTableProps) => { }; const handleEdit = (row: IFundingSourcesTableEntry) => { - // TOOD + props.openEditModal(row.funding_source_id); }; const handleDelete = (row: IFundingSourcesTableEntry) => { From f41ff5e484cbc6e61847456f2641b69e20e3a406 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 10 Aug 2023 13:53:03 -0700 Subject: [PATCH 057/125] Updates --- app/package-lock.json | 32 +++++++ app/package.json | 1 - app/src/constants/i18n.ts | 5 +- app/src/contexts/dialogContext.tsx | 91 ++++++------------- .../details/FundingSourceDetails.tsx | 2 - .../details/FundingSourcePage.tsx | 18 +++- app/src/layouts/BaseLayout.tsx | 13 +-- 7 files changed, 76 insertions(+), 86 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 2ea78562c3..ee96f58c49 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2570,6 +2570,38 @@ "reselect": "^4.1.8" } }, + "@mui/x-date-pickers": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.11.0.tgz", + "integrity": "sha512-yVGfpH3scUauLaHWvzckD0xswboC48YAaJ4568YTkKozXFSPPkvK7VGSQ+qo1u8K2UjYh1iZoff3k0EoDDPnww==", + "requires": { + "@babel/runtime": "^7.22.6", + "@mui/utils": "^5.14.1", + "@types/react-transition-group": "^4.4.6", + "clsx": "^1.2.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "dependencies": { + "@mui/utils": { + "version": "5.14.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.4.tgz", + "integrity": "sha512-4ANV0txPD3x0IcTCSEHKDWnsutg1K3m6Vz5IckkbLXVYu17oOZCVUdOKsb/txUmaCd0v0PmSRe5PW+Mlvns5dQ==", + "requires": { + "@babel/runtime": "^7.22.6", + "@types/prop-types": "^15.7.5", + "@types/react-is": "^18.2.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", diff --git a/app/package.json b/app/package.json index dbb33e037c..e6d5f86913 100644 --- a/app/package.json +++ b/app/package.json @@ -62,7 +62,6 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-dropzone": "~11.3.2", - "react-error-boundary": "^4.0.10", "react-leaflet": "~3.1.0", "react-leaflet-cluster": "~1.0.3", "react-number-format": "~4.5.2", diff --git a/app/src/constants/i18n.ts b/app/src/constants/i18n.ts index 17a34136cf..187ea2db10 100644 --- a/app/src/constants/i18n.ts +++ b/app/src/constants/i18n.ts @@ -313,5 +313,8 @@ export const FundingSourceI18N = { 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor sem. Aliquam erat volutpat. Donec placerat nisl magna, et faucibus arcu condimentum sed.', updateErrorTitle: 'Error Creating Funding Source', updateErrorText: - 'An error has occurred while attempting to update your Funding Source, please try again. If the error persists, please contact your system administrator.' + 'An error has occurred while attempting to update your Funding Source, please try again. If the error persists, please contact your system administrator.', + fetchFundingSourceErrorTitle: 'Error Fetching Funding Source', + fetchFundingSourceErrorText: + 'An error has occurred while attempting to fetch the Funding Source, please try again. If the error persists, please contact your system administrator.' }; diff --git a/app/src/contexts/dialogContext.tsx b/app/src/contexts/dialogContext.tsx index 9049160a12..918a818af6 100644 --- a/app/src/contexts/dialogContext.tsx +++ b/app/src/contexts/dialogContext.tsx @@ -1,4 +1,6 @@ -import { Alert, AlertProps } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { Color } from '@mui/material'; +import IconButton from '@mui/material/IconButton'; import Snackbar from '@mui/material/Snackbar'; import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; import YesNoDialog, { IYesNoDialogProps } from 'components/dialog/YesNoDialog'; @@ -8,6 +10,8 @@ export interface IDialogContext { /** * Set the yes no dialog props. * + * Note: Any props that are not provided, will default to whatever value was previously set (or the default value) + * * @memberof IDialogContext */ setYesNoDialog: (props: Partial) => void; @@ -21,6 +25,8 @@ export interface IDialogContext { /** * Set the error dialog props. * + * Note: Any props that are not provided, will default to whatever value was previously set (or the default value) + * * @memberof IDialogContext */ setErrorDialog: (props: Partial) => void; @@ -34,6 +40,8 @@ export interface IDialogContext { /** * Set the snackbar props. * + * Note: Any props that are not provided, will default to whatever value was previously set (or the default value) + * * @memberof IDialogContext */ setSnackbar: (props: Partial) => void; @@ -48,16 +56,10 @@ export interface IDialogContext { export interface ISnackbarProps { open: boolean; - color: AlertProps['color']; + onClose: () => void; + severity?: Color; + color?: Color; snackbarMessage: ReactNode; - /** - * Callback fired when the snackbar is closed. - * - * Note: this callback will be fired once (when either the snackbar times out or is manually closed by the user). - * - * @memberof ISnackbarProps - */ - onClose: (() => void) | (() => Promise); } export const defaultYesNoDialogProps: IYesNoDialogProps = { @@ -90,7 +92,6 @@ export const defaultErrorDialogProps: IErrorDialogProps = { export const defaultSnackbarProps: ISnackbarProps = { snackbarMessage: '', open: false, - color: 'success', onClose: () => { // default do nothing } @@ -125,60 +126,15 @@ export const DialogContextProvider: React.FC = (props) const [snackbarProps, setSnackbarProps] = useState(defaultSnackbarProps); const setYesNoDialog = function (partialProps: Partial) { - setYesNoDialogProps({ - ...defaultYesNoDialogProps, - ...partialProps, - onClose: () => { - partialProps?.onClose?.(); - closeYesNoDialog(); - }, - onYes: () => { - partialProps?.onYes?.(); - closeYesNoDialog(); - }, - onNo: () => { - partialProps?.onNo?.(); - closeYesNoDialog(); - } - }); - }; - - const closeYesNoDialog = function () { - setYesNoDialogProps({ ...defaultYesNoDialogProps, open: false }); - }; - - const setErrorDialog = function (partialProps: Partial) { - setErrorDialogProps({ - ...defaultErrorDialogProps, - ...partialProps, - onClose: () => { - partialProps?.onClose?.(); - closeErrorDialog(); - }, - onOk: () => { - partialProps?.onOk?.(); - closeErrorDialog(); - } - }); - }; - - const closeErrorDialog = function () { - setErrorDialogProps({ ...defaultErrorDialogProps, open: false }); + setYesNoDialogProps({ ...yesNoDialogProps, ...partialProps }); }; const setSnackbar = function (partialProps: Partial) { - setSnackbarProps({ - ...defaultSnackbarProps, - ...partialProps, - onClose: () => { - partialProps?.onClose?.(); - closeSnackbarDialog(); - } - }); + setSnackbarProps({ ...snackbarProps, ...partialProps }); }; - const closeSnackbarDialog = function () { - setSnackbarProps({ ...defaultSnackbarProps, open: false }); + const setErrorDialog = function (partialProps: Partial) { + setErrorDialogProps({ ...errorDialogProps, ...partialProps }); }; return ( @@ -201,11 +157,16 @@ export const DialogContextProvider: React.FC = (props) }} open={snackbarProps.open} autoHideDuration={6000} - onClose={snackbarProps.onClose}> - - {snackbarProps.snackbarMessage} - - + onClose={() => setSnackbar({ open: false })} + message={snackbarProps.snackbarMessage} + action={ + + setSnackbar({ open: false })}> + + + + } + /> ); }; diff --git a/app/src/features/funding-sources/details/FundingSourceDetails.tsx b/app/src/features/funding-sources/details/FundingSourceDetails.tsx index 380f6c76ba..047f50ab88 100644 --- a/app/src/features/funding-sources/details/FundingSourceDetails.tsx +++ b/app/src/features/funding-sources/details/FundingSourceDetails.tsx @@ -39,8 +39,6 @@ const FundingSourceDetails = (props: IFundingSourceDetailsProps) => { return null; }; - console.log(props.fundingSource); - return ( diff --git a/app/src/features/funding-sources/details/FundingSourcePage.tsx b/app/src/features/funding-sources/details/FundingSourcePage.tsx index 2bd7b14966..a302df00a9 100644 --- a/app/src/features/funding-sources/details/FundingSourcePage.tsx +++ b/app/src/features/funding-sources/details/FundingSourcePage.tsx @@ -1,8 +1,11 @@ import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import ComponentDialog from 'components/dialog/ComponentDialog'; +import { FundingSourceI18N } from 'constants/i18n'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; +import useDataLoaderError from 'hooks/useDataLoaderError'; import FundingSourceDetails from './FundingSourceDetails'; import FundingSourceSurveyReferences from './FundingSourceSurveyReferences'; @@ -19,20 +22,25 @@ const FundingSourcePage = (props: IFundingSourceDetailsProps) => { biohubApi.funding.getFundingSource(fundingSourceId) ); + useDataLoaderError(fundingSourceDataLoader, (dataLoader) => { + return { + dialogTitle: FundingSourceI18N.fetchFundingSourceErrorTitle, + dialogText: FundingSourceI18N.fetchFundingSourceErrorText, + dialogError: (dataLoader.error as APIError).message, + dialogErrorDetails: (dataLoader.error as APIError).errors + }; + }); + if (!props.fundingSourceId) { return null; } fundingSourceDataLoader.load(props.fundingSourceId); - if (!fundingSourceDataLoader.isReady) { + if (!fundingSourceDataLoader.isReady || !fundingSourceDataLoader.data) { return ; } - if (!fundingSourceDataLoader.data) { - return null; - } - return ( diff --git a/app/src/layouts/BaseLayout.tsx b/app/src/layouts/BaseLayout.tsx index a9bdd3830c..65b33b5b2f 100644 --- a/app/src/layouts/BaseLayout.tsx +++ b/app/src/layouts/BaseLayout.tsx @@ -8,7 +8,6 @@ import Footer from 'components/layout/Footer'; import Header from 'components/layout/Header'; import { DialogContextProvider } from 'contexts/dialogContext'; import { PropsWithChildren } from 'react'; -import { ErrorBoundary } from 'react-error-boundary'; interface IBaseLayoutProps { className?: string; @@ -58,17 +57,7 @@ const BaseLayout = (props: PropsWithChildren) => {
- { - console.log('======== ERROR BOUNDARY ========'); - console.log(err); - console.log('------------'); - console.log(info); - console.log('------------'); - }} - fallback={
Error Boundary: Something went wrong
}> - {props.children} -
+ {props.children}