@digihmis/esm-outpatient-app 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/icon-wrapper/icon-wrapper.extension.tsx +2 -17
- package/src/left-panel/left-panel.component.tsx +6 -100
- package/src/page-header/page-header.component.tsx +4 -25
- package/src/patient-management/clinical-encounters.component.tsx +243 -0
- package/src/patient-management/clinical-encounters.resource.ts +90 -0
- package/src/patient-management/patient-management.component.tsx +147 -75
- package/src/patient-management/patient-management.scss +181 -20
- package/src/routes.json +3 -3
- package/dist/117.js +0 -1
- package/dist/117.js.map +0 -1
- package/dist/132.js +0 -1
- package/dist/132.js.map +0 -1
- package/dist/152.js +0 -1
- package/dist/152.js.map +0 -1
- package/dist/209.js +0 -1
- package/dist/209.js.map +0 -1
- package/dist/317.js +0 -1
- package/dist/317.js.map +0 -1
- package/dist/349.js +0 -1
- package/dist/349.js.map +0 -1
- package/dist/371.js +0 -1
- package/dist/371.js.map +0 -1
- package/dist/378.js +0 -1
- package/dist/378.js.map +0 -1
- package/dist/395.js +0 -12
- package/dist/395.js.map +0 -1
- package/dist/442.js +0 -1
- package/dist/442.js.map +0 -1
- package/dist/45.js +0 -1
- package/dist/45.js.map +0 -1
- package/dist/466.js +0 -1
- package/dist/466.js.map +0 -1
- package/dist/508.js +0 -1
- package/dist/508.js.map +0 -1
- package/dist/564.js +0 -1
- package/dist/564.js.map +0 -1
- package/dist/61.js +0 -1
- package/dist/61.js.map +0 -1
- package/dist/640.js +0 -1
- package/dist/640.js.map +0 -1
- package/dist/689.js +0 -1
- package/dist/689.js.map +0 -1
- package/dist/709.js +0 -1
- package/dist/709.js.map +0 -1
- package/dist/712.js +0 -1
- package/dist/712.js.map +0 -1
- package/dist/720.js +0 -1
- package/dist/720.js.map +0 -1
- package/dist/742.js +0 -1
- package/dist/742.js.map +0 -1
- package/dist/759.js +0 -15
- package/dist/759.js.map +0 -1
- package/dist/771.js +0 -1
- package/dist/771.js.map +0 -1
- package/dist/797.js +0 -1
- package/dist/797.js.map +0 -1
- package/dist/806.js +0 -1
- package/dist/806.js.map +0 -1
- package/dist/912.js +0 -1
- package/dist/912.js.map +0 -1
- package/dist/913.js +0 -1
- package/dist/913.js.map +0 -1
- package/dist/940.js +0 -1
- package/dist/940.js.map +0 -1
- package/dist/974.js +0 -1
- package/dist/974.js.map +0 -1
- package/dist/digihmis-esm-outpatient-app.js +0 -6
- package/dist/digihmis-esm-outpatient-app.js.buildmanifest.json +0 -827
- package/dist/digihmis-esm-outpatient-app.js.map +0 -1
- package/dist/main.js +0 -17
- package/dist/main.js.map +0 -1
- package/dist/routes.json +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digihmis/esm-outpatient-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Outpatient microfrontend for the OpenMRS SPA",
|
|
5
5
|
"browser": "dist/digihmis-esm-outpatient-app.js",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"url": "https://github.com/its-kios09/DigiHMIS-apps/issues"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@digihmis/esm-commons-lib": "1.0.0",
|
|
40
41
|
"lodash-es": "^4.17.15",
|
|
41
42
|
"react-to-print": "2.14.13"
|
|
42
43
|
},
|
|
@@ -1,17 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
interface IconWrapperProps {
|
|
4
|
-
src: string;
|
|
5
|
-
alt?: string;
|
|
6
|
-
className?: string;
|
|
7
|
-
style?: React.CSSProperties;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const IconWrapper: React.FC<IconWrapperProps> = ({
|
|
11
|
-
src,
|
|
12
|
-
alt = 'Icon',
|
|
13
|
-
className,
|
|
14
|
-
style = { width: '100px', height: '148px' },
|
|
15
|
-
}) => {
|
|
16
|
-
return <img src={src} alt={alt} className={className} style={style} />;
|
|
17
|
-
};
|
|
1
|
+
// Moved to the shared library so every app uses one implementation.
|
|
2
|
+
export { IconWrapper, type IconWrapperProps } from '@digihmis/esm-commons-lib';
|
|
@@ -1,100 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import styles from './left-panel.scss';
|
|
8
|
-
|
|
9
|
-
export interface DashboardLinkConfig {
|
|
10
|
-
name: string;
|
|
11
|
-
title: string;
|
|
12
|
-
icon?: string | CarbonIconType;
|
|
13
|
-
showOnlyOnRoute?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const DashboardLink = ({ dashboardLinkConfig }: { dashboardLinkConfig: DashboardLinkConfig }) => {
|
|
17
|
-
const { t } = useTranslation();
|
|
18
|
-
const { name, title, icon, showOnlyOnRoute } = dashboardLinkConfig;
|
|
19
|
-
const location = useLocation();
|
|
20
|
-
const params = useParams();
|
|
21
|
-
const spaBasePath = window.getOpenmrsSpaBase() + 'home';
|
|
22
|
-
|
|
23
|
-
// Extract patientUuid from the current URL path if params.patientUuid is not available
|
|
24
|
-
const patientUuid = useMemo(() => {
|
|
25
|
-
if (params.patientUuid) {
|
|
26
|
-
return params.patientUuid;
|
|
27
|
-
}
|
|
28
|
-
// Extract UUID from path like /outpatient/UUID/file or /outpatient/patient/UUID/file
|
|
29
|
-
const pathParts = location.pathname.split('/');
|
|
30
|
-
const outpatientIndex = pathParts.indexOf('outpatient');
|
|
31
|
-
if (outpatientIndex !== -1 && pathParts.length > outpatientIndex + 1) {
|
|
32
|
-
const nextPart = pathParts[outpatientIndex + 1];
|
|
33
|
-
// Check if it's a UUID pattern (basic check)
|
|
34
|
-
if (nextPart && nextPart.length > 20 && nextPart.includes('-')) {
|
|
35
|
-
return nextPart;
|
|
36
|
-
}
|
|
37
|
-
// Check if path is /outpatient/patient/UUID/file
|
|
38
|
-
if (nextPart === 'patient' && pathParts.length > outpatientIndex + 2) {
|
|
39
|
-
const uuidPart = pathParts[outpatientIndex + 2];
|
|
40
|
-
if (uuidPart && uuidPart.length > 20 && uuidPart.includes('-')) {
|
|
41
|
-
return uuidPart;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
}, [params.patientUuid, location.pathname]);
|
|
47
|
-
|
|
48
|
-
const shouldShow = useMemo(() => {
|
|
49
|
-
if (!showOnlyOnRoute) {
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
return location.pathname.includes(showOnlyOnRoute);
|
|
53
|
-
}, [showOnlyOnRoute, location.pathname]);
|
|
54
|
-
|
|
55
|
-
const linkPath = useMemo(() => {
|
|
56
|
-
let path = name;
|
|
57
|
-
if (name.includes(':patientUuid') && patientUuid) {
|
|
58
|
-
path = name.replace(':patientUuid', patientUuid);
|
|
59
|
-
}
|
|
60
|
-
return path;
|
|
61
|
-
}, [name, patientUuid]);
|
|
62
|
-
|
|
63
|
-
const isActive = useMemo(() => {
|
|
64
|
-
if (name.includes(':patientUuid')) {
|
|
65
|
-
return location.pathname.includes('/file');
|
|
66
|
-
}
|
|
67
|
-
const urlSegment = decodeURIComponent(last(location.pathname.split('/')));
|
|
68
|
-
const nameSegment = name.split('/').at(-1);
|
|
69
|
-
return nameSegment === urlSegment;
|
|
70
|
-
}, [location.pathname, name]);
|
|
71
|
-
|
|
72
|
-
if (!shouldShow) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const IconComp = typeof icon !== 'string' ? (icon as CarbonIconType) : null;
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<ConfigurableLink
|
|
80
|
-
to={`${spaBasePath}/${linkPath}`}
|
|
81
|
-
className={`cds--side-nav__link ${isActive && 'active-left-nav-link'}`}>
|
|
82
|
-
{typeof icon === 'string' ? (
|
|
83
|
-
<MaybeIcon icon={icon} className={styles.icon} size={16} />
|
|
84
|
-
) : (
|
|
85
|
-
IconComp && <IconComp className={styles.icon} />
|
|
86
|
-
)}
|
|
87
|
-
{t(title)}
|
|
88
|
-
</ConfigurableLink>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export const createDashboardLink = (dashboardLinkConfig: DashboardLinkConfig) => () => {
|
|
93
|
-
return (
|
|
94
|
-
<BrowserRouter>
|
|
95
|
-
<DashboardLink dashboardLinkConfig={dashboardLinkConfig} />
|
|
96
|
-
</BrowserRouter>
|
|
97
|
-
);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export default DashboardLink;
|
|
1
|
+
// Moved to the shared library so every app uses one implementation.
|
|
2
|
+
export {
|
|
3
|
+
DashboardLink as default,
|
|
4
|
+
createDashboardLink,
|
|
5
|
+
type DashboardLinkConfig,
|
|
6
|
+
} from '@digihmis/esm-commons-lib';
|
|
@@ -1,35 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import styles from './page-header.scss';
|
|
3
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import { PageHeader as CommonPageHeader } from '@digihmis/esm-commons-lib';
|
|
6
5
|
import { ConfigObject } from '../config-schema';
|
|
7
|
-
import { IconWrapper } from '../icon-wrapper/icon-wrapper.extension';
|
|
8
6
|
|
|
7
|
+
// The shared header lives in @digihmis/esm-commons-lib; this passes the module's title + icon.
|
|
9
8
|
export const PageHeader: React.FC = () => {
|
|
10
9
|
const { t } = useTranslation();
|
|
11
|
-
const userSession = useSession();
|
|
12
10
|
const { outpatientIcon } = useConfig<ConfigObject>();
|
|
13
|
-
const userLocation = userSession?.sessionLocation?.display;
|
|
14
|
-
|
|
15
11
|
return (
|
|
16
|
-
<
|
|
17
|
-
<div className={styles['left-justified-items']}>
|
|
18
|
-
<IconWrapper src={outpatientIcon.src} alt={outpatientIcon.alt} />
|
|
19
|
-
<div className={styles['page-labels']}>
|
|
20
|
-
<p className={styles['page-name']}>{userLocation}</p>
|
|
21
|
-
<p className={styles['page-subtitle']}>{t('outpatient', 'Outpatient')}</p>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
<div className={styles['right-justified-items']}>
|
|
25
|
-
<div className={styles.extensionSlot}>
|
|
26
|
-
<ExtensionSlot name={'provider-banner-slot'} />
|
|
27
|
-
<div className={styles['date-and-location']}>
|
|
28
|
-
<Calendar size={16} />
|
|
29
|
-
<span className={styles.value}>{formatDate(new Date(), { mode: 'wide', noToday: true })}</span>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
12
|
+
<CommonPageHeader title={t('outpatient', 'Outpatient')} iconSrc={outpatientIcon.src} iconAlt={outpatientIcon.alt} />
|
|
34
13
|
);
|
|
35
14
|
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
DataTable,
|
|
6
|
+
DataTableSkeleton,
|
|
7
|
+
Layer,
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableContainer,
|
|
12
|
+
TableExpandedRow,
|
|
13
|
+
TableExpandHeader,
|
|
14
|
+
TableExpandRow,
|
|
15
|
+
TableHead,
|
|
16
|
+
TableHeader,
|
|
17
|
+
TableRow,
|
|
18
|
+
Tag,
|
|
19
|
+
Tile,
|
|
20
|
+
} from '@carbon/react';
|
|
21
|
+
import { Add, Printer, Stethoscope } from '@carbon/react/icons';
|
|
22
|
+
import { formatDatetime, parseDate } from '@openmrs/esm-framework';
|
|
23
|
+
import {
|
|
24
|
+
type ClinicalEncounter,
|
|
25
|
+
type EncounterObs,
|
|
26
|
+
openVisitSummary,
|
|
27
|
+
useClinicalEncounters,
|
|
28
|
+
useClinicalEncounterFieldLabels,
|
|
29
|
+
} from './clinical-encounters.resource';
|
|
30
|
+
import styles from './patient-management.scss';
|
|
31
|
+
|
|
32
|
+
interface ClinicalEncountersListProps {
|
|
33
|
+
patientUuid: string;
|
|
34
|
+
onStartConsultation: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Uniform sentence case for observation labels, matching the printed discharge/case summary. */
|
|
38
|
+
function toSentenceCase(text: string): string {
|
|
39
|
+
if (!text) {
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
const lower = text.toLowerCase();
|
|
43
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Render an obs value whether it is coded (an object with display), boolean, numeric or free text. */
|
|
47
|
+
function formatObsValue(value: unknown): string {
|
|
48
|
+
if (value == null || value === '') {
|
|
49
|
+
return '—';
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'object') {
|
|
52
|
+
const coded = value as { display?: string };
|
|
53
|
+
return coded.display ?? String(value);
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'boolean') {
|
|
56
|
+
return value ? 'Yes' : 'No';
|
|
57
|
+
}
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function providerName(encounter: ClinicalEncounter): string | null {
|
|
62
|
+
return encounter.encounterProviders?.[0]?.provider?.display ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The saved clinical-encounter records for a patient, shown on the Clinical encounter tab. */
|
|
66
|
+
const ClinicalEncountersList: React.FC<ClinicalEncountersListProps> = ({ patientUuid, onStartConsultation }) => {
|
|
67
|
+
const { t } = useTranslation();
|
|
68
|
+
const { encounters, isLoading } = useClinicalEncounters(patientUuid);
|
|
69
|
+
const fieldLabels = useClinicalEncounterFieldLabels();
|
|
70
|
+
|
|
71
|
+
const headers = [
|
|
72
|
+
{ key: 'date', header: t('date', 'Date') },
|
|
73
|
+
{ key: 'provider', header: t('provider', 'Provider') },
|
|
74
|
+
{ key: 'diagnoses', header: t('diagnoses', 'Diagnoses') },
|
|
75
|
+
{ key: 'summary', header: t('summary', 'Summary') },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const byUuid = React.useMemo(() => new Map(encounters.map((e) => [e.uuid, e])), [encounters]);
|
|
79
|
+
|
|
80
|
+
const rows = encounters.map((e) => {
|
|
81
|
+
const obsCount = (e.obs ?? []).filter((o) => o.concept).length;
|
|
82
|
+
const summary = [
|
|
83
|
+
obsCount ? t('findingsCount', '{{count}} findings', { count: obsCount }) : null,
|
|
84
|
+
e.orders?.length ? t('ordersCount', '{{count}} orders', { count: e.orders.length }) : null,
|
|
85
|
+
]
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.join(' · ');
|
|
88
|
+
return {
|
|
89
|
+
id: e.uuid,
|
|
90
|
+
date: formatDatetime(parseDate(e.encounterDatetime), { mode: 'standard' }),
|
|
91
|
+
provider: providerName(e) ?? '—',
|
|
92
|
+
diagnoses: e.diagnoses?.length ? e.diagnoses.map((d) => d.display).join(', ') : '—',
|
|
93
|
+
summary: summary || '—',
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (isLoading) {
|
|
98
|
+
return <DataTableSkeleton role="progressbar" showHeader={false} showToolbar={false} rowCount={4} columnCount={4} />;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (encounters.length === 0) {
|
|
102
|
+
return (
|
|
103
|
+
<Layer>
|
|
104
|
+
<Tile className={styles.emptyTile}>
|
|
105
|
+
<Stethoscope size={32} className={styles.emptyIcon} />
|
|
106
|
+
<p className={styles.emptyTitle}>{t('noConsultationsTitle', 'No consultations yet')}</p>
|
|
107
|
+
<p className={styles.emptyBody}>
|
|
108
|
+
{t('noConsultationsBody', 'Start a consultation to record findings for this visit.')}
|
|
109
|
+
</p>
|
|
110
|
+
<Button kind="ghost" size="sm" renderIcon={Add} onClick={onStartConsultation}>
|
|
111
|
+
{t('startConsultation', 'Start consultation')}
|
|
112
|
+
</Button>
|
|
113
|
+
</Tile>
|
|
114
|
+
</Layer>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className={styles.encounterList}>
|
|
120
|
+
<div className={styles.encounterListHeader}>
|
|
121
|
+
<span className={styles.encounterCount}>
|
|
122
|
+
{t('clinicalEncountersCount', '{{count}} clinical encounter(s)', { count: encounters.length })}
|
|
123
|
+
</span>
|
|
124
|
+
<Button kind="ghost" size="sm" renderIcon={Add} onClick={onStartConsultation}>
|
|
125
|
+
{t('startConsultation', 'Start consultation')}
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<DataTable rows={rows} headers={headers} size="sm" useZebraStyles>
|
|
130
|
+
{({ rows, headers, getHeaderProps, getRowProps, getExpandedRowProps, getTableProps, getTableContainerProps }) => (
|
|
131
|
+
<TableContainer {...getTableContainerProps()}>
|
|
132
|
+
<Table {...getTableProps()} aria-label={t('clinicalEncounters', 'Clinical encounters')}>
|
|
133
|
+
<TableHead>
|
|
134
|
+
<TableRow>
|
|
135
|
+
<TableExpandHeader aria-label={t('expandRow', 'Expand row')} />
|
|
136
|
+
{headers.map((header) => {
|
|
137
|
+
const { key, ...headerProps } = getHeaderProps({ header });
|
|
138
|
+
return (
|
|
139
|
+
<TableHeader key={header.key} {...headerProps}>
|
|
140
|
+
{header.header}
|
|
141
|
+
</TableHeader>
|
|
142
|
+
);
|
|
143
|
+
})}
|
|
144
|
+
<TableHeader aria-label={t('actions', 'Actions')} />
|
|
145
|
+
</TableRow>
|
|
146
|
+
</TableHead>
|
|
147
|
+
<TableBody>
|
|
148
|
+
{rows.map((row) => {
|
|
149
|
+
const { key, ...rowProps } = getRowProps({ row });
|
|
150
|
+
const visitUuid = byUuid.get(row.id)?.visit?.uuid;
|
|
151
|
+
return (
|
|
152
|
+
<React.Fragment key={row.id}>
|
|
153
|
+
<TableExpandRow {...rowProps}>
|
|
154
|
+
{row.cells.map((cell) => (
|
|
155
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
156
|
+
))}
|
|
157
|
+
<TableCell className={styles.rowActionCell}>
|
|
158
|
+
{visitUuid && (
|
|
159
|
+
<Button
|
|
160
|
+
kind="ghost"
|
|
161
|
+
size="sm"
|
|
162
|
+
hasIconOnly
|
|
163
|
+
renderIcon={Printer}
|
|
164
|
+
iconDescription={t('printSummary', 'Print summary')}
|
|
165
|
+
tooltipPosition="left"
|
|
166
|
+
onClick={() => openVisitSummary(visitUuid)}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</TableCell>
|
|
170
|
+
</TableExpandRow>
|
|
171
|
+
<TableExpandedRow colSpan={headers.length + 2} {...getExpandedRowProps({ row })}>
|
|
172
|
+
{byUuid.get(row.id) && (
|
|
173
|
+
<EncounterDetail encounter={byUuid.get(row.id)!} fieldLabels={fieldLabels} t={t} />
|
|
174
|
+
)}
|
|
175
|
+
</TableExpandedRow>
|
|
176
|
+
</React.Fragment>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
</TableBody>
|
|
180
|
+
</Table>
|
|
181
|
+
</TableContainer>
|
|
182
|
+
)}
|
|
183
|
+
</DataTable>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const EncounterDetail: React.FC<{
|
|
189
|
+
encounter: ClinicalEncounter;
|
|
190
|
+
fieldLabels: Record<string, string>;
|
|
191
|
+
t: (k: string, f: string) => string;
|
|
192
|
+
}> = ({ encounter, fieldLabels, t }) => {
|
|
193
|
+
const obs = (encounter.obs ?? []).filter((o: EncounterObs) => o.concept);
|
|
194
|
+
const hasNothing = !encounter.diagnoses?.length && !obs.length && !encounter.orders?.length;
|
|
195
|
+
return (
|
|
196
|
+
<div className={styles.detail}>
|
|
197
|
+
{encounter.diagnoses?.length > 0 && (
|
|
198
|
+
<section className={styles.detailSection}>
|
|
199
|
+
<h6 className={styles.detailLabel}>{t('diagnoses', 'Diagnoses')}</h6>
|
|
200
|
+
<div className={styles.tagRow}>
|
|
201
|
+
{encounter.diagnoses.map((d, i) => (
|
|
202
|
+
<Tag key={i} type={d.rank === 1 ? 'red' : 'gray'}>
|
|
203
|
+
{d.display}
|
|
204
|
+
{d.certainty === 'CONFIRMED' ? ` · ${t('confirmed', 'Confirmed')}` : ` · ${t('presumed', 'Presumed')}`}
|
|
205
|
+
</Tag>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{obs.length > 0 && (
|
|
212
|
+
<section className={styles.detailSection}>
|
|
213
|
+
<h6 className={styles.detailLabel}>{t('findings', 'Findings')}</h6>
|
|
214
|
+
<dl className={styles.obsGrid}>
|
|
215
|
+
{obs.map((o) => (
|
|
216
|
+
<div key={o.uuid} className={styles.obsRow}>
|
|
217
|
+
<dt className={styles.obsName}>
|
|
218
|
+
{(o.concept?.uuid && fieldLabels[o.concept.uuid]) || toSentenceCase(o.concept?.display ?? '')}
|
|
219
|
+
</dt>
|
|
220
|
+
<dd className={styles.obsValue}>{formatObsValue(o.value)}</dd>
|
|
221
|
+
</div>
|
|
222
|
+
))}
|
|
223
|
+
</dl>
|
|
224
|
+
</section>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{encounter.orders?.length > 0 && (
|
|
228
|
+
<section className={styles.detailSection}>
|
|
229
|
+
<h6 className={styles.detailLabel}>{t('orders', 'Orders')}</h6>
|
|
230
|
+
<ul className={styles.orderList}>
|
|
231
|
+
{encounter.orders.map((o, i) => (
|
|
232
|
+
<li key={i}>{o.display}</li>
|
|
233
|
+
))}
|
|
234
|
+
</ul>
|
|
235
|
+
</section>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{hasNothing && <p className={styles.emptyBody}>{t('noRecordedData', 'No data was recorded on this encounter.')}</p>}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export default ClinicalEncountersList;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
|
|
4
|
+
/** The Clinical Encounter type the custom form engine saves against. */
|
|
5
|
+
const CLINICAL_ENCOUNTER_TYPE = 'd89b11c0-78f3-45a4-9300-587d91145462';
|
|
6
|
+
|
|
7
|
+
export interface EncounterObs {
|
|
8
|
+
uuid: string;
|
|
9
|
+
concept?: { uuid: string; display: string };
|
|
10
|
+
value: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EncounterDiagnosis {
|
|
14
|
+
display: string;
|
|
15
|
+
certainty?: string;
|
|
16
|
+
rank?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EncounterOrderRef {
|
|
20
|
+
display: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ClinicalEncounter {
|
|
24
|
+
uuid: string;
|
|
25
|
+
encounterDatetime: string;
|
|
26
|
+
encounterProviders: Array<{ provider?: { display: string } }>;
|
|
27
|
+
visit?: { uuid: string };
|
|
28
|
+
obs: Array<EncounterObs>;
|
|
29
|
+
diagnoses: Array<EncounterDiagnosis>;
|
|
30
|
+
orders: Array<EncounterOrderRef>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const REP =
|
|
34
|
+
'custom:(uuid,encounterDatetime,encounterProviders:(provider:(display)),visit:(uuid),' +
|
|
35
|
+
'obs:(uuid,concept:(uuid,display),value),diagnoses:(display,certainty,rank),orders:(display))';
|
|
36
|
+
|
|
37
|
+
/** The clinical-encounter form id whose field labels drive the records-table observation labels. */
|
|
38
|
+
const CLINICAL_ENCOUNTER_FORM = 'clinical-encounter';
|
|
39
|
+
|
|
40
|
+
interface RawFormField {
|
|
41
|
+
concept?: string;
|
|
42
|
+
label?: string;
|
|
43
|
+
}
|
|
44
|
+
interface RawFormSection {
|
|
45
|
+
fields?: Array<RawFormField>;
|
|
46
|
+
}
|
|
47
|
+
interface RawFormPage {
|
|
48
|
+
sections?: Array<RawFormSection>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Concept-uuid -> authored form field label for the clinical-encounter form, so the records table
|
|
53
|
+
* shows each observation under its form label (matching the printed summary). Empty until loaded.
|
|
54
|
+
*/
|
|
55
|
+
export function useClinicalEncounterFieldLabels(): Record<string, string> {
|
|
56
|
+
const url = `${restBaseUrl}/digihmisforms/form/${CLINICAL_ENCOUNTER_FORM}`;
|
|
57
|
+
const { data } = useSWR<FetchResponse<{ pages?: Array<RawFormPage> }>>(url, openmrsFetch);
|
|
58
|
+
const labels: Record<string, string> = {};
|
|
59
|
+
for (const page of data?.data?.pages ?? []) {
|
|
60
|
+
for (const section of page.sections ?? []) {
|
|
61
|
+
for (const field of section.fields ?? []) {
|
|
62
|
+
if (field.concept && field.label) {
|
|
63
|
+
labels[field.concept] = field.label;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return labels;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The patient's saved clinical-encounter records, most recent first. */
|
|
72
|
+
export function useClinicalEncounters(patientUuid?: string) {
|
|
73
|
+
const url = patientUuid
|
|
74
|
+
? `${restBaseUrl}/encounter?patient=${patientUuid}&encounterType=${CLINICAL_ENCOUNTER_TYPE}&v=${REP}&limit=50`
|
|
75
|
+
: null;
|
|
76
|
+
const { data, error, isLoading, mutate } = useSWR<FetchResponse<{ results: Array<ClinicalEncounter> }>>(
|
|
77
|
+
url,
|
|
78
|
+
openmrsFetch,
|
|
79
|
+
);
|
|
80
|
+
const encounters = (data?.data?.results ?? [])
|
|
81
|
+
.slice()
|
|
82
|
+
.sort((a, b) => (a.encounterDatetime < b.encounterDatetime ? 1 : -1));
|
|
83
|
+
return { encounters, error, isLoading, mutate };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Open a visit's discharge/case summary PDF (digihmis-module-print) in a new tab. */
|
|
87
|
+
export function openVisitSummary(visitUuid: string) {
|
|
88
|
+
const base = (window as unknown as { openmrsBase?: string }).openmrsBase ?? '/openmrs';
|
|
89
|
+
window.open(`${base}${restBaseUrl}/digihmisprint/dischargeSummary?visitUuid=${visitUuid}`, '_blank', 'noopener');
|
|
90
|
+
}
|