@ampath/esm-dispensing-app 1.10.0-next.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.
Files changed (277) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +80 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +12 -0
  7. package/.tx/config +11 -0
  8. package/.yarn/versions/1c40b9b6.yml +0 -0
  9. package/.yarn/versions/ff162597.yml +0 -0
  10. package/LICENSE +401 -0
  11. package/README.md +124 -0
  12. package/__mocks__/react-i18next.js +51 -0
  13. package/dist/1043.js +1 -0
  14. package/dist/1043.js.map +1 -0
  15. package/dist/1119.js +1 -0
  16. package/dist/1197.js +1 -0
  17. package/dist/2146.js +1 -0
  18. package/dist/2177.js +2 -0
  19. package/dist/2177.js.LICENSE.txt +9 -0
  20. package/dist/2177.js.map +1 -0
  21. package/dist/2690.js +1 -0
  22. package/dist/2890.js +2 -0
  23. package/dist/2890.js.LICENSE.txt +14 -0
  24. package/dist/2890.js.map +1 -0
  25. package/dist/2898.js +1 -0
  26. package/dist/2898.js.map +1 -0
  27. package/dist/3041.js +1 -0
  28. package/dist/3041.js.map +1 -0
  29. package/dist/3099.js +1 -0
  30. package/dist/3184.js +2 -0
  31. package/dist/3184.js.LICENSE.txt +14 -0
  32. package/dist/3184.js.map +1 -0
  33. package/dist/3568.js +1 -0
  34. package/dist/3568.js.map +1 -0
  35. package/dist/3584.js +1 -0
  36. package/dist/4055.js +1 -0
  37. package/dist/4099.js +1 -0
  38. package/dist/4099.js.map +1 -0
  39. package/dist/4132.js +1 -0
  40. package/dist/4225.js +1 -0
  41. package/dist/4225.js.map +1 -0
  42. package/dist/4300.js +1 -0
  43. package/dist/4335.js +1 -0
  44. package/dist/4353.js +1 -0
  45. package/dist/4353.js.map +1 -0
  46. package/dist/439.js +1 -0
  47. package/dist/4618.js +1 -0
  48. package/dist/4652.js +1 -0
  49. package/dist/4944.js +1 -0
  50. package/dist/5173.js +1 -0
  51. package/dist/5241.js +1 -0
  52. package/dist/5422.js +1 -0
  53. package/dist/5422.js.map +1 -0
  54. package/dist/5442.js +1 -0
  55. package/dist/5661.js +1 -0
  56. package/dist/5897.js +1 -0
  57. package/dist/5897.js.map +1 -0
  58. package/dist/6022.js +1 -0
  59. package/dist/609.js +1 -0
  60. package/dist/609.js.map +1 -0
  61. package/dist/6468.js +1 -0
  62. package/dist/6540.js +2 -0
  63. package/dist/6540.js.LICENSE.txt +9 -0
  64. package/dist/6540.js.map +1 -0
  65. package/dist/6589.js +1 -0
  66. package/dist/6606.js +1 -0
  67. package/dist/6606.js.map +1 -0
  68. package/dist/6679.js +1 -0
  69. package/dist/6825.js +1 -0
  70. package/dist/6825.js.map +1 -0
  71. package/dist/6840.js +1 -0
  72. package/dist/6841.js +1 -0
  73. package/dist/6841.js.map +1 -0
  74. package/dist/6859.js +1 -0
  75. package/dist/7097.js +1 -0
  76. package/dist/7159.js +1 -0
  77. package/dist/723.js +1 -0
  78. package/dist/7240.js +1 -0
  79. package/dist/7240.js.map +1 -0
  80. package/dist/7255.js +1 -0
  81. package/dist/7255.js.map +1 -0
  82. package/dist/7617.js +1 -0
  83. package/dist/795.js +1 -0
  84. package/dist/8163.js +1 -0
  85. package/dist/8349.js +1 -0
  86. package/dist/8371.js +1 -0
  87. package/dist/8569.js +2 -0
  88. package/dist/8569.js.LICENSE.txt +41 -0
  89. package/dist/8569.js.map +1 -0
  90. package/dist/8600.js +1 -0
  91. package/dist/8600.js.map +1 -0
  92. package/dist/8618.js +1 -0
  93. package/dist/8885.js +1 -0
  94. package/dist/8885.js.map +1 -0
  95. package/dist/890.js +1 -0
  96. package/dist/9214.js +1 -0
  97. package/dist/9538.js +1 -0
  98. package/dist/9569.js +1 -0
  99. package/dist/961.js +2 -0
  100. package/dist/961.js.LICENSE.txt +19 -0
  101. package/dist/961.js.map +1 -0
  102. package/dist/963.js +1 -0
  103. package/dist/963.js.map +1 -0
  104. package/dist/986.js +1 -0
  105. package/dist/9879.js +1 -0
  106. package/dist/9895.js +1 -0
  107. package/dist/9900.js +1 -0
  108. package/dist/9913.js +1 -0
  109. package/dist/main.js +2 -0
  110. package/dist/main.js.LICENSE.txt +51 -0
  111. package/dist/main.js.map +1 -0
  112. package/dist/openmrs-esm-dispensing-app.js +1 -0
  113. package/dist/openmrs-esm-dispensing-app.js.buildmanifest.json +1645 -0
  114. package/dist/openmrs-esm-dispensing-app.js.map +1 -0
  115. package/dist/routes.json +1 -0
  116. package/e2e/README.md +119 -0
  117. package/e2e/commands/drug-order-operations.ts +43 -0
  118. package/e2e/commands/encounter-operations.ts +60 -0
  119. package/e2e/commands/index.ts +5 -0
  120. package/e2e/commands/patient-operations.ts +109 -0
  121. package/e2e/commands/provider-operations.ts +9 -0
  122. package/e2e/commands/types/index.ts +157 -0
  123. package/e2e/commands/visit-operations.ts +38 -0
  124. package/e2e/core/global-setup.ts +32 -0
  125. package/e2e/core/index.ts +1 -0
  126. package/e2e/core/test.ts +31 -0
  127. package/e2e/fixtures/api.ts +27 -0
  128. package/e2e/fixtures/index.ts +1 -0
  129. package/e2e/pages/dispensing-page.ts +9 -0
  130. package/e2e/pages/index.ts +1 -0
  131. package/e2e/specs/active-prescriptions.spec.ts +72 -0
  132. package/e2e/specs/close-prescription.spec.ts +71 -0
  133. package/e2e/specs/dispense-medication.spec.ts +71 -0
  134. package/e2e/specs/pause-prescription.spec.ts +72 -0
  135. package/e2e/support/github/Dockerfile +34 -0
  136. package/e2e/support/github/docker-compose.yml +24 -0
  137. package/e2e/support/github/run-e2e-docker-env.sh +42 -0
  138. package/e2e/types/index.ts +157 -0
  139. package/example.env +7 -0
  140. package/jest.config.js +24 -0
  141. package/package.json +110 -0
  142. package/playwright.config.ts +36 -0
  143. package/prettier.config.js +8 -0
  144. package/src/components/action-buttons.component.tsx +87 -0
  145. package/src/components/action-buttons.scss +16 -0
  146. package/src/components/action-buttons.test.tsx +217 -0
  147. package/src/components/medication-card.component.tsx +37 -0
  148. package/src/components/medication-card.scss +20 -0
  149. package/src/components/medication-card.test.tsx +36 -0
  150. package/src/components/medication-dispense-review.scss +108 -0
  151. package/src/components/medication-event.component.tsx +96 -0
  152. package/src/components/medication-event.scss +44 -0
  153. package/src/components/medication-event.test.tsx +212 -0
  154. package/src/components/prescription-actions/close-action-button.component.tsx +50 -0
  155. package/src/components/prescription-actions/dispense-action-button.component.tsx +57 -0
  156. package/src/components/prescription-actions/pause-action-button.component.tsx +49 -0
  157. package/src/conditions/conditions.component.tsx +118 -0
  158. package/src/conditions/conditions.resource.ts +100 -0
  159. package/src/conditions/conditions.scss +26 -0
  160. package/src/conditions/conditions.test.tsx +200 -0
  161. package/src/config-schema.ts +192 -0
  162. package/src/constants.ts +22 -0
  163. package/src/dashboard/dispensing-dashboard-link.component.tsx +36 -0
  164. package/src/dashboard/dispensing-dashboard.component.tsx +36 -0
  165. package/src/declarations.d.ts +2 -0
  166. package/src/diagnoses/diagnoses.component.tsx +111 -0
  167. package/src/diagnoses/diagnoses.resource.ts +30 -0
  168. package/src/diagnoses/diagnoses.scss +31 -0
  169. package/src/dispensing-link.component.tsx +9 -0
  170. package/src/dispensing-tiles/dispensing-tile.component.tsx +42 -0
  171. package/src/dispensing-tiles/dispensing-tile.scss +43 -0
  172. package/src/dispensing-tiles/dispensing-tiles.component.tsx +39 -0
  173. package/src/dispensing-tiles/dispensing-tiles.resource.tsx +30 -0
  174. package/src/dispensing-tiles/dispensing-tiles.scss +11 -0
  175. package/src/dispensing.component.tsx +31 -0
  176. package/src/dispensing.scss +5 -0
  177. package/src/dispensing.test.tsx +9 -0
  178. package/src/fill-prescription/fill-prescription-button.component.tsx +103 -0
  179. package/src/fill-prescription/fill-prescription-button.scss +8 -0
  180. package/src/fill-prescription/on-prescription-filled.modal.tsx +140 -0
  181. package/src/fill-prescription/on-prescription-filled.scss +7 -0
  182. package/src/forms/close-dispense-form.workspace.tsx +194 -0
  183. package/src/forms/dispense-form.workspace.test.tsx +334 -0
  184. package/src/forms/dispense-form.workspace.tsx +324 -0
  185. package/src/forms/forms.scss +152 -0
  186. package/src/forms/medication-dispense-review.component.tsx +649 -0
  187. package/src/forms/medication-dispense-review.test.tsx +158 -0
  188. package/src/forms/pause-dispense-form.workspace.tsx +196 -0
  189. package/src/forms/stock-dispense/stock-dispense.component.tsx +126 -0
  190. package/src/forms/stock-dispense/stock.resource.tsx +67 -0
  191. package/src/history/delete-confirm.modal.tsx +35 -0
  192. package/src/history/history-and-comments.component.tsx +338 -0
  193. package/src/history/history-and-comments.scss +54 -0
  194. package/src/index.ts +57 -0
  195. package/src/location/location.resource.test.tsx +108 -0
  196. package/src/location/location.resource.tsx +32 -0
  197. package/src/medication/medication.resource.test.tsx +156 -0
  198. package/src/medication/medication.resource.tsx +45 -0
  199. package/src/medication-dispense/medication-dispense.resource.test.tsx +243 -0
  200. package/src/medication-dispense/medication-dispense.resource.tsx +178 -0
  201. package/src/medication-request/medication-request.resource.test.tsx +1333 -0
  202. package/src/medication-request/medication-request.resource.tsx +257 -0
  203. package/src/patient/patient-info-cell.component.tsx +26 -0
  204. package/src/patient/patient.resources.ts +14 -0
  205. package/src/pharmacy-header/pharmacy-header.component.tsx +35 -0
  206. package/src/pharmacy-header/pharmacy-header.scss +55 -0
  207. package/src/pharmacy-header/pharmacy-illustration.component.tsx +30 -0
  208. package/src/prescriptions/patient-search-tab-panel.component.tsx +58 -0
  209. package/src/prescriptions/patient-search-tab-panel.scss +26 -0
  210. package/src/prescriptions/prescription-actions.component.tsx +24 -0
  211. package/src/prescriptions/prescription-actions.scss +14 -0
  212. package/src/prescriptions/prescription-details.component.tsx +152 -0
  213. package/src/prescriptions/prescription-details.scss +87 -0
  214. package/src/prescriptions/prescription-details.test.tsx +267 -0
  215. package/src/prescriptions/prescription-expanded.component.tsx +58 -0
  216. package/src/prescriptions/prescription-expanded.scss +56 -0
  217. package/src/prescriptions/prescription-tab-lists.component.tsx +70 -0
  218. package/src/prescriptions/prescription-tab-panel.component.tsx +83 -0
  219. package/src/prescriptions/prescriptions-table.component.tsx +189 -0
  220. package/src/prescriptions/prescriptions.scss +152 -0
  221. package/src/print-prescription/prescription-print-action.component.tsx +30 -0
  222. package/src/print-prescription/prescription-print-preview.modal.tsx +92 -0
  223. package/src/print-prescription/prescription-printout.component.tsx +154 -0
  224. package/src/print-prescription/print-prescription.scss +75 -0
  225. package/src/print-prescription/printable-prescriptions.component.tsx +57 -0
  226. package/src/routes.json +137 -0
  227. package/src/types.ts +530 -0
  228. package/src/utils.test.ts +2947 -0
  229. package/src/utils.ts +637 -0
  230. package/tools/i18next-parser.config.js +89 -0
  231. package/tools/setup-tests.ts +8 -0
  232. package/tools/update-openmrs-deps.mjs +42 -0
  233. package/translations/am.json +133 -0
  234. package/translations/ar.json +133 -0
  235. package/translations/ar_SY.json +133 -0
  236. package/translations/bn.json +133 -0
  237. package/translations/cs.json +133 -0
  238. package/translations/de.json +133 -0
  239. package/translations/en.json +133 -0
  240. package/translations/en_US.json +133 -0
  241. package/translations/es.json +133 -0
  242. package/translations/es_MX.json +133 -0
  243. package/translations/fr.json +133 -0
  244. package/translations/he.json +133 -0
  245. package/translations/hi.json +133 -0
  246. package/translations/hi_IN.json +133 -0
  247. package/translations/id.json +133 -0
  248. package/translations/it.json +133 -0
  249. package/translations/ka.json +133 -0
  250. package/translations/km.json +133 -0
  251. package/translations/ku.json +133 -0
  252. package/translations/ky.json +133 -0
  253. package/translations/lg.json +133 -0
  254. package/translations/ne.json +133 -0
  255. package/translations/pl.json +133 -0
  256. package/translations/pt.json +133 -0
  257. package/translations/pt_BR.json +133 -0
  258. package/translations/qu.json +133 -0
  259. package/translations/ro_RO.json +133 -0
  260. package/translations/ru_RU.json +133 -0
  261. package/translations/si.json +133 -0
  262. package/translations/sq.json +133 -0
  263. package/translations/sw.json +133 -0
  264. package/translations/sw_KE.json +133 -0
  265. package/translations/tr.json +133 -0
  266. package/translations/tr_TR.json +133 -0
  267. package/translations/uk.json +133 -0
  268. package/translations/uz.json +133 -0
  269. package/translations/uz@Latn.json +133 -0
  270. package/translations/uz_UZ.json +133 -0
  271. package/translations/vi.json +133 -0
  272. package/translations/zh.json +133 -0
  273. package/translations/zh_CN.json +133 -0
  274. package/translations/zh_TW.json +133 -0
  275. package/tsconfig.json +23 -0
  276. package/turbo.json +41 -0
  277. package/webpack.config.js +1 -0
