From 7c587036ef974f5b6b0ab1e4931b4edaa249a266 Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Mon, 6 Jan 2025 19:08:31 +0530 Subject: [PATCH 1/7] Enhance encounter data handling by adding encounterId prop across multiple components (#9793) --- .../Common/Charts/ObservationChart.tsx | 7 + .../Common/Charts/ObservationHistoryTable.tsx | 3 + .../ConsultationDetails/ObservationsList.tsx | 146 ++++--- .../QuestionnaireResponsesList.tsx | 5 +- .../StructuredResponseView.tsx | 16 +- .../Encounters/tabs/EncounterPlotsTab.tsx | 1 + .../Encounters/tabs/EncounterUpdatesTab.tsx | 383 ++---------------- 7 files changed, 143 insertions(+), 418 deletions(-) diff --git a/src/components/Common/Charts/ObservationChart.tsx b/src/components/Common/Charts/ObservationChart.tsx index 9d1abfd9cc5..f79d529abb4 100644 --- a/src/components/Common/Charts/ObservationChart.tsx +++ b/src/components/Common/Charts/ObservationChart.tsx @@ -51,6 +51,7 @@ interface ObservationVisualizerProps { codeGroups: CodeGroup[]; height?: number; gridCols?: number; + encounterId: string; } interface ChartData { @@ -98,6 +99,7 @@ const formatChartDate = ( export const ObservationVisualizer = ({ patientId, codeGroups, + encounterId, height = 300, gridCols = 2, }: ObservationVisualizerProps) => { @@ -108,10 +110,14 @@ export const ObservationVisualizer = ({ queryKey: [ "observations", patientId, + encounterId, allCodes.map((c) => c.code).join(","), ], queryFn: query(routes.observationsAnalyse, { pathParams: { patientId }, + queryParams: { + encounter: encounterId, + }, body: { codes: allCodes, }, @@ -380,6 +386,7 @@ export const ObservationVisualizer = ({ diff --git a/src/components/Common/Charts/ObservationHistoryTable.tsx b/src/components/Common/Charts/ObservationHistoryTable.tsx index e698309de4c..9e3940bff17 100644 --- a/src/components/Common/Charts/ObservationHistoryTable.tsx +++ b/src/components/Common/Charts/ObservationHistoryTable.tsx @@ -28,6 +28,7 @@ interface PaginatedResponse { interface ObservationHistoryTableProps { patientId: string; + encounterId: string; codes: Code[]; } @@ -45,6 +46,7 @@ const formatDate = (dateString: string) => { export const ObservationHistoryTable = ({ patientId, + encounterId, codes, }: ObservationHistoryTableProps) => { const { ref, inView } = useInView(); @@ -57,6 +59,7 @@ export const ObservationHistoryTable = ({ const response = await query(routes.listObservations, { pathParams: { patientId }, queryParams: { + encounter: encounterId, limit: String(LIMIT), codes: codes.map((c) => c.code).join(","), offset: String(pageParam), diff --git a/src/components/Facility/ConsultationDetails/ObservationsList.tsx b/src/components/Facility/ConsultationDetails/ObservationsList.tsx index 9a98c2a73fa..a71c2a0fbfa 100644 --- a/src/components/Facility/ConsultationDetails/ObservationsList.tsx +++ b/src/components/Facility/ConsultationDetails/ObservationsList.tsx @@ -1,15 +1,22 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { useInView } from "react-intersection-observer"; import CareIcon from "@/CAREUI/icons/CareIcon"; -import PaginatedList from "@/CAREUI/misc/PaginatedList"; import { Card } from "@/components/ui/card"; import routes from "@/Utils/request/api"; +import query from "@/Utils/request/query"; +import { HTTPError } from "@/Utils/request/types"; +import { PaginatedResponse } from "@/Utils/request/types"; import { formatDateTime } from "@/Utils/utils"; import { Encounter } from "@/types/emr/encounter"; import { Observation } from "@/types/emr/observation"; +const LIMIT = 20; + interface Props { encounter: Encounter; } @@ -18,63 +25,96 @@ export default function ObservationsList(props: Props) { const { t } = useTranslation(); const patientId = props.encounter.patient.id; const encounterId = props.encounter.id; + const { ref, inView } = useInView(); + + const { data, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage } = + useInfiniteQuery, HTTPError>({ + queryKey: ["observations", patientId, encounterId], + queryFn: async ({ pageParam = 0 }) => { + const response = await query(routes.listObservations, { + pathParams: { patientId }, + queryParams: { + encounter: encounterId, + ignore_group: true, + limit: String(LIMIT), + offset: String(pageParam), + }, + })({ signal: new AbortController().signal }); + return response as PaginatedResponse; + }, + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + const currentOffset = allPages.length * LIMIT; + return currentOffset < lastPage.count ? currentOffset : null; + }, + }); + + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + if (isLoading) { + return ( + +
+ {t("loading")} +
+
+ ); + } + + const observations = data?.pages.flatMap((page) => page.results) ?? []; + + if (observations.length === 0) { + return ( + +
+ {t("no_observations")} +
+
+ ); + } return ( - - {() => ( -
-
- - -
- {t("no_observations")} +
+
+ {observations.map((item: Observation) => ( + +
+
+ + {formatDateTime(item.effective_datetime)} +
+
+ {item.main_code.display || item.main_code.code} +
+ {item.value.value_quantity && ( +
+ {item.value.value_quantity.value}{" "} + {item.value.value_quantity.code.display}
- - - - className="grid gap-4"> - {(item) => ( - -
-
- - {formatDateTime(item.effective_datetime)} -
-
- {item.main_code.display || item.main_code.code} -
- {item.value.value_quantity && ( -
- {item.value.value_quantity.value}{" "} - {item.value.value_quantity.code.display} -
- )} - {item.value.value && ( -
{item.value.value}
- )} - {item.note && ( -
- {item.note} -
- )} -
-
)} - - -
- + {item.value.value && ( +
{item.value.value}
+ )} + {item.note && ( +
+ {item.note} +
+ )} +
+ + ))} + {hasNextPage && ( +
+
+ {isFetchingNextPage ? t("loading_more") : t("load_more")}
-
- )} - + )} +
+
); } diff --git a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx index b82b37aaca5..11f9ea4529b 100644 --- a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx +++ b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx @@ -149,7 +149,9 @@ export default function QuestionnaireResponsesList({ encounter }: Props) { route={routes.getQuestionnaireResponses} pathParams={{ patientId: encounter.patient.id, - encounterId: encounter.id, + }} + query={{ + encounter: encounter.id, }} > {() => ( @@ -264,6 +266,7 @@ export default function QuestionnaireResponsesList({ encounter }: Props) { type={type} id={response.id} patientId={encounter.patient.id} + encounterId={encounter.id} /> ); }, diff --git a/src/components/Facility/ConsultationDetails/StructuredResponseView.tsx b/src/components/Facility/ConsultationDetails/StructuredResponseView.tsx index ba4e3ba2e98..9d6b06ae2b8 100644 --- a/src/components/Facility/ConsultationDetails/StructuredResponseView.tsx +++ b/src/components/Facility/ConsultationDetails/StructuredResponseView.tsx @@ -16,9 +16,15 @@ interface Props { type: string; id: string; patientId: string; + encounterId: string; } -export function StructuredResponseView({ type, id, patientId }: Props) { +export function StructuredResponseView({ + type, + id, + patientId, + encounterId, +}: Props) { const getRouteAndParams = () => { const params: Record = { patientId }; switch (type) { @@ -26,19 +32,20 @@ export function StructuredResponseView({ type, id, patientId }: Props) { return { route: symptomApi.retrieveSymptom, pathParams: { ...params, symptomId: id }, + queryParams: { encounter: encounterId }, }; case "diagnosis": return { route: diagnosisApi.retrieveDiagnosis, pathParams: { ...params, diagnosisId: id }, + queryParams: { encounter: encounterId }, }; case "allergy_intolerance": return { route: allergyApi.retrieveAllergy, pathParams: { ...params, allergyId: id }, + queryParams: { encounter: encounterId }, }; - default: - return null; } }; @@ -47,7 +54,8 @@ export function StructuredResponseView({ type, id, patientId }: Props) { const { data, isLoading, error } = useQuery({ queryKey: [type, id], queryFn: query(routeConfig?.route as any, { - pathParams: routeConfig?.pathParams || { patientId }, + pathParams: routeConfig?.pathParams, + queryParams: routeConfig?.queryParams, }), enabled: !!id && !!routeConfig, }); diff --git a/src/pages/Encounters/tabs/EncounterPlotsTab.tsx b/src/pages/Encounters/tabs/EncounterPlotsTab.tsx index c0857b2d000..cc9c6d57689 100644 --- a/src/pages/Encounters/tabs/EncounterPlotsTab.tsx +++ b/src/pages/Encounters/tabs/EncounterPlotsTab.tsx @@ -55,6 +55,7 @@ export const EncounterPlotsTab = (props: EncounterTabProps) => { diff --git a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx index f8eab9f6743..fb49046d1cb 100644 --- a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx @@ -5,372 +5,35 @@ import { SymptomsList } from "@/components/Patient/symptoms/list"; import { EncounterTabProps } from "@/pages/Encounters/EncounterShow"; -export const EncounterUpdatesTab = (props: EncounterTabProps) => { +export const EncounterUpdatesTab = ({ + encounter, + patient, +}: EncounterTabProps) => { return ( -
-
-
-
- +
+ {/* Main Content Area */} +
+ {/* Left Column - Symptoms, Diagnoses, and Questionnaire Responses */} +
+ {/* Symptoms Section */} +
+
-
- -
-
- -
-
- {/*
- - - - - {props.consultationData.history_of_present_illness && ( -
-
-

- History of Present Illness -

-
- -
-
-
- )} - - {props.consultationData.examination_details && ( -
-
-

- Examination details and Clinical conditions:{" "} -

-
- -
-
-
- )} - {props.consultationData.treatment_plan && ( -
-
-

- Treatment Summary -

-
- -
-
-
- )} - {props.consultationData.consultation_notes && ( -
-
-

- General Instructions -

-
- -
-
-
- )} - {(props.consultationData.operation ?? - props.consultationData.special_instruction) && ( -
-
-

- Notes -

-
- {props.consultationData.operation && ( -
-
Operation
- -
- )} - - {props.consultationData.special_instruction && ( -
-
Special Instruction
- -
- )} -
-
-
- )} + {/* Diagnoses Section */} +
+
- {props.consultationData.procedure && - props.consultationData.procedure.length > 0 && ( -
-
- - - - - - - - - - - {props.consultationData.procedure?.map( - (procedure, index) => ( - - - - - - - ), - )} - -
- Procedure - - Notes - - Repetitive - - Time / Frequency -
- {procedure.procedure} - - {procedure.notes} - - {procedure.repetitive ? "Yes" : "No"} - - {procedure.repetitive - ? procedure.frequency - : formatDateTime(String(procedure.time))} -
-
-
- )} - {props.consultationData.intubation_start_date && ( -
-
-

- Date/Size/LL:{" "} -

-
-
- Intubation Date{" - "} - - {formatDateTime( - props.consultationData.intubation_start_date, - )} - -
-
- Extubation Date{" - "} - - {props.consultationData.intubation_end_date && - formatDateTime( - props.consultationData.intubation_end_date, - )} - -
-
- ETT/TT (mmid){" - "} - - {props.consultationData.ett_tt} - -
-
- Cuff Pressure (mmhg){" - "} - - {props.consultationData.cuff_pressure} - -
-
-
-
- )} - - {props.consultationData.lines?.length > 0 && ( -
-
-

- Lines and Catheters -

-
- {props.consultationData.lines?.map( - (line: any, idx: number) => ( -
-
{line.type}
-

- Details: -
- {line.other_type} -

-

- Insertion Date:{" "} - - {formatDateTime(line.start_date)} - -

-

- Site/Level of Fixation:
- - {line.site} - -

-
- ), - )} -
-
-
- )} -
-
-
-

- Body Details -

-
-
- Gender {" - "} - - {props.patientData.gender ?? "-"} - -
-
- Age {" - "} - - {formatPatientAge(props.patientData)} - -
-
- Weight {" - "} - - {props.consultationData.weight - ? `${props.consultationData.weight} kg` - : "Unspecified"} - -
-
- Height {" - "} - - {props.consultationData.height - ? `${props.consultationData.height} cm` - : "Unspecified"} - -
-
- Body Surface Area {" - "} - - {props.consultationData.weight && - props.consultationData.height ? ( - <> - {Math.sqrt( - (Number(props.consultationData.weight) * - Number(props.consultationData.height)) / - 3600, - ).toFixed(2)} - m2 - - ) : ( - "Unspecified" - )} - -
-
- Blood Group {" - "} - - {props.patientData.blood_group ?? "-"} - -
-
-
-
- {((props.patientData.is_antenatal && - isAntenatal(props.patientData.last_menstruation_start_date)) || - isPostPartum(props.patientData.date_of_delivery)) && ( -
-

- Perinatal Status -

- -
- {props.patientData.is_antenatal && - isAntenatal( - props.patientData.last_menstruation_start_date, - ) && ( - - )} - {isPostPartum(props.patientData.date_of_delivery) && ( - - )} -
- {props.patientData.last_menstruation_start_date && ( -

- - Last Menstruation: - - {formatDate( - props.patientData.last_menstruation_start_date, - )} - -

- )} - - {props.patientData.date_of_delivery && ( -

- - Date of Delivery: - - {formatDate(props.patientData.date_of_delivery)} - -

- )} -
- )} + {/* Questionnaire Responses Section */} +
+
-
*/} -
- +
+ + {/* Right Column - Observations */} +
+
From b2083512fd26ff0a0df5a4fb4bc89d023ecb7d56 Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Mon, 6 Jan 2025 19:29:03 +0530 Subject: [PATCH 2/7] Disable encounter create during save (#9795) * Enhance encounter data handling by adding encounterId prop across multiple components - Added `encounterId` prop to `ObservationChart`, `ObservationHistoryTable`, `ObservationsList`, `QuestionnaireResponsesList`, and `StructuredResponseView` components to improve data fetching and display related to specific encounters. - Updated query parameters in API calls to include `encounterId` for better data context. - Refactored `EncounterPlotsTab` and `EncounterUpdatesTab` to pass the new `encounterId` prop, ensuring consistent data handling across the application. This change improves the overall functionality and user experience by ensuring that encounter-specific data is accurately retrieved and displayed. * fix: disable encounter create button during save to prevent multiple submissions #9794 --- src/components/Encounter/CreateEncounterForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Encounter/CreateEncounterForm.tsx b/src/components/Encounter/CreateEncounterForm.tsx index 2f1d5dbdc07..8bc81f74865 100644 --- a/src/components/Encounter/CreateEncounterForm.tsx +++ b/src/components/Encounter/CreateEncounterForm.tsx @@ -145,7 +145,7 @@ export default function CreateEncounterForm({ }, }); - const { mutate: createEncounter } = useMutation({ + const { mutate: createEncounter, isPending } = useMutation({ mutationFn: mutate(routes.encounter.create), onSuccess: (data: Encounter) => { toast.success("Encounter created successfully"); @@ -318,8 +318,8 @@ export default function CreateEncounterForm({ }} /> - From 96192bc42c6e0f8dac3871702c4ac27c2f10e39c Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Mon, 6 Jan 2025 21:58:12 +0530 Subject: [PATCH 3/7] Refactor PatientHome and EncounterShow components; update FacilityOrganizationSelector labels - Removed unused state and commented-out code in PatientHome for improved readability. - Enhanced patient data display by updating the last updated and created by fields to use `updated_by` instead of `modified_by`. - Updated date formatting functions to ensure consistent display of patient and encounter dates. - Changed labels in FacilityOrganizationSelector from "Organization" to "Select Department" and adjusted related text for clarity. These changes streamline the codebase and improve user interface clarity. --- src/components/Patient/PatientHome.tsx | 195 ++++-------------- src/pages/Encounters/EncounterShow.tsx | 57 +---- .../FacilityOrganizationSelector.tsx | 8 +- src/types/emr/newPatient.ts | 2 +- 4 files changed, 44 insertions(+), 218 deletions(-) diff --git a/src/components/Patient/PatientHome.tsx b/src/components/Patient/PatientHome.tsx index f2e0ea7f737..713031169e7 100644 --- a/src/components/Patient/PatientHome.tsx +++ b/src/components/Patient/PatientHome.tsx @@ -13,7 +13,7 @@ import { patientTabs } from "@/components/Patient/PatientDetailsTab"; import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; -import { formatPatientAge } from "@/Utils/utils"; +import { formatDateTime, formatPatientAge, relativeDate } from "@/Utils/utils"; import { Patient } from "@/types/emr/newPatient"; export const PatientHome = (props: { @@ -22,18 +22,9 @@ export const PatientHome = (props: { page: (typeof patientTabs)[0]["route"]; }) => { const { facilityId, id, page } = props; - // const [patientData, setPatientData] = useState({}); const { t } = useTranslation(); - // const [assignedVolunteer, setAssignedVolunteer] = useState< - // AssignedToObjectModel | undefined - // >(patientData.assigned_to_object); - - // useEffect(() => { - // setAssignedVolunteer(patientData.assigned_to_object); - // }, [patientData.assigned_to_object]); - const { data: patientData, isLoading } = useQuery({ queryKey: ["patient", id], queryFn: query(routes.patient.getPatient, { @@ -44,67 +35,10 @@ export const PatientHome = (props: { enabled: !!id, }); - // const handleAssignedVolunteer = async () => { - // const previousVolunteerId = patientData?.assigned_to; - - // const { res, data } = await request(routes.patchPatient, { - // pathParams: { - // id: patientData.id as string, - // }, - // body: { - // assigned_to: (assignedVolunteer as UserBareMinimum)?.id || null, - // }, - // }); - - // if (res?.ok && data) { - // setPatientData(data); - - // if (!previousVolunteerId && assignedVolunteer) { - // Notification.Success({ - // msg: t("volunteer_assigned"), - // }); - // } else if (previousVolunteerId && assignedVolunteer) { - // Notification.Success({ - // msg: t("volunteer_update"), - // }); - // } else if (!assignedVolunteer) { - // Notification.Success({ - // msg: t("volunteer_unassigned"), - // }); - // } - - // refetch(); - // } - - // setOpenAssignVolunteerDialog(false); - - // if (errors["assignedVolunteer"]) delete errors["assignedVolunteer"]; - // }; - if (isLoading) { return ; } - // const handlePatientTransfer = async (value: boolean) => { - // await request(routes.patchPatient, { - // pathParams: { - // id: patientData.id as string, - // }, - // body: { allow_transfer: value }, - // onResponse: ({ res }) => { - // if (res?.status === 200) { - // setPatientData((prev) => ({ - // ...prev, - // allow_transfer: value, - // })); - // Notification.Success({ - // msg: t("transfer_status_updated"), - // }); - // } - // }, - // }); - // }; - const Tab = patientTabs.find((t) => t.route === page)?.component; if (!patientData) { @@ -154,36 +88,6 @@ export const PatientHome = (props: {
-
-
-
- {/* {patientData?.is_active && - (!patientData?.last_consultation || - patientData?.last_consultation?.discharge_date) && ( -
- -
- )} */} -
-
-
@@ -247,26 +151,6 @@ export const PatientHome = (props: {
- - {/* {NonReadOnlyUsers && ( -
- -
- )} */}
@@ -276,53 +160,50 @@ export const PatientHome = (props: { id="actions" className="my-2 flex h-full flex-col justify-between space-y-2" > -
- {/*
-
-
- {t("last_updated_by")}{" "} - - {patientData.last_edited?.first_name}{" "} - {patientData.last_edited?.last_name} - -
-
-
- - {patientData.modified_date - ? formatDateTime(patientData.modified_date) - : "--:--"} - +
+
+
+ {t("last_updated_by")}{" "} + + {patientData.updated_by.first_name}{" "} + {patientData.updated_by.last_name} + +
+
+
+ {patientData.modified_date - ? relativeDate(patientData.modified_date) + ? formatDateTime(patientData.modified_date) : "--:--"} -
-
-
*/} - { - // TODO: Add this back when backend provides created_date - /*
-
- {t("patient_profile_created_by")}{" "} - - {patientData.created_by?.first_name}{" "} - {patientData.created_by?.last_name} + {patientData.modified_date + ? relativeDate(patientData.modified_date) + : "--:--"}
-
-
- - {patientData.created_date - ? formatDateTime(patientData.created_date) - : "--:--"} - +
+
+ +
+
+ {t("patient_profile_created_by")}{" "} + + {patientData.created_by.first_name}{" "} + {patientData.created_by.last_name} + +
+
+
+ {patientData.created_date - ? relativeDate(patientData.created_date) + ? formatDateTime(patientData.created_date) : "--:--"} -
+ + {patientData.created_date + ? relativeDate(patientData.created_date) + : "--:--"}
-
*/ - } +
+
diff --git a/src/pages/Encounters/EncounterShow.tsx b/src/pages/Encounters/EncounterShow.tsx index fd494760724..0440aa49aae 100644 --- a/src/pages/Encounters/EncounterShow.tsx +++ b/src/pages/Encounters/EncounterShow.tsx @@ -247,43 +247,9 @@ export const EncounterShow = (props: Props) => { { - // consultationQuery.refetch(); - // patientDataQuery.refetch(); - }} + fetchPatientData={() => {}} /> - {/*
- {consultationData.admitted_to && ( -
-
- Patient - {consultationData.discharge_date - ? " Discharged from" - : " Admitted to"} - - {consultationData.admitted_to} - -
- {(consultationData.discharge_date ?? - consultationData.encounter_date) && ( -
- {relativeTime( - consultationData.discharge_date - ? consultationData.discharge_date - : consultationData.encounter_date, - )} -
- )} -
- {consultationData.encounter_date && - formatDateTime(consultationData.encounter_date)} - {consultationData.discharge_date && - ` - ${formatDateTime(consultationData.discharge_date)}`} -
-
- )} -
*/}
@@ -292,12 +258,6 @@ export const EncounterShow = (props: Props) => {   {formatDateTime(encounterData.modified_date)} - {/* */}
@@ -329,20 +289,5 @@ export const EncounterShow = (props: Props) => {
- - // - - // {showPatientNotesPopup && ( - // - // )} ); }; diff --git a/src/pages/FacilityOrganization/components/FacilityOrganizationSelector.tsx b/src/pages/FacilityOrganization/components/FacilityOrganizationSelector.tsx index 4d4f6c16b0c..92b0778d184 100644 --- a/src/pages/FacilityOrganization/components/FacilityOrganizationSelector.tsx +++ b/src/pages/FacilityOrganization/components/FacilityOrganizationSelector.tsx @@ -106,7 +106,7 @@ export default function FacilityOrganizationSelector( }; return ( - +
{/* Selected Organization Display */} {selectedOrganization && ( @@ -116,12 +116,12 @@ export default function FacilityOrganizationSelector(

{selectedOrganization.name}

{selectedOrganization.has_children && (

- You can select a sub-organization or keep this selection + You can select a sub-department or keep this selection

)}
{selectedOrganization.has_children && ( - Has Sub-organizations + Has Sub-departments )}
@@ -178,7 +178,7 @@ export default function FacilityOrganizationSelector( handleLevelChange(value, selectedLevels.length) } placeholder={`Select ${ - selectedLevels.length ? "sub-organization" : "organization" + selectedLevels.length ? "sub-department" : "Department" }...`} /> diff --git a/src/types/emr/newPatient.ts b/src/types/emr/newPatient.ts index 377a7cd2d98..3e5973b81fd 100644 --- a/src/types/emr/newPatient.ts +++ b/src/types/emr/newPatient.ts @@ -33,7 +33,7 @@ export interface Patient { modified_date: string; geo_organization: Organization; created_by: UserBareMinimum; - modified_by: UserBareMinimum; + updated_by: UserBareMinimum; } export interface PartialPatientModel { From 9f7715d1722bdc56adadccc48367fe555acd165e Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 6 Jan 2025 22:17:23 +0530 Subject: [PATCH 4/7] Partial Cleanup Public Router | Public Pages Header --- public/locale/en.json | 6 +- src/App.tsx | 2 +- src/Providers/AuthUserProvider.tsx | 30 +++++- src/Routers/PatientRouter.tsx | 4 +- .../{SessionRouter.tsx => PublicRouter.tsx} | 2 +- src/Routers/index.tsx | 4 +- src/components/Auth/Login.tsx | 14 ++- src/components/Common/LoginHeader.tsx | 94 +++++++++++++++++++ src/pages/Facility/FacilitiesPage.tsx | 47 +--------- src/pages/Facility/FacilityDetailsPage.tsx | 54 +---------- src/pages/Landing/LandingPage.tsx | 16 +--- src/pages/Patient/index.tsx | 2 +- 12 files changed, 156 insertions(+), 119 deletions(-) rename src/Routers/{SessionRouter.tsx => PublicRouter.tsx} (98%) create mode 100644 src/components/Common/LoginHeader.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 0d34499cb35..a6b499278b3 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -660,6 +660,7 @@ "cylinders": "Cylinders", "cylinders_per_day": "Cylinders/day", "daily_rounds": "Daily Rounds", + "dashboard": "Dashboard", "date": "Date", "date_and_time": "Date and Time", "date_declared_positive": "Date of declaring positive", @@ -1147,6 +1148,7 @@ "log_report": "Log Report", "log_update": "Log Update", "log_updates": "Log Updates", + "logged_in_as": "Logged in as", "login": "Login", "logout": "Log Out", "longitude_invalid": "Longitude must be between -180 and 180", @@ -1364,6 +1366,7 @@ "patient_consultation__treatment__summary__spo2": "SpO2", "patient_consultation__treatment__summary__temperature": "Temperature", "patient_created": "Patient Created", + "patient_dashboard": "Patient Dashboard", "patient_details": "Patient Details", "patient_details_incomplete": "Patient Details Incomplete", "patient_face": "Patient Face", @@ -1680,7 +1683,8 @@ "show_default_presets": "Show Default Presets", "show_patient_presets": "Show Patient Presets", "show_unread_notifications": "Show Unread", - "sign_out": "Sign Out", + "sign_in": "Sign in", + "sign_out": "Sign out", "skill_add_error": "Error while adding skill", "skill_added_successfully": "Skill added successfully", "skills": "Skills", diff --git a/src/App.tsx b/src/App.tsx index c30bd29db1f..e1afe64e966 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,7 +45,7 @@ const App = () => { } + unauthorized={} otpAuthorized={} > diff --git a/src/Providers/AuthUserProvider.tsx b/src/Providers/AuthUserProvider.tsx index 66c3df15994..a9d6394448b 100644 --- a/src/Providers/AuthUserProvider.tsx +++ b/src/Providers/AuthUserProvider.tsx @@ -27,14 +27,19 @@ export default function AuthUserProvider({ otpAuthorized, }: Props) { const queryClient = useQueryClient(); + const [accessToken, setAccessToken] = useState( + localStorage.getItem(LocalStorageKeys.accessToken), + ); const { data: user, isLoading } = useQuery({ queryKey: ["currentUser"], queryFn: query(routes.currentUser, { silent: true }), retry: false, + enabled: !!accessToken, }); const [isOTPAuthorized, setIsOTPAuthorized] = useState(false); + console.log("isOTPAuthorized", isOTPAuthorized); const tokenData: TokenData = JSON.parse( localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", @@ -62,6 +67,17 @@ export default function AuthUserProvider({ ); }, [user]); + useEffect(() => { + // Listen for localStorage changes + const listener = (event: StorageEvent) => { + if (event.key === LocalStorageKeys.accessToken) { + setAccessToken(event.newValue); + } + }; + addEventListener("storage", listener); + return () => removeEventListener("storage", listener); + }, []); + const signIn = useCallback( async (creds: { username: string; password: string }) => { const query = await request(routes.login, { body: creds }); @@ -70,7 +86,7 @@ export default function AuthUserProvider({ localStorage.setItem(LocalStorageKeys.accessToken, query.data.access); localStorage.setItem(LocalStorageKeys.refreshToken, query.data.refresh); - await queryClient.resetQueries({ queryKey: ["currentUser"] }); + await queryClient.invalidateQueries({ queryKey: ["currentUser"] }); if (location.pathname === "/" || location.pathname === "/login") { navigate(getRedirectOr("/")); @@ -120,9 +136,19 @@ export default function AuthUserProvider({ return ; } + const SelectedRouter = () => { + if (user) { + return children; + } else if (isOTPAuthorized) { + return otpAuthorized; + } else { + return unauthorized; + } + }; + return ( - {!user ? (isOTPAuthorized ? otpAuthorized : unauthorized) : children} + ); } diff --git a/src/Routers/PatientRouter.tsx b/src/Routers/PatientRouter.tsx index 734c4205a87..ded69ce375d 100644 --- a/src/Routers/PatientRouter.tsx +++ b/src/Routers/PatientRouter.tsx @@ -13,7 +13,7 @@ import { AppointmentSuccess } from "@/pages/Appoinments/Success"; import { FacilitiesPage } from "@/pages/Facility/FacilitiesPage"; import PatientIndex from "@/pages/Patient/index"; -import SessionRouter from "./SessionRouter"; +import PublicRouter from "./PublicRouter"; const PatientRoutes = { "/nearby_facilities": () => , @@ -39,7 +39,7 @@ export default function PatientRouter() { const pages = useRoutes(PatientRoutes); if (!pages) { - return ; + return ; } return ( diff --git a/src/Routers/SessionRouter.tsx b/src/Routers/PublicRouter.tsx similarity index 98% rename from src/Routers/SessionRouter.tsx rename to src/Routers/PublicRouter.tsx index 56c36536b8d..62c67eb8cbf 100644 --- a/src/Routers/SessionRouter.tsx +++ b/src/Routers/PublicRouter.tsx @@ -60,6 +60,6 @@ export const routes = { "/invalid-reset": () => , }; -export default function SessionRouter() { +export default function PublicRouter() { return useRoutes(routes) || ; } diff --git a/src/Routers/index.tsx b/src/Routers/index.tsx index eef2c30f2d9..0c0140a5e2d 100644 --- a/src/Routers/index.tsx +++ b/src/Routers/index.tsx @@ -1,7 +1,7 @@ import AppRouter from "@/Routers/AppRouter"; import PatientRouter from "@/Routers/PatientRouter"; -import SessionRouter from "@/Routers/SessionRouter"; +import PublicRouter from "@/Routers/PublicRouter"; -const routers = { PatientRouter, SessionRouter, AppRouter }; +const routers = { PatientRouter, PublicRouter, AppRouter }; export default routers; diff --git a/src/components/Auth/Login.tsx b/src/components/Auth/Login.tsx index 531093588e4..fa2df3f0c17 100644 --- a/src/components/Auth/Login.tsx +++ b/src/components/Auth/Login.tsx @@ -1,6 +1,6 @@ import careConfig from "@careConfig"; import { useMutation } from "@tanstack/react-query"; -import { Link } from "raviger"; +import { Link, useQueryParams } from "raviger"; import { useEffect, useState } from "react"; import ReCaptcha from "react-google-recaptcha"; import { useTranslation } from "react-i18next"; @@ -45,6 +45,10 @@ interface LoginFormData { type LoginMode = "staff" | "patient"; +interface LoginProps { + forgot?: boolean; +} + interface OtpError { type: string; loc: string[]; @@ -62,7 +66,7 @@ interface OtpLoginData { otp: string; } -const Login = (props: { forgot?: boolean }) => { +const Login = (props: LoginProps) => { const { signIn } = useAuthContext(); const { reCaptchaSiteKey, urls, stateLogo, customLogo, customLogoAlt } = careConfig; @@ -72,6 +76,8 @@ const Login = (props: { forgot?: boolean }) => { password: "", }; const { forgot } = props; + const [params] = useQueryParams(); + const { mode } = params; const initErr: any = {}; const [form, setForm] = useState(initForm); const [errors, setErrors] = useState(initErr); @@ -80,7 +86,9 @@ const Login = (props: { forgot?: boolean }) => { // display spinner while login is under progress const [loading, setLoading] = useState(false); const [forgotPassword, setForgotPassword] = useState(forgot); - const [loginMode, setLoginMode] = useState("staff"); + const [loginMode, setLoginMode] = useState( + mode === "patient" ? "patient" : "staff", + ); const [isOtpSent, setIsOtpSent] = useState(false); const [phone, setPhone] = useState(""); const [otp, setOtp] = useState(""); diff --git a/src/components/Common/LoginHeader.tsx b/src/components/Common/LoginHeader.tsx new file mode 100644 index 00000000000..6a8bb48a0f6 --- /dev/null +++ b/src/components/Common/LoginHeader.tsx @@ -0,0 +1,94 @@ +import dayjs from "dayjs"; +import { navigate } from "raviger"; +import { useTranslation } from "react-i18next"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import { usePatientSignOut } from "@/hooks/usePatientSignOut"; + +import { LocalStorageKeys } from "@/common/constants"; + +import { TokenData } from "@/types/auth/otpToken"; + +export const LoginHeader = () => { + const { t } = useTranslation(); + const signOut = usePatientSignOut(); + + const tokenData: TokenData = JSON.parse( + localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", + ); + + const isLoggedIn = + tokenData.token && + Object.keys(tokenData).length > 0 && + dayjs(tokenData.createdAt).isAfter(dayjs().subtract(14, "minutes")); + + if (isLoggedIn) { + const phoneNumber = tokenData.phoneNumber?.replace("+91", "") || ""; + const initials = phoneNumber.slice(-2); + + return ( +
+
+ + + + + + + + {tokenData.phoneNumber} + + + + {t("sign_out")} + + + +
+
+ ); + } + + return ( +
+
+ +
+
+ ); +}; diff --git a/src/pages/Facility/FacilitiesPage.tsx b/src/pages/Facility/FacilitiesPage.tsx index d0f9f4a6eb2..7ef989674fe 100644 --- a/src/pages/Facility/FacilitiesPage.tsx +++ b/src/pages/Facility/FacilitiesPage.tsx @@ -1,25 +1,22 @@ import careConfig from "@careConfig"; import { useQuery } from "@tanstack/react-query"; -import dayjs from "dayjs"; -import { Link, navigate } from "raviger"; +import { Link } from "raviger"; import { useTranslation } from "react-i18next"; -import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import Loading from "@/components/Common/Loading"; +import { LoginHeader } from "@/components/Common/LoginHeader"; import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields"; import { FacilityModel } from "@/components/Facility/models"; import useFilters from "@/hooks/useFilters"; -import { CarePatientTokenKey } from "@/common/constants"; import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants"; import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; import { PaginatedResponse } from "@/Utils/request/types"; -import { TokenData } from "@/types/auth/otpToken"; import { FacilityCard } from "./components/FacilityCard"; @@ -30,10 +27,6 @@ export function FacilitiesPage() { limit: RESULTS_PER_PAGE_LIMIT, }); - const tokenData: TokenData = JSON.parse( - localStorage.getItem(CarePatientTokenKey) || "{}", - ); - const { t } = useTranslation(); const { data: facilitiesResponse, isLoading } = useQuery< @@ -55,40 +48,6 @@ export function FacilitiesPage() { enabled: !!qParams.organization, }); - const GetLoginHeader = () => { - if ( - tokenData && - dayjs(tokenData.createdAt).isAfter(dayjs().subtract(14, "minutes")) - ) { - return ( -
-
- -
-
- ); - } - return ( -
-
- -
-
- ); - }; - return (
@@ -97,7 +56,7 @@ export function FacilitiesPage() { Care Logo
- +
@@ -79,10 +73,6 @@ export function FacilityDetailsPage({ id }: Props) { ); } - const tokenData: TokenData = JSON.parse( - localStorage.getItem(CarePatientTokenKey) || "{}", - ); - if (!facility) { return (
@@ -102,47 +92,9 @@ export function FacilityDetailsPage({ id }: Props) { ); } - const GetLoginHeader = () => { - if ( - tokenData && - dayjs(tokenData.createdAt).isAfter(dayjs().subtract(14, "minutes")) - ) { - return ( -
-
-
- Logged in as{" "} - {tokenData.phoneNumber} -
- -
-
- ); - } - return ( -
-
- -
-
- ); - }; - return (
-
+
- +
diff --git a/src/pages/Landing/LandingPage.tsx b/src/pages/Landing/LandingPage.tsx index 52bbc413b99..b001ccf51b3 100644 --- a/src/pages/Landing/LandingPage.tsx +++ b/src/pages/Landing/LandingPage.tsx @@ -13,6 +13,8 @@ import { CommandItem, } from "@/components/ui/command"; +import { LoginHeader } from "@/components/Common/LoginHeader"; + import query from "@/Utils/request/query"; import { PaginatedResponse } from "@/Utils/request/types"; import { Organization } from "@/types/organization/organization"; @@ -96,17 +98,9 @@ export function LandingPage() { return (
{/* Header */} -
-
- -
-
+
+ +
{/* Main Content */}
diff --git a/src/pages/Patient/index.tsx b/src/pages/Patient/index.tsx index 083febc7f10..e773891556a 100644 --- a/src/pages/Patient/index.tsx +++ b/src/pages/Patient/index.tsx @@ -159,7 +159,7 @@ function PatientIndex() { const appointmentDate = appointmentTime.format("DD MMMM YYYY"); const appointmentTimeSlot = appointmentTime.format("hh:mm a"); return ( - +
From 6b838b855df049d6c6ca5cb2e30ec1f183f4a4b3 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 6 Jan 2025 22:40:16 +0530 Subject: [PATCH 5/7] Rewire enableWhen --- .../QuestionTypes/QuestionGroup.tsx | 59 ++++++++++++++++- .../QuestionTypes/QuestionInput.tsx | 63 ++----------------- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx b/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx index 4f87f04a711..8aeae84d554 100644 --- a/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx +++ b/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx @@ -6,7 +6,7 @@ import { Label } from "@/components/ui/label"; import { QuestionValidationError } from "@/types/questionnaire/batch"; import type { QuestionnaireResponse } from "@/types/questionnaire/form"; -import type { Question } from "@/types/questionnaire/question"; +import type { EnableWhen, Question } from "@/types/questionnaire/question"; import { QuestionInput } from "./QuestionInput"; @@ -22,6 +22,57 @@ interface QuestionGroupProps { facilityId: string; } +function isQuestionEnabled( + question: Question, + questionnaireResponses: QuestionnaireResponse[], +) { + if (!question.enable_when?.length) return true; + + const checkCondition = (enableWhen: EnableWhen) => { + const dependentValue = questionnaireResponses.find( + (v) => v.link_id === enableWhen.question, + )?.values[0]; + + // Early return if no dependent value exists + if (!dependentValue?.value) return false; + + switch (enableWhen.operator) { + case "exists": + return dependentValue !== undefined && dependentValue !== null; + case "equals": + return dependentValue.value === enableWhen.answer; + case "not_equals": + return dependentValue.value !== enableWhen.answer; + case "greater": + return ( + typeof dependentValue.value === "number" && + dependentValue.value > enableWhen.answer + ); + case "less": + return ( + typeof dependentValue.value === "number" && + dependentValue.value < enableWhen.answer + ); + case "greater_or_equals": + return ( + typeof dependentValue.value === "number" && + dependentValue.value >= enableWhen.answer + ); + case "less_or_equals": + return ( + typeof dependentValue.value === "number" && + dependentValue.value <= enableWhen.answer + ); + default: + return true; + } + }; + + return question.enable_behavior === "any" + ? question.enable_when.some(checkCondition) + : question.enable_when.every(checkCondition); +} + export const QuestionGroup = memo(function QuestionGroup({ question, encounterId, @@ -33,6 +84,12 @@ export const QuestionGroup = memo(function QuestionGroup({ activeGroupId, facilityId, }: QuestionGroupProps) { + const isEnabled = isQuestionEnabled(question, questionnaireResponses); + + if (!isEnabled) { + return null; + } + if (question.type !== "group") { return ( { - if (!question.enable_when?.length) return true; - - const checkCondition = (enableWhen: EnableWhen) => { - const dependentValue = questionnaireResponses.find( - (v) => v.link_id === enableWhen.question, - )?.values[0]; - - // Early return if no dependent value exists - if (!dependentValue?.value) return false; - - switch (enableWhen.operator) { - case "exists": - return dependentValue !== undefined && dependentValue !== null; - case "equals": - return dependentValue.value === enableWhen.answer; - case "not_equals": - return dependentValue.value !== enableWhen.answer; - case "greater": - return ( - typeof dependentValue.value === "number" && - dependentValue.value > enableWhen.answer - ); - case "less": - return ( - typeof dependentValue.value === "number" && - dependentValue.value < enableWhen.answer - ); - case "greater_or_equals": - return ( - typeof dependentValue.value === "number" && - dependentValue.value >= enableWhen.answer - ); - case "less_or_equals": - return ( - typeof dependentValue.value === "number" && - dependentValue.value <= enableWhen.answer - ); - default: - return true; - } - }; - - return question.enable_behavior === "any" - ? question.enable_when.some(checkCondition) - : question.enable_when.every(checkCondition); - }; - const handleAddValue = () => { updateQuestionnaireResponseCB({ ...questionnaireResponse, @@ -121,14 +73,12 @@ export function QuestionInput({ }; const renderSingleInput = (index: number = 0) => { - const isEnabled = isQuestionEnabled(); - const commonProps = { classes: question.styling_metadata?.classes, question, questionnaireResponse, updateQuestionnaireResponseCB, - disabled: !isEnabled || disabled, + disabled, clearError, index, }; @@ -221,7 +171,7 @@ export function QuestionInput({ size="sm" onClick={handleAddValue} className="mt-2" - disabled={!isQuestionEnabled() || disabled} + disabled={disabled} > Add Another @@ -231,11 +181,6 @@ export function QuestionInput({ ); }; - const isEnabled = isQuestionEnabled(); - if (!isEnabled && question.disabled_display === "hidden") { - return null; - } - const error = errors.find((e) => e.question_id === question.id)?.error; return ( @@ -247,7 +192,7 @@ export function QuestionInput({ )}
From 6956474f1861bbed4e71ca635449b199d95c4bcf Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 6 Jan 2025 23:27:56 +0530 Subject: [PATCH 6/7] Remove localStorage watch from AuthUserProvider --- src/Providers/AuthUserProvider.tsx | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/Providers/AuthUserProvider.tsx b/src/Providers/AuthUserProvider.tsx index a9d6394448b..5cfc64ca1f2 100644 --- a/src/Providers/AuthUserProvider.tsx +++ b/src/Providers/AuthUserProvider.tsx @@ -27,19 +27,15 @@ export default function AuthUserProvider({ otpAuthorized, }: Props) { const queryClient = useQueryClient(); - const [accessToken, setAccessToken] = useState( - localStorage.getItem(LocalStorageKeys.accessToken), - ); const { data: user, isLoading } = useQuery({ queryKey: ["currentUser"], queryFn: query(routes.currentUser, { silent: true }), retry: false, - enabled: !!accessToken, + enabled: !!localStorage.getItem(LocalStorageKeys.accessToken), }); const [isOTPAuthorized, setIsOTPAuthorized] = useState(false); - console.log("isOTPAuthorized", isOTPAuthorized); const tokenData: TokenData = JSON.parse( localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", @@ -67,17 +63,6 @@ export default function AuthUserProvider({ ); }, [user]); - useEffect(() => { - // Listen for localStorage changes - const listener = (event: StorageEvent) => { - if (event.key === LocalStorageKeys.accessToken) { - setAccessToken(event.newValue); - } - }; - addEventListener("storage", listener); - return () => removeEventListener("storage", listener); - }, []); - const signIn = useCallback( async (creds: { username: string; password: string }) => { const query = await request(routes.login, { body: creds }); From 9a839bd7dc5586408a952efc9a1697a4e4d2bad4 Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Tue, 7 Jan 2025 01:37:15 +0530 Subject: [PATCH 7/7] Implement immediate redirection after successful login --- src/components/Auth/Login.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/Auth/Login.tsx b/src/components/Auth/Login.tsx index fa2df3f0c17..a7f9018dce5 100644 --- a/src/components/Auth/Login.tsx +++ b/src/components/Auth/Login.tsx @@ -103,6 +103,7 @@ const Login = (props: LoginProps) => { }, onSuccess: ({ res }) => { setCaptcha(res?.status === 429); + window.location.href = "/"; }, }); @@ -160,10 +161,7 @@ const Login = (props: LoginProps) => { createdAt: new Date().toISOString(), }; localStorage.setItem(CarePatientTokenKey, JSON.stringify(tokenData)); - Notification.Success({ msg: t("verify_otp_success_login") }); - setTimeout(() => { - window.location.href = "/patient/home"; - }, 200); + window.location.href = "/patient/home"; } },