@bathiran212/esm-patient-chart-app 7.0.0

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 (382) hide show
  1. package/README.md +3 -0
  2. package/dist/108.js +1 -0
  3. package/dist/108.js.map +1 -0
  4. package/dist/1339.js +1 -0
  5. package/dist/1339.js.map +1 -0
  6. package/dist/1480.js +1 -0
  7. package/dist/1480.js.map +1 -0
  8. package/dist/1543.js +1 -0
  9. package/dist/1543.js.map +1 -0
  10. package/dist/1582.js +1 -0
  11. package/dist/1582.js.map +1 -0
  12. package/dist/1646.js +1 -0
  13. package/dist/1646.js.map +1 -0
  14. package/dist/1797.js +1 -0
  15. package/dist/1797.js.map +1 -0
  16. package/dist/1869.js +1 -0
  17. package/dist/1869.js.map +1 -0
  18. package/dist/1877.js +1 -0
  19. package/dist/1877.js.map +1 -0
  20. package/dist/2020.js +1 -0
  21. package/dist/2020.js.map +1 -0
  22. package/dist/2246.js +1 -0
  23. package/dist/2246.js.map +1 -0
  24. package/dist/2317.js +1 -0
  25. package/dist/2317.js.map +1 -0
  26. package/dist/2416.js +1 -0
  27. package/dist/2416.js.map +1 -0
  28. package/dist/2790.js +1 -0
  29. package/dist/2790.js.map +1 -0
  30. package/dist/282.js +1 -0
  31. package/dist/282.js.map +1 -0
  32. package/dist/2881.js +1 -0
  33. package/dist/2881.js.map +1 -0
  34. package/dist/3137.js +1 -0
  35. package/dist/3137.js.map +1 -0
  36. package/dist/3378.js +1 -0
  37. package/dist/3378.js.map +1 -0
  38. package/dist/3390.js +1 -0
  39. package/dist/3390.js.map +1 -0
  40. package/dist/3536.js +1 -0
  41. package/dist/3536.js.map +1 -0
  42. package/dist/3720.js +1 -0
  43. package/dist/3720.js.map +1 -0
  44. package/dist/3857.js +1 -0
  45. package/dist/3857.js.map +1 -0
  46. package/dist/3925.js +1 -0
  47. package/dist/3925.js.map +1 -0
  48. package/dist/3963.js +1 -0
  49. package/dist/3963.js.map +1 -0
  50. package/dist/3989.js +1 -0
  51. package/dist/3989.js.map +1 -0
  52. package/dist/4092.js +1 -0
  53. package/dist/4092.js.map +1 -0
  54. package/dist/4106.js +1 -0
  55. package/dist/4106.js.map +1 -0
  56. package/dist/4111.js +1 -0
  57. package/dist/4111.js.map +1 -0
  58. package/dist/4145.js +1 -0
  59. package/dist/4145.js.map +1 -0
  60. package/dist/434.js +1 -0
  61. package/dist/434.js.map +1 -0
  62. package/dist/4348.js +1 -0
  63. package/dist/4348.js.map +1 -0
  64. package/dist/4383.js +1 -0
  65. package/dist/4383.js.map +1 -0
  66. package/dist/4540.js +1 -0
  67. package/dist/4540.js.map +1 -0
  68. package/dist/4658.js +1 -0
  69. package/dist/4658.js.map +1 -0
  70. package/dist/466.js +1 -0
  71. package/dist/466.js.map +1 -0
  72. package/dist/4913.js +1 -0
  73. package/dist/4913.js.map +1 -0
  74. package/dist/4928.js +1 -0
  75. package/dist/4928.js.map +1 -0
  76. package/dist/5069.js +1 -0
  77. package/dist/5069.js.map +1 -0
  78. package/dist/5117.js +1 -0
  79. package/dist/5117.js.map +1 -0
  80. package/dist/5132.js +1 -0
  81. package/dist/5132.js.map +1 -0
  82. package/dist/5145.js +1 -0
  83. package/dist/5145.js.map +1 -0
  84. package/dist/52.js +1 -0
  85. package/dist/52.js.map +1 -0
  86. package/dist/5422.js +1 -0
  87. package/dist/5422.js.map +1 -0
  88. package/dist/5503.js +1 -0
  89. package/dist/5503.js.map +1 -0
  90. package/dist/5549.js +1 -0
  91. package/dist/5549.js.map +1 -0
  92. package/dist/556.js +1 -0
  93. package/dist/556.js.map +1 -0
  94. package/dist/5644.js +1 -0
  95. package/dist/5644.js.map +1 -0
  96. package/dist/5697.js +1 -0
  97. package/dist/5697.js.map +1 -0
  98. package/dist/5793.js +1 -0
  99. package/dist/5793.js.map +1 -0
  100. package/dist/5940.js +1 -0
  101. package/dist/5940.js.map +1 -0
  102. package/dist/5952.js +1 -0
  103. package/dist/5952.js.map +1 -0
  104. package/dist/6047.js +1 -0
  105. package/dist/6047.js.map +1 -0
  106. package/dist/6371.js +1 -0
  107. package/dist/6371.js.map +1 -0
  108. package/dist/6377.js +1 -0
  109. package/dist/6377.js.map +1 -0
  110. package/dist/6444.js +1 -0
  111. package/dist/6444.js.map +1 -0
  112. package/dist/6479.js +1 -0
  113. package/dist/6479.js.map +1 -0
  114. package/dist/6508.js +1 -0
  115. package/dist/6508.js.map +1 -0
  116. package/dist/6724.js +1 -0
  117. package/dist/6724.js.map +1 -0
  118. package/dist/6759.js +27 -0
  119. package/dist/6759.js.map +1 -0
  120. package/dist/689.js +1 -0
  121. package/dist/689.js.map +1 -0
  122. package/dist/6904.js +1 -0
  123. package/dist/6904.js.map +1 -0
  124. package/dist/7045.js +1 -0
  125. package/dist/7045.js.map +1 -0
  126. package/dist/7175.js +1 -0
  127. package/dist/7175.js.map +1 -0
  128. package/dist/7182.js +1 -0
  129. package/dist/7182.js.map +1 -0
  130. package/dist/7302.js +1 -0
  131. package/dist/7302.js.map +1 -0
  132. package/dist/7646.js +17 -0
  133. package/dist/7646.js.map +1 -0
  134. package/dist/7742.js +1 -0
  135. package/dist/7742.js.map +1 -0
  136. package/dist/7912.js +1 -0
  137. package/dist/7912.js.map +1 -0
  138. package/dist/8105.js +21 -0
  139. package/dist/8105.js.map +1 -0
  140. package/dist/8202.js +1 -0
  141. package/dist/8202.js.map +1 -0
  142. package/dist/8349.js +1 -0
  143. package/dist/8349.js.map +1 -0
  144. package/dist/8358.js +1 -0
  145. package/dist/8358.js.map +1 -0
  146. package/dist/8359.js +1 -0
  147. package/dist/8359.js.map +1 -0
  148. package/dist/8695.js +1 -0
  149. package/dist/8695.js.map +1 -0
  150. package/dist/8702.js +1 -0
  151. package/dist/8702.js.map +1 -0
  152. package/dist/8894.js +1 -0
  153. package/dist/8894.js.map +1 -0
  154. package/dist/8958.js +1 -0
  155. package/dist/8958.js.map +1 -0
  156. package/dist/903.js +1 -0
  157. package/dist/903.js.map +1 -0
  158. package/dist/9061.js +1 -0
  159. package/dist/9061.js.map +1 -0
  160. package/dist/9072.js +1 -0
  161. package/dist/9072.js.map +1 -0
  162. package/dist/9105.js +1 -0
  163. package/dist/9105.js.map +1 -0
  164. package/dist/9107.js +1 -0
  165. package/dist/9107.js.map +1 -0
  166. package/dist/9456.js +1 -0
  167. package/dist/9456.js.map +1 -0
  168. package/dist/9586.js +1 -0
  169. package/dist/9586.js.map +1 -0
  170. package/dist/9712.js +1 -0
  171. package/dist/9712.js.map +1 -0
  172. package/dist/9771.js +1 -0
  173. package/dist/9771.js.map +1 -0
  174. package/dist/9806.js +1 -0
  175. package/dist/9806.js.map +1 -0
  176. package/dist/9873.js +1 -0
  177. package/dist/9873.js.map +1 -0
  178. package/dist/9927.js +1 -0
  179. package/dist/9927.js.map +1 -0
  180. package/dist/main.js +6 -0
  181. package/dist/main.js.map +1 -0
  182. package/dist/openmrs-esm-patient-chart-app.js +6 -0
  183. package/dist/openmrs-esm-patient-chart-app.js.buildmanifest.json +2386 -0
  184. package/dist/openmrs-esm-patient-chart-app.js.map +1 -0
  185. package/dist/routes.json +1 -0
  186. package/package.json +63 -0
  187. package/rspack.config.js +1 -0
  188. package/src/actions-buttons/action-button.scss +3 -0
  189. package/src/actions-buttons/delete-visit.component.tsx +41 -0
  190. package/src/actions-buttons/delete-visit.test.tsx +26 -0
  191. package/src/actions-buttons/mark-patient-alive.component.tsx +42 -0
  192. package/src/actions-buttons/mark-patient-deceased.component.tsx +35 -0
  193. package/src/actions-buttons/start-visit.component.tsx +41 -0
  194. package/src/actions-buttons/start-visit.test.tsx +44 -0
  195. package/src/actions-buttons/stop-visit.component.tsx +39 -0
  196. package/src/actions-buttons/stop-visit.test.tsx +27 -0
  197. package/src/clinical-views/encounter-list/encounter-list-tabs.extension.tsx +78 -0
  198. package/src/clinical-views/encounter-list/encounter-list-tabs.scss +7 -0
  199. package/src/clinical-views/encounter-list/encounter-list.component.tsx +306 -0
  200. package/src/clinical-views/encounter-list/encounter-list.scss +36 -0
  201. package/src/clinical-views/encounter-list/table.component.tsx +63 -0
  202. package/src/clinical-views/encounter-list/table.scss +11 -0
  203. package/src/clinical-views/encounter-list/tag.component.test.tsx +307 -0
  204. package/src/clinical-views/encounter-list/tag.component.tsx +43 -0
  205. package/src/clinical-views/encounter-tile/clinical-views-summary.component.tsx +40 -0
  206. package/src/clinical-views/encounter-tile/encounter-tile.component.tsx +94 -0
  207. package/src/clinical-views/encounter-tile/tile.scss +82 -0
  208. package/src/clinical-views/hooks/index.ts +3 -0
  209. package/src/clinical-views/hooks/useEncounterRows.ts +60 -0
  210. package/src/clinical-views/hooks/useEncountersByVisit.ts +13 -0
  211. package/src/clinical-views/hooks/useFormsJson.ts +15 -0
  212. package/src/clinical-views/hooks/useLastEncounter.ts +29 -0
  213. package/src/clinical-views/types.ts +305 -0
  214. package/src/clinical-views/utils/concept-utils.ts +24 -0
  215. package/src/clinical-views/utils/encounter-list-config-builder.ts +160 -0
  216. package/src/clinical-views/utils/encounter-list.resource.ts +26 -0
  217. package/src/clinical-views/utils/helpers.ts +226 -0
  218. package/src/clinical-views/utils/index.ts +90 -0
  219. package/src/config-schema.ts +235 -0
  220. package/src/constants.ts +11 -0
  221. package/src/dashboard.meta.ts +15 -0
  222. package/src/data.resource.ts +117 -0
  223. package/src/declarations.d.ts +4 -0
  224. package/src/index.ts +204 -0
  225. package/src/loader/loader.component.tsx +11 -0
  226. package/src/loader/loader.scss +9 -0
  227. package/src/mark-patient-alive/mark-patient-alive.modal.tsx +54 -0
  228. package/src/mark-patient-deceased/mark-patient-deceased-form.scss +175 -0
  229. package/src/mark-patient-deceased/mark-patient-deceased-form.test.tsx +203 -0
  230. package/src/mark-patient-deceased/mark-patient-deceased-form.workspace.tsx +295 -0
  231. package/src/offline.ts +41 -0
  232. package/src/patient-banner-tags/visit-attribute-tags.extension.tsx +62 -0
  233. package/src/patient-banner-tags/visit-attribute-tags.scss +8 -0
  234. package/src/patient-chart/chart-review/chart-review.component.tsx +138 -0
  235. package/src/patient-chart/chart-review/chart-review.test.tsx +77 -0
  236. package/src/patient-chart/chart-review/dashboard-view.component.tsx +85 -0
  237. package/src/patient-chart/chart-review/dashboard-view.scss +84 -0
  238. package/src/patient-chart/patient-chart.component.tsx +71 -0
  239. package/src/patient-chart/patient-chart.resources.test.ts +238 -0
  240. package/src/patient-chart/patient-chart.resources.ts +231 -0
  241. package/src/patient-chart/patient-chart.scss +65 -0
  242. package/src/patient-details-tile/patient-details-tile.component.tsx +25 -0
  243. package/src/patient-details-tile/patient-details-tile.scss +24 -0
  244. package/src/root.component.tsx +35 -0
  245. package/src/root.scss +54 -0
  246. package/src/routes.json +267 -0
  247. package/src/side-nav/side-menu.component.tsx +10 -0
  248. package/src/side-nav/side-menu.scss +38 -0
  249. package/src/side-nav/side-menu.test.tsx +27 -0
  250. package/src/utils.test.ts +17 -0
  251. package/src/utils.ts +5 -0
  252. package/src/visit/hooks/useDefaultFacilityLocation.tsx +15 -0
  253. package/src/visit/hooks/useDefaultVisitLocation.tsx +24 -0
  254. package/src/visit/hooks/useDeleteVisit.test.tsx +267 -0
  255. package/src/visit/hooks/useDeleteVisit.tsx +103 -0
  256. package/src/visit/hooks/useOfflineVisitType.tsx +18 -0
  257. package/src/visit/hooks/useRecommendedVisitTypes.tsx +34 -0
  258. package/src/visit/hooks/useVisitAttributeType.tsx +102 -0
  259. package/src/visit/start-visit-button.component.tsx +47 -0
  260. package/src/visit/start-visit-button.test.tsx +32 -0
  261. package/src/visit/visit-action-items/delete-visit-action-item.component.tsx +60 -0
  262. package/src/visit/visit-action-items/delete-visit-action-item.test.tsx +48 -0
  263. package/src/visit/visit-action-items/edit-visit-details.component.tsx +79 -0
  264. package/src/visit/visit-form/base-visit-type.component.tsx +121 -0
  265. package/src/visit/visit-form/base-visit-type.scss +75 -0
  266. package/src/visit/visit-form/base-visit-type.test.tsx +153 -0
  267. package/src/visit/visit-form/exported-visit-form.workspace.tsx +755 -0
  268. package/src/visit/visit-form/location-selector.component.tsx +86 -0
  269. package/src/visit/visit-form/location-selector.test.tsx +146 -0
  270. package/src/visit/visit-form/recommended-visit-type.component.tsx +32 -0
  271. package/src/visit/visit-form/visit-attribute-type.component.tsx +258 -0
  272. package/src/visit/visit-form/visit-attribute-type.scss +5 -0
  273. package/src/visit/visit-form/visit-date-time.component.tsx +206 -0
  274. package/src/visit/visit-form/visit-form.resource.ts +401 -0
  275. package/src/visit/visit-form/visit-form.scss +167 -0
  276. package/src/visit/visit-form/visit-form.test.tsx +1233 -0
  277. package/src/visit/visit-form/visit-form.workspace.tsx +61 -0
  278. package/src/visit/visit-form/visit-type.test.tsx +88 -0
  279. package/src/visit/visit-history-table/visit-actions-cell.component.tsx +20 -0
  280. package/src/visit/visit-history-table/visit-actions-cell.scss +4 -0
  281. package/src/visit/visit-history-table/visit-date-cell.component.tsx +19 -0
  282. package/src/visit/visit-history-table/visit-diagnoses-cell.component.tsx +18 -0
  283. package/src/visit/visit-history-table/visit-history-table.component.tsx +145 -0
  284. package/src/visit/visit-history-table/visit-history-table.scss +25 -0
  285. package/src/visit/visit-history-table/visit-type-cell.component.tsx +15 -0
  286. package/src/visit/visit-prompt/delete-visit-dialog.modal.tsx +46 -0
  287. package/src/visit/visit-prompt/delete-visit-dialog.test.tsx +79 -0
  288. package/src/visit/visit-prompt/end-visit-dialog.modal.tsx +82 -0
  289. package/src/visit/visit-prompt/end-visit-dialog.scss +7 -0
  290. package/src/visit/visit-prompt/end-visit-dialog.test.tsx +131 -0
  291. package/src/visit/visit-prompt/modify-visit-date.modal.tsx +40 -0
  292. package/src/visit/visit-prompt/start-visit-dialog.modal.tsx +64 -0
  293. package/src/visit/visit-prompt/start-visit-dialog.scss +10 -0
  294. package/src/visit/visit-prompt/start-visit-dialog.test.tsx +40 -0
  295. package/src/visit/visits-widget/active-visit-buttons/active-visit-buttons.scss +7 -0
  296. package/src/visit/visits-widget/active-visit-buttons/active-visit-buttons.tsx +178 -0
  297. package/src/visit/visits-widget/current-visit-summary.extension.tsx +48 -0
  298. package/src/visit/visits-widget/current-visit-summary.scss +10 -0
  299. package/src/visit/visits-widget/current-visit-summary.test.tsx +85 -0
  300. package/src/visit/visits-widget/encounter-observations/encounter-observations.component.tsx +67 -0
  301. package/src/visit/visits-widget/encounter-observations/index.ts +3 -0
  302. package/src/visit/visits-widget/encounter-observations/styles.scss +22 -0
  303. package/src/visit/visits-widget/past-visits-components/delete-encounter.modal.tsx +47 -0
  304. package/src/visit/visits-widget/past-visits-components/delete-encounter.scss +9 -0
  305. package/src/visit/visits-widget/past-visits-components/encounters-table/all-encounters-table.component.tsx +49 -0
  306. package/src/visit/visits-widget/past-visits-components/encounters-table/completed-forms-table.component.tsx +67 -0
  307. package/src/visit/visits-widget/past-visits-components/encounters-table/completed-forms-table.test.tsx +146 -0
  308. package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.component.tsx +452 -0
  309. package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.resource.test.ts +156 -0
  310. package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.resource.ts +215 -0
  311. package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.scss +113 -0
  312. package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.test.tsx +432 -0
  313. package/src/visit/visits-widget/past-visits-components/encounters-table/visit-completed-forms-table.component.tsx +61 -0
  314. package/src/visit/visits-widget/past-visits-components/encounters-table/visit-completed-forms-table.test.tsx +125 -0
  315. package/src/visit/visits-widget/past-visits-components/encounters-table/visit-encounters-table.component.tsx +47 -0
  316. package/src/visit/visits-widget/past-visits-components/medications-summary.component.tsx +163 -0
  317. package/src/visit/visits-widget/past-visits-components/notes-summary.component.tsx +66 -0
  318. package/src/visit/visits-widget/past-visits-components/patient-notes-summary.component.tsx +318 -0
  319. package/src/visit/visits-widget/past-visits-components/tests-summary.component.tsx +16 -0
  320. package/src/visit/visits-widget/past-visits-components/visit-summary.component.tsx +192 -0
  321. package/src/visit/visits-widget/past-visits-components/visit-summary.scss +72 -0
  322. package/src/visit/visits-widget/past-visits-components/visit-summary.test.tsx +105 -0
  323. package/src/visit/visits-widget/single-visit-details/visit-timeline/visit-timeline.component.tsx +94 -0
  324. package/src/visit/visits-widget/single-visit-details/visit-timeline/visit-timeline.scss +60 -0
  325. package/src/visit/visits-widget/visit-context/retrospective-data-date-time-picker/restrospective-date-time-picker.scss +35 -0
  326. package/src/visit/visits-widget/visit-context/retrospective-data-date-time-picker/retrospective-date-time-picker.component.tsx +140 -0
  327. package/src/visit/visits-widget/visit-context/visit-context-header.extension.tsx +61 -0
  328. package/src/visit/visits-widget/visit-context/visit-context-header.scss +45 -0
  329. package/src/visit/visits-widget/visit-context/visit-context-header.test.tsx +59 -0
  330. package/src/visit/visits-widget/visit-context/visit-context-info.component.tsx +37 -0
  331. package/src/visit/visits-widget/visit-context/visit-context-info.scss +12 -0
  332. package/src/visit/visits-widget/visit-context/visit-context-switcher.modal.tsx +166 -0
  333. package/src/visit/visits-widget/visit-context/visit-context-switcher.scss +83 -0
  334. package/src/visit/visits-widget/visit-context/visit-context-switcher.test.tsx +79 -0
  335. package/src/visit/visits-widget/visit-detail-overview.component.tsx +67 -0
  336. package/src/visit/visits-widget/visit-detail-overview.scss +301 -0
  337. package/src/visit/visits-widget/visit-detail-overview.test.tsx +205 -0
  338. package/src/visit/visits-widget/visit.resource.tsx +146 -0
  339. package/translations/am.json +209 -0
  340. package/translations/ar.json +209 -0
  341. package/translations/ar_SY.json +209 -0
  342. package/translations/bn.json +209 -0
  343. package/translations/cs.json +209 -0
  344. package/translations/de.json +209 -0
  345. package/translations/en.json +209 -0
  346. package/translations/en_US.json +209 -0
  347. package/translations/es.json +209 -0
  348. package/translations/es_MX.json +209 -0
  349. package/translations/fr.json +209 -0
  350. package/translations/he.json +209 -0
  351. package/translations/hi.json +209 -0
  352. package/translations/hi_IN.json +209 -0
  353. package/translations/id.json +209 -0
  354. package/translations/it.json +209 -0
  355. package/translations/ka.json +209 -0
  356. package/translations/km.json +209 -0
  357. package/translations/ku.json +209 -0
  358. package/translations/ky.json +209 -0
  359. package/translations/lg.json +209 -0
  360. package/translations/ne.json +209 -0
  361. package/translations/pl.json +209 -0
  362. package/translations/pt.json +209 -0
  363. package/translations/pt_BR.json +209 -0
  364. package/translations/qu.json +209 -0
  365. package/translations/ro_RO.json +209 -0
  366. package/translations/ru_RU.json +209 -0
  367. package/translations/si.json +209 -0
  368. package/translations/sq.json +209 -0
  369. package/translations/sw.json +209 -0
  370. package/translations/sw_KE.json +209 -0
  371. package/translations/tr.json +209 -0
  372. package/translations/tr_TR.json +209 -0
  373. package/translations/uk.json +209 -0
  374. package/translations/uz.json +209 -0
  375. package/translations/uz@Latn.json +209 -0
  376. package/translations/uz_UZ.json +209 -0
  377. package/translations/vi.json +209 -0
  378. package/translations/zh.json +209 -0
  379. package/translations/zh_CN.json +209 -0
  380. package/translations/zh_TW.json +209 -0
  381. package/tsconfig.json +4 -0
  382. package/vitest.config.ts +4 -0
