Skip to content

Commit

Permalink
Adds support for filtering patients by diagnoses (ohcnetwork#7062)
Browse files Browse the repository at this point in the history
* Adds support for filtering by diagnoses

* code cleanup

* sort diagnoses filter based on priority
  • Loading branch information
rithviknishad authored Jan 25, 2024
1 parent 5cc5072 commit 35e78bd
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 11 deletions.
17 changes: 9 additions & 8 deletions src/Common/hooks/useAsyncOptions.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { debounce } from "lodash-es";
import { useMemo, useState } from "react";
import { useDispatch } from "react-redux";
import { mergeQueryOptions } from "../../Utils/utils";

interface IUseAsyncOptionsArgs {
debounceInterval?: number;
queryResponseExtractor?: (data: any) => any;
}

/**
* Deprecated. This is no longer needed as `useQuery` with `mergeQueryOptions`
* can be reused for this.
*
* Hook to implement async autocompletes with ease and typesafety.
*
* See `DiagnosisSelectFormField` for usage.
Expand Down Expand Up @@ -51,14 +55,11 @@ export function useAsyncOptions<T extends Record<string, unknown>>(
);

const mergeValueWithQueryOptions = (selected?: T[]) => {
if (!selected?.length) return queryOptions;

return [
...selected,
...queryOptions.filter(
(option) => !selected.find((s) => s[uniqueKey] === option[uniqueKey])
),
];
return mergeQueryOptions(
selected ?? [],
queryOptions,
(obj) => obj[uniqueKey]
);
};

return {
Expand Down
13 changes: 13 additions & 0 deletions src/Components/Diagnosis/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import routes from "../../Redux/api";
import request from "../../Utils/request/request";
import { ICD11DiagnosisModel } from "./types";

// TODO: cache ICD11 responses and hit the cache if present instead of making an API call.

export const getDiagnosisById = async (id: ICD11DiagnosisModel["id"]) => {
return (await request(routes.getICD11Diagnosis, { pathParams: { id } })).data;
};

export const getDiagnosesByIds = async (ids: ICD11DiagnosisModel["id"][]) => {
return Promise.all([...new Set(ids)].map(getDiagnosisById));
};
81 changes: 81 additions & 0 deletions src/Components/Patient/DiagnosesFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useEffect, useState } from "react";
import { ICD11DiagnosisModel } from "../Diagnosis/types";
import { getDiagnosesByIds } from "../Diagnosis/utils";
import { useTranslation } from "react-i18next";
import AutocompleteMultiSelectFormField from "../Form/FormFields/AutocompleteMultiselect";
import useQuery from "../../Utils/request/useQuery";
import routes from "../../Redux/api";
import { mergeQueryOptions } from "../../Utils/utils";
import { debounce } from "lodash-es";

export const FILTER_BY_DIAGNOSES_KEYS = [
"diagnoses",
"diagnoses_confirmed",
"diagnoses_unconfirmed",
"diagnoses_provisional",
"diagnoses_differential",
] as const;

export const DIAGNOSES_FILTER_LABELS = {
diagnoses: "Diagnoses (of any verification status)",
diagnoses_unconfirmed: "Unconfirmed Diagnoses",
diagnoses_provisional: "Provisional Diagnoses",
diagnoses_differential: "Differential Diagnoses",
diagnoses_confirmed: "Confirmed Diagnoses",
} as const;

export type DiagnosesFilterKey = (typeof FILTER_BY_DIAGNOSES_KEYS)[number];

interface Props {
name: DiagnosesFilterKey;
value?: string;
onChange: (event: { name: DiagnosesFilterKey; value: string }) => void;
}
export default function DiagnosesFilter(props: Props) {
const { t } = useTranslation();
const [diagnoses, setDiagnoses] = useState<ICD11DiagnosisModel[]>([]);
const { data, loading, refetch } = useQuery(routes.listICD11Diagnosis);

useEffect(() => {
if (!props.value) {
setDiagnoses([]);
return;
}
if (diagnoses.map((d) => d.id).join(",") === props.value) {
return;
}

// Re-use the objects which we already have, fetch the rest.
const ids = props.value.split(",");
const existing = diagnoses.filter(({ id }) => ids.includes(id));
const objIds = existing.map((o) => o.id);
const diagnosesToBeFetched = ids.filter((id) => !objIds.includes(id));
getDiagnosesByIds(diagnosesToBeFetched).then((data) => {
const retrieved = data.filter(Boolean) as ICD11DiagnosisModel[];
setDiagnoses([...existing, ...retrieved]);
});
}, [props.value]);

return (
<AutocompleteMultiSelectFormField
label={DIAGNOSES_FILTER_LABELS[props.name]}
labelClassName="text-sm"
name="icd11_search"
className="w-full"
placeholder={t("search_icd11_placeholder")}
value={diagnoses}
onChange={(e) => {
setDiagnoses(e.value);
props.onChange({
name: props.name,
value: e.value.map((o) => o.id).join(","),
});
}}
options={mergeQueryOptions(diagnoses, data ?? [], (obj) => obj.id)}
optionLabel={(option) => option.label}
optionValue={(option) => option}
onQuery={debounce((query: string) => refetch({ query: { query } }), 300)}
isLoading={loading}
/>
);
}
50 changes: 50 additions & 0 deletions src/Components/Patient/ManagePatients.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ import { triggerGoal } from "../../Integrations/Plausible.js";
import useAuthUser from "../../Common/hooks/useAuthUser.js";
import useQuery from "../../Utils/request/useQuery.js";
import routes from "../../Redux/api.js";
import {
DIAGNOSES_FILTER_LABELS,
DiagnosesFilterKey,
FILTER_BY_DIAGNOSES_KEYS,
} from "./DiagnosesFilter.js";
import { ICD11DiagnosisModel } from "../Diagnosis/types.js";
import { getDiagnosesByIds } from "../Diagnosis/utils.js";

const Loading = lazy(() => import("../Common/Loading"));

Expand Down Expand Up @@ -110,6 +117,7 @@ export const PatientManager = () => {
name: "",
});
const authUser = useAuthUser();
const [diagnoses, setDiagnoses] = useState<ICD11DiagnosisModel[]>([]);
const [showDialog, setShowDialog] = useState(false);
const [showDoctors, setShowDoctors] = useState(false);
const [showDoctorConnect, setShowDoctorConnect] = useState(false);
Expand Down Expand Up @@ -231,8 +239,33 @@ export const PatientManager = () => {
qParams.last_consultation_is_telemedicine || undefined,
is_antenatal: qParams.is_antenatal || undefined,
ventilator_interface: qParams.ventilator_interface || undefined,
diagnoses: qParams.diagnoses || undefined,
diagnoses_confirmed: qParams.diagnoses_confirmed || undefined,
diagnoses_provisional: qParams.diagnoses_provisional || undefined,
diagnoses_unconfirmed: qParams.diagnoses_unconfirmed || undefined,
diagnoses_differential: qParams.diagnoses_differential || undefined,
};

useEffect(() => {
const ids: string[] = [];
FILTER_BY_DIAGNOSES_KEYS.forEach((key) => {
ids.push(...(qParams[key] ?? "").split(",").filter(Boolean));
});
const existing = diagnoses.filter(({ id }) => ids.includes(id));
const objIds = existing.map((o) => o.id);
const diagnosesToBeFetched = ids.filter((id) => !objIds.includes(id));
getDiagnosesByIds(diagnosesToBeFetched).then((data) => {
const retrieved = data.filter(Boolean) as ICD11DiagnosisModel[];
setDiagnoses([...existing, ...retrieved]);
});
}, [
qParams.diagnoses,
qParams.diagnoses_confirmed,
qParams.diagnoses_provisional,
qParams.diagnoses_unconfirmed,
qParams.diagnoses_differential,
]);

useEffect(() => {
if (params.facility) {
setShowDoctorConnect(true);
Expand Down Expand Up @@ -395,6 +428,11 @@ export const PatientManager = () => {
qParams.last_consultation_is_telemedicine,
qParams.is_antenatal,
qParams.ventilator_interface,
qParams.diagnoses,
qParams.diagnoses_confirmed,
qParams.diagnoses_provisional,
qParams.diagnoses_unconfirmed,
qParams.diagnoses_differential,
]);

const getTheCategoryFromId = () => {
Expand Down Expand Up @@ -520,6 +558,11 @@ export const PatientManager = () => {
});
};

const getDiagnosisFilterValue = (key: DiagnosesFilterKey) => {
const ids: string[] = (qParams[key] ?? "").split(",");
return ids.map((id) => diagnoses.find((obj) => obj.id == id)?.label ?? id);
};

let patientList: ReactNode[] = [];
if (data && data.length) {
patientList = data.map((patient: any) => {
Expand Down Expand Up @@ -1025,6 +1068,13 @@ export const PatientManager = () => {
...range("Age", "age"),
badge("SRF ID", "srf_id"),
{ name: "LSG Body", value: localbodyName, paramKey: "lsgBody" },
...FILTER_BY_DIAGNOSES_KEYS.map((key) =>
value(
DIAGNOSES_FILTER_LABELS[key],
key,
getDiagnosisFilterValue(key).join(", ")
)
),
badge("Declared Status", "is_declared_positive"),
...dateRange("Result", "date_of_result"),
...dateRange("Declared positive", "date_declared_positive"),
Expand Down
34 changes: 34 additions & 0 deletions src/Components/Patient/PatientFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from "../Form/FormFields/Utils";
import MultiSelectMenuV2 from "../Form/MultiSelectMenuV2";
import SelectMenuV2 from "../Form/SelectMenuV2";
import DiagnosesFilter, { FILTER_BY_DIAGNOSES_KEYS } from "./DiagnosesFilter";

const getDate = (value: any) =>
value && dayjs(value).isValid() && dayjs(value).toDate();
Expand Down Expand Up @@ -98,6 +99,11 @@ export default function PatientFilter(props: any) {
filter.last_consultation_is_telemedicine || null,
is_antenatal: filter.is_antenatal || null,
ventilator_interface: filter.ventilator_interface || null,
diagnoses: filter.diagnoses || null,
diagnoses_confirmed: filter.diagnoses_confirmed || null,
diagnoses_provisional: filter.diagnoses_provisional || null,
diagnoses_unconfirmed: filter.diagnoses_unconfirmed || null,
diagnoses_differential: filter.diagnoses_differential || null,
});
const dispatch: any = useDispatch();

Expand Down Expand Up @@ -211,6 +217,11 @@ export default function PatientFilter(props: any) {
last_consultation_is_telemedicine,
is_antenatal,
ventilator_interface,
diagnoses,
diagnoses_confirmed,
diagnoses_provisional,
diagnoses_unconfirmed,
diagnoses_differential,
} = filterState;
const data = {
district: district || "",
Expand Down Expand Up @@ -273,6 +284,11 @@ export default function PatientFilter(props: any) {
last_consultation_is_telemedicine || "",
is_antenatal: is_antenatal || "",
ventilator_interface: ventilator_interface || "",
diagnoses: diagnoses || "",
diagnoses_confirmed: diagnoses_confirmed || "",
diagnoses_provisional: diagnoses_provisional || "",
diagnoses_unconfirmed: diagnoses_unconfirmed || "",
diagnoses_differential: diagnoses_differential || "",
};
onChange(data);
};
Expand Down Expand Up @@ -459,6 +475,24 @@ export default function PatientFilter(props: any) {
</div>
</div>
</AccordionV2>
<AccordionV2
title={
<h1 className="mb-4 text-left text-xl font-bold text-purple-500">
ICD-11 Diagnoses based
</h1>
}
expanded
className="w-full"
>
{FILTER_BY_DIAGNOSES_KEYS.map((name) => (
<DiagnosesFilter
key={name}
name={name}
value={filterState[name]}
onChange={handleFormFieldChange}
/>
))}
</AccordionV2>
<AccordionV2
title={
<h1 className="mb-4 text-left text-xl font-bold text-purple-500">
Expand Down
7 changes: 7 additions & 0 deletions src/Redux/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
InvestigationGroup,
InvestigationType,
} from "../Components/Facility/Investigations";
import { ICD11DiagnosisModel } from "../Components/Diagnosis/types";

/**
* A fake function that returns an empty object casted to type T
Expand Down Expand Up @@ -975,6 +976,12 @@ const routes = {
// ICD11
listICD11Diagnosis: {
path: "/api/v1/icd/",
TRes: Type<ICD11DiagnosisModel[]>(),
},
getICD11Diagnosis: {
path: "/api/v1/icd/{id}/",
TRes: Type<ICD11DiagnosisModel>(),
enableExperimentalCache: true,
},
// Medibase
listMedibaseMedicines: {
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/request/useQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function useQuery<TData>(
const resolvedOptions =
options && overrides
? mergeRequestOptions(options, overrides)
: options;
: overrides ?? options;

setLoading(true);
const response = await request(route, resolvedOptions);
Expand Down
4 changes: 2 additions & 2 deletions src/Utils/request/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { QueryParams, RequestOptions } from "./types";
export function makeUrl(
path: string,
query?: QueryParams,
pathParams?: Record<string, string>
pathParams?: Record<string, string | number>
) {
if (pathParams) {
path = Object.entries(pathParams).reduce(
(acc, [key, value]) => acc.replace(`{${key}}`, value),
(acc, [key, value]) => acc.replace(`{${key}}`, `${value}`),
path
);
}
Expand Down
14 changes: 14 additions & 0 deletions src/Utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,17 @@ export const isValidUrl = (url?: string) => {
return false;
}
};

export const mergeQueryOptions = <T extends object>(
selected: T[],
queryOptions: T[],
compareBy: (obj: T) => T[keyof T]
) => {
if (!selected.length) return queryOptions;
return [
...selected,
...queryOptions.filter(
(option) => !selected.find((s) => compareBy(s) === compareBy(option))
),
];
};

0 comments on commit 35e78bd

Please sign in to comment.