diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4fe94183a2f..40e5259b1c6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,7 +26,7 @@ "cpus": 4 }, "waitFor": "onCreateCommand", - "postCreateCommand": "npm install", + "postCreateCommand": "npm run install-all", "postAttachCommand": { "server": "npm run dev" }, diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 500dbd92be0..0f588568bbc 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -70,7 +70,7 @@ jobs: node-version: "20" - name: Install dependencies 📦 - run: npm install + run: npm run install-all - name: Build ⚙️ run: npm run build diff --git a/Dockerfile b/Dockerfile index 5061a977585..3a96ab3c28a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,8 @@ RUN npm install COPY . . +RUN npm run setup + RUN npm run build 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/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts index d6991e82ff0..f0b2f1b74eb 100644 --- a/cypress/pageobject/Asset/AssetCreation.ts +++ b/cypress/pageobject/Asset/AssetCreation.ts @@ -154,7 +154,7 @@ export class AssetPage { } clickConfigureAsset() { - cy.get("#submit").contains("Set Configuration").click(); + cy.get("#submit").contains("Update").click(); } clickConfigureVital() { diff --git a/cypress/pageobject/Facility/FacilityHome.ts b/cypress/pageobject/Facility/FacilityHome.ts index e021171ff0a..dea7de0e7b6 100644 --- a/cypress/pageobject/Facility/FacilityHome.ts +++ b/cypress/pageobject/Facility/FacilityHome.ts @@ -72,7 +72,7 @@ class FacilityHome { } verifyOccupancyBadgeVisibility() { - cy.get("#occupany-badge").should("be.visible"); + cy.get('[data-test-id="occupancy-badge"]').should("be.visible"); } verifyAndCloseNotifyModal() { 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/scripts/sort-locales.js b/scripts/sort-locales.js index cf365a41d09..adff586a93b 100644 --- a/scripts/sort-locales.js +++ b/scripts/sort-locales.js @@ -1,26 +1,15 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const fs = require("fs"); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const path = require("path"); - -const directory = "src/Locale"; -fs.readdir(directory, (err, files) => { - if (err) throw err; +const file = "src/Locale/en.json"; - files.forEach((file) => { - if (file.endsWith(".json")) { - const filePath = path.join(directory, file); - const data = JSON.parse(fs.readFileSync(filePath, "utf8")); +const data = JSON.parse(fs.readFileSync(file, "utf8")); - const sortedData = Object.keys(data) - .sort() - .reduce((acc, key) => { - acc[key] = data[key]; - return acc; - }, {}); +const sortedData = Object.keys(data) + .sort() + .reduce((acc, key) => { + acc[key] = data[key]; + return acc; + }, {}); - fs.writeFileSync(filePath, JSON.stringify(sortedData, null, 2) + "\n"); - } - }); -}); +fs.writeFileSync(file, JSON.stringify(sortedData, null, 2) + "\n"); diff --git a/src/CAREUI/display/Count.tsx b/src/CAREUI/display/Count.tsx index 997b58794eb..6b28ca4f962 100644 --- a/src/CAREUI/display/Count.tsx +++ b/src/CAREUI/display/Count.tsx @@ -11,15 +11,13 @@ interface Props { export default function CountBlock(props: Props) { return ( -
-
-
+
+
+
-
+
{props.text}
{props.loading ? ( diff --git a/src/CAREUI/interactive/KeyboardShortcut.tsx b/src/CAREUI/interactive/KeyboardShortcut.tsx index 1d2bebeb316..4ad46c99387 100644 --- a/src/CAREUI/interactive/KeyboardShortcut.tsx +++ b/src/CAREUI/interactive/KeyboardShortcut.tsx @@ -31,7 +31,10 @@ export default function KeyboardShortcut(props: Props) { )} {(props.altShortcuts || [props.shortcut]).map((shortcut, idx, arr) => ( <> - + {shortcut.map((key, idx, keys) => ( <> {SHORTCUT_KEY_MAP[key] || key} @@ -42,7 +45,12 @@ export default function KeyboardShortcut(props: Props) { ))} {idx !== arr.length - 1 && ( - or + + or + )} ))} diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index d46473bfd2d..c5e221fb277 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -839,20 +839,6 @@ export const LOCATION_BED_TYPES = [ { id: "REGULAR", name: "Regular" }, ] as const; -export const ASSET_META_TYPE = [ - { id: "CAMERA", text: "Camera(ONVIF)" }, - { id: "HL7MONITOR", text: "Vitals Monitor(HL7)" }, -]; - -export const CAMERA_TYPE = [ - { id: "HIKVISION", text: "ONVIF Camera (HIKVISION)" }, -]; - -export const GENDER: { [key: number]: string } = GENDER_TYPES.reduce( - (acc, curr) => ({ ...acc, [curr.id]: curr.text }), - {}, -); - export type CameraPTZ = { icon?: IconName; label: string; @@ -1158,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/Assets/AssetConfigure.tsx b/src/Components/Assets/AssetConfigure.tsx index aa6b7c9221e..9f5f73f296d 100644 --- a/src/Components/Assets/AssetConfigure.tsx +++ b/src/Components/Assets/AssetConfigure.tsx @@ -1,6 +1,6 @@ import Loading from "../Common/Loading"; import HL7Monitor from "./AssetType/HL7Monitor"; -import ONVIFCamera from "./AssetType/ONVIFCamera"; +import ConfigureCamera from "../CameraFeed/ConfigureCamera"; import Page from "../Common/components/Page"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; @@ -11,13 +11,11 @@ interface AssetConfigureProps { } const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => { - const { - data: asset, - loading, - refetch, - } = useQuery(routes.getAsset, { pathParams: { external_id: assetId } }); + const { data: asset, refetch } = useQuery(routes.getAsset, { + pathParams: { external_id: assetId }, + }); - if (loading || !asset) { + if (!asset) { return ; } @@ -63,12 +61,7 @@ const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => { }} backUrl={`/facility/${facilityId}/assets/${assetId}`} > - refetch()} - /> + refetch()} /> ); }; diff --git a/src/Components/Assets/AssetType/HL7Monitor.tsx b/src/Components/Assets/AssetType/HL7Monitor.tsx index 6583157018d..383af38fa28 100644 --- a/src/Components/Assets/AssetType/HL7Monitor.tsx +++ b/src/Components/Assets/AssetType/HL7Monitor.tsx @@ -1,7 +1,6 @@ import { SyntheticEvent, useEffect, useState } from "react"; -import { AssetData, ResolvedMiddleware } from "../AssetTypes"; +import { AssetClass, AssetData, ResolvedMiddleware } from "../AssetTypes"; import * as Notification from "../../../Utils/Notifications.js"; -import MonitorConfigure from "../configure/MonitorConfigure"; import Loading from "../../Common/Loading"; import { checkIfValidIP } from "../../../Common/validation"; import Card from "../../../CAREUI/display/Card"; @@ -13,6 +12,10 @@ import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatien import useAuthUser from "../../../Common/hooks/useAuthUser"; import request from "../../../Utils/request/request"; import routes from "../../../Redux/api"; +import { BedModel } from "../../Facility/models"; +import useQuery from "../../../Utils/request/useQuery"; +import { FieldLabel } from "../../Form/FormFields/FormField"; +import { BedSelect } from "../../Common/BedSelect"; interface HL7MonitorProps { assetId: string; @@ -151,3 +154,79 @@ const HL7Monitor = (props: HL7MonitorProps) => { ); }; export default HL7Monitor; + +const saveLink = async (assetId: string, bedId: string) => { + await request(routes.createAssetBed, { + body: { + asset: assetId, + bed: bedId, + }, + }); + Notification.Success({ msg: "AssetBed Link created successfully" }); +}; +const updateLink = async ( + assetbedId: string, + assetId: string, + bed: BedModel, +) => { + await request(routes.partialUpdateAssetBed, { + pathParams: { external_id: assetbedId }, + body: { + asset: assetId, + bed: bed.id ?? "", + }, + }); + Notification.Success({ msg: "AssetBed Link updated successfully" }); +}; + +function MonitorConfigure({ asset }: { asset: AssetData }) { + const [bed, setBed] = useState({}); + const [shouldUpdateLink, setShouldUpdateLink] = useState(false); + const { data: assetBed } = useQuery(routes.listAssetBeds, { + query: { asset: asset.id }, + onResponse: ({ res, data }) => { + if (res?.status === 200 && data && data.results.length > 0) { + setBed(data.results[0].bed_object); + setShouldUpdateLink(true); + } + }, + }); + + return ( +
{ + e.preventDefault(); + if (shouldUpdateLink) { + updateLink( + assetBed?.results[0].id as string, + asset.id as string, + bed as BedModel, + ); + } else { + saveLink(asset.id as string, bed?.id as string); + } + }} + > +
+
+ Bed + setBed(selected as BedModel)} + selected={bed} + error="" + multiple={false} + location={asset?.location_object?.id} + facility={asset?.location_object?.facility?.id} + not_occupied_by_asset_type={AssetClass.HL7MONITOR} + className="w-full" + /> +
+ + + {shouldUpdateLink ? "Update Bed" : "Save Bed"} + +
+
+ ); +} diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx deleted file mode 100644 index f79894d089f..00000000000 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { useEffect, useState } from "react"; -import { AssetData, ResolvedMiddleware } from "../AssetTypes"; -import * as Notification from "../../../Utils/Notifications.js"; -import { BedModel } from "../../Facility/models"; -import { getCameraConfig } from "../../../Utils/transformUtils"; -import CameraConfigure from "../configure/CameraConfigure"; -import Loading from "../../Common/Loading"; -import { checkIfValidIP } from "../../../Common/validation"; -import TextFormField from "../../Form/FormFields/TextFormField"; -import { Submit } from "../../Common/components/ButtonV2"; -import { SyntheticEvent } from "react"; -import useAuthUser from "../../../Common/hooks/useAuthUser"; - -import request from "../../../Utils/request/request"; -import routes from "../../../Redux/api"; -import useQuery from "../../../Utils/request/useQuery"; - -import CareIcon from "../../../CAREUI/icons/CareIcon"; -import useOperateCamera, { - PTZPayload, -} from "../../CameraFeed/useOperateCamera"; - -interface Props { - assetId: string; - facilityId: string; - asset: any; - onUpdated?: () => void; -} - -const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { - const [isLoading, setIsLoading] = useState(true); - const [assetType, setAssetType] = useState(""); - const [middlewareHostname, setMiddlewareHostname] = useState(""); - const [resolvedMiddleware, setResolvedMiddleware] = - useState(); - const [cameraAddress, setCameraAddress] = useState(""); - const [ipadrdress_error, setIpAddress_error] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [streamUuid, setStreamUuid] = useState(""); - const [bed, setBed] = useState({}); - const [newPreset, setNewPreset] = useState(""); - const [loadingAddPreset, setLoadingAddPreset] = useState(false); - const [loadingSetConfiguration, setLoadingSetConfiguration] = useState(false); - const [refreshPresetsHash, setRefreshPresetsHash] = useState( - Number(new Date()), - ); - const { data: facility, loading } = useQuery(routes.getPermittedFacility, { - pathParams: { id: facilityId }, - }); - const authUser = useAuthUser(); - - const { operate } = useOperateCamera(assetId ?? "", true); - - useEffect(() => { - if (asset) { - setAssetType(asset?.asset_class); - setResolvedMiddleware(asset?.resolved_middleware); - const cameraConfig = getCameraConfig(asset); - setMiddlewareHostname(cameraConfig.middleware_hostname); - setCameraAddress(cameraConfig.hostname); - setUsername(cameraConfig.username); - setPassword(cameraConfig.password); - setStreamUuid(cameraConfig.accessKey); - } - setIsLoading(false); - }, [asset]); - - const handleSubmit = async (e: SyntheticEvent) => { - e.preventDefault(); - if (checkIfValidIP(cameraAddress)) { - setLoadingSetConfiguration(true); - setIpAddress_error(""); - const data = { - meta: { - asset_type: "CAMERA", - middleware_hostname: middlewareHostname, - local_ip_address: cameraAddress, - camera_access_key: `${username}:${password}:${streamUuid}`, - }, - }; - const { res } = await request(routes.partialUpdateAsset, { - pathParams: { external_id: assetId }, - body: data, - }); - if (res?.status === 200) { - Notification.Success({ msg: "Asset Configured Successfully" }); - onUpdated?.(); - } else { - Notification.Error({ msg: "Something went wrong!" }); - } - setLoadingSetConfiguration(false); - } else { - setIpAddress_error("IP address is invalid"); - } - }; - - const addPreset = async (e: SyntheticEvent) => { - e.preventDefault(); - const meta = { - bed_id: bed.id, - preset_name: newPreset, - }; - try { - setLoadingAddPreset(true); - - const { data } = await operate({ type: "get_status" }); - const { position } = (data as { result: { position: PTZPayload } }) - .result; - - const { res } = await request(routes.createAssetBed, { - body: { - meta: { ...meta, position }, - asset: assetId, - bed: bed?.id as string, - }, - }); - if (res?.status === 201) { - Notification.Success({ - msg: "Preset Added Successfully", - }); - setBed({}); - setNewPreset(""); - setRefreshPresetsHash(Number(new Date())); - } else { - Notification.Error({ - msg: "Something went wrong..!", - }); - } - } catch (e) { - Notification.Error({ - msg: "Something went wrong..!", - }); - } - setLoadingAddPreset(false); - }; - if (isLoading || loading || !facility) return ; - - return ( -
- {["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && ( -
-
- -

Middleware Hostname

- {resolvedMiddleware?.source != "asset" && ( -
- - - Middleware hostname sourced from asset{" "} - {resolvedMiddleware?.source} - -
- )} -
- } - placeholder={resolvedMiddleware?.hostname} - value={middlewareHostname} - onChange={({ value }) => setMiddlewareHostname(value)} - /> - setCameraAddress(value)} - error={ipadrdress_error} - /> - setUsername(value)} - /> - setPassword(value)} - /> - setStreamUuid(value)} - /> -
-
- -
- - )} - - {assetType === "ONVIF" ? ( - - ) : null} -
- ); -}; -export default ONVIFCamera; diff --git a/src/Components/Assets/AssetTypes.tsx b/src/Components/Assets/AssetTypes.tsx index e3dee31bb7f..2f8e086a813 100644 --- a/src/Components/Assets/AssetTypes.tsx +++ b/src/Components/Assets/AssetTypes.tsx @@ -109,6 +109,9 @@ export interface AssetData { latest_status: string; last_service: AssetService; meta?: { + middleware_hostname?: string; + local_ip_address?: string; + camera_access_key?: string; [key: string]: any; }; } diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx deleted file mode 100644 index e5e017db8dc..00000000000 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { SyntheticEvent } from "react"; -import { AssetData } from "../AssetTypes"; -import CameraFeedOld from "../../CameraFeed/CameraFeedOld"; -import { BedSelect } from "../../Common/BedSelect"; -import { BedModel } from "../../Facility/models"; -import { getCameraConfig } from "../../../Utils/transformUtils"; -import { Submit } from "../../Common/components/ButtonV2"; -import TextFormField from "../../Form/FormFields/TextFormField"; -import Card from "../../../CAREUI/display/Card"; -import { FieldErrorText } from "../../Form/FormFields/FormField"; - -interface CameraConfigureProps { - asset: AssetData; - addPreset(e: SyntheticEvent): void; - setBed(bed: BedModel): void; - bed: BedModel; - newPreset: string; - setNewPreset(preset: string): void; - refreshPresetsHash: number; - facilityMiddlewareHostname: string; - isLoading: boolean; -} -export default function CameraConfigure(props: CameraConfigureProps) { - const { - asset, - addPreset, - setBed, - bed, - isLoading, - newPreset, - setNewPreset, - refreshPresetsHash, - facilityMiddlewareHostname, - } = props; - - return ( -
- -
-
-
- - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={asset?.location_object?.id} - facility={asset?.location_object?.facility?.id} - /> -
-
- - setNewPreset(e.value)} - errorClassName="hidden" - /> - {newPreset.length > 12 && ( - - )} -
-
-
- -
-
-
- - - -
- ); -} diff --git a/src/Components/Assets/configure/MonitorConfigure.tsx b/src/Components/Assets/configure/MonitorConfigure.tsx deleted file mode 100644 index 785b82873de..00000000000 --- a/src/Components/Assets/configure/MonitorConfigure.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useState } from "react"; -import { BedSelect } from "../../Common/BedSelect"; -import { BedModel } from "../../Facility/models"; -import { AssetClass, AssetData } from "../AssetTypes"; -import * as Notification from "../../../Utils/Notifications.js"; -import { Submit } from "../../Common/components/ButtonV2"; -import { FieldLabel } from "../../Form/FormFields/FormField"; -import request from "../../../Utils/request/request"; -import routes from "../../../Redux/api"; -import useQuery from "../../../Utils/request/useQuery"; -import CareIcon from "../../../CAREUI/icons/CareIcon"; - -const saveLink = async (assetId: string, bedId: string) => { - await request(routes.createAssetBed, { - body: { - asset: assetId, - bed: bedId, - }, - }); - Notification.Success({ msg: "AssetBed Link created successfully" }); -}; -const update_Link = async ( - assetbedId: string, - assetId: string, - bed: BedModel, -) => { - await request(routes.partialUpdateAssetBed, { - pathParams: { external_id: assetbedId }, - body: { - asset: assetId, - bed: bed.id ?? "", - }, - }); - Notification.Success({ msg: "AssetBed Link updated successfully" }); -}; - -export default function MonitorConfigure({ asset }: { asset: AssetData }) { - const [bed, setBed] = useState({}); - const [updateLink, setUpdateLink] = useState(false); - const { data: assetBed } = useQuery(routes.listAssetBeds, { - query: { asset: asset.id }, - onResponse: ({ res, data }) => { - if (res?.status === 200 && data && data.results.length > 0) { - setBed(data.results[0].bed_object); - setUpdateLink(true); - } - }, - }); - - return ( -
{ - e.preventDefault(); - if (updateLink) { - update_Link( - assetBed?.results[0].id as string, - asset.id as string, - bed as BedModel, - ); - } else { - saveLink(asset.id as string, bed?.id as string); - } - }} - > -
-
- Bed - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={asset?.location_object?.id} - facility={asset?.location_object?.facility?.id} - not_occupied_by_asset_type={AssetClass.HL7MONITOR} - className="w-full" - /> -
- - - {updateLink ? "Update Bed" : "Save Bed"} - -
-
- ); -} diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index e49a63f7028..3c9b25b6fc9 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -78,7 +78,7 @@ export default function CameraFeed(props: Props) { .operate({ type: "get_stream_token" }) .then(({ res, data }) => { if (res?.status != 200) { - setState("authentication_error"); + setState("host_unreachable"); return props.onStreamError?.(); } const result = data?.result as { token: string }; @@ -183,13 +183,15 @@ export default function CameraFeed(props: Props) { {props.children}
- - {props.asset.name} - - +
+ + {props.asset.name} + + +
{!isIOS && (
); - case "authentication_error": - return ( - - ); case "offline": return ( { - const { cameraPTZ } = props; - return ( - - ); -}; - -const CameraFeedOld = (props: any) => { - const middlewareHostname = props.middlewareHostname; - const [presetsPage, setPresetsPage] = useState(0); - const cameraAsset = props.asset; - const [presets, setPresets] = useState([]); - const [bedPresets, setBedPresets] = useState([]); - const [showDefaultPresets, setShowDefaultPresets] = useState(false); - const [precision, setPrecision] = useState(1); - const [streamStatus, setStreamStatus] = useState( - StreamStatus.Offline, - ); - const [videoStartTime, setVideoStartTime] = useState(null); - const [bed, setBed] = useState({}); - const [presetName, setPresetName] = useState(""); - const [loading, setLoading] = useState(); - const dispatch: any = useDispatch(); - const [page, setPage] = useState({ - count: 0, - limit: 8, - offset: 0, - }); - const [toDelete, setToDelete] = useState(null); - const [toUpdate, setToUpdate] = useState(null); - const [_isFullscreen, setFullscreen] = useFullscreen(); - - const { width } = useWindowDimensions(); - const extremeSmallScreenBreakpoint = 320; - const isExtremeSmallScreen = - width <= extremeSmallScreenBreakpoint ? true : false; - const liveFeedPlayerRef = useRef(null); - const [streamUrl, setStreamUrl] = useState(""); - - const refreshPresetsHash = props.refreshPresetsHash; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [currentPreset, setCurrentPreset] = useState(); - const { - absoluteMove, - getCameraStatus, - getStreamToken, - getPTZPayload, - getPresets, - gotoPreset, - relativeMove, - } = useFeedPTZ({ - config: { - middlewareHostname, - ...cameraAsset, - }, - dispatch, - }); - - const fetchCameraPresets = () => - getPresets({ - onSuccess: (resp) => { - setPresets(resp); - }, - onError: (resp) => { - resp instanceof AxiosError && - Notification.Error({ - msg: "Camera is offline", - }); - }, - }); - - const calculateVideoLiveDelay = () => { - const video = liveFeedPlayerRef.current as HTMLVideoElement; - if (!video || !videoStartTime) return 0; - - const timeDifference = - (new Date().getTime() - videoStartTime.getTime()) / 1000; - - return timeDifference - video.currentTime; - }; - - const getBedPresets = async (id: any) => { - const bedAssets = await dispatch( - listAssetBeds({ - asset: id, - limit: page.limit, - offset: page.offset, - }), - ); - setBedPresets(bedAssets?.data?.results); - setPage({ - ...page, - count: bedAssets?.data?.count, - }); - }; - - const deletePreset = async (id: any) => { - const res = await dispatch(deleteAssetBed(id)); - if (res?.status === 204) { - Notification.Success({ msg: "Preset deleted successfully" }); - getBedPresets(cameraAsset.id); - } else { - Notification.Error({ - msg: "Error while deleting Preset: " + (res?.data?.detail || ""), - }); - } - setToDelete(null); - }; - - const updatePreset = async (currentPreset: any) => { - const data = { - bed_id: bed.id, - preset_name: presetName, - }; - const response = await dispatch( - partialUpdateAssetBed( - { - asset: currentPreset.asset_object.id, - bed: bed.id, - meta: { - ...currentPreset.meta, - ...data, - }, - }, - currentPreset?.id, - ), - ); - if (response && response.status === 200) { - Notification.Success({ msg: "Preset Updated" }); - } else { - Notification.Error({ msg: "Something Went Wrong" }); - } - getBedPresets(cameraAsset?.id); - fetchCameraPresets(); - setToUpdate(null); - }; - - const gotoBedPreset = (preset: any) => { - setLoading("Moving"); - absoluteMove(preset.meta.position, { - onSuccess: () => setLoading(undefined), - }); - }; - - useEffect(() => { - if (cameraAsset?.hostname) { - fetchCameraPresets(); - setTimeout(() => { - startStreamFeed(); - }, 1000); - } - }, []); - - useEffect(() => { - setPresetName(toUpdate?.meta?.preset_name); - setBed(toUpdate?.bed_object); - }, [toUpdate]); - - useEffect(() => { - getBedPresets(cameraAsset.id); - if (bedPresets?.[0]?.position) { - absoluteMove(bedPresets[0]?.position, {}); - } - }, [page.offset, cameraAsset.id, refreshPresetsHash]); - - const startStreamFeed = useCallback(async () => { - if (!liveFeedPlayerRef.current) return; - - await getStreamToken({ - onSuccess: (data) => { - setStreamUrl( - `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0&token=${data.token}`, - ); - }, - onError: () => { - setStreamStatus(StreamStatus.Offline); - }, - }); - }, [liveFeedPlayerRef.current]); - - const viewOptions = (page: number) => { - return presets - ? Object.entries(presets) - .map(([key, value]) => ({ label: key, value })) - .slice(page, page + 10) - : Array.from(Array(10), (_, i) => ({ - label: "Monitor " + (i + 1), - value: i + 1, - })); - }; - useEffect(() => { - let tId: any; - if (streamStatus !== StreamStatus.Playing) { - setStreamStatus(StreamStatus.Loading); - tId = setTimeout(() => { - startStreamFeed(); - }, 5000); - } - - return () => { - clearTimeout(tId); - }; - }, [startStreamFeed, streamStatus]); - - const handlePagination = (cOffset: number) => { - setPage({ - ...page, - offset: cOffset, - }); - }; - - const cameraPTZActionCBs: { [key: string]: (option: any) => void } = { - precision: () => { - setPrecision((precision: number) => - precision === 16 ? 1 : precision * 2, - ); - }, - reset: async () => { - setStreamStatus(StreamStatus.Loading); - setVideoStartTime(null); - await startStreamFeed(); - }, - fullScreen: () => { - if (!liveFeedPlayerRef.current) return; - setFullscreen(true, liveFeedPlayerRef.current); - }, - updatePreset: (option) => { - getCameraStatus({ - onSuccess: async (data) => { - if (currentPreset?.asset_object?.id && data?.position) { - setLoading(option.loadingLabel); - console.log("Updating Preset"); - const response = await dispatch( - partialUpdateAssetBed( - { - asset: currentPreset.asset_object.id, - bed: currentPreset.bed_object.id, - meta: { - ...currentPreset.meta, - position: data?.position, - }, - }, - currentPreset?.id, - ), - ); - if (response && response.status === 200) { - Notification.Success({ msg: "Preset Updated" }); - getBedPresets(cameraAsset?.id); - fetchCameraPresets(); - } - setLoading(undefined); - } - }, - }); - }, - other: (option) => { - setLoading(option.loadingLabel); - relativeMove(getPTZPayload(option.action, precision), { - onSuccess: () => setLoading(undefined), - }); - }, - }; - - const cameraPTZ = getCameraPTZ(precision).map((option) => { - const cb = - cameraPTZActionCBs[ - cameraPTZActionCBs[option.action] ? option.action : "other" - ]; - return { ...option, callback: () => cb(option) }; - }); - - // Voluntarily disabling eslint, since length of `cameraPTZ` is constant and - // hence shall not cause issues. (https://news.ycombinator.com/item?id=24363703) - for (const option of cameraPTZ) { - if (!option.shortcutKey) continue; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKeyboardShortcut(option.shortcutKey, option.callback); - } - - return ( - - {toDelete && ( - -

- Preset: {toDelete.meta.preset_name} -

-

- Bed: {toDelete.bed_object.name} -

- - } - action="Delete" - variant="danger" - onClose={() => setToDelete(null)} - onConfirm={() => deletePreset(toDelete.id)} - /> - )} - {toUpdate && ( - setToUpdate(null)} - onConfirm={() => updatePreset(toUpdate)} - > -
- setPresetName(value)} - /> -
- Bed - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={cameraAsset.location_id} - facility={cameraAsset.facility_id} - /> -
-
-
- )} -
-
-
-
- { - setVideoStartTime(() => new Date()); - }} - onWaiting={() => { - const delay = calculateVideoLiveDelay(); - if (delay > 5) { - setStreamStatus(StreamStatus.Loading); - } - }} - onSuccess={() => setStreamStatus(StreamStatus.Playing)} - onError={() => setStreamStatus(StreamStatus.Offline)} - /> - - {streamStatus === StreamStatus.Playing && - calculateVideoLiveDelay() > 3 && ( -
- - Slow Network Detected -
- )} - - {loading && ( -
-
-
-

{loading}

-
-
- )} - {/* { streamStatus > 0 && */} -
- {streamStatus === StreamStatus.Offline && ( -
-

- STATUS: OFFLINE -

-

- Feed is currently not live. -

-

- Click refresh button to try again. -

-
- )} - {streamStatus === StreamStatus.Stop && ( -
-

- STATUS: STOPPED -

-

Feed is Stooped.

-

- Click refresh button to start feed. -

-
- )} - {streamStatus === StreamStatus.Loading && ( -
-

- STATUS: LOADING -

-

- Fetching latest feed. -

-
- )} -
-
-
- {cameraPTZ.map((option) => { - const shortcutKeyDescription = - option.shortcutKey && - option.shortcutKey - .join(" + ") - .replace("Control", "Ctrl") - .replace("ArrowUp", "↑") - .replace("ArrowDown", "↓") - .replace("ArrowLeft", "←") - .replace("ArrowRight", "→"); - - return ( - - ); - })} -
- -
-
-
- -
- -
-
- {showDefaultPresets ? ( - <> - {viewOptions(presetsPage)?.map((option: any, i) => ( - - ))} - - ) : ( - <> - {bedPresets?.map((preset: any, index: number) => ( -
- -
- - -
-
- ))} - - )} -
- {/* Page Number Next and Prev buttons */} - {showDefaultPresets ? ( -
- - -
- ) : ( -
- - -
- )} - {props?.showRefreshButton && ( - - )} -
-
-
-
- - ); -}; - -export default CameraFeedOld; diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx index 7268397b81a..3e447e34e21 100644 --- a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx +++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx @@ -1,32 +1,29 @@ import { useState } from "react"; -import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; +import { AssetData } from "../Assets/AssetTypes"; import CameraFeed from "./CameraFeed"; import useQuery from "../../Utils/request/useQuery"; -import routes from "../../Redux/api"; -import useSlug from "../../Common/hooks/useSlug"; -import { CameraPresetDropdown } from "./AssetBedSelect"; +import { CameraPresetDropdown } from "./CameraPresetSelect"; import useOperateCamera from "./useOperateCamera"; import { classNames } from "../../Utils/utils"; +import { CameraPreset, FeedRoutes } from "./routes"; interface Props { asset: AssetData; } export default function LocationFeedTile(props: Props) { - const facility = useSlug("facility"); - const [preset, setPreset] = useState(); - - const { data, loading } = useQuery(routes.listAssetBeds, { - query: { limit: 100, facility, asset: props.asset?.id }, + const [preset, setPreset] = useState(); + const { operate, key } = useOperateCamera(props.asset.id); + const { data, loading } = useQuery(FeedRoutes.listAssetPresets, { + pathParams: { asset_id: props.asset.id }, + query: { limit: 100 }, }); - const { operate, key } = useOperateCamera(props.asset.id, true); - return ( string; - onChange?: (value: AssetBedModel) => void; + options: CameraPreset[]; + value?: CameraPreset; + label?: (value: CameraPreset) => string; + onChange?: (value: CameraPreset) => void; } export default function CameraPresetSelect(props: Props) { @@ -71,16 +70,13 @@ export const CameraPresetDropdown = ( props: Props & { placeholder: string }, ) => { const selected = props.value; - - const options = props.options.filter(({ meta }) => meta.type !== "boundary"); - const label = props.label ?? defaultLabel; return (
- {options.length === 0 + {props.options.length === 0 ? "No presets" : selected ? label(selected) @@ -113,7 +109,7 @@ export const CameraPresetDropdown = ( as="ul" className="absolute z-20 max-h-48 w-full overflow-auto rounded-b-lg bg-white py-1 text-base shadow-lg ring-1 ring-secondary-500 focus:outline-none md:max-h-60" > - {options?.map((obj) => ( + {props.options.map((obj) => ( { - return `${bed_object.name}: ${meta.preset_name}`; +const defaultLabel = (preset: CameraPreset) => { + return `${preset.asset_bed.bed_object.name}: ${preset.name}`; }; diff --git a/src/Components/CameraFeed/ConfigureCamera.tsx b/src/Components/CameraFeed/ConfigureCamera.tsx new file mode 100644 index 00000000000..052f93a9b05 --- /dev/null +++ b/src/Components/CameraFeed/ConfigureCamera.tsx @@ -0,0 +1,643 @@ +import { useEffect, useState } from "react"; +import { AssetData } from "../Assets/AssetTypes"; +import { getCameraConfig, makeAccessKey } from "../../Utils/transformUtils"; +import TextFormField from "../Form/FormFields/TextFormField"; +import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; +import useAuthUser from "../../Common/hooks/useAuthUser"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import useOperateCamera from "./useOperateCamera"; +import CameraFeed from "./CameraFeed"; +import { useTranslation } from "react-i18next"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import { Error, Success } from "../../Utils/Notifications"; +import { useQueryParams } from "raviger"; +import useQuery from "../../Utils/request/useQuery"; +import { classNames, compareBy } from "../../Utils/utils"; +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import { CameraPreset, FeedRoutes, GetStatusResponse } from "./routes"; +import DialogModal from "../Common/Dialog"; +import { + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from "@headlessui/react"; +import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2"; +import Loading from "../Common/Loading"; +import ConfirmDialog from "../Common/ConfirmDialog"; +import { FieldLabel } from "../Form/FormFields/FormField"; +import { checkIfValidIP } from "../../Common/validation"; +import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; + +interface Props { + asset: AssetData; + onUpdated: () => void; +} + +type OnvifPreset = { name: string; value: number }; + +export default function ConfigureCamera(props: Props) { + const { t } = useTranslation(); + const authUser = useAuthUser(); + + const presetNameSuggestions = [ + t("patient_face"), + t("patient_body"), + t("vitals_monitor"), + ]; + + const [query, setQuery] = useQueryParams<{ bed?: string }>(); + const [meta, setMeta] = useState(props.asset.meta); + const [onvifPresets, setOnvifPresets] = useState(); + const [currentOnvifPreset, setCurrentOnvifPreset] = useState(); + const [createPreset, setCreatePreset] = useState(); + const [editPreset, setEditPreset] = useState<{ + preset: CameraPreset["id"]; + position?: CameraPreset["position"]; + }>(); + const [presetName, setPresetName] = useState(""); + const [showUnlinkConfirmation, setShowUnlinkConfirmation] = useState(false); + + const assetBedsQuery = useQuery(routes.listAssetBeds, { + query: { asset: props.asset.id, limit: 50 }, + }); + + const bedsQuery = useQuery(routes.listFacilityBeds, { + query: { location: props.asset.location_object.id, limit: 50 }, + }); + + const linkedAssetBeds = assetBedsQuery.data?.results.sort( + compareBy("created_date"), + ); + + const linkedBedIDs = linkedAssetBeds?.map((a) => a.bed_object.id!); + const unlinkedBeds = + linkedBedIDs && + bedsQuery.data?.results + .filter((bed) => !linkedBedIDs.includes(bed.id!)) + .sort(compareBy("created_date")); + + const firstBedId = + linkedAssetBeds?.[0]?.bed_object.id ?? unlinkedBeds?.[0]?.id; + useEffect(() => { + if (!query.bed && firstBedId) { + setQuery({ bed: firstBedId }); + } + }, [query.bed, firstBedId]); + + const selectedAssetBed = linkedAssetBeds?.find( + (a) => a.bed_object.id === query.bed, + ); + const selectedUnlinkedBed = unlinkedBeds?.find((bed) => bed.id === query.bed); + + const cameraPresetsQuery = useQuery(FeedRoutes.listAssetBedPresets, { + pathParams: { assetbed_id: selectedAssetBed?.id ?? "" }, + query: { position: true, limit: 50 }, + prefetch: !!selectedAssetBed?.id, + }); + + useEffect(() => setMeta(props.asset.meta), [props.asset]); + + const accessKeyAttributes = getCameraConfig(meta); + + const { operate, key } = useOperateCamera(props.asset.id); + + if (!["DistrictAdmin", "StateAdmin"].includes(authUser.user_type)) { + return ( +
+ +
+ ); + } + + return ( +
+
+
{ + e.preventDefault(); + const { res } = await request(routes.partialUpdateAsset, { + pathParams: { external_id: props.asset.id }, + body: { meta }, + }); + + if (res?.ok) { + Success({ msg: "Asset Configured Successfully" }); + props.onUpdated(); + } + }} + > +

{t("feed_configurations")}

+
+ +

{t("middleware_hostname")}

+ {!!props.asset.resolved_middleware && + props.asset.resolved_middleware.source != "asset" && ( +
+ + + {t("middleware_hostname_sourced_from", { + source: props.asset.resolved_middleware?.source, + })} + +
+ )} +
+ } + placeholder={ + props.asset.resolved_middleware?.hostname ?? + t("middleware_hostname_example") + } + value={meta?.middleware_hostname} + onChange={({ value }) => + setMeta({ ...meta, middleware_hostname: value }) + } + /> + + setMeta({ ...meta, local_ip_address: value }) + } + error={ + meta?.local_ip_address && !checkIfValidIP(meta.local_ip_address) + ? t("invalid_ip_address") + : undefined + } + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + username: value, + }), + }) + } + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + password: value, + }), + }) + } + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + accessKey: value, + }), + }) + } + /> +
+
+ +
+ + +
+ { + if (!onvifPresets) { + setOnvifPresets( + Object.entries(presets).map(([name, value]) => ({ + name, + value, + })), + ); + } + }} + > +
+ { + setCurrentOnvifPreset(preset); + operate({ + type: "goto_preset", + data: { + preset: preset.value, + }, + }); + }} + disabled={!onvifPresets?.length} + > +
+ + + {!onvifPresets?.length + ? t("no_presets") + : (currentOnvifPreset?.name ?? + t("move_to_onvif_preset"))} + + + + + + + {onvifPresets?.map((obj) => ( + + classNames( + dropdownOptionClassNames(args), + "px-2 py-1.5", + ) + } + value={obj} + > + {obj.name} + + ))} + +
+
+
+
+
+
+ + {!linkedAssetBeds?.length && !unlinkedBeds?.length ? ( +
+ +

{t("location_beds_empty")}

+

{t("add_beds_to_configure_presets")}

+
+
+ ) : ( +
+

{t("manage_bed_presets")}

+
+ + {cameraPresetsQuery.loading && } + {selectedAssetBed && ( + <> + setShowUnlinkConfirmation(false)} + onConfirm={async () => { + const { res } = await request(routes.deleteAssetBed, { + pathParams: { external_id: selectedAssetBed.id }, + }); + + if (res?.ok) { + Success({ + msg: `${selectedAssetBed.bed_object.name} was unlinked from ${selectedAssetBed.asset_object.name}.`, + }); + setShowUnlinkConfirmation(false); + assetBedsQuery.refetch(); + } + }} + /> + { + setCreatePreset(undefined); + setPresetName(""); + }} + > + setPresetName(value)} + errorClassName="hidden" + placeholder={t("preset_name_placeholder")} + suggestions={presetNameSuggestions} + /> +
+ { + const { res } = await request(FeedRoutes.createPreset, { + pathParams: { assetbed_id: selectedAssetBed.id }, + body: { + name: presetName, + position: createPreset!, + }, + }); + if (!res?.ok) { + return; + } + setCreatePreset(undefined); + setPresetName(""); + Success({ msg: "Preset created" }); + cameraPresetsQuery.refetch(); + }} + disabled={!presetName} + /> +
+
+
+
    +
  • { + const { data } = await operate({ type: "get_status" }); + if (!data) { + Error({ msg: t("unable_to_get_current_position") }); + return; + } + setCreatePreset( + (data as GetStatusResponse).result.position, + ); + }} + > + + {t("add_preset")} +
  • + {cameraPresetsQuery.data?.results.map((preset) => ( +
  • + { + setEditPreset(undefined); + setPresetName(""); + }} + > +
    + setPresetName(value)} + placeholder={t("preset_name_placeholder")} + suggestions={presetNameSuggestions} + /> + {t("position")} + { + if (!value) { + setEditPreset({ + ...editPreset!, + position: undefined, + }); + return; + } + + const { data } = await operate({ + type: "get_status", + }); + if (!data) { + Error({ + msg: t("unable_to_get_current_position"), + }); + return; + } + setEditPreset({ + ...editPreset!, + position: (data as GetStatusResponse).result + .position!, + }); + }} + /> +
    + { + setEditPreset(undefined); + setPresetName(""); + }} + /> + { + const { res } = await request( + FeedRoutes.deletePreset, + { + pathParams: { + assetbed_id: selectedAssetBed.id, + id: preset.id, + }, + }, + ); + if (!res?.ok) { + return; + } + Success({ msg: t("preset_deleted") }); + cameraPresetsQuery.refetch(); + setEditPreset(undefined); + setPresetName(""); + }} + variant="danger" + > + + {t("delete")} + + { + const { res } = await request( + FeedRoutes.updatePreset, + { + pathParams: { + assetbed_id: selectedAssetBed.id, + id: preset.id, + }, + body: { + name: presetName || undefined, + position: editPreset?.position, + }, + }, + ); + if (!res?.ok) { + return; + } + Success({ msg: t("preset_updated") }); + setEditPreset(undefined); + setPresetName(""); + cameraPresetsQuery.refetch(); + }} + /> +
    + +
    + {preset.name} + + + +
    + + operate({ + type: "absolute_move", + data: preset.position!, + }) + } + > + + {t("view")} + + { + setEditPreset({ preset: preset.id }); + }} + > + + {t("update")} + +
    +
    +
  • + ))} +
