From c1cfd19318ae51a72f52f7baa7b3dbb773d72904 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 17 Mar 2023 20:13:26 +0530 Subject: [PATCH] HCX Integration - Claims Disabled (#5108) Co-authored-by: Mathew Alex Co-authored-by: rithviknishad Co-authored-by: Gigin George --- .env | 2 +- .github/workflows/codeql-analysis.yml | 1 + .github/workflows/comment-p1-issues.yml | 2 +- .github/workflows/cypress.yaml | 1 + .github/workflows/deploy.yaml | 1 + .github/workflows/issue-automation.yml | 6 +- .github/workflows/label-deploy-failed.yml | 2 +- .github/workflows/label-merge-conflict.yml | 1 + .github/workflows/label-wip.yml | 3 +- .github/workflows/ossar-analysis.yml | 1 + .github/workflows/stale.yml | 1 + netlify.toml | 2 +- package.json | 4 +- src/Common/hooks/useConfig.ts | 4 + src/Common/hooks/useMessageListener.ts | 16 + src/Components/Common/Dialog.tsx | 16 +- .../PMJAYProcedurePackageAutocomplete.tsx | 61 ++ .../prescription-builder/ProcedureBuilder.tsx | 3 +- .../Facility/ConsultationClaims.tsx | 93 +++ .../Facility/ConsultationDetails.tsx | 71 ++- .../Form/FormFields/Autocomplete.tsx | 49 +- .../Form/FormFields/SelectFormField.tsx | 2 +- src/Components/HCX/ClaimCreatedModal.tsx | 57 ++ src/Components/HCX/ClaimDetailCard.tsx | 162 +++++ src/Components/HCX/ClaimsItemsBuilder.tsx | 164 +++++ src/Components/HCX/CreateClaimCard.tsx | 242 ++++++++ .../HCX/InsuranceDetailsBuilder.tsx | 150 +++++ src/Components/HCX/InsurerAutocomplete.tsx | 41 ++ .../HCX/PatientInsuranceDetailsEditor.tsx | 132 ++++ src/Components/HCX/PolicyEligibilityCheck.tsx | 190 ++++++ src/Components/HCX/constants.ts | 58 ++ src/Components/HCX/misc.ts | 9 + src/Components/HCX/models.ts | 76 +++ src/Components/HCX/validators.ts | 15 + src/Components/Patient/FileUpload.tsx | 23 +- src/Components/Patient/PatientInfoCard.tsx | 91 +-- src/Components/Patient/PatientRegister.tsx | 110 +++- src/Redux/actions.tsx | 84 +++ src/Redux/api.tsx | 82 +++ src/Router/AppRouter.tsx | 575 +++++++++--------- src/Utils/utils.ts | 6 + src/service-worker.ts | 18 +- vercel.json | 2 +- 43 files changed, 2266 insertions(+), 363 deletions(-) create mode 100644 src/Common/hooks/useMessageListener.ts create mode 100644 src/Components/Common/PMJAYProcedurePackageAutocomplete.tsx create mode 100644 src/Components/Facility/ConsultationClaims.tsx create mode 100644 src/Components/HCX/ClaimCreatedModal.tsx create mode 100644 src/Components/HCX/ClaimDetailCard.tsx create mode 100644 src/Components/HCX/ClaimsItemsBuilder.tsx create mode 100644 src/Components/HCX/CreateClaimCard.tsx create mode 100644 src/Components/HCX/InsuranceDetailsBuilder.tsx create mode 100644 src/Components/HCX/InsurerAutocomplete.tsx create mode 100644 src/Components/HCX/PatientInsuranceDetailsEditor.tsx create mode 100644 src/Components/HCX/PolicyEligibilityCheck.tsx create mode 100644 src/Components/HCX/constants.ts create mode 100644 src/Components/HCX/misc.ts create mode 100644 src/Components/HCX/models.ts create mode 100644 src/Components/HCX/validators.ts diff --git a/.env b/.env index 7e27e19a0d2..73e0544cd46 100644 --- a/.env +++ b/.env @@ -6,4 +6,4 @@ REACT_APP_COVER_IMAGE=https://cdn.coronasafe.network/care_logo.svg REACT_APP_COVER_IMAGE_ALT=https://cdn.coronasafe.network/care_logo.svg # Dev envs -ESLINT_NO_DEV_ERRORS=true \ No newline at end of file +ESLINT_NO_DEV_ERRORS=true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2bbbe132c0d..b91d810cadd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,6 +9,7 @@ on: jobs: CodeQL-Build: runs-on: ubuntu-latest + if: github.repository == 'coronasafe/care_fe' permissions: security-events: write actions: read diff --git a/.github/workflows/comment-p1-issues.yml b/.github/workflows/comment-p1-issues.yml index 50c1c1c1743..930b7d14c90 100644 --- a/.github/workflows/comment-p1-issues.yml +++ b/.github/workflows/comment-p1-issues.yml @@ -7,7 +7,7 @@ on: jobs: add-comment: - if: github.event.label.name == 'P1' + if: github.event.label.name == 'P1' && github.repository == 'coronasafe/care_fe' runs-on: ubuntu-latest permissions: issues: write diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 9e9821ced48..b948166742d 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -11,6 +11,7 @@ on: jobs: cypress-run: + if: github.repository == 'coronasafe/care_fe' runs-on: ubuntu-latest steps: - name: Checkout 📥 diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 1a87592e2f2..1d69c6610e6 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -21,6 +21,7 @@ env: jobs: test: + if: github.repository == 'coronasafe/care_fe' runs-on: ubuntu-latest name: Test steps: diff --git a/.github/workflows/issue-automation.yml b/.github/workflows/issue-automation.yml index 6410abadf78..421243bf605 100644 --- a/.github/workflows/issue-automation.yml +++ b/.github/workflows/issue-automation.yml @@ -10,7 +10,7 @@ jobs: issue_opened_and_reopened: name: issue_opened_and_reopened runs-on: ubuntu-latest - if: github.event_name == 'issues' && github.event.action == 'opened' || github.event.action == 'reopened' + if: github.repository == 'coronasafe/care_fe' && github.event_name == 'issues' && github.event.action == 'opened' || github.event.action == 'reopened' steps: - name: 'Move issue to "Triage"' uses: leonsteinhaeuser/project-beta-automations@v1.2.1 @@ -23,7 +23,7 @@ jobs: issue_closed: name: issue_closed runs-on: ubuntu-latest - if: github.event_name == 'issues' && github.event.action == 'closed' + if: github.repository == 'coronasafe/care_fe' && github.event_name == 'issues' && github.event.action == 'closed' steps: - name: 'Moved issue to "Done"' uses: leonsteinhaeuser/project-beta-automations@v1.2.1 @@ -36,7 +36,7 @@ jobs: issue_assigned: name: issue_assigned runs-on: ubuntu-latest - if: github.event_name == 'issues' && github.event.action == 'assigned' + if: github.repository == 'coronasafe/care_fe' && github.event_name == 'issues' && github.event.action == 'assigned' steps: - name: 'Move issue to "In Progress"' uses: leonsteinhaeuser/project-beta-automations@v1.2.1 diff --git a/.github/workflows/label-deploy-failed.yml b/.github/workflows/label-deploy-failed.yml index f35f5331acf..cbb6d269891 100644 --- a/.github/workflows/label-deploy-failed.yml +++ b/.github/workflows/label-deploy-failed.yml @@ -13,7 +13,7 @@ jobs: auto-label-deploy-failed: runs-on: ubuntu-latest if: | - github.event.issue.pull_request && + github.repository == 'coronasafe/care_fe' && github.event.issue.pull_request && contains(github.event.comment.body, 'Deploy Preview') steps: - name: Add 'Deploy-Failed' diff --git a/.github/workflows/label-merge-conflict.yml b/.github/workflows/label-merge-conflict.yml index 3a4a2fec949..5ae616effa7 100644 --- a/.github/workflows/label-merge-conflict.yml +++ b/.github/workflows/label-merge-conflict.yml @@ -12,6 +12,7 @@ on: jobs: auto-label: + if: github.repository == 'coronasafe/care_fe' runs-on: ubuntu-latest steps: - uses: prince-chrismc/label-merge-conflicts-action@v2 diff --git a/.github/workflows/label-wip.yml b/.github/workflows/label-wip.yml index 4c39b62a685..786710a6d8a 100644 --- a/.github/workflows/label-wip.yml +++ b/.github/workflows/label-wip.yml @@ -9,6 +9,7 @@ on: jobs: check-linked-issues: + if: github.repository == 'coronasafe/care_fe' name: Check linked issues runs-on: ubuntu-latest outputs: @@ -28,7 +29,7 @@ jobs: runs-on: ubuntu-latest needs: check-linked-issues permissions: write-all - if: join(needs.check-linked-issues.outputs.linked_issues) != '' + if: github.repository == 'coronasafe/care_fe' && 'join(needs.check-linked-issues.outputs.linked_issues) != '' steps: - name: Label uses: actions/github-script@v6 diff --git a/.github/workflows/ossar-analysis.yml b/.github/workflows/ossar-analysis.yml index 9488125ca7b..213761fce97 100644 --- a/.github/workflows/ossar-analysis.yml +++ b/.github/workflows/ossar-analysis.yml @@ -9,6 +9,7 @@ on: jobs: OSSAR-Scan: + if: github.repository == 'coronasafe/care_fe' # OSSAR runs on windows-latest. # ubuntu-latest and macos-latest support coming soon runs-on: windows-latest diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 49d44ee008a..0e4c9005787 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,6 +6,7 @@ on: jobs: stale: runs-on: ubuntu-latest + if: github.repository == 'coronasafe/care_fe' steps: - uses: actions/stale@v6 with: diff --git a/netlify.toml b/netlify.toml index 0747b7a495a..e7410b19e80 100644 --- a/netlify.toml +++ b/netlify.toml @@ -25,7 +25,7 @@ NODE_OPTIONS = "--max_old_space_size=4096" [[redirects]] from = "/api/*" -to = "https://careapi.coronasafe.in/api/:splat" +to = "https://carehcxapi.coronasafe.in/api/:splat" status = 200 force = true diff --git a/package.json b/package.json index 1f24e600de8..5a79db6646f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "coronasafe Contributors" ], "homepage": "https://care.coronasafe.in", - "proxy": "https://careapi.coronasafe.in", + "proxy": "https://carehcxapi.coronasafe.in", "main": "./src/index.tsx", "keywords": [ "Coronasafe", @@ -271,4 +271,4 @@ "engines": { "node": "16.x || 18.x" } -} \ No newline at end of file +} diff --git a/src/Common/hooks/useConfig.ts b/src/Common/hooks/useConfig.ts index ac7d8b5dd5e..eb168c7c4f5 100644 --- a/src/Common/hooks/useConfig.ts +++ b/src/Common/hooks/useConfig.ts @@ -49,6 +49,10 @@ export interface IConfig { * URL of the sample format for external result import. */ sample_format_external_result_import: string; + /** + * Env to enable HCX features + */ + enable_hcx: boolean; } const useConfig = () => { diff --git a/src/Common/hooks/useMessageListener.ts b/src/Common/hooks/useMessageListener.ts new file mode 100644 index 00000000000..eb5673da7fc --- /dev/null +++ b/src/Common/hooks/useMessageListener.ts @@ -0,0 +1,16 @@ +import { useEffect } from "react"; + +type onMessage = (data: any) => void; + +export const useMessageListener = (onMessage: onMessage) => { + useEffect(() => { + const handleMessage = (e: MessageEvent) => { + onMessage(e.data); + }; + navigator.serviceWorker.addEventListener("message", handleMessage); + + return () => { + navigator.serviceWorker.removeEventListener("message", handleMessage); + }; + }); +}; diff --git a/src/Components/Common/Dialog.tsx b/src/Components/Common/Dialog.tsx index 4b8a6596948..bfd4c87a3ea 100644 --- a/src/Components/Common/Dialog.tsx +++ b/src/Components/Common/Dialog.tsx @@ -9,6 +9,7 @@ type DialogProps = { onClose: () => void; children: React.ReactNode; className?: string; + titleAction?: React.ReactNode; fixedWidth?: boolean; }; @@ -35,7 +36,7 @@ const DialogModal = (props: DialogProps) => { leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
@@ -58,13 +59,16 @@ const DialogModal = (props: DialogProps) => { > -

{title}

+
+

{title}

+

+ {description} +

+
+ {props.titleAction}
-
-

{description}

-
{children} diff --git a/src/Components/Common/PMJAYProcedurePackageAutocomplete.tsx b/src/Components/Common/PMJAYProcedurePackageAutocomplete.tsx new file mode 100644 index 00000000000..f99e07ab5db --- /dev/null +++ b/src/Components/Common/PMJAYProcedurePackageAutocomplete.tsx @@ -0,0 +1,61 @@ +import { useAsyncOptions } from "../../Common/hooks/useAsyncOptions"; +import { listPMJYPackages } from "../../Redux/actions"; +import { Autocomplete } from "../Form/FormFields/Autocomplete"; +import FormField from "../Form/FormFields/FormField"; +import { + FormFieldBaseProps, + useFormFieldPropsResolver, +} from "../Form/FormFields/Utils"; + +type PMJAYPackageItem = { + name?: string; + code?: string; + price?: number; + package_name?: string; +}; + +type Props = FormFieldBaseProps; + +export default function PMJAYProcedurePackageAutocomplete(props: Props) { + const field = useFormFieldPropsResolver(props as any); + + const { fetchOptions, isLoading, options } = + useAsyncOptions("code"); + + return ( + + { + // TODO: update backend to return price as number instead + return { + ...o, + price: + o.price && parseFloat(o.price?.toString().replaceAll(",", "")), + }; + })} + optionLabel={optionLabel} + optionDescription={optionDescription} + optionValue={(option) => option} + onQuery={(query) => fetchOptions(listPMJYPackages(query))} + isLoading={isLoading} + /> + + ); +} + +const optionLabel = (option: PMJAYPackageItem) => { + if (option.name) return option.name; + if (option.package_name) return `${option.package_name} (Package)`; + return "Unknown"; +}; + +const optionDescription = (option: PMJAYPackageItem) => { + const code = option.code || "Unknown"; + const packageName = option.package_name || "Unknown"; + return `Package: ${packageName} (${code})`; +}; diff --git a/src/Components/Common/prescription-builder/ProcedureBuilder.tsx b/src/Components/Common/prescription-builder/ProcedureBuilder.tsx index 0dcece81781..ef548f31903 100644 --- a/src/Components/Common/prescription-builder/ProcedureBuilder.tsx +++ b/src/Components/Common/prescription-builder/ProcedureBuilder.tsx @@ -109,7 +109,8 @@ export default function ProcedureBuilder(props: Props) { {procedure.repetitive ? (
- Frequency{" *"} + Frequency + {" *"}
setActiveIdx(i)} diff --git a/src/Components/Facility/ConsultationClaims.tsx b/src/Components/Facility/ConsultationClaims.tsx new file mode 100644 index 00000000000..77bf7e8410d --- /dev/null +++ b/src/Components/Facility/ConsultationClaims.tsx @@ -0,0 +1,93 @@ +import { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { HCXActions } from "../../Redux/actions"; +import PageTitle from "../Common/PageTitle"; +import ClaimDetailCard from "../HCX/ClaimDetailCard"; +import CreateClaimCard from "../HCX/CreateClaimCard"; +import { HCXClaimModel } from "../HCX/models"; +import { useMessageListener } from "../../Common/hooks/useMessageListener"; +import { navigate } from "raviger"; +import * as Notification from "../../Utils/Notifications"; + +interface Props { + facilityId: string; + patientId: string; + consultationId: string; +} + +export default function ConsultationClaims({ + facilityId, + consultationId, + patientId, +}: Props) { + const dispatch = useDispatch(); + const [claims, setClaims] = useState(); + const [isCreateLoading, setIsCreateLoading] = useState(false); + + const fetchClaims = useCallback(async () => { + const res = await dispatch( + HCXActions.claims.list({ + ordering: "-modified_date", + consultation: consultationId, + }) + ); + + if (res.data && res.data.results) { + setClaims(res.data.results); + if (isCreateLoading) + Notification.Success({ msg: "Fetched Claim Approval Results" }); + } else { + if (isCreateLoading) + Notification.Success({ msg: "Error Fetched Claim Approval Results" }); + } + setIsCreateLoading(false); + }, [dispatch, consultationId]); + + useEffect(() => { + fetchClaims(); + }, [fetchClaims]); + + useMessageListener((data) => { + if ( + data.type === "MESSAGE" && + (data.from === "claim/on_submit" || data.from === "preauth/on_submit") && + data.message === "success" + ) { + fetchClaims(); + } + }); + + return ( +
+ { + navigate( + `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}` + ); + return false; + }} + /> + +
+
+ +
+ +
+ {claims?.map((claim) => ( +
+ +
+ ))} +
+
+
+ ); +} diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx index 3e182a709d8..1cd931c8ffd 100644 --- a/src/Components/Facility/ConsultationDetails.tsx +++ b/src/Components/Facility/ConsultationDetails.tsx @@ -1,11 +1,11 @@ import { navigate } from "raviger"; import { Button, CircularProgress } from "@material-ui/core"; import moment from "moment"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { statusType, useAbortableEffect } from "../../Common/utils"; import * as Notification from "../../Utils/Notifications"; -import { getConsultation, getPatient } from "../../Redux/actions"; +import { getConsultation, getPatient, HCXActions } from "../../Redux/actions"; import loadable from "@loadable/component"; import { ConsultationModel, ICD11DiagnosisModel } from "./models"; import { PatientModel } from "../Patient/models"; @@ -60,7 +60,13 @@ import PRNPrescriptionBuilder, { PRNPrescriptionType, } from "../Common/prescription-builder/PRNPrescriptionBuilder"; import { formatDate } from "../../Utils/utils"; +import CreateClaimCard from "../HCX/CreateClaimCard"; +import { HCXClaimModel } from "../HCX/models"; +import ClaimDetailCard from "../HCX/ClaimDetailCard"; +import { useMessageListener } from "../../Common/hooks/useMessageListener"; import Chip from "../../CAREUI/display/Chip"; +import useConfig from "../../Common/hooks/useConfig"; + interface PreDischargeFormInterface { discharge_reason: string; discharge_notes: string; @@ -112,6 +118,45 @@ export const ConsultationDetails = (props: any) => { ); const [PRNAdvice, setPRNAdvice] = useState([]); + const [latestClaim, setLatestClaim] = useState(); + const [isCreateClaimLoading, setIsCreateClaimLoading] = useState(false); + const { enable_hcx } = useConfig(); + + const fetchLatestClaim = useCallback(async () => { + const res = await dispatch( + HCXActions.claims.list({ + ordering: "-modified_date", + use: "claim", + consultation: consultationId, + }) + ); + + if (res.data?.results?.length) { + setLatestClaim(res.data.results[0]); + if (isCreateClaimLoading) + Notification.Success({ msg: "Fetched Claim Approval Results" }); + } else { + setLatestClaim(undefined); + if (isCreateClaimLoading) + Notification.Success({ msg: "Error Fetched Claim Approval Results" }); + } + setIsCreateClaimLoading(false); + }, [consultationId, dispatch]); + + useEffect(() => { + fetchLatestClaim(); + }, [fetchLatestClaim]); + + useMessageListener((data) => { + if ( + data.type === "MESSAGE" && + (data.from === "claim/on_submit" || data.from === "preauth/on_submit") && + data.message === "success" + ) { + fetchLatestClaim(); + } + }); + const handleClickOpen = () => { setOpen(true); }; @@ -446,7 +491,7 @@ export const ConsultationDetails = (props: any) => { } show={openDischargeDialog} onClose={handleDischargeClose} - className="md:max-w-2xl" + className="md:max-w-3xl" >
{ )}
+ {enable_hcx && ( + // TODO: if policy and approved pre-auth exists +
+

Claim Insurance

+ {latestClaim ? ( + + ) : ( + + )} +
+ )} +
{isSendingDischargeApi ? ( @@ -583,7 +646,7 @@ export const ConsultationDetails = (props: any) => { }, }} breadcrumbs={true} - backUrl={"/patients"} + backUrl="/patients" />
{patientData.is_active && ( diff --git a/src/Components/Form/FormFields/Autocomplete.tsx b/src/Components/Form/FormFields/Autocomplete.tsx index 5b96c68a6be..bb5d2a8a139 100644 --- a/src/Components/Form/FormFields/Autocomplete.tsx +++ b/src/Components/Form/FormFields/Autocomplete.tsx @@ -13,9 +13,11 @@ type AutocompleteFormFieldProps = FormFieldBaseProps & { options: T[]; optionLabel: OptionCallback; optionValue?: OptionCallback; + optionDescription?: OptionCallback; optionIcon?: OptionCallback; onQuery?: (query: string) => void; dropdownIcon?: React.ReactNode | undefined; + allowRawInput?: boolean; }; const AutocompleteFormField = ( @@ -36,7 +38,9 @@ const AutocompleteFormField = ( optionLabel={props.optionLabel} optionIcon={props.optionIcon} optionValue={props.optionValue} + optionDescription={props.optionDescription} onQuery={props.onQuery} + allowRawInput={props.allowRawInput} requiredError={field.error ? props.required : false} /> @@ -54,10 +58,12 @@ type AutocompleteProps = { optionLabel: OptionCallback; optionIcon?: OptionCallback; optionValue?: OptionCallback; + optionDescription?: OptionCallback; className?: string; onQuery?: (query: string) => void; requiredError?: boolean; isLoading?: boolean; + allowRawInput?: boolean; } & ( | { required?: false; @@ -82,16 +88,42 @@ export const Autocomplete = (props: AutocompleteProps) => { props.onQuery && props.onQuery(query); }, [query]); - const options = props.options.map((option) => { + const mappedOptions = props.options.map((option) => { const label = props.optionLabel(option); + const description = + props.optionDescription && props.optionDescription(option); return { label, - search: label.toLowerCase(), + description, + search: + label.toLowerCase() + (description ? description.toLowerCase() : ""), icon: props.optionIcon && props.optionIcon(option), value: props.optionValue ? props.optionValue(option) : option, }; }); + const getOptions = () => { + if (!query) return mappedOptions; + + const knownOption = mappedOptions.find( + (o) => o.value == props.value || o.label == props.value + ); + + if (knownOption) return mappedOptions; + return [ + { + label: query, + description: undefined, + search: query.toLowerCase(), + icon: , + value: query, + }, + ...mappedOptions, + ]; + }; + + const options = props.allowRawInput ? getOptions() : mappedOptions; + const value = options.find((o) => props.value == o.value); const filteredOptions = options.filter((o) => o.search.includes(query)); @@ -137,9 +169,16 @@ export const Autocomplete = (props: AutocompleteProps) => { className={dropdownOptionClassNames} value={option} > -
- {option.label} - {option.icon} +
+
+ {option.label} + {option.icon} +
+ {option.description && ( +
+ {option.description} +
+ )}
))} diff --git a/src/Components/Form/FormFields/SelectFormField.tsx b/src/Components/Form/FormFields/SelectFormField.tsx index 28153fd577a..3c6613bb662 100644 --- a/src/Components/Form/FormFields/SelectFormField.tsx +++ b/src/Components/Form/FormFields/SelectFormField.tsx @@ -17,7 +17,7 @@ type SelectFormFieldProps = FormFieldBaseProps & { }; export const SelectFormField = (props: SelectFormFieldProps) => { - const field = useFormFieldPropsResolver(props); + const field = useFormFieldPropsResolver(props); return ( void; +} + +export default function ClaimCreatedModal({ claim, ...props }: Props) { + const dispatch = useDispatch(); + const [isMakingClaim, setIsMakingClaim] = useState(false); + + const { use } = claim; + + const handleSubmit = async () => { + setIsMakingClaim(true); + + const res = await dispatch(HCXActions.makeClaim(claim.id!)); + if (res.data) { + Notification.Success({ msg: `${use} requested` }); + props.onClose(); + } + + setIsMakingClaim(false); + }; + return ( + + {isMakingClaim && ( + + )} + {isMakingClaim + ? `Requesting ${use === "Claim" ? "Claim" : "Pre-Authorization"}...` + : `Request ${use === "Claim" ? "Claim" : "Pre-Authorization"}`} + + } + > +
+ +
+
+ ); +} diff --git a/src/Components/HCX/ClaimDetailCard.tsx b/src/Components/HCX/ClaimDetailCard.tsx new file mode 100644 index 00000000000..f44215b83e4 --- /dev/null +++ b/src/Components/HCX/ClaimDetailCard.tsx @@ -0,0 +1,162 @@ +import { classNames, formatCurrency, formatDate } from "../../Utils/utils"; +import { HCXClaimModel } from "../HCX/models"; + +interface IProps { + claim: HCXClaimModel; +} + +export default function ClaimDetailCard({ claim }: IProps) { + const status = + claim.outcome === "Processing Complete" + ? claim.error_text + ? "Rejected" + : "Approved" + : "Pending"; + + return ( +
+
+
+

+ #{claim.id?.slice(0, 5)} +

+ +

+ Created on{" "} + + . +

+
+
+ {claim.use && ( + + {claim.use} + + )} + + {status} + +
+
+
+
+

+ {claim.policy_object?.policy_id || "NA"} +

+

Policy ID

+
+
+

+ {claim.policy_object?.subscriber_id || "NA"} +

+

Subscriber ID

+
+
+

+ {claim.policy_object?.insurer_id || "NA"} +

+

Insurer ID

+
+
+

+ {claim.policy_object?.insurer_name || "NA"} +

+

Insurer Name

+
+
+
+ + + + + + + + + + + {claim.items?.map((item) => ( + + + + + + + ))} + + + + + + + + + + + + + + +
+ Items + + Price +
+
{item.name}
+
{item.id}
+
+ {formatCurrency(item.price)} +
+ Total Claim Amount + + Total Claim Amount + + {claim.total_claim_amount && + formatCurrency(claim.total_claim_amount)} +
+ Total Amount Approved + + Total Amount Approved + + {claim.total_amount_approved + ? formatCurrency(claim.total_amount_approved) + : "NA"} +
+
+ {claim.error_text && ( +
+ {claim.error_text} +
+ )} +
+ ); +} diff --git a/src/Components/HCX/ClaimsItemsBuilder.tsx b/src/Components/HCX/ClaimsItemsBuilder.tsx new file mode 100644 index 00000000000..9b329759dfe --- /dev/null +++ b/src/Components/HCX/ClaimsItemsBuilder.tsx @@ -0,0 +1,164 @@ +import CareIcon from "../../CAREUI/icons/CareIcon"; +import ButtonV2 from "../Common/components/ButtonV2"; +import PMJAYProcedurePackageAutocomplete from "../Common/PMJAYProcedurePackageAutocomplete"; +import AutocompleteFormField from "../Form/FormFields/Autocomplete"; +import FormField, { FieldLabel } from "../Form/FormFields/FormField"; +import TextFormField from "../Form/FormFields/TextFormField"; +import { + FieldChangeEvent, + FormFieldBaseProps, + useFormFieldPropsResolver, +} from "../Form/FormFields/Utils"; +import { ITEM_CATEGORIES } from "./constants"; +import { HCXItemModel } from "./models"; + +type Props = FormFieldBaseProps; + +export default function ClaimsItemsBuilder(props: Props) { + const field = useFormFieldPropsResolver(props as any); + + const handleUpdate = (index: number) => { + return (event: FieldChangeEvent) => { + if (event.name === "hbp") { + field.handleChange( + (props.value || [])?.map((obj, i) => + i === index + ? { + ...obj, + id: event.value.code, + name: event.value.name, + price: event.value.price, + } + : obj + ) + ); + } else { + field.handleChange( + (props.value || [])?.map((obj, i) => + i === index ? { ...obj, [event.name]: event.value } : obj + ) + ); + } + }; + }; + + const handleRemove = (index: number) => { + return () => { + field.handleChange((props.value || [])?.filter((obj, i) => i !== index)); + }; + }; + + return ( + +
+ {props.value?.map((obj, index) => { + return ( +
+
+ + Item {index + 1} + + {!props.disabled && ( + + Delete + + + )} +
+ +
+ o.display} + optionValue={(o) => o.code} + value={obj.category} + onChange={handleUpdate(index)} + disabled={props.disabled} + errorClassName="hidden" + /> + {obj.category === "HBP" && !obj.id ? ( + <> + + + ) : ( + <> + o.code} + // optionDescription={(o) => o.name || ""} + // optionValue={(o) => o.code} + onChange={handleUpdate(index)} + value={obj.id} + disabled={props.disabled} + errorClassName="hidden" + /> + o.name || o.code} + // optionDescription={(o) => o.code} + // optionValue={(o) => o.name || o.code} + disabled={props.disabled} + errorClassName="hidden" + // options={PROCEDURES} + /> + + handleUpdate(index)({ + name: event.name, + value: parseFloat(event.value), + }) + } + disabled={props.disabled} + errorClassName="hidden" + /> + + )} +
+
+ ); + })} +
+
+ ); +} diff --git a/src/Components/HCX/CreateClaimCard.tsx b/src/Components/HCX/CreateClaimCard.tsx new file mode 100644 index 00000000000..c0a10f00216 --- /dev/null +++ b/src/Components/HCX/CreateClaimCard.tsx @@ -0,0 +1,242 @@ +import { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { getConsultation, HCXActions } from "../../Redux/actions"; +import * as Notification from "../../Utils/Notifications"; +import { classNames, formatCurrency } from "../../Utils/utils"; +import ButtonV2, { Submit } from "../Common/components/ButtonV2"; +import ClaimsItemsBuilder from "./ClaimsItemsBuilder"; +import { HCXClaimModel, HCXPolicyModel, HCXItemModel } from "./models"; +import HCXPolicyEligibilityCheck from "./PolicyEligibilityCheck"; +import DialogModal from "../Common/Dialog"; +import PatientInsuranceDetailsEditor from "./PatientInsuranceDetailsEditor"; +import ClaimCreatedModal from "./ClaimCreatedModal"; +import { ProcedureType } from "../Common/prescription-builder/ProcedureBuilder"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; + +interface Props { + consultationId: string; + patientId: string; + setIsCreating: (creating: boolean) => void; + isCreating: boolean; + use?: "preauthorization" | "claim"; +} + +export default function CreateClaimCard({ + consultationId, + patientId, + setIsCreating, + isCreating, + use = "preauthorization", +}: Props) { + const dispatch = useDispatch(); + const [showAddPolicy, setShowAddPolicy] = useState(false); + const [policy, setPolicy] = useState(); + const [items, setItems] = useState(); + const [itemsError, setItemsError] = useState(); + const [createdClaim, setCreatedClaim] = useState(); + const [use_, setUse_] = useState(use); + + console.log(items); + + useEffect(() => { + async function autoFill() { + const latestApprovedPreAuthsRes = await dispatch( + HCXActions.preauths.list(consultationId) + ); + + if (latestApprovedPreAuthsRes.data?.results?.length) { + // TODO: offload outcome filter to server side once payer server is back + const latestApprovedPreAuth = ( + latestApprovedPreAuthsRes.data.results as HCXClaimModel[] + ).find((o) => o.outcome === "Processing Complete"); + if (latestApprovedPreAuth) { + setPolicy(latestApprovedPreAuth.policy_object); + setItems(latestApprovedPreAuth.items || []); + return; + } + } + + const res = await dispatch(getConsultation(consultationId as any)); + + if (res.data && Array.isArray(res.data.procedure)) { + setItems( + res.data.procedure.map((obj: ProcedureType) => { + return { + id: obj.procedure, + name: obj.procedure, + price: 0.0, + category: "900000", // provider's packages + }; + }) + ); + } else { + setItems([]); + } + } + + autoFill(); + }, [consultationId, dispatch]); + + const validate = () => { + if (!policy) { + Notification.Error({ msg: "Please select a policy" }); + return false; + } + if (policy?.outcome !== "Processing Complete") { + Notification.Error({ msg: "Please select an eligible policy" }); + return false; + } + if (!items || items.length === 0) { + setItemsError("Please add at least one item"); + return false; + } + if (items?.some((p) => !p.id || !p.name || p.price === 0 || !p.category)) { + setItemsError("Please fill all the item details"); + return false; + } + + return true; + }; + + const handleSubmit = async () => { + if (!validate()) return; + + setIsCreating(true); + + const res = await dispatch( + HCXActions.claims.create({ + policy: policy?.id, + items, + consultation: consultationId, + use, + }) + ); + + if (res.data) { + setItems([]); + setItemsError(undefined); + setPolicy(undefined); + setCreatedClaim(res.data); + } else { + Notification.Error({ msg: "Failed to create pre-authorization" }); + } + + setIsCreating(false); + }; + + return ( +
+ {createdClaim && ( + setCreatedClaim(undefined)} + /> + )} + setShowAddPolicy(false)} + description="Add or edit patient's insurance details" + className="w-full max-w-screen-md" + > + setShowAddPolicy(false)} + /> + + {/* Check Insurance Policy Eligibility */} +
+
+

+ Check Insurance Policy Eligibility +

+ setShowAddPolicy(true)} ghost border> + + Edit Patient Insurance Details + +
+ +
+ + {/* Procedures */} +
+
+

Items

+ + setItems([...(items || []), { name: "", id: "", price: 0 }]) + } + > + + Add Item + +
+ + Select a policy to add items + + setItems(value)} + error={itemsError} + /> +
+ {"Total Amount: "} + {items ? ( + + {formatCurrency( + items.map((p) => p.price).reduce((a, b) => a + b, 0.0) + )} + + ) : ( + "--" + )} +
+
+ +
+ + setUse_(value as "preauthorization" | "claim") + } + position="below" + optionLabel={(value) => value.label} + optionValue={(value) => value.id as "preauthorization" | "claim"} + /> + + {isCreating && } + {isCreating + ? `Creating ${use === "claim" ? "Claim" : "Pre-Authorization"}...` + : "Proceed"} + +
+
+ ); +} diff --git a/src/Components/HCX/InsuranceDetailsBuilder.tsx b/src/Components/HCX/InsuranceDetailsBuilder.tsx new file mode 100644 index 00000000000..c713de0b873 --- /dev/null +++ b/src/Components/HCX/InsuranceDetailsBuilder.tsx @@ -0,0 +1,150 @@ +import { + FieldChangeEvent, + FormFieldBaseProps, + useFormFieldPropsResolver, +} from "../Form/FormFields/Utils"; +import FormField, { FieldLabel } from "../Form/FormFields/FormField"; +import { HCXPolicyModel } from "./models"; +import ButtonV2 from "../Common/components/ButtonV2"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import TextFormField from "../Form/FormFields/TextFormField"; +import { useDispatch } from "react-redux"; +import { HCXActions } from "../../Redux/actions"; +import { classNames } from "../../Utils/utils"; +import InsurerAutocomplete from "./InsurerAutocomplete"; +import useConfig from "../../Common/hooks/useConfig"; + +type Props = FormFieldBaseProps & { gridView?: boolean }; + +export default function InsuranceDetailsBuilder(props: Props) { + const field = useFormFieldPropsResolver(props as any); + const dispatch = useDispatch(); + + const handleUpdate = (index: number) => { + return (event: FieldChangeEvent) => { + field.handleChange( + (props.value || [])?.map((obj, i) => + i === index ? { ...obj, [event.name]: event.value } : obj + ) + ); + }; + }; + + const handleUpdates = (index: number) => { + return (diffs: object) => { + field.handleChange( + (props.value || [])?.map((obj, i) => + i === index ? { ...obj, ...diffs } : obj + ) + ); + }; + }; + + const handleRemove = (index: number) => { + return () => { + field.handleChange( + (props.value || [])?.filter((obj, i) => { + if (obj.id && i === index) { + dispatch(HCXActions.policies.delete(obj.id)); + } + return i !== index; + }) + ); + }; + }; + + return ( + +
+ {props.value?.length === 0 && ( + + No insurance details added + + )} + {props.value?.map((policy, index) => ( + + ))} +
+
+ ); +} + +const InsuranceDetailEditCard = ({ + policy, + handleUpdate, + handleUpdates, + handleRemove, + gridView, +}: { + policy: HCXPolicyModel; + handleUpdate: (event: FieldChangeEvent) => void; + handleUpdates: (diffs: object) => void; + handleRemove: () => void; + gridView?: boolean; +}) => { + const { enable_hcx } = useConfig(); + const seletedInsurer = + policy.insurer_id || policy.insurer_name + ? { code: policy.insurer_id, name: policy.insurer_name } + : undefined; + + return ( +
+
+ Policy + + Delete + + +
+ +
+ + + {enable_hcx && ( + + handleUpdates({ + insurer_id: value.code, + insurer_name: value.name, + }) + } + /> + )} +
+
+ ); +}; diff --git a/src/Components/HCX/InsurerAutocomplete.tsx b/src/Components/HCX/InsurerAutocomplete.tsx new file mode 100644 index 00000000000..ca73822410c --- /dev/null +++ b/src/Components/HCX/InsurerAutocomplete.tsx @@ -0,0 +1,41 @@ +import { useAsyncOptions } from "../../Common/hooks/useAsyncOptions"; +import { HCXActions } from "../../Redux/actions"; +import { Autocomplete } from "../Form/FormFields/Autocomplete"; +import FormField from "../Form/FormFields/FormField"; +import { + FormFieldBaseProps, + useFormFieldPropsResolver, +} from "../Form/FormFields/Utils"; + +export type InsurerOptionModel = { + name: string; + code: string; +}; + +type Props = FormFieldBaseProps & { + placeholder?: string; +}; + +export default function InsurerAutocomplete(props: Props) { + const field = useFormFieldPropsResolver(props as any); + const { fetchOptions, isLoading, options } = + useAsyncOptions("code"); + + return ( + + option.name} + optionDescription={(option) => option.code} + optionValue={(option) => option} + onQuery={(query) => fetchOptions(HCXActions.payors.list(query))} + isLoading={isLoading} + /> + + ); +} diff --git a/src/Components/HCX/PatientInsuranceDetailsEditor.tsx b/src/Components/HCX/PatientInsuranceDetailsEditor.tsx new file mode 100644 index 00000000000..8c827c3eadd --- /dev/null +++ b/src/Components/HCX/PatientInsuranceDetailsEditor.tsx @@ -0,0 +1,132 @@ +import { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { HCXActions } from "../../Redux/actions"; +import * as Notifications from "../../Utils/Notifications"; +import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; +import InsuranceDetailsBuilder from "./InsuranceDetailsBuilder"; +import { HCXPolicyModel } from "./models"; +import HCXPolicyValidator from "./validators"; + +interface Props { + patient: string; + onSubmitted?: () => void; + onCancel?: () => void; +} + +export default function PatientInsuranceDetailsEditor({ + patient, + onSubmitted, + onCancel, +}: Props) { + const dispatch = useDispatch(); + const [insuranceDetails, setInsuranceDetails] = useState(); + const [insuranceDetailsError, setInsuranceDetailsError] = useState(); + const [isUpdating, setIsUpdating] = useState(false); + + useEffect(() => { + const fetchPatientInsuranceDetails = async () => { + const res = await dispatch(HCXActions.policies.list({ patient })); + if (res && res.data) { + if (res.data.results.length) { + setInsuranceDetails(res.data.results); + } else { + setInsuranceDetails([ + { + subscriber_id: "", + policy_id: "", + insurer_id: "", + insurer_name: "", + }, + ]); + } + } else { + Notifications.Error({ msg: "Something went wrong " }); + } + }; + + fetchPatientInsuranceDetails(); + }, [dispatch, patient]); + + const handleSubmit = async () => { + // Validate + if (!insuranceDetails) return; + const insuranceDetailsError = insuranceDetails + .map(HCXPolicyValidator) + .find((error) => !!error); + setInsuranceDetailsError(insuranceDetailsError); + if (insuranceDetailsError) return; + + // Submit + setIsUpdating(true); + await Promise.all( + insuranceDetails.map(async (obj) => { + const policy: HCXPolicyModel = { ...obj, patient }; + const policyRes = await (policy.id + ? dispatch(HCXActions.policies.update(policy.id, policy)) + : dispatch(HCXActions.policies.create(policy))); + + const eligibilityCheckRes = await dispatch( + HCXActions.checkEligibility(policyRes.data.id) + ); + if (eligibilityCheckRes.status === 200) { + Notifications.Success({ msg: "Checking Policy Eligibility..." }); + } else { + Notifications.Error({ msg: "Something Went Wrong..." }); + } + }) + ); + setIsUpdating(false); + onSubmitted?.(); + }; + + return ( +
+ setInsuranceDetails(value)} + error={insuranceDetailsError} + gridView + disabled={isUpdating} + /> + +
+ + setInsuranceDetails([ + ...(insuranceDetails || []), + { + id: "", + subscriber_id: "", + policy_id: "", + insurer_id: "", + insurer_name: "", + }, + ]) + } + > + + Add Insurance Details + +
+ + + {isUpdating ? ( + <> + + Updating... + + ) : ( + "Update" + )} + +
+
+ ); +} diff --git a/src/Components/HCX/PolicyEligibilityCheck.tsx b/src/Components/HCX/PolicyEligibilityCheck.tsx new file mode 100644 index 00000000000..5f22e0d2813 --- /dev/null +++ b/src/Components/HCX/PolicyEligibilityCheck.tsx @@ -0,0 +1,190 @@ +import { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { HCXActions } from "../../Redux/actions"; +import ButtonV2 from "../Common/components/ButtonV2"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import { HCXPolicyModel } from "./models"; +import { useMessageListener } from "../../Common/hooks/useMessageListener"; +import * as Notification from "../../Utils/Notifications.js"; + +interface Props { + className?: string; + patient: string; + onEligiblePolicySelected: (policy: HCXPolicyModel | undefined) => void; +} + +export default function HCXPolicyEligibilityCheck({ + className, + patient, + onEligiblePolicySelected, +}: Props) { + const dispatch = useDispatch(); + const [insuranceDetails, setInsuranceDetails] = useState(); + const [policy, setPolicy] = useState(); + const [eligibility, setEligibility] = useState< + Record + >({}); + const [isChecking, setIsChecking] = useState(false); + + const fetchPatientInsuranceDetails = useCallback(async () => { + setInsuranceDetails(undefined); + setEligibility({}); + + const res = await dispatch(HCXActions.policies.list({ patient })); + + if (res.data?.results) { + const results = res.data.results as HCXPolicyModel[]; + setInsuranceDetails(results); + setEligibility( + results.reduce?.((acc: any, policy: HCXPolicyModel) => { + if (policy.outcome) + acc[policy.id!] = + !policy.error_text && policy.outcome === "Processing Complete"; + return acc; + }, {}) + ); + setIsChecking(false); + } + }, [patient, dispatch]); + + useEffect(() => { + fetchPatientInsuranceDetails(); + }, [fetchPatientInsuranceDetails]); + + useMessageListener((data) => { + if ( + data.type === "MESSAGE" && + data.from === "coverageelegibility/on_check" + ) { + fetchPatientInsuranceDetails(); + } + }); + + useEffect(() => { + if (policy && eligibility[policy]) { + const eligiblePolicy = insuranceDetails?.find((p) => p.id === policy); + onEligiblePolicySelected(eligiblePolicy); + } else { + onEligiblePolicySelected(undefined); + } + }, [policy, insuranceDetails, eligibility, onEligiblePolicySelected]); + + const checkEligibility = async () => { + if (!policy) return; + + // Skip checking eligibility if we already know the policy is eligible + if (eligibility[policy]) return; + + setIsChecking(true); + + const res = await dispatch(HCXActions.checkEligibility(policy)); + if (res.status === 200) { + Notification.Success({ msg: "Checking Policy Eligibility..." }); + } else { + Notification.Error({ msg: "Something Went Wrong..." }); + } + }; + + return ( +
+
+ option.id as string} + optionLabel={(option) => option.policy_id} + optionSelectedLabel={(option) => + option.id && eligibility[option.id] !== undefined ? ( +
+ {option.policy_id} + +
+ ) : ( + option.policy_id + ) + } + optionIcon={(option) => + eligibility[option.id!] !== undefined && ( + + ) + } + onChange={({ value }) => setPolicy(value)} + value={policy} + placeholder={ + insuranceDetails + ? insuranceDetails.length + ? "Select a policy to check eligibility" + : "No policies for the patient" + : "Loading..." + } + disabled={!insuranceDetails} + optionDescription={(option) => ( +
+
+ + Member ID + + {option.subscriber_id} + + + + Insurer ID + + {option.insurer_id} + + + + Insurer Name + + {option.insurer_name} + + +
+ {option.error_text && ( + + {option.error_text} + + )} +
+ )} + /> + + {isChecking ? ( + <> + + Checking ... + + ) : ( + "Check Eligibility" + )} + +
+
+ ); +} + +const EligibilityChip = ({ eligible }: { eligible: boolean }) => { + return ( +
+ + + {eligible ? "Eligible" : "Not Eligible"} + +
+ ); +}; diff --git a/src/Components/HCX/constants.ts b/src/Components/HCX/constants.ts new file mode 100644 index 00000000000..ed4235a3386 --- /dev/null +++ b/src/Components/HCX/constants.ts @@ -0,0 +1,58 @@ +interface ItemCategory { + code: string; + system: string; + display: string; +} + +export const ITEM_CATEGORIES: ItemCategory[] = [ + { + code: "100000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "Room & Nursing Charges", + }, + { + code: "200000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "ICU Charges", + }, + { + code: "300000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "OT Charges", + }, + { + code: "400000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "Medicine & Consumables Charges", + }, + { + code: "500000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "Professional Fees Charges", + }, + { + code: "600000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "Investigation Charges", + }, + { + code: "700000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "Ambulance Charges", + }, + { + code: "800000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "Miscellaneous Charges", + }, + { + code: "900000", + system: "https://irdai.gov.in/benefit-billing-group-code", + display: "Provider Package Charges", + }, + { + code: "HBP", + system: "https://pmjay.gov.in/benefit-billing-group-code", + display: "NHA Package Charges", + }, +]; diff --git a/src/Components/HCX/misc.ts b/src/Components/HCX/misc.ts new file mode 100644 index 00000000000..9476f19c081 --- /dev/null +++ b/src/Components/HCX/misc.ts @@ -0,0 +1,9 @@ +export interface PerformedByModel { + id: string; + first_name: string; + last_name: string; + username: string; + email: string; + user_type: string; + last_login: string; +} diff --git a/src/Components/HCX/models.ts b/src/Components/HCX/models.ts new file mode 100644 index 00000000000..7e624c474d7 --- /dev/null +++ b/src/Components/HCX/models.ts @@ -0,0 +1,76 @@ +import { ConsultationModel } from "../Facility/models"; +import { PatientModel } from "../Patient/models"; +import { PerformedByModel } from "./misc"; + +export type HCXPriority = "Immediate" | "Normal" | "Deferred"; + +export type HCXPolicyStatus = + | "Active" + | "Cancelled" + | "Draft" + | "Entered in Error"; +export type HCXPolicyPurpose = + | "Auth Requirements" + | "Benefits" + | "Discovery" + | "Validation"; +export type HCXPolicyOutcome = + | "Queued" + | "Processing Complete" + | "Error" + | "Partial Processing"; + +export interface HCXPolicyModel { + id?: string; + patient?: string; + patient_object?: PatientModel; + subscriber_id: string; + policy_id: string; + insurer_id: string; + insurer_name: string; + status?: HCXPolicyStatus; + priority?: "Immediate" | "Normal" | "Deferred"; + purpose?: "Auth Requirements" | "Benefits" | "Discovery" | "Validation"; + outcome?: "Queued" | "Processing Complete" | "Error" | "Partial Processing"; + error_text?: string; + created_date?: string; + modified_date?: string; +} + +export interface HCXItemModel { + id: string; + name: string; + price: number; + category?: string; +} + +export type HCXClaimUse = "Claim" | "Pre-Authorization" | "Pre-Determination"; +export type HCXClaimStatus = HCXPolicyStatus; +export type HCXClaimType = + | "Institutional" + | "Oral" + | "Pharmacy" + | "Professional" + | "Vision"; +export type HCXClaimOutcome = HCXPolicyOutcome; + +export interface HCXClaimModel { + id?: string; + consultation: string; + consultation_object?: ConsultationModel; + policy: string; + policy_object?: HCXPolicyModel; + items?: HCXItemModel[]; + total_claim_amount?: number; + total_amount_approved?: number; + use?: HCXClaimUse; + status?: HCXClaimStatus; + priority?: HCXPriority; + type?: HCXClaimType; + outcome?: HCXClaimOutcome; + error_text?: string; + created_by?: PerformedByModel; + last_modified_by?: PerformedByModel; + created_date?: string; + modified_date?: string; +} diff --git a/src/Components/HCX/validators.ts b/src/Components/HCX/validators.ts new file mode 100644 index 00000000000..ec2196797d7 --- /dev/null +++ b/src/Components/HCX/validators.ts @@ -0,0 +1,15 @@ +import useConfig from "../../Common/hooks/useConfig"; +import { FieldValidator } from "../Form/FieldValidators"; +import { HCXPolicyModel } from "./models"; + +const HCXPolicyValidator: FieldValidator = (value) => { + const { enable_hcx } = useConfig(); + if ( + !value.policy_id.trim() || + !value.subscriber_id.trim() || + (enable_hcx && (!value.insurer_id.trim() || !value.insurer_name.trim())) + ) + return "All fields are mandatory"; +}; + +export default HCXPolicyValidator; diff --git a/src/Components/Patient/FileUpload.tsx b/src/Components/Patient/FileUpload.tsx index 87e0c24de02..c905bbe2692 100644 --- a/src/Components/Patient/FileUpload.tsx +++ b/src/Components/Patient/FileUpload.tsx @@ -84,13 +84,14 @@ export const LinearProgressWithLabel = (props: any) => { interface FileUploadProps { type: string; - patientId: any; - facilityId: any; - consultationId: any; + patientId?: any; + facilityId?: any; + consultationId?: any; hideBack: boolean; - audio: boolean; + audio?: boolean; unspecified: boolean; sampleId?: number; + claimId?: string; } interface URLS { @@ -131,6 +132,7 @@ export const FileUpload = (props: FileUploadProps) => { audio, unspecified, sampleId, + claimId, } = props; const id = patientId; const dispatch: any = useDispatch(); @@ -246,11 +248,13 @@ export const FileUpload = (props: FileUploadProps) => { PATIENT: "Upload Patient Files", CONSULTATION: "Upload Consultation Files", SAMPLE_MANAGEMENT: "Upload Sample Report", + CLAIM: "Upload Supporting Info", }; const VIEW_HEADING: { [index: string]: string } = { PATIENT: "View Patient Files", CONSULTATION: "View Consultation Files", SAMPLE_MANAGEMENT: "View Sample Report", + CLAIM: "Supporting Info", }; const handleClose = () => { @@ -266,15 +270,14 @@ export const FileUpload = (props: FileUploadProps) => { const getAssociatedId = () => { switch (type) { - case "PATIENT": { + case "PATIENT": return patientId; - } - case "CONSULTATION": { + case "CONSULTATION": return consultationId; - } - case "SAMPLE_MANAGEMENT": { + case "SAMPLE_MANAGEMENT": return sampleId; - } + case "CLAIM": + return claimId; } }; diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 1e181d736cd..4a8dc71762a 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -14,6 +14,7 @@ import moment from "moment"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; import * as Notification from "../../Utils/Notifications.js"; +import useConfig from "../../Common/hooks/useConfig"; export default function PatientInfoCard(props: { patient: PatientModel; @@ -21,6 +22,7 @@ export default function PatientInfoCard(props: { fetchPatientData?: (state: { aborted: boolean }) => void; }) { const [open, setOpen] = useState(false); + const { enable_hcx } = useConfig(); const patient = props.patient; const ip_no = props.ip_no; @@ -275,48 +277,61 @@ export default function PatientInfoCard(props: { "file-medical", patient.last_consultation?.id, ], - ].map( - (action: any, i) => - action[3] && ( -
- { - if ( + ] + .concat( + enable_hcx + ? [ + [ + `/facility/${patient.facility}/patient/${patient.id}/consultation/${patient.last_consultation?.id}/claims`, + "Claims", + "copy-landscape", + patient.last_consultation?.id, + ], + ] + : [] + ) + .map( + (action: any, i) => + action[3] && ( +
+ - -

{action[1]}

-
- {action[4] && action[4][0] && ( - <> -

- {action[4][1]} -

- - )} -
- ) - )} + onClick={() => { + if ( + patient.last_consultation?.admitted && + !patient.last_consultation?.current_bed && + i === 1 + ) { + Notification.Error({ + msg: "Please assign a bed to the patient", + }); + setOpen(true); + } + }} + align="start" + className="w-full" + > + +

{action[1]}

+
+ {action[4] && action[4][0] && ( + <> +

+ {action[4][1]} +

+ + )} +
+ ) + )}
diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index b1fd2df48ea..d6fe4dc62f4 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -35,6 +35,7 @@ import { getWardByLocalBody, externalResult, getAnyFacility, + HCXActions, } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; import AlertDialog from "../Common/AlertDialog"; @@ -69,7 +70,12 @@ import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; import useConfig from "../../Common/hooks/useConfig"; import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date"; +import InsuranceDetailsBuilder from "../HCX/InsuranceDetailsBuilder"; +import { HCXPolicyModel } from "../HCX/models"; +import HCXPolicyValidator from "../HCX/validators"; +import { FieldError } from "../Form/FieldValidators"; import useAppHistory from "../../Common/hooks/useAppHistory"; + // const debounce = require("lodash.debounce"); interface PatientRegisterProps extends PatientModel { @@ -191,7 +197,7 @@ const getDate = (value: any) => export const PatientRegister = (props: PatientRegisterProps) => { const { goBack } = useAppHistory(); - const { gov_data_api_key } = useConfig(); + const { gov_data_api_key, enable_hcx } = useConfig(); const dispatchAction: any = useDispatch(); const { facilityId, id } = props; const [state, dispatch] = useReducer(patientFormReducer, initialState); @@ -221,6 +227,11 @@ export const PatientRegister = (props: PatientRegisterProps) => { const [patientName, setPatientName] = useState(""); const [{ extId }, setQuery] = useQueryParams(); const [showAutoFilledPincode, setShowAutoFilledPincode] = useState(false); + const [insuranceDetails, setInsuranceDetails] = useState( + [] + ); + const [insuranceDetailsError, setInsuranceDetailsError] = + useState(); useEffect(() => { if (extId) { @@ -462,6 +473,24 @@ export const PatientRegister = (props: PatientRegisterProps) => { [dispatchAction, fetchDistricts, fetchLocalBody, fetchWards, id] ); + useEffect(() => { + const fetchPatientInsuranceDetails = async () => { + if (!id) { + setInsuranceDetails([]); + return; + } + + const res = await dispatchAction( + HCXActions.policies.list({ patient: id }) + ); + if (res && res.data) { + setInsuranceDetails(res.data.results); + } + }; + + fetchPatientInsuranceDetails(); + }, [dispatchAction, id]); + const fetchStates = useCallback( async (status: statusType) => { setIsStateLoading(true); @@ -502,6 +531,16 @@ export const PatientRegister = (props: PatientRegisterProps) => { let invalidForm = false; let error_div = ""; + const insuranceDetailsError = insuranceDetails + .map(HCXPolicyValidator) + .find((error) => !!error); + setInsuranceDetailsError(insuranceDetailsError); + + if (insuranceDetailsError) { + invalidForm = true; + error_div = "insurance_details"; + } + Object.keys(state.form).forEach((field) => { let phoneNumber, emergency_phone_number; switch (field) { @@ -865,8 +904,39 @@ export const PatientRegister = (props: PatientRegisterProps) => { ? updatePatient(data, { id }) : createPatient({ ...data, facility: facilityId }) ); - setIsLoading(false); if (res && res.data && res.status != 400) { + await Promise.all( + insuranceDetails.map(async (obj) => { + const policy = { + ...obj, + patient: res.data.id, + insurer_id: obj.insurer_id || undefined, + insurer_name: obj.insurer_name || undefined, + }; + const policyRes = await (policy.id + ? dispatchAction( + HCXActions.policies.update( + policy.id, + policy as HCXPolicyModel + ) + ) + : dispatchAction( + HCXActions.policies.create(policy as HCXPolicyModel) + )); + + if (enable_hcx) { + const eligibilityCheckRes = await dispatchAction( + HCXActions.checkEligibility(policyRes.data.id) + ); + if (eligibilityCheckRes.status === 200) { + Notification.Success({ msg: "Checking Policy Eligibility..." }); + } else { + Notification.Error({ msg: "Something Went Wrong..." }); + } + } + }) + ); + dispatch({ type: "set_form", form: initForm }); if (!id) { setAlertMessage({ @@ -884,6 +954,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { goBack(); } } + setIsLoading(false); } }; @@ -2010,6 +2081,41 @@ export const PatientRegister = (props: PatientRegisterProps) => {
+
+
+

+ Insurance Details +

+ + setInsuranceDetails([ + ...insuranceDetails, + { + id: "", + subscriber_id: "", + policy_id: "", + insurer_id: "", + insurer_name: "", + }, + ]) + } + > + + Add Insurance Details + +
+ setInsuranceDetails(value)} + error={insuranceDetailsError} + gridView + /> +