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

(feat) O3-4235: Add config support for observations display on active visits table #1392

Merged
merged 8 commits into from
Dec 3, 2024
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useState, useCallback } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
DataTable,
DataTableSkeleton,
Expand All @@ -20,19 +20,22 @@ import {
} from '@carbon/react';
import { useTranslation } from 'react-i18next';
import {
useLayoutType,
ConfigurableLink,
ErrorState,
ExtensionSlot,
isDesktop,
type OpenmrsResource,
useConfig,
useLayoutType,
usePagination,
ExtensionSlot,
ErrorState,
ConfigurableLink,
} from '@openmrs/esm-framework';
import { EmptyDataIllustration } from './empty-data-illustration.component';
import { useActiveVisits } from './active-visits.resource';
import { useActiveVisits, useActiveVisitsSorting, useObsConcepts } from './active-visits.resource';
import styles from './active-visits.scss';
import { type ActiveVisitsConfigSchema } from '../config-schema';
import { type ActiveVisit } from '../types';

function generateTableHeaders(t, config) {
function generateTableHeaders(t, config, obsConcepts) {
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
let headersIndex = 0;

const headers = [
Expand All @@ -51,7 +54,7 @@ function generateTableHeaders(t, config) {
});
});

if (!config?.activeVisit?.identifiers) {
if (!config?.activeVisits?.identifiers) {
headers.push({
id: headersIndex++,
header: t('idNumber', 'ID Number'),
Expand All @@ -67,6 +70,15 @@ function generateTableHeaders(t, config) {
});
});

// Add headers for obs concepts
obsConcepts?.forEach((concept: OpenmrsResource) => {
headers.push({
id: headersIndex++,
header: concept.display,
key: `obs-${concept.uuid}`,
});
});

headers.push(
{
id: headersIndex++,
Expand Down Expand Up @@ -95,13 +107,37 @@ function generateTableHeaders(t, config) {

const ActiveVisitsTable = () => {
const { t } = useTranslation();
const config = useConfig();
const config = useConfig<ActiveVisitsConfigSchema>();
const layout = useLayoutType();
const pageSizes = config?.activeVisits?.pageSizes ?? [10, 20, 30, 40, 50];
const [pageSize, setPageSize] = useState(config?.activeVisits?.pageSize ?? 10);
const { obsConcepts, isLoadingObsConcepts } = useObsConcepts(config.activeVisits.obs);
const { activeVisits, isLoading, isValidating, error } = useActiveVisits();
const [searchString, setSearchString] = useState('');
const headerData = useMemo(() => generateTableHeaders(t, config), [config, t]);
const headerData = useMemo(() => generateTableHeaders(t, config, obsConcepts), [config, t, obsConcepts]);

const transformVisitForDisplay = useCallback(
(visit: ActiveVisit) => {
const displayData = { ...visit };

// Add observation values to the display data
obsConcepts?.forEach((concept) => {
const obsValues = visit?.observations?.[concept.uuid] ?? [];
const latestObs = obsValues[0];

if (latestObs) {
// Handle both string and object values
displayData[`obs-${concept.uuid}`] =
typeof latestObs.value === 'object' ? latestObs.value.display : latestObs.value;
} else {
displayData[`obs-${concept.uuid}`] = '--';
}
});

return displayData;
},
[obsConcepts],
);

const searchResults = useMemo(() => {
if (activeVisits !== undefined && activeVisits.length > 0) {
Expand All @@ -118,10 +154,11 @@ const ActiveVisitsTable = () => {
}
}

return activeVisits;
}, [searchString, activeVisits]);
return activeVisits.map(transformVisitForDisplay);
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
}, [searchString, activeVisits, transformVisitForDisplay]);

const { paginated, goTo, results, currentPage } = usePagination(searchResults, pageSize);
const { sortedRows, sortRow } = useActiveVisitsSorting(searchResults);
const { paginated, goTo, results, currentPage } = usePagination(sortedRows, pageSize as number);

const handleSearch = useCallback(
(e) => {
Expand All @@ -131,7 +168,7 @@ const ActiveVisitsTable = () => {
[goTo, setSearchString],
);

if (isLoading) {
if (isLoading || isLoadingObsConcepts) {
return (
<div className={styles.activeVisitsContainer}>
<div className={styles.activeVisitsDetailHeaderContainer}>
Expand Down Expand Up @@ -187,125 +224,128 @@ const ActiveVisitsTable = () => {
</Layer>
</div>
);
} else {
return (
<div className={styles.activeVisitsContainer}>
<div className={styles.activeVisitsDetailHeaderContainer}>
<div className={!isDesktop(layout) ? styles.tabletHeading : styles.desktopHeading}>
<h4>{t('activeVisits', 'Active Visits')}</h4>
</div>
<div className={styles.backgroundDataFetchingIndicator}>
<span>{isValidating ? <InlineLoading /> : null}</span>
</div>
}

return (
<div className={styles.activeVisitsContainer}>
<div className={styles.activeVisitsDetailHeaderContainer}>
<div className={!isDesktop(layout) ? styles.tabletHeading : styles.desktopHeading}>
<h4>{t('activeVisits', 'Active Visits')}</h4>
</div>
<Search
labelText=""
placeholder={t('filterTable', 'Filter table')}
onChange={handleSearch}
size={isDesktop(layout) ? 'sm' : 'lg'}
/>
<DataTable
rows={results}
headers={headerData}
size={isDesktop(layout) ? 'sm' : 'lg'}
useZebraStyles={activeVisits?.length > 1 ? true : false}>
{({ rows, headers, getHeaderProps, getTableProps, getRowProps, getExpandHeaderProps }) => (
<TableContainer className={styles.tableContainer}>
<Table className={styles.activeVisitsTable} {...getTableProps()}>
<TableHead>
<TableRow>
<TableExpandHeader enableToggle {...getExpandHeaderProps()} />
{headers.map((header) => (
<TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => {
const currentVisit = activeVisits.find((visit) => visit.id === row.id);
<div className={styles.backgroundDataFetchingIndicator}>
<span>{isValidating ? <InlineLoading /> : null}</span>
</div>
</div>
<Search
labelText=""
placeholder={t('filterTable', 'Filter table')}
onChange={handleSearch}
size={isDesktop(layout) ? 'sm' : 'lg'}
/>
<DataTable
isSortable
useStaticWidth
rows={results}
headers={headerData}
sortRow={sortRow}
size={isDesktop(layout) ? 'sm' : 'lg'}
useZebraStyles={activeVisits?.length > 1}>
{({ rows, headers, getHeaderProps, getTableProps, getRowProps, getExpandHeaderProps }) => (
<TableContainer className={styles.tableContainer}>
<Table className={styles.activeVisitsTable} {...getTableProps()}>
<TableHead>
<TableRow>
<TableExpandHeader enableToggle {...getExpandHeaderProps()} />
{headers.map((header) => (
<TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => {
const currentVisit = activeVisits.find((visit) => visit.id === row.id);

if (!currentVisit) {
return null;
}
if (!currentVisit) {
return null;
}

const patientChartUrl = '${openmrsSpaBase}/patient/${patientUuid}/chart/Patient%20Summary';
const patientChartUrl = '${openmrsSpaBase}/patient/${patientUuid}/chart/Patient%20Summary';
Copy link
Member

Choose a reason for hiding this comment

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

You need to redirect to just /chart and the dashboard to redirect, let it be Patient Summary or any other is handled by the patient chart itself.


return (
<React.Fragment key={`active-visit-row-${index}`}>
<TableExpandRow
{...getRowProps({ row })}
data-testid={`activeVisitRow${currentVisit.patientUuid || 'unknown'}`}>
{row.cells.map((cell) => (
<TableCell key={`active-visit-row-${index}-cell-${cell.id}`} data-testid={cell.id}>
{cell.info.header === 'name' && currentVisit.patientUuid ? (
<ConfigurableLink
to={patientChartUrl}
templateParams={{ patientUuid: currentVisit.patientUuid }}>
{cell.value}
</ConfigurableLink>
) : (
cell.value
)}
</TableCell>
))}
</TableExpandRow>
{row.isExpanded ? (
<TableRow className={styles.expandedActiveVisitRow}>
<th colSpan={headers.length + 2}>
<ExtensionSlot
className={styles.visitSummaryContainer}
name="visit-summary-slot"
state={{
patientUuid: currentVisit.patientUuid,
visitUuid: currentVisit.visitUuid,
}}
/>
</th>
</TableRow>
) : (
<TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
)}
</React.Fragment>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</DataTable>
{searchResults?.length === 0 && (
<div className={styles.filterEmptyState}>
<Layer level={0}>
<Tile className={styles.filterEmptyStateTile}>
<p className={styles.filterEmptyStateContent}>{t('noVisitsToDisplay', 'No visits to display')}</p>
<p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
</Tile>
</Layer>
</div>
)}
{paginated && (
<Pagination
forwardText="Next page"
backwardText="Previous page"
page={currentPage}
pageSize={pageSize}
pageSizes={pageSizes}
totalItems={searchResults?.length}
className={styles.pagination}
size={isDesktop(layout) ? 'sm' : 'lg'}
onChange={({ pageSize: newPageSize, page: newPage }) => {
if (newPageSize !== pageSize) {
setPageSize(newPageSize);
}
if (newPage !== currentPage) {
goTo(newPage);
}
}}
/>
return (
<React.Fragment key={`active-visit-row-${index}`}>
<TableExpandRow
{...getRowProps({ row })}
data-testid={`activeVisitRow${currentVisit.patientUuid || 'unknown'}`}>
{row.cells.map((cell) => (
<TableCell key={`active-visit-row-${index}-cell-${cell.id}`} data-testid={cell.id}>
{cell.info.header === 'name' && currentVisit.patientUuid ? (
<ConfigurableLink
to={patientChartUrl}
templateParams={{ patientUuid: currentVisit.patientUuid }}>
{cell.value}
</ConfigurableLink>
) : (
cell.value
)}
</TableCell>
))}
</TableExpandRow>
{row.isExpanded ? (
<TableRow className={styles.expandedActiveVisitRow}>
<th colSpan={headers.length + 2}>
<ExtensionSlot
className={styles.visitSummaryContainer}
name="visit-summary-slot"
state={{
patientUuid: currentVisit.patientUuid,
visitUuid: currentVisit.visitUuid,
}}
/>
</th>
</TableRow>
) : (
<TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
)}
</React.Fragment>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</div>
);
}
</DataTable>
{searchResults?.length === 0 && (
<div className={styles.filterEmptyState}>
<Layer level={0}>
<Tile className={styles.filterEmptyStateTile}>
<p className={styles.filterEmptyStateContent}>{t('noVisitsToDisplay', 'No visits to display')}</p>
<p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
</Tile>
</Layer>
</div>
)}
{paginated && (
<Pagination
forwardText="Next page"
backwardText="Previous page"
page={currentPage}
pageSize={pageSize}
pageSizes={pageSizes}
totalItems={searchResults?.length}
className={styles.pagination}
size={isDesktop(layout) ? 'sm' : 'lg'}
onChange={({ pageSize: newPageSize, page: newPage }) => {
if (newPageSize !== pageSize) {
setPageSize(newPageSize);
}
if (newPage !== currentPage) {
goTo(newPage);
}
}}
/>
)}
</div>
);
};

export default ActiveVisitsTable;
Loading