@kenyaemr/esm-express-workflow-app 5.4.4-pre.43 → 5.4.4-pre.430

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 (627) hide show
  1. package/.turbo/turbo-build.log +14 -7
  2. package/dist/1084.js +1 -0
  3. package/dist/1084.js.map +1 -0
  4. package/dist/1278.js +1 -0
  5. package/dist/1278.js.map +1 -0
  6. package/dist/1324.js +1 -0
  7. package/dist/1324.js.map +1 -0
  8. package/dist/1375.js +66 -0
  9. package/dist/1375.js.map +1 -0
  10. package/dist/1402.js +1 -0
  11. package/dist/1402.js.map +1 -0
  12. package/dist/1442.js +1 -0
  13. package/dist/1442.js.map +1 -0
  14. package/dist/1520.js +1 -0
  15. package/dist/1520.js.map +1 -0
  16. package/dist/1559.js +1 -0
  17. package/dist/1559.js.map +1 -0
  18. package/dist/1593.js +1 -0
  19. package/dist/1593.js.map +1 -0
  20. package/dist/162.js +1 -0
  21. package/dist/162.js.map +1 -0
  22. package/dist/1620.js +1 -0
  23. package/dist/1620.js.map +1 -0
  24. package/dist/1658.js +1 -0
  25. package/dist/1658.js.map +1 -0
  26. package/dist/1659.js +1 -0
  27. package/dist/1659.js.map +1 -0
  28. package/dist/1789.js +1 -0
  29. package/dist/1789.js.map +1 -0
  30. package/dist/1827.js +1 -0
  31. package/dist/1827.js.map +1 -0
  32. package/dist/1831.js +1 -0
  33. package/dist/1831.js.map +1 -0
  34. package/dist/1854.js +1 -0
  35. package/dist/1854.js.map +1 -0
  36. package/dist/196.js +1 -0
  37. package/dist/196.js.map +1 -0
  38. package/dist/2127.js +1 -0
  39. package/dist/2127.js.map +1 -0
  40. package/dist/2152.js +1 -0
  41. package/dist/2152.js.map +1 -0
  42. package/dist/2202.js +1 -0
  43. package/dist/2202.js.map +1 -0
  44. package/dist/221.js +1 -0
  45. package/dist/221.js.map +1 -0
  46. package/dist/2269.js +1 -0
  47. package/dist/2269.js.map +1 -0
  48. package/dist/2293.js +1 -0
  49. package/dist/2293.js.map +1 -0
  50. package/dist/234.js +1 -0
  51. package/dist/234.js.map +1 -0
  52. package/dist/2412.js +1 -1
  53. package/dist/2412.js.map +1 -1
  54. package/dist/2466.js +1 -0
  55. package/dist/2466.js.map +1 -0
  56. package/dist/2534.js +1 -1
  57. package/dist/2534.js.map +1 -1
  58. package/dist/2553.js +1 -0
  59. package/dist/2553.js.map +1 -0
  60. package/dist/2556.js +1 -0
  61. package/dist/2556.js.map +1 -0
  62. package/dist/2648.js +1 -0
  63. package/dist/2648.js.map +1 -0
  64. package/dist/274.js +1 -0
  65. package/dist/274.js.map +1 -0
  66. package/dist/2788.js +1 -0
  67. package/dist/2788.js.map +1 -0
  68. package/dist/2842.js +1 -0
  69. package/dist/2842.js.map +1 -0
  70. package/dist/2874.js +1 -0
  71. package/dist/2874.js.map +1 -0
  72. package/dist/2916.js +1 -0
  73. package/dist/2916.js.map +1 -0
  74. package/dist/2947.js +1 -1
  75. package/dist/2947.js.map +1 -1
  76. package/dist/3012.js +1 -0
  77. package/dist/3012.js.map +1 -0
  78. package/dist/3087.js +1 -0
  79. package/dist/3087.js.map +1 -0
  80. package/dist/3335.js +1 -0
  81. package/dist/3335.js.map +1 -0
  82. package/dist/3378.js +1 -0
  83. package/dist/3378.js.map +1 -0
  84. package/dist/3504.js +1 -0
  85. package/dist/3504.js.map +1 -0
  86. package/dist/3509.js +1 -1
  87. package/dist/3509.js.map +1 -1
  88. package/dist/356.js +1 -0
  89. package/dist/356.js.map +1 -0
  90. package/dist/3690.js +1 -0
  91. package/dist/3690.js.map +1 -0
  92. package/dist/371.js +1 -0
  93. package/dist/371.js.map +1 -0
  94. package/dist/3747.js +1 -0
  95. package/dist/3747.js.map +1 -0
  96. package/dist/3819.js +1 -0
  97. package/dist/3819.js.map +1 -0
  98. package/dist/3820.js +1 -0
  99. package/dist/3820.js.map +1 -0
  100. package/dist/3910.js +1 -0
  101. package/dist/3910.js.map +1 -0
  102. package/dist/393.js +6 -0
  103. package/dist/393.js.map +1 -0
  104. package/dist/3933.js +1 -0
  105. package/dist/3933.js.map +1 -0
  106. package/dist/3989.js +1 -0
  107. package/dist/3989.js.map +1 -0
  108. package/dist/3995.js +1 -0
  109. package/dist/3995.js.map +1 -0
  110. package/dist/4032.js +1 -0
  111. package/dist/4032.js.map +1 -0
  112. package/dist/4145.js +1 -1
  113. package/dist/4145.js.map +1 -1
  114. package/dist/4178.js +1 -1
  115. package/dist/4178.js.map +1 -1
  116. package/dist/4191.js +1 -0
  117. package/dist/4191.js.map +1 -0
  118. package/dist/4212.js +1 -0
  119. package/dist/4212.js.map +1 -0
  120. package/dist/4215.js +1 -0
  121. package/dist/4215.js.map +1 -0
  122. package/dist/4523.js +1 -0
  123. package/dist/4523.js.map +1 -0
  124. package/dist/4588.js +1 -0
  125. package/dist/4588.js.map +1 -0
  126. package/dist/4617.js +1 -0
  127. package/dist/4617.js.map +1 -0
  128. package/dist/466.js +1 -1
  129. package/dist/466.js.map +1 -1
  130. package/dist/4688.js +1 -0
  131. package/dist/{7288.js.map → 4688.js.map} +1 -1
  132. package/dist/470.js +1 -0
  133. package/dist/470.js.map +1 -0
  134. package/dist/4759.js +1 -0
  135. package/dist/4759.js.map +1 -0
  136. package/dist/4818.js +1 -1
  137. package/dist/4818.js.map +1 -1
  138. package/dist/5086.js +1 -1
  139. package/dist/5086.js.map +1 -1
  140. package/dist/5093.js +1 -0
  141. package/dist/5093.js.map +1 -0
  142. package/dist/5097.js +1 -0
  143. package/dist/5097.js.map +1 -0
  144. package/dist/510.js +1 -0
  145. package/dist/510.js.map +1 -0
  146. package/dist/5117.js +1 -0
  147. package/dist/5117.js.map +1 -0
  148. package/dist/5279.js +1 -0
  149. package/dist/5279.js.map +1 -0
  150. package/dist/5434.js +1 -0
  151. package/dist/5434.js.map +1 -0
  152. package/dist/5514.js +1 -0
  153. package/dist/5514.js.map +1 -0
  154. package/dist/5568.js +1 -0
  155. package/dist/5568.js.map +1 -0
  156. package/dist/5576.js +1 -1
  157. package/dist/5576.js.map +1 -1
  158. package/dist/5630.js +1 -0
  159. package/dist/5630.js.map +1 -0
  160. package/dist/5676.js +1 -1
  161. package/dist/5676.js.map +1 -1
  162. package/dist/5695.js +1 -0
  163. package/dist/5695.js.map +1 -0
  164. package/dist/5697.js +1 -1
  165. package/dist/5697.js.map +1 -1
  166. package/dist/5728.js +1 -0
  167. package/dist/5728.js.map +1 -0
  168. package/dist/5809.js +1 -1
  169. package/dist/5809.js.map +1 -1
  170. package/dist/585.js +1 -0
  171. package/dist/585.js.map +1 -0
  172. package/dist/5887.js +1 -0
  173. package/dist/5887.js.map +1 -0
  174. package/dist/5958.js +1 -0
  175. package/dist/5958.js.map +1 -0
  176. package/dist/5964.js +1 -0
  177. package/dist/5964.js.map +1 -0
  178. package/dist/6008.js +1 -0
  179. package/dist/602.js +1 -0
  180. package/dist/602.js.map +1 -0
  181. package/dist/6020.js +1 -0
  182. package/dist/6020.js.map +1 -0
  183. package/dist/6161.js +1 -0
  184. package/dist/6161.js.map +1 -0
  185. package/dist/6273.js +1 -0
  186. package/dist/6273.js.map +1 -0
  187. package/dist/6337.js +1 -0
  188. package/dist/6337.js.map +1 -0
  189. package/dist/6427.js +1 -1
  190. package/dist/6427.js.map +1 -1
  191. package/dist/6429.js +1 -0
  192. package/dist/6429.js.map +1 -0
  193. package/dist/6478.js +7 -0
  194. package/dist/6478.js.map +1 -0
  195. package/dist/6508.js +1 -0
  196. package/dist/6508.js.map +1 -0
  197. package/dist/6634.js +1 -0
  198. package/dist/6634.js.map +1 -0
  199. package/dist/6767.js +1 -0
  200. package/dist/6767.js.map +1 -0
  201. package/dist/6783.js +1 -0
  202. package/dist/6783.js.map +1 -0
  203. package/dist/6868.js +1 -1
  204. package/dist/6868.js.map +1 -1
  205. package/dist/688.js +1 -0
  206. package/dist/688.js.map +1 -0
  207. package/dist/689.js +1 -1
  208. package/dist/689.js.map +1 -1
  209. package/dist/694.js +1 -0
  210. package/dist/694.js.map +1 -0
  211. package/dist/6987.js +1 -0
  212. package/dist/6987.js.map +1 -0
  213. package/dist/7061.js +1 -0
  214. package/dist/7061.js.map +1 -0
  215. package/dist/7078.js +1 -0
  216. package/dist/7078.js.map +1 -0
  217. package/dist/710.js +1 -0
  218. package/dist/710.js.map +1 -0
  219. package/dist/7128.js +1 -0
  220. package/dist/7128.js.map +1 -0
  221. package/dist/7209.js +1 -0
  222. package/dist/7209.js.map +1 -0
  223. package/dist/7424.js +1 -0
  224. package/dist/7424.js.map +1 -0
  225. package/dist/770.js +1 -0
  226. package/dist/770.js.map +1 -0
  227. package/dist/7707.js +1 -0
  228. package/dist/7707.js.map +1 -0
  229. package/dist/7784.js +1 -0
  230. package/dist/7784.js.map +1 -0
  231. package/dist/781.js +1 -1
  232. package/dist/781.js.map +1 -1
  233. package/dist/7824.js +1 -0
  234. package/dist/7824.js.map +1 -0
  235. package/dist/7832.js +1 -0
  236. package/dist/7832.js.map +1 -0
  237. package/dist/8178.js +1 -0
  238. package/dist/8178.js.map +1 -0
  239. package/dist/820.js +1 -0
  240. package/dist/820.js.map +1 -0
  241. package/dist/8224.js +1 -0
  242. package/dist/8224.js.map +1 -0
  243. package/dist/8225.js +1 -0
  244. package/dist/8225.js.map +1 -0
  245. package/dist/8240.js +1 -1
  246. package/dist/8240.js.map +1 -1
  247. package/dist/8258.js +1 -0
  248. package/dist/8258.js.map +1 -0
  249. package/dist/8702.js +1 -0
  250. package/dist/8702.js.map +1 -0
  251. package/dist/8725.js +1 -0
  252. package/dist/8725.js.map +1 -0
  253. package/dist/8762.js +1 -0
  254. package/dist/8762.js.map +1 -0
  255. package/dist/8763.js +1 -0
  256. package/dist/8763.js.map +1 -0
  257. package/dist/8815.js +1 -0
  258. package/dist/8815.js.map +1 -0
  259. package/dist/8882.js +1 -0
  260. package/dist/8882.js.map +1 -0
  261. package/dist/8924.js +1 -1
  262. package/dist/8924.js.map +1 -1
  263. package/dist/8937.js +1 -0
  264. package/dist/8937.js.map +1 -0
  265. package/dist/9.js +1 -1
  266. package/dist/9.js.map +1 -1
  267. package/dist/9061.js +1 -0
  268. package/dist/9061.js.map +1 -0
  269. package/dist/9087.js +1 -0
  270. package/dist/9087.js.map +1 -0
  271. package/dist/9301.js +1 -0
  272. package/dist/9301.js.map +1 -0
  273. package/dist/9324.js +1 -0
  274. package/dist/9324.js.map +1 -0
  275. package/dist/9497.js +1 -0
  276. package/dist/9497.js.map +1 -0
  277. package/dist/9520.js +1 -1
  278. package/dist/9520.js.map +1 -1
  279. package/dist/9526.js +1 -0
  280. package/dist/9526.js.map +1 -0
  281. package/dist/9621.js +1 -0
  282. package/dist/9621.js.map +1 -0
  283. package/dist/9676.js +17 -0
  284. package/dist/9676.js.map +1 -0
  285. package/dist/977.js +1 -0
  286. package/dist/977.js.map +1 -0
  287. package/dist/9771.js +1 -0
  288. package/dist/9771.js.map +1 -0
  289. package/dist/9806.js +1 -0
  290. package/dist/9806.js.map +1 -0
  291. package/dist/989.js +1 -0
  292. package/dist/989.js.map +1 -0
  293. package/dist/kenyaemr-esm-express-workflow-app.js +6 -5
  294. package/dist/kenyaemr-esm-express-workflow-app.js.buildmanifest.json +1407 -809
  295. package/dist/kenyaemr-esm-express-workflow-app.js.map +1 -1
  296. package/dist/main.js +22 -5
  297. package/dist/main.js.map +1 -1
  298. package/dist/routes.json +1 -1
  299. package/package.json +4 -6
  300. package/src/components/admissions/admissions-dashboard.component.tsx +2 -2
  301. package/src/components/anaesthetic/anaesthetic-dashboard.meta.ts +8 -0
  302. package/src/components/anaesthetic/anaesthetic-data-form.component.tsx +442 -0
  303. package/src/components/anaesthetic/anaesthetic-data-form.scss +278 -0
  304. package/src/components/anaesthetic/anaesthetic-link.component.tsx +21 -0
  305. package/src/components/anaesthetic/anaesthetic.component.tsx +618 -0
  306. package/src/components/anaesthetic/anaesthetic.resource.ts +1503 -0
  307. package/src/components/anaesthetic/anaesthetic.scss +1885 -0
  308. package/src/components/anaesthetic/forms/anaesthetic-form.component.tsx +304 -0
  309. package/src/components/anaesthetic/forms/anaesthetic-record-form.component.tsx +707 -0
  310. package/src/components/anaesthetic/forms/icd11-diagnosis-search.component.tsx +178 -0
  311. package/src/components/anaesthetic/forms/icd11-diagnosis-search.scss +126 -0
  312. package/src/components/anaesthetic/forms/index.ts +2 -0
  313. package/src/components/anaesthetic/forms/interoperative-record-drug-given-form.component.tsx +231 -0
  314. package/src/components/anaesthetic/forms/post-operative-summary-form.component.tsx +171 -0
  315. package/src/components/anaesthetic/forms/time-picker-dropdown.component.tsx +202 -0
  316. package/src/components/anaesthetic/forms/time-picker-dropdown.scss +107 -0
  317. package/src/components/anaesthetic/forms/time-picker-with-clock.component.tsx +174 -0
  318. package/src/components/anaesthetic/forms/time-picker-with-clock.scss +178 -0
  319. package/src/components/anaesthetic/forms/useAnaestheticData.ts +16 -0
  320. package/src/components/anaesthetic/graphs/anaesthetic-graph-wrapper.component.tsx +266 -0
  321. package/src/components/anaesthetic/graphs/anaesthetic-graph.component.tsx +185 -0
  322. package/src/components/anaesthetic/graphs/index.ts +2 -0
  323. package/src/components/anaesthetic/graphs/interoperative-record-drug-given-graph-wrapper.component.tsx +197 -0
  324. package/src/components/anaesthetic/graphs/interoperative-record-drug-given-graph.component.tsx +93 -0
  325. package/src/components/anaesthetic/resources/anaesthetic-amniotic-fluid.resource.ts +277 -0
  326. package/src/components/anaesthetic/resources/anaesthetic-form.resource.ts +716 -0
  327. package/src/components/anaesthetic/resources/anaesthetic.resource.ts +378 -0
  328. package/src/components/anaesthetic/types/index.ts +1349 -0
  329. package/src/components/consultation/clinical-encounter/clinical-encounter.component.tsx +4 -20
  330. package/src/components/consultation/clinical-encounter/encounter-details.component.tsx +9 -9
  331. package/src/components/consultation/clinical-encounter/encounter-details.test.tsx +19 -18
  332. package/src/components/consultation/consultation.utils.tsx +1 -1
  333. package/src/components/consultation/dashboard.component.tsx +0 -2
  334. package/src/components/consultation/index.ts +1 -1
  335. package/src/components/consultation/tabs/index.ts +67 -0
  336. package/src/components/facility-dashboard/components/header/home-header.component.tsx +8 -13
  337. package/src/components/laboratory/index.ts +11 -0
  338. package/src/components/laboratory/lab-table.component.tsx +46 -12
  339. package/src/components/laboratory/laboratory-tabs.component.tsx +3 -17
  340. package/src/components/mch/dashboard.component.tsx +0 -2
  341. package/src/components/mch/mch.component.tsx +1 -3
  342. package/src/components/pharmacy/index.ts +1 -1
  343. package/src/components/pharmacy/orders/pharmacy-orders.component.tsx +44 -6
  344. package/src/components/pharmacy/orders/pharmacy-orders.test.tsx +8 -7
  345. package/src/components/pharmacy/pharmacy-tabs.component.tsx +4 -20
  346. package/src/components/pharmacy/tabs/index.ts +21 -0
  347. package/src/components/preauth/index.ts +22 -0
  348. package/src/components/preauth/preauth-dashboard.component.tsx +20 -0
  349. package/src/components/procedures/index.ts +16 -0
  350. package/src/components/procedures/procedure-queues/index.ts +13 -0
  351. package/src/components/procedures/procedure-queues/procedure-form-extras.component.tsx +33 -0
  352. package/src/components/procedures/procedure-queues/procedure-form-extras.scss +5 -0
  353. package/src/components/procedures/procedure-queues/procedure-queues.component.tsx +35 -0
  354. package/src/components/procedures/procedure-queues/procedure-queues.resources.ts +37 -0
  355. package/src/components/procedures/procedure-queues/procedure-room-tabs.component.tsx +80 -0
  356. package/src/components/procedures/procedure-queues/procedure-rooms.scss +13 -0
  357. package/src/components/procedures/procedure-queues/queue-procedure-tabs.component.tsx +34 -0
  358. package/src/components/procedures/procedure-queues/summary-cards.component.tsx +81 -0
  359. package/src/components/procedures/procedure-queues/summary-cards.scss +35 -0
  360. package/src/components/procedures/procedures-table.component.tsx +77 -19
  361. package/src/components/procedures/procedures-tabs.component.test.tsx +57 -0
  362. package/src/components/procedures/procedures-tabs.component.tsx +3 -18
  363. package/src/components/radiology-and-imaging/index.ts +11 -0
  364. package/src/components/radiology-and-imaging/radiology-and-imaging-table.component.tsx +73 -21
  365. package/src/components/radiology-and-imaging/radiology-and-imaging.component.tsx +3 -19
  366. package/src/components/registration/card/HIE-card/hie-card.component.tsx +32 -44
  367. package/src/components/registration/card/Local-card/local-card.component.tsx +22 -24
  368. package/src/components/registration/checkin-form-extra/checkin-form-extra.extension.tsx +8 -2
  369. package/src/components/registration/checkin-form-extra/queue-fields.component.tsx +18 -19
  370. package/src/components/registration/dependants/dependants.component.tsx +37 -60
  371. package/src/components/registration/dependants/dependants.resource.ts +75 -46
  372. package/src/components/registration/end-visit-modal/end-visit-dialog.modal.tsx +320 -0
  373. package/src/components/registration/end-visit-modal/end-visit-dialog.scss +12 -0
  374. package/src/components/registration/end-visit-modal/end-visit.resource.tsx +59 -0
  375. package/src/components/registration/end-visit-modal/sha-end-visit.resource.ts +285 -0
  376. package/src/components/registration/search-bar/search-bar.resource.ts +32 -93
  377. package/src/components/registration/start-visit-form/hooks/useDefaultFacilityLocation.tsx +15 -0
  378. package/src/components/registration/start-visit-form/hooks/useDefaultVisitLocation.tsx +24 -0
  379. package/src/components/registration/start-visit-form/hooks/useOfflineVisitType.tsx +18 -0
  380. package/src/components/registration/start-visit-form/hooks/useRecommendedVisitTypes.tsx +36 -0
  381. package/src/components/registration/start-visit-form/hooks/useVisitAttributeType.tsx +102 -0
  382. package/src/components/registration/start-visit-form/overflow-menu-extension/custom-end-active-visit.extension.tsx +44 -0
  383. package/src/components/registration/start-visit-form/overflow-menu-extension/overflow-menu-item.extension.tsx +67 -0
  384. package/src/components/registration/start-visit-form/overflow-menu-extension/overflow-menu-item.scss +3 -0
  385. package/src/components/registration/start-visit-form/start-visit-workspace/base-visit-type.component.tsx +126 -0
  386. package/src/components/registration/start-visit-form/start-visit-workspace/base-visit-type.scss +75 -0
  387. package/src/components/registration/start-visit-form/start-visit-workspace/exported-visit-form.workspace.tsx +778 -0
  388. package/src/components/registration/start-visit-form/start-visit-workspace/location-selector.component.tsx +86 -0
  389. package/src/components/registration/start-visit-form/start-visit-workspace/recommended-visit-type.component.tsx +32 -0
  390. package/src/components/registration/start-visit-form/start-visit-workspace/visit-attribute-type.component.tsx +257 -0
  391. package/src/components/registration/start-visit-form/start-visit-workspace/visit-attribute-type.scss +5 -0
  392. package/src/components/registration/start-visit-form/start-visit-workspace/visit-date-time.component.tsx +193 -0
  393. package/src/components/registration/start-visit-form/start-visit-workspace/visit-form.resource.ts +390 -0
  394. package/src/components/registration/start-visit-form/start-visit-workspace/visit-form.scss +144 -0
  395. package/src/components/registration/start-visit-form/visit-form-workspace/visit-form.workspace.tsx +48 -0
  396. package/src/components/registration/type/index.ts +101 -0
  397. package/src/components/reports/index.ts +1 -0
  398. package/src/components/triage/dashboard.component.tsx +0 -2
  399. package/src/config-schema.ts +476 -0
  400. package/src/hooks/useOrders.tsx +2 -2
  401. package/src/hooks/useServiceQueues.tsx +1 -1
  402. package/src/index.ts +81 -15
  403. package/src/routes.json +298 -32
  404. package/src/shared/dashboard-link/dashboard-link.component.tsx +11 -8
  405. package/src/shared/orders/OrderTable.scss +7 -0
  406. package/src/shared/orders/OrderTable.tsx +68 -8
  407. package/src/shared/orders/print-order/print-order.component.tsx +158 -0
  408. package/src/shared/orders/print-order/print-order.scss +225 -0
  409. package/src/shared/orders/print-prescription/print-prescription.component.tsx +65 -0
  410. package/src/shared/orders/print-prescription/print-prescription.resource.ts +45 -0
  411. package/src/shared/orders/print-prescription/print-prescription.scss +25 -0
  412. package/src/shared/otp-verification/index.ts +112 -29
  413. package/src/shared/otp-verification/otp-verification.modal.tsx +1110 -122
  414. package/src/shared/otp-verification/otp-verification.scss +374 -0
  415. package/src/shared/patient-chart/patient-summary-dashboard/patient-summary-dashboard.component.tsx +5 -84
  416. package/src/shared/pin-put/pinput.component.test.tsx +7 -6
  417. package/src/shared/tabs/create-tab-extension.tsx +33 -0
  418. package/src/shared/tabs/extension-tabs.component.tsx +51 -33
  419. package/src/shared/utils/index.ts +140 -5
  420. package/src/types/index.ts +12 -0
  421. package/translations/am.json +443 -131
  422. package/translations/en.json +327 -15
  423. package/translations/fr.json +372 -63
  424. package/translations/sw.json +347 -35
  425. package/tsconfig.json +1 -0
  426. package/dist/1049.js +0 -1
  427. package/dist/1049.js.map +0 -1
  428. package/dist/1072.js +0 -1
  429. package/dist/1072.js.map +0 -1
  430. package/dist/1091.js +0 -1
  431. package/dist/1091.js.map +0 -1
  432. package/dist/1172.js +0 -1
  433. package/dist/1172.js.map +0 -1
  434. package/dist/132.js +0 -1
  435. package/dist/132.js.map +0 -1
  436. package/dist/1354.js +0 -1
  437. package/dist/1354.js.map +0 -1
  438. package/dist/1704.js +0 -1
  439. package/dist/1704.js.map +0 -1
  440. package/dist/1729.js +0 -17
  441. package/dist/1729.js.map +0 -1
  442. package/dist/1757.js +0 -1
  443. package/dist/1757.js.map +0 -1
  444. package/dist/1761.js +0 -1
  445. package/dist/1761.js.map +0 -1
  446. package/dist/1809.js +0 -7
  447. package/dist/1809.js.map +0 -1
  448. package/dist/1842.js +0 -1
  449. package/dist/1874.js +0 -1
  450. package/dist/1874.js.map +0 -1
  451. package/dist/1962.js +0 -1
  452. package/dist/1962.js.map +0 -1
  453. package/dist/2007.js +0 -1
  454. package/dist/2007.js.map +0 -1
  455. package/dist/2024.js +0 -1
  456. package/dist/2024.js.map +0 -1
  457. package/dist/2045.js +0 -1
  458. package/dist/2045.js.map +0 -1
  459. package/dist/2163.js +0 -1
  460. package/dist/2163.js.map +0 -1
  461. package/dist/2191.js +0 -1
  462. package/dist/2191.js.map +0 -1
  463. package/dist/2267.js +0 -1
  464. package/dist/2267.js.map +0 -1
  465. package/dist/231.js +0 -1
  466. package/dist/231.js.map +0 -1
  467. package/dist/236.js +0 -1
  468. package/dist/236.js.map +0 -1
  469. package/dist/2367.js +0 -1
  470. package/dist/2367.js.map +0 -1
  471. package/dist/2463.js +0 -1
  472. package/dist/2463.js.map +0 -1
  473. package/dist/2561.js +0 -1
  474. package/dist/2561.js.map +0 -1
  475. package/dist/2810.js +0 -2
  476. package/dist/2810.js.map +0 -1
  477. package/dist/2867.js +0 -1
  478. package/dist/2867.js.map +0 -1
  479. package/dist/2909.js +0 -1
  480. package/dist/2909.js.map +0 -1
  481. package/dist/2949.js +0 -1
  482. package/dist/2949.js.map +0 -1
  483. package/dist/3040.js +0 -1
  484. package/dist/3119.js +0 -1
  485. package/dist/3119.js.map +0 -1
  486. package/dist/3215.js +0 -1
  487. package/dist/3215.js.map +0 -1
  488. package/dist/322.js +0 -1
  489. package/dist/322.js.map +0 -1
  490. package/dist/3228.js +0 -1
  491. package/dist/3228.js.map +0 -1
  492. package/dist/330.js +0 -1
  493. package/dist/330.js.map +0 -1
  494. package/dist/3472.js +0 -1
  495. package/dist/3472.js.map +0 -1
  496. package/dist/351.js +0 -1
  497. package/dist/351.js.map +0 -1
  498. package/dist/3576.js +0 -1
  499. package/dist/3576.js.map +0 -1
  500. package/dist/397.js +0 -1
  501. package/dist/397.js.map +0 -1
  502. package/dist/4007.js +0 -66
  503. package/dist/4007.js.map +0 -1
  504. package/dist/4200.js +0 -1
  505. package/dist/4200.js.map +0 -1
  506. package/dist/4224.js +0 -1
  507. package/dist/4224.js.map +0 -1
  508. package/dist/43.js +0 -1
  509. package/dist/43.js.map +0 -1
  510. package/dist/4308.js +0 -1
  511. package/dist/4308.js.map +0 -1
  512. package/dist/4399.js +0 -1
  513. package/dist/4399.js.map +0 -1
  514. package/dist/4505.js +0 -1
  515. package/dist/4505.js.map +0 -1
  516. package/dist/4724.js +0 -1
  517. package/dist/4724.js.map +0 -1
  518. package/dist/498.js +0 -1
  519. package/dist/498.js.map +0 -1
  520. package/dist/509.js +0 -1
  521. package/dist/509.js.map +0 -1
  522. package/dist/5096.js +0 -1
  523. package/dist/5096.js.map +0 -1
  524. package/dist/5159.js +0 -13
  525. package/dist/5159.js.map +0 -1
  526. package/dist/5184.js +0 -1
  527. package/dist/5184.js.map +0 -1
  528. package/dist/5270.js +0 -1
  529. package/dist/5270.js.map +0 -1
  530. package/dist/5361.js +0 -1
  531. package/dist/5361.js.map +0 -1
  532. package/dist/5537.js +0 -1
  533. package/dist/5537.js.map +0 -1
  534. package/dist/566.js +0 -1
  535. package/dist/566.js.map +0 -1
  536. package/dist/5733.js +0 -1
  537. package/dist/5733.js.map +0 -1
  538. package/dist/5854.js +0 -1
  539. package/dist/5854.js.map +0 -1
  540. package/dist/5882.js +0 -1
  541. package/dist/6004.js +0 -1
  542. package/dist/6004.js.map +0 -1
  543. package/dist/6121.js +0 -3
  544. package/dist/6121.js.map +0 -1
  545. package/dist/6137.js +0 -1
  546. package/dist/6137.js.map +0 -1
  547. package/dist/6356.js +0 -1
  548. package/dist/6356.js.map +0 -1
  549. package/dist/6571.js +0 -1
  550. package/dist/6571.js.map +0 -1
  551. package/dist/6721.js +0 -1
  552. package/dist/6721.js.map +0 -1
  553. package/dist/6858.js +0 -1
  554. package/dist/6858.js.map +0 -1
  555. package/dist/6925.js +0 -1
  556. package/dist/6925.js.map +0 -1
  557. package/dist/6960.js +0 -1
  558. package/dist/6960.js.map +0 -1
  559. package/dist/7085.js +0 -1
  560. package/dist/7085.js.map +0 -1
  561. package/dist/7127.js +0 -1
  562. package/dist/7176.js +0 -1
  563. package/dist/7176.js.map +0 -1
  564. package/dist/7237.js +0 -1
  565. package/dist/7237.js.map +0 -1
  566. package/dist/725.js +0 -1
  567. package/dist/725.js.map +0 -1
  568. package/dist/7288.js +0 -1
  569. package/dist/7329.js +0 -1
  570. package/dist/7329.js.map +0 -1
  571. package/dist/7334.js +0 -1
  572. package/dist/7334.js.map +0 -1
  573. package/dist/7461.js +0 -1
  574. package/dist/7461.js.map +0 -1
  575. package/dist/7517.js +0 -1
  576. package/dist/7517.js.map +0 -1
  577. package/dist/7837.js +0 -1
  578. package/dist/7837.js.map +0 -1
  579. package/dist/8103.js +0 -1
  580. package/dist/8103.js.map +0 -1
  581. package/dist/8110.js +0 -1
  582. package/dist/8110.js.map +0 -1
  583. package/dist/8179.js +0 -6
  584. package/dist/8179.js.map +0 -1
  585. package/dist/8189.js +0 -1
  586. package/dist/8189.js.map +0 -1
  587. package/dist/8209.js +0 -1
  588. package/dist/8209.js.map +0 -1
  589. package/dist/8249.js +0 -1
  590. package/dist/8249.js.map +0 -1
  591. package/dist/8285.js +0 -1
  592. package/dist/8285.js.map +0 -1
  593. package/dist/8470.js +0 -1
  594. package/dist/8470.js.map +0 -1
  595. package/dist/8502.js +0 -1
  596. package/dist/8502.js.map +0 -1
  597. package/dist/866.js +0 -1
  598. package/dist/866.js.map +0 -1
  599. package/dist/8889.js +0 -11
  600. package/dist/8889.js.map +0 -1
  601. package/dist/8990.js +0 -1
  602. package/dist/8990.js.map +0 -1
  603. package/dist/8994.js +0 -1
  604. package/dist/8994.js.map +0 -1
  605. package/dist/9049.js +0 -1
  606. package/dist/9049.js.map +0 -1
  607. package/dist/9054.js +0 -1
  608. package/dist/9054.js.map +0 -1
  609. package/dist/916.js +0 -1
  610. package/dist/9231.js +0 -1
  611. package/dist/9231.js.map +0 -1
  612. package/dist/9408.js +0 -1
  613. package/dist/9408.js.map +0 -1
  614. package/dist/9425.js +0 -1
  615. package/dist/9466.js +0 -1
  616. package/dist/9466.js.map +0 -1
  617. package/dist/9919.js +0 -1
  618. package/dist/9919.js.map +0 -1
  619. package/dist/9931.js +0 -1
  620. package/dist/9931.js.map +0 -1
  621. package/jest.config.js +0 -3
  622. package/src/shared/orders/OrdersTabs.tsx +0 -58
  623. package/src/shared/patient-chart/patient-chart.component.tsx +0 -61
  624. package/src/shared/patient-chart/patient-chart.resources.ts +0 -180
  625. package/src/shared/patient-chart/patient-chart.scss +0 -15
  626. package/src/shared/patient-chart/useInitialize.ts +0 -24
  627. /package/src/shared/{patient-chart/patient-summary-dashboard → tabs}/patient-summary-dashboard.scss +0 -0
