From ff63195d38f5bdfb684c2f6c429cffde217063ec Mon Sep 17 00:00:00 2001 From: Tanuj Nainwal <125687187+Tanuj1718@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:44:38 +0530 Subject: [PATCH 1/6] fix: Resolve creation of patient name despite having numeric values (#9152) --- src/components/Patient/PatientRegister.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Patient/PatientRegister.tsx b/src/components/Patient/PatientRegister.tsx index 5cc958d44de..a7a7743aee4 100644 --- a/src/components/Patient/PatientRegister.tsx +++ b/src/components/Patient/PatientRegister.tsx @@ -67,7 +67,7 @@ import { } from "@/common/constants"; import countryList from "@/common/static/countries.json"; import { statusType, useAbortableEffect } from "@/common/utils"; -import { validatePincode } from "@/common/validation"; +import { validateName, validatePincode } from "@/common/validation"; import { PLUGIN_Component } from "@/PluginEngine"; import { RestoreDraftButton } from "@/Utils/AutoSave"; @@ -421,6 +421,10 @@ export const PatientRegister = (props: PatientRegisterProps) => { switch (field) { case "address": case "name": + if (!validateName(form[field])) { + errors[field] = "Please enter valid name"; + } + return; case "gender": errors[field] = RequiredFieldValidator()(form[field]); return; From 41ed5be0b175f501f54bcc57060af4cb6aaa37c7 Mon Sep 17 00:00:00 2001 From: Aditya Jindal Date: Wed, 20 Nov 2024 07:45:48 +0530 Subject: [PATCH 2/6] Adjusted SpO2 thresholds and labels in DailyRounds component to match medical standards (#9118) --- public/locale/en.json | 4 ++++ src/components/Patient/DailyRounds.tsx | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 2eb4822c27e..5faa645b133 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -205,6 +205,10 @@ "SORT_OPTIONS__name": "Patient name A-Z", "SORT_OPTIONS__review_time": "Oldest review date first", "SORT_OPTIONS__taken_at": "Oldest taken date first", + "SPO2_LEVEL_MILD_HYPOXEMIA": "Mild Hypoxemia", + "SPO2_LEVEL_MODERATE_HYPOXEMIA": "Moderate Hypoxemia", + "SPO2_LEVEL_NORMAL": "Normal", + "SPO2_LEVEL_SEVERE_HYPOXEMIA": "Severe Hypoxemia", "Submit": "Submit", "TELEMEDICINE": "Telemedicine", "TRANSPORTATION TO BE ARRANGED": "Transportation", diff --git a/src/components/Patient/DailyRounds.tsx b/src/components/Patient/DailyRounds.tsx index 2afd2b0a95d..4e8573d6273 100644 --- a/src/components/Patient/DailyRounds.tsx +++ b/src/components/Patient/DailyRounds.tsx @@ -800,17 +800,22 @@ export const DailyRounds = (props: any) => { { value: 0, className: "text-danger-500", - label: "Low", + label: t("SPO2_LEVEL_SEVERE_HYPOXEMIA"), }, { - value: 90, - className: "text-primary-500", - label: "Normal", + value: 86, + className: "text-danger-500", + label: t("SPO2_LEVEL_MODERATE_HYPOXEMIA"), }, { - value: 100, - className: "text-danger-500", - label: "High", + value: 91, + className: "text-warning-400", + label: t("SPO2_LEVEL_MILD_HYPOXEMIA"), + }, + { + value: 95, + className: "text-primary-500", + label: t("SPO2_LEVEL_NORMAL"), }, ]} /> From b076d4f7a3179eb7ef783cb48b7d6cecf349cf6c Mon Sep 17 00:00:00 2001 From: Aditya Jindal Date: Wed, 20 Nov 2024 07:46:07 +0530 Subject: [PATCH 3/6] Fix: Year Of Birth Field Validation Condition in Patient transfer form (#9133) --- .../Facility/TransferPatientDialog.tsx | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/components/Facility/TransferPatientDialog.tsx b/src/components/Facility/TransferPatientDialog.tsx index 530bc513f0d..153ea17fa92 100644 --- a/src/components/Facility/TransferPatientDialog.tsx +++ b/src/components/Facility/TransferPatientDialog.tsx @@ -75,22 +75,38 @@ const TransferPatientDialog = (props: Props) => { const maxYear = new Date().getFullYear(); const handleChange = (e: FieldChangeEvent) => { - if ( - e.name === "year_of_birth" && - parseInt((e.value as string) || "0") > maxYear - ) { + const value = String(e.value); + + if (e.name === "year_of_birth") { + if (value.length <= 4) { + dispatch({ + type: "set_form", + form: { ...state.form, [e.name]: e.value }, + }); + } + } else { dispatch({ - type: "set_error", - errors: { - ...state.errors, - [e.name]: `Cannot be greater than ${maxYear}`, - }, + type: "set_form", + form: { ...state.form, [e.name]: e.value }, }); - return; + } + }; + + const handleOnBlur = (e: React.FocusEvent) => { + const yearValue = Number(state.form.year_of_birth); + if (!state.form.year_of_birth) return; + let errorMessage = ""; + if (yearValue > maxYear) { + errorMessage = `Cannot be greater than ${maxYear}`; + } else if (yearValue < 1900) { + errorMessage = `Cannot be smaller than 1900`; } dispatch({ - type: "set_form", - form: { ...state.form, [e.name]: e.value }, + type: "set_error", + errors: { + ...state.errors, + [e.target.name]: errorMessage, + }, }); }; @@ -115,6 +131,11 @@ const TransferPatientDialog = (props: Props) => { errors[field] = `Cannot be greater than ${maxYear}`; invalidForm = true; } + + if (parseInt(state.form[field] || "0") < 1900) { + errors[field] = `Cannot be smaller than 1900`; + invalidForm = true; + } return; default: return; @@ -193,9 +214,8 @@ const TransferPatientDialog = (props: Props) => { label="Year of birth" labelClassName="text-sm" value={state.form.year_of_birth} - min="1900" - max={maxYear} onChange={handleChange} + onBlur={handleOnBlur} placeholder="Enter year of birth" error={state.errors.year_of_birth} /> From 64ed2a32feba89beeab031377afc9cb27373f518 Mon Sep 17 00:00:00 2001 From: Aditya Jindal Date: Wed, 20 Nov 2024 07:47:48 +0530 Subject: [PATCH 4/6] Fix: TypeError in notification list (#8935) --- .../Notifications/NotificationsList.tsx | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/src/components/Notifications/NotificationsList.tsx b/src/components/Notifications/NotificationsList.tsx index 17033d67623..f9b65b35201 100644 --- a/src/components/Notifications/NotificationsList.tsx +++ b/src/components/Notifications/NotificationsList.tsx @@ -244,7 +244,10 @@ export default function NotificationsList({ const handleSubscribeClick = () => { const status = isSubscribed; - if (status === "NotSubscribed" || status === "SubscribedOnAnotherDevice") { + if (!navigator.serviceWorker) { + return; + } + if (["NotSubscribed", "SubscribedOnAnotherDevice"].includes(status)) { if (Notification.permission === "denied") { Warn({ msg: t("notification_permission_denied"), @@ -286,49 +289,47 @@ export default function NotificationsList({ let manageResults: any = null; - const unsubscribe = () => { - navigator.serviceWorker.ready - .then(function (reg) { - setIsSubscribing(true); - reg.pushManager - .getSubscription() - .then(function (subscription) { - subscription - ?.unsubscribe() - .then(async function (_successful) { - const data = { - pf_endpoint: "", - pf_p256dh: "", - pf_auth: "", - }; - - await request(routes.updateUserPnconfig, { - pathParams: { username: username }, - body: data, - }); - - Warn({ - msg: t("unsubscribed_successfully"), - }); - - setIsSubscribed("NotSubscribed"); - setIsSubscribing(false); - }) - .catch(function (_e) { - Error({ - msg: t("unsubscribe_failed"), - }); - }); - }) - .catch(function (_e) { - Error({ msg: t("subscription_error") }); + const unsubscribe = async () => { + try { + const reg = await navigator.serviceWorker.ready; + + if (!reg.pushManager) { + Error({ msg: t("unsubscribe_failed") }); + return; + } + + setIsSubscribing(true); + + const subscription = await reg.pushManager.getSubscription(); + + if (subscription) { + try { + await subscription.unsubscribe(); + + await request(routes.updateUserPnconfig, { + pathParams: { username }, + body: { + pf_endpoint: "", + pf_p256dh: "", + pf_auth: "", + }, }); - }) - .catch(function (_e) { - Sentry.captureException(_e); - }); - }; + setIsSubscribed("NotSubscribed"); + Warn({ + msg: t("unsubscribed_successfully"), + }); + } catch (e) { + Error({ msg: t("unsubscribe_failed") }); + } + } + } catch (e) { + Sentry.captureException(e); + Error({ msg: t("subscription_error") }); + } finally { + setIsSubscribing(false); + } + }; async function subscribe() { setIsSubscribing(true); try { From f79a2a4be651ffa3b7f9f107f8f08b480b467c0b Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 20 Nov 2024 07:49:15 +0530 Subject: [PATCH 5/6] Enhancements to the nursing care procedures and routines tables (#9079) --- .../ConsultationNursingTab.tsx | 203 ++++++++++-------- .../Consultations/LogUpdateAnalyseTable.tsx | 93 ++++++++ .../Facility/Consultations/NursingPlot.tsx | 132 ------------ src/components/Facility/models.tsx | 5 +- 4 files changed, 213 insertions(+), 220 deletions(-) create mode 100644 src/components/Facility/Consultations/LogUpdateAnalyseTable.tsx delete mode 100644 src/components/Facility/Consultations/NursingPlot.tsx diff --git a/src/components/Facility/ConsultationDetails/ConsultationNursingTab.tsx b/src/components/Facility/ConsultationDetails/ConsultationNursingTab.tsx index 81078197a45..6a7e33c6c95 100644 --- a/src/components/Facility/ConsultationDetails/ConsultationNursingTab.tsx +++ b/src/components/Facility/ConsultationDetails/ConsultationNursingTab.tsx @@ -5,42 +5,18 @@ import Loading from "@/components/Common/Loading"; import PageTitle from "@/components/Common/PageTitle"; import Pagination from "@/components/Common/Pagination"; import { ConsultationTabProps } from "@/components/Facility/ConsultationDetails/index"; -import { NursingPlot } from "@/components/Facility/Consultations/NursingPlot"; +import LogUpdateAnalyseTable from "@/components/Facility/Consultations/LogUpdateAnalyseTable"; import { + NursingPlotFields, + NursingPlotRes, RoutineAnalysisRes, RoutineFields, } from "@/components/Facility/models"; -import { PAGINATION_LIMIT } from "@/common/constants"; +import { NURSING_CARE_PROCEDURES, PAGINATION_LIMIT } from "@/common/constants"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; -import { classNames, formatDate, formatTime } from "@/Utils/utils"; - -export default function ConsultationNursingTab(props: ConsultationTabProps) { - const { t } = useTranslation(); - return ( -
- -
-

{t("routine")}

- -
-
-

{t("nursing_care")}

- -
-
- ); -} const REVERSE_CHOICES = { appetite: { @@ -114,6 +90,92 @@ const ROUTINE_ROWS = [ { subField: true, field: "appetite" } as const, ]; +const NursingPlot = ({ consultationId }: ConsultationTabProps) => { + const { t } = useTranslation(); + const [results, setResults] = useState<{ [date: string]: NursingPlotRes }>( + {}, + ); + const [currentPage, setCurrentPage] = useState(1); + const [totalCount, setTotalCount] = useState(0); + + useEffect(() => { + const fetchDailyRounds = async ( + currentPage: number, + consultationId: string, + ) => { + const { res, data } = await request(routes.dailyRoundsAnalyse, { + body: { page: currentPage, fields: NursingPlotFields }, + pathParams: { consultationId }, + }); + if (res?.ok && data) { + setResults(data.results as { [date: string]: NursingPlotRes }); + setTotalCount(data.count); + } + }; + + fetchDailyRounds(currentPage, consultationId); + }, [consultationId, currentPage]); + + const handlePagination = (page: number) => setCurrentPage(page); + + let fieldsToDisplay = new Set(); + + /** + * Transforms nursing procedure results into a structured format where dates are mapped to procedures and their descriptions. + * Groups nursing data by date, collecting unique procedures and their corresponding descriptions. + */ + const tableData = Object.entries(results).reduce( + (acc: Record>, [date, result]) => { + if ("nursing" in result) { + result.nursing.forEach((field) => { + if (field.procedure && !acc[date]) acc[date] = {}; + acc[date][field.procedure] = field.description; + // Add procedure to the set of procedures to display + fieldsToDisplay.add(field.procedure); + }); + } + return acc; + }, + {}, + ); + + fieldsToDisplay = fieldsToDisplay.intersection( + new Set(NURSING_CARE_PROCEDURES), + ); + + const rows = Array.from(fieldsToDisplay).map((procedure) => ({ + field: procedure, + title: t(`NURSING_CARE_PROCEDURE__${procedure}`), + })); + + return ( +
+
+ {fieldsToDisplay.size == 0 ? ( +
+
+ {t("no_data_found")} +
+
+ ) : ( + + )} +
+ + {totalCount > PAGINATION_LIMIT && fieldsToDisplay.size > 0 && ( +
+ +
+ )} +
+ ); +}; + const RoutineSection = ({ consultationId }: ConsultationTabProps) => { const { t } = useTranslation(); const [page, setPage] = useState(1); @@ -158,65 +220,11 @@ const RoutineSection = ({ consultationId }: ConsultationTabProps) => { return (
-
- - - - - ))} - - - - {ROUTINE_ROWS.map((row) => ( - - - {row.field && - Object.values(results).map((obj, idx) => ( - - ))} - - ))} - -
- {Object.keys(results).map((date) => ( - -

{formatDate(date)}

-

{formatTime(date)}

-
- {row.title ?? t(`LOG_UPDATE_FIELD_LABEL__${row.field!}`)} - - {(() => { - const value = obj[row.field]; - if (value == null) { - return "-"; - } - if (typeof value === "boolean") { - return t(value ? "yes" : "no"); - } - const choices = REVERSE_CHOICES[row.field]; - const choice = `${row.field.toUpperCase()}__${choices[value as keyof typeof choices]}`; - return t(choice); - })()} -
-
+ {totalCount != null && totalCount > PAGINATION_LIMIT && (
@@ -231,3 +239,24 @@ const RoutineSection = ({ consultationId }: ConsultationTabProps) => {
); }; + +export default function ConsultationNursingTab(props: ConsultationTabProps) { + const { t } = useTranslation(); + return ( +
+ +
+

{t("routine")}

+ +
+
+

{t("nursing_care")}

+ +
+
+ ); +} diff --git a/src/components/Facility/Consultations/LogUpdateAnalyseTable.tsx b/src/components/Facility/Consultations/LogUpdateAnalyseTable.tsx new file mode 100644 index 00000000000..43e59bebe7d --- /dev/null +++ b/src/components/Facility/Consultations/LogUpdateAnalyseTable.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { classNames, formatDate, formatTime } from "@/Utils/utils"; + +interface SharedSectionTableProps { + data: Record>; + rows: Array<{ title?: string; field?: string; subField?: boolean }>; + choices?: Record>; +} + +const LogUpdateAnalyseTable: React.FC = ({ + data, + rows, + choices = {}, +}) => { + const { t } = useTranslation(); + + const dataValues = React.useMemo(() => Object.values(data), [data]); + + const getDisplayValue = ( + value: string | boolean | null | undefined, + field?: string, + ): string => { + if (typeof value === "boolean") { + return t(value ? "yes" : "no"); + } + + if (field && choices[field]) { + const choiceMap = choices[field]; + const choice = + typeof value === "string" || typeof value === "number" + ? choiceMap[value] + : undefined; + return choice ? t(`${field.toUpperCase()}__${choice}`) : "-"; + } + + return typeof value === "string" ? value : "-"; + }; + + return ( +
+ + + + + {Object.keys(data).map((date) => ( + + ))} + + + + {rows.map((row) => ( + + + {dataValues.map((obj, idx) => { + const value = row.field ? obj[row.field] : undefined; + return ( + + ); + })} + + ))} + +
+

{formatDate(date)}

+

{formatTime(date)}

+
+ {row.title ?? t(`LOG_UPDATE_FIELD_LABEL__${row.field!}`)} + + {row.field ? getDisplayValue(value, row.field) : "-"} +
+
+ ); +}; + +export default LogUpdateAnalyseTable; diff --git a/src/components/Facility/Consultations/NursingPlot.tsx b/src/components/Facility/Consultations/NursingPlot.tsx deleted file mode 100644 index 13f5bb64201..00000000000 --- a/src/components/Facility/Consultations/NursingPlot.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import Pagination from "@/components/Common/Pagination"; -import { NursingPlotFields } from "@/components/Facility/models"; - -import { NURSING_CARE_PROCEDURES, PAGINATION_LIMIT } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; -import { formatDateTime } from "@/Utils/utils"; - -export const NursingPlot = ({ consultationId }: any) => { - const { t } = useTranslation(); - const [results, setResults] = useState({}); - const [currentPage, setCurrentPage] = useState(1); - const [totalCount, setTotalCount] = useState(0); - - useEffect(() => { - const fetchDailyRounds = async ( - currentPage: number, - consultationId: string, - ) => { - const { res, data } = await request(routes.dailyRoundsAnalyse, { - body: { page: currentPage, fields: NursingPlotFields }, - pathParams: { - consultationId, - }, - }); - if (res && res.ok && data) { - setResults(data.results); - setTotalCount(data.count); - } - }; - - fetchDailyRounds(currentPage, consultationId); - }, [consultationId, currentPage]); - - const handlePagination = (page: number) => { - setCurrentPage(page); - }; - - const data = Object.entries(results).map((key: any) => { - return { - date: formatDateTime(key[0]), - nursing: key[1]["nursing"], - }; - }); - - const dataToDisplay = data - .map((x) => - x.nursing.map((f: any) => { - f["date"] = x.date; - return f; - }), - ) - .reduce((accumulator, value) => accumulator.concat(value), []); - - const filterEmpty = (field: (typeof NURSING_CARE_PROCEDURES)[number]) => { - const filtered = dataToDisplay.filter((i: any) => i.procedure === field); - return filtered.length > 0; - }; - - const areFieldsEmpty = () => { - let emptyFieldCount = 0; - for (const field of NURSING_CARE_PROCEDURES) { - if (!filterEmpty(field)) emptyFieldCount++; - } - if (emptyFieldCount === NURSING_CARE_PROCEDURES.length) return true; - else return false; - }; - - return ( -
-
-
-
- {areFieldsEmpty() && ( -
-
- {t("no_data_found")} -
-
- )} - {NURSING_CARE_PROCEDURES.map( - (f) => - filterEmpty(f) && ( -
-
-
-

- {t(`NURSING_CARE_PROCEDURE__${f}`)} -

-
-
-
- {dataToDisplay - .filter((i: any) => i.procedure === f) - .map((care: any, index: number) => ( -
-
- {care.date} -
-
- {care.description} -
-
- ))} -
-
- ), - )} -
-
-
- - {!areFieldsEmpty() && totalCount > PAGINATION_LIMIT && ( -
- -
- )} -
- ); -}; diff --git a/src/components/Facility/models.tsx b/src/components/Facility/models.tsx index 79272ac3de7..a984efe6283 100644 --- a/src/components/Facility/models.tsx +++ b/src/components/Facility/models.tsx @@ -391,7 +391,10 @@ export const NursingPlotFields = [ ] as const satisfies (keyof DailyRoundsModel)[]; export type NursingPlotRes = { - nursing: any[]; + nursing: Array<{ + procedure: string; + description: string; + }>; }; export const RoutineFields = [ From 85a445c4dfe0720c26523b35831d55c5ce16604c Mon Sep 17 00:00:00 2001 From: JavidSumra <112365664+JavidSumra@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:34:55 +0530 Subject: [PATCH 6/6] Add function to authorize user to register patient (#9000) --- src/components/Facility/FacilityHome.tsx | 26 +++++---- src/components/Patient/PatientRegister.tsx | 61 +++++++++++++--------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/components/Facility/FacilityHome.tsx b/src/components/Facility/FacilityHome.tsx index 02c3f5386cb..e38c187590e 100644 --- a/src/components/Facility/FacilityHome.tsx +++ b/src/components/Facility/FacilityHome.tsx @@ -48,6 +48,8 @@ import uploadFile from "@/Utils/request/uploadFile"; import useQuery from "@/Utils/request/useQuery"; import { sleep } from "@/Utils/utils"; +import { patientRegisterAuth } from "../Patient/PatientRegister"; + type Props = { facilityId: string; }; @@ -458,17 +460,19 @@ export const FacilityHome = ({ facilityId }: Props) => { {CameraFeedPermittedUserTypes.includes(authUser.user_type) && ( )} - navigate(`/facility/${facilityId}/patient`)} - authorizeFor={NonReadOnlyUsers} - > - - {t("add_details_of_patient")} - + {patientRegisterAuth(authUser, facilityData, facilityId) && ( + navigate(`/facility/${facilityId}/patient`)} + authorizeFor={NonReadOnlyUsers} + > + + {t("add_details_of_patient")} + + )} { return ; } - const PatientRegisterAuth = () => { - const showAllFacilityUsers = ["DistrictAdmin", "StateAdmin"]; - if ( - !showAllFacilityUsers.includes(authUser.user_type) && - authUser.home_facility_object?.id === facilityId - ) { - return true; - } - if ( - authUser.user_type === "DistrictAdmin" && - authUser.district === facilityObject?.district - ) { - return true; - } - if ( - authUser.user_type === "StateAdmin" && - authUser.state === facilityObject?.state - ) { - return true; - } - - return false; - }; - - if (!isLoading && facilityId && facilityObject && !PatientRegisterAuth()) { + if ( + !isLoading && + facilityId && + facilityObject && + !patientRegisterAuth(authUser, facilityObject, facilityId) + ) { return ; } @@ -1717,3 +1700,31 @@ export const PatientRegister = (props: PatientRegisterProps) => { ); }; + +export function patientRegisterAuth( + authUser: UserModel, + facilityObject: FacilityModel | undefined, + facilityId: string, +) { + const showAllFacilityUsers = ["DistrictAdmin", "StateAdmin"]; + if ( + !showAllFacilityUsers.includes(authUser.user_type) && + authUser.home_facility_object?.id === facilityId + ) { + return true; + } + if ( + authUser.user_type === "DistrictAdmin" && + authUser.district === facilityObject?.district + ) { + return true; + } + if ( + authUser.user_type === "StateAdmin" && + authUser.state === facilityObject?.state + ) { + return true; + } + + return false; +}