Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add all dashboard export
Browse files Browse the repository at this point in the history
rafasdc committed Dec 4, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent ddf4ba5 commit 70e92da
Showing 9 changed files with 1,243 additions and 5 deletions.
90 changes: 90 additions & 0 deletions app/backend/lib/dashboard/column_options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Columns } from 'write-excel-file';

const columnOptions: Columns = [
// column 1
{},
// column 2
{ width: 12 },
// column 3
{},
// column 4
{},
// column 5
{},
// column 6 (internal status)
{ width: 20 },
// column 7 (external status)
{ width: 20 },
// column 8
{},
// column 9 (title)
{ width: 20 },
// column 10
{},
// column 11
{},
// column 12
{},
// column 13 (Federal Funding Source)
{ width: 20 },
// column 14
{},
// column 15
{},
// column 16
{},
// column 17
{},
// column 18
{},
// column 19
{},
// column 20
{},
// column 21
{},
// column 22
{},
// column 23
{},
// column 24
{},
// column 25
{},
// column 26
{},
// column 27
{},
// column 28
{},
// column 29
{},
// column 30
{},
// column 31 (bc funding)
{ width: 24 },
// column 32 (applicant amount)
{ width: 24 },
// column 33 (other funds requested)
{ width: 24 },
// column 34 (total fnha funding)
{ width: 24 },
// column 35 (total budget)
{ width: 24 },
// column 36
{},
// column 37
{},
// column 38
{},
// column 39
{},
// column 40
{},
// column 41
{},
// column 42
{},
];

export default columnOptions;
586 changes: 586 additions & 0 deletions app/backend/lib/dashboard/dashboard.ts

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions app/backend/lib/dashboard/dashboard_export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Router } from 'express';
import RateLimit from 'express-rate-limit';
import {
generateApplicationData,
generateCbcData,
generateDashboardExport,
} from './dashboard';

const dashboardExport = Router();

const limiter = RateLimit({
windowMs: 1 * 60 * 1000,
max: 2000,
});

dashboardExport.post('/api/dashboard/export', limiter, async (req, res) => {
const cbcIds = req.body.cbc;
const ccbcIds = req.body.ccbc;
const applicationData = await generateApplicationData(ccbcIds, req);
const cbcData = await generateCbcData(cbcIds, req);
const blob = await generateDashboardExport(applicationData, cbcData);

const buffer = Buffer.from(await blob.arrayBuffer());
res.send(buffer);
});

export default dashboardExport;
330 changes: 330 additions & 0 deletions app/backend/lib/dashboard/header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
import { DateTime } from 'luxon';
import { Row } from 'write-excel-file';

