diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc5b3e19f..4770ab0d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## [1.190.11](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.190.10...v1.190.11) (2024-09-12) + ## [1.190.10](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.190.9...v1.190.10) (2024-09-10) ## [1.190.9](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.190.8...v1.190.9) (2024-09-06) diff --git a/app/backend/lib/emails/templates/assesmentSecondReviewChange.ts b/app/backend/lib/emails/templates/assesmentSecondReviewChange.ts index eafdf18220..9212b213ba 100644 --- a/app/backend/lib/emails/templates/assesmentSecondReviewChange.ts +++ b/app/backend/lib/emails/templates/assesmentSecondReviewChange.ts @@ -1,20 +1,9 @@ +import ASSESSMENT_TYPES from '../../../../data/assessmentTypes'; import { EmailTemplate, EmailTemplateProvider, } from '../handleEmailNotification'; -const formats = { - projectManagement: { - type: 'Project Management assessment', - slug: 'project-management', - }, - permitting: { type: 'Permitting assessment', slug: 'permitting' }, - technical: { type: 'Technical assessment', slug: 'technical' }, - gis: { type: 'GIS assessment', slug: 'gis' }, - financialRisk: { type: 'Financial Risk assessment', slug: 'financial-risk' }, - screening: { type: 'Eligibility Screening', slug: 'screening' }, -}; - const assesmentSecondReviewChange: EmailTemplateProvider = ( applicationId: string, url: string, @@ -22,7 +11,7 @@ const assesmentSecondReviewChange: EmailTemplateProvider = ( params: any ): EmailTemplate => { const { ccbcNumber, assessmentType } = params; - const { type, slug } = formats[assessmentType]; + const { type, slug } = ASSESSMENT_TYPES[assessmentType]; return { emailTo: [34, 71], // Temporary IDs to handle email recipients diff --git a/app/backend/lib/emails/templates/assessmentAssigneeChange.ts b/app/backend/lib/emails/templates/assessmentAssigneeChange.ts index 30e8fca4bf..1587ebf495 100644 --- a/app/backend/lib/emails/templates/assessmentAssigneeChange.ts +++ b/app/backend/lib/emails/templates/assessmentAssigneeChange.ts @@ -1,5 +1,5 @@ import { Context } from 'backend/lib/ches/sendEmailMerge'; - +import ASSESSMENT_TYPES from '../../../../data/assessmentTypes'; import { EmailTemplate, EmailTemplateProvider, @@ -77,8 +77,8 @@ const assessmentAssigneeChange: EmailTemplateProvider = async ( ([assignor, assignments]) => { const alerts = (assignments as Array).map((assignment) => { return { - url: `${url}/analyst/application/${assignment.applicationId}/assessments/${assignment.assessmentType}`, - type: assignment.assessmentType, + url: `${url}/analyst/application/${assignment.applicationId}/assessments/${ASSESSMENT_TYPES[assignment.assessmentType].slug}`, + type: ASSESSMENT_TYPES[assignment.assessmentType].type, ccbcNumber: assignment.ccbcNumber, applicationId: assignment.applicationId, }; @@ -121,7 +121,7 @@ const assessmentAssigneeChange: EmailTemplateProvider = async ( body: `{% for action in actions %} {{ action.assignors }} has assigned you the following assessment(s): {% endfor %}`, contexts, diff --git a/app/components/AnalystDashboard/AssessmentAssignmentTable.tsx b/app/components/AnalystDashboard/AssessmentAssignmentTable.tsx index c547b67ad7..7c9f9e401e 100644 --- a/app/components/AnalystDashboard/AssessmentAssignmentTable.tsx +++ b/app/components/AnalystDashboard/AssessmentAssignmentTable.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; import cookie from 'js-cookie'; @@ -148,6 +148,18 @@ const findAssessment = (assessments, assessmentType) => { }; }; +const findNotification = (notifications, notificationType) => { + const data = notifications.find( + ({ node }) => node?.notificationType === notificationType + ); + + return { + jsonData: data?.node?.jsonData, + notificationType, + createdAt: data?.node?.createdAt, + }; +}; + const StyledLink = styled.a` color: ${(props) => props.theme.color.links}; text-decoration: none; @@ -235,11 +247,7 @@ const AssessmentAssignmentTable: React.FC = ({ query }) => { } } } - notificationsByApplicationId( - orderBy: CREATED_AT_DESC - first: 1 - condition: { notificationType: "assignment_technical" } - ) { + assessmentNotifications { __id edges { node { @@ -429,9 +437,8 @@ const AssessmentAssignmentTable: React.FC = ({ query }) => { zones, allAnalysts, assessmentConnection: application.allAssessments.__id, - notifications: application.notificationsByApplicationId.edges, notificationConnectionId: - application.notificationsByApplicationId.__id, + application.assessmentNotifications?.__id, pmAssessment: findAssessment( application.allAssessments.edges, 'projectManagement' @@ -440,6 +447,10 @@ const AssessmentAssignmentTable: React.FC = ({ query }) => { application.allAssessments.edges, 'technical' ), + techNotification: findNotification( + application.assessmentNotifications.edges, + 'assignment_technical' + ), permittingAssessment: findAssessment( application.allAssessments.edges, 'permitting' @@ -456,6 +467,10 @@ const AssessmentAssignmentTable: React.FC = ({ query }) => { application.allAssessments.edges, 'financialRisk' ), + financialRiskNotification: findNotification( + application.assessmentNotifications.edges, + 'assignment_financialRisk' + ), organizationName, }; } @@ -465,46 +480,62 @@ const AssessmentAssignmentTable: React.FC = ({ query }) => { [allApplications, allAnalysts] ); - const getUserEmailByAssignedTo = (assignedTo: string) => { - const analyst = allAnalysts.edges.find( - ({ node }) => `${node.givenName} ${node.familyName}` === assignedTo - ); - return analyst ? analyst.node.email : null; - }; - - const assignments = useMemo( - () => - tableData - .filter((data: any) => { - const lastSentAt = data.notifications[0]?.node?.createdAt - ? new Date(data.notifications[0]?.node?.createdAt) - : null; - return new Date(data.techAssessment.updatedAt) >= lastSentAt; - }) - .filter( - (data: any) => - data.techAssessment.jsonData.assignedTo && - data.techAssessment.jsonData.assignedTo !== - data.notifications[0]?.node?.jsonData?.to - ) - .map((data: any) => { - return { - ccbcNumber: data.ccbcNumber, - applicationId: data.applicationId, - notificationConnectionId: data.notificationConnectionId, - updatedBy: data.techAssessment.updatedBy, - updatedAt: data.techAssessment.updatedAt, - assignedTo: data.techAssessment.jsonData?.assignedTo, - assigneeEmail: getUserEmailByAssignedTo( - data.techAssessment.jsonData?.assignedTo - ), - assessmentType: 'technical', - }; - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [tableData] + const getUserEmailByAssignedTo = useCallback( + (assignedTo: string) => { + const analyst = allAnalysts.edges.find( + ({ node }) => `${node.givenName} ${node.familyName}` === assignedTo + ); + return analyst ? analyst.node.email : null; + }, + [allAnalysts] ); + const assignments = useMemo(() => { + const createAssignment = ( + application, + assessmentKey, + assessmentType = assessmentKey + ) => { + const { updatedAt, jsonData, updatedBy } = + application[`${assessmentKey}Assessment`]; + const notification = application[`${assessmentKey}Notification`]; + const lastNotificationSentAt = notification?.createdAt + ? new Date(notification.createdAt) + : null; + + const assessmentChanged = + jsonData?.assignedTo && + jsonData.assignedTo !== notification?.jsonData?.to; + + if (new Date(updatedAt) >= lastNotificationSentAt && assessmentChanged) { + return { + ccbcNumber: application.ccbcNumber, + applicationId: application.applicationId, + notificationConnectionId: application.notificationConnectionId, + updatedBy, + updatedAt, + assignedTo: jsonData.assignedTo, + assigneeEmail: getUserEmailByAssignedTo(jsonData.assignedTo), + assessmentType, + }; + } + return null; + }; + + return tableData.reduce((assignmentsList, application) => { + const techAssignment = createAssignment(application, 'tech', 'technical'); + const financialAssignment = createAssignment( + application, + 'financialRisk' + ); + + if (techAssignment) assignmentsList.push(techAssignment); + if (financialAssignment) assignmentsList.push(financialAssignment); + + return assignmentsList; + }, []); + }, [getUserEmailByAssignedTo, tableData]); + const columns = useMemo[]>(() => { // Sonarcloud duplicate lines const sharedAssessmentCell = { diff --git a/app/data/assessmentTypes.ts b/app/data/assessmentTypes.ts new file mode 100644 index 0000000000..05d9a5c921 --- /dev/null +++ b/app/data/assessmentTypes.ts @@ -0,0 +1,13 @@ +const ASSESSMENT_TYPES = { + projectManagement: { + type: 'Project Management assessment', + slug: 'project-management', + }, + permitting: { type: 'Permitting assessment', slug: 'permitting' }, + technical: { type: 'Technical assessment', slug: 'technical' }, + gis: { type: 'GIS assessment', slug: 'gis' }, + financialRisk: { type: 'Financial Risk assessment', slug: 'financial-risk' }, + screening: { type: 'Eligibility Screening', slug: 'screening' }, +}; + +export default ASSESSMENT_TYPES; diff --git a/app/schema/schema.graphql b/app/schema/schema.graphql index 18fc3720b3..5da3fb06bf 100644 --- a/app/schema/schema.graphql +++ b/app/schema/schema.graphql @@ -29924,6 +29924,32 @@ type Application implements Node { """Computed column that takes the slug to return an assessment form""" assessmentForm(_assessmentDataType: String!): AssessmentData + """Computed column to get assessment notifications by assessment type""" + assessmentNotifications( + """Only read the first `n` values of the set.""" + first: Int + + """Only read the last `n` values of the set.""" + last: Int + + """ + Skip the first `n` values from our `after` cursor, an alternative to cursor + based pagination. May not be used with `last`. + """ + offset: Int + + """Read all values in the set before (above) this cursor.""" + before: Cursor + + """Read all values in the set after (below) this cursor.""" + after: Cursor + + """ + A filter to be used in determining which values should be returned by the collection. + """ + filter: NotificationFilter + ): NotificationsConnection! + """Computed column to return conditional approval data""" conditionalApproval: ConditionalApprovalData diff --git a/app/tests/backend/lib/emails/templates/assessmentAssigneeChange.test.ts b/app/tests/backend/lib/emails/templates/assessmentAssigneeChange.test.ts index 7a3c17c82b..bfa361db36 100644 --- a/app/tests/backend/lib/emails/templates/assessmentAssigneeChange.test.ts +++ b/app/tests/backend/lib/emails/templates/assessmentAssigneeChange.test.ts @@ -58,7 +58,7 @@ describe('assessmentAssigneeChange template', () => { }, { applicationId: 3, - assessmentType: 'technical', + assessmentType: 'financialRisk', assignedTo: 'Tester 2', assigneeEmail: 'tester2@mail.com', ccbcNumber: 'CCBC-000003', @@ -93,7 +93,7 @@ describe('assessmentAssigneeChange template', () => { { ccbcNumber: 'CCBC-000001', applicationId: 1, - type: 'technical', + type: 'Technical assessment', url: 'http://mock_host.ca/analyst/application/1/assessments/technical', }, ], @@ -106,7 +106,7 @@ describe('assessmentAssigneeChange template', () => { { ccbcNumber: 'CCBC-000002', applicationId: 2, - type: 'technical', + type: 'Technical assessment', url: 'http://mock_host.ca/analyst/application/2/assessments/technical', }, ], @@ -117,8 +117,8 @@ describe('assessmentAssigneeChange template', () => { { ccbcNumber: 'CCBC-000003', applicationId: 3, - type: 'technical', - url: 'http://mock_host.ca/analyst/application/3/assessments/technical', + type: 'Financial Risk assessment', + url: 'http://mock_host.ca/analyst/application/3/assessments/financial-risk', }, ], assignors: 'Assignor 3', diff --git a/db/deploy/computed_columns/application_assessment_notifications.sql b/db/deploy/computed_columns/application_assessment_notifications.sql new file mode 100644 index 0000000000..56a705366d --- /dev/null +++ b/db/deploy/computed_columns/application_assessment_notifications.sql @@ -0,0 +1,19 @@ +-- Deploy ccbc:computed_columns/application_assessment_notifications to pg + +BEGIN; + +create or replace function ccbc_public.application_assessment_notifications(application ccbc_public.application) returns setof ccbc_public.notification as +$$ + select distinct on (notification_type) * + from ccbc_public.notification + where application_id = application_id + ORDER BY notification_type, created_at DESC; +$$ language sql stable; + +grant execute on function ccbc_public.application_assessment_notifications to ccbc_analyst; +grant execute on function ccbc_public.application_assessment_notifications to ccbc_admin; +grant execute on function ccbc_public.application_assessment_notifications to ccbc_auth_user; + +comment on function ccbc_public.application_assessment_notifications is 'Computed column to get assessment notifications by assessment type'; + +COMMIT; diff --git a/db/revert/computed_columns/application_assessment_notifications.sql b/db/revert/computed_columns/application_assessment_notifications.sql new file mode 100644 index 0000000000..80100d2326 --- /dev/null +++ b/db/revert/computed_columns/application_assessment_notifications.sql @@ -0,0 +1,6 @@ + +BEGIN; + +drop function ccbc_public.application_assessment_notifications; + +COMMIT; diff --git a/db/sqitch.plan b/db/sqitch.plan index a1a4a82b86..62af336e70 100644 --- a/db/sqitch.plan +++ b/db/sqitch.plan @@ -668,3 +668,5 @@ tables/communities_source_data_001_service_account 2024-08-28T16:32:48Z Rafael S @1.190.8 2024-09-04T23:08:50Z CCBC Service Account # release v1.190.8 @1.190.9 2024-09-06T20:13:21Z CCBC Service Account # release v1.190.9 @1.190.10 2024-09-10T23:10:53Z CCBC Service Account # release v1.190.10 +computed_columns/application_assessment_notifications 2024-09-06T21:58:13Z ,,, # Add notifications by assessment type field +@1.190.11 2024-09-12T18:13:42Z CCBC Service Account # release v1.190.11 diff --git a/package.json b/package.json index d11ff12057..eae6c03d5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CONN-CCBC-portal", - "version": "1.190.10", + "version": "1.190.11", "main": "index.js", "repository": "https://github.com/bcgov/CONN-CCBC-portal.git", "author": "Romer, Meherzad CITZ:EX ",