@kenyaemr/esm-patient-list-management-app 7.0.2-pre.65

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 (103) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/dist/130.js +2 -0
  3. package/dist/130.js.LICENSE.txt +3 -0
  4. package/dist/130.js.map +1 -0
  5. package/dist/139.js +1 -0
  6. package/dist/139.js.map +1 -0
  7. package/dist/255.js +2 -0
  8. package/dist/255.js.LICENSE.txt +9 -0
  9. package/dist/255.js.map +1 -0
  10. package/dist/271.js +1 -0
  11. package/dist/319.js +1 -0
  12. package/dist/382.js +1 -0
  13. package/dist/382.js.map +1 -0
  14. package/dist/443.js +1 -0
  15. package/dist/443.js.map +1 -0
  16. package/dist/460.js +1 -0
  17. package/dist/548.js +1 -0
  18. package/dist/548.js.map +1 -0
  19. package/dist/574.js +1 -0
  20. package/dist/591.js +2 -0
  21. package/dist/591.js.LICENSE.txt +32 -0
  22. package/dist/591.js.map +1 -0
  23. package/dist/635.js +1 -0
  24. package/dist/635.js.map +1 -0
  25. package/dist/644.js +1 -0
  26. package/dist/729.js +1 -0
  27. package/dist/729.js.map +1 -0
  28. package/dist/757.js +1 -0
  29. package/dist/784.js +2 -0
  30. package/dist/784.js.LICENSE.txt +9 -0
  31. package/dist/784.js.map +1 -0
  32. package/dist/788.js +1 -0
  33. package/dist/807.js +1 -0
  34. package/dist/833.js +1 -0
  35. package/dist/930.js +2 -0
  36. package/dist/930.js.LICENSE.txt +35 -0
  37. package/dist/930.js.map +1 -0
  38. package/dist/kenyaemr-esm-patient-list-management-app.js +1 -0
  39. package/dist/kenyaemr-esm-patient-list-management-app.js.buildmanifest.json +580 -0
  40. package/dist/kenyaemr-esm-patient-list-management-app.js.map +1 -0
  41. package/dist/main.js +2 -0
  42. package/dist/main.js.LICENSE.txt +45 -0
  43. package/dist/main.js.map +1 -0
  44. package/dist/routes.json +1 -0
  45. package/jest.config.js +3 -0
  46. package/package.json +56 -0
  47. package/src/add-patient/add-patient.component.tsx +271 -0
  48. package/src/add-patient/add-patient.scss +51 -0
  49. package/src/add-patient-to-patient-list-menu-item.component.tsx +48 -0
  50. package/src/add-patient-to-patient-list-menu-item.test.tsx +33 -0
  51. package/src/api/api-remote.ts +211 -0
  52. package/src/api/hooks.ts +150 -0
  53. package/src/api/types.ts +102 -0
  54. package/src/config-schema.ts +25 -0
  55. package/src/constants.ts +5 -0
  56. package/src/create-edit-patient-list/create-edit-list.component.tsx +170 -0
  57. package/src/create-edit-patient-list/create-edit-patient-list.scss +31 -0
  58. package/src/createDashboardLink.component.tsx +40 -0
  59. package/src/dashboard.meta.ts +5 -0
  60. package/src/declarations.d.ts +5 -0
  61. package/src/empty-state/empty-data-illustration.component.tsx +42 -0
  62. package/src/empty-state/empty-state.component.tsx +41 -0
  63. package/src/empty-state/empty-state.scss +24 -0
  64. package/src/error-state/error-state.component.tsx +35 -0
  65. package/src/error-state/error-state.scss +50 -0
  66. package/src/header/header.component.tsx +51 -0
  67. package/src/header/header.scss +52 -0
  68. package/src/illo.component.tsx +25 -0
  69. package/src/index.ts +41 -0
  70. package/src/list-details/list-details.component.tsx +201 -0
  71. package/src/list-details/list-details.scss +47 -0
  72. package/src/list-details/list-details.test.tsx +112 -0
  73. package/src/list-details/patient-list-detail.test.tsx +105 -0
  74. package/src/list-details-table/list-details-table.component.tsx +402 -0
  75. package/src/list-details-table/list-details-table.scss +143 -0
  76. package/src/list-details-table/list-details-table.test.tsx +94 -0
  77. package/src/lists-dashboard/lists-dashboard.component.tsx +104 -0
  78. package/src/lists-dashboard/lists-dashboard.scss +110 -0
  79. package/src/lists-dashboard/lists-dashboard.test.tsx +129 -0
  80. package/src/lists-table/custom-pagination.component.tsx +43 -0
  81. package/src/lists-table/custom-pagination.scss +67 -0
  82. package/src/lists-table/lists-table.component.tsx +317 -0
  83. package/src/lists-table/lists-table.scss +100 -0
  84. package/src/lists-table/lists-table.test.tsx +189 -0
  85. package/src/lists-table/use-pagination-info.component.tsx +35 -0
  86. package/src/offline.ts +31 -0
  87. package/src/overlay.component.tsx +49 -0
  88. package/src/overlay.scss +89 -0
  89. package/src/overlay.test.tsx +60 -0
  90. package/src/root.component.tsx +19 -0
  91. package/src/routes.json +36 -0
  92. package/src/style.scss +46 -0
  93. package/translations/am.json +84 -0
  94. package/translations/ar.json +84 -0
  95. package/translations/en.json +84 -0
  96. package/translations/es.json +84 -0
  97. package/translations/fr.json +84 -0
  98. package/translations/he.json +84 -0
  99. package/translations/km.json +84 -0
  100. package/translations/zh.json +84 -0
  101. package/translations/zh_CN.json +84 -0
  102. package/tsconfig.json +5 -0
  103. package/webpack.config.js +1 -0
