@kenyaemr/esm-express-workflow-app 5.4.3 → 5.4.4-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +13 -11
- package/dist/1074.js +1 -0
- package/dist/1074.js.map +1 -0
- package/dist/1084.js +1 -0
- package/dist/1084.js.map +1 -0
- package/dist/12.js +17 -0
- package/dist/12.js.map +1 -0
- package/dist/126.js +1 -0
- package/dist/126.js.map +1 -0
- package/dist/1311.js +1 -0
- package/dist/1311.js.map +1 -0
- package/dist/1323.js +1 -0
- package/dist/1323.js.map +1 -0
- package/dist/1469.js +1 -0
- package/dist/1469.js.map +1 -0
- package/dist/1506.js +13 -0
- package/dist/1506.js.map +1 -0
- package/dist/1760.js +1 -0
- package/dist/1760.js.map +1 -0
- package/dist/1780.js +1 -0
- package/dist/1780.js.map +1 -0
- package/dist/1791.js +1 -0
- package/dist/1791.js.map +1 -0
- package/dist/1884.js +1 -0
- package/dist/1884.js.map +1 -0
- package/dist/1972.js +1 -0
- package/dist/1972.js.map +1 -0
- package/dist/1990.js +1 -0
- package/dist/1990.js.map +1 -0
- package/dist/2016.js +1 -0
- package/dist/2016.js.map +1 -0
- package/dist/2024.js +1 -0
- package/dist/2024.js.map +1 -0
- package/dist/2153.js +1 -0
- package/dist/2153.js.map +1 -0
- package/dist/216.js +1 -0
- package/dist/216.js.map +1 -0
- package/dist/2225.js +1 -0
- package/dist/2225.js.map +1 -0
- package/dist/2294.js +1 -0
- package/dist/2294.js.map +1 -0
- package/dist/2345.js +1 -0
- package/dist/2345.js.map +1 -0
- package/dist/2386.js +1 -0
- package/dist/2386.js.map +1 -0
- package/dist/2438.js +1 -0
- package/dist/2438.js.map +1 -0
- package/dist/2499.js +1 -0
- package/dist/2499.js.map +1 -0
- package/dist/2500.js +1 -0
- package/dist/2500.js.map +1 -0
- package/dist/2586.js +1 -0
- package/dist/2586.js.map +1 -0
- package/dist/2625.js +1 -0
- package/dist/2625.js.map +1 -0
- package/dist/2685.js +1 -0
- package/dist/2685.js.map +1 -0
- package/dist/2757.js +1 -0
- package/dist/2757.js.map +1 -0
- package/dist/2851.js +1 -0
- package/dist/2851.js.map +1 -0
- package/dist/2881.js +1 -0
- package/dist/2881.js.map +1 -0
- package/dist/2948.js +1 -0
- package/dist/2948.js.map +1 -0
- package/dist/2968.js +1 -0
- package/dist/2968.js.map +1 -0
- package/dist/2998.js +1 -0
- package/dist/2998.js.map +1 -0
- package/dist/30.js +1 -0
- package/dist/30.js.map +1 -0
- package/dist/3089.js +1 -0
- package/dist/3089.js.map +1 -0
- package/dist/3139.js +1 -0
- package/dist/3139.js.map +1 -0
- package/dist/3220.js +1 -0
- package/dist/3220.js.map +1 -0
- package/dist/3265.js +1 -0
- package/dist/3265.js.map +1 -0
- package/dist/3548.js +1 -0
- package/dist/3548.js.map +1 -0
- package/dist/3567.js +1 -0
- package/dist/3567.js.map +1 -0
- package/dist/3571.js +1 -0
- package/dist/3571.js.map +1 -0
- package/dist/3691.js +1 -0
- package/dist/3691.js.map +1 -0
- package/dist/3730.js +1 -0
- package/dist/3730.js.map +1 -0
- package/dist/3923.js +1 -0
- package/dist/3923.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/3963.js.map +1 -0
- package/dist/4024.js +1 -0
- package/dist/4024.js.map +1 -0
- package/dist/405.js +1 -0
- package/dist/405.js.map +1 -0
- package/dist/4296.js +1 -0
- package/dist/4296.js.map +1 -0
- package/dist/4504.js +1 -0
- package/dist/4504.js.map +1 -0
- package/dist/4564.js +1 -0
- package/dist/4564.js.map +1 -0
- package/dist/4581.js +1 -0
- package/dist/4581.js.map +1 -0
- package/dist/4670.js +1 -0
- package/dist/4670.js.map +1 -0
- package/dist/4680.js +1 -0
- package/dist/4680.js.map +1 -0
- package/dist/4682.js +1 -0
- package/dist/4682.js.map +1 -0
- package/dist/4735.js +1 -0
- package/dist/4735.js.map +1 -0
- package/dist/4737.js +1 -0
- package/dist/4737.js.map +1 -0
- package/dist/4744.js +1 -0
- package/dist/4744.js.map +1 -0
- package/dist/4795.js +1 -0
- package/dist/4795.js.map +1 -0
- package/dist/4813.js +2 -0
- package/dist/4813.js.map +1 -0
- package/dist/4818.js +1 -0
- package/dist/4818.js.map +1 -0
- package/dist/4858.js +1 -0
- package/dist/4858.js.map +1 -0
- package/dist/487.js +1 -0
- package/dist/487.js.map +1 -0
- package/dist/4970.js +1 -0
- package/dist/4970.js.map +1 -0
- package/dist/5047.js +1 -0
- package/dist/5047.js.map +1 -0
- package/dist/5054.js +1 -0
- package/dist/5054.js.map +1 -0
- package/dist/5202.js +1 -0
- package/dist/5202.js.map +1 -0
- package/dist/5491.js +1 -0
- package/dist/5491.js.map +1 -0
- package/dist/5592.js +1 -0
- package/dist/5592.js.map +1 -0
- package/dist/5669.js +1 -0
- package/dist/5669.js.map +1 -0
- package/dist/5787.js +1 -0
- package/dist/5787.js.map +1 -0
- package/dist/586.js +1 -0
- package/dist/586.js.map +1 -0
- package/dist/5879.js +1 -0
- package/dist/5879.js.map +1 -0
- package/dist/5904.js +1 -0
- package/dist/5904.js.map +1 -0
- package/dist/5986.js +1 -0
- package/dist/5986.js.map +1 -0
- package/dist/6007.js +11 -0
- package/dist/6007.js.map +1 -0
- package/dist/6075.js +1 -0
- package/dist/6075.js.map +1 -0
- package/dist/6258.js +1 -0
- package/dist/6258.js.map +1 -0
- package/dist/629.js +1 -0
- package/dist/629.js.map +1 -0
- package/dist/6328.js +1 -0
- package/dist/6328.js.map +1 -0
- package/dist/6355.js +1 -0
- package/dist/6355.js.map +1 -0
- package/dist/64.js +1 -0
- package/dist/64.js.map +1 -0
- package/dist/6445.js +1 -0
- package/dist/6445.js.map +1 -0
- package/dist/6456.js +1 -0
- package/dist/6466.js +3 -0
- package/dist/6466.js.map +1 -0
- package/dist/6663.js +1 -0
- package/dist/6663.js.map +1 -0
- package/dist/6720.js +1 -0
- package/dist/6720.js.map +1 -0
- package/dist/6798.js +66 -0
- package/dist/6798.js.map +1 -0
- package/dist/6925.js +1 -0
- package/dist/6925.js.map +1 -0
- package/dist/7201.js +1 -0
- package/dist/7201.js.map +1 -0
- package/dist/7234.js +1 -0
- package/dist/7234.js.map +1 -0
- package/dist/7261.js +1 -0
- package/dist/7261.js.map +1 -0
- package/dist/7326.js +1 -0
- package/dist/7359.js +1 -0
- package/dist/7405.js +1 -0
- package/dist/7405.js.map +1 -0
- package/dist/7591.js +1 -0
- package/dist/7591.js.map +1 -0
- package/dist/7602.js +1 -0
- package/dist/7602.js.map +1 -0
- package/dist/7607.js +1 -0
- package/dist/7701.js +1 -0
- package/dist/7701.js.map +1 -0
- package/dist/7717.js +1 -0
- package/dist/7717.js.map +1 -0
- package/dist/7739.js +1 -0
- package/dist/7739.js.map +1 -0
- package/dist/7788.js +1 -0
- package/dist/7788.js.map +1 -0
- package/dist/7819.js +1 -0
- package/dist/7819.js.map +1 -0
- package/dist/7820.js +1 -0
- package/dist/7820.js.map +1 -0
- package/dist/7844.js +1 -0
- package/dist/7844.js.map +1 -0
- package/dist/7971.js +1 -0
- package/dist/7971.js.map +1 -0
- package/dist/807.js +1 -0
- package/dist/807.js.map +1 -0
- package/dist/8159.js +7 -0
- package/dist/8159.js.map +1 -0
- package/dist/8338.js +1 -0
- package/dist/8338.js.map +1 -0
- package/dist/845.js +1 -0
- package/dist/845.js.map +1 -0
- package/dist/8566.js +1 -0
- package/dist/8566.js.map +1 -0
- package/dist/8570.js +1 -0
- package/dist/8570.js.map +1 -0
- package/dist/8613.js +1 -0
- package/dist/8613.js.map +1 -0
- package/dist/8682.js +1 -0
- package/dist/8682.js.map +1 -0
- package/dist/87.js +1 -0
- package/dist/87.js.map +1 -0
- package/dist/8727.js +1 -0
- package/dist/8766.js +1 -0
- package/dist/8766.js.map +1 -0
- package/dist/8799.js +1 -0
- package/dist/8799.js.map +1 -0
- package/dist/8828.js +1 -0
- package/dist/8828.js.map +1 -0
- package/dist/8860.js +1 -0
- package/dist/8860.js.map +1 -0
- package/dist/8911.js +1 -0
- package/dist/8911.js.map +1 -0
- package/dist/8930.js +1 -0
- package/dist/8930.js.map +1 -0
- package/dist/8971.js +1 -0
- package/dist/8971.js.map +1 -0
- package/dist/9011.js +1 -0
- package/dist/9011.js.map +1 -0
- package/dist/9111.js +1 -0
- package/dist/9111.js.map +1 -0
- package/dist/9124.js +1 -0
- package/dist/9124.js.map +1 -0
- package/dist/9157.js +1 -0
- package/dist/9157.js.map +1 -0
- package/dist/9161.js +1 -0
- package/dist/9161.js.map +1 -0
- package/dist/9182.js +1 -0
- package/dist/921.js +1 -0
- package/dist/921.js.map +1 -0
- package/dist/9212.js +1 -0
- package/dist/9212.js.map +1 -0
- package/dist/9255.js +1 -0
- package/dist/9255.js.map +1 -0
- package/dist/9257.js +1 -0
- package/dist/9257.js.map +1 -0
- package/dist/9343.js +1 -0
- package/dist/9343.js.map +1 -0
- package/dist/9404.js +1 -0
- package/dist/9404.js.map +1 -0
- package/dist/9446.js +1 -0
- package/dist/9446.js.map +1 -0
- package/dist/9449.js +1 -0
- package/dist/9449.js.map +1 -0
- package/dist/9488.js +1 -0
- package/dist/9488.js.map +1 -0
- package/dist/9514.js +1 -0
- package/dist/9514.js.map +1 -0
- package/dist/9528.js +1 -0
- package/dist/9528.js.map +1 -0
- package/dist/9535.js +1 -0
- package/dist/9535.js.map +1 -0
- package/dist/9606.js +1 -0
- package/dist/9606.js.map +1 -0
- package/dist/9834.js +1 -0
- package/dist/9834.js.map +1 -0
- package/dist/9845.js +1 -0
- package/dist/9845.js.map +1 -0
- package/dist/9848.js +1 -0
- package/dist/9848.js.map +1 -0
- package/dist/kenyaemr-esm-express-workflow-app.js +5 -5
- package/dist/kenyaemr-esm-express-workflow-app.js.buildmanifest.json +3394 -173
- package/dist/kenyaemr-esm-express-workflow-app.js.map +1 -1
- package/dist/main.js +7 -106
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +7 -8
- package/rspack.config.js +1 -1
- package/src/components/accounting/index.tsx +11 -10
- package/src/components/admissions/admissions-dashboard.component.tsx +2 -2
- package/src/components/admissions/index.tsx +11 -10
- package/src/components/anaesthetic/anaesthetic-dashboard.meta.ts +8 -0
- package/src/components/anaesthetic/anaesthetic-data-form.component.tsx +442 -0
- package/src/components/anaesthetic/anaesthetic-data-form.scss +278 -0
- package/src/components/anaesthetic/anaesthetic-link.component.tsx +21 -0
- package/src/components/anaesthetic/anaesthetic.component.tsx +618 -0
- package/src/components/anaesthetic/anaesthetic.resource.ts +1503 -0
- package/src/components/anaesthetic/anaesthetic.scss +1885 -0
- package/src/components/anaesthetic/forms/anaesthetic-form.component.tsx +304 -0
- package/src/components/anaesthetic/forms/anaesthetic-record-form.component.tsx +707 -0
- package/src/components/anaesthetic/forms/icd11-diagnosis-search.component.tsx +178 -0
- package/src/components/anaesthetic/forms/icd11-diagnosis-search.scss +126 -0
- package/src/components/anaesthetic/forms/index.ts +2 -0
- package/src/components/anaesthetic/forms/interoperative-record-drug-given-form.component.tsx +231 -0
- package/src/components/anaesthetic/forms/post-operative-summary-form.component.tsx +171 -0
- package/src/components/anaesthetic/forms/time-picker-dropdown.component.tsx +202 -0
- package/src/components/anaesthetic/forms/time-picker-dropdown.scss +107 -0
- package/src/components/anaesthetic/forms/time-picker-with-clock.component.tsx +174 -0
- package/src/components/anaesthetic/forms/time-picker-with-clock.scss +178 -0
- package/src/components/anaesthetic/forms/useAnaestheticData.ts +16 -0
- package/src/components/anaesthetic/graphs/anaesthetic-graph-wrapper.component.tsx +266 -0
- package/src/components/anaesthetic/graphs/anaesthetic-graph.component.tsx +185 -0
- package/src/components/anaesthetic/graphs/index.ts +2 -0
- package/src/components/anaesthetic/graphs/interoperative-record-drug-given-graph-wrapper.component.tsx +197 -0
- package/src/components/anaesthetic/graphs/interoperative-record-drug-given-graph.component.tsx +93 -0
- package/src/components/anaesthetic/resources/anaesthetic-amniotic-fluid.resource.ts +277 -0
- package/src/components/anaesthetic/resources/anaesthetic-form.resource.ts +716 -0
- package/src/components/anaesthetic/resources/anaesthetic.resource.ts +378 -0
- package/src/components/anaesthetic/types/index.ts +1349 -0
- package/src/components/appointments/index.ts +10 -8
- package/src/components/consultation/clinical-encounter/clinical-encounter.component.tsx +4 -20
- package/src/components/consultation/clinical-encounter/encounter-details.component.tsx +9 -9
- package/src/components/consultation/clinical-encounter/encounter-details.test.tsx +19 -18
- package/src/components/consultation/consultation-context.tsx +19 -0
- package/src/components/consultation/consultation-summary-cards.component.tsx +124 -0
- package/src/components/consultation/consultation.component.tsx +41 -268
- package/src/components/consultation/consultation.resource.ts +67 -24
- package/src/components/consultation/consultation.scss +5 -0
- package/src/components/consultation/consultation.utils.tsx +222 -0
- package/src/components/consultation/dashboard.component.tsx +0 -2
- package/src/components/consultation/index.ts +28 -21
- package/src/components/consultation/tabs/index.ts +67 -0
- package/src/components/facility-dashboard/components/header/home-header.component.tsx +8 -13
- package/src/components/facility-dashboard/index.tsx +10 -9
- package/src/components/laboratory/index.ts +22 -10
- package/src/components/laboratory/lab-table.component.tsx +40 -12
- package/src/components/laboratory/laboratory-tabs.component.tsx +3 -17
- package/src/components/mch/dashboard.component.tsx +0 -2
- package/src/components/mch/index.tsx +10 -9
- package/src/components/mch/mch.component.tsx +1 -3
- package/src/components/mch/mch.consultation.component.tsx +27 -15
- package/src/components/mch/mch.triage.component.tsx +42 -33
- package/src/components/pharmacy/index.ts +21 -19
- package/src/components/pharmacy/orders/pharmacy-orders.component.tsx +35 -13
- package/src/components/pharmacy/orders/pharmacy-orders.test.tsx +8 -7
- package/src/components/pharmacy/pharmacy-tabs.component.tsx +4 -20
- package/src/components/pharmacy/tabs/index.ts +21 -0
- package/src/components/preauth/index.ts +22 -0
- package/src/components/preauth/preauth-dashboard.component.tsx +20 -0
- package/src/components/procedures/index.ts +27 -10
- package/src/components/procedures/procedure-queues/index.ts +13 -0
- package/src/components/procedures/procedure-queues/procedure-form-extras.component.tsx +33 -0
- package/src/components/procedures/procedure-queues/procedure-form-extras.scss +5 -0
- package/src/components/procedures/procedure-queues/procedure-queues.component.tsx +35 -0
- package/src/components/procedures/procedure-queues/procedure-queues.resources.ts +37 -0
- package/src/components/procedures/procedure-queues/procedure-room-tabs.component.tsx +80 -0
- package/src/components/procedures/procedure-queues/procedure-rooms.scss +13 -0
- package/src/components/procedures/procedure-queues/queue-procedure-tabs.component.tsx +34 -0
- package/src/components/procedures/procedure-queues/summary-cards.component.tsx +81 -0
- package/src/components/procedures/procedure-queues/summary-cards.scss +35 -0
- package/src/components/procedures/procedures-table.component.tsx +69 -19
- package/src/components/procedures/procedures-tabs.component.test.tsx +57 -0
- package/src/components/procedures/procedures-tabs.component.tsx +3 -18
- package/src/components/radiology-and-imaging/index.ts +25 -10
- package/src/components/radiology-and-imaging/radiology-and-imaging-table.component.tsx +62 -21
- package/src/components/radiology-and-imaging/radiology-and-imaging.component.tsx +3 -19
- package/src/components/registration/card/HIE-card/hie-card.component.tsx +32 -44
- package/src/components/registration/card/Local-card/local-card.component.tsx +22 -24
- package/src/components/registration/checkin-form-extra/checkin-form-extra.extension.tsx +8 -2
- package/src/components/registration/checkin-form-extra/queue-fields.component.tsx +18 -19
- package/src/components/registration/dependants/dependants.component.tsx +37 -60
- package/src/components/registration/dependants/dependants.resource.ts +79 -50
- package/src/components/registration/end-visit-modal/end-visit-dialog.modal.tsx +320 -0
- package/src/components/registration/end-visit-modal/end-visit-dialog.scss +12 -0
- package/src/components/registration/end-visit-modal/end-visit.resource.tsx +59 -0
- package/src/components/registration/end-visit-modal/sha-end-visit.resource.ts +285 -0
- package/src/components/registration/helper/index.ts +4 -0
- package/src/components/registration/index.ts +10 -9
- package/src/components/registration/search-bar/search-bar.resource.ts +32 -93
- package/src/components/registration/start-visit-form/hooks/useDefaultFacilityLocation.tsx +15 -0
- package/src/components/registration/start-visit-form/hooks/useDefaultVisitLocation.tsx +24 -0
- package/src/components/registration/start-visit-form/hooks/useOfflineVisitType.tsx +18 -0
- package/src/components/registration/start-visit-form/hooks/useRecommendedVisitTypes.tsx +36 -0
- package/src/components/registration/start-visit-form/hooks/useVisitAttributeType.tsx +102 -0
- package/src/components/registration/start-visit-form/overflow-menu-extension/custom-end-active-visit.extension.tsx +44 -0
- package/src/components/registration/start-visit-form/overflow-menu-extension/overflow-menu-item.extension.tsx +57 -0
- package/src/components/registration/start-visit-form/overflow-menu-extension/overflow-menu-item.scss +3 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/base-visit-type.component.tsx +126 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/base-visit-type.scss +75 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/exported-visit-form.workspace.tsx +759 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/location-selector.component.tsx +86 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/recommended-visit-type.component.tsx +32 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/visit-attribute-type.component.tsx +257 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/visit-attribute-type.scss +5 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/visit-date-time.component.tsx +193 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/visit-form.resource.ts +389 -0
- package/src/components/registration/start-visit-form/start-visit-workspace/visit-form.scss +144 -0
- package/src/components/registration/start-visit-form/visit-form-workspace/visit-form.workspace.tsx +48 -0
- package/src/components/registration/type/index.ts +101 -0
- package/src/components/reports/index.ts +11 -9
- package/src/components/triage/dashboard.component.tsx +0 -2
- package/src/components/triage/index.ts +10 -9
- package/src/components/triage/triage.component.tsx +92 -42
- package/src/components/triage/triage.resource.ts +56 -50
- package/src/components/triage/triage.scss +6 -0
- package/src/config-schema.ts +482 -0
- package/src/hooks/useOrders.tsx +2 -2
- package/src/hooks/useServiceQueues.tsx +95 -25
- package/src/index.ts +84 -28
- package/src/routes.json +290 -32
- package/src/shared/dashboard-link/dashboard-link.component.tsx +11 -8
- package/src/shared/orders/OrderTable.tsx +19 -3
- package/src/shared/otp-verification/index.ts +112 -29
- package/src/shared/otp-verification/otp-verification.modal.tsx +1110 -122
- package/src/shared/otp-verification/otp-verification.scss +374 -0
- package/src/shared/patient-chart/patient-summary-dashboard/patient-summary-dashboard.component.tsx +5 -85
- package/src/shared/pin-put/pinput.component.test.tsx +7 -6
- package/src/shared/queue/queue-entry/queue-entry-table.component.tsx +99 -88
- package/src/shared/queue/queue-entry/queue-entry-table.scss +4 -0
- package/src/shared/queue/queue-summary-cards.component.tsx +32 -0
- package/src/shared/queue/queue-tab.component.tsx +182 -70
- package/src/shared/queue/queue-tab.scss +1 -0
- package/src/shared/queue/queue-workflow-context.tsx +90 -0
- package/src/shared/tabs/create-tab-extension.tsx +33 -0
- package/src/shared/tabs/extension-tabs.component.tsx +51 -30
- package/src/shared/utils/index.ts +119 -5
- package/src/types/index.ts +29 -2
- package/translations/am.json +426 -133
- package/translations/en.json +312 -19
- package/translations/fr.json +351 -63
- package/translations/sw.json +330 -37
- package/dist/127.js +0 -1
- package/dist/200.js +0 -1
- package/dist/200.js.map +0 -1
- package/dist/24.js +0 -1
- package/dist/24.js.map +0 -1
- package/dist/267.js +0 -1
- package/dist/267.js.map +0 -1
- package/dist/285.js +0 -1
- package/dist/285.js.map +0 -1
- package/dist/329.js +0 -1
- package/dist/329.js.map +0 -1
- package/dist/40.js +0 -1
- package/dist/466.js +0 -1
- package/dist/466.js.map +0 -1
- package/dist/472.js +0 -66
- package/dist/472.js.map +0 -1
- package/dist/490.js +0 -1
- package/dist/490.js.map +0 -1
- package/dist/54.js +0 -1
- package/dist/54.js.map +0 -1
- package/dist/689.js +0 -1
- package/dist/689.js.map +0 -1
- package/dist/697.js +0 -1
- package/dist/697.js.map +0 -1
- package/dist/704.js +0 -1
- package/dist/704.js.map +0 -1
- package/dist/710.js +0 -1
- package/dist/710.js.map +0 -1
- package/dist/729.js +0 -17
- package/dist/729.js.map +0 -1
- package/dist/805.js +0 -37
- package/dist/805.js.map +0 -1
- package/dist/847.js +0 -1
- package/dist/847.js.map +0 -1
- package/dist/85.js +0 -1
- package/dist/85.js.map +0 -1
- package/dist/882.js +0 -1
- package/dist/91.js +0 -1
- package/dist/91.js.map +0 -1
- package/dist/916.js +0 -1
- package/dist/994.js +0 -1
- package/dist/994.js.map +0 -1
- package/dist/998.js +0 -1
- package/dist/998.js.map +0 -1
- package/jest.config.js +0 -3
- package/src/shared/orders/OrdersTabs.tsx +0 -58
- package/src/shared/patient-chart/patient-chart.component.tsx +0 -61
- package/src/shared/patient-chart/patient-chart.resources.ts +0 -184
- package/src/shared/patient-chart/patient-chart.scss +0 -15
- package/src/shared/patient-chart/useInitialize.ts +0 -24
- /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
|
-
|
|
10
|
-
|
|
11
|
+
NumberInput,
|
|
12
|
+
TextArea,
|
|
11
13
|
} from '@carbon/react';
|
|
12
|
-
import
|
|
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<{
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 (
|
|
98
|
-
setError({
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
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
|
-
{
|
|
765
|
+
{headerEyebrow && <p className={styles.eyebrow}>{headerEyebrow}</p>}
|
|
766
|
+
{headerTitle}
|
|
135
767
|
</ModalHeader>
|
|
768
|
+
|
|
136
769
|
<ModalBody>
|
|
137
|
-
{
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
146
|
-
? t('
|
|
147
|
-
: t('
|
|
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
|
-
<
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
170
|
-
<p className={styles.
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<
|
|
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
|
-
)
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 === '
|
|
1246
|
+
{mode === 'whitelist-submit' && (
|
|
264
1247
|
<Button
|
|
265
|
-
disabled={!isValidPhoneNumber(newPhoneNumber) || requestingOtp}
|
|
266
1248
|
kind="primary"
|
|
267
|
-
onClick={
|
|
268
|
-
className={styles.button}
|
|
269
|
-
{
|
|
270
|
-
|
|
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('
|
|
1260
|
+
t('submitWhitelist', 'Submit request')
|
|
273
1261
|
)}
|
|
274
1262
|
</Button>
|
|
275
1263
|
)}
|