Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(refactor) O3-4059 Fix Inaccurate Test Result Time Stamps #2067

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,23 @@ import {
TableHeader,
TableRow,
} from '@carbon/react';
import { ArrowRightIcon, showModal, useLayoutType, isDesktop, formatDate } from '@openmrs/esm-framework';
import {
ArrowRightIcon,
showModal,
useLayoutType,
isDesktop,
formatDatetime,
formatDate,
} 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 +58,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 +90,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 +112,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 +123,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}>{formatDate(new Date(subRows.date), { mode: 'standard' })}</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(/15-Oct-2024/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