@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,755 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useSWRConfig } from 'swr';
6
+ import {
7
+ Button,
8
+ ButtonSet,
9
+ ContentSwitcher,
10
+ Form,
11
+ FormGroup,
12
+ InlineLoading,
13
+ InlineNotification,
14
+ RadioButton,
15
+ RadioButtonGroup,
16
+ Row,
17
+ Stack,
18
+ Switch,
19
+ } from '@carbon/react';
20
+ import { zodResolver } from '@hookform/resolvers/zod';
21
+ import {
22
+ Extension,
23
+ ExtensionSlot,
24
+ OpenmrsFetchError,
25
+ saveVisit,
26
+ showSnackbar,
27
+ updateVisit,
28
+ useConfig,
29
+ useConnectivity,
30
+ useEmrConfiguration,
31
+ useLayoutType,
32
+ useVisit,
33
+ Workspace2,
34
+ type AssignedExtension,
35
+ type NewVisitPayload,
36
+ type Visit,
37
+ type Workspace2DefinitionProps,
38
+ } from '@openmrs/esm-framework';
39
+ import {
40
+ createOfflineVisitForPatient,
41
+ invalidateCurrentVisit,
42
+ invalidateVisitAndEncounterData,
43
+ useActivePatientEnrollment,
44
+ } from '@openmrs/esm-patient-common-lib';
45
+ import { MemoizedRecommendedVisitType } from './recommended-visit-type.component';
46
+ import {
47
+ convertToDate,
48
+ createVisitAttribute,
49
+ deleteVisitAttribute,
50
+ extractErrorMessagesFromResponse,
51
+ updateVisitAttribute,
52
+ useAllowOverlappingVisits,
53
+ useConditionalVisitTypes,
54
+ useEarliestAllowedVisitStartDate,
55
+ useVisitFormCallbacks,
56
+ useVisitFormSchemaAndDefaultValues,
57
+ visitStatuses,
58
+ type ErrorObject,
59
+ type VisitFormCallbacks,
60
+ type VisitFormData,
61
+ } from './visit-form.resource';
62
+ import { type ChartConfig } from '../../config-schema';
63
+ import { useVisitAttributeTypes } from '../hooks/useVisitAttributeType';
64
+ import BaseVisitType from './base-visit-type.component';
65
+ import LocationSelector from './location-selector.component';
66
+ import VisitAttributeTypeFields from './visit-attribute-type.component';
67
+ import VisitDateTimeSection from './visit-date-time.component';
68
+ import styles from './visit-form.scss';
69
+
70
+ interface VisitAttribute {
71
+ attributeType: string;
72
+ value: string;
73
+ }
74
+
75
+ /**
76
+ * Extra visit information provided by extensions via the extra-visit-attribute-slot.
77
+ * Extensions can use this to add custom attributes to visits.
78
+ */
79
+ export interface ExtraVisitInfo {
80
+ /**
81
+ * Optional callback that extensions can provide to perform additional
82
+ * work after the visit is created/updated (e.g. creating a bill).
83
+ * Called after visit creation succeeds and before the workspace closes.
84
+ * May return a Promise; it will be awaited before closing the workspace.
85
+ */
86
+ handleCreateExtraVisitInfo?: () => void | Promise<void>;
87
+ /**
88
+ * Array of visit attributes to be included in the visit payload.
89
+ * Each attribute must have an attributeType (UUID) and a value (string).
90
+ *
91
+ * Note: These attributes are only included when creating a new visit.
92
+ * They are not used when editing an existing visit because the backend
93
+ * rejects inline attribute updates with a maxOccurs violation, and
94
+ * these entries lack the UUIDs needed for update/delete operations.
95
+ */
96
+ attributes?: Array<VisitAttribute>;
97
+ }
98
+
99
+ export interface ExportedVisitFormProps {
100
+ /**
101
+ * A unique string identifying where the visit form is opened from.
102
+ * This string is passed into various extensions within the form to
103
+ * affect how / if they should be rendered.
104
+ */
105
+ openedFrom: string;
106
+ showPatientHeader?: boolean;
107
+ onVisitStarted?: (visit: Visit) => void;
108
+ patient: fhir.Patient;
109
+ patientUuid: string;
110
+ visitContext: Visit;
111
+ }
112
+
113
+ /**
114
+ * This workspace is meant for use outside the patient chart.
115
+ * @see visit-form.workspace.tsx
116
+ */
117
+ const ExportedVisitForm: React.FC<Workspace2DefinitionProps<ExportedVisitFormProps, {}, {}>> = ({
118
+ closeWorkspace,
119
+ workspaceProps: {
120
+ openedFrom,
121
+ showPatientHeader = false,
122
+ onVisitStarted,
123
+ patient,
124
+ patientUuid,
125
+ visitContext: visitToEdit,
126
+ },
127
+ }) => {
128
+ const { t } = useTranslation();
129
+ const isTablet = useLayoutType() === 'tablet';
130
+ const isOnline = useConnectivity();
131
+ const config = useConfig<ChartConfig>();
132
+ const { emrConfiguration } = useEmrConfiguration();
133
+ const [visitTypeContentSwitcherIndex, setVisitTypeContentSwitcherIndex] = useState(
134
+ config.showRecommendedVisitTypeTab ? 0 : 1,
135
+ );
136
+ const visitHeaderSlotState = useMemo(() => ({ patientUuid, patient }), [patientUuid, patient]);
137
+ const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid);
138
+ const { activeVisit, isLoading: isLoadingVisit } = useVisit(patientUuid);
139
+ const { allowOverlappingVisits, isLoading: isLoadingOverlapSetting } = useAllowOverlappingVisits();
140
+
141
+ const { mutate: globalMutate } = useSWRConfig();
142
+ const allVisitTypes = useConditionalVisitTypes();
143
+ const { earliestAllowedStartDate, isLoading: isLoadingBirthdateCheck } =
144
+ useEarliestAllowedVisitStartDate(patientUuid);
145
+
146
+ const [isVisitSaved, setIsVisitSaved] = useState(false);
147
+ const [errorFetchingResources, setErrorFetchingResources] = useState<{
148
+ blockSavingForm: boolean;
149
+ } | null>(null);
150
+ const { visitAttributeTypes } = useVisitAttributeTypes();
151
+ const [visitFormCallbacks, setVisitFormCallbacks] = useVisitFormCallbacks();
152
+ const [extraVisitInfo, setExtraVisitInfo] = useState<ExtraVisitInfo | null>(null);
153
+ const { visitFormSchema, defaultValues, firstEncounterDateTime, lastEncounterDateTime } =
154
+ useVisitFormSchemaAndDefaultValues(visitToEdit, earliestAllowedStartDate);
155
+
156
+ const methods = useForm<VisitFormData>({
157
+ mode: 'all',
158
+ resolver: zodResolver(visitFormSchema),
159
+ defaultValues,
160
+ });
161
+
162
+ const {
163
+ handleSubmit,
164
+ control,
165
+ getValues,
166
+ formState: { errors, isDirty, isSubmitting },
167
+ reset,
168
+ } = methods;
169
+
170
+ const visitStatus = useWatch({ control, name: 'visitStatus' });
171
+ const hasActiveVisitConflict = !visitToEdit && visitStatus !== 'past' && !!activeVisit && !allowOverlappingVisits;
172
+
173
+ // default values are cached so form needs to be reset when they change (e.g. when default visit location finishes loading)
174
+ useEffect(() => {
175
+ reset(defaultValues, {
176
+ keepDirty: true,
177
+ keepDirtyValues: true,
178
+ keepErrors: true,
179
+ keepTouched: true,
180
+ });
181
+ }, [defaultValues, reset]);
182
+
183
+ const getErrorDescription = useCallback(
184
+ (error: unknown) => {
185
+ if (OpenmrsFetchError && error instanceof OpenmrsFetchError) {
186
+ return typeof error.responseBody === 'string'
187
+ ? error.responseBody
188
+ : extractErrorMessagesFromResponse(error.responseBody as ErrorObject, t);
189
+ }
190
+ return (error as Error)?.message;
191
+ },
192
+ [t],
193
+ );
194
+
195
+ const handleVisitAttributes = useCallback(
196
+ (visitAttributes: { [p: string]: string }, visitUuid: string) => {
197
+ const existingVisitAttributeTypes =
198
+ visitToEdit?.attributes?.map((attribute) => attribute.attributeType.uuid) || [];
199
+
200
+ const promises = [];
201
+
202
+ for (const [attributeType, value] of Object.entries(visitAttributes)) {
203
+ if (attributeType && existingVisitAttributeTypes.includes(attributeType)) {
204
+ const attributeToEdit = visitToEdit.attributes.find((attr) => attr.attributeType.uuid === attributeType);
205
+
206
+ if (attributeToEdit) {
207
+ // continue to next attribute if the previous value is same as new value
208
+ const isSameValue =
209
+ typeof attributeToEdit.value === 'object'
210
+ ? attributeToEdit.value.uuid === value
211
+ : attributeToEdit.value === value;
212
+
213
+ if (isSameValue) {
214
+ continue;
215
+ }
216
+
217
+ if (value) {
218
+ // Update attribute with new value
219
+ promises.push(
220
+ updateVisitAttribute(visitUuid, attributeToEdit.uuid, value).catch((error) => {
221
+ showSnackbar({
222
+ title: t('errorUpdatingVisitAttribute', 'Error updating the {{attributeName}} visit attribute', {
223
+ attributeName: attributeToEdit.attributeType.display,
224
+ }),
225
+ kind: 'error',
226
+ isLowContrast: false,
227
+ subtitle: getErrorDescription(error),
228
+ });
229
+ return Promise.reject(error); // short-circuit promise chain
230
+ }),
231
+ );
232
+ } else {
233
+ // Delete attribute if no value is provided
234
+ promises.push(
235
+ deleteVisitAttribute(visitUuid, attributeToEdit.uuid).catch((error) => {
236
+ showSnackbar({
237
+ title: t('errorDeletingVisitAttribute', 'Error deleting the {{attributeName}} visit attribute', {
238
+ attributeName: attributeToEdit.attributeType.display,
239
+ }),
240
+ kind: 'error',
241
+ isLowContrast: false,
242
+ subtitle: getErrorDescription(error),
243
+ });
244
+ return Promise.reject(error); // short-circuit promise chain
245
+ }),
246
+ );
247
+ }
248
+ }
249
+ } else {
250
+ if (value) {
251
+ promises.push(
252
+ createVisitAttribute(visitUuid, attributeType, value).catch((error) => {
253
+ showSnackbar({
254
+ title: t('errorCreatingVisitAttribute', 'Error creating the {{attributeName}} visit attribute', {
255
+ attributeName: visitAttributeTypes?.find((type) => type.uuid === attributeType)?.display,
256
+ }),
257
+ kind: 'error',
258
+ isLowContrast: false,
259
+ subtitle: getErrorDescription(error),
260
+ });
261
+ return Promise.reject(error); // short-circuit promise chain
262
+ }),
263
+ );
264
+ }
265
+ }
266
+ }
267
+
268
+ return Promise.all(promises);
269
+ },
270
+ [getErrorDescription, visitToEdit, t, visitAttributeTypes],
271
+ );
272
+
273
+ const onSubmit = useCallback(
274
+ async (data: VisitFormData) => {
275
+ const {
276
+ visitStatus,
277
+ visitStartTimeFormat,
278
+ visitStartDate,
279
+ visitLocation,
280
+ visitStartTime,
281
+ visitType,
282
+ visitAttributes,
283
+ visitStopDate,
284
+ visitStopTime,
285
+ visitStopTimeFormat,
286
+ } = data;
287
+
288
+ const { handleCreateExtraVisitInfo, attributes: extraAttributes } = extraVisitInfo ?? {};
289
+ const hasStartTime = ['ongoing', 'past'].includes(visitStatus);
290
+ const hasStopTime = 'past' === visitStatus;
291
+ const startDatetime = convertToDate(visitStartDate, visitStartTime, visitStartTimeFormat);
292
+ const stopDatetime = convertToDate(visitStopDate, visitStopTime, visitStopTimeFormat);
293
+
294
+ // For new visits, include attributes in the payload for atomic creation (avoids orphaned visits).
295
+ // For edits, attributes are managed separately (backend rejects inline updates with maxOccurs).
296
+ const formAttributes = !visitToEdit
297
+ ? Object.entries(visitAttributes)
298
+ .filter(([, value]) => value)
299
+ .map(([attributeType, value]) => ({ attributeType, value }))
300
+ : [];
301
+
302
+ // Deduplicate by attributeType, with form attributes taking precedence over extra attributes
303
+ const formAttributeTypes = new Set(formAttributes.map((attr) => attr.attributeType));
304
+ const deduplicatedExtraAttributes = (extraAttributes ?? []).filter(
305
+ (attr) => attr?.attributeType && !formAttributeTypes.has(attr.attributeType),
306
+ );
307
+
308
+ const inlineAttributes = !visitToEdit
309
+ ? [...formAttributes, ...deduplicatedExtraAttributes].filter(
310
+ (attr) => attr?.attributeType?.length > 0 && attr?.value?.length > 0,
311
+ )
312
+ : [];
313
+
314
+ let payload: NewVisitPayload = {
315
+ visitType: visitType,
316
+ location: visitLocation?.uuid,
317
+ startDatetime: hasStartTime ? startDatetime : null,
318
+ stopDatetime: hasStopTime ? stopDatetime : null,
319
+ // The request throws 400 (Bad request) error when the patient is passed in the update payload for existing visit
320
+ ...(!visitToEdit && { patient: patientUuid }),
321
+ ...(inlineAttributes.length > 0 && { attributes: inlineAttributes }),
322
+ };
323
+
324
+ const abortController = new AbortController();
325
+ if (isOnline) {
326
+ const visitRequest = visitToEdit?.uuid
327
+ ? updateVisit(visitToEdit?.uuid, payload, abortController)
328
+ : saveVisit(payload, abortController);
329
+
330
+ await visitRequest
331
+ .then((response) => {
332
+ showSnackbar({
333
+ isLowContrast: true,
334
+ kind: 'success',
335
+ subtitle: !visitToEdit
336
+ ? t('visitStartedSuccessfully', '{{visit}} started successfully', {
337
+ visit: response?.data?.visitType?.display ?? t('visit', 'Visit'),
338
+ })
339
+ : t('visitDetailsUpdatedSuccessfully', '{{visit}} updated successfully', {
340
+ visit: response?.data?.visitType?.display ?? t('pastVisit', 'Past visit'),
341
+ }),
342
+ title: !visitToEdit
343
+ ? t('visitStarted', 'Visit started')
344
+ : t('visitDetailsUpdated', 'Visit details updated'),
345
+ });
346
+ return response;
347
+ })
348
+ .catch((error) => {
349
+ showSnackbar({
350
+ title: !visitToEdit
351
+ ? t('startVisitError', 'Error starting visit')
352
+ : t('errorUpdatingVisitDetails', 'Error updating visit details'),
353
+ kind: 'error',
354
+ isLowContrast: false,
355
+ subtitle: getErrorDescription(error),
356
+ });
357
+ return Promise.reject(error); // short-circuit promise chain
358
+ })
359
+ .then(async (response) => {
360
+ // now that visit is created / updated, we run post-submit actions
361
+ // to update visit attributes or any other OnVisitCreatedOrUpdated actions
362
+ const visit = response.data;
363
+ setIsVisitSaved(true);
364
+
365
+ // Use targeted SWR invalidation instead of global mutateVisit
366
+ // This will invalidate visit history and encounter tables for this patient
367
+ // (if visitContext is updated, it should have been invalidated with mutateSavedOrUpdatedVisit)
368
+ invalidateVisitAndEncounterData(globalMutate, patientUuid);
369
+ invalidateCurrentVisit(globalMutate, patientUuid);
370
+
371
+ const visitAttributesRequest = visitToEdit
372
+ ? handleVisitAttributes(visitAttributes, response.data.uuid).then((visitAttributesResponses) => {
373
+ if (visitAttributesResponses.length > 0) {
374
+ showSnackbar({
375
+ isLowContrast: true,
376
+ kind: 'success',
377
+ title: t(
378
+ 'additionalVisitInformationUpdatedSuccessfully',
379
+ 'Additional visit information updated successfully',
380
+ ),
381
+ });
382
+ }
383
+ })
384
+ : Promise.resolve();
385
+
386
+ const onVisitCreatedOrUpdatedRequests = [...visitFormCallbacks.values()].map((callbacks) =>
387
+ callbacks.onVisitCreatedOrUpdated(visit),
388
+ );
389
+
390
+ await Promise.all([visitAttributesRequest, ...onVisitCreatedOrUpdatedRequests]);
391
+ await handleCreateExtraVisitInfo?.();
392
+ await closeWorkspace({ discardUnsavedChanges: true });
393
+ onVisitStarted?.(visit);
394
+ })
395
+ .catch(() => {
396
+ // do nothing, this catches any reject promises used for short-circuiting
397
+ });
398
+ } else {
399
+ await createOfflineVisitForPatient(
400
+ patientUuid,
401
+ visitLocation.uuid,
402
+ config.offlineVisitTypeUuid,
403
+ payload.startDatetime,
404
+ ).then(
405
+ async (visit) => {
406
+ setIsVisitSaved(true);
407
+ // Also invalidate visit history and encounter tables
408
+ invalidateVisitAndEncounterData(globalMutate, patientUuid);
409
+ invalidateCurrentVisit(globalMutate, patientUuid);
410
+ showSnackbar({
411
+ isLowContrast: true,
412
+ kind: 'success',
413
+ subtitle: t('visitStartedSuccessfully', '{{visit}} started successfully', {
414
+ visit: t('offlineVisit', 'Offline Visit'),
415
+ }),
416
+ title: t('visitStarted', 'Visit started'),
417
+ });
418
+ await closeWorkspace({ discardUnsavedChanges: true });
419
+ onVisitStarted?.(visit);
420
+ },
421
+ (error: Error) => {
422
+ showSnackbar({
423
+ title: t('startVisitError', 'Error starting visit'),
424
+ kind: 'error',
425
+ isLowContrast: false,
426
+ subtitle: error?.message,
427
+ });
428
+ },
429
+ );
430
+ }
431
+ },
432
+ [
433
+ closeWorkspace,
434
+ config.offlineVisitTypeUuid,
435
+ extraVisitInfo,
436
+ getErrorDescription,
437
+ globalMutate,
438
+ handleVisitAttributes,
439
+ isOnline,
440
+ onVisitStarted,
441
+ patientUuid,
442
+ t,
443
+ visitFormCallbacks,
444
+ visitToEdit,
445
+ ],
446
+ );
447
+
448
+ return (
449
+ <Workspace2
450
+ title={visitToEdit ? t('editVisit', 'Edit visit') : t('startVisitWorkspaceTitle', 'Start a visit')}
451
+ hasUnsavedChanges={isDirty && !isVisitSaved}
452
+ >
453
+ <FormProvider {...methods}>
454
+ <Form className={styles.form} onSubmit={handleSubmit(onSubmit)} data-openmrs-role="Start Visit Form">
455
+ {showPatientHeader && patient && (
456
+ <ExtensionSlot
457
+ name="patient-header-slot"
458
+ state={{
459
+ patient,
460
+ patientUuid: patientUuid,
461
+ hideActionsOverflow: true,
462
+ }}
463
+ />
464
+ )}
465
+ {errorFetchingResources && (
466
+ <InlineNotification
467
+ kind={errorFetchingResources?.blockSavingForm ? 'error' : 'warning'}
468
+ lowContrast
469
+ className={styles.inlineNotification}
470
+ title={t('partOfFormDidntLoad', 'Part of the form did not load')}
471
+ subtitle={t('refreshToTryAgain', 'Please refresh to try again')}
472
+ />
473
+ )}
474
+ <div>
475
+ {isTablet && (
476
+ <Row className={styles.headerGridRow}>
477
+ <ExtensionSlot
478
+ name="visit-form-header-slot"
479
+ className={styles.dataGridRow}
480
+ state={visitHeaderSlotState}
481
+ />
482
+ </Row>
483
+ )}
484
+ <Stack gap={4} className={styles.container}>
485
+ <section>
486
+ <FormGroup legendText={t('theVisitIs', 'The visit is')}>
487
+ <Controller
488
+ name="visitStatus"
489
+ control={control}
490
+ render={({ field: { onChange, value } }) => {
491
+ const validVisitStatuses = visitToEdit ? ['ongoing', 'past'] : visitStatuses;
492
+ const idx = validVisitStatuses.indexOf(value);
493
+ const selectedIndex = idx >= 0 ? idx : 0;
494
+
495
+ // For some reason, Carbon throws NPE when trying to conditionally
496
+ // render a <Switch> component
497
+ return visitToEdit ? (
498
+ <ContentSwitcher
499
+ selectedIndex={selectedIndex}
500
+ onChange={({ name }) => onChange(name)}
501
+ size="md"
502
+ >
503
+ <Switch name="ongoing">{t('ongoing', 'Ongoing')}</Switch>
504
+ <Switch name="past">{t('ended', 'Ended')}</Switch>
505
+ </ContentSwitcher>
506
+ ) : (
507
+ <ContentSwitcher
508
+ selectedIndex={selectedIndex}
509
+ onChange={({ name }) => onChange(name)}
510
+ size="md"
511
+ >
512
+ <Switch name="new">{t('new', 'New')}</Switch>
513
+ <Switch name="ongoing">{t('ongoing', 'Ongoing')}</Switch>
514
+ <Switch name="past">{t('inThePast', 'In the past')}</Switch>
515
+ </ContentSwitcher>
516
+ );
517
+ }}
518
+ />
519
+ </FormGroup>
520
+ </section>
521
+ {hasActiveVisitConflict && (
522
+ <InlineNotification
523
+ className={styles.inlineNotification}
524
+ kind="info"
525
+ lowContrast
526
+ title={t('activeVisitExists', 'This patient already has an active visit')}
527
+ subtitle={t('endActiveVisitFirst', 'You must end the current visit before starting a new one.')}
528
+ />
529
+ )}
530
+ {!hasActiveVisitConflict && (
531
+ <>
532
+ <VisitDateTimeSection
533
+ {...{ control, firstEncounterDateTime, lastEncounterDateTime }}
534
+ earliestStartDate={earliestAllowedStartDate?.getTime()}
535
+ />
536
+ {/* Upcoming appointments. This get shown when config.showUpcomingAppointments is true. */}
537
+ {config.showUpcomingAppointments && (
538
+ <section>
539
+ <div className={styles.sectionField}>
540
+ <VisitFormExtensionSlot
541
+ name="visit-form-top-slot"
542
+ patientUuid={patientUuid}
543
+ visitFormOpenedFrom={openedFrom}
544
+ setVisitFormCallbacks={setVisitFormCallbacks}
545
+ />
546
+ </div>
547
+ </section>
548
+ )}
549
+
550
+ {/* This field lets the user select a location for the visit. The location is required for the visit to be saved. Defaults to the active session location */}
551
+ <LocationSelector control={control} />
552
+
553
+ {/* Lists available program types. This feature is dependent on the `showRecommendedVisitTypeTab` config being set
554
+ to true. */}
555
+ {config.showRecommendedVisitTypeTab && (
556
+ <section>
557
+ <h1 className={styles.sectionTitle}>{t('program', 'Program')}</h1>
558
+ <FormGroup
559
+ legendText={t('selectProgramType', 'Select program type')}
560
+ className={styles.sectionField}
561
+ >
562
+ <Controller
563
+ name="programType"
564
+ control={control}
565
+ render={({ field: { onChange } }) => (
566
+ <RadioButtonGroup
567
+ orientation="vertical"
568
+ onChange={(uuid: string) =>
569
+ onChange(activePatientEnrollment.find(({ program }) => program.uuid === uuid)?.uuid)
570
+ }
571
+ name="program-type-radio-group"
572
+ >
573
+ {activePatientEnrollment.map(({ uuid, display, program }) => (
574
+ <RadioButton
575
+ key={uuid}
576
+ className={styles.radioButton}
577
+ id={uuid}
578
+ labelText={display}
579
+ value={program.uuid}
580
+ />
581
+ ))}
582
+ </RadioButtonGroup>
583
+ )}
584
+ />
585
+ </FormGroup>
586
+ </section>
587
+ )}
588
+
589
+ {/* Lists available visit types if no atFacilityVisitType enabled. The content switcher only gets shown when recommended visit types are enabled */}
590
+ {!emrConfiguration?.atFacilityVisitType && (
591
+ <section>
592
+ <h1 className={styles.sectionTitle}>{t('visitType_title', 'Visit Type')}</h1>
593
+ <div className={styles.sectionField}>
594
+ {config.showRecommendedVisitTypeTab ? (
595
+ <>
596
+ <ContentSwitcher
597
+ selectedIndex={visitTypeContentSwitcherIndex}
598
+ onChange={({ index }) => setVisitTypeContentSwitcherIndex(index)}
599
+ size="md"
600
+ >
601
+ <Switch name="recommended">{t('recommended', 'Recommended')}</Switch>
602
+ <Switch name="all">{t('all', 'All')}</Switch>
603
+ </ContentSwitcher>
604
+ {visitTypeContentSwitcherIndex === 0 && !isLoading && (
605
+ <MemoizedRecommendedVisitType
606
+ patientUuid={patientUuid}
607
+ patientProgramEnrollment={(() => {
608
+ return activePatientEnrollment?.find(
609
+ ({ program }) => program.uuid === getValues('programType'),
610
+ );
611
+ })()}
612
+ locationUuid={getValues('visitLocation')?.uuid}
613
+ />
614
+ )}
615
+ {visitTypeContentSwitcherIndex === 1 && <BaseVisitType visitTypes={allVisitTypes} />}
616
+ </>
617
+ ) : (
618
+ // Defaults to showing all possible visit types if recommended visits are not enabled
619
+ <BaseVisitType visitTypes={allVisitTypes} />
620
+ )}
621
+ </div>
622
+
623
+ {errors?.visitType && (
624
+ <section>
625
+ <div className={styles.sectionField}>
626
+ <InlineNotification
627
+ role="alert"
628
+ style={{ margin: '0', minWidth: '100%' }}
629
+ kind="error"
630
+ lowContrast={true}
631
+ title={t('missingVisitType', 'Missing visit type')}
632
+ subtitle={t('selectVisitType', 'Please select a Visit Type')}
633
+ />
634
+ </div>
635
+ </section>
636
+ )}
637
+ </section>
638
+ )}
639
+
640
+ <ExtensionSlot state={{ patientUuid, setExtraVisitInfo }} name="extra-visit-attribute-slot" />
641
+
642
+ {/* Visit type attribute fields. These get shown when visit attribute types are configured */}
643
+ <section>
644
+ <h1 className={styles.sectionTitle}>{isTablet && t('visitAttributes', 'Visit attributes')}</h1>
645
+ <div className={styles.sectionField}>
646
+ <VisitAttributeTypeFields setErrorFetchingResources={setErrorFetchingResources} />
647
+ </div>
648
+ </section>
649
+
650
+ {/* Queue location and queue fields. These get shown when config.showServiceQueueFields is true,
651
+ or when the form is opened from the queues app */}
652
+ <section>
653
+ <div className={styles.sectionField}>
654
+ <VisitFormExtensionSlot
655
+ name="visit-form-bottom-slot"
656
+ patientUuid={patientUuid}
657
+ visitFormOpenedFrom={openedFrom}
658
+ setVisitFormCallbacks={setVisitFormCallbacks}
659
+ />
660
+ </div>
661
+ </section>
662
+ </>
663
+ )}
664
+ </Stack>
665
+ </div>
666
+ <ButtonSet
667
+ className={classNames(styles.buttonSet, {
668
+ [styles.tablet]: isTablet,
669
+ [styles.desktop]: !isTablet,
670
+ })}
671
+ >
672
+ <Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
673
+ {t('discard', 'Discard')}
674
+ </Button>
675
+ <Button
676
+ className={styles.button}
677
+ disabled={
678
+ isSubmitting ||
679
+ isLoadingVisit ||
680
+ isLoadingBirthdateCheck ||
681
+ isLoadingOverlapSetting ||
682
+ errorFetchingResources?.blockSavingForm ||
683
+ hasActiveVisitConflict
684
+ }
685
+ kind="primary"
686
+ type="submit"
687
+ >
688
+ {isSubmitting ? (
689
+ <InlineLoading
690
+ className={styles.spinner}
691
+ description={
692
+ visitToEdit
693
+ ? t('updatingVisit', 'Updating visit') + '...'
694
+ : t('startingVisit', 'Starting visit') + '...'
695
+ }
696
+ />
697
+ ) : (
698
+ <span>{visitToEdit ? t('updateVisit', 'Update visit') : t('startVisit', 'Start visit')}</span>
699
+ )}
700
+ </Button>
701
+ </ButtonSet>
702
+ </Form>
703
+ </FormProvider>
704
+ </Workspace2>
705
+ );
706
+ };
707
+
708
+ interface VisitFormExtensionSlotProps {
709
+ name: string;
710
+ patientUuid: string;
711
+ visitFormOpenedFrom: string;
712
+ setVisitFormCallbacks: React.Dispatch<React.SetStateAction<Map<string, VisitFormCallbacks>>>;
713
+ }
714
+
715
+ type VisitFormExtensionState = {
716
+ patientUuid: string;
717
+
718
+ /**
719
+ * This function allows an extension to register callbacks for visit form submission.
720
+ * This callbacks can be used to make further requests. The callbacks should handle its own UI notification
721
+ * on success / failure, and its returned Promise MUST resolve on success and MUST reject on failure.
722
+ * @param callback
723
+ * @returns
724
+ */
725
+ setVisitFormCallbacks(callbacks: VisitFormCallbacks);
726
+
727
+ visitFormOpenedFrom: string;
728
+ patientChartConfig: ChartConfig;
729
+ };
730
+
731
+ const VisitFormExtensionSlot: React.FC<VisitFormExtensionSlotProps> = React.memo(
732
+ ({ name, patientUuid, visitFormOpenedFrom, setVisitFormCallbacks }) => {
733
+ const config = useConfig<ChartConfig>();
734
+
735
+ return (
736
+ <ExtensionSlot name={name}>
737
+ {(extension: AssignedExtension) => {
738
+ const state: VisitFormExtensionState = {
739
+ patientUuid,
740
+ setVisitFormCallbacks: (callbacks) => {
741
+ setVisitFormCallbacks((old) => {
742
+ return new Map(old).set(extension.id, callbacks);
743
+ });
744
+ },
745
+ visitFormOpenedFrom,
746
+ patientChartConfig: config,
747
+ };
748
+ return <Extension state={state} />;
749
+ }}
750
+ </ExtensionSlot>
751
+ );
752
+ },
753
+ );
754
+
755
+ export default ExportedVisitForm;