Skip to content

Commit

Permalink
(refactor) O3-4059 Fix Inaccurate Test Result Time Stamps
Browse files Browse the repository at this point in the history
  • Loading branch information
dependabot[bot] authored and CynthiaKamau committed Oct 18, 2024
1 parent eab6490 commit 9f9b4cf
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type TimelineData,
} from './filter-types';
import reducer from './filter-reducer';
import { type MappedObservation, type TestResult, type GroupedObservation, type Observation } from '../../types';

const initialState: ReducerState = {
checkboxes: {},
Expand All @@ -22,6 +23,7 @@ const initialContext = {
state: initialState,
...initialState,
timelineData: null,
tableData: null,
trendlineData: null,
activeTests: [],
someChecked: false,
Expand Down Expand Up @@ -93,6 +95,50 @@ const FilterProvider = ({ roots, children }: FilterProviderProps) => {
};
}, [activeTests, state.tests]);

const tableData = useMemo<GroupedObservation[]>(() => {
const flattenedObs: Observation[] = [];

for (const key in state.tests) {
const test = state.tests[key] as TestResult;
if (test.obs && Array.isArray(test.obs)) {
test.obs.forEach((obs) => {
const flattenedEntry = {
...obs,
key: key,
...test,
};
flattenedObs.push(flattenedEntry);
});
}
}

const groupedObs: Record<string, GroupedObservation> = {};

flattenedObs.forEach((curr: MappedObservation) => {
const flatNameParts = curr.flatName.split('-');
const groupKey = flatNameParts.length > 1 ? flatNameParts[1].trim() : flatNameParts[0].trim();
const dateKey = new Date(curr.obsDatetime).toISOString().split('T')[0];

const compositeKey = `${groupKey}__${dateKey}`;
if (!groupedObs[compositeKey]) {
groupedObs[compositeKey] = {
key: groupKey,
date: dateKey,
flatName: curr.flatName,
entries: [],
};
}

groupedObs[compositeKey].entries.push(curr);
});

const resultArray = Object.values(groupedObs).sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);

return resultArray;
}, [state.tests]);

