Skip to content

Commit

Permalink
Feature/site metrics export (#316)
Browse files Browse the repository at this point in the history
* Begin refactoring code for computing site metrics

* Move functions to single file

* Add file

* Enabled site metric download

* Remove print statements

* Add some columns, adjust some formatting

* Add some columns, adjust some formatting

* Add warning before downloading

* Adjust text
  • Loading branch information
davidborland authored Apr 28, 2023
1 parent bf2f78a commit 33bad8e
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 23,778 deletions.
23,753 changes: 55 additions & 23,698 deletions frontend/package-lock.json

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions frontend/src/components/Forms/SiteMetricsDownload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import {
Button, Checkbox, Divider, Fade, FormControlLabel, FormGroup, Paper, Popper, Tooltip, Typography,
} from '@material-ui/core'
import api from '../../Api'
import { DownloadIcon } from '../Icons/Download'
import { Warning as WarningIcon } from '@material-ui/icons';
import { useStore } from '../../contexts'
import { CSVLink } from 'react-csv'
import { convertEnrollmentData, computeMetrics } from '../../utils/sites'

//

const columns = [
{ label: 'Proposal ID', key: 'ProposalID' },
{ label: 'Proposal Name', key: 'ProposalTitle' },
{ label: 'Protocol (Short Description)', key: 'ProposalDescription' },
{ label: 'CTSA Name', key: 'ctsaName' },
{ label: 'Site ID', key: 'siteId' },
{ label: 'CTSA ID', key: 'ctsaId' },
{ label: 'Site name', key: 'siteName' },
{ label: 'Site Number', key: 'siteNumber' },
{ label: 'PI', key: 'principalInvestigator' },
{ label: 'Date Protocol Sent', key: 'dateRegPacketSent' },
{ label: 'Contract Sent', key: 'dateContractSent' },
{ label: 'IRB Submission', key: 'dateIrbSubmission' },
{ label: 'IRB Approval', key: 'dateIrbApproval' },
{ label: 'ContractExecution', key: 'dateContractExecution' },
{ label: 'Site Activation', key: 'dateSiteActivated' },
{ label: 'FPFV', key: 'fpfv' },
{ label: 'LPFV', key: 'lpfv' },
{ label: 'Enrollment', key: 'enrollment'},
{ label: 'Patients Consented', key: 'patientsConsentedCount'},
{ label: 'Patients Enrolled', key: 'patientsEnrolledCount'},
{ label: 'Patients Withdrawn', key: 'patientsWithdrawnCount'},
{ label: 'Patients Expected', key: 'patientsExpectedCount'},
{ label: 'Protocol Deviations', key: 'protocolDeviationsCount'},
{ label: 'Lost to Follow Up', key: 'lostToFollowUp'},
{ label: 'Protocol to FPFV', key: 'protocolToFpfv' },
{ label: 'Contract Execution Time', key: 'contractExecutionTime' },
{ label: 'sIRB Approval Time', key: 'sirbApprovalTime' },
{ label: 'Site Open to FPFV', key: 'siteOpenToFpfv' },
{ label: 'Site Open to LPFV', key: 'siteOpenToLpfv' },
{ label: 'Percent of consented patients randomized', key: 'percentConsentedPtsRandomized' },
{ label: 'Actual to expected randomized patient ratio', key: 'actualToExpectedRandomizedPtRatio' },
{ label: 'Ratio of randomized patients that dropped out of the study', key: 'ratioRandomizedPtsDropout' },
{ label: 'Major protocol deviations per randomized patient', key: 'majorProtocolDeviationsPerRandomizedPt' },
{ label: 'Number of Queries', key: 'queriesCount' },
{ label: 'Queries per patient', key: 'queriesPerConsentedPatient' },
];

export const SiteMetricsDownload = () => {
const [{ proposals }] = useStore()
const [sites, setSites] = useState([])
const [popperAnchor, setPopperAnchor] = useState(null)
const [open, setOpen] = useState(false)

useEffect(() => {
const getSites = async () => {
try {
const sites = []
for (const proposal of proposals) {
const response = await axios.get(api.studySitesByProposalId(proposal.proposalID))

const proposalSites = response.data

proposalSites.forEach(site => {
convertEnrollmentData(site)
computeMetrics(site)

site.ProposalTitle = proposal.shortTitle
site.ProposalDescription = proposal.shortDescription
});

sites.push(...proposalSites)
}

setSites(sites)
}
catch (error) {
console.log(error)
}
};

getSites();
}, [proposals])

const onClickOpen = event => {
setPopperAnchor(event.currentTarget)
setOpen(prevOpen => !prevOpen)
}

return (
<>
<Tooltip title='Download site metrics' aria-label='Download site metrics'>
<Button
variant='outlined'
startIcon={ <DownloadIcon /> }
onClick={ onClickOpen }
>Site Metrics</Button>
</Tooltip>
<Popper
open={ open }
anchorEl={ popperAnchor }
placement='bottom'
transition
style={{ zIndex: 99 }}
>
{
({ TransitionProps }) => (
<Fade { ...TransitionProps } timeout={ 350 }>
<Paper style={{ width: '200px',
display: 'flex',
flexDirection: 'column', }}>
<Typography
variant='h6'
style={{ padding: '0.5rem 1rem' }}
>
Site Metrics Download
</Typography>

<Divider />

<div style={{ padding: '0.5rem', display: 'flex', gap: '0.5rem' }}>
<div><WarningIcon /></div>
<div>After you click the DOWNLOAD button wait for the file to finish downloading before opening, or data may be missing.</div>
</div>

<Divider />

<Button
component={ CSVLink }
headers={ columns }
data={ sites }
filename='site-metrics'
>Download</Button>
</Paper>
</Fade>
)
}
</Popper>
</>
)
}
1 change: 1 addition & 0 deletions frontend/src/components/Forms/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './DownloadButton'
export * from './DropZone'
export * from './ProposalDetails'
export * from './StudiesDownload'
export * from './SiteMetricsDownload'
export * from './TaskManager'
46 changes: 10 additions & 36 deletions frontend/src/components/Tables/DetailPanels/SiteDetailPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,7 @@ import React from 'react'
import { Grid, List, ListItemIcon, ListItem, ListItemText } from '@material-ui/core'
import { DetailPanel } from './DetailPanel'
import { StarBullet } from '../../Bullets'
import { formatDate } from '../../../utils/DateFormat'

const invalidDisplay = 'N/A'

export const dayCount = (startDate, endDate) => {
if (startDate && endDate) {
const num = Math.round((new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24))
return `${ num } day${ num === 1 ? '' : 's' }`
} else {
return invalidDisplay
}
}

export const displayRatio = (a, b, precision = 2) => {
a = parseInt(a)
b = parseInt(b)
if ( !a || !b ) {
return invalidDisplay
}
if (a === 0) {
if (b === 0) return invalidDisplay
return `0% (${ a }/${ b })`
}
return b !== 0
? `${ (100 * a/b).toFixed(precision) }% (${ a }/${ b })`
: `N/A`
}
import { dayCountDisplay, percentDisplay, invalidDisplay } from '../../../utils/sites'

const displayRatioAsWholeNumberString = (a, b) => {
return b === 0 ? invalidDisplay : `${ Math.round(a / b) }${ a } / ${ b }`
Expand All @@ -53,23 +27,23 @@ export const SiteDetailPanel = props => {

<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="Activation (protocol to FPFV):" secondary={ dayCount(dateRegPacketSent, fpfv) } />
<ListItemText primary="Activation (protocol to FPFV):" secondary={ dayCountDisplay(dateRegPacketSent, fpfv) } />
</ListItem>
<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="Contract execution time:" secondary={ dayCount(dateContractSent, dateContractExecution) } />
<ListItemText primary="Contract execution time:" secondary={ dayCountDisplay(dateContractSent, dateContractExecution) } />
</ListItem>
<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="sIRB approval time:" secondary={ dayCount(dateIrbSubmission, dateIrbApproval) } />
<ListItemText primary="sIRB approval time:" secondary={ dayCountDisplay(dateIrbSubmission, dateIrbApproval) } />
</ListItem>
<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="Site open to FPFV:" secondary={ dayCount(dateSiteActivated, fpfv) } />
<ListItemText primary="Site open to FPFV:" secondary={ dayCountDisplay(dateSiteActivated, fpfv) } />
</ListItem>
<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="Site open to LPFV:" secondary={ dayCount(dateSiteActivated, lpfv) } />
<ListItemText primary="Site open to LPFV:" secondary={ dayCountDisplay(dateSiteActivated, lpfv) } />
</ListItem>
</List>
</Grid>
Expand All @@ -80,19 +54,19 @@ export const SiteDetailPanel = props => {

<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="Percent of consented patients randomized:" secondary={ displayRatio(patientsEnrolledCount, patientsConsentedCount) } />
<ListItemText primary="Percent of consented patients randomized:" secondary={ percentDisplay(patientsEnrolledCount, patientsConsentedCount) } />
</ListItem>
<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="Actual to expected randomized patient ratio:" secondary={ displayRatio(patientsEnrolledCount, patientsExpectedCount) } />
<ListItemText primary="Actual to expected randomized patient ratio:" secondary={ percentDisplay(patientsEnrolledCount, patientsExpectedCount) } />
</ListItem>
<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="Ratio of randomized patients that dropped out of the study:" secondary={ displayRatio(patientsWithdrawnCount, patientsEnrolledCount) } />
<ListItemText primary="Ratio of randomized patients that dropped out of the study:" secondary={ percentDisplay(patientsWithdrawnCount, patientsEnrolledCount) } />
</ListItem>
<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
<ListItemText primary="Major protocol deviations per randomized patients:" secondary={ displayRatio( protocolDeviationsCount, patientsEnrolledCount) } />
<ListItemText primary="Major protocol deviations per randomized patients:" secondary={ percentDisplay( protocolDeviationsCount, patientsEnrolledCount) } />
</ListItem>
<ListItem>
<ListItemIcon><StarBullet /></ListItemIcon>
Expand Down
47 changes: 13 additions & 34 deletions frontend/src/components/Tables/SitesTable.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import React, { useContext, useEffect } from 'react'
import MaterialTable from 'material-table'
import { StoreContext } from '../../contexts/StoreContext'
import { SiteDetailPanel, dayCount, displayRatio } from './DetailPanels'
import { SiteDetailPanel } from './DetailPanels'
import { EnrollmentBar } from '../Widgets/EnrollmentBar'

const invalidDisplay = 'N/A'

const ratioAsWholeNumberString = (a, b) => {
return b === 0 ? invalidDisplay : Math.round(a / b)
}
import { computeMetrics } from '../../utils/sites'

export const SitesTable = props => {
let { title, sites } = props
Expand All @@ -26,24 +21,8 @@ export const SitesTable = props => {
site.protocol = shortTitle
}

site.protocolToFpfv = dayCount(site.dateRegPacketSent, site.fpfv)
site.contractExecutionTime = dayCount(site.dateContractSent, site.dateContractExecution)
site.sirbApprovalTime = dayCount(site.dateIrbSubmission, site.dateIrbApproval)
site.siteOpenToFpfv = dayCount(site.dateSiteActivated, site.fpfv)
site.protocolToLpfv = dayCount(site.dateSiteActivated, site.lpfv)
site.percentConsentedPtsRandomized = displayRatio(site.patientsEnrolledCount, site.patientsConsentedCount)
site.actualToExpectedRandomizedPtRatio = displayRatio(site.patientsEnrolledCount, site.patientsExpectedCount)
site.ratioRandomizedPtsDropout = displayRatio(site.patientsWithdrawnCount, site.patientsEnrolledCount)
site.majorProtocolDeviationsPerRandomizedPt = displayRatio( site.protocolDeviationsCount, site.patientsEnrolledCount)
site.queriesPerConsentedPatient = ratioAsWholeNumberString(site.queriesCount, site.patientsConsentedCount )
computeMetrics(site)
site.shortDescription = site.protocol.shortDescription

// Enrollment
const enrolled = site.patientsEnrolledCount
const expected = site.patientsExpectedCount
const percentEnrolled = expected === 0 ? 0 : Math.round(enrolled / expected * 100)
site.enrollment = `${ enrolled } / ${ expected }: ${ percentEnrolled }%`
site.percentEnrolled = percentEnrolled;
}
}
}, [sites, store.proposals])
Expand Down Expand Up @@ -101,17 +80,17 @@ export const SitesTable = props => {
{ title: 'Patients Expected', field: 'patientsExpectedCount', hidden: true, },
{ title: 'Protocol Deviations', field: 'protocolDeviationsCount', hidden: true, },
{ title: 'Lost to Follow Up', field: 'lostToFollowUp', hidden: true, },
{ title: 'Protocol to FPFV', field: 'protocolToFpfv', hidden: true, },
{ title: 'Contract Execution Time', field: 'contractExecutionTime', hidden: true, },
{ title: 'sIRB Approval Time', field: 'sirbApprovalTime', hidden: true, },
{ title: 'Site Open to FPFV', field: 'siteOpenToFpfv', hidden: true, },
{ title: 'Site Open to LPFV', field: 'protocolToLpfv', hidden: true, },
{ title: 'Percent of consented patients randomized', field: 'percentConsentedPtsRandomized', hidden: true, },
{ title: 'Actual to expected randomized patient ratio', field: 'actualToExpectedRandomizedPtRatio', hidden: true, },
{ title: 'Ratio of randomized patients that dropout of the study', field: 'ratioRandomizedPtsDropout', hidden: true, },
{ title: 'Major Protocol deviations per randomized patient', field: 'majorProtocolDeviationsPerRandomizedPt', hidden: true, },
{ title: 'Protocol to FPFV', field: 'protocolToFpfvDisplay', hidden: true, },
{ title: 'Contract Execution Time', field: 'contractExecutionTimeDisplay', hidden: true, },
{ title: 'sIRB Approval Time', field: 'sirbApprovalTimeDisplay', hidden: true, },
{ title: 'Site Open to FPFV', field: 'siteOpenToFpfvDisplay', hidden: true, },
{ title: 'Site Open to LPFV', field: 'protocolToLpfvDisplay', hidden: true, },
{ title: 'Percent of consented patients randomized', field: 'percentConsentedPtsRandomizedDisplay', hidden: true, },
{ title: 'Actual to expected randomized patient ratio', field: 'actualToExpectedRandomizedPtRatioDisplay', hidden: true, },
{ title: 'Ratio of randomized patients that dropout of the study', field: 'ratioRandomizedPtsDropoutDisplay', hidden: true, },
{ title: 'Major Protocol deviations per randomized patient', field: 'majorProtocolDeviationsPerRandomizedPtDisplay', hidden: true, },
{ title: 'Number of Queries', field: 'queriesCount', hidden: true, },
{ title: 'Queries per patient', field: 'queriesPerConsentedPatient', hidden: true, },
{ title: 'Queries per patient', field: 'queriesPerConsentedPatientDisplay', hidden: true, },
]
}
data={ sites }
Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/sites/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './isSiteActive'
export * from './siteMetrics'
Loading

0 comments on commit 33bad8e

Please sign in to comment.