setup care abdm plugin and added manage patient options component with necessary setup

This commit is contained in:
Khavin Shankar 2024-11-03 11:25:47 +05:30
commit 94df59f4ab
10 changed files with 750 additions and 0 deletions

49
.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependencies
node_modules
.pnp
.pnp.js
# Testing
coverage
# Production
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# TypeScript
*.tsbuildinfo
# Vite
vite.config.ts.timestamp-*
# Temporary files
*.tmp
*.bak

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "care_abdm_fe",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {},
"peerDependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}

253
src/api.tsx Normal file
View File

@ -0,0 +1,253 @@
import { Type } from "@/Redux/api";
import { PaginatedResponse } from "@/Utils/request/types";
import {
AbhaNumberModel,
ConsentRequestModel,
CreateConsentTBody,
HealthFacilityModel,
HealthInformationModel,
IcreateHealthFacilityTBody,
IpartialUpdateHealthFacilityTBody,
} from "./types";
const routes = {
consent: {
list: {
path: "/api/abdm/consent/",
method: "GET",
TRes: Type<PaginatedResponse<ConsentRequestModel>>(),
},
create: {
path: "/api/abdm/consent/",
method: "POST",
TRes: Type<ConsentRequestModel>(),
TBody: Type<CreateConsentTBody>(),
},
get: {
path: "/api/abdm/consent/{id}/",
method: "GET",
},
checkStatus: {
path: "/api/abdm/v3/hiu/consent_request_status/",
method: "POST",
TBody: Type<{
consent_request: string;
}>(),
TRes: Type<{
detail: string;
}>(),
},
},
healthInformation: {
get: {
path: "/api/abdm/health_information/{artefactId}",
method: "GET",
TRes: Type<HealthInformationModel>(),
},
},
healthFacility: {
list: {
path: "/api/abdm/health_facility/",
method: "GET",
},
create: {
path: "/api/abdm/health_facility/",
method: "POST",
TRes: Type<HealthFacilityModel>(),
TBody: Type<IcreateHealthFacilityTBody>(),
},
get: {
path: "/api/abdm/health_facility/{facility_id}/",
method: "GET",
TRes: Type<HealthFacilityModel>(),
},
update: {
path: "/api/abdm/health_facility/{facility_id}/",
method: "PUT",
TRes: Type<HealthFacilityModel>(),
TBody: Type<IcreateHealthFacilityTBody>(),
},
partialUpdate: {
path: "/api/abdm/health_facility/{facility_id}/",
method: "PATCH",
TRes: Type<HealthFacilityModel>(),
TBody: Type<IpartialUpdateHealthFacilityTBody>(),
},
registerAsService: {
path: "/api/abdm/health_facility/{facility_id}/register_service/",
method: "POST",
TRes: Type<HealthFacilityModel>(),
TBody: Type<IcreateHealthFacilityTBody>(),
},
},
abhaNumber: {
get: {
path: "/api/abdm/abha_number/{abhaNumberId}/",
method: "GET",
TRes: Type<AbhaNumberModel>(),
},
create: {
path: "/api/abdm/abha_number/",
method: "POST",
TBody: Type<Partial<AbhaNumberModel>>(),
TRes: Type<AbhaNumberModel>(),
},
},
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;
}>(),
},
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;
}>(),
},
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;
}>(),
},
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;
}>(),
},
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[];
}>(),
},
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;
}>(),
},
linkAbhaNumberAndPatient: {
path: "/api/abdm/v3/health_id/link_patient/",
method: "POST",
TBody: Type<{
abha_number: string;
patient: string;
}>(),
TRes: Type<{
detail: string;
}>(),
},
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[];
}>(),
},
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;
}>(),
},
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;
}>(),
},
getAbhaCard: {
path: "/api/abdm/v3/health_id/abha_card",
method: "GET",
TRes: Type<Blob>(),
},
},
} as const;
export default routes;

View File

@ -0,0 +1,31 @@
import { useConsultation } from "@/components/Facility/ConsultationDetails/ConsultationContext";
import useQuery from "@/Utils/request/useQuery";
import routes from "../api";
import { AbhaNumberModel, HealthFacilityModel } from "../types";
const ConsultationContextEnabler = () => {
const { patient, setValue } = useConsultation<{
abhaNumber?: AbhaNumberModel;
healthFacility?: HealthFacilityModel;
}>();
useQuery(routes.abhaNumber.get, {
pathParams: { abhaNumberId: patient?.id ?? "" },
silent: true,
onResponse(res) {
setValue("abhaNumber", res.data);
},
});
useQuery(routes.healthFacility.get, {
pathParams: { facility_id: patient?.facility ?? "" },
silent: true,
onResponse(res) {
setValue("healthFacility", res.data);
},
});
return <></>;
};
export default ConsultationContextEnabler;

View File

