@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,338 @@
1
+ import React, { useMemo } from 'react';
2
+ import { OverflowMenu, OverflowMenuItem, SkeletonText, Tag, Tile } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {
5
+ formatDatetime,
6
+ launchWorkspace2,
7
+ parseDate,
8
+ type Session,
9
+ showModal,
10
+ showSnackbar,
11
+ useConfig,
12
+ userHasAccess,
13
+ useSession,
14
+ } from '@openmrs/esm-framework';
15
+ import {
16
+ updateMedicationRequestFulfillerStatus,
17
+ usePrescriptionDetails,
18
+ } from '../medication-request/medication-request.resource';
19
+ import { deleteMedicationDispense } from '../medication-dispense/medication-dispense.resource';
20
+ import { type MedicationDispense, MedicationDispenseStatus, type MedicationRequestBundle } from '../types';
21
+ import {
22
+ PRIVILEGE_DELETE_DISPENSE,
23
+ PRIVILEGE_DELETE_DISPENSE_THIS_PROVIDER_ONLY,
24
+ PRIVILEGE_EDIT_DISPENSE,
25
+ } from '../constants';
26
+ import {
27
+ computeNewFulfillerStatusAfterDelete,
28
+ computeQuantityRemaining,
29
+ computeTotalQuantityDispensed,
30
+ getFulfillerStatus,
31
+ getMedicationRequestBundleContainingMedicationDispense,
32
+ getUuidFromReference,
33
+ markEncounterAsStale,
34
+ revalidate,
35
+ sortMedicationDispensesByWhenHandedOver,
36
+ } from '../utils';
37
+ import { type PharmacyConfig } from '../config-schema';
38
+ import MedicationEvent from '../components/medication-event.component';
39
+ import styles from './history-and-comments.scss';
40
+
41
+ const HistoryAndComments: React.FC<{
42
+ encounterUuid: string;
43
+ patientUuid: string;
44
+ }> = ({ encounterUuid, patientUuid }) => {
45
+ const { t } = useTranslation();
46
+ const config = useConfig<PharmacyConfig>();
47
+ const { medicationRequestBundles, prescriptionDate, error, isLoading } = usePrescriptionDetails(
48
+ encounterUuid,
49
+ config.refreshInterval,
50
+ );
51
+
52
+ const generateDispenseVerbiage = (medicationDispense: MedicationDispense): string | null => {
53
+ if (medicationDispense.status === MedicationDispenseStatus.completed) {
54
+ return t('dispensedMedication', 'dispensed medication');
55
+ } else if (medicationDispense.status === MedicationDispenseStatus.on_hold) {
56
+ return t('pausedDispense', 'paused dispense');
57
+ } else if (medicationDispense.status === MedicationDispenseStatus.declined) {
58
+ return t('closedDispense', 'closed dispense');
59
+ } else {
60
+ return null;
61
+ }
62
+ };
63
+
64
+ const { sortedDispenses, requests, hasHistory } = useMemo(() => {
65
+ const dispenses = medicationRequestBundles?.flatMap((bundle) => bundle.dispenses) ?? [];
66
+ const requests = medicationRequestBundles?.flatMap((bundle) => bundle.request) ?? [];
67
+ return {
68
+ sortedDispenses: [...dispenses].sort(sortMedicationDispensesByWhenHandedOver),
69
+ requests,
70
+ hasHistory: dispenses.length > 0 || requests.length > 0,
71
+ };
72
+ }, [medicationRequestBundles]);
73
+
74
+ return (
75
+ <div className={styles.historyAndCommentsContainer}>
76
+ {isLoading && (
77
+ <Tile>
78
+ <SkeletonText paragraph lineCount={2} />
79
+ </Tile>
80
+ )}
81
+ {error && (
82
+ <p className={styles.error}>
83
+ {t('errorLoadingHistory', 'Error loading history')}: {error.message}
84
+ </p>
85
+ )}
86
+ {!isLoading && !error && !hasHistory && (
87
+ <p className={styles.emptyState}>{t('noHistoryFound', 'No history found')}</p>
88
+ )}
89
+ {!isLoading && !error && hasHistory && (
90
+ <>
91
+ {sortedDispenses.map((dispense) => (
92
+ <div key={dispense.id}>
93
+ <h5 className={styles.historyHeader}>
94
+ {dispense.performer && dispense.performer[0]?.actor?.display} {generateDispenseVerbiage(dispense)} —{' '}
95
+ {formatDatetime(parseDate(dispense.whenHandedOver))}
96
+ </h5>
97
+ <MedicationEvent
98
+ medicationEvent={dispense}
99
+ status={<DispenseTag medicationDispense={dispense} />}
100
+ isDispenseEvent>
101
+ <MedicationDispenseActionMenu
102
+ medicationDispense={dispense}
103
+ medicationRequestBundle={getMedicationRequestBundleContainingMedicationDispense(
104
+ medicationRequestBundles,
105
+ dispense,
106
+ )}
107
+ patientUuid={patientUuid}
108
+ encounterUuid={encounterUuid}
109
+ />
110
+ </MedicationEvent>
111
+ </div>
112
+ ))}
113
+ {requests.map((request) => (
114
+ <div key={request.id}>
115
+ <h5 className={styles.historyHeader}>
116
+ {request.requester.display} {t('orderedMedication', 'ordered medication')} —{' '}
117
+ {formatDatetime(prescriptionDate)}
118
+ </h5>
119
+ <MedicationEvent medicationEvent={request} status={<Tag type="green">{t('ordered', 'Ordered')}</Tag>} />
120
+ </div>
121
+ ))}
122
+ </>
123
+ )}
124
+ </div>
125
+ );
126
+ };
127
+ interface MedicationDispenseActionMenuProps {
128
+ medicationDispense: MedicationDispense;
129
+ medicationRequestBundle: MedicationRequestBundle;
130
+ patientUuid: string;
131
+ encounterUuid: string;
132
+ }
133
+
134
+ const MedicationDispenseActionMenu: React.FC<MedicationDispenseActionMenuProps> = ({
135
+ medicationDispense,
136
+ medicationRequestBundle,
137
+ patientUuid,
138
+ encounterUuid,
139
+ }) => {
140
+ const { t } = useTranslation();
141
+ const session = useSession();
142
+ const config = useConfig<PharmacyConfig>();
143
+ const userCanEdit = (session: Session): boolean =>
144
+ session?.user && userHasAccess(PRIVILEGE_EDIT_DISPENSE, session.user);
145
+
146
+ const userCanDelete = (session: Session, medicationDispense: MedicationDispense): boolean => {
147
+ if (session?.user) {
148
+ if (userHasAccess(PRIVILEGE_DELETE_DISPENSE, session.user)) {
149
+ return true;
150
+ } else if (
151
+ userHasAccess(PRIVILEGE_DELETE_DISPENSE_THIS_PROVIDER_ONLY, session.user) &&
152
+ session.currentProvider?.uuid &&
153
+ medicationDispense.performer?.find(
154
+ (performer) =>
155
+ performer?.actor?.reference?.length > 1 &&
156
+ performer.actor.reference.split('/')[1] === session.currentProvider.uuid,
157
+ ) !== undefined
158
+ ) {
159
+ return true;
160
+ }
161
+ }
162
+ return false;
163
+ };
164
+
165
+ const getDispenseWorkspaceConfig = (
166
+ medicationDispense: MedicationDispense,
167
+ medicationRequestBundle: MedicationRequestBundle,
168
+ ): { workspaceName: string; props: Record<string, unknown> } | undefined => {
169
+ if (medicationDispense.status === MedicationDispenseStatus.completed) {
170
+ // note that since this is an edit, quantity remaining needs to include quantity that is part of this dispense
171
+ let quantityRemaining = null;
172
+ let quantityDispensed = null;
173
+ if (config.dispenseBehavior.restrictTotalQuantityDispensed) {
174
+ quantityRemaining =
175
+ computeQuantityRemaining(medicationRequestBundle) +
176
+ (medicationDispense?.quantity ? medicationDispense.quantity.value : 0);
177
+ if (medicationRequestBundle.dispenses) {
178
+ quantityDispensed = computeTotalQuantityDispensed(medicationRequestBundle.dispenses);
179
+ }
180
+ }
181
+
182
+ const dispenseFormProps = {
183
+ patientUuid,
184
+ encounterUuid,
185
+ medicationDispense,
186
+ medicationRequestBundle,
187
+ quantityRemaining,
188
+ quantityDispensed,
189
+ mode: 'edit',
190
+ };
191
+
192
+ return { workspaceName: 'dispense-workspace', props: dispenseFormProps };
193
+ } else if (medicationDispense.status === MedicationDispenseStatus.on_hold) {
194
+ const pauseDispenseFormProps = {
195
+ patientUuid,
196
+ encounterUuid,
197
+ medicationDispense,
198
+ mode: 'edit',
199
+ };
200
+ return { workspaceName: 'pause-dispense-workspace', props: pauseDispenseFormProps };
201
+ } else if (medicationDispense.status === MedicationDispenseStatus.declined) {
202
+ const closeDispenseFormProps = {
203
+ patientUuid,
204
+ encounterUuid,
205
+ medicationDispense,
206
+ mode: 'edit',
207
+ };
208
+ return { workspaceName: 'close-dispense-workspace', props: closeDispenseFormProps };
209
+ }
210
+ };
211
+
212
+ const getWorkspaceTitle = (medicationDispense: MedicationDispense): string | undefined => {
213
+ if (medicationDispense.status === MedicationDispenseStatus.completed) {
214
+ return t('editDispenseRecord', 'Edit Dispense Record');
215
+ } else if (medicationDispense.status === MedicationDispenseStatus.on_hold) {
216
+ return t('editPauseRecord', 'Edit Pause Record');
217
+ } else if (medicationDispense.status === MedicationDispenseStatus.declined) {
218
+ return t('editCloseRecord', 'Edit Close Record');
219
+ }
220
+ };
221
+
222
+ const handleDelete = (
223
+ medicationDispense: MedicationDispense,
224
+ medicationRequestBundle: MedicationRequestBundle,
225
+ ): void => {
226
+ const currentFulfillerStatus = getFulfillerStatus(medicationRequestBundle.request);
227
+ const newFulfillerStatus = computeNewFulfillerStatusAfterDelete(
228
+ medicationDispense,
229
+ medicationRequestBundle,
230
+ config.dispenseBehavior.restrictTotalQuantityDispensed,
231
+ );
232
+ markEncounterAsStale(encounterUuid);
233
+ deleteMedicationDispense(medicationDispense.id)
234
+ .then(() => {
235
+ showSnackbar({
236
+ kind: 'success',
237
+ title: t('success', 'Success'),
238
+ subtitle: t('medicationDispenseDeleted', 'Medication dispense was deleted successfully'),
239
+ });
240
+ if (currentFulfillerStatus !== newFulfillerStatus) {
241
+ updateMedicationRequestFulfillerStatus(
242
+ getUuidFromReference(
243
+ medicationDispense.authorizingPrescription[0].reference, // assumes authorizing prescription exist
244
+ ),
245
+ newFulfillerStatus,
246
+ )
247
+ .then(() => {
248
+ revalidate(encounterUuid);
249
+ })
250
+ .catch(() => {
251
+ showSnackbar({
252
+ kind: 'error',
253
+ title: t('updateStatusFailed', 'Update Status Failed'),
254
+ subtitle: t('couldNotUpdateMedicationRequestStatus', 'Could not update medication request status'),
255
+ });
256
+ });
257
+ }
258
+ revalidate(encounterUuid);
259
+ })
260
+ .catch(() => {
261
+ showSnackbar({
262
+ kind: 'error',
263
+ title: t('deleteFailed', 'Delete Failed'),
264
+ subtitle: t('couldNotDeleteMedicationDispense', 'Could not delete medication dispense'),
265
+ });
266
+ });
267
+ };
268
+
269
+ const editable = userCanEdit(session);
270
+ const deletable = userCanDelete(session, medicationDispense);
271
+
272
+ const handleEdit = () => {
273
+ const { workspaceName, props } = getDispenseWorkspaceConfig(medicationDispense, medicationRequestBundle) as {
274
+ workspaceName: string;
275
+ props: Record<string, unknown>;
276
+ };
277
+ const customWorkspaceTitle = getWorkspaceTitle(medicationDispense);
278
+ launchWorkspace2(workspaceName, { customWorkspaceTitle, ...props });
279
+ };
280
+
281
+ const handleDeleteClick = ({ medicationDispense, medicationRequestBundle }) => {
282
+ const dispose = showModal('delete-confirm-modal', {
283
+ title: t('deleteDispenseRecord', 'Delete Dispense Record'),
284
+ message: t('deleteDispenseRecordMessage', 'Are you sure you want to delete this dispense record?'),
285
+ onDelete: () => {
286
+ handleDelete(medicationDispense, medicationRequestBundle);
287
+ dispose();
288
+ },
289
+ onClose: () => {
290
+ dispose();
291
+ },
292
+ });
293
+ };
294
+
295
+ if (!editable && !deletable) {
296
+ return null;
297
+ } else {
298
+ return (
299
+ <OverflowMenu
300
+ aria-label={t('medicationDispenseActionMenu', 'Medication Dispense Action Menu')}
301
+ className={styles.medicationEventActionMenu}
302
+ flipped>
303
+ {editable && (
304
+ <OverflowMenuItem
305
+ className={styles.menuitem}
306
+ itemText={t('editRecord', 'Edit record')}
307
+ onClick={handleEdit}
308
+ />
309
+ )}
310
+ {deletable && (
311
+ <OverflowMenuItem
312
+ className={styles.menuitem}
313
+ hasDivider
314
+ isDelete
315
+ itemText={t('delete', 'Delete')}
316
+ onClick={() => handleDeleteClick({ medicationDispense, medicationRequestBundle })}
317
+ />
318
+ )}
319
+ </OverflowMenu>
320
+ );
321
+ }
322
+ };
323
+
324
+ const DispenseTag: React.FC<{ medicationDispense: MedicationDispense }> = ({ medicationDispense }) => {
325
+ const { t } = useTranslation();
326
+
327
+ if (medicationDispense.status === MedicationDispenseStatus.completed) {
328
+ return <Tag type="gray">{t('dispensed', 'Dispensed')}</Tag>;
329
+ } else if (medicationDispense.status === MedicationDispenseStatus.on_hold) {
330
+ return <Tag type="red">{t('paused', 'Paused')}</Tag>;
331
+ } else if (medicationDispense.status === MedicationDispenseStatus.declined) {
332
+ return <Tag type="red">{t('closed', 'Closed')}</Tag>;
333
+ } else {
334
+ return null;
335
+ }
336
+ };
337
+
338
+ export default HistoryAndComments;
@@ -0,0 +1,54 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .historyAndCommentsContainer {
6
+ max-width: 80%;
7
+
8
+ form {
9
+ display: flex;
10
+ flex-flow: row wrap;
11
+ align-items: center;
12
+ }
13
+
14
+ form input {
15
+ vertical-align: middle;
16
+ margin: 0 0.625rem 0 0;
17
+ padding: 0.625rem;
18
+ border: 1px solid #8d8d8d !important;
19
+ background-color: white;
20
+ }
21
+ form button {
22
+ width: 90px;
23
+ height: 1.25rem;
24
+ min-height: layout.$spacing-08 !important;
25
+ }
26
+
27
+ form :global(.cds--btn--ghost) {
28
+ width: 8rem !important;
29
+ }
30
+
31
+ .medicationEventActionMenu {
32
+ float: right;
33
+ }
34
+ }
35
+
36
+ .menuitem {
37
+ max-width: none;
38
+ }
39
+
40
+ .historyHeader {
41
+ padding-top: layout.$spacing-03;
42
+ padding-bottom: layout.$spacing-03;
43
+ font-size: 0.9rem;
44
+ }
45
+
46
+ .error {
47
+ color: $danger;
48
+ }
49
+
50
+ .emptyState {
51
+ @include type.type-style('body-01');
52
+ color: $text-02;
53
+ margin: layout.$spacing-05 0;
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework';
2
+ import { configSchema } from './config-schema';
3
+ import CloseActionButton from './components/prescription-actions/close-action-button.component';
4
+ import DispenseActionButton from './components/prescription-actions/dispense-action-button.component';
5
+ import DispensingComponent from './dispensing.component';
6
+ import DispensingDashboardComponent from './dashboard/dispensing-dashboard.component';
7
+ import DispensingLinkComponent from './dispensing-link.component';
8
+ import DispensingLinkHomepageComponent from './dashboard/dispensing-dashboard-link.component';
9
+ import PauseActionButton from './components/prescription-actions/pause-action-button.component';
10
+
11
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
12
+
13
+ const moduleName = '@openmrs/esm-dispensing-app';
14
+
15
+ const options = {
16
+ featureName: 'dispensing',
17
+ moduleName,
18
+ };
19
+
20
+ export function startupApp() {
21
+ defineConfigSchema(moduleName, configSchema);
22
+ }
23
+
24
+ export const dispensing = getSyncLifecycle(DispensingComponent, options);
25
+
26
+ export const dispensingLink = getSyncLifecycle(DispensingLinkComponent, options);
27
+
28
+ export const dispensingDashboard = getSyncLifecycle(DispensingDashboardComponent, options);
29
+
30
+ export const dispensingDashboardLink = getSyncLifecycle(DispensingLinkHomepageComponent, options);
31
+
32
+ // Prescription action buttons
33
+ export const closeActionButton = getSyncLifecycle(CloseActionButton, options);
34
+ export const dispenseActionButton = getSyncLifecycle(DispenseActionButton, options);
35
+ export const pauseActionButton = getSyncLifecycle(PauseActionButton, options);
36
+
37
+ // Dispensing workspace
38
+ // t('closePrescription', 'Close prescription')
39
+ export const closeDispenseWorkspace = getAsyncLifecycle(() => import('./forms/close-dispense-form.workspace'), options);
40
+ // t('dispensePrescription', 'Dispense prescription')
41
+ export const dispenseWorkspace = getAsyncLifecycle(() => import('./forms/dispense-form.workspace'), options);
42
+ // t('pausePrescription', 'Pause prescription')
43
+ export const pauseDispenseWorkspace = getAsyncLifecycle(() => import('./forms/pause-dispense-form.workspace'), options);
44
+
45
+ export const printPrescriptionPreviewModal = getAsyncLifecycle(
46
+ () => import('./print-prescription/prescription-print-preview.modal'),
47
+ options,
48
+ );
49
+ export const deleteConfirmModal = getAsyncLifecycle(() => import('./history/delete-confirm.modal'), options);
50
+
51
+ export const patientDiagnoses = getAsyncLifecycle(() => import('./diagnoses/diagnoses.component'), options);
52
+ export const patientConditions = getAsyncLifecycle(() => import('./conditions/conditions.component'), options);
53
+
54
+ export const onPrescriptionFilledModal = getAsyncLifecycle(
55
+ () => import('./fill-prescription/on-prescription-filled.modal'),
56
+ options,
57
+ );
@@ -0,0 +1,108 @@
1
+ import useSWR from 'swr';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { openmrsFetch } from '@openmrs/esm-framework';
4
+ import { useLocations } from './location.resource';
5
+ import { type PharmacyConfig } from '../config-schema';
6
+
7
+ jest.mocked(openmrsFetch);
8
+ jest.mock('swr');
9
+
10
+ const pharmacyConfig: PharmacyConfig = {
11
+ appName: '',
12
+ actionButtons: {
13
+ pauseButton: {
14
+ enabled: true,
15
+ },
16
+ closeButton: {
17
+ enabled: true,
18
+ },
19
+ },
20
+ dispenseBehavior: {
21
+ allowModifyingPrescription: false,
22
+ restrictTotalQuantityDispensed: false,
23
+ },
24
+ dispenserProviderRoles: [],
25
+ locationBehavior: {
26
+ locationColumn: { enabled: false },
27
+ locationFilter: {
28
+ enabled: false,
29
+ tag: 'Login Location',
30
+ associatedPharmacyLocationAttribute: 'Associated Pharmacy Location',
31
+ },
32
+ },
33
+ refreshInterval: 10000,
34
+ medicationRequestExpirationPeriodInDays: 0,
35
+ valueSets: {
36
+ reasonForPause: { uuid: '' },
37
+ reasonForClose: { uuid: '' },
38
+ substitutionReason: { uuid: '' },
39
+ substitutionType: { uuid: '' },
40
+ },
41
+ enableStockDispense: false,
42
+ completeOrderWithThisDispense: false,
43
+ validateBatch: false,
44
+ leftNavMode: 'collapsed',
45
+ customTabs: [],
46
+ };
47
+
48
+ describe('Location Resource tests', () => {
49
+ test('useLoginLocations should call proper endpoint via SWR', () => {
50
+ // @ts-ignore
51
+ useSWR.mockImplementation(() => ({
52
+ data: { data: 'mockedLoginLocations' },
53
+ }));
54
+
55
+ renderHook(() => useLocations(pharmacyConfig));
56
+ expect(useSWR).toHaveBeenCalledWith(
57
+ '/ws/rest/v1/location?tag=Login%20Location&v=custom:(uuid,name,attributes:(attributeType:(name),value:(uuid))',
58
+ openmrsFetch,
59
+ );
60
+ });
61
+ test('useLoginLocations should parse into Login Locations Array', () => {
62
+ // @ts-ignore
63
+ const queryResultsBundle = {
64
+ results: [
65
+ {
66
+ uuid: '2bcb9215-8cd6-11eb-b7be-0242ac110002',
67
+ name: 'KGH Triage',
68
+ attributes: [],
69
+ },
70
+ {
71
+ uuid: '5981f962-6eec-453d-89ce-2f9ac48d096f',
72
+ name: 'KGH MCH',
73
+ attributes: [
74
+ {
75
+ attributeType: {
76
+ name: 'Associated Pharmacy Location',
77
+ },
78
+ value: {
79
+ uuid: '84b9b680-786c-4388-9e7c-805614c13b5a',
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ {
85
+ uuid: '7b959d2f-11f3-4611-b2e4-700200625d61',
86
+ name: 'KGH NCD',
87
+ attributes: [],
88
+ },
89
+ ],
90
+ };
91
+
92
+ // @ts-ignore
93
+ useSWR.mockImplementation(() => ({ data: { data: queryResultsBundle } }));
94
+ const { result } = renderHook(() => useLocations(pharmacyConfig));
95
+ const { locations } = result.current;
96
+ expect(locations.length).toBe(3);
97
+ // should be sorted by name alphabetically
98
+ expect(locations[0].id).toBe('5981f962-6eec-453d-89ce-2f9ac48d096f');
99
+ expect(locations[0].name).toBe('KGH MCH');
100
+ expect(locations[0].associatedPharmacyLocation).toBe('84b9b680-786c-4388-9e7c-805614c13b5a');
101
+ expect(locations[1].id).toBe('7b959d2f-11f3-4611-b2e4-700200625d61');
102
+ expect(locations[1].name).toBe('KGH NCD');
103
+ expect(locations[1].associatedPharmacyLocation).toBe(null);
104
+ expect(locations[2].id).toBe('2bcb9215-8cd6-11eb-b7be-0242ac110002');
105
+ expect(locations[2].name).toBe('KGH Triage');
106
+ expect(locations[2].associatedPharmacyLocation).toBe(null);
107
+ });
108
+ });
@@ -0,0 +1,32 @@
1
+ import useSWR from 'swr';
2
+ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
3
+ import { type SimpleLocation } from '../types';
4
+ import { type PharmacyConfig } from '../config-schema';
5
+ import { useMemo } from 'react';
6
+
7
+ export function useLocations(config: PharmacyConfig) {
8
+ const { data, error } = useSWR<FetchResponse, Error>(
9
+ `${restBaseUrl}/location?tag=${encodeURIComponent(config.locationBehavior.locationFilter.tag)}&v=custom:(uuid,name,attributes:(attributeType:(name),value:(uuid))`,
10
+ openmrsFetch,
11
+ );
12
+
13
+ // parse down to a simple representation of locations
14
+ const locations: Array<SimpleLocation> = useMemo(() => {
15
+ return data?.data?.results
16
+ ?.map((e) => ({
17
+ id: e.uuid,
18
+ name: e.name,
19
+ associatedPharmacyLocation:
20
+ e.attributes?.find(
21
+ (a) => a.attributeType.name === config.locationBehavior.locationFilter.associatedPharmacyLocationAttribute,
22
+ )?.value?.uuid ?? null,
23
+ }))
24
+ .sort((a, b) => a.name.localeCompare(b.name));
25
+ }, [data?.data?.results, config]);
26
+
27
+ return {
28
+ locations,
29
+ error,
30
+ isLoading: !locations && !error,
31
+ };
32
+ }