diff --git a/cypress/e2e/assets_spec/asset_homepage.cy.ts b/cypress/e2e/assets_spec/asset_homepage.cy.ts index 61f7d8a52c0..a2c1c7734d2 100644 --- a/cypress/e2e/assets_spec/asset_homepage.cy.ts +++ b/cypress/e2e/assets_spec/asset_homepage.cy.ts @@ -27,6 +27,7 @@ describe("Asset Tab", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/assets"); }); @@ -90,7 +91,7 @@ describe("Asset Tab", () => { assetPage.selectImportOption(); assetPage.selectImportFacility("Dummy Facility 1"); assetPage.importAssetFile(); - assetPage.selectImportLocation("Camera Locations"); + assetPage.selectImportLocation("Camera Loc"); assetPage.clickImportAsset(); }); diff --git a/cypress/e2e/assets_spec/assets_creation.cy.ts b/cypress/e2e/assets_spec/assets_creation.cy.ts index 3e7b24eeca6..dde4e0d0e8b 100644 --- a/cypress/e2e/assets_spec/assets_creation.cy.ts +++ b/cypress/e2e/assets_spec/assets_creation.cy.ts @@ -19,6 +19,7 @@ describe("Asset", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/assets"); }); diff --git a/cypress/e2e/assets_spec/assets_manage.cy.ts b/cypress/e2e/assets_spec/assets_manage.cy.ts index 164bc239253..92a4f5be9b8 100644 --- a/cypress/e2e/assets_spec/assets_manage.cy.ts +++ b/cypress/e2e/assets_spec/assets_manage.cy.ts @@ -29,6 +29,7 @@ describe("Asset", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/assets"); }); diff --git a/cypress/e2e/death_report_spec/death_report.cy.ts b/cypress/e2e/death_report_spec/death_report.cy.ts index a9e7eb657d8..c051e1ee776 100644 --- a/cypress/e2e/death_report_spec/death_report.cy.ts +++ b/cypress/e2e/death_report_spec/death_report.cy.ts @@ -11,6 +11,7 @@ describe("Death Report", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/"); cy.intercept("**/api/v1/patient/**").as("getPatients"); cy.get("#facility-patients").contains("Patients").click({ force: true }); diff --git a/cypress/e2e/external_results_spec/external_result.cy.ts b/cypress/e2e/external_results_spec/external_result.cy.ts index 38f7c31373a..deb92ae5171 100644 --- a/cypress/e2e/external_results_spec/external_result.cy.ts +++ b/cypress/e2e/external_results_spec/external_result.cy.ts @@ -16,6 +16,7 @@ describe("Edit Profile Testing", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/external_results"); }); diff --git a/cypress/e2e/external_results_spec/filter.cy.ts b/cypress/e2e/external_results_spec/filter.cy.ts index 8654bb49e63..577b6a46d98 100644 --- a/cypress/e2e/external_results_spec/filter.cy.ts +++ b/cypress/e2e/external_results_spec/filter.cy.ts @@ -8,6 +8,7 @@ describe("External Results Filters", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/external_results"); cy.contains("Filters").click(); }); diff --git a/cypress/e2e/facility_spec/facility_creation.cy.ts b/cypress/e2e/facility_spec/facility_creation.cy.ts index cc2d1b7f921..9fe78f84256 100644 --- a/cypress/e2e/facility_spec/facility_creation.cy.ts +++ b/cypress/e2e/facility_spec/facility_creation.cy.ts @@ -217,6 +217,7 @@ describe("Facility Creation", () => { .should("be.visible"); // verify the facility homepage cy.visit("/facility"); + cy.clearAllFilters(); manageUserPage.typeFacilitySearch(facilityName); facilityPage.verifyFacilityBadgeContent(facilityName); manageUserPage.assertFacilityInCard(facilityName); diff --git a/cypress/e2e/facility_spec/inventory.cy.ts b/cypress/e2e/facility_spec/inventory.cy.ts index c28035dd885..79077d6e6a6 100644 --- a/cypress/e2e/facility_spec/inventory.cy.ts +++ b/cypress/e2e/facility_spec/inventory.cy.ts @@ -13,6 +13,7 @@ describe("Inventory Management Section", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/"); cy.viewport(1280, 720); }); diff --git a/cypress/e2e/facility_spec/locations.cy.ts b/cypress/e2e/facility_spec/locations.cy.ts index cac9adbdde1..59651c7c7c9 100644 --- a/cypress/e2e/facility_spec/locations.cy.ts +++ b/cypress/e2e/facility_spec/locations.cy.ts @@ -9,6 +9,7 @@ describe("Location Management Section", () => { beforeEach(() => { cy.viewport(1280, 720); cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/"); cy.intercept("GET", "**/api/v1/facility/**").as("getFacilities"); cy.get("[id='facility-details']").first().click(); @@ -23,6 +24,8 @@ describe("Location Management Section", () => { cy.contains("Add New Location").click(); cy.get("[name='name']").type("Test Location"); cy.get("textarea[name='description']").type("Test Description"); + cy.get("#location-type").click(); + cy.get("#location-type-option-ICU").click(); cy.intercept(/\/api\/v1\/facility\/[\w-]+\/asset_location\//).as( "addLocation" ); diff --git a/cypress/e2e/patient_spec/patient_crud.cy.ts b/cypress/e2e/patient_spec/patient_crud.cy.ts index 57bae83a880..b9843c1556d 100644 --- a/cypress/e2e/patient_spec/patient_crud.cy.ts +++ b/cypress/e2e/patient_spec/patient_crud.cy.ts @@ -27,6 +27,7 @@ describe("Patient Creation with consultation", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/patients"); }); diff --git a/cypress/e2e/patient_spec/patient_manage.cy.ts b/cypress/e2e/patient_spec/patient_manage.cy.ts index e8d381286aa..89d00b6bb9f 100644 --- a/cypress/e2e/patient_spec/patient_manage.cy.ts +++ b/cypress/e2e/patient_spec/patient_manage.cy.ts @@ -15,6 +15,7 @@ describe("Patient", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/patients"); }); diff --git a/cypress/e2e/resource_spec/filter.cy.ts b/cypress/e2e/resource_spec/filter.cy.ts index e4db00050e1..c457f867ed4 100644 --- a/cypress/e2e/resource_spec/filter.cy.ts +++ b/cypress/e2e/resource_spec/filter.cy.ts @@ -8,12 +8,13 @@ describe("Resource filter", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/resource"); - cy.contains("Filters").click(); }); it("filter by origin facility", () => { cy.intercept(/\/api\/v1\/getallfacilities/).as("facilities_filter"); + cy.contains("Filters").click(); cy.get("[name='origin_facility']") .type("Dummy Facility 1") .wait("@facilities_filter"); @@ -23,6 +24,7 @@ describe("Resource filter", () => { it("filter by resource approval facility", () => { cy.intercept(/\/api\/v1\/getallfacilities/).as("facilities_filter"); + cy.contains("Filters").click(); cy.get("[name='approving_facility']") .type("Dummy Shifting Center") .wait("@facilities_filter"); @@ -32,6 +34,7 @@ describe("Resource filter", () => { it("filter by assigned facility", () => { cy.intercept(/\/api\/v1\/getallfacilities/).as("facilities_filter"); + cy.contains("Filters").click(); cy.get("[name='assigned_facility']").type("Dummy Shifting Center"); cy.wait("@facilities_filter"); cy.get("[role='option']").first().click(); @@ -45,26 +48,27 @@ describe("Resource filter", () => { "DESC Modified Date", "ASC Created Date", ].forEach((option) => { + cy.contains("Filters").click(); cy.get("div [id='ordering'] > div > button").click(); cy.get("li").contains(option).click(); cy.intercept(/\/api\/v1\/resource/).as("resource_filter"); cy.contains("Apply").click().wait("@resource_filter"); - cy.contains("Filters").click(); }); }); it("filter by emergency case", () => { ["yes", "no"].forEach((option) => { + cy.contains("Filters").click(); cy.get("div [id='emergency'] > div > button").click(); cy.get("li").contains(option).click(); cy.intercept(/\/api\/v1\/resource/).as("resource_filter"); cy.contains("Apply").click().wait("@resource_filter"); - cy.contains("Filters").click(); }); }); it("filter by created date", () => { cy.intercept(/\/api\/v1\/resource/).as("resource_filter"); + cy.contains("Filters").click(); cy.get("input[name='created_date_start']").click(); cy.get("#date-1").click(); cy.get("#date-1").click(); @@ -74,6 +78,7 @@ describe("Resource filter", () => { it("filter by modified date", () => { cy.intercept(/\/api\/v1\/resource/).as("resource_filter"); + cy.contains("Filters").click(); cy.get("input[name='modified_date_start']").click(); cy.get("#date-1").click(); cy.get("#date-1").click(); @@ -82,8 +87,7 @@ describe("Resource filter", () => { }); afterEach(() => { - cy.contains("Filters").click({ force: true }); - cy.contains("Clear").click(); + cy.clearAllFilters(); cy.saveLocalStorage(); }); }); diff --git a/cypress/e2e/resource_spec/resources.cy.ts b/cypress/e2e/resource_spec/resources.cy.ts index acf179db120..a774023059f 100644 --- a/cypress/e2e/resource_spec/resources.cy.ts +++ b/cypress/e2e/resource_spec/resources.cy.ts @@ -17,6 +17,7 @@ describe("Resource Page", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/resource"); }); diff --git a/cypress/e2e/sample_test_spec/filter.cy.ts b/cypress/e2e/sample_test_spec/filter.cy.ts index 8fe8fa50b81..a015d1ba7c5 100644 --- a/cypress/e2e/sample_test_spec/filter.cy.ts +++ b/cypress/e2e/sample_test_spec/filter.cy.ts @@ -8,6 +8,7 @@ describe("Sample Filter", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/sample"); cy.contains("Advanced Filters").click(); }); diff --git a/cypress/e2e/sample_test_spec/sample_test.cy.ts b/cypress/e2e/sample_test_spec/sample_test.cy.ts index ec12abbfdfb..1a134fffdac 100644 --- a/cypress/e2e/sample_test_spec/sample_test.cy.ts +++ b/cypress/e2e/sample_test_spec/sample_test.cy.ts @@ -8,6 +8,7 @@ describe("Sample List", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/sample"); }); diff --git a/cypress/e2e/shifting_spec/filter.cy.ts b/cypress/e2e/shifting_spec/filter.cy.ts index 3790198a4db..a142a657013 100644 --- a/cypress/e2e/shifting_spec/filter.cy.ts +++ b/cypress/e2e/shifting_spec/filter.cy.ts @@ -11,6 +11,7 @@ describe("Shifting section filter", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/shifting"); shiftingPage.advancedFilterButton().click(); }); diff --git a/cypress/e2e/shifting_spec/shifting.cy.ts b/cypress/e2e/shifting_spec/shifting.cy.ts index bc78e55f175..9cdecc59990 100644 --- a/cypress/e2e/shifting_spec/shifting.cy.ts +++ b/cypress/e2e/shifting_spec/shifting.cy.ts @@ -8,6 +8,7 @@ describe("Shifting Page", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/shifting"); }); diff --git a/cypress/e2e/users_spec/user_creation.cy.ts b/cypress/e2e/users_spec/user_creation.cy.ts index 9090f72505a..55d4b3bef1a 100644 --- a/cypress/e2e/users_spec/user_creation.cy.ts +++ b/cypress/e2e/users_spec/user_creation.cy.ts @@ -63,6 +63,7 @@ describe("User Creation", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/users"); }); diff --git a/cypress/e2e/users_spec/user_homepage.cy.ts b/cypress/e2e/users_spec/user_homepage.cy.ts index 060132105e6..3a633bd65a4 100644 --- a/cypress/e2e/users_spec/user_homepage.cy.ts +++ b/cypress/e2e/users_spec/user_homepage.cy.ts @@ -19,6 +19,7 @@ describe("User Homepage", () => { beforeEach(() => { cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); cy.awaitUrl("/users"); }); diff --git a/cypress/e2e/users_spec/user_manage.cy.ts b/cypress/e2e/users_spec/user_manage.cy.ts index 9fe3aea75f9..35818751526 100644 --- a/cypress/e2e/users_spec/user_manage.cy.ts +++ b/cypress/e2e/users_spec/user_manage.cy.ts @@ -27,6 +27,9 @@ describe("Manage User", () => { beforeEach(() => { cy.restoreLocalStorage(); + console.log(localStorage); + cy.clearLocalStorage(/filters--.+/); + console.log(localStorage); cy.awaitUrl("/users"); }); diff --git a/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts index 93bbc87c9ab..60e3f3ece2d 100644 --- a/cypress/pageobject/Asset/AssetCreation.ts +++ b/cypress/pageobject/Asset/AssetCreation.ts @@ -239,7 +239,7 @@ export class AssetPage { } selectjsonexportbutton() { - cy.intercept("GET", "**/api/v1/asset/?json=true**").as("getJsonexport"); + cy.intercept("GET", "**/api/v1/asset/?**json=true**").as("getJsonexport"); cy.get("#export-json-option").click(); cy.wait("@getJsonexport").then(({ request, response }) => { expect(response.statusCode).to.eq(200); @@ -248,7 +248,7 @@ export class AssetPage { } selectcsvexportbutton() { - cy.intercept("GET", "**/api/v1/asset/?csv=true**").as("getCsvexport"); + cy.intercept("GET", "**/api/v1/asset/?**csv=true**").as("getCsvexport"); cy.get("#export-csv-option").click(); cy.wait("@getCsvexport").then(({ request, response }) => { expect(response.statusCode).to.eq(200); diff --git a/cypress/pageobject/Asset/AssetFilters.ts b/cypress/pageobject/Asset/AssetFilters.ts index a16b61f4fc5..9a8d9781786 100644 --- a/cypress/pageobject/Asset/AssetFilters.ts +++ b/cypress/pageobject/Asset/AssetFilters.ts @@ -40,7 +40,20 @@ export class AssetFilters { cy.intercept("GET", "**/api/v1/asset/**").as("clearAssets"); cy.get("#clear-filter").click(); cy.wait("@clearAssets").its("response.statusCode").should("eq", 200); - cy.url().should("match", /\/assets$/); + cy.location("pathname").should("match", /\/assets$/); + cy.url().then((url) => { + const queryParams = new URL(url).searchParams; + let allEmpty = true; + const blacklistedKeys = ["page", "limit", "offset"]; + + queryParams.forEach((value, key) => { + if (value !== "" && !blacklistedKeys.includes(key)) { + allEmpty = false; + } + }); + + expect(allEmpty).to.be.true; + }); } clickadvancefilter() { cy.intercept("GET", "**/api/v1/getallfacilities/**").as("advancefilter"); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 2dd8c477233..0c491814102 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -109,3 +109,7 @@ Cypress.Commands.add("getAttached", (selector) => { }) .then(() => cy.wrap($el)); }); + +Cypress.Commands.add("clearAllFilters", () => { + return cy.get("#clear-all-filters").click(); +}); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index a4db7ef3f92..ea1a9b73a0b 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -12,6 +12,8 @@ declare global { url: string, disableLoginVerification?: boolean ): Chainable; + getAttached(selector: string): Chainable; + clearAllFilters(): Chainable; } } } diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index ff9ee273356..9c019786170 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "baseUrl": "../node_modules", "target": "es5", - "lib": ["es5", "dom"], + "lib": ["es5", "dom", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020"], "types": ["cypress"], "typeRoots": ["./support"], "resolveJsonModule": true diff --git a/public/External-Results-Template.csv b/public/External-Results-Template.csv new file mode 100644 index 00000000000..3ab2afc65ba --- /dev/null +++ b/public/External-Results-Template.csv @@ -0,0 +1,3 @@ +District,srf id,name,age,age in,gender,mobile number,address,ward,local body,local body type,source,Sample Collection Date,result date,test type,lab name,sample type,patient status,Is Repeat,patient category,result +Ernakulam,00/EKM/0000,Bodhi CSN,24,years,m,8888888888,"CSN HQ +Kochi, Kerala ",7,Poothrikka,grama panchayath,Secondary contact aparna,2020-10-14,2020-10-14,Antigen,Karothukuzhi Laboratory,Ag-SD_Biosensor_Standard_Q_COVID-19_Ag_detection_kit,Asymptomatic,NO,Cat 17: All individuals who wish to get themselves tested,Negative \ No newline at end of file diff --git a/public/config.json b/public/config.json index 444362cfb20..74e509fee59 100644 --- a/public/config.json +++ b/public/config.json @@ -21,6 +21,6 @@ "kasp_string": "KASP", "kasp_full_string": "Karunya Arogya Suraksha Padhathi", "sample_format_asset_import": "https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=11JaEhNHdyCHth4YQs_44YaRlP77Rrqe81VSEfg1glko&exportFormat=xlsx", - "sample_format_external_result_import": "https://docs.google.com/spreadsheets/d/17VfgryA6OYSYgtQZeXU9mp7kNvLySeEawvnLBO_1nuE/export?format=csv&id=17VfgryA6OYSYgtQZeXU9mp7kNvLySeEawvnLBO_1nuE", + "sample_format_external_result_import": "/External-Results-Template.csv", "enable_abdm": true } \ No newline at end of file diff --git a/src/CAREUI/display/NetworkSignal.tsx b/src/CAREUI/display/NetworkSignal.tsx new file mode 100644 index 00000000000..b0ae2c541cb --- /dev/null +++ b/src/CAREUI/display/NetworkSignal.tsx @@ -0,0 +1,57 @@ +import { classNames } from "../../Utils/utils"; +import CareIcon from "../icons/CareIcon"; + +interface Props { + /** + * Strength of the signal, from 0 to 3 + * + * undefined: Error + * 0: No signal + * 1: Weak signal + * 2: Medium signal + * 3: Strong signal + */ + strength?: number; + children?: React.ReactNode; +} + +export default function NetworkSignal({ strength, children }: Props) { + return ( +
+
+ {strength === undefined ? ( + + ) : ( + Array.from({ length: 3 }, (_, i) => ( +
i ? "bg-current" : "bg-zinc-600" + )} + /> + )) + )} +
+ {children} +
+ ); +} diff --git a/src/CAREUI/display/RecordMeta.tsx b/src/CAREUI/display/RecordMeta.tsx index 944ddf27c8f..818553d9207 100644 --- a/src/CAREUI/display/RecordMeta.tsx +++ b/src/CAREUI/display/RecordMeta.tsx @@ -11,6 +11,7 @@ interface Props { time?: string; prefix?: ReactNode; className?: string; + inlineClassName?: string; user?: { first_name: string; last_name: string; @@ -23,7 +24,14 @@ interface Props { * A generic component to display relative time along with a tooltip and a user * if provided. */ -const RecordMeta = ({ time, user, prefix, className, inlineUser }: Props) => { +const RecordMeta = ({ + time, + user, + prefix, + className, + inlineClassName, + inlineUser, +}: Props) => { const isOnline = user && isUserOnline(user); let child = ( @@ -47,11 +55,11 @@ const RecordMeta = ({ time, user, prefix, className, inlineUser }: Props) => { if (prefix || user) { child = ( -
+
{prefix} {child} {user && inlineUser && by} - {user && } + {user && !inlineUser && } {user && inlineUser && ( {formatName(user)} )} diff --git a/src/CAREUI/interactive/KeyboardShortcut.tsx b/src/CAREUI/interactive/KeyboardShortcut.tsx new file mode 100644 index 00000000000..47a9fbfca28 --- /dev/null +++ b/src/CAREUI/interactive/KeyboardShortcut.tsx @@ -0,0 +1,49 @@ +import useKeyboardShortcut from "use-keyboard-shortcut"; +import { classNames, isAppleDevice } from "../../Utils/utils"; + +interface Props { + children: React.ReactNode; + shortcut: string[]; + onTrigger: () => void; + shortcutSeperator?: string; + helpText?: string; + tooltipClassName?: string; +} + +export default function KeyboardShortcut(props: Props) { + useKeyboardShortcut(props.shortcut, props.onTrigger, { + overrideSystem: true, + }); + + return ( +
+ {props.children} + + {props.helpText} + + {getShortcutKeyDescription(props.shortcut).join(" + ")} + + +
+ ); +} + +const SHORTCUT_KEY_MAP = { + Meta: "⌘", + Shift: "⇧Shift", + Alt: "⌥Alt", + Control: isAppleDevice ? "⌃Ctrl" : "Ctrl", + ArrowUp: "↑", + ArrowDown: "↓", + ArrowLeft: "←", + ArrowRight: "→", +} as Record; + +export const getShortcutKeyDescription = (shortcut: string[]) => { + return shortcut.map((key) => SHORTCUT_KEY_MAP[key] || key); +}; diff --git a/src/CAREUI/misc/Fullscreen.tsx b/src/CAREUI/misc/Fullscreen.tsx new file mode 100644 index 00000000000..5cfa7865128 --- /dev/null +++ b/src/CAREUI/misc/Fullscreen.tsx @@ -0,0 +1,43 @@ +import { useEffect, useRef } from "react"; + +interface Props { + className?: string; + fullscreenClassName?: string; + children: React.ReactNode; + fullscreen: boolean; + onExit: () => void; +} + +export default function Fullscreen(props: Props) { + const ref = useRef(null); + + useEffect(() => { + if (props.fullscreen) { + ref.current?.requestFullscreen(); + } else { + document.exitFullscreen(); + } + }, [props.fullscreen]); + + useEffect(() => { + const listener = () => { + if (!document.fullscreenElement) { + props.onExit(); + } + }; + + document.addEventListener("fullscreenchange", listener); + return () => { + document.removeEventListener("fullscreenchange", listener); + }; + }, [props.onExit]); + + return ( +
+ {props.children} +
+ ); +} diff --git a/src/Common/hooks/useFeedPTZ.ts b/src/Common/hooks/useFeedPTZ.ts index 32cc5e6bc25..b064e9180ee 100644 --- a/src/Common/hooks/useFeedPTZ.ts +++ b/src/Common/hooks/useFeedPTZ.ts @@ -1,3 +1,10 @@ +/** + * 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 { diff --git a/src/Common/hooks/useFilters.tsx b/src/Common/hooks/useFilters.tsx index 129a696041b..a62180c8a29 100644 --- a/src/Common/hooks/useFilters.tsx +++ b/src/Common/hooks/useFilters.tsx @@ -1,5 +1,5 @@ import { useQueryParams } from "raviger"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import GenericFilterBadge from "../../CAREUI/display/FilterBadge"; import PaginationComponent from "../../Components/Common/Pagination"; @@ -37,6 +37,25 @@ export default function useFilters({ limit = 14 }: { limit?: number }) { const removeFilters = (keys: string[]) => updateQuery(keys.reduce((acc, key) => ({ ...acc, [key]: "" }), qParams)); + useEffect(() => { + const localFilters = JSON.parse( + localStorage.getItem("filters--" + window.location.pathname) || "{}" + ); + const blacklistLocalFilters = ["page", "limit", "offset"]; + const newFilters = { ...localFilters, ...qParams }; + const filteredNewFilters = blacklistLocalFilters.reduce( + (acc, key) => ({ ...acc, [key]: undefined }), + newFilters + ); + + localStorage.setItem( + "filters--" + window.location.pathname, + JSON.stringify(filteredNewFilters) + ); + + updateQuery(newFilters); + }, [qParams]); + const FilterBadge = ({ name, value, paramKey }: FilterBadgeProps) => { if (Array.isArray(paramKey)) return ( @@ -131,11 +150,36 @@ export default function useFilters({ limit = 14 }: { limit?: number }) { }) => { const compiledBadges = badges(badgeUtils); const { t } = useTranslation(); + + const activeFilters = compiledBadges.reduce((acc, badge) => { + const { paramKey } = badge; + + if (Array.isArray(paramKey)) { + const active = paramKey.filter((key) => qParams[key]); + if (active) acc.concat(active); + } else { + if (qParams[paramKey]) acc.push(paramKey); + } + + return acc; + }, [] as string[]); + return (
{compiledBadges.map((props) => ( ))} + {activeFilters.length >= 1 && ( + + )} {children}
); @@ -201,6 +245,7 @@ export default function useFilters({ limit = 14 }: { limit?: number }) { show: showFilters, setShow: setShowFilters, filter: qParams, + removeFilters, onChange: (filter: FilterState) => { updateQuery(filter); setShowFilters(false); diff --git a/src/Common/hooks/useMSEplayer.ts b/src/Common/hooks/useMSEplayer.ts index 4d1bb36b9ac..898da28f3ad 100644 --- a/src/Common/hooks/useMSEplayer.ts +++ b/src/Common/hooks/useMSEplayer.ts @@ -5,12 +5,6 @@ export interface IAsset { middlewareHostname: string; } -interface PTZPayload { - x: number; - y: number; - zoom: number; -} - interface UseMSEMediaPlayerOption { config: IAsset; url?: string; @@ -40,16 +34,6 @@ export interface IOptions { onSuccess?: (resp: any) => void; onError?: (err: any) => void; } - -enum PTZ { - Up = "up", - Down = "down", - Left = "left", - Right = "right", - ZoomIn = "zoomIn", - ZoomOut = "zoomOut", -} - const stopStream = ({ middlewareHostname, @@ -69,38 +53,9 @@ const stopStream = .catch((err) => options.onError && options.onError(err)); }; -export const getPTZPayload = (action: PTZ): PTZPayload => { - let x = 0; - let y = 0; - let zoom = 0; - switch (action) { - case PTZ.Up: - y = 0.1; - break; - case PTZ.Down: - y = -0.1; - break; - case PTZ.Left: - x = -0.1; - break; - case PTZ.Right: - x = 0.1; - break; - case PTZ.ZoomIn: - zoom = 0.1; - break; - case PTZ.ZoomOut: - zoom = -0.1; - break; - } - - return { x, y, zoom }; -}; - /** * MSE player utility */ - const Utf8ArrayToStr = (array: string | any[] | Uint8Array) => { let out, i, c; let char2, char3; diff --git a/src/Components/Assets/AssetFilter.tsx b/src/Components/Assets/AssetFilter.tsx index 98f7f52e1e1..9aeac85e734 100644 --- a/src/Components/Assets/AssetFilter.tsx +++ b/src/Components/Assets/AssetFilter.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import { navigate, useQueryParams } from "raviger"; +import { useQueryParams } from "raviger"; import { FacilitySelect } from "../Common/FacilitySelect"; import { FacilityModel } from "../Facility/models"; import { LocationSelect } from "../Common/LocationSelect"; @@ -19,7 +19,7 @@ const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); function AssetFilter(props: any) { - const { filter, onChange, closeFilter } = props; + const { filter, onChange, closeFilter, removeFilters } = props; const [facility, setFacility] = useState({ name: "" }); const [asset_type, setAssetType] = useState( filter.asset_type ? filter.asset_type : "" @@ -54,16 +54,16 @@ function AssetFilter(props: any) { }, [facility.id, qParams.facility, qParams.location]); const clearFilter = useCallback(() => { - setFacility({ name: "" }); - setAssetType(""); - setAssetStatus(""); - setAssetClass(""); - setFacilityId(""); - setLocationId(""); + removeFilters([ + "facility", + "asset_type", + "asset_class", + "status", + "location", + "warranty_amc_end_of_validity_before", + "warranty_amc_end_of_validity_after", + ]); closeFilter(); - const searchQuery = qParams?.search && `?search=${qParams?.search}`; - if (searchQuery) navigate(`/assets${searchQuery}`); - else navigate("/assets"); }, [qParams]); const applyFilter = () => { diff --git a/src/Components/Assets/AssetTypes.tsx b/src/Components/Assets/AssetTypes.tsx index a4005404da1..041d3d0a81e 100644 --- a/src/Components/Assets/AssetTypes.tsx +++ b/src/Components/Assets/AssetTypes.tsx @@ -2,17 +2,25 @@ import { BedModel } from "../Facility/models"; import { PerformedByModel } from "../HCX/misc"; import { PatientModel } from "../Patient/models"; +export enum AssetLocationType { + OTHER = "OTHER", + WARD = "WARD", + ICU = "ICU", +} + export interface AssetLocationObject { id: string; name: string; description: string; created_date?: string; modified_date?: string; + location_type: AssetLocationType; middleware_address?: string; facility: { id: string; name: string; }; + middleware_address?: string; } export enum AssetType { diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index 58472c4ff25..4aad207c25a 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -12,7 +12,7 @@ import CircularProgress from "../Common/components/CircularProgress"; import { LocalStorageKeys } from "../../Common/constants"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; -import { handleRedirection } from "../../Utils/utils"; +import { handleRedirection, invalidateFiltersCache } from "../../Utils/utils"; export const Login = (props: { forgot?: boolean }) => { const { @@ -91,6 +91,7 @@ export const Login = (props: { forgot?: boolean }) => { const handleSubmit = async (e: any) => { e.preventDefault(); + invalidateFiltersCache(); const valid = validateData(); if (valid) { // replaces button with spinner diff --git a/src/Components/CameraFeed/AssetBedSelect.tsx b/src/Components/CameraFeed/AssetBedSelect.tsx new file mode 100644 index 00000000000..17701dccbde --- /dev/null +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -0,0 +1,77 @@ +import { Fragment } from "react"; +import useSlug from "../../Common/hooks/useSlug"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; +import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; +import { BedModel } from "../Facility/models"; +import { Listbox, Transition } from "@headlessui/react"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +interface Props { + asset?: AssetData; + bed?: BedModel; + value?: AssetBedModel; + onChange?: (value: AssetBedModel) => void; +} + +export default function AssetBedSelect(props: Props) { + const facility = useSlug("facility"); + + const { data, loading } = useQuery(routes.listAssetBeds, { + query: { + limit: 100, + facility, + asset: props.asset?.id, + bed: props.bed?.id, + }, + }); + + const selected = props.value; + + return ( + +
+ + + {selected?.bed_object.name ?? "No Preset"} + + + + + + + + {data?.results.map((obj) => ( + + `relative cursor-default select-none px-2 py-1 ${ + active ? "bg-zinc-700 text-white" : "text-zinc-400" + }` + } + value={obj} + > + {({ selected }) => ( + <> + + {obj.bed_object.name}: {obj.meta.preset_name} + + + )} + + ))} + + +
+
+ ); +} diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx new file mode 100644 index 00000000000..4ec039e4e70 --- /dev/null +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -0,0 +1,199 @@ +import { LegacyRef, useCallback, useEffect, useRef, useState } from "react"; +import { AssetData } from "../Assets/AssetTypes"; +import useOperateCamera, { PTZPayload } from "./useOperateCamera"; +import usePlayer from "./usePlayer"; +import { getStreamUrl } from "./utils"; +import ReactPlayer from "react-player"; +import { classNames, isIOS } from "../../Utils/utils"; +import FeedAlert, { FeedAlertState } from "./FeedAlert"; +import FeedNetworkSignal from "./FeedNetworkSignal"; +import NoFeedAvailable from "./NoFeedAvailable"; +import FeedControls from "./FeedControls"; +import Fullscreen from "../../CAREUI/misc/Fullscreen"; + +interface Props { + children?: React.ReactNode; + asset: AssetData; + fallbackMiddleware: string; // TODO: remove this in favour of `asset.resolved_middleware.hostname` once https://github.com/coronasafe/care/pull/1741 is merged + preset?: PTZPayload; + silent?: boolean; + className?: string; + // Callbacks + onCameraPresetsObtained?: (presets: Record) => void; + onStreamSuccess?: () => void; + onStreamError?: () => void; + // Controls + constrolsDisabled?: boolean; + shortcutsDisabled?: boolean; +} + +export default function CameraFeed(props: Props) { + const playerRef = useRef(null); + const streamUrl = getStreamUrl(props.asset, props.fallbackMiddleware); + + const player = usePlayer(streamUrl, playerRef); + const operate = useOperateCamera(props.asset.id, props.silent); + + const [isFullscreen, setFullscreen] = useState(false); + const [state, setState] = useState(); + useEffect(() => setState(player.status), [player.status, setState]); + + // Move camera when selected preset has changed + useEffect(() => { + async function move(preset: PTZPayload) { + setState("moving"); + const { res } = await operate({ type: "absolute_move", data: preset }); + setTimeout(() => setState((s) => (s === "moving" ? undefined : s)), 4000); + if (res?.status === 500) { + setState("host_unreachable"); + } + } + + if (props.preset) { + move(props.preset); + } + }, [props.preset]); + + // Get camera presets (only if onCameraPresetsObtained is provided) + useEffect(() => { + if (!props.onCameraPresetsObtained) return; + async function getPresets(cb: (presets: Record) => void) { + const { res, data } = await operate({ type: "get_presets" }); + if (res?.ok && data) { + cb((data as { result: Record }).result); + } + } + getPresets(props.onCameraPresetsObtained); + }, [operate, props.onCameraPresetsObtained]); + + const initializeStream = useCallback(() => { + player.initializeStream({ + onSuccess: async () => { + props.onStreamSuccess?.(); + const { res } = await operate({ type: "get_status" }); + if (res?.status === 500) { + setState("host_unreachable"); + } + }, + onError: props.onStreamError, + }); + }, [player.initializeStream, props.onStreamSuccess, props.onStreamError]); + + // Start stream on mount + useEffect(() => initializeStream(), [initializeStream]); + + const resetStream = () => { + setState("loading"); + initializeStream(); + }; + + return ( + setFullscreen(false)}> +
+
+
+ + {props.asset.name} + +
+ +
+
+ {props.children} +
+ +
+ {/* Notifications */} + + + {/* No Feed informations */} + {state === "host_unreachable" && ( + + )} + {player.status === "offline" && ( + + )} + + {/* Video Player */} + {isIOS ? ( +
+ } + controls={false} + playsinline + playing + muted + width="100%" + height="100%" + onPlay={player.onPlayCB} + onEnded={() => player.setStatus("stop")} + onError={(e, _, hlsInstance) => { + if (e === "hlsError") { + const recovered = hlsInstance.recoverMediaError(); + console.info(recovered); + } + }} + /> +
+ ) : ( +
+
+
+ ); +} diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx new file mode 100644 index 00000000000..b52071a8597 --- /dev/null +++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; +import CameraFeed from "./CameraFeed"; +import AssetBedSelect from "./AssetBedSelect"; + +interface Props { + asset: AssetData; + fallbackMiddleware?: string; +} + +export default function LocationFeedTile(props: Props) { + const [preset, setPreset] = useState(); + + return ( + +
+ +
+
+ ); +} diff --git a/src/Components/CameraFeed/CentralLiveMonitoring/LiveMonitoringFilters.tsx b/src/Components/CameraFeed/CentralLiveMonitoring/LiveMonitoringFilters.tsx new file mode 100644 index 00000000000..628518191de --- /dev/null +++ b/src/Components/CameraFeed/CentralLiveMonitoring/LiveMonitoringFilters.tsx @@ -0,0 +1,141 @@ +import { Popover, Transition } from "@headlessui/react"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import { FieldLabel } from "../../Form/FormFields/FormField"; +import { LocationSelect } from "../../Common/LocationSelect"; +import Pagination from "../../Common/Pagination"; +import useFilters from "../../../Common/hooks/useFilters"; +import { Fragment } from "react"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import useSlug from "../../../Common/hooks/useSlug"; +import { classNames } from "../../../Utils/utils"; + +interface Props { + perPageLimit: number; + isFullscreen: boolean; + setFullscreen: (state: boolean) => void; + totalCount: number; +} + +const LiveMonitoringFilters = (props: Props) => { + const facilityId = useSlug("facility"); + const { qParams, updateQuery, removeFilter, updatePage } = useFilters({ + limit: props.perPageLimit, + }); + + return ( +
+ + + + + Settings and Filters + + + + +
+
+
+ + {props.totalCount}{" "} + Camera(s) present + +
+
+
+
+ + Filter by Location + +
+ + location + ? updateQuery({ location }) + : removeFilter("location") + } + selected={qParams.location} + showAll={false} + multiple={false} + facilityId={facilityId} + errors="" + errorClassName="hidden" + /> +
+
+ {/* { + if (value) { + updateQuery({ [name]: value }); + } else { + removeFilter(name); + } + }} + labelClassName="text-sm" + errorClassName="hidden" + /> + { + if (value) { + updateQuery({ [name]: value }); + } else { + removeFilter(name); + } + }} + labelClassName="text-sm" + errorClassName="hidden" + /> */} + props.setFullscreen(!props.isFullscreen)} + className="tooltip !h-11" + > + + {props.isFullscreen ? "Exit Fullscreen" : "Fullscreen"} + +
+
+
+
+
+ + updatePage(page)} + /> +
+ ); +}; + +export default LiveMonitoringFilters; diff --git a/src/Components/CameraFeed/CentralLiveMonitoring/index.tsx b/src/Components/CameraFeed/CentralLiveMonitoring/index.tsx new file mode 100644 index 00000000000..d83b01201b9 --- /dev/null +++ b/src/Components/CameraFeed/CentralLiveMonitoring/index.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import Loading from "../../Common/Loading"; +import Page from "../../Common/components/Page"; +import useQuery from "../../../Utils/request/useQuery"; +import routes from "../../../Redux/api"; +import LocationFeedTile from "../CameraFeedWithBedPresets"; +import Fullscreen from "../../../CAREUI/misc/Fullscreen"; +import useBreakpoints from "../../../Common/hooks/useBreakpoints"; +import { useQueryParams } from "raviger"; +import LiveMonitoringFilters from "./LiveMonitoringFilters"; + +export default function CentralLiveMonitoring(props: { facilityId: string }) { + const [isFullscreen, setFullscreen] = useState(false); + const limit = useBreakpoints({ default: 4, "3xl": 9 }); + + const [qParams] = useQueryParams(); + + const facilityQuery = useQuery(routes.getPermittedFacility, { + pathParams: { id: props.facilityId }, + }); + + const { data, loading } = useQuery(routes.listAssets, { + query: { + ...qParams, + limit, + offset: (qParams.page ? qParams.page - 1 : 0) * limit, + asset_class: "ONVIF", + facility: props.facilityId, + location: qParams.location, + in_use_by_consultation: qParams.in_use_by_consultation, + }, + }); + + const totalCount = data?.count ?? 0; + + return ( + + } + > + {loading || + data === undefined || + facilityQuery.data === undefined || + facilityQuery.loading ? ( + + ) : data.results.length === 0 ? ( +
+ No Camera present in this location or facility. +
+ ) : ( + setFullscreen(false)} + > +
+ {data.results.map((asset) => ( +
+ +
+ ))} +
+
+ )} +
+ ); +} diff --git a/src/Components/CameraFeed/FeedAlert.tsx b/src/Components/CameraFeed/FeedAlert.tsx new file mode 100644 index 00000000000..0c2eb6aa429 --- /dev/null +++ b/src/Components/CameraFeed/FeedAlert.tsx @@ -0,0 +1,72 @@ +import { Transition } from "@headlessui/react"; +import { Fragment, useEffect, useState } from "react"; +import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon"; +import { classNames } from "../../Utils/utils"; +import { StreamStatus } from "./usePlayer"; + +export type FeedAlertState = + | StreamStatus + | "moving" + | "zooming" + | "saving_preset" + | "host_unreachable"; + +interface Props { + state?: FeedAlertState; +} + +const ALERT_ICON_MAP: Record = { + playing: "l-play-circle", + stop: "l-stop-circle", + offline: "l-exclamation-triangle", + loading: "l-spinner", + moving: "l-expand-from-corner", + zooming: "l-search", + saving_preset: "l-save", + host_unreachable: "l-exclamation-triangle", +}; + +export default function FeedAlert({ state }: Props) { + const [show, setShow] = useState(false); + + useEffect(() => { + if (!state) return; + + setShow(true); + + if (state !== "loading") { + const timeout = setTimeout(() => setShow(false), 4000); + return () => { + clearTimeout(timeout); + }; + } + }, [state, setShow]); + + return ( + +
+ {state && ( + + )} + + {state?.replace("_", " ")} + +
+
+ ); +} diff --git a/src/Components/CameraFeed/FeedButton.tsx b/src/Components/CameraFeed/FeedButton.tsx new file mode 100644 index 00000000000..033ffcc11ed --- /dev/null +++ b/src/Components/CameraFeed/FeedButton.tsx @@ -0,0 +1,42 @@ +import KeyboardShortcut from "../../CAREUI/interactive/KeyboardShortcut"; +import { classNames } from "../../Utils/utils"; + +interface Props { + className?: string; + children?: React.ReactNode; + readonly shortcut?: string[]; + onTrigger: () => void; + helpText?: string; + shortcutsDisabled?: boolean; + tooltipClassName?: string; +} + +export default function FeedButton(props: Props) { + const child = ( + + ); + + if (props.shortcutsDisabled || !props.shortcut) { + return child; + } + + return ( + + {child} + + ); +} diff --git a/src/Components/CameraFeed/FeedControls.tsx b/src/Components/CameraFeed/FeedControls.tsx new file mode 100644 index 00000000000..3a5afb76209 --- /dev/null +++ b/src/Components/CameraFeed/FeedControls.tsx @@ -0,0 +1,217 @@ +import { useState } from "react"; +import { isAppleDevice } from "../../Utils/utils"; +import FeedButton from "./FeedButton"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { PTZPayload } from "./useOperateCamera"; + +const Actions = { + UP: 1 << 0, + DOWN: 1 << 1, + LEFT: 1 << 2, + RIGHT: 1 << 3, + ZOOM_IN: 1 << 4, + ZOOM_OUT: 1 << 5, +} as const; + +const Shortcuts = { + MoveUp: [isAppleDevice ? "Meta" : "Ctrl", "Shift", "ArrowUp"], + MoveLeft: [isAppleDevice ? "Meta" : "Ctrl", "Shift", "ArrowLeft"], + MoveDown: [isAppleDevice ? "Meta" : "Ctrl", "Shift", "ArrowDown"], + MoveRight: [isAppleDevice ? "Meta" : "Ctrl", "Shift", "ArrowRight"], + TogglePrecision: ["Shift", "P"], + ZoomIn: [isAppleDevice ? "Meta" : "Ctrl", "I"], + ZoomOut: [isAppleDevice ? "Meta" : "Ctrl", "O"], + Reset: ["Shift", "R"], + SavePreset: [isAppleDevice ? "Meta" : "Ctrl", "Shift", "S"], + Fullscreen: ["Shift", "F"], +}; + +export type PTZAction = keyof typeof Actions; + +/** + * Returns the PTZ payload for the given action + * + * Example: + * ``` + * payload(Actions.TOP | Actions.LEFT); + * ``` + * + * @param action An Actions or a combination of Actions + * @param precision Precision of the PTZ action + * @returns The PTZ payload + */ +const payload = (action: number, precision: number) => { + let [x, y, zoom] = [0, 0, 0]; + const delta = 0.1 / Math.max(1, precision); + + const _ = (direction: number) => action & direction && delta; + + x -= _(Actions.LEFT); + x += _(Actions.RIGHT); + y += _(Actions.UP); + y -= _(Actions.DOWN); + zoom += _(Actions.ZOOM_IN); + zoom -= _(Actions.ZOOM_OUT); + + return { x, y, zoom }; +}; + +interface Props { + shortcutsDisabled?: boolean; + onMove: (payload: PTZPayload) => void; + isFullscreen: boolean; + setFullscreen: (state: boolean) => void; + onReset: () => void; +} + +export default function FeedControls({ shortcutsDisabled, ...props }: Props) { + const [precision, setPrecision] = useState(1); + const togglePrecision = () => setPrecision((p) => (p === 16 ? 1 : p << 1)); + + const move = (direction: number) => () => { + props.onMove(payload(direction, precision)); + }; + + return ( +
+
+
    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • + +
  • + + {precision}x + +
  • + +
  • + + + +
  • + +
  • + + + +
  • + +
  • + + + +
  • + +
  • + + + +
  • +
+
+ +
+
+ + + + + + + + + + {/* TODO: implement this when this is used in where presets can be saved. */} + {/* console.error("Not implemented")} + shortcutsDisabled={shortcutsDisabled} + > + + */} + props.setFullscreen(!props.isFullscreen)} + shortcutsDisabled={shortcutsDisabled} + > + + +
+
+
+ ); +} diff --git a/src/Components/CameraFeed/FeedNetworkSignal.tsx b/src/Components/CameraFeed/FeedNetworkSignal.tsx new file mode 100644 index 00000000000..55b62ba8620 --- /dev/null +++ b/src/Components/CameraFeed/FeedNetworkSignal.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import { calculateVideoDelay } from "./utils"; +import NetworkSignal from "../../CAREUI/display/NetworkSignal"; +import { StreamStatus } from "./usePlayer"; + +interface Props { + playerRef: React.RefObject; + playedOn: Date | undefined; + status: StreamStatus; + onReset: () => void; +} + +export default function FeedNetworkSignal(props: Props) { + const [videoDelay, setVideoDelay] = useState(); + useEffect(() => { + const interval = setInterval(() => { + const delay = calculateVideoDelay(props.playerRef, props.playedOn); + setVideoDelay(delay); + + // Voluntarily resetting for negative delays too as: + // 1. We should not allow users to see what happens in the future! + // They'll figure out that we have a time machine in our hands. + // 2. This value may become negative when the web-socket stream + // disconnects while the tab was not in focus. + if (-5 > delay || delay > 5) { + props.onReset(); + } + }, 500); + + return () => { + clearInterval(interval); + }; + }, [props.playedOn, props.onReset, setVideoDelay]); + + return ( + + + {videoDelay ? ( + `${(videoDelay * 1e3) | 1} ms` + ) : ( + No signal + )} + + + ); +} + +const getStrength = (status: StreamStatus, videoDelay?: number) => { + if (status !== "playing" || videoDelay === undefined) { + return 0; + } + + const ms = videoDelay * 1e3; + + if (ms < 500) return 3; + if (ms < 2000) return 2; + return 1; +}; diff --git a/src/Components/CameraFeed/NoFeedAvailable.tsx b/src/Components/CameraFeed/NoFeedAvailable.tsx new file mode 100644 index 00000000000..aaf74dadec6 --- /dev/null +++ b/src/Components/CameraFeed/NoFeedAvailable.tsx @@ -0,0 +1,58 @@ +import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon"; +import { classNames } from "../../Utils/utils"; +import { AssetData } from "../Assets/AssetTypes"; +import ButtonV2 from "../Common/components/ButtonV2"; + +interface Props { + className?: string; + icon: IconName; + message: string; + streamUrl: string; + onResetClick: () => void; + asset: AssetData; +} + +export default function NoFeedAvailable(props: Props) { + const redactedURL = props.streamUrl + // Replace all uuids in the URL with "ID_REDACTED" + .replace(/[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}/gi, "***") + // Replace all numbers in the URL's path params with "ID_REDACTED" + .replace(/\/\d+/g, "/***"); + + return ( +
+ + {props.message} + + {redactedURL} + +
+ + + Retry + + + + Configure + +
+
+ ); +} diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts new file mode 100644 index 00000000000..f3374a1776d --- /dev/null +++ b/src/Components/CameraFeed/routes.ts @@ -0,0 +1,11 @@ +import { Type } from "../../Redux/api"; +import { OperationAction } from "./useOperateCamera"; + +export const FeedRoutes = { + operateAsset: { + path: "/api/v1/asset/{id}/operate_assets/", + method: "POST", + TRes: Type(), + TBody: Type<{ action: OperationAction }>(), + }, +} as const; diff --git a/src/Components/CameraFeed/useOperateCamera.ts b/src/Components/CameraFeed/useOperateCamera.ts new file mode 100644 index 00000000000..259e45fcc89 --- /dev/null +++ b/src/Components/CameraFeed/useOperateCamera.ts @@ -0,0 +1,54 @@ +import request from "../../Utils/request/request"; +import { FeedRoutes } from "./routes"; + +export interface PTZPayload { + x: number; + y: number; + zoom: number; +} + +interface GetStatusOperation { + type: "get_status"; +} + +interface GetPresetsOperation { + type: "get_presets"; +} + +interface GoToPresetOperation { + type: "goto_preset"; + data: { + preset: number; + }; +} + +interface AbsoluteMoveOperation { + type: "absolute_move"; + data: PTZPayload; +} + +interface RelativeMoveOperation { + type: "relative_move"; + data: PTZPayload; +} + +export type OperationAction = + | GetStatusOperation + | GetPresetsOperation + | GoToPresetOperation + | AbsoluteMoveOperation + | RelativeMoveOperation; + +/** + * 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) { + return (action: OperationAction) => { + return request(FeedRoutes.operateAsset, { + pathParams: { id }, + body: { action }, + silent, + }); + }; +} diff --git a/src/Components/CameraFeed/usePlayer.tsx b/src/Components/CameraFeed/usePlayer.tsx new file mode 100644 index 00000000000..b106a25378f --- /dev/null +++ b/src/Components/CameraFeed/usePlayer.tsx @@ -0,0 +1,58 @@ +import { MutableRefObject, useCallback, useState } from "react"; +import ReactPlayer from "react-player"; +import { isIOS } from "../../Utils/utils"; +import { useHLSPLayer } from "../../Common/hooks/useHLSPlayer"; +import { IOptions, useMSEMediaPlayer } from "../../Common/hooks/useMSEplayer"; + +export type StreamStatus = "playing" | "stop" | "loading" | "offline"; + +export default function usePlayer( + streamUrl: string, + ref: MutableRefObject +) { + const [playedOn, setPlayedOn] = useState(); + const [status, setStatus] = useState("stop"); + + // Voluntarily disabling react-hooks/rules-of-hooks for this line as order of + // hooks is maintained (since platform won't change in runtime) + const _start = isIOS + ? // eslint-disable-next-line react-hooks/rules-of-hooks + useHLSPLayer(ref.current as ReactPlayer).startStream + : // eslint-disable-next-line react-hooks/rules-of-hooks + useMSEMediaPlayer({ + // Voluntarily set to "" as it's used by `stopStream` only (which is not + // used by this hook) + config: { middlewareHostname: "" }, + url: streamUrl, + videoEl: ref.current as HTMLVideoElement, + }).startStream; + + const initializeStream = useCallback( + ({ onSuccess, onError }: IOptions) => { + setPlayedOn(undefined); + setStatus("loading"); + _start({ + onSuccess, + onError: (args) => { + setStatus("offline"); + onError?.(args); + }, + }); + }, + [ref.current, streamUrl] + ); + + const onPlayCB = () => { + // Voluntarily updating only if previously undefined (as this method may be invoked by the HTML video element on tab re-focus) + setPlayedOn((prev) => (prev === undefined ? new Date() : prev)); + setStatus("playing"); + }; + + return { + status, + setStatus, + initializeStream, + playedOn, + onPlayCB, + }; +} diff --git a/src/Components/CameraFeed/utils.ts b/src/Components/CameraFeed/utils.ts new file mode 100644 index 00000000000..b5b8920fd5a --- /dev/null +++ b/src/Components/CameraFeed/utils.ts @@ -0,0 +1,28 @@ +import { MutableRefObject } from "react"; +import { AssetData } from "../Assets/AssetTypes"; +import { getCameraConfig } from "../../Utils/transformUtils"; +import { isIOS } from "../../Utils/utils"; + +export const calculateVideoDelay = ( + ref: MutableRefObject, + playedOn?: Date +) => { + const video = ref.current; + + if (!video || !playedOn) { + return 0; + } + + const playedDuration = (new Date().getTime() - playedOn.getTime()) / 1e3; + return playedDuration - video.currentTime; +}; + +export const getStreamUrl = (asset: AssetData, fallbackMiddleware?: string) => { + const config = getCameraConfig(asset); + const host = config.middleware_hostname || fallbackMiddleware; + const uuid = config.accessKey; + + return isIOS + ? `https://${host}/stream/${uuid}/channel/0/hls/live/index.m3u8?uuid=${uuid}&channel=0` + : `wss://${host}/stream/${uuid}/channel/0/mse?uuid=${uuid}&channel=0`; +}; diff --git a/src/Components/Common/BedSelect.tsx b/src/Components/Common/BedSelect.tsx index d903d3b62e0..94caded12a3 100644 --- a/src/Components/Common/BedSelect.tsx +++ b/src/Components/Common/BedSelect.tsx @@ -74,7 +74,7 @@ export const BedSelect = (props: BedSelectProps) => { optionLabel={(option: any) => { if (Object.keys(option).length === 0) return ""; return ( - `${option.name} ${option?.location_object?.name || t("unknown")}` || + `${option.name}, ${option?.location_object?.name || t("unknown")}` || option?.location_object?.name ); }} diff --git a/src/Components/Common/SkillSelect.tsx b/src/Components/Common/SkillSelect.tsx index 3257971d03c..941c29790d1 100644 --- a/src/Components/Common/SkillSelect.tsx +++ b/src/Components/Common/SkillSelect.tsx @@ -1,8 +1,8 @@ import { useCallback } from "react"; import { useDispatch } from "react-redux"; -import { getAllSkills, getUserListSkills } from "../../Redux/actions"; +import { getAllSkills } from "../../Redux/actions"; import AutoCompleteAsync from "../Form/AutoCompleteAsync"; -import { SkillObjectModel } from "../Users/models"; +import { SkillModel, SkillObjectModel } from "../Users/models"; interface SkillSelectProps { id?: string; @@ -17,6 +17,7 @@ interface SkillSelectProps { selected: SkillObjectModel | SkillObjectModel[] | null; setSelected: (selected: SkillObjectModel) => void; username?: string; + userSkills?: SkillModel[]; } export const SkillSelect = (props: SkillSelectProps) => { @@ -32,7 +33,8 @@ export const SkillSelect = (props: SkillSelectProps) => { disabled = false, className = "", errors = "", - username, + //username, + userSkills, } = props; const dispatchAction: any = useDispatch(); @@ -47,21 +49,16 @@ export const SkillSelect = (props: SkillSelectProps) => { }; const res = await dispatchAction(getAllSkills(params)); - - const linkedSkills = await dispatchAction( - getUserListSkills({ username: username }) - ); - - const skillsList = linkedSkills?.data?.results; const skillsID: string[] = []; - skillsList.map((skill: any) => skillsID.push(skill.skill_object.id)); + userSkills?.map((skill: SkillModel) => + skillsID.push(skill.skill_object.id) + ); const skills = res?.data?.results.filter( (skill: any) => !skillsID.includes(skill.id) ); - return skills; }, - [dispatchAction, searchAll, showAll] + [dispatchAction, searchAll, userSkills, showAll] ); return ( diff --git a/src/Components/Common/components/Page.tsx b/src/Components/Common/components/Page.tsx index d3ca7e5e009..ce5e84deefe 100644 --- a/src/Components/Common/components/Page.tsx +++ b/src/Components/Common/components/Page.tsx @@ -1,16 +1,33 @@ -import { RefObject } from "react"; +import { RefObject, useContext, useEffect } from "react"; import PageTitle, { PageTitleProps } from "../PageTitle"; import { classNames } from "../../../Utils/utils"; +import { SidebarShrinkContext } from "../Sidebar/Sidebar"; interface PageProps extends PageTitleProps { - children: any; - options?: any; + children: React.ReactNode | React.ReactNode[]; + options?: React.ReactNode | React.ReactNode[]; className?: string; noImplicitPadding?: boolean; ref?: RefObject; + /** + * If true, the sidebar will be collapsed when mounted, and restored to original state when unmounted. + * @default false + **/ + collapseSidebar?: boolean; } export default function Page(props: PageProps) { + const sidebar = useContext(SidebarShrinkContext); + + useEffect(() => { + if (!props.collapseSidebar) return; + + sidebar.setShrinked(true); + return () => { + sidebar.setShrinked(sidebar.shrinked); + }; + }, [props.collapseSidebar]); + let padding = ""; if (!props.noImplicitPadding) { if (!props.hideBack || props.componentRight) diff --git a/src/Components/ExternalResult/ExternalResultUpload.tsx b/src/Components/ExternalResult/ExternalResultUpload.tsx index 20a2cec3341..5a9262b2990 100644 --- a/src/Components/ExternalResult/ExternalResultUpload.tsx +++ b/src/Components/ExternalResult/ExternalResultUpload.tsx @@ -112,6 +112,8 @@ export default function ExternalResultUpload() { {" "} {t("sample_format")} diff --git a/src/Components/ExternalResult/ListFilter.tsx b/src/Components/ExternalResult/ListFilter.tsx index 75358196ffd..898fe9ecb31 100644 --- a/src/Components/ExternalResult/ListFilter.tsx +++ b/src/Components/ExternalResult/ListFilter.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import useMergeState from "../../Common/hooks/useMergeState"; -import { navigate } from "raviger"; import { useTranslation } from "react-i18next"; import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; import TextFormField from "../Form/FormFields/TextFormField"; @@ -27,7 +26,7 @@ const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); export default function ListFilter(props: any) { - const { filter, onChange, closeFilter, dataList } = props; + const { filter, onChange, closeFilter, dataList, removeFilters } = props; const [wardList, setWardList] = useState([]); const [lsgList, setLsgList] = useState([]); const [wards, setWards] = useState([]); @@ -198,10 +197,11 @@ export default function ListFilter(props: any) { advancedFilter={props} onApply={applyFilter} onClear={() => { - navigate("/external_results"); - setFilterState(clearFilterState); - setSelectedLsgs([]); - setWards([]); + removeFilters([ + ...Object.keys(clearFilterState), + "wards", + "local_bodies", + ]); closeFilter(); }} > diff --git a/src/Components/ExternalResult/ResultList.tsx b/src/Components/ExternalResult/ResultList.tsx index 9aa365c7303..8fddc7cadc9 100644 --- a/src/Components/ExternalResult/ResultList.tsx +++ b/src/Components/ExternalResult/ResultList.tsx @@ -284,6 +284,13 @@ export default function ResultList() { +
+ {qParams.local_bodies && + dataList.lsgList.map((x) => lsgWardBadge("LSG", x, "local_bodies"))} + {qParams.wards && + dataList.wardList.map((x) => lsgWardBadge("Ward", x, "wards"))} +
+ [ badge("Name", "name"), @@ -294,10 +301,7 @@ export default function ResultList() { badge("SRF ID", "srf_id"), ]} /> -
- {dataList.lsgList.map((x) => lsgWardBadge("LSG", x, "local_bodies"))} - {dataList.wardList.map((x) => lsgWardBadge("Ward", x, "wards"))} -
+
diff --git a/src/Components/Facility/AddLocationForm.tsx b/src/Components/Facility/AddLocationForm.tsx index e71b68cc95c..81d9bc0750c 100644 --- a/src/Components/Facility/AddLocationForm.tsx +++ b/src/Components/Facility/AddLocationForm.tsx @@ -12,6 +12,8 @@ import { Submit, Cancel } from "../Common/components/ButtonV2"; import TextFormField from "../Form/FormFields/TextFormField"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import Page from "../Common/components/Page"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import { AssetLocationType } from "../Assets/AssetTypes"; const Loading = lazy(() => import("../Common/Loading")); @@ -29,10 +31,12 @@ export const AddLocationForm = (props: LocationFormProps) => { const [description, setDescription] = useState(""); const [facilityName, setFacilityName] = useState(""); const [locationName, setLocationName] = useState(""); + const [locationType, setLocationType] = useState(""); const [errors, setErrors] = useState({ name: "", description: "", middlewareAddress: "", + locationType: "", }); const headerText = !locationId ? "Add Location" : "Update Location"; const buttonText = !locationId ? "Add Location" : "Update Location"; @@ -53,6 +57,7 @@ export const AddLocationForm = (props: LocationFormProps) => { setName(res?.data?.name || ""); setLocationName(res?.data?.name || ""); setDescription(res?.data?.description || ""); + setLocationType(res?.data?.location_type || ""); setMiddlewareAddress(res?.data?.middleware_address || ""); } setIsLoading(false); @@ -66,6 +71,7 @@ export const AddLocationForm = (props: LocationFormProps) => { name: "", description: "", middlewareAddress: "", + locationType: "", }; if (name.trim().length === 0) { @@ -73,6 +79,11 @@ export const AddLocationForm = (props: LocationFormProps) => { formValid = false; } + if (locationType.trim().length === 0) { + error.locationType = "Location Type is required"; + formValid = false; + } + if ( middlewareAddress && middlewareAddress.match( @@ -98,6 +109,7 @@ export const AddLocationForm = (props: LocationFormProps) => { name, description, middleware_address: middlewareAddress, + location_type: locationType, }; const res = await dispatchAction( @@ -172,6 +184,27 @@ export const AddLocationForm = (props: LocationFormProps) => { error={errors.description} /> +
+ title} + optionValue={({ value }) => value} + value={locationType} + required + onChange={({ value }) => setLocationType(value)} + error={errors.locationType} + /> +
(); const [isFullscreen, setFullscreen] = useFullscreen(); - const sidebar = useContext(SidebarShrinkContext); const [facilityObject, setFacilityObject] = useState(); const [data, setData] = @@ -52,15 +50,6 @@ export default function CentralNursingStation({ facilityId }: Props) { limit: PER_PAGE_LIMIT, }); - // To automatically collapse sidebar. - useEffect(() => { - sidebar.setShrinked(true); - - return () => { - sidebar.setShrinked(sidebar.shrinked); - }; - }, []); - useEffect(() => { async function fetchFacilityOrObject() { if (facilityObject) return facilityObject; @@ -135,6 +124,7 @@ export default function CentralNursingStation({ facilityId }: Props) { backUrl={`/facility/${facilityId}/`} noImplicitPadding breadcrumbs={false} + collapseSidebar options={
@@ -264,8 +254,8 @@ export default function CentralNursingStation({ facilityId }: Props) {
) : (
- {data.map((props) => ( -
+ {data.map((props, i) => ( +
import("../../Common/PageTitle")); export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { const dispatch: any = useDispatch(); - const [showAutomatedRounds, setShowAutomatedRounds] = useState(true); const [hl7SocketUrl, setHL7SocketUrl] = useState(); const [ventilatorSocketUrl, setVentilatorSocketUrl] = useState(); const [monitorBedData, setMonitorBedData] = useState(); @@ -674,31 +673,7 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
-
- -
- setShowAutomatedRounds((s) => !s)} - /> - -
-
- +
diff --git a/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx b/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx index 63b5087cff8..ff738f4acb6 100644 --- a/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx +++ b/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx @@ -4,10 +4,11 @@ import CareIcon from "../../../../CAREUI/icons/CareIcon"; import ButtonV2 from "../../../Common/components/ButtonV2"; import { DailyRoundsModel } from "../../../Patient/models"; import LogUpdateCardAttribute from "./LogUpdateCardAttribute"; +import { ConsultationModel } from "../../models"; interface Props { round: DailyRoundsModel; - consultationData: any; + consultationData: ConsultationModel; onViewDetails: () => void; onUpdateLog?: () => void; } diff --git a/src/Components/Facility/Consultations/DailyRoundsFilter.tsx b/src/Components/Facility/Consultations/DailyRoundsFilter.tsx new file mode 100644 index 00000000000..62b8d63e824 --- /dev/null +++ b/src/Components/Facility/Consultations/DailyRoundsFilter.tsx @@ -0,0 +1,115 @@ +import { Popover, Transition } from "@headlessui/react"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import { Fragment } from "react"; +import { SelectFormField } from "../../Form/FormFields/SelectFormField"; +import TextFormField from "../../Form/FormFields/TextFormField"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import dayjs from "dayjs"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DailyRoundTypes, DailyRoundsModel } from "../../Patient/models"; +import { FieldChangeEvent } from "../../Form/FormFields/Utils"; + +type FilterState = { + rounds_type?: DailyRoundsModel["rounds_type"]; + taken_at_after?: string; + taken_at_before?: string; +}; + +interface Props { + onApply: (filter: FilterState) => void; +} + +export default function DailyRoundsFilter(props: Props) { + const { t } = useTranslation(); + const [filter, setFilter] = useState({}); + + const field = (name: keyof FilterState) => ({ + name, + value: filter[name], + onChange: (e: FieldChangeEvent) => + setFilter({ ...filter, [e.name]: e.value }), + labelClassName: "text-sm", + errorClassName: "hidden", + }); + + return ( +
+ + + + + {t("filter")} + + + + +
+
+
+ + {t("filter_by")} + +
+
+
+ t(o)} + optionValue={(o) => o} + /> + + + + + { + setFilter({}); + props.onApply({}); + }} + border + className="w-full" + > + {t("clear")} + + + + props.onApply(filter)} + border + className="w-full" + > + {t("apply")} + + +
+
+
+
+
+
+ ); +} diff --git a/src/Components/Facility/Consultations/DailyRoundsList.tsx b/src/Components/Facility/Consultations/DailyRoundsList.tsx index ffc70ddf175..2060d8657e7 100644 --- a/src/Components/Facility/Consultations/DailyRoundsList.tsx +++ b/src/Components/Facility/Consultations/DailyRoundsList.tsx @@ -6,88 +6,84 @@ import { useTranslation } from "react-i18next"; import LoadingLogUpdateCard from "./DailyRounds/LoadingCard"; import routes from "../../../Redux/api"; import PaginatedList from "../../../CAREUI/misc/PaginatedList"; +import PageTitle from "../../Common/PageTitle"; +import DailyRoundsFilter from "./DailyRoundsFilter"; +import { ConsultationModel } from "../models"; +import { useSlugs } from "../../../Common/hooks/useSlug"; -export const DailyRoundsList = (props: any) => { +interface Props { + consultation: ConsultationModel; +} + +export default function DailyRoundsList({ consultation }: Props) { + const [facilityId, patientId, consultationId] = useSlugs( + "facility", + "patient", + "consultation" + ); const { t } = useTranslation(); - const { - facilityId, - patientId, - consultationId, - consultationData, - showAutomatedRounds, - } = props; + + const consultationUrl = `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}`; return ( - {(_) => ( -
-
- - - {t("no_consultation_updates")} - - - - <> - {Array.from({ length: 3 }).map((_, i) => ( - - ))} - - - className="flex grow flex-col gap-3"> - {(item, items) => { - if (item.rounds_type === "AUTOMATED") { + {({ refetch }) => ( + <> +
+ + refetch({ query })} /> +
+ +
+
+ + + {t("no_consultation_updates")} + + + + <> + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + + + className="flex grow flex-col gap-3"> + {(item, items) => { + if (item.rounds_type === "AUTOMATED") { + return ( + + ); + } + + const itemUrl = + item.rounds_type === "NORMAL" + ? `${consultationUrl}/daily-rounds/${item.id}` + : `${consultationUrl}/daily_rounds/${item.id}`; + return ( - navigate(itemUrl)} + onUpdateLog={() => navigate(`${itemUrl}/update`)} /> ); - } - return ( - { - if (item.rounds_type === "NORMAL") { - navigate( - `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/daily-rounds/${item.id}` - ); - } else { - navigate( - `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/daily_rounds/${item.id}` - ); - } - }} - onUpdateLog={() => { - if (item.rounds_type === "NORMAL") { - navigate( - `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/daily-rounds/${item.id}/update` - ); - } else { - navigate( - `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/daily_rounds/${item.id}/update` - ); - } - }} - /> - ); - }} - -
- + }} + +
+ +
-
+ )} ); -}; +} diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx index 43d5b89cb38..fcd6d50360f 100644 --- a/src/Components/Facility/Consultations/Feed.tsx +++ b/src/Components/Facility/Consultations/Feed.tsx @@ -577,7 +577,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { ); } )} -
+
diff --git a/src/Components/Facility/Consultations/LiveFeed.tsx b/src/Components/Facility/Consultations/LiveFeed.tsx index cd3f055921e..a8f98a3c53b 100644 --- a/src/Components/Facility/Consultations/LiveFeed.tsx +++ b/src/Components/Facility/Consultations/LiveFeed.tsx @@ -461,7 +461,7 @@ const LiveFeed = (props: any) => { ); })} -
+
diff --git a/src/Components/Facility/FacilityFilter/index.tsx b/src/Components/Facility/FacilityFilter/index.tsx index fd6edd664d1..e9270ec7a63 100644 --- a/src/Components/Facility/FacilityFilter/index.tsx +++ b/src/Components/Facility/FacilityFilter/index.tsx @@ -1,4 +1,3 @@ -import { navigate } from "raviger"; import { FACILITY_TYPES } from "../../../Common/constants"; import useMergeState from "../../../Common/hooks/useMergeState"; import FiltersSlideover from "../../../CAREUI/interactive/FiltersSlideover"; @@ -18,7 +17,7 @@ const clearFilterState = { function FacilityFilter(props: any) { const { t } = useTranslation(); - const { filter, onChange, closeFilter } = props; + const { filter, onChange, closeFilter, removeFilters } = props; const [filterState, setFilterState] = useMergeState({ state: filter.state || "", @@ -66,8 +65,7 @@ function FacilityFilter(props: any) { advancedFilter={props} onApply={applyFilter} onClear={() => { - navigate("/facility"); - setFilterState(clearFilterState); + removeFilters(Object.keys(clearFilterState)); closeFilter(); }} > diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index 3ad295a3df3..b36a3a7b36d 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -4,7 +4,7 @@ import AuthorizeFor, { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import { FacilityModel } from "./models"; import { FACILITY_FEATURE_TYPES, USER_TYPES } from "../../Common/constants"; import DropdownMenu, { DropdownItem } from "../Common/components/Menu"; -import { lazy, useState } from "react"; +import { Fragment, lazy, useState } from "react"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; @@ -27,6 +27,10 @@ import useQuery from "../../Utils/request/useQuery.js"; import { FacilityHomeTriage } from "./FacilityHomeTriage.js"; import { FacilityDoctorList } from "./FacilityDoctorList.js"; import { FacilityBedCapacity } from "./FacilityBedCapacity.js"; +import useSlug from "../../Common/hooks/useSlug.js"; +import { Popover, Transition } from "@headlessui/react"; +import { FieldLabel } from "../Form/FormFields/FormField.js"; +import { LocationSelect } from "../Common/LocationSelect.js"; const Loading = lazy(() => import("../Common/Loading")); @@ -265,7 +269,7 @@ export const FacilityHome = (props: any) => {
- {facilityData?.features?.some((feature: any) => + {facilityData?.features?.some((feature) => FACILITY_FEATURE_TYPES.some((f) => f.id === feature) ) && (

Available features

@@ -390,16 +394,7 @@ export const FacilityHome = (props: any) => { Central Nursing Station - navigate(`/facility/${facilityId}/livefeed`)} - > - - Live Monitoring - + { ); }; + +const LiveMonitoringButton = () => { + const facilityId = useSlug("facility"); + const [location, setLocation] = useState(); + + return ( + + + + + Live Monitoring + + + + +
+
+
+ + Choose a location + +
+ setLocation(v as string | undefined)} + selected={location ?? null} + showAll={false} + multiple={false} + facilityId={facilityId} + errors="" + errorClassName="hidden" + /> +
+
+ + Open Live Monitoring + +
+
+
+
+
+ ); +}; diff --git a/src/Components/Facility/LiveFeedScreen.tsx b/src/Components/Facility/LiveFeedScreen.tsx deleted file mode 100644 index e744ef404cd..00000000000 --- a/src/Components/Facility/LiveFeedScreen.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { Fragment, useContext, useEffect, useState } from "react"; -import useFilters from "../../Common/hooks/useFilters"; -import useFullscreen from "../../Common/hooks/useFullscreen"; -import { FacilityModel } from "./models"; -import Loading from "../Common/Loading"; -import Page from "../Common/components/Page"; -import ButtonV2 from "../Common/components/ButtonV2"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import { classNames } from "../../Utils/utils"; -import { LocationSelect } from "../Common/LocationSelect"; -import Pagination from "../Common/Pagination"; -import { SidebarShrinkContext } from "../Common/Sidebar/Sidebar"; -import { AssetData } from "../Assets/AssetTypes"; -import { Popover, Transition } from "@headlessui/react"; -import { FieldLabel } from "../Form/FormFields/FormField"; -import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; -import LiveFeedTile from "./LiveFeedTile"; -import { getCameraConfig } from "../../Utils/transformUtils"; -import { getPermittedFacility, listAssets } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; - -const PER_PAGE_LIMIT = 6; - -interface Props { - facilityId: string; -} - -export default function LiveFeedScreen({ facilityId }: Props) { - const dispatch = useDispatch(); - const [isFullscreen, setFullscreen] = useFullscreen(); - const sidebar = useContext(SidebarShrinkContext); - - const [facility, setFacility] = useState(); - const [assets, setAssets] = useState(); - const [totalCount, setTotalCount] = useState(0); - const { qParams, updateQuery, removeFilter, updatePage } = useFilters({ - limit: PER_PAGE_LIMIT, - }); - - const [refresh_presets_hash, setRefreshPresetsHash] = useState( - Number(new Date()) - ); - - // To automatically collapse sidebar. - useEffect(() => { - sidebar.setShrinked(true); - - return () => { - sidebar.setShrinked(sidebar.shrinked); - }; - }, []); - - useEffect(() => { - async function fetchFacilityOrObject() { - if (facility) return facility; - const res = await dispatch(getPermittedFacility(facilityId)); - if (res.status !== 200) return; - setFacility(res.data); - return res.data as FacilityModel; - } - - async function fetchData() { - setAssets(undefined); - - const filters = { - ...qParams, - page: qParams.page || 1, - limit: PER_PAGE_LIMIT, - offset: (qParams.page ? qParams.page - 1 : 0) * PER_PAGE_LIMIT, - asset_class: "ONVIF", - facility: facilityId || "", - location: qParams.location, - bed_is_occupied: qParams.bed_is_occupied, - }; - - const [facilityObj, res] = await Promise.all([ - fetchFacilityOrObject(), - dispatch(listAssets(filters)), - ]); - - if (!facilityObj || res.status !== 200) { - return; - } - console.log(facilityObj, res.data); - const entries = res.data.results; - - setTotalCount(entries.length); - setAssets(entries); - } - fetchData(); - setRefreshPresetsHash(Number(new Date())); - }, [ - dispatch, - facilityId, - qParams.page, - qParams.location, - qParams.ordering, - qParams.bed_is_occupied, - ]); - - return ( - - - - - - Settings and Filters - - - - -
-
-
- - {totalCount} Camera - present - -
-
-
-
- - Filter by Location - -
- { - location - ? updateQuery({ location }) - : removeFilter("location"); - }} - selected={qParams.location} - showAll={false} - multiple={false} - facilityId={facilityId} - errors="" - errorClassName="hidden" - /> -
-
- { - if (value) { - updateQuery({ [name]: value }); - } else { - removeFilter(name); - } - }} - labelClassName="text-sm" - errorClassName="hidden" - /> - setFullscreen(!isFullscreen)} - className="tooltip !h-11" - > - - {isFullscreen ? "Exit Fullscreen" : "Fullscreen"} - -
-
-
-
-
- - updatePage(page)} - /> -
- } - > - {assets === undefined ? ( - - ) : assets.length === 0 ? ( -
- No Camera present in this location or facility. -
- ) : ( -
- {assets.map((asset, idx) => ( -
- {/* */} - -
- ))} -
- )} - - ); -} diff --git a/src/Components/Facility/LiveFeedTile.tsx b/src/Components/Facility/LiveFeedTile.tsx deleted file mode 100644 index 945240b9c6c..00000000000 --- a/src/Components/Facility/LiveFeedTile.tsx +++ /dev/null @@ -1,1045 +0,0 @@ -// import axios from "axios"; -// import React, { useEffect, useState, useRef, useCallback } from "react"; -// import * as Notification from "../../Utils/Notifications.js"; -// import { useDispatch } from "react-redux"; -// import ReactPlayer from "react-player"; -// import { getAsset, listAssetBeds } from "../../Redux/actions"; -// import { statusType, useAbortableEffect } from "../../Common/utils"; -// import { useTranslation } from "react-i18next"; -// import useFullscreen from "../../Common/hooks/useFullscreen.js"; -// interface LiveFeedTileProps { -// assetId: string; -// } - -// interface CameraPosition { -// x: number; -// y: number; -// zoom: number; -// } - -// // string:string dictionary -// interface CameraPreset { -// [key: string]: string; -// } - -// export default function LiveFeedTile(props: LiveFeedTileProps) { -// const dispatch: any = useDispatch(); -// const { assetId } = props; -// const [sourceUrl, setSourceUrl] = useState(); -// const [asset, setAsset] = useState(); -// const [presets, setPresets] = useState([]); -// const [bedPresets, setBedPresets] = useState([]); -// const [loading, setLoading] = useState(true); -// // const [showControls, setShowControls] = useState(false); -// const [showDefaultPresets, setShowDefaultPresets] = useState(false); -// const [position, setPosition] = useState({ -// x: 0, -// y: 0, -// zoom: 0, -// }); -// const { t } = useTranslation(); -// const [_isFullscreen, setFullscreen] = useFullscreen(); -// // const [toggle, setToggle] = useState(false); - -// useEffect(() => { -// let loadingTimeout: any; -// if (loading === true) -// loadingTimeout = setTimeout(() => { -// setLoading(false); -// }, 6000); -// return () => { -// if (loadingTimeout) clearTimeout(loadingTimeout); -// }; -// }, [loading]); - -// const fetchData = useCallback( -// async (status: statusType) => { -// setLoading(true); -// console.log("fetching asset"); -// const assetData: any = await dispatch(getAsset(assetId)); -// if (!status.aborted) { -// // setLoading(false); -// if (!assetData.data) -// Notification.Error({ -// msg: t("something_went_wrong"), -// }); -// else { -// setAsset(assetData.data); -// } -// } -// }, -// [dispatch, assetId] -// ); - -// useAbortableEffect( -// (status: statusType) => fetchData(status), -// [dispatch, fetchData] -// ); -// const requestStream = () => { -// axios -// .post(`https://${asset.meta.middleware_hostname}/start`, { -// uri: "rtsp://remote:qwerty123@192.168.1.64:554/", -// }) -// .then((resp: any) => { -// setSourceUrl( -// `https://${asset.meta.middleware_hostname}${resp.data.uri}` -// ); -// }) -// .catch((_ex: any) => { -// // console.error('Error while refreshing',ex); -// }); -// }; -// const stopStream = (url: string | undefined) => { -// console.log("stop", url); -// if (url) { -// const urlSegments = url.split("/"); -// const id = urlSegments?.pop(); -// axios -// .post(`https://${asset.meta.middleware_hostname}/stop`, { -// id, -// }) -// .then((resp: any) => { -// console.log(resp); -// // setSourceUrl(`https://${middlewareHostname}${resp.data.uri}`); -// }) -// .catch((_ex: any) => { -// // console.error('Error while refreshing',ex); -// }); -// } -// }; -// const getCameraStatus = (asset: any) => { -// axios -// .get( -// `https://${asset.meta.middleware_hostname}/status?hostname=${asset.hostname}&port=${asset.port}&username=${asset.username}&password=${asset.password}` -// ) -// .then((resp: any) => { -// setPosition(resp.data.position); -// }) -// .catch((_ex: any) => { -// // console.error('Error while refreshing',ex); -// }); -// }; -// const getPresets = (asset: any) => { -// const url = `https://${asset.meta.middleware_hostname}/presets?hostname=${asset.hostname}&port=${asset.port}&username=${asset.username}&password=${asset.password}`; -// axios -// .get(url) -// .then((resp: any) => { -// setPresets(resp.data); -// }) -// .catch((_ex: any) => { -// // console.error("Error while refreshing", ex); -// }); -// }; -// const getBedPresets = async (_asset: any) => { -// const bedAssets = await dispatch(listAssetBeds({ asset: props.assetId })); -// setBedPresets(bedAssets.data.results); -// }; -// const gotoBedPreset = (preset: any) => { -// absoluteMove(preset.meta.position); -// }; -// const gotoPreset = (preset: number) => { -// axios -// .post(`https://${asset.meta.middleware_hostname}/gotoPreset`, { -// ...asset, -// preset, -// }) -// .then((resp: any) => { -// console.log(resp.data); -// }) -// .catch((_ex: any) => { -// // console.error('Error while refreshing',ex); -// }); -// }; -// const requestPTZ = (action: string) => { -// setLoading(true); -// if (!position) { -// getCameraStatus(asset); -// } else { -// const data = { -// x: 0, -// y: 0, -// zoom: 0, -// } as any; -// console.log(action); -// // Relative X Y Coordinates -// switch (action) { -// case "up": -// data.y = 0.05; -// break; -// case "down": -// data.y = -0.05; -// break; -// case "left": -// data.x = -0.05; -// break; -// case "right": -// data.x = 0.05; -// break; -// case "zoomIn": -// data.zoom = 0.05; -// break; -// case "zoomOut": -// data.zoom = -0.05; -// break; -// case "stop": -// stopStream(sourceUrl); -// setSourceUrl(undefined); -// return; -// case "reset": -// setSourceUrl(undefined); -// requestStream(); -// return; -// default: -// break; -// } -// axios -// .post(`https://${asset.meta.middleware_hostname}/relativeMove`, { -// ...data, -// ...asset, -// }) -// .then((resp: any) => { -// console.log(resp.data); -// getCameraStatus(asset); -// }) -// .catch((_ex: any) => { -// // console.error('Error while refreshing',ex); -// }); -// } -// }; - -// const absoluteMove = (data: any) => { -// setLoading(true); -// axios -// .post(`https://${asset.meta.middleware_hostname}/absoluteMove`, { -// ...data, -// ...asset, -// }) -// .then((_resp: any) => { -// getCameraStatus(asset); -// }) -// .catch((ex: any) => { -// console.error("Error while absolute move", ex); -// }); -// }; - -// useEffect(() => { -// if (asset) { -// getPresets(asset); -// getBedPresets(asset); -// requestStream(); -// } -// }, [asset]); - -// useEffect(() => { -// if (bedPresets.length > 0) absoluteMove(bedPresets[0].meta.position); -// }, [bedPresets]); - -// // useEffect(() => { -// // const timer = setTimeout(() => { -// // setShowControls(toggle); -// // }, 300); -// // return () => clearTimeout(timer); -// // }, [toggle]); - -// const liveFeedPlayerRef = useRef(null); -// const handleClickFullscreen = () => { -// if (liveFeedPlayerRef.current) { -// setFullscreen(true, liveFeedPlayerRef.current.wrapper); -// } -// }; - -// const viewOptions = presets -// ? Object.entries(presets) -// .map(([key, value]) => ({ label: key, value })) -// .slice(0, 10) -// : Array.from(Array(10), (_, i) => ({ -// label: t("monitor") + (i + 1), -// value: i + 1, -// })); - -// const cameraPTZ = [ -// { icon: "fa fa-arrow-up", label: t("up"), action: "up" }, -// { icon: "fa fa-arrow-down", label: t("down"), action: "down" }, -// { icon: "fa fa-arrow-left", label: t("left"), action: "left" }, -// { icon: "fa fa-arrow-right", label: t("right"), action: "right" }, -// { icon: "fa fa-search-plus", label: t("zoom_in"), action: "zoomIn" }, -// { icon: "fa fa-search-minus", label: t("zoom_out"), action: "zoomOut" }, -// { icon: "fa fa-stop", label: t("stop"), action: "stop" }, -// { icon: "fa fa-undo", label: t("reset"), action: "reset" }, -// ]; - -// return ( -//
-//
-//
-//
-// {sourceUrl ? ( -//
-// { -// // requestStream(); -// console.log("Error", e); -// console.log("Data", data); -// console.log("HLS Instance", hlsInstance); -// console.log("HLS Global", hlsGlobal); -// if (e === "hlsError") { -// const recovered = hlsInstance.recoverMediaError(); -// console.log(recovered); -// } -// }} -// /> -//
-// ) : ( -//
-//

-// STATUS: OFFLINE -//

-//

-// {t("feed_is_currently_not_live")} -//

-//
-// )} -//
-//
-//
-//
-//
-// {cameraPTZ.map((option: any) => ( -//
{ -// // console.log(option.action); -// requestPTZ(option.action); -// }} -// > -// -//
-// ))} -// -//
-// {/*
-// -//
*/} -//
-//
-//
-//
-// {/* div with "Loading" at the center */} -//
-// -// -// -// -//
{t("moving_camera")}
-//
-//
-//
-//
-// -// {showDefaultPresets -// ? viewOptions.map((option: any) => ( -//
{ -// setLoading(true); -// gotoPreset(option.value); -// }} -// > -// -//
-// )) -// : bedPresets.map((preset: any, index: number) => ( -//
{ -// setLoading(true); -// gotoBedPreset(preset); -// }} -// key={preset.id} -// > -// -//
-// ))} -//
-//
-//
-// ); -// } - -import { useEffect, useState, useRef } from "react"; -import { useDispatch } from "react-redux"; -import useKeyboardShortcut from "use-keyboard-shortcut"; -import { - listAssetBeds, - partialUpdateAssetBed, - deleteAssetBed, -} from "../../Redux/actions"; -import { getCameraPTZ } from "../../Common/constants"; -import { - StreamStatus, - useMSEMediaPlayer, -} from "../../Common/hooks/useMSEplayer"; -import { useFeedPTZ } from "../../Common/hooks/useFeedPTZ"; -import * as Notification from "../../Utils/Notifications.js"; -import { AxiosError } from "axios"; -import { BedSelect } from "../Common/BedSelect"; -import { BedModel } from "./models"; -import useWindowDimensions from "../../Common/hooks/useWindowDimensions"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import ConfirmDialog from "../Common/ConfirmDialog"; -import { FieldLabel } from "../Form/FormFields/FormField"; -import useFullscreen from "../../Common/hooks/useFullscreen"; -import { FeedCameraPTZHelpButton } from "./Consultations/Feed"; - -const LiveFeed = (props: any) => { - const middlewareHostname = - props.middlewareHostname || "dev_middleware.coronasafe.live"; - 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 [preset, setNewPreset] = 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 videoEl = liveFeedPlayerRef.current as HTMLVideoElement; - - const url = `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0`; - - const { startStream } = useMSEMediaPlayer({ - config: { - middlewareHostname, - ...cameraAsset, - }, - url, - videoEl, - }); - - const refreshPresetsHash = props.refreshPresetsHash; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [currentPreset, setCurrentPreset] = useState(); - const { - absoluteMove, - getCameraStatus, - 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: preset, - }; - 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(); - } - }, []); - - useEffect(() => { - setNewPreset(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 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(() => { - startStream({ - onSuccess: () => setStreamStatus(StreamStatus.Playing), - onError: () => setStreamStatus(StreamStatus.Offline), - }); - }, 500); - } - - return () => { - clearTimeout(tId); - }; - }, [startStream, 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: () => { - setStreamStatus(StreamStatus.Loading); - setVideoStartTime(null); - startStream({ - onSuccess: () => setStreamStatus(StreamStatus.Playing), - onError: () => setStreamStatus(StreamStatus.Offline), - }); - }, - fullScreen: () => { - if (!liveFeedPlayerRef.current) return; - setFullscreen(true, liveFeedPlayerRef.current); - }, - updatePreset: (option) => { - getCameraStatus({ - onSuccess: async (data) => { - console.log({ currentPreset, 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)} - > -
- Bed - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={cameraAsset.location_id} - facility={cameraAsset.facility_id} - /> -
-
- )} -
-
-
- {/* ADD VIDEO PLAYER HERE */} -
- - - {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 LiveFeed; diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index cdc7074c145..0d8a112e021 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -7,6 +7,7 @@ import { RouteToFacility } from "../Common/RouteToFacilitySelect"; import { ConsultationDiagnosis, CreateDiagnosis } from "../Diagnosis/types"; export interface LocalBodyModel { + id: number; name: string; body_type: number; localbody_code: string; @@ -107,7 +108,7 @@ export interface ConsultationModel { history_of_present_illness?: string; facility?: number; facility_name?: string; - id?: string; + id: string; modified_date?: string; other_symptoms?: string; patient?: string; diff --git a/src/Components/Form/FormFields/SelectFormField.tsx b/src/Components/Form/FormFields/SelectFormField.tsx index 3c6613bb662..5afe4f11d63 100644 --- a/src/Components/Form/FormFields/SelectFormField.tsx +++ b/src/Components/Form/FormFields/SelectFormField.tsx @@ -7,7 +7,7 @@ type OptionCallback = (option: T) => R; type SelectFormFieldProps = FormFieldBaseProps & { placeholder?: React.ReactNode; - options: T[]; + options: readonly T[]; position?: "above" | "below"; optionLabel: OptionCallback; optionSelectedLabel?: OptionCallback; diff --git a/src/Components/Patient/PatientFilter.tsx b/src/Components/Patient/PatientFilter.tsx index 10b170f4547..0599cb2625b 100644 --- a/src/Components/Patient/PatientFilter.tsx +++ b/src/Components/Patient/PatientFilter.tsx @@ -1,7 +1,5 @@ import dayjs from "dayjs"; -import { navigate } from "raviger"; import { useCallback, useEffect } from "react"; -import { useDispatch } from "react-redux"; import CareIcon from "../../CAREUI/icons/CareIcon"; import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; import { @@ -19,6 +17,7 @@ import { getAnyFacility, getDistrict, } from "../../Redux/actions"; +import { useDispatch } from "react-redux"; import { dateQueryString } from "../../Utils/utils"; import { DateRange } from "../Common/DateRangeInputV2"; import { FacilitySelect } from "../Common/FacilitySelect"; @@ -41,7 +40,7 @@ const getDate = (value: any) => export default function PatientFilter(props: any) { const { kasp_enabled, kasp_string } = useConfig(); - const { filter, onChange, closeFilter } = props; + const { filter, onChange, closeFilter, removeFilters } = props; const [filterState, setFilterState] = useMergeState({ district: filter.district || "", @@ -337,8 +336,7 @@ export default function PatientFilter(props: any) { advancedFilter={props} onApply={applyFilter} onClear={() => { - navigate("/patients"); - setFilterState(clearFilterState); + removeFilters(Object.keys(clearFilterState)); closeFilter(); }} > diff --git a/src/Components/Patient/SampleFilters.tsx b/src/Components/Patient/SampleFilters.tsx index db22e54ac80..44af9a6a4d5 100644 --- a/src/Components/Patient/SampleFilters.tsx +++ b/src/Components/Patient/SampleFilters.tsx @@ -4,7 +4,6 @@ import { SAMPLE_TEST_RESULT, SAMPLE_TYPE_CHOICES, } from "../../Common/constants"; -import { navigate } from "raviger"; import { FacilitySelect } from "../Common/FacilitySelect"; import { FacilityModel } from "../Facility/models"; import { getAnyFacility } from "../../Redux/actions"; @@ -25,7 +24,7 @@ const clearFilterState = { }; export default function UserFilter(props: any) { - const { filter, onChange, closeFilter } = props; + const { filter, onChange, closeFilter, removeFilters } = props; const [filterState, setFilterState] = useMergeState({ status: filter.status || "", @@ -74,8 +73,7 @@ export default function UserFilter(props: any) { advancedFilter={props} onApply={applyFilter} onClear={() => { - navigate("/sample"); - setFilterState(clearFilterState); + removeFilters(Object.keys(clearFilterState)); closeFilter(); }} > diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index 341e13e3c80..af69d8464bc 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -269,6 +269,8 @@ export interface DailyRoundsOutput { quantity: number; } +export const DailyRoundTypes = ["NORMAL", "VENTILATOR", "AUTOMATED"] as const; + export interface DailyRoundsModel { ventilator_spo2?: number; spo2?: string; @@ -290,7 +292,7 @@ export interface DailyRoundsModel { medication_given?: Array; additional_symptoms_text?: string; current_health?: string; - id?: any; + id: string; other_symptoms?: string; admitted_to?: string; patient_category?: PatientCategory; @@ -299,7 +301,7 @@ export interface DailyRoundsModel { created_date?: string; modified_date?: string; taken_at?: string; - rounds_type?: "NORMAL" | "VENTILATOR" | "ICU" | "AUTOMATED"; + rounds_type: (typeof DailyRoundTypes)[number]; last_updated_by_telemedicine?: boolean; created_by_telemedicine?: boolean; created_by?: { @@ -314,6 +316,7 @@ export interface DailyRoundsModel { }; bed?: string; } + export interface FacilityNameModel { id?: string; name?: string; diff --git a/src/Components/Resource/ListFilter.tsx b/src/Components/Resource/ListFilter.tsx index afe48eedfd0..e232e404520 100644 --- a/src/Components/Resource/ListFilter.tsx +++ b/src/Components/Resource/ListFilter.tsx @@ -2,7 +2,6 @@ import { FacilitySelect } from "../Common/FacilitySelect"; import { RESOURCE_FILTER_ORDER } from "../../Common/constants"; import { RESOURCE_CHOICES } from "../../Common/constants"; import useMergeState from "../../Common/hooks/useMergeState"; -import { navigate } from "raviger"; import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; import { FieldLabel } from "../Form/FormFields/FormField"; import CircularProgress from "../Common/components/CircularProgress"; @@ -35,7 +34,7 @@ const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); export default function ListFilter(props: any) { - const { filter, onChange, closeFilter } = props; + const { filter, onChange, closeFilter, removeFilters } = props; const [filterState, setFilterState] = useMergeState({ origin_facility: filter.origin_facility || "", origin_facility_ref: null, @@ -140,8 +139,7 @@ export default function ListFilter(props: any) { advancedFilter={props} onApply={applyFilter} onClear={() => { - navigate("/resource"); - setFilterState(clearFilterState); + removeFilters(Object.keys(clearFilterState)); closeFilter(); }} > diff --git a/src/Components/Shifting/ListFilter.tsx b/src/Components/Shifting/ListFilter.tsx index ef366d5b038..8d54ec91ce7 100644 --- a/src/Components/Shifting/ListFilter.tsx +++ b/src/Components/Shifting/ListFilter.tsx @@ -18,7 +18,6 @@ import DateRangeFormField from "../Form/FormFields/DateRangeFormField"; import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; -import { navigate } from "raviger"; import useConfig from "../../Common/hooks/useConfig"; import useMergeState from "../../Common/hooks/useMergeState"; @@ -58,7 +57,7 @@ const getDate = (value: any) => export default function ListFilter(props: any) { const { kasp_enabled, kasp_string, wartime_shifting } = useConfig(); - const { filter, onChange, closeFilter } = props; + const { filter, onChange, closeFilter, removeFilters } = props; const { t } = useTranslation(); @@ -221,8 +220,7 @@ export default function ListFilter(props: any) { advancedFilter={props} onApply={applyFilter} onClear={() => { - navigate("/shifting"); - setFilterState(clearFilterState); + removeFilters(Object.keys(clearFilterState)); closeFilter(); }} > diff --git a/src/Components/Shifting/ShiftDetails.tsx b/src/Components/Shifting/ShiftDetails.tsx index 3d2a1f60b8b..a898b0cde15 100644 --- a/src/Components/Shifting/ShiftDetails.tsx +++ b/src/Components/Shifting/ShiftDetails.tsx @@ -806,13 +806,12 @@ export default function ShiftDetails(props: { id: string }) { {t("created")}
-
- {data?.created_by_object?.first_name} - {data?.created_by_object?.last_name} -
-
- {data?.created_date && formatDateTime(data?.created_date)} -
+
@@ -820,14 +819,12 @@ export default function ShiftDetails(props: { id: string }) { {t("last_edited")}
-
- {data?.last_edited_by_object?.first_name}{" "} - {data?.last_edited_by_object?.last_name} -
-
- {data?.modified_date && - formatDateTime(data?.modified_date)} -
+
diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index 94791e55ae1..3b469081754 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -1,17 +1,5 @@ import * as Notification from "../../Utils/Notifications.js"; -import { - addUserFacility, - clearHomeFacility, - deleteUser, - deleteUserFacility, - getDistrict, - getUserList, - getUserListFacility, - partialUpdateUser, -} from "../../Redux/actions"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { lazy, useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; +import { lazy, useState } from "react"; import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; import ButtonV2, { Submit } from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; @@ -36,6 +24,9 @@ import Page from "../Common/components/Page.js"; import dayjs from "dayjs"; import TextFormField from "../Form/FormFields/TextFormField.js"; import useAuthUser from "../../Common/hooks/useAuthUser.js"; +import routes from "../../Redux/api.js"; +import useQuery from "../../Utils/request/useQuery.js"; +import request from "../../Utils/request/request.js"; const Loading = lazy(() => import("../Common/Loading")); @@ -49,19 +40,13 @@ export default function ManageUsers() { advancedFilter, resultsPerPage, } = useFilters({ limit: 18 }); - const dispatch: any = useDispatch(); - const initialData: any[] = []; let manageUsers: any = null; - const [users, setUsers] = useState(initialData); - const [isLoading, setIsLoading] = useState(false); const [expandSkillList, setExpandSkillList] = useState(false); - const [totalCount, setTotalCount] = useState(0); - const [districtName, setDistrictName] = useState(); const [expandFacilityList, setExpandFacilityList] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [expandWorkingHours, setExpandWorkingHours] = useState(false); const authUser = useAuthUser(); - const [weeklyHours, setWeeklyHours] = useState(0); + const [weeklyHours, setWeeklyHours] = useState("0"); const userIndex = USER_TYPES.indexOf(authUser.user_type); const userTypes = authUser.is_superuser ? [...USER_TYPES] @@ -79,58 +64,32 @@ export default function ManageUsers() { const isExtremeSmallScreen = width <= extremeSmallScreenBreakpoint ? true : false; - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const params = { - limit: resultsPerPage, - offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, - username: qParams.username, - first_name: qParams.first_name, - last_name: qParams.last_name, - phone_number: qParams.phone_number, - alt_phone_number: qParams.alt_phone_number, - user_type: qParams.user_type, - district_id: qParams.district_id, - }; - if (qParams.district_id) { - const dis = await dispatch(getDistrict(qParams.district_id)); - if (!status.aborted) { - if (dis && dis.data) { - setDistrictName(dis.data.name); - } - } - } else { - setDistrictName(undefined); - } - const res = await dispatch(getUserList(params)); - if (!status.aborted) { - if (res && res.data) { - setUsers(res.data.results); - setTotalCount(res.data.count); - } - setIsLoading(false); - } + const { + data: userListData, + loading: userListLoading, + refetch: refetchUserList, + } = useQuery(routes.userList, { + query: { + limit: resultsPerPage.toString(), + offset: ( + (qParams.page ? qParams.page - 1 : 0) * resultsPerPage + ).toString(), + username: qParams.username, + first_name: qParams.first_name, + last_name: qParams.last_name, + phone_number: qParams.phone_number, + alt_phone_number: qParams.alt_phone_number, + user_type: qParams.user_type, + district_id: qParams.district_id, }, - [ - resultsPerPage, - qParams.page, - qParams.username, - qParams.first_name, - qParams.last_name, - qParams.phone_number, - qParams.alt_phone_number, - qParams.user_type, - qParams.district_id, - dispatch, - ] - ); + }); - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [fetchData] + const { data: districtData, loading: districtDataLoading } = useQuery( + routes.getDistrict, + { + prefetch: !!qParams.district_id, + pathParams: { id: qParams.district_id }, + } ); const addUser = ( @@ -150,17 +109,15 @@ export default function ManageUsers() { const handleWorkingHourSubmit = async () => { const username = selectedUser; - if (!username || !weeklyHours || weeklyHours < 0 || weeklyHours > 168) { + if (!username || !weeklyHours || +weeklyHours < 0 || +weeklyHours > 168) { setWeeklyHoursError("Value should be between 0 and 168"); return; } - const res = await dispatch( - partialUpdateUser(username, { - weekly_working_hours: weeklyHours, - }) - ); - - if (res?.data) { + const { res, data, error } = await request(routes.partialUpdateUser, { + pathParams: { username }, + body: { weekly_working_hours: weeklyHours }, + }); + if (res && res.status === 200 && data) { Notification.Success({ msg: "Working hours updated successfully", }); @@ -168,29 +125,30 @@ export default function ManageUsers() { setSelectedUser(null); } else { Notification.Error({ - msg: "Error while updating working hours: " + (res.data.detail || ""), + msg: "Error while updating working hours: " + (error || ""), }); } - setWeeklyHours(0); + setWeeklyHours("0"); setWeeklyHoursError(""); - fetchData({ aborted: false }); + await refetchUserList(); }; const handleSubmit = async () => { - const username = userData.username; - const res = await dispatch(deleteUser(username)); + const { res, error } = await request(routes.deleteUser, { + pathParams: { username: userData.username }, + }); if (res?.status === 204) { Notification.Success({ msg: "User deleted successfully", }); } else { Notification.Error({ - msg: "Error while deleting User: " + (res?.data?.detail || ""), + msg: "Error while deleting User: " + (error || ""), }); } setUserData({ show: false, username: "", name: "" }); - fetchData({ aborted: false }); + await refetchUserList(); }; const handleDelete = (user: any) => { @@ -214,9 +172,9 @@ export default function ManageUsers() { let userList: any[] = []; - users && - users.length && - (userList = users.map((user: any, idx) => { + userListData?.results && + userListData.results.length && + (userList = userListData.results.map((user: any, idx) => { const cur_online = isUserOnline(user); return (
; - } else if (users?.length) { + } else if (userListData?.results.length) { manageUsers = (
{userList}
- +
); - } else if (users && users.length === 0) { + } else if (userListData?.results && userListData?.results.length === 0) { manageUsers = (
No Users Found
@@ -505,7 +463,7 @@ export default function ManageUsers() { open={expandWorkingHours} setOpen={(state) => { setExpandWorkingHours(state); - setWeeklyHours(0); + setWeeklyHours("0"); setWeeklyHoursError(""); }} slideFrom="right" @@ -539,8 +497,8 @@ export default function ManageUsers() {
@@ -574,7 +532,11 @@ export default function ManageUsers() { phoneNumber(), phoneNumber("WhatsApp no.", "alt_phone_number"), badge("Role", "user_type"), - value("District", "district_id", districtName || ""), + value( + "District", + "district_id", + qParams.district_id ? districtData?.name || "" : "" + ), ]} />
@@ -596,8 +558,6 @@ export default function ManageUsers() { function UserFacilities(props: { user: any }) { const { user } = props; const username = user.username; - const dispatch: any = useDispatch(); - const [facilities, setFacilities] = useState([]); const [isLoading, setIsLoading] = useState(false); const [facility, setFacility] = useState(null); const [unlinkFacilityData, setUnlinkFacilityData] = useState<{ @@ -635,62 +595,59 @@ function UserFacilities(props: { user: any }) { }); }; - const fetchFacilities = async () => { - setIsLoading(true); - const res = await dispatch(getUserListFacility({ username })); - if (res && res.data) { - setFacilities(res.data); - } - setIsLoading(false); - }; + const { + data: userFacilities, + loading: userFacilitiesLoading, + refetch: refetchUserFacilities, + } = useQuery(routes.userListFacility, { + pathParams: { username }, + }); const updateHomeFacility = async (username: string, facility: any) => { setIsLoading(true); - const res = await dispatch( - partialUpdateUser(username, { home_facility: facility.id }) - ); + const { res } = await request(routes.partialUpdateUser, { + pathParams: { username }, + body: { home_facility: facility.id.toString() }, + }); if (res && res.status === 200) user.home_facility_object = facility; - fetchFacilities(); + await refetchUserFacilities(); setIsLoading(false); }; const handleUnlinkFacilitySubmit = async () => { setIsLoading(true); if (unlinkFacilityData.isHomeFacility) { - const res = await dispatch( - clearHomeFacility(unlinkFacilityData.userName) - ); + const { res } = await request(routes.clearHomeFacility, { + pathParams: { username }, + }); if (res && res.status === 204) user.home_facility_object = null; } else { - await dispatch( - deleteUserFacility( - unlinkFacilityData.userName, - String(unlinkFacilityData?.facility?.id) - ) - ); + await request(routes.deleteUserFacility, { + pathParams: { username }, + body: { facility: unlinkFacilityData?.facility?.id?.toString() }, + }); } - fetchFacilities(); - setIsLoading(false); + await refetchUserFacilities(); hideUnlinkFacilityModal(); + setIsLoading(false); }; const addFacility = async (username: string, facility: any) => { setIsLoading(true); - const res = await dispatch(addUserFacility(username, String(facility.id))); + const { res } = await request(routes.addUserFacility, { + pathParams: { username }, + body: { facility: facility.id.toString() }, + }); if (res?.status !== 201) { Notification.Error({ msg: "Error while linking facility", }); } + await refetchUserFacilities(); setIsLoading(false); setFacility(null); - fetchFacilities(); }; - useEffect(() => { - fetchFacilities(); - }, []); - return (
{unlinkFacilityData.show && ( @@ -723,7 +680,7 @@ function UserFacilities(props: { user: any }) { Add
- {isLoading ? ( + {isLoading || userFacilitiesLoading ? (
@@ -761,13 +718,13 @@ function UserFacilities(props: { user: any }) { )} {/* Linked Facilities section */} - {facilities.length > 0 && ( + {userFacilities?.length && (
Linked Facilities
- {facilities.map((facility: any, i: number) => { + {userFacilities.map((facility: any, i: number) => { if (user?.home_facility_object?.id === facility.id) { // skip if it's a home facility return null; @@ -831,7 +788,7 @@ function UserFacilities(props: { user: any }) {
)} - {!user?.home_facility_object && facilities.length === 0 && ( + {!user?.home_facility_object && !userFacilities?.length && (
{ /* added const {t} hook here and relevant text to Common.json to avoid eslint error */ const { t } = useTranslation(); - const [skills, setSkills] = useState([]); const [selectedSkill, setSelectedSkill] = useState( null ); const [isLoading, setIsLoading] = useState(false); const [deleteSkill, setDeleteSkill] = useState(null); - const dispatch: any = useDispatch(); - const fetchSkills = useCallback( - async (username: string) => { - setIsLoading(true); - const res = await dispatch(getUserListSkills({ username })); - if (res && res.data) { - setSkills(res.data.results); - } - setIsLoading(false); - }, - [dispatch] - ); + const { + data: skills, + loading: skillsLoading, + refetch: refetchUserSkills, + } = useQuery(routes.userListSkill, { + pathParams: { username }, + }); const addSkill = useCallback( async (username: string, skill: SkillObjectModel | null) => { if (!skill) return; setIsLoading(true); - const res = await dispatch(addUserSkill(username, skill.id)); - if (res?.status !== 201) { + const { res } = await request(routes.addUserSkill, { + pathParams: { username }, + body: { skill: skill.id }, + }); + if (!res?.ok) { Notification.Error({ msg: "Error while adding skill", }); @@ -62,36 +56,32 @@ export default ({ show, setShow, username }: IProps) => { } setSelectedSkill(null); setIsLoading(false); - fetchSkills(username); + await refetchUserSkills(); }, - [dispatch, fetchSkills] + [refetchUserSkills] ); const removeSkill = useCallback( async (username: string, skillId: string) => { - const res = await dispatch(deleteUserSkill(username, skillId)); + const { res } = await request(routes.deleteUserSkill, { + pathParams: { username, id: skillId }, + }); if (res?.status !== 204) { Notification.Error({ msg: "Error while unlinking skill", }); } setDeleteSkill(null); - fetchSkills(username); + await refetchUserSkills(); }, - [dispatch, fetchSkills] + [refetchUserSkills] ); - useEffect(() => { - setIsLoading(true); - if (username) fetchSkills(username); - setIsLoading(false); - }, [username, fetchSkills]); - const authorizeForAddSkill = useIsAuthorized( AuthorizeFor(["DistrictAdmin", "StateAdmin"]) ); - const hasSkills = useMemo(() => skills.length > 0, [skills]); + const hasSkills = skills?.results?.length || 0 > 0; return (
@@ -114,7 +104,7 @@ export default ({ show, setShow, username }: IProps) => { >
- {!isLoading && ( + {(!isLoading || !skillsLoading) && (
{ setSelected={setSelectedSkill} errors="" username={username} + userSkills={skills?.results || []} /> { )}
)} - {isLoading ? ( + {isLoading || skillsLoading ? (
@@ -151,8 +142,8 @@ export default ({ show, setShow, username }: IProps) => {
{hasSkills ? ( diff --git a/src/Components/Users/UserAdd.tsx b/src/Components/Users/UserAdd.tsx index a6553bad01b..7df0089cdac 100644 --- a/src/Components/Users/UserAdd.tsx +++ b/src/Components/Users/UserAdd.tsx @@ -1,26 +1,17 @@ import { Link, navigate } from "raviger"; -import { lazy, useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; +import { lazy, useEffect, useState } from "react"; import { GENDER_TYPES, USER_TYPES, USER_TYPE_OPTIONS, } from "../../Common/constants"; -import { statusType, useAbortableEffect } from "../../Common/utils"; +import { useAbortableEffect } from "../../Common/utils"; import { validateEmailAddress, validateName, validatePassword, validateUsername, } from "../../Common/validation"; -import { - addUser, - getDistrictByState, - getLocalbodyByDistrict, - getStates, - getUserListFacility, - checkUsername, -} from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; import { FacilitySelect } from "../Common/FacilitySelect"; import { FacilityModel } from "../Facility/models"; @@ -45,6 +36,9 @@ import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave"; import dayjs from "../../Utils/dayjs"; import useAuthUser from "../../Common/hooks/useAuthUser"; import { PhoneNumberValidator } from "../Form/FieldValidators"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; +import useQuery from "../../Utils/request/useQuery"; const Loading = lazy(() => import("../Common/Loading")); @@ -163,7 +157,6 @@ export const validateRule = ( export const UserAdd = (props: UserProps) => { const { goBack } = useAppHistory(); - const dispatchAction: any = useDispatch(); const { userId } = props; const [state, dispatch] = useAutoSaveReducer( @@ -171,13 +164,9 @@ export const UserAdd = (props: UserProps) => { initialState ); const [isLoading, setIsLoading] = useState(false); - const [isStateLoading, setIsStateLoading] = useState(false); - const [isDistrictLoading, setIsDistrictLoading] = useState(false); - const [isLocalbodyLoading, setIsLocalbodyLoading] = useState(false); - const [_current_user_facilities, setFacilities] = useState< - Array - >([]); const [states, setStates] = useState([]); + const [selectedStateId, setSelectedStateId] = useState(0); + const [selectedDistrictId, setSelectedDistrictId] = useState(0); const [districts, setDistricts] = useState([]); const [localBodies, setLocalBodies] = useState([]); const [selectedFacility, setSelectedFacility] = useState([]); @@ -198,9 +187,9 @@ export const UserAdd = (props: UserProps) => { const check_username = async (username: string) => { setUsernameExists(userExistsEnums.checking); - const usernameCheck = await dispatchAction( - checkUsername({ username: username }) - ); + const { res: usernameCheck } = await request(routes.checkUsername, { + pathParams: { username }, + }); if (usernameCheck === undefined || usernameCheck.status === 409) setUsernameExists(userExistsEnums.exists); else if (usernameCheck.status === 200) @@ -254,101 +243,45 @@ export const UserAdd = (props: UserProps) => { state.form.user_type === "StaffReadOnly" ); - const fetchDistricts = useCallback( - async (id: number) => { - if (id > 0) { - setIsDistrictLoading(true); - const districtList = await dispatchAction(getDistrictByState({ id })); - if (districtList) { - if (userIndex <= USER_TYPES.indexOf("DistrictAdmin")) { - setDistricts([ - { - id: authUser.district!, - name: authUser.district_object?.name as string, - }, - ]); - } else { - setDistricts(districtList.data); - } - } - setIsDistrictLoading(false); + const { loading: isDistrictLoading } = useQuery(routes.getDistrictByState, { + prefetch: !!(selectedStateId > 0), + pathParams: { id: selectedStateId.toString() }, + onResponse: (result) => { + if (!result || !result.res || !result.data) return; + if (userIndex <= USER_TYPES.indexOf("DistrictAdmin")) { + setDistricts([authUser.district_object!]); + } else { + setDistricts(result.data); } }, - [dispatchAction] - ); - - const fetchLocalBody = useCallback( - async (id: number) => { - if (id > 0) { - setIsLocalbodyLoading(true); - const localBodyList = await dispatchAction( - getLocalbodyByDistrict({ id }) - ); - setIsLocalbodyLoading(false); - if (localBodyList) { - if (userIndex <= USER_TYPES.indexOf("LocalBodyAdmin")) { - setLocalBodies([ - { - id: authUser.local_body!, - name: authUser.local_body_object?.name as string, - }, - ]); - } else { - setLocalBodies(localBodyList.data); - } - } - } - }, - [dispatchAction] - ); - - const fetchStates = useCallback( - async (status: statusType) => { - setIsStateLoading(true); - const statesRes = await dispatchAction(getStates()); - if (!status.aborted && statesRes.data.results) { - if (userIndex <= USER_TYPES.indexOf("StateAdmin")) { - setStates([ - { - id: authUser.state!, - name: authUser.state_object?.name as string, - }, - ]); + }); + + const { loading: isLocalbodyLoading } = useQuery( + routes.getAllLocalBodyByDistrict, + { + prefetch: !!(selectedDistrictId > 0), + pathParams: { id: selectedDistrictId.toString() }, + onResponse: (result) => { + if (!result || !result.res || !result.data) return; + if (userIndex <= USER_TYPES.indexOf("LocalBodyAdmin")) { + setLocalBodies([authUser.local_body_object!]); } else { - setStates(statesRes.data.results); + setLocalBodies(result.data); } - } - setIsStateLoading(false); - }, - [dispatchAction] - ); - - const fetchFacilities = useCallback( - async (status: any) => { - setIsStateLoading(true); - const res = await dispatchAction( - getUserListFacility({ username: authUser.username }) - ); - if (!status.aborted && res && res.data) { - setFacilities(res.data); - } - setIsStateLoading(false); - }, - [dispatchAction, authUser.username] + }, + } ); - useAbortableEffect( - (status: statusType) => { - fetchStates(status); - if ( - authUser.user_type === "Staff" || - authUser.user_type === "StaffReadOnly" - ) { - fetchFacilities(status); + const { loading: isStateLoading } = useQuery(routes.statesList, { + onResponse: (result) => { + if (!result || !result.res || !result.data) return; + if (userIndex <= USER_TYPES.indexOf("StateAdmin")) { + setStates([authUser.state_object!]); + } else { + setStates(result.data.results); } }, - [dispatch] - ); + }); const handleDateChange = (e: FieldChangeEvent) => { if (dayjs(e.value).isValid()) { @@ -605,13 +538,10 @@ export const UserAdd = (props: UserProps) => { : undefined, }; - const res = await dispatchAction(addUser(data)); - if ( - res && - (res.data || res.data === "") && - res.status >= 200 && - res.status < 300 - ) { + const { res } = await request(routes.addUser, { + body: data, + }); + if (res?.ok) { dispatch({ type: "set_form", form: initForm }); if (!userId) { Notification.Success({ @@ -916,7 +846,7 @@ export const UserAdd = (props: UserProps) => { optionValue={(o) => o.id} onChange={(e) => { handleFieldChange(e); - if (e) fetchDistricts(e.value); + if (e) setSelectedStateId(e.value); }} /> )} @@ -934,7 +864,7 @@ export const UserAdd = (props: UserProps) => { optionValue={(o) => o.id} onChange={(e) => { handleFieldChange(e); - if (e) fetchLocalBody(e.value); + if (e) setSelectedDistrictId(e.value); }} /> )} diff --git a/src/Components/Users/UserFilter.tsx b/src/Components/Users/UserFilter.tsx index 3dca52d2463..ed65030f8a7 100644 --- a/src/Components/Users/UserFilter.tsx +++ b/src/Components/Users/UserFilter.tsx @@ -1,7 +1,3 @@ -import { useEffect } from "react"; -import { useDispatch } from "react-redux"; -import { getDistrict } from "../../Redux/actions"; -import { navigate } from "raviger"; import DistrictSelect from "../Facility/FacilityFilter/DistrictSelect"; import { parsePhoneNumber } from "../../Utils/utils"; import TextFormField from "../Form/FormFields/TextFormField"; @@ -11,6 +7,8 @@ import { USER_TYPE_OPTIONS } from "../../Common/constants"; import useMergeState from "../../Common/hooks/useMergeState"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; const parsePhoneNumberForFilterParam = (phoneNumber: string) => { if (!phoneNumber) return ""; @@ -20,8 +18,7 @@ const parsePhoneNumberForFilterParam = (phoneNumber: string) => { }; export default function UserFilter(props: any) { - const { filter, onChange, closeFilter } = props; - const dispatch: any = useDispatch(); + const { filter, onChange, closeFilter, removeFilters } = props; const [filterState, setFilterState] = useMergeState({ first_name: filter.first_name || "", last_name: filter.last_name || "", @@ -69,17 +66,14 @@ export default function UserFilter(props: any) { onChange(data); }; - useEffect(() => { - async function fetchData() { - if (filter.district_id) { - const { data: districtData } = await dispatch( - getDistrict(filter.district_id, "district") - ); - setFilterState({ district_ref: districtData }); - } - } - fetchData(); - }, [dispatch]); + useQuery(routes.getDistrict, { + prefetch: !!filter.district_id, + pathParams: { id: filter.district_id }, + onResponse: (result) => { + if (!result || !result.data || !result.res) return; + setFilterState({ district_ref: result.data }); + }, + }); const handleChange = ({ name, value }: any) => setFilterState({ ...filterState, [name]: value }); @@ -89,8 +83,7 @@ export default function UserFilter(props: any) { advancedFilter={props} onApply={applyFilter} onClear={() => { - navigate("/users"); - setFilterState(clearFilterState); + removeFilters(Object.keys(clearFilterState)); closeFilter(); }} > diff --git a/src/Components/Users/UserProfile.tsx b/src/Components/Users/UserProfile.tsx index 441a6862634..76a94745c1a 100644 --- a/src/Components/Users/UserProfile.tsx +++ b/src/Components/Users/UserProfile.tsx @@ -1,13 +1,5 @@ -import { useState, useCallback, useReducer, lazy, FormEvent } from "react"; -import { statusType, useAbortableEffect } from "../../Common/utils"; +import { useState, useReducer, lazy, FormEvent } from "react"; import { GENDER_TYPES } from "../../Common/constants"; -import { useDispatch } from "react-redux"; -import { - getUserDetails, - getUserListSkills, - partialUpdateUser, - updateUserPassword, -} from "../../Redux/actions"; import { validateEmailAddress } from "../../Common/validation"; import * as Notification from "../../Utils/Notifications.js"; import LanguageSelector from "../../Components/Common/LanguageSelector"; @@ -18,15 +10,32 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; -import { SkillModel, SkillObjectModel } from "../Users/models"; +import { GenderType, SkillModel, UpdatePasswordForm } from "../Users/models"; import UpdatableApp, { checkForUpdate } from "../Common/UpdatableApp"; import dayjs from "../../Utils/dayjs"; import useAuthUser from "../../Common/hooks/useAuthUser"; import { PhoneNumberValidator } from "../Form/FieldValidators"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; const Loading = lazy(() => import("../Common/Loading")); type EditForm = { + firstName: string; + lastName: string; + age: string; + gender: GenderType; + email: string; + phoneNumber: string; + altPhoneNumber: string; + user_type: string | undefined; + doctor_qualification: string | undefined; + doctor_experience_commenced_on: number | string | undefined; + doctor_medical_council_registration: string | undefined; + weekly_working_hours: string | undefined; +}; +type ErrorForm = { firstName: string; lastName: string; age: string; @@ -34,6 +43,7 @@ type EditForm = { email: string; phoneNumber: string; altPhoneNumber: string; + user_type: string | undefined; doctor_qualification: string | undefined; doctor_experience_commenced_on: number | string | undefined; doctor_medical_council_registration: string | undefined; @@ -41,27 +51,28 @@ type EditForm = { }; type State = { form: EditForm; - errors: EditForm; + errors: ErrorForm; }; type Action = | { type: "set_form"; form: EditForm } - | { type: "set_error"; errors: EditForm }; + | { type: "set_error"; errors: ErrorForm }; const initForm: EditForm = { firstName: "", lastName: "", age: "", - gender: "", + gender: "Male", email: "", phoneNumber: "", altPhoneNumber: "", + user_type: "", doctor_qualification: undefined, doctor_experience_commenced_on: undefined, doctor_medical_council_registration: undefined, weekly_working_hours: undefined, }; -const initError: EditForm = Object.assign( +const initError: ErrorForm = Object.assign( {}, ...Object.keys(initForm).map((k) => ({ [k]: "" })) ); @@ -87,9 +98,9 @@ const editFormReducer = (state: State, action: Action) => { } } }; + export default function UserProfile() { const [states, dispatch] = useReducer(editFormReducer, initialState); - const reduxDispatch: any = useDispatch(); const [updateStatus, setUpdateStatus] = useState({ isChecking: false, isUpdateAvailable: false, @@ -119,57 +130,44 @@ export default function UserProfile() { const [showEdit, setShowEdit] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const dispatchAction: any = useDispatch(); - - const initialDetails: any = [{}]; - const [details, setDetails] = useState(initialDetails); - - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const res = await dispatchAction(getUserDetails(authUser.username)); - const resSkills = await dispatchAction( - getUserListSkills({ username: authUser.username }) - ); - if (!status.aborted) { - if (res && res.data && resSkills) { - res.data.skills = resSkills.data.results.map( - (skill: SkillModel) => skill.skill_object - ); - setDetails(res.data); - const formData: EditForm = { - firstName: res.data.first_name, - lastName: res.data.last_name, - age: res.data.age, - gender: res.data.gender, - email: res.data.email, - phoneNumber: res.data.phone_number, - altPhoneNumber: res.data.alt_phone_number, - doctor_qualification: res.data.doctor_qualification, - doctor_experience_commenced_on: dayjs().diff( - dayjs(res.data.doctor_experience_commenced_on), - "years" - ), - doctor_medical_council_registration: - res.data.doctor_medical_council_registration, - weekly_working_hours: res.data.weekly_working_hours, - }; - dispatch({ - type: "set_form", - form: formData, - }); - } - setIsLoading(false); - } - }, - [dispatchAction, authUser.username] - ); - useAbortableEffect( - (status: statusType) => { - fetchData(status); + const { + data: userData, + loading: isUserLoading, + refetch: refetchUserData, + } = useQuery(routes.getUserDetails, { + pathParams: { username: authUser.username }, + onResponse: (result) => { + if (!result || !result.res || !result.data) return; + const formData: EditForm = { + firstName: result.data.first_name, + lastName: result.data.last_name, + age: result.data.age?.toString() || "", + gender: result.data.gender || "Male", + email: result.data.email, + phoneNumber: result.data.phone_number?.toString() || "", + altPhoneNumber: result.data.alt_phone_number?.toString() || "", + user_type: result.data.user_type, + doctor_qualification: result.data.doctor_qualification, + doctor_experience_commenced_on: dayjs().diff( + dayjs(result.data.doctor_experience_commenced_on), + "years" + ), + doctor_medical_council_registration: + result.data.doctor_medical_council_registration, + weekly_working_hours: result.data.weekly_working_hours, + }; + dispatch({ + type: "set_form", + form: formData, + }); }, - [fetchData] + }); + + const { data: skillsView, loading: isSkillsLoading } = useQuery( + routes.userListSkill, + { + pathParams: { username: authUser.username }, + } ); const validateForm = () => { @@ -244,7 +242,7 @@ export default function UserProfile() { case "doctor_qualification": case "doctor_experience_commenced_on": case "doctor_medical_council_registration": - if (details.user_type === "Doctor" && !states.form[field]) { + if (states.form.user_type === "Doctor" && !states.form[field]) { errors[field] = "Field is required"; invalidForm = true; } @@ -298,13 +296,13 @@ export default function UserProfile() { phone_number: parsePhoneNumber(states.form.phoneNumber) ?? "", alt_phone_number: parsePhoneNumber(states.form.altPhoneNumber) ?? "", gender: states.form.gender, - age: states.form.age, + age: +states.form.age, doctor_qualification: - details.user_type === "Doctor" + states.form.user_type === "Doctor" ? states.form.doctor_qualification : undefined, doctor_experience_commenced_on: - details.user_type === "Doctor" + states.form.user_type === "Doctor" ? dayjs() .subtract( parseInt( @@ -316,34 +314,27 @@ export default function UserProfile() { .format("YYYY-MM-DD") : undefined, doctor_medical_council_registration: - details.user_type === "Doctor" + states.form.user_type === "Doctor" ? states.form.doctor_medical_council_registration : undefined, weekly_working_hours: states.form.weekly_working_hours, }; - const res = await dispatchAction( - partialUpdateUser(authUser.username, data) - ); - if (res && res.data) { + const { res } = await request(routes.partialUpdateUser, { + pathParams: { username: authUser.username }, + body: data, + }); + if (res?.ok) { Notification.Success({ msg: "Details updated successfully", }); - window.location.reload(); - setDetails({ - ...details, - first_name: states.form.firstName, - last_name: states.form.lastName, - age: states.form.age, - gender: states.form.gender, - email: states.form.email, - phone_number: states.form.phoneNumber, - alt_phone_number: states.form.altPhoneNumber, - }); + await refetchUserData(); setShowEdit(false); } } }; + const isLoading = isUserLoading || isSkillsLoading; + if (isLoading) { return ; } @@ -367,7 +358,7 @@ export default function UserProfile() { } }; - const changePassword = (e: any) => { + const changePassword = async (e: any) => { e.preventDefault(); //validating form if ( @@ -377,30 +368,28 @@ export default function UserProfile() { msg: "Passwords are different in the new and the confirmation column.", }); } else { - setIsLoading(true); - const form = { + const form: UpdatePasswordForm = { old_password: changePasswordForm.old_password, username: authUser.username, new_password: changePasswordForm.new_password_1, }; - reduxDispatch(updateUserPassword(form)).then((resp: any) => { - setIsLoading(false); - const res = resp && resp.data; - if (res.message === "Password updated successfully") { - Notification.Success({ - msg: "Password changed!", - }); - } else { - Notification.Error({ - msg: "There was some error. Please try again in some time.", - }); - } - setChangePasswordForm({ - ...changePasswordForm, - new_password_1: "", - new_password_2: "", - old_password: "", + const { res, data } = await request(routes.updatePassword, { + body: form, + }); + if (res?.ok && data?.message === "Password updated successfully") { + Notification.Success({ + msg: "Password changed!", + }); + } else { + Notification.Error({ + msg: "There was some error. Please try again in some time.", }); + } + setChangePasswordForm({ + ...changePasswordForm, + new_password_1: "", + new_password_2: "", + old_password: "", }); } }; @@ -432,7 +421,7 @@ export default function UserProfile() {
- {!showEdit && ( + {!showEdit && !isLoading && (
- {details.username || "-"} + {userData?.username || "-"}
- {details.phone_number || "-"} + {userData?.phone_number || "-"}
@@ -466,7 +455,7 @@ export default function UserProfile() { Whatsapp No
- {details.alt_phone_number || "-"} + {userData?.alt_phone_number || "-"}
- {details.email || "-"} + {userData?.email || "-"}
- {details.first_name || "-"} + {userData?.first_name || "-"}
- {details.last_name || "-"} + {userData?.last_name || "-"}
@@ -507,7 +496,7 @@ export default function UserProfile() { Age
- {details.age || "-"} + {userData?.age || "-"}
@@ -516,7 +505,7 @@ export default function UserProfile() {
{" "} - {details.user_type || "-"} + {userData?.user_type || "-"}
- {details.gender || "-"} + {userData?.gender || "-"}
@@ -535,7 +524,7 @@ export default function UserProfile() { Local Body
- {details.local_body_object?.name || "-"} + {userData?.local_body_object?.name || "-"}
@@ -543,7 +532,7 @@ export default function UserProfile() { District
- {details.district_object?.name || "-"} + {userData?.district_object?.name || "-"}
@@ -551,7 +540,7 @@ export default function UserProfile() { State
- {details.state_object?.name || "-"} + {userData?.state_object?.name || "-"}
@@ -563,11 +552,13 @@ export default function UserProfile() { className="flex flex-wrap gap-2" id="already-linked-skills" > - {details.skills && details.skills.length - ? details.skills?.map((skill: SkillObjectModel) => { + {skillsView?.results?.length + ? skillsView.results?.map((skill: SkillModel) => { return ( -

{skill.name}

+

+ {skill.skill_object.name} +

); }) @@ -583,7 +574,7 @@ export default function UserProfile() { Average weekly working hours
- {details.weekly_working_hours ?? "-"} + {userData?.weekly_working_hours || "-"}
@@ -649,7 +640,7 @@ export default function UserProfile() { required type="email" /> - {details.user_type === "Doctor" && ( + {states.form.user_type === "Doctor" && ( <> - {Object.entries(labels).map(([label, className]) => ( - + {Object.entries(labels).map(([label, className], i) => ( + {label} ))} diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index 7e357bc04b5..31528afa390 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -45,6 +45,7 @@ "clear": "Clear", "apply": "Apply", "filter_by": "Filter By", + "filter": "Filter", "ordering": "Ordering", "phone_number": "Phone Number", "emergency_contact_number": "Emergency Contact Number", @@ -157,5 +158,6 @@ "edit": "Edit", "clear_selection": "Clear selection", "select_date": "Select date", - "DD/MM/YYYY": "DD/MM/YYYY" + "DD/MM/YYYY": "DD/MM/YYYY", + "clear_all_filters": "Clear All Filters" } diff --git a/src/Locale/en/Consultation.json b/src/Locale/en/Consultation.json index a40f03c4f24..54b587eb81e 100644 --- a/src/Locale/en/Consultation.json +++ b/src/Locale/en/Consultation.json @@ -12,5 +12,8 @@ "discharge_summary_not_ready": "Discharge summary is not ready yet.", "download_discharge_summary": "Download discharge summary", "email_discharge_summary_description": "Enter your valid email address to receive the discharge summary", - "generated_summary_caution": "This is a computer generated summary using the information captured in the CARE system." + "generated_summary_caution": "This is a computer generated summary using the information captured in the CARE system.", + "NORMAL": "Normal", + "VENTILATOR": "Critical Care", + "AUTOMATED": "Automated" } diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index e380949887b..2395c9ad163 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -15,11 +15,8 @@ export const getCurrentUser = () => { export const signupUser = (params: object) => { return fireRequest("createUser", [], params); }; -export const addUser = (params: object) => { - return fireRequest("addUser", [], params); -}; export const deleteUser = (username: string) => { - return fireRequest("deleteUser", [username], {}); + return fireRequest("deleteUser", [], {}, { username }); }; export const checkResetToken = (params: object) => { @@ -34,10 +31,6 @@ export const postForgotPassword = (form: object) => { return fireRequest("forgotPassword", [], form); }; -export const updateUserPassword = (form: object) => { - return fireRequest("updatePassword", [], form); -}; - export const getUserPnconfig = (pathParams: object) => { return fireRequest("getUserPnconfig", [], {}, pathParams); }; @@ -62,14 +55,6 @@ export const deleteFacilityCoverImage = (id: string) => { export const getUserList = (params: object, key?: string) => { return fireRequest("userList", [], params, null, key); }; - -export const getUserListSkills = (pathParam: object) => { - return fireRequest("userListSkill", [], {}, pathParam); -}; - -export const partialUpdateUser = (username: string, data: any) => { - return fireRequest("partialUpdateUser", [], data, { username }); -}; export const getUserListFacility = (pathParam: object) => { return fireRequest("userListFacility", [], {}, pathParam); }; @@ -95,10 +80,6 @@ export const deleteUserFacility = (username: string, facility: string) => { ); }; -export const clearHomeFacility = (username: string) => { - return fireRequest("clearHomeFacility", [], {}, { username }); -}; - export const getPermittedFacilities = (params: object) => { return fireRequest("getPermittedFacilities", [], params); }; @@ -605,20 +586,6 @@ export const dischargePatient = (params: object, pathParams: object) => { //Profile -export const checkUsername = (params: object) => { - return fireRequest("checkUsername", [], {}, params, undefined, true); -}; - -export const getUserDetails = (username: string, suppress?: boolean) => { - return fireRequest( - "getUserDetails", - [], - {}, - { username: username }, - undefined, - suppress ?? true - ); -}; export const updateUserDetails = (username: string, data: object) => { return fireRequest("updateUserDetails", [username], data); }; diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 4172effe432..2a7681975a4 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -33,6 +33,7 @@ import { ConsultationModel, CreateBedBody, CurrentBed, + DistrictModel, DailyRoundsBody, DailyRoundsRes, DoctorModal, @@ -40,9 +41,10 @@ import { IFacilityNotificationRequest, IFacilityNotificationResponse, IUserFacilityRequest, - LocationModel, PatientStatsModel, WardModel, + LocationModel, + StateModel, } from "../Components/Facility/models"; import { IDeleteExternalResult, @@ -52,9 +54,13 @@ import { ILocalBodyByDistrict, IPartialUpdateExternalResult, } from "../Components/ExternalResult/models"; - +import { + SkillModel, + SkillObjectModel, + UpdatePasswordForm, + UserModel, +} from "../Components/Users/models"; import { Prescription } from "../Components/Medicine/models"; -import { UserModel } from "../Components/Users/models"; import { DailyRoundsModel, PatientModel } from "../Components/Patient/models"; import { PaginatedResponse } from "../Utils/request/types"; import { @@ -149,6 +155,8 @@ const routes = { updatePassword: { path: "/api/v1/password_change/", method: "PUT", + TRes: Type>(), + TBody: Type(), }, // User Endpoints currentUser: { @@ -164,11 +172,14 @@ const routes = { userListSkill: { path: "/api/v1/users/{username}/skill/", + method: "GET", + TRes: Type>(), }, userListFacility: { path: "/api/v1/users/{username}/get_facilities/", - TRes: Type(), + method: "GET", + TRes: Type(), }, addUserFacility: { @@ -181,6 +192,8 @@ const routes = { addUserSkill: { path: "/api/v1/users/{username}/skill/", method: "POST", + TBody: Type<{ skill: string }>(), + TRes: Type(), }, deleteUserFacility: { @@ -193,11 +206,13 @@ const routes = { clearHomeFacility: { path: "/api/v1/users/{username}/clear_home_facility/", method: "DELETE", + TRes: Type>(), }, deleteUserSkill: { path: "/api/v1/users/{username}/skill/{id}/", method: "DELETE", + TRes: Type>(), }, createUser: { @@ -214,6 +229,8 @@ const routes = { partialUpdateUser: { path: "/api/v1/users/{username}/", method: "PATCH", + TRes: Type(), + TBody: Type>(), }, deleteUser: { @@ -225,6 +242,7 @@ const routes = { addUser: { path: "/api/v1/users/add_user/", method: "POST", + TRes: Type(), }, searchUser: { @@ -251,6 +269,8 @@ const routes = { getAllSkills: { path: "/api/v1/skill/", + method: "GET", + TRes: Type>(), }, // Facility Endpoints @@ -651,6 +671,8 @@ const routes = { // States statesList: { path: "/api/v1/state/", + method: "GET", + TRes: Type>(), }, getState: { @@ -661,9 +683,13 @@ const routes = { getDistrict: { path: "/api/v1/district/{id}/", + method: "GET", + TRes: Type(), }, getDistrictByState: { path: "/api/v1/state/{id}/districts/", + method: "GET", + TRes: Type(), }, getDistrictByName: { path: "/api/v1/district/", @@ -775,11 +801,13 @@ const routes = { checkUsername: { path: "/api/v1/users/{username}/check_availability/", method: "GET", + TRes: Type>(), }, getUserDetails: { path: "/api/v1/users/{username}/", method: "GET", + TRes: Type(), }, updateUserDetails: { path: "/api/v1/users", @@ -968,7 +996,7 @@ const routes = { // Assets endpoints listAssets: { - path: "/api/v1/asset", + path: "/api/v1/asset/", method: "GET", TRes: Type>(), }, diff --git a/src/Routers/routes/FacilityLocationRoutes.tsx b/src/Routers/routes/FacilityLocationRoutes.tsx index c43673b60f5..aa08661da8b 100644 --- a/src/Routers/routes/FacilityLocationRoutes.tsx +++ b/src/Routers/routes/FacilityLocationRoutes.tsx @@ -3,6 +3,7 @@ import { AddInventoryForm } from "../../Components/Facility/AddInventoryForm"; import { AddLocationForm } from "../../Components/Facility/AddLocationForm"; import { BedManagement } from "../../Components/Facility/BedManagement"; import LocationManagement from "../../Components/Facility/LocationManagement"; +import CentralLiveMonitoring from "../../Components/CameraFeed/CentralLiveMonitoring"; export default { "/facility/:facilityId/location": ({ facilityId }: any) => ( @@ -35,4 +36,7 @@ export default { }: any) => ( ), + "/facility/:facilityId/live-monitoring": (props: any) => ( + + ), }; diff --git a/src/Routers/routes/FacilityRoutes.tsx b/src/Routers/routes/FacilityRoutes.tsx index be52ef4e72c..77247df9189 100644 --- a/src/Routers/routes/FacilityRoutes.tsx +++ b/src/Routers/routes/FacilityRoutes.tsx @@ -8,7 +8,6 @@ import ResourceCreate from "../../Components/Resource/ResourceCreate"; import CentralNursingStation from "../../Components/Facility/CentralNursingStation"; import FacilityLocationRoutes from "./FacilityLocationRoutes"; import FacilityInventoryRoutes from "./FacilityInventoryRoutes"; -import LiveFeedScreen from "../../Components/Facility/LiveFeedScreen"; export default { "/facility": () => , @@ -22,9 +21,6 @@ export default { "/facility/:facilityId/cns": ({ facilityId }: any) => ( ), - "/facility/:facilityId/livefeed": ({ facilityId }: any) => ( - - ), "/facility/:facilityId": ({ facilityId }: any) => ( ), diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts index ec919c79490..f22dca369f2 100644 --- a/src/Utils/request/utils.ts +++ b/src/Utils/request/utils.ts @@ -82,7 +82,10 @@ export function mergeRequestOptions( ...overrides, query: { ...options.query, ...overrides.query }, - body: { ...(options.body ?? {}), ...(overrides.body ?? {}) }, + body: (options.body || overrides.body) && { + ...(options.body ?? {}), + ...(overrides.body ?? {}), + }, pathParams: { ...options.pathParams, ...overrides.pathParams }, onResponse: (res) => { diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index cd75fdba7e7..4c4fdacbc28 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -175,10 +175,15 @@ function _isAppleDevice() { } /** - * `true` if device is iOS, else `false` + * `true` if device is an Apple device, else `false` */ export const isAppleDevice = _isAppleDevice(); +/** + * `true` if device is an iOS device, else `false` + */ +export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); + /** * Conditionally concatenate classes. An alternate replacement for `clsx`. * @@ -454,3 +459,11 @@ export const scrollTo = (id: string | boolean) => { const element = document.querySelector(`#${id}`); element?.scrollIntoView({ behavior: "smooth", block: "center" }); }; + +export const invalidateFiltersCache = () => { + for (const key in localStorage) { + if (key.startsWith("filters--")) { + localStorage.removeItem(key); + } + } +}; diff --git a/src/style/CAREUI.css b/src/style/CAREUI.css index 404fcc3c4da..eff9ae91640 100644 --- a/src/style/CAREUI.css +++ b/src/style/CAREUI.css @@ -39,7 +39,7 @@ .tooltip .tooltip-text { visibility: hidden; opacity: 0; - @apply bg-black/75 backdrop-blur text-white text-center p-2 rounded absolute z-50 text-sm block transition-all whitespace-nowrap + @apply bg-black/75 backdrop-blur text-white text-center p-2 rounded absolute z-50 text-sm block transition-opacity whitespace-nowrap pointer-events-none } .tooltip .tooltip-left { diff --git a/src/style/index.css b/src/style/index.css index 787290bab26..8767498e42a 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -564,12 +564,6 @@ button:disabled, background: rgba(0, 0, 0, 0.4); } -@media (max-width:640px) { - .hideonmobilescreen { - display: none; - } -} - @media (min-width:1000px) { .manualGrid { display: grid !important