From b373a0d22e7d686a4f02f9cfdc9a5f77f0f69569 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 18 Oct 2024 19:00:22 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=B7=20Support=20for=20linking=20multip?= =?UTF-8?q?le=20cameras=20to=20a=20bed=20(#8253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress/pageobject/Asset/AssetCreation.ts | 2 +- src/CAREUI/interactive/KeyboardShortcut.tsx | 12 +- src/Common/constants.tsx | 14 - src/Components/Assets/AssetConfigure.tsx | 19 +- .../Assets/AssetType/HL7Monitor.tsx | 83 ++- .../Assets/AssetType/ONVIFCamera.tsx | 227 ------ src/Components/Assets/AssetTypes.tsx | 3 + .../Assets/configure/CameraConfigure.tsx | 88 --- .../Assets/configure/MonitorConfigure.tsx | 87 --- src/Components/CameraFeed/CameraFeed.tsx | 29 +- src/Components/CameraFeed/CameraFeedOld.tsx | 685 ------------------ .../CameraFeed/CameraFeedWithBedPresets.tsx | 21 +- ...etBedSelect.tsx => CameraPresetSelect.tsx} | 24 +- src/Components/CameraFeed/ConfigureCamera.tsx | 643 ++++++++++++++++ src/Components/CameraFeed/FeedAlert.tsx | 4 +- src/Components/CameraFeed/FeedControls.tsx | 2 - src/Components/CameraFeed/routes.ts | 49 ++ src/Components/CameraFeed/useFeedPTZ.ts | 208 ------ src/Components/CameraFeed/useOperateCamera.ts | 6 +- src/Components/CameraFeed/utils.ts | 2 +- src/Components/Common/MonitorAssetPopover.tsx | 2 +- .../ConsultationFeedTab.tsx | 91 +-- .../Facility/ConsultationDetails/index.tsx | 39 +- src/Components/Facility/FacilityUsers.tsx | 6 +- .../Form/FormFields/TextFormField.tsx | 23 + src/Locale/en.json | 36 +- src/Locale/hi.json | 4 +- src/Locale/kn.json | 4 +- src/Locale/ml.json | 4 +- src/Locale/ta.json | 4 +- src/Redux/actions.tsx | 27 - src/Utils/transformUtils.ts | 17 +- 32 files changed, 975 insertions(+), 1490 deletions(-) delete mode 100644 src/Components/Assets/AssetType/ONVIFCamera.tsx delete mode 100644 src/Components/Assets/configure/CameraConfigure.tsx delete mode 100644 src/Components/Assets/configure/MonitorConfigure.tsx delete mode 100644 src/Components/CameraFeed/CameraFeedOld.tsx rename src/Components/CameraFeed/{AssetBedSelect.tsx => CameraPresetSelect.tsx} (88%) create mode 100644 src/Components/CameraFeed/ConfigureCamera.tsx delete mode 100644 src/Components/CameraFeed/useFeedPTZ.ts diff --git a/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts index d6991e82ff0..f0b2f1b74eb 100644 --- a/cypress/pageobject/Asset/AssetCreation.ts +++ b/cypress/pageobject/Asset/AssetCreation.ts @@ -154,7 +154,7 @@ export class AssetPage { } clickConfigureAsset() { - cy.get("#submit").contains("Set Configuration").click(); + cy.get("#submit").contains("Update").click(); } clickConfigureVital() { diff --git a/src/CAREUI/interactive/KeyboardShortcut.tsx b/src/CAREUI/interactive/KeyboardShortcut.tsx index 1d2bebeb316..4ad46c99387 100644 --- a/src/CAREUI/interactive/KeyboardShortcut.tsx +++ b/src/CAREUI/interactive/KeyboardShortcut.tsx @@ -31,7 +31,10 @@ export default function KeyboardShortcut(props: Props) { )} {(props.altShortcuts || [props.shortcut]).map((shortcut, idx, arr) => ( <> - + {shortcut.map((key, idx, keys) => ( <> {SHORTCUT_KEY_MAP[key] || key} @@ -42,7 +45,12 @@ export default function KeyboardShortcut(props: Props) { ))} {idx !== arr.length - 1 && ( - or + + or + )} ))} diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index d46473bfd2d..08d21dc1edc 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -839,20 +839,6 @@ export const LOCATION_BED_TYPES = [ { id: "REGULAR", name: "Regular" }, ] as const; -export const ASSET_META_TYPE = [ - { id: "CAMERA", text: "Camera(ONVIF)" }, - { id: "HL7MONITOR", text: "Vitals Monitor(HL7)" }, -]; - -export const CAMERA_TYPE = [ - { id: "HIKVISION", text: "ONVIF Camera (HIKVISION)" }, -]; - -export const GENDER: { [key: number]: string } = GENDER_TYPES.reduce( - (acc, curr) => ({ ...acc, [curr.id]: curr.text }), - {}, -); - export type CameraPTZ = { icon?: IconName; label: string; diff --git a/src/Components/Assets/AssetConfigure.tsx b/src/Components/Assets/AssetConfigure.tsx index aa6b7c9221e..9f5f73f296d 100644 --- a/src/Components/Assets/AssetConfigure.tsx +++ b/src/Components/Assets/AssetConfigure.tsx @@ -1,6 +1,6 @@ import Loading from "../Common/Loading"; import HL7Monitor from "./AssetType/HL7Monitor"; -import ONVIFCamera from "./AssetType/ONVIFCamera"; +import ConfigureCamera from "../CameraFeed/ConfigureCamera"; import Page from "../Common/components/Page"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; @@ -11,13 +11,11 @@ interface AssetConfigureProps { } const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => { - const { - data: asset, - loading, - refetch, - } = useQuery(routes.getAsset, { pathParams: { external_id: assetId } }); + const { data: asset, refetch } = useQuery(routes.getAsset, { + pathParams: { external_id: assetId }, + }); - if (loading || !asset) { + if (!asset) { return ; } @@ -63,12 +61,7 @@ const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => { }} backUrl={`/facility/${facilityId}/assets/${assetId}`} > - refetch()} - /> + refetch()} /> ); }; diff --git a/src/Components/Assets/AssetType/HL7Monitor.tsx b/src/Components/Assets/AssetType/HL7Monitor.tsx index 6583157018d..383af38fa28 100644 --- a/src/Components/Assets/AssetType/HL7Monitor.tsx +++ b/src/Components/Assets/AssetType/HL7Monitor.tsx @@ -1,7 +1,6 @@ import { SyntheticEvent, useEffect, useState } from "react"; -import { AssetData, ResolvedMiddleware } from "../AssetTypes"; +import { AssetClass, AssetData, ResolvedMiddleware } from "../AssetTypes"; import * as Notification from "../../../Utils/Notifications.js"; -import MonitorConfigure from "../configure/MonitorConfigure"; import Loading from "../../Common/Loading"; import { checkIfValidIP } from "../../../Common/validation"; import Card from "../../../CAREUI/display/Card"; @@ -13,6 +12,10 @@ import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatien import useAuthUser from "../../../Common/hooks/useAuthUser"; import request from "../../../Utils/request/request"; import routes from "../../../Redux/api"; +import { BedModel } from "../../Facility/models"; +import useQuery from "../../../Utils/request/useQuery"; +import { FieldLabel } from "../../Form/FormFields/FormField"; +import { BedSelect } from "../../Common/BedSelect"; interface HL7MonitorProps { assetId: string; @@ -151,3 +154,79 @@ const HL7Monitor = (props: HL7MonitorProps) => { ); }; export default HL7Monitor; + +const saveLink = async (assetId: string, bedId: string) => { + await request(routes.createAssetBed, { + body: { + asset: assetId, + bed: bedId, + }, + }); + Notification.Success({ msg: "AssetBed Link created successfully" }); +}; +const updateLink = async ( + assetbedId: string, + assetId: string, + bed: BedModel, +) => { + await request(routes.partialUpdateAssetBed, { + pathParams: { external_id: assetbedId }, + body: { + asset: assetId, + bed: bed.id ?? "", + }, + }); + Notification.Success({ msg: "AssetBed Link updated successfully" }); +}; + +function MonitorConfigure({ asset }: { asset: AssetData }) { + const [bed, setBed] = useState({}); + const [shouldUpdateLink, setShouldUpdateLink] = useState(false); + const { data: assetBed } = useQuery(routes.listAssetBeds, { + query: { asset: asset.id }, + onResponse: ({ res, data }) => { + if (res?.status === 200 && data && data.results.length > 0) { + setBed(data.results[0].bed_object); + setShouldUpdateLink(true); + } + }, + }); + + return ( +
{ + e.preventDefault(); + if (shouldUpdateLink) { + updateLink( + assetBed?.results[0].id as string, + asset.id as string, + bed as BedModel, + ); + } else { + saveLink(asset.id as string, bed?.id as string); + } + }} + > +
+
+ Bed + setBed(selected as BedModel)} + selected={bed} + error="" + multiple={false} + location={asset?.location_object?.id} + facility={asset?.location_object?.facility?.id} + not_occupied_by_asset_type={AssetClass.HL7MONITOR} + className="w-full" + /> +
+ + + {shouldUpdateLink ? "Update Bed" : "Save Bed"} + +
+
+ ); +} diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx deleted file mode 100644 index f79894d089f..00000000000 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { useEffect, useState } from "react"; -import { AssetData, ResolvedMiddleware } from "../AssetTypes"; -import * as Notification from "../../../Utils/Notifications.js"; -import { BedModel } from "../../Facility/models"; -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 useAuthUser from "../../../Common/hooks/useAuthUser"; - -import request from "../../../Utils/request/request"; -import routes from "../../../Redux/api"; -import useQuery from "../../../Utils/request/useQuery"; - -import CareIcon from "../../../CAREUI/icons/CareIcon"; -import useOperateCamera, { - PTZPayload, -} from "../../CameraFeed/useOperateCamera"; - -interface Props { - assetId: string; - facilityId: string; - asset: any; - onUpdated?: () => void; -} - -const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { - const [isLoading, setIsLoading] = useState(true); - const [assetType, setAssetType] = useState(""); - const [middlewareHostname, setMiddlewareHostname] = useState(""); - const [resolvedMiddleware, setResolvedMiddleware] = - useState(); - const [cameraAddress, setCameraAddress] = useState(""); - const [ipadrdress_error, setIpAddress_error] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [streamUuid, setStreamUuid] = useState(""); - const [bed, setBed] = useState({}); - const [newPreset, setNewPreset] = useState(""); - const [loadingAddPreset, setLoadingAddPreset] = useState(false); - const [loadingSetConfiguration, setLoadingSetConfiguration] = useState(false); - const [refreshPresetsHash, setRefreshPresetsHash] = useState( - Number(new Date()), - ); - const { data: facility, loading } = useQuery(routes.getPermittedFacility, { - pathParams: { id: facilityId }, - }); - const authUser = useAuthUser(); - - const { operate } = useOperateCamera(assetId ?? "", true); - - useEffect(() => { - if (asset) { - setAssetType(asset?.asset_class); - setResolvedMiddleware(asset?.resolved_middleware); - const cameraConfig = getCameraConfig(asset); - setMiddlewareHostname(cameraConfig.middleware_hostname); - setCameraAddress(cameraConfig.hostname); - setUsername(cameraConfig.username); - setPassword(cameraConfig.password); - setStreamUuid(cameraConfig.accessKey); - } - setIsLoading(false); - }, [asset]); - - const handleSubmit = async (e: SyntheticEvent) => { - e.preventDefault(); - if (checkIfValidIP(cameraAddress)) { - setLoadingSetConfiguration(true); - setIpAddress_error(""); - const data = { - meta: { - asset_type: "CAMERA", - middleware_hostname: middlewareHostname, - local_ip_address: cameraAddress, - camera_access_key: `${username}:${password}:${streamUuid}`, - }, - }; - const { res } = await request(routes.partialUpdateAsset, { - pathParams: { external_id: assetId }, - body: data, - }); - if (res?.status === 200) { - Notification.Success({ msg: "Asset Configured Successfully" }); - onUpdated?.(); - } else { - Notification.Error({ msg: "Something went wrong!" }); - } - setLoadingSetConfiguration(false); - } else { - setIpAddress_error("IP address is invalid"); - } - }; - - const addPreset = async (e: SyntheticEvent) => { - e.preventDefault(); - const meta = { - bed_id: bed.id, - preset_name: newPreset, - }; - try { - setLoadingAddPreset(true); - - const { data } = await operate({ type: "get_status" }); - const { position } = (data as { result: { position: PTZPayload } }) - .result; - - const { res } = await request(routes.createAssetBed, { - body: { - meta: { ...meta, position }, - asset: assetId, - bed: bed?.id as string, - }, - }); - if (res?.status === 201) { - Notification.Success({ - msg: "Preset Added Successfully", - }); - setBed({}); - setNewPreset(""); - setRefreshPresetsHash(Number(new Date())); - } else { - Notification.Error({ - msg: "Something went wrong..!", - }); - } - } catch (e) { - Notification.Error({ - msg: "Something went wrong..!", - }); - } - setLoadingAddPreset(false); - }; - if (isLoading || loading || !facility) return ; - - return ( -
- {["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && ( -
-
- -

Middleware Hostname

- {resolvedMiddleware?.source != "asset" && ( -
- - - Middleware hostname sourced from asset{" "} - {resolvedMiddleware?.source} - -
- )} -
- } - placeholder={resolvedMiddleware?.hostname} - value={middlewareHostname} - onChange={({ value }) => setMiddlewareHostname(value)} - /> - setCameraAddress(value)} - error={ipadrdress_error} - /> - setUsername(value)} - /> - setPassword(value)} - /> - setStreamUuid(value)} - /> -
-
- -
- - )} - - {assetType === "ONVIF" ? ( - - ) : null} - - ); -}; -export default ONVIFCamera; diff --git a/src/Components/Assets/AssetTypes.tsx b/src/Components/Assets/AssetTypes.tsx index e3dee31bb7f..2f8e086a813 100644 --- a/src/Components/Assets/AssetTypes.tsx +++ b/src/Components/Assets/AssetTypes.tsx @@ -109,6 +109,9 @@ export interface AssetData { latest_status: string; last_service: AssetService; meta?: { + middleware_hostname?: string; + local_ip_address?: string; + camera_access_key?: string; [key: string]: any; }; } diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx deleted file mode 100644 index e5e017db8dc..00000000000 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { SyntheticEvent } from "react"; -import { AssetData } from "../AssetTypes"; -import CameraFeedOld from "../../CameraFeed/CameraFeedOld"; -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"; -import { FieldErrorText } from "../../Form/FormFields/FormField"; - -interface CameraConfigureProps { - asset: AssetData; - addPreset(e: SyntheticEvent): void; - setBed(bed: BedModel): void; - bed: BedModel; - 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; - - return ( -
- -
-
-
- - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={asset?.location_object?.id} - facility={asset?.location_object?.facility?.id} - /> -
-
- - setNewPreset(e.value)} - errorClassName="hidden" - /> - {newPreset.length > 12 && ( - - )} -
-
-
- -
-
-
- - - -
- ); -} diff --git a/src/Components/Assets/configure/MonitorConfigure.tsx b/src/Components/Assets/configure/MonitorConfigure.tsx deleted file mode 100644 index 785b82873de..00000000000 --- a/src/Components/Assets/configure/MonitorConfigure.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useState } from "react"; -import { BedSelect } from "../../Common/BedSelect"; -import { BedModel } from "../../Facility/models"; -import { AssetClass, AssetData } from "../AssetTypes"; -import * as Notification from "../../../Utils/Notifications.js"; -import { Submit } from "../../Common/components/ButtonV2"; -import { FieldLabel } from "../../Form/FormFields/FormField"; -import request from "../../../Utils/request/request"; -import routes from "../../../Redux/api"; -import useQuery from "../../../Utils/request/useQuery"; -import CareIcon from "../../../CAREUI/icons/CareIcon"; - -const saveLink = async (assetId: string, bedId: string) => { - await request(routes.createAssetBed, { - body: { - asset: assetId, - bed: bedId, - }, - }); - Notification.Success({ msg: "AssetBed Link created successfully" }); -}; -const update_Link = async ( - assetbedId: string, - assetId: string, - bed: BedModel, -) => { - await request(routes.partialUpdateAssetBed, { - pathParams: { external_id: assetbedId }, - body: { - asset: assetId, - bed: bed.id ?? "", - }, - }); - Notification.Success({ msg: "AssetBed Link updated successfully" }); -}; - -export default function MonitorConfigure({ asset }: { asset: AssetData }) { - const [bed, setBed] = useState({}); - const [updateLink, setUpdateLink] = useState(false); - const { data: assetBed } = useQuery(routes.listAssetBeds, { - query: { asset: asset.id }, - onResponse: ({ res, data }) => { - if (res?.status === 200 && data && data.results.length > 0) { - setBed(data.results[0].bed_object); - setUpdateLink(true); - } - }, - }); - - return ( -
{ - e.preventDefault(); - if (updateLink) { - update_Link( - assetBed?.results[0].id as string, - asset.id as string, - bed as BedModel, - ); - } else { - saveLink(asset.id as string, bed?.id as string); - } - }} - > -
-
- Bed - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={asset?.location_object?.id} - facility={asset?.location_object?.facility?.id} - not_occupied_by_asset_type={AssetClass.HL7MONITOR} - className="w-full" - /> -
- - - {updateLink ? "Update Bed" : "Save Bed"} - -
-
- ); -} diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index e49a63f7028..3c9b25b6fc9 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -78,7 +78,7 @@ export default function CameraFeed(props: Props) { .operate({ type: "get_stream_token" }) .then(({ res, data }) => { if (res?.status != 200) { - setState("authentication_error"); + setState("host_unreachable"); return props.onStreamError?.(); } const result = data?.result as { token: string }; @@ -183,13 +183,15 @@ export default function CameraFeed(props: Props) { {props.children}
- - {props.asset.name} - - +
+ + {props.asset.name} + + +
{!isIOS && (
); - case "authentication_error": - return ( - - ); case "offline": return ( { - const { cameraPTZ } = props; - return ( - - ); -}; - -const CameraFeedOld = (props: any) => { - const middlewareHostname = props.middlewareHostname; - const [presetsPage, setPresetsPage] = useState(0); - const cameraAsset = props.asset; - const [presets, setPresets] = useState([]); - const [bedPresets, setBedPresets] = useState([]); - const [showDefaultPresets, setShowDefaultPresets] = useState(false); - const [precision, setPrecision] = useState(1); - const [streamStatus, setStreamStatus] = useState( - StreamStatus.Offline, - ); - const [videoStartTime, setVideoStartTime] = useState(null); - const [bed, setBed] = useState({}); - const [presetName, setPresetName] = useState(""); - const [loading, setLoading] = useState(); - const dispatch: any = useDispatch(); - const [page, setPage] = useState({ - count: 0, - limit: 8, - offset: 0, - }); - const [toDelete, setToDelete] = useState(null); - const [toUpdate, setToUpdate] = useState(null); - const [_isFullscreen, setFullscreen] = useFullscreen(); - - const { width } = useWindowDimensions(); - const extremeSmallScreenBreakpoint = 320; - const isExtremeSmallScreen = - width <= extremeSmallScreenBreakpoint ? true : false; - const liveFeedPlayerRef = useRef(null); - const [streamUrl, setStreamUrl] = useState(""); - - const refreshPresetsHash = props.refreshPresetsHash; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [currentPreset, setCurrentPreset] = useState(); - const { - absoluteMove, - getCameraStatus, - getStreamToken, - getPTZPayload, - getPresets, - gotoPreset, - relativeMove, - } = useFeedPTZ({ - config: { - middlewareHostname, - ...cameraAsset, - }, - dispatch, - }); - - const fetchCameraPresets = () => - getPresets({ - onSuccess: (resp) => { - setPresets(resp); - }, - onError: (resp) => { - resp instanceof AxiosError && - Notification.Error({ - msg: "Camera is offline", - }); - }, - }); - - const calculateVideoLiveDelay = () => { - const video = liveFeedPlayerRef.current as HTMLVideoElement; - if (!video || !videoStartTime) return 0; - - const timeDifference = - (new Date().getTime() - videoStartTime.getTime()) / 1000; - - return timeDifference - video.currentTime; - }; - - const getBedPresets = async (id: any) => { - const bedAssets = await dispatch( - listAssetBeds({ - asset: id, - limit: page.limit, - offset: page.offset, - }), - ); - setBedPresets(bedAssets?.data?.results); - setPage({ - ...page, - count: bedAssets?.data?.count, - }); - }; - - const deletePreset = async (id: any) => { - const res = await dispatch(deleteAssetBed(id)); - if (res?.status === 204) { - Notification.Success({ msg: "Preset deleted successfully" }); - getBedPresets(cameraAsset.id); - } else { - Notification.Error({ - msg: "Error while deleting Preset: " + (res?.data?.detail || ""), - }); - } - setToDelete(null); - }; - - const updatePreset = async (currentPreset: any) => { - const data = { - bed_id: bed.id, - preset_name: presetName, - }; - const response = await dispatch( - partialUpdateAssetBed( - { - asset: currentPreset.asset_object.id, - bed: bed.id, - meta: { - ...currentPreset.meta, - ...data, - }, - }, - currentPreset?.id, - ), - ); - if (response && response.status === 200) { - Notification.Success({ msg: "Preset Updated" }); - } else { - Notification.Error({ msg: "Something Went Wrong" }); - } - getBedPresets(cameraAsset?.id); - fetchCameraPresets(); - setToUpdate(null); - }; - - const gotoBedPreset = (preset: any) => { - setLoading("Moving"); - absoluteMove(preset.meta.position, { - onSuccess: () => setLoading(undefined), - }); - }; - - useEffect(() => { - if (cameraAsset?.hostname) { - fetchCameraPresets(); - setTimeout(() => { - startStreamFeed(); - }, 1000); - } - }, []); - - useEffect(() => { - setPresetName(toUpdate?.meta?.preset_name); - setBed(toUpdate?.bed_object); - }, [toUpdate]); - - useEffect(() => { - getBedPresets(cameraAsset.id); - if (bedPresets?.[0]?.position) { - absoluteMove(bedPresets[0]?.position, {}); - } - }, [page.offset, cameraAsset.id, refreshPresetsHash]); - - const startStreamFeed = useCallback(async () => { - if (!liveFeedPlayerRef.current) return; - - await getStreamToken({ - onSuccess: (data) => { - setStreamUrl( - `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0&token=${data.token}`, - ); - }, - onError: () => { - setStreamStatus(StreamStatus.Offline); - }, - }); - }, [liveFeedPlayerRef.current]); - - const viewOptions = (page: number) => { - return presets - ? Object.entries(presets) - .map(([key, value]) => ({ label: key, value })) - .slice(page, page + 10) - : Array.from(Array(10), (_, i) => ({ - label: "Monitor " + (i + 1), - value: i + 1, - })); - }; - useEffect(() => { - let tId: any; - if (streamStatus !== StreamStatus.Playing) { - setStreamStatus(StreamStatus.Loading); - tId = setTimeout(() => { - startStreamFeed(); - }, 5000); - } - - return () => { - clearTimeout(tId); - }; - }, [startStreamFeed, streamStatus]); - - const handlePagination = (cOffset: number) => { - setPage({ - ...page, - offset: cOffset, - }); - }; - - const cameraPTZActionCBs: { [key: string]: (option: any) => void } = { - precision: () => { - setPrecision((precision: number) => - precision === 16 ? 1 : precision * 2, - ); - }, - reset: async () => { - setStreamStatus(StreamStatus.Loading); - setVideoStartTime(null); - await startStreamFeed(); - }, - fullScreen: () => { - if (!liveFeedPlayerRef.current) return; - setFullscreen(true, liveFeedPlayerRef.current); - }, - updatePreset: (option) => { - getCameraStatus({ - onSuccess: async (data) => { - if (currentPreset?.asset_object?.id && data?.position) { - setLoading(option.loadingLabel); - console.log("Updating Preset"); - const response = await dispatch( - partialUpdateAssetBed( - { - asset: currentPreset.asset_object.id, - bed: currentPreset.bed_object.id, - meta: { - ...currentPreset.meta, - position: data?.position, - }, - }, - currentPreset?.id, - ), - ); - if (response && response.status === 200) { - Notification.Success({ msg: "Preset Updated" }); - getBedPresets(cameraAsset?.id); - fetchCameraPresets(); - } - setLoading(undefined); - } - }, - }); - }, - other: (option) => { - setLoading(option.loadingLabel); - relativeMove(getPTZPayload(option.action, precision), { - onSuccess: () => setLoading(undefined), - }); - }, - }; - - const cameraPTZ = getCameraPTZ(precision).map((option) => { - const cb = - cameraPTZActionCBs[ - cameraPTZActionCBs[option.action] ? option.action : "other" - ]; - return { ...option, callback: () => cb(option) }; - }); - - // Voluntarily disabling eslint, since length of `cameraPTZ` is constant and - // hence shall not cause issues. (https://news.ycombinator.com/item?id=24363703) - for (const option of cameraPTZ) { - if (!option.shortcutKey) continue; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKeyboardShortcut(option.shortcutKey, option.callback); - } - - return ( - - {toDelete && ( - -

- Preset: {toDelete.meta.preset_name} -

-

- Bed: {toDelete.bed_object.name} -

- - } - action="Delete" - variant="danger" - onClose={() => setToDelete(null)} - onConfirm={() => deletePreset(toDelete.id)} - /> - )} - {toUpdate && ( - setToUpdate(null)} - onConfirm={() => updatePreset(toUpdate)} - > -
- setPresetName(value)} - /> -
- Bed - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={cameraAsset.location_id} - facility={cameraAsset.facility_id} - /> -
-
-
- )} -
-
-
-
- { - setVideoStartTime(() => new Date()); - }} - onWaiting={() => { - const delay = calculateVideoLiveDelay(); - if (delay > 5) { - setStreamStatus(StreamStatus.Loading); - } - }} - onSuccess={() => setStreamStatus(StreamStatus.Playing)} - onError={() => setStreamStatus(StreamStatus.Offline)} - /> - - {streamStatus === StreamStatus.Playing && - calculateVideoLiveDelay() > 3 && ( -
- - Slow Network Detected -
- )} - - {loading && ( -
-
-
-

{loading}

-
-
- )} - {/* { streamStatus > 0 && */} -
- {streamStatus === StreamStatus.Offline && ( -
-

- STATUS: OFFLINE -

-

- Feed is currently not live. -

-

- Click refresh button to try again. -

-
- )} - {streamStatus === StreamStatus.Stop && ( -
-

- STATUS: STOPPED -

-

Feed is Stooped.

-

- Click refresh button to start feed. -

-
- )} - {streamStatus === StreamStatus.Loading && ( -
-

- STATUS: LOADING -

-

- Fetching latest feed. -

-
- )} -
-
-
- {cameraPTZ.map((option) => { - const shortcutKeyDescription = - option.shortcutKey && - option.shortcutKey - .join(" + ") - .replace("Control", "Ctrl") - .replace("ArrowUp", "↑") - .replace("ArrowDown", "↓") - .replace("ArrowLeft", "←") - .replace("ArrowRight", "→"); - - return ( - - ); - })} -
- -
-
-
- -
- -
-
- {showDefaultPresets ? ( - <> - {viewOptions(presetsPage)?.map((option: any, i) => ( - - ))} - - ) : ( - <> - {bedPresets?.map((preset: any, index: number) => ( -
- -
- - -
-
- ))} - - )} -
- {/* Page Number Next and Prev buttons */} - {showDefaultPresets ? ( -
- - -
- ) : ( -
- - -
- )} - {props?.showRefreshButton && ( - - )} -
-
-
-
- - ); -}; - -export default CameraFeedOld; diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx index 7268397b81a..3e447e34e21 100644 --- a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx +++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx @@ -1,32 +1,29 @@ import { useState } from "react"; -import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; +import { AssetData } from "../Assets/AssetTypes"; import CameraFeed from "./CameraFeed"; import useQuery from "../../Utils/request/useQuery"; -import routes from "../../Redux/api"; -import useSlug from "../../Common/hooks/useSlug"; -import { CameraPresetDropdown } from "./AssetBedSelect"; +import { CameraPresetDropdown } from "./CameraPresetSelect"; import useOperateCamera from "./useOperateCamera"; import { classNames } from "../../Utils/utils"; +import { CameraPreset, FeedRoutes } from "./routes"; interface Props { asset: AssetData; } export default function LocationFeedTile(props: Props) { - const facility = useSlug("facility"); - const [preset, setPreset] = useState(); - - const { data, loading } = useQuery(routes.listAssetBeds, { - query: { limit: 100, facility, asset: props.asset?.id }, + const [preset, setPreset] = useState(); + const { operate, key } = useOperateCamera(props.asset.id); + const { data, loading } = useQuery(FeedRoutes.listAssetPresets, { + pathParams: { asset_id: props.asset.id }, + query: { limit: 100 }, }); - const { operate, key } = useOperateCamera(props.asset.id, true); - return ( string; - onChange?: (value: AssetBedModel) => void; + options: CameraPreset[]; + value?: CameraPreset; + label?: (value: CameraPreset) => string; + onChange?: (value: CameraPreset) => void; } export default function CameraPresetSelect(props: Props) { @@ -71,16 +70,13 @@ export const CameraPresetDropdown = ( props: Props & { placeholder: string }, ) => { const selected = props.value; - - const options = props.options.filter(({ meta }) => meta.type !== "boundary"); - const label = props.label ?? defaultLabel; return (
- {options.length === 0 + {props.options.length === 0 ? "No presets" : selected ? label(selected) @@ -113,7 +109,7 @@ export const CameraPresetDropdown = ( as="ul" className="absolute z-20 max-h-48 w-full overflow-auto rounded-b-lg bg-white py-1 text-base shadow-lg ring-1 ring-secondary-500 focus:outline-none md:max-h-60" > - {options?.map((obj) => ( + {props.options.map((obj) => ( { - return `${bed_object.name}: ${meta.preset_name}`; +const defaultLabel = (preset: CameraPreset) => { + return `${preset.asset_bed.bed_object.name}: ${preset.name}`; }; diff --git a/src/Components/CameraFeed/ConfigureCamera.tsx b/src/Components/CameraFeed/ConfigureCamera.tsx new file mode 100644 index 00000000000..052f93a9b05 --- /dev/null +++ b/src/Components/CameraFeed/ConfigureCamera.tsx @@ -0,0 +1,643 @@ +import { useEffect, useState } from "react"; +import { AssetData } from "../Assets/AssetTypes"; +import { getCameraConfig, makeAccessKey } from "../../Utils/transformUtils"; +import TextFormField from "../Form/FormFields/TextFormField"; +import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; +import useAuthUser from "../../Common/hooks/useAuthUser"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import useOperateCamera from "./useOperateCamera"; +import CameraFeed from "./CameraFeed"; +import { useTranslation } from "react-i18next"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import { Error, Success } from "../../Utils/Notifications"; +import { useQueryParams } from "raviger"; +import useQuery from "../../Utils/request/useQuery"; +import { classNames, compareBy } from "../../Utils/utils"; +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import { CameraPreset, FeedRoutes, GetStatusResponse } from "./routes"; +import DialogModal from "../Common/Dialog"; +import { + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from "@headlessui/react"; +import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2"; +import Loading from "../Common/Loading"; +import ConfirmDialog from "../Common/ConfirmDialog"; +import { FieldLabel } from "../Form/FormFields/FormField"; +import { checkIfValidIP } from "../../Common/validation"; +import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; + +interface Props { + asset: AssetData; + onUpdated: () => void; +} + +type OnvifPreset = { name: string; value: number }; + +export default function ConfigureCamera(props: Props) { + const { t } = useTranslation(); + const authUser = useAuthUser(); + + const presetNameSuggestions = [ + t("patient_face"), + t("patient_body"), + t("vitals_monitor"), + ]; + + const [query, setQuery] = useQueryParams<{ bed?: string }>(); + const [meta, setMeta] = useState(props.asset.meta); + const [onvifPresets, setOnvifPresets] = useState(); + const [currentOnvifPreset, setCurrentOnvifPreset] = useState(); + const [createPreset, setCreatePreset] = useState(); + const [editPreset, setEditPreset] = useState<{ + preset: CameraPreset["id"]; + position?: CameraPreset["position"]; + }>(); + const [presetName, setPresetName] = useState(""); + const [showUnlinkConfirmation, setShowUnlinkConfirmation] = useState(false); + + const assetBedsQuery = useQuery(routes.listAssetBeds, { + query: { asset: props.asset.id, limit: 50 }, + }); + + const bedsQuery = useQuery(routes.listFacilityBeds, { + query: { location: props.asset.location_object.id, limit: 50 }, + }); + + const linkedAssetBeds = assetBedsQuery.data?.results.sort( + compareBy("created_date"), + ); + + const linkedBedIDs = linkedAssetBeds?.map((a) => a.bed_object.id!); + const unlinkedBeds = + linkedBedIDs && + bedsQuery.data?.results + .filter((bed) => !linkedBedIDs.includes(bed.id!)) + .sort(compareBy("created_date")); + + const firstBedId = + linkedAssetBeds?.[0]?.bed_object.id ?? unlinkedBeds?.[0]?.id; + useEffect(() => { + if (!query.bed && firstBedId) { + setQuery({ bed: firstBedId }); + } + }, [query.bed, firstBedId]); + + const selectedAssetBed = linkedAssetBeds?.find( + (a) => a.bed_object.id === query.bed, + ); + const selectedUnlinkedBed = unlinkedBeds?.find((bed) => bed.id === query.bed); + + const cameraPresetsQuery = useQuery(FeedRoutes.listAssetBedPresets, { + pathParams: { assetbed_id: selectedAssetBed?.id ?? "" }, + query: { position: true, limit: 50 }, + prefetch: !!selectedAssetBed?.id, + }); + + useEffect(() => setMeta(props.asset.meta), [props.asset]); + + const accessKeyAttributes = getCameraConfig(meta); + + const { operate, key } = useOperateCamera(props.asset.id); + + if (!["DistrictAdmin", "StateAdmin"].includes(authUser.user_type)) { + return ( +
+ +
+ ); + } + + return ( +
+
+
{ + e.preventDefault(); + const { res } = await request(routes.partialUpdateAsset, { + pathParams: { external_id: props.asset.id }, + body: { meta }, + }); + + if (res?.ok) { + Success({ msg: "Asset Configured Successfully" }); + props.onUpdated(); + } + }} + > +

{t("feed_configurations")}

+
+ +

{t("middleware_hostname")}

+ {!!props.asset.resolved_middleware && + props.asset.resolved_middleware.source != "asset" && ( +
+ + + {t("middleware_hostname_sourced_from", { + source: props.asset.resolved_middleware?.source, + })} + +
+ )} +
+ } + placeholder={ + props.asset.resolved_middleware?.hostname ?? + t("middleware_hostname_example") + } + value={meta?.middleware_hostname} + onChange={({ value }) => + setMeta({ ...meta, middleware_hostname: value }) + } + /> + + setMeta({ ...meta, local_ip_address: value }) + } + error={ + meta?.local_ip_address && !checkIfValidIP(meta.local_ip_address) + ? t("invalid_ip_address") + : undefined + } + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + username: value, + }), + }) + } + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + password: value, + }), + }) + } + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + accessKey: value, + }), + }) + } + /> +
+
+ +
+ + +
+ { + if (!onvifPresets) { + setOnvifPresets( + Object.entries(presets).map(([name, value]) => ({ + name, + value, + })), + ); + } + }} + > +
+ { + setCurrentOnvifPreset(preset); + operate({ + type: "goto_preset", + data: { + preset: preset.value, + }, + }); + }} + disabled={!onvifPresets?.length} + > +
+ + + {!onvifPresets?.length + ? t("no_presets") + : (currentOnvifPreset?.name ?? + t("move_to_onvif_preset"))} + + + + + + + {onvifPresets?.map((obj) => ( + + classNames( + dropdownOptionClassNames(args), + "px-2 py-1.5", + ) + } + value={obj} + > + {obj.name} + + ))} + +
+
+
+
+
+
+ + {!linkedAssetBeds?.length && !unlinkedBeds?.length ? ( +
+ +

{t("location_beds_empty")}

+

{t("add_beds_to_configure_presets")}

+
+
+ ) : ( +
+

{t("manage_bed_presets")}

+
+ + {cameraPresetsQuery.loading && } + {selectedAssetBed && ( + <> + setShowUnlinkConfirmation(false)} + onConfirm={async () => { + const { res } = await request(routes.deleteAssetBed, { + pathParams: { external_id: selectedAssetBed.id }, + }); + + if (res?.ok) { + Success({ + msg: `${selectedAssetBed.bed_object.name} was unlinked from ${selectedAssetBed.asset_object.name}.`, + }); + setShowUnlinkConfirmation(false); + assetBedsQuery.refetch(); + } + }} + /> + { + setCreatePreset(undefined); + setPresetName(""); + }} + > + setPresetName(value)} + errorClassName="hidden" + placeholder={t("preset_name_placeholder")} + suggestions={presetNameSuggestions} + /> +
+ { + const { res } = await request(FeedRoutes.createPreset, { + pathParams: { assetbed_id: selectedAssetBed.id }, + body: { + name: presetName, + position: createPreset!, + }, + }); + if (!res?.ok) { + return; + } + setCreatePreset(undefined); + setPresetName(""); + Success({ msg: "Preset created" }); + cameraPresetsQuery.refetch(); + }} + disabled={!presetName} + /> +
+
+
+
    +
  • { + const { data } = await operate({ type: "get_status" }); + if (!data) { + Error({ msg: t("unable_to_get_current_position") }); + return; + } + setCreatePreset( + (data as GetStatusResponse).result.position, + ); + }} + > + + {t("add_preset")} +
  • + {cameraPresetsQuery.data?.results.map((preset) => ( +
  • + { + setEditPreset(undefined); + setPresetName(""); + }} + > +
    + setPresetName(value)} + placeholder={t("preset_name_placeholder")} + suggestions={presetNameSuggestions} + /> + {t("position")} + { + if (!value) { + setEditPreset({ + ...editPreset!, + position: undefined, + }); + return; + } + + const { data } = await operate({ + type: "get_status", + }); + if (!data) { + Error({ + msg: t("unable_to_get_current_position"), + }); + return; + } + setEditPreset({ + ...editPreset!, + position: (data as GetStatusResponse).result + .position!, + }); + }} + /> +
    + { + setEditPreset(undefined); + setPresetName(""); + }} + /> + { + const { res } = await request( + FeedRoutes.deletePreset, + { + pathParams: { + assetbed_id: selectedAssetBed.id, + id: preset.id, + }, + }, + ); + if (!res?.ok) { + return; + } + Success({ msg: t("preset_deleted") }); + cameraPresetsQuery.refetch(); + setEditPreset(undefined); + setPresetName(""); + }} + variant="danger" + > + + {t("delete")} + + { + const { res } = await request( + FeedRoutes.updatePreset, + { + pathParams: { + assetbed_id: selectedAssetBed.id, + id: preset.id, + }, + body: { + name: presetName || undefined, + position: editPreset?.position, + }, + }, + ); + if (!res?.ok) { + return; + } + Success({ msg: t("preset_updated") }); + setEditPreset(undefined); + setPresetName(""); + cameraPresetsQuery.refetch(); + }} + /> +
    + +
    + {preset.name} + + + +
    + + operate({ + type: "absolute_move", + data: preset.position!, + }) + } + > + + {t("view")} + + { + setEditPreset({ preset: preset.id }); + }} + > + + {t("update")} + +
    +
    +
  • + ))} +
