diff --git a/package.json b/package.json index f420183f36..29b8ab6f1d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@hospitalrun/components": "~1.16.0", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", + "@types/json2csv": "~5.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.5.0", "date-fns": "~2.16.0", @@ -15,6 +16,7 @@ "i18next": "~19.7.0", "i18next-browser-languagedetector": "~6.0.0", "i18next-xhr-backend": "~3.2.2", + "json2csv": "~5.0.1", "lodash": "^4.17.15", "node-sass": "~4.14.0", "pouchdb": "~7.2.1", diff --git a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx index d01ef96a52..c0168d6794 100644 --- a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx @@ -1,4 +1,5 @@ -import { Table } from '@hospitalrun/components' +import { Table, Dropdown } from '@hospitalrun/components' +import format from 'date-fns/format' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -6,7 +7,7 @@ import { act } from 'react-dom/test-utils' import { Router } from 'react-router' import IncidentFilter from '../../../incidents/IncidentFilter' -import ViewIncidentsTable from '../../../incidents/list/ViewIncidentsTable' +import ViewIncidentsTable, { populateExportData } from '../../../incidents/list/ViewIncidentsTable' import IncidentSearchRequest from '../../../incidents/model/IncidentSearchRequest' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' @@ -73,6 +74,58 @@ describe('View Incidents Table', () => { expect(incidentsTable.prop('actionsHeaderText')).toEqual('actions.label') }) + it('should display a download button', async () => { + const expectedIncidents: Incident[] = [ + { + id: 'incidentId1', + code: 'someCode', + date: new Date(2020, 7, 4, 0, 0, 0, 0).toISOString(), + reportedOn: new Date(2020, 8, 4, 0, 0, 0, 0).toISOString(), + reportedBy: 'com.test:user', + status: 'reported', + } as Incident, + ] + const { wrapper } = await setup({ status: IncidentFilter.all }, expectedIncidents) + + const dropDownButton = wrapper.find(Dropdown) + expect(dropDownButton.exists()).toBeTruthy() + }) + + it('should populate export data correctly', async () => { + const data = [ + { + category: 'asdf', + categoryItem: 'asdf', + code: 'I-eClU6OdkR', + createdAt: '2020-09-06T04:02:38.011Z', + date: '2020-09-06T04:02:32.855Z', + department: 'asdf', + description: 'asdf', + id: 'af9f968f-61d9-47c3-9321-5da3f381c38b', + reportedBy: 'some user', + reportedOn: '2020-09-06T04:02:38.011Z', + rev: '1-91d1ba60588b779c9554c7e20e15419c', + status: 'reported', + updatedAt: '2020-09-06T04:02:38.011Z', + }, + ] + + const expectedExportData = [ + { + code: 'I-eClU6OdkR', + date: format(new Date(data[0].date), 'yyyy-MM-dd hh:mm a'), + reportedBy: 'some user', + reportedOn: format(new Date(data[0].reportedOn), 'yyyy-MM-dd hh:mm a'), + status: 'reported', + }, + ] + + const exportData = [{}] + populateExportData(exportData, data) + + expect(exportData).toEqual(expectedExportData) + }) + it('should format the data correctly', async () => { const expectedIncidents: Incident[] = [ { diff --git a/src/__tests__/shared/utils/DataHelpers.test.ts b/src/__tests__/shared/utils/DataHelpers.test.ts new file mode 100644 index 0000000000..16342f7414 --- /dev/null +++ b/src/__tests__/shared/utils/DataHelpers.test.ts @@ -0,0 +1,33 @@ +import { getCSV, DownloadLink } from '../../../shared/util/DataHelpers' + +describe('Use Data Helpers util', () => { + it('should construct csv', () => { + const input = [ + { + code: 'I-eClU6OdkR', + date: '2020-09-06 12:02 PM', + reportedBy: 'some user', + reportedOn: '2020-09-06 12:02 PM', + status: 'reported', + }, + ] + const output = getCSV(input).replace(/(\r\n|\n|\r)/gm, '') + const expectedOutput = + '"code","date","reportedBy","reportedOn","status""I-eClU6OdkR","2020-09-06 12:02 PM","some user","2020-09-06 12:02 PM","reported"' + expect(output).toMatch(expectedOutput) + }) + + it('should download data as expected', () => { + const response = DownloadLink('data to be downloaded', 'filename.txt') + + const element = document.createElement('a') + element.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent('data to be downloaded')}`, + ) + element.setAttribute('download', 'filename.txt') + + element.style.display = 'none' + expect(response).toEqual(element) + }) +}) diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index d57532e3f4..e38f723de2 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -1,9 +1,10 @@ -import { Spinner, Table } from '@hospitalrun/components' +import { Spinner, Table, Dropdown } from '@hospitalrun/components' import format from 'date-fns/format' import React from 'react' import { useHistory } from 'react-router' import useTranslator from '../../shared/hooks/useTranslator' +import { DownloadLink, getCSV } from '../../shared/util/DataHelpers' import { extractUsername } from '../../shared/util/extractUsername' import useIncidents from '../hooks/useIncidents' import IncidentSearchRequest from '../model/IncidentSearchRequest' @@ -12,6 +13,27 @@ interface Props { searchRequest: IncidentSearchRequest } +export function populateExportData(dataToPopulate: any, theData: any) { + let first = true + if (theData != null) { + theData.forEach((elm: any) => { + const entry = { + code: elm.code, + date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), + reportedBy: elm.reportedBy, + reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), + status: elm.status, + } + if (first) { + dataToPopulate[0] = entry + first = false + } else { + dataToPopulate.push(entry) + } + }) + } +} + function ViewIncidentsTable(props: Props) { const { searchRequest } = props const { t } = useTranslator() @@ -22,33 +44,85 @@ function ViewIncidentsTable(props: Props) { return } + // filter data + const exportData = [{}] + + function downloadCSV() { + populateExportData(exportData, data) + + const csv = getCSV(exportData) + + const incidentsText = t('incidents.label') + + const filename = incidentsText + .concat('-') + .concat(format(new Date(Date.now()), 'yyyy-MM-dd--hh-mma')) + .concat('.csv') + + DownloadLink(csv, filename) + } + + const dropdownItems = [ + { + onClick: function runfun() { + downloadCSV() + }, + text: 'CSV', + }, + ] + + const dropStyle = { + marginLeft: 'auto', // note the capital 'W' here + marginBottom: '4px', // 'ms' is the only lowercase vendor prefix + } + return ( - row.id} - data={data} - columns={[ - { label: t('incidents.reports.code'), key: 'code' }, - { - label: t('incidents.reports.dateOfIncident'), - key: 'date', - formatter: (row) => (row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : ''), - }, - { - label: t('incidents.reports.reportedBy'), - key: 'reportedBy', - formatter: (row) => extractUsername(row.reportedBy), - }, - { - label: t('incidents.reports.reportedOn'), - key: 'reportedOn', - formatter: (row) => - row.reportedOn ? format(new Date(row.reportedOn), 'yyyy-MM-dd hh:mm a') : '', - }, - { label: t('incidents.reports.status'), key: 'status' }, - ]} - actionsHeaderText={t('actions.label')} - actions={[{ label: t('actions.view'), action: (row) => history.push(`incidents/${row.id}`) }]} - /> + <> + +
row.id} + data={data} + columns={[ + { + label: t('incidents.reports.code'), + key: 'code', + }, + { + label: t('incidents.reports.dateOfIncident'), + key: 'date', + formatter: (row) => (row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : ''), + }, + { + label: t('incidents.reports.reportedBy'), + key: 'reportedBy', + formatter: (row) => extractUsername(row.reportedBy), + }, + { + label: t('incidents.reports.reportedOn'), + key: 'reportedOn', + formatter: (row) => + row.reportedOn ? format(new Date(row.reportedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { + label: t('incidents.reports.status'), + key: 'status', + }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: t('actions.view'), + action: (row) => history.push(`incidents/${row.id}`), + }, + ]} + /> + ) } diff --git a/src/shared/locales/enUs/translations/incidents/index.ts b/src/shared/locales/enUs/translations/incidents/index.ts index 94029c1580..429418910e 100644 --- a/src/shared/locales/enUs/translations/incidents/index.ts +++ b/src/shared/locales/enUs/translations/incidents/index.ts @@ -17,6 +17,7 @@ export default { resolve: 'Resolve Incident', dateOfIncident: 'Date of Incident', department: 'Department', + download: 'Download', category: 'Category', categoryItem: 'Category Item', description: 'Description of Incident', diff --git a/src/shared/util/DataHelpers.tsx b/src/shared/util/DataHelpers.tsx new file mode 100644 index 0000000000..c8797f3235 --- /dev/null +++ b/src/shared/util/DataHelpers.tsx @@ -0,0 +1,22 @@ +import { Parser } from 'json2csv' + +export function getCSV(data: T[]): string { + const fields = Object.keys(data[0]) + const opts = { fields } + const parser = new Parser(opts) + const csv = parser.parse(data) + return csv +} + +export function DownloadLink(data: string, fileName: string) { + const text = data + const element = document.createElement('a') + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) + element.setAttribute('download', fileName) + + element.style.display = 'none' + document.body.appendChild(element) + element.click() + + return document.body.removeChild(element) +}