@@ -1,22 +1,69 @@
1
1
  import {
2
2
  Button,
3
3
  ButtonSet,
4
+ Dropdown,
5
+ FileUploader,
4
6
  InlineLoading,
5
7
  InlineNotification,
6
8
  ModalBody,
7
9
  ModalFooter,
8
10
  ModalHeader,
9
- TextInput,
10
- IconButton,
11
+ NumberInput,
12
+ TextArea,
11
13
  } from '@carbon/react';
12
- import React, { FC, useState, useEffect } from 'react';
14
+ import {
15
+ Phone,
16
+ Edit,
17
+ FingerprintRecognition,
18
+ ChatBot,
19
+ RuleLocked,
20
+ Renew,
21
+ ChevronRight,
22
+ ArrowLeft,
23
+ Time,
24
+ } from '@carbon/react/icons';
25
+ import React, { FC, useState, useEffect, useCallback, useRef } from 'react';
13
26
  import { useTranslation } from 'react-i18next';
14
27
  import styles from './otp-verification.scss';
15
28
  import PinPut from '../pin-put/pinput.component';
16
- import { Phone, Edit } from '@carbon/react/icons';
17
29
  import { PHONE_NUMBER_REGEX } from '../../constants';
18
30
  import OTPCountdown from './pin-counter/pin-counter.component';
