diff --git a/cypress/e2e/camera_spec/camera_boundary.cy.ts b/cypress/e2e/camera_spec/camera_boundary.cy.ts new file mode 100644 index 00000000000..3658844cd7c --- /dev/null +++ b/cypress/e2e/camera_spec/camera_boundary.cy.ts @@ -0,0 +1,73 @@ +import { cy, describe, before, beforeEach, it } from "local-cypress"; +const user = { username: "devdistrictadmin", password: "Coronasafe@123" }; +describe("Camera Boundary", () => { + before(() => { + cy.loginByApi(user.username, user.password); + cy.saveLocalStorage(); + }); + beforeEach(() => { + cy.restoreLocalStorage(); + cy.awaitUrl("/assets"); + cy.get("input[id='search']").type("Dummy Camera 30"); + cy.contains("a", "Dummy Camera").click(); + cy.get("button[id='configure-asset']").click(); + }); + + it("Add new boundary", () => { + cy.get("input[name='bed']").type("Dummy Bed 4"); + cy.get("li[role='option']").contains("Dummy Bed 4").click(); + cy.wait(2000); + cy.get("button[id='add-boundary-preset']").click(); + cy.get("label[id='boundary-preset-name']").should("exist"); + }); + + it("Update boundary", () => { + cy.get("input[name='bed']").type("Dummy Bed 4"); + cy.get("li[role='option']").contains("Dummy Bed 4").click(); + cy.wait(2000); + cy.get("button[id='update-boundary-preset']").click(); + cy.intercept("**/api/v1/assetbed/**").as("updateBoundary"); + cy.get("button") + .find("svg.care-svg-icon__baseline.care-l-angle-right") + .should("be.visible") + .first() + .click(); + cy.wait("@updateBoundary"); + cy.get("button").contains("Next").click(); + cy.get("button") + .find("svg.care-svg-icon__baseline.care-l-angle-right") + .should("be.visible") + .first() + .click(); + cy.wait("@updateBoundary"); + cy.get("button").contains("Next").click(); + cy.get("button") + .find("svg.care-svg-icon__baseline.care-l-angle-up") + .should("be.visible") + .first() + .click(); + cy.wait("@updateBoundary"); + cy.get("button").contains("Next").click(); + cy.get("button") + .find("svg.care-svg-icon__baseline.care-l-angle-down") + .should("be.visible") + .first() + .click(); + cy.wait("@updateBoundary"); + cy.get("button").contains("Done").click(); + }); + + it("Delete boundary", () => { + cy.get("input[name='bed']").type("Dummy Bed 4"); + cy.get("li[role='option']").contains("Dummy Bed 4").click(); + cy.wait(1000); + cy.intercept("**/api/v1/assetbed/**").as("deleteBoundary"); + cy.get("button[id='delete-boundary-preset']").click(); + cy.get("button").contains("Delete").click(); + cy.wait("@deleteBoundary"); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); +}); diff --git a/cypress/e2e/camera_spec/patient_privacy.cy.ts b/cypress/e2e/camera_spec/patient_privacy.cy.ts new file mode 100644 index 00000000000..1e466b18a76 --- /dev/null +++ b/cypress/e2e/camera_spec/patient_privacy.cy.ts @@ -0,0 +1,33 @@ +import { cy, describe, before, beforeEach, it } from "local-cypress"; +const user = { username: "devdistrictadmin", password: "Coronasafe@123" }; + +describe("Patient Privacy", () => { + before(() => { + cy.loginByApi(user.username, user.password); + cy.saveLocalStorage(); + }); + beforeEach(() => { + cy.restoreLocalStorage(); + cy.awaitUrl("/patients"); + cy.intercept("**/api/v1/consultation/**").as("consultation"); + cy.get("input[id='name']").type("Dummy Patient 16"); + cy.contains("a", "Dummy Patient 16").click(); + cy.wait("@consultation"); + // assign bed to patient + cy.get("button").contains("Assign Bed").click(); + cy.get("input[name='bed']").type("Dummy Bed 6"); + cy.get("li[role='option']").contains("Dummy Bed 6").click(); + cy.wait(2000); + cy.get("button").contains("Move to bed").click(); + cy.wait(2000); + // open the feeds page + cy.url().then((urlValue) => cy.awaitUrl(urlValue + "/feed")); + cy.wait("@consultation"); + }); + + it("Toggle privacy", () => { + cy.intercept("**/api/v1/consultationbed/**").as("togglePrivacy"); + cy.get("button[id='privacy-toggle']").click(); + cy.wait("@togglePrivacy"); + }); +}); diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index e452ed5abb2..711950492c5 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -714,7 +714,7 @@ export const RHYTHM_CHOICES: Array = [ { id: 10, text: "IRREGULAR", desc: "Irregular" }, ]; -export const LOCATION_BED_TYPES: Array = [ +export const LOCATION_BED_TYPES: Array<{ id: string; name: string }> = [ { id: "ISOLATION", name: "Isolation" }, { id: "ICU", name: "ICU" }, { id: "BED_WITH_OXYGEN_SUPPORT", name: "Bed with oxygen support" }, @@ -1031,6 +1031,14 @@ export const XLSXAssetImportSchema = { }, }; +export type direction = "left" | "right" | "up" | "down" | null; +export interface BoundaryRange { + max_x: number; + min_x: number; + max_y: number; + min_y: number; +} + export const USER_TYPES_MAP = { Pharmacist: "Pharmacist", Volunteer: "Volunteer", diff --git a/src/Common/hooks/useFeedPTZ.ts b/src/Common/hooks/useFeedPTZ.ts index b064e9180ee..04969ccfbf0 100644 --- a/src/Common/hooks/useFeedPTZ.ts +++ b/src/Common/hooks/useFeedPTZ.ts @@ -15,6 +15,8 @@ interface PTZPayload { x: number; y: number; zoom: number; + id?: string | null; + camera_state?: PTZState | null; } export interface PTZState { @@ -55,6 +57,9 @@ interface UseMSEMediaPlayerReturnType { getCameraStatus: (options: IOptions) => void; getPresets: (options: IOptions) => void; gotoPreset: (payload: IGotoPresetPayload, options: IOptions) => void; + lockAsset: (options: IOptions) => void; + unlockAsset: (options: IOptions) => void; + requestAccess: (options: IOptions) => void; } interface IOptions { @@ -196,6 +201,57 @@ export const getPTZPayload = ( return { x, y, zoom }; }; +const lockAsset = + (config: IAsset, dispatch: any) => + async (options: IOptions = {}) => { + if (!config.id) return; + const resp = await dispatch( + operateAsset(config.id, { + action: { + type: "lock_camera", + }, + }) + ); + resp && + (resp.status === 200 + ? options?.onSuccess && options.onSuccess(resp.data.result) + : options?.onError && options.onError(resp)); + }; + +const requestAccess = + (config: IAsset, dispatch: any) => + async (options: IOptions = {}) => { + if (!config.id) return; + const resp = await dispatch( + operateAsset(config.id, { + action: { + type: "request_access", + }, + }) + ); + resp && + (resp.status === 200 + ? options?.onSuccess && options.onSuccess(resp.data.result) + : options?.onError && options.onError(resp)); + }; + +const unlockAsset = + (config: IAsset, dispatch: any) => + async (options: IOptions = {}) => { + if (!config.id) return; + const resp = await dispatch( + operateAsset(config.id, { + action: { + type: "unlock_camera", + }, + }) + ); + resp && + (resp.status === 200 + ? options?.onSuccess && options.onSuccess(resp.data.result) + : options?.onError && options.onError(resp)); + }; + export const useFeedPTZ = ({ config, dispatch, @@ -207,5 +263,8 @@ export const useFeedPTZ = ({ getCameraStatus: getCameraStatus(config, dispatch), getPresets: getPresets(config, dispatch), gotoPreset: gotoPreset(config, dispatch), + lockAsset: lockAsset(config, dispatch), + unlockAsset: unlockAsset(config, dispatch), + requestAccess: requestAccess(config, dispatch), }; }; diff --git a/src/Common/hooks/useNotificationSubscribe.ts b/src/Common/hooks/useNotificationSubscribe.ts new file mode 100644 index 00000000000..5cb4e6732cb --- /dev/null +++ b/src/Common/hooks/useNotificationSubscribe.ts @@ -0,0 +1,140 @@ +import { useState } from "react"; +import * as Sentry from "@sentry/browser"; +import { useTranslation } from "react-i18next"; +import { Error } from "../../Utils/Notifications.js"; +import useAuthUser from "../../Common/hooks/useAuthUser"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; + +type SubscriptionStatusType = + | "" + | "NotSubscribed" + | "SubscribedOnThisDevice" + | "SubscribedOnAnotherDevice"; + +export default function useNotificationSubscribe() { + const { username } = useAuthUser(); + const [subscriptionStatus, setSubscriptionStatus] = + useState(""); + const [isSubscribing, setIsSubscribing] = useState(false); + const { t } = useTranslation(); + + const intialSubscriptionState = async () => { + try { + const { data } = await request(routes.getUserPnconfig, { + pathParams: { username }, + }); + const reg = await navigator.serviceWorker.ready; + const subscription = await reg.pushManager.getSubscription(); + if (!subscription && !data?.pf_endpoint) { + setSubscriptionStatus("NotSubscribed"); + } else if (subscription?.endpoint === data?.pf_endpoint) { + setSubscriptionStatus("SubscribedOnThisDevice"); + } else { + setSubscriptionStatus("SubscribedOnAnotherDevice"); + } + } catch (error) { + Sentry.captureException(error); + } + }; + + const handleSubscribeClick = () => { + const status = subscriptionStatus; + if (status === "NotSubscribed" || status === "SubscribedOnAnotherDevice") { + subscribe(); + } else { + unsubscribe(); + } + }; + + const unsubscribe = () => { + navigator.serviceWorker.ready + .then(function (reg) { + setIsSubscribing(true); + reg.pushManager + .getSubscription() + .then(function (subscription) { + subscription + ?.unsubscribe() + .then(async function (_successful) { + const data = { + pf_endpoint: "", + pf_p256dh: "", + pf_auth: "", + }; + + await request(routes.updateUserPnconfig, { + body: data, + pathParams: { username }, + }); + + setSubscriptionStatus("NotSubscribed"); + setIsSubscribing(false); + }) + .catch(function (_e) { + Error({ + msg: t("unsubscribe_failed"), + }); + }); + }) + .catch(function (_e) { + Error({ msg: t("subscription_error") }); + }); + }) + .catch(function (_e) { + Sentry.captureException(_e); + }); + }; + + async function subscribe() { + setIsSubscribing(true); + + const { data: responseData } = await request(routes.getPublicKey); + + const public_key = responseData?.public_key; + const sw = await navigator.serviceWorker.ready; + const push = await sw.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: public_key, + }); + const p256dh = btoa( + String.fromCharCode.apply( + null, + new Uint8Array(push.getKey("p256dh") as any) as any + ) + ); + const auth = btoa( + String.fromCharCode.apply( + null, + new Uint8Array(push.getKey("auth") as any) as any + ) + ); + + const data = { + pf_endpoint: push.endpoint, + pf_p256dh: p256dh, + pf_auth: auth, + }; + + const { res } = await request(routes.updateUserPnconfig, { + body: data, + pathParams: { username }, + }); + + if (res && res?.status >= 200 && res?.status <= 300) { + setSubscriptionStatus("SubscribedOnThisDevice"); + } + setIsSubscribing(false); + } + + return { + subscriptionStatus, + isSubscribing, + setSubscriptionStatus, + setIsSubscribing, + handleSubscribeClick, + intialSubscriptionState, + subscribe, + unsubscribe, + }; +} diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx index 86b7199cdac..b03d1581eb6 100644 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx @@ -4,12 +4,14 @@ import * as Notification from "../../../Utils/Notifications.js"; import { BedModel } from "../../Facility/models"; import axios from "axios"; import { getCameraConfig } from "../../../Utils/transformUtils"; -import CameraConfigure from "../configure/CameraConfigure"; import Loading from "../../Common/Loading"; import { checkIfValidIP } from "../../../Common/validation"; import TextFormField from "../../Form/FormFields/TextFormField"; import { Submit } from "../../Common/components/ButtonV2"; import { SyntheticEvent } from "react"; +import LiveFeed from "../../Facility/Consultations/LiveFeed"; +import Card from "../../../CAREUI/display/Card"; +import { BoundaryRange } from "../../../Common/constants"; import useAuthUser from "../../../Common/hooks/useAuthUser"; import request from "../../../Utils/request/request"; @@ -43,6 +45,91 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { const [refreshPresetsHash, setRefreshPresetsHash] = useState( Number(new Date()) ); + const [boundaryPreset, setBoundaryPreset] = useState(null); + const [toUpdateBoundary, setToUpdateBoundary] = useState(false); + const [loadingAddBoundaryPreset, setLoadingAddBoundaryPreset] = + useState(false); + const [updateBoundaryNotif, setUpdateBoundaryNotif] = + useState("notUpdated"); + const [presets, setPresets] = useState([]); + + const mapZoomToBuffer = (zoom: number): number => { + interface bufferAtZoom { + [key: string]: number; + } + const bufferAtMaxZoom: bufferAtZoom = { + "0.3": 0.2, + "0.4": 0.1, + "0.5": 0.05, + }; + let buffer = 0; + Object.keys(bufferAtMaxZoom).forEach((key: string) => { + if (zoom <= Number(key)) { + buffer = bufferAtMaxZoom[key]; + } + }); + return buffer !== 0 ? buffer : 0.0625; + }; + + const calcBoundary = (presets: any[]): BoundaryRange => { + const INT_MAX = 0.9; + const boundary: BoundaryRange = { + max_x: -INT_MAX, + min_x: INT_MAX, + max_y: -INT_MAX, + min_y: INT_MAX, + }; + + const edgePresetsZoom: BoundaryRange = { + max_x: 0, + min_x: 0, + max_y: 0, + min_y: 0, + }; + + presets.forEach((preset: any) => { + if (preset?.meta?.position) { + const position = preset.meta.position; + if (position.x > boundary.max_x) { + boundary.max_x = position.x; + edgePresetsZoom.max_x = position.zoom; + } + if (position.x < boundary.min_x) { + boundary.min_x = position.x; + edgePresetsZoom.min_x = position.zoom; + } + if (position.y > boundary.max_y) { + boundary.max_y = position.y; + edgePresetsZoom.max_y = position.zoom; + } + if (position.y < boundary.min_y) { + boundary.min_y = position.y; + edgePresetsZoom.min_y = position.zoom; + } + } + }); + + Object.keys(edgePresetsZoom).forEach((key) => { + const zoom = edgePresetsZoom[key as keyof BoundaryRange]; + const buffer = mapZoomToBuffer(zoom); + + if (key == "max_x" || key == "max_y") { + boundary[key] = boundary[key] + buffer; + } else { + boundary[key as keyof BoundaryRange] = + boundary[key as keyof BoundaryRange] - buffer; + } + }); + if (boundary.max_x <= boundary.min_x || boundary.max_y <= boundary.min_y) { + return { + max_x: INT_MAX, + min_x: -INT_MAX, + max_y: INT_MAX, + min_y: -INT_MAX, + }; + } + return boundary; + }; const { data: facility, loading } = useQuery(routes.getPermittedFacility, { pathParams: { id: facilityId }, }); @@ -62,6 +149,40 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { setIsLoading(false); }, [asset]); + const fetchBoundaryBedPreset = async () => { + const { res, data } = await request(routes.listAssetBeds, { + query: { bed: bed.id }, + }); + + if (res && res.status === 200 && data) { + let bedAssets: any[] = data.results; + + if (bedAssets.length > 0) { + let boundaryPreset = null; + bedAssets = bedAssets.filter((bedAsset: any) => { + if (bedAsset?.asset_object?.meta?.asset_type != "CAMERA") { + return false; + } else if (bedAsset?.meta?.type == "boundary") { + boundaryPreset = bedAsset; + return false; + } else if (bedAsset?.meta?.position) { + return true; + } + return false; + }); + if (boundaryPreset) { + setBoundaryPreset(boundaryPreset); + } else { + setBoundaryPreset(null); + } + setPresets(bedAssets); + } + } else { + setPresets([]); + setBoundaryPreset(null); + } + }; + const handleSubmit = async (e: SyntheticEvent) => { e.preventDefault(); if (checkIfValidIP(cameraAddress)) { @@ -91,8 +212,120 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { } }; - const addPreset = async (e: SyntheticEvent) => { - e.preventDefault(); + const addBoundaryPreset = async () => { + const config = getCameraConfig(asset as AssetData); + try { + setLoadingAddBoundaryPreset(true); + + if (bed?.id) { + const presetData = await axios.get( + `https://${resolvedMiddleware?.hostname}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}` + ); + const range = calcBoundary(presets); + const meta = { + type: "boundary", + preset_name: `${bed?.name} boundary`, + bed_id: bed?.id, + error: presetData.data.error, + utcTime: presetData.data.utcTime, + range: range, + }; + + const { res } = await request(routes.createAssetBed, { + body: { + meta: meta, + asset: assetId, + bed: bed?.id as string, + }, + }); + if (res?.status === 201) { + Notification.Success({ + msg: "Boundary Preset Added Successfully", + }); + // setBed({}); + setRefreshPresetsHash(Number(new Date())); + } else { + Notification.Error({ + msg: "Failed to add Boundary Preset", + }); + } + } else { + Notification.Error({ + msg: "Please select a bed to add Boundary Preset", + }); + } + } catch (e) { + console.log(e); + Notification.Error({ + msg: "Something went wrong..!", + }); + } + setLoadingAddBoundaryPreset(false); + }; + + const updateBoundaryPreset = async () => { + if (boundaryPreset && bed?.id) { + try { + if ( + !boundaryPreset?.asset_object?.id || + !boundaryPreset?.bed_object?.id + ) { + Notification.Error({ + msg: "Something went wrong..!", + }); + return; + } + const data = { + asset: boundaryPreset.asset_object.id, + bed: boundaryPreset.bed_object.id, + meta: boundaryPreset.meta, + }; + + const { res } = await request(routes.partialUpdateAssetBed, { + body: data, + pathParams: { external_id: boundaryPreset.id }, + }); + if (res?.status === 200) { + setUpdateBoundaryNotif("updated"); + } else { + setUpdateBoundaryNotif("error"); + Notification.Error({ + msg: "Failed to modify Boundary Preset", + }); + } + } catch (e) { + Notification.Error({ + msg: "Something went wrong..!", + }); + } + } + }; + const deleteBoundaryPreset = async () => { + if (boundaryPreset) { + try { + const { res } = await request(routes.deleteAssetBed, { + pathParams: { external_id: boundaryPreset.id }, + }); + if (res?.status === 204) { + Notification.Success({ + msg: "Boundary Preset Deleted Successfully", + }); + // setBed({}); + setRefreshPresetsHash(Number(new Date())); + } else { + Notification.Error({ + msg: "Failed to delete Boundary Preset", + }); + } + } catch (e) { + Notification.Error({ + msg: "Something went wrong..!", + }); + } + } + }; + + const addPreset = async () => { const config = getCameraConfig(asset as AssetData); const data = { bed_id: bed.id, @@ -115,14 +348,13 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { Notification.Success({ msg: "Preset Added Successfully", }); - setBed({}); - setNewPreset(""); - setRefreshPresetsHash(Number(new Date())); } else { Notification.Error({ msg: "Something went wrong..!", }); } + setNewPreset(""); + setRefreshPresetsHash(Number(new Date())); } catch (e) { Notification.Error({ msg: "Something went wrong..!", @@ -205,17 +437,33 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { )} {assetType === "ONVIF" ? ( - + <> + + + + ) : null} ); diff --git a/src/Components/Assets/configure/CameraBoundaryConfigure.tsx b/src/Components/Assets/configure/CameraBoundaryConfigure.tsx new file mode 100644 index 00000000000..bcaf0f94ff7 --- /dev/null +++ b/src/Components/Assets/configure/CameraBoundaryConfigure.tsx @@ -0,0 +1,282 @@ +import { useState } from "react"; +import { BedModel } from "../../Facility/models"; +import ConfirmDialog from "../../Common/ConfirmDialog"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import { direction } from "../../../Common/constants"; +import * as Notification from "../../../Utils/Notifications.js"; +interface CameraBoundaryConfigureProps { + addBoundaryPreset: () => void; + deleteBoundaryPreset: () => void; + boundaryPreset: any; + bed: BedModel; + toUpdateBoundary: boolean; + setToUpdateBoundary: (toUpdate: boolean) => void; + loadingAddBoundaryPreset: boolean; + toAddPreset: boolean; + setDirection: (direction: direction) => void; + isPreview: boolean; + previewBoundary: () => void; +} + +interface UpdateCameraBoundaryConfigureProps { + direction: direction; + setDirection(direction: direction): void; + setToUpdateBoundary: (toUpdate: boolean) => void; + updateBoundaryInfo: Record; + setUpdateBoundaryInfo: (info: Record) => void; + updateBoundaryNotif: string; + setUpdateBoundaryNotif: (notif: string) => void; +} +export default function CameraBoundaryConfigure( + props: CameraBoundaryConfigureProps +) { + const { + addBoundaryPreset, + deleteBoundaryPreset, + boundaryPreset, + bed, + toUpdateBoundary, + setToUpdateBoundary, + loadingAddBoundaryPreset, + toAddPreset, + setDirection, + isPreview, + previewBoundary, + } = props; + const [toDeleteBoundary, setToDeleteBoundary] = useState(null); + return ( + <> + {toDeleteBoundary && ( + +

+ Boundary preset:{" "} + {toDeleteBoundary.meta.preset_name} +

+

+ Bed: {toDeleteBoundary.bed_object.name} +

+ + } + action="Delete" + variant="danger" + onClose={() => setToDeleteBoundary(null)} + onConfirm={() => { + deleteBoundaryPreset(); + setToDeleteBoundary(null); + }} + /> + )} + + {bed?.id && !boundaryPreset ? ( +
+ +
+ ) : ( + <> + {bed?.id && !toUpdateBoundary && ( +
+
+ +
{`${ + !boundaryPreset + ? bed?.name + : boundaryPreset?.meta?.preset_name + } ${!boundaryPreset ? "boundary" : ""}`}
+
+
+ + + +
+
+ )} + + )} + + ); +} + +export function UpdateCameraBoundaryConfigure( + props: UpdateCameraBoundaryConfigureProps +) { + const { + direction, + setDirection, + setToUpdateBoundary, + updateBoundaryInfo, + setUpdateBoundaryInfo, + updateBoundaryNotif, + setUpdateBoundaryNotif, + } = props; + + const translation: Record = { + left: "Left", + right: "Right", + up: "Top", + down: "Bottom", + }; + + const handlePrevButtonClick = () => { + if (updateBoundaryNotif === "updated") { + Notification.Success({ + msg: `${translation[direction as string]} boundary updated`, + }); + } + setUpdateBoundaryNotif("notUpdated"); + switch (direction) { + case "left": + setToUpdateBoundary(false); + setDirection(null); + setUpdateBoundaryInfo({ + left: false, + right: false, + up: false, + down: false, + }); + break; + + case "right": + setDirection("left"); + break; + + case "up": + setDirection("right"); + break; + + case "down": + setDirection("up"); + break; + + default: + break; + } + }; + + const showUpdateBoundaryInfo = (dir: string, updated: boolean) => { + if (dir == direction) { + return ( +
+ Updating +
+ ); + } + if (updated) { + return ( +
+ Updated +
+ ); + } + return ( +
+ Not updated +
+ ); + }; + + const handleNextButtonClick = () => { + if (updateBoundaryNotif === "updated") { + Notification.Success({ + msg: `${translation[direction as string]} boundary updated`, + }); + } + setUpdateBoundaryNotif("notUpdated"); + switch (direction) { + case "left": + setDirection("right"); + break; + case "right": + setDirection("up"); + break; + case "up": + setDirection("down"); + break; + case "down": + setDirection(null); + setToUpdateBoundary(false); + setUpdateBoundaryInfo({ + left: false, + right: false, + up: false, + down: false, + }); + break; + default: + break; + } + }; + + return ( +
+
+ Update boundary +
+
+ {["left", "right", "up", "down"].map((dir) => { + return ( +
+
{translation[dir]}
+
+ {showUpdateBoundaryInfo(dir, updateBoundaryInfo[dir])} +
+
+ ); + })} +
+
+ + +
+
+ ); +} diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx index c3ba434ef3f..743744acf57 100644 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ b/src/Components/Assets/configure/CameraConfigure.tsx @@ -1,81 +1,55 @@ -import { SyntheticEvent } from "react"; -import { AssetData } from "../AssetTypes"; -import LiveFeed from "../../Facility/Consultations/LiveFeed"; -import { BedSelect } from "../../Common/BedSelect"; -import { BedModel } from "../../Facility/models"; -import { getCameraConfig } from "../../../Utils/transformUtils"; -import { Submit } from "../../Common/components/ButtonV2"; import TextFormField from "../../Form/FormFields/TextFormField"; -import Card from "../../../CAREUI/display/Card"; interface CameraConfigureProps { - asset: AssetData; - addPreset(e: SyntheticEvent): void; - setBed(bed: BedModel): void; - bed: BedModel; + addPreset: () => void; + setToAddPreset: (toAddPreset: boolean) => void; newPreset: string; setNewPreset(preset: string): void; - refreshPresetsHash: number; - facilityMiddlewareHostname: string; isLoading: boolean; } export default function CameraConfigure(props: CameraConfigureProps) { - const { - asset, - addPreset, - setBed, - bed, - isLoading, - newPreset, - setNewPreset, - refreshPresetsHash, - facilityMiddlewareHostname, - } = props; + const { addPreset, newPreset, setNewPreset, isLoading, setToAddPreset } = + props; return ( -
- -
-
-
- - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={asset?.location_object?.id} - facility={asset?.location_object?.facility?.id} - /> -
-
- - setNewPreset(e.value)} - error="" - /> -
-
-
- -
-
-
- - +
+ Add preset +
+
+ + setNewPreset(e.value)} + error="" /> - +
+ + +
+
); } diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts index f3374a1776d..32eaf595f0d 100644 --- a/src/Components/CameraFeed/routes.ts +++ b/src/Components/CameraFeed/routes.ts @@ -5,7 +5,7 @@ export const FeedRoutes = { operateAsset: { path: "/api/v1/asset/{id}/operate_assets/", method: "POST", - TRes: Type(), + TRes: Type(), TBody: Type<{ action: OperationAction }>(), }, } as const; diff --git a/src/Components/Common/BedSelect.tsx b/src/Components/Common/BedSelect.tsx index a548b7a9eb5..eac45e20670 100644 --- a/src/Components/Common/BedSelect.tsx +++ b/src/Components/Common/BedSelect.tsx @@ -62,6 +62,7 @@ export const BedSelect = (props: BedSelectProps) => { return ( import("../../Common/PageTitle")); +import PatientPrivacyToggle from "../../Patient/PatientPrivacyToggle"; +const Page = lazy(() => import("../../Common/components/Page")); export const ConsultationFeedTab = (props: ConsultationTabProps) => { return ( -
- + + } + > -
+ ); }; diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx index 476dcf0f401..beffd6dfc17 100644 --- a/src/Components/Facility/Consultations/Feed.tsx +++ b/src/Components/Facility/Consultations/Feed.tsx @@ -13,6 +13,8 @@ import { } from "../../../Common/hooks/useMSEplayer"; import { PTZState, useFeedPTZ } from "../../../Common/hooks/useFeedPTZ"; import { useEffect, useRef, useState } from "react"; +// import { statusType, useAbortableEffect } from "../../../Common/utils"; +import ButtonV2 from "../../Common/components/ButtonV2.js"; import CareIcon from "../../../CAREUI/icons/CareIcon.js"; import FeedButton from "./FeedButton"; @@ -23,6 +25,8 @@ import { useDispatch } from "react-redux"; import { useHLSPLayer } from "../../../Common/hooks/useHLSPlayer"; import useKeyboardShortcut from "use-keyboard-shortcut"; import useFullscreen from "../../../Common/hooks/useFullscreen.js"; +import { useMessageListener } from "../../../Common/hooks/useMessageListener.js"; +import useNotificationSubscribe from "../../../Common/hooks/useNotificationSubscribe.js"; import { triggerGoal } from "../../../Integrations/Plausible.js"; import useAuthUser from "../../../Common/hooks/useAuthUser.js"; import Spinner from "../../Common/Spinner.js"; @@ -34,10 +38,19 @@ interface IFeedProps { consultationId: any; } +interface cameraOccupier { + username?: string; + firstName?: string; + lastName?: string; + role?: string; + homeFacility?: string; +} + const PATIENT_DEFAULT_PRESET = "Patient View".trim().toLowerCase(); -export const Feed: React.FC = ({ consultationId }) => { +export const Feed: React.FC = ({ facilityId, consultationId }) => { const dispatch: any = useDispatch(); + const CAMERA_ACCESS_TIMEOUT = 10 * 60; //seconds const videoWrapper = useRef(null); @@ -53,13 +66,173 @@ export const Feed: React.FC = ({ consultationId }) => { const [bed, setBed] = useState(); const [precision, setPrecision] = useState(1); const [cameraState, setCameraState] = useState(null); + const [boundaryPreset, setBoundaryPreset] = useState(); const [isFullscreen, setFullscreen] = useFullscreen(); + + // Information about subscription and camera occupier in case asset is not occupied by the current user + const [showSubscriptionInfo, setShowSubscriptionInfo] = useState(false); + const [showCameraOccupierInfo, setShowCameraOccupierInfo] = useState(false); + const [cameraOccupier, setCameraOccupier] = useState({}); + const [timeoutSeconds, setTimeoutSeconds] = useState(CAMERA_ACCESS_TIMEOUT); + const [isRequestingAccess, setIsRequestingAccess] = useState(false); + + const [borderAlert, setBorderAlert] = useState(null); + const [privacy, setPrivacy] = useState(false); + const [privacyLockedBy, setPrivacyLockedBy] = useState(""); const [videoStartTime, setVideoStartTime] = useState(null); const [statusReported, setStatusReported] = useState(false); const [resolvedMiddleware, setResolvedMiddleware] = useState(); const authUser = useAuthUser(); + // Notification hook to get subscription info + const { + subscriptionStatus, + isSubscribing, + intialSubscriptionState, + subscribe, + } = useNotificationSubscribe(); + + useEffect(() => { + intialSubscriptionState(); + }, [dispatch, subscriptionStatus]); + + // display subscription info + const subscriptionInfo = () => { + return ( +
{ + setShowSubscriptionInfo(false); + }} + > + {showSubscriptionInfo && ( +
+
+ {subscriptionStatus != "SubscribedOnThisDevice" + ? "Subscribe to get real time information about camera access" + : "You are subscribed, and will get real time information about camera access"} +
+ {subscriptionStatus != "SubscribedOnThisDevice" && ( + + {isSubscribing && } + + Subscribe + + )} +
+ )} +
{ + setShowSubscriptionInfo(true); + }} + > + +
+
+ ); + }; + + //display current cameraoccupier info incase the asset is not occupied by the current user + const currentCameraOccupierInfo = () => { + return ( +
{ + setShowCameraOccupierInfo(false); + }} + > + {showCameraOccupierInfo && ( +
+
+ Camera is being used by... +
+ +
+
{`${cameraOccupier.firstName} ${cameraOccupier.lastName}-`}
+
{`${cameraOccupier.role}`}
+
+ {cameraOccupier.homeFacility && ( +
{`${cameraOccupier.homeFacility}`}
+ )} + { + setIsRequestingAccess(true); + requestAccess({ + onSuccess: () => { + Notification.Success({ msg: "Request sent" }); + setIsRequestingAccess(false); + }, + onError: () => { + Notification.Error({ msg: "Request failed" }); + setIsRequestingAccess(false); + }, + }); + }} + ghost + variant="secondary" + size="small" + border + > + {isRequestingAccess && } + Request Access + +
+ )} +
{ + setShowCameraOccupierInfo(true); + }} + > +
+ {cameraOccupier?.firstName?.[0] ? ( + cameraOccupier?.firstName?.[0].toUpperCase() + ) : ( + + )} +
+
+
+ ); + }; + + useEffect(() => { + const fetchFacility = async () => { + const { data, res } = await request(routes.getPermittedFacility, { + pathParams: { id: facilityId }, + }); + + if (res?.ok && data) { + setResolvedMiddleware({ + hostname: data?.middleware_address ?? "", + source: "asset", + }); + // setFacilityMiddlewareHostname(res.data.middleware_address); + // useQuery(routes.getPermittedFacilities, { + // pathParams: { id: facilityId || "" }, + // onResponse: ({ res, data }) => { + // if (res && res.status === 200 && data && data.middleware_address) { + // setFacilityMiddlewareHostname(data.middleware_address); + } + }; + + fetchFacility(); + }, []); + + // const fallbackMiddleware = + // cameraAsset.location_middleware || resolvedMiddleware; + + // const currentMiddleware = + // cameraAsset.middleware_address || fallbackMiddleware; + useEffect(() => { if (cameraState) { setCameraState({ @@ -86,29 +259,33 @@ export const Feed: React.FC = ({ consultationId }) => { const { loading: getConsultationLoading } = useQuery(routes.getConsultation, { pathParams: { id: consultationId }, - onResponse: ({ res, data }) => { + onResponse: async ({ res, data }) => { if (res && res.status === 200 && data) { const consultationBedId = data.current_bed?.bed_object?.id; if (consultationBedId) { - (async () => { - const { res: listAssetBedsRes, data: listAssetBedsData } = - await request(routes.listAssetBeds, { - query: { - bed: consultationBedId, - }, - }); - setBed(consultationBedId); - const bedAssets: any = { - ...listAssetBedsRes, - data: { - ...listAssetBedsData, - results: listAssetBedsData?.results.filter((asset) => { - return asset?.asset_object?.meta?.asset_type === "CAMERA"; - }), + const { res: listAssetBedsRes, data: listAssetBedsData } = + await request(routes.listAssetBeds, { + query: { + bed: consultationBedId, }, - }; + }); + setBed(consultationBedId); - if (bedAssets?.data?.results?.length) { + const bedAssets: any = { + ...listAssetBedsRes, + data: { + ...listAssetBedsData, + results: listAssetBedsData?.results.filter((asset) => { + return asset?.asset_object?.meta?.asset_type === "CAMERA"; + }), + }, + }; + + if (bedAssets?.data?.results?.length) { + bedAssets.data.results = bedAssets.data.results.filter( + (bedAsset: any) => bedAsset.meta.type !== "boundary" + ); + if (bedAssets.data?.results?.length) { const { camera_access_key } = bedAssets.data.results[0].asset_object.meta; const config = camera_access_key.split(":"); @@ -122,16 +299,17 @@ export const Feed: React.FC = ({ consultationId }) => { bedAssets.data.results[0].asset_object.location_object ?.middleware_address, }); - setResolvedMiddleware( - bedAssets.data.results[0].asset_object.resolved_middleware - ); setCameraConfig(bedAssets.data.results[0].meta); setCameraState({ ...bedAssets.data.results[0].meta.position, precision: 1, }); } - })(); + } + if (data?.current_bed?.privacy) { + setPrivacy(data?.current_bed?.privacy); + setPrivacyLockedBy(data?.current_bed?.meta?.locked_by); + } } } }, @@ -187,6 +365,9 @@ export const Feed: React.FC = ({ consultationId }) => { getPTZPayload, getPresets, relativeMove, + lockAsset, + unlockAsset, + requestAccess, } = useFeedPTZ({ config: cameraAsset, dispatch, @@ -204,10 +385,24 @@ export const Feed: React.FC = ({ consultationId }) => { const getBedPresets = async (asset: any) => { if (asset.id && bed) { - const { data: bedAssets } = await request(routes.listAssetBeds, { + const { data } = await request(routes.listAssetBeds, { query: { asset: asset.id, bed }, }); - setBedPresets(bedAssets?.results); + if (data?.results?.length) { + data.results = data.results.filter((bedAsset: any) => { + if (bedAsset.meta.type === "boundary") { + setBoundaryPreset(bedAsset); + return false; + } else { + return true; + } + }); + } + setBedPresets(data?.results); + // const { data: bedAssets } = await request(routes.listAssetBeds, { + // query: { asset: asset.id, bed }, + // }); + // setBedPresets(bedAssets?.results); } }; @@ -234,8 +429,19 @@ export const Feed: React.FC = ({ consultationId }) => { startStreamFeed(); }, 1000); getPresets({ - onSuccess: (resp) => setPresets(resp), - onError: (_) => { + onSuccess: (resp) => { + setPresets(resp); + setCameraOccupier({}); + }, + onError: (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + Notification.Error({ + msg: `Camera is being used by ${cameraOccupier?.firstName} ${cameraOccupier?.lastName}`, + }); + } else { + setCameraOccupier({}); + } Notification.Error({ msg: "Fetching presets failed", }); @@ -245,8 +451,108 @@ export const Feed: React.FC = ({ consultationId }) => { } }, [cameraAsset, resolvedMiddleware?.hostname]); + //lock and unlock asset on mount and unmount + useEffect(() => { + if (cameraAsset.id) { + lockAsset({ + onError: async (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + } + }, + onSuccess() { + setCameraOccupier({}); + }, + }); + } + + window.addEventListener("beforeunload", () => { + if (cameraAsset.id) { + unlockAsset({}); + } + }); + + return () => { + if (cameraAsset.id) { + unlockAsset({}); + } + window.removeEventListener("beforeunload", () => { + if (cameraAsset.id) { + unlockAsset({}); + } + }); + }; + }, [cameraAsset.id]); + + //count down from CAMERA_ACCESS_TIMEOUT when mouse is idle to unlock asset after timeout + useEffect(() => { + const interval = setInterval(() => { + setTimeoutSeconds((prevSeconds) => prevSeconds - 1); + }, 1000); + + const resetTimer = () => { + setTimeoutSeconds(CAMERA_ACCESS_TIMEOUT); + }; + + document.addEventListener("mousemove", resetTimer); + + if (cameraOccupier.username) { + clearInterval(interval); + setTimeoutSeconds(CAMERA_ACCESS_TIMEOUT); + removeEventListener("mousemove", resetTimer); + } + + return () => { + clearInterval(interval); + document.removeEventListener("mousemove", resetTimer); + }; + }, [cameraOccupier]); + + //unlock asset after timeout useEffect(() => { - let tId: any; + if (timeoutSeconds === 0) { + unlockAsset({}); + setTimeoutSeconds(CAMERA_ACCESS_TIMEOUT); + setTimeout(() => { + lockAsset({ + onError: async (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + } + }, + onSuccess() { + setCameraOccupier({}); + }, + }); + }, 2000); + } + }, [timeoutSeconds]); + + //Listen to push notifications for- + //1) camera access request + //2) camera access granted + useMessageListener((data) => { + if (data?.status == "success" && data?.asset_id === cameraAsset?.id) { + lockAsset({ + onError: async (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + } + }, + onSuccess: () => { + setCameraOccupier({}); + setTimeoutSeconds(CAMERA_ACCESS_TIMEOUT); + }, + }); + } else if (data.status == "request") { + Notification.Warn({ + msg: `${data?.firstName} ${data?.lastName} is requesting access to the camera`, + }); + } + }); + + useEffect(() => { + let tId: NodeJS.Timeout; if (streamStatus !== StreamStatus.Playing) { if (streamStatus !== StreamStatus.Offline) { setStreamStatus(StreamStatus.Loading); @@ -266,7 +572,7 @@ export const Feed: React.FC = ({ consultationId }) => { return () => { clearTimeout(tId); }; - }, [startStream, streamStatus]); + }, [startStream, streamStatus, authUser.id, consultationId, statusReported]); useEffect(() => { if (!currentPreset && streamStatus === StreamStatus.Playing) { @@ -283,11 +589,20 @@ export const Feed: React.FC = ({ consultationId }) => { onSuccess: () => { setLoading(CAMERA_STATES.IDLE); setCurrentPreset(preset); + setCameraOccupier({}); }, onError: (err: Record) => { + if (err.status === 409) { + setCameraOccupier(err.data as cameraOccupier); + Notification.Error({ + msg: `Camera is being used by ${cameraOccupier?.firstName} ${cameraOccupier?.lastName}`, + }); + } else { + setCameraOccupier({}); + } setLoading(CAMERA_STATES.IDLE); const responseData = err.data.result; - if (responseData.status) { + if (responseData?.status) { switch (responseData.status) { case "error": if (responseData.error.code === "EHOSTUNREACH") { @@ -315,6 +630,13 @@ export const Feed: React.FC = ({ consultationId }) => { } }, [bedPresets, streamStatus]); + const borderFlash: (dir: any) => void = (dir: any) => { + setBorderAlert(dir); + setTimeout(() => { + setBorderAlert(null); + }, 3000); + }; + const cameraPTZActionCBs: { [key: string]: (option: any, value?: any) => void; } = { @@ -349,6 +671,7 @@ export const Feed: React.FC = ({ consultationId }) => { updatePreset: (option) => { getCameraStatus({ onSuccess: async (data) => { + setCameraOccupier({}); if (currentPreset?.asset_object?.id && data?.position) { setLoading(option.loadingLabel); const { res, data: assetBedData } = await request( @@ -367,19 +690,138 @@ export const Feed: React.FC = ({ consultationId }) => { ); if (res && assetBedData && res.status === 200) { Notification.Success({ msg: "Preset Updated" }); - await getBedPresets(cameraAsset?.id); - getPresets({}); + getBedPresets(cameraAsset?.id); + getPresets({ + onSuccess: () => { + setCameraOccupier({}); + }, + onError: (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + Notification.Error({ + msg: `Camera is being used by ${cameraOccupier?.firstName} ${cameraOccupier?.lastName}`, + }); + } else { + setCameraOccupier({}); + } + }, + }); + // await getBedPresets(cameraAsset?.id); + // getPresets({}); } setLoading(CAMERA_STATES.IDLE); } }, + onError: (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + Notification.Error({ + msg: `Camera is being used by ${cameraOccupier?.firstName} ${cameraOccupier?.lastName}`, + }); + } else { + setCameraOccupier({}); + } + }, }); }, other: (option, value) => { setLoading(option.loadingLabel); - relativeMove(getPTZPayload(option.action, precision, value), { - onSuccess: () => setLoading(CAMERA_STATES.IDLE), + let payLoad = getPTZPayload(option.action, precision, value); + if (boundaryPreset?.meta?.range && cameraState) { + const range = boundaryPreset.meta.range; + if (option.action == "up" && cameraState.y + payLoad.y > range.max_y) { + borderFlash("top"); + setLoading(CAMERA_STATES.IDLE); + return; + } else if ( + option.action == "down" && + cameraState.y + payLoad.y < range.min_y + ) { + borderFlash("bottom"); + setLoading(CAMERA_STATES.IDLE); + return; + } else if ( + option.action == "left" && + cameraState.x + payLoad.x < range.min_x + ) { + borderFlash("left"); + setLoading(CAMERA_STATES.IDLE); + return; + } else if ( + option.action == "right" && + cameraState.x + payLoad.x > range.max_x + ) { + borderFlash("right"); + setLoading(CAMERA_STATES.IDLE); + return; + } else if ( + option.action == "zoomOut" && + cameraState.zoom + payLoad.zoom < 0 + ) { + Notification.Error({ msg: "Cannot zoom out" }); + setLoading(CAMERA_STATES.IDLE); + return; + } + } + //insert boundaryPreset.id in payload + if (boundaryPreset?.id) { + payLoad = { + ...payLoad, + id: boundaryPreset.id, + camera_state: cameraState, + }; + } + + relativeMove(payLoad, { + onSuccess: () => { + setLoading(CAMERA_STATES.IDLE); + setCameraOccupier({}); + }, + onError: async (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + Notification.Error({ + msg: `Camera is being used by ${cameraOccupier?.firstName} ${cameraOccupier?.lastName}`, + }); + } else { + setCameraOccupier({}); + } + setLoading(CAMERA_STATES.IDLE); + }, }); + if (cameraState) { + let x = cameraState.x; + let y = cameraState.y; + let zoom = cameraState.zoom; + switch (option.action) { + case "left": + x += -0.1 / cameraState.precision; + break; + + case "right": + x += 0.1 / cameraState.precision; + break; + + case "down": + y += -0.1 / cameraState.precision; + break; + + case "up": + y += 0.1 / cameraState.precision; + break; + + case "zoomIn": + zoom += 0.1 / cameraState.precision; + break; + case "zoomOut": + zoom += -0.1 / cameraState.precision; + break; + default: + break; + } + + setCameraState({ ...cameraState, x: x, y: y, zoom: zoom }); + } }, }; @@ -399,6 +841,22 @@ export const Feed: React.FC = ({ consultationId }) => { useKeyboardShortcut(option.shortcutKey, option.callback); } + const PrivacyOnCard = () => { + return ( +
+ +
+ Feed is unavailable due to privacy mode +
+
+ ); + }; + + if (privacy && privacyLockedBy !== authUser.username) { + return ; + } + + // if (isLoading) return ; if (getConsultationLoading) return ; return ( @@ -416,6 +874,7 @@ export const Feed: React.FC = ({ consultationId }) => { absoluteMove(preset.meta.position, { onSuccess: () => { setLoading(CAMERA_STATES.IDLE); + setCameraOccupier({}); setCurrentPreset(preset); console.log( "onSuccess: Set Preset to " + preset?.meta?.preset_name @@ -427,7 +886,15 @@ export const Feed: React.FC = ({ consultationId }) => { result: "success", }); }, - onError: () => { + onError: async (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + Notification.Error({ + msg: `Camera is being used by ${cameraOccupier?.firstName} ${cameraOccupier?.lastName}`, + }); + } else { + setCameraOccupier({}); + } setLoading(CAMERA_STATES.IDLE); setCurrentPreset(preset); console.log( @@ -441,7 +908,21 @@ export const Feed: React.FC = ({ consultationId }) => { }); }, }); - getCameraStatus({}); + getCameraStatus({ + onSuccess: () => { + setCameraOccupier({}); + }, + onError: (resp) => { + if (resp.status === 409) { + setCameraOccupier(resp.data as cameraOccupier); + Notification.Error({ + msg: `Camera is being used by ${cameraOccupier?.firstName} ${cameraOccupier?.lastName}`, + }); + } else { + setCameraOccupier({}); + } + }, + }); }} className={classNames( "block border border-gray-500 px-4 py-2 first:rounded-l last:rounded-r", @@ -455,196 +936,208 @@ export const Feed: React.FC = ({ consultationId }) => { ))} +
+ {cameraOccupier?.username && currentCameraOccupierInfo()} + {subscriptionInfo()} +
- {isIOS ? ( - { - setVideoStartTime(() => new Date()); - }} - width="100%" - height="100%" - onBuffer={() => { - const delay = calculateVideoLiveDelay(); - if (delay > 5) { - setStreamStatus(StreamStatus.Loading); - } - }} - onError={(e: any, _: any, hlsInstance: any) => { - if (e === "hlsError") { - const recovered = hlsInstance.recoverMediaError(); - console.log(recovered); - } - }} - onEnded={() => { - setStreamStatus(StreamStatus.Stop); - }} - /> - ) : ( -
diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 58386bef3a4..86d59205693 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -166,6 +166,11 @@ export interface ConsultationModel { investigation?: InvestigationType[]; } +export interface PatientPrivacyModel { + status: string; + detail: string; +} + export interface PatientStatsModel { id?: string; entryDate?: string; @@ -229,6 +234,7 @@ export interface BedModel { }; location?: string; is_occupied?: boolean; + meta?: object; created_date?: string; modified_date?: string; } @@ -243,6 +249,7 @@ export interface CurrentBed { modified_date: string; start_date: string; end_date: string; + privacy?: boolean; meta: Record; } diff --git a/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx b/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx index d0dae7ddff5..dc45ed68a81 100644 --- a/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx +++ b/src/Components/Form/FormFields/NumericWithUnitsFormField.tsx @@ -35,9 +35,7 @@ export default function NumericWithUnitsFormField(props: Props) { autoComplete={props.autoComplete} required={field.required} value={numValue} - onChange={(e) => - field.handleChange(Number(e.target.value) + " " + unitValue) - } + onChange={(e) => field.handleChange(e.target.value + " " + unitValue)} />