@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,47 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button } from '@carbon/react';
4
+ import { launchWorkspace2, showSnackbar } from '@openmrs/esm-framework';
5
+
6
+ interface StartVisitButtonProps {
7
+ patientUuid: string;
8
+ handleReturnToSearchList?: () => void;
9
+ hidePatientSearch?: () => void;
10
+ }
11
+
12
+ /**
13
+ * This button shows up in search results patient cards for patients with no active visit
14
+ */
15
+ const StartVisitButton = ({ patientUuid, handleReturnToSearchList, hidePatientSearch }: StartVisitButtonProps) => {
16
+ const { t } = useTranslation();
17
+ const startVisitWorkspaceForm = 'start-visit-workspace-form';
18
+
19
+ const handleStartVisit = useCallback(() => {
20
+ hidePatientSearch?.();
21
+
22
+ try {
23
+ launchWorkspace2(startVisitWorkspaceForm, {
24
+ patientUuid,
25
+ openedFrom: 'patient-chart-start-visit',
26
+ handleReturnToSearchList,
27
+ });
28
+ } catch (error) {
29
+ console.error('Error launching visit form workspace:', error);
30
+
31
+ showSnackbar({
32
+ isLowContrast: false,
33
+ kind: 'error',
34
+ title: t('errorStartingVisit', 'Error starting visit'),
35
+ subtitle: error.message ?? t('errorStartingVisitDescription', 'An error occurred while starting the visit'),
36
+ });
37
+ }
38
+ }, [patientUuid, t, handleReturnToSearchList, hidePatientSearch]);
39
+
40
+ return (
41
+ <Button aria-label={t('startVisit', 'Start visit')} kind="primary" onClick={handleStartVisit}>
42
+ {t('startVisit', 'Start visit')}
43
+ </Button>
44
+ );
45
+ };
46
+
47
+ export default StartVisitButton;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { vi, describe, it, expect } from 'vitest';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { render, screen } from '@testing-library/react';
5
+ import { launchWorkspace2 } from '@openmrs/esm-framework';
6
+ import { mockPatient } from 'tools';
7
+ import StartVisitButton from './start-visit-button.component';
8
+
9
+ const mockLaunchWorkspace = vi.mocked(launchWorkspace2);
10
+
11
+ describe('StartVisitButton', () => {
12
+ it('renders the start visit button', () => {
13
+ render(<StartVisitButton patientUuid={mockPatient.id} />);
14
+
15
+ expect(screen.getByRole('button', { name: /start visit/i })).toBeInTheDocument();
16
+ });
17
+
18
+ it('clicking the button launches the start visit form', async () => {
19
+ const user = userEvent.setup();
20
+
21
+ render(<StartVisitButton patientUuid={mockPatient.id} />);
22
+
23
+ const startVisitButton = screen.getByRole('button', { name: /start visit/i });
24
+ await user.click(startVisitButton);
25
+
26
+ expect(mockLaunchWorkspace).toHaveBeenCalledTimes(1);
27
+ expect(mockLaunchWorkspace).toHaveBeenCalledWith('start-visit-workspace-form', {
28
+ patientUuid: mockPatient.id,
29
+ openedFrom: 'patient-chart-start-visit',
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { Button, IconButton } from '@carbon/react';
3
+ import {
4
+ TrashCanIcon,
5
+ UserHasAccess,
6
+ type Visit,
7
+ getCoreTranslation,
8
+ showModal,
9
+ useLayoutType,
10
+ } from '@openmrs/esm-framework';
11
+ import { useTranslation } from 'react-i18next';
12
+
13
+ interface DeleteVisitActionItemProps {
14
+ patientUuid: string;
15
+ visit: Visit;
16
+
17
+ /**
18
+ * If true, renders as IconButton instead
19
+ */
20
+ compact?: boolean;
21
+ }
22
+
23
+ const DeleteVisitActionItem: React.FC<DeleteVisitActionItemProps> = ({ visit, compact }) => {
24
+ const { t } = useTranslation();
25
+ const isTablet = useLayoutType() === 'tablet';
26
+ const responsiveSize = isTablet ? 'lg' : 'sm';
27
+
28
+ const deleteVisit = () => {
29
+ const dispose = showModal('delete-visit-dialog', {
30
+ visit,
31
+ closeModal: () => dispose(),
32
+ });
33
+ };
34
+
35
+ if (visit?.encounters?.length) {
36
+ return null;
37
+ }
38
+
39
+ return (
40
+ <UserHasAccess privilege="Delete Visits">
41
+ {compact ? (
42
+ <IconButton
43
+ onClick={deleteVisit}
44
+ label={getCoreTranslation('delete')}
45
+ kind="ghost"
46
+ size={responsiveSize}
47
+ align="top-end"
48
+ >
49
+ <TrashCanIcon size={16} />
50
+ </IconButton>
51
+ ) : (
52
+ <Button onClick={deleteVisit} kind="danger--ghost" renderIcon={TrashCanIcon} size={responsiveSize}>
53
+ {t('deleteVisit', 'Delete visit')}
54
+ </Button>
55
+ )}
56
+ </UserHasAccess>
57
+ );
58
+ };
59
+
60
+ export default DeleteVisitActionItem;
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { vi, describe, it, expect } from 'vitest';
3
+ import { render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { showModal, type Visit } from '@openmrs/esm-framework';
6
+ import { mockCurrentVisit } from '__mocks__';
7
+ import DeleteVisitActionItem from './delete-visit-action-item.component';
8
+
9
+ const mockShowModal = vi.mocked(showModal);
10
+
11
+ describe('DeleteVisitActionItem', () => {
12
+ it('renders a delete visit button when the visit has no encounters', () => {
13
+ render(<DeleteVisitActionItem patientUuid="some-uuid" visit={mockCurrentVisit} />);
14
+
15
+ expect(screen.getByRole('button', { name: /delete visit/i })).toBeInTheDocument();
16
+ });
17
+
18
+ it('renders a compact icon button when compact is true', () => {
19
+ render(<DeleteVisitActionItem patientUuid="some-uuid" visit={mockCurrentVisit} compact />);
20
+
21
+ expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
22
+ });
23
+
24
+ it('renders nothing when the visit has encounters', () => {
25
+ const visitWithEncounters = {
26
+ ...mockCurrentVisit,
27
+ encounters: [{ uuid: 'enc-1' }],
28
+ } as Visit;
29
+
30
+ const { container } = render(<DeleteVisitActionItem patientUuid="some-uuid" visit={visitWithEncounters} />);
31
+
32
+ expect(container).toBeEmptyDOMElement();
33
+ });
34
+
35
+ it('opens the delete visit modal with the visit when clicked', async () => {
36
+ const user = userEvent.setup();
37
+ mockShowModal.mockReturnValue(vi.fn());
38
+
39
+ render(<DeleteVisitActionItem patientUuid="some-uuid" visit={mockCurrentVisit} />);
40
+
41
+ await user.click(screen.getByRole('button', { name: /delete visit/i }));
42
+
43
+ expect(mockShowModal).toHaveBeenCalledWith('delete-visit-dialog', {
44
+ visit: mockCurrentVisit,
45
+ closeModal: expect.any(Function),
46
+ });
47
+ });
48
+ });
@@ -0,0 +1,79 @@
1
+ import React from 'react';
2
+ import { Button, IconButton } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {
5
+ EditIcon,
6
+ UserHasAccess,
7
+ type Visit,
8
+ getCoreTranslation,
9
+ launchWorkspace2,
10
+ useLayoutType,
11
+ } from '@openmrs/esm-framework';
12
+ import { type VisitFormProps } from '../visit-form/visit-form.workspace';
13
+ import {
14
+ invalidateVisitAndEncounterData,
15
+ invalidateVisitByUuid,
16
+ type PatientWorkspaceGroupProps,
17
+ } from '@openmrs/esm-patient-common-lib';
18
+ import { useSWRConfig } from 'swr';
19
+
20
+ interface EditVisitDetailsActionItemProps {
21
+ visit: Visit;
22
+ patient: fhir.Patient;
23
+
24
+ /**
25
+ * If true, renders as IconButton instead
26
+ */
27
+ compact?: boolean;
28
+ }
29
+
30
+ /**
31
+ * This component
32
+ */
33
+ const EditVisitDetailsActionItem: React.FC<EditVisitDetailsActionItemProps> = ({ visit, patient, compact }) => {
34
+ const { t } = useTranslation();
35
+ const { mutate: globalMutate } = useSWRConfig();
36
+
37
+ const isTablet = useLayoutType() === 'tablet';
38
+ const responsiveSize = isTablet ? 'lg' : 'sm';
39
+ const patientUuid = patient.id;
40
+
41
+ const editVisitDetails = () => {
42
+ launchWorkspace2<VisitFormProps, {}, PatientWorkspaceGroupProps>(
43
+ 'start-visit-workspace-form',
44
+ { openedFrom: 'patient-chart-edit-visit' },
45
+ {},
46
+ {
47
+ patient,
48
+ patientUuid: patientUuid,
49
+ visitContext: visit,
50
+ mutateVisitContext: () => {
51
+ invalidateVisitByUuid(globalMutate, visit.uuid);
52
+ invalidateVisitAndEncounterData(globalMutate, patientUuid);
53
+ },
54
+ },
55
+ );
56
+ };
57
+
58
+ return (
59
+ <UserHasAccess privilege="Edit Visits">
60
+ {compact ? (
61
+ <IconButton
62
+ onClick={editVisitDetails}
63
+ label={getCoreTranslation('edit')}
64
+ size={responsiveSize}
65
+ kind="ghost"
66
+ align="top-end"
67
+ >
68
+ <EditIcon size={16} />
69
+ </IconButton>
70
+ ) : (
71
+ <Button onClick={editVisitDetails} kind="ghost" renderIcon={EditIcon} size={responsiveSize}>
72
+ {t('editVisitDetails', 'Edit visit details')}
73
+ </Button>
74
+ )}
75
+ </UserHasAccess>
76
+ );
77
+ };
78
+
79
+ export default EditVisitDetailsActionItem;
@@ -0,0 +1,121 @@
1
+ import React, { useState, useMemo, useCallback, useEffect, useRef, type ChangeEvent } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Layer, RadioButton, RadioButtonGroup, Search, StructuredListSkeleton, Tile } from '@carbon/react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useFormContext, Controller } from 'react-hook-form';
6
+ import { PatientChartPagination } from '@openmrs/esm-patient-common-lib';
7
+ import { useDebounce, useLayoutType, usePagination, type VisitType } from '@openmrs/esm-framework';
8
+ import { type VisitFormData } from './visit-form.resource';
9
+ import styles from './base-visit-type.scss';
10
+
11
+ interface BaseVisitTypeProps {
12
+ visitTypes: Array<VisitType>;
13
+ }
14
+
15
+ const BaseVisitType: React.FC<BaseVisitTypeProps> = ({ visitTypes }) => {
16
+ const { t } = useTranslation();
17
+ const { control, setValue } = useFormContext<VisitFormData>();
18
+ const isTablet = useLayoutType() === 'tablet';
19
+ const [searchTerm, setSearchTerm] = useState<string>('');
20
+ const debouncedSearchTerm = useDebounce(searchTerm, 300);
21
+
22
+ const searchResults = useMemo(() => {
23
+ if (!debouncedSearchTerm.trim()) {
24
+ return visitTypes;
25
+ }
26
+ const lowercasedTerm = debouncedSearchTerm.toLowerCase();
27
+ return visitTypes.filter((visitType) => visitType.display.toLowerCase().includes(lowercasedTerm));
28
+ }, [debouncedSearchTerm, visitTypes]);
29
+
30
+ const { results, currentPage, goTo } = usePagination(searchResults, 5);
31
+ const hasNoMatchingSearchResults = debouncedSearchTerm.trim() !== '' && searchResults.length === 0;
32
+ const prevSearchTermRef = useRef(debouncedSearchTerm);
33
+
34
+ const handleSearchTermChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
35
+ setSearchTerm(e.target.value);
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ if (searchResults?.length === 1) {
40
+ setValue('visitType', searchResults[0].uuid);
41
+ }
42
+ }, [searchResults, setValue]);
43
+
44
+ // Reset pagination to page 1 when search term changes
45
+ useEffect(() => {
46
+ if (prevSearchTermRef.current !== debouncedSearchTerm && currentPage !== 1) {
47
+ goTo(1);
48
+ }
49
+ prevSearchTermRef.current = debouncedSearchTerm;
50
+ }, [debouncedSearchTerm, currentPage, goTo]);
51
+
52
+ const searchComponent = (
53
+ <Search
54
+ labelText={t('searchForAVisitType', 'Search for a visit type')}
55
+ onChange={handleSearchTermChange}
56
+ placeholder={t('searchForAVisitType', 'Search for a visit type')}
57
+ value={searchTerm}
58
+ />
59
+ );
60
+
61
+ return (
62
+ <div className={classNames(styles.visitTypeOverviewWrapper, isTablet ? styles.tablet : styles.desktop)}>
63
+ {visitTypes.length ? (
64
+ <>
65
+ {isTablet ? <Layer>{searchComponent}</Layer> : searchComponent}
66
+
67
+ {hasNoMatchingSearchResults ? (
68
+ <div className={styles.tileContainer}>
69
+ <Tile className={styles.tile}>
70
+ <div className={styles.tileContent}>
71
+ <p className={styles.content}>{t('noVisitTypesToDisplay', 'No visit types to display')}</p>
72
+ <p className={styles.helper}>{t('checkFilters', 'Check the filters above')}</p>
73
+ </div>
74
+ </Tile>
75
+ </div>
76
+ ) : (
77
+ <Controller
78
+ name="visitType"
79
+ control={control}
80
+ render={({ field: { onChange, value } }) => (
81
+ <RadioButtonGroup
82
+ className={styles.radioButtonGroup}
83
+ name="visit-types"
84
+ onChange={onChange}
85
+ orientation="vertical"
86
+ valueSelected={value}
87
+ >
88
+ {results.map(({ display, uuid }) => (
89
+ <RadioButton
90
+ className={styles.radioButton}
91
+ id={`visit-type-${uuid}`}
92
+ key={uuid}
93
+ labelText={display}
94
+ value={uuid}
95
+ />
96
+ ))}
97
+ </RadioButtonGroup>
98
+ )}
99
+ />
100
+ )}
101
+
102
+ {!hasNoMatchingSearchResults && (
103
+ <div className={styles.paginationContainer}>
104
+ <PatientChartPagination
105
+ currentItems={results.length}
106
+ onPageNumberChange={({ page }) => goTo(page)}
107
+ pageNumber={currentPage}
108
+ pageSize={5}
109
+ totalItems={searchResults.length}
110
+ />
111
+ </div>
112
+ )}
113
+ </>
114
+ ) : (
115
+ <StructuredListSkeleton className={styles.skeleton} />
116
+ )}
117
+ </div>
118
+ );
119
+ };
120
+
121
+ export default BaseVisitType;
@@ -0,0 +1,75 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .tablet {
7
+ background-color: $ui-02;
8
+ }
9
+
10
+ .desktop {
11
+ background-color: $ui-01;
12
+
13
+ .paginationContainer div {
14
+ background-color: $ui-01;
15
+ }
16
+ }
17
+
18
+ .visitTypeOverviewWrapper {
19
+ margin: layout.$spacing-03 0;
20
+ border: 0.0625rem solid $grey-2;
21
+
22
+ &:has(.skeleton) {
23
+ border-bottom: none !important;
24
+ }
25
+ }
26
+
27
+ .visitTypeOverviewWrapper div:nth-child(3) > div:nth-child(2) {
28
+ position: relative;
29
+ }
30
+
31
+ .visitTypeOverviewWrapper div:nth-child(3) span * {
32
+ display: none;
33
+ }
34
+
35
+ .radioButtonGroup {
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: flex-start;
39
+ margin-top: layout.$spacing-03;
40
+ min-height: layout.$spacing-10;
41
+ width: 100%;
42
+ @include type.type-style('body-compact-01');
43
+ }
44
+
45
+ .radioButton {
46
+ padding: layout.$spacing-02 layout.$spacing-05;
47
+ margin: layout.$spacing-03 0;
48
+ }
49
+
50
+ .tileContainer {
51
+ background-color: $ui-02;
52
+ padding: layout.$spacing-09 0;
53
+ }
54
+
55
+ .tile {
56
+ margin: auto;
57
+ width: fit-content;
58
+ }
59
+
60
+ .tileContent {
61
+ display: flex;
62
+ flex-direction: column;
63
+ align-items: center;
64
+ }
65
+
66
+ .content {
67
+ @include type.type-style('heading-compact-02');
68
+ color: $text-02;
69
+ margin-bottom: layout.$spacing-03;
70
+ }
71
+
72
+ .helper {
73
+ @include type.type-style('body-compact-01');
74
+ color: $text-02;
75
+ }
@@ -0,0 +1,153 @@
1
+ import React from 'react';
2
+ import { vi, describe, it, expect, test } from 'vitest';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { screen, render } from '@testing-library/react';
5
+ import { useVisitTypes } from '@openmrs/esm-framework';
6
+ import { mockVisitTypes } from '__mocks__';
7
+ import BaseVisitType from './base-visit-type.component';
8
+
9
+ vi.mock('lodash-es/debounce', () => vi.fn((fn) => fn));
10
+
11
+ const mockUseVisitTypes = vi.mocked(useVisitTypes);
12
+
13
+ vi.mock('react-hook-form', async () => ({
14
+ ...((await vi.importActual('react-hook-form')) as object),
15
+ useFormContext: vi.fn().mockImplementation(() => ({
16
+ handleSubmit: () => vi.fn(),
17
+ control: {
18
+ register: vi.fn(),
19
+ unregister: vi.fn(),
20
+ getFieldState: vi.fn(),
21
+ _names: {
22
+ array: new Set('test'),
23
+ mount: new Set('test'),
24
+ unMount: new Set('test'),
25
+ watch: new Set('test'),
26
+ focus: 'test',
27
+ watchAll: false,
28
+ },
29
+ _subjects: {
30
+ watch: vi.fn(),
31
+ array: vi.fn(),
32
+ state: vi.fn(),
33
+ },
34
+ _getWatch: vi.fn(),
35
+ _formValues: [],
36
+ _defaultValues: [],
37
+ },
38
+ getValues: () => {
39
+ return [];
40
+ },
41
+ setValue: () => vi.fn(),
42
+ formState: () => vi.fn(),
43
+ watch: () => vi.fn(),
44
+ })),
45
+ Controller: ({ render }) =>
46
+ render({
47
+ field: {
48
+ onChange: vi.fn(),
49
+ onBlur: vi.fn(),
50
+ value: '',
51
+ ref: vi.fn(),
52
+ },
53
+ formState: {
54
+ isSubmitted: false,
55
+ },
56
+ fieldState: {
57
+ isTouched: false,
58
+ },
59
+ }),
60
+ useSubscribe: () => ({
61
+ r: { current: { subject: { subscribe: () => vi.fn() } } },
62
+ }),
63
+ }));
64
+
65
+ describe('VisitTypeOverview', () => {
66
+ const renderVisitTypeOverview = () => {
67
+ mockUseVisitTypes.mockReturnValue(mockVisitTypes);
68
+ render(<BaseVisitType visitTypes={mockVisitTypes} />);
69
+ };
70
+
71
+ it('renders a list of the available visit types', () => {
72
+ renderVisitTypeOverview();
73
+
74
+ mockVisitTypes.forEach((visitType) => {
75
+ const radio = screen.getByRole('radio', { name: new RegExp(visitType.display, 'i') });
76
+ expect(radio).toBeInTheDocument();
77
+ expect(radio).not.toBeChecked();
78
+ });
79
+ });
80
+
81
+ it('allows keyboard navigation through visit types', async () => {
82
+ const user = userEvent.setup();
83
+
84
+ renderVisitTypeOverview();
85
+
86
+ const firstVisitType = screen.getByRole('radio', { name: new RegExp(mockVisitTypes[0].display, 'i') });
87
+ firstVisitType.focus();
88
+
89
+ await user.keyboard('{ArrowDown}');
90
+ expect(screen.getByRole('radio', { name: new RegExp(mockVisitTypes[1].display, 'i') })).toHaveFocus();
91
+
92
+ await user.keyboard('{ArrowUp}');
93
+ expect(firstVisitType).toHaveFocus();
94
+ });
95
+
96
+ it('clears the search input when the clear button is clicked', async () => {
97
+ const user = userEvent.setup();
98
+
99
+ renderVisitTypeOverview();
100
+
101
+ const searchInput = screen.getByRole('searchbox');
102
+ await user.type(searchInput, 'HIV');
103
+
104
+ const clearButton = screen.getByRole('button', { name: /clear/i });
105
+ await user.click(clearButton);
106
+
107
+ expect(searchInput).toHaveValue('');
108
+ mockVisitTypes.forEach((visitType) => {
109
+ expect(screen.getByRole('radio', { name: new RegExp(visitType.display, 'i') })).toBeInTheDocument();
110
+ });
111
+ });
112
+
113
+ it('searches for a matching visit type when the user types in the search input', async () => {
114
+ const user = userEvent.setup();
115
+
116
+ renderVisitTypeOverview();
117
+
118
+ const hivVisit = screen.getByRole('radio', { name: /HIV Return Visit/i });
119
+ const outpatientVisit = screen.getByRole('radio', { name: /Outpatient Visit/i });
120
+
121
+ expect(outpatientVisit).toBeInTheDocument();
122
+ expect(hivVisit).toBeInTheDocument();
123
+
124
+ const searchInput = screen.getByRole('searchbox');
125
+ await user.type(searchInput, 'HIV');
126
+
127
+ expect(outpatientVisit).toBeEmptyDOMElement();
128
+ expect(hivVisit).toBeInTheDocument();
129
+ });
130
+
131
+ it('renders an empty state when a search yields no matching results', async () => {
132
+ const user = userEvent.setup();
133
+
134
+ renderVisitTypeOverview();
135
+
136
+ const searchInput = screen.getByRole('searchbox');
137
+ await user.type(searchInput, 'NonexistentVisitType');
138
+
139
+ expect(screen.getByText(/no visit types to display/i)).toBeInTheDocument();
140
+ expect(screen.getByText(/check the filters above/i)).toBeInTheDocument();
141
+ });
142
+
143
+ it('selects a visit type when clicked', async () => {
144
+ const user = userEvent.setup();
145
+
146
+ renderVisitTypeOverview();
147
+
148
+ const hivVisit = screen.getByRole('radio', { name: /HIV Return Visit/i });
149
+ await user.click(hivVisit);
150
+
151
+ expect(hivVisit).toBeChecked();
152
+ });
153
+ });