Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIMS-2134 Export Tables with Dates #2722

Merged
merged 3 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions express-api/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/dist
/uploads
2 changes: 1 addition & 1 deletion react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"@mui/x-date-pickers": "7.18.0",
"@turf/turf": "7.1.0",
"dayjs": "1.11.10",
"node-xlsx": "0.24.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-error-boundary": "4.0.12",
Expand All @@ -39,6 +38,7 @@
"supercluster": "8.0.1",
"typescript-eslint": "8.8.0",
"use-supercluster": "1.2.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "3.23.8"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Box } from '@mui/material';
import { SnackBarContext } from '@/contexts/snackbarContext';
import { LookupContext } from '@/contexts/lookupContext';
import useHistoryAwareNavigate from '@/hooks/useHistoryAwareNavigate';
import { makeDateOrUndefined } from '@/utilities/helperFunctions';

const AdministrativeAreasTable = () => {
const api = usePimsApi();
Expand Down Expand Up @@ -110,7 +111,7 @@ const AdministrativeAreasTable = () => {
'RegionalDistricts',
adminArea.RegionalDistrictId,
)?.Name,
'Created On': adminArea.CreatedOn,
'Created On': makeDateOrUndefined(adminArea.CreatedOn),
Disabled: adminArea.IsDisabled,
};
});
Expand Down
5 changes: 3 additions & 2 deletions react-app/src/components/agencies/AgencyTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { dateColumnType } from '../table/CustomColumns';
import { SnackBarContext } from '@/contexts/snackbarContext';
import useHistoryAwareNavigate from '@/hooks/useHistoryAwareNavigate';
import { LookupContext } from '@/contexts/lookupContext';
import { makeDateOrUndefined } from '@/utilities/helperFunctions';

interface IAgencyTable {
rowClickHandler: GridEventListener<'rowClick'>;
Expand Down Expand Up @@ -142,8 +143,8 @@ const AgencyTable = (props: IAgencyTable) => {
Disabled: agency.IsDisabled,
Notifications: agency.SendEmail,
SendTo: agency.Email,
Created: agency.CreatedOn,
Updated: agency.UpdatedOn,
Created: makeDateOrUndefined(agency.CreatedOn),
Updated: makeDateOrUndefined(agency.UpdatedOn),
};
});
};
Expand Down
71 changes: 44 additions & 27 deletions react-app/src/components/projects/ProjectsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ProjectTask } from '@/constants/projectTasks';
import { NotificationType } from '@/constants/notificationTypes';
import { MonetaryType } from '@/constants/monetaryTypes';
import { LookupContext } from '@/contexts/lookupContext';
import { makeDateOrUndefined } from '@/utilities/helperFunctions';