@ -0,0 +1,78 @@
import ABHAProfileModal from "@/components/ABDM/ABHAProfileModal";
import FetchRecordsModal from "@/components/ABDM/FetchRecordsModal";
import * as Notification from "@/Utils/Notifications";
import routes from "../api";
import LinkAbhaNumber from "@/components/ABDM/LinkAbhaNumber";
import request from "@/Utils/request/request";
import { useQueryParams } from "raviger";
import { ExtendPatientInfoCardComponentType } from "@/pluginTypes";
import { useTranslation } from "react-i18next";
import { useConsultation } from "@/components/Facility/ConsultationDetails/ConsultationContext";
import { AbhaNumberModel } from "../types";
const ExtendPatientInfoCard: ExtendPatientInfoCardComponentType = ({
patient,
fetchPatientData,
}) => {
const { t } = useTranslation();
const [qParams, setQParams] = useQueryParams();
const { abhaNumber } = useConsultation<{
abhaNumber?: AbhaNumberModel;
}>();
return (
<>
<LinkAbhaNumber
show={qParams.show_link_abha_number === "true"}
onClose={() => {
setQParams({ ...qParams, show_link_abha_number: undefined });
}}
onSuccess={async (abhaProfile) => {
const { res, data } = await request(
routes.healthId.linkAbhaNumberAndPatient,
{
body: {
patient: patient.id,
abha_number: abhaProfile.external_id,
},
},
);
if (res?.status === 200 && data) {
Notification.Success({
msg: t("abha_number_linked_successfully"),
});
fetchPatientData?.({ aborted: false });
setQParams({
...qParams,
show_link_abha_number: undefined,
show_abha_profile: "true",
});
} else {
Notification.Error({
msg: t("failed_to_link_abha_number"),
});
}
}}
/>
<ABHAProfileModal
patientId={patient.id}
abha={abhaNumber}
show={qParams.show_abha_profile === "true"}
onClose={() => {
setQParams({ ...qParams, show_abha_profile: undefined });
}}
/>
<FetchRecordsModal
abha={abhaNumber}
show={qParams.show_fetch_records === "true"}
onClose={() => {
setQParams({ ...qParams, show_fetch_records: undefined });
}}
/>
</>
);
};
export default ExtendPatientInfoCard;

View File