useEffect(() => {
if (roots?.length && !Object.keys(state?.parents).length) {
actions.initialize(roots);
Expand All @@ -113,6 +159,7 @@ const FilterProvider = ({ roots, children }: FilterProviderProps) => {
value={{
...state,
timelineData,
tableData,
activeTests,
someChecked,
totalResultsCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface TimelineData {

export interface FilterContextProps extends ReducerState {
timelineData: TimelineData;
tableData?: any;
activeTests: string[];
someChecked: boolean;
totalResultsCount: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ import {
TableHeader,
TableRow,
} from '@carbon/react';
import { ArrowRightIcon, showModal, useLayoutType, isDesktop, formatDate } from '@openmrs/esm-framework';
import { ArrowRightIcon, showModal, useLayoutType, isDesktop, formatDatetime } from '@openmrs/esm-framework';
import { getPatientUuidFromUrl, type OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib';
import { type RowData } from '../filter/filter-types';
import styles from './individual-results-table.scss';
import { type GroupedObservation } from '../../types';

interface IndividualResultsTableProps {
isLoading: boolean;
parent: {
display: string;
};
subRows: Array<RowData>;
subRows: GroupedObservation;
index: number;
title: string;
}

const getClasses = (interpretation: OBSERVATION_INTERPRETATION) => {
Expand Down Expand Up @@ -53,12 +51,12 @@ const getClasses = (interpretation: OBSERVATION_INTERPRETATION) => {
}
};

const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({ isLoading, parent, subRows, index }) => {
const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({ isLoading, subRows, index, title }) => {
const { t } = useTranslation();
const layout = useLayoutType();
const patientUuid = getPatientUuidFromUrl();

const headerTitle = t(parent.display);
const headerTitle = t(title);

const launchResultsDialog = useCallback(
(title: string, testUuid: string) => {
Expand All @@ -85,9 +83,9 @@ const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({ isLoadi

const tableRows = useMemo(
() =>
subRows.map((row, i) => {
const { units = '', range = '', obs: values } = row;
const isString = isNaN(parseFloat(values?.[0]?.value));
subRows.entries.map((row, i) => {
const { units = '', range = '' } = row;
const isString = isNaN(parseFloat(row.value));

return {
...row,
Expand All @@ -107,8 +105,8 @@ const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({ isLoadi
</span>
),
value: {
value: `${row.obs[0]?.value ?? ''} ${row.units ?? ''}`,
interpretation: row.obs[0]?.interpretation,
value: `${row.value} ${row.units ?? ''}`,
interpretation: row?.interpretation,
},
referenceRange: `${range || '--'} ${units || '--'}`,
};
Expand All @@ -118,19 +116,15 @@ const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({ isLoadi

if (isLoading) return <DataTableSkeleton role="progressbar" compact={isDesktop} zebra />;

if (subRows?.length) {
if (subRows.entries?.length) {
return (
<DataTable rows={tableRows} headers={tableHeaders} data-floating-menu-container useZebraStyles>
{({ rows, headers, getHeaderProps, getTableProps }) => (
<TableContainer>
<div className={styles.cardTitle}>
<h4 className={styles.resultType}>{headerTitle}</h4>
<div className={styles.displayFlex}>
<span className={styles.date}>
{subRows[0]?.obs[0]?.obsDatetime
? formatDate(new Date(subRows[0]?.obs[0]?.obsDatetime), { mode: 'standard' })
: ''}
</span>
<span className={styles.date}>{subRows.date ?? ''}</span>
<Button
className={styles.viewTimeline}
iconDescription="view timeline"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,50 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { type OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib';
import IndividualResultsTable from './individual-results-table.component';
import { type GroupedObservation } from '../../types';

describe('IndividualResultsTable', () => {
const mockSubRows = [
{
obs: [
{
obsDatetime: '2021-01-13 02:10:06.0',
value: '52.1',
interpretation: 'NORMAL' as const as OBSERVATION_INTERPRETATION,
},
],
datatype: 'Numeric',
lowAbsolute: 0,
display: 'Prothrombin time',
conceptUuid: '161481AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
units: 'Minute',
flatName: 'Hematology-Prothrombin Time (with INR)-Prothrombin time',
hasData: true,
entries: [
null,
null,
null,
{
obsDatetime: '2021-01-13 02:10:06.0',
value: '52.1',
interpretation: 'NORMAL' as const as OBSERVATION_INTERPRETATION,
},
],
},
];
const mockSubRows = {
key: 'HIV viral load',
date: '2024-10-15',
flatName: 'HIV viral load-HIV viral load',
entries: [
{
obsDatetime: '2024-10-15 03:20:19.0',
value: '45',
interpretation: 'NORMAL',
key: 'HIV viral load-HIV viral load',
datatype: 'Numeric',
lowAbsolute: 0,
display: 'HIV viral load',
conceptUuid: '856AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
units: 'copies/ml',
flatName: 'HIV viral load-HIV viral load',
hasData: true,
},
],
} as GroupedObservation;

const mockEmptySubRows = {
key: 'HIV viral load',
date: '2024-10-15',
flatName: 'HIV viral load-HIV viral load',
entries: [],
} as GroupedObservation;

it('renders a loading skeleton when fetching results data', () => {
render(<IndividualResultsTable isLoading={true} parent={{ display: 'Parent Test' }} subRows={[]} index={0} />);
render(<IndividualResultsTable isLoading={true} subRows={mockEmptySubRows} index={0} title={'HIV viral load'} />);

expect(screen.getByRole('progressbar')).toBeInTheDocument();
});

it('renders a tabular overview of the available test result data', () => {
render(
<IndividualResultsTable isLoading={false} parent={{ display: 'Parent Test' }} subRows={mockSubRows} index={0} />,
);
render(<IndividualResultsTable isLoading={false} subRows={mockSubRows} index={0} title={'HIV viral load'} />);

expect(screen.getByText(/13-jan-2021/i)).toBeInTheDocument();
expect(screen.getByText(/2024-10-15/i)).toBeInTheDocument();
expect(screen.getByText(/test name/i)).toBeInTheDocument();
expect(screen.getByText(/reference range/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /view timeline/i })).toBeInTheDocument();
expect(screen.getByRole('row', { name: /prothrombin time 52.1 minute -- minute/i })).toBeInTheDocument();
expect(screen.getByRole('row', { name: /hiv viral load 45 copies\/ml -- copies\/ml/i })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const PanelTimelineComponent: React.FC<PanelTimelineComponentProps> = ({ activeP
const { t } = useTranslation();
const rows: Array<ObsRecord> = activePanel ? [activePanel, ...activePanel?.relatedObs] : [];
const mappedObservations = Object.fromEntries(rows.map((obs) => [obs.name, groupedObservations[obs.conceptUuid]]));

const allTimes = []
.concat(...Object.values(mappedObservations).map((obsRecords) => obsRecords.map((obs) => obs.effectiveDateTime)))
.sort((time1, time2) => Date.parse(time2) - Date.parse(time1));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { useTranslation } from 'react-i18next';
import { AccordionSkeleton, DataTableSkeleton, Button, Layer } from '@carbon/react';
import { useLayoutType, TreeViewAltIcon } from '@openmrs/esm-framework';
import { EmptyState } from '@openmrs/esm-patient-common-lib';
import { type viewOpts } from '../../types';
import { type GroupedObservation, type viewOpts } from '../../types';
import FilterSet, { FilterContext } from '../filter';
import GroupedTimeline from '../grouped-timeline';
import IndividualResultsTable from '../individual-results-table/individual-results-table.component';
import TabletOverlay from '../tablet-overlay';
import Trendline from '../trendline/trendline.component';
import usePanelData from '../panel-view/usePanelData';
import styles from '../results-viewer/results-viewer.scss';
import RecentOverview from '../overview/recent-overview.component';

interface TreeViewProps {
patientUuid: string;
Expand All @@ -28,41 +29,34 @@ const GroupedPanelsTables: React.FC<{ className: string; loadingPanelData: boole
loadingPanelData,
}) => {
const { t } = useTranslation();
const { timelineData, parents, checkboxes, someChecked, lowestParents } = useContext(FilterContext);
const { checkboxes, someChecked, tableData } = useContext(FilterContext);
const selectedCheckboxes = Object.keys(checkboxes).filter((key) => checkboxes[key]);

const {
data: { rowData },
} = timelineData;

const filteredParents = lowestParents?.filter(
(parent) => parents[parent.flatName].some((kid) => checkboxes[kid]) || !someChecked,
);

if (rowData && rowData?.length === 0) {
if (tableData && tableData?.length === 0) {
return <EmptyState displayText={t('data', 'data')} headerTitle={t('dataTimelineText', 'Data timeline')} />;
}

return (
<Layer className={className}>
{filteredParents?.map((parent, index) => {
const subRows = someChecked
? rowData?.filter(
(row: { flatName: string }) =>
parents[parent.flatName].includes(row.flatName) && checkboxes[row.flatName],
)
: rowData?.filter((row: { flatName: string }) => parents[parent.flatName].includes(row.flatName));

return subRows.length > 0 ? (
<div
key={parent.flatName}
className={classNames({
[styles.border]: subRows.length,
})}
>
<IndividualResultsTable isLoading={loadingPanelData} parent={parent} subRows={subRows} index={index} />
</div>
) : null;
})}
{tableData
?.filter((row) => !someChecked || selectedCheckboxes.some((selectedKey) => row.flatName.includes(selectedKey)))
.map((subRows: GroupedObservation, index) => {
return subRows.entries?.length > 0 ? (
<div
key={index}
className={classNames({
[styles.border]: subRows?.entries.length,
})}
>
<IndividualResultsTable
isLoading={loadingPanelData}
subRows={subRows}
index={index}
title={subRows.key}
/>
</div>
) : null;
})}
</Layer>
);
};
Expand All @@ -72,8 +66,8 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, basePath, testUuid, is
const [showTreeOverlay, setShowTreeOverlay] = useState(false);
const { t } = useTranslation();

const { timelineData, resetTree, someChecked } = useContext(FilterContext);
const { panels, isLoading: isLoadingPanelData, groupedObservations } = usePanelData();
const { timelineData, resetTree } = useContext(FilterContext);
const { isLoading: isLoadingPanelData } = usePanelData();

if (tablet) {
return (
Expand Down Expand Up @@ -128,11 +122,7 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, basePath, testUuid, is
<GroupedPanelsTables className={styles.groupPanelsTables} loadingPanelData={isLoading} />
</div>
) : view === 'over-time' ? (
panels.map((panel) => (
<div key={`panel-${panel.id}`} className={styles.panelViewTimeline}>
<GroupedTimeline patientUuid={patientUuid} />
</div>
))
<GroupedTimeline patientUuid={patientUuid} />
) : null}
</div>
</>
Expand Down
Loading

0 comments on commit 9f9b4cf

Please sign in to comment.