const ProjectsTable = () => {
const [totalCount, setTotalCount] = useState(0);
Expand Down Expand Up @@ -148,7 +149,7 @@ const ProjectsTable = () => {
: lookup.getLookupValueById('Agencies', project.AgencyId)?.Name,
Agency: lookup.getLookupValueById('Agencies', project.AgencyId)?.Name,
'Created By': `${project.CreatedBy?.FirstName} ${project.CreatedBy?.LastName}`,
'Created On': project.CreatedOn,
'Created On': makeDateOrUndefined(project.CreatedOn),
'Exemption Requested': project.Tasks?.find(
(task) => task.TaskId === ProjectTask.EXEMPTION_REQUESTED,
)?.IsCompleted,
Expand Down Expand Up @@ -186,32 +187,48 @@ const ProjectsTable = () => {
AgencyResponseNote: project.Notes?.find(
(note) => note.NoteTypeId === NoteTypes.AGENCY_INTEREST,
)?.Note,
'Submitted On': project.SubmittedOn,
'Approved On': project.ApprovedOn,
'ERP Initial Notification Sent On': project.Notifications.find(
(n) => n.TemplateId === NotificationType.NEW_PROPERTIES_ON_ERP,
)?.SendOn,
'ERP Thirty Day Notification Sent On': project.Notifications.find(
(n) => n.TemplateId === NotificationType.THIRTY_DAY_ERP_NOTIFICATION_PARENT_AGENCIES,
)?.SendOn,
'ERP Sixty Day Notification Sent On': project.Notifications.find(
(n) => n.TemplateId === NotificationType.SIXTY_DAY_ERP_NOTIFICATION_PARENT_AGENCIES,
)?.SendOn,
'ERP Ninety Day Notification Sent On': project.Notifications.find(
(n) => n.TemplateId === NotificationType.NINTY_DAY_ERP_NOTIFICATION_PARENT_AGENCIES,
)?.SendOn,
'Transferred Within GRE On': project.Timestamps.find(
(timestamp) => timestamp.TimestampTypeId === TimestampType.TRANSFERRED_WITHIN_GRE_ON,
)?.Date,
'ERP Clearance Notification Sent On': project.Timestamps.find(
(timestamp) => timestamp.TimestampTypeId === TimestampType.CLEARANCE_NOTIFICATION_SENT_ON,
)?.Date,
'Disposed On': project.Timestamps.find(
(timestamp) => timestamp.TimestampTypeId === TimestampType.DISPOSED_ON,
)?.Date,
'Marketed On': project.Timestamps.find(
(timestamp) => timestamp.TimestampTypeId === TimestampType.MARKETED_ON,
)?.Date,
'Submitted On': makeDateOrUndefined(project.SubmittedOn),
'Approved On': makeDateOrUndefined(project.ApprovedOn),
'ERP Initial Notification Sent On': makeDateOrUndefined(
project.Notifications.find((n) => n.TemplateId === NotificationType.NEW_PROPERTIES_ON_ERP)
?.SendOn,
),
'ERP Thirty Day Notification Sent On': makeDateOrUndefined(
project.Notifications.find(
(n) => n.TemplateId === NotificationType.THIRTY_DAY_ERP_NOTIFICATION_PARENT_AGENCIES,
)?.SendOn,
),
'ERP Sixty Day Notification Sent On': makeDateOrUndefined(
project.Notifications.find(
(n) => n.TemplateId === NotificationType.SIXTY_DAY_ERP_NOTIFICATION_PARENT_AGENCIES,
)?.SendOn,
),
'ERP Ninety Day Notification Sent On': makeDateOrUndefined(
project.Notifications.find(
(n) => n.TemplateId === NotificationType.NINTY_DAY_ERP_NOTIFICATION_PARENT_AGENCIES,
)?.SendOn,
),
'Transferred Within GRE On': makeDateOrUndefined(
project.Timestamps.find(
(timestamp) => timestamp.TimestampTypeId === TimestampType.TRANSFERRED_WITHIN_GRE_ON,
)?.Date,
),
'ERP Clearance Notification Sent On': makeDateOrUndefined(
project.Timestamps.find(
(timestamp) =>
timestamp.TimestampTypeId === TimestampType.CLEARANCE_NOTIFICATION_SENT_ON,
)?.Date,
),
'Disposed On': makeDateOrUndefined(
project.Timestamps.find(
(timestamp) => timestamp.TimestampTypeId === TimestampType.DISPOSED_ON,
)?.Date,
),
'Marketed On': makeDateOrUndefined(
project.Timestamps.find(
(timestamp) => timestamp.TimestampTypeId === TimestampType.MARKETED_ON,
)?.Date,
),
'Offers Note': project.Notes?.find((note) => note.NoteTypeId === NoteTypes.OFFER)?.Note,
Purchaser: project.Notes?.find((note) => note.NoteTypeId === NoteTypes.PURCHASER)?.Note,
};
Expand Down
6 changes: 3 additions & 3 deletions react-app/src/components/users/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Agency } from '@/hooks/api/useAgencyApi';
import { User } from '@/hooks/api/useUsersApi';
import { LookupContext } from '@/contexts/lookupContext';
import { Role } from '@/constants/roles';
import { getProvider } from '@/utilities/helperFunctions';
import { getProvider, makeDateOrUndefined } from '@/utilities/helperFunctions';

const CustomMenuItem = (props: PropsWithChildren & { value: string }) => {
const theme = useTheme();
Expand Down Expand Up @@ -227,9 +227,9 @@ const UsersTable = (props: IUsersTable) => {
Email: user.Email,
Status: user.Status,
Agency: user.Agency?.Name,
'Last Login': user.LastLogin,
'Last Login': makeDateOrUndefined(user.LastLogin),
Role: user.Role?.Name,
'Created On': user.CreatedOn,
'Created On': makeDateOrUndefined(user.CreatedOn),
Position: user.Position,
};
});
Expand Down
54 changes: 26 additions & 28 deletions react-app/src/pages/BulkUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import {
Typography,
} from '@mui/material';
import { DataGrid, gridClasses, GridColDef } from '@mui/x-data-grid';
import React, { useEffect, useMemo, useState } from 'react';
import xlsx from 'node-xlsx';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import sheetjs from 'xlsx';
import { SnackBarContext } from '@/contexts/snackbarContext';