@@ -0,0 +1,452 @@
1
+ import React, { type ComponentProps, useCallback, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useSWRConfig } from 'swr';
4
+ import {
5
+ Button,
6
+ ComboBox,
7
+ DataTable,
8
+ DataTableSkeleton,
9
+ Layer,
10
+ OverflowMenu,
11
+ OverflowMenuItem,
12
+ Pagination,
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableContainer,
17
+ TableExpandHeader,
18
+ TableExpandRow,
19
+ TableExpandedRow,
20
+ TableHead,
21
+ TableHeader,
22
+ TableRow,
23
+ TableSelectAll,
24
+ TableSelectRow,
25
+ TableToolbar,
26
+ TableToolbarContent,
27
+ Tile,
28
+ } from '@carbon/react';
29
+ import {
30
+ EditIcon,
31
+ isDesktop,
32
+ launchWorkspace2,
33
+ showModal,
34
+ showSnackbar,
35
+ TrashCanIcon,
36
+ useConfig,
37
+ useLayoutType,
38
+ userHasAccess,
39
+ useSession,
40
+ type EncounterType,
41
+ ExtensionSlot,
42
+ useFeatureFlag,
43
+ PrinterIcon,
44
+ } from '@openmrs/esm-framework';
45
+ import { invalidateVisitAndEncounterData, usePatientChartStore } from '@openmrs/esm-patient-common-lib';
46
+ import { type ChartConfig } from '../../../../config-schema';
47
+ import { jsonSchemaResourceName } from '../../../../constants';
48
+ import {
49
+ deleteEncounter,
50
+ downloadPdf,
51
+ mapEncounter,
52
+ useEncounterTypes,
53
+ type EncountersTableProps,
54
+ type MappedEncounter,
55
+ } from './encounters-table.resource';
56
+ import EncounterObservations from '../../encounter-observations';
57
+ import styles from './encounters-table.scss';
58
+
59
+ /**
60
+ * This components is used by the AllEncountersTable and VisitEncountersTable to display
61
+ * a table of encounters, with the actual data, pagination and filtering logic passed in
62
+ * as props.
63
+ */
64
+ const EncountersTable: React.FC<EncountersTableProps> = ({
65
+ currentPage,
66
+ encounterTypeToFilter,
67
+ goTo,
68
+ isLoading,
69
+ pageSize,
70
+ paginatedEncounters,
71
+ patientUuid,
72
+ setEncounterTypeToFilter,
73
+ setPageSize,
74
+ showEncounterTypeFilter,
75
+ showVisitType,
76
+ totalCount,
77
+ isSelectable,
78
+ canPrintEncounters,
79
+ }) => {
80
+ const { t } = useTranslation();
81
+ const pageSizes = [10, 20, 30, 40, 50];
82
+ const desktopLayout = isDesktop(useLayoutType());
83
+ const session = useSession();
84
+ const { mutateVisitContext, patient } = usePatientChartStore(patientUuid);
85
+ const { mutate } = useSWRConfig();
86
+ const responsiveSize = desktopLayout ? 'sm' : 'lg';
87
+ const { data: encounterTypes, isLoading: isLoadingEncounterTypes } = useEncounterTypes();
88
+ const enableEmbeddedFormView = useFeatureFlag('enable-embedded-form-view');
89
+ const { encounterEditableDuration, encounterEditableDurationOverridePrivileges } = useConfig<ChartConfig>();
90
+ const [isPrinting, setIsPrinting] = useState(false);
91
+
92
+ const paginatedMappedEncounters = useMemo(
93
+ () => (paginatedEncounters ?? []).map(mapEncounter).filter(Boolean),
94
+ [paginatedEncounters],
95
+ );
96
+
97
+ const encountersByUuid = useMemo(
98
+ () => new Map(paginatedMappedEncounters?.map((encounter) => [encounter.id, encounter]) ?? []),
99
+ [paginatedMappedEncounters],
100
+ );
101
+
102
+ const tableHeaders = [
103
+ {
104
+ header: t('dateAndTime', 'Date & time'),
105
+ key: 'datetime',
106
+ },
107
+ ...(showVisitType
108
+ ? [
109
+ {
110
+ header: t('visitType', 'Visit type'),
111
+ key: 'visitType',
112
+ },
113
+ ]
114
+ : []),
115
+ {
116
+ header: t('encounterType', 'Encounter type'),
117
+ key: 'encounterType',
118
+ },
119
+ {
120
+ header: t('form', 'Form name'),
121
+ key: 'formName',
122
+ },
123
+ {
124
+ header: t('provider', 'Provider'),
125
+ key: 'provider',
126
+ },
127
+ ];
128
+
129
+ const handleDeleteEncounter = useCallback(
130
+ (encounterUuid: string, encounterTypeName?: string) => {
131
+ const dispose = showModal('delete-encounter-modal', {
132
+ close: () => dispose(),
133
+ encounterTypeName: encounterTypeName || '',
134
+ onConfirmation: () => {
135
+ const abortController = new AbortController();
136
+ deleteEncounter(encounterUuid, abortController)
137
+ .then(() => {
138
+ // Update current visit data for critical components
139
+ mutateVisitContext?.();
140
+
141
+ // Also invalidate visit history and encounter tables since the encounter was deleted
142
+ invalidateVisitAndEncounterData(mutate, patientUuid);
143
+
144
+ showSnackbar({
145
+ isLowContrast: true,
146
+ title: t('encounterDeleted', 'Encounter deleted'),
147
+ subtitle: t('encounterSuccessfullyDeleted', 'The encounter has been deleted successfully'),
148
+ kind: 'success',
149
+ });
150
+ })
151
+ .catch(() => {
152
+ showSnackbar({
153
+ isLowContrast: false,
154
+ title: t('error', 'Error'),
155
+ subtitle: t(
156
+ 'encounterWithError',
157
+ 'The encounter could not be deleted successfully. If the error persists, please contact your system administrator.',
158
+ ),
159
+ kind: 'error',
160
+ });
161
+ });
162
+ dispose();
163
+ },
164
+ });
165
+ },
166
+ [mutate, mutateVisitContext, patientUuid, t],
167
+ );
168
+
169
+ const handlePrintSelected = (selectedRows: Array<any>) => {
170
+ const selectedEncounterUuids = selectedRows.map((row) => row.id);
171
+ setIsPrinting(true);
172
+ downloadPdf(selectedEncounterUuids, t).finally(() => setIsPrinting(false));
173
+ };
174
+
175
+ if (isLoadingEncounterTypes || isLoading) {
176
+ return <DataTableSkeleton role="progressbar" zebra />;
177
+ }
178
+
179
+ return (
180
+ <div className={styles.container}>
181
+ <DataTable
182
+ headers={tableHeaders}
183
+ overflowMenuOnHover={desktopLayout}
184
+ rows={paginatedMappedEncounters ?? []}
185
+ size={responsiveSize}
186
+ useZebraStyles={totalCount > 1}
187
+ >
188
+ {({
189
+ rows,
190
+ headers,
191
+ getHeaderProps,
192
+ getRowProps,
193
+ getExpandHeaderProps,
194
+ getToolbarProps,
195
+ getTableProps,
196
+ getSelectionProps,
197
+ selectedRows,
198
+ }: {
199
+ headers: Array<{ header: React.ReactNode; key: string }>;
200
+ rows: Array<{ id: string; isExpanded: boolean; cells: Array<{ id: string; value: React.ReactNode }> }>;
201
+ [key: string]: any;
202
+ }) => {
203
+ const selectedRowsCount = selectedRows.length;
204
+ return (
205
+ <TableContainer className={styles.tableContainer}>
206
+ {showEncounterTypeFilter && (
207
+ <TableToolbar {...getToolbarProps()}>
208
+ <TableToolbarContent>
209
+ <div className={styles.filterContainer}>
210
+ <ComboBox
211
+ aria-label={t('filterByEncounterType', 'Filter by encounter type')}
212
+ className={styles.substitutionType}
213
+ id="encounterTypeFilter"
214
+ items={encounterTypes}
215
+ itemToString={(item: EncounterType) => item?.display}
216
+ onChange={({ selectedItem }) => setEncounterTypeToFilter(selectedItem)}
217
+ placeholder={t('filterByEncounterType', 'Filter by encounter type')}
218
+ selectedItem={encounterTypeToFilter}
219
+ size={responsiveSize}
220
+ />
221
+ </div>
222
+ {isSelectable && canPrintEncounters && (
223
+ <Button
224
+ kind="ghost"
225
+ size={responsiveSize}
226
+ renderIcon={PrinterIcon}
227
+ disabled={selectedRowsCount === 0 || isPrinting}
228
+ onClick={() => handlePrintSelected(selectedRows)}
229
+ >
230
+ {isPrinting ? t('generating', 'Generating...') : t('printSelected', 'Print selected')}
231
+ </Button>
232
+ )}
233
+ </TableToolbarContent>
234
+ </TableToolbar>
235
+ )}
236
+ <Table {...getTableProps()}>
237
+ <TableHead>
238
+ <TableRow>
239
+ <TableExpandHeader enableToggle {...getExpandHeaderProps()} />
240
+ {isSelectable && canPrintEncounters && <TableSelectAll {...getSelectionProps()} />}
241
+ {headers.map((header, i) => (
242
+ <TableHeader className={styles.tableHeader} key={i} {...getHeaderProps({ header })}>
243
+ {header.header}
244
+ </TableHeader>
245
+ ))}
246
+ <TableHeader aria-label={t('actions', 'Actions')} />
247
+ </TableRow>
248
+ </TableHead>
249
+ <TableBody>
250
+ {rows?.map((row) => {
251
+ const encounter = encountersByUuid.get(row.id);
252
+
253
+ if (!encounter) return null;
254
+
255
+ const isVisitNoteEncounter = (encounter: MappedEncounter) =>
256
+ encounter.encounterType === 'Visit Note' && !encounter.form;
257
+
258
+ const supportsEmbeddedFormView = (encounter: MappedEncounter) =>
259
+ encounter.form?.uuid &&
260
+ encounter.form.resources?.some((resource) => resource.name === jsonSchemaResourceName);
261
+
262
+ const encounterAgeInMinutes =
263
+ (Date.now() - new Date(encounter.rawDatetime).getTime()) / (1000 * 60);
264
+
265
+ const canDeleteEncounter =
266
+ userHasAccess(encounter.editPrivilege, session?.user) &&
267
+ (encounterEditableDuration === 0 ||
268
+ (encounterEditableDuration > 0 && encounterAgeInMinutes <= encounterEditableDuration) ||
269
+ encounterEditableDurationOverridePrivileges.some((privilege) =>
270
+ userHasAccess(privilege, session?.user),
271
+ ));
272
+
273
+ const canEditEncounter =
274
+ canDeleteEncounter && (encounter.form?.uuid || isVisitNoteEncounter(encounter));
275
+
276
+ const canPrintEncounter = canPrintEncounters && supportsEmbeddedFormView(encounter);
277
+
278
+ return (
279
+ <React.Fragment key={encounter.id}>
280
+ <TableExpandRow {...getRowProps({ row })}>
281
+ {isSelectable && canPrintEncounters && <TableSelectRow {...getSelectionProps({ row })} />}
282
+ {row.cells.map((cell) => (
283
+ <TableCell key={cell.id}>{cell.value}</TableCell>
284
+ ))}
285
+ <TableCell className="cds--table-column-menu">
286
+ <Layer className={styles.layer}>
287
+ {(canDeleteEncounter || canPrintEncounter) && (
288
+ <OverflowMenu
289
+ aria-label={t('encounterTableActionsMenu', 'Encounter table actions menu')}
290
+ flipped
291
+ size={responsiveSize}
292
+ align="left"
293
+ >
294
+ {canEditEncounter && (
295
+ <OverflowMenuItem
296
+ className={styles.menuItem}
297
+ itemText={t('editThisEncounter', 'Edit this encounter')}
298
+ onClick={() => {
299
+ if (isVisitNoteEncounter(encounter)) {
300
+ launchWorkspace2('visit-notes-form-workspace', {
301
+ encounter,
302
+ formContext: 'editing',
303
+ patientUuid,
304
+ });
305
+ } else {
306
+ launchWorkspace2('patient-form-entry-workspace', {
307
+ form: encounter.form,
308
+ encounterUuid: encounter.id,
309
+ });
310
+ }
311
+ }}
312
+ />
313
+ )}
314
+ {canPrintEncounter && (
315
+ <OverflowMenuItem
316
+ className={styles.menuItem}
317
+ itemText={t('printEncounter', 'Print this encounter')}
318
+ disabled={isPrinting}
319
+ onClick={() => {
320
+ setIsPrinting(true);
321
+ downloadPdf([encounter.id], t).finally(() => setIsPrinting(false));
322
+ }}
323
+ />
324
+ )}
325
+ {canDeleteEncounter && (
326
+ <OverflowMenuItem
327
+ className={styles.menuItem}
328
+ hasDivider
329
+ isDelete
330
+ itemText={t('deleteThisEncounter', 'Delete this encounter')}
331
+ onClick={() => handleDeleteEncounter(encounter.id, encounter.form?.display)}
332
+ />
333
+ )}
334
+ </OverflowMenu>
335
+ )}
336
+ </Layer>
337
+ </TableCell>
338
+ </TableExpandRow>
339
+ {row.isExpanded ? (
340
+ <TableExpandedRow
341
+ className={styles.expandedRow}
342
+ colSpan={headers.length + (isSelectable ? 3 : 2)}
343
+ >
344
+ <>
345
+ {enableEmbeddedFormView && supportsEmbeddedFormView(encounter) ? (
346
+ <ExtensionSlot
347
+ name="form-widget-slot"
348
+ state={{
349
+ additionalProps: { mode: 'embedded-view' },
350
+ visitUuid: encounter.visitUuid ?? null,
351
+ visitTypeUuid: encounter.visitTypeUuid ?? null,
352
+ visitStartDatetime: encounter.visitStartDatetime ?? null,
353
+ visitStopDatetime: encounter.visitStopDatetime ?? null,
354
+ patientUuid: patientUuid,
355
+ patient: patient,
356
+ formUuid: encounter.form.uuid,
357
+ encounterUuid: encounter.id,
358
+ promptBeforeClosing: () => {},
359
+ }}
360
+ />
361
+ ) : (
362
+ <EncounterObservations observations={encounter.obs} />
363
+ )}
364
+ <>
365
+ {canEditEncounter && (
366
+ <Button
367
+ kind="ghost"
368
+ onClick={() => {
369
+ if (isVisitNoteEncounter(encounter)) {
370
+ launchWorkspace2('visit-notes-form-workspace', {
371
+ encounter,
372
+ formContext: 'editing',
373
+ patientUuid,
374
+ });
375
+ } else {
376
+ launchWorkspace2('patient-form-entry-workspace', {
377
+ form: encounter.form,
378
+ encounterUuid: encounter.id,
379
+ });
380
+ }
381
+ }}
382
+ renderIcon={(props: ComponentProps<typeof EditIcon>) => (
383
+ <EditIcon size={16} {...props} />
384
+ )}
385
+ >
386
+ {t('editThisEncounter', 'Edit this encounter')}
387
+ </Button>
388
+ )}
389
+ {canDeleteEncounter && (
390
+ <Button
391
+ kind="danger--ghost"
392
+ onClick={() => handleDeleteEncounter(encounter.id, encounter.form?.display)}
393
+ renderIcon={(props: ComponentProps<typeof TrashCanIcon>) => (
394
+ <TrashCanIcon size={16} {...props} />
395
+ )}
396
+ >
397
+ {t('deleteThisEncounter', 'Delete this encounter')}
398
+ </Button>
399
+ )}
400
+ </>
401
+ </>
402
+ </TableExpandedRow>
403
+ ) : (
404
+ <TableExpandedRow
405
+ className={styles.hiddenRow}
406
+ colSpan={headers.length + (isSelectable ? 3 : 2)}
407
+ />
408
+ )}
409
+ </React.Fragment>
410
+ );
411
+ })}
412
+ </TableBody>
413
+ </Table>
414
+ {rows?.length === 0 && (
415
+ <div className={styles.tileContainer}>
416
+ <Tile className={styles.tile}>
417
+ <div className={styles.tileContent}>
418
+ <p className={styles.content}>{t('noEncountersToDisplay', 'No encounters to display')}</p>
419
+ {showEncounterTypeFilter && encounterTypeToFilter && (
420
+ <p className={styles.helper}>{t('checkFilters', 'Check the filters above')}</p>
421
+ )}
422
+ </div>
423
+ </Tile>
424
+ </div>
425
+ )}
426
+ </TableContainer>
427
+ );
428
+ }}
429
+ </DataTable>
430
+ {
431
+ <Pagination
432
+ forwardText={t('nextPage', 'Next page')}
433
+ backwardText={t('previousPage', 'Previous page')}
434
+ page={currentPage}
435
+ pageSize={pageSize}
436
+ pageSizes={pageSizes}
437
+ totalItems={totalCount}
438
+ onChange={({ pageSize: newPageSize, page }) => {
439
+ if (newPageSize !== pageSize) {
440
+ setPageSize(newPageSize);
441
+ }
442
+ if (page !== currentPage) {
443
+ goTo(page);
444
+ }
445
+ }}
446
+ />
447
+ }
448
+ </div>
449
+ );
450
+ };
451
+
452
+ export default EncountersTable;
@@ -0,0 +1,156 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
3
+ import { useAllEncounters, encounterHasJsonSchemaForm } from './encounters-table.resource';
4
+
5
+ const mockUseOpenmrsFetchAll = vi.fn();
6
+
7
+ vi.mock('@openmrs/esm-framework', () => ({
8
+ makeUrl: vi.fn((path: string) => `http://localhost/${path}`),
9
+ restBaseUrl: '/ws/rest/v1',
10
+ useOpenmrsFetchAll: (...args: unknown[]) => mockUseOpenmrsFetchAll(...args),
11
+ }));
12
+
13
+ describe('encounterHasJsonSchemaForm', () => {
14
+ it('returns true when encounter has form with JSON schema resource', () => {
15
+ const encounter = {
16
+ uuid: 'encounter-1',
17
+ form: {
18
+ uuid: 'form-1',
19
+ display: 'Test Form',
20
+ resources: [{ name: 'JSON schema', valueReference: '{}' }],
21
+ },
22
+ } as any;
23
+
24
+ expect(encounterHasJsonSchemaForm(encounter)).toBe(true);
25
+ });
26
+
27
+ it('returns false when encounter has no form', () => {
28
+ const encounter = {
29
+ uuid: 'encounter-1',
30
+ form: null,
31
+ } as any;
32
+
33
+ expect(encounterHasJsonSchemaForm(encounter)).toBe(false);
34
+ });
35
+
36
+ it('returns false when form is undefined', () => {
37
+ const encounter = {
38
+ uuid: 'encounter-1',
39
+ } as any;
40
+
41
+ expect(encounterHasJsonSchemaForm(encounter)).toBe(false);
42
+ });
43
+
44
+ it('returns false when form has no resources array', () => {
45
+ const encounter = {
46
+ uuid: 'encounter-1',
47
+ form: {
48
+ uuid: 'form-1',
49
+ display: 'Test Form',
50
+ },
51
+ } as any;
52
+
53
+ expect(encounterHasJsonSchemaForm(encounter)).toBe(false);
54
+ });
55
+
56
+ it('returns false when form has empty resources array', () => {
57
+ const encounter = {
58
+ uuid: 'encounter-1',
59
+ form: {
60
+ uuid: 'form-1',
61
+ display: 'Test Form',
62
+ resources: [],
63
+ },
64
+ } as any;
65
+
66
+ expect(encounterHasJsonSchemaForm(encounter)).toBe(false);
67
+ });
68
+
69
+ it('returns false when resources do not contain JSON schema', () => {
70
+ const encounter = {
71
+ uuid: 'encounter-1',
72
+ form: {
73
+ uuid: 'form-1',
74
+ display: 'Test Form',
75
+ resources: [{ name: 'Some other resource', valueReference: '{}' }],
76
+ },
77
+ } as any;
78
+
79
+ expect(encounterHasJsonSchemaForm(encounter)).toBe(false);
80
+ });
81
+
82
+ it('returns true when resources contain multiple items including JSON schema', () => {
83
+ const encounter = {
84
+ uuid: 'encounter-1',
85
+ form: {
86
+ uuid: 'form-1',
87
+ display: 'Test Form',
88
+ resources: [
89
+ { name: 'XML template', valueReference: '<xml/>' },
90
+ { name: 'JSON schema', valueReference: '{}' },
91
+ { name: 'Metadata', valueReference: 'meta' },
92
+ ],
93
+ },
94
+ } as any;
95
+
96
+ expect(encounterHasJsonSchemaForm(encounter)).toBe(true);
97
+ });
98
+ });
99
+
100
+ describe('useAllEncounters', () => {
101
+ beforeEach(() => {
102
+ vi.clearAllMocks();
103
+ });
104
+
105
+ it('calls useOpenmrsFetchAll with correct URL when patientUuid is provided', () => {
106
+ const mockData = [
107
+ { uuid: 'enc-1', display: 'Encounter 1' },
108
+ { uuid: 'enc-2', display: 'Encounter 2' },
109
+ ];
110
+
111
+ mockUseOpenmrsFetchAll.mockReturnValue({
112
+ data: mockData,
113
+ isLoading: false,
114
+ error: undefined,
115
+ } as any);
116
+
117
+ renderHook(() => useAllEncounters('patient-123'));
118
+
119
+ expect(mockUseOpenmrsFetchAll).toHaveBeenCalled();
120
+ const callArg = mockUseOpenmrsFetchAll.mock.calls[0][0];
121
+ expect(callArg).toContain('patient=patient-123');
122
+ expect(callArg).toContain('order=desc');
123
+ });
124
+
125
+ it('passes encounterType filter to the URL when provided', () => {
126
+ mockUseOpenmrsFetchAll.mockReturnValue({
127
+ data: [],
128
+ isLoading: false,
129
+ error: undefined,
130
+ } as any);
131
+
132
+ renderHook(() => useAllEncounters('patient-123', 'encounter-type-uuid'));
133
+
134
+ expect(mockUseOpenmrsFetchAll).toHaveBeenCalled();
135
+ const callArg = mockUseOpenmrsFetchAll.mock.calls[0][0];
136
+ expect(callArg).toContain('encounterType=encounter-type-uuid');
137
+ });
138
+
139
+ it('includes custom representation in the URL', () => {
140
+ mockUseOpenmrsFetchAll.mockReturnValue({
141
+ data: [],
142
+ isLoading: false,
143
+ error: undefined,
144
+ } as any);
145
+
146
+ renderHook(() => useAllEncounters('patient-123'));
147
+
148
+ expect(mockUseOpenmrsFetchAll).toHaveBeenCalled();
149
+ const callArg = mockUseOpenmrsFetchAll.mock.calls[0][0];
150
+ expect(callArg).toContain('v=custom');
151
+ expect(callArg).toContain('encounterDatetime');
152
+ expect(callArg).toContain('form');
153
+ expect(callArg).toContain('obs');
154
+ expect(callArg).toContain('encounterProviders');
155
+ });
156
+ });