=> {
+ const key = args
+ .map((arg) =>
+ typeof arg === "object"
+ ? arg instanceof Date
+ ? arg.getTime().toString()
+ : JSON.stringify(Object.entries(arg).sort())
+ : String(arg),
+ )
+ .join("|");
+ if (!cache.has(key)) {
+ if (cache.size >= MAX_CACHE_SIZE) {
+ const firstKey: any = cache.keys().next().value;
+ cache.delete(firstKey);
+ }
+ cache.set(key, fn(...args));
+ }
+ return cache.get(key)!;
+ }) as T;
+};
export const transformData = memoize((data: InvestigationResponse) => {
- const sessions = _.chain(data)
- .map((value: any) => {
- return {
- ...value.session_object,
- facility_name: value.consultation_object?.facility_name,
- facility_id: value.consultation_object?.facility,
- };
- })
- .uniqBy("session_external_id")
- .orderBy("session_created_date", "desc")
- .value();
- const groupByInvestigation = _.chain(data)
- .groupBy("investigation_object.external_id")
- .values()
- .value();
+ const sessions = Array.from(
+ new Map(
+ data.map((value: any) => [
+ value.session_object.session_external_id,
+ {
+ ...value.session_object,
+ facility_name: value.consultation_object?.facility_name,
+ facility_id: value.consultation_object?.facility,
+ },
+ ]),
+ ).values(),
+ ).sort(
+ (a, b) =>
+ new Date(b.session_created_date).getTime() -
+ new Date(a.session_created_date).getTime(),
+ );
+
+ const groupByInvestigation = Object.values(
+ data.reduce(
+ (acc, value: Investigation) => {
+ const key = value.investigation_object.external_id;
+ if (!acc[key]) acc[key] = [];
+ acc[key].push(value);
+ return acc;
+ },
+ {} as { [key: string]: Investigation[] },
+ ),
+ );
+
+ const sessionMap = new Map(
+ sessions.map((session, index) => [session.session_external_id, index]),
+ );
const reqData = groupByInvestigation.map((value: any) => {
const sessionValues = Array.from({ length: sessions.length });
value.forEach((val: any) => {
- const sessionIndex = findIndex(sessions, [
- "session_external_id",
- val.session_object.session_external_id,
- ]);
+ const sessionIndex =
+ sessionMap.get(val.session_object.session_external_id) ?? -1;
if (sessionIndex > -1) {
sessionValues[sessionIndex] = {
min: val.investigation_object.min_value,
@@ -58,6 +97,7 @@ export const transformData = memoize((data: InvestigationResponse) => {
sessionValues,
};
});
+
return { sessions, data: reqData };
});
diff --git a/src/components/Facility/Investigations/ShowInvestigation.tsx b/src/components/Facility/Investigations/ShowInvestigation.tsx
index 11e9b5cc0ad..05f2dd8eecf 100644
--- a/src/components/Facility/Investigations/ShowInvestigation.tsx
+++ b/src/components/Facility/Investigations/ShowInvestigation.tsx
@@ -1,5 +1,3 @@
-import _ from "lodash";
-import { set } from "lodash-es";
import { useCallback, useReducer } from "react";
import { useTranslation } from "react-i18next";
@@ -12,6 +10,8 @@ import routes from "@/Utils/request/api";
import request from "@/Utils/request/request";
import useQuery from "@/Utils/request/useQuery";
+// import { setNestedValueSafely } from "@/Utils/utils";
+
const initialState = {
changedFields: {},
initialValues: {},
@@ -92,8 +92,25 @@ export default function ShowInvestigation(props: ShowInvestigationProps) {
});
const handleValueChange = (value: any, name: string) => {
+ const keys = name.split(".");
+ // Validate keys to prevent prototype pollution - coderabbit suggested
+ if (
+ keys.some((key) =>
+ ["__proto__", "constructor", "prototype"].includes(key),
+ )
+ ) {
+ return;
+ }
+
const changedFields = { ...state.changedFields };
- set(changedFields, name, value);
+ let current = changedFields;
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!current[key]) current[key] = {};
+ current = current[key];
+ }
+
+ current[keys[keys.length - 1]] = value;
dispatch({ type: "set_changed_fields", changedFields });
};
@@ -151,15 +168,19 @@ export default function ShowInvestigation(props: ShowInvestigationProps) {
};
const handleUpdateCancel = useCallback(() => {
- const changedValues = _.chain(state.initialValues)
- .map((val: any, _key: string) => ({
- id: val?.id,
- initialValue: val?.notes || val?.value || null,
- value: val?.value || null,
- notes: val?.notes || null,
- }))
- .reduce((acc: any, cur: any) => ({ ...acc, [cur.id]: cur }), {})
- .value();
+ const changedValues = Object.keys(state.initialValues).reduce(
+ (acc: any, key: any) => {
+ const val = state.initialValues[key];
+ acc[key] = {
+ id: val?.id,
+ initialValue: val?.notes || val?.value || null,
+ value: val?.value || null,
+ notes: val?.notes || null,
+ };
+ return acc;
+ },
+ {},
+ );
dispatch({ type: "set_changed_fields", changedFields: changedValues });
}, [state.initialValues]);
diff --git a/src/components/Facility/Investigations/Table.tsx b/src/components/Facility/Investigations/Table.tsx
index 3a267279eb7..49dd924480b 100644
--- a/src/components/Facility/Investigations/Table.tsx
+++ b/src/components/Facility/Investigations/Table.tsx
@@ -1,4 +1,3 @@
-import { set } from "lodash-es";
import { useState } from "react";
import { SelectFormField } from "@/components/Form/FormFields/SelectFormField";
@@ -60,7 +59,24 @@ export const TestTable = ({ title, data, state, dispatch }: any) => {
const handleValueChange = (value: any, name: string) => {
const form = { ...state };
- set(form, name, value);
+ const keys = name.split(".");
+
+ // Validate keys to prevent prototype pollution - coderabbit suggested
+ if (
+ keys.some((key) =>
+ ["__proto__", "constructor", "prototype"].includes(key),
+ )
+ ) {
+ return;
+ }
+
+ let current = form;
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!current[key]) current[key] = {};
+ current = current[key];
+ }
+ current[keys[keys.length - 1]] = value;
dispatch({ type: "set_form", form });
};
diff --git a/src/components/Form/AutoCompleteAsync.tsx b/src/components/Form/AutoCompleteAsync.tsx
index b0cb9208d73..48c36258765 100644
--- a/src/components/Form/AutoCompleteAsync.tsx
+++ b/src/components/Form/AutoCompleteAsync.tsx
@@ -5,8 +5,7 @@ import {
ComboboxOption,
ComboboxOptions,
} from "@headlessui/react";
-import { debounce } from "lodash-es";
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import CareIcon from "@/CAREUI/icons/CareIcon";
@@ -17,6 +16,8 @@ import {
dropdownOptionClassNames,
} from "@/components/Form/MultiSelectMenuV2";
+import useDebounce from "@/hooks/useDebounce";
+
import { classNames } from "@/Utils/utils";
interface Props {
@@ -69,27 +70,29 @@ const AutoCompleteAsync = (props: Props) => {
const hasSelection =
(!multiple && selected) || (multiple && selected?.length > 0);
- const fetchDataAsync = useMemo(
- () =>
- debounce(async (query: string) => {
- setLoading(true);
- const data = ((await fetchData(query)) || [])?.filter((d: any) =>
- filter ? filter(d) : true,
- );
+ const fetchDataDebounced = useDebounce(async (searchQuery: string) => {
+ setLoading(true);
+ try {
+ const fetchedData = (await fetchData(searchQuery)) || [];
+ const filteredData = filter
+ ? fetchedData.filter((item: any) => filter(item))
+ : fetchedData;
- if (showNOptions !== undefined) {
- setData(data.slice(0, showNOptions));
- } else {
- setData(data);
- }
- setLoading(false);
- }, debounceTime),
- [fetchData, showNOptions, debounceTime],
- );
+ setData(
+ showNOptions !== undefined
+ ? filteredData.slice(0, showNOptions)
+ : filteredData,
+ );
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, debounceTime);
useEffect(() => {
- fetchDataAsync(query);
- }, [query, fetchDataAsync]);
+ fetchDataDebounced(query);
+ }, [query, fetchDataDebounced]);
return (
diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx
index e31467b6c2a..63b8c3614e5 100644
--- a/src/components/Form/Form.tsx
+++ b/src/components/Form/Form.tsx
@@ -1,4 +1,3 @@
-import { isEmpty, omitBy } from "lodash-es";
import { useEffect, useMemo, useState } from "react";
import { Cancel, Submit } from "@/components/Common/ButtonV2";
@@ -17,7 +16,7 @@ import {
import { DraftSection, useAutoSaveReducer } from "@/Utils/AutoSave";
import * as Notification from "@/Utils/Notifications";
-import { classNames } from "@/Utils/utils";
+import { classNames, isEmpty, omitBy } from "@/Utils/utils";
type Props = {
className?: string;
@@ -59,6 +58,7 @@ const Form = ({
if (validate) {
const errors = omitBy(validate(state.form), isEmpty) as FormErrors;
+
if (Object.keys(errors).length) {
dispatch({ type: "set_errors", errors });
diff --git a/src/components/Patient/DiagnosesFilter.tsx b/src/components/Patient/DiagnosesFilter.tsx
index c8fc2bc1a44..912a10cfde2 100644
--- a/src/components/Patient/DiagnosesFilter.tsx
+++ b/src/components/Patient/DiagnosesFilter.tsx
@@ -1,4 +1,3 @@
-import { debounce } from "lodash-es";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -6,6 +5,8 @@ import { ICD11DiagnosisModel } from "@/components/Diagnosis/types";
import { getDiagnosesByIds } from "@/components/Diagnosis/utils";
import AutocompleteMultiSelectFormField from "@/components/Form/FormFields/AutocompleteMultiselect";
+import useDebounce from "@/hooks/useDebounce";
+
import { Error } from "@/Utils/Notifications";
import routes from "@/Utils/request/api";
import useQuery from "@/Utils/request/useQuery";
@@ -34,6 +35,7 @@ interface Props {
value?: string;
onChange: (event: { name: DiagnosesFilterKey; value: string }) => void;
}
+
export default function DiagnosesFilter(props: Props) {
const { t } = useTranslation();
const [diagnoses, setDiagnoses] = useState([]);
@@ -42,6 +44,11 @@ export default function DiagnosesFilter(props: Props) {
prefetch: false,
});
+ const handleQuery = useDebounce(
+ (query: string) => refetch({ query: { query } }),
+ 300,
+ );
+
useEffect(() => {
if (res?.status === 500) {
Error({ msg: "ICD-11 Diagnosis functionality is facing issues." });
@@ -88,7 +95,7 @@ export default function DiagnosesFilter(props: Props) {
options={mergeQueryOptions(diagnoses, data ?? [], (obj) => obj.id)}
optionLabel={(option) => option.label}
optionValue={(option) => option}
- onQuery={debounce((query: string) => refetch({ query: { query } }), 300)}
+ onQuery={handleQuery}
isLoading={loading}
/>
);
diff --git a/src/components/Patient/PatientRegister.tsx b/src/components/Patient/PatientRegister.tsx
index 5828517adf9..a7098c24264 100644
--- a/src/components/Patient/PatientRegister.tsx
+++ b/src/components/Patient/PatientRegister.tsx
@@ -1,6 +1,4 @@
import careConfig from "@careConfig";
-import { startCase, toLower } from "lodash-es";
-import { debounce } from "lodash-es";
import { navigate } from "raviger";
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -56,6 +54,7 @@ import { UserModel } from "@/components/Users/models";
import useAppHistory from "@/hooks/useAppHistory";
import useAuthUser from "@/hooks/useAuthUser";
+import useDebounce from "@/hooks/useDebounce";
import {
BLOOD_GROUPS,
@@ -656,7 +655,7 @@ export const PatientRegister = (props: PatientRegisterProps) => {
? formData.last_vaccinated_date
: null
: null,
- name: startCase(toLower(formData.name)),
+ name: formData.name,
pincode: formData.pincode ? formData.pincode : undefined,
gender: Number(formData.gender),
nationality: formData.nationality,
@@ -776,7 +775,7 @@ export const PatientRegister = (props: PatientRegisterProps) => {
});
};
- const duplicateCheck = debounce(async (phoneNo: string) => {
+ const duplicateCheck = useDebounce(async (phoneNo: string) => {
if (
phoneNo &&
PhoneNumberValidator()(parsePhoneNumber(phoneNo) ?? "") === undefined
@@ -800,6 +799,11 @@ export const PatientRegister = (props: PatientRegisterProps) => {
});
}
}
+ } else {
+ setStatusDialog({
+ show: false,
+ patientList: [],
+ });
}
}, 300);
@@ -1022,6 +1026,7 @@ export const PatientRegister = (props: PatientRegisterProps) => {
{...field("name")}
type="text"
label={"Name"}
+ autoCapitalize="words"
/>
diff --git a/src/components/Patient/SampleDetails.tsx b/src/components/Patient/SampleDetails.tsx
index 2cadda8b22e..9a779446d54 100644
--- a/src/components/Patient/SampleDetails.tsx
+++ b/src/components/Patient/SampleDetails.tsx
@@ -1,4 +1,3 @@
-import { camelCase, capitalize, startCase } from "lodash-es";
import { Link, navigate } from "raviger";
import { useTranslation } from "react-i18next";
@@ -271,11 +270,11 @@ export const SampleDetails = ({ id }: DetailRoute) => {
{t("status")}:{" "}
{" "}
- {startCase(camelCase(flow.status))}
+ {t(`SAMPLE_TEST_HISTORY__${flow.status}`) || "Unknown"}
{t("label")}:{" "}
- {capitalize(flow.notes)}
+ {flow.notes}
@@ -385,8 +384,8 @@ export const SampleDetails = ({ id }: DetailRoute) => {
{t("doctors_name")}:
-
- {startCase(camelCase(sampleDetails.doctor_name))}
+
+ {sampleDetails.doctor_name}
)}
@@ -514,9 +513,9 @@ export const SampleDetails = ({ id }: DetailRoute) => {
{t("sample_type")}:{" "}
-
- {startCase(camelCase(sampleDetails.sample_type))}
-
+
+ {sampleDetails.sample_type}
+
)}
{sampleDetails?.sample_type === "OTHER TYPE" && (
diff --git a/src/components/Patient/SampleTestCard.tsx b/src/components/Patient/SampleTestCard.tsx
index c22155c494e..f1d59980c23 100644
--- a/src/components/Patient/SampleTestCard.tsx
+++ b/src/components/Patient/SampleTestCard.tsx
@@ -1,6 +1,6 @@
-import { camelCase, startCase } from "lodash-es";
import { navigate } from "raviger";
import { useState } from "react";
+import { useTranslation } from "react-i18next";
import ButtonV2 from "@/components/Common/ButtonV2";
import RelativeDateUserMention from "@/components/Common/RelativeDateUserMention";
@@ -24,6 +24,7 @@ interface SampleDetailsProps {
}
export const SampleTestCard = (props: SampleDetailsProps) => {
+ const { t } = useTranslation();
const { itemData, handleApproval, facilityId, patientId, refetch } = props;
const [statusDialog, setStatusDialog] = useState<{
@@ -103,9 +104,9 @@ export const SampleTestCard = (props: SampleDetailsProps) => {