19
- import { maskPhoneNumber } from '../utils';
31
+ import { extractFetchError, maskPhoneNumber } from '../utils';
32
+
33
+ const MAX_VERIFY_ATTEMPTS = 3;
34
+ const BIOMETRIC_AUTO_CLOSE_MS = 1500;
35
+ const MIN_REASON_LENGTH = 10;
36
+
37
+ const WHITELIST_POLL_INTERVAL_MS = 5_000;
38
+ const WHITELIST_POLL_TIMEOUT_MS = 60_000;
39
+
40
+ const BIOMETRIC_POLL_INTERVAL_MS = 3_000;
41
+ const BIOMETRIC_POLL_TIMEOUT_MS = 3 * 60_000;
42
+
43
+ type Mode =
44
+ | 'auth-landing'
45
+ | 'landing'
46
+ | 'verify-otp'
47
+ | 'biometric'
48
+ | 'biometric-failed'
49
+ | 'whitelist-submit'
50
+ | 'whitelist-waiting';
51
+
52
+ type WhitelistReason = {
53
+ code: string;
54
+ label: string;
55
+ description: string;
56
+ review_type: 'AUTOMATIC' | 'MANUAL';
57
+ requires_attachments: boolean;
58
+ };
59
+
60
+ export type WhitelistPollResult = {
61
+ is_whitelisted: boolean;
62
+ has_pending: boolean;
63
+ is_rejected?: boolean;
64
+ latest_status: string | null;
65
+ reviewer_note?: string | null;
66
+ };
20
67
 
