From eb09ecd41b31a09d41c37acb688ab52238662959 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Wed, 20 Sep 2023 18:49:06 +0530 Subject: [PATCH 01/16] Activate asset config for district/state admins only (#6309) --- src/Components/Assets/AssetManage.tsx | 3 +- .../Assets/AssetType/HL7Monitor.tsx | 71 ++++++------ .../Assets/AssetType/ONVIFCamera.tsx | 107 +++++++++--------- 3 files changed, 93 insertions(+), 88 deletions(-) diff --git a/src/Components/Assets/AssetManage.tsx b/src/Components/Assets/AssetManage.tsx index 27f0f2d8bc0..4565c48b33d 100644 --- a/src/Components/Assets/AssetManage.tsx +++ b/src/Components/Assets/AssetManage.tsx @@ -28,7 +28,7 @@ import RecordMeta from "../../CAREUI/display/RecordMeta"; import { useTranslation } from "react-i18next"; const Loading = lazy(() => import("../Common/Loading")); import * as Notification from "../../Utils/Notifications.js"; -import AuthorizeFor, { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; +import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import Uptime from "../Common/Uptime"; import useAuthUser from "../../Common/hooks/useAuthUser"; import dayjs from "dayjs"; @@ -453,7 +453,6 @@ const AssetManage = (props: AssetManageProps) => { } id="configure-asset" data-testid="asset-configure-button" - authorizeFor={AuthorizeFor(["DistrictAdmin", "StateAdmin"])} > {t("configure")} diff --git a/src/Components/Assets/AssetType/HL7Monitor.tsx b/src/Components/Assets/AssetType/HL7Monitor.tsx index b19190ed410..55f4d0c258e 100644 --- a/src/Components/Assets/AssetType/HL7Monitor.tsx +++ b/src/Components/Assets/AssetType/HL7Monitor.tsx @@ -15,6 +15,7 @@ import CareIcon from "../../../CAREUI/icons/CareIcon"; import TextFormField from "../../Form/FormFields/TextFormField"; import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor"; import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor"; +import useAuthUser from "../../../Common/hooks/useAuthUser"; interface HL7MonitorProps { assetId: string; @@ -31,7 +32,7 @@ const HL7Monitor = (props: HL7MonitorProps) => { const [isLoading, setIsLoading] = useState(true); const [localipAddress, setLocalIPAddress] = useState(""); const [ipadrdress_error, setIpAddress_error] = useState(""); - + const authUser = useAuthUser(); const dispatch = useDispatch(); useEffect(() => { @@ -87,40 +88,42 @@ const HL7Monitor = (props: HL7MonitorProps) => { return (
-
- -
-

Connection

-
- setMiddlewareHostname(e.value)} - errorClassName="hidden" - /> - setLocalIPAddress(e.value)} - required - error={ipadrdress_error} - /> - - - Save Configuration - -
-
-
- {["HL7MONITOR"].includes(assetType) && ( - - + {["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && ( +
+ +
+

Connection

+
+ setMiddlewareHostname(e.value)} + errorClassName="hidden" + /> + setLocalIPAddress(e.value)} + required + error={ipadrdress_error} + /> + + + Save Configuration + +
+
- )} -
+ {["HL7MONITOR"].includes(assetType) && ( + + + + )} +
+ )} {assetType === "HL7MONITOR" && ( { ); const [refreshHash, setRefreshHash] = useState(Number(new Date())); const dispatch = useDispatch(); - + const authUser = useAuthUser(); useEffect(() => { const fetchFacility = async () => { const res = await dispatch(getPermittedFacility(facilityId)); @@ -147,57 +148,59 @@ const ONVIFCamera = (props: ONVIFCameraProps) => { return (
-
-
- setMiddlewareHostname(value)} - /> - setCameraAddress(value)} - error={ipadrdress_error} - /> - setUsername(value)} - /> - setPassword(value)} - /> - setStreamUuid(value)} - /> -
-
- -
-
+ {["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && ( +
+
+ setMiddlewareHostname(value)} + /> + setCameraAddress(value)} + error={ipadrdress_error} + /> + setUsername(value)} + /> + setPassword(value)} + /> + setStreamUuid(value)} + /> +
+
+ +
+
+ )} {assetType === "ONVIF" ? ( Date: Wed, 20 Sep 2023 18:49:40 +0530 Subject: [PATCH 02/16] Enhanced UI for Principle diagnosis (#6264) * highlight principal diagnosis * refactor * use chip * add badge * add tooltip --- .../Facility/ConsultationDetails/index.tsx | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 490c704f295..3d9fa0f6e75 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -8,7 +8,7 @@ import { ConsultationModel, ICD11DiagnosisModel } from "../models"; import { getConsultation, getPatient } from "../../../Redux/actions"; import { statusType, useAbortableEffect } from "../../../Common/utils"; import { lazy, useCallback, useState } from "react"; - +import ToolTip from "../../Common/utils/Tooltip"; import ButtonV2 from "../../Common/components/ButtonV2"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import DischargeModal from "../DischargeModal"; @@ -191,10 +191,20 @@ export const ConsultationDetails = (props: any) => { return diagnoses.length ? (

{label}

- - {diagnoses.slice(0, !showMore ? nshow : undefined).map((diagnosis) => ( -

{diagnosis.label}

- ))} + {diagnoses.slice(0, !showMore ? nshow : undefined).map((diagnosis) => + diagnosis.id === consultationData.icd11_principal_diagnosis ? ( +
+

{diagnosis.label}

+
+ + + +
+
+ ) : ( +

{diagnosis.label}

+ ) + )} {diagnoses.length > nshow && ( <> {!showMore ? ( @@ -359,22 +369,6 @@ export const ConsultationDetails = (props: any) => {
)*/} - {consultationData.icd11_principal_diagnosis && ( - - d.id === consultationData.icd11_principal_diagnosis - )!, - ]} - /> - )} - Date: Wed, 20 Sep 2023 20:55:30 +0530 Subject: [PATCH 03/16] Removed react-swipeable-views package (#6312) * removed SwipeableViews from ManagePatients.tsx * Removed react-swipeable-views package --- package-lock.json | 137 +--------------------- package.json | 2 - src/Components/Patient/ManagePatients.tsx | 15 +-- 3 files changed, 7 insertions(+), 147 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f655932fd7..4871fbe374b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,6 @@ "react-player": "^2.12.0", "react-qr-reader": "^2.2.1", "react-redux": "^8.1.1", - "react-swipeable-views": "^0.14.0", "react-transition-group": "^4.4.5", "react-webcam": "^7.1.1", "read-excel-file": "^5.6.1", @@ -85,7 +84,6 @@ "@types/react-dom": "^18.2.6", "@types/react-google-recaptcha": "^2.1.5", "@types/react-qr-reader": "^2.1.4", - "@types/react-swipeable-views": "^0.13.2", "@types/react-transition-group": "^4.4.6", "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", @@ -5639,15 +5637,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-swipeable-views": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@types/react-swipeable-views/-/react-swipeable-views-0.13.2.tgz", - "integrity": "sha512-FiszBm9M0JicAgzO/IwDqpfHQRUEjPZA88UexYsVD6qHJBf5LrbGjR5Mw4+yZbf8ZxJneNqOsZbe4WGjOYG7iQ==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", @@ -11996,11 +11985,6 @@ "node": ">=4.0" } }, - "node_modules/keycode": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", - "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==" - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -14273,6 +14257,7 @@ "node_modules/npm/node_modules/brace-expansion": { "version": "2.0.1", "dev": true, + "inBundle": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -15267,15 +15252,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/npm/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/npm/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -18125,19 +18101,6 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, - "node_modules/react-event-listener": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz", - "integrity": "sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==", - "dependencies": { - "@babel/runtime": "^7.2.0", - "prop-types": "^15.6.0", - "warning": "^4.0.1" - }, - "peerDependencies": { - "react": "^16.3.0" - } - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -18306,91 +18269,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-swipeable-views": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-swipeable-views/-/react-swipeable-views-0.14.0.tgz", - "integrity": "sha512-wrTT6bi2nC3JbmyNAsPXffUXLn0DVT9SbbcFr36gKpbaCgEp7rX/OFxsu5hPc/NBsUhHyoSRGvwqJNNrWTwCww==", - "dependencies": { - "@babel/runtime": "7.0.0", - "prop-types": "^15.5.4", - "react-swipeable-views-core": "^0.14.0", - "react-swipeable-views-utils": "^0.14.0", - "warning": "^4.0.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/react-swipeable-views-core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-swipeable-views-core/-/react-swipeable-views-core-0.14.0.tgz", - "integrity": "sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==", - "dependencies": { - "@babel/runtime": "7.0.0", - "warning": "^4.0.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/react-swipeable-views-core/node_modules/@babel/runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", - "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", - "dependencies": { - "regenerator-runtime": "^0.12.0" - } - }, - "node_modules/react-swipeable-views-core/node_modules/regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" - }, - "node_modules/react-swipeable-views-utils": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-swipeable-views-utils/-/react-swipeable-views-utils-0.14.0.tgz", - "integrity": "sha512-W+fXBOsDqgFK1/g7MzRMVcDurp3LqO3ksC8UgInh2P/tKgb5DusuuB1geKHFc6o1wKl+4oyER4Zh3Lxmr8xbXA==", - "dependencies": { - "@babel/runtime": "7.0.0", - "keycode": "^2.1.7", - "prop-types": "^15.6.0", - "react-event-listener": "^0.6.0", - "react-swipeable-views-core": "^0.14.0", - "shallow-equal": "^1.2.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/react-swipeable-views-utils/node_modules/@babel/runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", - "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", - "dependencies": { - "regenerator-runtime": "^0.12.0" - } - }, - "node_modules/react-swipeable-views-utils/node_modules/regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" - }, - "node_modules/react-swipeable-views/node_modules/@babel/runtime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", - "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", - "dependencies": { - "regenerator-runtime": "^0.12.0" - } - }, - "node_modules/react-swipeable-views/node_modules/regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -19371,11 +19249,6 @@ "node": ">=8" } }, - "node_modules/shallow-equal": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", - "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -21268,14 +21141,6 @@ "makeerror": "1.0.12" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 8ade8c604be..246734dc3d4 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,6 @@ "react-player": "^2.12.0", "react-qr-reader": "^2.2.1", "react-redux": "^8.1.1", - "react-swipeable-views": "^0.14.0", "react-transition-group": "^4.4.5", "react-webcam": "^7.1.1", "read-excel-file": "^5.6.1", @@ -125,7 +124,6 @@ "@types/react-dom": "^18.2.6", "@types/react-google-recaptcha": "^2.1.5", "@types/react-qr-reader": "^2.1.4", - "@types/react-swipeable-views": "^0.13.2", "@types/react-transition-group": "^4.4.6", "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 71a3ad08c8c..e15db9f3b40 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -36,7 +36,6 @@ import RecordMeta from "../../CAREUI/display/RecordMeta"; import SearchInput from "../Form/SearchInput"; import SortDropdownMenu from "../Common/SortDropdown"; import SwitchTabs from "../Common/components/SwitchTabs"; -import SwipeableViews from "react-swipeable-views"; import { parseOptionId } from "../../Common/utils"; import { parsePhoneNumber } from "../../Utils/utils.js"; import { useDispatch } from "react-redux"; @@ -985,14 +984,12 @@ export const PatientManager = () => {
- - -
{managePatients}
-
- -
{managePatients}
-
-
+ +
{managePatients}
+
+ +
{managePatients}
+
Date: Thu, 21 Sep 2023 10:07:59 +0530 Subject: [PATCH 04/16] Refactor `App.tsx`, Providers, Integrations; Remove usage on redux's states; Adds support for notifications to `useQuery` and `request` (#6287) * Refactor `App.tsx`, providers, integrations and remove redux state usage. * remove axios dependency * fixes trailing slash redirect for token refresh * handle notifications * fix auth state management and notifications * fix suspense fallback * support for reattempts * request; make new headers on every attempt --- src/App.tsx | 127 +++--------------- src/CAREUI/misc/ThemedFavicon.tsx | 14 ++ src/Common/hooks/useAppHistory.ts | 2 +- src/Common/hooks/useFilters.tsx | 2 +- .../Facility/ConsultationDetails/index.tsx | 2 +- .../Facility/Consultations/Feed.tsx | 2 +- src/Components/Patient/ManagePatients.tsx | 2 +- src/Components/Patient/PatientHome.tsx | 2 +- .../VitalsMonitor/HL7PatientVitalsMonitor.tsx | 2 +- .../Common => Integrations}/Plausible.tsx | 4 +- src/Integrations/Sentry.tsx | 25 ++++ src/Integrations/index.tsx | 6 + src/Providers/AppConfigProvider.tsx | 25 ++++ src/Providers/AuthUserProvider.tsx | 63 +++++++++ .../misc => Providers}/HistoryAPIProvider.tsx | 4 +- src/Providers/index.tsx | 5 + src/Redux/api.tsx | 12 +- src/{Router => Routers}/AppRouter.tsx | 0 src/{Router => Routers}/SessionRouter.tsx | 0 src/Routers/index.tsx | 6 + src/Utils/request/handleResponse.ts | 38 ++++++ src/Utils/request/request.ts | 54 ++++++-- src/Utils/request/types.ts | 11 +- src/Utils/request/useQuery.ts | 24 +--- src/Utils/request/utils.ts | 9 ++ 25 files changed, 295 insertions(+), 146 deletions(-) create mode 100644 src/CAREUI/misc/ThemedFavicon.tsx rename src/{Components/Common => Integrations}/Plausible.tsx (95%) create mode 100644 src/Integrations/Sentry.tsx create mode 100644 src/Integrations/index.tsx create mode 100644 src/Providers/AppConfigProvider.tsx create mode 100644 src/Providers/AuthUserProvider.tsx rename src/{CAREUI/misc => Providers}/HistoryAPIProvider.tsx (93%) create mode 100644 src/Providers/index.tsx rename src/{Router => Routers}/AppRouter.tsx (100%) rename src/{Router => Routers}/SessionRouter.tsx (100%) create mode 100644 src/Routers/index.tsx create mode 100644 src/Utils/request/handleResponse.ts diff --git a/src/App.tsx b/src/App.tsx index e00ca18f652..f89dfd11e74 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,111 +1,28 @@ -import * as Sentry from "@sentry/browser"; - -import { FC, Suspense, lazy, useEffect, useState } from "react"; -import { getConfig, getCurrentUser } from "./Redux/actions"; -import { statusType, useAbortableEffect } from "./Common/utils"; -import { useDispatch, useSelector } from "react-redux"; - -import AppRouter from "./Router/AppRouter"; -import { HistoryAPIProvider } from "./CAREUI/misc/HistoryAPIProvider"; -import { AppConfigContext, IConfig } from "./Common/hooks/useConfig"; -import { LocalStorageKeys } from "./Common/constants"; -import Plausible from "./Components/Common/Plausible"; -import SessionRouter from "./Router/SessionRouter"; -import axios from "axios"; -import { AuthUserContext } from "./Common/hooks/useAuthUser"; - -const Loading = lazy(() => import("./Components/Common/Loading")); - -const App: FC = () => { - const dispatch: any = useDispatch(); - const state: any = useSelector((state) => state); - const { currentUser, config } = state; - const [user, setUser] = useState(null); - - useAbortableEffect(async () => { - const res = await dispatch(getConfig()); - if (res.data && res.status < 400) { - const config = res.data as IConfig; - - if (config?.sentry_dsn && import.meta.env.PROD) { - Sentry.init({ - environment: config.sentry_environment, - dsn: config.sentry_dsn, - }); - } - - localStorage.setItem("config", JSON.stringify(config)); - } - }, [dispatch]); - - const updateRefreshToken = () => { - const refresh = localStorage.getItem(LocalStorageKeys.refreshToken); - // const access = localStorage.getItem(LocalStorageKeys.accessToken); - // if (!access && refresh) { - // localStorage.removeItem(LocalStorageKeys.refreshToken); - // document.location.reload(); - // return; - // } - if (!refresh) { - return; - } - axios - .post("/api/v1/auth/token/refresh/", { - refresh, - }) - .then((resp) => { - localStorage.setItem(LocalStorageKeys.accessToken, resp.data.access); - localStorage.setItem(LocalStorageKeys.refreshToken, resp.data.refresh); - }); - }; - useEffect(() => { - updateRefreshToken(); - setInterval(updateRefreshToken, 5 * 60 * 1000); - }, [user]); - - useAbortableEffect( - async (status: statusType) => { - const res = await dispatch(getCurrentUser()); - if (!status.aborted && res && res.statusCode === 200) { - setUser(res.data); - } - }, - [dispatch] - ); - - useEffect(() => { - const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); - const favicon: any = document.querySelector("link[rel~='icon']"); - if (darkThemeMq.matches) { - favicon.href = "/favicon-light.ico"; - } else { - favicon.href = "/favicon.ico"; - } - }, []); - - if ( - !currentUser || - currentUser.isFetching || - !config || - config.isFetching || - !config.data - ) { - return ; - } - +import { Suspense } from "react"; +import Routers from "./Routers"; +import { + AppConfigProvider, + AuthUserProvider, + HistoryAPIProvider, +} from "./Providers"; +import ThemedFavicon from "./CAREUI/misc/ThemedFavicon"; +import Intergrations from "./Integrations"; +import Loading from "./Components/Common/Loading"; + +const App = () => { return ( }> + - - {currentUser?.data ? ( - - - - ) : ( - - )} - - + + }> + + + + {/* Integrations */} + + + ); diff --git a/src/CAREUI/misc/ThemedFavicon.tsx b/src/CAREUI/misc/ThemedFavicon.tsx new file mode 100644 index 00000000000..55b8d34ec7e --- /dev/null +++ b/src/CAREUI/misc/ThemedFavicon.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; + +export default function ThemedFavicon() { + useEffect(() => { + const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); + const favicon = document.querySelector( + "link[rel~='icon']" + ) as HTMLLinkElement; + + favicon.href = darkThemeMq.matches ? "/favicon-light.ico" : "/favicon.ico"; + }, []); + + return null; +} diff --git a/src/Common/hooks/useAppHistory.ts b/src/Common/hooks/useAppHistory.ts index f4ee2f11f78..a4605db082b 100644 --- a/src/Common/hooks/useAppHistory.ts +++ b/src/Common/hooks/useAppHistory.ts @@ -3,7 +3,7 @@ import { useContext } from "react"; import { HistoryContext, ResetHistoryContext, -} from "../../CAREUI/misc/HistoryAPIProvider"; +} from "../../Providers/HistoryAPIProvider"; export default function useAppHistory() { const history = useContext(HistoryContext); diff --git a/src/Common/hooks/useFilters.tsx b/src/Common/hooks/useFilters.tsx index 309cdc37787..129a696041b 100644 --- a/src/Common/hooks/useFilters.tsx +++ b/src/Common/hooks/useFilters.tsx @@ -196,7 +196,7 @@ export default function useFilters({ limit = 14 }: { limit?: number }) { FilterBadge, FilterBadges, Pagination, - // TODO: update this props to be compliant with new FiltersSlideOver when #3996 is merged. + advancedFilter: { show: showFilters, setShow: setShowFilters, diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 3d9fa0f6e75..356257f9ac6 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -23,7 +23,7 @@ import { navigate } from "raviger"; import { useDispatch } from "react-redux"; import { useQueryParams } from "raviger"; import { useTranslation } from "react-i18next"; -import { triggerGoal } from "../../Common/Plausible"; +import { triggerGoal } from "../../../Integrations/Plausible"; import useAuthUser from "../../../Common/hooks/useAuthUser"; import { ConsultationUpdatesTab } from "./ConsultationUpdatesTab"; import { ConsultationABGTab } from "./ConsultationABGTab"; diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx index 84b3e62ef9e..75ab72d168c 100644 --- a/src/Components/Facility/Consultations/Feed.tsx +++ b/src/Components/Facility/Consultations/Feed.tsx @@ -30,7 +30,7 @@ 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 { triggerGoal } from "../../Common/Plausible.js"; +import { triggerGoal } from "../../../Integrations/Plausible.js"; import useAuthUser from "../../../Common/hooks/useAuthUser.js"; interface IFeedProps { diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index e15db9f3b40..423edb894c9 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -43,7 +43,7 @@ import useFilters from "../../Common/hooks/useFilters"; import { useTranslation } from "react-i18next"; import Page from "../Common/components/Page.js"; import dayjs from "dayjs"; -import { triggerGoal } from "../Common/Plausible.js"; +import { triggerGoal } from "../../Integrations/Plausible.js"; import useAuthUser from "../../Common/hooks/useAuthUser.js"; const Loading = lazy(() => import("../Common/Loading")); diff --git a/src/Components/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index 3d39f377462..8fdc0f51280 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -30,7 +30,7 @@ import Page from "../Common/components/Page"; import ConfirmDialog from "../Common/ConfirmDialog"; import UserAutocompleteFormField from "../Common/UserAutocompleteFormField"; import dayjs from "../../Utils/dayjs"; -import { triggerGoal } from "../Common/Plausible"; +import { triggerGoal } from "../../Integrations/Plausible"; import useAuthUser from "../../Common/hooks/useAuthUser"; const Loading = lazy(() => import("../Common/Loading")); diff --git a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx index c0b089df8b0..1b45fd80ddc 100644 --- a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx +++ b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx @@ -6,7 +6,7 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import WaveformLabels from "./WaveformLabels"; import { classNames } from "../../Utils/utils"; import { IVitalsComponentProps, VitalsValueBase } from "./types"; -import { triggerGoal } from "../Common/Plausible"; +import { triggerGoal } from "../../Integrations/Plausible"; import useAuthUser from "../../Common/hooks/useAuthUser"; export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) { diff --git a/src/Components/Common/Plausible.tsx b/src/Integrations/Plausible.tsx similarity index 95% rename from src/Components/Common/Plausible.tsx rename to src/Integrations/Plausible.tsx index 77589e0f923..c9b040749d4 100644 --- a/src/Components/Common/Plausible.tsx +++ b/src/Integrations/Plausible.tsx @@ -1,6 +1,6 @@ import { useLocationChange } from "raviger"; -import useConfig from "../../Common/hooks/useConfig"; -import Script from "./Script"; +import useConfig from "../Common/hooks/useConfig"; +import Script from "../Components/Common/Script"; import { useEffect } from "react"; export default function Plausible() { diff --git a/src/Integrations/Sentry.tsx b/src/Integrations/Sentry.tsx new file mode 100644 index 00000000000..466f93487c4 --- /dev/null +++ b/src/Integrations/Sentry.tsx @@ -0,0 +1,25 @@ +import { useEffect } from "react"; +import useConfig from "../Common/hooks/useConfig"; + +interface Props { + disabled?: boolean; +} + +export default function Sentry({ disabled }: Props) { + const { sentry_dsn, sentry_environment } = useConfig(); + + useEffect(() => { + if (disabled || !sentry_dsn) { + return; + } + + import("@sentry/browser").then((Sentry) => { + Sentry.init({ + environment: sentry_environment, + dsn: sentry_dsn, + }); + }); + }, [sentry_dsn, sentry_environment, disabled]); + + return null; +} diff --git a/src/Integrations/index.tsx b/src/Integrations/index.tsx new file mode 100644 index 00000000000..aeb0399a452 --- /dev/null +++ b/src/Integrations/index.tsx @@ -0,0 +1,6 @@ +import Sentry from "./Sentry"; +import Plausible from "./Plausible"; + +const Intergrations = { Sentry, Plausible }; + +export default Intergrations; diff --git a/src/Providers/AppConfigProvider.tsx b/src/Providers/AppConfigProvider.tsx new file mode 100644 index 00000000000..7492a83c126 --- /dev/null +++ b/src/Providers/AppConfigProvider.tsx @@ -0,0 +1,25 @@ +import { AppConfigContext } from "../Common/hooks/useConfig"; +import Loading from "../Components/Common/Loading"; +import routes from "../Redux/api"; +import useQuery from "../Utils/request/useQuery"; + +interface Props { + children: React.ReactNode; +} + +export default function AppConfigProvider({ children }: Props) { + const { data, loading } = useQuery(routes.config, { + refetchOnWindowFocus: false, + prefetch: true, + }); + + if (loading || data === undefined) { + return ; + } + + return ( + + {children} + + ); +} diff --git a/src/Providers/AuthUserProvider.tsx b/src/Providers/AuthUserProvider.tsx new file mode 100644 index 00000000000..64027a17215 --- /dev/null +++ b/src/Providers/AuthUserProvider.tsx @@ -0,0 +1,63 @@ +import { useEffect } from "react"; +import { AuthUserContext } from "../Common/hooks/useAuthUser"; +import Loading from "../Components/Common/Loading"; +import routes from "../Redux/api"; +import useQuery from "../Utils/request/useQuery"; +import { LocalStorageKeys } from "../Common/constants"; +import request from "../Utils/request/request"; + +interface Props { + children: React.ReactNode; + unauthorized: React.ReactNode; +} + +export default function AuthUserProvider({ children, unauthorized }: Props) { + const { res, data, loading } = useQuery(routes.currentUser, { + refetchOnWindowFocus: false, + prefetch: true, + silent: true, + }); + + useEffect(() => { + if (!data) { + return; + } + + updateRefreshToken(true); + setInterval(() => updateRefreshToken(), 5 * 60 * 1000); // TODO: move this interval to config.json + }, [data]); + + if (loading || !res) { + return ; + } + + if (res.status !== 200 || !data) { + return unauthorized; + } + + return ( + {children} + ); +} + +const updateRefreshToken = async (silent = false) => { + const refresh = localStorage.getItem(LocalStorageKeys.refreshToken); + + if (!refresh) { + return; + } + + const { res, data } = await request(routes.token_refresh, { + body: { refresh }, + silent, + }); + + if (res?.status !== 200 || !data) { + localStorage.removeItem(LocalStorageKeys.accessToken); + localStorage.removeItem(LocalStorageKeys.refreshToken); + return; + } + + localStorage.setItem(LocalStorageKeys.accessToken, data.access); + localStorage.setItem(LocalStorageKeys.refreshToken, data.refresh); +}; diff --git a/src/CAREUI/misc/HistoryAPIProvider.tsx b/src/Providers/HistoryAPIProvider.tsx similarity index 93% rename from src/CAREUI/misc/HistoryAPIProvider.tsx rename to src/Providers/HistoryAPIProvider.tsx index e82e379053f..3fa4b6fab86 100644 --- a/src/CAREUI/misc/HistoryAPIProvider.tsx +++ b/src/Providers/HistoryAPIProvider.tsx @@ -5,7 +5,7 @@ export const HistoryContext = createContext([]); // eslint-disable-next-line @typescript-eslint/no-empty-function export const ResetHistoryContext = createContext(() => {}); -export const HistoryAPIProvider = (props: { children: ReactNode }) => { +export default function HistoryAPIProvider(props: { children: ReactNode }) { const [history, setHistory] = useState([]); useLocationChange( @@ -36,4 +36,4 @@ export const HistoryAPIProvider = (props: { children: ReactNode }) => { ); -}; +} diff --git a/src/Providers/index.tsx b/src/Providers/index.tsx new file mode 100644 index 00000000000..728ee9e1f91 --- /dev/null +++ b/src/Providers/index.tsx @@ -0,0 +1,5 @@ +import AppConfigProvider from "./AppConfigProvider"; +import AuthUserProvider from "./AuthUserProvider"; +import HistoryAPIProvider from "./HistoryAPIProvider"; + +export { AppConfigProvider, AuthUserProvider, HistoryAPIProvider }; diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index b98a099f439..cb124b5bb80 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -1,4 +1,6 @@ +import { IConfig } from "../Common/hooks/useConfig"; import { LocationModel } from "../Components/Facility/models"; +import { UserModel } from "../Components/Users/models"; import { PaginatedResponse } from "../Utils/request/types"; /** @@ -9,11 +11,17 @@ function Res(): T { return {} as T; } +interface JwtTokenObtainPair { + access: string; + refresh: string; +} + const routes = { config: { path: import.meta.env.REACT_APP_CONFIG ?? "/config.json", method: "GET", noAuth: true, + TRes: Res(), }, // Auth Endpoints @@ -24,8 +32,9 @@ const routes = { }, token_refresh: { - path: "/api/v1/auth/token/refresh", + path: "/api/v1/auth/token/refresh/", method: "POST", + TRes: Res(), }, token_verify: { @@ -55,6 +64,7 @@ const routes = { // User Endpoints currentUser: { path: "/api/v1/users/getcurrentuser/", + TRes: Res(), }, userList: { diff --git a/src/Router/AppRouter.tsx b/src/Routers/AppRouter.tsx similarity index 100% rename from src/Router/AppRouter.tsx rename to src/Routers/AppRouter.tsx diff --git a/src/Router/SessionRouter.tsx b/src/Routers/SessionRouter.tsx similarity index 100% rename from src/Router/SessionRouter.tsx rename to src/Routers/SessionRouter.tsx diff --git a/src/Routers/index.tsx b/src/Routers/index.tsx new file mode 100644 index 00000000000..8a27df7b59d --- /dev/null +++ b/src/Routers/index.tsx @@ -0,0 +1,6 @@ +import SessionRouter from "./SessionRouter"; +import AppRouter from "./AppRouter"; + +const routers = { SessionRouter, AppRouter }; + +export default routers; diff --git a/src/Utils/request/handleResponse.ts b/src/Utils/request/handleResponse.ts new file mode 100644 index 00000000000..2ecad95ac88 --- /dev/null +++ b/src/Utils/request/handleResponse.ts @@ -0,0 +1,38 @@ +import { RequestResult } from "./types"; +import * as Notifications from "../Notifications"; +import { navigate } from "raviger"; + +export default function handleResponse( + { res, error }: RequestResult, + silent?: boolean +) { + const notify = silent ? undefined : Notifications; + + if (res === undefined) { + return; + } + + // 5xx errors + if (res.status >= 500 && res.status < 600) { + console.error("Server error", res); + notify?.Error({ msg: "Something went wrong...!" }); + return; + } + + // 400/406 Bad Request + if (res.status === 400 || res.status === 406) { + notify?.BadRequest({ errs: error }); + return; + } + + // Other 400 Errors + if (res.status >= 400) { + // Invalid token + if (!silent && error?.code === "token_not_valid") { + navigate("/session-expired"); + } + + notify?.Error({ msg: error?.detail || "Something went wrong...!" }); + return; + } +} diff --git a/src/Utils/request/request.ts b/src/Utils/request/request.ts index 2dc938fa6f1..45c8e5ce664 100644 --- a/src/Utils/request/request.ts +++ b/src/Utils/request/request.ts @@ -1,4 +1,5 @@ -import { RequestOptions, Route } from "./types"; +import handleResponse from "./handleResponse"; +import { RequestOptions, RequestResult, Route } from "./types"; import { makeHeaders, makeUrl } from "./utils"; interface Options extends RequestOptions { @@ -7,21 +8,56 @@ interface Options extends RequestOptions { export default async function request( { path, method, noAuth }: Route, - { query, body, pathParams, controller }: Options = {} -) { + { + query, + body, + pathParams, + controller, + onResponse, + silent, + reattempts = 3, + }: Options = {} +): Promise> { const signal = controller?.signal; - - const headers = makeHeaders(noAuth ?? false); const url = makeUrl(path, query, pathParams); - const options: RequestInit = { headers, method, signal }; + const options: RequestInit = { method, signal }; if (body) { options.body = JSON.stringify(body); } - const res = await fetch(url, options); - const data: TData = await res.json(); + let result: RequestResult = { + res: undefined, + data: undefined, + error: undefined, + }; + + for (let i = 0; i < reattempts + 1; i++) { + options.headers = makeHeaders(noAuth ?? false); + + try { + const res = await fetch(url, options); + const data: TData = await res.json(); + + result = { + res, + data: res.ok ? data : undefined, + error: res.ok ? undefined : (data as Record), + }; + + onResponse?.(result); + handleResponse(result, silent); + + return result; + } catch (error: any) { + result = { error, res: undefined, data: undefined }; + } + } - return { res, data }; + console.error( + `Request failed after ${reattempts + 1} attempts`, + result.error + ); + return result; } diff --git a/src/Utils/request/types.ts b/src/Utils/request/types.ts index e7f0f9544a3..7f01ef91d9c 100644 --- a/src/Utils/request/types.ts +++ b/src/Utils/request/types.ts @@ -18,10 +18,19 @@ export interface MutationRoute extends RouteBase { export type Route = QueryRoute | MutationRoute; -export interface RequestOptions { +export interface RequestResult { + res: Response | undefined; + data: TData | undefined; + error: undefined | Record; +} + +export interface RequestOptions { query?: QueryParams; body?: object; pathParams?: Record; + onResponse?: (res: RequestResult) => void; + silent?: boolean; + reattempts?: number; } export interface PaginatedResponse { diff --git a/src/Utils/request/useQuery.ts b/src/Utils/request/useQuery.ts index e459a579e3e..a370b909fe9 100644 --- a/src/Utils/request/useQuery.ts +++ b/src/Utils/request/useQuery.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { QueryRoute, RequestOptions } from "./types"; +import { QueryRoute, RequestOptions, RequestResult } from "./types"; import request from "./request"; import { mergeRequestOptions } from "./utils"; @@ -12,9 +12,7 @@ export default function useQuery( route: QueryRoute, options?: QueryOptions ) { - const [res, setRes] = useState(); - const [data, setData] = useState(); - const [error, setError] = useState(); + const [response, setResponse] = useState>(); const [loading, setLoading] = useState(false); const controllerRef = useRef(); @@ -32,20 +30,8 @@ export default function useQuery( : 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); - } + setResponse(await request(route, resolvedOptions)); + setLoading(false); }, [route, JSON.stringify(options)] ); @@ -66,5 +52,5 @@ export default function useQuery( } }, [runQuery, options?.refetchOnWindowFocus]); - return { res, data, error, loading, refetch: runQuery }; + return { ...response, loading, refetch: runQuery }; } diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts index 21236a8145f..5a4fb7e2b2e 100644 --- a/src/Utils/request/utils.ts +++ b/src/Utils/request/utils.ts @@ -74,8 +74,17 @@ export function mergeRequestOptions( overrides: RequestOptions ): RequestOptions { return { + ...options, + ...overrides, + query: { ...options.query, ...overrides.query }, body: { ...options.body, ...overrides.body }, pathParams: { ...options.pathParams, ...overrides.pathParams }, + + onResponse: (res) => { + options.onResponse?.(res); + overrides.onResponse?.(res); + }, + silent: overrides.silent || options.silent, }; } From 35de3f107c1432acaa4481572a191fa6714b8869 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 21 Sep 2023 10:50:09 +0530 Subject: [PATCH 05/16] Added documentation on `request` and `useQuery` (#6315) --- src/Utils/request/README.md | 142 ++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/Utils/request/README.md diff --git a/src/Utils/request/README.md b/src/Utils/request/README.md new file mode 100644 index 00000000000..a9948d48f41 --- /dev/null +++ b/src/Utils/request/README.md @@ -0,0 +1,142 @@ +# CARE's data fetching utilities: `useQuery` and `request` + +There are two main ways to fetch data in CARE: `useQuery` and `request`. Both of these utilities are built on top of [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). + +## `useQuery` + +`useQuery` is a React hook that allows you to fetch data and automatically update the UI when the data changes. It is +a wrapper around `request` that is designed to be used in React components. Only "GET" requests are supported with `useQuery`. For other request methods (mutations), use `request`. + +### Usage + +```jsx +import { useQuery } from "@care/request"; +import FooRoutes from "@foo/routes"; + +export default function FooDetails({ children, id }) { + const { res, data, loading, error } = useQuery(FooRoutes.getFoo, { + pathParams: { id }, + }); + + /* 🪄 Here typeof data is automatically inferred from the specified route. */ + + if (loading) return ; + + if (res.status === 403) { + navigate("/forbidden"); + return null; + } + + if (error) { + return ; + } + + return ( +
+ {data.id} + {data.name} +
+ ); +} +``` + +### API + +```ts +useQuery(route: Route, options?: QueryOptions): ReturnType; +``` + +#### `route` + +A route object that specifies the endpoint to fetch data from. + +```ts +const FooRoutes = { + getFoo: { + path: "/api/v1/foo/{id}/", // 👈 The path to the endpoint. Slug parameters can be specified using curly braces. + + method: "GET", // 👈 The HTTP method to use. Optional; defaults to "GET". + TRes: Res, // 👈 The type of the response body (for type inference). + noAuth: true, // 👈 Whether to skip adding the Authorization header to the request. + }, +} as const; // 👈 This is important for type inference to work properly. +``` + +#### `options` + +An object that specifies options for the request. + +```ts +const options = { + prefetch: true, // 👈 Whether to prefetch the data when the component mounts. + refetchOnWindowFocus: true, // 👈 Whether to refetch the data when the window regains focus. + + // The following options are passed directly to the underlying `request` function. + + pathParams: { id: "123" }, // 👈 The slug parameters to use in the path. + // If you accidentally forget to specify a slug parameter an error will be + // thrown before the request is made. + + query: { limit: 10 }, // 👈 The query parameters to be added to the request URL. + body: { name: "foo" }, // 👈 The body to be sent with the request. + headers: { "X-Foo": "bar" }, // 👈 Additional headers to be sent with the request. (Coming soon...) + + silent: true, // 👈 Whether to suppress notifications for this request. + // This is useful for requests that are made in the background. + + reattempts: 3, // 👈 The number of times to retry the request if it fails. + // Reattempts are only made if the request fails due to a network error. Responses with + // status codes in the 400s and 500s are not retried. + + onResponse: (res) => { // 👈 An optional callback that is called after the response is received. + if (res.status === 403) { + navigate("/forbidden"); + } + }, + // This is useful for handling responses with status codes in the 400s and 500s for a specific request. +}; +``` + +#### `ReturnType` + +The `useQuery` hook returns an object with the following properties: + +```ts +{ + res: Res | undefined; // 👈 The response object. `undefined` if the request has not been made yet. + + data: TRes | null; // 👈 The response body. `null` if the request has not been made yet. + + error: any; // 👈 The error that occurred while making the request if any. + + loading: boolean; // 👈 Whether the request is currently in progress. + + refetch: () => void; // 👈 A function that can be called to refetch the data. + // Ideal for revalidating stale data after a mutation. +} +``` + +## `request` + +`request` is a function that allows you to fetch data. It is a wrapper around `fetch` that adds some useful features. It can be used in both React components and non-React code. For fetching data in React components, prefer using `useQuery`. For mutations, use `request`. + +### `request` usage + +```ts +import { request } from "@care/request"; +import FooRoutes from "@foo/routes"; + +export default async function updateFoo(id: string, object: Foo) { + const { res, data } = await request(FooRoutes.updateFoo, { + pathParams: { id }, + body: object, // 👈 The body is automatically serialized to JSON. + }); + + if (res.status === 403) { + navigate("/forbidden"); + return null; + } + + return data; +} +``` From 5009a86abfb96e9b21f8371635176e4c3f73c5a7 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 21 Sep 2023 11:24:21 +0530 Subject: [PATCH 06/16] Prescriptions: Shrink discontinued prescriptions + Flip MAR timeline + Freeze primary columns in horizontal scroll (#6282) * MAR Table: reverse adm. intervals (#6177) * MAR Table: freeze columns (fixes #6177) * MAR Table: shrink discontinued medicines (#6151) * MAR Table: fix column freeze * fix discontinued button alignment * fix reversed slots and improper snapping * fix mobile responsiveness --- src/Common/hooks/useRangePagination.ts | 32 ++- .../PrescriptionAdministrationsTable.tsx | 266 +++++++++++------- src/Redux/actions.tsx | 2 +- 3 files changed, 180 insertions(+), 120 deletions(-) diff --git a/src/Common/hooks/useRangePagination.ts b/src/Common/hooks/useRangePagination.ts index 7652ae546c1..e6bbe9f573e 100644 --- a/src/Common/hooks/useRangePagination.ts +++ b/src/Common/hooks/useRangePagination.ts @@ -9,17 +9,18 @@ interface Props { bounds: DateRange; perPage: number; slots?: number; - defaultEnd?: boolean; + snapToLatest?: boolean; + reverse?: boolean; } const useRangePagination = ({ bounds, perPage, ...props }: Props) => { const [currentRange, setCurrentRange] = useState( - getInitialBounds(bounds, perPage, props.defaultEnd) + getInitialBounds(bounds, perPage, props.snapToLatest) ); useEffect(() => { - setCurrentRange(getInitialBounds(bounds, perPage, props.defaultEnd)); - }, [bounds, perPage, props.defaultEnd]); + setCurrentRange(getInitialBounds(bounds, perPage, props.snapToLatest)); + }, [bounds, perPage, props.snapToLatest]); const next = () => { const { end } = currentRange; @@ -62,17 +63,24 @@ const useRangePagination = ({ bounds, perPage, ...props }: Props) => { } const slots: DateRange[] = []; - const { start } = currentRange; + const { start, end } = currentRange; const delta = perPage / props.slots; for (let i = 0; i < props.slots; i++) { - slots.push({ - start: new Date(start.valueOf() + delta * i), - end: new Date(start.valueOf() + delta * (i + 1)), - }); + if (props.snapToLatest) { + slots.push({ + start: new Date(end.valueOf() - delta * (i - 1)), + end: new Date(end.valueOf() - delta * i), + }); + } else { + slots.push({ + start: new Date(start.valueOf() + delta * i), + end: new Date(start.valueOf() + delta * (i + 1)), + }); + } } - return slots; + return props.reverse ? slots.reverse() : slots; }, [currentRange, props.slots, perPage]); return { @@ -90,7 +98,7 @@ export default useRangePagination; const getInitialBounds = ( bounds: DateRange, perPage: number, - defaultEnd?: boolean + snapToLatest?: boolean ) => { const deltaBounds = bounds.end.valueOf() - bounds.start.valueOf(); @@ -98,7 +106,7 @@ const getInitialBounds = ( return bounds; } - if (defaultEnd) { + if (snapToLatest) { return { start: new Date(bounds.end.valueOf() - perPage), end: bounds.end, diff --git a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx index 81282126d7c..470caa1042b 100644 --- a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx +++ b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx @@ -47,6 +47,10 @@ export default function PrescriptionAdministrationsTable({ const { t } = useTranslation(); const [state, setState] = useState(); + + const [showDiscontinued, setShowDiscontinued] = useState(false); + const [discontinuedCount, setDiscontinuedCount] = useState(); + const pagination = useRangePagination({ bounds: state?.administrationsTimeBounds ?? { start: new Date(), @@ -54,7 +58,8 @@ export default function PrescriptionAdministrationsTable({ }, perPage: 24 * 60 * 60 * 1000, slots: 24, - defaultEnd: true, + snapToLatest: true, + reverse: true, }); const [showBulkAdminister, setShowBulkAdminister] = useState(false); @@ -64,8 +69,13 @@ export default function PrescriptionAdministrationsTable({ ); const refetch = useCallback(async () => { + const filters = { + is_prn: prn, + prescription_type: "REGULAR", + }; + const res = await dispatch( - list({ is_prn: prn, prescription_type: "REGULAR" }) + list(showDiscontinued ? filters : { ...filters, discontinued: false }) ); setState({ @@ -74,7 +84,14 @@ export default function PrescriptionAdministrationsTable({ ), administrationsTimeBounds: getAdministrationBounds(res.data.results), }); - }, [consultation_id, dispatch]); + + if (showDiscontinued === false) { + const discontinuedRes = await dispatch( + list({ ...filters, discontinued: true, limit: 0 }) + ); + setDiscontinuedCount(discontinuedRes.data.count); + } + }, [consultation_id, showDiscontinued, dispatch]); useEffect(() => { refetch(); @@ -141,17 +158,22 @@ export default function PrescriptionAdministrationsTable({ } /> -
- +
+
- - - )) - : pagination.slots?.map(({ start, end }, index) => ( - - ))} + : pagination.slots + ?.map(({ start, end }, index) => ( + + )) + .reverse()}
{t("medicine")} -