@ -0,0 +1,124 @@
import { triggerGoal } from "@core/Integrations/Plausible";
import useAuthUser from "@core/common/hooks/useAuthUser";
import { ManagePatientOptionsComponentType } from "@/pluginTypes";
import CareIcon from "@/CAREUI/icons/CareIcon";
import { useTranslation } from "react-i18next";
import { useConsultation } from "@/components/Facility/ConsultationDetails/ConsultationContext";
import { MenuItem } from "@headlessui/react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useQueryParams } from "raviger";
import { AbhaNumberModel, HealthFacilityModel } from "../types";
const ManagePatientOptions: ManagePatientOptionsComponentType = ({
consultation,
}) => {
const { t } = useTranslation();
const [qParams, setQParams] = useQueryParams();
const authUser = useAuthUser();
const { abhaNumber, healthFacility } = useConsultation<{
abhaNumber?: AbhaNumberModel;
healthFacility?: HealthFacilityModel;
}>();
if (!consultation) {
return null;
}
return (
<div>
{abhaNumber ? (
<>
<MenuItem>
{({ close }) => (
<>
<div
className="dropdown-item-primary pointer-events-auto m-2 flex cursor-pointer items-center justify-start gap-2 rounded border-0 p-2 text-sm font-normal transition-all duration-200 ease-in-out"
onClick={() => {
close();
setQParams({ ...qParams, show_abha_profile: "true" });
triggerGoal("Patient Card Button Clicked", {
buttonName: t("show_abha_profile"),
consultationId: consultation?.id,
userId: authUser?.id,
});
}}
>
<CareIcon
icon="l-user-square"
className="text-lg text-primary-500"
/>
<span>{t("show_abha_profile")}</span>
</div>
<div
className="dropdown-item-primary pointer-events-auto m-2 flex cursor-pointer items-center justify-start gap-2 rounded border-0 p-2 text-sm font-normal transition-all duration-200 ease-in-out"
onClick={() => {
close();
setQParams({ ...qParams, show_fetch_records: "true" });
triggerGoal("Patient Card Button Clicked", {
buttonName: t("hi__fetch_records"),
consultationId: consultation?.id,
userId: authUser?.id,
});
}}
>
<CareIcon
icon="l-file-network"
className="text-lg text-primary-500"
/>
<span>{t("hi__fetch_records")}</span>
</div>
</>
)}
</MenuItem>
</>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<MenuItem disabled={!healthFacility}>
{({ close, disabled }) => (
<div
className={cn(
"dropdown-item-primary pointer-events-auto m-2 flex cursor-pointer items-center justify-start gap-2 rounded border-0 p-2 text-sm font-normal transition-all duration-200 ease-in-out",
disabled && "pointer-events-none opacity-30",
)}
onClick={() => {
close();
setQParams({
...qParams,
show_link_abha_number: "true",
});
}}
>
<span className="flex w-full items-center justify-start gap-2">
<CareIcon
icon="l-link"
className="text-lg text-primary-500"
/>
<p>{t("generate_link_abha")}</p>
</span>
</div>
)}
</MenuItem>
</TooltipTrigger>
{!healthFacility && (
<TooltipContent className="max-w-sm break-words text-sm">
{t("abha_disabled_due_to_no_health_facility")}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)}
</div>
);
};
export default ManagePatientOptions;

4
src/index.ts Normal file
View File

@ -0,0 +1,4 @@
export { default as ManagePatientOptions } from "./components/ManagePatientOptions";
export { default as manifest } from "./manifest";
export { default as routes } from "./routes";

26
src/manifest.ts Normal file
View File

@ -0,0 +1,26 @@
import { lazy } from "react";
import routes from "./routes";
import { PluginManifest } from "@/pluginTypes";
const manifest: PluginManifest = {
plugin: "care_abdm",
routes,
extends: [],
components: {
ConsultationContextEnabler: lazy(
() => import("./components/ConsultationContextEnabler"),
),
ExtendPatientInfoCard: lazy(
() => import("./components/ExtendPatientInfoCard"),
),
ManagePatientOptions: lazy(
() => import("./components/ManagePatientOptions"),
),
// TODO: manage facility options
// TODO: patient registration form
// TODO: facility configuration form
},
navItems: [],
};
export default manifest;

5
src/routes.tsx Normal file
View File

@ -0,0 +1,5 @@
import { AppRoutes } from "@core/Routers/AppRouter";
const routes: AppRoutes = {};
export default routes;

160
src/types.ts Normal file
View File

@ -0,0 +1,160 @@
import { PatientModel } from "@/components/Patient/models";
import { UserBaseModel } from "@/components/Users/models";
export type AbhaNumberModel = {
id: number;
external_id: string;
created_date: string;
modified_date: string;
abha_number: string;
health_id: string;
name: string;
first_name: string | null;
middle_name: string | null;
last_name: string | null;
gender: "F" | "M" | "O";
date_of_birth: string | null;
address: string | null;
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 }
);
export type ConsentPurpose =
| "CAREMGT"
| "BTG"
| "PUBHLTH"
| "HPAYMT"
| "DSRCH"
| "PATRQT";
export type ConsentStatus =
| "REQUESTED"
| "GRANTED"
| "DENIED"
| "EXPIRED"
| "REVOKED";
export type ConsentHIType =
| "Prescription"
| "DiagnosticReport"
| "OPConsultation"
| "DischargeSummary"
| "ImmunizationRecord"
| "HealthDocumentRecord"
| "WellnessRecord";
export type ConsentAccessMode = "VIEW" | "STORE" | "QUERY" | "STREAM";
export type ConsentFrequencyUnit = "HOUR" | "DAY" | "WEEK" | "MONTH" | "YEAR";
export type ConsentCareContext = {
patientReference: string;
careContextReference: string;
};
export type ConsentModel = {
id: string;
consent_id: null | string;
patient_abha: string;
care_contexts: ConsentCareContext[];
status: ConsentStatus;
purpose: ConsentPurpose;
hi_types: ConsentHIType[];
access_mode: ConsentAccessMode;
from_time: string;
to_time: string;
expiry: string;
frequency_unit: ConsentFrequencyUnit;
frequency_value: number;
frequency_repeats: number;
hip: null | string;
hiu: null | string;
created_date: string;
modified_date: string;
};
export type CreateConsentTBody = {
patient_abha: string;
hi_types: ConsentHIType[];
purpose: ConsentPurpose;
from_time: Date | string;
to_time: Date | string;
expiry: Date | string;
access_mode?: ConsentAccessMode;
frequency_unit?: ConsentFrequencyUnit;
frequency_value?: number;
frequency_repeats?: number;
hip?: null | string;
};
export type ConsentArtefactModel = {
consent_request: string;
cm: null | string;
} & ConsentModel;
export type ConsentRequestModel = {
requester: UserBaseModel;
patient_abha_object: AbhaNumberModel;
consent_artefacts: ConsentArtefactModel[];
} & ConsentModel;
export type HealthFacilityModel = {
id: string;
registered: boolean;
external_id: string;
created_date: string;
modified_date: string;
hf_id: string;
facility: string;
detail?: string;
};
export type IcreateHealthFacilityTBody = {
facility: string;
hf_id: string;
};
export type IpartialUpdateHealthFacilityTBody = {
hf_id: string;
};
export type HealthInformationModel = {
data: {
content: string;
care_context_reference: string;
}[];
};