Skip to content

Commit e2809b2

Browse files
authored
Merge branch 'main' into NDT-592-Show-Community-names-in-All-Dashboard-rows
2 parents fc87025 + aab0ecb commit e2809b2

26 files changed

+912
-100
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# [1.209.0](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.208.0...v1.209.0) (2024-11-18)
2+
3+
### Features
4+
5+
- show added and removed communities in history ([a79809e](https://github.com/bcgov/CONN-CCBC-portal/commit/a79809e0f6385ec801b2793daec2627540981a2f))
6+
7+
# [1.208.0](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.207.2...v1.208.0) (2024-11-18)
8+
9+
### Features
10+
11+
- notification to analysts when community progress report due ([a072eb6](https://github.com/bcgov/CONN-CCBC-portal/commit/a072eb6d1ff36913f4d4875c8bca53a57291ad76))
12+
113
## [1.207.2](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.207.1...v1.207.2) (2024-11-15)
214

315
## [1.207.1](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.207.0...v1.207.1) (2024-11-15)
+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { Router } from 'express';
2+
import RateLimit from 'express-rate-limit';
3+
import getConfig from 'next/config';
4+
import getAuthRole from '../../utils/getAuthRole';
5+
import { performQuery } from './graphql';
6+
import handleEmailNotification from './emails/handleEmailNotification';
7+
import notifyCommunityReportDue from './emails/templates/notifyCommunityReportDue';
8+
import validateKeycloakToken from './keycloakValidate';
9+
10+
const limiter = RateLimit({
11+
windowMs: 1 * 60 * 1000,
12+
max: 30,
13+
});
14+
15+
const communityReportDueDate = Router();
16+
17+
function getNextQuarterStartDate(today: Date): Date {
18+
const currentYear = today.getFullYear();
19+
// Define quarter start months: March, June, September, December
20+
const quarterStartMonths = [2, 5, 8, 11]; // 0-based: 2=March, 5=June, 8=September, 11=December
21+
22+
const currentQuarterStartMonth = quarterStartMonths.find((month) => {
23+
const quarterStartDate = new Date(currentYear, month, 1, 0, 0, 0, 0);
24+
return quarterStartDate > today;
25+
});
26+
27+
if (currentQuarterStartMonth !== undefined) {
28+
return new Date(currentYear, currentQuarterStartMonth, 1, 0, 0, 0, 0);
29+
}
30+
// If no quarter start date is after today in the current year, return March 1 of next year
31+
return new Date(currentYear + 1, 2, 1, 0, 0, 0, 0); // 2=March
32+
}
33+
34+
const processCommunityReportsDueDates = async (req, res) => {
35+
const runtimeConfig = getConfig()?.publicRuntimeConfig ?? {};
36+
const isEnabledTimeMachine = runtimeConfig.ENABLE_MOCK_TIME;
37+
// GraphQL query to get all milestones with archivedAt: null
38+
const sowCommunityProgressQuery = `
39+
query MilestoneDatesQuery {
40+
allApplicationSowData(
41+
orderBy: AMENDMENT_NUMBER_DESC
42+
filter: {archivedAt: {isNull: true}}
43+
) {
44+
nodes {
45+
applicationId
46+
applicationByApplicationId {
47+
ccbcNumber
48+
organizationName
49+
projectName
50+
}
51+
jsonData
52+
}
53+
}
54+
}
55+
`;
56+
let result;
57+
const applicationRowIdsVisited = new Set();
58+
59+
try {
60+
result = await performQuery(sowCommunityProgressQuery, {}, req);
61+
} catch (error) {
62+
return res.status(500).json({ error: result.error }).end();
63+
}
64+
let today = null;
65+
if (isEnabledTimeMachine) {
66+
const mockedDate = req.cookies['mocks.mocked_date'];
67+
today = mockedDate ? new Date(mockedDate) : new Date();
68+
today.setUTCHours(0, 0, 0, 0);
69+
} else {
70+
today = new Date();
71+
today.setUTCHours(0, 0, 0, 0);
72+
}
73+
const nextQuarterDate = getNextQuarterStartDate(today);
74+
75+
// Function to check if a given due date string is within 30 to 31 days from today.
76+
const isWithin30To31Days = (dueDate: Date) => {
77+
const timeDiff = dueDate.getTime() - today.getTime();
78+
const daysDiff = Math.round(timeDiff / (1000 * 3600 * 24));
79+
return daysDiff === 30;
80+
};
81+
82+
// Traverse the result, if there is a milestone due date within 30 to 31 days from today,
83+
// add the application row ID, CCBC number, and whether it is a milestone 1 or 2 to a list.
84+
const communityReportData = result.data.allApplicationSowData.nodes.reduce(
85+
(acc, node) => {
86+
const { applicationId, applicationByApplicationId, jsonData } = node;
87+
if (applicationRowIdsVisited.has(applicationId)) {
88+
return acc;
89+
}
90+
const { ccbcNumber, organizationName, projectName } =
91+
applicationByApplicationId;
92+
93+
const projectStartDateString = jsonData.projectStartDate;
94+
95+
const projectStartDate = new Date(projectStartDateString);
96+
97+
if (today.getTime() > projectStartDate.getTime()) {
98+
if (isWithin30To31Days(nextQuarterDate)) {
99+
const applicationRowId = applicationId;
100+
if (!applicationRowIdsVisited.has(applicationRowId)) {
101+
acc.push({
102+
applicationRowId,
103+
ccbcNumber,
104+
organizationName,
105+
projectName,
106+
dueDate: nextQuarterDate.toLocaleDateString(),
107+
});
108+
applicationRowIdsVisited.add(applicationRowId);
109+
}
110+
}
111+
}
112+
return acc;
113+
},
114+
[]
115+
);
116+
117+
if (communityReportData.length > 0) {
118+
// Send an email to the analyst with the list of applications that have milestones due within 30 to 31 days.
119+
return handleEmailNotification(
120+
req,
121+
res,
122+
notifyCommunityReportDue,
123+
{ communityReportData },
124+
true
125+
);
126+
}
127+
128+
return res
129+
.status(200)
130+
.json({ message: 'No community progress reports due in 30 days' })
131+
.end();
132+
};
133+
134+
communityReportDueDate.get(
135+
'/api/analyst/community/upcoming',
136+
limiter,
137+
(req, res) => {
138+
const authRole = getAuthRole(req);
139+
const isRoleAuthorized =
140+
authRole?.pgRole === 'ccbc_admin' || authRole?.pgRole === 'super_admin';
141+
142+
if (!isRoleAuthorized) {
143+
return res.status(404).end();
144+
}
145+
return processCommunityReportsDueDates(req, res);
146+
}
147+
);
148+
149+
communityReportDueDate.get(
150+
'/api/analyst/cron-community',
151+
limiter,
152+
validateKeycloakToken,
153+
(req, res) => {
154+
req.claims.identity_provider = 'serviceaccount';
155+
processCommunityReportsDueDates(req, res);
156+
}
157+
);
158+
159+
export default communityReportDueDate;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { performQuery } from '../../graphql';
2+
import { Context } from '../../ches/sendEmailMerge';
3+
import {
4+
EmailTemplate,
5+
EmailTemplateProvider,
6+
replaceEmailsInNonProd,
7+
} from '../handleEmailNotification';
8+
9+
const getAnalystInfoByUserIds = `
10+
query getAnalystNameByUserIds($_rowIds: [Int!]!) {
11+
allAnalysts(filter: {rowId: {in: $_rowIds}}) {
12+
nodes {
13+
email
14+
givenName
15+
}
16+
}
17+
}
18+
`;
19+
const getEmails = async (ids: number[], req: any) => {
20+
const results = await performQuery(
21+
getAnalystInfoByUserIds,
22+
{ _rowIds: ids },
23+
req
24+
);
25+
return results?.data?.allAnalysts.nodes;
26+
};
27+
28+
const notifyCommunityReportDue: EmailTemplateProvider = async (
29+
applicationId: string,
30+
url: string,
31+
initiator: any,
32+
params: any,
33+
req
34+
): Promise<EmailTemplate> => {
35+
const { communityReportData } = params;
36+
const recipients = [70, 71];
37+
38+
const emails = await getEmails(recipients, req);
39+
40+
const contexts = emails.map((email) => {
41+
const { givenName, email: recipientEmail } = email;
42+
const emailTo = replaceEmailsInNonProd([recipientEmail]);
43+
const emailCC = replaceEmailsInNonProd([]);
44+
return {
45+
to: emailTo,
46+
cc: emailCC,
47+
context: {
48+
recipientName: givenName,
49+
communities: communityReportData,
50+
},
51+
delayTS: 0,
52+
tag: 'community-progress-report-due',
53+
} as Context;
54+
});
55+
56+
const subject = `Reminder: Community Progress Report${communityReportData.length > 1 ? 's' : ''} ${communityReportData.length > 1 ? 'are' : 'is'} coming Due`;
57+
58+
return {
59+
emailTo: [],
60+
emailCC: [],
61+
tag: 'community-progress-report-due',
62+
subject,
63+
body: `
64+
<p>Hi {{ recipientName | trim }} </p>
65+
<p>This is a notification to let you know that one or more Community Progress reports are coming due in 30 days: <p>
66+
<ul>
67+
{% for community in communities %}
68+
<li> Community Progress Report - {{ community.organizationName | trim }}. Project: {{ community.ccbcNumber }} {{ community.projectName }}. Due {{ community.dueDate }}</li>
69+
{% endfor %}
70+
</ul>
71+
<p>To unsubscribe from these email notifications, email [email protected]</p>
72+
`,
73+
contexts,
74+
params: { communityReportData },
75+
};
76+
};
77+
78+
export default notifyCommunityReportDue;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import styled from 'styled-components';
2+
3+
interface Props {
4+
action: string;
5+
communities: any[];
6+
}
7+
8+
const StyledCommunitiesContainer = styled.div`
9+
display: flex;
10+
align-items: center;
11+
`;
12+
13+
const StyledLeftContainer = styled.div`
14+
padding-right: 2%;
15+
width: 250px;
16+
`;
17+
18+
const StyledTable = styled.table`
19+
th {
20+
border: none;
21+
}
22+
tbody > tr {
23+
border-bottom: thin dashed;
24+
border-color: ${(props) => props.theme.color.borderGrey};
25+
td {
26+
width: 200px;
27+
max-width: 200px;
28+
border: none;
29+
}
30+
}
31+
`;
32+
33+
const StyledIdCell = styled.td`
34+
width: 100px !important;
35+
max-width: 100px;
36+
`;
37+
38+
const CbcHistoryCommunitiesTable: React.FC<Props> = ({
39+
action,
40+
communities,
41+
}) => {
42+
return (
43+
<StyledCommunitiesContainer
44+
style={{ display: 'flex', alignItems: 'center' }}
45+
>
46+
<StyledLeftContainer>{`${action} community location data`}</StyledLeftContainer>
47+
<div>
48+
<StyledTable>
49+
<thead>
50+
<tr>
51+
<th>Economic Region</th>
52+
<th>Regional District</th>
53+
<th>Geographic Name</th>
54+
<th>Type</th>
55+
<th>ID</th>
56+
</tr>
57+
</thead>
58+
<tbody>
59+
{communities?.map((community, index) => (
60+
<tr
61+
// eslint-disable-next-line react/no-array-index-key
62+
key={`${action}-${community.communities_source_data_id}-${index}`}
63+
data-key={`${action}-row-${index}`}
64+
>
65+
<td>{community.economic_region}</td>
66+
<td>{community.regional_district}</td>
67+
<td>{community.bc_geographic_name}</td>
68+
<td>{community.geographic_type}</td>
69+
<StyledIdCell>
70+
{community.communities_source_data_id}
71+
</StyledIdCell>
72+
</tr>
73+
))}
74+
</tbody>
75+
</StyledTable>
76+
</div>
77+
</StyledCommunitiesContainer>
78+
);
79+
};
80+
81+
export default CbcHistoryCommunitiesTable;

app/components/Analyst/CBC/History/CbcHistoryContent.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import HistoryDetails from 'components/Analyst/History/HistoryDetails';
22
import cbcData from 'formSchema/uiSchema/history/cbcData';
33
import { DateTime } from 'luxon';
44
import styled from 'styled-components';
5+
import CbcHistoryCommunitiesTable from './CbcHistoryCommunitiesTable';
56

67
const StyledContent = styled.span`
78
display: flex;
@@ -72,10 +73,23 @@ const HistoryContent = ({
7273
'updated_at',
7374
'change_reason',
7475
'cbc_data_id',
76+
'locations',
7577
]}
7678
diffSchema={cbcData}
7779
overrideParent="cbcData"
7880
/>
81+
{json?.locations?.added?.length > 0 && (
82+
<CbcHistoryCommunitiesTable
83+
action="Added"
84+
communities={json?.locations?.added}
85+
/>
86+
)}
87+
{json?.locations?.removed?.length > 0 && (
88+
<CbcHistoryCommunitiesTable
89+
action="Deleted"
90+
communities={json?.locations?.removed}
91+
/>
92+
)}
7993
{op === 'UPDATE' && changeReason !== '' && (
8094
<ChangeReason reason={changeReason} />
8195
)}

app/components/Analyst/CBC/History/CbcHistoryTable.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ const CbcHistoryTable: React.FC<Props> = ({ query }) => {
6565
json={{
6666
...historyItem.record?.json_data,
6767
project_number: historyItem.record?.project_number,
68+
locations: {
69+
added: historyItem.record?.added_communities,
70+
removed: historyItem.record?.deleted_communities,
71+
},
6872
}}
6973
prevJson={{
7074
...historyItem.oldRecord?.json_data,

app/next-env.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

0 commit comments

Comments
 (0)