@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.
Files changed (146) hide show
  1. package/README.md +4 -0
  2. package/dist/117.js +1 -0
  3. package/dist/117.js.map +1 -0
  4. package/dist/131.js +1 -0
  5. package/dist/131.js.map +1 -0
  6. package/dist/132.js +1 -0
  7. package/dist/132.js.map +1 -0
  8. package/dist/152.js +1 -0
  9. package/dist/152.js.map +1 -0
  10. package/dist/209.js +1 -0
  11. package/dist/209.js.map +1 -0
  12. package/dist/247.js +1 -0
  13. package/dist/247.js.map +1 -0
  14. package/dist/28.js +1 -0
  15. package/dist/28.js.map +1 -0
  16. package/dist/317.js +1 -0
  17. package/dist/317.js.map +1 -0
  18. package/dist/349.js +1 -0
  19. package/dist/349.js.map +1 -0
  20. package/dist/371.js +1 -0
  21. package/dist/371.js.map +1 -0
  22. package/dist/378.js +1 -0
  23. package/dist/378.js.map +1 -0
  24. package/dist/442.js +1 -0
  25. package/dist/442.js.map +1 -0
  26. package/dist/45.js +1 -0
  27. package/dist/45.js.map +1 -0
  28. package/dist/459.js +12 -0
  29. package/dist/459.js.map +1 -0
  30. package/dist/466.js +1 -0
  31. package/dist/466.js.map +1 -0
  32. package/dist/508.js +1 -0
  33. package/dist/508.js.map +1 -0
  34. package/dist/61.js +1 -0
  35. package/dist/61.js.map +1 -0
  36. package/dist/640.js +1 -0
  37. package/dist/640.js.map +1 -0
  38. package/dist/689.js +1 -0
  39. package/dist/689.js.map +1 -0
  40. package/dist/709.js +1 -0
  41. package/dist/709.js.map +1 -0
  42. package/dist/712.js +1 -0
  43. package/dist/712.js.map +1 -0
  44. package/dist/720.js +1 -0
  45. package/dist/720.js.map +1 -0
  46. package/dist/742.js +1 -0
  47. package/dist/742.js.map +1 -0
  48. package/dist/771.js +1 -0
  49. package/dist/771.js.map +1 -0
  50. package/dist/806.js +1 -0
  51. package/dist/806.js.map +1 -0
  52. package/dist/912.js +1 -0
  53. package/dist/912.js.map +1 -0
  54. package/dist/913.js +1 -0
  55. package/dist/913.js.map +1 -0
  56. package/dist/940.js +1 -0
  57. package/dist/940.js.map +1 -0
  58. package/dist/957.js +15 -0
  59. package/dist/957.js.map +1 -0
  60. package/dist/digihmis-esm-patient-registration-app.js +6 -0
  61. package/dist/digihmis-esm-patient-registration-app.js.buildmanifest.json +827 -0
  62. package/dist/digihmis-esm-patient-registration-app.js.map +1 -0
  63. package/dist/main.js +17 -0
  64. package/dist/main.js.map +1 -0
  65. package/dist/routes.json +1 -0
  66. package/jest.config.js +26 -0
  67. package/package.json +51 -0
  68. package/rspack.config.js +1 -0
  69. package/src/active-visit-table/active-visit-table.component.tsx +240 -0
  70. package/src/active-visit-table/active-visits.resource.ts +338 -0
  71. package/src/active-visit-table/active-visits.scss +211 -0
  72. package/src/active-visit-table/empty-data-illustration.component.tsx +39 -0
  73. package/src/checkin-form/base-visit-type.component.tsx +102 -0
  74. package/src/checkin-form/base-visit-type.scss +75 -0
  75. package/src/checkin-form/checkin-form.scss +167 -0
  76. package/src/checkin-form/checkin-form.workspace.tsx +703 -0
  77. package/src/checkin-form/hooks/useDefaultFacilityLocation.ts +15 -0
  78. package/src/checkin-form/hooks/useDefaultVisitLocation.ts +24 -0
  79. package/src/checkin-form/hooks/useOfflineVisitType.ts +18 -0
  80. package/src/checkin-form/hooks/usePatientHistory.ts +40 -0
  81. package/src/checkin-form/hooks/useRecommendedVisitTypes.ts +34 -0
  82. package/src/checkin-form/hooks/useVisitAttributeType.ts +102 -0
  83. package/src/checkin-form/location-selector.component.tsx +86 -0
  84. package/src/checkin-form/recommended-visit-type.component.tsx +32 -0
  85. package/src/checkin-form/visit-attribute-type.component.tsx +256 -0
  86. package/src/checkin-form/visit-attribute-type.scss +5 -0
  87. package/src/checkin-form/visit-date-time.component.tsx +201 -0
  88. package/src/checkin-form/visit-form.resource.ts +308 -0
  89. package/src/components/custom-card/compact-patient-banner.component.tsx +70 -0
  90. package/src/components/custom-card/compact-patient-banner.scss +34 -0
  91. package/src/components/custom-card/custom-loader.component.tsx +22 -0
  92. package/src/components/custom-card/patient-search-context.ts +30 -0
  93. package/src/components/custom-input/autosuggest/autosuggest.component.tsx +213 -0
  94. package/src/components/custom-input/autosuggest/autosuggest.scss +158 -0
  95. package/src/components/custom-input/autosuggest/autosuggest.test.tsx +168 -0
  96. package/src/components/custom-patient-banner/custom-patient-banner.component.tsx +39 -0
  97. package/src/components/custom-patient-banner/custom-patient-banner.resource.ts +32 -0
  98. package/src/components/custom-patient-banner/custom-patient-banner.scss +26 -0
  99. package/src/config-schema.ts +511 -0
  100. package/src/constant.ts +6 -0
  101. package/src/constants/index.ts +1 -0
  102. package/src/dashboard.meta.ts +8 -0
  103. package/src/declarations.d.ts +6 -0
  104. package/src/empty-svg/empty-svg.component.tsx +565 -0
  105. package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
  106. package/src/identifier-workspace/identifier-configure-form.workspace.tsx +278 -0
  107. package/src/identifier-workspace/identifier-configure.scss +106 -0
  108. package/src/identifier-workspace/identifier.resource.ts +90 -0
  109. package/src/index.ts +33 -0
  110. package/src/left-panel/left-panel.component.tsx +54 -0
  111. package/src/left-panel/left-panel.scss +13 -0
  112. package/src/page-header/page-header.component.tsx +35 -0
  113. package/src/page-header/page-header.scss +76 -0
  114. package/src/patient-tags/deceased-patient-tag.extension.tsx +41 -0
  115. package/src/patient-tags/deceased-patient-tag.scss +15 -0
  116. package/src/patient-tags/patient-tags.resource.ts +30 -0
  117. package/src/patient-tags/visit-tag.extension.tsx +43 -0
  118. package/src/patient-tags/visit-tag.scss +19 -0
  119. package/src/registration-form/form-manager.ts +449 -0
  120. package/src/registration-form/registration-form.component.tsx +1565 -0
  121. package/src/registration-form/registration-form.resource.ts +264 -0
  122. package/src/registration-form/registration-form.scss +282 -0
  123. package/src/routes.json +62 -0
  124. package/src/search-bar/search-bar.component.tsx +285 -0
  125. package/src/search-bar/search-bar.resource.ts +274 -0
  126. package/src/search-bar/search-bar.scss +427 -0
  127. package/src/search-dashboard/search-dashboard.component.tsx +17 -0
  128. package/src/search-dashboard/search-dashboard.scss +13 -0
  129. package/src/setup-tests.ts +1 -0
  130. package/src/type/index.ts +463 -0
  131. package/src/utils/fhir-mapper.ts +73 -0
  132. package/src/visit-summary/visit-summary.component.tsx +32 -0
  133. package/src/visit-summary/visit-summary.resource.ts +56 -0
  134. package/src/visit-summary/visit-summary.scss +74 -0
  135. package/translations/am.json +112 -0
  136. package/translations/ar.json +120 -0
  137. package/translations/en.json +112 -0
  138. package/translations/es.json +114 -0
  139. package/translations/fr.json +114 -0
  140. package/translations/he.json +114 -0
  141. package/translations/km.json +111 -0
  142. package/translations/pt.json +114 -0
  143. package/translations/sw.json +112 -0
  144. package/translations/vi.json +111 -0
  145. package/translations/zh.json +111 -0
  146. package/tsconfig.json +5 -0
@@ -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
+ }
@@ -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
+ };