const ResultsPaper = (props: {
results: ImportResult[];
Expand Down Expand Up @@ -96,6 +97,7 @@ const BulkUpload = () => {
const [file, setFile] = useState<File>();
const [fileProgress, setFileProgress] = useState(0);
const api = usePimsApi();
const snackbar = useContext(SnackBarContext);
const { submit } = useDataSubmitter(api.properties.uploadBulkSpreadsheet);
const { refreshData: refreshResults, data: importResults } = useDataLoader(() =>
api.properties.getImportResults({ quantity: 1, sortKey: 'CreatedOn', sortOrder: 'DESC' }),
Expand Down Expand Up @@ -128,25 +130,10 @@ const BulkUpload = () => {
];
const resultRows = useMemo(() => {
if (importResults?.length && importResults.at(0).Results != null)
return importResults
.at(0)
.Results.slice()
.sort((a, b) => a.rowNumber - b.rowNumber);
return importResults.at(0).Results.sort((a, b) => a.rowNumber - b.rowNumber);
else return [];
}, [importResults]);

const buildXlsxBuffer = (result: ImportResult) => {
const rows = [];
if (!result?.Results?.length) {
return xlsx.build([{ name: `Results`, data: [], options: {} }]);
}
rows.push(['Row No.', 'Action', 'Reason']);
for (const item of result.Results) {
rows.push([item.rowNumber, item.action, item.reason ?? '']);
}
return xlsx.build([{ name: `Results`, data: rows, options: {} }]);
};

return (
<Box
display={'flex'}
Expand Down Expand Up @@ -295,16 +282,27 @@ const BulkUpload = () => {
</Box>
<ResultsPaper
onDownloadClick={() => {
const buffer = buildXlsxBuffer(importResults?.at(0));
const blob = new Blob([buffer]);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
document.body.appendChild(a);
a.href = url;
a.download = `Result_${importResults?.at(0)?.FileName}_${importResults?.at(0)?.CreatedOn}.xlsx`;
a.click();
window.URL.revokeObjectURL(url);
a.remove();
if (importResults?.at(0)) {
const columnHeaders = ['Row No.', 'Action', 'Reason'];
const fileName = `Result_${importResults?.at(0)?.FileName}_${new Date(importResults?.at(0)?.CreatedOn).toLocaleDateString('iso')}.xlsx`;

// Build xlsx file
const worksheet = sheetjs.utils.aoa_to_sheet([
columnHeaders,
...resultRows.map((result) => [result.rowNumber, result.action, result.reason ?? '']),
]);
const workbook = sheetjs.utils.book_new();
sheetjs.utils.book_append_sheet(workbook, worksheet, 'Bulk Upload Results');
// Download the file
sheetjs.writeFile(workbook, fileName);
} else {
// Should never hit this point based on UI, but just in case.
snackbar.setMessageState({
style: snackbar.styles.warning,
open: true,
text: 'No results available for export.',
});
}
}}
rows={resultRows}
results={importResults}
Expand Down
36 changes: 12 additions & 24 deletions react-app/src/utilities/downloadExcelFile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GridRowId, GridValidRowModel } from '@mui/x-data-grid';
import xlsx from 'node-xlsx';
import sheetjs from 'xlsx';

/**
* @interface
Expand All @@ -25,30 +25,18 @@ export const downloadExcelFile = (props: IExcelDownloadProps) => {
if (data.length > 0) {
// Extract column headers
const columnHeaders = Object.keys(data.at(0).model);
// Build xlsx file as bit array buffer
const bitArray = xlsx.build([
{
name: tableName,
data: [columnHeaders, ...data.map((row) => Object.values(row.model))],
options: {}, // Required even if empty
},
]);
// Combine into one string of data
const binaryString = bitArray.reduce((acc, cur) => (acc += String.fromCharCode(cur)), '');
// Convert data into file
const file = window.btoa(binaryString);
const url = `data:application/xlsx;base64,${file}`;
// Create file name
const fileName = `${includeDate ? new Date().toISOString().substring(0, 10) : ''}_${tableName}${
const fileName = `${includeDate ? new Date().toLocaleDateString('iso') : ''}_${tableName}${
'-' + filterName || ''
}`;
// Download file
const a = document.createElement('a');
a.href = url;
a.download = `${fileName}.xlsx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}.xlsx`;
// Build xlsx file
const worksheet = sheetjs.utils.aoa_to_sheet(
[columnHeaders, ...data.map((row) => Object.values(row.model))],
{ cellDates: true },
);
const workbook = sheetjs.utils.book_new();
sheetjs.utils.book_append_sheet(workbook, worksheet, tableName);
// Download the file
sheetjs.writeFile(workbook, fileName);
}
};
11 changes: 11 additions & 0 deletions react-app/src/utilities/helperFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,14 @@ export const getProvider = (username: string, bcscIdentifier?: string) => {

export const validateEmail = (email: string): boolean =>
z.string().email().safeParse(email).success;

/**
* SheetJS does not like to receive invalid dates. Use this function to avoid that if value is unpopulated.
* @param date
* @returns Date or undefined
*/
export const makeDateOrUndefined = (date: unknown | undefined) => {
if (!date) return undefined;
if (typeof date == 'string' && (date as string).length === 0) return undefined;
return new Date(date as string | number | Date);
};
Loading