@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
package/src/utils.ts ADDED
@@ -0,0 +1,637 @@
1
+ import dayjs from 'dayjs';
2
+ import template from 'lodash/template';
3
+ import { mutate } from 'swr';
4
+ import {
5
+ type Coding,
6
+ type DispensingStore,
7
+ type DosageInstruction,
8
+ type Medication,
9
+ type MedicationDispense,
10
+ MedicationDispenseStatus,
11
+ type MedicationReferenceOrCodeableConcept,
12
+ type MedicationRequest,
13
+ type MedicationRequestBundle,
14
+ MedicationRequestCombinedStatus,
15
+ MedicationRequestFulfillerStatus,
16
+ MedicationRequestStatus,
17
+ type Quantity,
18
+ } from './types';
19
+ import { createGlobalStore, fhirBaseUrl, parseDate, useStore } from '@openmrs/esm-framework';
20
+ import {
21
+ OPENMRS_FHIR_EXT_DISPENSE_RECORDED,
22
+ OPENMRS_FHIR_EXT_MEDICINE,
23
+ OPENMRS_FHIR_EXT_REQUEST_FULFILLER_STATUS,
24
+ PRESCRIPTION_DETAILS_ENDPOINT,
25
+ PRESCRIPTIONS_TABLE_ENDPOINT,
26
+ } from './constants';
27
+
28
+ const unitsDontMatchErrorMessage =
29
+ "Misconfiguration, please contact your System Administrator: Can't calculate quantity dispensed if units don't match. Likely issue: allowModifyingPrescription and restrictTotalQuantityDispensed configuration parameters both set to true. " +
30
+ 'Either set restrictTotalQuantityDispensed to false or set allowModifyingPrescription to false. If you have previously entered dispense events that modified prescriptions, you will likely need to clean up or remove that data before setting restrictTotalQuantityDispensed to true.';
31
+
32
+ /**
33
+ * Computes the fulfiller status for a bundle
34
+ *
35
+ * @param medicationRequestBundle
36
+ * @param restrictTotalQuantityDispensed
37
+ * @param isDeleteOfCompletedDispense was this a delete event of a completed dispense?
38
+ */
39
+ export function computeFulfillerStatus(
40
+ medicationRequestBundle: MedicationRequestBundle,
41
+ restrictTotalQuantityDispensed: boolean,
42
+ isDeleteOfCompletedDispense: boolean = false,
43
+ ): MedicationRequestFulfillerStatus {
44
+ if (restrictTotalQuantityDispensed && computeQuantityRemaining(medicationRequestBundle) <= 0) {
45
+ // if we set to restrict total quantity dispenses and quantity remaining less than 0, set status to completed
46
+ return MedicationRequestFulfillerStatus.completed;
47
+ }
48
+
49
+ // otherwise, set based on most recent dispense status as follows
50
+ const mostRecentMedicationDispenseStatus = getMostRecentMedicationDispenseStatus(medicationRequestBundle.dispenses);
51
+
52
+ // if the most recent dispense was declined, set the fulfiller status to declined
53
+ if (mostRecentMedicationDispenseStatus === MedicationDispenseStatus.declined) {
54
+ return MedicationRequestFulfillerStatus.declined;
55
+ }
56
+
57
+ // if the most recent dispense was on hold, set the fulfiller status to on hold
58
+ if (mostRecentMedicationDispenseStatus === MedicationDispenseStatus.on_hold) {
59
+ return MedicationRequestFulfillerStatus.on_hold;
60
+ }
61
+
62
+ // a little more complicated logic for the "completed" status:
63
+ // if we are in 'restrictTotalQuantityDispense' mode, skip, just clear out the "completed" since our calculation above did not flag it as completed
64
+ // otherwise, if the most recent dispense was completed, set the fulfiller status to completed *if* the current request status is completed *and* this is not a delete of completed dispense
65
+ // the idea here is that entering/editing a dispense event without providing further info should not change the status of the overall order, but a delete of completed dispense should always remove any completed status on the overall order
66
+ if (!restrictTotalQuantityDispensed && mostRecentMedicationDispenseStatus === MedicationDispenseStatus.completed) {
67
+ return medicationRequestBundle.request.status === MedicationRequestStatus.completed && !isDeleteOfCompletedDispense
68
+ ? MedicationRequestFulfillerStatus.completed
69
+ : null;
70
+ }
71
+
72
+ return null;
73
+ }
74
+ /**
75
+ * Within the UI, the "status" of a request we want to display to the pharmacist is
76
+ * a combination of the status and the fulfiller statuts; given a request
77
+ * this calculates the actual status we want to display to the pharmacist
78
+ *
79
+ * @param medicationRequests
80
+ * @param medicationRequestExpirationPeriodInDays
81
+ */
82
+ export function computeMedicationRequestCombinedStatus(
83
+ medicationRequest: MedicationRequest,
84
+ medicationRequestExpirationPeriondInDays: number,
85
+ ): MedicationRequestCombinedStatus {
86
+ const medicationRequestStatus: MedicationRequestStatus = computeMedicationRequestStatus(
87
+ medicationRequest,
88
+ medicationRequestExpirationPeriondInDays,
89
+ );
90
+ const medicationRequestFulfillerStatus: MedicationRequestFulfillerStatus = getFulfillerStatus(medicationRequest);
91
+
92
+ // if the request is no longer active, that status takes precedent
93
+ if (medicationRequestStatus !== MedicationRequestStatus.active) {
94
+ if (medicationRequestStatus === MedicationRequestStatus.expired) {
95
+ return MedicationRequestCombinedStatus.expired;
96
+ } else if (medicationRequestStatus === MedicationRequestStatus.completed) {
97
+ return MedicationRequestCombinedStatus.completed;
98
+ } else if (medicationRequestStatus === MedicationRequestStatus.cancelled) {
99
+ return MedicationRequestCombinedStatus.cancelled;
100
+ }
101
+ }
102
+ // otherwise, if the medication dispense status is paused or closed, return that
103
+ if (medicationRequestFulfillerStatus === MedicationRequestFulfillerStatus.declined) {
104
+ return MedicationRequestCombinedStatus.declined;
105
+ } else if (medicationRequestFulfillerStatus === MedicationRequestFulfillerStatus.on_hold) {
106
+ return MedicationRequestCombinedStatus.on_hold;
107
+ }
108
+
109
+ // otherwise, return active
110
+ return MedicationRequestCombinedStatus.active;
111
+ }
112
+
113
+ /**
114
+ * Calculates the status of a medication request given the request and the expiration period in days
115
+ * Necessary to handle the (admittedly confusing) fact that the Dispense ESMs idea of "expired" is different
116
+ * from that of the OpenMRS Backend, see logic below
117
+ *
118
+ * @param medicationRequests
119
+ * @param medicationRequestExpirationPeriodInDays
120
+ */
121
+ export function computeMedicationRequestStatus(
122
+ medicationRequest: MedicationRequest,
123
+ medicationRequestExpirationPeriodInDays: number,
124
+ ): MedicationRequestStatus {
125
+ if (
126
+ medicationRequest.status === MedicationRequestStatus.cancelled ||
127
+ medicationRequest.status === MedicationRequestStatus.completed
128
+ ) {
129
+ return medicationRequest.status;
130
+ }
131
+
132
+ // expired is not based on based actual medication request expired status, but calculated from our configurable expiration period in days
133
+ // NOTE: the assumption here is that the validityPeriod.start is equal to encounter datetime of the associated encounter, because we use the encounter date when doing backend querying
134
+ if (
135
+ medicationRequest.dispenseRequest?.validityPeriod?.start &&
136
+ dayjs(medicationRequest.dispenseRequest.validityPeriod.start).isBefore(
137
+ dayjs().startOf('day').subtract(medicationRequestExpirationPeriodInDays, 'day'),
138
+ )
139
+ ) {
140
+ return MedicationRequestStatus.expired;
141
+ }
142
+
143
+ return MedicationRequestStatus.active;
144
+ }
145
+
146
+ /**
147
+ * Captures the logic to compute the new fulfiller status after a dispense event, where dispense event = a medication dispense where medication is actually dispensed (as opposed one with status "on_hold" or "declined")
148
+ *
149
+ * @param medicationDispense the medication dispense being added or editing
150
+ * @param medicationRequestBundle the entire existing bundle associated with the dispense being added/edited
151
+ * @param restrictTotalQuantityDispensed value of the "dispenseBehavior.restrictTotalQuantityDispensed"
152
+ */
153
+ export function computeNewFulfillerStatusAfterDispenseEvent(
154
+ medicationDispense: MedicationDispense,
155
+ medicationRequestBundle: MedicationRequestBundle,
156
+ restrictTotalQuantityDispensed: boolean,
157
+ ) {
158
+ // add or edit the existing bundle as necessary
159
+ let dispenses = [...medicationRequestBundle.dispenses];
160
+
161
+ if (!medicationDispense.id) {
162
+ // new dispense, add to the array
163
+ dispenses = [medicationDispense, ...dispenses];
164
+ } else {
165
+ // edited dispense, swap out
166
+ dispenses = dispenses.map((dispense) => (dispense.id === medicationDispense.id ? medicationDispense : dispense));
167
+ }
168
+
169
+ // then call computeFulfillerStatus to compute status
170
+ return computeFulfillerStatus(
171
+ {
172
+ request: medicationRequestBundle.request,
173
+ dispenses: dispenses,
174
+ },
175
+ restrictTotalQuantityDispensed,
176
+ false,
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Captures the logic to compute the new fulfiller status after a medication dispense is deleted
182
+ *
183
+ * @param deletedMedicationDispense the medication dispense we are deleting delete
184
+ * @param medicationRequestBundle the entire existing bundle associated with the dispense being delete
185
+ * @param restrictTotalQuantityDispensed value of the "dispenseBehavior.restrictTotalQuantityDispensed"
186
+ */
187
+ export function computeNewFulfillerStatusAfterDelete(
188
+ deletedMedicationDispense: MedicationDispense,
189
+ medicationRequestBundle: MedicationRequestBundle,
190
+ restrictTotalQuantityDispensed: boolean,
191
+ ): MedicationRequestFulfillerStatus {
192
+ // filter out the dispense being deleted and call computeFulfillerStatus
193
+ return computeFulfillerStatus(
194
+ {
195
+ request: medicationRequestBundle.request,
196
+ dispenses: medicationRequestBundle.dispenses.filter((dispense) => dispense.id !== deletedMedicationDispense.id),
197
+ },
198
+ restrictTotalQuantityDispensed,
199
+ deletedMedicationDispense.status === MedicationDispenseStatus.completed,
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Given a set of medication requests, calculates the "combined" status (see computeMedicationRequestCombinedStatus)
205
+ * of each, and then, from those determines the overall status of the "prescription" (where "prescription"
206
+ * means all medication requests in a single encounter)
207
+ * @param medicationRequests
208
+ * @param medicationRequestExpirationPeriodInDays
209
+ */
210
+ export function computePrescriptionStatus(
211
+ medicationRequests: Array<MedicationRequest>,
212
+ medicationRequestExpirationPeriodInDays: number,
213
+ ): MedicationRequestCombinedStatus {
214
+ if (!medicationRequests || medicationRequests.length === 0) {
215
+ return null;
216
+ }
217
+
218
+ const medicationRequestCombinedStatuses: Array<MedicationRequestCombinedStatus> = medicationRequests.map(
219
+ (medicationRequest) =>
220
+ computeMedicationRequestCombinedStatus(medicationRequest, medicationRequestExpirationPeriodInDays),
221
+ );
222
+
223
+ if (medicationRequestCombinedStatuses.includes(MedicationRequestCombinedStatus.active)) {
224
+ return MedicationRequestCombinedStatus.active;
225
+ } else if (medicationRequestCombinedStatuses.includes(MedicationRequestCombinedStatus.on_hold)) {
226
+ return MedicationRequestCombinedStatus.on_hold;
227
+ } else if (medicationRequestCombinedStatuses.includes(MedicationRequestCombinedStatus.completed)) {
228
+ return MedicationRequestCombinedStatus.completed;
229
+ } else if (medicationRequestCombinedStatuses.includes(MedicationRequestCombinedStatus.declined)) {
230
+ return MedicationRequestCombinedStatus.declined;
231
+ } else if (medicationRequestCombinedStatuses.includes(MedicationRequestCombinedStatus.cancelled)) {
232
+ return MedicationRequestCombinedStatus.cancelled;
233
+ } else if (medicationRequestCombinedStatuses.includes(MedicationRequestCombinedStatus.expired)) {
234
+ return MedicationRequestCombinedStatus.expired;
235
+ }
236
+
237
+ return null;
238
+ }
239
+
240
+ /**
241
+ * Calculates the prescription status and then returns the actual message code we want to display to the end user
242
+ *
243
+ * @param medicationRequests
244
+ * @param medicationRequestExpirationPeriodInDays
245
+ */
246
+ export function computePrescriptionStatusMessageCode(
247
+ medicationRequests: Array<MedicationRequest>,
248
+ medicationRequestExpirationPeriodInDays: number,
249
+ ): string {
250
+ const medicationRequestCombinedStatus: MedicationRequestCombinedStatus = computePrescriptionStatus(
251
+ medicationRequests,
252
+ medicationRequestExpirationPeriodInDays,
253
+ );
254
+
255
+ if (medicationRequestCombinedStatus === null) {
256
+ return null;
257
+ } else if (medicationRequestCombinedStatus === MedicationRequestCombinedStatus.active) {
258
+ return 'active';
259
+ } else if (medicationRequestCombinedStatus === MedicationRequestCombinedStatus.on_hold) {
260
+ return 'paused';
261
+ } else if (medicationRequestCombinedStatus === MedicationRequestCombinedStatus.completed) {
262
+ return 'completed';
263
+ } else if (medicationRequestCombinedStatus === MedicationRequestCombinedStatus.declined) {
264
+ return 'closed';
265
+ } else if (medicationRequestCombinedStatus === MedicationRequestCombinedStatus.expired) {
266
+ return 'expired';
267
+ } else if (medicationRequestCombinedStatus === MedicationRequestCombinedStatus.cancelled) {
268
+ return 'cancelled';
269
+ }
270
+ return null;
271
+ }
272
+
273
+ export function computeQuantityRemaining(medicationRequestBundle: MedicationRequestBundle): number {
274
+ if (medicationRequestBundle) {
275
+ // hard protect against quantity type mistmatch
276
+ if (!getQuantityUnitsMatch([medicationRequestBundle.request, ...medicationRequestBundle.dispenses])) {
277
+ throw new Error(unitsDontMatchErrorMessage);
278
+ }
279
+
280
+ return (
281
+ computeTotalQuantityOrdered(medicationRequestBundle.request) -
282
+ computeTotalQuantityDispensed(medicationRequestBundle.dispenses)
283
+ );
284
+ }
285
+ return 0;
286
+ }
287
+
288
+ /**
289
+ * Given a set of medication dispenses, calculate the total quantity dispensed
290
+ * @param medicationDispenses
291
+ */
292
+ export function computeTotalQuantityDispensed(medicationDispenses: Array<MedicationDispense>): number {
293
+ if (medicationDispenses) {
294
+ if (!getQuantityUnitsMatch(medicationDispenses)) {
295
+ throw new Error(unitsDontMatchErrorMessage);
296
+ }
297
+ const quantity = medicationDispenses
298
+ .map((medicationDispense) => (medicationDispense.quantity?.value ? medicationDispense.quantity?.value : 0))
299
+ .reduce((acc, currentValue) => acc + currentValue, 0);
300
+ return quantity;
301
+ } else {
302
+ return 0;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Given a medication request, calculate the total quantity ordered (including all refills)
308
+ * @param medicationRequest
309
+ */
310
+ export function computeTotalQuantityOrdered(medicationRequest: MedicationRequest): number {
311
+ const refillsAllowed = getRefillsAllowed(medicationRequest);
312
+ if (medicationRequest.dispenseRequest?.quantity?.value) {
313
+ return medicationRequest.dispenseRequest.quantity.value * (1 + (refillsAllowed ? refillsAllowed : 0));
314
+ } else {
315
+ return null;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Given a medication request and an array of medication dispenses, fetch all dispenses authorized by that request
321
+ *
322
+ * @param medicationRequest
323
+ * @param medicationDispenses
324
+ */
325
+ export function getAssociatedMedicationDispenses(
326
+ medicationRequest: MedicationRequest,
327
+ medicationDispenses: Array<MedicationDispense>,
328
+ ): Array<MedicationDispense> {
329
+ return medicationDispenses?.filter((medicationDispense) =>
330
+ medicationDispense?.authorizingPrescription?.some((prescription) =>
331
+ prescription.reference.endsWith(medicationRequest.id),
332
+ ),
333
+ );
334
+ }
335
+
336
+ /**
337
+ * Given a medication dispense and an array of medication requests, fetch request which authorized this request
338
+ *
339
+ * @param medicationDispense
340
+ * @param medicationRequests
341
+ */
342
+ export function getAssociatedMedicationRequest(
343
+ medicationDispense: MedicationDispense,
344
+ medicationRequests: Array<MedicationRequest>,
345
+ ): MedicationRequest {
346
+ return medicationRequests.find((medicationRequest) =>
347
+ medicationDispense?.authorizingPrescription?.some((prescription) =>
348
+ prescription.reference.endsWith(medicationRequest.id),
349
+ ),
350
+ );
351
+ }
352
+
353
+ /**
354
+ * Given an array of CodeableConcept codings, return the first one without an associated system (which should be the concept-referenced-by-uuid coding)
355
+ * @param codings
356
+ */
357
+ export function getConceptCoding(codings: Coding[]): Coding {
358
+ return codings ? codings.find((c) => !('system' in c) || c.system === undefined) : null;
359
+ }
360
+
361
+ /**
362
+ * Given an array of CodeableConcept codings, return the display for the first one without an associated system (which should be the concept-referenced-by-uuid coding)
363
+ * @param codings
364
+ */
365
+ export function getConceptCodingDisplay(codings: Coding[]): string {
366
+ return getConceptCoding(codings)?.display;
367
+ }
368
+
369
+ /**
370
+ * Given an array of CodeableConcept codings, return the code for the first one without an associated system (which should be the concept-referenced-by-uuid coding)
371
+ * @param codings
372
+ */
373
+ export function getConceptCodingUuid(codings: Coding[]): string {
374
+ return getConceptCoding(codings)?.code;
375
+ }
376
+
377
+ /**
378
+ * Fetch the "recorded" extension off a medication request
379
+ * @param medicationDispense
380
+ */
381
+ export function getDateRecorded(medicationDispense: MedicationDispense): string {
382
+ return medicationDispense?.extension?.find((ext) => ext.url === OPENMRS_FHIR_EXT_DISPENSE_RECORDED)?.valueDateTime;
383
+ }
384
+
385
+ export function getDosageInstruction(dosageInstructions: Array<DosageInstruction>): DosageInstruction {
386
+ if (dosageInstructions?.length > 0) {
387
+ return dosageInstructions[0];
388
+ }
389
+ return null;
390
+ }
391
+
392
+ /**
393
+ * Fetch the "fulfiller status" extension off a medication request
394
+ * @param medicationDispense
395
+ */
396
+ export function getFulfillerStatus(medicationRequest: MedicationRequest): MedicationRequestFulfillerStatus {
397
+ return medicationRequest?.extension?.find((ext) => ext.url === OPENMRS_FHIR_EXT_REQUEST_FULFILLER_STATUS)?.valueCode;
398
+ }
399
+
400
+ export function getMedicationsByConceptEndpoint(conceptUuid: string): string {
401
+ return `${fhirBaseUrl}/Medication?code=${conceptUuid}`;
402
+ }
403
+
404
+ /**
405
+ * Given a medication reference/codeable concept, format for display
406
+ * When we have a medication reference (ie a coded Drug reference in the OpenMRS model) we simply use the display property associated with the medication reference
407
+ * When we do not have medication reference, we display the associated concept and the OpenMRS DrugOrder.drugNonCoded string (which is stored in the codeable concept text field)
408
+ * (this may be slightly duplicative, but protects against the case when the provider only enters the formulation, not the drug, in the drugNonCoded field)
409
+ * @param medication
410
+ */
411
+ export function getMedicationDisplay(medication: MedicationReferenceOrCodeableConcept): string {
412
+ return medication.medicationReference
413
+ ? medication.medicationReference.display
414
+ : getConceptCodingDisplay(medication?.medicationCodeableConcept.coding) +
415
+ ': ' +
416
+ medication?.medicationCodeableConcept.text;
417
+ }
418
+
419
+ // TODO does this need to null-check
420
+ export function getMedicationReferenceOrCodeableConcept(
421
+ resource: MedicationRequest | MedicationDispense,
422
+ ): MedicationReferenceOrCodeableConcept {
423
+ return {
424
+ medicationReference: resource.medicationReference,
425
+ medicationCodeableConcept: resource.medicationCodeableConcept,
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Given a set of medication requests, return the status of the one with the most recent recorded date
431
+ */
432
+ export function getMostRecentMedicationDispenseStatus(
433
+ medicationDispenses: Array<MedicationDispense>,
434
+ ): MedicationDispenseStatus {
435
+ const sorted = medicationDispenses?.sort(sortMedicationDispensesByWhenHandedOver);
436
+ return sorted && sorted.length > 0 ? sorted[0].status : null;
437
+ }
438
+
439
+ /**
440
+ * Given a set of medication requests, return the status of the one with the next most recent recorded date
441
+ * (used when deleting the most recent, as we may need to update fulfiller status based on the next recent)
442
+ */
443
+ export function getNextMostRecentMedicationDispenseStatus(
444
+ medicationDispenses: Array<MedicationDispense>,
445
+ ): MedicationDispenseStatus {
446
+ const sorted = medicationDispenses?.sort(sortMedicationDispensesByWhenHandedOver);
447
+ return sorted && sorted.length > 1 ? sorted[1].status : null;
448
+ }
449
+
450
+ export function getMedicationRequestBundleContainingMedicationDispense(
451
+ medicationRequestBundles: Array<MedicationRequestBundle>,
452
+ medicationDispense: MedicationDispense,
453
+ ) {
454
+ return medicationRequestBundles.find((bundle) =>
455
+ bundle.dispenses.find((dispense) => dispense.id === medicationDispense.id),
456
+ );
457
+ }
458
+
459
+ /**
460
+ * Given a FHIR Medication, returns the string value stored in the "http://fhir.openmrs.org/ext/medicine#drugName" extension
461
+ * @param medication
462
+ */
463
+ export function getOpenMRSMedicineDrugName(medication: Medication): string {
464
+ if (!medication || !medication.extension) {
465
+ return null;
466
+ }
467
+
468
+ const medicineExtension = medication.extension.find((ext) => ext.url === OPENMRS_FHIR_EXT_MEDICINE);
469
+
470
+ if (!medicineExtension || !medicineExtension.extension) {
471
+ return null;
472
+ }
473
+
474
+ const medicationExtensionDrugName = medicineExtension.extension.find(
475
+ (ext) => ext.url === OPENMRS_FHIR_EXT_MEDICINE + '#drugName',
476
+ );
477
+
478
+ return medicationExtensionDrugName ? medicationExtensionDrugName.valueString : null;
479
+ }
480
+
481
+ export function getPrescriptionDetailsEndpoint(encounterUuid: string): string {
482
+ return `${fhirBaseUrl}/${PRESCRIPTION_DETAILS_ENDPOINT}?encounter=${encounterUuid}&_revinclude=MedicationDispense:prescription&_include=MedicationRequest:encounter`;
483
+ }
484
+
485
+ export function getPrescriptionTableEndpoint(
486
+ customPrescriptionsTableEndpoint: string,
487
+ status: string,
488
+ pageOffset: number,
489
+ pageSize: number,
490
+ date: string,
491
+ patientSearchTerm: string,
492
+ location: string,
493
+ ): string {
494
+ // use custom endpoint if provided, otherwise only include the "date" parameter when requesting "active" results
495
+
496
+ const compiledUrl = template(
497
+ customPrescriptionsTableEndpoint
498
+ ? customPrescriptionsTableEndpoint
499
+ : status === 'ACTIVE'
500
+ ? '${fhirBaseUrl}/${PRESCRIPTIONS_TABLE_ENDPOINT}&_getpagesoffset=${pageOffset}&_count=${pageSize}&date=ge${date}&status=${status}' +
501
+ (patientSearchTerm ? '&patientSearchTerm=${patientSearchTerm}' : '') +
502
+ (location ? '&location=${location}' : '')
503
+ : '${fhirBaseUrl}/${PRESCRIPTIONS_TABLE_ENDPOINT}&_getpagesoffset=${pageOffset}&_count=${pageSize}&status=${status}' +
504
+ (patientSearchTerm ? '&patientSearchTerm=${patientSearchTerm}' : '') +
505
+ (location ? '&location=${location}' : ''),
506
+ );
507
+ return compiledUrl({
508
+ fhirBaseUrl,
509
+ PRESCRIPTIONS_TABLE_ENDPOINT,
510
+ status,
511
+ pageOffset,
512
+ pageSize,
513
+ date,
514
+ patientSearchTerm,
515
+ location,
516
+ });
517
+ }
518
+
519
+ export function getQuantity(resource: MedicationRequest | MedicationDispense): Quantity {
520
+ if (resource.resourceType == 'MedicationRequest') {
521
+ return (resource as MedicationRequest).dispenseRequest?.quantity;
522
+ }
523
+ if (resource.resourceType == 'MedicationDispense') {
524
+ return (resource as MedicationDispense).quantity;
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Returns true/false whether the quantity units on all the resources are identical (or match)
530
+ * @param resources
531
+ */
532
+ export function getQuantityUnitsMatch(resources: Array<MedicationRequest | MedicationDispense>): boolean {
533
+ if (resources) {
534
+ const quantityUnitsArray = resources.map((resource) => getQuantity(resource)?.code).filter((quantity) => quantity);
535
+ if (quantityUnitsArray.length > 0) {
536
+ return quantityUnitsArray.every((element) => element === quantityUnitsArray[0]);
537
+ } else {
538
+ return true; // consider true if empty
539
+ }
540
+ } else {
541
+ return true; // consider true if null
542
+ }
543
+ }
544
+
545
+ export function getRefillsAllowed(resource: MedicationRequest | MedicationDispense): number {
546
+ if (resource.resourceType == 'MedicationRequest') {
547
+ return (resource as MedicationRequest).dispenseRequest?.numberOfRepeatsAllowed;
548
+ } else {
549
+ return null; // dispense doesn't have a "refills allowed" component
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Given a refernece in format "MedicationReference/uuid" or just "uuid", returns just the uuid compoennt
555
+ */
556
+ export function getUuidFromReference(reference: string): string {
557
+ if (reference?.includes('/')) {
558
+ return reference.split('/')[1];
559
+ } else {
560
+ return reference;
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Returns true/false whether the most passed in medication dispense status is the most recent
566
+ * @param medicationDispenses
567
+ */
568
+ export function isMostRecentMedicationDispense(
569
+ medicationDispense: MedicationDispense,
570
+ medicationDispenses: Array<MedicationDispense>,
571
+ ): boolean {
572
+ const sorted = medicationDispenses?.sort(sortMedicationDispensesByWhenHandedOver);
573
+
574
+ // prettier-ignore
575
+ return medicationDispense &&
576
+ sorted &&
577
+ sorted.length > 0 &&
578
+ sorted[0].id === medicationDispense.id ? true : false;
579
+ }
580
+
581
+ /**
582
+ * Revalidated (reloads) both the prescription associated with the encounter uuid,
583
+ * and the entire prescription table
584
+ * @param encounterUuid
585
+ */
586
+ export async function revalidate(encounterUuid: string) {
587
+ await mutate(
588
+ (key) =>
589
+ typeof key === 'string' &&
590
+ (key.startsWith(`${fhirBaseUrl}/${PRESCRIPTIONS_TABLE_ENDPOINT}`) ||
591
+ key.startsWith(`${fhirBaseUrl}/${PRESCRIPTION_DETAILS_ENDPOINT}?encounter=${encounterUuid}`)),
592
+ );
593
+ dispensingStore.setState((state) => ({
594
+ staleEncounterUuids: state.staleEncounterUuids.filter((uuid) => uuid !== encounterUuid),
595
+ }));
596
+ }
597
+
598
+ /**
599
+ * Mark the specified encounter as stale. The encounter will be unmarked on calling
600
+ * revalidate() to reload the data associated with the encounter.
601
+ */
602
+ export function markEncounterAsStale(encounterUuid: string) {
603
+ dispensingStore.setState((state) => ({
604
+ staleEncounterUuids: [...state.staleEncounterUuids, encounterUuid],
605
+ }));
606
+ }
607
+
608
+ export function sortMedicationDispensesByWhenHandedOver(a: MedicationDispense, b: MedicationDispense): number {
609
+ if (b.whenHandedOver === null) {
610
+ return 1;
611
+ } else if (a.whenHandedOver === null) {
612
+ return -1;
613
+ }
614
+ const dateDiff = parseDate(b.whenHandedOver).getTime() - parseDate(a.whenHandedOver).getTime();
615
+ if (dateDiff !== 0) {
616
+ return dateDiff;
617
+ } else {
618
+ return a.id.localeCompare(b.id); // just to enforce a standard order if two dates are equals
619
+ }
620
+ }
621
+
622
+ // we assume this is a free text dosage if none of the following are specified
623
+ export function calculateIsFreeTextDosage(dosageInstruction: DosageInstruction | null) {
624
+ return (
625
+ (!dosageInstruction?.doseAndRate || !dosageInstruction?.doseAndRate[0]?.doseQuantity?.value) &&
626
+ !dosageInstruction?.timing?.code?.coding[0]?.code &&
627
+ !dosageInstruction?.route
628
+ );
629
+ }
630
+
631
+ const dispensingStore = createGlobalStore<DispensingStore>('dispensing-store', {
632
+ staleEncounterUuids: [],
633
+ });
634
+
635
+ export function useStaleEncounterUuids() {
636
+ return useStore(dispensingStore);
637
+ }