@digihmis/esm-patient-registration-app 1.0.0
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/README.md +4 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/131.js +1 -0
- package/dist/131.js.map +1 -0
- package/dist/132.js +1 -0
- package/dist/132.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/247.js +1 -0
- package/dist/247.js.map +1 -0
- package/dist/28.js +1 -0
- package/dist/28.js.map +1 -0
- package/dist/317.js +1 -0
- package/dist/317.js.map +1 -0
- package/dist/349.js +1 -0
- package/dist/349.js.map +1 -0
- package/dist/371.js +1 -0
- package/dist/371.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/45.js +1 -0
- package/dist/45.js.map +1 -0
- package/dist/459.js +12 -0
- package/dist/459.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/640.js +1 -0
- package/dist/640.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/709.js +1 -0
- package/dist/709.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/720.js +1 -0
- package/dist/720.js.map +1 -0
- package/dist/742.js +1 -0
- package/dist/742.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/806.js +1 -0
- package/dist/806.js.map +1 -0
- package/dist/912.js +1 -0
- package/dist/912.js.map +1 -0
- package/dist/913.js +1 -0
- package/dist/913.js.map +1 -0
- package/dist/940.js +1 -0
- package/dist/940.js.map +1 -0
- package/dist/957.js +15 -0
- package/dist/957.js.map +1 -0
- package/dist/digihmis-esm-patient-registration-app.js +6 -0
- package/dist/digihmis-esm-patient-registration-app.js.buildmanifest.json +827 -0
- package/dist/digihmis-esm-patient-registration-app.js.map +1 -0
- package/dist/main.js +17 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/rspack.config.js +1 -0
- package/src/active-visit-table/active-visit-table.component.tsx +240 -0
- package/src/active-visit-table/active-visits.resource.ts +338 -0
- package/src/active-visit-table/active-visits.scss +211 -0
- package/src/active-visit-table/empty-data-illustration.component.tsx +39 -0
- package/src/checkin-form/base-visit-type.component.tsx +102 -0
- package/src/checkin-form/base-visit-type.scss +75 -0
- package/src/checkin-form/checkin-form.scss +167 -0
- package/src/checkin-form/checkin-form.workspace.tsx +703 -0
- package/src/checkin-form/hooks/useDefaultFacilityLocation.ts +15 -0
- package/src/checkin-form/hooks/useDefaultVisitLocation.ts +24 -0
- package/src/checkin-form/hooks/useOfflineVisitType.ts +18 -0
- package/src/checkin-form/hooks/usePatientHistory.ts +40 -0
- package/src/checkin-form/hooks/useRecommendedVisitTypes.ts +34 -0
- package/src/checkin-form/hooks/useVisitAttributeType.ts +102 -0
- package/src/checkin-form/location-selector.component.tsx +86 -0
- package/src/checkin-form/recommended-visit-type.component.tsx +32 -0
- package/src/checkin-form/visit-attribute-type.component.tsx +256 -0
- package/src/checkin-form/visit-attribute-type.scss +5 -0
- package/src/checkin-form/visit-date-time.component.tsx +201 -0
- package/src/checkin-form/visit-form.resource.ts +308 -0
- package/src/components/custom-card/compact-patient-banner.component.tsx +70 -0
- package/src/components/custom-card/compact-patient-banner.scss +34 -0
- package/src/components/custom-card/custom-loader.component.tsx +22 -0
- package/src/components/custom-card/patient-search-context.ts +30 -0
- package/src/components/custom-input/autosuggest/autosuggest.component.tsx +213 -0
- package/src/components/custom-input/autosuggest/autosuggest.scss +158 -0
- package/src/components/custom-input/autosuggest/autosuggest.test.tsx +168 -0
- package/src/components/custom-patient-banner/custom-patient-banner.component.tsx +39 -0
- package/src/components/custom-patient-banner/custom-patient-banner.resource.ts +32 -0
- package/src/components/custom-patient-banner/custom-patient-banner.scss +26 -0
- package/src/config-schema.ts +511 -0
- package/src/constant.ts +6 -0
- package/src/constants/index.ts +1 -0
- package/src/dashboard.meta.ts +8 -0
- package/src/declarations.d.ts +6 -0
- package/src/empty-svg/empty-svg.component.tsx +565 -0
- package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
- package/src/identifier-workspace/identifier-configure-form.workspace.tsx +278 -0
- package/src/identifier-workspace/identifier-configure.scss +106 -0
- package/src/identifier-workspace/identifier.resource.ts +90 -0
- package/src/index.ts +33 -0
- package/src/left-panel/left-panel.component.tsx +54 -0
- package/src/left-panel/left-panel.scss +13 -0
- package/src/page-header/page-header.component.tsx +35 -0
- package/src/page-header/page-header.scss +76 -0
- package/src/patient-tags/deceased-patient-tag.extension.tsx +41 -0
- package/src/patient-tags/deceased-patient-tag.scss +15 -0
- package/src/patient-tags/patient-tags.resource.ts +30 -0
- package/src/patient-tags/visit-tag.extension.tsx +43 -0
- package/src/patient-tags/visit-tag.scss +19 -0
- package/src/registration-form/form-manager.ts +449 -0
- package/src/registration-form/registration-form.component.tsx +1565 -0
- package/src/registration-form/registration-form.resource.ts +264 -0
- package/src/registration-form/registration-form.scss +282 -0
- package/src/routes.json +62 -0
- package/src/search-bar/search-bar.component.tsx +285 -0
- package/src/search-bar/search-bar.resource.ts +274 -0
- package/src/search-bar/search-bar.scss +427 -0
- package/src/search-dashboard/search-dashboard.component.tsx +17 -0
- package/src/search-dashboard/search-dashboard.scss +13 -0
- package/src/setup-tests.ts +1 -0
- package/src/type/index.ts +463 -0
- package/src/utils/fhir-mapper.ts +73 -0
- package/src/visit-summary/visit-summary.component.tsx +32 -0
- package/src/visit-summary/visit-summary.resource.ts +56 -0
- package/src/visit-summary/visit-summary.scss +74 -0
- package/translations/am.json +112 -0
- package/translations/ar.json +120 -0
- package/translations/en.json +112 -0
- package/translations/es.json +114 -0
- package/translations/fr.json +114 -0
- package/translations/he.json +114 -0
- package/translations/km.json +111 -0
- package/translations/pt.json +114 -0
- package/translations/sw.json +112 -0
- package/translations/vi.json +111 -0
- package/translations/zh.json +111 -0
- package/tsconfig.json +5 -0
package/dist/routes.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"extensions":[{"name":"registration-dashboard-link","slot":"registration-dashboard-slot","component":"leftPanelLink","meta":{"name":"patient-registration","slot":"registration-homepage-dashboard-slot","title":"search"}},{"name":"search-dashboard","slot":"search-dashboard-slot","component":"searchDashboard"},{"name":"registration-form","slot":"registration-form-slot","component":"registrationForm"},{"name":"visit-tag","slot":"patient-banner-tags-slot","component":"visitTag","online":true,"offline":true},{"name":"deceased-patient-tag","slot":"patient-banner-tags-slot","component":"deceasedPatientTag","online":true,"offline":true}],"workspaces":[{"name":"configure-identifiers","component":"identifierConfigureForm","title":"configureIdentifiersForm","type":"other-form","canMaximize":true,"width":"extra-wide"},{"name":"check-in-form","component":"checkInWorkspace","title":"checkInFormTitle","type":"other-form","canMaximize":true,"width":"extra-wide"}],"version":"1.0.0"}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
clearMocks: true,
|
|
4
|
+
transform: {
|
|
5
|
+
'^.+\\.[jt]sx?$': ['@swc/jest'],
|
|
6
|
+
},
|
|
7
|
+
transformIgnorePatterns: ['/node_modules/(?!@openmrs|.+\\.pnp\\.[^\\/]+$)'],
|
|
8
|
+
moduleNameMapper: {
|
|
9
|
+
'\\.(s?css)$': 'identity-obj-proxy',
|
|
10
|
+
'@openmrs/esm-framework': '@openmrs/esm-framework/mock',
|
|
11
|
+
'lodash-es': 'lodash',
|
|
12
|
+
'^dexie$': require.resolve('dexie'),
|
|
13
|
+
},
|
|
14
|
+
collectCoverageFrom: [
|
|
15
|
+
'**/src/**/*.tsx',
|
|
16
|
+
'!**/node_modules/**',
|
|
17
|
+
'!**/vendor/**',
|
|
18
|
+
'!**/src/**/*.test.*',
|
|
19
|
+
'!**/src/declarations.d.ts',
|
|
20
|
+
],
|
|
21
|
+
setupFilesAfterEnv: ['<rootDir>/setupTests.ts'],
|
|
22
|
+
testEnvironment: 'jsdom',
|
|
23
|
+
testEnvironmentOptions: {
|
|
24
|
+
url: 'http://localhost/',
|
|
25
|
+
},
|
|
26
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@digihmis/esm-patient-registration-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Patient Registration microfrontend for the OpenMRS SPA",
|
|
5
|
+
"browser": "dist/digihmis-esm-patient-registration-app.js",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"source": true,
|
|
8
|
+
"license": "MPL-2.0",
|
|
9
|
+
"homepage": "https://github.com/its-kios09/DigiHMIS-apps#readme",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"analyze": "rspack --mode=production --env analyze=true",
|
|
12
|
+
"build": "rspack --mode=production",
|
|
13
|
+
"coverage": "yarn test --coverage",
|
|
14
|
+
"debug": "npm run serve",
|
|
15
|
+
"extract-translations": "i18next 'src/**/*.component.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js",
|
|
16
|
+
"lint": "eslint src --ext ts,tsx",
|
|
17
|
+
"serve": "rspack serve --mode=development",
|
|
18
|
+
"start": "openmrs develop",
|
|
19
|
+
"test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests",
|
|
20
|
+
"test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js",
|
|
21
|
+
"typescript": "tsc"
|
|
22
|
+
},
|
|
23
|
+
"browserslist": [
|
|
24
|
+
"extends browserslist-config-openmrs"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"openmrs"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/its-kios09/DigiHMIS-apps.git#readme"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/its-kios09/DigiHMIS-apps/issues"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"lodash-es": "^4.17.15",
|
|
41
|
+
"react-to-print": "2.14.13"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@carbon/react": "1.x",
|
|
45
|
+
"@openmrs/esm-framework": "8.x",
|
|
46
|
+
"react": "^18.1.0",
|
|
47
|
+
"react-i18next": "16.x",
|
|
48
|
+
"react-router-dom": "6.x",
|
|
49
|
+
"swr": "2.x"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/rspack.config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../../tools/rs-pack-code-spliting');
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
DataTable,
|
|
4
|
+
DataTableSkeleton,
|
|
5
|
+
InlineLoading,
|
|
6
|
+
Layer,
|
|
7
|
+
Pagination,
|
|
8
|
+
Search,
|
|
9
|
+
Table,
|
|
10
|
+
TableBody,
|
|
11
|
+
TableCell,
|
|
12
|
+
TableContainer,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow,
|
|
16
|
+
Tile,
|
|
17
|
+
} from '@carbon/react';
|
|
18
|
+
import { useTranslation } from 'react-i18next';
|
|
19
|
+
import {
|
|
20
|
+
ConfigurableLink,
|
|
21
|
+
ErrorState,
|
|
22
|
+
ExtensionSlot,
|
|
23
|
+
isDesktop,
|
|
24
|
+
useConfig,
|
|
25
|
+
useLayoutType,
|
|
26
|
+
usePagination,
|
|
27
|
+
} from '@openmrs/esm-framework';
|
|
28
|
+
import styles from './active-visits.scss';
|
|
29
|
+
import { type ActiveVisitsConfigSchema } from '../config-schema';
|
|
30
|
+
import { type ActiveVisit } from '../type';
|
|
31
|
+
import { useActiveVisits, useActiveVisitsSorting, useObsConcepts, useTableHeaders } from './active-visits.resource';
|
|
32
|
+
import { EmptyDataIllustration } from './empty-data-illustration.component';
|
|
33
|
+
import upperCase from 'lodash-es/upperCase';
|
|
34
|
+
|
|
35
|
+
interface ActiveVisitsTableProps {
|
|
36
|
+
mutate?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ActiveVisitsTable: React.FC<ActiveVisitsTableProps> = ({ mutate }) => {
|
|
40
|
+
const { t } = useTranslation();
|
|
41
|
+
const config = useConfig<ActiveVisitsConfigSchema>();
|
|
42
|
+
const layout = useLayoutType();
|
|
43
|
+
const pageSizes = config?.activeVisits?.pageSizes ?? [10, 20, 30, 40, 50];
|
|
44
|
+
const [pageSize, setPageSize] = useState(config?.activeVisits?.pageSize ?? 10);
|
|
45
|
+
const { obsConcepts, isLoadingObsConcepts } = useObsConcepts(config.activeVisits.obs);
|
|
46
|
+
const { activeVisits, isLoading, isValidating, error, mutate: mutateActiveVisits } = useActiveVisits();
|
|
47
|
+
const [searchString, setSearchString] = useState('');
|
|
48
|
+
const headerData = useTableHeaders(obsConcepts);
|
|
49
|
+
|
|
50
|
+
const transformVisitForDisplay = useCallback((visit: ActiveVisit) => {
|
|
51
|
+
const displayData = { ...visit };
|
|
52
|
+
|
|
53
|
+
return displayData;
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const searchResults = useMemo(() => {
|
|
57
|
+
if (activeVisits !== undefined && activeVisits.length > 0) {
|
|
58
|
+
if (searchString && searchString.trim() !== '') {
|
|
59
|
+
const search = searchString.toLowerCase();
|
|
60
|
+
return activeVisits?.filter((activeVisitRow) =>
|
|
61
|
+
Object.entries(activeVisitRow).some(([header, value]) => {
|
|
62
|
+
if (header === 'patientUuid') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return `${value}`.toLowerCase().includes(search);
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return activeVisits.map(transformVisitForDisplay);
|
|
72
|
+
}, [searchString, activeVisits, transformVisitForDisplay]);
|
|
73
|
+
|
|
74
|
+
const { sortedRows, sortRow } = useActiveVisitsSorting(searchResults);
|
|
75
|
+
const { paginated, goTo, results, currentPage } = usePagination(sortedRows, pageSize as number);
|
|
76
|
+
|
|
77
|
+
const handleSearch = useCallback(
|
|
78
|
+
(e) => {
|
|
79
|
+
goTo(1);
|
|
80
|
+
setSearchString(e.target.value);
|
|
81
|
+
},
|
|
82
|
+
[goTo, setSearchString],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (isLoading || isLoadingObsConcepts) {
|
|
86
|
+
return (
|
|
87
|
+
<div className={styles.activeVisitsContainer}>
|
|
88
|
+
<div className={styles.activeVisitsDetailHeaderContainer}>
|
|
89
|
+
<div className={!isDesktop(layout) ? styles.tabletHeading : styles.desktopHeading}>
|
|
90
|
+
<h4>{t('activeVisits', 'Active Visits')}</h4>
|
|
91
|
+
</div>
|
|
92
|
+
<div className={styles.backgroundDataFetchingIndicator}>
|
|
93
|
+
<span>{isValidating ? <InlineLoading /> : null}</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<Search
|
|
97
|
+
labelText={t('filterActiveVisits', 'Filter active visits')}
|
|
98
|
+
placeholder={t('filterTable', 'Filter table')}
|
|
99
|
+
onChange={handleSearch}
|
|
100
|
+
size={isDesktop(layout) ? 'sm' : 'lg'}
|
|
101
|
+
disabled
|
|
102
|
+
/>
|
|
103
|
+
<DataTableSkeleton
|
|
104
|
+
rowCount={pageSize}
|
|
105
|
+
showHeader={false}
|
|
106
|
+
showToolbar={false}
|
|
107
|
+
zebra
|
|
108
|
+
columnCount={headerData?.length}
|
|
109
|
+
size={isDesktop(layout) ? 'sm' : 'lg'}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (error) {
|
|
116
|
+
return (
|
|
117
|
+
<div className={styles.activeVisitsContainer}>
|
|
118
|
+
<Layer>
|
|
119
|
+
<ErrorState error={error} headerTitle={t('activeVisits', 'Active Visits')} />
|
|
120
|
+
</Layer>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!activeVisits.length) {
|
|
126
|
+
return (
|
|
127
|
+
<div className={styles.activeVisitsContainer}>
|
|
128
|
+
<Layer>
|
|
129
|
+
<Tile className={styles.tile}>
|
|
130
|
+
<div className={!isDesktop(layout) ? styles.tabletHeading : styles.desktopHeading}>
|
|
131
|
+
<h4>{t('activeVisits', 'Active Visits')}</h4>
|
|
132
|
+
</div>
|
|
133
|
+
<p className={styles.content}>
|
|
134
|
+
{t('noActiveVisitsForLocation', 'There are no active visits to display for this location.')}
|
|
135
|
+
</p>
|
|
136
|
+
</Tile>
|
|
137
|
+
</Layer>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className={styles.activeVisitsContainer}>
|
|
144
|
+
<div className={styles.activeVisitsDetailHeaderContainer}>
|
|
145
|
+
<div className={!isDesktop(layout) ? styles.tabletHeading : styles.desktopHeading}>
|
|
146
|
+
<h4>{t('activeVisits', 'Active Visits')}</h4>
|
|
147
|
+
</div>
|
|
148
|
+
<div className={styles.backgroundDataFetchingIndicator}>
|
|
149
|
+
<span>{isValidating ? <InlineLoading /> : null}</span>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<Search
|
|
153
|
+
labelText={t('filterActiveVisits', 'Filter active visits')}
|
|
154
|
+
placeholder={t('filterTable', 'Filter table')}
|
|
155
|
+
onChange={handleSearch}
|
|
156
|
+
size={isDesktop(layout) ? 'sm' : 'lg'}
|
|
157
|
+
/>
|
|
158
|
+
<DataTable
|
|
159
|
+
isSortable
|
|
160
|
+
useStaticWidth
|
|
161
|
+
rows={results}
|
|
162
|
+
headers={headerData}
|
|
163
|
+
sortRow={sortRow}
|
|
164
|
+
size={isDesktop(layout) ? 'sm' : 'lg'}
|
|
165
|
+
useZebraStyles={activeVisits?.length > 1}>
|
|
166
|
+
{({ rows, headers, getHeaderProps, getTableProps, getRowProps }) => (
|
|
167
|
+
<TableContainer className={styles.tableContainer}>
|
|
168
|
+
<Table className={styles.activeVisitsTable} {...getTableProps()}>
|
|
169
|
+
<TableHead>
|
|
170
|
+
<TableRow>
|
|
171
|
+
{headers.map((header) => (
|
|
172
|
+
<TableHeader key={header.key} {...getHeaderProps({ header })}>
|
|
173
|
+
{header.header}
|
|
174
|
+
</TableHeader>
|
|
175
|
+
))}
|
|
176
|
+
</TableRow>
|
|
177
|
+
</TableHead>
|
|
178
|
+
<TableBody>
|
|
179
|
+
{rows.map((row, index) => {
|
|
180
|
+
const currentVisit = activeVisits.find((visit) => visit.id === row.id);
|
|
181
|
+
|
|
182
|
+
if (!currentVisit) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<TableRow
|
|
188
|
+
key={`active-visit-row-${index}`}
|
|
189
|
+
{...getRowProps({ row })}
|
|
190
|
+
data-testid={`activeVisitRow${currentVisit.patientUuid || 'unknown'}`}>
|
|
191
|
+
{row.cells.map((cell) => (
|
|
192
|
+
<TableCell key={`active-visit-row-${index}-cell-${cell.id}`} data-testid={cell.id}>
|
|
193
|
+
{cell.info.header === 'name' || cell.info.header === 'status'
|
|
194
|
+
? upperCase(cell.value)
|
|
195
|
+
: cell.value}
|
|
196
|
+
</TableCell>
|
|
197
|
+
))}
|
|
198
|
+
</TableRow>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</TableBody>
|
|
202
|
+
</Table>
|
|
203
|
+
</TableContainer>
|
|
204
|
+
)}
|
|
205
|
+
</DataTable>
|
|
206
|
+
{searchResults?.length === 0 && (
|
|
207
|
+
<div className={styles.filterEmptyState}>
|
|
208
|
+
<Layer level={0}>
|
|
209
|
+
<Tile className={styles.filterEmptyStateTile}>
|
|
210
|
+
<p className={styles.filterEmptyStateContent}>{t('noVisitsToDisplay', 'No visits to display')}</p>
|
|
211
|
+
<p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
|
|
212
|
+
</Tile>
|
|
213
|
+
</Layer>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
{paginated && (
|
|
217
|
+
<Pagination
|
|
218
|
+
forwardText="Next page"
|
|
219
|
+
backwardText="Previous page"
|
|
220
|
+
page={currentPage}
|
|
221
|
+
pageSize={pageSize}
|
|
222
|
+
pageSizes={pageSizes}
|
|
223
|
+
totalItems={searchResults?.length}
|
|
224
|
+
className={styles.pagination}
|
|
225
|
+
size={isDesktop(layout) ? 'sm' : 'lg'}
|
|
226
|
+
onChange={({ pageSize: newPageSize, page: newPage }) => {
|
|
227
|
+
if (newPageSize !== pageSize) {
|
|
228
|
+
setPageSize(newPageSize);
|
|
229
|
+
}
|
|
230
|
+
if (newPage !== currentPage) {
|
|
231
|
+
goTo(newPage);
|
|
232
|
+
}
|
|
233
|
+
}}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export default ActiveVisitsTable;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import last from 'lodash-es/last';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import isToday from 'dayjs/plugin/isToday';
|
|
6
|
+
import useSWR from 'swr';
|
|
7
|
+
import useSWRInfinite from 'swr/infinite';
|
|
8
|
+
import {
|
|
9
|
+
type FetchResponse,
|
|
10
|
+
type OpenmrsResource,
|
|
11
|
+
type Visit,
|
|
12
|
+
formatDatetime,
|
|
13
|
+
openmrsFetch,
|
|
14
|
+
parseDate,
|
|
15
|
+
restBaseUrl,
|
|
16
|
+
useConfig,
|
|
17
|
+
useSession,
|
|
18
|
+
} from '@openmrs/esm-framework';
|
|
19
|
+
import { type ActiveVisitsConfigSchema } from '../config-schema';
|
|
20
|
+
import { VisitResponse, type ActiveVisit } from '../type';
|
|
21
|
+
|
|
22
|
+
dayjs.extend(isToday);
|
|
23
|
+
|
|
24
|
+
export function useActiveVisits() {
|
|
25
|
+
const session = useSession();
|
|
26
|
+
const config = useConfig<ActiveVisitsConfigSchema>();
|
|
27
|
+
const sessionLocation = session?.sessionLocation?.uuid;
|
|
28
|
+
|
|
29
|
+
const customRepresentation =
|
|
30
|
+
'custom:(uuid,patient:(uuid,identifiers:(identifier,uuid,identifierType:(name,uuid)),' +
|
|
31
|
+
'person:(age,display,gender,uuid,attributes:(value,attributeType:(uuid,display)))),' +
|
|
32
|
+
'visitType:(uuid,name,display),location:(uuid,name,display),startDatetime,stopDatetime,' +
|
|
33
|
+
'encounters:(encounterDatetime,obs:(uuid,concept:(uuid,display),value)))';
|
|
34
|
+
|
|
35
|
+
const getUrl = (pageIndex, previousPageData: FetchResponse<VisitResponse>) => {
|
|
36
|
+
if (pageIndex && !previousPageData?.data?.links?.some((link) => link.rel === 'next')) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let url = `${restBaseUrl}/visit?v=${customRepresentation}&`;
|
|
41
|
+
let urlSearchParams = new URLSearchParams();
|
|
42
|
+
|
|
43
|
+
urlSearchParams.append('includeParentLocations', 'true');
|
|
44
|
+
urlSearchParams.append('includeInactive', 'false');
|
|
45
|
+
urlSearchParams.append('totalCount', 'true');
|
|
46
|
+
urlSearchParams.append('location', `${sessionLocation}`);
|
|
47
|
+
|
|
48
|
+
if (pageIndex) {
|
|
49
|
+
urlSearchParams.append('startIndex', `${pageIndex * 50}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return url + urlSearchParams.toString();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const {
|
|
56
|
+
data,
|
|
57
|
+
error,
|
|
58
|
+
isLoading,
|
|
59
|
+
isValidating,
|
|
60
|
+
size: pageNumber,
|
|
61
|
+
setSize,
|
|
62
|
+
mutate,
|
|
63
|
+
} = useSWRInfinite<FetchResponse<VisitResponse>, Error>(sessionLocation ? getUrl : null, openmrsFetch);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (data && data?.[pageNumber - 1]?.data?.links?.some((link) => link.rel === 'next')) {
|
|
67
|
+
setSize((currentSize) => currentSize + 1);
|
|
68
|
+
}
|
|
69
|
+
}, [data, pageNumber, setSize]);
|
|
70
|
+
|
|
71
|
+
const mapVisitProperties = (visit: Visit): ActiveVisit => {
|
|
72
|
+
// create base object
|
|
73
|
+
const age = visit?.patient?.person?.age;
|
|
74
|
+
const activeVisits: ActiveVisit = {
|
|
75
|
+
age: age ? String(age) : null,
|
|
76
|
+
id: visit.uuid,
|
|
77
|
+
idNumber: null,
|
|
78
|
+
gender: visit?.patient?.person?.gender,
|
|
79
|
+
location: visit?.location?.uuid,
|
|
80
|
+
name: visit?.patient?.person?.display,
|
|
81
|
+
patientUuid: visit?.patient?.uuid,
|
|
82
|
+
visitStartTime: formatDatetime(parseDate(visit?.startDatetime)),
|
|
83
|
+
visitType: visit?.visitType?.display,
|
|
84
|
+
visitUuid: visit.uuid,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// in case no configuration is given the previous behavior remains the same
|
|
88
|
+
if (!config?.activeVisits?.identifiers) {
|
|
89
|
+
activeVisits.idNumber = visit?.patient?.identifiers[0]?.identifier ?? '--';
|
|
90
|
+
} else {
|
|
91
|
+
// map identifiers on config
|
|
92
|
+
config?.activeVisits?.identifiers?.map((configIdentifier) => {
|
|
93
|
+
// check if in the current visit the patient has in his identifiers the current identifierType name
|
|
94
|
+
const visitIdentifier = visit?.patient?.identifiers.find(
|
|
95
|
+
(visitIdentifier) => visitIdentifier?.identifierType?.name === configIdentifier?.identifierName,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// add the new identifier or rewrite existing one to activeVisit object
|
|
99
|
+
// the parameter will corresponds to the name of the key value of the configuration
|
|
100
|
+
// and the respective value is the visit identifier
|
|
101
|
+
// If there isn't a identifier we display this default text '--'
|
|
102
|
+
activeVisits[configIdentifier.header?.key] = visitIdentifier?.identifier ?? '--';
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// map attributes on config
|
|
107
|
+
config?.activeVisits?.attributes?.map(({ display, header }) => {
|
|
108
|
+
// check if in the current visit the person has in his attributes the current display
|
|
109
|
+
const personAttributes = visit?.patient?.person?.attributes.find(
|
|
110
|
+
(personAttributes) => personAttributes?.attributeType?.display === display,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// add the new attribute or rewrite existing one to activeVisit object
|
|
114
|
+
// the parameter will correspond to the name of the key value of the configuration
|
|
115
|
+
// and the respective value is the persons value
|
|
116
|
+
// If there isn't a attribute we display this default text '--'
|
|
117
|
+
activeVisits[header?.key] = personAttributes?.value ?? '--';
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Add flattened observations
|
|
121
|
+
const allObs = visit.encounters.reduce((accumulator, encounter) => {
|
|
122
|
+
return [...accumulator, ...(encounter.obs || [])];
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
activeVisits.observations = allObs.reduce((map, obs) => {
|
|
126
|
+
const key = obs.concept.uuid;
|
|
127
|
+
if (!map[key]) {
|
|
128
|
+
map[key] = [];
|
|
129
|
+
}
|
|
130
|
+
map[key].push({
|
|
131
|
+
value: obs.value,
|
|
132
|
+
uuid: obs.uuid,
|
|
133
|
+
});
|
|
134
|
+
return map;
|
|
135
|
+
}, {});
|
|
136
|
+
|
|
137
|
+
return activeVisits;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const formattedActiveVisits: Array<ActiveVisit> = data
|
|
141
|
+
? [].concat(...data?.map((res) => res?.data?.results?.map(mapVisitProperties)))
|
|
142
|
+
: [];
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
activeVisits: formattedActiveVisits,
|
|
146
|
+
error,
|
|
147
|
+
isLoading,
|
|
148
|
+
isValidating,
|
|
149
|
+
mutate,
|
|
150
|
+
totalResults: data?.[0]?.data?.totalCount ?? 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function useObsConcepts(uuids: Array<string>): {
|
|
155
|
+
obsConcepts: Array<OpenmrsResource> | undefined;
|
|
156
|
+
isLoadingObsConcepts: boolean;
|
|
157
|
+
} {
|
|
158
|
+
const fetchConcept = async (uuid: string): Promise<OpenmrsResource | null> => {
|
|
159
|
+
try {
|
|
160
|
+
const response = await openmrsFetch(`${restBaseUrl}/concept/${uuid}?v=custom:(uuid,display)`);
|
|
161
|
+
return response?.data;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`Error fetching concept for UUID: ${uuid}`, error);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const { data, isLoading, error } = useSWR(uuids.length > 0 ? ['obs-concepts', uuids] : null, async () => {
|
|
169
|
+
const results = await Promise.all(uuids.map(fetchConcept));
|
|
170
|
+
return results.filter((concept) => concept !== null);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return useMemo(
|
|
174
|
+
() => ({
|
|
175
|
+
obsConcepts: data ?? [],
|
|
176
|
+
isLoadingObsConcepts: isLoading,
|
|
177
|
+
}),
|
|
178
|
+
[data, isLoading],
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function useActiveVisitsSorting(tableRows: Array<any>) {
|
|
183
|
+
const [sortParams, setSortParams] = useState<{
|
|
184
|
+
key: string;
|
|
185
|
+
sortDirection: 'ASC' | 'DESC' | 'NONE';
|
|
186
|
+
}>({ key: 'visitStartTime', sortDirection: 'DESC' });
|
|
187
|
+
|
|
188
|
+
const sortRow = (cellA, cellB, { key, sortDirection }) => {
|
|
189
|
+
setSortParams({ key, sortDirection });
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const getSortValue = (item: any, key: string) => {
|
|
193
|
+
// For observation columns
|
|
194
|
+
if (key.startsWith('obs-')) {
|
|
195
|
+
const conceptUuid = key.replace('obs-', '');
|
|
196
|
+
const obsValue = item?.observations?.[conceptUuid]?.[0]?.value;
|
|
197
|
+
|
|
198
|
+
if (!obsValue) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (typeof obsValue === 'object' && obsValue.display) {
|
|
202
|
+
return obsValue.display.toLowerCase();
|
|
203
|
+
}
|
|
204
|
+
return obsValue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const value = item[key];
|
|
208
|
+
if (value == null) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (key === 'visitStartTime') {
|
|
213
|
+
return new Date(value).getTime();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (key === 'age' && !isNaN(value)) {
|
|
217
|
+
return Number(value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return String(value).toLowerCase();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const sortedRows = useMemo(() => {
|
|
224
|
+
if (sortParams.sortDirection === 'NONE') {
|
|
225
|
+
return tableRows;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return [...tableRows].sort((a, b) => {
|
|
229
|
+
const valueA = getSortValue(a, sortParams.key);
|
|
230
|
+
const valueB = getSortValue(b, sortParams.key);
|
|
231
|
+
|
|
232
|
+
if (valueA === null && valueB === null) {
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
if (valueA === null) {
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
if (valueB === null) {
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (typeof valueA === 'number' && typeof valueB === 'number') {
|
|
243
|
+
return sortParams.sortDirection === 'DESC' ? valueB - valueA : valueA - valueB;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const compareResult = String(valueA).localeCompare(String(valueB), undefined, {
|
|
247
|
+
numeric: true,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return sortParams.sortDirection === 'DESC' ? -compareResult : compareResult;
|
|
251
|
+
});
|
|
252
|
+
}, [sortParams, tableRows]);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
sortedRows,
|
|
256
|
+
sortRow,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function useTableHeaders(obsConcepts: OpenmrsResource[]) {
|
|
261
|
+
const { t } = useTranslation();
|
|
262
|
+
const config = useConfig<ActiveVisitsConfigSchema>();
|
|
263
|
+
|
|
264
|
+
return useMemo(() => {
|
|
265
|
+
let headersIndex = 0;
|
|
266
|
+
|
|
267
|
+
const headers = [
|
|
268
|
+
{
|
|
269
|
+
id: headersIndex++,
|
|
270
|
+
header: t('visitStartTime', 'Visit Time'),
|
|
271
|
+
key: 'visitStartTime',
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
config?.activeVisits?.identifiers?.forEach((identifier) => {
|
|
276
|
+
headers.push({
|
|
277
|
+
id: headersIndex++,
|
|
278
|
+
header: t(identifier?.header?.key, identifier?.header?.default),
|
|
279
|
+
key: identifier?.header?.key,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (!config?.activeVisits?.identifiers) {
|
|
284
|
+
headers.push({
|
|
285
|
+
id: headersIndex++,
|
|
286
|
+
header: t('idNumber', 'ID Number'),
|
|
287
|
+
key: 'idNumber',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
config?.activeVisits?.attributes?.forEach((attribute) => {
|
|
292
|
+
headers.push({
|
|
293
|
+
id: headersIndex++,
|
|
294
|
+
header: t(attribute?.header?.key, attribute?.header?.default),
|
|
295
|
+
key: attribute?.header?.key,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Add headers for obs concepts
|
|
300
|
+
obsConcepts?.forEach((concept) => {
|
|
301
|
+
headers.push({
|
|
302
|
+
id: headersIndex++,
|
|
303
|
+
header: concept.display,
|
|
304
|
+
key: `obs-${concept.uuid}`,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
headers.push(
|
|
309
|
+
{
|
|
310
|
+
id: headersIndex++,
|
|
311
|
+
header: t('name', 'Name'),
|
|
312
|
+
key: 'name',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: headersIndex++,
|
|
316
|
+
header: t('gender', 'Gender'),
|
|
317
|
+
key: 'gender',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: headersIndex++,
|
|
321
|
+
header: t('age', 'Age'),
|
|
322
|
+
key: 'age',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: headersIndex++,
|
|
326
|
+
header: t('visitType', 'Visit Type'),
|
|
327
|
+
key: 'visitType',
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return headers;
|
|
332
|
+
}, [t, config, obsConcepts]);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export const getOriginFromPathName = (pathname = '') => {
|
|
336
|
+
const from = pathname.split('/');
|
|
337
|
+
return last(from);
|
|
338
|
+
};
|