{["fullScreen", "reset", "updatePreset", "zoomIn", "zoomOut"].map(
(button, index) => {
- if (isIOS && button === "reset") return null;
const option = cameraPTZ.find(
(option) => option.action === button
);
@@ -531,6 +557,13 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
clickAction={() => cameraPTZ[4].callback()}
/>
+ {streamStatus === StreamStatus.Playing &&
+ calculateVideoLiveDelay() > 3 && (
+
{[
false,
diff --git a/src/Components/Facility/LocationManagement.tsx b/src/Components/Facility/LocationManagement.tsx
index 01e0c246ecb..38dcfc1f389 100644
--- a/src/Components/Facility/LocationManagement.tsx
+++ b/src/Components/Facility/LocationManagement.tsx
@@ -1,176 +1,99 @@
-import { useCallback, useState, ReactElement, lazy } from "react";
-
-import { useDispatch } from "react-redux";
-import { statusType, useAbortableEffect } from "../../Common/utils";
-import { listFacilityAssetLocation, getAnyFacility } from "../../Redux/actions";
-import Pagination from "../Common/Pagination";
-import { LocationModel } from "./models";
+import { lazy } from "react";
import ButtonV2 from "../Common/components/ButtonV2";
import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor";
import CareIcon from "../../CAREUI/icons/CareIcon";
import Page from "../Common/components/Page";
-const Loading = lazy(() => import("../Common/Loading"));
+import routes from "../../Redux/api";
+import PaginatedList from "../../CAREUI/misc/PaginatedList";
+import { LocationModel } from "./models";
-interface LocationManagementProps {
- facilityId: string;
-}
+const Loading = lazy(() => import("../Common/Loading"));
-interface LocationRowProps {
- id: string;
+interface Props {
facilityId: string;
- name: string;
- description: string;
}
-const LocationRow = (props: LocationRowProps) => {
- const { id, facilityId, name, description } = props;
-
+export default function LocationManagement({ facilityId }: Props) {
return (
-
-
-
-
{name}
-
{description}
-
-
-
-
-
- Edit
-
-
(
+
+
+ Add New Location
+
+ }
>
-
- Manage Beds
-
-
-
- );
-};
-
-export const LocationManagement = (props: LocationManagementProps) => {
- const { facilityId } = props;
- const dispatchAction: any = useDispatch();
- const [isLoading, setIsLoading] = useState(false);
- let location: ReactElement | null = null;
- let locationsList: ReactElement[] | ReactElement = [];
- const [locations, setLocations] = useState
([]);
- const [offset, setOffset] = useState(0);
- const [currentPage, setCurrentPage] = useState(1);
- const [totalCount, setTotalCount] = useState(0);
- const [facilityName, setFacilityName] = useState("");
- const limit = 14;
-
- const fetchData = useCallback(
- async (status: statusType) => {
- setIsLoading(true);
- const facility = await dispatchAction(getAnyFacility(facilityId));
-
- setFacilityName(facility?.data?.name || "");
-
- const res = await dispatchAction(
- listFacilityAssetLocation(
- { limit, offset },
- { facility_external_id: facilityId }
- )
- );
- if (!status.aborted) {
- if (res?.data) {
- setLocations(res.data.results);
- setTotalCount(res.data.count);
- }
- setIsLoading(false);
- }
- },
- [dispatchAction, offset, facilityId]
- );
-
- useAbortableEffect(
- (status: statusType) => {
- fetchData(status);
- },
- [fetchData]
- );
+
+
+
+ Add New Location
+
+
+
+ No locations available
+
- const handlePagination = (page: number, limit: number) => {
- const offset = (page - 1) * limit;
- setCurrentPage(page);
- setOffset(offset);
- };
+
+
+
- if (locations?.length) {
- locationsList = locations.map((locationItem: LocationModel) => (
-
- ));
- } else if (locations && locations.length === 0) {
- locationsList = (
-
- No locations available
-
- );
- }
+ className="my-8 flex grow flex-col gap-3 lg:mx-8">
+ {(item) => }
+
- if (locations) {
- location = (
- <>
-
- {locationsList}
-
- {totalCount > limit && (
-
-
+
- )}
- >
- );
- }
-
- if (isLoading || !locations) {
- return
;
- }
+
+ )}
+
+ );
+}
- return (
-
-
-
-
-
- Add New Location
-
-
- {location}
+const Location = ({ name, description, id }: LocationModel) => (
+
+
+
+
+
+
+ Edit
+
+
+
+ Manage Beds
+
+
+
+);
diff --git a/src/Components/VitalsMonitor/HL7DeviceClient.ts b/src/Components/VitalsMonitor/HL7DeviceClient.ts
index 72f6406193a..7e13622b31f 100644
--- a/src/Components/VitalsMonitor/HL7DeviceClient.ts
+++ b/src/Components/VitalsMonitor/HL7DeviceClient.ts
@@ -1,11 +1,31 @@
import { EventEmitter } from "events";
import { VitalsDataBase, VitalsValueBase, VitalsWaveformBase } from "./types";
-const WAVEFORM_KEY_MAP: Record
= {
- II: "ecg-waveform",
- Pleth: "pleth-waveform",
- Respiration: "spo2-waveform",
-};
+const ECG_WAVENAME_KEYS = [
+ "I",
+ "II",
+ "III",
+ "aVR",
+ "aVL",
+ "aVF",
+ "V1",
+ "V2",
+ "V3",
+ "V4",
+ "V5",
+ "V6",
+] as const;
+
+const WAVEFORM_KEY_MAP: Record =
+ {
+ Pleth: "pleth-waveform",
+ Respiration: "spo2-waveform",
+
+ // Maps each ECG wave name to the event "ecg-waveform"
+ ...(Object.fromEntries(
+ ECG_WAVENAME_KEYS.map((key) => [key, "ecg-waveform"])
+ ) as Record),
+ };
/**
* Provides the API for connecting to the Vitals Monitor WebSocket and emitting
@@ -74,8 +94,10 @@ export interface HL7VitalsValueData extends VitalsDataBase, VitalsValueBase {
| "body-temperature2";
}
+type EcgWaveName = (typeof ECG_WAVENAME_KEYS)[number];
+
export interface HL7VitalsWaveformData extends VitalsWaveformBase {
- "wave-name": "II" | "Pleth" | "Respiration";
+ "wave-name": EcgWaveName | "Pleth" | "Respiration";
}
export interface HL7VitalsBloodPressureData extends VitalsDataBase {
diff --git a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx
index a340c21e28b..c0b089df8b0 100644
--- a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx
+++ b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx
@@ -88,7 +88,7 @@ export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) {
{/* Pulse Rate */}
❤️
diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx
index 01a91d4ce3a..b98a099f439 100644
--- a/src/Redux/api.tsx
+++ b/src/Redux/api.tsx
@@ -1,14 +1,15 @@
-interface Route {
- path: string;
- method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
- noAuth?: boolean;
+import { LocationModel } from "../Components/Facility/models";
+import { PaginatedResponse } from "../Utils/request/types";
+
+/**
+ * A fake function that returns an empty object casted to type T
+ * @returns Empty object as type T
+ */
+function Res(): T {
+ return {} as T;
}
-interface Routes {
- [name: string]: Route;
-}
-
-const routes: Routes = {
+const routes = {
config: {
path: import.meta.env.REACT_APP_CONFIG ?? "/config.json",
method: "GET",
@@ -187,6 +188,7 @@ const routes: Routes = {
listFacilityAssetLocation: {
path: "/api/v1/facility/{facility_external_id}/asset_location/",
method: "GET",
+ TRes: Res>(),
},
createFacilityAssetLocation: {
path: "/api/v1/facility/{facility_external_id}/asset_location/",
@@ -1065,6 +1067,6 @@ const routes: Routes = {
path: "/api/v1/hcx/make_claim/",
method: "POST",
},
-};
+} as const;
export default routes;
diff --git a/src/Router/AppRouter.tsx b/src/Router/AppRouter.tsx
index 24b24cfdf09..f1449f13bc2 100644
--- a/src/Router/AppRouter.tsx
+++ b/src/Router/AppRouter.tsx
@@ -49,7 +49,7 @@ import ShowPushNotification from "../Components/Notifications/ShowPushNotificati
import { NoticeBoard } from "../Components/Notifications/NoticeBoard";
import { AddLocationForm } from "../Components/Facility/AddLocationForm";
import { AddBedForm } from "../Components/Facility/AddBedForm";
-import { LocationManagement } from "../Components/Facility/LocationManagement";
+import LocationManagement from "../Components/Facility/LocationManagement";
import { BedManagement } from "../Components/Facility/BedManagement";
import AssetsList from "../Components/Assets/AssetsList";
import AssetManage from "../Components/Assets/AssetManage";
diff --git a/src/Utils/request/request.ts b/src/Utils/request/request.ts
new file mode 100644
index 00000000000..2dc938fa6f1
--- /dev/null
+++ b/src/Utils/request/request.ts
@@ -0,0 +1,27 @@
+import { RequestOptions, Route } from "./types";
+import { makeHeaders, makeUrl } from "./utils";
+
+interface Options extends RequestOptions {
+ controller?: AbortController;
+}
+
+export default async function request(
+ { path, method, noAuth }: Route,
+ { query, body, pathParams, controller }: Options = {}
+) {
+ const signal = controller?.signal;
+
+ const headers = makeHeaders(noAuth ?? false);
+ const url = makeUrl(path, query, pathParams);
+
+ const options: RequestInit = { headers, method, signal };
+
+ if (body) {
+ options.body = JSON.stringify(body);
+ }
+
+ const res = await fetch(url, options);
+ const data: TData = await res.json();
+
+ return { res, data };
+}
diff --git a/src/Utils/request/types.ts b/src/Utils/request/types.ts
new file mode 100644
index 00000000000..e7f0f9544a3
--- /dev/null
+++ b/src/Utils/request/types.ts
@@ -0,0 +1,32 @@
+type QueryParamValue = string | number | boolean | null | undefined;
+
+export type QueryParams = Record;
+
+interface RouteBase {
+ path: string;
+ TRes: TData;
+ noAuth?: boolean;
+}
+
+export interface QueryRoute extends RouteBase {
+ method?: "GET";
+}
+
+export interface MutationRoute extends RouteBase {
+ method: "POST" | "PUT" | "PATCH" | "DELETE";
+}
+
+export type Route = QueryRoute | MutationRoute;
+
+export interface RequestOptions {
+ query?: QueryParams;
+ body?: object;
+ pathParams?: Record;
+}
+
+export interface PaginatedResponse {
+ count: number;
+ next: string | null;
+ previous: string | null;
+ results: TItem[];
+}
diff --git a/src/Utils/request/useQuery.ts b/src/Utils/request/useQuery.ts
new file mode 100644
index 00000000000..e459a579e3e
--- /dev/null
+++ b/src/Utils/request/useQuery.ts
@@ -0,0 +1,70 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { QueryRoute, RequestOptions } from "./types";
+import request from "./request";
+import { mergeRequestOptions } from "./utils";
+
+export interface QueryOptions extends RequestOptions {
+ prefetch?: boolean;
+ refetchOnWindowFocus?: boolean;
+}
+
+export default function useQuery(
+ route: QueryRoute,
+ options?: QueryOptions
+) {
+ const [res, setRes] = useState();
+ const [data, setData] = useState();
+ const [error, setError] = useState();
+ const [loading, setLoading] = useState(false);
+
+ const controllerRef = useRef();
+
+ const runQuery = useCallback(
+ async (overrides?: QueryOptions) => {
+ controllerRef.current?.abort();
+
+ const controller = new AbortController();
+ controllerRef.current = controller;
+
+ const resolvedOptions =
+ options && overrides
+ ? mergeRequestOptions(options, overrides)
+ : options;
+
+ setLoading(true);
+
+ try {
+ const { res, data } = await request(route, resolvedOptions);
+
+ setRes(res);
+ setData(res.ok ? data : undefined);
+ setError(res.ok ? undefined : data);
+ } catch (error) {
+ console.error(error);
+ setData(undefined);
+ setError(error);
+ } finally {
+ setLoading(false);
+ }
+ },
+ [route, JSON.stringify(options)]
+ );
+
+ useEffect(() => {
+ if (options?.prefetch ?? true) {
+ runQuery();
+ }
+ }, [runQuery, options?.prefetch]);
+
+ useEffect(() => {
+ if (options?.refetchOnWindowFocus) {
+ const onFocus = () => runQuery();
+
+ window.addEventListener("focus", onFocus);
+
+ return () => window.removeEventListener("focus", onFocus);
+ }
+ }, [runQuery, options?.refetchOnWindowFocus]);
+
+ return { res, data, error, loading, refetch: runQuery };
+}
diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts
new file mode 100644
index 00000000000..21236a8145f
--- /dev/null
+++ b/src/Utils/request/utils.ts
@@ -0,0 +1,81 @@
+import { LocalStorageKeys } from "../../Common/constants";
+import { QueryParams, RequestOptions } from "./types";
+
+export function makeUrl(
+ path: string,
+ query?: QueryParams,
+ pathParams?: Record
+) {
+ if (pathParams) {
+ path = Object.entries(pathParams).reduce(
+ (acc, [key, value]) => acc.replace(`{${key}}`, value),
+ path
+ );
+ }
+
+ ensurePathNotMissingReplacements(path);
+
+ if (query) {
+ path += `?${makeQueryParams(query)}`;
+ }
+
+ return path;
+}
+
+const makeQueryParams = (query: QueryParams) => {
+ const qParams = new URLSearchParams();
+
+ Object.entries(query).forEach(([key, value]) => {
+ if (value !== undefined) {
+ qParams.set(key, `${value}`);
+ }
+ });
+
+ return qParams.toString();
+};
+
+const ensurePathNotMissingReplacements = (path: string) => {
+ const missingParams = path.match(/\{.*\}/g);
+
+ if (missingParams) {
+ throw new Error(`Missing path params: ${missingParams.join(", ")}`);
+ }
+};
+
+export function makeHeaders(noAuth: boolean) {
+ const headers = new Headers({
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ });
+
+ if (!noAuth) {
+ const token = getAuthorizationHeader();
+
+ if (token) {
+ headers.append("Authorization", token);
+ }
+ }
+
+ return headers;
+}
+
+export function getAuthorizationHeader() {
+ const bearerToken = localStorage.getItem(LocalStorageKeys.accessToken);
+
+ if (bearerToken) {
+ return `Bearer ${bearerToken}`;
+ }
+
+ return null;
+}
+
+export function mergeRequestOptions(
+ options: RequestOptions,
+ overrides: RequestOptions
+): RequestOptions {
+ return {
+ query: { ...options.query, ...overrides.query },
+ body: { ...options.body, ...overrides.body },
+ pathParams: { ...options.pathParams, ...overrides.pathParams },
+ };
+}