21
68
  type OTPVerificationModalProps = {
22
69
  onClose?: () => void;
@@ -29,6 +76,48 @@ type OTPVerificationModalProps = {
29
76
  onRequestOtp?: (phoneNumber: string) => Promise<void>;
30
77
  expiryMinutes?: number;
31
78
  onCleanup?: () => void;
79
+
80
+ authMode?: 'otp-only' | 'multi';
81
+ whitelistedForOTP?: boolean;
82
+ facilityBiometricsEnforced?: boolean;
83
+ onStartBiometric?: () => Promise<{
84
+ embed_url: string;
85
+ authorization_code: string;
86
+ consent_token: string;
87
+ token: string;
88
+ guid: string;
89
+ }>;
90
+ onBiometricSuccess?: (result: {
91
+ authorization_code: string;
92
+ consent_token: string;
93
+ guid: string;
94
+ }) => Promise<void> | void;
95
+ onBiometricCancel?: (consentToken: string | null) => Promise<void> | void;
96
+ onCheckBiometricStatus?: (token: string) => Promise<{
97
+ status: 'PENDING' | 'APPROVED' | 'REJECTED';
98
+ is_complete: boolean;
99
+ is_approved: boolean;
100
+ guid: string | null;
101
+ authorization_code: string | null;
102
+ }>;
103
+ patientCRId?: string;
104
+
105
+ whitelistReasons?: WhitelistReason[];
106
+
107
+ onSubmitWhitelist?: (params: {
108
+ reasonType: string;
109
+ reason: string;
110
+ biometricAttempts: number;
111
+ attachment: File | null;
112
+ }) => Promise<{ success: boolean; review_type?: string; error?: string }>;
113
+
114
+ onCheckWhitelistStatus?: (crId: string) => Promise<WhitelistPollResult>;
115
+
116
+ onRequestWhitelist?: () => void;
117
+
118
+ onRejected?: () => void;
119
+ ekycOrigin?: string;
120
+ visitAction?: 'start' | 'end';
32
121
  };
33
122
 
34
123
  const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
@@ -42,29 +131,156 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
42
131
  onVerificationSuccess,
43
132
  expiryMinutes = 5,
44
133
  onCleanup,
134
+ authMode = 'otp-only',
135
+ whitelistedForOTP = false,
136
+ facilityBiometricsEnforced = false,
137
+ onStartBiometric,
138
+ onBiometricSuccess,
139
+ onBiometricCancel,
140
+ onCheckBiometricStatus,
141
+ patientCRId,
142
+ whitelistReasons = [],
143
+ onSubmitWhitelist,
144
+ onCheckWhitelistStatus,
145
+ onRequestWhitelist,
146
+ onRejected,
147
+ ekycOrigin,
148
+ visitAction = 'start',
45
149
  }) => {
46
150
  const { t } = useTranslation();
151
+ const canUseOtp = !facilityBiometricsEnforced || whitelistedForOTP;
47
152
  const [otp, setOtp] = useState('');
48
153
  const [newPhoneNumber, setNewPhoneNumber] = useState(phoneNumber);
49
154
  const [isLoading, setIsLoading] = useState(false);
50
- const [error, setError] = useState<{ type: 'request' | 'verification'; error: Error } | null>(null);
51
- const [mode, setMode] = useState<'landing' | 'verify-otp' | 'change-number'>('landing');
155
+ const [error, setError] = useState<{
156
+ type: 'request' | 'verification' | 'biometric' | 'whitelist';
157
+ message: string;
158
+ } | null>(null);
52
159
  const [requestingOtp, setRequestingOtp] = useState(false);
53
160
  const [currentPhoneNumber, setCurrentPhoneNumber] = useState(phoneNumber);
161
+ const [manualCheckLoading, setManualCheckLoading] = useState(false);
54
162
 
55
163
  const [countdownResetTrigger, setCountdownResetTrigger] = useState(0);
56
164
  const [isCountdownActive, setIsCountdownActive] = useState(false);
57
165
 
166
+ const [failedVerifyCount, setFailedVerifyCount] = useState(0);
167
+ const showEscapeHatch = failedVerifyCount >= MAX_VERIFY_ATTEMPTS;
168
+
169
+ const [biometricLoading, setBiometricLoading] = useState(false);
170
+ const [biometricEmbedUrl, setBiometricEmbedUrl] = useState<string | null>(null);
171
+ const biometricResultRef = useRef<{
172
+ authorization_code: string;
173
+ consent_token: string;
174
+ token: string;
175
+ guid: string;
176
+ } | null>(null);
177
+ const [whitelistReasonCode, setWhitelistReasonCode] = useState<string>('');
178
+ const [whitelistReasonText, setWhitelistReasonText] = useState<string>('');
179
+ const [whitelistBiometricAttempts, setWhitelistBiometricAttempts] = useState<number>(0);
180
+ const [whitelistAttachment, setWhitelistAttachment] = useState<File | null>(null);
181
+ const [whitelistSubmitting, setWhitelistSubmitting] = useState(false);
182
+ const [checkingPendingRequest, setCheckingPendingRequest] = useState(false);
183
+
184
+ const [whitelistReviewType, setWhitelistReviewType] = useState<string | null>(null);
185
+ const [pollElapsedSec, setPollElapsedSec] = useState(0);
186
+ const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
187
+ const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
188
+ const pollStartTimeRef = useRef<number>(0);
189
+
190
+ const [autoPollEnded, setAutoPollEnded] = useState(false);
191
+
192
+ const [biometricPollElapsedSec, setBiometricPollElapsedSec] = useState(0);
193
+ const [biometricFinalizing, setBiometricFinalizing] = useState(false);
194
+ const [biometricRetrying, setBiometricRetrying] = useState(false);
195
+ const biometricPollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
196
+ const biometricPollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
197
+ const biometricPollStartRef = useRef<number>(0);
198
+
199
+ const [mode, setMode] = useState<Mode>(authMode === 'multi' ? 'auth-landing' : 'landing');
200
+
201
+ const stopBiometricPolling = useCallback(() => {
202
+ if (biometricPollIntervalRef.current) {
203
+ clearInterval(biometricPollIntervalRef.current);
204
+ biometricPollIntervalRef.current = null;
205
+ }
206
+ if (biometricPollTimeoutRef.current) {
207
+ clearTimeout(biometricPollTimeoutRef.current);
208
+ biometricPollTimeoutRef.current = null;
209
+ }
210
+ }, []);
211
+
212
+ // Reset mode and critical state when authMode or context changes (e.g., when modal is reused for different flows)
213
+ useEffect(() => {
214
+ const initialMode = authMode === 'multi' ? 'auth-landing' : 'landing';
215
+ setMode(initialMode);
216
+ setError(null);
217
+ setOtp('');
218
+ setFailedVerifyCount(0);
219
+
220
+ // Reset biometric state
221
+ setBiometricEmbedUrl(null);
222
+ setBiometricLoading(false);
223
+ setBiometricRetrying(false);
224
+ setBiometricFinalizing(false);
225
+ setBiometricPollElapsedSec(0);
226
+ biometricResultRef.current = null;
227
+
228
+ // Reset phone number and whitelist states
229
+ setNewPhoneNumber(phoneNumber);
230
+ setCurrentPhoneNumber(phoneNumber);
231
+ setWhitelistReasonCode('');
232
+ setWhitelistReasonText('');
233
+ setWhitelistBiometricAttempts(0);
234
+ setWhitelistAttachment(null);
235
+ setWhitelistSubmitting(false);
236
+
237
+ // Stop any active polling
238
+ stopBiometricPolling();
239
+ }, [authMode, phoneNumber, patientCRId, stopBiometricPolling]);
240
+
58
241
  useEffect(() => {
59
242
  return () => {
60
243
  onCleanup?.();
244
+ if (pollIntervalRef.current) {
245
+ clearInterval(pollIntervalRef.current);
246
+ }
247
+ if (pollTimeoutRef.current) {
248
+ clearTimeout(pollTimeoutRef.current);
249
+ }
250
+ if (biometricPollIntervalRef.current) {
251
+ clearInterval(biometricPollIntervalRef.current);
252
+ }
253
+ if (biometricPollTimeoutRef.current) {
254
+ clearTimeout(biometricPollTimeoutRef.current);
255
+ }
61
256
  };
62
257
  }, [onCleanup]);
63
258
 
64
- const handleClose = () => {
259
+ const selectedReason = whitelistReasons.find((r) => r.code === whitelistReasonCode);
260
+ const reasonRequiresAttachment = selectedReason?.requires_attachments ?? false;
261
+ const pollElapsedDisplay = `${Math.floor(pollElapsedSec / 60)}:${String(pollElapsedSec % 60).padStart(2, '0')}`;
262
+ const pollTotalDisplay = `${Math.floor(WHITELIST_POLL_TIMEOUT_MS / 60_000)}:00`;
263
+
264
+ const stopPolling = useCallback(() => {
265
+ if (pollIntervalRef.current) {
266
+ clearInterval(pollIntervalRef.current);
267
+ pollIntervalRef.current = null;
268
+ }
269
+ if (pollTimeoutRef.current) {
270
+ clearTimeout(pollTimeoutRef.current);
271
+ pollTimeoutRef.current = null;
272
+ }
273
+ }, []);
274
+
275
+ const handleClose = useCallback(() => {
276
+ stopPolling();
277
+ stopBiometricPolling();
278
+ if (biometricResultRef.current && (mode === 'biometric' || mode === 'biometric-failed')) {
279
+ onBiometricCancel?.(biometricResultRef.current.consent_token);
280
+ }
65
281
  onCleanup?.();
66
282
  onClose?.();
67
- };
283
+ }, [biometricResultRef, mode, onBiometricCancel, onCleanup, onClose, stopPolling, stopBiometricPolling]);
68
284
 
69
285
  const handleVerify = async () => {
70
286
  setError(null);
@@ -72,11 +288,13 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
72
288
  setIsLoading(true);
73
289
  await onVerify?.(otp);
74
290
  onVerificationSuccess?.();
75
- setTimeout(() => {
76
- handleClose();
77
- }, 1000);
78
- } catch (error) {
79
- setError({ type: 'verification', error: error as Error });
291
+ setTimeout(() => handleClose(), 1000);
292
+ } catch (err: unknown) {
293
+ setFailedVerifyCount((n) => n + 1);
294
+ setError({
295
+ type: 'verification',
296
+ message: extractFetchError(err, t('otpVerificationFailed', 'OTP verification failed. Please try again.')),
297
+ });
80
298
  } finally {
81
299
  setIsLoading(false);
82
300
  }
@@ -87,149 +305,916 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
87
305
  try {
88
306
  setRequestingOtp(true);
89
307
  await onRequestOtp?.(phone);
90
-
91
308
  setOtp('');
92
309
  setCurrentPhoneNumber(phone);
93
310
  setMode('verify-otp');
311
+ setFailedVerifyCount(0);
94
312
 
95
313
  setIsCountdownActive(true);
96
314
  setCountdownResetTrigger((prev) => prev + 1);
97
- } catch (error) {
98
- setError({ type: 'request', error: error as Error });
315
+ } catch (err: unknown) {
316
+ setError({
317
+ type: 'request',
318
+ message: extractFetchError(err, t('otpRequestFailed', 'Failed to send OTP. Please try again.')),
319
+ });
99
320
  } finally {
100
321
  setRequestingOtp(false);
101
322
  }
102
323
  };
