From 15fd7fe0499478c7fdd34d925d3134290cb02758 Mon Sep 17 00:00:00 2001 From: SHRAMAN PAUL <110323017+shramanpaul@users.noreply.github.com> Date: Thu, 4 Jan 2024 08:51:56 +0530 Subject: [PATCH 01/16] added the draft feature in the doctor's notes (#6932) * added the draft feature in the doctor's note section * added the draft feature in the doctor's note section * made the draft specific to consultation * Update src/Components/Facility/PatientNotesSlideover.tsx made it consultation specific Co-authored-by: Rithvik Nishad --------- Co-authored-by: Rithvik Nishad --- src/Components/Facility/PatientNotesSlideover.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index 4279e3c1877..5ecf30cfcdf 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -23,7 +23,6 @@ interface PatientNotesProps { export default function PatientNotesSlideover(props: PatientNotesProps) { const [show, setShow] = useState(true); const [patientActive, setPatientActive] = useState(true); - const [noteField, setNoteField] = useState(""); const [reload, setReload] = useState(false); const [focused, setFocused] = useState(false); @@ -37,6 +36,11 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { const { facilityId, patientId, consultationId, setShowPatientNotesPopup } = props; + const localStorageKey = `patientNotesNoteField_${consultationId}`; + const [noteField, setNoteField] = useState( + localStorage.getItem(localStorageKey) || "" + ); + const onAddNote = async () => { const payload = { note: noteField, @@ -127,6 +131,10 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { ); + useEffect(() => { + localStorage.setItem(localStorageKey, noteField); + }, [noteField, localStorageKey]); + return (
Date: Thu, 4 Jan 2024 08:53:10 +0530 Subject: [PATCH 02/16] Remove scroll bar when header text overflows in patient details. (#6910) * remove scroll bar * add key to div --- src/Components/Patient/PatientHome.tsx | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Components/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index dc400ddc79b..22edfb16192 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -311,20 +311,18 @@ export const PatientHome = (props: any) => { patientData.medical_history.length ) { const medHis = patientData.medical_history; - patientMedHis = medHis.map((item: any, idx: number) => ( -
- {item?.disease !== "NO" && ( - <> -
- {item.disease} -
-
- {item.details} -
- - )} -
- )); + patientMedHis = medHis + .filter((item) => item.disease !== "NO") + .map((item, idx) => ( +
+
+ {item.disease} +
+
+ {item.details} +
+
+ )); } let consultationList, sampleList; From 1a0bd2c0f53f984decfcd3a0a77c451d2f92785a Mon Sep 17 00:00:00 2001 From: Pranshu Aggarwal <70687348+Pranshu1902@users.noreply.github.com> Date: Thu, 4 Jan 2024 08:53:57 +0530 Subject: [PATCH 03/16] Show icu admission date on consultations dashboard (#6779) * show icu admission date * refactor * handle case when data isn't present * fix padding * update variable to encounter_date * show admission date in banner * fix placement --- src/Components/Patient/PatientInfoCard.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index eb589667ece..68555837d6e 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -320,7 +320,7 @@ export default function PatientInfoCard(props: { ); })}
- {!!consultation?.discharge_date && ( + {consultation?.discharge_date ? (
@@ -346,6 +346,23 @@ export default function PatientInfoCard(props: {
+ ) : ( +
+ + {consultation?.encounter_date && ( +
+ Admission on{" "} + {formatDateTime(consultation?.encounter_date)} +
+ )} + {consultation?.icu_admission_date && ( +
+ , ICU Admission on{" "} + {formatDateTime(consultation?.icu_admission_date)} +
+ )} +
+
)} From 1244a2ac9498da606babfedec75a759e95cf64ad Mon Sep 17 00:00:00 2001 From: Ashraf Mohammed <98876115+AshrafMd-1@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:00:20 +0530 Subject: [PATCH 04/16] correct checkbox activity (#6946) --- src/Components/Patient/PatientRegister.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index 77088ef57e7..546d35cc487 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -544,6 +544,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { setFacilityName(""); } } + fetchFacilityName(); }, [dispatchAction, facilityId]); @@ -953,6 +954,14 @@ export const PatientRegister = (props: PatientRegisterProps) => { } else { values.splice(values.indexOf(id), 1); } + + if (id !== 1 && values.includes(1)) { + values.splice(values.indexOf(1), 1); + } else if (id === 1) { + values.length = 0; + values.push(1); + } + field("medical_history").onChange({ name: "medical_history", value: values, From d366f644bd50491a603f5c5247da2a5a8256c238 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:00:40 +0530 Subject: [PATCH 05/16] Redesign Bed Capacity Cards (#6925) * Redesign Bed Capacity Cards * useQuery/Request * Remove reverse-spin animation and keyframes from tailwind.config.js * Refactor BedTypeCard component to remove unnecessary code --- cypress/e2e/facility_spec/locations.cy.ts | 1 - src/Components/ExternalResult/models.ts | 3 + src/Components/Facility/BedCapacity.tsx | 125 +++++----- src/Components/Facility/BedTypeCard.tsx | 216 +++++++----------- .../Facility/FacilityBedCapacity.tsx | 2 +- src/Redux/actions.tsx | 23 -- src/Redux/api.tsx | 7 +- 7 files changed, 146 insertions(+), 231 deletions(-) diff --git a/cypress/e2e/facility_spec/locations.cy.ts b/cypress/e2e/facility_spec/locations.cy.ts index d6377ba97c3..d39464ce14b 100644 --- a/cypress/e2e/facility_spec/locations.cy.ts +++ b/cypress/e2e/facility_spec/locations.cy.ts @@ -6,7 +6,6 @@ import FacilityLocation from "../../pageobject/Facility/FacilityLocation"; import { AssetPagination } from "../../pageobject/Asset/AssetPagination"; import FacilityHome from "../../pageobject/Facility/FacilityHome"; - describe("Location Management Section", () => { const assetPage = new AssetPage(); const userCreationPage = new UserCreationPage(); diff --git a/src/Components/ExternalResult/models.ts b/src/Components/ExternalResult/models.ts index 8ccaba04d05..7b136c76cef 100644 --- a/src/Components/ExternalResult/models.ts +++ b/src/Components/ExternalResult/models.ts @@ -53,6 +53,9 @@ export interface ILocalBodies { export interface IDeleteExternalResult { detail: string; } +export interface IDeleteBedCapacity { + detail: string; +} export interface IPartialUpdateExternalResult { address: string; diff --git a/src/Components/Facility/BedCapacity.tsx b/src/Components/Facility/BedCapacity.tsx index dba563e5876..9416f5981f2 100644 --- a/src/Components/Facility/BedCapacity.tsx +++ b/src/Components/Facility/BedCapacity.tsx @@ -1,11 +1,4 @@ -import { useCallback, useEffect, useReducer, useState } from "react"; -import { useDispatch } from "react-redux"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { - createCapacity, - listCapacity, - getCapacityBed, -} from "../../Redux/actions"; +import { useEffect, useReducer, useState } from "react"; import * as Notification from "../../Utils/Notifications.js"; import { CapacityModal, OptionsType } from "./models"; import TextFormField from "../Form/FormFields/TextFormField"; @@ -14,6 +7,8 @@ import { SelectFormField } from "../Form/FormFields/SelectFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; import useConfig from "../../Common/hooks/useConfig"; import { getBedTypes } from "../../Common/constants"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; interface BedCapacityProps extends CapacityModal { facilityId: string; @@ -55,7 +50,6 @@ const bedCountReducer = (state = initialState, action: any) => { export const BedCapacity = (props: BedCapacityProps) => { const config = useConfig(); - const dispatchAction: any = useDispatch(); const { facilityId, handleClose, handleUpdate, className, id } = props; const [state, dispatch] = useReducer(bedCountReducer, initialState); const [isLastOptionType, setIsLastOptionType] = useState(false); @@ -67,63 +61,53 @@ export const BedCapacity = (props: BedCapacityProps) => { ? `Save ${!isLastOptionType ? "& Add More" : "Bed Capacity"}` : "Update Bed Capacity"; - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - if (!id) { - // Add Form functionality - const capacityRes = await dispatchAction( - listCapacity({}, { facilityId }) - ); - if (!status.aborted) { - if (capacityRes && capacityRes.data) { - const existingData = capacityRes.data.results; - // if all options are diabled - if (existingData.length === getBedTypes(config).length) { - return; - } - // disable existing bed types - const updatedBedTypes = getBedTypes(config).map( - (type: OptionsType) => { - const isExisting = existingData.find( - (i: CapacityModal) => i.room_type === type.id - ); - return { - ...type, - disabled: !!isExisting, - }; - } - ); - setBedTypes(updatedBedTypes); - } - } - } else { - // Edit Form functionality - const res = await dispatchAction( - getCapacityBed({ facilityId: facilityId, bed_id: id }) - ); - if (res && res.data) { - dispatch({ - type: "set_form", - form: { - bedType: res.data.room_type, - totalCapacity: res.data.total_capacity, - currentOccupancy: res.data.current_capacity, - }, - }); + async function fetchCapacityBed() { + setIsLoading(true); + if (!id) { + // Add Form functionality + const capacityQuery = await request(routes.getCapacity, { + pathParams: { facilityId: props.facilityId }, + }); + if (capacityQuery?.data) { + const existingData = capacityQuery.data?.results; + // if all options are diabled + if (existingData.length === getBedTypes(config).length) { + return; } + // disable existing bed types + const updatedBedTypes = getBedTypes(config).map((type: OptionsType) => { + const isExisting = existingData.find( + (i: CapacityModal) => i.room_type === type.id + ); + return { + ...type, + disabled: !!isExisting, + }; + }); + setBedTypes(updatedBedTypes); } - setIsLoading(false); - }, - [dispatchAction, facilityId, id] - ); + } else { + // Edit Form functionality + const capacityQuery = await request(routes.getCapacityBed, { + pathParams: { facilityId: props.facilityId, bed_id: id.toString() }, + }); + if (capacityQuery.data) { + dispatch({ + type: "set_form", + form: { + bedType: capacityQuery.data.room_type, + totalCapacity: capacityQuery.data.total_capacity, + currentOccupancy: capacityQuery.data.current_capacity, + }, + }); + } + } + setIsLoading(false); + } - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [dispatch, fetchData, id] - ); + useEffect(() => { + fetchCapacityBed(); + }, []); useEffect(() => { const lastBedType = @@ -179,21 +163,24 @@ export const BedCapacity = (props: BedCapacityProps) => { const valid = validateData(); if (valid) { setIsLoading(true); - const data = { + const bodyData = { room_type: Number(state.form.bedType), total_capacity: Number(state.form.totalCapacity), current_capacity: Number(state.form.currentOccupancy), }; - const res = await dispatchAction( - createCapacity(id, data, { facilityId }) + const { data } = await request( + id ? routes.updateCapacity : routes.createCapacity, + { + pathParams: { facilityId, ...(id ? { bed_id: id.toString() } : {}) }, + body: bodyData, + } ); setIsLoading(false); - if (res && res.data) { - // disable last added bed type + if (data) { const updatedBedTypes = bedTypes.map((type: OptionsType) => { return { ...type, - disabled: res.data.room_type !== type.id ? type.disabled : true, + disabled: data.room_type !== type.id ? type.disabled : true, }; }); setBedTypes(updatedBedTypes); diff --git a/src/Components/Facility/BedTypeCard.tsx b/src/Components/Facility/BedTypeCard.tsx index 59e0661169d..67e22f31f42 100644 --- a/src/Components/Facility/BedTypeCard.tsx +++ b/src/Components/Facility/BedTypeCard.tsx @@ -1,8 +1,5 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import * as Notification from "../../Utils/Notifications"; -import { animated, config, useSpring } from "@react-spring/web"; -import { useDispatch } from "react-redux"; -import { deleteCapacity } from "../../Redux/actions"; import { BedCapacity } from "./BedCapacity"; import DialogModal from "../Common/Dialog"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -10,7 +7,8 @@ import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import CareIcon from "../../CAREUI/icons/CareIcon"; import RecordMeta from "../../CAREUI/display/RecordMeta"; import ConfirmDialog from "../Common/ConfirmDialog"; -import { useTranslation } from "react-i18next"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; interface BedTypeCardProps { facilityId?: string; @@ -24,9 +22,6 @@ interface BedTypeCardProps { handleUpdate: () => void; } -const CIRCLE_PATH = - "M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"; - export const BedTypeCard: React.FC = ({ facilityId, bedCapacityId, @@ -38,20 +33,19 @@ export const BedTypeCard: React.FC = ({ removeBedType, handleUpdate, }) => { - const { t } = useTranslation(); - const dispatchAction: any = useDispatch(); + const [isRefreshing, setIsRefreshing] = useState(false); const [openDeleteDialog, setOpenDeleteDialog] = useState(false); const [open, setOpen] = useState(false); const [selectedId, setSelectedId] = useState(-1); const handleDeleteSubmit = async () => { if (room_type) { - const res = await dispatchAction( - deleteCapacity({ - facilityId: facilityId, - bed_id: room_type, - }) - ); - if (res && res.status == 204) { + const { res } = await request(routes.deleteCapacityBed, { + pathParams: { + facilityId: facilityId ?? "", + bed_id: room_type.toString(), + }, + }); + if (res?.status == 204) { Notification.Success({ msg: "Bed type deleted successfully", }); @@ -63,138 +57,88 @@ export const BedTypeCard: React.FC = ({ } }; - const _p = total ? Math.round((used / total) * 100) : 0; + useEffect(() => { + if (isRefreshing) { + setTimeout(() => { + setIsRefreshing(false); + }, 500); + } + }, [isRefreshing]); - const { occupied, totalCount, progress, innerProgress } = useSpring({ - from: { occupied: 0, totalCount: 0, progress: "0, 100", innerProgress: 0 }, - to: { - occupied: used, - totalCount: total, - progress: `${Number.isNaN(_p) ? 0 : _p}, 100`, - innerProgress: Number.isNaN(_p) ? 0 : _p, - }, - delay: 0, - config: config.slow, - }); + const usedPercent = total ? Math.round((used / total) * 100) : 0; return (
-
-

+

+
{label} -

+
+ + {usedPercent}% + +
+
+ {used} / {total} +
+
+
+
+
-
-
-
-
- - - - -
-
- - {innerProgress.to( - (x: number) => `${Math.round(x) || 0}%` - )} - -
- { -
- -
- } -
-
+
+ {" "} + Currently Occupied / Total Capacity{" "} +
+ {facilityId ? ( +
+
+ {lastUpdated && ( + + )}
-
-
-

- Used: - - {occupied.to((x: number) => Math.round(x))} - -

-
-
-

- Total: - - {totalCount.to((x: number) => Math.round(x))} - -

-
+
+ { + setSelectedId(room_type || 0); + setOpen(true); + }} + authorizeFor={NonReadOnlyUsers} + className="tooltip bg-opacity/20 flex aspect-square h-7 w-7 flex-col items-center justify-center rounded bg-gray-300 px-4 py-0" + variant="secondary" + ghost + > + + Edit + + + setOpenDeleteDialog(true)} + authorizeFor={NonReadOnlyUsers} + className=" tooltip bg-opacity/10 flex aspect-square h-7 w-7 flex-col items-center justify-center rounded bg-red-100 px-4 py-0 hover:bg-red-200" + variant="secondary" + ghost + > + + Delete +
- {facilityId && ( -
- {lastUpdated && ( - - )} -
- { - setSelectedId(room_type || 0); - setOpen(true); - }} - authorizeFor={NonReadOnlyUsers} - className="tooltip p-2" - variant="secondary" - ghost - > - - Edit - - setOpenDeleteDialog(true)} - authorizeFor={NonReadOnlyUsers} - className="tooltip p-2" - variant="danger" - ghost - > - - Delete - -
-
- )}
-

- No Data Available -

-
+ ) : ( +
+ )}
{ return (
-
+
Bed Capacity
{ }; // Capacity/Triage/Doctor -export const createCapacity = ( - id: number | undefined, - params: object, - pathParam: object -) => { - return id - ? fireRequest("updateCapacity", [id], params, pathParam) - : fireRequest("createCapacity", [], params, pathParam); -}; export const createDoctor = ( id: number | undefined, params: object, @@ -206,26 +197,12 @@ export const getTriageInfo = (pathParam: object) => { export const getTriageDetails = (pathParam: object) => { return fireRequest("getTriageDetails", [], {}, pathParam); }; -export const listCapacity = (params: object, pathParam: object) => { - return fireRequest("getCapacity", [], params, pathParam); -}; export const listDoctor = (params: object, pathParam: object) => { return fireRequest("listDoctor", [], params, pathParam); }; -export const getCapacity = (id: number, pathParam: object) => { - return fireRequest("getCapacity", [id], {}, pathParam); -}; - -export const getCapacityBed = (pathParam: object) => { - return fireRequest("getCapacityBed", [], {}, pathParam); -}; - export const getDoctor = (pathParam: object) => { return fireRequest("getDoctor", [], {}, pathParam); }; -export const deleteCapacity = (pathParam: object) => { - return fireRequest("deleteCapacityBed", [], {}, pathParam); -}; //Patient export const searchPatient = (params: object) => { diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 2b8d4f8f51b..c511a48fcd9 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -51,6 +51,7 @@ import { BedModel, } from "../Components/Facility/models"; import { + IDeleteBedCapacity, IDeleteExternalResult, IExternalResult, IExternalResultCsv, @@ -529,6 +530,7 @@ const routes = { createCapacity: { path: "/api/v1/facility/{facilityId}/capacity/", method: "POST", + TRes: Type(), }, createDoctor: { @@ -543,11 +545,13 @@ const routes = { getCapacityBed: { path: "/api/v1/facility/{facilityId}/capacity/{bed_id}/", + TRes: Type(), }, deleteCapacityBed: { path: "/api/v1/facility/{facilityId}/capacity/{bed_id}/", method: "DELETE", + TRes: Type(), }, listDoctor: { @@ -559,8 +563,9 @@ const routes = { }, updateCapacity: { - path: "/api/v1/facility/{facilityId}/capacity", + path: "/api/v1/facility/{facilityId}/capacity/{bed_id}/", method: "PUT", + TRes: Type(), }, updateDoctor: { From ff271f8030b1cac47035e37ec98d630f7dbd433b Mon Sep 17 00:00:00 2001 From: Pranshu Aggarwal <70687348+Pranshu1902@users.noreply.github.com> Date: Thu, 4 Jan 2024 20:05:36 +0530 Subject: [PATCH 06/16] Hide delete facility option for users who dont have access (#6953) * Hide delete facility option for users who dont have access * refactor Co-authored-by: Rithvik Nishad * fix lint and cypress tests --------- Co-authored-by: Rithvik Nishad Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> --- cypress/e2e/patient_spec/patient_crud.cy.ts | 2 +- src/Components/Facility/FacilityHome.tsx | 27 ++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cypress/e2e/patient_spec/patient_crud.cy.ts b/cypress/e2e/patient_spec/patient_crud.cy.ts index 06a27333b9e..66c8b35e236 100644 --- a/cypress/e2e/patient_spec/patient_crud.cy.ts +++ b/cypress/e2e/patient_spec/patient_crud.cy.ts @@ -8,7 +8,7 @@ import { emergency_phone_number, phone_number, } from "../../pageobject/constants"; -const yearOfBirth = "2023"; +const yearOfBirth = "2001"; const calculateAge = () => { const currentYear = new Date().getFullYear(); diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index 2341d231675..52a7cd24027 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -1,6 +1,6 @@ import * as Notification from "../../Utils/Notifications.js"; -import AuthorizeFor, { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; +import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import { FacilityModel } from "./models"; import { FACILITY_FEATURE_TYPES, USER_TYPES } from "../../Common/constants"; import DropdownMenu, { DropdownItem } from "../Common/components/Menu"; @@ -99,6 +99,10 @@ export const FacilityHome = (props: any) => { USER_TYPES.findIndex((type) => type == authUser.user_type) >= StaffUserTypeIndex; + const hasPermissionToDeleteFacility = + authUser.user_type === "DistrictAdmin" || + authUser.user_type === "StateAdmin"; + const editCoverImageTooltip = hasPermissionToEditCoverImage && (
@@ -372,16 +376,17 @@ export const FacilityHome = (props: any) => { > View Users - setOpenDeleteDialog(true)} - className="flex items-center gap-3" - icon={} - authorizeFor={AuthorizeFor(["DistrictAdmin", "StateAdmin"])} - > - Delete Facility - + {hasPermissionToDeleteFacility && ( + setOpenDeleteDialog(true)} + className="flex items-center gap-3" + icon={} + > + Delete Facility + + )}
From 75e64f7b76413dd6c66077c33bd84ddc8a766ef5 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 4 Jan 2024 21:04:12 +0530 Subject: [PATCH 07/16] Central Nursing Station: Migrate to `useQuery` and update to use `resolved_middleware` (#6967) * Remove Legacy HL7Monitor Components * Use `resolved_middleware` in ConsultationUpdates Tab * CNS: Migrate to `useQuery` and use `resolved_middleware` * remove unused redux actions --- src/Components/CameraFeed/utils.ts | 6 +- .../Facility/CentralNursingStation.tsx | 90 ++--- .../ConsultationUpdatesTab.tsx | 52 +-- src/Components/Facility/LegacyFacilityCNS.tsx | 313 ------------------ src/Components/Facility/LegacyMonitorCard.tsx | 47 --- .../Patient/LegacyPatientVitalsCard.tsx | 288 ---------------- src/Components/VitalsMonitor/utils.ts | 17 + src/Redux/actions.tsx | 9 - src/Redux/api.tsx | 2 + 9 files changed, 61 insertions(+), 763 deletions(-) delete mode 100644 src/Components/Facility/LegacyFacilityCNS.tsx delete mode 100644 src/Components/Facility/LegacyMonitorCard.tsx delete mode 100644 src/Components/Patient/LegacyPatientVitalsCard.tsx diff --git a/src/Components/CameraFeed/utils.ts b/src/Components/CameraFeed/utils.ts index e2793d76b41..fc940558aa0 100644 --- a/src/Components/CameraFeed/utils.ts +++ b/src/Components/CameraFeed/utils.ts @@ -1,5 +1,5 @@ import { MutableRefObject } from "react"; -import { AssetData } from "../Assets/AssetTypes"; +import { AssetClass, AssetData } from "../Assets/AssetTypes"; import { getCameraConfig } from "../../Utils/transformUtils"; import { isIOS } from "../../Utils/utils"; @@ -18,6 +18,10 @@ export const calculateVideoDelay = ( }; export const getStreamUrl = (asset: AssetData) => { + if (asset.asset_class !== AssetClass.ONVIF) { + throw "getStreamUrl can be invoked only for ONVIF Assets"; + } + const config = getCameraConfig(asset); const host = asset.resolved_middleware?.hostname; const uuid = config.accessKey; diff --git a/src/Components/Facility/CentralNursingStation.tsx b/src/Components/Facility/CentralNursingStation.tsx index 9e8febe88fd..75214b5506c 100644 --- a/src/Components/Facility/CentralNursingStation.tsx +++ b/src/Components/Facility/CentralNursingStation.tsx @@ -1,13 +1,7 @@ -import { useDispatch } from "react-redux"; import useFullscreen from "../../Common/hooks/useFullscreen"; -import { Fragment, useEffect, useState } from "react"; -import { - getPermittedFacility, - listPatientAssetBeds, -} from "../../Redux/actions"; +import { Fragment } from "react"; import HL7PatientVitalsMonitor from "../VitalsMonitor/HL7PatientVitalsMonitor"; import useFilters from "../../Common/hooks/useFilters"; -import { FacilityModel } from "./models"; import Loading from "../Common/Loading"; import Page from "../Common/components/Page"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -15,7 +9,6 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import { classNames } from "../../Utils/utils"; import { LocationSelect } from "../Common/LocationSelect"; import Pagination from "../Common/Pagination"; -import { PatientAssetBed } from "../Assets/AssetTypes"; import { Popover, Transition } from "@headlessui/react"; import { FieldLabel } from "../Form/FormFields/FormField"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; @@ -23,6 +16,9 @@ import { useTranslation } from "react-i18next"; import { SortOption } from "../Common/SortDropdown"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; import useVitalsAspectRatioConfig from "../VitalsMonitor/useVitalsAspectRatioConfig"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import { getVitalsMonitorSocketUrl } from "../VitalsMonitor/utils"; const PER_PAGE_LIMIT = 6; @@ -39,72 +35,28 @@ interface Props { export default function CentralNursingStation({ facilityId }: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); const [isFullscreen, setFullscreen] = useFullscreen(); - - const [facilityObject, setFacilityObject] = useState(); - const [data, setData] = - useState[0][]>(); - const [totalCount, setTotalCount] = useState(0); const { qParams, updateQuery, removeFilter, updatePage } = useFilters({ limit: PER_PAGE_LIMIT, }); + const query = useQuery(routes.listPatientAssetBeds, { + pathParams: { facility_external_id: facilityId }, + query: { + ...qParams, + page: qParams.page || 1, + limit: PER_PAGE_LIMIT, + offset: (qParams.page ? qParams.page - 1 : 0) * PER_PAGE_LIMIT, + asset_class: "HL7MONITOR", + ordering: qParams.ordering || "bed__name", + bed_is_occupied: qParams.bed_is_occupied ?? true, + }, + }); - useEffect(() => { - async function fetchFacilityOrObject() { - if (facilityObject) return facilityObject; - const res = await dispatch(getPermittedFacility(facilityId)); - if (res.status !== 200) return; - setFacilityObject(res.data); - return res.data as FacilityModel; - } - - async function fetchData() { - setData(undefined); - - const filters = { - ...qParams, - page: qParams.page || 1, - limit: PER_PAGE_LIMIT, - offset: (qParams.page ? qParams.page - 1 : 0) * PER_PAGE_LIMIT, - asset_class: "HL7MONITOR", - ordering: qParams.ordering || "bed__name", - bed_is_occupied: qParams.bed_is_occupied ?? true, - }; - - const [facilityObj, res] = await Promise.all([ - fetchFacilityOrObject(), - dispatch(listPatientAssetBeds(facilityId, filters)), - ]); - - if (!facilityObj || res.status !== 200) { - return; - } - - const entries = res.data.results as PatientAssetBed[]; - - setTotalCount(res.data.count); - setData( - entries.map(({ patient, asset, bed }) => { - const middleware = asset.resolved_middleware?.hostname; - const local_ip_address = asset.meta?.local_ip_address; - - return { - patientAssetBed: { patient, asset, bed }, - socketUrl: `wss://${middleware}/observations/${local_ip_address}`, - }; - }) - ); - } - fetchData(); - }, [ - dispatch, - facilityId, - qParams.page, - qParams.location, - qParams.ordering, - qParams.bed_is_occupied, - ]); + const totalCount = query.data?.count ?? 0; + const data = query.data?.results.map((obj) => ({ + patientAssetBed: obj, + socketUrl: getVitalsMonitorSocketUrl(obj.asset), + })); const { config, hash } = useVitalsAspectRatioConfig({ default: 6 / 11, diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index beac7f595a8..ed2d4277c54 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -2,8 +2,8 @@ import { lazy, useEffect, useState } from "react"; import { ConsultationTabProps } from "./index"; import { AssetBedModel, AssetClass, AssetData } from "../../Assets/AssetTypes"; import { useDispatch } from "react-redux"; -import { getPermittedFacility, listAssetBeds } from "../../../Redux/actions"; -import { BedModel, FacilityModel } from "../models"; +import { listAssetBeds } from "../../../Redux/actions"; +import { BedModel } from "../models"; import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor"; import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor"; import useVitalsAspectRatioConfig from "../../VitalsMonitor/useVitalsAspectRatioConfig"; @@ -13,6 +13,7 @@ import Chip from "../../../CAREUI/display/Chip"; import { formatAge, formatDate, formatDateTime } from "../../../Utils/utils"; import ReadMore from "../../Common/components/Readmore"; import DailyRoundsList from "../Consultations/DailyRoundsList"; +import { getVitalsMonitorSocketUrl } from "../../VitalsMonitor/utils"; const PageTitle = lazy(() => import("../../Common/PageTitle")); @@ -40,32 +41,22 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { return; const fetchData = async () => { - const [facilityRes, assetBedRes] = await Promise.all([ - dispatch(getPermittedFacility(props.consultationData.facility as any)), - dispatch( - listAssetBeds({ - facility: props.consultationData.facility as any, - bed: props.consultationData.current_bed?.bed_object.id, - }) - ), - ]); - - const { middleware_address } = facilityRes.data as FacilityModel; + const assetBedRes = await dispatch( + listAssetBeds({ + facility: props.consultationData.facility as any, + bed: props.consultationData.current_bed?.bed_object.id, + }) + ); const assetBeds = assetBedRes?.data?.results as AssetBedModel[]; const monitorBedData = assetBeds?.find( (i) => i.asset_object?.asset_class === AssetClass.HL7MONITOR ); + setMonitorBedData(monitorBedData); - const assetDataForMonitor = monitorBedData?.asset_object; - const hl7Meta = assetDataForMonitor?.meta; - const hl7Middleware = - hl7Meta?.middleware_hostname || - assetDataForMonitor?.location_object?.middleware_address || - middleware_address; - if (hl7Middleware && hl7Meta?.local_ip_address) { + if (monitorBedData?.asset_object) { setHL7SocketUrl( - `wss://${hl7Middleware}/observations/${hl7Meta.local_ip_address}` + getVitalsMonitorSocketUrl(monitorBedData?.asset_object) ); } @@ -73,6 +64,7 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { props.consultationData?.current_bed?.assets_objects?.find( (i) => i.asset_class === AssetClass.VENTILATOR ); + let ventilatorBedData; if (consultationBedVentilator) { ventilatorBedData = { @@ -84,25 +76,13 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { (i) => i.asset_object.asset_class === AssetClass.VENTILATOR ); } + setVentilatorBedData(ventilatorBedData); - const ventilatorMeta = ventilatorBedData?.asset_object?.meta; - const ventilatorMiddleware = - ventilatorMeta?.middleware_hostname || - consultationBedVentilator?.location_object.middleware_address || - middleware_address; - if (ventilatorMiddleware && ventilatorMeta?.local_ip_address) { + if (ventilatorBedData?.asset_object) { setVentilatorSocketUrl( - `wss://${ventilatorMiddleware}/observations/${ventilatorMeta?.local_ip_address}` + getVitalsMonitorSocketUrl(ventilatorBedData?.asset_object) ); } - - if ( - !(hl7Middleware && hl7Meta?.local_ip_address) && - !(ventilatorMiddleware && ventilatorMeta?.local_ip_address) - ) { - setHL7SocketUrl(undefined); - setVentilatorSocketUrl(undefined); - } }; fetchData(); diff --git a/src/Components/Facility/LegacyFacilityCNS.tsx b/src/Components/Facility/LegacyFacilityCNS.tsx deleted file mode 100644 index 5b0005c9daf..00000000000 --- a/src/Components/Facility/LegacyFacilityCNS.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { navigate } from "raviger"; -import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import { - getAllPatient, - getPermittedFacility, - listAssetBeds, -} from "../../Redux/actions"; -import { classNames } from "../../Utils/utils"; -import { AssetData, AssetLocationObject } from "../Assets/AssetTypes"; -import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; -import Page from "../Common/components/Page"; -import Loading from "../Common/Loading"; -import Pagination from "../Common/Pagination"; -import { PatientModel } from "../Patient/models"; -import { FacilityModel } from "./models"; -import AutocompleteFormField from "../Form/FormFields/Autocomplete"; -import { uniqBy } from "lodash-es"; -import DialogModal from "../Common/Dialog"; -import { LegacyMonitorCard } from "./LegacyMonitorCard"; - -interface Monitor { - patient: PatientModel; - asset: AssetData; - socketUrl: string; -} - -const PER_PAGE_LIMIT = 6; -const CNS_REFRESH_INTERVAL = 0.5 * 60e3; - -export default function LegacyFacilityCNS({ - facilityId, -}: { - facilityId: string; -}) { - const dispatch = useDispatch(); - const [isFullscreen, setIsFullscreen] = useState(false); - const [monitors, setMonitors] = useState(); - const [facility, setFacility] = useState(); - const [currentPage, setCurrentPage] = useState(1); - const [defaultShowAllLocation, setDefaultShowAllLocation] = useState(true); - const searchParams = new URLSearchParams(window.location.search); - - // this wil set ?page=1 param in url if it is not present - useEffect(() => { - if (!searchParams.get("page")) { - navigate(`/facility/${facilityId}/cns?page=1`); - } - }, []); - const [location, setLocation] = useState(); - const [showSelectLocation, setShowSelectLocation] = useState(false); - - useEffect(() => { - const onFullscreenChange = () => - setIsFullscreen(!!document.fullscreenElement); - document.addEventListener("fullscreenchange", onFullscreenChange); - return () => - document.removeEventListener("fullscreenchange", onFullscreenChange); - }, []); - - useEffect(() => { - async function fetchFacility() { - const res = await dispatch(getPermittedFacility(facilityId || "")); - if (res.status === 200) setFacility(res.data); - } - fetchFacility(); - }, [facilityId, dispatch]); - - useEffect(() => { - if (!facility) return; - const middlewareHostname = facility.middleware_address; - - async function fetchPatients() { - const res = await dispatch( - getAllPatient( - { facility: facilityId, is_active: "True" }, - "cns-patient-list" - ) - ); - if (res.status === 200) { - const patients = res.data.results as PatientModel[]; - return patients.filter( - (patient) => !!patient.last_consultation?.current_bed?.bed_object.id - ); - } - } - - async function fetchPatientMonitorAsset(patient: PatientModel) { - const res = await dispatch( - listAssetBeds( - { - bed: patient.last_consultation?.current_bed?.bed_object?.id, - }, - `asset-bed-${patient.id}` - ) - ); - - if (res.status !== 200) return; - - const asset = res.data.results.find( - (assetBed: any) => - assetBed.asset_object.meta?.asset_type === "HL7MONITOR" - )?.asset_object as AssetData | undefined; - - if (!asset) return; - - const socketUrl = `wss://${middlewareHostname}/observations/${asset.meta?.local_ip_address}`; - - return { patient, asset, socketUrl } as Monitor; - } - - async function fetchMonitors() { - const patients = await fetchPatients(); - if (!patients) return; - - const monitors = await Promise.all( - patients.map((patient) => fetchPatientMonitorAsset(patient)) - ); - return monitors.filter((monitor) => !!monitor) as Monitor[]; - } - - fetchMonitors().then((monitors) => { - setCurrentPage(Number(searchParams.get("page"))); - setMonitors(monitors); - }); - - const interval = setInterval(() => { - fetchMonitors().then(setMonitors); - }, CNS_REFRESH_INTERVAL); - - return () => clearInterval(interval); - }, [dispatch, facility, facilityId]); - - if (!monitors) return ; - return ( - - {monitors?.length > 0 ? ( - <> - setShowSelectLocation(true)} - > - - Change Location - - { - if (isFullscreen) { - document.exitFullscreen(); - } else { - document.documentElement.requestFullscreen(); - } - }} - className="tooltip !h-11" - > - - - {isFullscreen ? "Exit Fullscreen" : "Fullscreen"} - - - - ) : ( - <> - history.go(-2)} - > - Go Back - - - )} - - { - setCurrentPage(page); - navigate(`/facility/${facilityId}/cns?page=${page}`); - }} - data={{ - totalCount: defaultShowAllLocation - ? monitors.length - : monitors.filter( - (m) => m.asset.location_object.id === location?.id - ).length, - }} - defaultPerPage={PER_PAGE_LIMIT} - /> -
- } - > - setShowSelectLocation(false)} - className="w-full max-w-md" - > - {!monitors && } -
- setLocation(value)} - options={ - monitors - ? uniqBy( - monitors.map((m) => m.asset.location_object), - "id" - ) - : [] - } - isLoading={!monitors} - optionLabel={(location) => location.name} - optionDescription={(location) => - location.description + - " (" + - monitors.filter((m) => m.asset.location_object.id === location.id) - .length + - " patients)" - } - optionValue={(location) => location} - disabled={!monitors} - /> -
- { - setDefaultShowAllLocation(true); - setShowSelectLocation(false); - }} - > - Show All Locations - - { - setDefaultShowAllLocation(false); - setShowSelectLocation(false); - }} - className="my-2 mr-2" - label="Confirm" - /> - setShowSelectLocation(false)} - className="my-2 mr-2" - /> -
-
-
- {monitors.length === 0 && ( -
- No patients are currently monitored -
- )} -
- {defaultShowAllLocation - ? monitors - ?.slice( - (currentPage - 1) * PER_PAGE_LIMIT, - currentPage * PER_PAGE_LIMIT - ) - .map(({ patient, socketUrl, asset }) => ( - - )) - : monitors - ?.filter((m) => m.asset.location_object.id === location?.id) - ?.slice( - (currentPage - 1) * PER_PAGE_LIMIT, - currentPage * PER_PAGE_LIMIT - ) - .map(({ patient, socketUrl, asset }) => ( - - ))} -
- - ); -} diff --git a/src/Components/Facility/LegacyMonitorCard.tsx b/src/Components/Facility/LegacyMonitorCard.tsx deleted file mode 100644 index 61bff3d607b..00000000000 --- a/src/Components/Facility/LegacyMonitorCard.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { GENDER_TYPES } from "../../Common/constants"; -import { Link } from "raviger"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import { PatientModel } from "../Patient/models"; -import LegacyPatientVitalsCard from "../Patient/LegacyPatientVitalsCard"; -import { AssetLocationObject } from "../Assets/AssetTypes"; -import { formatAge } from "../../Utils/utils"; - -interface MonitorCardProps { - facilityId: string; - patient: PatientModel; - socketUrl: string; - location: AssetLocationObject; -} - -export const LegacyMonitorCard = ({ - facilityId, - patient, - socketUrl, - location, -}: MonitorCardProps) => { - return ( -
-
- - {patient.name} - - - {formatAge(patient.age, patient.date_of_birth)} |{" "} - {GENDER_TYPES.find((g) => g.id === patient.gender)?.icon} - - - - {patient.last_consultation?.current_bed?.bed_object?.name} - - - - {location.name} - -
- -
- ); -}; diff --git a/src/Components/Patient/LegacyPatientVitalsCard.tsx b/src/Components/Patient/LegacyPatientVitalsCard.tsx deleted file mode 100644 index 6577e4920d4..00000000000 --- a/src/Components/Patient/LegacyPatientVitalsCard.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { ReactNode, useEffect, useRef, useState } from "react"; -import { useDispatch } from "react-redux"; -import { listAssetBeds, getPermittedFacility } from "../../Redux/actions"; -import { classNames } from "../../Utils/utils"; -import { AssetData } from "../Assets/AssetTypes"; -import ToolTip from "../Common/utils/Tooltip"; -import { PatientModel } from "./models"; -import Waveform, { WaveformType } from "./Waveform"; - -export interface IPatientVitalsCardProps { - facilityId?: string; - patient?: PatientModel; - socketUrl?: string; - shrinked?: boolean; -} - -const getVital = ( - patientObservations: any, - vital: string, - fallbackValue?: any -) => { - if (patientObservations) { - const vitalValues = patientObservations[vital]; - if (vitalValues) { - const returnValue = vitalValues?.value; - - if (returnValue !== undefined && returnValue !== null) { - return returnValue; - } - } - } - if (fallbackValue) { - return fallbackValue; - } - return ""; -}; - -export default function LegacyPatientVitalsCard({ - patient, - socketUrl, - facilityId, - shrinked, -}: IPatientVitalsCardProps) { - const wsClient = useRef(); - const [waveforms, setWaveForms] = useState(null); - const dispatch: any = useDispatch(); - const [middlewareHostname, setMiddlewareHostname] = useState(""); - const [wsUrl, setWsUrl] = useState(""); - const [patientObservations, setPatientObservations] = useState(); - const [stats, setStats] = useState(false); - - useEffect(() => { - const fetchFacility = async () => { - const res = await dispatch(getPermittedFacility(facilityId || "")); - - if (res.status === 200 && res.data) { - setMiddlewareHostname(res.data.middleware_address); - } - }; - - if (facilityId) fetchFacility(); - }, [dispatch, facilityId]); - - useEffect(() => { - const fetchAssetData = async () => { - let bedAssets = await dispatch( - listAssetBeds({ - bed: patient?.last_consultation?.current_bed?.bed_object?.id, - }) - ); - bedAssets = { - ...bedAssets, - data: { - ...bedAssets.data, - results: bedAssets.data.results.filter((assetBed: any) => - assetBed.asset_object.meta?.asset_type === "HL7MONITOR" - ? true - : false - ), - }, - }; - - if (bedAssets.data.results.length > 0) { - const asset: AssetData = bedAssets.data.results[0].asset_object; - if (asset?.meta?.local_ip_address) { - setWsUrl( - `wss://${middlewareHostname}/observations/${asset?.meta?.local_ip_address}` - ); - } - } - }; - - if (patient?.last_consultation?.current_bed?.bed_object?.id) - fetchAssetData(); - }, [ - dispatch, - middlewareHostname, - patient?.last_consultation?.current_bed?.bed_object?.id, - ]); - - const connectWs = (url: string) => { - wsClient.current = new WebSocket(url); - wsClient.current.addEventListener("message", (e) => { - const newObservations = JSON.parse(e.data || "{}"); - if (newObservations.length > 0) { - setWaveForms( - newObservations.filter((o: any) => o.observation_id === "waveform") - ); - const newObservationsMap = newObservations.reduce( - (acc: any, curr: { observation_id: any }) => ({ - ...acc, - [curr.observation_id]: curr, - }), - {} - ); - setPatientObservations(newObservationsMap); - } - }); - }; - - useEffect(() => { - if (socketUrl || wsUrl) connectWs(socketUrl || wsUrl); - - return () => { - wsClient.current?.close(); - }; - }, [wsUrl, socketUrl]); - - useEffect(() => { - return () => { - wsClient.current?.close(); - setWaveForms(null); - }; - }, [socketUrl, patient]); - - type VitalType = { - label: ReactNode; - liveKey: string; - vitalKey: string; - waveformKey?: string; - waveformColor?: string; - waveformName?: string; - waveformDefaultSpace?: boolean; - wavetype?: "STREAM" | "REFRESH"; - }; - - const vitals: VitalType[] = [ - { - label: shrinked ? "Pulse" : "Pulse Rate", - liveKey: "pulse-rate", - vitalKey: "pulse", - waveformKey: "II", - waveformColor: "limegreen", - waveformName: "ECG", - wavetype: "REFRESH", - }, - { - label: shrinked ? "BP" : "Blood Pressure", - liveKey: "bp", - vitalKey: "bp", - }, - { - label: ( - <> - SpO2 - - ), - liveKey: "SpO2", - vitalKey: "ventilator_spo2", - waveformKey: "Pleth", - waveformColor: "yellow", - }, - { - label: <>R. Rate, - liveKey: "respiratory-rate", - vitalKey: "resp", - waveformKey: "Respiration", - waveformColor: "cyan", - //waveformDefaultSpace: true - }, - { - label: shrinked ? "Temp. (°F)" : "Temperature (°F)", - liveKey: "body-temperature1", - vitalKey: "temperature", - }, - ]; - - return ( -
-
-
- {waveforms ? ( - <> - {vitals.map((v, i) => { - const waveform: any = waveforms.filter( - (w) => w["wave-name"] === v.waveformKey - )[0]; - return v.waveformKey && waveform ? ( - w["wave-name"] === v.waveformKey) - .map((w) => w.data) - .join(" "), - }} - title={v.waveformName || v.waveformKey} - color={v.waveformColor} - metrics={stats} - classes={"h-[150px]"} - defaultSpace={v.waveformDefaultSpace} - wavetype={v.wavetype || "STREAM"} - /> - ) : ( -
- ); - })} -
- - - -
- - ) : ( -
-
- -
No Live data at the moment!
-
-
- )} -
-
- {vitals.map((vital, i) => { - const liveReading = getVital(patientObservations, vital.liveKey); - return ( -
-

- {liveReading || - (vital.vitalKey === "bp" - ? `${ - patient?.last_consultation?.last_daily_round?.bp - .systolic || "--" - }/${ - patient?.last_consultation?.last_daily_round?.bp - .diastolic || "--" - }` - : patient?.last_consultation?.last_daily_round?.[ - vital.vitalKey || "" - ]) || - "--"} -

-
-
- {vital.label} -
-
- ); - })} -
-
-
- ); -} diff --git a/src/Components/VitalsMonitor/utils.ts b/src/Components/VitalsMonitor/utils.ts index 2e38f83b65b..21ba769dc83 100644 --- a/src/Components/VitalsMonitor/utils.ts +++ b/src/Components/VitalsMonitor/utils.ts @@ -1,3 +1,4 @@ +import { AssetClass, AssetData } from "../Assets/AssetTypes"; import { ChannelOptions, VitalsWaveformBase } from "./types"; /** @@ -73,3 +74,19 @@ export const getVitalsCanvasSizeAndDuration = ( duration: DEFAULT_DURATION * (ratio / DEFAULT_RATIO), }; }; + +export const getVitalsMonitorSocketUrl = (asset: AssetData) => { + if ( + asset.asset_class !== AssetClass.HL7MONITOR && + asset.asset_class !== AssetClass.VENTILATOR + ) { + throw "getVitalsMonitorSocketUrl can be invoked only for HL7MONITOR or VENTILATOR assets"; + } + + const middleware = asset.resolved_middleware?.hostname; + const ipAddress = asset.meta?.local_ip_address; + + if (middleware && ipAddress) { + return `wss://${middleware}/observations/${ipAddress}`; + } +}; diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 2b301b09e38..9d9ab59795b 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -28,10 +28,6 @@ export const getAllSkills = (params: object) => { return fireRequest("getAllSkills", [], params); }; -export const getPermittedFacility = (id: number | string, key?: string) => { - return fireRequest("getPermittedFacility", [], {}, { id: id }, key); -}; - export const getAnyFacility = (id: number | string, key?: string) => { return fireRequest("getAnyFacility", [], {}, { id: id }, key); }; @@ -109,11 +105,6 @@ export const deleteAssetBed = (asset_id: string) => } ); -export const listPatientAssetBeds = ( - facility_external_id: string, - params: object -) => fireRequest("listPatientAssetBeds", [], params, { facility_external_id }); - // Facility Beds export const listFacilityBeds = (params: object) => fireRequest("listFacilityBeds", [], params, {}); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index c511a48fcd9..8f8fc76484d 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -27,6 +27,7 @@ import { AssetServiceUpdate, AssetTransaction, AssetUpdate, + PatientAssetBed, } from "../Components/Assets/AssetTypes"; import { CapacityModal, @@ -395,6 +396,7 @@ const routes = { listPatientAssetBeds: { path: "/api/v1/facility/{facility_external_id}/patient_asset_beds/", method: "GET", + TRes: Type>(), }, // Facility Beds From 9f1b29c1a8b7dd2614f0fc41d8c5f4a3f52e45d8 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Thu, 4 Jan 2024 21:25:58 +0530 Subject: [PATCH 08/16] Refactor DailyRoundsList component and DefaultLogUpdateCard component (#6973) --- .../Facility/ConsultationDetails/ConsultationUpdatesTab.tsx | 2 +- .../Consultations/DailyRounds/DefaultLogUpdateCard.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index ed2d4277c54..5e5b574dd07 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -652,7 +652,7 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
-
+
diff --git a/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx b/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx index 4cb90c900b1..34518afcb9b 100644 --- a/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx +++ b/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx @@ -32,12 +32,13 @@ const DefaultLogUpdateCard = ({ round, ...props }: Props) => { attributeKey="other_details" attributeValue={round.other_details} /> -
+
@@ -48,6 +49,7 @@ const DefaultLogUpdateCard = ({ round, ...props }: Props) => { border ghost size="small" + className="w-full" onClick={props.onUpdateLog} > From fedb2e20698731f10cf69d5750f3763b416def32 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Sat, 6 Jan 2024 19:22:30 +0530 Subject: [PATCH 09/16] Fix blood pressure being negative values when field is cleared (#6986) * Fix negative values from being internally recorded in blood pressure * make validator name more verbose and readable * remove console logs --- .../Common/BloodPressureFormField.tsx | 47 ++++++++++++------- .../Facility/Consultations/Mews.tsx | 2 - .../Patient/DailyRoundListDetails.tsx | 10 +--- src/Components/Patient/DailyRounds.tsx | 44 ++++------------- src/Components/Patient/models.tsx | 10 ++-- 5 files changed, 46 insertions(+), 67 deletions(-) diff --git a/src/Components/Common/BloodPressureFormField.tsx b/src/Components/Common/BloodPressureFormField.tsx index 3ff2774b900..e64b2a15ff6 100644 --- a/src/Components/Common/BloodPressureFormField.tsx +++ b/src/Components/Common/BloodPressureFormField.tsx @@ -1,3 +1,4 @@ +import { FieldValidator } from "../Form/FieldValidators"; import FormField from "../Form/FormFields/FormField"; import RangeAutocompleteFormField from "../Form/FormFields/RangeAutocompleteFormField"; import { @@ -5,40 +6,36 @@ import { FormFieldBaseProps, useFormFieldPropsResolver, } from "../Form/FormFields/Utils"; +import { DailyRoundsModel } from "../Patient/models"; -export interface BloodPressure { - systolic: number; - diastolic: number; -} +type BloodPressure = NonNullable; -type Props = FormFieldBaseProps>; +type Props = FormFieldBaseProps; export default function BloodPressureFormField(props: Props) { const field = useFormFieldPropsResolver(props as any); const handleChange = (event: FieldChangeEvent) => { - field.onChange({ - name: field.name, - value: { - ...field.value, - [event.name]: event.value ?? -1, - }, - }); + const value: BloodPressure = { + ...field.value, + [event.name]: event.value, + }; + value.mean = meanArterialPressure(value); + field.onChange({ name: field.name, value }); }; const map = !!props.value?.diastolic && !!props.value.systolic && - meanArterialPressure(props.value as BloodPressure); + meanArterialPressure(props.value); return ( MAP: {map.toFixed(1)} - ) : undefined, + labelSuffix: map ? ( + MAP: {map.toFixed(1)} + ) : undefined, }} >
@@ -108,5 +105,19 @@ export const meanArterialPressure = ({ diastolic, systolic, }: BloodPressure) => { - return (2 * diastolic + systolic) / 3; + if (diastolic != null && systolic != null) { + return (2 * diastolic + systolic) / 3; + } +}; + +export const BloodPressureValidator: FieldValidator = (bp) => { + if (Object.values(bp).every((v) => v == null)) { + return; + } + if (bp.diastolic == null) { + return "Diastolic is missing. Either specify both or clear both."; + } + if (bp.systolic == null) { + return "Systolic is missing. Either specify both or clear both."; + } }; diff --git a/src/Components/Facility/Consultations/Mews.tsx b/src/Components/Facility/Consultations/Mews.tsx index 5160e42f9f2..14e7d7f9e63 100644 --- a/src/Components/Facility/Consultations/Mews.tsx +++ b/src/Components/Facility/Consultations/Mews.tsx @@ -26,7 +26,6 @@ const getHeartRateScore = (value?: number) => { const getSystolicBPScore = (value?: number) => { if (typeof value !== "number") return; - if (value === -1) return; if (value <= 70) return 3; if (value <= 80) return 2; @@ -38,7 +37,6 @@ const getSystolicBPScore = (value?: number) => { }; const getTempRange = (value?: number) => { - console.log(value); if (typeof value !== "number") return; if (value < 95) return 2; diff --git a/src/Components/Patient/DailyRoundListDetails.tsx b/src/Components/Patient/DailyRoundListDetails.tsx index 0a73607d688..8f313c0a51d 100644 --- a/src/Components/Patient/DailyRoundListDetails.tsx +++ b/src/Components/Patient/DailyRoundListDetails.tsx @@ -158,20 +158,14 @@ export const DailyRoundListDetails = (props: any) => { Systolic:{" "} - {dailyRoundListDetailsData.bp?.systolic && - dailyRoundListDetailsData.bp?.systolic !== -1 - ? dailyRoundListDetailsData.bp?.systolic - : "-"} + {dailyRoundListDetailsData.bp?.systolic ?? "-"}
{" "} Diastolic: - {dailyRoundListDetailsData.bp?.diastolic && - dailyRoundListDetailsData.bp?.diastolic !== -1 - ? dailyRoundListDetailsData.bp?.diastolic - : "-"} + {dailyRoundListDetailsData.bp?.diastolic ?? "-"}
diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index c12aea42bee..56e26dff022 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -23,7 +23,7 @@ import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave"; import * as Notification from "../../Utils/Notifications"; import { formatDateTime } from "../../Utils/utils"; import BloodPressureFormField, { - meanArterialPressure, + BloodPressureValidator, } from "../Common/BloodPressureFormField"; import { SymptomsSelect } from "../Common/SymptomsSelect"; import TemperatureFormField from "../Common/TemperatureFormField"; @@ -63,9 +63,9 @@ const initForm: any = { ventilator_spo2: null, consciousness_level: "UNKNOWN", bp: { - systolic: -1, - diastolic: -1, - mean: -1, + systolic: undefined, + diastolic: undefined, + mean: undefined, }, // bed: null, }; @@ -247,18 +247,14 @@ export const DailyRounds = (props: any) => { invalidForm = true; } return; - case "bp": - if ( - (state.form.bp?.systolic && - state.form.bp?.diastolic && - state.form.bp.systolic !== -1 && - state.form.bp.diastolic === -1) || - (state.form.bp.systolic === -1 && state.form.bp.diastolic !== -1) - ) { - errors.bp = "Please enter both systolic and diastolic values"; + case "bp": { + const error = BloodPressureValidator(state.form.bp); + if (error) { + errors.bp = error; invalidForm = true; } return; + } default: return; } @@ -303,27 +299,7 @@ export const DailyRounds = (props: any) => { if (["NORMAL", "TELEMEDICINE"].includes(state.form.rounds_type)) { data = { ...data, - bp: - state.form.bp?.systolic !== -1 && state.form.bp?.diastolic !== -1 - ? { - systolic: state.form.bp?.systolic - ? Number(state.form.bp?.systolic) - : -1, - diastolic: state.form.bp?.diastolic - ? Number(state.form.bp?.diastolic) - : -1, - mean: - state.form.bp?.systolic && state.form.bp?.diastolic - ? parseFloat( - meanArterialPressure(state.form.bp).toFixed(2) - ) - : -1, - } - : { - systolic: -1, - diastolic: -1, - mean: -1, - }, + bp: state.form.bp ?? {}, pulse: state.form.pulse ?? null, resp: state.form.resp ?? null, temperature: state.form.temperature ?? null, diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index e1ce5d53050..fc87bedcb14 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -282,9 +282,9 @@ export interface DailyRoundsModel { rhythm?: string; rhythm_detail?: string; bp?: { - diastolic: number; - mean: number; - systolic: number; + diastolic?: number; + mean?: number; + systolic?: number; }; pulse?: number; resp?: number; @@ -297,7 +297,7 @@ export interface DailyRoundsModel { medication_given?: Array; additional_symptoms_text?: string; current_health?: string; - id: string; + id?: string; other_symptoms?: string; admitted_to?: string; patient_category?: PatientCategory; @@ -314,7 +314,7 @@ export interface DailyRoundsModel { | "AGITATED_OR_CONFUSED" | "ONSET_OF_AGITATION_AND_CONFUSION" | "UNKNOWN"; - rounds_type: (typeof DailyRoundTypes)[number]; + rounds_type?: (typeof DailyRoundTypes)[number]; last_updated_by_telemedicine?: boolean; created_by_telemedicine?: boolean; created_by?: { From 7f6cc0c016809768aef236d03429c7021bef46de Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Sat, 6 Jan 2024 20:25:12 +0530 Subject: [PATCH 10/16] hacks/treat-negative-bp-as-undefined (#6987) --- .../Consultations/PrimaryParametersPlot.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Components/Facility/Consultations/PrimaryParametersPlot.tsx b/src/Components/Facility/Consultations/PrimaryParametersPlot.tsx index 4ff81acf868..ecd3a7d2648 100644 --- a/src/Components/Facility/Consultations/PrimaryParametersPlot.tsx +++ b/src/Components/Facility/Consultations/PrimaryParametersPlot.tsx @@ -17,6 +17,17 @@ interface PrimaryParametersPlotProps { consultationId: string; } +const sanitizeBPAttribute = (value: number | undefined) => { + // Temp. hack until the cleaning of daily rounds as a db migration is done. + // TODO: remove once migration is merged. + + if (value == null || value < 0) { + return; + } + + return value; +}; + export const PrimaryParametersPlot = ({ consultationId, }: PrimaryParametersPlotProps) => { @@ -77,19 +88,19 @@ export const PrimaryParametersPlot = ({ { name: "diastolic", data: Object.values(results) - .map((p: any) => p.bp && p.bp.diastolic) + .map((p: any) => p.bp && sanitizeBPAttribute(p.bp.diastolic)) .reverse(), }, { name: "systolic", data: Object.values(results) - .map((p: any) => p.bp && p.bp.systolic) + .map((p: any) => p.bp && sanitizeBPAttribute(p.bp.systolic)) .reverse(), }, { name: "mean", data: Object.values(results) - .map((p: any) => p.bp && p.bp.mean) + .map((p: any) => p.bp && sanitizeBPAttribute(p.bp.mean)) .reverse(), }, ]; From 37b451a401d510f9ace9b7dca803679b353d30fe Mon Sep 17 00:00:00 2001 From: Gokulram A Date: Mon, 8 Jan 2024 17:39:43 +0530 Subject: [PATCH 11/16] Fix: hide Monitors without a patient checkbox filter is not toggling (#6992) * fix: hide Monitors without a patient checkbox filter is not toggling state * remove state variable isBedOccupied * fix typo * fix boolean qParam being string * fix loading state and bugs * fix initial state --------- Co-authored-by: rithviknishad --- .../Facility/CentralNursingStation.tsx | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Components/Facility/CentralNursingStation.tsx b/src/Components/Facility/CentralNursingStation.tsx index 75214b5506c..186dbbd48e9 100644 --- a/src/Components/Facility/CentralNursingStation.tsx +++ b/src/Components/Facility/CentralNursingStation.tsx @@ -48,7 +48,8 @@ export default function CentralNursingStation({ facilityId }: Props) { offset: (qParams.page ? qParams.page - 1 : 0) * PER_PAGE_LIMIT, asset_class: "HL7MONITOR", ordering: qParams.ordering || "bed__name", - bed_is_occupied: qParams.bed_is_occupied ?? true, + bed_is_occupied: + (qParams.hide_monitors_without_patient ?? "true") === "true", }, }); @@ -149,19 +150,12 @@ export default function CentralNursingStation({ facilityId }: Props) { errorClassName="hidden" /> { - if (value) { - updateQuery({ [name]: value }); - } else { - removeFilter(name); - } - }} + value={JSON.parse( + qParams.hide_monitors_without_patient ?? true + )} + onChange={(e) => updateQuery({ [e.name]: e.value })} labelClassName="text-sm" errorClassName="hidden" /> @@ -197,7 +191,7 @@ export default function CentralNursingStation({ facilityId }: Props) {
} > - {data === undefined ? ( + {data === undefined || query.loading ? ( ) : data.length === 0 ? (
From 557b0d09c70edc944872833941b05a7c3fa88a89 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:11:13 +0530 Subject: [PATCH 12/16] New Cypress Test | Facility Inventory Management | Facility Module (#6979) * inventory management * inventory management error * inventory management error * POM conversion * error --- cypress/e2e/facility_spec/inventory.cy.ts | 56 +++++++++++++++++-- .../pageobject/Facility/FacilityCreation.ts | 50 +++++++++++++++++ src/Components/Facility/InventoryList.tsx | 2 + src/Components/Facility/InventoryLog.tsx | 5 +- src/Components/Facility/MinQuantityList.tsx | 2 + .../Facility/MinQuantityRequiredModal.tsx | 7 ++- 6 files changed, 115 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/facility_spec/inventory.cy.ts b/cypress/e2e/facility_spec/inventory.cy.ts index 79077d6e6a6..e7f1c35cb93 100644 --- a/cypress/e2e/facility_spec/inventory.cy.ts +++ b/cypress/e2e/facility_spec/inventory.cy.ts @@ -1,10 +1,12 @@ import { cy, describe, before, beforeEach, it, afterEach } from "local-cypress"; import FacilityPage from "../../pageobject/Facility/FacilityCreation"; import LoginPage from "../../pageobject/Login/LoginPage"; +import FacilityHome from "../../pageobject/Facility/FacilityHome"; describe("Inventory Management Section", () => { const facilityPage = new FacilityPage(); const loginPage = new LoginPage(); + const facilityHome = new FacilityHome(); before(() => { loginPage.loginAsDisctrictAdmin(); @@ -16,18 +18,64 @@ describe("Inventory Management Section", () => { cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/"); cy.viewport(1280, 720); - }); - - it("Adds Inventory", () => { facilityPage.visitAlreadyCreatedFacility(); facilityPage.clickManageFacilityDropdown(); facilityPage.clickInventoryManagementOption(); + }); + + it("Add New Inventory | Modify data and delete last entry ", () => { + // add a new item facilityPage.clickManageInventory(); - facilityPage.fillInventoryDetails("Liquid Oxygen", "Add Stock", "120"); + facilityPage.fillInventoryDetails("PPE", "Add Stock", "10"); facilityPage.clickAddInventory(); facilityPage.verifySuccessNotification("Inventory created successfully"); + facilityPage.clickManageInventory(); + // modify the new item + facilityPage.fillInventoryDetails("PPE", "Use Stock", "5"); + facilityPage.clickAddInventory(); + facilityPage.verifySuccessNotification("Inventory created successfully"); + // verify the new modification + facilityPage.verifyPpeQuantity("PPE"); + facilityPage.verifyPpeQuantity("5"); + // delete the last Entry + facilityPage.clickPpeQuantity(); + facilityPage.clickLastEntry(); + // verify the last entry deletion + facilityPage.verifyStockInRow("#row-0", "Added Stock"); + facilityPage.verifyStockInRow("#row-1", "Used Stock"); + cy.wait(3000); + facilityHome.navigateBack(); + facilityPage.verifyPpeQuantity("PPE"); }); + it("Add New Inventory | Verify Backend and manual Minimum", () => { + // Add Inventory + facilityPage.clickManageInventory(); + facilityPage.fillInventoryDetails("PPE", "Add Stock", "5"); + facilityPage.clickAddInventory(); + facilityPage.verifySuccessNotification("Inventory created successfully"); + // Verify Backend minimum badge + facilityPage.verifyBadgeWithText(".badge-danger", "Low Stock"); + // modify with manual minimum badge + facilityPage.clickAddMinimumQuanitity(); + cy.wait(3000); + cy.get("body").then(($body) => { + if ($body.find("#update-minimum-quantity").is(":visible")) { + // If the 'update-minimum-quantity' element is visible, click it + facilityPage.clickUpdateMinimumQuantity(); + facilityPage.setQuantity("5"); + facilityPage.clickSaveUpdateMinimumQuantity(); + } else { + // Otherwise, click the 'set-minimum-quantity' element + facilityPage.clickSetMinimumQuantity(); + facilityPage.fillInventoryMinimumDetails("PPE", "1"); + facilityPage.clickSetButton(); + facilityPage.verifySuccessNotification( + "Minimum quantiy updated successfully" + ); + } + }); + }); afterEach(() => { cy.saveLocalStorage(); }); diff --git a/cypress/pageobject/Facility/FacilityCreation.ts b/cypress/pageobject/Facility/FacilityCreation.ts index 2d6aa9ff375..bb6cf0e19f2 100644 --- a/cypress/pageobject/Facility/FacilityCreation.ts +++ b/cypress/pageobject/Facility/FacilityCreation.ts @@ -372,6 +372,12 @@ class FacilityPage { cy.get("[name='quantity']").type(quantity); } + fillInventoryMinimumDetails(name: string, quantity: string) { + cy.get("div#id").click(); + cy.get("div#id ul li").contains(name).click(); + cy.get("[name='quantity']").type(quantity); + } + clickAddInventory() { cy.intercept("POST", "**/api/v1/facility/*/inventory/").as( "createInventory" @@ -380,6 +386,10 @@ class FacilityPage { cy.wait("@createInventory").its("response.statusCode").should("eq", 201); } + clickSetButton() { + cy.get("#submit").contains("Set").click(); + } + fillResourceRequestDetails( name: string, phone_number: string, @@ -443,6 +453,46 @@ class FacilityPage { } }); } + + verifyPpeQuantity(text: string) { + cy.get("#PPE").contains(text).should("be.visible"); + } + + clickPpeQuantity() { + cy.get("#PPE").click(); + } + + clickLastEntry() { + cy.get("#delete-last-entry").click(); + } + + verifyStockInRow(rowId: string, stockText: string) { + cy.get(rowId).contains(stockText).should("be.visible"); + } + + verifyBadgeWithText(badgeClass: string, text: string) { + cy.get(badgeClass).contains(text).should("exist"); + } + + clickAddMinimumQuanitity() { + cy.get("#add-minimum-quantity").click(); + } + + clickUpdateMinimumQuantity() { + cy.get("#update-minimum-quantity").first().click(); + } + + setQuantity(quantity: string) { + cy.get("#quantity").click().clear().click().type(quantity); + } + + clickSaveUpdateMinimumQuantity() { + cy.get("#save-update-minimumquanitity").click(); + } + + clickSetMinimumQuantity() { + cy.get("#set-minimum-quantity").click(); + } } export default FacilityPage; diff --git a/src/Components/Facility/InventoryList.tsx b/src/Components/Facility/InventoryList.tsx index 26f1af87191..837b0b2ce2a 100644 --- a/src/Components/Facility/InventoryList.tsx +++ b/src/Components/Facility/InventoryList.tsx @@ -71,6 +71,7 @@ export default function InventoryList(props: any) { if (inventory?.length) { inventoryList = inventory.map((inventoryItem: any) => ( diff --git a/src/Components/Facility/InventoryLog.tsx b/src/Components/Facility/InventoryLog.tsx index 5518b5e320f..8369a7d1562 100644 --- a/src/Components/Facility/InventoryLog.tsx +++ b/src/Components/Facility/InventoryLog.tsx @@ -117,8 +117,8 @@ export default function InventoryLog(props: any) { let inventoryList: any = []; if (inventory?.length) { - inventoryList = inventory.map((inventoryItem: any) => ( - + inventoryList = inventory.map((inventoryItem: any, index) => ( +
@@ -270,6 +270,7 @@ export default function InventoryLog(props: any) { as accident.
removeLastInventoryLog(inventory[0].item_object.id) diff --git a/src/Components/Facility/MinQuantityList.tsx b/src/Components/Facility/MinQuantityList.tsx index 631f432a9dc..941722bd810 100644 --- a/src/Components/Facility/MinQuantityList.tsx +++ b/src/Components/Facility/MinQuantityList.tsx @@ -126,6 +126,7 @@ export default function MinQuantityList(props: any) { {inventoryItem.item_object?.default_unit?.name}

{ />
- + Update From cc3886007e46c41f908567e0a32438785eedcfd0 Mon Sep 17 00:00:00 2001 From: Aditya Pachauri <103623274+AdityyaX@users.noreply.github.com> Date: Wed, 10 Jan 2024 13:25:44 +0530 Subject: [PATCH 13/16] Updated DOB Date Format in Patient details page (#6950) * date format changed * Update src/Components/Patient/PatientHome.tsx Co-authored-by: Rithvik Nishad * Update src/Components/Patient/PatientHome.tsx Co-authored-by: Rithvik Nishad * fix imports --------- Co-authored-by: Rithvik Nishad Co-authored-by: rithviknishad --- src/Components/Patient/PatientHome.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Components/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index 22edfb16192..32118e5951e 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -19,7 +19,12 @@ import { ConsultationModel } from "../Facility/models"; import { PatientModel, SampleTestModel } from "./models"; import { SampleTestCard } from "./SampleTestCard"; import Chip from "../../CAREUI/display/Chip"; -import { classNames, formatAge, formatDateTime } from "../../Utils/utils"; +import { + classNames, + formatAge, + formatDate, + formatDateTime, +} from "../../Utils/utils"; import ButtonV2 from "../Common/components/ButtonV2"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import RelativeDateUserMention from "../Common/RelativeDateUserMention"; @@ -570,7 +575,7 @@ export const PatientHome = (props: any) => { Date of Birth
- {patientData?.date_of_birth} + {formatDate(patientData?.date_of_birth)}
From 197cb39f42ea578922db0f45983e842613454ce0 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 10 Jan 2024 13:28:10 +0530 Subject: [PATCH 14/16] Migrate `useDispatch` with `useQuery/request` in `src/Components/Common/**` (#6889) * Migrate to `useQuery` in `AssetSelect` * Migrate `useQuery` in `BedSelect` * Migrate `useQuery` in `DistrictAutocompleteFormField` * Migrate `useQuery` in `StateAutocompleteFormField` * Migrate `useQuery` in `LocalBodyAutocompleteFormField` * Migrate `useQuery` in `FacilitySelect` * Migrate `useQuery` in `LocationSelect` * Migrate `useQuery` in `SkillSelect` * Migrate `useQuery` in `InvestigationBuilder` * Migrate `useQuery` in `Uptime` (Asset) * fix location select not working for single select * uptime: fix issue with reverse --- src/Components/Common/AssetSelect.tsx | 28 ++---- src/Components/Common/BedSelect.tsx | 24 +++-- .../Common/DistrictAutocompleteFormField.tsx | 46 +++------- src/Components/Common/FacilitySelect.tsx | 20 ++--- .../Common/LocalBodyAutocompleteFormField.tsx | 48 +++------- src/Components/Common/LocationSelect.tsx | 89 +++++++------------ src/Components/Common/SkillSelect.tsx | 25 ++---- .../Common/StateAutocompleteFormField.tsx | 36 ++------ src/Components/Common/Uptime.tsx | 42 +++------ .../InvestigationBuilder.tsx | 37 +++----- src/Components/Facility/AssetCreate.tsx | 2 +- src/Components/Facility/ConsultationForm.tsx | 13 ++- .../Facility/Investigations/Reports/index.tsx | 4 +- .../Facility/Investigations/index.tsx | 12 +-- src/Components/Facility/models.tsx | 10 ++- src/Components/Users/SkillsSlideOver.tsx | 2 - src/Redux/actions.tsx | 15 +--- src/Redux/api.tsx | 12 ++- 18 files changed, 156 insertions(+), 309 deletions(-) diff --git a/src/Components/Common/AssetSelect.tsx b/src/Components/Common/AssetSelect.tsx index 0e47c7cfbfd..3f4f928032d 100644 --- a/src/Components/Common/AssetSelect.tsx +++ b/src/Components/Common/AssetSelect.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react"; -import { useDispatch } from "react-redux"; -import { listAssets } from "../../Redux/actions"; import AutoCompleteAsync from "../Form/AutoCompleteAsync"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; interface AssetSelectProps { name: string; @@ -16,7 +16,6 @@ interface AssetSelectProps { asset_class?: string; showAll?: boolean; showNOptions?: number; - freeText?: boolean; selected: any; setSelected: (selected: any) => void; } @@ -33,21 +32,13 @@ export const AssetSelect = (props: AssetSelectProps) => { is_permanent = null, showNOptions = 10, className = "", - freeText = false, errors = "", asset_class = "", } = props; - const dispatchAction: any = useDispatch(); - const AssetSearch = useCallback( async (text: string) => { - const params: Partial & { - limit: number; - offset: number; - search_text: string; - asset_class: string; - } = { + const query = { limit: 50, offset: 0, search_text: text, @@ -56,17 +47,12 @@ export const AssetSelect = (props: AssetSelectProps) => { in_use_by_consultation, is_permanent, asset_class, - }; + } as const; - const res = await dispatchAction(listAssets(params)); - if (freeText) - res?.data?.results?.push({ - id: -1, - name: text, - }); - return res?.data?.results; + const { data } = await request(routes.listAssets, { query }); + return data?.results; }, - [dispatchAction] + [asset_class, facility, in_use_by_consultation, is_permanent, is_working] ); return ( diff --git a/src/Components/Common/BedSelect.tsx b/src/Components/Common/BedSelect.tsx index 94caded12a3..a548b7a9eb5 100644 --- a/src/Components/Common/BedSelect.tsx +++ b/src/Components/Common/BedSelect.tsx @@ -1,9 +1,9 @@ import { useCallback } from "react"; -import { useDispatch } from "react-redux"; -import { listFacilityBeds } from "../../Redux/actions"; import { BedModel } from "../Facility/models"; import AutoCompleteAsync from "../Form/AutoCompleteAsync"; import { useTranslation } from "react-i18next"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; interface BedSelectProps { name: string; @@ -34,13 +34,11 @@ export const BedSelect = (props: BedSelectProps) => { location, showNOptions = 20, } = props; - - const dispatchAction: any = useDispatch(); const { t } = useTranslation(); const onBedSearch = useCallback( async (text: string) => { - const params = { + const query = { limit: 50, offset: 0, search_text: text, @@ -49,17 +47,17 @@ export const BedSelect = (props: BedSelectProps) => { location, }; - const res = await dispatchAction(listFacilityBeds(params)); - if (res && res.data) { - let beds = res.data.results; - if (unoccupiedOnly) { - beds = beds.filter((bed: BedModel) => bed?.is_occupied === false); - } + const { data } = await request(routes.listFacilityBeds, { query }); - return beds; + if (unoccupiedOnly) { + return data?.results?.filter( + (bed: BedModel) => bed?.is_occupied === false + ); } + + return data?.results; }, - [dispatchAction, facility, location, searchAll, unoccupiedOnly] + [facility, location, searchAll, unoccupiedOnly] ); return ( diff --git a/src/Components/Common/DistrictAutocompleteFormField.tsx b/src/Components/Common/DistrictAutocompleteFormField.tsx index 122b576a2fc..dbee6c9f08d 100644 --- a/src/Components/Common/DistrictAutocompleteFormField.tsx +++ b/src/Components/Common/DistrictAutocompleteFormField.tsx @@ -1,51 +1,27 @@ -import { useDispatch } from "react-redux"; import { FormFieldBaseProps } from "../Form/FormFields/Utils"; -import { IState } from "./StateAutocompleteFormField"; import AutocompleteFormField from "../Form/FormFields/Autocomplete"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { useCallback, useState } from "react"; -import { getDistrictByState } from "../../Redux/actions"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import { DistrictModel, StateModel } from "../Facility/models"; -export type IDistrict = { - id: number; - name: string; -}; - -type Props = FormFieldBaseProps & { +type Props = FormFieldBaseProps & { placeholder?: string; - state?: IState["id"]; + state?: StateModel["id"]; }; export default function DistrictAutocompleteFormField(props: Props) { - const dispatch = useDispatch(); - const [districts, setDistricts] = useState(); - - const fetchDistricts = useCallback( - async (status: any) => { - setDistricts(undefined); - if (!props.state) { - return; - } - const res = await dispatch(getDistrictByState({ id: props.state })); - if (!status.aborted && res.data) { - setDistricts(res.data); - } - }, - [dispatch, props.state] - ); - - useAbortableEffect( - (status: statusType) => fetchDistricts(status), - [props.state] - ); + const { data, loading } = useQuery(routes.getDistrictByState, { + pathParams: { id: props.state?.toString() ?? "" }, + prefetch: props.state !== undefined, + }); return ( option.name} optionValue={(option) => option.id} - isLoading={!!(props.state && districts === undefined)} + isLoading={loading} disabled={!props.state} /> ); diff --git a/src/Components/Common/FacilitySelect.tsx b/src/Components/Common/FacilitySelect.tsx index 1aabc36013b..17f67a7def1 100644 --- a/src/Components/Common/FacilitySelect.tsx +++ b/src/Components/Common/FacilitySelect.tsx @@ -1,8 +1,8 @@ import { useCallback } from "react"; -import { useDispatch } from "react-redux"; -import { getAllFacilities, getPermittedFacilities } from "../../Redux/actions"; import AutoCompleteAsync from "../Form/AutoCompleteAsync"; import { FacilityModel } from "../Facility/models"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; interface FacilitySelectProps { name: string; @@ -37,11 +37,9 @@ export const FacilitySelect = (props: FacilitySelectProps) => { errors = "", } = props; - const dispatchAction: any = useDispatch(); - const facilitySearch = useCallback( async (text: string) => { - const params = { + const query = { limit: 50, offset: 0, search_text: text, @@ -51,17 +49,19 @@ export const FacilitySelect = (props: FacilitySelectProps) => { district, }; - const res = await dispatchAction( - showAll ? getAllFacilities(params) : getPermittedFacilities(params) + const { data } = await request( + showAll ? routes.getAllFacilities : routes.getPermittedFacilities, + { query } ); + if (freeText) - res?.data?.results?.push({ + data?.results?.push({ id: -1, name: text, }); - return res?.data?.results; + return data?.results; }, - [dispatchAction, searchAll, showAll, facilityType, district] + [searchAll, showAll, facilityType, district, exclude_user, freeText] ); return ( diff --git a/src/Components/Common/LocalBodyAutocompleteFormField.tsx b/src/Components/Common/LocalBodyAutocompleteFormField.tsx index eacfbc35b89..32023fa559f 100644 --- a/src/Components/Common/LocalBodyAutocompleteFormField.tsx +++ b/src/Components/Common/LocalBodyAutocompleteFormField.tsx @@ -1,53 +1,27 @@ -import { useDispatch } from "react-redux"; import { FormFieldBaseProps } from "../Form/FormFields/Utils"; import AutocompleteFormField from "../Form/FormFields/Autocomplete"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { useCallback, useState } from "react"; -import { getLocalbodyByDistrict } from "../../Redux/actions"; -import { IDistrict } from "./DistrictAutocompleteFormField"; +import { DistrictModel, LocalBodyModel } from "../Facility/models"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; -export type ILocalBody = { - id: number; - name: string; -}; - -type Props = FormFieldBaseProps & { +type Props = FormFieldBaseProps & { placeholder?: string; - district?: IDistrict["id"]; + district?: DistrictModel["id"]; }; export default function LocalBodyAutocompleteFormField(props: Props) { - const dispatch = useDispatch(); - const [localBodies, setLocalBodies] = useState(); - - const fetchLocalBodies = useCallback( - async (status: any) => { - setLocalBodies(undefined); - if (!props.district) { - return; - } - const res = await dispatch( - getLocalbodyByDistrict({ id: props.district }) - ); - if (!status.aborted && res && res.data) { - setLocalBodies(res.data); - } - }, - [dispatch, props.district] - ); - - useAbortableEffect( - (status: statusType) => fetchLocalBodies(status), - [props.district] - ); + const { data, loading } = useQuery(routes.getLocalbodyByDistrict, { + pathParams: { id: props.district?.toString() ?? "" }, + prefetch: props.district !== undefined, + }); return ( option.name} optionValue={(option) => option.id} - isLoading={!!(props.district && localBodies === undefined)} + isLoading={loading} disabled={!props.district} /> ); diff --git a/src/Components/Common/LocationSelect.tsx b/src/Components/Common/LocationSelect.tsx index d20c843fad8..dcade42a596 100644 --- a/src/Components/Common/LocationSelect.tsx +++ b/src/Components/Common/LocationSelect.tsx @@ -1,8 +1,7 @@ -import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { listFacilityAssetLocation } from "../../Redux/actions"; import AutocompleteFormField from "../Form/FormFields/Autocomplete"; import AutocompleteMultiSelectFormField from "../Form/FormFields/AutocompleteMultiselect"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; interface LocationSelectProps { name: string; disabled?: boolean; @@ -11,7 +10,7 @@ interface LocationSelectProps { className?: string; searchAll?: boolean; multiple?: boolean; - facilityId: number | string; + facilityId: string; showAll?: boolean; selected: string | string[] | null; setSelected: (selected: string | string[] | null) => void; @@ -19,76 +18,48 @@ interface LocationSelectProps { } export const LocationSelect = (props: LocationSelectProps) => { - const { - name, - multiple, - selected, - setSelected, - errors, - className = "", - facilityId, - disabled = false, - } = props; - const [locations, setLocations] = useState<{ name: string; id: string }[]>( - [] + const { data, loading, refetch } = useQuery( + routes.listFacilityAssetLocation, + { + query: { + limit: 14, + }, + pathParams: { + facility_external_id: props.facilityId, + }, + prefetch: props.facilityId !== undefined, + } ); - const [query, setQuery] = useState(""); - const [loading, setLoading] = useState(false); - const dispatchAction: any = useDispatch(); - - const handleValueChange = (current: string[]) => { - if (multiple) setSelected(current); - else setSelected(current ? current[0] : ""); - }; - - useEffect(() => { - if (!facilityId) return; - const params = { - limit: 14, - search_text: query, - }; - setLoading(true); - dispatchAction( - listFacilityAssetLocation(params, { facility_external_id: facilityId }) - ).then(({ data }: any) => { - setLocations(data.results); - setLoading(false); - }); - }, [query, facilityId]); return props.multiple ? ( handleValueChange(value as unknown as string[])} - onQuery={(query) => { - setQuery(query); - }} + name={props.name} + disabled={props.disabled} + value={props.selected as unknown as string[]} + options={data?.results ?? []} + onChange={({ value }) => props.setSelected(value)} + onQuery={(search_text) => refetch({ query: { search_text } })} placeholder="Search by location name" optionLabel={(option) => option.name} optionValue={(option) => option.id} - error={errors} - className={className} + error={props.errors} + className={props.className} errorClassName={props.errorClassName} /> ) : ( handleValueChange([value])} - onQuery={(query) => { - setQuery(query); - }} + name={props.name} + disabled={props.disabled} + value={props.selected as string} + options={data?.results ?? []} + onChange={({ value }) => props.setSelected(value)} + onQuery={(search_text) => refetch({ query: { search_text } })} isLoading={loading} placeholder="Search by location name" optionLabel={(option) => option.name} optionValue={(option) => option.id} - error={errors} - className={className} + error={props.errors} + className={props.className} errorClassName={props.errorClassName} /> ); diff --git a/src/Components/Common/SkillSelect.tsx b/src/Components/Common/SkillSelect.tsx index 941c29790d1..e0d9aafdb13 100644 --- a/src/Components/Common/SkillSelect.tsx +++ b/src/Components/Common/SkillSelect.tsx @@ -1,8 +1,8 @@ import { useCallback } from "react"; -import { useDispatch } from "react-redux"; -import { getAllSkills } from "../../Redux/actions"; import AutoCompleteAsync from "../Form/AutoCompleteAsync"; import { SkillModel, SkillObjectModel } from "../Users/models"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; interface SkillSelectProps { id?: string; @@ -11,12 +11,10 @@ interface SkillSelectProps { className?: string; searchAll?: boolean; multiple?: boolean; - showAll?: boolean; showNOptions?: number; disabled?: boolean; selected: SkillObjectModel | SkillObjectModel[] | null; setSelected: (selected: SkillObjectModel) => void; - username?: string; userSkills?: SkillModel[]; } @@ -28,37 +26,28 @@ export const SkillSelect = (props: SkillSelectProps) => { selected, setSelected, searchAll, - showAll = true, showNOptions = 10, disabled = false, className = "", errors = "", - //username, userSkills, } = props; - const dispatchAction: any = useDispatch(); - const skillSearch = useCallback( async (text: string) => { - const params = { + const query = { limit: 50, offset: 0, search_text: text, all: searchAll, }; - const res = await dispatchAction(getAllSkills(params)); - const skillsID: string[] = []; - userSkills?.map((skill: SkillModel) => - skillsID.push(skill.skill_object.id) - ); - const skills = res?.data?.results.filter( - (skill: any) => !skillsID.includes(skill.id) + const { data } = await request(routes.getAllSkills, { query }); + return data?.results.filter( + (skill) => !userSkills?.some((userSkill) => userSkill.id === skill.id) ); - return skills; }, - [dispatchAction, searchAll, userSkills, showAll] + [searchAll, userSkills] ); return ( diff --git a/src/Components/Common/StateAutocompleteFormField.tsx b/src/Components/Common/StateAutocompleteFormField.tsx index 384f46c3a7c..77d52768198 100644 --- a/src/Components/Common/StateAutocompleteFormField.tsx +++ b/src/Components/Common/StateAutocompleteFormField.tsx @@ -1,45 +1,23 @@ -import { useCallback, useState } from "react"; import AutocompleteFormField from "../Form/FormFields/Autocomplete"; import { FormFieldBaseProps } from "../Form/FormFields/Utils"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { useDispatch } from "react-redux"; -import { getStates } from "../../Redux/actions"; +import { StateModel } from "../Facility/models"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; -export type IState = { - id: number; - name: string; -}; - -type Props = FormFieldBaseProps & { +type Props = FormFieldBaseProps & { placeholder?: string; }; export default function StateAutocompleteFormField(props: Props) { - const dispatch = useDispatch(); - const [states, setStates] = useState(); - - const fetchStates = useCallback( - async (status: any) => { - setStates(undefined); - const res = await dispatch(getStates()); - if (!status.aborted && res && res.data) { - setStates(res.data.results); - } - }, - [dispatch] - ); - - useAbortableEffect((status: statusType) => { - fetchStates(status); - }, []); + const { data, loading } = useQuery(routes.statesList); return ( option.name} optionValue={(option) => option.id} - isLoading={states === undefined} + isLoading={loading} /> ); } diff --git a/src/Components/Common/Uptime.tsx b/src/Components/Common/Uptime.tsx index 6f6966b9053..e3c7dd7a439 100644 --- a/src/Components/Common/Uptime.tsx +++ b/src/Components/Common/Uptime.tsx @@ -1,12 +1,10 @@ import { Popover } from "@headlessui/react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { listAssetAvailability } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; -import * as Notification from "../../Utils/Notifications.js"; +import { useEffect, useRef, useState } from "react"; import { AssetStatus, AssetUptimeRecord } from "../Assets/AssetTypes"; -import { reverse } from "lodash-es"; import { classNames } from "../../Utils/utils"; import dayjs from "../../Utils/dayjs"; +import useQuery from "../../Utils/request/useQuery.js"; +import routes from "../../Redux/api.js"; const STATUS_COLORS = { Operational: "bg-green-500", @@ -63,7 +61,7 @@ function UptimeInfo({ <> Status Updates
- {reverse(incidents)?.map((incident, index) => { + {incidents.reverse().map((incident, index) => { const prevIncident = incidents[index - 1]; let endTimestamp; let ongoing = false; @@ -171,16 +169,16 @@ export default function Uptime(props: { assetId: string }) { const [summary, setSummary] = useState<{ [key: number]: AssetUptimeRecord[]; }>([]); - const [availabilityData, setAvailabilityData] = useState( - [] - ); - const [loading, setLoading] = useState(true); + const { data, loading } = useQuery(routes.listAssetAvailability, { + query: { external_id: props.assetId }, + onResponse: ({ data }) => setUptimeRecord(data?.results.reverse() ?? []), + }); + const availabilityData = data?.results ?? []; const graphElem = useRef(null); const [numDays, setNumDays] = useState( Math.floor((window.innerWidth - 1024) / 20) ); const [hoveredDay, setHoveredDay] = useState(-1); - const dispatch = useDispatch(); const handleResize = () => { const containerWidth = graphElem.current?.clientWidth ?? window.innerWidth; @@ -268,25 +266,6 @@ export default function Uptime(props: { assetId: string }) { return Math.round((upStatus / (days * 3)) * 100); } - const fetchData = useCallback(async () => { - setLoading(true); - setLoading(false); - - const availabilityData = await dispatch( - listAssetAvailability({ - external_id: props.assetId, - }) - ); - if (availabilityData?.data) { - setAvailabilityData(availabilityData.data.results); - setUptimeRecord(reverse(availabilityData.data.results)); - } else { - Notification.Error({ - msg: "Error fetching availability history", - }); - } - }, [dispatch, props.assetId]); - useEffect(() => { setTimeout(() => { handleResize(); @@ -297,8 +276,7 @@ export default function Uptime(props: { assetId: string }) { useEffect(() => { handleResize(); - fetchData(); - }, [props.assetId, fetchData]); + }, []); const getStatusColor = (day: number) => { if (summary[day]) { diff --git a/src/Components/Common/prescription-builder/InvestigationBuilder.tsx b/src/Components/Common/prescription-builder/InvestigationBuilder.tsx index 67ac412c2e0..cb0bbac1f66 100644 --- a/src/Components/Common/prescription-builder/InvestigationBuilder.tsx +++ b/src/Components/Common/prescription-builder/InvestigationBuilder.tsx @@ -1,12 +1,10 @@ import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { - listInvestigationGroups, - listInvestigations, -} from "../../../Redux/actions"; import { PrescriptionDropdown } from "./PrescriptionDropdown"; import { PrescriptionMultiDropdown } from "./PrescriptionMultiselect"; import CareIcon from "../../../CAREUI/icons/CareIcon"; +import request from "../../../Utils/request/request"; +import routes from "../../../Redux/api"; + export type InvestigationType = { type?: string[]; repetitive?: boolean; @@ -35,7 +33,6 @@ export default function InvestigationBuilder( ) { const { investigations, setInvestigations } = props; const [investigationsList, setInvestigationsList] = useState([]); - const dispatch: any = useDispatch(); const [activeIdx, setActiveIdx] = useState(null); const additionalInvestigations = [ ["Vitals", ["Temp", "Blood Pressure", "Respiratory Rate", "Pulse Rate"]], @@ -85,26 +82,20 @@ export default function InvestigationBuilder( }; const fetchInvestigations = async () => { - const res = await dispatch(listInvestigations({})); - if (res && res.data) { - return res.data.results.map( - (investigation: any) => - investigation.name + - " -- " + - investigation.groups - .map((group: any) => " ( " + group.name + " ) ") - .join(", ") - ); - } - return []; + const { data } = await request(routes.listInvestigations); + return ( + data?.results.map( + (investigation) => + `${investigation.name} -- ${investigation.groups + .map((group) => ` ( ${group.name} ) `) + .join(", ")}` + ) ?? [] + ); }; const fetchInvestigationGroups = async () => { - const res = await dispatch(listInvestigationGroups({})); - if (res && res.data) { - return res.data.results.map((group: any) => group.name + " (GROUP)"); - } - return []; + const { data } = await request(routes.listInvestigationGroups); + return data?.results.map((group) => `${group.name} (GROUP)`) ?? []; }; return ( diff --git a/src/Components/Facility/AssetCreate.tsx b/src/Components/Facility/AssetCreate.tsx index a1e06a8c7ab..35f71a746fd 100644 --- a/src/Components/Facility/AssetCreate.tsx +++ b/src/Components/Facility/AssetCreate.tsx @@ -563,7 +563,7 @@ const AssetCreate = (props: AssetProps) => { selected={location} showAll={false} multiple={false} - facilityId={facilityId as unknown as number} + facilityId={facilityId} errors={state.errors.location} />
diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 77962cc7703..63e18645e13 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -229,11 +229,16 @@ type ConsultationFormSection = | "Treatment Plan" | "Bed Status"; -export const ConsultationForm = (props: any) => { +type Props = { + facilityId: string; + patientId: string; + id?: string; +}; + +export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { const { goBack } = useAppHistory(); const { kasp_enabled, kasp_string } = useConfig(); const dispatchAction: any = useDispatch(); - const { facilityId, patientId, id } = props; const [state, dispatch] = useAutoSaveReducer( consultationFormReducer, initialState @@ -344,7 +349,7 @@ export const ConsultationForm = (props: any) => { const fetchData = useCallback( async (status: statusType) => { if (!patientId) setIsLoading(true); - const res = await dispatchAction(getConsultation(id)); + const res = await dispatchAction(getConsultation(id!)); handleFormFieldChange({ name: "InvestigationAdvice", value: !Array.isArray(res.data.investigation) @@ -743,7 +748,7 @@ export const ConsultationForm = (props: any) => { }; const res = await dispatchAction( - id ? updateConsultation(id, data) : createConsultation(data) + id ? updateConsultation(id!, data) : createConsultation(data) ); setIsLoading(false); if (res?.data && res.status !== 400) { diff --git a/src/Components/Facility/Investigations/Reports/index.tsx b/src/Components/Facility/Investigations/Reports/index.tsx index 9b3c29d40f8..b868e8e8af0 100644 --- a/src/Components/Facility/Investigations/Reports/index.tsx +++ b/src/Components/Facility/Investigations/Reports/index.tsx @@ -1,6 +1,6 @@ import * as Notification from "../../../../Utils/Notifications"; import _ from "lodash-es"; -import { Group, InvestigationType } from ".."; +import { InvestigationGroup, InvestigationType } from ".."; import { getPatient, getPatientInvestigation, @@ -22,7 +22,7 @@ import { useRef } from "react"; const RESULT_PER_PAGE = 14; interface InitialState { - investigationGroups: Group[]; + investigationGroups: InvestigationGroup[]; selectedGroup: string[]; investigations: InvestigationType[]; selectedInvestigations: any[]; diff --git a/src/Components/Facility/Investigations/index.tsx b/src/Components/Facility/Investigations/index.tsx index 91836b6bbec..95147ec4c89 100644 --- a/src/Components/Facility/Investigations/index.tsx +++ b/src/Components/Facility/Investigations/index.tsx @@ -22,7 +22,7 @@ const initialState = { form: {}, }; -export interface Group { +export interface InvestigationGroup { external_id: string; name: string; } @@ -38,9 +38,9 @@ export interface InvestigationType { unit?: string; choices?: string; ideal_value?: string; - groups: Group[]; + groups: InvestigationGroup[]; } -type SearchItem = Group | InvestigationType; +type SearchItem = InvestigationGroup | InvestigationType; function isInvestigation(e: SearchItem): e is InvestigationType { return (e as InvestigationType).groups !== undefined; } @@ -67,7 +67,7 @@ const listOfInvestigations = ( ); }; -const findGroup = (group_id: string, groups: Group[]) => { +const findGroup = (group_id: string, groups: InvestigationGroup[]) => { return groups.find((g) => g.external_id === group_id); }; @@ -106,7 +106,9 @@ const Investigation = (props: { const [selectedGroup, setSelectedGroup] = useState([]); const [state, setState] = useReducer(testFormReducer, initialState); const [investigations, setInvestigations] = useState([]); - const [investigationGroups, setInvestigationGroups] = useState([]); + const [investigationGroups, setInvestigationGroups] = useState< + InvestigationGroup[] + >([]); const [selectedInvestigations, setSelectedInvestigations] = useState< InvestigationType[] >([]); diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index c3ac910970c..f26f458d4ca 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -203,11 +203,11 @@ export interface InventoryItemsModel { } export interface LocationModel { - id?: string; - name?: string; + id: string; + name: string; description?: string; middleware_address?: string; - location_type?: AssetLocationType; + location_type: AssetLocationType; facility?: { name: string; }; @@ -218,8 +218,8 @@ export interface LocationModel { export interface BedModel { id?: string; bed_type?: string; - description?: string; name?: string; + description?: string; facility?: string; location_object?: { name: string; @@ -227,6 +227,8 @@ export interface BedModel { }; location?: string; is_occupied?: boolean; + created_date?: string; + modified_date?: string; } export interface CurrentBed { diff --git a/src/Components/Users/SkillsSlideOver.tsx b/src/Components/Users/SkillsSlideOver.tsx index 1819333451d..63353479f0d 100644 --- a/src/Components/Users/SkillsSlideOver.tsx +++ b/src/Components/Users/SkillsSlideOver.tsx @@ -115,12 +115,10 @@ export default ({ show, setShow, username }: IProps) => { multiple={false} name="skill" disabled={!authorizeForAddSkill} - showAll={true} showNOptions={Infinity} selected={selectedSkill} setSelected={setSelectedSkill} errors="" - username={username} userSkills={skills?.results || []} /> { return fireRequest("getPermittedFacilities", [], params); }; -export const getAllFacilities = (params: object) => { - return fireRequest("getAllFacilities", [], params); -}; - -export const getAllSkills = (params: object) => { - return fireRequest("getAllSkills", [], params); -}; - export const getAnyFacility = (id: number | string, key?: string) => { return fireRequest("getAnyFacility", [], {}, { id: id }, key); }; @@ -296,10 +288,10 @@ export const createConsultation = (params: object) => { export const getConsultationList = (params: object) => { return fireRequest("getConsultationList", [], params); }; -export const getConsultation = (id: number) => { +export const getConsultation = (id: string) => { return fireRequest("getConsultation", [], {}, { id: id }); }; -export const updateConsultation = (id: number, params: object) => { +export const updateConsultation = (id: string, params: object) => { return fireRequest("updateConsultation", [], params, { id: id }); }; //Inventory @@ -506,9 +498,6 @@ export const updateAsset = (id: string, params: object) => export const operateAsset = (id: string, params: object) => fireRequest("operateAsset", [], params, { external_id: id }); -export const listAssetAvailability = (params: object) => - fireRequest("listAssetAvailability", [], params); - export const listPMJYPackages = (query?: string) => fireRequest("listPMJYPackages", [], { query }); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 8f8fc76484d..db049b1ed71 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -27,6 +27,7 @@ import { AssetServiceUpdate, AssetTransaction, AssetUpdate, + AssetUptimeRecord, PatientAssetBed, } from "../Components/Assets/AssetTypes"; import { @@ -77,6 +78,10 @@ import { import { IComment, IResource } from "../Components/Resource/models"; import { IShift } from "../Components/Shifting/models"; import { HCXPolicyModel } from "../Components/HCX/models"; +import { + InvestigationGroup, + InvestigationType, +} from "../Components/Facility/Investigations"; /** * A fake function that returns an empty object casted to type T @@ -286,6 +291,7 @@ const routes = { getAllFacilities: { path: "/api/v1/getallfacilities", + TRes: Type>(), }, createFacility: { @@ -933,10 +939,12 @@ const routes = { listInvestigations: { path: "/api/v1/investigation/", method: "GET", + TRes: Type>(), }, listInvestigationGroups: { - path: "/api/v1/investigation/group", + path: "/api/v1/investigation/group/", method: "GET", + TRes: Type>(), }, createInvestigation: { path: "/api/v1/consultation/{consultation_external_id}/investigation/", @@ -1232,10 +1240,12 @@ const routes = { listAssetAvailability: { path: "/api/v1/asset_availability/", method: "GET", + TRes: Type>(), }, getAssetAvailability: { path: "/api/v1/asset_availability/{id}", method: "GET", + TRes: Type(), }, // Prescription endpoints From f4f9b1e79edb5a8ed8b9eecddc92176a97f9fdb3 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 10 Jan 2024 19:25:50 +0530 Subject: [PATCH 15/16] Migrate to useQuery for Bed Management (#6975) --- src/Components/Facility/AddBedForm.tsx | 187 +++++++++++-------------- src/Locale/en/Bed.json | 10 +- src/Locale/en/Common.json | 3 +- src/Locale/en/Facility.json | 3 +- src/Redux/actions.tsx | 29 ---- src/Redux/api.tsx | 5 + 6 files changed, 100 insertions(+), 137 deletions(-) diff --git a/src/Components/Facility/AddBedForm.tsx b/src/Components/Facility/AddBedForm.tsx index 931a95da6a2..379a9a532b8 100644 --- a/src/Components/Facility/AddBedForm.tsx +++ b/src/Components/Facility/AddBedForm.tsx @@ -1,44 +1,41 @@ import Card from "../../CAREUI/display/Card"; -import { useState, useEffect, lazy, SyntheticEvent } from "react"; -import { useDispatch } from "react-redux"; -import { - createFacilityBed, - getAnyFacility, - getFacilityAssetLocation, - getFacilityBed, - updateFacilityBed, -} from "../../Redux/actions"; +import { useState, lazy, SyntheticEvent } from "react"; import * as Notification from "../../Utils/Notifications.js"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; import { LOCATION_BED_TYPES } from "../../Common/constants"; -import { navigate } from "raviger"; import { Cancel, Submit } from "../Common/components/ButtonV2"; import TextFormField from "../Form/FormFields/TextFormField"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import Page from "../Common/components/Page"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import useAppHistory from "../../Common/hooks/useAppHistory"; +import request from "../../Utils/request/request"; +import { useTranslation } from "react-i18next"; const Loading = lazy(() => import("../Common/Loading")); -interface BedFormProps { +interface Props { facilityId: string; locationId: string; bedId?: string; } -export const AddBedForm = (props: BedFormProps) => { - const { facilityId, locationId, bedId } = props; - const dispatchAction: any = useDispatch(); - const [isLoading, setIsLoading] = useState(false); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [bedType, setBedType] = useState(""); - const [facilityName, setFacilityName] = useState(""); - const [locationName, setLocationName] = useState(""); - const [bedName, setBedName] = useState(""); +export const AddBedForm = ({ facilityId, locationId, bedId }: Props) => { + const { t } = useTranslation(); + const { goBack } = useAppHistory(); + const [state, setState] = useState({ + name: "", + description: "", + bed_type: "", + facility: facilityId, + location: locationId, + }); + const [multipleBeds, setMultipleBeds] = useState(false); - const [numberOfBeds, setNumberOfBeds] = useState(1); //default = 1 + const [numberOfBeds, setNumberOfBeds] = useState(1); const [errors, setErrors] = useState({ name: "", description: "", @@ -46,43 +43,32 @@ export const AddBedForm = (props: BedFormProps) => { numberOfBeds: "", }); - const headerText = !bedId ? "Add Bed" : "Update Bed"; - const buttonText = !bedId ? "Add Bed(s)" : "Update Bed"; - - useEffect(() => { - async function fetchFacilityLocationAndBed() { - setIsLoading(true); - if (facilityId) { - const res = await dispatchAction(getAnyFacility(facilityId)); - setFacilityName(res?.data?.name || ""); - } - if (facilityId && locationId) { - const res = await dispatchAction( - getFacilityAssetLocation(facilityId, locationId) - ); - setLocationName(res?.data?.name || ""); - } - if (facilityId && locationId && bedId) { - const res = await dispatchAction( - getFacilityBed(facilityId, locationId, bedId) - ); - setName(res?.data?.name || ""); - setBedName(res?.data?.name || ""); - setDescription(res?.data?.description || ""); - setBedType(res?.data?.bed_type || ""); - setNumberOfBeds(res?.data?.number_of_beds || ""); - } - setIsLoading(false); - } + const { data: location } = useQuery(routes.getFacilityAssetLocation, { + pathParams: { + facility_external_id: facilityId, + external_id: locationId, + }, + }); - fetchFacilityLocationAndBed(); - }, [dispatchAction, facilityId, locationId]); + const { data, loading } = useQuery(routes.getFacilityBed, { + pathParams: { external_id: bedId ?? "" }, + prefetch: !!bedId, + onResponse: ({ data }) => { + setState({ + name: data?.name ?? "", + description: data?.description ?? "", + bed_type: data?.bed_type ?? "", + facility: facilityId, + location: locationId, + }); + }, + }); const validateInputs = (data: { name: string; description: string; bed_type: string; - number_of_beds: number; + number_of_beds?: number; }) => { let isValid = true; if (!data.name) { @@ -96,7 +82,7 @@ export const AddBedForm = (props: BedFormProps) => { if (multipleBeds === false) { setNumberOfBeds(1); } - if (data.number_of_beds < 1) { + if (data.number_of_beds !== undefined && data.number_of_beds < 1) { isValid = false; setErrors((prev) => ({ ...prev, @@ -115,63 +101,56 @@ export const AddBedForm = (props: BedFormProps) => { return isValid; }; - const handleCancel = () => - navigate(`/facility/${facilityId}/location/${locationId}/beds`, { - replace: true, - }); - const handleSubmit = async (e: SyntheticEvent) => { e.preventDefault(); - const data = { - name, - description, - bed_type: bedType, - number_of_beds: bedId ? 1 : numberOfBeds, - }; + const data = bedId + ? { ...state } + : { ...state, number_of_beds: numberOfBeds }; if (!validateInputs(data)) return; - setIsLoading(true); - - const res = await dispatchAction( - bedId - ? updateFacilityBed(data, facilityId, bedId, locationId) - : createFacilityBed(data, facilityId, locationId) - ); - setIsLoading(false); - if (res && (res.status === 201 || res.status === 200)) { - const notificationMessage = bedId - ? "Bed updated successfully" - : "Bed(s) created successfully"; + const onSuccess = (msg: string) => { + Notification.Success({ msg }); + goBack(); + }; - navigate(`/facility/${facilityId}/location/${locationId}/beds`, { - replace: true, + if (bedId) { + // Update + const { res } = await request(routes.updateFacilityBed, { + pathParams: { external_id: bedId }, + body: { ...data, facility: facilityId, location: locationId }, }); - Notification.Success({ - msg: notificationMessage, + res?.ok && onSuccess("Bed updated successfully"); + } else { + // Create + const { res } = await request(routes.createFacilityBed, { + body: { ...data, facility: facilityId, location: locationId }, }); + res?.ok && onSuccess("Bed(s) created successfully"); } }; - if (isLoading) { + if (loading) { return ; } + const action = t(!bedId ? "add_beds" : "update_bed"); + return (
{ setName(e.value)} + value={state.name} + onChange={(e) => setState((p) => ({ ...p, [e.name]: e.value }))} error={errors.name} /> setDescription(e.value)} + value={state.description} + onChange={(e) => setState((p) => ({ ...p, [e.name]: e.value }))} error={errors.description} /> @@ -203,13 +182,13 @@ export const AddBedForm = (props: BedFormProps) => { id="bed-type" className="w-full" name="bed_type" - label="Bed Type" + label={t("bed_type")} required options={LOCATION_BED_TYPES} optionLabel={(option) => option.name} optionValue={(option) => option.id} - value={bedType} - onChange={(e) => setBedType(e.value)} + value={state.bed_type} + onChange={(e) => setState((p) => ({ ...p, [e.name]: e.value }))} error={errors.bedType} /> @@ -217,10 +196,12 @@ export const AddBedForm = (props: BedFormProps) => { <> { - if (multipleBeds) setNumberOfBeds(1); - setMultipleBeds(!multipleBeds); + label={t("make_multiple_beds_label")} + onChange={({ value }) => { + setMultipleBeds(value); + if (value) { + setNumberOfBeds(1); + } }} name={"multipleBeds"} /> @@ -228,7 +209,7 @@ export const AddBedForm = (props: BedFormProps) => { id="numberofbed" name="number_of_beds" disabled={!multipleBeds} - label="Number of beds" + label={t("number_of_beds")} type="number" value={numberOfBeds.toString()} min={1} @@ -236,17 +217,17 @@ export const AddBedForm = (props: BedFormProps) => { onChange={(e) => setNumberOfBeds(Number(e.value))} error={ numberOfBeds > 100 - ? "Number of beds cannot be greater than 100" + ? t("number_of_beds_out_of_range_error") : undefined } /> )}
- + goBack()} /> 100} />
diff --git a/src/Locale/en/Bed.json b/src/Locale/en/Bed.json index 7b839c2a384..b410293959b 100644 --- a/src/Locale/en/Bed.json +++ b/src/Locale/en/Bed.json @@ -3,5 +3,11 @@ "BED_WITH_OXYGEN_SUPPORT": "Bed with Oxygen Support", "REGULAR": "Regular", "ICU": "ICU", - "ISOLATION": "Isolation" -} + "ISOLATION": "Isolation", + "add_beds": "Add Bed(s)", + "update_bed": "Update Bed", + "bed_type": "Bed Type", + "make_multiple_beds_label": "Do you want to make multiple beds?", + "number_of_beds": "Number of beds", + "number_of_beds_out_of_range_error": "Number of beds cannot be greater than 100" +} \ No newline at end of file diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index 31528afa390..99adb77451c 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -84,6 +84,7 @@ "age": "Age", "is": "Is", "reason": "Reason", + "description": "Description", "name": "Name", "address": "Address", "phone": "Phone", @@ -160,4 +161,4 @@ "select_date": "Select date", "DD/MM/YYYY": "DD/MM/YYYY", "clear_all_filters": "Clear All Filters" -} +} \ No newline at end of file diff --git a/src/Locale/en/Facility.json b/src/Locale/en/Facility.json index 5e69f8108af..aefc9c3b4a8 100644 --- a/src/Locale/en/Facility.json +++ b/src/Locale/en/Facility.json @@ -33,7 +33,6 @@ "asset_location": "Asset Location", "asset_type": "Asset Type", "asset_class": "Asset Class", - "description": "Description", "details_about_the_equipment": "Details about the equipment", "working_status": "Working Status", "why_the_asset_is_not_working": "Why the asset is not working?", @@ -53,4 +52,4 @@ "notes": "Notes", "create_asset": "Create Asset", "create_add_more": "Create & Add More" -} +} \ No newline at end of file diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index e517d7df3ce..d6de3c3b960 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -112,35 +112,6 @@ export const createFacilityBed = ( {} ); -export const getFacilityBed = ( - facility_external_id: string, - location_id: string, - external_id: string -) => - fireRequest( - "getFacilityBed", - [], - { facility: facility_external_id, location: location_id }, - { external_id } - ); -export const updateFacilityBed = ( - params: object, - facility_external_id: string, - external_id: string, - location_id: string -) => - fireRequest( - "updateFacilityBed", - [], - { ...params, facility: facility_external_id, location: location_id }, - { - external_id, - } - ); -export const deleteFacilityBed = (external_id: string) => { - return fireRequest("deleteFacilityBed", [], {}, { external_id }); -}; - // Download Actions export const downloadFacility = () => { return fireRequest("downloadFacility"); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index db049b1ed71..ee8c8f86ccf 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -414,14 +414,19 @@ const routes = { createFacilityBed: { path: "/api/v1/bed/", method: "POST", + TBody: Type(), + TRes: Type(), }, getFacilityBed: { path: "/api/v1/bed/{external_id}/", method: "GET", + TRes: Type(), }, updateFacilityBed: { path: "/api/v1/bed/{external_id}/", method: "PUT", + TBody: Type(), + TRes: Type(), }, deleteFacilityBed: { path: "/api/v1/bed/{external_id}/", From 5cf75e8e71268e854ffc96627086bb2007657453 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 10 Jan 2024 19:35:41 +0530 Subject: [PATCH 16/16] Add video connect link and Cypress Tests for User Profile (#6924) * Add video connect link to user profile * Add useAuthUser hook and triggerGoal function * Refactor DoctorVideoSlideover to filter doctors with alt_phone_number or video_connect_link * Add user profile tests * Update input fields with click before clearing and typing --- cypress/e2e/users_spec/user_profile.cy.ts | 85 +++++++++++++++++++ cypress/pageobject/Login/LoginPage.ts | 4 + cypress/pageobject/Users/ManageUserPage.ts | 12 +++ cypress/pageobject/Users/UserProfilePage.ts | 84 ++++++++++++++++++ .../Facility/DoctorVideoSlideover.tsx | 44 +++++++++- src/Components/Users/UserProfile.tsx | 43 +++++++++- src/Components/Users/models.tsx | 2 + src/Utils/utils.ts | 9 ++ 8 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 cypress/e2e/users_spec/user_profile.cy.ts create mode 100644 cypress/pageobject/Users/UserProfilePage.ts diff --git a/cypress/e2e/users_spec/user_profile.cy.ts b/cypress/e2e/users_spec/user_profile.cy.ts new file mode 100644 index 00000000000..b6fe64b2455 --- /dev/null +++ b/cypress/e2e/users_spec/user_profile.cy.ts @@ -0,0 +1,85 @@ +import { cy, describe, before, beforeEach, it, afterEach } from "local-cypress"; +import LoginPage from "../../pageobject/Login/LoginPage"; +import UserProfilePage from "../../pageobject/Users/UserProfilePage"; +import ManageUserPage from "../../pageobject/Users/ManageUserPage"; + +describe("Manage User Profile", () => { + const loginPage = new LoginPage(); + const userProfilePage = new UserProfilePage(); + const manageUserPage = new ManageUserPage(); + + const age = "30"; + const gender = "Male"; + const email = "test@example.com"; + const phone = "+918899887788"; + const workinghours = "8"; + const doctorQualification = "MBBS"; + const doctorYoE = "10"; + const medicalCouncilRegistration = "1234567890"; + + const facilitySearch = "Dummy Facility 1"; + + before(() => { + loginPage.loginAsDevDoctor(); + cy.saveLocalStorage(); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + console.log(localStorage); + cy.clearLocalStorage(/filters--.+/); + console.log(localStorage); + cy.awaitUrl("/user/profile"); + }); + + it("Set Age, Gender, Email, Phone and Working Hours for a user and verify its reflection in user profile", () => { + userProfilePage.clickEditProfileButton(); + + userProfilePage.typeAge(age); + userProfilePage.selectGender(gender); + userProfilePage.typeEmail(email); + userProfilePage.typePhone(phone); + userProfilePage.typeWhatsApp(phone); + userProfilePage.typeWorkingHours(workinghours); + userProfilePage.typeDoctorQualification(doctorQualification); + userProfilePage.typeDoctorYoE(doctorYoE); + userProfilePage.typeMedicalCouncilRegistration(medicalCouncilRegistration); + + userProfilePage.clickUpdateButton(); + + cy.verifyNotification("Details updated successfully"); + + userProfilePage.assertAge(age); + userProfilePage.assertGender(gender); + userProfilePage.assertEmail(email); + userProfilePage.assertPhone(phone); + userProfilePage.assertWhatsApp(phone); + userProfilePage.assertWorkingHours(workinghours); + }); + + it("Adding video connect link for a user and verify its reflection in user profile and doctor connect", () => { + // verify the user doesn't have any video connect link + userProfilePage.assertVideoConnectLink("-"); + // Link a new video connect link and ensure it is under video connect link + userProfilePage.clickEditProfileButton(); + userProfilePage.typeVideoConnectLink("https://www.example.com"); + userProfilePage.clickUpdateButton(); + userProfilePage.assertVideoConnectLink("https://www.example.com"); + // Edit the video connect link and ensure it is updated + userProfilePage.clickEditProfileButton(); + userProfilePage.typeVideoConnectLink("https://www.test.com"); + userProfilePage.clickUpdateButton(); + userProfilePage.assertVideoConnectLink("https://www.test.com"); + // Go to particular facility doctor connect and verify the video connect link is present + manageUserPage.navigateToFacility(); + manageUserPage.typeFacilitySearch(facilitySearch); + manageUserPage.assertFacilityInCard(facilitySearch); + manageUserPage.clickFacilityPatients(); + manageUserPage.clickDoctorConnectButton(); + manageUserPage.assertVideoConnectLink("Dev Doctor", "https://www.test.com"); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); +}); diff --git a/cypress/pageobject/Login/LoginPage.ts b/cypress/pageobject/Login/LoginPage.ts index f691d5f9e15..f4f188f11d6 100644 --- a/cypress/pageobject/Login/LoginPage.ts +++ b/cypress/pageobject/Login/LoginPage.ts @@ -6,6 +6,10 @@ class LoginPage { cy.loginByApi("devdistrictadmin", "Coronasafe@123"); } + loginAsDevDoctor(): void { + cy.loginByApi("devdoctor", "Coronasafe@123"); + } + login(username: string, password: string): void { cy.loginByApi(username, password); } diff --git a/cypress/pageobject/Users/ManageUserPage.ts b/cypress/pageobject/Users/ManageUserPage.ts index 2d1ebbc14f0..622229745f7 100644 --- a/cypress/pageobject/Users/ManageUserPage.ts +++ b/cypress/pageobject/Users/ManageUserPage.ts @@ -149,6 +149,18 @@ export class ManageUserPage { cy.get("#doctor-connect-home-doctor").should("contain.text", realName); cy.get("#doctor-connect-remote-doctor").should("contain.text", realName); } + + assertVideoConnectLink(docName: string, link: string) { + cy.get("ul#options") + .find("li") + .contains(docName) + .within(() => { + cy.get("a").should(($a) => { + const hrefs = $a.map((i, el) => Cypress.$(el).attr("href")).get(); + expect(hrefs).to.include(link); + }); + }); + } } export default ManageUserPage; diff --git a/cypress/pageobject/Users/UserProfilePage.ts b/cypress/pageobject/Users/UserProfilePage.ts new file mode 100644 index 00000000000..3f71a29181b --- /dev/null +++ b/cypress/pageobject/Users/UserProfilePage.ts @@ -0,0 +1,84 @@ +export default class UserProfilePage { + assertVideoConnectLink(link: string) { + cy.get("#videoconnectlink-profile-details").should("contain.text", link); + } + + clickEditProfileButton() { + cy.get("#edit-cancel-profile-button").click(); + } + + typeVideoConnectLink(link: string) { + cy.get("#video_connect_link").click().clear().type(link); + } + + clickUpdateButton() { + cy.get("#submit").click(); + } + + typeAge(age: string) { + cy.get("#age").click().clear().type(age); + } + + selectGender(gender: string) { + cy.get("#gender").click(); + cy.get("#gender-option-" + gender).click(); + } + + typeEmail(email: string) { + cy.get("#email").click().clear().type(email); + } + + typePhone(phone: string) { + cy.get("#phoneNumber").click().clear().type(phone); + } + + typeWhatsApp(phone: string) { + cy.get("#altPhoneNumber").click().clear().type(phone); + } + + typeWorkingHours(workinghours: string) { + cy.get("#weekly_working_hours").click().clear().type(workinghours); + } + + typeDoctorQualification = (doctorQualification: string) => { + cy.get("#doctor_qualification").click().clear().type(doctorQualification); + }; + + typeDoctorYoE = (doctorYoE: string) => { + cy.get("#doctor_experience_commenced_on").click().clear().type(doctorYoE); + }; + + typeMedicalCouncilRegistration = (medicalCouncilRegistration: string) => { + cy.get("#doctor_medical_council_registration") + .click() + .clear() + .type(medicalCouncilRegistration); + }; + + assertAge(age: string) { + cy.get("#age-profile-details").should("contain.text", age); + } + + assertGender(gender: string) { + cy.get("#gender-profile-details").should("contain.text", gender); + } + + assertEmail(email: string) { + cy.get("#emailid-profile-details").should("contain.text", email); + } + + assertPhone(phone: string) { + cy.get("#contactno-profile-details").should("contain.text", phone); + } + + assertWhatsApp(phone: string) { + cy.get("#whatsapp-profile-details").should("contain.text", phone); + } + + assertWorkingHours(workinghours: string) { + cy.get("#averageworkinghour-profile-details").should( + "contain.text", + workinghours + ); + } +} diff --git a/src/Components/Facility/DoctorVideoSlideover.tsx b/src/Components/Facility/DoctorVideoSlideover.tsx index 5302f7f9d53..f16b7ba1811 100644 --- a/src/Components/Facility/DoctorVideoSlideover.tsx +++ b/src/Components/Facility/DoctorVideoSlideover.tsx @@ -6,6 +6,8 @@ import { UserAssignedModel } from "../Users/models"; import { SkillObjectModel } from "../Users/models"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { relativeTime } from "../../Utils/utils"; +import useAuthUser from "../../Common/hooks/useAuthUser"; +import { triggerGoal } from "../../Integrations/Plausible"; export default function DoctorVideoSlideover(props: { show: boolean; @@ -22,10 +24,12 @@ export default function DoctorVideoSlideover(props: { const res = await dispatchAction( getFacilityUsers(facilityId, { limit: 50 }) ); - if (res && res.data) { + if (res?.data) { setDoctors( res.data.results - .filter((user: any) => user.alt_phone_number) + .filter( + (user: any) => user.alt_phone_number || user.video_connect_link + ) .sort((a: any, b: any) => { return Number(a.last_login) - Number(b.last_login); }) @@ -106,6 +110,7 @@ function UserListItem(props: { user: UserAssignedModel }) { const user = props.user; const icon = user.user_type === "Doctor" ? "fa-user-doctor " : " fa-user-nurse"; + const authUser = useAuthUser(); return (
  • @@ -152,6 +157,27 @@ function UserListItem(props: { user: UserAssignedModel }) { {user.first_name} {user.last_name}
    + {user.video_connect_link && ( + { + triggerGoal("Doctor Connect Click", { + medium: "Video Call", + userId: authUser?.id, + targetUserType: user.user_type, + }); + }} + target="_blank" + rel="noopener noreferrer" + > +
    + + Connect on a Video Call + + +
    +
    + )} { + triggerGoal("Doctor Connect Click", { + medium: "WhatsApp", + userId: authUser?.id, + targetUserType: user.user_type, + }); + }} target="_blank" rel="noopener noreferrer" > @@ -176,6 +209,13 @@ function UserListItem(props: { user: UserAssignedModel }) { href={ user.alt_phone_number ? `tel:${user.alt_phone_number}` : "#" } + onClick={() => { + triggerGoal("Doctor Connect Click", { + medium: "Phone Call", + userId: authUser?.id, + targetUserType: user.user_type, + }); + }} >
    diff --git a/src/Components/Users/UserProfile.tsx b/src/Components/Users/UserProfile.tsx index 2dc75d33613..1fcc8fb385e 100644 --- a/src/Components/Users/UserProfile.tsx +++ b/src/Components/Users/UserProfile.tsx @@ -5,7 +5,7 @@ import * as Notification from "../../Utils/Notifications.js"; import LanguageSelector from "../../Components/Common/LanguageSelector"; import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2, { Submit } from "../Common/components/ButtonV2"; -import { classNames, parsePhoneNumber } from "../../Utils/utils"; +import { classNames, isValidUrl, parsePhoneNumber } from "../../Utils/utils"; import CareIcon from "../../CAREUI/icons/CareIcon"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; @@ -27,6 +27,7 @@ type EditForm = { age: string; gender: GenderType; email: string; + video_connect_link: string | undefined; phoneNumber: string; altPhoneNumber: string; user_type: string | undefined; @@ -41,6 +42,7 @@ type ErrorForm = { age: string; gender: string; email: string; + video_connect_link: string | undefined; phoneNumber: string; altPhoneNumber: string; user_type: string | undefined; @@ -62,6 +64,7 @@ const initForm: EditForm = { lastName: "", age: "", gender: "Male", + video_connect_link: "", email: "", phoneNumber: "", altPhoneNumber: "", @@ -145,6 +148,7 @@ export default function UserProfile() { age: result.data.age?.toString() || "", gender: result.data.gender || "Male", email: result.data.email, + video_connect_link: result.data.video_connect_link, phoneNumber: result.data.phone_number?.toString() || "", altPhoneNumber: result.data.alt_phone_number?.toString() || "", user_type: result.data.user_type, @@ -262,6 +266,14 @@ export default function UserProfile() { invalidForm = true; } return; + case "video_connect_link": + if (states.form[field]) { + if (isValidUrl(states.form[field]) === false) { + errors[field] = "Please enter a valid url"; + invalidForm = true; + } + } + return; } }); dispatch({ type: "set_error", errors }); @@ -294,6 +306,7 @@ export default function UserProfile() { first_name: states.form.firstName, last_name: states.form.lastName, email: states.form.email, + video_connect_link: states.form.video_connect_link, phone_number: parsePhoneNumber(states.form.phoneNumber) ?? "", alt_phone_number: parsePhoneNumber(states.form.altPhoneNumber) ?? "", gender: states.form.gender, @@ -578,6 +591,28 @@ export default function UserProfile() { {userData?.weekly_working_hours || "-"}
    +
    )} @@ -679,6 +714,12 @@ export default function UserProfile() { min={0} max={168} /> +
  • diff --git a/src/Components/Users/models.tsx b/src/Components/Users/models.tsx index 6fa0bfb3e42..a6cd0d7c8ff 100644 --- a/src/Components/Users/models.tsx +++ b/src/Components/Users/models.tsx @@ -28,6 +28,7 @@ export type UserModel = UserBareMinimum & { local_body?: number; district?: number; state?: number; + video_connect_link: string; phone_number?: string; alt_phone_number?: string; gender?: GenderType; @@ -62,6 +63,7 @@ export interface UserAssignedModel extends UserBareMinimum { state?: number; phone_number?: string; alt_phone_number?: string; + video_connect_link: string; gender?: number; age?: number; is_superuser?: boolean; diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index df6bd46d131..c518bcffe7d 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -462,3 +462,12 @@ export const compareBy = (key: keyof T) => { return a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0; }; }; + +export const isValidUrl = (url?: string) => { + try { + new URL(url ?? ""); + return true; + } catch { + return false; + } +};