diff --git a/e2e/specs/results-viewer.spec.ts b/e2e/specs/results-viewer.spec.ts index 839aa1f38a..cf69581e32 100644 --- a/e2e/specs/results-viewer.spec.ts +++ b/e2e/specs/results-viewer.spec.ts @@ -264,21 +264,23 @@ test('Record and edit test results', async ({ page }) => { await resultsViewerPage.goTo(patient.uuid); }); - await test.step('And I click on the `Panel` tab', async () => { - await page.getByRole('tab', { name: /panel/i }).click(); + await test.step('And I click on the `Individual tests` tab', async () => { + await page.getByRole('tab', { name: /individual tests/i }).click(); }); await test.step('Then I should see the newly entered test results reflect in the results viewer', async () => { - for (const { resultsPageReference, value } of completeBloodCountData) { - await test.step(resultsPageReference, async () => { - const row = page.locator(`tr:has-text("${resultsPageReference}")`); - const valueCell = row.locator('td:nth-child(2)'); - await expect(valueCell).toContainText(value); - }); - } + await test.step('Then I should see the newly entered test results reflect in the results viewer', async () => { + for (const { resultsPageReference, value } of completeBloodCountData) { + await test.step(resultsPageReference, async () => { + const row = page.locator(`tr:has-text("${resultsPageReference}"):has(td:has-text("${value}"))`).first(); + const valueCell = row.locator('td:nth-child(2)'); + await expect(valueCell).toContainText(value); + }); + } + }); for (const { resultsPageReference, value } of chemistryResultsData) { await test.step(resultsPageReference, async () => { - const row = page.locator(`tr:has-text("${resultsPageReference}")`); + const row = page.locator(`tr:has-text("${resultsPageReference}"):has(td:has-text("${value}"))`).first(); const valueCell = row.locator('td:nth-child(2)'); await expect(valueCell).toContainText(value); }); @@ -338,21 +340,21 @@ test('Record and edit test results', async ({ page }) => { await resultsViewerPage.goTo(patient.uuid); }); - await test.step('And I click on the `Panel` tab', async () => { - await page.getByRole('tab', { name: /panel/i }).click(); + await test.step('And I click on the `Individual tests` tab', async () => { + await page.getByRole('tab', { name: /individual tests/i }).click(); }); await test.step('Then I should see the updated results reflect in the results viewer', async () => { for (const { resultsPageReference, updatedValue } of completeBloodCountData) { await test.step(resultsPageReference, async () => { - const row = page.locator(`tr:has-text("${resultsPageReference}")`); + const row = page.locator(`tr:has-text("${resultsPageReference}"):has(td:has-text("${updatedValue}"))`).first(); const valueCell = row.locator('td:nth-child(2)'); await expect(valueCell).toContainText(updatedValue); }); } for (const { resultsPageReference, updatedValue } of chemistryResultsData) { await test.step(resultsPageReference, async () => { - const row = page.locator(`tr:has-text("${resultsPageReference}")`); + const row = page.locator(`tr:has-text("${resultsPageReference}"):has(td:has-text("${updatedValue}"))`).first(); const valueCell = row.locator('td:nth-child(2)'); await expect(valueCell).toContainText(updatedValue); }); diff --git a/packages/esm-patient-labs-app/src/config-schema.ts b/packages/esm-patient-labs-app/src/config-schema.ts index 445a49de7f..f851627b9d 100644 --- a/packages/esm-patient-labs-app/src/config-schema.ts +++ b/packages/esm-patient-labs-app/src/config-schema.ts @@ -32,12 +32,6 @@ export const configSchema = { }, ], }, - showPrintButton: { - _type: Type.Boolean, - _default: true, - _description: - 'Whether or not to display the print button in the Test Results dashboard. When set to `true`, a print button is shown alongside the panel and tree view content switcher. When clicked, a modal pops up showing a datatable with the available test results. Once the user selects an appropriate date range, they can click on the print button in the modal to print the data', - }, orders: { labOrderTypeUuid: { _type: Type.UUID, @@ -97,7 +91,6 @@ export interface OrderReason { } export interface ConfigObject { resultsViewerConcepts: Array; - showPrintButton: boolean; orders: { labOrderTypeUuid: string; labOrderableConcepts: Array; diff --git a/packages/esm-patient-labs-app/src/test-results/filter/filter-set.component.tsx b/packages/esm-patient-labs-app/src/test-results/filter/filter-set.component.tsx index 04c7b91324..7da54802c5 100644 --- a/packages/esm-patient-labs-app/src/test-results/filter/filter-set.component.tsx +++ b/packages/esm-patient-labs-app/src/test-results/filter/filter-set.component.tsx @@ -1,7 +1,6 @@ import React, { useContext, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Accordion, AccordionItem, Button, Checkbox, Search } from '@carbon/react'; -import { TreeViewAlt, Close, Search as SearchIcon } from '@carbon/react/icons'; +import { Accordion, AccordionItem, Button, Checkbox } from '@carbon/react'; import { useConfig, useLayoutType } from '@openmrs/esm-framework'; import { type ConfigObject } from '../../config-schema'; import type { FilterNodeProps, FilterLeafProps } from './filter-types'; @@ -17,6 +16,10 @@ interface FilterSetProps { hideFilterSetHeader?: boolean; } +interface filterNodeParentProps extends Pick { + itemNumber: number; +} + function filterTreeNode(inputValue, treeNode) { // If the tree node's display value contains the user input, or any of its children's display contains the user input, return true if ( @@ -31,15 +34,10 @@ function filterTreeNode(inputValue, treeNode) { return false; } -const FilterSet: React.FC = ({ hideFilterSetHeader = false }) => { +const FilterSet: React.FC = () => { const { roots } = useContext(FilterContext); - const config = useConfig(); - const tablet = useLayoutType() === 'tablet'; - const { t } = useTranslation(); - const { resetTree } = useContext(FilterContext); const [searchTerm, setSearchTerm] = useState(''); const [treeDataFiltered, setTreeDataFiltered] = useState(roots); - const [showSearchInput, setShowSearchInput] = useState(false); useEffect(() => { const filteredData = roots.filter((node) => filterTreeNode(searchTerm, node)); @@ -47,40 +45,12 @@ const FilterSet: React.FC = ({ hideFilterSetHeader = false }) => }, [searchTerm, roots]); return ( -
- {!hideFilterSetHeader && - (!showSearchInput ? ( -
-

{t('tree', 'Tree')}

-
- - - -
-
- ) : ( -
- setSearchTerm(evt.target.value)} /> - -
- ))} +
{treeDataFiltered?.length > 0 ? ( treeDataFiltered?.map((root, index) => ( -
- +
+
)) ) : ( @@ -91,11 +61,48 @@ const FilterSet: React.FC = ({ hideFilterSetHeader = false }) => ); }; +const FilterNodeParent = ({ root, itemNumber }: filterNodeParentProps) => { + const config = useConfig(); + const { t } = useTranslation(); + const tablet = useLayoutType() === 'tablet'; + const [expandAll, setExpandAll] = useState(undefined); + + if (!root.subSets) return; + + const filterParent = root.subSets.map((node) => { + return ( + + ); + }); + + return ( +
+
+
{t(root.display)}
+ +
+ {filterParent} +
+ ); +}; + const FilterNode = ({ root, level, open }: FilterNodeProps) => { const tablet = useLayoutType() === 'tablet'; const { checkboxes, parents, updateParent } = useContext(FilterContext); const indeterminate = isIndeterminate(parents[root.flatName], checkboxes); const allChildrenChecked = parents[root.flatName]?.every((kid) => checkboxes[kid]); + return ( { id={root?.flatName} checked={root.hasData && allChildrenChecked} indeterminate={indeterminate} - labelText={`${root?.display} (${parents?.[root?.flatName]?.length})`} + labelText={`${root?.display} (${parents?.[root?.flatName]?.length ?? 0})`} onChange={() => updateParent(root.flatName)} disabled={!root.hasData} /> diff --git a/packages/esm-patient-labs-app/src/test-results/filter/filter-set.scss b/packages/esm-patient-labs-app/src/test-results/filter/filter-set.scss index c2a8120531..106465cb95 100644 --- a/packages/esm-patient-labs-app/src/test-results/filter/filter-set.scss +++ b/packages/esm-patient-labs-app/src/test-results/filter/filter-set.scss @@ -1,61 +1,53 @@ @use '@carbon/layout'; -@use '@carbon/type'; -@use '@openmrs/esm-styleguide/src/vars' as *; +@use '@carbon/styles/scss/type'; +@use '@carbon/colors'; +@import '@openmrs/esm-styleguide/src/vars'; -.stickyFilterSet { - position: sticky; - top: 6.5rem; - overflow-y: hidden; +.filterSetContent { + max-height: calc(100vh - 9.5rem); + overflow-y: auto; } -.filterSetHeader { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: layout.$spacing-03; +// background of filter, and spacing between containers +.nestedAccordion { background-color: $openmrs-background-grey; - position: sticky; - top: 0; - z-index: 1; + margin: layout.$spacing-02 0; + + @media (min-width: $breakpoint-small-desktop-min) { + margin: layout.$spacing-02 0; + } - h4 { - @include type.type-style('heading-compact-02'); - color: $text-02; + :global(.cds--accordion__item) { + border: none; } - .filterSetActions { - display: flex; - justify-content: flex-end; - align-items: center; + :global(.cds--accordion__item:last-child) { + border: none; } } -.filterTreeSearchHeader { +.treeNodeHeader { display: flex; + justify-content: space-between; + border-bottom: 1px solid colors.$gray-20; + padding: layout.$spacing-03 0; margin-bottom: layout.$spacing-03; - background-color: $openmrs-background-grey; - position: sticky; - top: 0; - z-index: 1; + align-items: center; } -.filterSetContent { - max-height: calc(100vh - 9.5rem); - overflow-y: auto; +.treeNodeHeaderTablet { + padding-left: layout.$spacing-05; + margin-bottom: 0; } -// background of filter, and spacing between containers -.nestedAccordion { - background-color: $openmrs-background-grey; - margin: layout.$spacing-02 0; - @media (min-width: $breakpoint-small-desktop-min) { - background-color: $ui-background; - } +.nestedAccordionTablet { + margin-bottom: layout.$spacing-05; } // our special accordion rules .nestedAccordion > :global(.cds--accordion--start) > :global(.cds--accordion__item--active) { border-left: 0.375rem solid var(--brand-01); + @media (max-width: $breakpoint-tablet-max) { margin: layout.$spacing-06 0; } @@ -75,6 +67,7 @@ .cds--accordion__item--active > .cds--accordion__content { display: block; + padding-right: layout.$spacing-05; } .cds--accordion__title { diff --git a/packages/esm-patient-labs-app/src/test-results/grouped-timeline/grouped-timeline.component.tsx b/packages/esm-patient-labs-app/src/test-results/grouped-timeline/grouped-timeline.component.tsx index f131a061c7..2d6fa7e298 100644 --- a/packages/esm-patient-labs-app/src/test-results/grouped-timeline/grouped-timeline.component.tsx +++ b/packages/esm-patient-labs-app/src/test-results/grouped-timeline/grouped-timeline.component.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'rea import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { EmptyState } from '@openmrs/esm-patient-common-lib'; -import { ConfigurableLink, usePatient } from '@openmrs/esm-framework'; +import { ConfigurableLink, showModal, usePatient } from '@openmrs/esm-framework'; import { Grid, ShadowBox } from '../panel-timeline/helpers'; import { makeThrottled, testResultsBasePath } from '../helpers'; import type { @@ -30,6 +30,14 @@ const PanelNameCorner: React.FC = ({ showShadow, panelName const NewRowStartCell = ({ title, range, units, conceptUuid, shadow = false, isString = false }) => { const { patientUuid } = usePatient(); + const launchResultsDialog = (patientUuid: string, title: string, testUuid: string) => { + const dispose = showModal('timeline-results-modal', { + closeDeleteModal: () => dispose(), + patientUuid, + testUuid, + title, + }); + }; return (
- {!isString ? ( - - {title} - - ) : ( - {title} - )} + + {!isString ? ( + launchResultsDialog(patientUuid, title, conceptUuid)} + > + {title} + + ) : ( + {title} + )} + {range} {units} @@ -292,18 +302,19 @@ export const GroupedTimeline = () => { ) : rowData?.filter((row: { flatName: string }) => parents[parent.flatName].includes(row.flatName)); - // show kid rows return ( - + subRows?.length > 0 && ( + + ) ); } else return null; })} diff --git a/packages/esm-patient-labs-app/src/test-results/grouped-timeline/grouped-timeline.scss b/packages/esm-patient-labs-app/src/test-results/grouped-timeline/grouped-timeline.scss index 0cb5191eb7..33ed799b42 100644 --- a/packages/esm-patient-labs-app/src/test-results/grouped-timeline/grouped-timeline.scss +++ b/packages/esm-patient-labs-app/src/test-results/grouped-timeline/grouped-timeline.scss @@ -98,6 +98,7 @@ align-self: flex-start; background-color: $ui-03; padding: 0.6rem layout.$spacing-05; + border-top: 1px solid colors.$gray-30; } .rowStartCell { @@ -237,3 +238,13 @@ grid-row: 2 / -1; grid-column: 1 / 2; } + +.trendline-link-view { + @include type.type-style('helper-text-01'); + color: colors.$blue-60; + cursor: pointer; +} + +.trendline-link-view:hover { + text-decoration: underline; +} diff --git a/packages/esm-patient-labs-app/src/test-results/individual-results-table/individual-results-table.component.tsx b/packages/esm-patient-labs-app/src/test-results/individual-results-table/individual-results-table.component.tsx new file mode 100644 index 0000000000..a38ca4ab89 --- /dev/null +++ b/packages/esm-patient-labs-app/src/test-results/individual-results-table/individual-results-table.component.tsx @@ -0,0 +1,167 @@ +import React, { useMemo } from 'react'; +import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import { + DataTableSkeleton, + Button, + DataTable, + TableContainer, + Table, + TableHead, + TableRow, + TableHeader, + TableBody, + TableCell, +} from '@carbon/react'; +import { ArrowRight } from '@carbon/react/icons'; +import { showModal, useLayoutType, isDesktop, formatDate } from '@openmrs/esm-framework'; +import { getPatientUuidFromUrl, type OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; +import styles from './individual-results-table.scss'; + +const IndividualResultsTable = ({ isLoading, parent, subRows, index }) => { + const { t } = useTranslation(); + const layout = useLayoutType(); + const patientUuid = getPatientUuidFromUrl(); + + const headerTitle = t(parent.display); + + const launchResultsDialog = (patientUuid: string, title: string, testUuid: string) => { + const dispose = showModal('timeline-results-modal', { + closeDeleteModal: () => dispose(), + patientUuid, + testUuid, + title, + }); + }; + + const tableHeaders = [ + { key: 'testName', header: t('testName', 'Test Name') }, + { + key: 'value', + header: t('value', 'Value'), + }, + { key: 'referenceRange', header: t('referenceRange', 'Reference range') }, + ]; + + const tableRows = useMemo(() => { + const rowData = subRows.flatMap((row, i) => { + const { units = '', range = '', obs: values } = row; + const isString = isNaN(parseFloat(values?.[0]?.value)); + + return { + ...row, + id: `${i}-${index}`, + testName: ( + + {!isString ? ( + launchResultsDialog(patientUuid, row.display, row.conceptUuid)} + > + {row.display} + + ) : ( + {row.display} + )} + + ), + value: { + value: (row.obs[0]?.value ? row.obs[0]?.value : '') + +' ' + (row?.units ? ` ${row?.units}` : ''), + interpretation: row.obs[0]?.interpretation, + }, + referenceRange: `${range || '--'} ${units || '--'}`, + }; + }); + + return rowData; + }, [index, patientUuid, subRows]); + + if (isLoading) return ; + if (subRows?.length) { + return ( +
+ + {({ rows, headers, getHeaderProps, getTableProps }) => ( + +
+

{headerTitle}

+
+ + {subRows[0]?.obs[0]?.obsDatetime + ? formatDate(new Date(subRows[0]?.obs[0]?.obsDatetime), { mode: 'standard' }) + : ''} + + +
+
+ + + + {headers.map((header) => ( + {header.header} + ))} + + + + {rows.map((row) => { + return ( + + {row.cells.map((cell) => + cell?.value?.interpretation ? ( + +

{cell?.value?.value ?? cell?.value}

+
+ ) : ( + +

{cell?.value}

+
+ ), + )} +
+ ); + })} +
+
+
+ )} +
+
+ ); + } +}; + +export default IndividualResultsTable; + +export const getClasses = (interpretation: OBSERVATION_INTERPRETATION) => { + switch (interpretation) { + case 'OFF_SCALE_HIGH': + return styles['off-scale-high']; + + case 'CRITICALLY_HIGH': + return styles['critically-high']; + + case 'HIGH': + return styles['high']; + + case 'OFF_SCALE_LOW': + return styles['off-scale-low']; + + case 'CRITICALLY_LOW': + return styles['critically-low']; + + case 'LOW': + return styles['low']; + + case 'NORMAL': + default: + return ''; + } +}; diff --git a/packages/esm-patient-labs-app/src/test-results/individual-results-table/individual-results-table.scss b/packages/esm-patient-labs-app/src/test-results/individual-results-table/individual-results-table.scss new file mode 100644 index 0000000000..4bbae0eadb --- /dev/null +++ b/packages/esm-patient-labs-app/src/test-results/individual-results-table/individual-results-table.scss @@ -0,0 +1,137 @@ +@use '@carbon/layout'; +@use '@carbon/colors'; +@use '@carbon/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.off-scale-high, +.off-scale-low, +.critically-high, +.critically-low, +.high, +.low { + p { + @include type.type-style('heading-compact-01'); + } +} + +.high, +.low { + background-color: colors.$orange-10 !important; + border-top: 1px solid colors.$orange-20 !important; +} + +.critically-high, +.critically-low { + background-color: colors.$red-20 !important; + border-top: 1px solid colors.$red-20-hover !important; +} + +.off-scale-low { + p::after { + content: ' ↓↓↓'; + } +} + +.off-scale-high { + p::after { + content: ' ↑↑↑'; + } +} + +.critically-low { + p::after { + content: ' ↓↓'; + } +} + +.critically-high { + p::after { + content: ' ↑↑'; + } +} + +.low { + p::after { + content: ' ↓'; + } +} + +.high { + p::after { + content: ' ↑'; + } +} + +.trendline-link-view { + color: colors.$blue-60; + cursor: pointer; +} + +.trendline-link-view:hover { + text-decoration: underline; +} + +.resultType { + @include type.type-style('heading-compact-02'); + color: $text-02; + margin-bottom: 5px; +} + +.date { + color: $text-02; + padding-right: 0.625rem; +} + +.date::after { + content: ''; + display: block; + width: layout.$spacing-07; + padding-top: 0.188rem; + border-bottom: 0.375rem solid var(--brand-03); +} + +.displayFlex { + display: flex; + flex-direction: row; + justify-content: flex-start; +} + +.viewTimeline { + display: flex; + margin-left: auto; +} + +.cardTitle { + background-color: white; + padding: 0.5rem; +} + +.tableContainer { + padding: 0; + + :global(.cds--data-table-content) { + border: 1px solid $ui-03; + border-bottom: none; + overflow: visible; + } + + :global(.cds--data-table-header) { + padding: 0; + } + + :global(.cds--table-toolbar) { + position: relative; + height: layout.$spacing-07; + overflow: visible; + top: 0; + } + + &:global(.cds--data-table-container) { + background: none !important; + } + + :global(.cds--toolbar-content) { + height: layout.$spacing-05; + margin-bottom: layout.$spacing-02; + } +} diff --git a/packages/esm-patient-labs-app/src/test-results/panel-timeline/helpers.tsx b/packages/esm-patient-labs-app/src/test-results/panel-timeline/helpers.tsx index 9b47014ace..3a3a8f0c89 100644 --- a/packages/esm-patient-labs-app/src/test-results/panel-timeline/helpers.tsx +++ b/packages/esm-patient-labs-app/src/test-results/panel-timeline/helpers.tsx @@ -36,16 +36,24 @@ export const Grid: React.FC<{ style: React.CSSProperties; padding?: boolean; dataColumns: number; -}> = ({ dataColumns, style = {}, padding = false, ...props }) => ( -
-); +}> = ({ dataColumns, style = {}, padding = false, ...props }) => { + const minColumnWidth = 4; + const maxColumnWidth = 10; + + const dynamicColumnWidth = Math.max(minColumnWidth, Math.min(maxColumnWidth, 100 / dataColumns)); + + return ( +
+ ); +}; export const PaddingContainer = React.forwardRef((props, ref) => (
diff --git a/packages/esm-patient-labs-app/src/test-results/panel-timeline/timeline.component.tsx b/packages/esm-patient-labs-app/src/test-results/panel-timeline/timeline.component.tsx index 5d75d2c741..92d5541a80 100644 --- a/packages/esm-patient-labs-app/src/test-results/panel-timeline/timeline.component.tsx +++ b/packages/esm-patient-labs-app/src/test-results/panel-timeline/timeline.component.tsx @@ -1,11 +1,11 @@ import React from 'react'; import classNames from 'classnames'; -import { EmptyState } from '@openmrs/esm-patient-common-lib'; import { PaddingContainer, TimeSlots, Grid, RowStartCell, GridItems, ShadowBox } from './helpers'; import { type ParsedTimeType } from '../filter/filter-types'; import type { ObsRecord } from '../../types'; import useScrollIndicator from './useScroll'; import styles from './timeline.scss'; +import { EmptyState } from '@openmrs/esm-patient-common-lib'; interface PanelNameCornerProps { showShadow: boolean; diff --git a/packages/esm-patient-labs-app/src/test-results/panel-timeline/timeline.scss b/packages/esm-patient-labs-app/src/test-results/panel-timeline/timeline.scss index ec0b385e6e..16178351d1 100644 --- a/packages/esm-patient-labs-app/src/test-results/panel-timeline/timeline.scss +++ b/packages/esm-patient-labs-app/src/test-results/panel-timeline/timeline.scss @@ -11,6 +11,11 @@ gap: 1px; justify-items: center; align-items: center; + border: 0.5px solid colors.$gray-30; +} + +.grid:hover { + border: none; } .day-column, @@ -61,7 +66,6 @@ left: 0; display: grid; grid-auto-flow: row; - gap: 0.5px; justify-items: baseline; align-items: center; padding: layout.$spacing-05; @@ -77,9 +81,7 @@ display: grid; grid-auto-flow: row; - column-gap: 1px; row-gap: 0px; - background-color: $color-gray-30; grid-template: auto auto / 9em auto; border-collapse: collapse; } @@ -109,6 +111,7 @@ left: 0px; top: 0px; z-index: 3; + border-right: 0.5px solid colors.$gray-30; span { @include type.type-style('heading-compact-01'); @@ -197,10 +200,8 @@ .recent-results-grid { display: grid; - background-color: white; overflow: auto; max-height: calc(100vh - 9rem); - border: 1px solid colors.$gray-50; } .recent-results-grid::-webkit-scrollbar { @@ -216,3 +217,4 @@ .trendline-link-view:hover { text-decoration: underline; } + diff --git a/packages/esm-patient-labs-app/src/test-results/results-viewer/results-viewer.extension.tsx b/packages/esm-patient-labs-app/src/test-results/results-viewer/results-viewer.extension.tsx index 929a91616d..7d5efdf71e 100644 --- a/packages/esm-patient-labs-app/src/test-results/results-viewer/results-viewer.extension.tsx +++ b/packages/esm-patient-labs-app/src/test-results/results-viewer/results-viewer.extension.tsx @@ -3,9 +3,8 @@ import classNames from 'classnames'; import { type TFunction, useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { ContentSwitcher, Switch, Button } from '@carbon/react'; -import { Printer, Renew } from '@carbon/react/icons'; import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; -import { navigate, showModal, useConfig, useLayoutType } from '@openmrs/esm-framework'; +import { navigate, useConfig, useLayoutType } from '@openmrs/esm-framework'; import { FilterContext, FilterProvider } from '../filter'; import { useGetManyObstreeData } from '../grouped-timeline'; import { testResultsBasePath } from '../helpers'; @@ -15,9 +14,9 @@ import TreeViewWrapper from '../tree-view/tree-view-wrapper.component'; import Trendline from '../trendline/trendline.component'; import type { ConfigObject } from '../../config-schema'; import styles from './results-viewer.scss'; +import { type viewOpts } from '../../types'; type panelOpts = 'tree' | 'panel'; -type viewOpts = 'split' | 'full'; interface RefreshDataButtonProps { isTablet: boolean; @@ -59,14 +58,12 @@ const RoutedResultsViewer: React.FC = ({ basePath, patientUu const ResultsViewer: React.FC = ({ patientUuid, basePath, loading }) => { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; - const [view, setView] = useState('split'); - const config = useConfig() as ConfigObject; + const [view, setView] = useState('individual-test'); const [selectedSection, setSelectedSection] = useState('tree'); - const { totalResultsCount } = useContext(FilterContext); + const { totalResultsCount, resetTree } = useContext(FilterContext); const { type, testUuid } = useParams(); const isExpanded = view === 'full'; const trendlineView = testUuid && type === 'trendline'; - const showPrintButton = config.showPrintButton; const responsiveSize = isTablet ? 'lg' : 'md'; const navigateBackFromTrendlineView = useCallback(() => { @@ -75,13 +72,6 @@ const ResultsViewer: React.FC = ({ patientUuid, basePath, lo }); }, [patientUuid]); - const openPrintModal = useCallback(() => { - const dispose = showModal('print-modal', { - patientUuid, - closeDialog: () => dispose(), - }); - }, [patientUuid]); - if (isTablet) { return (
@@ -94,11 +84,10 @@ const ResultsViewer: React.FC = ({ patientUuid, basePath, lo selectedIndex={['panel', 'tree'].indexOf(selectedSection)} onChange={({ name }: { name: panelOpts }) => setSelectedSection(name)} > - - + +
-
{selectedSection === 'tree' ? ( = ({ patientUuid, basePath, lo type={type} expanded={isExpanded} testUuid={testUuid} + view={view} /> ) : selectedSection === 'panel' ? ( = ({ patientUuid, basePath, lo
-

{`${t('results', 'Results')} ${ - totalResultsCount ? `(${totalResultsCount})` : '' - }`}

-
- setSelectedSection(name)} - > - - - - {showPrintButton && ( - - )} -
+

{t('tests', 'Tests')}

+
+

{`${t('results', 'Results')} ${ + totalResultsCount ? `(${totalResultsCount})` : '' + }`}

setView(name)} selectedIndex={isExpanded ? 1 : 0} size={responsiveSize} > - - + +
-
- {selectedSection === 'tree' ? ( - - ) : selectedSection === 'panel' ? ( - - ) : null} +
); }; -function RefreshDataButton({ isTablet, t }: RefreshDataButtonProps) { - return ( - - ); -} - export default RoutedResultsViewer; diff --git a/packages/esm-patient-labs-app/src/test-results/results-viewer/results-viewer.scss b/packages/esm-patient-labs-app/src/test-results/results-viewer/results-viewer.scss index e23744be84..ab992049f0 100644 --- a/packages/esm-patient-labs-app/src/test-results/results-viewer/results-viewer.scss +++ b/packages/esm-patient-labs-app/src/test-results/results-viewer/results-viewer.scss @@ -2,6 +2,24 @@ @use '@carbon/type'; @use '@openmrs/esm-styleguide/src/vars' as *; +:global(.omrs-breakpoint-gt-small-desktop .-esm-patient-chart__patient-chart__widthContained___Ow0JH) { + max-width: 150rem; +} + +@media (min-width: 1200px) { + :global(.-esm-patient-chart__patient-chart__widthContained___Ow0JH) { + max-width: 150rem; + } +} + +:global(.cds--data-table td, .cds--data-table th) { + vertical-align: top; +} + +:global(.cds--data-table--md td) { + padding-top: 0.1rem; +} + .resultsContainer { padding: 0; width: 100%; @@ -33,34 +51,39 @@ .leftHeaderSection { display: flex; align-items: center; + justify-content: space-between; } .viewOptsContentSwitcherContainer { - margin-left: auto; + display: grid; + grid-template-columns: 2fr 1fr; + justify-content: space-between; +} + +.viewOptionsText { + margin-top: layout.$spacing-03; + justify-self: start; } .viewOptionsSwitcher { - max-width: 10rem; + text-align: right; + justify-self: end; } .leftSection { - width: 45%; + width: 350px; margin-right: layout.$spacing-05; } .rightSection, .rightSectionHeader { - width: 55%; + width: 70%; &.fullView { width: 100%; } } -.rightSectionHeader { - display: flex; -} - .rightSection :global(.cds--skeleton) { width: 100%; } diff --git a/packages/esm-patient-labs-app/src/test-results/tree-view/tree-view-wrapper.component.tsx b/packages/esm-patient-labs-app/src/test-results/tree-view/tree-view-wrapper.component.tsx index b1a0bf7ff3..31219a1e2b 100644 --- a/packages/esm-patient-labs-app/src/test-results/tree-view/tree-view-wrapper.component.tsx +++ b/packages/esm-patient-labs-app/src/test-results/tree-view/tree-view-wrapper.component.tsx @@ -5,6 +5,7 @@ import { FilterProvider } from '../filter/filter-context'; import TreeView from './tree-view.component'; import { useConfig } from '@openmrs/esm-framework'; import { useGetManyObstreeData } from '../grouped-timeline'; +import { type viewOpts } from '../../types'; interface TreeViewWrapperProps { patientUuid: string; @@ -12,6 +13,7 @@ interface TreeViewWrapperProps { testUuid: string; expanded: boolean; type: string; + view?: viewOpts; } const TreeViewWrapper: React.FC = (props) => { diff --git a/packages/esm-patient-labs-app/src/test-results/tree-view/tree-view.component.tsx b/packages/esm-patient-labs-app/src/test-results/tree-view/tree-view.component.tsx index 67f0653a44..4918329f70 100644 --- a/packages/esm-patient-labs-app/src/test-results/tree-view/tree-view.component.tsx +++ b/packages/esm-patient-labs-app/src/test-results/tree-view/tree-view.component.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { AccordionSkeleton, DataTableSkeleton, Button } from '@carbon/react'; @@ -6,11 +6,13 @@ import { TreeViewAlt } from '@carbon/react/icons'; import { useLayoutType } from '@openmrs/esm-framework'; import FilterSet, { FilterContext } from '../filter'; import GroupedTimeline from '../grouped-timeline'; -import PanelTimelineComponent from '../panel-timeline/panel-timeline-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 { type viewOpts } from '../../types'; +import IndividualResultsTable from '../individual-results-table/individual-results-table.component'; +import { EmptyState } from '@openmrs/esm-patient-common-lib'; interface TreeViewProps { patientUuid: string; @@ -19,9 +21,52 @@ interface TreeViewProps { loading: boolean; expanded: boolean; type: string; + view?: viewOpts; } -const TreeView: React.FC = ({ patientUuid, basePath, testUuid, loading, expanded, type }) => { +const GroupedPanelsTables = ({ loadingPanelData }) => { + const { timelineData, parents, checkboxes, someChecked, lowestParents } = useContext(FilterContext); + const [panelName, setPanelName] = useState(''); + const { t } = useTranslation(); + let shownGroups = 0; + + const { + data: { rowData }, + } = timelineData; + + useEffect(() => { + setPanelName(''); + }, [rowData]); + + const filteredParents = lowestParents?.filter( + (parent) => parents[parent.flatName].some((kid) => checkboxes[kid]) || !someChecked, + ); + + if (rowData && rowData?.length === 0) { + return ; + } + + return ( + <> + {filteredParents?.map((parent, index) => { + shownGroups += 1; + 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 ( +
+ +
+ ); + })} + + ); +}; + +const TreeView: React.FC = ({ patientUuid, basePath, testUuid, loading, expanded, type, view }) => { const tablet = useLayoutType() === 'tablet'; const [showTreeOverlay, setShowTreeOverlay] = useState(false); const { t } = useTranslation(); @@ -77,17 +122,17 @@ const TreeView: React.FC = ({ patientUuid, basePath, testUuid, lo ) : loading || isLoadingPanelData ? ( - ) : someChecked ? ( - - ) : ( - // If no filter is selected from the filter view - // All the test results recorded for the patient needs to be shown + ) : view === 'individual-test' ? ( +
+ +
+ ) : view === 'over-time' ? ( panels.map((panel) => (
- +
)) - )} + ) : null}
); diff --git a/packages/esm-patient-labs-app/src/types.ts b/packages/esm-patient-labs-app/src/types.ts index ad6e5f42a6..5b66651cbd 100644 --- a/packages/esm-patient-labs-app/src/types.ts +++ b/packages/esm-patient-labs-app/src/types.ts @@ -132,3 +132,5 @@ export interface ObservationSet { uuid: string; meta: ConceptMeta; } + +export type viewOpts = 'individual-test' | 'over-time' | 'full'; diff --git a/packages/esm-patient-labs-app/translations/en.json b/packages/esm-patient-labs-app/translations/en.json index 7590a896c6..855a148ee6 100644 --- a/packages/esm-patient-labs-app/translations/en.json +++ b/packages/esm-patient-labs-app/translations/en.json @@ -23,9 +23,9 @@ "errorFetchingTestTypes": "Error fetching results for \"{{searchTerm}}\"", "errorLoadingTestTypes": "Error occured when loading test types", "female": "Female", - "full": "Full", "goToDrugOrderForm": "Order form", "hideResultsTable": "Hide results table", + "individualTests": "Individual tests", "labOrders": "Lab orders", "labReferenceNumber": "Lab reference number", "male": "Male", @@ -44,6 +44,7 @@ "ordered": "Ordered", "orderReason": "Order reason", "other": "Other", + "overTime": "Over time", "panel": "Panel", "panels": "panels", "pleaseRequiredFields": "Please fill all required fields", @@ -54,8 +55,8 @@ "recentResults": "Recent Results", "recentTestResults": "recent test results", "referenceRange": "Reference range", - "refreshData": "Refresh data", "removeFromBasket": "Remove from basket", + "reset": "Reset", "resetTreeText": "Reset tree", "resetView": "to reset this view", "resulted": "Resulted", @@ -75,12 +76,12 @@ "seeAllResults": "See all results", "showResultsTable": "Show results table", "showTree": "Show tree", - "split": "Split", "startDate": "Start date", "testName": "Test name", "testResults": "test results", "testResults_title": "Test Results", "testResultsData": "Test results data", + "tests": "Tests", "testType": "Test type", "timeline": "Timeline", "tree": "Tree", @@ -99,5 +100,6 @@ "unknown": "Unknown", "usingADifferentTerm": "using a different term", "value": "Value", - "view": "View" + "view": "View", + "viewTimeline": "View timeline" }