+
+ + setShowUnlinkConfirmation(true)} + > + {t("unlink_camera_and_bed")} + +
+
+ + )} + {selectedUnlinkedBed && ( +
+ +

{t("bed_not_linked_to_camera")}

+

{t("create_preset_prerequisite")}

+ { + const { res } = await request(routes.createAssetBed, { + body: { + asset: props.asset.id, + bed: selectedUnlinkedBed.id, + }, + }); + if (res?.ok) { + Success({ msg: t("camera_bed_link_success") }); + assetBedsQuery.refetch(); + } + }} + className="mt-6" + > + {t("link_camera_and_bed")} + +
+
+ )} +
+
+ )} +
+ ); +} diff --git a/src/Components/CameraFeed/FeedAlert.tsx b/src/Components/CameraFeed/FeedAlert.tsx index 09f3b21ae42..86c5468decf 100644 --- a/src/Components/CameraFeed/FeedAlert.tsx +++ b/src/Components/CameraFeed/FeedAlert.tsx @@ -9,8 +9,7 @@ export type FeedAlertState = | "moving" | "zooming" | "saving_preset" - | "host_unreachable" - | "authentication_error"; + | "host_unreachable"; interface Props { state?: FeedAlertState; @@ -25,7 +24,6 @@ const ALERT_ICON_MAP: Partial> = { zooming: "l-search", saving_preset: "l-save", host_unreachable: "l-exclamation-triangle", - authentication_error: "l-exclamation-triangle", }; export default function FeedAlert({ state }: Props) { diff --git a/src/Components/CameraFeed/FeedControls.tsx b/src/Components/CameraFeed/FeedControls.tsx index 1bacd6c1cfc..36d3aa96cd2 100644 --- a/src/Components/CameraFeed/FeedControls.tsx +++ b/src/Components/CameraFeed/FeedControls.tsx @@ -15,8 +15,6 @@ const Actions = { const metaKey = isAppleDevice ? "Meta" : "Control"; -export type PTZAction = keyof typeof Actions; - /** * Returns the PTZ payload for the given action * diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts index aecbdc655fa..db983d6a383 100644 --- a/src/Components/CameraFeed/routes.ts +++ b/src/Components/CameraFeed/routes.ts @@ -1,4 +1,8 @@ import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { WritableOnly } from "../../Utils/types"; +import { AssetBedModel } from "../Assets/AssetTypes"; +import { PerformedByModel } from "../HCX/misc"; import { OperationAction, PTZPayload } from "./useOperateCamera"; export type GetStatusResponse = { @@ -23,6 +27,18 @@ export type GetPresetsResponse = { result: Record; }; +export type CameraPreset = { + readonly id: string; + name: string; + readonly asset_bed: AssetBedModel; + position: PTZPayload; + readonly created_by: PerformedByModel; + readonly updated_by: PerformedByModel; + readonly created_date: string; + readonly modified_date: string; + readonly is_migrated: boolean; +}; + export const FeedRoutes = { operateAsset: { path: "/api/v1/asset/{id}/operate_assets/", @@ -32,4 +48,37 @@ export const FeedRoutes = { >(), TBody: Type<{ action: OperationAction }>(), }, + + listAssetBedPresets: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/", + method: "GET", + TRes: Type>(), + }, + listAssetPresets: { + path: "/api/v1/asset/{asset_id}/camera_presets/", + method: "GET", + TRes: Type>(), + }, + listBedPresets: { + path: "/api/v1/bed/{bed_id}/camera_presets/", + method: "GET", + TRes: Type>(), + }, + createPreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/", + method: "POST", + TRes: Type(), + TBody: Type>(), + }, + updatePreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/", + method: "PATCH", + TRes: Type(), + TBody: Type>>(), + }, + deletePreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/", + method: "DELETE", + TRes: Type(), + }, } as const; diff --git a/src/Components/CameraFeed/useFeedPTZ.ts b/src/Components/CameraFeed/useFeedPTZ.ts deleted file mode 100644 index fb704baf972..00000000000 --- a/src/Components/CameraFeed/useFeedPTZ.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Deprecated. Use `useOperateAsset` instead. - * - * Preserving for backwards compatibility and preventing merge conflict with a - * co-related PR. Will be removed in the future. - */ - -import { operateAsset } from "../../Redux/actions"; - -export interface IAsset { - id: string; -} - -interface PTZPayload { - x: number; - y: number; - zoom: number; -} - -interface UseMSEMediaPlayerOption { - config: IAsset; - dispatch: any; -} - -interface UseMSEMediaPlayerReturnType { - absoluteMove: (payload: PTZPayload, options: IOptions) => void; - relativeMove: (payload: PTZPayload, options: IOptions) => void; - getPTZPayload: ( - action: PTZ, - precision?: number, - value?: number, - ) => PTZPayload; - getCameraStatus: (options: IOptions) => void; - getStreamToken: (options: IOptions) => void; - getPresets: (options: IOptions) => void; - gotoPreset: (payload: IGotoPresetPayload, options: IOptions) => void; -} - -interface IOptions { - onSuccess?: (resp: Record) => void; - onError?: (resp: Record) => void; -} - -export enum PTZ { - Up = "up", - Down = "down", - Left = "left", - Right = "right", - ZoomIn = "zoomIn", - ZoomOut = "zoomOut", -} - -const getCameraStatus = - (config: IAsset, dispatch: any) => - async (options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "get_status", - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const getStreamToken = - (config: IAsset, dispatch: any) => - async (options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "get_stream_token", - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const getPresets = - (config: IAsset, dispatch: any) => - async (options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "get_presets", - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -interface IGotoPresetPayload { - preset: string; -} - -const gotoPreset = - (config: IAsset, dispatch: any) => - async (payload: IGotoPresetPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "goto_preset", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const absoluteMove = - (config: IAsset, dispatch: any) => - async (payload: PTZPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "absolute_move", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const relativeMove = - (config: IAsset, dispatch: any) => - async (payload: PTZPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "relative_move", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -export const getPTZPayload = ( - action: PTZ, - precision = 1, - value?: number, -): PTZPayload => { - let x = 0; - let y = 0; - let zoom = 0; - const delta = !value ? 0.1 / Math.max(1, precision) : value; - switch (action) { - case PTZ.Up: - y = delta; - break; - case PTZ.Down: - y = -delta; - break; - case PTZ.Left: - x = -delta; - break; - case PTZ.Right: - x = delta; - break; - case PTZ.ZoomIn: - zoom = delta; - break; - case PTZ.ZoomOut: - zoom = -delta; - break; - } - - return { x, y, zoom }; -}; - -export const useFeedPTZ = ({ - config, - dispatch, -}: UseMSEMediaPlayerOption): UseMSEMediaPlayerReturnType => { - return { - absoluteMove: absoluteMove(config, dispatch), - relativeMove: relativeMove(config, dispatch), - getPTZPayload, - getCameraStatus: getCameraStatus(config, dispatch), - getStreamToken: getStreamToken(config, dispatch), - getPresets: getPresets(config, dispatch), - gotoPreset: gotoPreset(config, dispatch), - }; -}; diff --git a/src/Components/CameraFeed/useOperateCamera.ts b/src/Components/CameraFeed/useOperateCamera.ts index bfddbf5b887..0e65fb0130c 100644 --- a/src/Components/CameraFeed/useOperateCamera.ts +++ b/src/Components/CameraFeed/useOperateCamera.ts @@ -54,7 +54,7 @@ export type OperationAction = * This hook is used to control the PTZ of a camera asset and retrieve other related information. * @param id The external id of the camera asset */ -export default function useOperateCamera(id: string, silent = false) { +export default function useOperateCamera(id: string) { const [key, setKey] = useState(0); return { @@ -70,14 +70,14 @@ export default function useOperateCamera(id: string, silent = false) { type: "get_status", }, }, - silent, + silent: true, }); } return request(FeedRoutes.operateAsset, { pathParams: { id }, body: { action }, - silent, + silent: true, }); }, }; diff --git a/src/Components/CameraFeed/utils.ts b/src/Components/CameraFeed/utils.ts index 5556237d579..1cb721ebcc3 100644 --- a/src/Components/CameraFeed/utils.ts +++ b/src/Components/CameraFeed/utils.ts @@ -21,7 +21,7 @@ export const getStreamUrl = (asset: AssetData, token?: string) => { throw "getStreamUrl can be invoked only for ONVIF Assets"; } - const config = getCameraConfig(asset); + const config = getCameraConfig(asset.meta); const host = asset.resolved_middleware?.hostname; const uuid = config.accessKey; diff --git a/src/Components/Common/MonitorAssetPopover.tsx b/src/Components/Common/MonitorAssetPopover.tsx index 5293c0a6cd4..3d7d55ad271 100644 --- a/src/Components/Common/MonitorAssetPopover.tsx +++ b/src/Components/Common/MonitorAssetPopover.tsx @@ -62,7 +62,7 @@ const MonitorAssetPopover = ({

- {t("local_ipaddress")}: + {t("local_ip_address")}:

{asset?.meta?.local_ip_address} diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index 2e999d1956e..91922c99205 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -1,14 +1,11 @@ import { useEffect, useRef, useState } from "react"; import { ConsultationTabProps } from "./index"; -import { AssetBedModel, AssetData } from "../../Assets/AssetTypes"; -import routes from "../../../Redux/api"; import useQuery from "../../../Utils/request/useQuery"; import CameraFeed from "../../CameraFeed/CameraFeed"; import Loading from "../../Common/Loading"; -import AssetBedSelect from "../../CameraFeed/AssetBedSelect"; +import CameraPresetSelect from "../../CameraFeed/CameraPresetSelect"; import { triggerGoal } from "../../../Integrations/Plausible"; import useAuthUser from "../../../Common/hooks/useAuthUser"; -import useSlug from "../../../Common/hooks/useSlug"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import ButtonV2 from "../../Common/components/ButtonV2"; import useOperateCamera, { @@ -20,18 +17,19 @@ import ConfirmDialog from "../../Common/ConfirmDialog"; import useBreakpoints from "../../../Common/hooks/useBreakpoints"; import { Warn } from "../../../Utils/Notifications"; import { useTranslation } from "react-i18next"; -import { GetStatusResponse } from "../../CameraFeed/routes"; +import { + CameraPreset, + FeedRoutes, + GetStatusResponse, +} from "../../CameraFeed/routes"; import StillWatching from "../../CameraFeed/StillWatching"; export const ConsultationFeedTab = (props: ConsultationTabProps) => { const { t } = useTranslation(); const authUser = useAuthUser(); - const facility = useSlug("facility"); const bed = props.consultationData.current_bed?.bed_object; const feedStateSessionKey = `encounterFeedState[${props.consultationId}]`; - - const [asset, setAsset] = useState(); - const [preset, setPreset] = useState(); + const [preset, setPreset] = useState(); const [showPresetSaveConfirmation, setShowPresetSaveConfirmation] = useState(false); const [isUpdatingPreset, setIsUpdatingPreset] = useState(false); @@ -52,23 +50,22 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { } }, []); - const { key, operate } = useOperateCamera(asset?.id ?? "", true); + const asset = preset?.asset_bed.asset_object; + + const { key, operate } = useOperateCamera(asset?.id ?? ""); - const { data, loading, refetch } = useQuery(routes.listAssetBeds, { - query: { limit: 100, facility, bed: bed?.id, asset: asset?.id }, + const presetsQuery = useQuery(FeedRoutes.listBedPresets, { + pathParams: { bed_id: bed?.id ?? "" }, + query: { limit: 100 }, prefetch: !!bed, onResponse: ({ data }) => { if (!data) { return; } - const presets = data.results.filter( - (obj) => - obj.asset_object.meta?.asset_type === "CAMERA" && - obj.meta.type !== "boundary", - ); - + const presets = data.results; const lastStateJSON = sessionStorage.getItem(feedStateSessionKey); + const preset = (() => { if (lastStateJSON) { @@ -77,23 +74,33 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { return presets.find((obj) => obj.id === lastState.value); } if (lastState.type === "position") { + const assetBedObj = presets.find( + (p) => p.asset_bed.id === lastState.assetBed, + )?.asset_bed; + + if (!assetBedObj) { + return; + } + return { ...presets[0], id: "", - meta: { ...presets[0].meta, position: lastState.value }, - }; + asset_bed: assetBedObj, + position: lastState.value, + } satisfies CameraPreset; } } })() ?? presets[0]; + console.log({ preset, presets }); + if (preset) { setPreset(preset); - setAsset(preset.asset_object); } }, }); - const presets = data?.results; + const presets = presetsQuery.data?.results; const handleUpdatePreset = async () => { if (!preset) return; @@ -102,17 +109,17 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { const { data } = await operate({ type: "get_status" }); const { position } = (data as { result: { position: PTZPayload } }).result; - - const { data: updated } = await request(routes.partialUpdateAssetBed, { - pathParams: { external_id: preset.id }, + const { data: updated } = await request(FeedRoutes.updatePreset, { + pathParams: { + assetbed_id: preset.asset_bed.id, + id: preset.id, + }, body: { - asset: preset.asset_object.id, - bed: preset.bed_object.id, - meta: { ...preset.meta, position }, + position, }, }); - await refetch(); + await presetsQuery.refetch(); setPreset(updated); setHasMoved(false); @@ -124,7 +131,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { if (divRef.current) { divRef.current.scrollIntoView({ behavior: "smooth" }); } - }, [!!bed, loading, !!asset, divRef.current]); + }, [!!bed, presetsQuery.loading, !!asset, divRef.current]); useEffect(() => { if (preset?.id) { @@ -138,7 +145,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { } }, [feedStateSessionKey, preset]); - if (loading) { + if (presetsQuery.loading) { return ; } @@ -169,8 +176,11 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { { + if (!preset) { + return; + } setHasMoved(true); setTimeout(async () => { const { data } = await operate({ type: "get_status" }); @@ -179,6 +189,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { feedStateSessionKey, JSON.stringify({ type: "position", + assetBed: preset.asset_bed.id, value: (data as GetStatusResponse).result.position, } satisfies LastAccessedPosition), ); @@ -204,26 +215,19 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => {

{presets ? ( <> - obj.meta.preset_name} + label={(obj) => obj.name} value={preset} onChange={(value) => { triggerGoal("Camera Preset Clicked", { - presetName: preset?.meta?.preset_name, + presetName: preset?.name, consultationId: props.consultationId, userId: authUser.id, result: "success", }); setHasMoved(false); - // Voluntarily copying to trigger change of reference of the position attribute, so that the useEffect of CameraFeed that handles the moves gets triggered. - setPreset({ - ...value, - meta: { - ...value.meta, - position: { ...value.meta.position }, - }, - }); + setPreset(value); }} /> {isUpdatingPreset ? ( @@ -269,6 +273,7 @@ type LastAccessedPreset = { type LastAccessedPosition = { type: "position"; + assetBed: string; value: PTZPayload; }; diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 6970898a158..3b34689221d 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -1,10 +1,6 @@ import { GENDER_TYPES } from "../../../Common/constants"; import { ConsultationModel } from "../models"; -import { - getConsultation, - getPatient, - listAssetBeds, -} from "../../../Redux/actions"; +import { getConsultation, getPatient } from "../../../Redux/actions"; import { statusType, useAbortableEffect } from "../../../Common/utils"; import { useCallback, useState } from "react"; import DoctorVideoSlideover from "../DoctorVideoSlideover"; @@ -35,7 +31,6 @@ import { ConsultationNeurologicalMonitoringTab } from "./ConsultationNeurologica import ABDMRecordsTab from "../../ABDM/ABDMRecordsTab"; import { ConsultationNutritionTab } from "./ConsultationNutritionTab"; import PatientNotesSlideover from "../PatientNotesSlideover"; -import { AssetBedModel } from "../../Assets/AssetTypes"; import PatientInfoCard from "../../Patient/PatientInfoCard"; import RelativeDateUserMention from "../../Common/RelativeDateUserMention"; import DiagnosesListAccordion from "../../Diagnosis/DiagnosesListAccordion"; @@ -129,20 +124,24 @@ export const ConsultationDetails = (props: any) => { ); } setConsultationData(data); - const assetRes = data?.current_bed?.bed_object?.id - ? await dispatch( - listAssetBeds({ - bed: data?.current_bed?.bed_object?.id, - }), - ) - : null; - const isCameraAttachedRes = - assetRes != null - ? assetRes.data.results.some((asset: AssetBedModel) => { - return asset?.asset_object?.asset_class === "ONVIF"; - }) - : false; - setIsCameraAttached(isCameraAttachedRes); + + setIsCameraAttached( + await (async () => { + const bedId = data?.current_bed?.bed_object?.id; + if (!bedId) { + return false; + } + const { data: assetBeds } = await request(routes.listAssetBeds, { + query: { bed: bedId }, + }); + if (!assetBeds) { + return false; + } + return assetBeds.results.some( + (a) => a.asset_object.asset_class === "ONVIF", + ); + })(), + ); // Get patient data const id = res.data.patient; diff --git a/src/Components/Facility/FacilityUsers.tsx b/src/Components/Facility/FacilityUsers.tsx index 92cc4b35d10..f7fe62810fc 100644 --- a/src/Components/Facility/FacilityUsers.tsx +++ b/src/Components/Facility/FacilityUsers.tsx @@ -4,7 +4,7 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import { RESULTS_PER_PAGE_LIMIT } from "../../Common/constants"; import * as Notification from "../../Utils/Notifications.js"; import { formatName, isUserOnline, relativeTime } from "../../Utils/utils"; -import SlideOverCustom from "../../CAREUI/interactive/SlideOver"; +import SlideOver from "../../CAREUI/interactive/SlideOver"; import Pagination from "../Common/Pagination"; import UserDetails from "../Common/UserDetails"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -279,7 +279,7 @@ export default function FacilityUsers(props: any) { /> )}
- - +
{manageUsers}
diff --git a/src/Components/Form/FormFields/TextFormField.tsx b/src/Components/Form/FormFields/TextFormField.tsx index 0593dc9a2d3..12d6623cd15 100644 --- a/src/Components/Form/FormFields/TextFormField.tsx +++ b/src/Components/Form/FormFields/TextFormField.tsx @@ -25,6 +25,7 @@ export type TextFormFieldProps = FormFieldBaseProps & { onKeyDown?: (event: React.KeyboardEvent) => void; onFocus?: (event: React.FocusEvent) => void; onBlur?: (event: React.FocusEvent) => void; + suggestions?: string[]; }; const TextFormField = forwardRef((props: TextFormFieldProps, ref) => { @@ -125,6 +126,28 @@ const TextFormField = forwardRef((props: TextFormFieldProps, ref) => { ); } + if ( + props.suggestions?.length && + !props.suggestions.includes(`${field.value}`) + ) { + child = ( +
+ {child} +
    + {props.suggestions.map((suggestion) => ( +
  • field.handleChange(suggestion)} + > + {suggestion} +
  • + ))} +
+
+ ); + } + return {child}; }); diff --git a/src/Locale/en.json b/src/Locale/en.json index d4808fa185b..08f9ded2130 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -222,6 +222,7 @@ "add_as": "Add as", "add_attachments": "Add Attachments", "add_beds": "Add Bed(s)", + "add_beds_to_configure_presets": "Add beds to this location to configure presets for them.", "add_details_of_patient": "Add Details of Patient", "add_location": "Add Location", "add_new_user": "Add New User", @@ -229,6 +230,7 @@ "add_policy": "Add Insurance Policy", "add_prescription_medication": "Add Prescription Medication", "add_prescription_to_consultation_note": "Add a new prescription to this consultation.", + "add_preset": "Add preset", "add_prn_prescription": "Add PRN Prescription", "add_remarks": "Add remarks", "add_spoke": "Add Spoke Facility", @@ -296,6 +298,7 @@ "bed_capacity": "Bed Capacity", "bed_created_notification_one": "{{count}} Bed created successfully", "bed_created_notification_other": "{{count}} Beds created successfully", + "bed_not_linked_to_camera": "This bed has not been linked to this camera.", "bed_search_placeholder": "Search by beds name", "bed_type": "Bed Type", "bed_type__100": "ICU Bed", @@ -314,7 +317,9 @@ "bradycardia": "Bradycardia", "breathlessness_level": "Breathlessness level", "camera": "Camera", + "camera_bed_link_success": "Camera linked to bed successfully.", "camera_permission_denied": "Camera Permission denied", + "camera_was_linked_to_bed": "This camera was linked to this bed", "cancel": "Cancel", "capture": "Capture", "capture_cover_photo": "Capture Cover Photo", @@ -385,7 +390,7 @@ "confirm_password": "Confirm Password", "confirm_transfer_complete": "Confirm Transfer Complete!", "confirmed": "Confirmed", - "consultation_not_filed": "You have not filed any consultation for this patient yet.", + "consultation_not_filed": "You have not filed a consultation for this patient yet.", "consultation_not_filed_description": "Please file a consultation for this patient to continue.", "consultation_notes": "General Instructions (Advice)", "consultation_updates": "Consultation updates", @@ -408,6 +413,9 @@ "create_consultation": "Create Consultation", "create_facility": "Create a new facility", "create_new_asset": "Create New Asset", + "create_position_preset": "Create a new position preset", + "create_position_preset_description": "Creates a new position preset in Care from the current position of the camera for the given name", + "create_preset_prerequisite": "To create presets for this bed, you'll need to link the camera to the bed first.", "create_resource_request": "Create Resource Request", "created": "Created", "created_date": "Created Date", @@ -535,6 +543,7 @@ "facility_type": "Facility Type", "failed_to_link_abha_number": "Failed to link ABHA number", "features": "Features", + "feed_configurations": "Feed Configurations", "feed_is_currently_not_live": "Feed is currently not live", "feed_optimal_experience_for_apple_phones": "For optimal viewing experience, consider rotating your device. Ensure auto-rotate is enabled in your device settings.", "feed_optimal_experience_for_phones": "For optimal viewing experience, consider rotating your device.", @@ -591,6 +600,7 @@ "international_mobile": "International Mobile", "invalid_asset_id_msg": "Oops! The asset ID you entered does not appear to be valid.", "invalid_email": "Please Enter a Valid Email Address", + "invalid_ip_address": "Invalid IP Address", "invalid_link_msg": "It appears that the password reset link you have used is either invalid or expired. Please request a new password reset link.", "invalid_password": "Password doesn't meet the requirements", "invalid_password_reset_link": "Invalid password reset link", @@ -634,6 +644,7 @@ "latitude_invalid": "Latitude must be between -90 and 90", "left": "Left", "length": "Length ({{unit}})", + "link_camera_and_bed": "Link bed to Camera", "linked_facilities": "Linked Facilities", "linked_skills": "Linked Skills", "liquid_oxygen_capacity": "Liquid Oxygen Capacity", @@ -646,8 +657,10 @@ "load_more": "Load More", "loading": "Loading...", "local_body": "Local body", - "local_ipaddress": "Local IP Address", + "local_ip_address": "Local IP Address", + "local_ip_address_example": "e.g. 192.168.0.123", "location": "Location", + "location_beds_empty": "No beds available in this location", "location_management": "Location Management", "log_lab_results": "Log Lab Results", "log_report": "Log Report", @@ -655,7 +668,9 @@ "longitude_invalid": "Longitude must be between -180 and 180", "lsg": "Lsg", "make_multiple_beds_label": "Do you want to make multiple beds?", + "manage_bed_presets": "Manage Presets of Bed", "manage_prescriptions": "Manage Prescriptions", + "manage_preset": "Manage preset {{ name }}", "manufacturer": "Manufacturer", "map_acronym": "M.A.P.", "mark_all_as_read": "Mark all as Read", @@ -673,6 +688,8 @@ "medicines_administered": "Medicine(s) administered", "medicines_administered_error": "Error administering medicine(s)", "middleware_hostname": "Middleware Hostname", + "middleware_hostname_example": "e.g. example.ohc.network", + "middleware_hostname_sourced_from": "Middleware hostname sourced from {{ source }}", "min_password_len_8": "Minimum password length 8", "min_time_bw_doses": "Min. time b/w doses", "mobile": "Mobile", @@ -682,6 +699,7 @@ "modified_date": "Modified Date", "monitor": "Monitor", "more_info": "More Info", + "move_to_onvif_preset": "Move to an ONVIF Preset", "moving_camera": "Moving Camera", "name": "Name", "name_of_hospital": "Name of Hospital", @@ -711,6 +729,7 @@ "no_patients_to_show": "No patients to show.", "no_policy_added": "No Insurance Policy Added", "no_policy_found": "No Insurance Policy Found for this Patient", + "no_presets": "No Presets", "no_remarks": "No remarks", "no_results_found": "No Results Found", "no_staff": "No staff found", @@ -757,6 +776,7 @@ "password_reset_success": "Password Reset successfully", "password_sent": "Password Reset Email Sent", "patient_address": "Patient Address", + "patient_body": "Patient Body", "patient_category": "Patient Category", "patient_consultation__admission": "Date of admission", "patient_consultation__consultation_notes": "General Instructions", @@ -771,6 +791,7 @@ "patient_consultation__treatment__summary__temperature": "Temperature", "patient_created": "Patient Created", "patient_details": "Patient Details", + "patient_face": "Patient Face", "patient_name": "Patient name", "patient_no": "OP/IP No", "patient_notes_thread__Doctors": "Doctor's Discussions", @@ -813,6 +834,7 @@ "policy__policy_id__example": "POL001", "policy__subscriber_id": "Member ID", "policy__subscriber_id__example": "SUB001", + "position": "Position", "post_your_comment": "Post Your Comment", "powered_by": "Powered By", "preferred_facility_type": "Preferred Facility Type", @@ -827,6 +849,9 @@ "prescriptions__medicine": "Medicine", "prescriptions__route": "Route", "prescriptions__start_date": "Prescribed On", + "preset_deleted": "Preset deleted", + "preset_name_placeholder": "Specify an identifiable name for the new preset", + "preset_updated": "Preset updated", "prev_sessions": "Prev Sessions", "principal": "Principal", "principal_diagnosis": "Principal diagnosis", @@ -963,6 +988,7 @@ "stop": "Stop", "stream_stop_due_to_inativity": "The live feed will stop streaming due to inactivity", "stream_stopped_due_to_inativity": "The live feed has stopped streaming due to inactivity", + "stream_uuid": "Stream UUID", "sub_category": "Sub Category", "submit": "Submit", "submitting": "Submitting", @@ -998,9 +1024,13 @@ "type_to_search": "Type to search", "type_your_comment": "Type your comment", "type_your_reason_here": "Type your reason here", + "unable_to_get_current_position": "Unable to get current position.", "unconfirmed": "Unconfirmed", "unique_id": "Unique Id", "unknown": "Unknown", + "unlink_asset_bed_and_presets": "Delete linked presets and unlink bed", + "unlink_asset_bed_caution": "This action will also delete all presets that are associated to this camera and bed.", + "unlink_camera_and_bed": "Unlink this bed from this camera", "unsubscribe": "Unsubscribe", "unsubscribe_failed": "Unsubscribe failed.", "unsupported_browser": "Unsupported Browser", @@ -1015,6 +1045,7 @@ "update_facility": "Update Facility", "update_facility_middleware_success": "Facility middleware updated successfully", "update_log": "Update Log", + "update_preset_position_to_current": "Update preset's position to camera's current position", "update_record": "Update Record", "update_record_for_asset": "Update record for asset", "update_shift_request": "Update Shift Request", @@ -1045,6 +1076,7 @@ "view_users": "View Users", "virtual_nursing_assistant": "Virtual Nursing Assistant", "vitals": "Vitals", + "vitals_monitor": "Vitals Monitor", "ward": "Ward", "warranty_amc_expiry": "Warranty / AMC Expiry", "what_facility_assign_the_patient_to": "What facility would you like to assign the patient to", diff --git a/src/Locale/hi.json b/src/Locale/hi.json index 93d9ef32f34..e235c558978 100644 --- a/src/Locale/hi.json +++ b/src/Locale/hi.json @@ -458,7 +458,7 @@ "load_more": "और लोड करें", "loading": "लोड हो रहा है...", "local_body": "स्थानीय निकाय", - "local_ipaddress": "स्थानीय आईपी पता", + "local_ip_address": "स्थानीय आईपी पता", "location": "जगह", "location_management": "स्थान प्रबंधन", "log_lab_results": "लॉग लैब परिणाम", @@ -810,4 +810,4 @@ "you_need_at_least_a_location_to_create_an_assest": "संपत्ति बनाने के लिए आपको कम से कम एक स्थान की आवश्यकता होगी।", "zoom_in": "ज़ूम इन", "zoom_out": "ज़ूम आउट" -} +} \ No newline at end of file diff --git a/src/Locale/kn.json b/src/Locale/kn.json index acaebe8e421..dc46e49394f 100644 --- a/src/Locale/kn.json +++ b/src/Locale/kn.json @@ -459,7 +459,7 @@ "load_more": "ಇನ್ನಷ್ಟು ಲೋಡ್ ಮಾಡಿ", "loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ...", "local_body": "ಸ್ಥಳೀಯ ಸಂಸ್ಥೆ", - "local_ipaddress": "ಸ್ಥಳೀಯ IP ವಿಳಾಸ", + "local_ip_address": "ಸ್ಥಳೀಯ IP ವಿಳಾಸ", "location": "ಸ್ಥಳ", "location_management": "ಸ್ಥಳ ನಿರ್ವಹಣೆ", "log_lab_results": "ಲಾಗ್ ಲ್ಯಾಬ್ ಫಲಿತಾಂಶಗಳು", @@ -810,4 +810,4 @@ "you_need_at_least_a_location_to_create_an_assest": "ಆಸ್ತಿಯನ್ನು ರಚಿಸಲು ನಿಮಗೆ ಕನಿಷ್ಠ ಸ್ಥಳದ ಅಗತ್ಯವಿದೆ.", "zoom_in": "ಜೂಮ್ ಇನ್", "zoom_out": "ಜೂಮ್ ಔಟ್" -} +} \ No newline at end of file diff --git a/src/Locale/ml.json b/src/Locale/ml.json index 8460b6f8b7c..d830d3a46ec 100644 --- a/src/Locale/ml.json +++ b/src/Locale/ml.json @@ -458,7 +458,7 @@ "load_more": "കൂടുതൽ ലോഡ് ചെയ്യുക", "loading": "ലോഡ് ചെയ്യുന്നു...", "local_body": "തദ്ദേശ സ്ഥാപനം", - "local_ipaddress": "പ്രാദേശിക ഐപി വിലാസം", + "local_ip_address": "പ്രാദേശിക ഐപി വിലാസം", "location": "സ്ഥാനം", "location_management": "ലൊക്കേഷൻ മാനേജ്മെൻ്റ്", "log_lab_results": "ലോഗ് ലാബ് ഫലങ്ങൾ", @@ -810,4 +810,4 @@ "you_need_at_least_a_location_to_create_an_assest": "ഒരു അസസ്‌റ്റ് സൃഷ്‌ടിക്കാൻ നിങ്ങൾക്ക് ഒരു ലൊക്കേഷനെങ്കിലും ആവശ്യമാണ്.", "zoom_in": "സൂം ഇൻ ചെയ്യുക", "zoom_out": "സൂം ഔട്ട്" -} +} \ No newline at end of file diff --git a/src/Locale/ta.json b/src/Locale/ta.json index 1fbf82c29aa..042f5a068c4 100644 --- a/src/Locale/ta.json +++ b/src/Locale/ta.json @@ -458,7 +458,7 @@ "load_more": "மேலும் ஏற்றவும்", "loading": "ஏற்றுகிறது...", "local_body": "உள்ளூர் அமைப்பு", - "local_ipaddress": "உள்ளூர் ஐபி முகவரி", + "local_ip_address": "உள்ளூர் ஐபி முகவரி", "location": "இடம்", "location_management": "இருப்பிட மேலாண்மை", "log_lab_results": "பதிவு ஆய்வக முடிவுகள்", @@ -810,4 +810,4 @@ "you_need_at_least_a_location_to_create_an_assest": "ஒரு அசெஸ்ட்டை உருவாக்க குறைந்தபட்சம் ஒரு இருப்பிடமாவது தேவை.", "zoom_in": "பெரிதாக்கவும்", "zoom_out": "பெரிதாக்கவும்" -} +} \ No newline at end of file diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 47d3530d065..29fe2fed2ef 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1,29 +1,5 @@ import { fireRequest } from "./fireRequest"; -// asset bed -export const listAssetBeds = (params: object, altKey?: string) => - fireRequest("listAssetBeds", [], params, {}, altKey); - -export const partialUpdateAssetBed = (params: object, asset_id: string) => - fireRequest( - "partialUpdateAssetBed", - [], - { ...params }, - { - external_id: asset_id, - }, - ); - -export const deleteAssetBed = (asset_id: string) => - fireRequest( - "deleteAssetBed", - [], - {}, - { - external_id: asset_id, - }, - ); - export const getPatient = (pathParam: object) => { return fireRequest("getPatient", [], {}, pathParam); }; @@ -41,6 +17,3 @@ export const getConsultation = (id: string) => { export const dischargePatient = (params: object, pathParams: object) => { return fireRequest("dischargePatient", [], params, pathParams); }; - -export const operateAsset = (id: string, params: object) => - fireRequest("operateAsset", [], params, { external_id: id }); diff --git a/src/Utils/transformUtils.ts b/src/Utils/transformUtils.ts index 0050a0bcbb6..4aa63da734c 100644 --- a/src/Utils/transformUtils.ts +++ b/src/Utils/transformUtils.ts @@ -1,16 +1,23 @@ import { AssetData } from "../Components/Assets/AssetTypes"; -export const getCameraConfig = (asset: AssetData) => { - const { meta } = asset; +export const getCameraConfig = (meta: AssetData["meta"]) => { return { middleware_hostname: meta?.middleware_hostname, - id: asset?.id, hostname: meta?.local_ip_address, username: meta?.camera_access_key?.split(":")[0], password: meta?.camera_access_key?.split(":")[1], accessKey: meta?.camera_access_key?.split(":")[2], port: 80, - location_id: asset?.location_object?.id, - facility_id: asset?.location_object?.facility?.id, }; }; + +export const makeAccessKey = ( + attrs: Pick< + ReturnType, + "username" | "password" | "accessKey" + >, +) => { + return [attrs.username, attrs.password, attrs.accessKey] + .map((a) => a ?? "") + .join(":"); +};