103
324
 
104
- const handleUseDifferentNumber = () => {
105
- setOtp('');
106
- setIsCountdownActive(false);
107
- setNewPhoneNumber(currentPhoneNumber);
108
- setMode('change-number');
325
+ const handleStartBiometric = async () => {
326
+ setError(null);
327
+ setBiometricLoading(true);
328
+ try {
329
+ const result = await onStartBiometric?.();
330
+ if (!result || !result.embed_url) {
331
+ throw new Error(t('biometricNoEmbedUrl', 'Biometric session did not return an embed URL.'));
332
+ }
333
+ biometricResultRef.current = {
334
+ authorization_code: result.authorization_code,
335
+ consent_token: result.consent_token,
336
+ token: result.token,
337
+ guid: result.guid,
338
+ };
339
+ setBiometricEmbedUrl(result.embed_url);
340
+ setMode('biometric');
341
+ } catch (err) {
342
+ setError({
343
+ type: 'biometric',
344
+ message: extractFetchError(err, t('biometricStartFailed', 'Could not start biometric session.')),
345
+ });
346
+ setMode('biometric-failed');
347
+ } finally {
348
+ setBiometricLoading(false);
349
+ }
350
+ };
351
+
352
+ const handleRetryBiometric = async () => {
353
+ setError(null);
354
+ setBiometricRetrying(true);
355
+ try {
356
+ if (biometricResultRef.current?.consent_token) {
357
+ try {
358
+ await onBiometricCancel?.(biometricResultRef.current.consent_token);
359
+ } catch (rejectErr) {
360
+ console.warn('Failed to reject previous biometric auth:', rejectErr);
361
+ }
362
+ }
363
+ biometricResultRef.current = null;
364
+ setBiometricEmbedUrl(null);
365
+ await handleStartBiometric();
366
+ } finally {
367
+ setBiometricRetrying(false);
368
+ }
369
+ };
370
+ useEffect(() => {
371
+ if (mode !== 'biometric') {
372
+ stopBiometricPolling();
373
+ return;
374
+ }
375
+
376
+ const finalize = async (result: NonNullable<typeof biometricResultRef.current>) => {
377
+ setBiometricFinalizing(true);
378
+ try {
379
+ await onBiometricSuccess?.(result);
380
+ setTimeout(() => handleClose(), BIOMETRIC_AUTO_CLOSE_MS);
381
+ } catch (err) {
382
+ setError({
383
+ type: 'biometric',
384
+ message: extractFetchError(
385
+ err,
386
+ t('biometricVisitFailed', 'Biometric was approved but visit creation failed.'),
387
+ ),
388
+ });
389
+ setMode('biometric-failed');
390
+ } finally {
391
+ setBiometricFinalizing(false);
392
+ }
393
+ };
394
+
395
+ const handleMessage = (event: MessageEvent) => {
396
+ if (ekycOrigin && event.origin !== ekycOrigin) {
397
+ return;
398
+ }
399
+ const data = event.data;
400
+ if (!data || typeof data !== 'object') {
401
+ return;
402
+ }
403
+ if (data.type !== 'EKYC_COMPLETE') {
404
+ return;
405
+ }
406
+
407
+ if (data.status === 'SUCCESS' && biometricResultRef.current) {
408
+ stopBiometricPolling();
409
+ finalize(biometricResultRef.current).catch((err) => {
410
+ console.warn('Biometric finalize (postMessage) failed:', err);
411
+ });
412
+ } else {
413
+ stopBiometricPolling();
414
+ setMode('biometric-failed');
415
+ }
416
+ };
417
+ window.addEventListener('message', handleMessage);
418
+
419
+ if (onCheckBiometricStatus && biometricResultRef.current?.token) {
420
+ const token = biometricResultRef.current.token;
421
+ biometricPollStartRef.current = Date.now();
422
+ setBiometricPollElapsedSec(0);
423
+
424
+ const doPoll = async () => {
425
+ try {
426
+ const status = await onCheckBiometricStatus(token);
427
+ const elapsed = Math.floor((Date.now() - biometricPollStartRef.current) / 1000);
428
+ setBiometricPollElapsedSec(elapsed);
429
+
430
+ if (status.is_approved && biometricResultRef.current) {
431
+ stopBiometricPolling();
432
+ const refreshedGuid = status.guid ?? biometricResultRef.current.guid;
433
+ await finalize({
434
+ ...biometricResultRef.current,
435
+ guid: refreshedGuid,
436
+ authorization_code: status.authorization_code ?? biometricResultRef.current.authorization_code,
437
+ });
438
+ return;
439
+ }
440
+ if (status.status === 'REJECTED' || (status.is_complete && !status.is_approved)) {
441
+ stopBiometricPolling();
442
+ setError({
443
+ type: 'biometric',
444
+ message: t('biometricRejected', 'Biometric verification was rejected. You can try again or use OTP.'),
445
+ });
446
+ setMode('biometric-failed');
447
+ return;
448
+ }
449
+ } catch (pollErr) {
450
+ console.warn('Biometric status poll failed:', pollErr);
451
+ }
452
+ };
453
+ const runPoll = () => {
454
+ doPoll().catch((err) => {
455
+ console.warn('Biometric poll wrapper error:', err);
456
+ });
457
+ };
458
+
459
+ runPoll();
460
+ biometricPollIntervalRef.current = setInterval(runPoll, BIOMETRIC_POLL_INTERVAL_MS);
461
+ biometricPollTimeoutRef.current = setTimeout(() => {
462
+ stopBiometricPolling();
463
+ setError({
464
+ type: 'biometric',
465
+ message: t('biometricTimeout', 'Biometric verification did not complete in time. Please try again.'),
466
+ });
467
+ setMode('biometric-failed');
468
+ }, BIOMETRIC_POLL_TIMEOUT_MS);
469
+ }
470
+
471
+ return () => {
472
+ window.removeEventListener('message', handleMessage);
473
+ stopBiometricPolling();
474
+ };
475
+ }, [mode, ekycOrigin, onBiometricSuccess, onCheckBiometricStatus, handleClose, stopBiometricPolling, t]);
476
+
477
+ const checkWhitelistStatusOnce = useCallback(
478
+ async (opts?: { manual?: boolean }) => {
479
+ if (!patientCRId || !onCheckWhitelistStatus) {
480
+ return;
481
+ }
482
+ if (opts?.manual) {
483
+ setManualCheckLoading(true);
484
+ }
485
+ try {
486
+ const status = await onCheckWhitelistStatus(patientCRId);
487
+ const elapsed = Math.floor((Date.now() - pollStartTimeRef.current) / 1000);
488
+ setPollElapsedSec(elapsed);
489
+
490
+ if (status.is_whitelisted) {
491
+ stopPolling();
492
+ setError(null);
493
+ setMode('landing');
494
+ return;
495
+ }
496
+
497
+ if (status.is_rejected) {
498
+ stopPolling();
499
+ setError({
500
+ type: 'whitelist',
501
+ message:
502
+ status.reviewer_note?.trim() ||
503
+ t(
504
+ 'whitelistRejected',
505
+ 'The whitelist request was rejected by SHA. You may submit a new request or use biometrics.',
506
+ ),
507
+ });
508
+ setMode('biometric-failed');
509
+ return;
510
+ }
511
+
512
+ if (opts?.manual && !status.is_whitelisted && !status.is_rejected) {
513
+ setError({
514
+ type: 'whitelist',
515
+ message: t(
516
+ 'whitelistStillPendingShort',
517
+ 'Request is still pending review. We will keep checking automatically.',
518
+ ),
519
+ });
520
+ }
521
+ } catch (pollErr) {
522
+ if (opts?.manual) {
523
+ setError({
524
+ type: 'whitelist',
525
+ message: extractFetchError(pollErr, t('whitelistCheckFailed', 'Status check failed. Please try again.')),
526
+ });
527
+ } else {
528
+ console.warn('Whitelist status poll failed:', pollErr);
529
+ }
530
+ } finally {
531
+ if (opts?.manual) {
532
+ setManualCheckLoading(false);
533
+ }
534
+ }
535
+ },
536
+ [patientCRId, onCheckWhitelistStatus, stopPolling, t],
537
+ );
538
+
539
+ const startPollingWhitelistStatus = useCallback(() => {
540
+ if (!patientCRId || !onCheckWhitelistStatus) {
541
+ return;
542
+ }
543
+
544
+ pollStartTimeRef.current = Date.now();
545
+ setPollElapsedSec(0);
546
+ setAutoPollEnded(false);
547
+
548
+ checkWhitelistStatusOnce();
549
+ pollIntervalRef.current = setInterval(() => checkWhitelistStatusOnce(), WHITELIST_POLL_INTERVAL_MS);
550
+
551
+ pollTimeoutRef.current = setTimeout(() => {
552
+ stopPolling();
553
+ setAutoPollEnded(true);
554
+ }, WHITELIST_POLL_TIMEOUT_MS);
555
+ }, [patientCRId, onCheckWhitelistStatus, checkWhitelistStatusOnce, stopPolling]);
556
+
557
+ const handleOpenWhitelistFlow = useCallback(async () => {
558
+ if (!patientCRId || !onSubmitWhitelist || whitelistReasons.length === 0) {
559
+ onRequestWhitelist?.();
560
+ handleClose();
561
+ return;
562
+ }
563
+
564
+ setError(null);
565
+
566
+ if (onCheckWhitelistStatus) {
567
+ try {
568
+ setCheckingPendingRequest(true);
569
+ const status = await onCheckWhitelistStatus(patientCRId);
570
+
571
+ if (status.is_whitelisted) {
572
+ setMode('landing');
573
+ return;
574
+ }
575
+
576
+ if (status.has_pending && !status.is_rejected) {
577
+ setWhitelistReviewType(null);
578
+ setMode('whitelist-waiting');
579
+ startPollingWhitelistStatus();
580
+ return;
581
+ }
582
+ } catch (err) {
583
+ console.warn('Pending whitelist status check failed; opening submission form:', err);
584
+ } finally {
585
+ setCheckingPendingRequest(false);
586
+ }
587
+ }
588
+
589
+ setWhitelistReasonCode('');
590
+ setWhitelistReasonText('');
591
+ setWhitelistBiometricAttempts(0);
592
+ setWhitelistAttachment(null);
593
+ setMode('whitelist-submit');
594
+ }, [
595
+ patientCRId,
596
+ onSubmitWhitelist,
597
+ whitelistReasons,
598
+ onCheckWhitelistStatus,
599
+ onRequestWhitelist,
600
+ handleClose,
601
+ startPollingWhitelistStatus,
602
+ ]);
603
+
604
+ const handleSubmitWhitelist = async () => {
605
+ if (!onSubmitWhitelist) {
606
+ return;
607
+ }
608
+ setError(null);
609
+
610
+ if (!whitelistReasonCode) {
611
+ setError({ type: 'whitelist', message: t('whitelistReasonRequired', 'Please select a reason.') });
612
+ return;
613
+ }
614
+ if (whitelistReasonText.trim().length < MIN_REASON_LENGTH) {
615
+ setError({
616
+ type: 'whitelist',
617
+ message: t('whitelistReasonTooShort', 'Justification must be at least {{min}} characters.', {
618
+ min: MIN_REASON_LENGTH,
619
+ }),
620
+ });
621
+ return;
622
+ }
623
+ if (reasonRequiresAttachment && !whitelistAttachment) {
624
+ setError({
625
+ type: 'whitelist',
626
+ message: t('whitelistAttachmentRequired', 'This reason requires an attachment (e.g. medical report).'),
627
+ });
628
+ return;
629
+ }
630
+ if (whitelistAttachment && whitelistAttachment.size === 0) {
631
+ setError({
632
+ type: 'whitelist',
633
+ message: t('whitelistAttachmentEmpty', 'The selected file is empty. Please re-select it.'),
634
+ });
635
+ return;
636
+ }
637
+
638
+ setWhitelistSubmitting(true);
639
+ try {
640
+ const result = await onSubmitWhitelist({
641
+ reasonType: whitelistReasonCode,
642
+ reason: whitelistReasonText.trim(),
643
+ biometricAttempts: whitelistBiometricAttempts,
644
+ attachment: whitelistAttachment,
645
+ });
646
+ if (!result.success) {
647
+ throw new Error(result.error ?? t('whitelistSubmitFailed', 'Whitelist submission failed.'));
648
+ }
649
+
650
+ setWhitelistReviewType(result.review_type ?? null);
651
+ setMode('whitelist-waiting');
652
+ startPollingWhitelistStatus();
653
+ } catch (err) {
654
+ setError({
655
+ type: 'whitelist',
656
+ message: extractFetchError(err, t('whitelistSubmitFailed', 'Whitelist submission failed.')),
657
+ });
658
+ } finally {
659
+ setWhitelistSubmitting(false);
660
+ }
661
+ };
662
+
663
+ const handleCancelWhitelistWaiting = () => {
664
+ stopPolling();
665
+ handleClose();
109
666
  };
110
667
 
111
- const handleBackToLanding = () => {
112
- setNewPhoneNumber(currentPhoneNumber);
668
+ const handleSwitchToOtpFromAuthLanding = () => {
113
669
  setMode('landing');
114
670
  };
115
671
 
672
+ const handleEscapeHatchFromVerifyOtp = () => {
673
+ if (canUseOtp) {
674
+ setOtp('');
675
+ setIsCountdownActive(false);
676
+ setMode('auth-landing');
677
+ } else {
678
+ handleOpenWhitelistFlow();
679
+ }
680
+ };
681
+
682
+ const handleBackFromWhitelist = () => {
683
+ setError(null);
684
+ setMode('auth-landing');
685
+ };
686
+
116
687
  const handleCountdownExpired = () => {
117
688
  setError({
118
689
  type: 'verification',
119
- error: new Error(t('otpExpiredMessage', 'OTP has expired. Please request a new one.')),
690
+ message: t('otpExpiredMessage', 'OTP has expired. Please request a new one.'),
120
691
  });
121
692
  setOtp('');
122
693
  setIsCountdownActive(false);
123
694
  setMode('landing');
124
- onCleanup?.();
125
695
  };
126
696
 
127
- const isValidPhoneNumber = (phone: string) => {
128
- return PHONE_NUMBER_REGEX.test(phone);
697
+ const handleReject = () => {
698
+ onRejected?.();
699
+ handleClose();
129
700
  };
130
701
 
702
+ const handleAttachmentChange = useCallback(
703
+ (event: any) => {
704
+ const fromAddedFiles: File[] = Array.isArray(event?.addedFiles) ? event.addedFiles : [];
705
+ const fromTargetFiles: File[] = event?.target?.files ? Array.from(event.target.files as FileList) : [];
706
+ const candidates = fromAddedFiles.length > 0 ? fromAddedFiles : fromTargetFiles;
707
+
708
+ const file = candidates.find((f) => f && f.size > 0) ?? null;
709
+
710
+ if (file) {
711
+ setError(null);
712
+ setWhitelistAttachment(file);
713
+ return;
714
+ }
715
+
716
+ if (candidates.length > 0) {
717
+ setWhitelistAttachment(null);
718
+ setError({
719
+ type: 'whitelist',
720
+ message: t('whitelistAttachmentEmpty', 'The selected file appears to be empty. Please re-select it.'),
721
+ });
722
+ return;
723
+ }
724
+ setWhitelistAttachment(null);
725
+ },
726
+ [t],
727
+ );
728
+ const isValidPhoneNumber = (phone: string) => PHONE_NUMBER_REGEX.test(phone);
729
+
730
+ const headerTitle = (() => {
731
+ switch (mode) {
732
+ case 'auth-landing':
733
+ return t('secureAuthTitle', 'Secure Patient Authentication');
734
+ case 'biometric':
735
+ return t('biometricCaptureTitle', 'Biometric capture');
736
+ case 'biometric-failed':
737
+ return t('secureAuthBiometricFailedTitle', 'Biometric Verification Failed');
738
+ case 'whitelist-submit':
739
+ return t('whitelistRequestTitle', 'Request OTP Whitelisting');
740
+ case 'whitelist-waiting':
741
+ return t('whitelistWaitingTitle', 'Waiting for whitelist approval');
742
+ default:
743
+ return t('otpVerification', 'OTP Verification');
744
+ }
745
+ })();
746
+
747
+ const headerEyebrow =
748
+ mode === 'auth-landing' ||
749
+ mode === 'biometric-failed' ||
750
+ mode === 'whitelist-submit' ||
751
+ mode === 'whitelist-waiting'
752
+ ? t('secureAuthEyebrow', 'Biometrics or OTP')
753
+ : null;
754
+
755
+ const reasonItems = whitelistReasons.map((r) => ({
756
+ id: r.code,
757
+ label: r.label,
758
+ description: r.description,
759
+ review_type: r.review_type,
760
+ requires_attachments: r.requires_attachments,
761
+ }));
131
762
  return (
132
763
  <React.Fragment>
133
764
  <ModalHeader className={styles.sectionHeader} closeModal={handleClose}>
134
- {t('otpVerification', 'OTP Verification')}
765
+ {headerEyebrow && <p className={styles.eyebrow}>{headerEyebrow}</p>}
766
+ {headerTitle}
135
767
  </ModalHeader>
768
+
136
769
  <ModalBody>
137
- {requestingOtp && <InlineLoading description={t('requestingOtp', 'Requesting OTP...')} />}
138
- {!requestingOtp && (
139
- <div>
140
- {error && (
770
+ {error && (
771
+ <>
772
+ <InlineNotification
773
+ lowContrast
774
+ kind={error.type === 'verification' ? 'warning' : 'error'}
775
+ title={
776
+ error.type === 'request'
777
+ ? t('otpRequestError', 'Error requesting OTP')
778
+ : error.type === 'biometric'
779
+ ? t('biometricError', 'Biometric error')
780
+ : error.type === 'whitelist'
781
+ ? t('whitelistError', 'Whitelist submission error')
782
+ : t('otpVerificationError', 'Error verifying OTP')
783
+ }
784
+ subtitle={error.message}
785
+ />
786
+ <br />
787
+ </>
788
+ )}
789
+
790
+ {mode === 'auth-landing' && (
791
+ <div className={styles.authLandingContainer}>
792
+ <p className={styles.authIntro}>
793
+ {t(
794
+ 'secureAuthIntro',
795
+ 'To ensure the safety of your health records, please verify your identity using one of the following methods:',
796
+ )}
797
+ </p>
798
+
799
+ <button className={styles.methodCard} onClick={handleStartBiometric} disabled={biometricLoading}>
800
+ <div className={styles.methodIconWrap}>
801
+ <FingerprintRecognition size={22} />
802
+ </div>
803
+ <div className={styles.methodContent}>
804
+ <p className={styles.methodTitle}>{t('btnUseBiometrics', 'Use of Biometrics')}</p>
805
+ <p className={styles.methodDesc}>
806
+ {t('secureAuthBiometricOption', 'Biometric authentication (fingerprint)')}
807
+ </p>
808
+ </div>
809
+ {biometricLoading ? <InlineLoading /> : <ChevronRight size={18} className={styles.methodArrow} />}
810
+ </button>
811
+
812
+ <button
813
+ className={`${styles.methodCard} ${!canUseOtp ? styles.methodCardDisabled : ''}`}
814
+ onClick={handleSwitchToOtpFromAuthLanding}
815
+ disabled={!canUseOtp}>
816
+ <div className={`${styles.methodIconWrap} ${styles.methodIconWarning}`}>
817
+ <ChatBot size={22} />
818
+ </div>
819
+ <div className={styles.methodContent}>
820
+ <div className={styles.methodTitleRow}>
821
+ <p className={styles.methodTitle}>{t('btnUseOtp', 'Use OTP')}</p>
822
+ {!canUseOtp && (
823
+ <span className={styles.badgeRequired}>{t('whitelistingRequired', 'Whitelisting required')}</span>
824
+ )}
825
+ </div>
826
+ <p className={styles.methodDesc}>
827
+ {t(
828
+ 'secureAuthOtpOption',
829
+ 'One-Time Password (Whitelisting is a must) sent to your registered phone number or email',
830
+ )}
831
+ </p>
832
+ </div>
833
+ {canUseOtp && <ChevronRight size={18} className={styles.methodArrow} />}
834
+ </button>
835
+
836
+ {!canUseOtp && (
141
837
  <>
838
+ <div className={styles.dividerWithText}>{t('or', 'or')}</div>
839
+ <button
840
+ className={styles.whitelistLink}
841
+ onClick={handleOpenWhitelistFlow}
842
+ disabled={checkingPendingRequest}>
843
+ {checkingPendingRequest ? (
844
+ <InlineLoading description={t('checkingExistingRequest', 'Checking existing requests…')} />
845
+ ) : (
846
+ <>
847
+ <RuleLocked size={16} />
848
+ <span>{t('btnRequestOtpWhitelisting', 'Request OTP Whitelisting')}</span>
849
+ </>
850
+ )}
851
+ </button>
852
+ </>
853
+ )}
854
+
855
+ <div className={styles.dataNotice}>
856
+ <RuleLocked size={14} />
857
+ <span>
858
+ {t(
859
+ 'secureAuthDataNotice',
860
+ 'All authentication data is securely handled and used only for identity verification.',
861
+ )}
862
+ </span>
863
+ </div>
864
+ </div>
865
+ )}
866
+
867
+ {mode === 'landing' && (
868
+ <div className={styles.otpTriggerContainer}>
869
+ <p>{t('confirmationTxt', 'Verify the phone number before OTP')}</p>
870
+ <div className={styles.phoneNumberDisplay}>
871
+ <Phone className={styles.phoneIcon} />
872
+ <span className={styles.phoneNumber}>
873
+ {currentPhoneNumber ? maskPhoneNumber(currentPhoneNumber) : '—'}
874
+ </span>
875
+ </div>
876
+ <div className={styles.expiryInfo}>
877
+ <p className={styles.expiryText}>
878
+ {t('otpExpiryInfo', 'The OTP will be valid for {{minutes}} minutes after it is sent.', {
879
+ minutes: expiryMinutes,
880
+ })}
881
+ </p>
882
+ </div>
883
+ </div>
884
+ )}
885
+
886
+ {mode === 'verify-otp' && (
887
+ <div className={styles.otpInputContainer}>
888
+ <div className={styles.otpInstruction}>
889
+ {t('enterOtpSentTo', 'Enter the OTP code sent to')} <strong>{maskPhoneNumber(currentPhoneNumber)}</strong>
890
+ </div>
891
+
892
+ {isCountdownActive && (
893
+ <div className={styles.countdownSection}>
894
+ <OTPCountdown
895
+ expiryMinutes={expiryMinutes}
896
+ isActive={isCountdownActive}
897
+ resetTrigger={countdownResetTrigger}
898
+ onExpired={handleCountdownExpired}
899
+ variant="default"
900
+ showIcon={true}
901
+ />
902
+ </div>
903
+ )}
904
+
905
+ <PinPut
906
+ value={otp}
907
+ onChange={setOtp}
908
+ numInputs={otpLength}
909
+ centerBoxes={centerBoxes}
910
+ obscureText={obscureText}
911
+ />
912
+ {showEscapeHatch && (
913
+ <div className={styles.escapeHatch}>
142
914
  <InlineNotification
143
915
  lowContrast
916
+ hideCloseButton
917
+ kind={whitelistedForOTP ? 'info' : 'warning'}
144
918
  title={
145
- error?.type === 'request'
146
- ? t('otpRequestError', 'Error requesting OTP')
147
- : t('otpVerificationError', 'Error Verifying OTP')
919
+ whitelistedForOTP
920
+ ? t('otpFailedWhitelisted', 'OTP not working?')
921
+ : t('otpFailedRepeatedly', 'OTP failed 3 times')
922
+ }
923
+ subtitle={
924
+ whitelistedForOTP
925
+ ? t(
926
+ 'otpFailedWhitelistedSubtitle',
927
+ 'Try biometrics instead — biometric capture is available for whitelisted patients.',
928
+ )
929
+ : facilityBiometricsEnforced
930
+ ? t(
931
+ 'otpFailedNotWhitelistedSubtitle',
932
+ 'Patient may not be receiving the SMS. Submit a whitelist request to bypass OTP for future visits.',
933
+ )
934
+ : t('otpFailedRetrySubtitle', 'OTP failed multiple times. Please try again or use biometrics.')
148
935
  }
149
- subtitle={error?.error?.message}
150
936
  />
151
- <br />
152
- </>
937
+ <Button
938
+ kind="ghost"
939
+ size="md"
940
+ className={styles.escapeHatchButton}
941
+ renderIcon={whitelistedForOTP ? FingerprintRecognition : RuleLocked}
942
+ onClick={handleEscapeHatchFromVerifyOtp}
943
+ disabled={checkingPendingRequest}>
944
+ {checkingPendingRequest ? (
945
+ <InlineLoading description={t('checkingExistingRequest', 'Checking existing requests…')} />
946
+ ) : whitelistedForOTP ? (
947
+ t('switchToBiometric', 'Switch to biometric')
948
+ ) : facilityBiometricsEnforced ? (
949
+ t('btnRequestOtpWhitelisting', 'Request OTP Whitelisting')
950
+ ) : (
951
+ t('switchToBiometric', 'Switch to biometric')
952
+ )}
953
+ </Button>
954
+ </div>
153
955
  )}
154
- {mode === 'landing' ? (
155
- <div className={styles.otpTriggerContainer}>
156
- <p>{t('confirmationTxt', 'Verify the phone number before OTP')}</p>
157
- <div className={styles.phoneNumberDisplay}>
158
- <Phone className={styles.phoneIcon} />
159
- <span className={styles.phoneNumber}>{maskPhoneNumber(currentPhoneNumber)}</span>
160
- <IconButton
161
- kind="ghost"
162
- size="sm"
163
- label={t('changePhoneNumber', 'Change phone number')}
164
- onClick={handleUseDifferentNumber}
165
- className={styles.editButton}>
166
- <Edit />
167
- </IconButton>
956
+ </div>
957
+ )}
958
+ {mode === 'biometric' && biometricEmbedUrl && (
959
+ <div className={styles.biometricContainer}>
960
+ <div className={styles.iframeWrapper}>
961
+ <iframe
962
+ src={biometricEmbedUrl}
963
+ className={styles.biometricIframe}
964
+ title={t('biometricCapture', 'Biometric capture')}
965
+ sandbox="allow-scripts allow-same-origin allow-forms"
966
+ />
967
+ </div>
968
+ <p className={styles.biometricHint}>
969
+ {t('biometricHint', 'Follow the on-screen instructions in the capture window.')}
970
+ </p>
971
+ <div className={styles.whitelistPollProgress}>
972
+ <InlineLoading
973
+ description={
974
+ biometricFinalizing
975
+ ? visitAction === 'end'
976
+ ? t('biometricEndingVisit', 'Verified ending visit…')
977
+ : t('biometricCreatingVisit', 'Verified creating visit…')
978
+ : t('biometricWaitingForApproval', 'Waiting for SHA to confirm capture…')
979
+ }
980
+ status="active"
981
+ />
982
+ </div>
983
+ <span className={styles.whitelistPollTimer}>
984
+ {Math.floor(biometricPollElapsedSec / 60)}:{String(biometricPollElapsedSec % 60).padStart(2, '0')} /{' '}
985
+ {Math.floor(BIOMETRIC_POLL_TIMEOUT_MS / 60_000)}:00
986
+ </span>
987
+ </div>
988
+ )}
989
+
990
+ {mode === 'biometric-failed' && (
991
+ <div className={styles.failedContainer}>
992
+ <InlineNotification
993
+ lowContrast
994
+ hideCloseButton
995
+ kind="error"
996
+ title={t('secureAuthBiometricFailedTitle', 'Biometric Verification Failed')}
997
+ subtitle={
998
+ canUseOtp
999
+ ? t(
1000
+ 'secureAuthBiometricFailedBodyWhitelisted',
1001
+ 'Please try again or use OTP verification to continue.',
1002
+ )
1003
+ : t(
1004
+ 'secureAuthBiometricFailedBody',
1005
+ 'Please try again or use OTP Whitelisting verification to continue.',
1006
+ )
1007
+ }
1008
+ />
1009
+
1010
+ <p className={styles.chooseLabel}>{t('secureAuthChooseReason', 'Choose an option')}</p>
1011
+
1012
+ {canUseOtp ? (
1013
+ <button className={styles.methodCard} onClick={handleSwitchToOtpFromAuthLanding}>
1014
+ <div className={`${styles.methodIconWrap} ${styles.methodIconWarning}`}>
1015
+ <ChatBot size={22} />
168
1016
  </div>
169
- <div className={styles.expiryInfo}>
170
- <p className={styles.expiryText}>
171
- {t('otpExpiryInfo', 'The OTP will be valid for {{minutes}} minutes after it is sent.', {
172
- minutes: expiryMinutes,
173
- })}
1017
+ <div className={styles.methodContent}>
1018
+ <p className={styles.methodTitle}>{t('btnUseOtp', 'Use OTP')}</p>
1019
+ <p className={styles.methodDesc}>
1020
+ {t('useOtpDesc', "Send a One-Time Password to the patient's registered phone or email")}
174
1021
  </p>
175
1022
  </div>
176
- </div>
177
- ) : mode === 'verify-otp' ? (
178
- <div className={styles.otpInputContainer}>
179
- <div className={styles.otpInstruction}>
180
- {t('enterOtpSentTo', 'Enter the OTP code sent to')}{' '}
181
- <strong>{maskPhoneNumber(currentPhoneNumber)}</strong>
1023
+ <ChevronRight size={18} className={styles.methodArrow} />
1024
+ </button>
1025
+ ) : (
1026
+ <button className={styles.methodCard} onClick={handleOpenWhitelistFlow} disabled={checkingPendingRequest}>
1027
+ <div className={`${styles.methodIconWrap} ${styles.methodIconWarning}`}>
1028
+ <RuleLocked size={22} />
1029
+ </div>
1030
+ <div className={styles.methodContent}>
1031
+ <p className={styles.methodTitle}>{t('btnRequestOtpWhitelisting', 'Request OTP Whitelisting')}</p>
1032
+ <p className={styles.methodDesc}>
1033
+ {t(
1034
+ 'requestWhitelistDesc',
1035
+ 'Submit a whitelisting request for this patient. Once approved, OTP can be used.',
1036
+ )}
1037
+ </p>
182
1038
  </div>
1039
+ {checkingPendingRequest ? <InlineLoading /> : <ChevronRight size={18} className={styles.methodArrow} />}
1040
+ </button>
1041
+ )}
183
1042
 
184
- {isCountdownActive && (
185
- <div className={styles.countdownSection}>
186
- <OTPCountdown
187
- expiryMinutes={expiryMinutes}
188
- isActive={isCountdownActive}
189
- resetTrigger={countdownResetTrigger}
190
- onExpired={handleCountdownExpired}
191
- variant="default"
192
- showIcon={true}
193
- />
194
- </div>
195
- )}
1043
+ <Button
1044
+ kind="ghost"
1045
+ size="md"
1046
+ renderIcon={Renew}
1047
+ className={styles.tryAgainButton}
1048
+ onClick={handleRetryBiometric}
1049
+ disabled={biometricRetrying || biometricLoading}>
1050
+ {biometricRetrying || biometricLoading ? (
1051
+ <InlineLoading description={t('biometricRetrying', 'Restarting…')} />
1052
+ ) : (
1053
+ t('tryBiometricsAgain', 'Try biometrics again')
1054
+ )}
1055
+ </Button>
1056
+ </div>
1057
+ )}
196
1058
 
197
- <PinPut
198
- value={otp}
199
- onChange={setOtp}
200
- numInputs={otpLength}
201
- centerBoxes={centerBoxes}
202
- obscureText={obscureText}
203
- />
204
- <Button kind="ghost" size="sm" className={styles.changeNumberLink} onClick={handleUseDifferentNumber}>
205
- {t('useADifferentNumber', 'Use a different number')}
206
- </Button>
207
- </div>
208
- ) : mode === 'change-number' ? (
209
- <div className={styles.changeNumberContainer}>
210
- <TextInput
211
- id={'otp-phone-number'}
212
- labelText={t('phoneNumber', 'Phone number')}
213
- value={newPhoneNumber}
214
- onChange={(ev) => setNewPhoneNumber(ev.target.value)}
215
- placeholder={t('enterPhoneNumber', 'Enter phone number')}
216
- className={styles.phoneInput}
217
- invalid={newPhoneNumber.length > 0 && !isValidPhoneNumber(newPhoneNumber)}
218
- invalidText={t('invalidPhoneNumber', 'Please enter a valid phone number')}
219
- />
220
- <Button kind="ghost" size="sm" onClick={handleBackToLanding} className={styles.backButton}>
221
- {t('back', 'Back')}
222
- </Button>
1059
+ {mode === 'whitelist-submit' && (
1060
+ <div className={styles.whitelistFormContainer}>
1061
+ <p className={styles.whitelistIntro}>
1062
+ {t(
1063
+ 'whitelistFormIntro',
1064
+ 'Submit a whitelist request so this patient can use OTP for the current visit. After approval, OTP will be sent automatically.',
1065
+ )}
1066
+ </p>
1067
+ <Dropdown
1068
+ id="whitelist-reason-type"
1069
+ titleText={t('whitelistReasonType', 'Reason')}
1070
+ label={t('whitelistReasonTypePlaceholder', 'Select a reason')}
1071
+ items={reasonItems}
1072
+ itemToString={(item) => (item ? item.label : '')}
1073
+ selectedItem={reasonItems.find((r) => r.id === whitelistReasonCode) ?? null}
1074
+ onChange={({ selectedItem }) => {
1075
+ setWhitelistReasonCode(selectedItem?.id ?? '');
1076
+ setWhitelistAttachment(null);
1077
+ }}
1078
+ disabled={whitelistSubmitting}
1079
+ />
1080
+ {selectedReason && (
1081
+ <div className={styles.whitelistReasonHint}>
1082
+ <p className={styles.whitelistReasonDesc}>{selectedReason.description}</p>
223
1083
  </div>
224
- ) : null}
1084
+ )}
1085
+ <TextArea
1086
+ id="whitelist-reason-text"
1087
+ labelText={t('whitelistJustification', 'Justification')}
1088
+ helperText={t(
1089
+ 'whitelistJustificationHelper',
1090
+ 'Explain why this patient cannot use biometric or OTP normally. Minimum {{min}} characters.',
1091
+ { min: MIN_REASON_LENGTH },
1092
+ )}
1093
+ value={whitelistReasonText}
1094
+ onChange={(ev) => setWhitelistReasonText(ev.target.value)}
1095
+ maxCount={500}
1096
+ enableCounter
1097
+ rows={4}
1098
+ disabled={whitelistSubmitting}
1099
+ invalid={whitelistReasonText.length > 0 && whitelistReasonText.trim().length < MIN_REASON_LENGTH}
1100
+ invalidText={t('whitelistJustificationTooShort', 'At least {{min}} characters required.', {
1101
+ min: MIN_REASON_LENGTH,
1102
+ })}
1103
+ />
1104
+ <NumberInput
1105
+ id="whitelist-biometric-attempts"
1106
+ label={t('biometricAttemptsLabel', 'Biometric attempts')}
1107
+ helperText={t(
1108
+ 'biometricAttemptsHelper',
1109
+ 'How many times biometric capture has been attempted (use 0 if not applicable).',
1110
+ )}
1111
+ min={0}
1112
+ max={50}
1113
+ value={whitelistBiometricAttempts}
1114
+ onChange={(_ev, { value }) => {
1115
+ const n = typeof value === 'number' ? value : Number(value) || 0;
1116
+ setWhitelistBiometricAttempts(Math.max(0, n));
1117
+ }}
1118
+ disabled={whitelistSubmitting}
1119
+ />
1120
+ {reasonRequiresAttachment && (
1121
+ <FileUploader
1122
+ accept={['.pdf', '.jpg', '.jpeg', '.png']}
1123
+ buttonKind="tertiary"
1124
+ buttonLabel={
1125
+ whitelistAttachment
1126
+ ? t('replaceAttachment', 'Replace attachment')
1127
+ : t('addAttachment', 'Add attachment')
1128
+ }
1129
+ filenameStatus="edit"
1130
+ labelDescription={t(
1131
+ 'attachmentRequiredDesc',
1132
+ 'Required for this reason. Attach a supporting document (PDF/JPG/PNG, max 5 MB).',
1133
+ )}
1134
+ labelTitle={t('attachment', 'Attachment')}
1135
+ multiple={false}
1136
+ onChange={handleAttachmentChange}
1137
+ onDelete={() => setWhitelistAttachment(null)}
1138
+ disabled={whitelistSubmitting}
1139
+ />
1140
+ )}
1141
+ <Button
1142
+ kind="ghost"
1143
+ onClick={handleBackFromWhitelist}
1144
+ renderIcon={ArrowLeft}
1145
+ size="md"
1146
+ className={styles.tryAgainButton}
1147
+ disabled={whitelistSubmitting}>
1148
+ {t('back', 'Back')}
1149
+ </Button>
1150
+ </div>
1151
+ )}
1152
+
1153
+ {mode === 'whitelist-waiting' && (
1154
+ <div className={styles.whitelistWaitingContainer}>
1155
+ <div className={styles.whitelistWaitingIconWrap}>
1156
+ <Time size={32} />
1157
+ </div>
1158
+ <h3 className={styles.whitelistWaitingTitle}>
1159
+ {autoPollEnded
1160
+ ? t('whitelistManualCheckTitle', 'Check status manually')
1161
+ : whitelistReviewType === 'AUTOMATIC'
1162
+ ? t('whitelistAutoApprovalProcessing', 'Processing automatic approval…')
1163
+ : t('whitelistAwaitingManualReview', 'Awaiting manual review…')}
1164
+ </h3>
1165
+ <p className={styles.whitelistWaitingBody}>
1166
+ {autoPollEnded
1167
+ ? t(
1168
+ 'whitelistManualCheckBody',
1169
+ 'Automatic checking has stopped. Tap "Check status now" to see if the request has been approved, or cancel and retry check-in later.',
1170
+ )
1171
+ : whitelistReviewType === 'AUTOMATIC'
1172
+ ? t(
1173
+ 'whitelistAutoApprovalBody',
1174
+ 'Your request was submitted. We are checking with SHA — this usually takes a few seconds. Once approved, OTP will be sent automatically.',
1175
+ )
1176
+ : t(
1177
+ 'whitelistManualReviewBody',
1178
+ 'Your request is queued for manual review by SHA. We will keep checking. If you would rather try again later, you can cancel below.',
1179
+ )}
1180
+ </p>
1181
+
1182
+ {!autoPollEnded && (
1183
+ <>
1184
+ <div className={styles.whitelistPollProgress}>
1185
+ <InlineLoading description={t('whitelistChecking', 'Checking status…')} status="active" />
1186
+ </div>
1187
+ <span className={styles.whitelistPollTimer}>
1188
+ {pollElapsedDisplay} / {pollTotalDisplay}
1189
+ </span>
1190
+ </>
1191
+ )}
1192
+ <Button
1193
+ kind="ghost"
1194
+ size="md"
1195
+ renderIcon={Renew}
1196
+ className={styles.tryAgainButton}
1197
+ onClick={() => checkWhitelistStatusOnce({ manual: true })}
1198
+ disabled={manualCheckLoading}>
1199
+ {manualCheckLoading ? (
1200
+ <InlineLoading description={t('whitelistChecking', 'Checking status…')} />
1201
+ ) : (
1202
+ t('checkStatusNow', 'Check status now')
1203
+ )}
1204
+ </Button>
225
1205
  </div>
226
1206
  )}
227
1207
  </ModalBody>
1208
+
228
1209
  <ModalFooter>
229
1210
  <ButtonSet className={styles.buttonSet}>
230
- <Button kind="secondary" onClick={handleClose} className={styles.button}>
231
- {t('cancel', 'Cancel')}
232
- </Button>
1211
+ {(mode === 'landing' ||
1212
+ mode === 'whitelist-submit' ||
1213
+ (mode !== 'auth-landing' && mode !== 'biometric' && mode !== 'biometric-failed')) && (
1214
+ <Button kind="secondary" onClick={handleCancelWhitelistWaiting} className={styles.button}>
1215
+ {t('btnCancel', 'Cancel')}
1216
+ </Button>
1217
+ )}
233
1218
 
234
1219
  {mode === 'landing' && (
235
1220
  <Button
@@ -249,27 +1234,30 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
249
1234
  )}
250
1235
 
251
1236
  {mode === 'verify-otp' && (
252
- <>
253
- <Button
254
- disabled={isLoading || otp.length !== otpLength}
255
- kind="primary"
256
- onClick={handleVerify}
257
- className={styles.button}>
258
- {isLoading ? <InlineLoading description={t('verifyingOtp', 'Verifying OTP')} /> : t('verify', 'Verify')}
259
- </Button>
260
- </>
1237
+ <Button
1238
+ disabled={isLoading || otp.length !== otpLength}
1239
+ kind="primary"
1240
+ onClick={handleVerify}
1241
+ className={styles.button}>
1242
+ {isLoading ? <InlineLoading description={t('verifyingOtp', 'Verifying OTP')} /> : t('verify', 'Verify')}
1243
+ </Button>
261
1244
  )}
262
1245
 
263
- {mode === 'change-number' && (
1246
+ {mode === 'whitelist-submit' && (
264
1247
  <Button
265
- disabled={!isValidPhoneNumber(newPhoneNumber) || requestingOtp}
266
1248
  kind="primary"
267
- onClick={() => handleRequestingOtp(newPhoneNumber)}
268
- className={styles.button}>
269
- {requestingOtp ? (
270
- <InlineLoading description={t('reSendingOtp', 'Resending OTP...')} />
1249
+ onClick={handleSubmitWhitelist}
1250
+ className={styles.button}
1251
+ disabled={
1252
+ whitelistSubmitting ||
1253
+ !whitelistReasonCode ||
1254
+ whitelistReasonText.trim().length < MIN_REASON_LENGTH ||
1255
+ (reasonRequiresAttachment && (!whitelistAttachment || whitelistAttachment.size === 0))
1256
+ }>
1257
+ {whitelistSubmitting ? (
1258
+ <InlineLoading description={t('submittingWhitelist', 'Submitting…')} />
271
1259
  ) : (
272
- t('sendOtp', 'Send OTP')
1260
+ t('submitWhitelist', 'Submit request')
273
1261
  )}
274
1262
  </Button>
275
1263
  )}