Dosage &

-

- {!state?.prescriptions[0]?.is_prn ? "Frequency" : "Indicator"} -

+
+
+ {t("medicine")} + +

Dosage &

+

+ {!state?.prescriptions[0]?.is_prn + ? "Frequency" + : "Indicator"} +

+
+
@@ -162,8 +184,10 @@ export default function PrescriptionAdministrationsTable({ border className="mx-2 px-1" variant="secondary" - disabled={!pagination.hasPrevious} - onClick={pagination.previous} + disabled={!pagination.hasNext} + onClick={pagination.next} + tooltip="Next 24 hours" + tooltipClassName="tooltip-bottom -translate-x-1/2 text-xs" > @@ -177,24 +201,26 @@ export default function PrescriptionAdministrationsTable({

-

{formatDateTime(start, "DD/MM")}

-

{formatDateTime(start, "HH:mm")}

- - - Administration(s) between -
- {formatTime(start)} and{" "} - {formatTime(end)} -
- on {formatDate(start)} -
-
+

{formatDateTime(end, "DD/MM")}

+

{formatDateTime(end, "HH:mm")}

+ + + Administration(s) between +
+ {formatTime(start)} and{" "} + {formatTime(end)} +
+ on {formatDate(start)} +
+
@@ -227,6 +255,23 @@ export default function PrescriptionAdministrationsTable({
+ {showDiscontinued === false && !!discontinuedCount && ( + setShowDiscontinued(true)} + > + + + + Show {discontinuedCount} other discontinued + prescription(s) + + + + )} + {state?.prescriptions.length === 0 && (
@@ -283,12 +328,7 @@ const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { }, [prescription.id, dispatch, props.intervals]); return ( - + <> {showDiscontinue && ( {
)} - setShowDetails(true)} + -
- - {prescription.medicine_object?.name ?? prescription.medicine_old} - + setShowDetails(true)} + > +
+
+ + {prescription.medicine_object?.name ?? + prescription.medicine_old} + - {prescription.discontinued && ( - - {t("discontinued")} - - )} + {prescription.discontinued && ( + + {t("discontinued")} + + )} - {prescription.route && ( - - {t(prescription.route)} - - )} -
- - - -

{prescription.dosage}

-

- {!prescription.is_prn - ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) - : prescription.indicator} -

- - - - {/* Administration Cells */} - {props.intervals.map(({ start, end }, index) => ( - - {administrations === undefined ? ( - - ) : ( - - )} + {prescription.route && ( + + {t(prescription.route)} + + )} +
+ +
+

{prescription.dosage}

+

+ {!prescription.is_prn + ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) + : prescription.indicator} +

+
+
- ))} - - - {/* Action Buttons */} - - setShowAdminister(true)} - > - {t("administer")} - - - + + + {/* Administration Cells */} + {props.intervals + .map(({ start, end }, index) => ( + + {administrations === undefined ? ( + + ) : ( + + )} + + )) + .reverse()} + + + {/* Action Buttons */} + + setShowAdminister(true)} + > + {t("administer")} + + + + ); }; diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 6e0d91fc59d..1a5bd7e4fb1 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1003,7 +1003,7 @@ export const PrescriptionActions = (consultation_external_id: string) => { const pathParams = { consultation_external_id }; return { - list: (query?: Partial) => { + list: (query?: Record) => { let altKey; if (query?.is_prn !== undefined) { altKey = query?.is_prn From f4bddb950bc0ce8b6af631373c56d34ce4d60acb Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Thu, 21 Sep 2023 11:31:56 +0530 Subject: [PATCH 07/16] Fix action field in consultation form (#6314) --- src/Components/Facility/ConsultationForm.tsx | 111 +++++++++---------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index ca2301b5964..6c7e8a6760e 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -97,7 +97,7 @@ type FormDetails = { procedure: ProcedureType[]; investigation: InvestigationType[]; is_telemedicine: BooleanStrings; - action?: string; + action?: number; assigned_to: string; assigned_to_object: UserModel | null; special_instruction: string; @@ -143,7 +143,7 @@ const initForm: FormDetails = { procedure: [], investigation: [], is_telemedicine: "false", - action: "NO_ACTION", + action: 10, assigned_to: "", assigned_to_object: null, special_instruction: "", @@ -279,15 +279,14 @@ export const ConsultationForm = (props: any) => { setIsLoading(true); const res = await dispatchAction(getPatient({ id: patientId })); if (res.data) { - setPatientName(res.data.name); - setFacilityName(res.data.facility_object.name); if (isUpdate) { - const form = { ...state.form }; - form.action = TELEMEDICINE_ACTIONS.find( - (a) => a.id === res.data.action - )?.text; - dispatch({ type: "set_form", form }); + dispatch({ + type: "set_form", + form: { ...state.form, action: res.data.action }, + }); } + setPatientName(res.data.name); + setFacilityName(res.data.facility_object.name); } } else { setPatientName(""); @@ -302,6 +301,49 @@ export const ConsultationForm = (props: any) => { !!state.form.symptoms.length && !state.form.symptoms.includes(1); const isOtherSymptomsSelected = state.form.symptoms.includes(9); + const handleFormFieldChange: FieldChangeEventHandler = (event) => { + if (event.name === "consultation_status" && event.value === "1") { + dispatch({ + type: "set_form", + form: { + ...state.form, + consultation_status: 1, + symptoms: [1], + symptoms_onset_date: new Date(), + category: "Critical", + suggestion: "DD", + }, + }); + } else if (event.name === "suggestion" && event.value === "DD") { + dispatch({ + type: "set_form", + form: { + ...state.form, + suggestion: "DD", + consultation_notes: "Patient declared dead", + verified_by: "Declared Dead", + }, + }); + } else if ( + event.name === "icd11_diagnoses_object" || + event.name === "icd11_provisional_diagnoses_object" + ) { + dispatch({ + type: "set_form", + form: { + ...state.form, + [event.name]: event.value, + icd11_principal_diagnosis: undefined, + }, + }); + } else { + dispatch({ + type: "set_form", + form: { ...state.form, [event.name]: event.value }, + }); + } + }; + const fetchData = useCallback( async (status: statusType) => { if (!patientId) setIsLoading(true); @@ -352,7 +394,7 @@ export const ConsultationForm = (props: any) => { death_confirmed_doctor: res.data?.death_confirmed_doctor || "", InvestigationAdvice: res.data.investigation, }; - dispatch({ type: "set_form", form: formData }); + dispatch({ type: "set_form", form: { ...state.form, ...formData } }); setBed(formData.bed); if (res.data.last_daily_round) { @@ -364,7 +406,7 @@ export const ConsultationForm = (props: any) => { setIsLoading(false); } }, - [dispatchAction, id] + [dispatchAction, id, patientName, patientId] ); useAbortableEffect( @@ -745,49 +787,6 @@ export const ConsultationForm = (props: any) => { } }; - const handleFormFieldChange: FieldChangeEventHandler = (event) => { - if (event.name === "consultation_status" && event.value === "1") { - dispatch({ - type: "set_form", - form: { - ...state.form, - consultation_status: 1, - symptoms: [1], - symptoms_onset_date: new Date(), - category: "Critical", - suggestion: "DD", - }, - }); - } else if (event.name === "suggestion" && event.value === "DD") { - dispatch({ - type: "set_form", - form: { - ...state.form, - suggestion: "DD", - consultation_notes: "Patient declared dead", - verified_by: "Declared Dead", - }, - }); - } else if ( - event.name === "icd11_diagnoses_object" || - event.name === "icd11_provisional_diagnoses_object" - ) { - dispatch({ - type: "set_form", - form: { - ...state.form, - [event.name]: event.value, - icd11_principal_diagnosis: undefined, - }, - }); - } else { - dispatch({ - type: "set_form", - form: { ...state.form, [event.name]: event.value }, - }); - } - }; - const handleDoctorSelect = (event: FieldChangeEvent) => { if (event.value?.id) { dispatch({ @@ -1347,12 +1346,12 @@ export const ConsultationForm = (props: any) => {
option.desc} - optionValue={(option) => option.text} + optionDescription={() => ""} />
From c0f0e4b0e4beaf2c7093667be80b7cd71efa8323 Mon Sep 17 00:00:00 2001 From: Pranshu Aggarwal <70687348+Pranshu1902@users.noreply.github.com> Date: Thu, 21 Sep 2023 19:54:42 +0530 Subject: [PATCH 08/16] fix loader (#6204) Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> --- src/Components/Patient/PatientNotes.tsx | 111 ++++++++++++------------ 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/src/Components/Patient/PatientNotes.tsx b/src/Components/Patient/PatientNotes.tsx index b7993ed1ea3..68a5ab4dc5d 100644 --- a/src/Components/Patient/PatientNotes.tsx +++ b/src/Components/Patient/PatientNotes.tsx @@ -101,10 +101,6 @@ const PatientNotes = (props: PatientNotesProps) => { }); }; - if (isLoading) { - return ; - } - return (
{

Add new notes