diff --git a/package-lock.json b/package-lock.json index 3d1b49ff3ad..84fc3844cb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,6 @@ "i18next": "^23.16.4", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^3.0.1", - "lodash-es": "^4.17.21", "postcss-loader": "^8.1.1", "qrcode.react": "^4.1.0", "raviger": "^4.1.2", @@ -74,7 +73,6 @@ "@types/events": "^3.0.3", "@types/google.maps": "^3.58.1", "@types/jsdom": "^21.1.7", - "@types/lodash-es": "^4.17.12", "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-copy-to-clipboard": "^5.0.7", @@ -5615,23 +5613,6 @@ "optional": true, "peer": true }, - "node_modules/@types/lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-jzqWo/uQP/iqeGGTjhgFp2yaCrCYTauASQcpdzESNCkHjSprBJVcZP9KG9aQ0q+xcsXiKd/iuw/4dLjS3Odc7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/mdast": { "version": "3.0.15", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", @@ -12748,12 +12729,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", diff --git a/package.json b/package.json index 8c76438826b..53464762bfd 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "i18next": "^23.16.4", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^3.0.1", - "lodash-es": "^4.17.21", "postcss-loader": "^8.1.1", "qrcode.react": "^4.1.0", "raviger": "^4.1.2", @@ -113,7 +112,6 @@ "@types/events": "^3.0.3", "@types/google.maps": "^3.58.1", "@types/jsdom": "^21.1.7", - "@types/lodash-es": "^4.17.12", "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-copy-to-clipboard": "^5.0.7", diff --git a/public/locale/en.json b/public/locale/en.json index f86d78fa3aa..d73268df952 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -181,6 +181,14 @@ "ROUNDS_TYPE__NORMAL": "Brief Update", "ROUNDS_TYPE__TELEMEDICINE": "Tele-medicine Log", "ROUNDS_TYPE__VENTILATOR": "Detailed Update", + "SAMPLE_TEST_HISTORY__APPROVED": "Approved", + "SAMPLE_TEST_HISTORY__COMPLETED": "Completed", + "SAMPLE_TEST_HISTORY__RECEIVED_AT_LAB": "Received At Lab", + "SAMPLE_TEST_HISTORY__REQUEST_SUBMITTED": "Request Submitted", + "SAMPLE_TEST_RESULT__AWAITING": "Awaiting", + "SAMPLE_TEST_RESULT__INVALID": "Invalid", + "SAMPLE_TEST_RESULT__NEGATIVE": "Negative", + "SAMPLE_TEST_RESULT__POSITIVE": "Positive", "SLEEP__EXCESSIVE": "Excessive", "SLEEP__NO_SLEEP": "No sleep", "SLEEP__SATISFACTORY": "Satisfactory", diff --git a/src/Utils/Notifications.js b/src/Utils/Notifications.js index 5b3ecdf143c..08bc90443b5 100644 --- a/src/Utils/Notifications.js +++ b/src/Utils/Notifications.js @@ -1,6 +1,5 @@ import { Stack, alert, defaultModules } from "@pnotify/core"; import * as PNotifyMobile from "@pnotify/mobile"; -import { camelCase, startCase } from "lodash-es"; defaultModules.set(PNotifyMobile, {}); @@ -35,6 +34,25 @@ const notify = (text, type) => { }); }; +/** + * Formats input string to a more human readable format + * @param {string} key - The key to format + * @returns {string} The formatted key + * @example + * formatKey("patient_name") => "Patient Name" + */ +const formatKey = (key) => { + return key + .replace(/[^a-zA-Z0-9]+/g, " ") // Replace non-alphanumeric characters with a space + .trim() + .split(" ") + .map( + (word) => + word.charAt(0).toLocaleUpperCase() + word.slice(1).toLocaleLowerCase(), + ) // Capitalize the first letter of each word and lowercase the rest + .join(" "); +}; + const notifyError = (error) => { let errorMsg = ""; if (typeof error === "string" || !error) { @@ -44,7 +62,7 @@ const notifyError = (error) => { errorMsg = error.detail; } else { for (let [key, value] of Object.entries(error)) { - let keyName = startCase(camelCase(key)); + let keyName = formatKey(key); if (Array.isArray(value)) { const uniques = [...new Set(value)]; errorMsg += `${keyName} - ${uniques.splice(0, 5).join(", ")}`; diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 3888406ad29..a7e6a0a78ec 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -544,3 +544,18 @@ export const fahrenheitToCelsius = (fahrenheit: number) => { export const keysOf = (obj: T) => { return Object.keys(obj) as (keyof T)[]; }; + +// Utility to check if a value is "empty" +export const isEmpty = (value: unknown) => { + return value === "" || value == undefined; +}; + +// equivalent to lodash omitBy +export function omitBy>( + obj: T, + predicate: (value: unknown) => boolean, +): Partial { + return Object.fromEntries( + Object.entries(obj).filter(([_, value]) => !predicate(value)), + ) as Partial; +} diff --git a/src/components/Common/ExcelFIleDragAndDrop.tsx b/src/components/Common/ExcelFIleDragAndDrop.tsx index 67c64f0f433..d868e03f2a4 100644 --- a/src/components/Common/ExcelFIleDragAndDrop.tsx +++ b/src/components/Common/ExcelFIleDragAndDrop.tsx @@ -1,4 +1,3 @@ -import { forIn } from "lodash-es"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as XLSX from "xlsx"; @@ -68,9 +67,9 @@ export default function ExcelFileDragAndDrop({ const data = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); //converts the date to string data.forEach((row: any) => { - forIn(row, (value: any, key: string) => { - if (value instanceof Date) { - row[key] = value.toISOString().split("T")[0]; + Object.keys(row).forEach((key) => { + if (row[key] instanceof Date) { + row[key] = row[key].toISOString().split("T")[0]; } }); }); diff --git a/src/components/Facility/Investigations/Reports/index.tsx b/src/components/Facility/Investigations/Reports/index.tsx index e711372da47..32f0abcb7bc 100644 --- a/src/components/Facility/Investigations/Reports/index.tsx +++ b/src/components/Facility/Investigations/Reports/index.tsx @@ -1,4 +1,3 @@ -import _ from "lodash"; import { useCallback, useReducer, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -179,16 +178,16 @@ const InvestigationReports = ({ id }: any) => { ), ); - const investigationList = _.chain(data) - .flatMap((i) => i?.data?.results) - .compact() - .flatten() - .map((i) => ({ - ...i, - name: `${i.name} ${i.groups[0].name && " | " + i.groups[0].name} `, - })) - .unionBy("external_id") - .value(); + const investigationList = Array.from( + data + .flatMap((i) => i?.data?.results || []) + .map((i) => ({ + ...i, + name: `${i.name} ${i.groups[0].name ? " | " + i.groups[0].name : ""}`, + })) + .reduce((map, item) => map.set(item.external_id, item), new Map()) + .values(), + ); dispatch({ type: "set_investigations", payload: investigationList }); dispatch({ diff --git a/src/components/Facility/Investigations/Reports/utils.tsx b/src/components/Facility/Investigations/Reports/utils.tsx index 073be541600..564ce53ebe2 100644 --- a/src/components/Facility/Investigations/Reports/utils.tsx +++ b/src/components/Facility/Investigations/Reports/utils.tsx @@ -1,31 +1,70 @@ -import _ from "lodash"; -import { findIndex, memoize } from "lodash-es"; +import { + Investigation, + InvestigationResponse, +} from "@/components/Facility/Investigations/Reports/types"; -import { InvestigationResponse } from "@/components/Facility/Investigations/Reports/types"; +const memoize = any>(fn: T): T => { + const cache = new Map>(); + const MAX_CACHE_SIZE = 1000; + return ((...args: Parameters): ReturnType => { + 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) => {
- {startCase(camelCase(itemData.status))} + {t(`SAMPLE_TEST_HISTORY__${itemData.status}`) || "Unknown"}
@@ -144,9 +145,9 @@ export const SampleTestCard = (props: SampleDetailsProps) => {
- {startCase(camelCase(itemData.result))} + {t(`SAMPLE_TEST_RESULT__${itemData.result}`) || "Unknown"}
diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 00000000000..66ee398f962 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,29 @@ +import { useEffect, useRef } from "react"; + +export default function useDebounce( + callback: (...args: T) => void, + delay: number, +) { + const callbackRef = useRef(callback); + const timeoutRef = useRef | null>(null); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + const debouncedCallback = (...args: T) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + callbackRef.current(...args); + }, delay); + }; + return debouncedCallback; +}