@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,152 @@
1
+ import React from 'react';
2
+ import { SkeletonText, Tag, Tile } from '@carbon/react';
3
+ import { WarningFilled } from '@carbon/react/icons';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { type PatientUuid, useConfig, UserHasAccess } from '@openmrs/esm-framework';
6
+ import { computeMedicationRequestCombinedStatus, getConceptCodingDisplay, useStaleEncounterUuids } from '../utils';
7
+ import { PRIVILEGE_CREATE_DISPENSE } from '../constants';
8
+ import { type AllergyIntolerance, type MedicationRequest, MedicationRequestCombinedStatus } from '../types';
9
+ import { type PharmacyConfig } from '../config-schema';
10
+ import { usePatientAllergies, usePrescriptionDetails } from '../medication-request/medication-request.resource';
11
+ import ActionButtons from '../components/action-buttons.component';
12
+ import MedicationEvent from '../components/medication-event.component';
13
+ import PrescriptionsActionsFooter from './prescription-actions.component';
14
+ import styles from './prescription-details.scss';
15
+
16
+ const PrescriptionDetails: React.FC<{
17
+ encounterUuid: string;
18
+ patientUuid: PatientUuid;
19
+ }> = ({ encounterUuid, patientUuid }) => {
20
+ const { t } = useTranslation();
21
+ const config = useConfig<PharmacyConfig>();
22
+ const {
23
+ allergies,
24
+ totalAllergies,
25
+ isLoading: isLoadingAllergies,
26
+ error: allergiesError,
27
+ } = usePatientAllergies(patientUuid, config.refreshInterval);
28
+ const { medicationRequestBundles, error, isLoading } = usePrescriptionDetails(encounterUuid, config.refreshInterval);
29
+ const { staleEncounterUuids } = useStaleEncounterUuids();
30
+
31
+ const generateStatusTag = (medicationRequest: MedicationRequest): React.ReactNode => {
32
+ const combinedStatus: MedicationRequestCombinedStatus = computeMedicationRequestCombinedStatus(
33
+ medicationRequest,
34
+ config.medicationRequestExpirationPeriodInDays,
35
+ );
36
+ if (combinedStatus === MedicationRequestCombinedStatus.cancelled) {
37
+ return <Tag type="red">{t('cancelled', 'Cancelled')}</Tag>;
38
+ }
39
+
40
+ if (combinedStatus === MedicationRequestCombinedStatus.completed) {
41
+ return <Tag type="green">{t('completed', 'Completed')}</Tag>;
42
+ }
43
+
44
+ if (combinedStatus === MedicationRequestCombinedStatus.expired) {
45
+ return <Tag type="red">{t('expired', 'Expired')}</Tag>;
46
+ }
47
+
48
+ if (combinedStatus === MedicationRequestCombinedStatus.declined) {
49
+ return <Tag type="red">{t('closed', 'Closed')}</Tag>;
50
+ }
51
+
52
+ if (combinedStatus === MedicationRequestCombinedStatus.on_hold) {
53
+ return <Tag type="red">{t('paused', 'Paused')}</Tag>;
54
+ }
55
+
56
+ return null;
57
+ };
58
+
59
+ const displayAllergies = (allergies: Array<AllergyIntolerance>): string => {
60
+ const fallbackLabel = t('unknownAllergy', 'Unknown allergy');
61
+ return allergies
62
+ .map((allergy) => {
63
+ // Prefer code.text as it contains the human-readable allergen name
64
+ // (especially important for "Other" type allergies where coding display is generic)
65
+ if (allergy.code?.text) {
66
+ return allergy.code.text;
67
+ }
68
+ if (allergy.code?.coding?.length) {
69
+ return getConceptCodingDisplay(allergy.code.coding) ?? allergy.code.coding[0]?.display;
70
+ }
71
+ return fallbackLabel;
72
+ })
73
+ .filter(Boolean)
74
+ .join(', ');
75
+ };
76
+
77
+ return (
78
+ <div className={styles.prescriptionContainer}>
79
+ {isLoadingAllergies && (
80
+ <Tile className={styles.skeletonTile}>
81
+ <SkeletonText />
82
+ </Tile>
83
+ )}
84
+ {!isLoadingAllergies && (
85
+ <Tile className={styles.allergiesTile}>
86
+ <div className={styles.allergiesContent}>
87
+ <div>
88
+ <WarningFilled size={24} className={styles.allergiesIcon} />
89
+ <p>
90
+ {allergiesError && (
91
+ <span className={styles.error}>
92
+ {t('errorLoadingAllergies', 'Error loading allergies')}: {allergiesError.message}
93
+ </span>
94
+ )}
95
+ {!allergiesError && totalAllergies > 0 && (
96
+ <span>
97
+ <span className={styles.allergiesCount}>
98
+ {t('allergiesCount', '{{ count }} allergies', {
99
+ count: totalAllergies,
100
+ })}
101
+ </span>{' '}
102
+ {displayAllergies(allergies)}
103
+ </span>
104
+ )}
105
+ {!allergiesError &&
106
+ typeof totalAllergies === 'number' &&
107
+ totalAllergies === 0 &&
108
+ t('noAllergyDetailsFound', 'No allergy details found')}
109
+ </p>
110
+ </div>
111
+ </div>
112
+ </Tile>
113
+ )}
114
+ <h5 className={styles.prescribedHeader}>{t('prescribed', 'Prescribed')}</h5>
115
+ {isLoading && (
116
+ <Tile className={styles.skeletonTile}>
117
+ <SkeletonText paragraph lineCount={2} />
118
+ </Tile>
119
+ )}
120
+ {error && (
121
+ <p className={styles.error}>
122
+ {t('errorLoadingPrescriptionDetails', 'Error loading prescription details')}: {error.message}
123
+ </p>
124
+ )}
125
+ {medicationRequestBundles &&
126
+ (medicationRequestBundles.length > 0 ? (
127
+ medicationRequestBundles.map((bundle) => (
128
+ <MedicationEvent
129
+ key={bundle.request.id}
130
+ medicationEvent={bundle.request}
131
+ status={generateStatusTag(bundle.request)}>
132
+ <UserHasAccess privilege={PRIVILEGE_CREATE_DISPENSE}>
133
+ <ActionButtons
134
+ patientUuid={patientUuid}
135
+ encounterUuid={encounterUuid}
136
+ medicationRequestBundle={bundle}
137
+ disabled={staleEncounterUuids.includes(encounterUuid)}
138
+ />
139
+ </UserHasAccess>
140
+ </MedicationEvent>
141
+ ))
142
+ ) : (
143
+ <p className={styles.emptyState}>{t('noPrescriptionsFound', 'No prescriptions found')}</p>
144
+ ))}
145
+ {medicationRequestBundles?.length > 0 && (
146
+ <PrescriptionsActionsFooter encounterUuid={encounterUuid} patientUuid={patientUuid} />
147
+ )}
148
+ </div>
149
+ );
150
+ };
151
+
152
+ export default PrescriptionDetails;
@@ -0,0 +1,87 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .prescriptionContainer {
6
+ display: flex;
7
+ flex-direction: column;
8
+ max-width: 80%;
9
+ margin-bottom: layout.$spacing-05;
10
+
11
+ &:global(.cds--tile) {
12
+ min-height: 3rem !important;
13
+ padding-left: 10px !important;
14
+ }
15
+ }
16
+
17
+ .prescriptionContainer > :global(.cds--tile) {
18
+ min-height: 3rem !important;
19
+ padding-left: 10px !important;
20
+ margin: auto;
21
+ }
22
+
23
+ .allergiesTile {
24
+ height: layout.$spacing-06;
25
+ padding: 0 layout.$spacing-03 0 0;
26
+ border-left: layout.$spacing-02 solid #f1c21b;
27
+ background-color: rgba(253, 209, 58, 0.3);
28
+ margin: auto;
29
+ width: 100%;
30
+ }
31
+
32
+ .allergiesContent {
33
+ margin: auto;
34
+ min-width: 100%;
35
+ height: 100%;
36
+ display: flex;
37
+ align-items: center;
38
+
39
+ div {
40
+ align-items: center;
41
+ display: flex;
42
+ flex-direction: row;
43
+ width: 100%;
44
+ }
45
+
46
+ p {
47
+ margin-left: 10px;
48
+ font-size: 0.9rem;
49
+ width: 80%;
50
+ }
51
+
52
+ a {
53
+ margin: auto;
54
+ color: #0f62fe;
55
+ text-decoration: none;
56
+ text-align: right;
57
+ }
58
+ }
59
+
60
+ svg.allergiesIcon {
61
+ fill: #f1c21b;
62
+ vertical-align: middle;
63
+ }
64
+
65
+ .allergiesCount {
66
+ font-weight: bold;
67
+ }
68
+
69
+ .prescribedHeader {
70
+ padding-top: layout.$spacing-03;
71
+ padding-bottom: layout.$spacing-03;
72
+ font-size: 0.9rem;
73
+ }
74
+
75
+ .error {
76
+ color: $danger;
77
+ }
78
+
79
+ .emptyState {
80
+ @include type.type-style('body-01');
81
+ color: $text-02;
82
+ margin: layout.$spacing-05 0;
83
+ }
84
+
85
+ .skeletonTile {
86
+ width: 100%;
87
+ }
@@ -0,0 +1,267 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import { usePrescriptionDetails, usePatientAllergies } from '../medication-request/medication-request.resource';
5
+ import { useStaleEncounterUuids } from '../utils';
6
+ import PrescriptionDetails from './prescription-details.component';
7
+
8
+ jest.mock('../medication-request/medication-request.resource');
9
+ jest.mock('../utils', () => ({
10
+ ...jest.requireActual('../utils'),
11
+ useStaleEncounterUuids: jest.fn(),
12
+ }));
13
+
14
+ const mockUseConfig = jest.mocked(useConfig);
15
+ const mockUsePrescriptionDetails = jest.mocked(usePrescriptionDetails);
16
+ const mockUsePatientAllergies = jest.mocked(usePatientAllergies);
17
+ const mockUseStaleEncounterUuids = jest.mocked(useStaleEncounterUuids);
18
+
19
+ const mockEncounterUuid = 'test-encounter-uuid';
20
+ const mockPatientUuid = 'test-patient-uuid';
21
+
22
+ describe('PrescriptionDetails', () => {
23
+ beforeEach(() => {
24
+ mockUseConfig.mockReturnValue({
25
+ refreshInterval: 10000,
26
+ medicationRequestExpirationPeriodInDays: 90,
27
+ dispenseBehavior: {
28
+ allowModifyingPrescription: false,
29
+ restrictTotalQuantityDispensed: false,
30
+ },
31
+ });
32
+ mockUseStaleEncounterUuids.mockReturnValue({
33
+ staleEncounterUuids: [],
34
+ });
35
+ });
36
+
37
+ describe('Allergies Display', () => {
38
+ it('does not show allergies content while loading', () => {
39
+ mockUsePatientAllergies.mockReturnValue({
40
+ allergies: [],
41
+ totalAllergies: undefined,
42
+ error: undefined,
43
+ isLoading: true,
44
+ });
45
+ mockUsePrescriptionDetails.mockReturnValue({
46
+ medicationRequestBundles: [],
47
+ prescriptionDate: new Date(),
48
+ error: undefined,
49
+ isLoading: false,
50
+ mutate: jest.fn(),
51
+ isValidating: false,
52
+ });
53
+
54
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
55
+
56
+ // While loading allergies, should not show allergy details or no allergies message
57
+ expect(screen.queryByText(/no allergy details found/i)).not.toBeInTheDocument();
58
+ });
59
+
60
+ it('shows error message when fetching allergies fails', () => {
61
+ const errorMessage = 'Network error';
62
+ mockUsePatientAllergies.mockReturnValue({
63
+ allergies: [],
64
+ totalAllergies: undefined,
65
+ error: new Error(errorMessage),
66
+ isLoading: false,
67
+ });
68
+ mockUsePrescriptionDetails.mockReturnValue({
69
+ medicationRequestBundles: [],
70
+ prescriptionDate: new Date(),
71
+ error: undefined,
72
+ isLoading: false,
73
+ mutate: jest.fn(),
74
+ isValidating: false,
75
+ });
76
+
77
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
78
+
79
+ expect(screen.getByText(/error loading allergies/i)).toBeInTheDocument();
80
+ });
81
+
82
+ it('shows no allergies message when patient has no allergies', () => {
83
+ mockUsePatientAllergies.mockReturnValue({
84
+ allergies: [],
85
+ totalAllergies: 0,
86
+ error: undefined,
87
+ isLoading: false,
88
+ });
89
+ mockUsePrescriptionDetails.mockReturnValue({
90
+ medicationRequestBundles: [],
91
+ prescriptionDate: new Date(),
92
+ error: undefined,
93
+ isLoading: false,
94
+ mutate: jest.fn(),
95
+ isValidating: false,
96
+ });
97
+
98
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
99
+
100
+ expect(screen.getByText(/no allergy details found/i)).toBeInTheDocument();
101
+ });
102
+
103
+ it('displays allergy count and names', () => {
104
+ mockUsePatientAllergies.mockReturnValue({
105
+ allergies: [
106
+ {
107
+ id: 'allergy-1',
108
+ code: {
109
+ text: 'Penicillin',
110
+ coding: [{ code: '123', display: 'Penicillin' }],
111
+ },
112
+ },
113
+ {
114
+ id: 'allergy-2',
115
+ code: {
116
+ text: 'Aspirin',
117
+ coding: [{ code: '456', display: 'Aspirin' }],
118
+ },
119
+ },
120
+ ] as any,
121
+ totalAllergies: 2,
122
+ error: undefined,
123
+ isLoading: false,
124
+ });
125
+ mockUsePrescriptionDetails.mockReturnValue({
126
+ medicationRequestBundles: [],
127
+ prescriptionDate: new Date(),
128
+ error: undefined,
129
+ isLoading: false,
130
+ mutate: jest.fn(),
131
+ isValidating: false,
132
+ });
133
+
134
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
135
+
136
+ // Translation mock doesn't interpolate, check for allergies text pattern
137
+ expect(screen.getByText(/allergies/i)).toBeInTheDocument();
138
+ expect(screen.getByText(/Penicillin/)).toBeInTheDocument();
139
+ expect(screen.getByText(/Aspirin/)).toBeInTheDocument();
140
+ });
141
+
142
+ it('prefers code.text over coding.display for "Other" type allergies', () => {
143
+ // This tests the fix we made for allergies like "Corn" where the coding
144
+ // display shows "Other" but code.text contains the actual allergen name
145
+ mockUsePatientAllergies.mockReturnValue({
146
+ allergies: [
147
+ {
148
+ id: 'allergy-1',
149
+ code: {
150
+ text: 'Corn', // Should prefer this
151
+ coding: [{ code: '5622', display: 'Other' }], // Over this
152
+ },
153
+ },
154
+ ] as any,
155
+ totalAllergies: 1,
156
+ error: undefined,
157
+ isLoading: false,
158
+ });
159
+ mockUsePrescriptionDetails.mockReturnValue({
160
+ medicationRequestBundles: [],
161
+ prescriptionDate: new Date(),
162
+ error: undefined,
163
+ isLoading: false,
164
+ mutate: jest.fn(),
165
+ isValidating: false,
166
+ });
167
+
168
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
169
+
170
+ expect(screen.getByText(/Corn/)).toBeInTheDocument();
171
+ expect(screen.queryByText('Other')).not.toBeInTheDocument();
172
+ });
173
+
174
+ it('falls back to coding.display when code.text is not available', () => {
175
+ mockUsePatientAllergies.mockReturnValue({
176
+ allergies: [
177
+ {
178
+ id: 'allergy-1',
179
+ code: {
180
+ coding: [{ code: '123', display: 'Sulfonamides' }],
181
+ },
182
+ },
183
+ ] as any,
184
+ totalAllergies: 1,
185
+ error: undefined,
186
+ isLoading: false,
187
+ });
188
+ mockUsePrescriptionDetails.mockReturnValue({
189
+ medicationRequestBundles: [],
190
+ prescriptionDate: new Date(),
191
+ error: undefined,
192
+ isLoading: false,
193
+ mutate: jest.fn(),
194
+ isValidating: false,
195
+ });
196
+
197
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
198
+
199
+ expect(screen.getByText(/Sulfonamides/)).toBeInTheDocument();
200
+ });
201
+ });
202
+
203
+ describe('Prescriptions Display', () => {
204
+ it('shows loading skeleton while fetching prescriptions', () => {
205
+ mockUsePatientAllergies.mockReturnValue({
206
+ allergies: [],
207
+ totalAllergies: 0,
208
+ error: undefined,
209
+ isLoading: false,
210
+ });
211
+ mockUsePrescriptionDetails.mockReturnValue({
212
+ medicationRequestBundles: [],
213
+ prescriptionDate: new Date(),
214
+ error: undefined,
215
+ isLoading: true,
216
+ mutate: jest.fn(),
217
+ isValidating: false,
218
+ });
219
+
220
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
221
+
222
+ expect(screen.getByText('Prescribed')).toBeInTheDocument();
223
+ });
224
+
225
+ it('shows error message when fetching prescriptions fails', () => {
226
+ mockUsePatientAllergies.mockReturnValue({
227
+ allergies: [],
228
+ totalAllergies: 0,
229
+ error: undefined,
230
+ isLoading: false,
231
+ });
232
+ mockUsePrescriptionDetails.mockReturnValue({
233
+ medicationRequestBundles: [],
234
+ prescriptionDate: new Date(),
235
+ error: new Error('Failed to load'),
236
+ isLoading: false,
237
+ mutate: jest.fn(),
238
+ isValidating: false,
239
+ });
240
+
241
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
242
+
243
+ expect(screen.getByText(/error loading prescription details/i)).toBeInTheDocument();
244
+ });
245
+
246
+ it('shows empty state when no prescriptions exist', () => {
247
+ mockUsePatientAllergies.mockReturnValue({
248
+ allergies: [],
249
+ totalAllergies: 0,
250
+ error: undefined,
251
+ isLoading: false,
252
+ });
253
+ mockUsePrescriptionDetails.mockReturnValue({
254
+ medicationRequestBundles: [],
255
+ prescriptionDate: new Date(),
256
+ error: undefined,
257
+ isLoading: false,
258
+ mutate: jest.fn(),
259
+ isValidating: false,
260
+ });
261
+
262
+ render(<PrescriptionDetails encounterUuid={mockEncounterUuid} patientUuid={mockPatientUuid} />);
263
+
264
+ expect(screen.getByText(/no prescriptions found/i)).toBeInTheDocument();
265
+ });
266
+ });
267
+ });
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import { getAssignedExtensions, ExtensionSlot, type PatientUuid } from '@openmrs/esm-framework';
3
+ import { Tab, Tabs, TabList, TabPanels, TabPanel } from '@carbon/react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import HistoryAndComments from '../history/history-and-comments.component';
6
+ import PrescriptionDetails from './prescription-details.component';
7
+ import styles from './prescription-expanded.scss';
8
+
9
+ interface TabItem {
10
+ name: string;
11
+ component: JSX.Element;
12
+ }
13
+
14
+ const PrescriptionExpanded: React.FC<{
15
+ encounterUuid: string;
16
+ patientUuid: PatientUuid;
17
+ }> = ({ encounterUuid, patientUuid }) => {
18
+ const { t } = useTranslation();
19
+ const conditionsAndDiagnosisExtensions = getAssignedExtensions('dispensing-condition-and-diagnoses');
20
+
21
+ const tabs: TabItem[] = [
22
+ {
23
+ name: t('prescriptionDetails', 'Prescription details'),
24
+ component: <PrescriptionDetails encounterUuid={encounterUuid} patientUuid={patientUuid} />,
25
+ },
26
+ conditionsAndDiagnosisExtensions && conditionsAndDiagnosisExtensions.length > 0
27
+ ? {
28
+ name: t('conditionsAndDiagnoses', 'Conditions and diagnoses'),
29
+ component: <ExtensionSlot name="dispensing-condition-and-diagnoses" state={{ patientUuid, encounterUuid }} />,
30
+ }
31
+ : null,
32
+ {
33
+ name: t('historyComments', 'History and comments'),
34
+ component: <HistoryAndComments encounterUuid={encounterUuid} patientUuid={patientUuid} />,
35
+ },
36
+ ];
37
+
38
+ return (
39
+ <div className={styles.expandedTabsParentContainer}>
40
+ <div className={styles.verticalTabs}>
41
+ <Tabs>
42
+ <TabList aria-label={t('tabList', 'Tab List')}>
43
+ {tabs.filter(Boolean).map((tab: TabItem, index: number) => (
44
+ <Tab key={index}>{tab.name}</Tab>
45
+ ))}
46
+ </TabList>
47
+ <TabPanels>
48
+ {tabs.filter(Boolean).map((tab: TabItem, index) => (
49
+ <TabPanel key={index}>{tab.component}</TabPanel>
50
+ ))}
51
+ </TabPanels>
52
+ </Tabs>
53
+ </div>
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export default PrescriptionExpanded;
@@ -0,0 +1,56 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .expandedTabsParentContainer {
5
+ display: flex;
6
+ flex-direction: column;
7
+ margin-bottom: layout.$spacing-05;
8
+ }
9
+
10
+ // Vertical tabs layout
11
+ .verticalTabs {
12
+ display: flex;
13
+ flex-direction: row;
14
+
15
+ :global(.cds--tabs) {
16
+ width: 20%;
17
+ min-width: 12rem;
18
+ overflow: visible !important;
19
+ }
20
+
21
+ :global(.cds--tabs-trigger-container),
22
+ :global(.cds--tabs--scrollable),
23
+ :global(.cds--tabs--scrollable__nav) {
24
+ overflow: visible !important;
25
+ height: auto !important;
26
+ max-height: none !important;
27
+ }
28
+
29
+ :global(.cds--tab--list) {
30
+ flex-direction: column !important;
31
+ overflow: visible !important;
32
+ height: auto !important;
33
+ max-height: none !important;
34
+ }
35
+
36
+ :global(.cds--tabs__nav-item) {
37
+ border-left: 3px solid $ui-03;
38
+
39
+ &:global(.cds--tabs__nav-item--selected) {
40
+ border-left-color: var(--brand-03);
41
+ }
42
+ }
43
+
44
+ :global(.cds--tabs__nav-link) {
45
+ border-bottom: none !important;
46
+ padding: layout.$spacing-04 layout.$spacing-05 !important;
47
+ max-width: none !important;
48
+ width: 100%;
49
+ }
50
+
51
+ :global(.cds--tab-content) {
52
+ flex: 1;
53
+ padding: layout.$spacing-05 0;
54
+ min-height: 8rem;
55
+ }
56
+ }
@@ -0,0 +1,70 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Tab, Tabs, TabList, TabPanels } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useConfig, useSession } from '@openmrs/esm-framework';
5
+ import { type CustomTab } from '../types';
6
+ import { type PharmacyConfig } from '../config-schema';
7
+ import PatientSearchTabPanel from './patient-search-tab-panel.component';
8
+ import PrescriptionTabPanel from './prescription-tab-panel.component';
9
+ import styles from './prescriptions.scss';
10
+
11
+ const PrescriptionTabLists: React.FC = () => {
12
+ const { t } = useTranslation();
13
+ const config = useConfig<PharmacyConfig>();
14
+ const session = useSession();
15
+ const [selectedTab, setSelectedTab] = useState(0);
16
+
17
+ // filter tabs based on session location
18
+ const customTabs: Array<CustomTab> = useMemo(() => {
19
+ return (
20
+ config?.customTabs?.filter(
21
+ (tab) => !tab.associatedLocations || tab.associatedLocations.includes(session.sessionLocation?.uuid),
22
+ ) || []
23
+ );
24
+ }, [session, config]);
25
+
26
+ const handleTabChange = (event) => {
27
+ setSelectedTab(event.selectedIndex);
28
+ };
29
+
30
+ return (
31
+ <main className="omrs-main-content">
32
+ <section className={styles.prescriptionTabsContainer}>
33
+ <Tabs onChange={handleTabChange}>
34
+ <TabList aria-label={t('tabList', 'Tab List')} contained className={styles.tabsContainer}>
35
+ <Tab title={t('search', 'Search')} id={'tab-search'} className={styles.tab}>
36
+ {t('search', 'Search')}
37
+ </Tab>
38
+ <Tab
39
+ title={t('activePrescriptions', 'Active Prescriptions')}
40
+ id={'tab-active-prescription'}
41
+ className={styles.tab}>
42
+ {t('activePrescriptions', 'Active Prescriptions')}
43
+ </Tab>
44
+ <Tab title={t('allPrescriptions', 'All Prescriptions')} id={'tab-all-prescription'} className={styles.tab}>
45
+ {t('allPrescriptions', 'All Prescriptions')}
46
+ </Tab>
47
+ {customTabs.map((tab, index) => (
48
+ <Tab title={t(tab.title)} id={'custom_tab_' + index} className={styles.tab}>
49
+ {t(tab.title)}
50
+ </Tab>
51
+ ))}
52
+ </TabList>
53
+ <TabPanels>
54
+ <PatientSearchTabPanel />
55
+ <PrescriptionTabPanel isTabActive={selectedTab === 1} status={'ACTIVE'} />
56
+ <PrescriptionTabPanel isTabActive={selectedTab === 2} status={''} />
57
+ {customTabs.map((tab, index) => (
58
+ <PrescriptionTabPanel
59
+ isTabActive={selectedTab === index + 3}
60
+ customPrescriptionsTableEndpoint={tab.customPrescriptionsTableEndpoint}
61
+ />
62
+ ))}
63
+ </TabPanels>
64
+ </Tabs>
65
+ </section>
66
+ </main>
67
+ );
68
+ };
69
+
70
+ export default PrescriptionTabLists;