@@ -0,0 +1,158 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { useConfig, useSession } from '@openmrs/esm-framework';
4
+ import { type MedicationDispense, MedicationDispenseStatus } from '../types';
5
+ import MedicationDispenseReview from './medication-dispense-review.component';
6
+ import { useProviders } from '../medication-dispense/medication-dispense.resource';
7
+
8
+ jest.mock('../medication-dispense/medication-dispense.resource', () => ({
9
+ ...jest.requireActual('../medication-dispense/medication-dispense.resource'),
10
+ useProviders: jest.fn(),
11
+ }));
12
+
13
+ const mockUseConfig = jest.mocked(useConfig);
14
+ const mockUseProviders = jest.mocked(useProviders);
15
+ const mockUseSession = jest.mocked(useSession);
16
+
17
+ beforeEach(() => {
18
+ mockUseConfig.mockReturnValue({
19
+ dispenseBehavior: {
20
+ allowModifyingPrescription: false,
21
+ restrictTotalQuantityDispensed: false,
22
+ },
23
+ valueSets: {
24
+ substitutionType: { uuid: '123' },
25
+ substitutionReason: { uuid: 'abc' },
26
+ },
27
+ });
28
+ mockUseProviders.mockReturnValue([]);
29
+ mockUseSession.mockReturnValue({
30
+ currentProvider: {
31
+ uuid: 'user-uuid-123',
32
+ identifier: 'user-identifier-123',
33
+ },
34
+ } as any);
35
+ });
36
+
37
+ describe('Medication Dispense Review Component tests', () => {
38
+ test('component should render medication dispense review', () => {
39
+ const medicationDispense: MedicationDispense = {
40
+ resourceType: 'MedicationDispense',
41
+ id: 'ff2fa24c-b0d4-457c-bbdf-7d6512b8b746',
42
+ type: {
43
+ coding: [
44
+ {
45
+ code: '123',
46
+ },
47
+ ],
48
+ },
49
+ meta: {
50
+ lastUpdated: '2023-01-10T10:52:36.000-05:00',
51
+ },
52
+ status: MedicationDispenseStatus.completed,
53
+ medicationReference: {
54
+ reference: 'Medication/78f96684-dfbe-11e9-8a34-2a2ae2dbcce4',
55
+ type: 'Medication',
56
+ display: 'Lamivudine (3TC), Oral solution, 10mg/mL, 240mL bottle',
57
+ },
58
+ subject: {
59
+ reference: 'Patient/b36dbb26-8309-4d51-80ed-07bbb63ab928',
60
+ type: 'Patient',
61
+ display: 'Test, Fiona (ZL EMR ID: Y480K4)',
62
+ },
63
+ performer: [
64
+ {
65
+ actor: {
66
+ reference: 'Practitioner/6558dfef-bfe1-488e-b048-ed286aded924',
67
+ type: 'Practitioner',
68
+ identifier: {
69
+ value: 'LADAL',
70
+ },
71
+ display: 'Dylan, Bob (Identifier: LADAL)',
72
+ },
73
+ },
74
+ ],
75
+ location: {
76
+ reference: 'Location/47521fa1-ac7f-4fdb-b167-a86fe223a3d9',
77
+ type: 'Location',
78
+ display: 'Klinik MNT',
79
+ },
80
+ authorizingPrescription: [
81
+ {
82
+ reference: 'MedicationRequest/9e416e5f-a435-433c-99d3-8b2fd1e8a421',
83
+ type: 'MedicationRequest',
84
+ },
85
+ ],
86
+ quantity: {
87
+ value: 30.0,
88
+ unit: 'Tablet',
89
+ system: 'http://snomed.info/sct',
90
+ code: '385055001',
91
+ },
92
+ whenPrepared: '2023-01-10T10:52:22-05:00',
93
+ whenHandedOver: '2023-01-10T10:52:22-05:00',
94
+ dosageInstruction: [
95
+ {
96
+ timing: {
97
+ code: {
98
+ coding: [
99
+ {
100
+ code: '023bee9c-2dbb-483c-8401-46f0ccf6b333',
101
+ display: 'ON (night)',
102
+ },
103
+ ],
104
+ text: 'ON (night)',
105
+ },
106
+ },
107
+ asNeededBoolean: false,
108
+ route: {
109
+ coding: [
110
+ {
111
+ code: '46aaaca8-1f21-410a-aac9-67bfcc1fd577',
112
+ display: 'Oral',
113
+ },
114
+ {
115
+ system: 'https://openconceptlab.org/orgs/CIEL/sources/CIEL',
116
+ code: '160240',
117
+ display: 'Oral',
118
+ },
119
+ {
120
+ system: 'http://snomed.info/sct',
121
+ code: '26643006',
122
+ display: 'Oral',
123
+ },
124
+ ],
125
+ text: 'Oral',
126
+ },
127
+ doseAndRate: [
128
+ {
129
+ doseQuantity: {
130
+ value: 1.0,
131
+ unit: 'Tablet',
132
+ system: 'http://snomed.info/sct',
133
+ code: '385055001',
134
+ },
135
+ },
136
+ ],
137
+ },
138
+ ],
139
+ substitution: {
140
+ wasSubstituted: false,
141
+ },
142
+ };
143
+
144
+ const mockUpdate = jest.fn();
145
+ render(
146
+ <MedicationDispenseReview
147
+ medicationDispense={medicationDispense}
148
+ updateMedicationDispense={mockUpdate}
149
+ isFreeTextDosage={false}
150
+ setIsFreeTextDosage={() => {}}
151
+ quantityRemaining={30}
152
+ quantityDispensed={30}
153
+ />,
154
+ );
155
+
156
+ // TODO test expected views and various interactions
157
+ });
158
+ });
@@ -0,0 +1,196 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, ComboBox, Form, InlineLoading } from '@carbon/react';
4
+ import {
5
+ ExtensionSlot,
6
+ getCoreTranslation,
7
+ ResponsiveWrapper,
8
+ showSnackbar,
9
+ useConfig,
10
+ usePatient,
11
+ Workspace2,
12
+ type Workspace2DefinitionProps,
13
+ } from '@openmrs/esm-framework';
14
+ import { saveMedicationDispense, useReasonForPauseValueSet } from '../medication-dispense/medication-dispense.resource';
15
+ import { updateMedicationRequestFulfillerStatus } from '../medication-request/medication-request.resource';
16
+ import { getUuidFromReference, markEncounterAsStale, revalidate } from '../utils';
17
+ import { type MedicationDispense, MedicationDispenseStatus, MedicationRequestFulfillerStatus } from '../types';
18
+ import { type PharmacyConfig } from '../config-schema';
19
+ import styles from './forms.scss';
20
+
21
+ type PauseDispenseFormProps = {
22
+ medicationDispense: MedicationDispense;
23
+ mode: 'enter' | 'edit';
24
+ patientUuid?: string;
25
+ encounterUuid: string;
26
+ customWorkspaceTitle?: string;
27
+ };
28
+
29
+ const PauseDispenseForm: React.FC<Workspace2DefinitionProps<PauseDispenseFormProps, {}, {}>> = ({
30
+ workspaceProps: { medicationDispense, mode, patientUuid, encounterUuid, customWorkspaceTitle },
31
+ closeWorkspace,
32
+ }) => {
33
+ const { t } = useTranslation();
34
+ const config = useConfig<PharmacyConfig>();
35
+ const { patient, isLoading } = usePatient(patientUuid);
36
+
37
+ // Keep track of medication dispense payload
38
+ const [medicationDispensePayload, setMedicationDispensePayload] = useState<MedicationDispense>();
39
+
40
+ // whether or not the form is valid and ready to submit
41
+ const [isValid, setIsValid] = useState(false);
42
+
43
+ // to prevent duplicate submits
44
+ const [isSubmitting, setIsSubmitting] = useState(false);
45
+ const [reasonsForPause, setReasonsForPause] = useState([]);
46
+ const { reasonForPauseValueSet } = useReasonForPauseValueSet(config.valueSets.reasonForPause.uuid);
47
+
48
+ useEffect(() => {
49
+ const reasonForPauseOptions = [];
50
+
51
+ if (reasonForPauseValueSet?.compose?.include) {
52
+ const uuidValueSet = reasonForPauseValueSet.compose.include.find((include) => !include.system);
53
+ if (uuidValueSet) {
54
+ uuidValueSet.concept?.forEach((concept) =>
55
+ reasonForPauseOptions.push({
56
+ id: concept.code,
57
+ text: concept.display,
58
+ }),
59
+ );
60
+ reasonForPauseOptions.sort((a, b) => a.text.localeCompare(b.text));
61
+ }
62
+ }
63
+ setReasonsForPause(reasonForPauseOptions);
64
+ }, [reasonForPauseValueSet]);
65
+
66
+ const handleSubmit = () => {
67
+ if (!isSubmitting) {
68
+ setIsSubmitting(true);
69
+ const abortController = new AbortController();
70
+ markEncounterAsStale(encounterUuid);
71
+ saveMedicationDispense(medicationDispensePayload, MedicationDispenseStatus.on_hold, abortController)
72
+ .then((response) => {
73
+ // only update request status when added a new dispense event, not updating
74
+ if (response.ok && !medicationDispense.id) {
75
+ return updateMedicationRequestFulfillerStatus(
76
+ getUuidFromReference(
77
+ medicationDispensePayload.authorizingPrescription[0].reference, // assumes authorizing prescription exist
78
+ ),
79
+ MedicationRequestFulfillerStatus.on_hold,
80
+ );
81
+ } else {
82
+ return response;
83
+ }
84
+ })
85
+ .then((response) => {
86
+ if (response.ok) {
87
+ revalidate(encounterUuid);
88
+ showSnackbar({
89
+ kind: 'success',
90
+ title: t(
91
+ mode === 'enter' ? 'medicationDispensePaused' : 'medicationDispenseUpdated',
92
+ mode === 'enter' ? 'Medication dispense paused.' : 'Dispense record successfully updated.',
93
+ ),
94
+ });
95
+ closeWorkspace({ discardUnsavedChanges: true });
96
+ }
97
+ })
98
+ .catch((error) => {
99
+ showSnackbar({
100
+ title: t(
101
+ mode === 'enter' ? 'medicationDispensePauseError' : 'medicationDispenseUpdatedError',
102
+ mode === 'enter' ? 'Error pausing medication dispense.' : 'Error updating dispense record',
103
+ ),
104
+ kind: 'error',
105
+ subtitle: error?.message,
106
+ });
107
+ setIsSubmitting(false);
108
+ });
109
+ }
110
+ };
111
+
112
+ const checkIsValid = () => {
113
+ if (medicationDispensePayload && medicationDispensePayload.statusReasonCodeableConcept?.coding[0].code) {
114
+ setIsValid(true);
115
+ } else {
116
+ setIsValid(false);
117
+ }
118
+ };
119
+
120
+ // initialize the internal dispense payload with the dispenses passed in as props
121
+ useEffect(() => setMedicationDispensePayload(medicationDispense), [medicationDispense]);
122
+
123
+ // check is valid on any changes
124
+ useEffect(checkIsValid, [medicationDispensePayload]);
125
+
126
+ const bannerState = useMemo(() => {
127
+ if (patient) {
128
+ return {
129
+ patient,
130
+ patientUuid,
131
+ hideActionsOverflow: true,
132
+ };
133
+ }
134
+ }, [patient, patientUuid]);
135
+
136
+ return (
137
+ <Workspace2 title={customWorkspaceTitle ?? t('pausePrescription', 'Pause prescription')}>
138
+ <Form className={styles.formWrapper}>
139
+ <div>
140
+ {isLoading && (
141
+ <InlineLoading
142
+ className={styles.bannerLoading}
143
+ iconDescription="Loading"
144
+ description="Loading banner"
145
+ status="active"
146
+ />
147
+ )}
148
+ {patient && <ExtensionSlot name="patient-header-slot" state={bannerState} />}
149
+ <section className={styles.formGroup}>
150
+ <ResponsiveWrapper>
151
+ <ComboBox
152
+ id="reasonForPause"
153
+ items={reasonsForPause}
154
+ titleText={t('reasonForPause', 'Reason for pause')}
155
+ itemToString={(item) => item?.text}
156
+ initialSelectedItem={{
157
+ id: medicationDispense.statusReasonCodeableConcept?.coding[0]?.code,
158
+ text: medicationDispense.statusReasonCodeableConcept?.text,
159
+ }}
160
+ onChange={({ selectedItem }) => {
161
+ setMedicationDispensePayload({
162
+ ...medicationDispensePayload,
163
+ statusReasonCodeableConcept: {
164
+ coding: [
165
+ {
166
+ code: selectedItem?.id,
167
+ },
168
+ ],
169
+ },
170
+ });
171
+ }}
172
+ />
173
+ </ResponsiveWrapper>
174
+ </section>
175
+ </div>
176
+ <section className={styles.buttonGroup}>
177
+ <Button
178
+ disabled={isSubmitting}
179
+ onClick={() => {
180
+ closeWorkspace();
181
+ }}
182
+ kind="secondary">
183
+ {getCoreTranslation('cancel', 'Cancel')}
184
+ </Button>
185
+ <Button disabled={!isValid || isSubmitting} onClick={handleSubmit}>
186
+ {/* t('pause', 'Pause')
187
+ t('saveChanges', 'Save changes') */}
188
+ {t(mode === 'enter' ? 'pause' : 'saveChanges', mode === 'enter' ? 'Pause' : 'Save changes')}
189
+ </Button>
190
+ </section>
191
+ </Form>
192
+ </Workspace2>
193
+ );
194
+ };
195
+
196
+ export default PauseDispenseForm;
@@ -0,0 +1,126 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ComboBox, InlineLoading, InlineNotification, Layer } from '@carbon/react';
4
+ import { formatDate, useConfig } from '@openmrs/esm-framework';
5
+ import { type MedicationDispense, type InventoryItem } from '../../types';
6
+ import { type PharmacyConfig } from '../../config-schema';
7
+ import { useDispenseStock } from './stock.resource';
8
+
9
+ type StockDispenseProps = {
10
+ medicationDispense: MedicationDispense;
11
+ updateInventoryItem: (inventoryItem: InventoryItem) => void;
12
+ inventoryItem: InventoryItem;
13
+ };
14
+
15
+ const StockDispense: React.FC<StockDispenseProps> = ({ medicationDispense, updateInventoryItem }) => {
16
+ const { t } = useTranslation();
17
+ const config = useConfig<PharmacyConfig>();
18
+
19
+ const drugUuid = medicationDispense?.medicationReference?.reference?.split('/')[1];
20
+ const { inventoryItems, error, isLoading } = useDispenseStock(drugUuid);
21
+ const validInventoryItems = inventoryItems
22
+ .filter((item) => isValidBatch(medicationDispense, item))
23
+ .sort((a, b) => new Date(a.expiration).getTime() - new Date(b.expiration).getTime());
24
+
25
+ function parseDate(dateString) {
26
+ return new Date(dateString);
27
+ }
28
+
29
+ //check whether the drug will expire before the medication period ends
30
+ function isValidBatch(medicationToDispense, inventoryItem) {
31
+ if (typeof config !== 'undefined' && !config.validateBatch) {
32
+ return true;
33
+ }
34
+ if (medicationToDispense?.dosageInstruction && medicationToDispense?.dosageInstruction.length > 0) {
35
+ return medicationToDispense.dosageInstruction.some((instruction) => {
36
+ if (
37
+ instruction.timing?.repeat?.duration &&
38
+ instruction.timing?.repeat?.durationUnit &&
39
+ inventoryItem.quantity > 0
40
+ ) {
41
+ const durationUnit = instruction.timing.repeat.durationUnit;
42
+ const durationValue = instruction.timing.repeat.duration;
43
+ const lastMedicationDate = new Date();
44
+
45
+ switch (durationUnit) {
46
+ case 's':
47
+ lastMedicationDate.setSeconds(lastMedicationDate.getSeconds() + durationValue);
48
+ break;
49
+ case 'min':
50
+ lastMedicationDate.setMinutes(lastMedicationDate.getMinutes() + durationValue);
51
+ break;
52
+ case 'h':
53
+ lastMedicationDate.setHours(lastMedicationDate.getHours() + durationValue);
54
+ break;
55
+ case 'd':
56
+ lastMedicationDate.setDate(lastMedicationDate.getDate() + durationValue);
57
+ break;
58
+ case 'wk':
59
+ lastMedicationDate.setDate(lastMedicationDate.getDate() + durationValue * 7);
60
+ break;
61
+ case 'mo':
62
+ lastMedicationDate.setMonth(lastMedicationDate.getMonth() + durationValue);
63
+ break;
64
+ case 'y':
65
+ lastMedicationDate.setFullYear(lastMedicationDate.getFullYear() + durationValue);
66
+ break;
67
+ default:
68
+ return false;
69
+ }
70
+
71
+ const expiryDate = parseDate(inventoryItem.expiration);
72
+ return expiryDate > lastMedicationDate;
73
+ }
74
+ return false;
75
+ });
76
+ }
77
+ return false;
78
+ }
79
+
80
+ const toStockDispense = (inventoryItems) => {
81
+ return t(
82
+ 'stockDispenseDetails',
83
+ 'Batch: {{batchNumber}} - Quantity: {{quantity}} ({{quantityUoM}}) - Expiry: {{expiration}}',
84
+ {
85
+ batchNumber: inventoryItems.batchNumber,
86
+ quantity: Math.floor(inventoryItems.quantity),
87
+ quantityUoM: inventoryItems.quantityUoM,
88
+ expiration: formatDate(new Date(inventoryItems.expiration)),
89
+ },
90
+ );
91
+ };
92
+
93
+ if (error) {
94
+ return (
95
+ <InlineNotification
96
+ aria-label="closes notification"
97
+ kind="error"
98
+ lowContrast={true}
99
+ statusIconDescription="notification"
100
+ subtitle={t('errorLoadingInventoryItems', 'Error fetching inventory items')}
101
+ title={t('error', 'Error')}
102
+ />
103
+ );
104
+ }
105
+
106
+ if (isLoading) {
107
+ return <InlineLoading description={t('loadingInventoryItems', 'Loading inventory items...')} />;
108
+ }
109
+
110
+ return (
111
+ <Layer>
112
+ <ComboBox
113
+ id="stockDispense"
114
+ items={validInventoryItems}
115
+ onChange={({ selectedItem }) => {
116
+ updateInventoryItem(selectedItem);
117
+ }}
118
+ itemToString={(item) => (item ? toStockDispense(item) : '')}
119
+ titleText={t('stockDispense', 'Stock Dispense')}
120
+ placeholder={t('selectStockDispense', 'Select stock to dispense from')}
121
+ />
122
+ </Layer>
123
+ );
124
+ };
125
+
126
+ export default StockDispense;
@@ -0,0 +1,67 @@
1
+ import useSWR from 'swr';
2
+ import { openmrsFetch, useSession } from '@openmrs/esm-framework';
3
+ import { type StockDispenseRequest, type InventoryItem, type MedicationDispense } from '../../types';
4
+ import { getUuidFromReference } from '../../utils';
5
+
6
+ //TODO: Add configuration to retrieve the stock dispense endpoint
7
+ // For stock dispense to work, stock management module should be installed and configured
8
+ /**
9
+ * Fetches the inventory items for a given drug UUID.
10
+ *
11
+ * @param {string} drugUuid - The UUID of the drug.
12
+ * @returns {Array} - The inventory items.
13
+ */
14
+ export const useDispenseStock = (drugUuid: string) => {
15
+ const session = useSession();
16
+ const url = `/ws/rest/v1/stockmanagement/stockiteminventory?v=default&totalCount=true&drugUuid=${drugUuid}&includeBatchNo=true&groupBy=LocationStockItemBatchNo&dispenseLocationUuid=${session?.sessionLocation?.uuid}&includeStrength=1&includeConceptRefIds=1&emptyBatch=1&emptyBatchLocationUuid=${session?.sessionLocation?.uuid}&dispenseAtLocation=1`;
17
+ const { data, error, isLoading } = useSWR<{ data: { results: Array<InventoryItem> } }>(url, openmrsFetch);
18
+ return { inventoryItems: data?.data?.results ?? [], error, isLoading };
19
+ };
20
+
21
+ /**
22
+ * Sends a POST request to the inventory dispense endpoint with the provided stock dispense request.
23
+ *
24
+ * @param {AbortController} abortController - The AbortController used to cancel the request.
25
+ * @returns {Promise<Response>} - A Promise that resolves to the response of the POST request.
26
+ */
27
+ export async function sendStockDispenseRequest(
28
+ stockDispenseRequest,
29
+ abortController: AbortController,
30
+ ): Promise<Response> {
31
+ const url = '/ws/rest/v1/stockmanagement/dispenserequest';
32
+ return await openmrsFetch(url, {
33
+ method: 'POST',
34
+ signal: abortController.signal,
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ },
38
+ body: JSON.stringify({ dispenseItems: [stockDispenseRequest] }),
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Creates a stock dispense request payload.
44
+ *
45
+ * @param inventoryItem - The inventory item to dispense.
46
+ * @param patientUuid - The UUID of the patient.
47
+ * @param encounterUuid - The UUID of the encounter.
48
+ * @param medicationDispensePayload - The medication dispense payload.
49
+ * @returns The stock dispense request payload.
50
+ */
51
+ export const createStockDispenseRequestPayload = (
52
+ inventoryItem: InventoryItem,
53
+ patientUuid: string,
54
+ encounterUuid: string,
55
+ medicationDispensePayload: MedicationDispense,
56
+ ): StockDispenseRequest => {
57
+ return {
58
+ dispenseLocation: inventoryItem.locationUuid,
59
+ patient: patientUuid,
60
+ order: getUuidFromReference(medicationDispensePayload.authorizingPrescription[0].reference),
61
+ encounter: encounterUuid,
62
+ stockItem: inventoryItem?.stockItemUuid,
63
+ stockBatch: inventoryItem.stockBatchUuid,
64
+ stockItemPackagingUOM: inventoryItem.quantityUoMUuid,
65
+ quantity: medicationDispensePayload.quantity.value,
66
+ };
67
+ };
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ const DeleteConfirmModal = ({
6
+ title,
7
+ onClose,
8
+ onDelete,
9
+
10
+ message,
11
+ }: {
12
+ isOpen: boolean;
13
+ onClose: () => void;
14
+ onDelete: () => void;
15
+ title: string;
16
+ message: string;
17
+ }) => {
18
+ const { t } = useTranslation();
19
+
20
+ return (
21
+ <React.Fragment>
22
+ <ModalHeader closeModal={onClose} title={title}></ModalHeader>
23
+ <ModalBody>{message}</ModalBody>
24
+ <ModalFooter>
25
+ <Button kind="secondary" onClick={onClose}>
26
+ {t('cancel', 'Cancel')}
27
+ </Button>
28
+ <Button kind="danger" onClick={onDelete}>
29
+ {t('delete', 'Delete')}
30
+ </Button>
31
+ </ModalFooter>
32
+ </React.Fragment>
33
+ );
34
+ };
35
+ export default DeleteConfirmModal;