Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

Commit

Permalink
Merge pull request #2311 from reidmeyer/IncidentsCSV
Browse files Browse the repository at this point in the history
feat(incidents): create downloadable incidents csv report
  • Loading branch information
fox1t committed Sep 7, 2020
2 parents 24f9358 + e50f08b commit 08d7965
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 29 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
"@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",
"escape-string-regexp": "~4.0.0",
"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",
Expand Down
57 changes: 55 additions & 2 deletions src/__tests__/incidents/list/ViewIncidentsTable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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'
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'
Expand Down Expand Up @@ -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[] = [
{
Expand Down
33 changes: 33 additions & 0 deletions src/__tests__/shared/utils/DataHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
128 changes: 101 additions & 27 deletions src/incidents/list/ViewIncidentsTable.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand All @@ -22,33 +44,85 @@ function ViewIncidentsTable(props: Props) {
return <Spinner type="DotLoader" loading />
}

// 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 (
<Table
getID={(row) => 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}`) }]}
/>
<>
<Dropdown
direction="down"
variant="secondary"
text={t('incidents.reports.download')}
style={dropStyle}
items={dropdownItems}
/>
<Table
getID={(row) => 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}`),
},
]}
/>
</>
)
}

Expand Down
1 change: 1 addition & 0 deletions src/shared/locales/enUs/translations/incidents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
22 changes: 22 additions & 0 deletions src/shared/util/DataHelpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Parser } from 'json2csv'

export function getCSV<T>(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)
}

1 comment on commit 08d7965

@vercel
Copy link

@vercel vercel bot commented on 08d7965 Sep 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.