From 5c39b2c927774eee8fcb3d59129eac39bf011362 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 12:42:55 +0530 Subject: [PATCH] Abdm V3 Plug (#8340) --- cypress/e2e/users_spec/UsersManage.cy.ts | 2 - cypress/e2e/users_spec/UsersProfile.cy.ts | 2 - netlify.toml | 12 +- package-lock.json | 15 +- package.json | 2 +- src/Common/constants.tsx | 30 +- src/Components/ABDM/ABDMFacilityRecords.tsx | 107 ++- src/Components/ABDM/ABDMRecordsTab.tsx | 58 +- src/Components/ABDM/ABHAProfileModal.tsx | 71 +- .../ABDM/ConfigureHealthFacility.tsx | 76 +- src/Components/ABDM/FetchRecordsModal.tsx | 67 +- src/Components/ABDM/HealthInformation.tsx | 24 +- src/Components/ABDM/LinkABHANumberModal.tsx | 875 ------------------ .../ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx | 732 +++++++++++++++ .../ABDM/LinkAbhaNumber/LinkWithOtp.tsx | 346 +++++++ .../ABDM/LinkAbhaNumber/LinkWithQr.tsx | 68 ++ src/Components/ABDM/LinkAbhaNumber/index.tsx | 140 +++ .../ABDM/LinkAbhaNumber/useMultiStepForm.ts | 70 ++ src/Components/ABDM/LinkCareContextModal.tsx | 107 --- src/Components/ABDM/models.ts | 138 --- src/Components/ABDM/types/abha.ts | 20 + src/Components/ABDM/types/health-facility.ts | 19 + src/Components/Common/components/ButtonV2.tsx | 145 ++- .../Facility/ConsultationDetails/index.tsx | 2 +- .../Form/FormFields/TextFormField.tsx | 55 +- src/Components/Patient/ManagePatients.tsx | 29 +- src/Components/Patient/PatientHome.tsx | 51 +- src/Components/Patient/PatientInfoCard.tsx | 69 +- src/Components/Patient/PatientRegister.tsx | 93 +- src/Components/Patient/Utils.ts | 19 + src/Components/Patient/models.tsx | 1 + src/Locale/en.json | 157 +++- src/Redux/api.tsx | 403 ++++---- src/Utils/request/request.ts | 5 + 34 files changed, 2345 insertions(+), 1665 deletions(-) delete mode 100644 src/Components/ABDM/LinkABHANumberModal.tsx create mode 100644 src/Components/ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx create mode 100644 src/Components/ABDM/LinkAbhaNumber/LinkWithOtp.tsx create mode 100644 src/Components/ABDM/LinkAbhaNumber/LinkWithQr.tsx create mode 100644 src/Components/ABDM/LinkAbhaNumber/index.tsx create mode 100644 src/Components/ABDM/LinkAbhaNumber/useMultiStepForm.ts delete mode 100644 src/Components/ABDM/LinkCareContextModal.tsx delete mode 100644 src/Components/ABDM/models.ts create mode 100644 src/Components/ABDM/types/health-facility.ts create mode 100644 src/Components/Patient/Utils.ts diff --git a/cypress/e2e/users_spec/UsersManage.cy.ts b/cypress/e2e/users_spec/UsersManage.cy.ts index 0d3a757df0f..3d9d18ff285 100644 --- a/cypress/e2e/users_spec/UsersManage.cy.ts +++ b/cypress/e2e/users_spec/UsersManage.cy.ts @@ -26,9 +26,7 @@ describe("Manage User", () => { beforeEach(() => { cy.restoreLocalStorage(); - console.log(localStorage); cy.clearLocalStorage(/filters--.+/); - console.log(localStorage); cy.awaitUrl("/users"); }); diff --git a/cypress/e2e/users_spec/UsersProfile.cy.ts b/cypress/e2e/users_spec/UsersProfile.cy.ts index 63fd71f5793..8dc1ad7ef0a 100644 --- a/cypress/e2e/users_spec/UsersProfile.cy.ts +++ b/cypress/e2e/users_spec/UsersProfile.cy.ts @@ -25,9 +25,7 @@ describe("Manage User Profile", () => { beforeEach(() => { cy.restoreLocalStorage(); - console.log(localStorage); cy.clearLocalStorage(/filters--.+/); - console.log(localStorage); cy.awaitUrl("/user/profile"); }); diff --git a/netlify.toml b/netlify.toml index c1d30732a53..59cbe33e2d4 100644 --- a/netlify.toml +++ b/netlify.toml @@ -13,12 +13,12 @@ to = "/index.html" status = 200 [[headers]] - for = "/*" - [headers.values] - cache-control = "max-age=0, no-store" - X-Frame-Options = "DENY" - X-Content-Type-Options = "nosniff" - Content-Security-Policy-Report-Only = ''' +for = "/*" +[headers.values] +cache-control = "max-age=0, no-store" +X-Frame-Options = "DENY" +X-Content-Type-Options = "nosniff" +Content-Security-Policy-Report-Only = ''' default-src 'self'; script-src 'self' 'nonce-f51b9742' https://plausible.10bedicu.in; style-src 'self' 'unsafe-inline'; diff --git a/package-lock.json b/package-lock.json index aa99337d8d6..ef892566879 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "echarts": "^5.5.1", "echarts-for-react": "^3.0.2", "events": "^3.3.0", - "hi-profiles": "^1.0.6", + "hi-profiles": "^1.1.0", "i18next": "^23.11.4", "i18next-browser-languagedetector": "^7.2.1", "lodash-es": "^4.17.21", @@ -4696,12 +4696,14 @@ "node_modules/@types/dom-webcodecs": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", - "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==" + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "license": "MIT" }, "node_modules/@types/emscripten": { "version": "1.39.13", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", - "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==" + "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.6", @@ -5345,6 +5347,7 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-2.0.8.tgz", "integrity": "sha512-/7WHsdC1a/Z909J2zZxqgpUQ1iI554fZxIagucH/tFu1MhZkNIeykYI1OdZgDEwV4CzuSi+h90wwNrhmETcmRw==", + "license": "MIT", "dependencies": { "barcode-detector": "^2.2.7", "webrtc-adapter": "9.0.1" @@ -5935,6 +5938,7 @@ "version": "2.2.11", "resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.2.11.tgz", "integrity": "sha512-N50XZ6Rav2sxTgHXOc38/mkpVJMan11GZ8Yqi1pPMZpTJSXuZ/FpIee6OtLehZX/Vs4ZOzGbp1DgXzFCfKggWA==", + "license": "MIT", "dependencies": { "@types/dom-webcodecs": "^0.1.13", "zxing-wasm": "1.2.14" @@ -16099,7 +16103,8 @@ "node_modules/sdp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz", - "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==" + "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==", + "license": "MIT" }, "node_modules/sdp-transform": { "version": "2.14.2", @@ -19201,6 +19206,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz", "integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==", + "license": "BSD-3-Clause", "dependencies": { "sdp": "^3.2.0" }, @@ -19925,6 +19931,7 @@ "version": "1.2.14", "resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.2.14.tgz", "integrity": "sha512-UaYfzSmFPIEmYDt/KyPvs/H02t8jO470BBFHUIlvtmloAm8f2zdAmOL93iWYQ5QYfSnTyFPg0yzZwznlBjfg4A==", + "license": "MIT", "dependencies": { "@types/emscripten": "^1.39.13" } diff --git a/package.json b/package.json index 57d0c3d3405..47da9afc795 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "echarts": "^5.5.1", "echarts-for-react": "^3.0.2", "events": "^3.3.0", - "hi-profiles": "^1.0.6", + "hi-profiles": "^1.1.0", "i18next": "^23.11.4", "i18next-browser-languagedetector": "^7.2.1", "lodash-es": "^4.17.21", diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 08d21dc1edc..c5e221fb277 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -1144,23 +1144,23 @@ export const AssetImportSchema: SchemaType = { // ABDM export const ABDM_CONSENT_PURPOSE = [ - { value: "CAREMGT", label: "Care Management" }, - { value: "BTG", label: "Break The Glass" }, - { value: "PUBHLTH", label: "Public Health" }, - { value: "HPAYMT", label: "Healthcare Payment" }, - { value: "DSRCH", label: "Disease Specific Healthcare Research" }, - { value: "PATRQT", label: "Self Requested" }, -] as { value: ConsentPurpose; label: string }[]; + "CAREMGT", + "BTG", + "PUBHLTH", + "HPAYMT", + "DSRCH", + "PATRQT", +] as ConsentPurpose[]; export const ABDM_HI_TYPE = [ - { value: "Prescription", label: "Prescription" }, - { value: "DiagnosticReport", label: "Diagnostic Report" }, - { value: "OPConsultation", label: "Op Consultation" }, - { value: "DischargeSummary", label: "Discharge Summary" }, - { value: "ImmunizationRecord", label: "Immunization Record" }, - { value: "HealthDocumentRecord", label: "Record Artifact" }, - { value: "WellnessRecord", label: "Wellness Record" }, -] as { value: ConsentHIType; label: string }[]; + "Prescription", + "DiagnosticReport", + "OPConsultation", + "DischargeSummary", + "ImmunizationRecord", + "HealthDocumentRecord", + "WellnessRecord", +] as ConsentHIType[]; export const USER_TYPES_MAP = { Pharmacist: "Pharmacist", diff --git a/src/Components/ABDM/ABDMFacilityRecords.tsx b/src/Components/ABDM/ABDMFacilityRecords.tsx index a37fc0d4932..5844e0359b5 100644 --- a/src/Components/ABDM/ABDMFacilityRecords.tsx +++ b/src/Components/ABDM/ABDMFacilityRecords.tsx @@ -1,33 +1,35 @@ import { Link } from "raviger"; import routes from "../../Redux/api"; import useQuery from "../../Utils/request/useQuery"; -import { formatDateTime } from "../../Utils/utils"; +import { classNames, formatDateTime } from "../../Utils/utils"; import Loading from "../Common/Loading"; import Page from "../Common/components/Page"; import CareIcon from "../../CAREUI/icons/CareIcon"; import ButtonV2 from "../Common/components/ButtonV2"; +import { useTranslation } from "react-i18next"; interface IProps { facilityId: string; } const TableHeads = [ - "Patient", - "Status", - "Created On", - "Consent Granted On", - // "Requested By", - "Health Information Range", - "Expires On", - "HI Profiles", + "consent__patient", + "consent__status", + "created_on", + "updated_on", + "consent__hi_range", + "expires_on", + "consent__hi_types", ]; export default function ABDMFacilityRecords({ facilityId }: IProps) { + const { t } = useTranslation(); + const { data: consentsResult, loading, refetch, - } = useQuery(routes.abha.listConsents, { + } = useQuery(routes.abdm.consent.list, { query: { facility: facilityId, ordering: "-created_date" }, }); @@ -36,7 +38,7 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) { } return ( - +
@@ -51,7 +53,7 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) { scope="col" className="px-3 py-3.5 text-center text-sm font-semibold text-secondary-900" > - {head} + {t(head)} ))} - Refresh + {t("refresh")} - View + {t("view")} @@ -84,9 +86,13 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) { consent.consent_artefacts?.[0]?.expiry ?? consent.expiry, ) < new Date() - ? "EXPIRED" - : (consent.consent_artefacts?.[0]?.status ?? - consent.status)} + ? t("consent__status__EXPIRED") + : t( + `consent__status__${ + consent.consent_artefacts?.[0]?.status ?? + consent.status + }`, + )} @@ -94,11 +100,30 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) { - {consent.consent_artefacts.length - ? formatDateTime( - consent.consent_artefacts[0].created_date, - ) - : "-"} + {consent.status === "EXPIRED" || + new Date( + consent.consent_artefacts?.[0]?.expiry ?? + consent.expiry, + ) < new Date() ? ( +

+ {formatDateTime( + consent.consent_artefacts?.[0]?.expiry ?? + consent.expiry, + )} + + {t("expired_on")} + +

+ ) : consent.status === "REQUESTED" ? ( + "-" + ) : ( +

+ {formatDateTime(consent.modified_date)} + + {t(`${consent.status.toLowerCase()}_on`)} + +

+ )} @@ -127,7 +152,7 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) { consent.hi_types )?.map((hiType) => ( - {hiType} + {t(`consent__hi_type__${hiType}`)} ))}
@@ -135,26 +160,22 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) {
- {(consent.consent_artefacts?.[0]?.status ?? - consent.status) === "GRANTED" && - new Date( - consent.consent_artefacts?.[0]?.expiry ?? - consent.expiry, - ) > new Date() ? ( - - View - - ) : ( -

- View -

- )} + new Date() + ? "cursor-pointer text-primary-600 hover:text-primary-900" + : "pointer-events-none cursor-not-allowed text-secondary-600 opacity-70", + )} + > + {t("view")} +
diff --git a/src/Components/ABDM/ABDMRecordsTab.tsx b/src/Components/ABDM/ABDMRecordsTab.tsx index 37ae1a629db..d1dc854984f 100644 --- a/src/Components/ABDM/ABDMRecordsTab.tsx +++ b/src/Components/ABDM/ABDMRecordsTab.tsx @@ -1,6 +1,5 @@ import { ConsentArtefactModel, ConsentRequestModel } from "./types/consent"; import dayjs from "dayjs"; -import { ABDM_CONSENT_PURPOSE } from "../../Common/constants"; import CareIcon from "../../CAREUI/icons/CareIcon"; import ButtonV2 from "../Common/components/ButtonV2"; import * as Notification from "../../Utils/Notifications.js"; @@ -10,12 +9,15 @@ import { Link } from "raviger"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import useQuery from "../../Utils/request/useQuery"; +import { useTranslation } from "react-i18next"; interface IConsentArtefactCardProps { artefact: ConsentArtefactModel; } function ConsentArtefactCard({ artefact }: IConsentArtefactCardProps) { + const { t } = useTranslation(); + return (

- created {dayjs(artefact.created_date).fromNow()} + {t("created_on")} {dayjs(artefact.created_date).fromNow()}

@@ -39,7 +41,7 @@ function ConsentArtefactCard({ artefact }: IConsentArtefactCardProps) { {dayjs(artefact.to_time).format("MMM DD YYYY")}

- expires in {dayjs(artefact.expiry).fromNow()} + {t("expires_on")} {dayjs(artefact.expiry).fromNow()}

@@ -50,7 +52,7 @@ function ConsentArtefactCard({ artefact }: IConsentArtefactCardProps) { key={hiType} className="flex items-center justify-center rounded-full bg-secondary-600 px-4 py-1.5 text-xs font-medium text-white" > - {hiType} + {t(`consent__hi_type__${hiType}`)} ); })} @@ -64,15 +66,14 @@ interface IConsentRequestCardProps { } function ConsentRequestCard({ consent }: IConsentRequestCardProps) { + const { t } = useTranslation(); + return (
- { - ABDM_CONSENT_PURPOSE.find((p) => p.value === consent.purpose) - ?.label - } + {t(`consent__purpose__${consent.purpose}`)}
{formatName(consent.requester)} @@ -84,39 +85,40 @@ function ConsentRequestCard({ consent }: IConsentRequestCardProps) { {dayjs(consent.to_time).format("MMM DD YYYY")}

- expires in {dayjs(consent.expiry).fromNow()} + {t("expires_on")} {dayjs(consent.expiry).fromNow()}

{ - const { res, error } = await request( - routes.abha.checkConsentStatus, + const { res, data } = await request( + routes.abdm.consent.checkStatus, { - pathParams: { id: consent.id }, + body: { + consent_request: consent.id, + }, }, ); - if (res?.status === 200) { + if (res?.status === 202) { Notification.Success({ - msg: "Checking Status!", + msg: data?.detail ?? t("checking_consent_status"), }); - } else { - Notification.Error({ - msg: error?.message ?? "Error while checking status!", + Notification.Warn({ + msg: t("async_operation_warning"), }); } }} ghost className="max-w-2xl text-sm text-secondary-700 hover:text-secondary-900" > - check status + {t("check_status")}

- created {dayjs(consent.created_date).fromNow()} + {t("created_on")} {dayjs(consent.created_date).fromNow()}

- modified {dayjs(consent.modified_date).fromNow()} + {t("modified_on")} {dayjs(consent.modified_date).fromNow()}

@@ -130,8 +132,8 @@ function ConsentRequestCard({ consent }: IConsentRequestCardProps) {

{consent.status === "REQUESTED" - ? "Waiting for the Patient to approve the consent request" - : "Patient has rejected the consent request"} + ? t("consent_request_waiting_approval") + : t("consent_request_rejected")}

)} @@ -147,7 +149,7 @@ function ConsentRequestCard({ consent }: IConsentRequestCardProps) { : "bg-secondary-600", )} > - {hiType} + {t(`consent__hi_type__${hiType}`)}
); })} @@ -161,7 +163,9 @@ interface IProps { } export default function ABDMRecordsTab({ patientId }: IProps) { - const { data, loading } = useQuery(routes.abha.listConsents, { + const { t } = useTranslation(); + + const { data, loading } = useQuery(routes.abdm.consent.list, { query: { patient: patientId, ordering: "-created_date", @@ -175,9 +179,11 @@ export default function ABDMRecordsTab({ patientId }: IProps) { if (!data?.results.length) { return (
-

No Records found

+

+ {t("no_records_found")} +

- Raise a consent request to fetch patient records over ABDM + {t("raise_consent_request")}

); diff --git a/src/Components/ABDM/ABHAProfileModal.tsx b/src/Components/ABDM/ABHAProfileModal.tsx index 99b888cc1ef..87d5369d191 100644 --- a/src/Components/ABDM/ABHAProfileModal.tsx +++ b/src/Components/ABDM/ABHAProfileModal.tsx @@ -8,6 +8,7 @@ import { useRef } from "react"; import request from "../../Utils/request/request"; import routes from "../../Redux/api"; import { AbhaNumberModel } from "./types/abha"; +import { useTranslation } from "react-i18next"; interface IProps { patientId?: string; @@ -17,32 +18,55 @@ interface IProps { } const ABHAProfileModal = ({ patientId, show, onClose, abha }: IProps) => { + const { t } = useTranslation(); + const printRef = useRef(null); const downloadAbhaCard = async (type: "pdf" | "png") => { - if (!patientId) return; - const { res, data } = await request(routes.abha.getAbhaCard, { - body: { - patient: patientId, - type: type, - }, + if (!patientId || !abha?.abha_number) return; + + Notify.Success({ msg: t("downloading_abha_card") }); + + const { res, data } = await request(routes.abdm.healthId.getAbhaCard, { + query: { abha_id: abha?.abha_number }, }); if (res?.status === 200 && data) { + const imageUrl = URL.createObjectURL(data); + if (type === "png") { const downloadLink = document.createElement("a"); - downloadLink.href = "data:application/octet-stream;base64," + data; + downloadLink.href = imageUrl; downloadLink.download = "abha.png"; downloadLink.click(); + URL.revokeObjectURL(imageUrl); } else { - const htmlPopup = ``; - - const printWindow = window.open("", "PDF"); + const printWindow = window.open("", "_blank"); + const htmlPopup = ` + + + Print Image + + + + + + + `; printWindow?.document.write(htmlPopup); - printWindow?.print(); + printWindow?.document.close(); + printWindow?.addEventListener("load", () => { + printWindow?.print(); + URL.revokeObjectURL(imageUrl); + }); } - } else { - Notify.Error({ msg: "Download Failed..." }); } }; @@ -50,7 +74,7 @@ const ABHAProfileModal = ({ patientId, show, onClose, abha }: IProps) => { -

ABHA Profile

+

{t("abha_profile")}

downloadAbhaCard("pdf")} @@ -93,16 +117,19 @@ const ABHAProfileModal = ({ patientId, show, onClose, abha }: IProps) => {
{[ { - label: "Name", + label: t("full_name"), value: abha?.name || `${abha?.first_name} ${abha?.middle_name} ${abha?.last_name}`, }, - { label: "DOB", value: abha?.date_of_birth }, - { label: "Gender", value: abha?.gender }, - { label: "ABHA Number", value: abha?.abha_number }, - { label: "ABHA ID", value: abha?.health_id?.split("@")[0] }, - { label: "Email", value: abha?.email }, + { label: t("date_of_birth"), value: abha?.date_of_birth }, + { label: t("gender"), value: abha?.gender }, + { label: t("abha_number"), value: abha?.abha_number }, + { + label: t("abha_address"), + value: abha?.health_id?.split("@")[0], + }, + { label: t("email"), value: abha?.email }, ].map((item, index) => item.value ? (
@@ -117,13 +144,13 @@ const ABHAProfileModal = ({ patientId, show, onClose, abha }: IProps) => {
{abha?.created_date && (
- Created On: + {t("created_on")}: {formatDateTime(abha.created_date)}
)} {abha?.modified_date && (
- Last Modified On: + {t("modified_on")}: {formatDateTime(abha.modified_date)}
)} diff --git a/src/Components/ABDM/ConfigureHealthFacility.tsx b/src/Components/ABDM/ConfigureHealthFacility.tsx index f78a65f2cd1..495f2e5d4f6 100644 --- a/src/Components/ABDM/ConfigureHealthFacility.tsx +++ b/src/Components/ABDM/ConfigureHealthFacility.tsx @@ -8,17 +8,32 @@ import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import { FieldChangeEvent } from "../Form/FormFields/Utils.js"; +import { IHealthFacility } from "./types/health-facility.js"; +import { useTranslation } from "react-i18next"; + import Loading from "@/Components/Common/Loading"; const initForm = { - health_facility: null, + health_facility: null as IHealthFacility | null, hf_id: "", }; + const initialState = { form: { ...initForm }, - errors: {}, + errors: {} as Partial>, }; -const FormReducer = (state = initialState, action: any) => { +const FormReducer = ( + state = initialState, + action: + | { + type: "set_form"; + form: typeof initialState.form; + } + | { + type: "set_error"; + errors: typeof initialState.errors; + }, +) => { switch (action.type) { case "set_form": { return { @@ -37,12 +52,20 @@ const FormReducer = (state = initialState, action: any) => { } }; -export const ConfigureHealthFacility = (props: any) => { +export interface IConfigureHealthFacilityProps { + facilityId: string; +} + +export const ConfigureHealthFacility = ( + props: IConfigureHealthFacilityProps, +) => { + const { t } = useTranslation(); + const [state, dispatch] = useReducer(FormReducer, initialState); const { facilityId } = props; const [isLoading, setIsLoading] = useState(false); - const { loading } = useQuery(routes.abha.getHealthFacility, { + const { loading } = useQuery(routes.abdm.healthFacility.get, { pathParams: { facility_id: facilityId }, silent: true, onResponse(res) { @@ -66,7 +89,7 @@ export const ConfigureHealthFacility = (props: any) => { if (!state.form.hf_id) { dispatch({ type: "set_error", - errors: { hf_id: ["Health Facility Id is required"] }, + errors: { hf_id: t("health_facility__validation__hf_id_required") }, }); setIsLoading(false); return; @@ -76,7 +99,7 @@ export const ConfigureHealthFacility = (props: any) => { let responseData = null; if (state.form.hf_id === state.form.health_facility?.hf_id) { const { res, data } = await request( - routes.abha.registerHealthFacilityAsService, + routes.abdm.healthFacility.registerAsService, { pathParams: { facility_id: facilityId, @@ -87,7 +110,7 @@ export const ConfigureHealthFacility = (props: any) => { responseData = data; } else if (state.form.health_facility) { const { res, data } = await request( - routes.abha.partialUpdateHealthFacility, + routes.abdm.healthFacility.partialUpdate, { pathParams: { facility_id: facilityId, @@ -100,11 +123,12 @@ export const ConfigureHealthFacility = (props: any) => { response = res; responseData = data; } else { - const { res, data } = await request(routes.abha.createHealthFacility, { + const { res, data } = await request(routes.abdm.healthFacility.create, { body: { facility: facilityId, hf_id: state.form.hf_id, }, + silent: true, }); response = res; responseData = data; @@ -112,18 +136,21 @@ export const ConfigureHealthFacility = (props: any) => { if (response?.ok && responseData?.registered) { Notification.Success({ - msg: "Health Facility config updated successfully", + msg: t("health_facility__config_update_success"), }); navigate(`/facility/${facilityId}`); } else { if (responseData?.registered === false) { Notification.Warn({ - msg: responseData?.detail || "Health ID registration failed", + msg: + responseData?.detail || + t("health_facility__config_registration_error"), }); navigate(`/facility/${facilityId}`); } else { Notification.Error({ - msg: responseData?.detail || "Health Facility config update failed", + msg: + responseData?.detail || t("health_facility__config_update_error"), }); } } @@ -148,7 +175,7 @@ export const ConfigureHealthFacility = (props: any) => {
{ <>
- The ABDM health facility is successfully linked with - care{" "} - and registered as a service in bridge + {t("health_facility__registered_1.1")}{" "} + + {t("health_facility__registered_1.2")} + - No Action Required. + {t("health_facility__registered_2")}
- Registered + {t("health_facility__registered_3")} ) : ( <>
- The ABDM health facility is successfully linked with - care{" "} + {t("health_facility__not_registered_1.1")}{" "} - but not registered as a service in bridge + {t("health_facility__not_registered_1.2")} - Click on Link Health Facility to - register the service + {t("health_facility__not_registered_2")} + {t("health_facility__not_registered_3")}
- Not Registered )}

@@ -206,7 +232,7 @@ export const ConfigureHealthFacility = (props: any) => { state.form.hf_id === state.form.health_facility?.hf_id && state.form.health_facility?.registered } - label="Link Health Facility" + label={t("health_facility__link")} />
diff --git a/src/Components/ABDM/FetchRecordsModal.tsx b/src/Components/ABDM/FetchRecordsModal.tsx index 721927c47a6..3da4af9199b 100644 --- a/src/Components/ABDM/FetchRecordsModal.tsx +++ b/src/Components/ABDM/FetchRecordsModal.tsx @@ -16,15 +16,12 @@ import DateFormField from "../Form/FormFields/DateFormField.js"; import request from "../../Utils/request/request.js"; import routes from "../../Redux/api"; import { useMessageListener } from "../../Common/hooks/useMessageListener.js"; -import CircularProgress from "../Common/components/CircularProgress.js"; -import CareIcon from "../../CAREUI/icons/CareIcon.js"; -import { classNames } from "../../Utils/utils.js"; import { AbhaNumberModel } from "./types/abha.js"; import { ConsentHIType, ConsentPurpose } from "./types/consent.js"; -import useNotificationSubscriptionState from "../../Common/hooks/useNotificationSubscriptionState.js"; +import { useTranslation } from "react-i18next"; -const getDate = (value: any) => - value && dayjs(value).isValid() && dayjs(value).toDate(); +const getDate = (value: string | Date) => + (value && dayjs(value).isValid() && dayjs(value).toDate()) || undefined; interface IProps { abha?: AbhaNumberModel; @@ -33,9 +30,11 @@ interface IProps { } export default function FetchRecordsModal({ abha, show, onClose }: IProps) { + const { t } = useTranslation(); + const [idVerificationStatus, setIdVerificationStatus] = useState< "pending" | "in-progress" | "verified" | "failed" - >("pending"); + >("verified"); const [purpose, setPurpose] = useState("CAREMGT"); const [fromDate, setFromDate] = useState( dayjs().subtract(30, "day").toDate(), @@ -46,10 +45,10 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { const [expiryDate, setExpiryDate] = useState( dayjs().add(30, "day").toDate(), ); - const [errors, setErrors] = useState({}); - const notificationSubscriptionState = useNotificationSubscriptionState([ - show, - ]); + const [errors, setErrors] = useState>({}); + // const notificationSubscriptionState = useNotificationSubscriptionState([ + // show, + // ]); useMessageListener((data) => { if (data.type === "MESSAGE" && data.from === "patients/on_find") { @@ -67,11 +66,11 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { - {["unsubscribed", "subscribed_on_other_device"].includes( + {/* {["unsubscribed", "subscribed_on_other_device"].includes( notificationSubscriptionState, ) && (

@@ -79,20 +78,20 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { Notifications needs to be enabled on this device to verify the patient.

- )} + )} */}
null} disabled - label="Patient Identifier" + label={t("consent_request__patient_identifier")} name="health_id" error={errors.health_id} className="flex-1" /> - { const { res } = await request(routes.abha.findPatient, { body: { @@ -131,17 +130,17 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { failed: "Retry", }[idVerificationStatus] } - + */}
o.label} - optionValue={(o) => o.value} + optionLabel={(o) => t(`consent__purpose__${o}`)} + optionValue={(o) => o} value={purpose} onChange={({ value }) => setPurpose(value)} required @@ -158,30 +157,30 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { setFromDate(e.value.start!); setToDate(e.value.end!); }} - label="Health Records range" + label={t("consent_request__date_range")} required /> { - setHiTypes(ABDM_HI_TYPE.map((type) => type.value)); + setHiTypes(ABDM_HI_TYPE); }} > - Select All + {t("select_all")} ) } value={hiTypes} - optionLabel={(option) => option.label} - optionValue={(option) => option.value} + optionLabel={(option) => t(`consent__hi_type__${option}`)} + optionValue={(option) => option} onChange={(e) => setHiTypes(e.value)} required /> @@ -191,7 +190,7 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { id="expiry_date" value={getDate(expiryDate)} onChange={(e) => setExpiryDate(e.value!)} - label="Consent Expiry Date" + label={t("consent_request__expiry")} required disablePast position="TOP-RIGHT" @@ -203,14 +202,14 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { if (idVerificationStatus !== "verified") { setErrors({ ...errors, - health_id: "Please verify the patient identifier", + health_id: t("verify_patient_identifier"), }); return; } setIsMakingConsentRequest(true); - const { res } = await request(routes.abha.createConsent, { + const { res } = await request(routes.abdm.consent.create, { body: { patient_abha: abha?.health_id as string, hi_types: hiTypes, @@ -223,17 +222,13 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { if (res?.status === 201) { Notification.Success({ - msg: "Consent requested successfully!", + msg: t("consent_requested_successfully"), }); navigate( `/facility/${abha?.patient_object?.facility}/abdm`, // ?? `/facility/${abha?.patient_object?.facility}/patient/${abha?.patient_object?.id}/consultation/${abha?.patient_object?.last_consultation?.id}/abdm`, ); - } else { - Notification.Error({ - msg: "Error while requesting consent!", - }); } setIsMakingConsentRequest(false); onClose(); @@ -241,7 +236,7 @@ export default function FetchRecordsModal({ abha, show, onClose }: IProps) { disabled={idVerificationStatus !== "verified"} loading={isMakingConsentRequest} > - Request Consent + {t("request_consent")}
diff --git a/src/Components/ABDM/HealthInformation.tsx b/src/Components/ABDM/HealthInformation.tsx index 1531f47c853..dd5a279bc1c 100644 --- a/src/Components/ABDM/HealthInformation.tsx +++ b/src/Components/ABDM/HealthInformation.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import routes from "../../Redux/api"; import useQuery from "../../Utils/request/useQuery"; import Loading from "../Common/Loading"; @@ -9,7 +10,9 @@ interface IProps { } export default function HealthInformation({ artefactId }: IProps) { - const { data, loading, error } = useQuery(routes.abha.getHealthInformation, { + const { t } = useTranslation(); + + const { data, loading, error } = useQuery(routes.abdm.healthInformation.get, { pathParams: { artefactId }, silent: true, }); @@ -18,7 +21,7 @@ export default function HealthInformation({ artefactId }: IProps) { return ; } - const parseData = (data: any) => { + const parseData = (data: string) => { try { return JSON.parse(data); } catch (e) { @@ -29,20 +32,19 @@ export default function HealthInformation({ artefactId }: IProps) { }; return ( - +
{!!error?.is_archived && ( <>

- This record has been archived + {t("hi__record_archived__title")}

- This record has been archived and is no longer available for - viewing. + {t("hi__record_archived_description")}

- This record was archived on{" "} - {new Date(error?.archived_time as string).toLocaleString()} as{" "} + {t("hi__record_archived_on")}{" "} + {new Date(error?.archived_time as string).toLocaleString()} -{" "} {error?.archived_reason as string}

@@ -50,13 +52,13 @@ export default function HealthInformation({ artefactId }: IProps) { {error && !error?.is_archived && ( <>

- This record hasn't been fetched yet + {t("hi__record_not_fetched_title")}

- This record hasn't been fetched yet. Please try again later. + {t("hi__record_not_fetched_description")}

- Waiting for the HIP to send the record. + {t("hi__waiting_for_record")}

)} diff --git a/src/Components/ABDM/LinkABHANumberModal.tsx b/src/Components/ABDM/LinkABHANumberModal.tsx deleted file mode 100644 index c2855ae7569..00000000000 --- a/src/Components/ABDM/LinkABHANumberModal.tsx +++ /dev/null @@ -1,875 +0,0 @@ -import * as Notify from "../../Utils/Notifications"; - -import Dropdown, { DropdownItem } from "../Common/components/Menu"; -import { useEffect, useState } from "react"; - -import ButtonV2 from "../Common/components/ButtonV2"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import CircularProgress from "../Common/components/CircularProgress"; -import DialogModal from "../Common/Dialog"; -import OtpFormField from "../Form/FormFields/OtpFormField"; -import QRScanner from "../Common/QRScanner"; -import TextFormField from "../Form/FormFields/TextFormField"; -import { classNames } from "../../Utils/utils"; -import request from "../../Utils/request/request"; -import routes from "../../Redux/api"; -import { ABDMError, ABHAQRContent } from "./models"; - -export const validateRule = ( - condition: boolean, - content: JSX.Element | string, -) => { - return ( -
- {condition ? ( - - ) : ( - - )}{" "} - - {content} - -
- ); -}; -interface Props { - patientId?: string; - patientMobile?: string | undefined; - onSuccess?: (abha: any) => void; - show: boolean; - onClose: () => void; -} - -type Step = - | "ScanExistingQR" - | "AadhaarVerification" - | "MobileVerification" - | "HealthIDCreation"; - -export default function LinkABHANumberModal({ - patientId, - patientMobile, - onSuccess, - ...props -}: Props) { - const [currentStep, setCurrentStep] = useState("AadhaarVerification"); - const [transactionId, setTransactionId] = useState(""); - - const title = ( -
- -

- {currentStep === "ScanExistingQR" - ? "Link Existing ABHA Number" - : "Generate ABHA number"} -

-
- ); - - return ( - -
- {currentStep === "ScanExistingQR" && ( - - )} - - {currentStep === "AadhaarVerification" && ( - { - setTransactionId(transactionId); - setCurrentStep("MobileVerification"); - }} - /> - )} - - {currentStep === "MobileVerification" && transactionId && ( - { - setTransactionId(transactionId); - setCurrentStep("HealthIDCreation"); - }} - patientMobile={patientMobile} - /> - )} - - {currentStep === "HealthIDCreation" && transactionId && ( - { - props.onClose(); - onSuccess?.(abha); - }} - patientId={patientId} - /> - )} -
- -
- {["AadhaarVerification", "MobileVerification", "HealthIDCreation"].find( - (step) => step === currentStep, - ) ? ( -

setCurrentStep("ScanExistingQR")} - className="cursor-pointer text-center text-sm text-blue-800" - > - Already have an ABHA number -

- ) : ( -

setCurrentStep("AadhaarVerification")} - className="cursor-pointer text-center text-sm text-blue-800" - > - Don't have an ABHA Number -

- )} -
-
- ); -} - -interface ScanABHAQRSectionProps { - patientId?: string; - onSuccess?: (abha: any) => void; - closeModal: () => void; -} - -const ScanABHAQRSection = ({ - patientId, - onSuccess, - closeModal, -}: ScanABHAQRSectionProps) => { - const [qrValue, setQrValue] = useState(""); - const [authMethods, setAuthMethods] = useState([]); - const [selectedAuthMethod, setSelectedAuthMethod] = useState(""); - const [txnId, setTxnId] = useState(""); - const [otp, setOtp] = useState(""); - const [acceptedDisclaimer, setAcceptedDisclaimer] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const supportedAuthMethods = ["MOBILE_OTP", "AADHAAR_OTP"]; - - if (isLoading) { - return ( -
- - Loading - - -
- ); - } - - return ( -
- { - if (value[0] && !isNaN(Number(value[0]))) { - // 92-1234-1234-1234 - if ([2, 7, 12].includes(value.length)) { - if (qrValue.length && qrValue[qrValue.length - 1] === "-") { - value.slice(value.length - 1); - } else { - value += "-"; - } - } - } - setQrValue(value); - }} - parse={async (value: string | null) => { - if (!value) return; - setIsLoading(true); - - try { - const abha = JSON.parse(value) as ABHAQRContent; - - const { res, data } = await request(routes.abha.linkViaQR, { - body: { - patientId, - hidn: abha?.hidn, - phr: (abha?.phr ?? abha?.hid) as string, - name: abha?.name, - gender: abha?.gender, - dob: abha?.dob.replace(/\//g, "-"), - address: abha?.address, - "dist name": abha?.["dist name"] ?? abha?.district_name, - "state name": abha?.["state name"] ?? abha?.state_name, - }, - }); - - if (res?.status === 200 || res?.status === 202) { - Notify.Success({ msg: "Request sent successfully" }); - onSuccess?.({ - ...data, - abha_profile: { - ...data?.abha_profile, - healthIdNumber: data?.abha_profile?.abha_number, - healthId: data?.abha_profile?.health_id, - mobile: abha?.mobile, - monthOfBirth: - data?.abha_profile?.date_of_birth?.split("-")[1], - dayOfBirth: data?.abha_profile?.date_of_birth?.split("-")[2], - yearOfBirth: data?.abha_profile?.date_of_birth?.split("-")[0], - }, - }); - } else { - Notify.Error({ msg: "Linking Failed" }); - } - } catch (e) { - Notify.Error({ msg: "Invalid ABHA QR" }); - } finally { - setIsLoading(false); - closeModal(); - } - }} - /> - {!txnId && ( -
- - { - setAcceptedDisclaimer(e.target.checked); - }} - className="mr-2 rounded border-secondary-700 shadow-sm ring-0 ring-offset-0" - /> - I declare that the ABHA No. of the patient is voluntarily provided - by the patient (or guardian or nominee of the patient). - -
- )} - {txnId && ( - setOtp(value as string)} - value={otp} - label="Enter 6 digit OTP!" - error="" - /> - )} -
- <> - {txnId ? ( - { - let response = null; - let Rdata = null; - let Rerror = null; - - switch (selectedAuthMethod) { - case "MOBILE_OTP": - { - const { res, data, error } = await request( - routes.abha.confirmWithMobileOtp, - { - body: { - otp: otp, - txnId: txnId, - patientId: patientId, - }, - }, - ); - response = res; - Rdata = data; - Rerror = error; - } - break; - - case "AADHAAR_OTP": - { - const { res, data, error } = await request( - routes.abha.confirmWithAadhaarOtp, - { - body: { - otp: otp, - txnId: txnId, - patientId: patientId, - }, - }, - ); - response = res; - Rdata = data; - Rerror = error; - } - break; - } - - if (response?.status === 200) { - onSuccess?.(Rdata); - Notify.Success({ - msg: "ABHA Number linked successfully", - }); - } else { - Notify.Error({ - msg: Rerror ?? "Something went wrong!", - }); - } - }} - > - Link - - ) : authMethods.length ? ( - - {authMethods.map((method) => ( - { - const { res, data } = await request( - routes.abha.initiateAbdmAuthentication, - { body: { authMethod: method, healthid: qrValue } }, - ); - - if (res?.status === 200 && data?.txnId) { - setSelectedAuthMethod(method); - setTxnId(data.txnId); - } - }} - > - {method.replace(/_/g, " ")} - - ))} - - ) : ( - { - const { res, data } = await request( - routes.abha.searchByHealthId, - { - body: { - healthId: qrValue, - }, - }, - ); - - if (res?.status === 200 && data?.authMethods) { - setAuthMethods( - data.authMethods?.filter?.((method: string) => - supportedAuthMethods.find( - (supported) => supported === method, - ), - ), - ); - } - }} - > - Verify - - )} - -
-
- ); -}; - -interface VerifyAadhaarSectionProps { - onVerified: (transactionId: string) => void; -} - -const VerifyAadhaarSection = ({ onVerified }: VerifyAadhaarSectionProps) => { - const [aadhaarNumber, setAadhaarNumber] = useState(""); - const [aadhaarNumberError, setAadhaarNumberError] = useState(); - - const [otp, setOtp] = useState(""); - const [otpError, setOtpError] = useState(); - - const [txnId, setTxnId] = useState(); - const [isSendingOtp, setIsSendingOtp] = useState(false); - const [isVerifyingOtp, setIsVerifyingOtp] = useState(false); - const [verified, setIsVerified] = useState(false); - const [acceptedDisclaimer1, setAcceptedDisclaimer1] = useState(false); - const [acceptedDisclaimer2, setAcceptedDisclaimer2] = useState(false); - - useEffect(() => { - if (verified && txnId) { - setTimeout(() => onVerified(txnId), 1000); - } - }, [verified]); - - const otpSent = !!txnId; - - const validateAadhaar = () => { - if (aadhaarNumber.length !== 12 && aadhaarNumber.length !== 16) { - setAadhaarNumberError( - "Should be a 12-digit aadhaar number or 16-digit virtual ID", - ); - return false; - } - - if (aadhaarNumber.includes(" ")) { - setAadhaarNumberError("Should not contain spaces"); - return false; - } - - return true; - }; - - const sendOtp = async () => { - if (!validateAadhaar()) return; - - setIsSendingOtp(true); - - const { res, data } = await request(routes.abha.generateAadhaarOtp, { - body: { - aadhaar: aadhaarNumber, - }, - }); - setIsSendingOtp(false); - - if (res?.status === 200 && data) { - const { txnId } = data; - setTxnId(txnId); - Notify.Success({ - msg: "OTP has been sent to the mobile number registered with the Aadhar number.", - }); - } else { - Notify.Error({ msg: JSON.stringify(data) }); - } - }; - - const resendOtp = async () => { - if (!validateAadhaar() || !txnId) return; - - setIsSendingOtp(true); - const { res, data } = await request(routes.abha.resendAadhaarOtp, { - body: { - txnId: txnId, - }, - silent: true, - }); - setIsSendingOtp(false); - - if (res?.status === 200 && data?.txnId) { - setTxnId(data.txnId); - Notify.Success({ - msg: "OTP has been resent to the mobile number registered with the Aadhar number.", - }); - } else { - Notify.Error({ - msg: - (data as unknown as ABDMError).details - ?.map((detail) => detail.message) - .join(", ") - .trim() || - (data as unknown as ABDMError).message || - "OTP resend failed", - }); - } - }; - - const validateOtp = () => { - if (otp.length !== 6) { - setOtpError("Must be a 6-digit code"); - return false; - } - - if (otp.includes(" ")) { - setOtpError("Should not contain spaces"); - return false; - } - return true; - }; - - const verifyOtp = async () => { - if (!validateOtp() || !txnId) return; - - setIsVerifyingOtp(true); - const { res, data } = await request(routes.abha.verifyAadhaarOtp, { - body: { - otp: otp, - txnId: txnId, - }, - }); - setIsVerifyingOtp(false); - - if (res?.status === 200 && data?.txnId) { - setTxnId(data.txnId); - Notify.Success({ msg: "OTP verified" }); - setIsVerified(true); - } else { - Notify.Error({ msg: "OTP verification failed" }); - } - }; - - return ( -
-
- setAadhaarNumber(value)} - error={aadhaarNumberError} - /> - - Aadhaar number will not be stored by CARE - -
- - {!otpSent && ( -
- - { - setAcceptedDisclaimer1(e.target.checked); - }} - className="mr-2 rounded border-secondary-700 shadow-sm ring-0 ring-offset-0" - /> - I declare that consent of the patient (or guardian or nominee of the - patient) is obtained for generation of such ABHA Number as per the{" "} - - Privacy Policy - - . - - - - { - setAcceptedDisclaimer2(e.target.checked); - }} - className="mr-2 rounded border-secondary-700 shadow-sm ring-0 ring-offset-0" - /> - I declare that the Aadhaar Number and demographic details of the - patient are shared voluntarily by the patient (or guardian or - nominee of the patient) through CARE with NHA for the sole purpose - of creation of ABHA Number. The patient understands that such data - of the patient will be collected, stored and utilized as per{" "} - - ABDM Health Data Management Policy - - . The patient authorizes NHA to use the Aadhaar number for - performing Aadhaar based authentication with UIDAI as per the - provisions of Aadhaar Act 2016 for the aforesaid purpose. - -
- )} - - {otpSent && ( - setOtp(value as string)} - value={otp} - label="Enter 6-digit OTP sent to the registered mobile" - disabled={isVerifyingOtp} - error={otpError} - /> - )} - -
- <> - - {(isSendingOtp && "Sending OTP...") || - (otpSent ? "Resend OTP" : "Send OTP")} - - - {otpSent && ( - - {(verified && "Verified") || - (isVerifyingOtp ? "Verifying..." : "Verify")} - - )} - -
-
- ); -}; - -interface VerifyMobileSectionProps { - transactionId: string; - onVerified: (transactionId: string) => void; - patientMobile?: string | undefined; -} - -const VerifyMobileSection = ({ - transactionId, - onVerified, - patientMobile, -}: VerifyMobileSectionProps) => { - const [mobile, setMobile] = useState(() => patientMobile || ""); - const [mobileError, setMobileError] = useState(); - - const [otp, setOtp] = useState(""); - const [otpError, setOtpError] = useState(); - - const [txnId, setTxnId] = useState(() => transactionId); - const [otpDispatched, setOtpDispatched] = useState(false); - const [isSendingOtp, setIsSendingOtp] = useState(false); - const [isVerifyingOtp, setIsVerifyingOtp] = useState(false); - const [verified, setIsVerified] = useState(false); - - useEffect(() => { - if (verified && txnId) { - setTimeout(() => onVerified(txnId), 1000); - } - }, [verified]); - - const validateMobile = () => { - if (mobile.length !== 10) { - setMobileError("Should contain 10-digits"); - return false; - } - - if (mobile.includes(" ")) { - setMobileError("Should not contain spaces"); - return false; - } - - return true; - }; - - const sendOtp = async () => { - if (!validateMobile()) return; - - setOtpDispatched(false); - setIsSendingOtp(true); - const { res, data } = await request(routes.abha.checkAndGenerateMobileOtp, { - body: { - mobile: mobile, - txnId: txnId, - }, - }); - setIsSendingOtp(false); - - if (res?.status === 200 && data) { - const { txnId, mobileLinked } = data; - setTxnId(txnId); - - if (mobileLinked) { - setIsVerified(true); - Notify.Success({ - msg: "Mobile number verified.", - }); - } else { - setOtpDispatched(true); - Notify.Success({ - msg: "OTP has been sent to the mobile number.", - }); - } - } else { - Notify.Error({ msg: JSON.stringify(data) }); - } - }; - - const validateOtp = () => { - if (otp.length !== 6) { - setOtpError("Must be a 6-digit code"); - return false; - } - - if (otp.includes(" ")) { - setOtpError("Should not contain spaces"); - return false; - } - return true; - }; - - const verifyOtp = async () => { - if (!validateOtp()) return; - - setIsVerifyingOtp(true); - const { res, data } = await request(routes.abha.verifyMobileOtp, { - body: { - txnId: txnId, - otp: otp, - }, - }); - setIsVerifyingOtp(false); - - if (res?.status === 200 && data?.txnId) { - setTxnId(data.txnId); - Notify.Success({ msg: "OTP verified" }); - setIsVerified(true); - } else { - Notify.Error({ msg: "OTP verification failed" }); - } - }; - - return ( -
- setMobile(value)} - error={mobileError} - /> - - {otpDispatched ? ( - setOtp(value as string)} - error={otpError} - /> - ) : ( -

- OTP is - generated if the above phone number is not linked with given Aadhaar - number. -

- )} - -
- - {(isSendingOtp && "Sending OTP...") || - (otpDispatched ? "Resend OTP" : "Send OTP")} - - - {otpDispatched && ( - - {(verified && "Verified") || - (isVerifyingOtp ? "Verifying..." : "Verify")} - - )} -
-
- ); -}; - -interface CreateHealthIDSectionProps { - transactionId: string; - onCreateSuccess: (abha: any) => void; - patientId?: string; -} - -const CreateHealthIDSection = ({ - transactionId, - onCreateSuccess, - patientId, -}: CreateHealthIDSectionProps) => { - const [healthId, setHealthId] = useState(""); - const [isCreating, setIsCreating] = useState(false); - const [isHealthIdInputInFocus, setIsHealthIdInputInFocus] = useState(false); - - const handleCreateHealthId = async () => { - setIsCreating(true); - const { res, data } = await request(routes.abha.createHealthId, { - body: { - healthId: healthId, - txnId: transactionId, - patientId: patientId, - }, - }); - if (res?.status === 200) { - Notify.Success({ msg: "Abha Address created" }); - onCreateSuccess(data); - } else { - Notify.Error({ msg: JSON.stringify(data) }); - } - setIsCreating(false); - }; - - return ( -
- { - setHealthId(value); - }} - onFocus={() => setIsHealthIdInputInFocus(true)} - onBlur={() => setIsHealthIdInputInFocus(false)} - /> - -

- Existing - ABHA Address is used if ABHA Number already exists. -

- - {isHealthIdInputInFocus && ( -
- {validateRule( - healthId.length >= 4, - "Should be atleast 4 character long", - )} - {validateRule( - isNaN(Number(healthId[0])) && healthId[0] !== ".", - "Shouldn't start with a number or dot (.)", - )} - {validateRule( - healthId[healthId.length - 1] !== ".", - "Shouldn't end with a dot (.)", - )} - {validateRule( - /^[0-9a-zA-Z.]+$/.test(healthId), - "Should only contain letters, numbers and dot (.)", - )} -
- )} - -
- - {isCreating ? "Creating Abha Address..." : "Create Abha Address"} - -
-
- ); -}; diff --git a/src/Components/ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx b/src/Components/ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx new file mode 100644 index 00000000000..e2fff0e51e6 --- /dev/null +++ b/src/Components/ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx @@ -0,0 +1,732 @@ +import { useTranslation } from "react-i18next"; +import useMultiStepForm, { InjectedStepProps } from "./useMultiStepForm"; +import { classNames } from "../../../Utils/utils"; +import TextFormField from "../../Form/FormFields/TextFormField"; +import { useEffect, useState } from "react"; +import ButtonV2, { ButtonWithTimer } from "../../Common/components/ButtonV2"; +import OtpFormField from "../../Form/FormFields/OtpFormField"; +import PhoneNumberFormField from "../../Form/FormFields/PhoneNumberFormField"; +import { AbhaNumberModel } from "../types/abha"; +import { validateRule } from "../../Users/UserAdd"; +import request from "../../../Utils/request/request"; +import routes from "../../../Redux/api"; +import * as Notify from "../../../Utils/Notifications"; +import CheckBoxFormField from "../../Form/FormFields/CheckBoxFormField"; + +const MAX_OTP_RESEND_ALLOWED = 2; + +type ICreateWithAadhaarProps = { + onSuccess: (abhaNumber: AbhaNumberModel) => void; +}; + +type Memory = { + aadhaarNumber: string; + mobileNumber: string; + + isLoading: boolean; + validationError: string; + + transactionId: string; + abhaNumber: AbhaNumberModel | null; + + resendOtpCount: number; +}; + +export default function CreateWithAadhaar({ + onSuccess, +}: ICreateWithAadhaarProps) { + const { currentStep } = useMultiStepForm( + [ + , + , + , + , + , + , + ], + { + aadhaarNumber: "", + mobileNumber: "+91", + isLoading: false, + validationError: "", + transactionId: "", + abhaNumber: null, + resendOtpCount: 0, + }, + ); + + return
{currentStep}
; +} + +type IEnterAadhaarProps = InjectedStepProps; + +function EnterAadhaar({ memory, setMemory, next }: IEnterAadhaarProps) { + const { t } = useTranslation(); + const [disclaimerAccepted, setDisclaimerAccepted] = useState([ + false, + false, + false, + false, + ]); + + const validateAadhaar = () => { + if ( + memory?.aadhaarNumber.length !== 12 && + memory?.aadhaarNumber.length !== 16 + ) { + setMemory((prev) => ({ + ...prev, + validationError: t("aadhaar_validation_length_error"), + })); + return false; + } + + if (memory?.aadhaarNumber.includes(" ")) { + setMemory((prev) => ({ + ...prev, + validationError: t("aadhaar_validation_space_error"), + })); + return false; + } + + return true; + }; + + const handleSubmit = async () => { + if (!validateAadhaar()) return; + + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request( + routes.abdm.healthId.abhaCreateSendAadhaarOtp, + { + body: { + aadhaar: memory!.aadhaarNumber, + }, + }, + ); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ ...prev, transactionId: data.transaction_id })); + Notify.Success({ + msg: data.detail ?? t("aadhaar_otp_send_success"), + }); + next(); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + return ( +
+
+ + setMemory((prev) => ({ ...prev, aadhaarNumber: value })) + } + error={memory?.validationError} + /> + + {t("aadhaar_number_will_not_be_stored")} + +
+ +
+ {disclaimerAccepted.map((isAccepted, i) => ( + { + setDisclaimerAccepted( + disclaimerAccepted.map((v, j) => (j === i ? e.value : v)), + ); + }} + className="mr-2 rounded border-gray-700" + labelClassName="text-xs text-gray-800" + errorClassName="hidden" + /> + ))} +
+ +
+ !v) || + memory?.aadhaarNumber.length === 0 + } + onClick={handleSubmit} + > + {t("send_otp")} + +
+
+ ); +} + +type IVerifyAadhaarProps = InjectedStepProps; + +function VerifyAadhaar({ memory, setMemory, next }: IVerifyAadhaarProps) { + const { t } = useTranslation(); + const [otp, setOtp] = useState(""); + + const validateMobileNumber = () => { + const phone = memory?.mobileNumber.replace("+91", "").replace(/ /g, ""); + if (phone?.length !== 10) { + setMemory((prev) => ({ + ...prev, + validationError: t("mobile_number_validation_error"), + })); + return false; + } + + return true; + }; + + const handleSubmit = async () => { + if (!validateMobileNumber()) return; + + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request( + routes.abdm.healthId.abhaCreateVerifyAadhaarOtp, + { + body: { + otp: otp, + transaction_id: memory?.transactionId, + mobile: memory?.mobileNumber.replace("+91", "").replace(/ /g, ""), + }, + }, + ); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ + ...prev, + transactionId: data.transaction_id, + abhaNumber: data.abha_number, + resendOtpCount: 0, + })); + Notify.Success({ + msg: data.detail ?? t("otp_verification_success"), + }); + next(); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + const handleResendOtp = async () => { + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request( + routes.abdm.healthId.abhaCreateSendAadhaarOtp, + { + body: { + aadhaar: memory!.aadhaarNumber, + // transaction_id: memory?.transactionId, + }, + silent: true, + }, + ); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ + ...prev, + transactionId: data.transaction_id, + resendOtpCount: prev.resendOtpCount + 1, + })); + Notify.Success({ + msg: data.detail ?? t("aadhaar_otp_send_success"), + }); + } else { + setMemory((prev) => ({ + ...prev, + resendOtpCount: Infinity, + })); + Notify.Success({ + msg: t("aadhaar_otp_send_error"), + }); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + return ( +
+
+ + setMemory((prev) => ({ ...prev, aadhaarNumber: value })) + } + /> + + {t("aadhaar_number_will_not_be_stored")} + +
+ +
+ setOtp(value as string)} + value={otp} + label={t("enter_aadhaar_otp")} + disabled={memory?.isLoading} + /> +
+ +
+ } + name="mobile_number" + value={memory?.mobileNumber} + onChange={(e) => { + if (!memory?.mobileNumber.startsWith("+91")) { + setMemory((prev) => ({ + ...prev, + validationError: t("only_indian_mobile_numbers_supported"), + })); + return; + } + + setMemory((prev) => ({ ...prev, mobileNumber: e.value })); + }} + error={memory?.validationError} + errorClassName="text-xs text-red-500" + types={["mobile"]} + /> +
+ +
+ 6 || memory?.mobileNumber.length === 0} + onClick={handleSubmit} + > + {t("verify_otp")} + + + {(memory?.resendOtpCount ?? 0) < MAX_OTP_RESEND_ALLOWED && ( + + {t("resend_otp")} + + )} +
+
+ ); +} + +type IHandleExistingAbhaNumberProps = InjectedStepProps & { + onSuccess: (abhaNumber: AbhaNumberModel) => void; +}; + +function HandleExistingAbhaNumber({ + memory, + onSuccess, + next, +}: IHandleExistingAbhaNumberProps) { + const { t } = useTranslation(); + + // skip this step for new abha number + useEffect(() => { + if (memory?.abhaNumber?.new) { + next(); + } + }, [memory?.abhaNumber, memory?.mobileNumber]); // eslint-disable-line + + return ( +
+

+ {t("abha_number_exists")} +

+

+ {t("abha_number_exists_description")} +

+
+ + {t("create_new_abha_address")} + + onSuccess(memory?.abhaNumber as AbhaNumberModel)} + > + {t("use_existing_abha_address")} + +

+ {memory?.abhaNumber?.health_id} +

+
+
+ ); +} + +type ILinkMobileNumberProps = InjectedStepProps; + +function LinkMobileNumber({ + memory, + goTo, + setMemory, + next, +}: ILinkMobileNumberProps) { + const { t } = useTranslation(); + + useEffect(() => { + if ( + memory?.abhaNumber?.mobile === + memory?.mobileNumber.replace("+91", "").replace(/ /g, "") + ) { + goTo(5); // skip linking mobile number + } + }, [memory?.abhaNumber, memory?.mobileNumber]); // eslint-disable-line + + const handleSubmit = async () => { + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request( + routes.abdm.healthId.abhaCreateLinkMobileNumber, + { + body: { + mobile: memory?.mobileNumber.replace("+91", "").replace(/ /g, ""), + transaction_id: memory?.transactionId, + }, + }, + ); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ + ...prev, + transactionId: data.transaction_id, + })); + Notify.Success({ + msg: data.detail ?? t("mobile_otp_send_success"), + }); + next(); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + return ( +
+
+ } + name="mobile_number" + value={memory?.mobileNumber} + disabled={true} + onChange={() => null} + types={["mobile"]} + /> +
+ +

+ {t("mobile_number_different_from_aadhaar_mobile_number")} +

+ +
+ + {t("send_otp")} + +
+
+ ); +} + +type IVerifyMobileNumberProps = InjectedStepProps; + +function VerifyMobileNumber({ + memory, + setMemory, + next, +}: IVerifyMobileNumberProps) { + const { t } = useTranslation(); + const [otp, setOtp] = useState(""); + + const handleSubmit = async () => { + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request( + routes.abdm.healthId.abhaCreateVerifyMobileNumber, + { + body: { + transaction_id: memory?.transactionId, + otp: otp, + }, + }, + ); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ + ...prev, + transactionId: data.transaction_id, + resendOtpCount: 0, + })); + Notify.Success({ + msg: data.detail ?? t("mobile_otp_verify_success"), + }); + next(); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + const handleResendOtp = async () => { + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request( + routes.abdm.healthId.abhaCreateLinkMobileNumber, + { + body: { + mobile: memory?.mobileNumber.replace("+91", "").replace(/ /g, ""), + transaction_id: memory?.transactionId, + }, + }, + ); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ + ...prev, + transactionId: data.transaction_id, + resendOtpCount: prev.resendOtpCount + 1, + })); + Notify.Success({ + msg: data.detail ?? t("mobile_otp_send_success"), + }); + } else { + setMemory((prev) => ({ + ...prev, + resendOtpCount: Infinity, + })); + Notify.Success({ + msg: t("mobile_otp_send_error"), + }); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + return ( +
+
+ } + name="mobile_number" + value={memory?.mobileNumber} + disabled={true} + onChange={() => null} + types={["mobile"]} + /> +
+ +
+ setOtp(value as string)} + value={otp} + label={t("enter_mobile_otp")} + disabled={memory?.isLoading} + /> +
+ +
+ + {t("verify_otp")} + + + {(memory?.resendOtpCount ?? 0) < MAX_OTP_RESEND_ALLOWED && ( + + {t("resend_otp")} + + )} +
+
+ ); +} + +type IChooseAbhaAddressProps = InjectedStepProps & { + onSuccess: (abhaNumber: AbhaNumberModel) => void; +}; + +function ChooseAbhaAddress({ + memory, + setMemory, + onSuccess, +}: IChooseAbhaAddressProps) { + const { t } = useTranslation(); + const [healthId, setHealthId] = useState(""); + const [suggestions, setSuggestions] = useState([]); + + useEffect(() => { + const fetchSuggestions = async () => { + const { res, data } = await request( + routes.abdm.healthId.abhaCreateAbhaAddressSuggestion, + { + body: { + transaction_id: memory?.transactionId, + }, + }, + ); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ ...prev, transactionId: data.transaction_id })); + setSuggestions(data.abha_addresses); + } + }; + + fetchSuggestions(); + }, [healthId, memory?.transactionId, setMemory]); + + const handleSubmit = async () => { + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request( + routes.abdm.healthId.abhaCreateEnrolAbhaAddress, + { + body: { + abha_address: healthId, + transaction_id: memory?.transactionId, + }, + }, + ); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ + ...prev, + transactionId: data.transaction_id, + abhaNumber: data.abha_number, + })); + Notify.Success({ + msg: data.detail ?? t("abha_address_created_success"), + }); + onSuccess(data.abha_number); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + return ( +
+ { + setHealthId(value); + }} + /> + +
+ {validateRule( + healthId.length >= 4, + t("abha_address_validation_length_error"), + )} + {validateRule( + isNaN(Number(healthId[0])) && healthId[0] !== ".", + t("abha_address_validation_start_error"), + )} + {validateRule( + healthId[healthId.length - 1] !== ".", + t("abha_address_validation_end_error"), + )} + {validateRule( + /^[0-9a-zA-Z._]+$/.test(healthId), + t("abha_address_validation_character_error"), + )} +
+ + {suggestions.length > 0 && ( +
+

+ {t("abha_address_suggestions")} +

+
+ {suggestions + .filter((suggestion) => suggestion !== healthId) + .map((suggestion) => ( +

setHealthId(suggestion)} + className="cursor-pointer rounded-md bg-primary-400 px-2.5 py-1 text-xs text-white" + > + {suggestion} +

+ ))} +
+
+ )} + +
+ + {t("create_abha_address")} + +
+
+ ); +} diff --git a/src/Components/ABDM/LinkAbhaNumber/LinkWithOtp.tsx b/src/Components/ABDM/LinkAbhaNumber/LinkWithOtp.tsx new file mode 100644 index 00000000000..5e71b1a19fc --- /dev/null +++ b/src/Components/ABDM/LinkAbhaNumber/LinkWithOtp.tsx @@ -0,0 +1,346 @@ +import { useTranslation } from "react-i18next"; +import { AbhaNumberModel } from "../types/abha"; +import useMultiStepForm, { InjectedStepProps } from "./useMultiStepForm"; +import { useMemo, useState } from "react"; +import TextFormField from "../../Form/FormFields/TextFormField"; +import { classNames } from "../../../Utils/utils"; +import ButtonV2, { ButtonWithTimer } from "../../Common/components/ButtonV2"; +import Dropdown, { DropdownItem } from "../../Common/components/Menu"; +import OtpFormField from "../../Form/FormFields/OtpFormField"; +import * as Notify from "../../../Utils/Notifications"; +import request from "../../../Utils/request/request"; +import routes from "../../../Redux/api"; +import CheckBoxFormField from "../../Form/FormFields/CheckBoxFormField"; + +const MAX_OTP_RESEND_ALLOWED = 2; + +type ILoginWithOtpProps = { + onSuccess: (abhaNumber: AbhaNumberModel) => void; +}; + +type Memory = { + id: string; + + isLoading: boolean; + validationError: string; + + transactionId: string; + type: "aadhaar" | "mobile" | "abha-number" | "abha-address"; + otp_system: "abdm" | "aadhaar"; + abhaNumber: AbhaNumberModel | null; + + resendOtpCount: number; +}; + +export default function LinkWithOtp({ onSuccess }: ILoginWithOtpProps) { + const { currentStep } = useMultiStepForm( + [ + , + , + ], + { + id: "", + isLoading: false, + validationError: "", + transactionId: "", + type: "aadhaar", + otp_system: "aadhaar", + abhaNumber: null, + resendOtpCount: 0, + }, + ); + + return
{currentStep}
; +} + +type IEnterIdProps = InjectedStepProps; + +const supportedAuthMethods = ["AADHAAR_OTP", "MOBILE_OTP"]; + +function EnterId({ memory, setMemory, next }: IEnterIdProps) { + const { t } = useTranslation(); + const [disclaimerAccepted, setDisclaimerAccepted] = useState([ + false, + false, + false, + ]); + const [authMethods, setAuthMethods] = useState([]); + + const valueType = useMemo(() => { + const id = memory?.id; + const isNumeric = !isNaN(Number(id?.trim())); + + if (isNumeric && (id?.length === 12 || id?.length === 16)) { + return "aadhaar"; + } else if (isNumeric && id?.length === 10) { + return "mobile"; + } else if (isNumeric && id?.length === 14) { + return "abha-number"; + } else { + return "abha-address"; + } + }, [memory?.id]); + + const handleGetAuthMethods = async () => { + setMemory((prev) => ({ ...prev, isLoading: true })); + + if (valueType === "aadhaar") { + setAuthMethods(["AADHAAR_OTP"]); + } else if (valueType === "mobile") { + setAuthMethods(["MOBILE_OTP"]); + } else { + const { res, data, error } = await request( + routes.abdm.healthId.abhaLoginCheckAuthMethods, + { + body: { + abha_address: memory?.id.replace(/-/g, "").replace(/ /g, ""), + }, + silent: true, + }, + ); + + if (res?.status === 200 && data) { + const methods = data.auth_methods.filter((method: string) => + supportedAuthMethods.find((supported) => supported === method), + ); + + if (methods.length === 0) { + Notify.Warn({ msg: t("get_auth_mode_error") }); + } + } else { + Notify.Error({ msg: error?.message ?? t("get_auth_mode_error") }); + } + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + const handleSendOtp = async (authMethod: string) => { + if (!supportedAuthMethods.includes(authMethod)) { + Notify.Warn({ msg: t("auth_method_unsupported") }); + return; + } + + const otp_system: "aadhaar" | "abdm" = + authMethod === "AADHAAR_OTP" ? "aadhaar" : "abdm"; + + setMemory((prev) => ({ + ...prev, + isLoading: true, + type: valueType, + otp_system, + })); + + const { res, data } = await request(routes.abdm.healthId.abhaLoginSendOtp, { + body: { + value: memory?.id, + type: valueType, + otp_system, + }, + }); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ + ...prev, + transactionId: data.transaction_id, + })); + Notify.Success({ msg: data.detail ?? t("send_otp_success") }); + next(); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + return ( +
+
+ { + setMemory((prev) => ({ ...prev, id: value })); + setAuthMethods([]); + }} + error={memory?.validationError} + /> + + {t("any_id_description")} + +
+ +
+ {disclaimerAccepted.map((isAccepted, i) => ( + { + setDisclaimerAccepted( + disclaimerAccepted.map((v, j) => (j === i ? e.value : v)), + ); + }} + className="mr-2 rounded border-gray-700" + labelClassName="text-xs text-gray-800" + errorClassName="hidden" + /> + ))} +
+ +
+ {authMethods.length === 0 ? ( + !v) || memory?.id.length === 0 + } + onClick={handleGetAuthMethods} + > + {t("get_auth_methods")} + + ) : ( + + {authMethods.map((method) => ( + handleSendOtp(method)}> + {t(`abha__auth_method__${method}`)} + + ))} + + )} +
+
+ ); +} + +type IVerifyIdProps = InjectedStepProps & { + onSuccess: (abhaNumber: AbhaNumberModel) => void; +}; + +function VerifyId({ memory, setMemory, onSuccess }: IVerifyIdProps) { + const { t } = useTranslation(); + const [otp, setOtp] = useState(""); + + const handleSubmit = async () => { + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request( + routes.abdm.healthId.abhaLoginVerifyOtp, + { + body: { + type: memory?.type, + transaction_id: memory?.transactionId, + otp, + otp_system: memory?.otp_system, + }, + }, + ); + + if (res?.status === 200 && data) { + Notify.Success({ msg: t("verify_otp_success") }); + onSuccess(data.abha_number); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + const handleResendOtp = async () => { + setMemory((prev) => ({ ...prev, isLoading: true })); + + const { res, data } = await request(routes.abdm.healthId.abhaLoginSendOtp, { + body: { + value: memory?.id, + type: memory?.type, + otp_system: memory?.otp_system, + }, + }); + + if (res?.status === 200 && data) { + setMemory((prev) => ({ + ...prev, + transactionId: data.transaction_id, + resendOtpCount: (prev.resendOtpCount ?? 0) + 1, + })); + Notify.Success({ msg: data.detail ?? t("send_otp_success") }); + } else { + setMemory((prev) => ({ + ...prev, + resendOtpCount: Infinity, + })); + Notify.Error({ msg: t("send_otp_error") }); + } + + setMemory((prev) => ({ ...prev, isLoading: false })); + }; + + return ( +
+
+ null} + /> + + {t("any_id_description")} + +
+ +
+ setOtp(value as string)} + value={otp} + label={t("enter_otp")} + disabled={memory?.isLoading} + /> +
+ +
+ + {t("verify_and_link")} + + + {(memory?.resendOtpCount ?? 0) < MAX_OTP_RESEND_ALLOWED && ( + + {t("resend_otp")} + + )} +
+
+ ); +} diff --git a/src/Components/ABDM/LinkAbhaNumber/LinkWithQr.tsx b/src/Components/ABDM/LinkAbhaNumber/LinkWithQr.tsx new file mode 100644 index 00000000000..7f9f9c7d853 --- /dev/null +++ b/src/Components/ABDM/LinkAbhaNumber/LinkWithQr.tsx @@ -0,0 +1,68 @@ +import { useTranslation } from "react-i18next"; +import { AbhaNumberModel, ABHAQRContent } from "../types/abha"; +import * as Notification from "../../../Utils/Notifications.js"; + +import { Scanner, IDetectedBarcode } from "@yudiel/react-qr-scanner"; +import request from "../../../Utils/request/request"; +import routes from "../../../Redux/api"; +import { useState } from "react"; + +type ILoginWithQrProps = { + onSuccess: (abhaNumber: AbhaNumberModel) => void; +}; + +export default function LinkWithQr({ onSuccess }: ILoginWithQrProps) { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + + return ( +
+ { + if (detectedCodes.length === 0) return; + + const scannedValue = detectedCodes[0].rawValue; + if (!scannedValue || isLoading) return; + + try { + const qrData = JSON.parse(scannedValue) as ABHAQRContent; + + setIsLoading(true); + const { res, data } = await request(routes.abdm.abhaNumber.create, { + body: { + abha_number: qrData.hidn, + health_id: qrData.hid || qrData.phr, + name: qrData.name, + gender: qrData.gender, + date_of_birth: qrData.dob, + address: qrData.address, + district: qrData.district_name || qrData["dist name"], + state: qrData.state_name || qrData["state name"], + mobile: qrData.mobile, + }, + }); + + if (res?.status === 201 && data) { + onSuccess(data); + } + setIsLoading(false); + } catch (e) { + Notification.Error({ + msg: t("abha__qr_scanning_error"), + }); + } + }} + onError={(e: unknown) => { + const errorMessage = e instanceof Error ? e.message : "Unknown error"; + Notification.Error({ + msg: errorMessage, + }); + }} + scanDelay={3000} + constraints={{ + facingMode: "environment", + }} + /> +
+ ); +} diff --git a/src/Components/ABDM/LinkAbhaNumber/index.tsx b/src/Components/ABDM/LinkAbhaNumber/index.tsx new file mode 100644 index 00000000000..f3ad9911197 --- /dev/null +++ b/src/Components/ABDM/LinkAbhaNumber/index.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import DialogModal from "../../Common/Dialog"; +import { AbhaNumberModel } from "../types/abha"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import { classNames } from "../../../Utils/utils"; +import CreateWithAadhaar from "./CreateWithAadhaar"; +import { useTranslation } from "react-i18next"; +import LinkWithOtp from "./LinkWithOtp"; +import LinkWithQr from "./LinkWithQr"; + +interface ILinkAbhaNumberProps { + show: boolean; + onClose: () => void; + onSuccess: (abhaNumber: AbhaNumberModel) => void; +} + +const ABHA_LINK_OPTIONS = { + create_with_aadhaar: { + title: "abha_link_options__create_with_aadhaar__title", + description: "abha_link_options__create_with_aadhaar__description", + disabled: false, + value: "create_with_aadhaar", + create: true, + }, + link_with_otp: { + title: "abha_link_options__link_with_otp__title", + description: "abha_link_options__link_with_otp__description", + disabled: false, + value: "link_with_otp", + create: false, + }, + create_with_driving_license: { + title: "abha_link_options__create_with_driving_license__title", + description: "abha_link_options__create_with_driving_license__description", + disabled: true, + value: "create_with_driving_license", + create: true, + }, + link_with_demographics: { + title: "abha_link_options__link_with_demographics__title", + description: "abha_link_options__link_with_demographics__description", + disabled: true, + value: "link_with_demographics", + create: false, + }, + link_with_qr: { + title: "abha_link_options__link_with_qr__title", + description: "abha_link_options__link_with_qr__description", + disabled: false, + value: "link_with_qr", + create: false, + }, +}; + +export default function LinkAbhaNumber({ + show, + onClose, + onSuccess, +}: ILinkAbhaNumberProps) { + const { t } = useTranslation(); + const [currentAbhaLinkOption, setCurrentAbhaLinkOption] = useState< + keyof typeof ABHA_LINK_OPTIONS + >("create_with_aadhaar"); + + return ( + + {currentAbhaLinkOption === "create_with_aadhaar" && ( + + )} + + {currentAbhaLinkOption === "link_with_otp" && ( + + )} + + {currentAbhaLinkOption === "link_with_qr" && ( + + )} + +
+

+ setCurrentAbhaLinkOption( + ABHA_LINK_OPTIONS[currentAbhaLinkOption].create + ? "link_with_otp" + : "create_with_aadhaar", + ) + } + className="cursor-pointer text-center text-sm text-blue-800" + > + {ABHA_LINK_OPTIONS[currentAbhaLinkOption].create + ? t("link_existing_abha_profile") + : t("create_new_abha_profile")} +

+
+ +
+

+ {t("try_different_abha_linking_option")} +

+
+ {Object.values(ABHA_LINK_OPTIONS) + .filter( + (option) => + option.value !== currentAbhaLinkOption && + ABHA_LINK_OPTIONS[currentAbhaLinkOption]?.create === + option.create, + ) + .sort((a) => (a.disabled ? 1 : -1)) + .map((option) => ( + + setCurrentAbhaLinkOption( + option.value as keyof typeof ABHA_LINK_OPTIONS, + ) + } + ghost + tooltip={ + option.disabled + ? t("abha_link_options__disabled_tooltip") + : t(option.description) + } + disabled={option.disabled} + tooltipClassName="top-full mt-1" + className={classNames( + "w-full border border-gray-400 text-secondary-800", + !option.disabled && "hover:border-primary-100", + )} + > + {t(option.title)} + + ))} +
+
+
+ ); +} diff --git a/src/Components/ABDM/LinkAbhaNumber/useMultiStepForm.ts b/src/Components/ABDM/LinkAbhaNumber/useMultiStepForm.ts new file mode 100644 index 00000000000..8107fff8370 --- /dev/null +++ b/src/Components/ABDM/LinkAbhaNumber/useMultiStepForm.ts @@ -0,0 +1,70 @@ +import { + cloneElement, + Dispatch, + ReactElement, + SetStateAction, + useCallback, + useMemo, + useState, +} from "react"; + +export interface InjectedStepProps { + currentStepIndex: number; + isFirstStep: boolean; + isLastStep: boolean; + next: () => void; + prev: () => void; + goTo: (step: number) => void; + memory: T | null; + setMemory: Dispatch>; +} + +export default function useMultiStepForm( + steps: ReactElement[], + initialValues?: T, +) { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [memory, setMemory] = useState(initialValues as T); + + const next = useCallback( + () => + setCurrentStepIndex((prev) => + steps.length - 1 > prev ? prev + 1 : prev, + ), + [steps.length], + ); + + const prev = useCallback( + () => setCurrentStepIndex((prev) => (prev > 0 ? prev - 1 : prev)), + [], + ); + + const goTo = useCallback( + (step: number) => + setCurrentStepIndex((prev) => + step >= 0 && step <= steps.length - 1 ? step : prev, + ), + [steps.length], + ); + + const options = useMemo( + () => ({ + currentStepIndex, + isFirstStep: currentStepIndex === 0, + isLastStep: currentStepIndex === steps.length - 1, + next, + prev, + goTo, + memory, + setMemory, + }), + [currentStepIndex, memory, next, prev, goTo, steps.length], + ); + + const currentStep = cloneElement(steps[currentStepIndex], { + ...options, + ...steps[currentStepIndex].props, + }); + + return { currentStep, ...options }; +} diff --git a/src/Components/ABDM/LinkCareContextModal.tsx b/src/Components/ABDM/LinkCareContextModal.tsx deleted file mode 100644 index 9ec9885bbca..00000000000 --- a/src/Components/ABDM/LinkCareContextModal.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as Notification from "../../Utils/Notifications.js"; - -import ButtonV2 from "../Common/components/ButtonV2"; -import DateFormField from "../Form/FormFields/DateFormField"; -import DialogModal from "../Common/Dialog"; -import TextFormField from "../Form/FormFields/TextFormField"; -import { useState } from "react"; -import routes from "../../Redux/api.js"; -import request from "../../Utils/request/request.js"; -import { AbhaNumberModel } from "./types/abha.js"; - -interface IProps { - consultationId: string; - abha?: AbhaNumberModel; - show: boolean; - onClose: () => void; -} - -const LinkCareContextModal = ({ - consultationId, - abha, - show, - onClose, -}: IProps) => { - const [acceptedDisclaimer, setAcceptedDisclaimer] = useState(false); - const [isLinkingCareContext, setIsLinkingCareContext] = useState(false); - - return ( - -
- null} - disabled - label="Name" - name="name" - error="" - /> - null} - disabled - label="Gender" - name="gender" - error="" - /> -
- null} - disabled - required - /> - -
- - { - setAcceptedDisclaimer(e.target.checked); - }} - className="mr-2 rounded border-secondary-700 shadow-sm ring-0 ring-offset-0" - /> - I declare that the data of the patient is voluntarily provided by the - patient (or guardian or nominee of the patient). - -
- -
- { - setIsLinkingCareContext(true); - const { res } = await request(routes.abha.linkCareContext, { - body: { - consultation: consultationId, - name: abha?.name, - gender: abha?.gender, - dob: abha?.date_of_birth, - }, - reattempts: 0, - }); - if (res?.status === 202) { - Notification.Success({ - msg: "Care Context sucessfully linked!", - }); - } - setIsLinkingCareContext(false); - onClose(); - }} - disabled={!acceptedDisclaimer} - loading={isLinkingCareContext} - > - Link Care Context - -
-
- ); -}; - -export default LinkCareContextModal; diff --git a/src/Components/ABDM/models.ts b/src/Components/ABDM/models.ts deleted file mode 100644 index 899ec948777..00000000000 --- a/src/Components/ABDM/models.ts +++ /dev/null @@ -1,138 +0,0 @@ -export interface ICreateHealthIdRequest { - healthId?: string; - // email: string; - // firstName: string; - // middleName: string; - // lastName: string; - // password: string; - // profilePhoto: string; - txnId: string; - patientId?: string; -} - -export interface ICreateHealthIdResponse { - email?: string; - firstName: string; - lastName: string; - healthId?: string; - healthIdNumber: string; -} - -export interface IHealthFacility { - id: string; - registered: boolean; - external_id: string; - created_date: string; - modified_date: string; - hf_id: string; - facility: string; - detail?: string; -} - -export interface ILinkABHANumber { - abha_profile: { - abha_number: string; - health_id: string; - date_of_birth: string; - }; -} - -export interface IConfirmMobileOtp { - otp: string; - txnId: string; - patientId?: string; - message?: string; -} - -export interface IHealthId { - authMethods?: string[]; -} - -export interface ABDMError { - code: string; - details?: { - code: string; - message: string; - }[]; - message: string; -} - -export interface IAadhaarOtp { - txnId: string; -} - -export interface ICheckAndGenerateMobileOtp { - mobileLinked: boolean; - txnId: string; -} - -export interface IAadhaarOtpTBody { - aadhaar?: string; - txnId?: string; -} - -export interface IVerifyAadhaarOtpTBody { - consultation?: string; - name?: string; - gender?: "M" | "F" | "O"; - dob?: string; - otp?: string; - txnId?: string; -} - -export interface IGenerateMobileOtpTBody { - mobile: string; - txnId: string; -} - -export interface ISearchByHealthIdTBody { - healthId: string; -} - -export interface IinitiateAbdmAuthenticationTBody { - authMethod: string; - healthid: string; -} - -export interface IgetAbhaCardTBody { - patient: string; - type: "pdf" | "png"; -} - -export interface IcreateHealthFacilityTBody { - facility: string; - hf_id: string; -} - -export interface IpartialUpdateHealthFacilityTBody { - hf_id: string; -} - -export interface ILinkViaQRBody { - hidn: string; - phr: string; - name: string; - gender: "M" | "F" | "O"; - dob: string; - address?: string; - "dist name"?: string; - "state name"?: string; - patientId?: string; -} - -export interface ABHAQRContent { - address: string; - distlgd: string; - district_name?: string; - state_name?: string; - dob: string; - gender: "M" | "F" | "O"; - hid?: string; - phr?: string; - "dist name"?: string; - hidn: string; - mobile: string; - name: string; - "state name"?: string; - statelgd: string; -} diff --git a/src/Components/ABDM/types/abha.ts b/src/Components/ABDM/types/abha.ts index fd03b30cdc4..d94549e2242 100644 --- a/src/Components/ABDM/types/abha.ts +++ b/src/Components/ABDM/types/abha.ts @@ -17,9 +17,29 @@ export type AbhaNumberModel = { district: string | null; state: string | null; pincode: string | null; + mobile: string | null; email: string | null; profile_photo: string | null; new: boolean; patient: string | null; patient_object: PatientModel | null; }; + +export type ABHAQRContent = { + hidn: string; + name: string; + gender: "M" | "F" | "O"; + dob: string; + mobile: string; + address: string; + distlgd: string; + statelgd: string; +} & ({ hid: string; phr?: never } | { phr: string; hid?: never }) & + ( + | { district_name: string; "dist name"?: never } + | { "dist name": string; district_name?: never } + ) & + ( + | { state_name: string; "state name"?: never } + | { "state name": string; state_name?: never } + ); diff --git a/src/Components/ABDM/types/health-facility.ts b/src/Components/ABDM/types/health-facility.ts new file mode 100644 index 00000000000..419003dbf8e --- /dev/null +++ b/src/Components/ABDM/types/health-facility.ts @@ -0,0 +1,19 @@ +export interface IHealthFacility { + id: string; + registered: boolean; + external_id: string; + created_date: string; + modified_date: string; + hf_id: string; + facility: string; + detail?: string; +} + +export interface IcreateHealthFacilityTBody { + facility: string; + hf_id: string; +} + +export interface IpartialUpdateHealthFacilityTBody { + hf_id: string; +} diff --git a/src/Components/Common/components/ButtonV2.tsx b/src/Components/Common/components/ButtonV2.tsx index bc67e150fdc..b7eab5f29d7 100644 --- a/src/Components/Common/components/ButtonV2.tsx +++ b/src/Components/Common/components/ButtonV2.tsx @@ -4,6 +4,8 @@ import CareIcon from "../../../CAREUI/icons/CareIcon"; import { Link } from "raviger"; import { classNames } from "../../../Utils/utils"; import { useTranslation } from "react-i18next"; +import { useEffect, useState } from "react"; +import Spinner from "../Spinner"; export type ButtonSize = "small" | "default" | "large"; export type ButtonShape = "square" | "circle"; @@ -22,36 +24,40 @@ export type RawButtonProps = Omit< "style" >; +export type ButtonStyleProps = { + /** + * - `"small"` has small text and minimal padding. + * - `"default"` has small text with normal padding. + * - `"large"` has base text size with large padding. + */ + size?: ButtonSize; + /** + * - `"square"` gives a button with minimally rounded corners. + * - `"circle"` gives a button with fully rounded corners. Ideal when only + * icons are present. + */ + circle?: boolean | undefined; + /** + * - `"primary"` is ideal for form submissions, etc. + * - `"secondary"` is ideal for things that have secondary importance. + * - `"danger"` is ideal for destructive or dangerous actions, such as delete. + * - `"warning"` is ideal for actions that require caution such as archive. + * - `"alert"` is ideal for actions that require alert. + */ + variant?: ButtonVariant; + /** If set, gives an elevated button with hover effects. */ + shadow?: boolean | undefined; + /** If set, removes the background to give a simple text button. */ + ghost?: boolean | undefined; + /** + * If set, applies border to the button. + */ + border?: boolean | undefined; +}; + export type ButtonProps = RawButtonProps & - AuthorizedElementProps & { - /** - * - `"small"` has small text and minimal padding. - * - `"default"` has small text with normal padding. - * - `"large"` has base text size with large padding. - */ - size?: ButtonSize; - /** - * - `"square"` gives a button with minimally rounded corners. - * - `"circle"` gives a button with fully rounded corners. Ideal when only - * icons are present. - */ - circle?: boolean | undefined; - /** - * - `"primary"` is ideal for form submissions, etc. - * - `"secondary"` is ideal for things that have secondary importance. - * - `"danger"` is ideal for destructive or dangerous actions, such as delete. - * - `"warning"` is ideal for actions that require caution such as archive. - * - `"alert"` is ideal for actions that require alert. - */ - variant?: ButtonVariant; - /** If set, gives an elevated button with hover effects. */ - shadow?: boolean | undefined; - /** If set, removes the background to give a simple text button. */ - ghost?: boolean | undefined; - /** - * If set, applies border to the button. - */ - border?: boolean | undefined; + AuthorizedElementProps & + ButtonStyleProps & { /** * Whether the button is disabled or not. * This is overriden to `true` if `loading` is `true`. @@ -83,10 +89,28 @@ export type ButtonProps = RawButtonProps & tooltipClassName?: string; }; -const ButtonV2 = ({ - authorizeFor, +export const buttonStyles = ({ size = "default", + circle = false, variant = "primary", + ghost = false, + border = false, + shadow = !ghost, +}: ButtonStyleProps) => { + return classNames( + "inline-flex h-min cursor-pointer items-center justify-center gap-2 whitespace-pre font-medium outline-offset-1 transition-all duration-200 ease-in-out disabled:cursor-not-allowed disabled:bg-secondary-200 disabled:text-secondary-500", + `button-size-${size}`, + `button-shape-${circle ? "circle" : "square"}`, + ghost ? `button-${variant}-ghost` : `button-${variant}-default`, + border && `button-${variant}-border`, + shadow && "shadow enabled:hover:shadow-md", + ); +}; + +const ButtonV2 = ({ + authorizeFor, + size, + variant, circle, shadow, ghost, @@ -103,14 +127,9 @@ const ButtonV2 = ({ shadow ??= !ghost; const className = classNames( - props.className, - "inline-flex h-min cursor-pointer items-center justify-center gap-2 whitespace-pre font-medium outline-offset-1 transition-all duration-200 ease-in-out disabled:cursor-not-allowed disabled:bg-secondary-200 disabled:text-secondary-500", - `button-size-${size}`, - `button-shape-${circle ? "circle" : "square"}`, - ghost ? `button-${variant}-ghost` : `button-${variant}-default`, - border && `button-${variant}-border`, - shadow && "shadow enabled:hover:shadow-md", + buttonStyles({ size, circle, variant, ghost, border, shadow }), tooltip && "tooltip", + props.className, ); if (tooltip) { @@ -202,3 +221,53 @@ export const Cancel = ({ label = "Cancel", ...props }: CommonButtonProps) => { /> ); }; + +export type ButtonWithTimerProps = CommonButtonProps & { + initialInverval?: number; + interval?: number; +}; + +export const ButtonWithTimer = ({ + initialInverval, + interval = 60, + ...buttonProps +}: ButtonWithTimerProps) => { + const [seconds, setSeconds] = useState(initialInverval ?? interval); + const [isButtonDisabled, setIsButtonDisabled] = useState(true); + + useEffect(() => { + let interval = undefined; + if (seconds > 0) { + interval = setInterval(() => { + setSeconds((prevSeconds) => prevSeconds - 1); + }, 1000); + } else { + setIsButtonDisabled(false); + clearInterval(interval); + } + return () => clearInterval(interval); + }, [seconds]); + + return ( +
+ { + await buttonProps.onClick?.(e); + setSeconds(interval); + setIsButtonDisabled(true); + }} + > + {!!(seconds && isButtonDisabled) && ( +
+ + {seconds} +
+ )} + + {buttonProps.children ?? buttonProps.label} +
+
+ ); +}; diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 3b34689221d..639c6dd9e40 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -168,7 +168,7 @@ export const ConsultationDetails = (props: any) => { // Get abha number data const { data: abhaNumberData } = await request( - routes.abha.getAbhaNumber, + routes.abdm.abhaNumber.get, { pathParams: { abhaNumberId: id ?? "" }, silent: true, diff --git a/src/Components/Form/FormFields/TextFormField.tsx b/src/Components/Form/FormFields/TextFormField.tsx index 12d6623cd15..a3663358a56 100644 --- a/src/Components/Form/FormFields/TextFormField.tsx +++ b/src/Components/Form/FormFields/TextFormField.tsx @@ -1,32 +1,30 @@ import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils"; -import { HTMLInputTypeAttribute, forwardRef, useState } from "react"; +import { + DetailedHTMLProps, + InputHTMLAttributes, + forwardRef, + useState, +} from "react"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import FormField from "./FormField"; import { classNames } from "../../../Utils/utils"; -export type TextFormFieldProps = FormFieldBaseProps & { - placeholder?: string; - value?: string | number; - autoComplete?: string; - type?: HTMLInputTypeAttribute; - className?: string | undefined; - inputClassName?: string | undefined; - removeDefaultClasses?: true | undefined; - leading?: React.ReactNode | undefined; - trailing?: React.ReactNode | undefined; - leadingFocused?: React.ReactNode | undefined; - trailingFocused?: React.ReactNode | undefined; - trailingPadding?: string | undefined; - leadingPadding?: string | undefined; - min?: string | number; - max?: string | number; - step?: string | number; - onKeyDown?: (event: React.KeyboardEvent) => void; - onFocus?: (event: React.FocusEvent) => void; - onBlur?: (event: React.FocusEvent) => void; - suggestions?: string[]; -}; +export type TextFormFieldProps = FormFieldBaseProps & + Omit< + DetailedHTMLProps, HTMLInputElement>, + "onChange" + > & { + inputClassName?: string | undefined; + removeDefaultClasses?: true | undefined; + leading?: React.ReactNode | undefined; + trailing?: React.ReactNode | undefined; + leadingFocused?: React.ReactNode | undefined; + trailingFocused?: React.ReactNode | undefined; + trailingPadding?: string | undefined; + leadingPadding?: string | undefined; + suggestions?: string[]; + }; const TextFormField = forwardRef((props: TextFormFieldProps, ref) => { const field = useFormFieldPropsResolver(props); @@ -44,7 +42,8 @@ const TextFormField = forwardRef((props: TextFormFieldProps, ref) => { let child = ( } id={field.id} className={classNames( "cui-input-base peer", @@ -55,18 +54,10 @@ const TextFormField = forwardRef((props: TextFormFieldProps, ref) => { )} disabled={field.disabled} type={props.type === "password" ? getPasswordFieldType() : props.type} - placeholder={props.placeholder} name={field.name} value={field.value} - min={props.min} - max={props.max} - autoComplete={props.autoComplete} required={field.required} - onFocus={props.onFocus} - onBlur={props.onBlur} onChange={(e) => field.handleChange(e.target.value)} - onKeyDown={props.onKeyDown} - step={props.step} /> ); diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 7d49ffdaad1..758d948864d 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -52,6 +52,7 @@ import { import { ICD11DiagnosisModel } from "../Diagnosis/types.js"; import { getDiagnosesByIds } from "../Diagnosis/utils.js"; import Tabs from "../Common/components/Tabs.js"; +import { isPatientMandatoryDataFilled } from "./Utils.js"; import request from "../../Utils/request/request.js"; import { Avatar } from "../Common/Avatar.js"; @@ -473,7 +474,9 @@ export const PatientManager = () => { if (data?.count) { patientList = data.results.map((patient) => { let patientUrl = ""; - if ( + if (!isPatientMandatoryDataFilled(patient)) { + patientUrl = `/facility/${patient.facility}/patient/${patient.id}`; + } else if ( patient.last_consultation && patient.last_consultation?.facility === patient.facility && !(patient.last_consultation?.discharge_date && patient.is_active) @@ -592,10 +595,26 @@ export const PatientManager = () => { )}
- {!patient.last_consultation || - patient.last_consultation?.facility !== patient.facility || - (patient.last_consultation?.discharge_date && - patient.is_active) ? ( + {!isPatientMandatoryDataFilled(patient) && ( + + + + + + + + )} + + {isPatientMandatoryDataFilled(patient) && + (!patient.last_consultation || + patient.last_consultation?.facility !== patient.facility || + (patient.last_consultation?.discharge_date && + patient.is_active)) ? ( { return OCCUPATION_TYPES.find((i) => i.value === occupation)?.text; }; @@ -341,6 +342,40 @@ export const PatientHome = (props: any) => { )} + + {isPatientMandatoryDataFilled(patientData) && + (patientData?.facility != patientData?.last_consultation?.facility || + (patientData.is_active && + patientData?.last_consultation?.discharge_date)) && ( +
+
+
+

+ + + {t("consultation_missing_warning")}{" "} + + {patientData.facility_object?.name || "-"}{" "} + + +

+
+
+
+ + {t("create_consultation")} + +
+
+ )}
diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 1f581b9b0e7..7e3a9563799 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -22,8 +22,6 @@ import { humanizeStrings, } from "../../Utils/utils.js"; import ABHAProfileModal from "../ABDM/ABHAProfileModal.js"; -import LinkABHANumberModal from "../ABDM/LinkABHANumberModal.js"; -import LinkCareContextModal from "../ABDM/LinkCareContextModal.js"; import DialogModal from "../Common/Dialog.js"; import ButtonV2 from "../Common/components/ButtonV2.js"; import Beds from "../Facility/Consultations/Beds.js"; @@ -43,6 +41,7 @@ import FetchRecordsModal from "../ABDM/FetchRecordsModal.js"; import { AbhaNumberModel } from "../ABDM/types/abha.js"; import { SkillModel } from "../Users/models.js"; import { AuthorizedForConsultationRelatedActions } from "../../CAREUI/misc/AuthorizedChild.js"; +import LinkAbhaNumber from "../ABDM/LinkAbhaNumber"; import careConfig from "@careConfig"; const formatSkills = (arr: SkillModel[]) => { @@ -75,7 +74,6 @@ export default function PatientInfoCard(props: { const [openDischargeSummaryDialog, setOpenDischargeSummaryDialog] = useState(false); const [openDischargeDialog, setOpenDischargeDialog] = useState(false); - const [showLinkCareContext, setShowLinkCareContext] = useState(false); const patient = props.patient; const consultation = props.consultation; @@ -748,7 +746,7 @@ export default function PatientInfoCard(props: { close(); setShowABHAProfile(true); triggerGoal("Patient Card Button Clicked", { - buttonName: "Show ABHA Profile", + buttonName: t("show_abha_profile"), consultationId: consultation?.id, userId: authUser?.id, }); @@ -758,25 +756,7 @@ export default function PatientInfoCard(props: { icon="l-user-square" className="text-lg text-primary-500" /> - Show ABHA Profile -
-
{ - triggerGoal("Patient Card Button Clicked", { - buttonName: "Link Care Context", - consultationId: consultation?.id, - userId: authUser?.id, - }); - close(); - setShowLinkCareContext(true); - }} - > - - Link Care Context + {t("show_abha_profile")}
- Fetch Records over ABDM + {t("hi__fetch_records")}
)} @@ -815,7 +795,7 @@ export default function PatientInfoCard(props: { icon="l-link" className="text-lg text-primary-500" /> -

Link ABHA Number

+

{t("link_abha_profile")}

)} @@ -957,12 +937,33 @@ export default function PatientInfoCard(props: {
- setShowLinkABHANumber(false)} - patientId={patient.id as any} - onSuccess={(_) => { - window.location.href += "?show-abha-profile=true"; + onSuccess={async (abhaProfile) => { + const { res, data } = await request( + routes.abdm.healthId.linkAbhaNumberAndPatient, + { + body: { + patient: patient.id, + abha_number: abhaProfile.external_id, + }, + }, + ); + + if (res?.status === 200 && data) { + Notification.Success({ + msg: t("abha_number_linked_successfully"), + }); + + props.fetchPatientData?.({ aborted: false }); + setShowLinkABHANumber(false); + setShowABHAProfile(true); + } else { + Notification.Error({ + msg: t("failed_to_link_abha_number"), + }); + } }} /> setShowABHAProfile(false)} /> - setShowLinkCareContext(false)} - /> { pathParams: { id: id ? id : 0 }, }); const { data: abhaNumberData } = await request( - routes.abha.getAbhaNumber, + routes.abdm.abhaNumber.get, { pathParams: { abhaNumberId: id ?? "" }, silent: true, @@ -589,7 +591,6 @@ export const PatientRegister = (props: PatientRegisterProps) => { } }); const data = { - abha_number: state.form.abha_number, phone_number: parsePhoneNumber(formData.phone_number), emergency_phone_number: parsePhoneNumber(formData.emergency_phone_number), date_of_birth: @@ -681,12 +682,15 @@ export const PatientRegister = (props: PatientRegisterProps) => { }); if (res?.ok && requestData) { if (state.form.abha_number) { - const { res, data } = await request(routes.abha.linkPatient, { - body: { - patient: requestData.id, - abha_number: state.form.abha_number, + const { res, data } = await request( + routes.abdm.healthId.linkAbhaNumberAndPatient, + { + body: { + patient: requestData.id, + abha_number: state.form.abha_number, + }, }, - }); + ); if (res?.status === 200 && data) { Notification.Success({ @@ -738,64 +742,64 @@ export const PatientRegister = (props: PatientRegisterProps) => { setIsLoading(false); }; - const handleAbhaLinking = ( - { - id, - abha_profile: { - healthIdNumber, - healthId, - name, - mobile, - gender, - monthOfBirth, - dayOfBirth, - yearOfBirth, - pincode, - }, - }: any, - field: any, + const populateAbhaValues = ( + abhaProfile: AbhaNumberModel, + field: FormContextValue, ) => { - const values: any = {}; - if (id) values["abha_number"] = id; - if (healthIdNumber) values["health_id_number"] = healthIdNumber; - if (healthId) values["health_id"] = healthId; + const values = { + abha_number: abhaProfile.external_id, + health_id_number: abhaProfile.abha_number, + health_id: abhaProfile.health_id, + }; - if (name) + if (abhaProfile.name) field("name").onChange({ name: "name", - value: name, + value: abhaProfile.name, }); - if (mobile) { + if (abhaProfile.mobile) { field("phone_number").onChange({ name: "phone_number", - value: parsePhoneNumber(mobile, "IN"), + value: parsePhoneNumber(abhaProfile.mobile, "IN"), }); field("emergency_phone_number").onChange({ name: "emergency_phone_number", - value: parsePhoneNumber(mobile, "IN"), + value: parsePhoneNumber(abhaProfile.mobile, "IN"), }); } - if (gender) + if (abhaProfile.gender) field("gender").onChange({ name: "gender", - value: gender === "M" ? "1" : gender === "F" ? "2" : "3", + value: { M: "1", F: "2", O: "3" }[abhaProfile.gender], }); - if (monthOfBirth && dayOfBirth && yearOfBirth) + if (abhaProfile.date_of_birth) field("date_of_birth").onChange({ name: "date_of_birth", - value: new Date(`${monthOfBirth}-${dayOfBirth}-${yearOfBirth}`), + value: new Date(abhaProfile.date_of_birth), }); - if (pincode) + if (abhaProfile.pincode) field("pincode").onChange({ name: "pincode", - value: pincode, + value: abhaProfile.pincode, }); + if (abhaProfile.address) { + field("address").onChange({ + name: "address", + value: abhaProfile.address, + }); + + field("permanent_address").onChange({ + name: "permanent_address", + value: abhaProfile.address, + }); + } + dispatch({ type: "set_form", form: { ...state.form, ...values } }); setShowLinkAbhaNumberModal(false); }; @@ -1035,16 +1039,17 @@ export const PatientRegister = (props: PatientRegisterProps) => { {careConfig.abdm.enabled && (
{showLinkAbhaNumberModal && ( - setShowLinkAbhaNumberModal(false)} - onSuccess={(data: any) => { + onSuccess={(data) => { if (id) { - navigate(`/facility/${facilityId}/patient/${id}`); - return; + Notification.Warn({ + msg: "To link Abha Number, please save the patient details", + }); } - handleAbhaLinking(data, field); + populateAbhaValues(data, field); }} /> )} diff --git a/src/Components/Patient/Utils.ts b/src/Components/Patient/Utils.ts new file mode 100644 index 00000000000..96e05e63232 --- /dev/null +++ b/src/Components/Patient/Utils.ts @@ -0,0 +1,19 @@ +import { PatientModel } from "./models"; + +export function isPatientMandatoryDataFilled(patient: PatientModel) { + return ( + patient.phone_number && + patient.emergency_phone_number && + patient.name && + patient.gender && + (patient.date_of_birth || patient.year_of_birth) && + patient.address && + patient.permanent_address && + patient.pincode && + patient.state && + patient.district && + patient.local_body && + ("medical_history" in patient ? patient.medical_history : true) && + patient.blood_group + ); +} diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index d9e275fdc4a..9843b867a5b 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -134,6 +134,7 @@ export interface PatientModel { created_by?: PerformedByModel; assigned_to?: { first_name?: string; username?: string; last_name?: string }; assigned_to_object?: AssignedToObjectModel; + occupation?: Occupation; meta_info?: PatientMeta; age?: string; } diff --git a/src/Locale/en.json b/src/Locale/en.json index 08f9ded2130..e516ec61733 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -213,7 +213,43 @@ "VENTILATOR_MODE__VCV": "Volume Control Ventilation (VCV)", "VENTILATOR_MODE__VC_SIMV": "Volume Controlled SIMV (VC-SIMV)", "View Facility": "View Facility", - "abha_number_linked_successfully": "ABHA number linked successfully", + "aadhaar_number": "Aadhaar Number", + "aadhaar_number_will_not_be_stored": "Aadhaar number will not be stored by CARE", + "aadhaar_otp_send_error": "Failed to send OTP. Please try again later.", + "aadhaar_otp_send_success": "OTP has been sent to the mobile number registered with the Aadhar number.", + "aadhaar_validation_length_error": "Should be a 12-digit aadhaar number or 16-digit virtual ID", + "aadhaar_validation_space_error": "Aadhaar number should not contain spaces", + "abha__auth_method__AADHAAR_OTP": "Aadhaar OTP", + "abha__auth_method__MOBILE_OTP": "Mobile OTP", + "abha__disclaimer_1": "I am voluntarily sharing my Aadhaar Number / Virtual ID issued by the Unique Identification Authority of India (\"UIDAI\"), and my demographic information for the purpose of creating an Ayushman Bharat Health Account number (\"ABHA number\") and Ayushman Bharat Health Account address (\"ABHA Address\"). I authorize NHA to use my Aadhaar number / Virtual ID for performing Aadhaar based authentication with UIDAI as per the provisions of the Aadhaar (Targeted Delivery of Financial and other Subsidies, Benefits and Services) Act, 2016 for the aforesaid purpose. I understand that UIDAI will share my e-KYC details, or response of \"Yes\" with NHA upon successful authentication.", + "abha__disclaimer_2": "I consent to usage of my ABHA address and ABHA number for linking of my legacy (past) health records and those which will be generated during this encounter.", + "abha__disclaimer_3": "I authorize the sharing of all my health records with healthcare provider(s) for the purpose of providing healthcare services to me during this encounter.", + "abha__disclaimer_4": "I consent to the anonymization and subsequent use of my health records for public health purposes.", + "abha__qr_scanning_error": "Error scanning QR code, Invalid QR code", + "abha_address": "ABHA Address", + "abha_address_created_error": "Failed to create Abha Address. Please try again later.", + "abha_address_created_success": "Abha Address has been created successfully.", + "abha_address_suggestions": "Here are some suggestions:", + "abha_address_validation_character_error": "Should only contain letters, numbers, underscore(_) or dot(.)", + "abha_address_validation_end_error": "Shouldn't end with a dot (.)", + "abha_address_validation_length_error": "Should be atleast 4 character long", + "abha_address_validation_start_error": "Shouldn't start with a number or dot (.)", + "abha_details": "ABHA Details", + "abha_link_options__create_with_aadhaar__description": "Create New ABHA Number Using Aadhaar Number", + "abha_link_options__create_with_aadhaar__title": "Create with Aadhaar", + "abha_link_options__create_with_driving_license__description": "Create New ABHA Number Using Driving License", + "abha_link_options__create_with_driving_license__title": "Create with Driving License", + "abha_link_options__disabled_tooltip": "We are currently working on this feature", + "abha_link_options__link_with_demographics__description": "Link Existing ABHA Number Using Demographic details", + "abha_link_options__link_with_demographics__title": "Link with Demographics", + "abha_link_options__link_with_otp__description": "Link Existing ABHA Number Using Mobile or Aadhaar OTP", + "abha_link_options__link_with_otp__title": "Link with OTP", + "abha_link_options__link_with_qr__title": "Link with ABHA QR", + "abha_number": "ABHA Number", + "abha_number_exists": "ABHA Number already exists", + "abha_number_exists_description": "There is an ABHA Number already linked with the given Aadhaar Number, Do you want to create a new ABHA Address?", + "abha_number_linked_successfully": "ABHA Number has been linked successfully.", + "abha_profile": "ABHA Profile", "access_level": "Access Level", "action_irreversible": "This action is irreversible", "active": "Active", @@ -253,6 +289,8 @@ "ambulance_number": "Ambulance No", "ambulance_phone_number": "Phone number of Ambulance", "antenatal": "Antenatal", + "any_id": "Enter any ID linked with your ABHA number", + "any_id_description": "Currently we support: Aadhaar Number / Mobile Number", "any_other_comments": "Any other comments", "apply": "Apply", "approved_by_district_covid_control_room": "Approved by District COVID Control Room", @@ -272,6 +310,7 @@ "assets": "Assets", "assigned_facility": "Facility assigned", "assigned_to": "Assigned to", + "async_operation_warning": "This operation may take some time. Please check back later.", "audio__allow_permission": "Please allow microphone permission in site settings", "audio__allow_permission_button": "Click here to know how to allow", "audio__allow_permission_helper": "You might have denied microphone access in the past.", @@ -284,6 +323,7 @@ "audio__start_again": "Start Again", "audit_log": "Audit Log", "auth_login_title": "Authorized Login", + "auth_method_unsupported": "This authentication method is not supported, please try a different method", "authorize_shift_delete": "Authorize shift delete", "auto_generated_for_care": "Auto Generated for Care", "available_features": "Available Features", @@ -333,6 +373,8 @@ "check_for_available_update": "Check for available update", "check_for_update": "Check for Update", "check_policy_eligibility": "Check Policy Eligibility", + "check_status": "Check Status", + "checking_consent_status": "Consent request status is being checked!", "checking_eligibility": "Checking Eligibility", "checking_for_update": "Checking for update", "checking_policy_eligibility": "Checking Policy Eligibility", @@ -390,6 +432,39 @@ "confirm_password": "Confirm Password", "confirm_transfer_complete": "Confirm Transfer Complete!", "confirmed": "Confirmed", + "consent__hi_range": "Health Information Range", + "consent__hi_type__DiagnosticReport": "Diagnostic Report", + "consent__hi_type__DischargeSummary": "Discharge Summary", + "consent__hi_type__HealthDocumentRecord": "Health Document Record", + "consent__hi_type__ImmunizationRecord": "Immunization Record", + "consent__hi_type__OPConsultation": "OP Consultation", + "consent__hi_type__Prescription": "Prescription", + "consent__hi_type__WellnessRecord": "Wellness Record", + "consent__hi_types": "HI Profiles", + "consent__patient": "Patient", + "consent__purpose": "Purpose", + "consent__purpose__BTG": "Break The Glass", + "consent__purpose__CAREMGT": "Care Management", + "consent__purpose__DSRCH": "Disease Specific Healthcare Research", + "consent__purpose__HPAYMT": "Healthcare Payment", + "consent__purpose__PATRQT": "Self Requested", + "consent__purpose__PUBHLTH": "Public Health", + "consent__status": "Status", + "consent__status__DENIED": "Denied", + "consent__status__EXPIRED": "Expired", + "consent__status__GRANTED": "Granted", + "consent__status__REQUESTED": "Requested", + "consent__status__REVOKED": "Revoked", + "consent_request__date_range": "Health Records Date Range", + "consent_request__expiry": "Consent Expiry Date", + "consent_request__hi_types": "Health Information Types", + "consent_request__hi_types_placeholder": "Select One or More HI Types", + "consent_request__patient_identifier": "Patient Identifier", + "consent_request__purpose": "Purpose of Request", + "consent_request_rejected": "Patient has rejected the consent request", + "consent_request_waiting_approval": "Waiting for the Patient to approve the consent request", + "consent_requested_successfully": "Consent requested successfully!", + "consultation_missing_warning": "You have not created a consultation for the patient in", "consultation_not_filed": "You have not filed a consultation for this patient yet.", "consultation_not_filed_description": "Please file a consultation for this patient to continue.", "consultation_notes": "General Instructions (Advice)", @@ -408,10 +483,13 @@ "covid_19_cat_gov": "Covid_19 Clinical Category as per Govt. of Kerala guideline (A/B/C)", "covid_19_death_reporting_form_1": "Covid-19 Death Reporting : Form 1", "create": "Create", + "create_abha_address": "Create ABHA Address", "create_add_more": "Create & Add More", "create_asset": "Create Asset", "create_consultation": "Create Consultation", "create_facility": "Create a new facility", + "create_new_abha_address": "Create New ABHA Address", + "create_new_abha_profile": "Don't have an ABHA Number", "create_new_asset": "Create New Asset", "create_position_preset": "Create a new position preset", "create_position_preset_description": "Creates a new position preset in Care from the current position of the camera for the given name", @@ -440,6 +518,7 @@ "delete_item": "Delete {{name}}", "delete_record": "Delete Record", "deleted_successfully": "{{name}} deleted successfully", + "denied_on": "Denied On", "describe_why_the_asset_is_not_working": "Describe why the asset is not working", "description": "Description", "details_about_the_equipment": "Details about the equipment", @@ -483,6 +562,7 @@ "download_discharge_summary": "Download discharge summary", "download_type": "Download Type", "downloading": "Downloading", + "downloading_abha_card": "Generating ABHA Card, Please hold on", "downloads": "Downloads", "drag_drop_image_to_upload": "Drag & drop image to upload", "duplicate_patient_record_birth_unknown": "Please contact your district care coordinator, the shifting facility or the patient themselves if you are not sure about the patient's year of birth.", @@ -524,8 +604,15 @@ "encounter_suggestion__OP": "Out-patient visit", "encounter_suggestion__R": "Consultation", "encounter_suggestion_edit_disallowed": "Not allowed to switch to this option in edit consultation", + "enter_aadhaar_number": "Enter a 12-digit Aadhaar ID", + "enter_aadhaar_otp": "Enter OTP sent to the registered mobile with Aadhaar", + "enter_abha_address": "Enter ABHA Address", + "enter_any_id": "Enter any ID linked with your ABHA number", "enter_file_name": "Enter File Name", "enter_message": "Start typing...", + "enter_mobile_number": "Enter Mobile Number", + "enter_mobile_otp": "Enter OTP sent to the given mobile number", + "enter_otp": "Enter OTP sent to the registered mobile with the respective ID", "enter_valid_age": "Please Enter Valid Age", "entered-in-error": "Entered in error", "error_404": "Error 404", @@ -535,13 +622,16 @@ "estimated_contact_date": "Estimated contact date", "expand_sidebar": "Expand Sidebar", "expected_burn_rate": "Expected Burn Rate", + "expired_on": "Expired On", + "expires_on": "Expires On", "facilities": "Facilities", "facility": "Facility", + "facility_consent_requests_page_title": "Patient Consent List", "facility_name": "Facility Name", "facility_preference": "Facility preference", "facility_search_placeholder": "Search by Facility / District Name", "facility_type": "Facility Type", - "failed_to_link_abha_number": "Failed to link ABHA number", + "failed_to_link_abha_number": "Failed to link ABHA Number. Please try again later.", "features": "Features", "feed_configurations": "Feed Configurations", "feed_is_currently_not_live": "Feed is currently not live", @@ -573,25 +663,53 @@ "forget_password": "Forgot password?", "forget_password_instruction": "Enter your username, and if it exists, we will send you a link to reset your password.", "frequency": "Frequency", + "full_name": "Full Name", "full_screen": "Full Screen", "gender": "Gender", "generate_report": "Generate Report", "generated_summary_caution": "This is a computer generated summary using the information captured in the CARE system.", "generating": "Generating", "generating_discharge_summary": "Generating discharge summary", + "get_auth_methods": "Get Available Authentication Methods", + "get_auth_mode_error": "Could not find any supported authentication methods, Please try again with a different authentication method", "get_tests": "Get Tests", "goal": "Our goal is to continuously improve the quality and accessibility of public healthcare services using digital tools.", + "granted_on": "Granted On", "has_domestic_healthcare_support": "Has domestic healthcare support?", + "health_facility__config_registration_error": "Health ID registration failed", + "health_facility__config_update_error": "Health Facility config update failed", + "health_facility__config_update_success": "Health Facility config updated successfully", + "health_facility__hf_id": "Health Facility Id", + "health_facility__link": "Link Health Facility", + "health_facility__not_registered_1.1": "The ABDM health facility is successfully linked with care", + "health_facility__not_registered_1.2": "but not registered as a service in bridge", + "health_facility__not_registered_2": "Click on Link Health Facility to register the service", + "health_facility__not_registered_3": "Not Registered", + "health_facility__registered_1.1": "The ABDM health facility is successfully linked with care", + "health_facility__registered_1.2": "and registered as a service in bridge", + "health_facility__registered_2": "No Action Required", + "health_facility__registered_3": "Registered", + "health_facility__validation__hf_id_required": "Health Facility Id is required", "help_confirmed": "There is sufficient diagnostic and/or clinical evidence to treat this as a confirmed condition.", "help_differential": "One of a set of potential (and typically mutually exclusive) diagnoses asserted to further guide the diagnostic process and preliminary treatment.", "help_entered-in-error": "The statement was entered in error and is not valid.", "help_provisional": "This is a tentative diagnosis - still a candidate that is under consideration.", "help_refuted": "This condition has been ruled out by subsequent diagnostic and clinical evidence.", "help_unconfirmed": "There is not sufficient diagnostic and/or clinical evidence to treat this as a confirmed condition.", + "hi__fetch_records": "Fetch Records over ABDM", + "hi__page_title": "Health Information", + "hi__record_archived__title": "This record has been archived", + "hi__record_archived_description": "This record has been archived and is no longer available for viewing.", + "hi__record_archived_on": " This record was archived on", + "hi__record_not_fetched_description": "This record hasn't been fetched yet. Please check back after some time.", + "hi__record_not_fetched_title": "This record hasn't been fetched yet", + "hi__waiting_for_record": "Waiting for the Host HIP to send the record.", "hide": "Hide", "home_facility": "Home Facility", "hubs": "Hub Facilities", + "i_declare": "I hereby declare that:", "icd11_as_recommended": "As per ICD-11 recommended by WHO", + "incomplete_patient_details_warning": "Patient details are incomplete. Please update the details before proceeding.", "inconsistent_dosage_units_error": "Dosage units must be same", "indian_mobile": "Indian Mobile", "indicator": "Indicator", @@ -644,7 +762,10 @@ "latitude_invalid": "Latitude must be between -90 and 90", "left": "Left", "length": "Length ({{unit}})", + "link_abha_number": "Link ABHA Number", + "link_abha_profile": "Link ABHA Profile", "link_camera_and_bed": "Link bed to Camera", + "link_existing_abha_profile": "Already have an ABHA number", "linked_facilities": "Linked Facilities", "linked_skills": "Linked Skills", "liquid_oxygen_capacity": "Liquid Oxygen Capacity", @@ -694,9 +815,16 @@ "min_time_bw_doses": "Min. time b/w doses", "mobile": "Mobile", "mobile_number": "Mobile Number", + "mobile_number_different_from_aadhaar_mobile_number": "We have noticed that you have entered a mobile number that is different from the one linked to your Aadhaar. We will send an OTP to this number to link it with your Abha number.", + "mobile_number_validation_error": "Enter a valid mobile number", + "mobile_otp_send_error": "Failed to send OTP. Please try again later.", + "mobile_otp_send_success": "OTP has been sent to the given mobile number.", + "mobile_otp_verify_error": "Failed to verify mobile number. Please try again later.", + "mobile_otp_verify_success": "Mobile number has been verified successfully.", "modification_caution_note": "No modifications possible once added", "modified": "Modified", "modified_date": "Modified Date", + "modified_on": "Modified On", "monitor": "Monitor", "more_info": "More Info", "move_to_onvif_preset": "Move to an ONVIF Preset", @@ -730,6 +858,7 @@ "no_policy_added": "No Insurance Policy Added", "no_policy_found": "No Insurance Policy Found for this Patient", "no_presets": "No Presets", + "no_records_found": "No records found", "no_remarks": "No remarks", "no_results_found": "No Results Found", "no_staff": "No staff found", @@ -756,6 +885,7 @@ "occupation": "Occupation", "on": "On", "ongoing_medications": "Ongoing Medications", + "only_indian_mobile_numbers_supported": "Currently only Indian numbers are supported", "open": "Open", "open_camera": "Open Camera", "open_live_monitoring": "Open Live Monitoring", @@ -764,6 +894,8 @@ "ordering": "Ordering", "origin_facility": "Current facility", "other_details": "Other details", + "otp_verification_error": "Failed to verify OTP. Please try again later.", + "otp_verification_success": "OTP has been verified successfully.", "out_of_range_error": "Value must be between {{ start }} and {{ end }}.", "oxygen_information": "Oxygen Information", "page_not_found": "Page Not Found", @@ -791,6 +923,7 @@ "patient_consultation__treatment__summary__temperature": "Temperature", "patient_created": "Patient Created", "patient_details": "Patient Details", + "patient_details_incomplete": "Patient Details Incomplete", "patient_face": "Patient Face", "patient_name": "Patient name", "patient_no": "OP/IP No", @@ -864,6 +997,7 @@ "profile": "Profile", "provisional": "Provisional", "qualification": "Qualification", + "raise_consent_request": "Raise a consent request to fetch patient records over ABDM", "ration_card__APL": "APL", "ration_card__BPL": "BPL", "ration_card__NO_CARD": "Non-card holder", @@ -882,6 +1016,7 @@ "redirected_to_create_consultation": "Note: You will be redirected to create consultation form. Please complete the form to finish the transfer process", "referral_letter": "Referral Letter", "referred_to": "Referred to", + "refresh": "Refresh", "refresh_list": "Refresh List", "refuted": "Refuted", "register_hospital": "Register Hospital", @@ -894,12 +1029,14 @@ "req_atleast_one_lowercase": "Require at least one lower case letter", "req_atleast_one_symbol": "Require at least one symbol", "req_atleast_one_uppercase": "Require at least one upper case", + "request_consent": "Request Consent", "request_description": "Description of Request", "request_description_placeholder": "Type your description here", "request_title": "Request Title", "request_title_placeholder": "Type your title here", "required": "Required", "required_quantity": "Required Quantity", + "resend_otp": "Resend OTP", "reset": "Reset", "reset_password": "Reset Password", "resource": "Resource", @@ -915,6 +1052,7 @@ "return_to_login": "Return to Login", "return_to_password_reset": "Return to Password Reset", "return_to_patient_dashboard": "Return to Patient Dashboard", + "revoked_on": "Revoked On", "right": "Right", "route": "Route", "routine": "Routine", @@ -934,6 +1072,7 @@ "search_resource": "Search Resource", "see_attachments": "See Attachments", "select": "Select", + "select_all": "Select All", "select_date": "Select date", "select_eligible_policy": "Select an Eligible Insurance Policy", "select_facility_for_discharged_patients_warning": "Facility needs to be selected to view discharged patients.", @@ -949,6 +1088,9 @@ "select_wards": "Select wards", "send_email": "Send Email", "send_message": "Send Message", + "send_otp": "Send OTP", + "send_otp_error": "Failed to send OTP. Please try again later.", + "send_otp_success": "OTP has been sent to the respective mobile number", "send_reset_link": "Send Reset Link", "serial_number": "Serial Number", "serviced_on": "Serviced on", @@ -966,6 +1108,7 @@ "shifting_deleted": "Shifting record has been deleted successfully.", "shifting_details": "Shifting details", "shifting_status": "Shifting status", + "show_abha_profile": "Show ABHA Profile", "show_all": "Show all", "show_all_notifications": "Show All", "show_default_presets": "Show Default Presets", @@ -1017,6 +1160,7 @@ "treatment_summary__heading": "INTERIM TREATMENT SUMMARY", "treatment_summary__print": "Print Treatment Summary", "try_again_later": "Try again later!", + "try_different_abha_linking_option": "Want to try a different linking option, here are some more:", "type_any_extra_comments_here": "type any extra comments here", "type_b_cylinders": "B Type Cylinders", "type_c_cylinders": "C Type Cylinders", @@ -1045,12 +1189,14 @@ "update_facility": "Update Facility", "update_facility_middleware_success": "Facility middleware updated successfully", "update_log": "Update Log", + "update_patient_details": "Update Patient Details", "update_preset_position_to_current": "Update preset's position to camera's current position", "update_record": "Update Record", "update_record_for_asset": "Update record for asset", "update_shift_request": "Update Shift Request", "update_status_details": "Update Status/Details", "updated": "Updated", + "updated_on": "Updated On", "updating": "Updating", "upload": "Upload", "upload_an_image": "Upload an image", @@ -1059,12 +1205,19 @@ "upload_headings__sample_report": "Upload Sample Report", "upload_headings__supporting_info": "Upload Supporting Info", "uploading": "Uploading", + "use_existing_abha_address": "Use Existing ABHA Address", "user_deleted_successfuly": "User Deleted Successfuly", "user_management": "User Management", "username": "Username", "users": "Users", "vehicle_preference": "Vehicle preference", "vendor_name": "Vendor Name", + "verify_and_link": "Verify and Link", + "verify_otp": "Verify OTP", + "verify_otp_error": "Failed to verify OTP. Please try again later.", + "verify_otp_success": "OTP has been verified successfully.", + "verify_patient_identifier": "Please verify the patient identifier", + "verify_using": "Verify Using", "video_conference_link": "Video Conference Link", "view": "View", "view_abdm_records": "View ABDM Records", diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index ea728d7f53c..d562908a140 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -50,25 +50,6 @@ import { SampleReportModel, SampleTestModel, } from "../Components/Patient/models"; -import { - IAadhaarOtp, - IAadhaarOtpTBody, - ICheckAndGenerateMobileOtp, - IConfirmMobileOtp, - ICreateHealthIdRequest, - ICreateHealthIdResponse, - IGenerateMobileOtpTBody, - IHealthFacility, - IHealthId, - ILinkABHANumber, - ILinkViaQRBody, - ISearchByHealthIdTBody, - IVerifyAadhaarOtpTBody, - IcreateHealthFacilityTBody, - IgetAbhaCardTBody, - IinitiateAbdmAuthenticationTBody, - IpartialUpdateHealthFacilityTBody, -} from "../Components/ABDM/models"; import { IComment, IResource } from "../Components/Resource/models"; import { IDeleteBedCapacity, @@ -117,8 +98,13 @@ import { import { InvestigationSessionType } from "../Components/Facility/Investigations/investigationsTab"; import { AbhaNumberModel } from "../Components/ABDM/types/abha"; import { ScribeModel } from "../Components/Scribe/Scribe"; -import { InsurerOptionModel } from "../Components/HCX/InsurerAutocomplete"; -import { PMJAYPackageItem } from "../Components/Common/PMJAYProcedurePackageAutocomplete"; +import { + IcreateHealthFacilityTBody, + IHealthFacility, + IpartialUpdateHealthFacilityTBody, +} from "../Components/ABDM/types/health-facility"; +import { PMJAYPackageItem } from "@/Components/Common/PMJAYProcedurePackageAutocomplete"; +import { InsurerOptionModel } from "@/Components/HCX/InsurerAutocomplete"; /** * A fake function that returns an empty object casted to type T @@ -1361,196 +1347,243 @@ const routes = { TBody: Type(), }, - abha: { - getAbhaNumber: { - path: "/api/v1/abdm/abha_numbers/{abhaNumberId}/", - method: "GET", - TRes: Type(), - }, - - // ABDM HealthID endpoints - generateAadhaarOtp: { - path: "/api/v1/abdm/healthid/generate_aadhaar_otp/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, - - resendAadhaarOtp: { - path: "/api/v1/abdm/healthid/resend_aadhaar_otp/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, - - verifyAadhaarOtp: { - path: "/api/v1/abdm/healthid/verify_aadhaar_otp/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, - - generateMobileOtp: { - path: "/api/v1/abdm/healthid/generate_mobile_otp/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, - - checkAndGenerateMobileOtp: { - path: "/api/v1/abdm/healthid/check_and_generate_mobile_otp/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + abdm: { + consent: { + list: { + path: "/api/abdm/consent/", + method: "GET", + TRes: Type>(), + }, - // TODO: resend mobile otp - verifyMobileOtp: { - path: "/api/v1/abdm/healthid/verify_mobile_otp/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + create: { + path: "/api/abdm/consent/", + method: "POST", + TRes: Type(), + TBody: Type(), + }, - createHealthId: { - path: "/api/v1/abdm/healthid/create_health_id/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + get: { + path: "/api/abdm/consent/{id}/", + method: "GET", + }, - linkPatient: { - path: "/api/v1/abdm/healthid/link_patient/", - method: "POST", - TBody: Type<{ abha_number: string; patient: string }>(), - TRes: Type(), + checkStatus: { + path: "/api/abdm/v3/hiu/consent_request_status/", + method: "POST", + TBody: Type<{ + consent_request: string; + }>(), + TRes: Type<{ + detail: string; + }>(), + }, }, - searchByHealthId: { - path: "/api/v1/abdm/healthid/search_by_health_id/", - method: "POST", - TRes: Type(), - TBody: Type(), + healthInformation: { + get: { + path: "/api/abdm/health_information/{artefactId}", + method: "GET", + TRes: Type(), + }, }, - initiateAbdmAuthentication: { - path: "/api/v1/abdm/healthid/auth_init/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + healthFacility: { + list: { + path: "/api/abdm/health_facility/", + method: "GET", + }, - confirmWithAadhaarOtp: { - path: "/api/v1/abdm/healthid/confirm_with_aadhaar_otp/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + create: { + path: "/api/abdm/health_facility/", + method: "POST", + TRes: Type(), + TBody: Type(), + }, - confirmWithMobileOtp: { - path: "/api/v1/abdm/healthid/confirm_with_mobile_otp/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + get: { + path: "/api/abdm/health_facility/{facility_id}/", + method: "GET", + TRes: Type(), + }, - linkViaQR: { - path: "/api/v1/abdm/healthid/link_via_qr/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + update: { + path: "/api/abdm/health_facility/{facility_id}/", + method: "PUT", + TRes: Type(), + TBody: Type(), + }, - linkCareContext: { - path: "/api/v1/abdm/healthid/add_care_context/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + partialUpdate: { + path: "/api/abdm/health_facility/{facility_id}/", + method: "PATCH", + TRes: Type(), + TBody: Type(), + }, - getAbhaCard: { - path: "/api/v1/abdm/healthid/get_abha_card/", - method: "POST", - TRes: Type(), - TBody: Type(), + registerAsService: { + path: "/api/abdm/health_facility/{facility_id}/register_service/", + method: "POST", + TRes: Type(), + TBody: Type(), + }, }, - // ABDM Health Facility - - listHealthFacility: { - path: "/api/v1/abdm/health_facility/", - method: "GET", + abhaNumber: { + get: { + path: "/api/abdm/abha_number/{abhaNumberId}/", + method: "GET", + TRes: Type(), + }, + create: { + path: "/api/abdm/abha_number/", + method: "POST", + TBody: Type>(), + TRes: Type(), + }, }, - createHealthFacility: { - path: "/api/v1/abdm/health_facility/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + healthId: { + abhaCreateSendAadhaarOtp: { + path: "/api/abdm/v3/health_id/create/send_aadhaar_otp/", + method: "POST", + TBody: Type<{ + aadhaar: string; + transaction_id?: string; + }>(), + TRes: Type<{ + transaction_id: string; + detail: string; + }>(), + }, - getHealthFacility: { - path: "/api/v1/abdm/health_facility/{facility_id}/", - method: "GET", - TRes: Type(), - }, + abhaCreateVerifyAadhaarOtp: { + path: "/api/abdm/v3/health_id/create/verify_aadhaar_otp/", + method: "POST", + TBody: Type<{ + transaction_id: string; + otp: string; + mobile: string; + }>(), + TRes: Type<{ + transaction_id: string; + detail: string; + is_new: boolean; + abha_number: AbhaNumberModel; + }>(), + }, - updateHealthFacility: { - path: "/api/v1/abdm/health_facility/{facility_id}/", - method: "PUT", - TRes: Type(), - TBody: Type(), - }, + abhaCreateLinkMobileNumber: { + path: "/api/abdm/v3/health_id/create/link_mobile_number/", + method: "POST", + TBody: Type<{ + transaction_id: string; + mobile: string; + }>(), + TRes: Type<{ + transaction_id: string; + detail: string; + }>(), + }, - partialUpdateHealthFacility: { - path: "/api/v1/abdm/health_facility/{facility_id}/", - method: "PATCH", - TRes: Type(), - TBody: Type(), - }, + abhaCreateVerifyMobileNumber: { + path: "/api/abdm/v3/health_id/create/verify_mobile_otp/", + method: "POST", + TBody: Type<{ + transaction_id: string; + otp: string; + }>(), + TRes: Type<{ + transaction_id: string; + detail: string; + }>(), + }, - registerHealthFacilityAsService: { - path: "/api/v1/abdm/health_facility/{facility_id}/register_service/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + abhaCreateAbhaAddressSuggestion: { + path: "/api/abdm/v3/health_id/create/abha_address_suggestion/", + method: "POST", + TBody: Type<{ + transaction_id: string; + }>(), + TRes: Type<{ + transaction_id: string; + abha_addresses: string[]; + }>(), + }, - listConsents: { - path: "/api/v1/abdm/consent/", - method: "GET", - TRes: Type>(), - }, + abhaCreateEnrolAbhaAddress: { + path: "/api/abdm/v3/health_id/create/enrol_abha_address/", + method: "POST", + TBody: Type<{ + transaction_id: string; + abha_address: string; + }>(), + TRes: Type<{ + detail?: string; + transaction_id: string; + health_id: string; + preferred_abha_address: string; + abha_number: AbhaNumberModel; + }>(), + }, - createConsent: { - path: "/api/v1/abdm/consent/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, + linkAbhaNumberAndPatient: { + path: "/api/abdm/v3/health_id/link_patient/", + method: "POST", + TBody: Type<{ + abha_number: string; + patient: string; + }>(), + TRes: Type<{ + detail: string; + }>(), + }, - getConsent: { - path: "/api/v1/abdm/consent/{id}/", - method: "GET", - }, + abhaLoginCheckAuthMethods: { + path: "/api/abdm/v3/health_id/login/check_auth_methods/", + method: "POST", + TBody: Type<{ + abha_address: string; + }>(), + TRes: Type<{ + abha_number: string; + auth_methods: string[]; + }>(), + }, - checkConsentStatus: { - path: "/api/v1/abdm/consent/{id}/status/", - method: "GET", - TRes: Type(), - }, + abhaLoginSendOtp: { + path: "/api/abdm/v3/health_id/login/send_otp/", + method: "POST", + TBody: Type<{ + type: "abha-number" | "abha-address" | "mobile" | "aadhaar"; + value: string; + otp_system: "abdm" | "aadhaar"; + }>(), + TRes: Type<{ + transaction_id: string; + detail: string; + }>(), + }, - getHealthInformation: { - path: "/api/v1/abdm/health_information/{artefactId}", - method: "GET", - TRes: Type(), - }, + abhaLoginVerifyOtp: { + path: "/api/abdm/v3/health_id/login/verify_otp/", + method: "POST", + TBody: Type<{ + type: "abha-number" | "abha-address" | "mobile" | "aadhaar"; + otp: string; + transaction_id: string; + otp_system: "abdm" | "aadhaar"; + }>(), + TRes: Type<{ + abha_number: AbhaNumberModel; + created: boolean; + }>(), + }, - findPatient: { - path: "/api/v1/abdm/patients/find/", - method: "POST", - TRes: Type(), - TBody: Type<{ id: string }>(), + getAbhaCard: { + path: "/api/abdm/v3/health_id/abha_card", + method: "GET", + TRes: Type(), + }, }, }, diff --git a/src/Utils/request/request.ts b/src/Utils/request/request.ts index 151cc30c460..3fa648316af 100644 --- a/src/Utils/request/request.ts +++ b/src/Utils/request/request.ts @@ -85,6 +85,11 @@ async function getResponseBody(res: Response): Promise { } const isJson = res.headers.get("content-type")?.includes("application/json"); + const isImage = res.headers.get("content-type")?.includes("image"); + + if (isImage) { + return (await res.blob()) as TData; + } if (!isJson) { return (await res.text()) as TData;