@@ -0,0 +1 @@
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":"^2.2.0"},"extensions":[{"name":"patient-lists-dashboard-link","component":"patientListDashboardLink","slot":"homepage-dashboard-slot","meta":{"name":"patient-lists","slot":"patient-lists-dashboard-slot","title":"Patient lists"}},{"component":"root","name":"patient-lists-dashboard","slot":"patient-lists-dashboard-slot"},{"name":"list-details-table","component":"listDetailsTable"},{"name":"add-patient-to-patient-list-modal","component":"addPatientToListModal"},{"name":"add-patient-to-patient-list-button","slot":"patient-actions-slot","component":"addPatientToPatientListMenuItem"}],"version":"7.0.2-pre.65"}
package/jest.config.js ADDED
@@ -0,0 +1,3 @@
1
+ const rootConfig = require('../../jest.config.js');
2
+
3
+ module.exports = rootConfig;
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@kenyaemr/esm-patient-list-management-app",
3
+ "version": "7.0.2-pre.65",
4
+ "description": "Microfrontend for managing patient lists in O3",
5
+ "browser": "dist/kenyaemr-esm-patient-list-management-app.js",
6
+ "main": "src/index.ts",
7
+ "source": true,
8
+ "license": "MPL-2.0",
9
+ "homepage": "https://github.com/openmrs/openmrs-esm-patient-management#readme",
10
+ "scripts": {
11
+ "start": "openmrs develop",
12
+ "serve": "webpack serve --mode=development",
13
+ "debug": "npm run serve",
14
+ "build": "webpack --mode production",
15
+ "analyze": "webpack --mode=production --env.analyze=true",
16
+ "lint": "cross-env eslint src --ext ts,tsx",
17
+ "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color",
18
+ "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color",
19
+ "coverage": "yarn test --coverage",
20
+ "typescript": "tsc",
21
+ "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.extension.tsx' 'src/**/*.workspace.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js"
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/openmrs/openmrs-esm-patient-management.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/openmrs/openmrs-esm-patient-management/issues"
38
+ },
39
+ "dependencies": {
40
+ "@carbon/react": "~1.37.0",
41
+ "dexie": "^3.0.3",
42
+ "fuzzy": "^0.1.3",
43
+ "lodash-es": "^4.17.15"
44
+ },
45
+ "peerDependencies": {
46
+ "@openmrs/esm-framework": "5.x",
47
+ "react": "18.x",
48
+ "react-i18next": "11.x",
49
+ "react-router-dom": "6.x",
50
+ "swr": "2.x"
51
+ },
52
+ "devDependencies": {
53
+ "webpack": "^5.74.0"
54
+ },
55
+ "stableVersion": "7.0.1"
56
+ }
@@ -0,0 +1,271 @@
1
+ import React, { useState, useEffect, useMemo, useCallback } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import type { TFunction } from 'i18next';
5
+ import useSWR from 'swr';
6
+ import { Button, Checkbox, Pagination, Search, CheckboxSkeleton } from '@carbon/react';
7
+ import {
8
+ getDynamicOfflineDataEntries,
9
+ putDynamicOfflineData,
10
+ syncDynamicOfflineData,
11
+ showSnackbar,
12
+ toOmrsIsoString,
13
+ usePagination,
14
+ navigate,
15
+ useConfig,
16
+ } from '@openmrs/esm-framework';
17
+ import { addPatientToList, getAllPatientLists, getPatientListIdsForPatient } from '../api/api-remote';
18
+ import { type ConfigSchema } from '../config-schema';
19
+ import styles from './add-patient.scss';
20
+
21
+ interface AddPatientProps {
22
+ closeModal: () => void;
23
+ patientUuid: string;
24
+ }
25
+
26
+ interface AddablePatientListViewModel {
27
+ addPatient(): Promise<void>;
28
+ displayName: string;
29
+ checked?: boolean;
30
+ id: string;
31
+ }
32
+
33
+ const AddPatient: React.FC<AddPatientProps> = ({ closeModal, patientUuid }) => {
34
+ const { t } = useTranslation();
35
+ const [searchValue, setSearchValue] = useState('');
36
+ const [selected, setSelected] = useState<Array<string>>([]);
37
+ const { data, isLoading } = useAddablePatientLists(patientUuid);
38
+
39
+ const handleCreateNewList = () => {
40
+ navigate({
41
+ to: window.getOpenmrsSpaBase() + 'home/patient-lists?new_cohort=true',
42
+ });
43
+
44
+ closeModal();
45
+ };
46
+
47
+ const handleSelectionChanged = useCallback((patientListId: string, listSelected: boolean) => {
48
+ if (listSelected) {
49
+ setSelected((prev) => [...prev, patientListId]);
50
+ } else {
51
+ setSelected((prev) => prev.filter((x) => x !== patientListId));
52
+ }
53
+ }, []);
54
+
55
+ const handleSubmit = useCallback(() => {
56
+ for (const selectedId of selected) {
57
+ const patientList = data.find((list) => list.id === selectedId);
58
+ if (!patientList) {
59
+ continue;
60
+ }
61
+
62
+ patientList
63
+ .addPatient()
64
+ .then(() =>
65
+ showSnackbar({
66
+ title: t('successfullyAdded', 'Successfully added'),
67
+ kind: 'success',
68
+ isLowContrast: true,
69
+ subtitle: `${t('successAddPatientToList', 'Patient added to list')}: ${patientList.displayName}`,
70
+ }),
71
+ )
72
+ .catch(() =>
73
+ showSnackbar({
74
+ title: t('error', 'Error'),
75
+ kind: 'error',
76
+ subtitle: `${t('errorAddPatientToList', 'Patient not added to list')}: ${patientList.displayName}`,
77
+ }),
78
+ );
79
+ }
80
+
81
+ closeModal();
82
+ }, [data, selected, closeModal, t]);
83
+
84
+ const searchResults = useMemo(() => {
85
+ if (!data) {
86
+ return [];
87
+ }
88
+
89
+ if (searchValue?.trim().length > 0) {
90
+ const search = searchValue.toLowerCase();
91
+ return data.filter((patientList) => patientList.displayName.toLowerCase().includes(search));
92
+ }
93
+
94
+ return data;
95
+ }, [searchValue, data]);
96
+
97
+ const { results, goTo, currentPage, paginated } = usePagination(searchResults, 5);
98
+
99
+ useEffect(() => {
100
+ if (currentPage !== 1) {
101
+ goTo(1);
102
+ }
103
+ }, [searchValue]);
104
+
105
+ return (
106
+ <div className={styles.modalContent}>
107
+ <div className={styles.modalHeader}>
108
+ <h1 className={styles.productiveHeading03}>{t('addPatientToList', 'Add patient to list')}</h1>
109
+ <h3 className={styles.bodyLong01} style={{ margin: '1rem 0' }}>
110
+ {t('searchForAListToAddThisPatientTo', 'Search for a list to add this patient to.')}
111
+ </h3>
112
+ </div>
113
+ <div style={{ marginBottom: '0.875rem' }}>
114
+ <Search
115
+ style={{ backgroundColor: 'white', borderBottom: '1px solid #e0e0e0' }}
116
+ labelText={t('searchForList', 'Search for a list')}
117
+ placeholder="Filter list"
118
+ onChange={({ target }) => {
119
+ setSearchValue(target.value);
120
+ }}
121
+ value={searchValue}
122
+ />
123
+ </div>
124
+ <div className={styles.patientListList}>
125
+ <fieldset className="cds--fieldset">
126
+ <p className="cds--label">{t('patientLists', 'Patient lists')}</p>
127
+ {!isLoading && results ? (
128
+ results.length > 0 ? (
129
+ results.map((patientList) => (
130
+ <div key={patientList.id} className={styles.checkbox}>
131
+ <Checkbox
132
+ key={patientList.id}
133
+ onChange={(e) => handleSelectionChanged(patientList.id, e.target.checked)}
134
+ checked={patientList.checked || selected.includes(patientList.id)}
135
+ disabled={patientList.checked}
136
+ labelText={patientList.displayName}
137
+ id={patientList.id}
138
+ />
139
+ </div>
140
+ ))
141
+ ) : (
142
+ <p className={styles.bodyLong01}>{t('noPatientListFound', 'No patient list found')}</p>
143
+ )
144
+ ) : (
145
+ <>
146
+ <div className={styles.checkbox}>
147
+ <CheckboxSkeleton />
148
+ </div>
149
+ <div className={styles.checkbox}>
150
+ <CheckboxSkeleton />
151
+ </div>
152
+ <div className={styles.checkbox}>
153
+ <CheckboxSkeleton />
154
+ </div>
155
+ <div className={styles.checkbox}>
156
+ <CheckboxSkeleton />
157
+ </div>
158
+ <div className={styles.checkbox}>
159
+ <CheckboxSkeleton />
160
+ </div>
161
+ </>
162
+ )}
163
+ </fieldset>
164
+ </div>
165
+ {paginated && (
166
+ <div className={styles.paginationContainer}>
167
+ <span className={classNames(styles.itemsCountDisplay, styles.bodyLong01)}>
168
+ {results.length * currentPage} / {searchResults.length} {t('items', 'items')}
169
+ </span>
170
+ <Pagination
171
+ className={styles.pagination}
172
+ forwardText=""
173
+ backwardText=""
174
+ page={currentPage}
175
+ pageSize={5}
176
+ pageSizes={[5]}
177
+ totalItems={searchResults.length}
178
+ onChange={({ page }) => goTo(page)}
179
+ />
180
+ </div>
181
+ )}
182
+ <div className={styles.buttonSet}>
183
+ <Button kind="ghost" size="xl" onClick={handleCreateNewList}>
184
+ {t('createNewPatientList', 'Create new patient list')}
185
+ </Button>
186
+ <div>
187
+ <Button kind="secondary" size="xl" onClick={closeModal}>
188
+ {t('cancel', 'Cancel')}
189
+ </Button>
190
+ <Button onClick={handleSubmit} size="xl">
191
+ {t('addToList', 'Add to list')}
192
+ </Button>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ );
197
+ };
198
+
199
+ // This entire modal is a little bit special since it not only displays the "real" patient lists (i.e. data from
200
+ // the cohorts/backend), but also a fake patient list which doesn't really exist in the backend:
201
+ // The offline patient list.
202
+ // When a patient is added to the offline list, that patient should become available offline, i.e.
203
+ // a dynamic offline data entry must be created.
204
+ // This is why the following abstracts away the differences between the real and the fake patient lists.
205
+ // The component doesn't really care about which is which - the only thing that matters is that the
206
+ // data can be fetched and that there is an "add patient" function.
207
+
208
+ export function useAddablePatientLists(patientUuid: string) {
209
+ const { t } = useTranslation();
210
+ const config = useConfig() as ConfigSchema;
211
+ return useSWR(['addablePatientLists', patientUuid], async () => {
212
+ // Using Promise.allSettled instead of Promise.all here because some distros might not have the
213
+ // cohort module installed, leading to the real patient list call failing.
214
+ // In that case we still want to show fake lists and *not* error out here.
215
+ const [fakeLists, realLists] = await Promise.allSettled([
216
+ findFakePatientListsWithoutPatient(patientUuid, t),
217
+ findRealPatientListsWithoutPatient(patientUuid, config.myListCohortTypeUUID, config.systemListCohortTypeUUID),
218
+ ]);
219
+
220
+ return [
221
+ ...(fakeLists.status === 'fulfilled' ? fakeLists.value : []),
222
+ ...(realLists.status === 'fulfilled' ? realLists.value : []),
223
+ ];
224
+ });
225
+ }
226
+
227
+ async function findRealPatientListsWithoutPatient(
228
+ patientUuid: string,
229
+ myListCohortUUID,
230
+ systemListCohortType,
231
+ ): Promise<Array<AddablePatientListViewModel>> {
232
+ const [allLists, listsIdsOfThisPatient] = await Promise.all([
233
+ getAllPatientLists({}, myListCohortUUID, systemListCohortType),
234
+ getPatientListIdsForPatient(patientUuid),
235
+ ]);
236
+
237
+ return allLists.map((list) => ({
238
+ id: list.id,
239
+ displayName: list.display,
240
+ checked: listsIdsOfThisPatient.includes(list.id),
241
+ async addPatient() {
242
+ await addPatientToList({
243
+ cohort: list.id,
244
+ patient: patientUuid,
245
+ startDate: toOmrsIsoString(new Date()),
246
+ });
247
+ },
248
+ }));
249
+ }
250
+
251
+ async function findFakePatientListsWithoutPatient(
252
+ patientUuid: string,
253
+ t: TFunction,
254
+ ): Promise<Array<AddablePatientListViewModel>> {
255
+ const offlinePatients = await getDynamicOfflineDataEntries('patient');
256
+ const isPatientOnOfflineList = offlinePatients.some((x) => x.identifier === patientUuid);
257
+ return isPatientOnOfflineList
258
+ ? []
259
+ : [
260
+ {
261
+ id: 'fake-offline-patient-list',
262
+ displayName: t('offlinePatients', 'Offline patients'),
263
+ async addPatient() {
264
+ await putDynamicOfflineData('patient', patientUuid);
265
+ await syncDynamicOfflineData('patient', patientUuid);
266
+ },
267
+ },
268
+ ];
269
+ }
270
+
271
+ export default AddPatient;
@@ -0,0 +1,51 @@
1
+ @import '../style.scss';
2
+
3
+ .modalContent {
4
+ width: 100%;
5
+ background-color: #f4f4f4;
6
+ }
7
+
8
+ .modalHeader {
9
+ margin: 1rem;
10
+ }
11
+
12
+ .patientListList {
13
+ padding: 0 1rem;
14
+ }
15
+
16
+ .checkbox {
17
+ padding-bottom: 0.875rem;
18
+ }
19
+
20
+ .pagination {
21
+ width: 100%;
22
+ overflow: hidden;
23
+ }
24
+
25
+ .paginationContainer {
26
+ display: flex;
27
+ justify-content: space-between;
28
+ align-items: center;
29
+ position: relative;
30
+ }
31
+
32
+ .itemsCountDisplay {
33
+ position: absolute;
34
+ top: 0;
35
+ left: 1rem;
36
+ height: 3rem;
37
+ display: flex;
38
+ align-items: center;
39
+ color: #525252;
40
+ }
41
+
42
+ .pagination > div:first-child {
43
+ visibility: hidden;
44
+ width: 0;
45
+ }
46
+
47
+ .buttonSet {
48
+ display: flex;
49
+ justify-content: space-between;
50
+ align-items: flex-start;
51
+ }
@@ -0,0 +1,48 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { showModal } from '@openmrs/esm-framework';
4
+
5
+ function closeOverflowMenu() {
6
+ document.body.dispatchEvent(
7
+ new MouseEvent('mousedown', {
8
+ view: window,
9
+ bubbles: true,
10
+ cancelable: true,
11
+ }),
12
+ );
13
+ }
14
+
15
+ interface AddPastVisitOverflowMenuItemProps {
16
+ patientUuid: string;
17
+ }
18
+
19
+ const AddPatientToPatientListMenuItem: React.FC<AddPastVisitOverflowMenuItemProps> = ({ patientUuid }) => {
20
+ const { t } = useTranslation();
21
+ const openModal = useCallback(() => {
22
+ const dispose = showModal('add-patient-to-patient-list-modal', {
23
+ closeModal: () => dispose(),
24
+ patientUuid,
25
+ });
26
+ closeOverflowMenu();
27
+ }, [patientUuid]);
28
+
29
+ return (
30
+ <>
31
+ <li className="cds--overflow-menu-options__option">
32
+ <button
33
+ className="cds--overflow-menu-options__btn"
34
+ role="menuitem"
35
+ title={t('openPatientList', 'Add to list')}
36
+ data-floating-menu-primary-focus
37
+ onClick={openModal}
38
+ style={{
39
+ maxWidth: '100vw',
40
+ }}>
41
+ <span className="cds--overflow-menu-options__option-content">{t('openPatientList', 'Add to list')}</span>
42
+ </button>
43
+ </li>
44
+ </>
45
+ );
46
+ };
47
+
48
+ export default AddPatientToPatientListMenuItem;
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { showModal } from '@openmrs/esm-framework';
5
+ import AddPatientToPatientListMenuItem from './add-patient-to-patient-list-menu-item.component';
6
+
7
+ const mockedShowModal = showModal as jest.Mock;
8
+
9
+ jest.mock('@openmrs/esm-framework');
10
+
11
+ const patientUuid = '6baa7963-68ea-497e-b258-6fb82382bd07';
12
+
13
+ describe('AddPatientToPatientListMenuItem', () => {
14
+ it('renders the button with the correct title', () => {
15
+ render(<AddPatientToPatientListMenuItem patientUuid={patientUuid} />);
16
+ const button = screen.getByRole('menuitem');
17
+
18
+ expect(button).toBeInTheDocument();
19
+ expect(button).toHaveTextContent('Add to list');
20
+ expect(button).toHaveAttribute('title', 'Add to list');
21
+ });
22
+
23
+ it('should open the modal on button click', async () => {
24
+ const user = userEvent.setup();
25
+
26
+ render(<AddPatientToPatientListMenuItem patientUuid={patientUuid} />);
27
+ const button = screen.getByRole('menuitem');
28
+
29
+ await user.click(button);
30
+
31
+ expect(mockedShowModal).toHaveBeenCalledWith('add-patient-to-patient-list-modal', expect.any(Object));
32
+ });
33
+ });
@@ -0,0 +1,211 @@
1
+ import { type LoggedInUser, openmrsFetch, refetchCurrentUser, restBaseUrl, fhirBaseUrl } from '@openmrs/esm-framework';
2
+ import {
3
+ type AddPatientData,
4
+ type CohortResponse,
5
+ type NewCohortData,
6
+ type NewCohortDataPayload,
7
+ type OpenmrsCohort,
8
+ type OpenmrsCohortMember,
9
+ type OpenmrsCohortRef,
10
+ type PatientListFilter,
11
+ type PatientListMember,
12
+ PatientListType,
13
+ type PatientListUpdate,
14
+ } from './types';
15
+
16
+ export const cohortUrl = `${restBaseUrl}/cohortm`;
17
+
18
+ async function postData(url: string, data = {}, ac = new AbortController()) {
19
+ const response = await openmrsFetch(url, {
20
+ signal: ac.signal,
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ },
25
+ body: JSON.stringify(data),
26
+ });
27
+
28
+ return response.data;
29
+ }
30
+
31
+ async function deleteData(url: string, data = {}, ac = new AbortController()) {
32
+ const response = await openmrsFetch(url, {
33
+ signal: ac.signal,
34
+ method: 'DELETE',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ },
38
+ body: JSON.stringify(data),
39
+ });
40
+
41
+ return response.data;
42
+ }
43
+
44
+ export async function getAllPatientLists(
45
+ filter: PatientListFilter = {},
46
+ myListCohortTypeUUID,
47
+ systemListCohortTypeUUID,
48
+ ac = new AbortController(),
49
+ ) {
50
+ const custom = 'custom:(uuid,name,description,display,size,attributes,cohortType)';
51
+ const query: Array<[string, string]> = [['v', custom]];
52
+
53
+ if (filter.name !== undefined && filter.name !== '') {
54
+ query.push(['q', filter.name]);
55
+ }
56
+
57
+ if (filter.isStarred !== undefined) {
58
+ // TODO: correct this; it definitely is "attributes", but then we'd get back a 500 right now.
59
+ query.push(['attribute', `starred:${filter.isStarred}`]);
60
+ }
61
+
62
+ if (filter.type === PatientListType.USER) {
63
+ query.push(['cohortType', myListCohortTypeUUID]);
64
+ } else if (filter.type === PatientListType.SYSTEM) {
65
+ query.push(['cohortType', systemListCohortTypeUUID]);
66
+ }
67
+
68
+ const params = query.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&');
69
+ const {
70
+ data: { results, error },
71
+ } = await openmrsFetch<CohortResponse<OpenmrsCohort>>(`${cohortUrl}/cohort?${params}`, {
72
+ signal: ac.signal,
73
+ });
74
+
75
+ if (error) {
76
+ throw error;
77
+ }
78
+
79
+ return results.map((cohort) => ({
80
+ id: cohort.uuid,
81
+ display: cohort.name,
82
+ description: cohort.description,
83
+ type: cohort.cohortType?.display,
84
+ size: cohort.size,
85
+ isStarred: false, // TODO
86
+ }));
87
+ }
88
+
89
+ export function starPatientList(userUuid: string, userProperties: LoggedInUser['userProperties']) {
90
+ return openmrsFetch(`${restBaseUrl}/user/${userUuid}`, {
91
+ method: 'POST',
92
+ headers: {
93
+ 'content-type': 'application/json',
94
+ },
95
+ body: {
96
+ userProperties,
97
+ },
98
+ }).then(() => {
99
+ refetchCurrentUser();
100
+ });
101
+ }
102
+
103
+ export function updatePatientList(id: string, update: PatientListUpdate) {
104
+ // TODO: Support updating a full patient list, i.e. including the `isStarred` value.
105
+ // Basically implement the (missing) functionality which was previously declared as "TODO" here:
106
+ // https://github.com/openmrs/openmrs-esm-patient-management/blob/25ec687afd37c383a0dbd4d8be8b8e09c8c53129/packages/esm-patient-list-management-app/src/api/api.ts#L89
107
+ return Promise.resolve();
108
+ }
109
+
110
+ export async function getPatientListMembers(cohortUuid: string, ac = new AbortController()) {
111
+ const {
112
+ data: { results, error },
113
+ } = await openmrsFetch<CohortResponse<OpenmrsCohortMember>>(
114
+ `${cohortUrl}/cohortmember?cohort=${cohortUuid}&v=default`,
115
+ {
116
+ signal: ac.signal,
117
+ },
118
+ );
119
+
120
+ if (error) {
121
+ throw error;
122
+ }
123
+
124
+ const currentDate = new Date();
125
+ const searchQuery = results.map((p) => p.patient.uuid).join(',');
126
+
127
+ const result = await openmrsFetch(`${fhirBaseUrl}/Patient/_search?_id=${searchQuery}`, {
128
+ method: 'POST',
129
+ signal: ac.signal,
130
+ });
131
+
132
+ const patients: Array<PatientListMember> = result.data.entry.map((e) => e.resource);
133
+ const validPatients = patients.filter((patient) => {
134
+ if (!patient.endDate) {
135
+ return true;
136
+ }
137
+
138
+ const endDate = new Date(patient.endDate);
139
+ return endDate >= currentDate;
140
+ });
141
+
142
+ return validPatients;
143
+ }
144
+
145
+ export async function getPatientListIdsForPatient(patientUuid: string, ac = new AbortController()) {
146
+ const {
147
+ data: { results, error },
148
+ } = await openmrsFetch<CohortResponse<OpenmrsCohortRef>>(
149
+ `${cohortUrl}/cohortmember?patient=${patientUuid}&v=default`,
150
+ {
151
+ signal: ac.signal,
152
+ },
153
+ );
154
+
155
+ if (error) {
156
+ throw error;
157
+ }
158
+
159
+ return results.map((ref) => ref.cohort.uuid);
160
+ }
161
+
162
+ export async function addPatientToList(data: AddPatientData) {
163
+ return postData(`${cohortUrl}/cohortmember`, data);
164
+ }
165
+
166
+ export async function removePatientFromList(cohortMembershipUuid: string) {
167
+ return postData(`${cohortUrl}/cohortmember/${cohortMembershipUuid}`, {
168
+ endDate: new Date(),
169
+ });
170
+ }
171
+
172
+ export async function createPatientList(cohort: NewCohortDataPayload, ac = new AbortController()) {
173
+ return postData(
174
+ `${cohortUrl}/cohort/`,
175
+ {
176
+ ...cohort,
177
+ startDate: new Date(),
178
+ groupCohort: false,
179
+ definitionHandlerClassname: 'org.openmrs.module.cohort.definition.handler.DefaultCohortDefinitionHandler',
180
+ },
181
+ ac,
182
+ );
183
+ }
184
+
185
+ export async function editPatientList(cohortUuid: string, cohort: NewCohortData, ac = new AbortController()) {
186
+ return postData(`${cohortUrl}/cohort/${cohortUuid}`, cohort, ac);
187
+ }
188
+
189
+ export async function deletePatientList(cohortUuid: string, ac = new AbortController()) {
190
+ return deleteData(
191
+ `${cohortUrl}/cohort/${cohortUuid}`,
192
+ {
193
+ voidReason: '',
194
+ },
195
+ ac,
196
+ );
197
+ }
198
+
199
+ export async function getPatientListName(patientListUuid: string) {
200
+ const abortController = new AbortController();
201
+
202
+ try {
203
+ const url = `${cohortUrl}/cohort/${patientListUuid}?`;
204
+ const { data } = await openmrsFetch<OpenmrsCohort>(url, {
205
+ signal: abortController.signal,
206
+ });
207
+ return data?.name;
208
+ } catch (error) {
209
+ console.error('Error resolving patient list name: ', error);
210
+ }
211
+ }