const HEADER_ROW: Row = [
{
value: 'Program',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Project Id',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Phase',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Zone',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Intake Number',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Internal Status',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'External Status',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Change Request Pending',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Project Title',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Project Description',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Current Operating Name',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: '830 Million Funding',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Federal Funding Source',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Federal Project #',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Project Type',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Transport Project Type',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Highway Project Type',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Last-Mile Technology',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Last-Mile Minimum Speed',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Connected Coast Network Dependent',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Project Location',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Economic Region',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Regional District',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Geographic Names',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Total Communities and Locales',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Indigenous Community Count',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Household Count',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Transport km',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},

{
value: 'Highway km',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Rest Areas',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'BC Funding Requested',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Applicant Amount',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Other Funds Requested',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Total FNHA Funding',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Total Project Budget',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Announced by BC/ISED',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Date Application Received',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Date Conditionally Approved',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Date Agreement Signed',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Proposed Start Date',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: '% Project Milestone Complete',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
{
value: 'Construction Completed On',
fontWeight: 'bold',
type: String,
height: 95,
wrap: true,
},
];

const generateHeaderInfoRow = () => {
const INFO_ROW: Row = [
{
value: `INTERNAL USE ONLY: Not to be distributed outside Ministry of Citizen's Services\nEXPORTED: ${DateTime.now().setZone('America/Los_Angeles').toLocaleString(DateTime.DATETIME_FULL)}`,
fontWeight: 'bold',
fontSize: 16,
type: String,
wrap: true,
backgroundColor: '#f2f2f2',
span: 12,
height: 50,
},
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
];

return INFO_ROW;
};

export { HEADER_ROW, generateHeaderInfoRow };
87 changes: 87 additions & 0 deletions app/backend/lib/dashboard/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
export const handleCbcCommunities = (cbcCommunities) => {
const economicRegions = new Set<string>();
const regionalDistricts = new Set<string>();
const bcGeographicNames = new Set<string>();

cbcCommunities.forEach((community) => {
const sourceData = community.communitiesSourceDataByCommunitiesSourceDataId;
if (sourceData) {
economicRegions.add(sourceData.economicRegion);
regionalDistricts.add(sourceData.regionalDistrict);
bcGeographicNames.add(sourceData.bcGeographicName);
}
});

return {
economicRegions: Array.from(economicRegions),
regionalDistricts: Array.from(regionalDistricts),
bcGeographicNames: Array.from(bcGeographicNames),
totalCount: cbcCommunities.length,
};
};

export const handleCcbcCommunities = (ccbcCommunities) => {
if (!ccbcCommunities) {
return null;
}
return ccbcCommunities.map((community) => community.name);
};

export const convertStatus = (status: string): string => {
switch (status) {
case 'analyst_withdrawn':
return 'Withdrawn';
case 'applicant_approved':
return 'Agreement Signed';
case 'applicant_cancelled':
return 'Cancelled';
case 'applicant_closed':
return 'Closed';
case 'applicant_complete':
return 'Complete';
case 'applicant_conditionally_approved':
return 'Conditionally Approved';
case 'applicant_on_hold':
return 'On Hold';
case 'applicant_received':
return 'Received';
case 'assessment':
return 'Assessment';
case 'cancelled':
return 'Cancelled';
case 'received':
return 'Received';
case 'submitted':
return 'Submitted';
case 'withdrawn':
return 'Withdrawn';
case 'conditionally_approved':
return 'Conditionally Approved';
case 'approved':
return 'Approved';
case 'on_hold':
return 'On Hold';
case 'closed':
return 'Closed';
case 'recommendation':
return 'Recommendation';
case 'complete':
return 'Complete';
default:
return status;
}
};

export const formatCurrency = (value: number | null | undefined): string => {
if (value === null || value === undefined) {
return '';
}

const numberValue = Number(value);

if (Number.isNaN(numberValue)) {
return '';
}

return `$${numberValue.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
};
81 changes: 77 additions & 4 deletions app/components/AnalystDashboard/AllDashboard.tsx
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import { graphql, useFragment } from 'react-relay';
import styled from 'styled-components';
import cookie from 'js-cookie';
import useMediaQuery from '@mui/material/useMediaQuery';
import * as Sentry from '@sentry/nextjs';
import {
MaterialReactTable,
useMaterialReactTable,
@@ -19,8 +20,8 @@ import {
MRT_ToggleFullScreenButton,
MRT_ShowHideColumnsButton,
MRT_FilterFns,
MRT_Row,
} from 'material-react-table';

import RowCount from 'components/Table/RowCount';
import AssignLead from 'components/Analyst/AssignLead';
import StatusPill from 'components/StatusPill';
@@ -33,6 +34,9 @@ import { Box, IconButton, TableCellProps } from '@mui/material';
import { useFeature } from '@growthbook/growthbook-react';
import getConfig from 'next/config';
import StatusInformationModal from 'components/Analyst/StatusInformationModal';
import { DateTime } from 'luxon';
import { useToast } from 'components/AppProvider';
import DownloadIcon from './DownloadIcon';
import {
filterZones,
sortStatus,
@@ -281,6 +285,8 @@ const AllDashboardTable: React.FC<Props> = ({ query }) => {
const isLargeUp = useMediaQuery('(min-width:1007px)');

const [isFirstRender, setIsFirstRender] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const { showToast, hideToast } = useToast();

const defaultFilters = [{ id: 'program', value: ['CCBC', 'CBC', 'OTHER'] }];
const enableTimeMachine =
@@ -322,6 +328,67 @@ const AllDashboardTable: React.FC<Props> = ({ query }) => {
});
const tableContainerRef = useRef(null);

const handleBlob = (blob, toastMessage, reportDate) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${reportDate}_Connectivity_Projects_Export.xlsx`;
document.body.appendChild(a); // Append to the DOM to ensure click works in Firefox
a.click();
a.remove(); // Remove the element after clicking
window.URL.revokeObjectURL(url); // Clean up and release object URL
showToast(toastMessage, 'success', 15000);
};

const handleError = (error) => {
Sentry.captureException(error);
showToast('An error occurred. Please try again.', 'error', 15000);
};

const handleDownload = async (rows: MRT_Row<any>[]) => {
setIsLoading(true);
hideToast();
const rowData = rows.map((row) => row.original);
const groupedData = rowData.reduce(
(result, item) => {
const program =
item.program === 'CCBC' || item.program === 'OTHER' ? 'ccbc' : 'cbc';

if (!result[program]) {
// eslint-disable-next-line no-param-reassign
result[program.toLowerCase()] = [];
}

result[program].push(item.rowId);
return result;
},
{} as Record<string, number[]>
);
await fetch('/api/dashboard/export', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(groupedData),
}).then((response) => {
response
.blob()
.then((blob) => {
handleBlob(
blob,
'Export successful',
DateTime.now()
.setZone('America/Los_Angeles')
.toLocaleString(DateTime.DATETIME_FULL)
);
})
.catch((error) => {
handleError(error);
});
});
setIsLoading(false);
};

const statusOrderMap = useMemo(() => {
return allApplicationStatusTypes?.nodes?.reduce((acc, status) => {
acc[status.name] = status.statusOrder;
@@ -522,7 +589,7 @@ const AllDashboardTable: React.FC<Props> = ({ query }) => {
}));

const allCbcApplications = showCbcProjects
? allCbcData.edges.map((project) => {
? (allCbcData.edges.map((project) => {
const cbcStatus = project.node.jsonData.projectStatus
? cbcProjectStatusConverter(project.node.jsonData.projectStatus)
: null;
@@ -548,7 +615,7 @@ const AllDashboardTable: React.FC<Props> = ({ query }) => {
showLink: showCbcProjectsLink,
communities: getCbcCommunities(project),
};
}) ?? []
}) ?? [])
: [];

return [...allCcbcApplications, ...allCbcApplications];
@@ -742,12 +809,18 @@ const AllDashboardTable: React.FC<Props> = ({ query }) => {
renderToolbarInternalActions: ({ table }) => (
<Box>
<IconButton size="small">
<StatusInformationIcon ModalComponent={StatusInformationModal} />
<DownloadIcon
handleClick={() => handleDownload(table.getRowModel().rows)}
isLoading={isLoading}
/>
</IconButton>
<MRT_ToggleFiltersButton table={table} />
<MRT_ShowHideColumnsButton table={table} />
<MRT_ToggleDensePaddingButton table={table} />
<MRT_ToggleFullScreenButton table={table} />
<IconButton size="small">
<StatusInformationIcon ModalComponent={StatusInformationModal} />
</IconButton>
</Box>
),
renderTopToolbarCustomActions: () => (
43 changes: 43 additions & 0 deletions app/components/AnalystDashboard/DownloadIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFileDownload } from '@fortawesome/free-solid-svg-icons';
import styled from 'styled-components';
import LoadingSpinner from 'components/LoadingSpinner';

const StyledFontAwesome = styled(FontAwesomeIcon)`
margin-left: 4px;
`;

const DownloadIcon = ({ handleClick, isLoading }) => {
const handleKeyDown = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
handleClick();
}
};

return (
<>
{isLoading ? (
<LoadingSpinner color="#345FA9" />
) : (
<div
role="button"
tabIndex={0}
onClick={!isLoading ? handleClick : null}
onKeyDown={!isLoading ? handleKeyDown : null}
aria-labelledby="Download Excel of current projects"
style={{ cursor: 'pointer' }}
data-testid="download-dashboard-icon"
>
<StyledFontAwesome
icon={faFileDownload}
fixedWidth
size="lg"
color="#345FA9"
/>
</div>
)}
</>
);
};

export default DownloadIcon;
2 changes: 1 addition & 1 deletion app/lib/helpers/ccbcSummaryGenerateFormData.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import review from 'formSchema/analyst/summary/review';
import review from '../../formSchema/analyst/summary/review';

const getEconomicRegions = (economicRegions) => {
if (!economicRegions) {
2 changes: 2 additions & 0 deletions app/server.ts
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ import s3upload from './backend/lib/s3upload';
import templateNine from './backend/lib/excel_import/template_nine';
import milestoneDue from './backend/lib/milestoneDueDate';
import communityReport from './backend/lib/communityReportsDueDate';
import dashboardExport from './backend/lib/dashboard/dashboard_export';

// Function to exclude middleware from certain routes
// The paths argument takes an array of strings containing routes to exclude from the middleware
@@ -155,6 +156,7 @@ app.prepare().then(async () => {
server.use('/', validation);
server.use('/', milestoneDue);
server.use('/', communityReport);
server.use('/', dashboardExport);

server.all('*', async (req, res) => handle(req, res));

0 comments on commit 70e92da

Please sign in to comment.