Skip to content

Commit

Permalink
Merge branch 'main' into PIMS-2135-Bulk-Upload-Bug
Browse files Browse the repository at this point in the history
  • Loading branch information
dbarkowsky authored Oct 11, 2024
2 parents b7276e9 + 79a4792 commit 559db32
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 86 deletions.
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);
};

0 comments on commit 559db32

Please sign in to comment.