+
+ + setShowUnlinkConfirmation(true)} + > + {t("unlink_camera_and_bed")} + +
+
+ + )} + {selectedUnlinkedBed && ( +
+ +

{t("bed_not_linked_to_camera")}

+

{t("create_preset_prerequisite")}

+ { + const { res } = await request(routes.createAssetBed, { + body: { + asset: props.asset.id, + bed: selectedUnlinkedBed.id, + }, + }); + if (res?.ok) { + Success({ msg: t("camera_bed_link_success") }); + assetBedsQuery.refetch(); + } + }} + className="mt-6" + > + {t("link_camera_and_bed")} + +
+
+ )} +
+
+ )} +
+ ); +} diff --git a/src/Components/CameraFeed/FeedAlert.tsx b/src/Components/CameraFeed/FeedAlert.tsx index 09f3b21ae42..86c5468decf 100644 --- a/src/Components/CameraFeed/FeedAlert.tsx +++ b/src/Components/CameraFeed/FeedAlert.tsx @@ -9,8 +9,7 @@ export type FeedAlertState = | "moving" | "zooming" | "saving_preset" - | "host_unreachable" - | "authentication_error"; + | "host_unreachable"; interface Props { state?: FeedAlertState; @@ -25,7 +24,6 @@ const ALERT_ICON_MAP: Partial> = { zooming: "l-search", saving_preset: "l-save", host_unreachable: "l-exclamation-triangle", - authentication_error: "l-exclamation-triangle", }; export default function FeedAlert({ state }: Props) { diff --git a/src/Components/CameraFeed/FeedControls.tsx b/src/Components/CameraFeed/FeedControls.tsx index 1bacd6c1cfc..36d3aa96cd2 100644 --- a/src/Components/CameraFeed/FeedControls.tsx +++ b/src/Components/CameraFeed/FeedControls.tsx @@ -15,8 +15,6 @@ const Actions = { const metaKey = isAppleDevice ? "Meta" : "Control"; -export type PTZAction = keyof typeof Actions; - /** * Returns the PTZ payload for the given action * diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts index aecbdc655fa..db983d6a383 100644 --- a/src/Components/CameraFeed/routes.ts +++ b/src/Components/CameraFeed/routes.ts @@ -1,4 +1,8 @@ import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { WritableOnly } from "../../Utils/types"; +import { AssetBedModel } from "../Assets/AssetTypes"; +import { PerformedByModel } from "../HCX/misc"; import { OperationAction, PTZPayload } from "./useOperateCamera"; export type GetStatusResponse = { @@ -23,6 +27,18 @@ export type GetPresetsResponse = { result: Record; }; +export type CameraPreset = { + readonly id: string; + name: string; + readonly asset_bed: AssetBedModel; + position: PTZPayload; + readonly created_by: PerformedByModel; + readonly updated_by: PerformedByModel; + readonly created_date: string; + readonly modified_date: string; + readonly is_migrated: boolean; +}; + export const FeedRoutes = { operateAsset: { path: "/api/v1/asset/{id}/operate_assets/", @@ -32,4 +48,37 @@ export const FeedRoutes = { >(), TBody: Type<{ action: OperationAction }>(), }, + + listAssetBedPresets: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/", + method: "GET", + TRes: Type>(), + }, + listAssetPresets: { + path: "/api/v1/asset/{asset_id}/camera_presets/", + method: "GET", + TRes: Type>(), + }, + listBedPresets: { + path: "/api/v1/bed/{bed_id}/camera_presets/", + method: "GET", + TRes: Type>(), + }, + createPreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/", + method: "POST", + TRes: Type(), + TBody: Type>(), + }, + updatePreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/", + method: "PATCH", + TRes: Type(), + TBody: Type>>(), + }, + deletePreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/", + method: "DELETE", + TRes: Type(), + }, } as const; diff --git a/src/Components/CameraFeed/useFeedPTZ.ts b/src/Components/CameraFeed/useFeedPTZ.ts deleted file mode 100644 index fb704baf972..00000000000 --- a/src/Components/CameraFeed/useFeedPTZ.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Deprecated. Use `useOperateAsset` instead. - * - * Preserving for backwards compatibility and preventing merge conflict with a - * co-related PR. Will be removed in the future. - */ - -import { operateAsset } from "../../Redux/actions"; - -export interface IAsset { - id: string; -} - -interface PTZPayload { - x: number; - y: number; - zoom: number; -} - -interface UseMSEMediaPlayerOption { - config: IAsset; - dispatch: any; -} - -interface UseMSEMediaPlayerReturnType { - absoluteMove: (payload: PTZPayload, options: IOptions) => void; - relativeMove: (payload: PTZPayload, options: IOptions) => void; - getPTZPayload: ( - action: PTZ, - precision?: number, - value?: number, - ) => PTZPayload; - getCameraStatus: (options: IOptions) => void; - getStreamToken: (options: IOptions) => void; - getPresets: (options: IOptions) => void; - gotoPreset: (payload: IGotoPresetPayload, options: IOptions) => void; -} - -interface IOptions { - onSuccess?: (resp: Record) => void; - onError?: (resp: Record) => void; -} - -export enum PTZ { - Up = "up", - Down = "down", - Left = "left", - Right = "right", - ZoomIn = "zoomIn", - ZoomOut = "zoomOut", -} - -const getCameraStatus = - (config: IAsset, dispatch: any) => - async (options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "get_status", - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const getStreamToken = - (config: IAsset, dispatch: any) => - async (options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "get_stream_token", - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const getPresets = - (config: IAsset, dispatch: any) => - async (options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "get_presets", - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -interface IGotoPresetPayload { - preset: string; -} - -const gotoPreset = - (config: IAsset, dispatch: any) => - async (payload: IGotoPresetPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "goto_preset", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const absoluteMove = - (config: IAsset, dispatch: any) => - async (payload: PTZPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "absolute_move", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const relativeMove = - (config: IAsset, dispatch: any) => - async (payload: PTZPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "relative_move", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -export const getPTZPayload = ( - action: PTZ, - precision = 1, - value?: number, -): PTZPayload => { - let x = 0; - let y = 0; - let zoom = 0; - const delta = !value ? 0.1 / Math.max(1, precision) : value; - switch (action) { - case PTZ.Up: - y = delta; - break; - case PTZ.Down: - y = -delta; - break; - case PTZ.Left: - x = -delta; - break; - case PTZ.Right: - x = delta; - break; - case PTZ.ZoomIn: - zoom = delta; - break; - case PTZ.ZoomOut: - zoom = -delta; - break; - } - - return { x, y, zoom }; -}; - -export const useFeedPTZ = ({ - config, - dispatch, -}: UseMSEMediaPlayerOption): UseMSEMediaPlayerReturnType => { - return { - absoluteMove: absoluteMove(config, dispatch), - relativeMove: relativeMove(config, dispatch), - getPTZPayload, - getCameraStatus: getCameraStatus(config, dispatch), - getStreamToken: getStreamToken(config, dispatch), - getPresets: getPresets(config, dispatch), - gotoPreset: gotoPreset(config, dispatch), - }; -}; diff --git a/src/Components/CameraFeed/useOperateCamera.ts b/src/Components/CameraFeed/useOperateCamera.ts index bfddbf5b887..0e65fb0130c 100644 --- a/src/Components/CameraFeed/useOperateCamera.ts +++ b/src/Components/CameraFeed/useOperateCamera.ts @@ -54,7 +54,7 @@ export type OperationAction = * This hook is used to control the PTZ of a camera asset and retrieve other related information. * @param id The external id of the camera asset */ -export default function useOperateCamera(id: string, silent = false) { +export default function useOperateCamera(id: string) { const [key, setKey] = useState(0); return { @@ -70,14 +70,14 @@ export default function useOperateCamera(id: string, silent = false) { type: "get_status", }, }, - silent, + silent: true, }); } return request(FeedRoutes.operateAsset, { pathParams: { id }, body: { action }, - silent, + silent: true, }); }, }; diff --git a/src/Components/CameraFeed/utils.ts b/src/Components/CameraFeed/utils.ts index 5556237d579..1cb721ebcc3 100644 --- a/src/Components/CameraFeed/utils.ts +++ b/src/Components/CameraFeed/utils.ts @@ -21,7 +21,7 @@ export const getStreamUrl = (asset: AssetData, token?: string) => { throw "getStreamUrl can be invoked only for ONVIF Assets"; } - const config = getCameraConfig(asset); + const config = getCameraConfig(asset.meta); const host = asset.resolved_middleware?.hostname; const uuid = config.accessKey; diff --git a/src/Components/Common/Avatar.tsx b/src/Components/Common/Avatar.tsx index a519901216b..e4d42dd72ae 100644 --- a/src/Components/Common/Avatar.tsx +++ b/src/Components/Common/Avatar.tsx @@ -1,4 +1,5 @@ -import React from "react"; +import { cn } from "@/lib/utils"; +import React, { useEffect, useRef, useState } from "react"; const colors: string[] = [ "#E6F3FF", // Light Blue @@ -44,43 +45,54 @@ const initials = (name: string): string => { interface AvatarProps { colors?: [string, string]; name: string; + imageUrl?: string; className?: string; - square?: boolean; // New prop to determine if the avatar should be square } const Avatar: React.FC = ({ colors: propColors, name, + imageUrl, className, - square = false, // Default to false for backwards compatibility }) => { - const [bgColor, fgColor] = propColors || toColor(name); + const [bgColor] = propColors || toColor(name); + const [width, setWidth] = useState(0); + const avatarRef = useRef(null); + + useEffect(() => { + const updateWidth = () => { + const avatarRect = avatarRef.current?.getBoundingClientRect(); + const width = avatarRect?.width || 0; + setWidth(width); + }; + updateWidth(); + document.addEventListener("resize", updateWidth); + return () => document.removeEventListener("resize", updateWidth); + }, []); return ( - - {square ? ( - + {imageUrl ? ( + {name} ) : ( - +
{initials(name)}
)} - - {initials(name)} - - +
); }; diff --git a/src/Components/Common/MonitorAssetPopover.tsx b/src/Components/Common/MonitorAssetPopover.tsx index 5293c0a6cd4..3d7d55ad271 100644 --- a/src/Components/Common/MonitorAssetPopover.tsx +++ b/src/Components/Common/MonitorAssetPopover.tsx @@ -62,7 +62,7 @@ const MonitorAssetPopover = ({

- {t("local_ipaddress")}: + {t("local_ip_address")}:

{asset?.meta?.local_ip_address} diff --git a/src/Components/Common/Sidebar/SidebarUserCard.tsx b/src/Components/Common/Sidebar/SidebarUserCard.tsx index 825507ffb25..70a64917224 100644 --- a/src/Components/Common/Sidebar/SidebarUserCard.tsx +++ b/src/Components/Common/Sidebar/SidebarUserCard.tsx @@ -26,7 +26,7 @@ const SidebarUserCard: React.FC = ({ shrinked }) => {

- + )} + + {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); }; @@ -922,7 +926,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { validate={validateForm} onSubmit={handleSubmit} submitLabel={buttonText} - onCancel={() => navigate("/facility")} + onCancel={() => goBack()} className="bg-transparent px-1 py-2 md:px-2" onDraftRestore={(newState) => { dispatch({ type: "set_state", state: newState }); @@ -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/Components/Shifting/BoardView.tsx b/src/Components/Shifting/BoardView.tsx index f83e0a4974e..c071cc4d934 100644 --- a/src/Components/Shifting/BoardView.tsx +++ b/src/Components/Shifting/BoardView.tsx @@ -143,7 +143,10 @@ export default function BoardView() { { const { data } = await request(routes.downloadShiftRequests, { - query: { ...formatFilter(qParams), csv: true }, + query: { + ...formatFilter({ ...qParams, status: board.text }), + csv: true, + }, }); return data ?? null; }} diff --git a/src/Locale/en.json b/src/Locale/en.json index a7c2d1a0c21..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", @@ -222,6 +258,7 @@ "add_as": "Add as", "add_attachments": "Add Attachments", "add_beds": "Add Bed(s)", + "add_beds_to_configure_presets": "Add beds to this location to configure presets for them.", "add_details_of_patient": "Add Details of Patient", "add_location": "Add Location", "add_new_user": "Add New User", @@ -229,6 +266,7 @@ "add_policy": "Add Insurance Policy", "add_prescription_medication": "Add Prescription Medication", "add_prescription_to_consultation_note": "Add a new prescription to this consultation.", + "add_preset": "Add preset", "add_prn_prescription": "Add PRN Prescription", "add_remarks": "Add remarks", "add_spoke": "Add Spoke Facility", @@ -251,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", @@ -270,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.", @@ -282,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", @@ -296,6 +338,7 @@ "bed_capacity": "Bed Capacity", "bed_created_notification_one": "{{count}} Bed created successfully", "bed_created_notification_other": "{{count}} Beds created successfully", + "bed_not_linked_to_camera": "This bed has not been linked to this camera.", "bed_search_placeholder": "Search by beds name", "bed_type": "Bed Type", "bed_type__100": "ICU Bed", @@ -314,7 +357,9 @@ "bradycardia": "Bradycardia", "breathlessness_level": "Breathlessness level", "camera": "Camera", + "camera_bed_link_success": "Camera linked to bed successfully.", "camera_permission_denied": "Camera Permission denied", + "camera_was_linked_to_bed": "This camera was linked to this bed", "cancel": "Cancel", "capture": "Capture", "capture_cover_photo": "Capture Cover Photo", @@ -328,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", @@ -385,7 +432,40 @@ "confirm_password": "Confirm Password", "confirm_transfer_complete": "Confirm Transfer Complete!", "confirmed": "Confirmed", - "consultation_not_filed": "You have not filed any consultation for this patient yet.", + "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)", "consultation_updates": "Consultation updates", @@ -403,11 +483,17 @@ "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", + "create_preset_prerequisite": "To create presets for this bed, you'll need to link the camera to the bed first.", "create_resource_request": "Create Resource Request", "created": "Created", "created_date": "Created Date", @@ -432,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", @@ -475,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.", @@ -516,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", @@ -527,14 +622,18 @@ "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", "feed_optimal_experience_for_apple_phones": "For optimal viewing experience, consider rotating your device. Ensure auto-rotate is enabled in your device settings.", "feed_optimal_experience_for_phones": "For optimal viewing experience, consider rotating your device.", @@ -564,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", @@ -591,6 +718,7 @@ "international_mobile": "International Mobile", "invalid_asset_id_msg": "Oops! The asset ID you entered does not appear to be valid.", "invalid_email": "Please Enter a Valid Email Address", + "invalid_ip_address": "Invalid IP Address", "invalid_link_msg": "It appears that the password reset link you have used is either invalid or expired. Please request a new password reset link.", "invalid_password": "Password doesn't meet the requirements", "invalid_password_reset_link": "Invalid password reset link", @@ -634,6 +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", @@ -642,11 +774,14 @@ "litres_per_day": "Litres/day", "live": "Live", "live_monitoring": "Live Monitoring", + "live_patients_total_beds": "Live Patients / Total beds", "load_more": "Load More", "loading": "Loading...", "local_body": "Local body", - "local_ipaddress": "Local IP Address", + "local_ip_address": "Local IP Address", + "local_ip_address_example": "e.g. 192.168.0.123", "location": "Location", + "location_beds_empty": "No beds available in this location", "location_management": "Location Management", "log_lab_results": "Log Lab Results", "log_report": "Log Report", @@ -654,7 +789,9 @@ "longitude_invalid": "Longitude must be between -180 and 180", "lsg": "Lsg", "make_multiple_beds_label": "Do you want to make multiple beds?", + "manage_bed_presets": "Manage Presets of Bed", "manage_prescriptions": "Manage Prescriptions", + "manage_preset": "Manage preset {{ name }}", "manufacturer": "Manufacturer", "map_acronym": "M.A.P.", "mark_all_as_read": "Mark all as Read", @@ -672,15 +809,25 @@ "medicines_administered": "Medicine(s) administered", "medicines_administered_error": "Error administering medicine(s)", "middleware_hostname": "Middleware Hostname", + "middleware_hostname_example": "e.g. example.ohc.network", + "middleware_hostname_sourced_from": "Middleware hostname sourced from {{ source }}", "min_password_len_8": "Minimum password length 8", "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", "moving_camera": "Moving Camera", "name": "Name", "name_of_hospital": "Name of Hospital", @@ -710,6 +857,8 @@ "no_patients_to_show": "No patients to show.", "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", @@ -732,9 +881,11 @@ "nursing_care": "Nursing Care", "nursing_information": "Nursing Information", "nutrition": "Nutrition", + "occupancy": "Occupancy", "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", @@ -743,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", @@ -755,6 +908,7 @@ "password_reset_success": "Password Reset successfully", "password_sent": "Password Reset Email Sent", "patient_address": "Patient Address", + "patient_body": "Patient Body", "patient_category": "Patient Category", "patient_consultation__admission": "Date of admission", "patient_consultation__consultation_notes": "General Instructions", @@ -769,6 +923,8 @@ "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", "patient_notes_thread__Doctors": "Doctor's Discussions", @@ -811,6 +967,7 @@ "policy__policy_id__example": "POL001", "policy__subscriber_id": "Member ID", "policy__subscriber_id__example": "SUB001", + "position": "Position", "post_your_comment": "Post Your Comment", "powered_by": "Powered By", "preferred_facility_type": "Preferred Facility Type", @@ -825,6 +982,9 @@ "prescriptions__medicine": "Medicine", "prescriptions__route": "Route", "prescriptions__start_date": "Prescribed On", + "preset_deleted": "Preset deleted", + "preset_name_placeholder": "Specify an identifiable name for the new preset", + "preset_updated": "Preset updated", "prev_sessions": "Prev Sessions", "principal": "Principal", "principal_diagnosis": "Principal diagnosis", @@ -837,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", @@ -855,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", @@ -867,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", @@ -888,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", @@ -907,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.", @@ -922,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", @@ -939,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", @@ -961,6 +1131,7 @@ "stop": "Stop", "stream_stop_due_to_inativity": "The live feed will stop streaming due to inactivity", "stream_stopped_due_to_inativity": "The live feed has stopped streaming due to inactivity", + "stream_uuid": "Stream UUID", "sub_category": "Sub Category", "submit": "Submit", "submitting": "Submitting", @@ -989,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", @@ -996,9 +1168,13 @@ "type_to_search": "Type to search", "type_your_comment": "Type your comment", "type_your_reason_here": "Type your reason here", + "unable_to_get_current_position": "Unable to get current position.", "unconfirmed": "Unconfirmed", "unique_id": "Unique Id", "unknown": "Unknown", + "unlink_asset_bed_and_presets": "Delete linked presets and unlink bed", + "unlink_asset_bed_caution": "This action will also delete all presets that are associated to this camera and bed.", + "unlink_camera_and_bed": "Unlink this bed from this camera", "unsubscribe": "Unsubscribe", "unsubscribe_failed": "Unsubscribe failed.", "unsupported_browser": "Unsupported Browser", @@ -1013,11 +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", @@ -1026,22 +1205,31 @@ "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", "view_asset": "View Assets", + "view_cns": "View CNS", "view_details": "View Details", "view_faciliy": "View Facility", "view_patients": "View Patients", "view_users": "View Users", "virtual_nursing_assistant": "Virtual Nursing Assistant", "vitals": "Vitals", + "vitals_monitor": "Vitals Monitor", "ward": "Ward", "warranty_amc_expiry": "Warranty / AMC Expiry", "what_facility_assign_the_patient_to": "What facility would you like to assign the patient to", diff --git a/src/Locale/hi.json b/src/Locale/hi.json index 93d9ef32f34..e235c558978 100644 --- a/src/Locale/hi.json +++ b/src/Locale/hi.json @@ -458,7 +458,7 @@ "load_more": "और लोड करें", "loading": "लोड हो रहा है...", "local_body": "स्थानीय निकाय", - "local_ipaddress": "स्थानीय आईपी पता", + "local_ip_address": "स्थानीय आईपी पता", "location": "जगह", "location_management": "स्थान प्रबंधन", "log_lab_results": "लॉग लैब परिणाम", @@ -810,4 +810,4 @@ "you_need_at_least_a_location_to_create_an_assest": "संपत्ति बनाने के लिए आपको कम से कम एक स्थान की आवश्यकता होगी।", "zoom_in": "ज़ूम इन", "zoom_out": "ज़ूम आउट" -} +} \ No newline at end of file diff --git a/src/Locale/kn.json b/src/Locale/kn.json index acaebe8e421..dc46e49394f 100644 --- a/src/Locale/kn.json +++ b/src/Locale/kn.json @@ -459,7 +459,7 @@ "load_more": "ಇನ್ನಷ್ಟು ಲೋಡ್ ಮಾಡಿ", "loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ...", "local_body": "ಸ್ಥಳೀಯ ಸಂಸ್ಥೆ", - "local_ipaddress": "ಸ್ಥಳೀಯ IP ವಿಳಾಸ", + "local_ip_address": "ಸ್ಥಳೀಯ IP ವಿಳಾಸ", "location": "ಸ್ಥಳ", "location_management": "ಸ್ಥಳ ನಿರ್ವಹಣೆ", "log_lab_results": "ಲಾಗ್ ಲ್ಯಾಬ್ ಫಲಿತಾಂಶಗಳು", @@ -810,4 +810,4 @@ "you_need_at_least_a_location_to_create_an_assest": "ಆಸ್ತಿಯನ್ನು ರಚಿಸಲು ನಿಮಗೆ ಕನಿಷ್ಠ ಸ್ಥಳದ ಅಗತ್ಯವಿದೆ.", "zoom_in": "ಜೂಮ್ ಇನ್", "zoom_out": "ಜೂಮ್ ಔಟ್" -} +} \ No newline at end of file diff --git a/src/Locale/ml.json b/src/Locale/ml.json index 8460b6f8b7c..d830d3a46ec 100644 --- a/src/Locale/ml.json +++ b/src/Locale/ml.json @@ -458,7 +458,7 @@ "load_more": "കൂടുതൽ ലോഡ് ചെയ്യുക", "loading": "ലോഡ് ചെയ്യുന്നു...", "local_body": "തദ്ദേശ സ്ഥാപനം", - "local_ipaddress": "പ്രാദേശിക ഐപി വിലാസം", + "local_ip_address": "പ്രാദേശിക ഐപി വിലാസം", "location": "സ്ഥാനം", "location_management": "ലൊക്കേഷൻ മാനേജ്മെൻ്റ്", "log_lab_results": "ലോഗ് ലാബ് ഫലങ്ങൾ", @@ -810,4 +810,4 @@ "you_need_at_least_a_location_to_create_an_assest": "ഒരു അസസ്‌റ്റ് സൃഷ്‌ടിക്കാൻ നിങ്ങൾക്ക് ഒരു ലൊക്കേഷനെങ്കിലും ആവശ്യമാണ്.", "zoom_in": "സൂം ഇൻ ചെയ്യുക", "zoom_out": "സൂം ഔട്ട്" -} +} \ No newline at end of file diff --git a/src/Locale/ta.json b/src/Locale/ta.json index 1fbf82c29aa..042f5a068c4 100644 --- a/src/Locale/ta.json +++ b/src/Locale/ta.json @@ -458,7 +458,7 @@ "load_more": "மேலும் ஏற்றவும்", "loading": "ஏற்றுகிறது...", "local_body": "உள்ளூர் அமைப்பு", - "local_ipaddress": "உள்ளூர் ஐபி முகவரி", + "local_ip_address": "உள்ளூர் ஐபி முகவரி", "location": "இடம்", "location_management": "இருப்பிட மேலாண்மை", "log_lab_results": "பதிவு ஆய்வக முடிவுகள்", @@ -810,4 +810,4 @@ "you_need_at_least_a_location_to_create_an_assest": "ஒரு அசெஸ்ட்டை உருவாக்க குறைந்தபட்சம் ஒரு இருப்பிடமாவது தேவை.", "zoom_in": "பெரிதாக்கவும்", "zoom_out": "பெரிதாக்கவும்" -} +} \ No newline at end of file diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 47d3530d065..29fe2fed2ef 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1,29 +1,5 @@ import { fireRequest } from "./fireRequest"; -// asset bed -export const listAssetBeds = (params: object, altKey?: string) => - fireRequest("listAssetBeds", [], params, {}, altKey); - -export const partialUpdateAssetBed = (params: object, asset_id: string) => - fireRequest( - "partialUpdateAssetBed", - [], - { ...params }, - { - external_id: asset_id, - }, - ); - -export const deleteAssetBed = (asset_id: string) => - fireRequest( - "deleteAssetBed", - [], - {}, - { - external_id: asset_id, - }, - ); - export const getPatient = (pathParam: object) => { return fireRequest("getPatient", [], {}, pathParam); }; @@ -41,6 +17,3 @@ export const getConsultation = (id: string) => { export const dischargePatient = (params: object, pathParams: object) => { return fireRequest("dischargePatient", [], params, pathParams); }; - -export const operateAsset = (id: string, params: object) => - fireRequest("operateAsset", [], params, { external_id: id }); 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; diff --git a/src/Utils/transformUtils.ts b/src/Utils/transformUtils.ts index 0050a0bcbb6..4aa63da734c 100644 --- a/src/Utils/transformUtils.ts +++ b/src/Utils/transformUtils.ts @@ -1,16 +1,23 @@ import { AssetData } from "../Components/Assets/AssetTypes"; -export const getCameraConfig = (asset: AssetData) => { - const { meta } = asset; +export const getCameraConfig = (meta: AssetData["meta"]) => { return { middleware_hostname: meta?.middleware_hostname, - id: asset?.id, hostname: meta?.local_ip_address, username: meta?.camera_access_key?.split(":")[0], password: meta?.camera_access_key?.split(":")[1], accessKey: meta?.camera_access_key?.split(":")[2], port: 80, - location_id: asset?.location_object?.id, - facility_id: asset?.location_object?.facility?.id, }; }; + +export const makeAccessKey = ( + attrs: Pick< + ReturnType, + "username" | "password" | "accessKey" + >, +) => { + return [attrs.username, attrs.password, attrs.accessKey] + .map((a) => a ?? "") + .join(":"); +};