From 9005adf2d0b81958761306750de9d417deae24e8 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:01:19 +0530 Subject: [PATCH 01/11] Existing Cypress POM Conversion | Create New User & Verify its reflection along with validation | User Tab (#6514) * cypress new test for advance filter * convert navigation to POM * facility redirection * minor change * user creation * user creation * revert package-lock --- cypress/e2e/users_spec/user_creation.cy.ts | 154 ++++++++++++++++ cypress/e2e/users_spec/user_crud.cy.ts | 199 --------------------- cypress/pageobject/Users/UserCreation.ts | 65 +++++++ src/Components/Users/ManageUsers.tsx | 6 +- 4 files changed, 224 insertions(+), 200 deletions(-) delete mode 100644 cypress/e2e/users_spec/user_crud.cy.ts create mode 100644 cypress/pageobject/Users/UserCreation.ts diff --git a/cypress/e2e/users_spec/user_creation.cy.ts b/cypress/e2e/users_spec/user_creation.cy.ts index 498697566c5..ae416d8d990 100644 --- a/cypress/e2e/users_spec/user_creation.cy.ts +++ b/cypress/e2e/users_spec/user_creation.cy.ts @@ -3,18 +3,45 @@ import LoginPage from "../../pageobject/Login/LoginPage"; import { AssetSearchPage } from "../../pageobject/Asset/AssetSearch"; import FacilityPage from "../../pageobject/Facility/FacilityCreation"; import { UserPage } from "../../pageobject/Users/UserSearch"; +import { UserCreationPage } from "../../pageobject/Users/UserCreation"; describe("User Creation", () => { const userPage = new UserPage(); const loginPage = new LoginPage(); + const userCreationPage = new UserCreationPage(); const facilityPage = new FacilityPage(); const assetSearchPage = new AssetSearchPage(); const fillFacilityName = "Dummy Facility 1"; + const makeid = (length: number) => { + let result = ""; + const characters = "abcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; + }; + const username = makeid(25); const alreadylinkedusersviews = [ "devdoctor", "devstaff2", "devdistrictadmin", ]; + const EXPECTED_ERROR_MESSAGES = [ + "Please select the User Type", + "Please enter valid phone number", + "Please enter the username", + "Please enter date in YYYY/MM/DD format", + "Please enter the password", + "Confirm password is required", + "First Name is required", + "Last Name is required", + "Please enter a valid email address", + "Please select the Gender", + "Please select the state", + "Please select the district", + "Please select the local body", + ]; before(() => { loginPage.loginAsDisctrictAdmin(); @@ -26,6 +53,53 @@ describe("User Creation", () => { cy.awaitUrl("/users"); }); + it("create new user and verify reflection", () => { + userCreationPage.clickElementById("addUserButton"); + userCreationPage.selectFacility("Dummy Shifting Center"); + userCreationPage.typeIntoElementById("username", username); + userCreationPage.typeIntoElementById("password", "Test@123"); + userCreationPage.selectHomeFacility("Dummy Shifting Center"); + userCreationPage.typeIntoElementById("phone_number", "9999999999"); + userCreationPage.setInputDate("date_of_birth", "date-input", "25081999"); + userCreationPage.selectDropdownOption("user_type", "Doctor"); + userCreationPage.typeIntoElementById("c_password", "Test@123"); + userCreationPage.typeIntoElementById("doctor_qualification", "MBBS"); + userCreationPage.typeIntoElementById("doctor_experience_commenced_on", "2"); + userCreationPage.typeIntoElementById( + "doctor_medical_council_registration", + "123456789" + ); + userCreationPage.typeIntoElementById("first_name", "cypress test"); + userCreationPage.typeIntoElementById("last_name", "staff user"); + userCreationPage.typeIntoElementById("email", "test@test.com"); + userCreationPage.selectDropdownOption("gender", "Male"); + userCreationPage.selectDropdownOption("state", "Kerala"); + userCreationPage.selectDropdownOption("district", "Ernakulam"); + userCreationPage.clickElementById("submit"); + userCreationPage.verifyNotification("User added successfully"); + userPage.typeInSearchInput(username); + userPage.checkUsernameText(username); + userCreationPage.verifyElementContainsText("name", "cypress test"); + userCreationPage.verifyElementContainsText("role", "Doctor"); + userCreationPage.verifyElementContainsText("district", "Ernakulam"); + userCreationPage.verifyElementContainsText( + "home_facility", + "Dummy Shifting Center" + ); + userCreationPage.verifyElementContainsText("doctor-qualification", "MBBS"); + userCreationPage.verifyElementContainsText("doctor-experience", "2"); + userCreationPage.verifyElementContainsText( + "medical-council-registration", + "123456789" + ); + }); + + it("create new user form throwing mandatory field error", () => { + userCreationPage.clickElementById("addUserButton"); + userCreationPage.clickElementById("submit"); + userCreationPage.verifyErrorMessages(EXPECTED_ERROR_MESSAGES); + }); + it("view user redirection from facility page", () => { cy.visit("/facility"); assetSearchPage.typeSearchKeyword(fillFacilityName); @@ -37,6 +111,86 @@ describe("User Creation", () => { userPage.verifyMultipleBadgesWithSameId(alreadylinkedusersviews); }); + // the below commented out codes, will be used in the upcoming refactoring of this module, since it is a inter-dependent of partially converted code, currently taking it down + + // it("link facility for user", () => { + // cy.contains("Linked Facilities").click(); + // cy.intercept(/\/api\/v1\/facility/).as("getFacilities"); + // cy.get("[name='facility']") + // .click() + // .type("Dummy Facility 1") + // .wait("@getFacilities"); + // cy.get("li[role='option']").first().click(); + // cy.intercept(/\/api\/v1\/users\/\w+\/add_facility\//).as("addFacility"); + // cy.get("button[id='link-facility']").click(); + // cy.wait("@addFacility") + // // .its("response.statusCode") + // // .should("eq", 201) + // .get("span") + // .contains("Facility - User Already has permission to this facility"); + // }); + + // describe("Edit User Profile & Error Validation", () => { + // before(() => { + // cy.loginByApi(username, "#@Cypress_test123"); + // cy.saveLocalStorage(); + // }); + + // beforeEach(() => { + // cy.restoreLocalStorage(); + // cy.awaitUrl("/user/profile"); + // cy.contains("button", "Edit User Profile").click(); + // }); + + // it("First name Field Updation " + username, () => { + // cy.get("input[name=firstName]").clear(); + // cy.contains("button[type='submit']", "Update").click(); + // cy.get("span.error-text").should("contain", "Field is required"); + // cy.get("input[name=firstName]").type("firstName updated"); + // cy.contains("button[type='submit']", "Update").click(); + // }); + + // it("Last name Field Updation " + username, () => { + // cy.get("input[name=lastName]").clear(); + // cy.contains("button[type='submit']", "Update").click(); + // cy.get("span.error-text").should("contain", "Field is required"); + // cy.get("input[name=lastName]").type("lastName updated"); + // cy.contains("button[type='submit']", "Update").click(); + // }); + + // it("Age Field Updation " + username, () => { + // cy.get("input[name=age]").clear(); + // cy.contains("button[type='submit']", "Update").click(); + // cy.get("span.error-text").should("contain", "This field is required"); + // cy.get("input[name=age]").type("11"); + // cy.contains("button[type='submit']", "Update").click(); + // }); + + // it("Phone number Field Updation " + username, () => { + // cy.get("input[name=phoneNumber]").clear(); + // cy.contains("button[type='submit']", "Update").click(); + // cy.get("span.error-text").should( + // "contain", + // "Please enter valid phone number" + // ); + // cy.get("input[name=phoneNumber]").type("+919999999999"); + // cy.contains("button[type='submit']", "Update").click(); + // }); + + // it("Whatsapp number Field Updation " + username, () => { + // cy.get("input[name=altPhoneNumber]").clear(); + // cy.get("input[name=altPhoneNumber]").type("+919999999999"); + // cy.contains("button[type='submit']", "Update").click(); + // }); + + // it("Email Field Updation " + username, () => { + // cy.get("input[name=email]").clear(); + // cy.contains("button[type='submit']", "Update").click(); + // cy.get("span.error-text").should("contain", "This field is required"); + // cy.get("input[name=email]").type("test@test.com"); + // cy.contains("button[type='submit']", "Update").click(); + // }); + afterEach(() => { cy.saveLocalStorage(); }); diff --git a/cypress/e2e/users_spec/user_crud.cy.ts b/cypress/e2e/users_spec/user_crud.cy.ts deleted file mode 100644 index 91104af518f..00000000000 --- a/cypress/e2e/users_spec/user_crud.cy.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { afterEach, before, beforeEach, cy, describe, it } from "local-cypress"; - -const makeid = (length: number) => { - let result = ""; - const characters = "abcdefghijklmnopqrstuvwxyz0123456789"; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; -}; - -const username = makeid(25); -const phone_number = "9999999999"; -const alt_phone_number = "9999999999"; - -describe("User management", () => { - before(() => { - cy.loginByApi("devdistrictadmin", "Coronasafe@123"); - cy.saveLocalStorage(); - }); - - beforeEach(() => { - cy.restoreLocalStorage(); - cy.awaitUrl("/user"); - }); - - it("create user", () => { - cy.contains("Add New User").click(); - cy.get("[id='user_type'] > div > button").click(); - cy.get("div").contains("Ward Admin").click(); - cy.get("[id='state'] > div > button").click(); - cy.get("div").contains("Kerala").click(); - cy.get("[id='district'] > div > button").click(); - cy.get("div").contains("Ernakulam").click(); - cy.get("[id='local_body'] > div > button").click(); - cy.get("div").contains("Aikaranad").click(); - cy.intercept(/\/api\/v1\/facility/).as("facility"); - cy.get("[name='facilities']") - .click() - .type("Dummy Facility 1") - .wait("@facility"); - cy.get("li[role='option']").first().click(); - cy.get("input[type='checkbox']").click(); - cy.get("[name='phone_number']").type(phone_number); - cy.get("[name='alt_phone_number']").type(alt_phone_number); - cy.intercept(/users/).as("check_availability"); - cy.get("#date_of_birth").should("be.visible").click(); - cy.get("#date-input").click().type("25081999"); - cy.get("[name='username']").type(username); - cy.wait("@check_availability").its("response.statusCode").should("eq", 200); - cy.get("[name='password']").type("#@Cypress_test123"); - cy.get("[name='c_password']").type("#@Cypress_test123"); - cy.get("[name='first_name']").type("Cypress Test"); - cy.get("[name='last_name']").type("Tester"); - cy.get("[name='email']").type("cypress@tester.com"); - cy.get("[id='gender'] > div > button").click(); - cy.get("div").contains("Male").click(); - cy.get("button[id='submit']").contains("Save User").click(); - cy.verifyNotification("User added successfully"); - }); - - it("view user and verify details", () => { - cy.contains("Advanced Filters").click(); - cy.get("[name='first_name']").type("Cypress Test"); - cy.get("[name='last_name']").type("Tester"); - cy.get("#role button").click(); - cy.contains("#role li", "Ward Admin").click(); - cy.get("input[name='district']").click(); - cy.get("input[name='district']").type("Ernakulam"); - cy.get("li[id^='headlessui-combobox-option']") - .contains("Ernakulam") - .click(); - cy.get("[placeholder='Phone Number']").click(); - cy.get("[placeholder='Phone Number']").type(phone_number); - cy.get("[placeholder='WhatsApp Phone Number']").type(alt_phone_number); - cy.contains("Apply").click(); - cy.intercept(/\/api\/v1\/users/).as("getUsers"); - cy.wait(1000); - cy.get("[name='username']").type(username); - cy.wait("@getUsers"); - cy.get("dd[id='count']").contains(/^1$/).click(); - cy.get("div[id='usr_0']").within(() => { - cy.intercept(`/api/v1/users/${username}/get_facilities/`).as( - "userFacility" - ); - cy.get("div[id='role']").contains(/^WardAdmin$/); - cy.get("div[id='name']").contains("Cypress Test Tester"); - cy.get("div[id='district']").contains(/^Ernakulam$/); - cy.get("div[id='local_body']").contains("Aikaranad"); - cy.get("div[id='created_by']").contains(/^devdistrictadmin$/); - cy.get("div[id='home_facility']").contains("No Home Facility"); - cy.get("button[id='facilities']").click(); - cy.wait("@userFacility") - .getAttached("div[id=facility_0] > div > span") - .contains("Dummy Facility 1"); - }); - }); - - it("link facility for user", () => { - cy.contains("Linked Facilities").click(); - cy.intercept(/\/api\/v1\/facility/).as("getFacilities"); - cy.get("[name='facility']") - .click() - .type("Dummy Facility 1") - .wait("@getFacilities"); - cy.get("li[role='option']").first().click(); - cy.intercept(/\/api\/v1\/users\/\w+\/add_facility\//).as("addFacility"); - cy.get("button[id='link-facility']").click(); - cy.wait("@addFacility") - // .its("response.statusCode") - // .should("eq", 201) - .get("span") - .contains("Facility - User Already has permission to this facility"); - }); - - afterEach(() => { - cy.saveLocalStorage(); - }); -}); - -describe("Edit User Profile & Error Validation", () => { - before(() => { - cy.loginByApi(username, "#@Cypress_test123"); - cy.saveLocalStorage(); - }); - - beforeEach(() => { - cy.restoreLocalStorage(); - cy.awaitUrl("/user/profile"); - cy.contains("button", "Edit User Profile").click(); - }); - - it("First name Field Updation " + username, () => { - cy.get("input[name=firstName]").clear(); - cy.contains("button[type='submit']", "Update").click(); - cy.get("span.error-text").should("contain", "Field is required"); - cy.get("input[name=firstName]").type("firstName updated"); - cy.contains("button[type='submit']", "Update").click(); - }); - - it("Last name Field Updation " + username, () => { - cy.get("input[name=lastName]").clear(); - cy.contains("button[type='submit']", "Update").click(); - cy.get("span.error-text").should("contain", "Field is required"); - cy.get("input[name=lastName]").type("lastName updated"); - cy.contains("button[type='submit']", "Update").click(); - }); - - it("Age Field Updation " + username, () => { - cy.get("input[name=age]").clear(); - cy.contains("button[type='submit']", "Update").click(); - cy.get("span.error-text").should("contain", "This field is required"); - cy.get("input[name=age]").type("11"); - cy.contains("button[type='submit']", "Update").click(); - }); - - it("Phone number Field Updation " + username, () => { - cy.get("input[name=phoneNumber]").clear(); - cy.contains("button[type='submit']", "Update").click(); - cy.get("span.error-text").should( - "contain", - "Please enter valid phone number" - ); - cy.get("input[name=phoneNumber]").type("+919999999999"); - cy.contains("button[type='submit']", "Update").click(); - }); - - it("Whatsapp number Field Updation " + username, () => { - cy.get("input[name=altPhoneNumber]").clear(); - cy.get("input[name=altPhoneNumber]").type("+919999999999"); - cy.contains("button[type='submit']", "Update").click(); - }); - - it("Email Field Updation " + username, () => { - cy.get("input[name=email]").clear(); - cy.contains("button[type='submit']", "Update").click(); - cy.get("span.error-text").should("contain", "This field is required"); - cy.get("input[name=email]").type("test@test.com"); - cy.contains("button[type='submit']", "Update").click(); - }); - - afterEach(() => { - cy.saveLocalStorage(); - }); -}); - -// describe("Delete User", () => { district admin wont be able to delete user -// it("deletes user", () => { -// cy.loginByApi("devdistrictadmin", "Coronasafe@123"); -// cy.awaitUrl("/user"); -// cy.get("[name='username']").type(username); -// cy.get("button") -// .should("contain", "Delete") -// .contains("Delete") -// .click(); -// cy.get("button.font-medium.btn.btn-danger").click(); -// }); -// }); diff --git a/cypress/pageobject/Users/UserCreation.ts b/cypress/pageobject/Users/UserCreation.ts new file mode 100644 index 00000000000..6bef3584ec8 --- /dev/null +++ b/cypress/pageobject/Users/UserCreation.ts @@ -0,0 +1,65 @@ +// UserCreation.ts +export class UserCreationPage { + clickElementById(elementId: string) { + cy.get("#" + elementId).click(); + } + + typeIntoElementById(elementId: string, value: string) { + cy.get("#" + elementId) + .click() + .type(value); + } + + typeIntoInputByName(inputName: string, value: string) { + cy.get("input[name='" + inputName + "']") + .click() + .type(value); + } + + selectOptionContainingText(text: string) { + cy.get("[role='option']").contains(text).click(); + } + + verifyNotification(message: string) { + cy.verifyNotification(message); + } + + selectFacility(name: string) { + this.typeIntoInputByName("facilities", name); + this.selectOptionContainingText(name); + } + + selectHomeFacility(name: string) { + this.clickElementById("home_facility"); + this.selectOptionContainingText(name); + } + + setInputDate( + dateElementId: string, + inputElementId: string, + dateValue: string + ) { + this.clickElementById(dateElementId); + this.typeIntoElementById(inputElementId, dateValue); + } + + selectDropdownOption(dropdownId: string, optionText: string) { + this.clickElementById(dropdownId); + this.selectOptionContainingText(optionText); + } + + verifyElementContainsText(elementId: string, expectedText: string) { + cy.get("#" + elementId).should("contain.text", expectedText); + } + + verifyErrorMessages(errorMessages: string[]) { + cy.get(".error-text").then(($errors) => { + const displayedErrorMessages = $errors + .map((_, el) => Cypress.$(el).text()) + .get(); + errorMessages.forEach((errorMessage) => { + expect(displayedErrorMessages).to.include(errorMessage); + }); + }); + } +} diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index 150c102aff1..3b55c40fc70 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -134,7 +134,11 @@ export default function ManageUsers() { ); const addUser = ( - navigate("/users/add")}> + navigate("/users/add")} + >

Add New User

From 7703f1f6056207cd388e74fc742445a89b42aa69 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Tue, 31 Oct 2023 03:04:10 +0530 Subject: [PATCH 02/11] existing user profile updation (#6527) --- cypress/e2e/users_spec/user_creation.cy.ts | 145 ++++++++++-------- cypress/pageobject/Users/UserCreation.ts | 14 ++ .../Common/Sidebar/SidebarUserCard.tsx | 1 + src/Components/Users/UserProfile.tsx | 48 ++++-- 4 files changed, 136 insertions(+), 72 deletions(-) diff --git a/cypress/e2e/users_spec/user_creation.cy.ts b/cypress/e2e/users_spec/user_creation.cy.ts index ae416d8d990..5a1e5d874de 100644 --- a/cypress/e2e/users_spec/user_creation.cy.ts +++ b/cypress/e2e/users_spec/user_creation.cy.ts @@ -4,6 +4,10 @@ import { AssetSearchPage } from "../../pageobject/Asset/AssetSearch"; import FacilityPage from "../../pageobject/Facility/FacilityCreation"; import { UserPage } from "../../pageobject/Users/UserSearch"; import { UserCreationPage } from "../../pageobject/Users/UserCreation"; +import { + emergency_phone_number, + phone_number, +} from "../../pageobject/constants"; describe("User Creation", () => { const userPage = new UserPage(); @@ -43,6 +47,15 @@ describe("User Creation", () => { "Please select the local body", ]; + const EXPECTED_PROFILE_ERROR_MESSAGES = [ + "Field is required", + "Field is required", + "This field is required", + "Please enter valid phone number", + "This field is required", + "This field is required", + ]; + before(() => { loginPage.loginAsDisctrictAdmin(); cy.saveLocalStorage(); @@ -53,13 +66,82 @@ describe("User Creation", () => { cy.awaitUrl("/users"); }); + it("Update the existing user profile and verify its reflection", () => { + userCreationPage.clickElementById("profilenamelink"); + userCreationPage.verifyElementContainsText( + "username-profile-details", + "devdistrictadmin" + ); + userCreationPage.clickElementById("edit-cancel-profile-button"); + userCreationPage.typeIntoElementByIdPostClear( + "firstName", + "District Editted" + ); + userCreationPage.typeIntoElementByIdPostClear("lastName", "Cypress"); + userCreationPage.typeIntoElementByIdPostClear("age", "22"); + userCreationPage.selectDropdownOption("gender", "Male"); + userCreationPage.typeIntoElementByIdPostClear( + "phoneNumber", + "+91" + phone_number + ); + userCreationPage.typeIntoElementByIdPostClear( + "altPhoneNumber", + "+91" + emergency_phone_number + ); + userCreationPage.typeIntoElementByIdPostClear("email", "test@test.com"); + userCreationPage.typeIntoElementByIdPostClear("weekly_working_hours", "14"); + userCreationPage.clickElementById("submit"); + userCreationPage.verifyElementContainsText( + "contactno-profile-details", + "+91" + phone_number + ); + userCreationPage.verifyElementContainsText( + "whatsapp-profile-details", + "+91" + emergency_phone_number + ); + userCreationPage.verifyElementContainsText( + "firstname-profile-details", + "District Editted" + ); + userCreationPage.verifyElementContainsText( + "lastname-profile-details", + "Cypress" + ); + userCreationPage.verifyElementContainsText("age-profile-details", "22"); + userCreationPage.verifyElementContainsText( + "emailid-profile-details", + "test@test.com" + ); + userCreationPage.verifyElementContainsText( + "gender-profile-details", + "Male" + ); + userCreationPage.verifyElementContainsText( + "averageworkinghour-profile-details", + "14" + ); + }); + + it("Update the existing user profile Form Mandatory File Error", () => { + userCreationPage.clickElementById("profilenamelink"); + userCreationPage.clickElementById("edit-cancel-profile-button"); + userCreationPage.clearIntoElementById("firstName"); + userCreationPage.clearIntoElementById("lastName"); + userCreationPage.clearIntoElementById("age"); + userCreationPage.clearIntoElementById("phoneNumber"); + userCreationPage.clearIntoElementById("altPhoneNumber"); + userCreationPage.clearIntoElementById("weekly_working_hours"); + userCreationPage.clickElementById("submit"); + userCreationPage.verifyErrorMessages(EXPECTED_PROFILE_ERROR_MESSAGES); + }); + it("create new user and verify reflection", () => { userCreationPage.clickElementById("addUserButton"); userCreationPage.selectFacility("Dummy Shifting Center"); userCreationPage.typeIntoElementById("username", username); userCreationPage.typeIntoElementById("password", "Test@123"); userCreationPage.selectHomeFacility("Dummy Shifting Center"); - userCreationPage.typeIntoElementById("phone_number", "9999999999"); + userCreationPage.typeIntoElementById("phone_number", phone_number); userCreationPage.setInputDate("date_of_birth", "date-input", "25081999"); userCreationPage.selectDropdownOption("user_type", "Doctor"); userCreationPage.typeIntoElementById("c_password", "Test@123"); @@ -130,67 +212,6 @@ describe("User Creation", () => { // .contains("Facility - User Already has permission to this facility"); // }); - // describe("Edit User Profile & Error Validation", () => { - // before(() => { - // cy.loginByApi(username, "#@Cypress_test123"); - // cy.saveLocalStorage(); - // }); - - // beforeEach(() => { - // cy.restoreLocalStorage(); - // cy.awaitUrl("/user/profile"); - // cy.contains("button", "Edit User Profile").click(); - // }); - - // it("First name Field Updation " + username, () => { - // cy.get("input[name=firstName]").clear(); - // cy.contains("button[type='submit']", "Update").click(); - // cy.get("span.error-text").should("contain", "Field is required"); - // cy.get("input[name=firstName]").type("firstName updated"); - // cy.contains("button[type='submit']", "Update").click(); - // }); - - // it("Last name Field Updation " + username, () => { - // cy.get("input[name=lastName]").clear(); - // cy.contains("button[type='submit']", "Update").click(); - // cy.get("span.error-text").should("contain", "Field is required"); - // cy.get("input[name=lastName]").type("lastName updated"); - // cy.contains("button[type='submit']", "Update").click(); - // }); - - // it("Age Field Updation " + username, () => { - // cy.get("input[name=age]").clear(); - // cy.contains("button[type='submit']", "Update").click(); - // cy.get("span.error-text").should("contain", "This field is required"); - // cy.get("input[name=age]").type("11"); - // cy.contains("button[type='submit']", "Update").click(); - // }); - - // it("Phone number Field Updation " + username, () => { - // cy.get("input[name=phoneNumber]").clear(); - // cy.contains("button[type='submit']", "Update").click(); - // cy.get("span.error-text").should( - // "contain", - // "Please enter valid phone number" - // ); - // cy.get("input[name=phoneNumber]").type("+919999999999"); - // cy.contains("button[type='submit']", "Update").click(); - // }); - - // it("Whatsapp number Field Updation " + username, () => { - // cy.get("input[name=altPhoneNumber]").clear(); - // cy.get("input[name=altPhoneNumber]").type("+919999999999"); - // cy.contains("button[type='submit']", "Update").click(); - // }); - - // it("Email Field Updation " + username, () => { - // cy.get("input[name=email]").clear(); - // cy.contains("button[type='submit']", "Update").click(); - // cy.get("span.error-text").should("contain", "This field is required"); - // cy.get("input[name=email]").type("test@test.com"); - // cy.contains("button[type='submit']", "Update").click(); - // }); - afterEach(() => { cy.saveLocalStorage(); }); diff --git a/cypress/pageobject/Users/UserCreation.ts b/cypress/pageobject/Users/UserCreation.ts index 6bef3584ec8..32127ffcb90 100644 --- a/cypress/pageobject/Users/UserCreation.ts +++ b/cypress/pageobject/Users/UserCreation.ts @@ -10,6 +10,20 @@ export class UserCreationPage { .type(value); } + typeIntoElementByIdPostClear(elementId: string, value: string) { + cy.get("#" + elementId) + .click() + .clear() + .click() + .type(value); + } + + clearIntoElementById(elementId: string) { + cy.get("#" + elementId) + .click() + .clear(); + } + typeIntoInputByName(inputName: string, value: string) { cy.get("input[name='" + inputName + "']") .click() diff --git a/src/Components/Common/Sidebar/SidebarUserCard.tsx b/src/Components/Common/Sidebar/SidebarUserCard.tsx index 6f243d430d0..59970e8a73c 100644 --- a/src/Components/Common/Sidebar/SidebarUserCard.tsx +++ b/src/Components/Common/Sidebar/SidebarUserCard.tsx @@ -37,6 +37,7 @@ const SidebarUserCard = ({ shrinked }: { shrinked: boolean }) => { {profileName} diff --git a/src/Components/Users/UserProfile.tsx b/src/Components/Users/UserProfile.tsx index 5c5ad46b9fa..0653ea452a2 100644 --- a/src/Components/Users/UserProfile.tsx +++ b/src/Components/Users/UserProfile.tsx @@ -417,7 +417,11 @@ export default function UserProfile() { Local Body, District and State are Non Editable Settings.

- setShowEdit(!showEdit)} type="button"> + setShowEdit(!showEdit)} + type="button" + id="edit-cancel-profile-button" + > {showEdit ? "Cancel" : "Edit User Profile"} handleSignOut(true)}> @@ -431,7 +435,10 @@ export default function UserProfile() { {!showEdit && (
-
+
Username
@@ -439,7 +446,10 @@ export default function UserProfile() { {details.username || "-"}
-
+
Contact No
@@ -448,7 +458,10 @@ export default function UserProfile() {
-
+
Whatsapp No
@@ -456,7 +469,10 @@ export default function UserProfile() { {details.alt_phone_number || "-"}
-
+
Email address
@@ -464,7 +480,10 @@ export default function UserProfile() { {details.email || "-"}
-
+
First Name
@@ -472,7 +491,10 @@ export default function UserProfile() { {details.first_name || "-"}
-
+
Last Name
@@ -480,7 +502,7 @@ export default function UserProfile() { {details.last_name || "-"}
-
+
Age
@@ -497,7 +519,10 @@ export default function UserProfile() { {details.user_type || "-"}
-
+
Gender
@@ -547,7 +572,10 @@ export default function UserProfile() {
-
+
Average weekly working hours
From bcd97300f0b56975d2130345a628c8c74e6d2217 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:50:41 +0530 Subject: [PATCH 03/11] Track patient card button clicks (#6521) * Track patient card button clicks * Add null check --- src/Components/Patient/PatientInfoCard.tsx | 27 ++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 1392b7af6eb..fd6213d8e08 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -8,7 +8,7 @@ import { TELEMEDICINE_ACTIONS, } from "../../Common/constants"; import { ConsultationModel, PatientCategory } from "../Facility/models"; -import { Switch } from "@headlessui/react"; +import { Switch, Menu } from "@headlessui/react"; import { Link } from "raviger"; import { useState } from "react"; @@ -26,8 +26,9 @@ import Beds from "../Facility/Consultations/Beds"; import { PatientModel } from "./models"; import request from "../../Utils/request/request.js"; import routes from "../../Redux/api.js"; -import { Menu } from "@headlessui/react"; import DropdownMenu from "../Common/components/Menu.js"; +import { triggerGoal } from "../../Integrations/Plausible.js"; +import useAuthUser from "../../Common/hooks/useAuthUser.js"; export default function PatientInfoCard(props: { patient: PatientModel; @@ -36,6 +37,8 @@ export default function PatientInfoCard(props: { consultationId: string; showAbhaProfile?: boolean; }) { + const authUser = useAuthUser(); + const [open, setOpen] = useState(false); const [showLinkABHANumber, setShowLinkABHANumber] = useState(false); const [showABHAProfile, setShowABHAProfile] = useState( @@ -459,6 +462,11 @@ export default function PatientInfoCard(props: { }); setOpen(true); } + triggerGoal("Patient Card Button Clicked", { + buttonName: action[1], + consultationId: consultation?.id, + userId: authUser?.id, + }); }} > { close(); setShowABHAProfile(true); + triggerGoal("Patient Card Button Clicked", { + buttonName: "Show ABHA Profile", + consultationId: consultation?.id, + userId: authUser?.id, + }); }} > @@ -497,6 +510,11 @@ export default function PatientInfoCard(props: {
{ + triggerGoal("Patient Card Button Clicked", { + buttonName: "Link Care Context", + consultationId: consultation?.id, + userId: authUser?.id, + }); close(); setShowLinkCareContext(true); }} @@ -532,6 +550,11 @@ export default function PatientInfoCard(props: { { + triggerGoal("Patient Card Button Clicked", { + buttonName: "Medico Legal Case", + consultationId: consultation?.id, + userId: authUser?.id, + }); setMedicoLegalCase(checked); switchMedicoLegalCase(checked); }} From 67463597abeaf8bb859088c4e38200e0809fd188 Mon Sep 17 00:00:00 2001 From: konavivekramakrishna <101407963+konavivekramakrishna@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:46:31 +0530 Subject: [PATCH 04/11] Replaced useDispatch w. useQuery/request: Resource (src/Components/Resource/**) (#6461) * replaced useDispatch in src/resource with useQuery and request * removed unused actions * fix loading * removed interface import * rm limit and offset * fixed useQuery Hooks * checking cypress failure * changed function name * removed external_id from models * fix naming * fix listfilter * fixed the condition in badgeList --- src/Components/Resource/BadgesList.tsx | 88 ++++------ src/Components/Resource/CommentSection.tsx | 152 ++++++++---------- src/Components/Resource/ListFilter.tsx | 87 ++++------ src/Components/Resource/ListView.tsx | 68 ++------ src/Components/Resource/ResourceBoard.tsx | 76 ++++----- src/Components/Resource/ResourceCreate.tsx | 41 ++--- src/Components/Resource/ResourceDetails.tsx | 50 ++---- .../Resource/ResourceDetailsUpdate.tsx | 96 ++++------- src/Components/Resource/models.ts | 41 +++++ src/Redux/actions.tsx | 21 --- src/Redux/api.tsx | 25 ++- 11 files changed, 305 insertions(+), 440 deletions(-) create mode 100644 src/Components/Resource/models.ts diff --git a/src/Components/Resource/BadgesList.tsx b/src/Components/Resource/BadgesList.tsx index 31b8f1c17ae..6977861d596 100644 --- a/src/Components/Resource/BadgesList.tsx +++ b/src/Components/Resource/BadgesList.tsx @@ -1,63 +1,25 @@ -import { useState, useEffect } from "react"; -import { getAnyFacility } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import { SHIFTING_FILTER_ORDER } from "../../Common/constants"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; + +function useFacilityQuery(facilityId: string | undefined) { + return useQuery(routes.getAnyFacility, { + pathParams: { id: String(facilityId) }, + prefetch: facilityId !== undefined, + }); +} export default function BadgesList(props: any) { const { appliedFilters, FilterBadges } = props; - const [orginFacilityName, setOrginFacilityName] = useState(""); - const [approvingFacilityName, setApprovingFacilityName] = useState(""); - const [assignedFacilityName, setAssignedFacilityName] = useState(""); - const dispatch: any = useDispatch(); - - useEffect(() => { - async function fetchData() { - if (!appliedFilters.origin_facility) return setOrginFacilityName(""); - const res = await dispatch( - getAnyFacility(appliedFilters.origin_facility, "origin_facility_name") - ); - setOrginFacilityName(res?.data?.name); - } - fetchData(); - }, [dispatch, appliedFilters.origin_facility]); - - useEffect(() => { - async function fetchData() { - if (!appliedFilters.approving_facility) - return setApprovingFacilityName(""); - const res = await dispatch( - getAnyFacility( - appliedFilters.approving_facility, - "approving_facility_name" - ) - ); - setApprovingFacilityName(res?.data?.name); - } - fetchData(); - }, [dispatch, appliedFilters.approving_facility]); - - useEffect(() => { - async function fetchData() { - if (!appliedFilters.assigned_facility) return setAssignedFacilityName(""); - const res = await dispatch( - getAnyFacility( - appliedFilters.assigned_facility, - "assigned_facility_name" - ) - ); - setAssignedFacilityName(res?.data?.name); - } - fetchData(); - }, [dispatch, appliedFilters.assigned_facility]); + const originFacility = useFacilityQuery(appliedFilters.origin_facility); + const approvingFacility = useFacilityQuery(appliedFilters.approving_facility); + const assignedFacility = useFacilityQuery(appliedFilters.assigned_facility); const getDescShiftingFilterOrder = (ordering: any) => { - let desc = ""; - SHIFTING_FILTER_ORDER.map((item: any) => { - if (item.text === ordering) { - desc = item.desc; - } - }); - return desc; + const foundItem = SHIFTING_FILTER_ORDER.find( + (item) => item.text === ordering + ); + return foundItem ? foundItem.desc : ""; }; return ( @@ -75,13 +37,25 @@ export default function BadgesList(props: any) { }), ...dateRange("Modified", "modified_date"), ...dateRange("Created", "created_date"), - value("Origin facility", "origin_facility", orginFacilityName), + value( + "Origin facility", + "origin_facility", + appliedFilters.origin_facility ? originFacility?.data?.name || "" : "" + ), value( "Approving facility", "approving_facility", - approvingFacilityName + appliedFilters.approving_facility + ? approvingFacility?.data?.name || "" + : "" + ), + value( + "Assigned facility", + "assigned_facility", + appliedFilters.assigned_facility + ? assignedFacility?.data?.name || "" + : "" ), - value("Assigned facility", "assigned_facility", assignedFacilityName), ]} /> ); diff --git a/src/Components/Resource/CommentSection.tsx b/src/Components/Resource/CommentSection.tsx index 01bd67454f5..25d8142dae7 100644 --- a/src/Components/Resource/CommentSection.tsx +++ b/src/Components/Resource/CommentSection.tsx @@ -1,60 +1,26 @@ -import { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { getResourceComments, addResourceComments } from "../../Redux/actions"; +import { useState } from "react"; import * as Notification from "../../Utils/Notifications.js"; -import Pagination from "../Common/Pagination"; import { formatDateTime } from "../../Utils/utils"; import CircularProgress from "../Common/components/CircularProgress"; import ButtonV2 from "../Common/components/ButtonV2"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import PaginatedList from "../../CAREUI/misc/PaginatedList"; +import { IComment } from "./models"; +import request from "../../Utils/request/request"; -interface CommentSectionProps { - id: string; -} -const CommentSection = (props: CommentSectionProps) => { - const dispatch: any = useDispatch(); - const initialData: any = []; - const [comments, setComments] = useState(initialData); +const CommentSection = (props: { id: string }) => { const [commentBox, setCommentBox] = useState(""); - const [isLoading, setIsLoading] = useState(true); - - const [currentPage, setCurrentPage] = useState(1); - const [totalCount, setTotalCount] = useState(0); - const [offset, setOffset] = useState(0); - const limit = 8; - - const handlePagination = (page: number, limit: number) => { - const offset = (page - 1) * limit; - setCurrentPage(page); - setOffset(offset); - }; - - const fetchData = useCallback( - async (status: statusType = { aborted: false }) => { - setIsLoading(true); - const res = await dispatch( - getResourceComments(props.id, { limit, offset }) - ); - if (!status.aborted) { - if (res && res.data) { - setComments(res.data?.results); - setTotalCount(res.data.count); - } - setIsLoading(false); - } - }, - [props.id, dispatch, offset] - ); - - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [fetchData] + const { loading, refetch: resourceRefetch } = useQuery( + routes.getResourceComments, + { + pathParams: { id: props.id }, + query: { limit: 8, offset: 0 }, + } ); - const onSubmitComment = () => { + const onSubmitComment = async () => { const payload = { comment: commentBox, }; @@ -64,13 +30,16 @@ const CommentSection = (props: CommentSectionProps) => { }); return; } - dispatch(addResourceComments(props.id, payload)).then((_: any) => { - Notification.Success({ msg: "Comment added successfully" }); - fetchData(); + const { res } = await request(routes.addResourceComments, { + pathParams: { id: props.id }, + body: payload, }); + if (res?.ok) { + Notification.Success({ msg: "Comment added successfully" }); + resourceRefetch(); + } setCommentBox(""); }; - return (
{ Post Your Comment
- {isLoading ? ( + {loading ? ( ) : ( - comments.map((comment: any) => ( -
-
-

{comment.comment}

-
-
- - {formatDateTime(comment.modified_date) || "-"} - -
-
-
- {comment.created_by_object?.first_name?.charAt(0) || "U"} + + {() => ( +
+ + No comments available + + + + + > + {(item) => } + +
+
- - {comment.created_by_object?.first_name || "Unknown"}{" "} - {comment.created_by_object?.last_name} -
-
- )) + )} + )}
- {totalCount > limit && ( -
- -
- )}
); }; export default CommentSection; + +export const Comment = ({ + comment, + created_by_object, + modified_date, +}: IComment) => ( +
+
+

{comment}

+
+
+ + {formatDateTime(modified_date) || "-"} + +
+
+
+ {created_by_object?.first_name?.charAt(0) || "U"} +
+ + {created_by_object?.first_name || "Unknown"}{" "} + {created_by_object?.last_name} + +
+
+); diff --git a/src/Components/Resource/ListFilter.tsx b/src/Components/Resource/ListFilter.tsx index 7a47732db64..afe48eedfd0 100644 --- a/src/Components/Resource/ListFilter.tsx +++ b/src/Components/Resource/ListFilter.tsx @@ -1,8 +1,5 @@ -import { useEffect, useState } from "react"; import { FacilitySelect } from "../Common/FacilitySelect"; import { RESOURCE_FILTER_ORDER } from "../../Common/constants"; -import { getAnyFacility } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import { RESOURCE_CHOICES } from "../../Common/constants"; import useMergeState from "../../Common/hooks/useMergeState"; import { navigate } from "raviger"; @@ -15,6 +12,8 @@ import { DateRange } from "../Common/DateRangeInputV2"; import DateRangeFormField from "../Form/FormFields/DateRangeFormField"; import dayjs from "dayjs"; import { dateQueryString } from "../../Utils/utils"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; const clearFilterState = { origin_facility: "", @@ -37,9 +36,6 @@ const getDate = (value: any) => export default function ListFilter(props: any) { const { filter, onChange, closeFilter } = props; - const [isOriginLoading, setOriginLoading] = useState(false); - const [isResourceLoading, setResourceLoading] = useState(false); - const [isAssignedLoading, setAssignedLoading] = useState(false); const [filterState, setFilterState] = useMergeState({ origin_facility: filter.origin_facility || "", origin_facility_ref: null, @@ -55,55 +51,42 @@ export default function ListFilter(props: any) { ordering: filter.ordering || null, status: filter.status || null, }); - const dispatch: any = useDispatch(); - useEffect(() => { - async function fetchData() { - if (filter.origin_facility) { - setOriginLoading(true); - const res = await dispatch( - getAnyFacility(filter.origin_facility, "origin_facility") - ); - if (res && res.data) { - setFilterState({ origin_facility_ref: res.data }); - } - setOriginLoading(false); + const { loading: orginFacilityLoading } = useQuery(routes.getAnyFacility, { + prefetch: filter.origin_facility !== undefined, + pathParams: { id: filter.origin_facility }, + onResponse: ({ res, data }) => { + if (res && data) { + setFilterState({ + origin_facility_ref: filter.origin_facility === "" ? "" : data, + }); } - } - fetchData(); - }, [dispatch]); + }, + }); - useEffect(() => { - async function fetchData() { - if (filter.approving_facility) { - setResourceLoading(true); - const res = await dispatch( - getAnyFacility(filter.approving_facility, "approving_facility") - ); - if (res && res.data) { - setFilterState({ approving_facility_ref: res.data }); - } - setResourceLoading(false); + const { loading: resourceFacilityLoading } = useQuery(routes.getAnyFacility, { + prefetch: filter.approving_facility !== undefined, + pathParams: { id: filter.approving_facility }, + onResponse: ({ res, data }) => { + if (res && data) { + setFilterState({ + approving_facility_ref: filter.approving_facility === "" ? "" : data, + }); } - } - fetchData(); - }, [dispatch]); + }, + }); - useEffect(() => { - async function fetchData() { - if (filter.assigned_facility) { - setAssignedLoading(true); - const res = await dispatch( - getAnyFacility(filter.assigned_facility, "assigned_facility") - ); - if (res && res.data) { - setFilterState({ assigned_facility_ref: res.data }); - } - setAssignedLoading(false); + const { loading: assignedFacilityLoading } = useQuery(routes.getAnyFacility, { + pathParams: { id: filter.assigned_facility }, + prefetch: filter.assigned_facility !== undefined, + onResponse: ({ res, data }) => { + if (res && data) { + setFilterState({ + assigned_facility_ref: filter.assigned_facility === "" ? "" : data, + }); } - } - fetchData(); - }, [dispatch]); + }, + }); const setFacility = (selected: any, name: string) => { setFilterState({ @@ -178,7 +161,7 @@ export default function ListFilter(props: any) {
Origin facility - {isOriginLoading ? ( + {orginFacilityLoading && filter.origin_facility ? ( ) : ( Resource approving facility - {isResourceLoading ? ( + {filter.approving_facility && resourceFacilityLoading ? ( ) : ( Assigned facility - {isAssignedLoading ? ( + {filter.approving_facility && assignedFacilityLoading ? ( ) : ( import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); export default function ListView() { - const dispatch: any = useDispatch(); const { qParams, Pagination, FilterBadges, advancedFilter, resultsPerPage } = useFilters({}); - const [data, setData] = useState([]); - const [totalCount, setTotalCount] = useState(0); - const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const onBoardViewBtnClick = () => navigate("/resource/board", { query: qParams }); const appliedFilters = formatFilter(qParams); - const refreshList = () => { - fetchData(); - }; - - const fetchData = () => { - setIsLoading(true); - dispatch( - listResourceRequests( - formatFilter({ - ...qParams, - offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, - }), - "resource-list-call" - ) - ).then((res: any) => { - if (res && res.data) { - setData(res.data.results); - setTotalCount(res.data.count); - } - setIsLoading(false); - }); - }; - - useEffect(() => { - fetchData(); - }, [ - qParams.status, - qParams.facility, - qParams.origin_facility, - qParams.approving_facility, - qParams.assigned_facility, - qParams.emergency, - qParams.created_date_before, - qParams.created_date_after, - qParams.modified_date_before, - qParams.modified_date_after, - qParams.ordering, - qParams.page, - ]); + const { loading, data, refetch } = useQuery(routes.listResourceRequests, { + query: formatFilter({ + ...qParams, + offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, + }), + }); const showResourceCardList = (data: any) => { if (data && !data.length) { @@ -218,14 +178,14 @@ export default function ListView() {
- {isLoading ? ( + {loading ? ( ) : (
- {showResourceCardList(data)} + {data?.results && showResourceCardList(data?.results)}
- +
)}
diff --git a/src/Components/Resource/ResourceBoard.tsx b/src/Components/Resource/ResourceBoard.tsx index 66bf559be0d..217f2941e10 100644 --- a/src/Components/Resource/ResourceBoard.tsx +++ b/src/Components/Resource/ResourceBoard.tsx @@ -1,15 +1,13 @@ import { useState, useEffect } from "react"; -import { useDispatch } from "react-redux"; -import { - listResourceRequests, - downloadResourceRequests, -} from "../../Redux/actions"; +import { downloadResourceRequests } from "../../Redux/actions"; import { navigate } from "raviger"; import { classNames } from "../../Utils/utils"; import { useDrag, useDrop } from "react-dnd"; import { formatDateTime } from "../../Utils/utils"; import { ExportButton } from "../Common/Export"; import dayjs from "../../Utils/dayjs"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; const limit = 14; @@ -118,7 +116,6 @@ const ResourceCard = ({ resource }: any) => {
- {resource.assigned_to_object && (
{ )}
-
@@ -273,13 +258,14 @@ export default function ResourceBoard({
- ) : data?.length > 0 ? ( + ) : data && data?.results.length > 0 ? ( boardFilter(board) ) : (

No requests to show.

)} {!isLoading.board && - data?.length < (totalCount || 0) && + data && + data?.results.length < (data?.count || 0) && (isLoading.more ? (
Loading diff --git a/src/Components/Resource/ResourceCreate.tsx b/src/Components/Resource/ResourceCreate.tsx index cf9e5f6f22a..d0a6c36272b 100644 --- a/src/Components/Resource/ResourceCreate.tsx +++ b/src/Components/Resource/ResourceCreate.tsx @@ -1,8 +1,7 @@ -import { useReducer, useState, useEffect, lazy } from "react"; +import { useReducer, useState, lazy } from "react"; import { FacilitySelect } from "../Common/FacilitySelect"; import * as Notification from "../../Utils/Notifications.js"; -import { useDispatch } from "react-redux"; import { navigate } from "raviger"; import { OptionsType, @@ -11,8 +10,6 @@ import { } from "../../Common/constants"; import { parsePhoneNumber } from "../../Utils/utils"; import { phonePreg } from "../../Common/validation"; - -import { createResource, getAnyFacility } from "../../Redux/actions"; import { Cancel, Submit } from "../Common/components/ButtonV2"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; @@ -26,6 +23,9 @@ import { FieldLabel } from "../Form/FormFields/FormField"; import Card from "../../CAREUI/display/Card"; import Page from "../Common/components/Page"; 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")); @@ -87,10 +87,7 @@ export default function ResourceCreate(props: resourceProps) { const { goBack } = useAppHistory(); const { facilityId } = props; const { t } = useTranslation(); - - const dispatchAction: any = useDispatch(); const [isLoading, setIsLoading] = useState(false); - const [facilityName, setFacilityName] = useState(""); const resourceFormReducer = (state = initialState, action: any) => { switch (action.type) { @@ -113,18 +110,10 @@ export default function ResourceCreate(props: resourceProps) { const [state, dispatch] = useReducer(resourceFormReducer, initialState); - useEffect(() => { - async function fetchFacilityName() { - if (facilityId) { - const res = await dispatchAction(getAnyFacility(facilityId)); - - setFacilityName(res?.data?.name || ""); - } else { - setFacilityName(""); - } - } - fetchFacilityName(); - }, [dispatchAction, facilityId]); + const { data: facilityData } = useQuery(routes.getAnyFacility, { + prefetch: facilityId !== undefined, + pathParams: { id: String(facilityId) }, + }); const validateForm = () => { const errors = { ...initError }; @@ -184,11 +173,11 @@ export default function ResourceCreate(props: resourceProps) { if (validForm) { setIsLoading(true); - const data = { + const resourceData = { status: "PENDING", category: state.form.category, sub_category: state.form.sub_category, - origin_facility: props.facilityId, + origin_facility: String(props.facilityId), approving_facility: (state.form.approving_facility || {}).id, assigned_facility: (state.form.assigned_facility || {}).id, emergency: state.form.emergency === "true", @@ -202,16 +191,18 @@ export default function ResourceCreate(props: resourceProps) { requested_quantity: state.form.requested_quantity || 0, }; - const res = await dispatchAction(createResource(data)); + const { res, data } = await request(routes.createResource, { + body: resourceData, + }); setIsLoading(false); - if (res && res.data && (res.status == 201 || res.status == 200)) { + if (res?.ok && data) { await dispatch({ type: "set_form", form: initForm }); Notification.Success({ msg: "Resource request created successfully", }); - navigate(`/resource/${res.data.id}`); + navigate(`/resource/${data.id}`); } } }; @@ -224,7 +215,7 @@ export default function ResourceCreate(props: resourceProps) { import("../Common/Loading")); export default function ResourceDetails(props: { id: string }) { - const dispatch: any = useDispatch(); - const initialData: any = {}; - const [data, setData] = useState(initialData); - const [isLoading, setIsLoading] = useState(true); const [isPrintMode, setIsPrintMode] = useState(false); - const [openDeleteResourceDialog, setOpenDeleteResourceDialog] = useState(false); - - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const res = await dispatch(getResourceDetails({ id: props.id })); - if (!status.aborted) { - if (res && res.data) { - setData(res.data); - } else { - navigate("/not-found"); - } - setIsLoading(false); + const { data, loading } = useQuery(routes.getResourceDetails, { + pathParams: { id: props.id }, + onResponse: ({ res, data }) => { + if (!res && !data) { + navigate("/not-found"); } }, - [props.id, dispatch] - ); - - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [fetchData] - ); + }); const handleResourceDelete = async () => { setOpenDeleteResourceDialog(true); - - const res = await dispatch(deleteResourceRecord(props.id)); + const { res, data } = await request(routes.deleteResourceRecord, { + pathParams: { id: props.id }, + }); if (res?.status === 204) { Notification.Success({ msg: "Resource record has been deleted successfully.", }); } else { Notification.Error({ - msg: "Error while deleting Resource: " + (res?.data?.detail || ""), + msg: "Error while deleting Resource: " + (data?.detail || ""), }); } @@ -223,7 +203,7 @@ export default function ResourceDetails(props: { id: string }) { ); }; - if (isLoading) { + if (loading || !data) { return ; } diff --git a/src/Components/Resource/ResourceDetailsUpdate.tsx b/src/Components/Resource/ResourceDetailsUpdate.tsx index ce0751e7b2e..c692618a774 100644 --- a/src/Components/Resource/ResourceDetailsUpdate.tsx +++ b/src/Components/Resource/ResourceDetailsUpdate.tsx @@ -1,15 +1,7 @@ import * as Notification from "../../Utils/Notifications.js"; - import { Cancel, Submit } from "../Common/components/ButtonV2"; -import { lazy, useCallback, useEffect, useReducer, useState } from "react"; -import { - getResourceDetails, - getUserList, - updateResource, -} from "../../Redux/actions"; +import { lazy, useReducer, useState } from "react"; import { navigate, useQueryParams } from "raviger"; -import { statusType, useAbortableEffect } from "../../Common/utils"; - import Card from "../../CAREUI/display/Card"; import CircularProgress from "../Common/components/CircularProgress"; import { FacilitySelect } from "../Common/FacilitySelect"; @@ -22,9 +14,11 @@ import { SelectFormField } from "../Form/FormFields/SelectFormField"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import TextFormField from "../Form/FormFields/TextFormField"; import UserAutocompleteFormField from "../Common/UserAutocompleteFormField"; - import useAppHistory from "../../Common/hooks/useAppHistory"; -import { useDispatch } from "react-redux"; +import useQuery from "../../Utils/request/useQuery.js"; +import routes from "../../Redux/api.js"; +import { UserModel } from "../Users/models.js"; +import request from "../../Utils/request/request.js"; const Loading = lazy(() => import("../Common/Loading")); @@ -67,13 +61,9 @@ const initialState = { export const ResourceDetailsUpdate = (props: resourceProps) => { const { goBack } = useAppHistory(); - const dispatchAction: any = useDispatch(); const [qParams, _] = useQueryParams(); const [isLoading, setIsLoading] = useState(true); - const [assignedQuantity, setAssignedQuantity] = useState(0); - const [requestTitle, setRequestTitle] = useState(""); - const [assignedUser, SetAssignedUser] = useState(null); - const [assignedUserLoading, setAssignedUserLoading] = useState(false); + const [assignedUser, SetAssignedUser] = useState(); const resourceFormReducer = (state = initialState, action: any) => { switch (action.type) { case "set_form": { @@ -95,23 +85,13 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { const [state, dispatch] = useReducer(resourceFormReducer, initialState); - useEffect(() => { - async function fetchData() { - if (state.form.assigned_to) { - setAssignedUserLoading(true); - - const res = await dispatchAction( - getUserList({ id: state.form.assigned_to }) - ); - - if (res && res.data && res.data.count) - SetAssignedUser(res.data.results[0]); - - setAssignedUserLoading(false); + const { loading: assignedUserLoading } = useQuery(routes.userList, { + onResponse: ({ res, data }) => { + if (res?.ok && data && data.count) { + SetAssignedUser(data.results[0]); } - } - fetchData(); - }, [dispatchAction, state.form.assigned_to]); + }, + }); const validateForm = () => { const errors = { ...initError }; @@ -147,13 +127,25 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { dispatch({ type: "set_form", form }); }; + const { data: resourceDetails } = useQuery(routes.getResourceDetails, { + pathParams: { id: props.id }, + onResponse: ({ res, data }) => { + if (res && data) { + const d = data; + d["status"] = qParams.status || data.status; + dispatch({ type: "set_form", form: d }); + } + setIsLoading(false); + }, + }); + const handleSubmit = async () => { const validForm = validateForm(); if (validForm) { setIsLoading(true); - const data = { + const resourceData = { category: "OXYGEN", status: state.form.status, origin_facility: state.form.origin_facility_object?.id, @@ -167,14 +159,17 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { assigned_quantity: state.form.status === "PENDING" ? state.form.assigned_quantity - : assignedQuantity, + : resourceDetails?.assigned_quantity || 0, }; - const res = await dispatchAction(updateResource(props.id, data)); + const { res, data } = await request(routes.updateResource, { + pathParams: { id: props.id }, + body: resourceData, + }); setIsLoading(false); - if (res && res.status == 200 && res.data) { - dispatch({ type: "set_form", form: res.data }); + if (res && res.status == 200 && data) { + dispatch({ type: "set_form", form: data }); Notification.Success({ msg: "Resource request updated successfully", }); @@ -186,31 +181,6 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { } }; - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const res = await dispatchAction(getResourceDetails({ id: props.id })); - if (!status.aborted) { - if (res && res.data) { - setRequestTitle(res.data.title); - setAssignedQuantity(res.data.assigned_quantity); - const d = res.data; - d["status"] = qParams.status || res.data.status; - dispatch({ type: "set_form", form: d }); - } - setIsLoading(false); - } - }, - [props.id, dispatchAction, qParams.status] - ); - - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [fetchData] - ); - if (isLoading) { return ; } @@ -219,7 +189,7 @@ export const ResourceDetailsUpdate = (props: resourceProps) => {
diff --git a/src/Components/Resource/models.ts b/src/Components/Resource/models.ts new file mode 100644 index 00000000000..f10ac988552 --- /dev/null +++ b/src/Components/Resource/models.ts @@ -0,0 +1,41 @@ +import { PerformedByModel } from "../HCX/misc"; + +export interface IComment { + id: string; + created_by_object: PerformedByModel; + created_date: string; + modified_date: string; + comment: string; + created_by: number; +} + +export interface IResource { + id: string; + title: string; + emergency: boolean; + status?: string; + origin_facility_object: { + name: string; + }; + approving_facility_object: { + name: string; + }; + assigned_facility_object: { + name: string; + }; + assigned_quantity: number; + modified_date: string; + category: any; + sub_category: number; + origin_facility: string; + approving_facility: string; + assigned_facility: string; + reason: string; + refering_facility_contact_name: string; + refering_facility_contact_number: string; + requested_quantity: number; + assigned_to_object: PerformedByModel; + created_by_object: PerformedByModel; + created_date: string; + last_edited_by_object: PerformedByModel; +} diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 1d2d6f4f7b6..15934d0957d 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -803,30 +803,9 @@ export const listMedibaseMedicines = ( }; // Resource -export const createResource = (params: object) => { - return fireRequest("createResource", [], params); -}; -export const updateResource = (id: string, params: object) => { - return fireRequest("updateResource", [id], params); -}; -export const deleteResourceRecord = (id: string) => { - return fireRequest("deleteResourceRecord", [id], {}); -}; -export const listResourceRequests = (params: object, key: string) => { - return fireRequest("listResourceRequests", [], params, null, key); -}; -export const getResourceDetails = (pathParam: object) => { - return fireRequest("getResourceDetails", [], {}, pathParam); -}; export const downloadResourceRequests = (params: object) => { return fireRequest("downloadResourceRequests", [], params); }; -export const getResourceComments = (id: string, params: object) => { - return fireRequest("getResourceComments", [], params, { id }); -}; -export const addResourceComments = (id: string, params: object) => { - return fireRequest("addResourceComments", [], params, { id }); -}; export const listAssets = (params: object) => fireRequest("listAssets", [], params); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 104fa0c0c75..01c356f6603 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -28,6 +28,7 @@ import { AssetUpdate, } from "../Components/Assets/AssetTypes"; import { + ConsultationModel, FacilityModel, LocationModel, WardModel, @@ -40,10 +41,13 @@ import { ILocalBodyByDistrict, IPartialUpdateExternalResult, } from "../Components/ExternalResult/models"; + import { Prescription } from "../Components/Medicine/models"; -import { PatientModel } from "../Components/Patient/models"; + import { UserModel } from "../Components/Users/models"; import { PaginatedResponse } from "../Utils/request/types"; +import { PatientModel } from "../Components/Patient/models"; +import { IComment, IResource } from "../Components/Resource/models"; /** * A fake function that returns an empty object casted to type T @@ -128,6 +132,8 @@ const routes = { userList: { path: "/api/v1/users/", + method: "GET", + TRes: Type>(), }, userListSkill: { @@ -843,21 +849,31 @@ const routes = { createResource: { path: "/api/v1/resource/", method: "POST", + TRes: Type(), + TBody: Type>(), }, updateResource: { - path: "/api/v1/resource", + path: "/api/v1/resource/{id}", method: "PUT", + TRes: Type(), + TBody: Type>(), }, deleteResourceRecord: { - path: "/api/v1/resource", + path: "/api/v1/resource/{id}", method: "DELETE", + TRes: Type<{ + detail?: string; + }>(), }, listResourceRequests: { path: "/api/v1/resource/", method: "GET", + TRes: Type>(), }, getResourceDetails: { path: "/api/v1/resource/{id}/", + method: "GET", + TRes: Type(), }, downloadResourceRequests: { path: "/api/v1/resource/", @@ -866,10 +882,13 @@ const routes = { getResourceComments: { path: "/api/v1/resource/{id}/comment/", method: "GET", + TRes: Type>(), }, addResourceComments: { path: "/api/v1/resource/{id}/comment/", method: "POST", + TRes: Type(), + TBody: Type>(), }, // Assets endpoints From 343ff7eeb01623e4c521fdb9ba2bc1c4fa895483 Mon Sep 17 00:00:00 2001 From: Ashraf Mohammed <98876115+AshrafMd-1@users.noreply.github.com> Date: Tue, 31 Oct 2023 04:17:38 -0700 Subject: [PATCH 05/11] Fixed bed number coming up two time in patients page. (#6526) * stop displaying bed name two times * resolve tooltip --- src/Components/Patient/ManagePatients.tsx | 26 ++++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 61b18faa367..7db0bcceb39 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -519,27 +519,23 @@ export const PatientManager = () => {
{patient?.last_consultation?.current_bed && patient?.last_consultation?.discharge_date === null ? ( -
- +
+ { patient?.last_consultation?.current_bed?.bed_object ?.location_object?.name } - - { - patient?.last_consultation?.current_bed?.bed_object - ?.location_object?.name - } - - {patient?.last_consultation?.current_bed?.bed_object.name} - - { - patient?.last_consultation?.current_bed?.bed_object - ?.name - } - + {patient?.last_consultation?.current_bed?.bed_object?.name} + + + { + patient?.last_consultation?.current_bed?.bed_object + ?.location_object?.name + } +
+ {patient?.last_consultation?.current_bed?.bed_object?.name}
) : patient.last_consultation?.suggestion === "DC" ? ( From b43f8d577e12b5b144ffe6772807eace8968629a Mon Sep 17 00:00:00 2001 From: Onkar Jadhav <56870381+Omkar76@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:51:06 +0530 Subject: [PATCH 06/11] Make header in patient dashboard more responsive. Fixes #6488 (#6515) --- .../Facility/ConsultationDetails/index.tsx | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index cedb1a843d3..202c9200b0a 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -271,7 +271,7 @@ export const ConsultationDetails = (props: any) => { />
-
diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx index 35a125ae07d..9004dea939d 100644 --- a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx +++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx @@ -119,15 +119,17 @@ export const AutocompleteMutliSelect = ( onChange={(event) => setQuery(event.target.value.toLowerCase())} autoComplete="off" /> - -
- {props.isLoading ? ( - - ) : ( - - )} -
-
+ {!props.disabled && ( + +
+ {props.isLoading ? ( + + ) : ( + + )} +
+
+ )}
{value.length !== 0 && (
diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 7db0bcceb39..7a78d94582e 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -16,9 +16,14 @@ import { getAllPatient, getAnyFacility, getDistrict, + getFacilityAssetLocation, getLocalBody, } from "../../Redux/actions"; -import { statusType, useAbortableEffect } from "../../Common/utils"; +import { + statusType, + useAbortableEffect, + parseOptionId, +} from "../../Common/utils"; import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -36,7 +41,6 @@ import RecordMeta from "../../CAREUI/display/RecordMeta"; import SearchInput from "../Form/SearchInput"; import SortDropdownMenu from "../Common/SortDropdown"; import SwitchTabs from "../Common/components/SwitchTabs"; -import { parseOptionId } from "../../Common/utils"; import { formatAge, parsePhoneNumber } from "../../Utils/utils.js"; import { useDispatch } from "react-redux"; import useFilters from "../../Common/hooks/useFilters"; @@ -104,6 +108,7 @@ export const PatientManager = () => { const [districtName, setDistrictName] = useState(""); const [localbodyName, setLocalbodyName] = useState(""); const [facilityBadgeName, setFacilityBadge] = useState(""); + const [locationBadgeName, setLocationBadge] = useState(""); const [phone_number, setPhoneNumber] = useState(""); const [phoneNumberError, setPhoneNumberError] = useState(""); const [emergency_phone_number, setEmergencyPhoneNumber] = useState(""); @@ -199,6 +204,8 @@ export const PatientManager = () => { qParams.last_consultation_admitted_bed_type_list || undefined, last_consultation_discharge_reason: qParams.last_consultation_discharge_reason || undefined, + last_consultation_current_bed__location: + qParams.last_consultation_current_bed__location || undefined, srf_id: qParams.srf_id || undefined, number_of_doses: qParams.number_of_doses || undefined, covin_id: qParams.covin_id || undefined, @@ -344,6 +351,7 @@ export const PatientManager = () => { qParams.age_min, qParams.last_consultation_admitted_bed_type_list, qParams.last_consultation_discharge_reason, + qParams.last_consultation_current_bed__location, qParams.facility, qParams.facility_type, qParams.district, @@ -443,12 +451,32 @@ export const PatientManager = () => { [dispatch, qParams.facility] ); + const fetchLocationBadgeName = useCallback( + async (status: statusType) => { + const res = + qParams.last_consultation_current_bed__location && + (await dispatch( + getFacilityAssetLocation( + qParams.facility, + qParams.last_consultation_current_bed__location + ) + )); + + if (!status.aborted) { + setLocationBadge(res?.data?.name); + } + }, + [dispatch, qParams.last_consultation_current_bed__location] + ); + useAbortableEffect( (status: statusType) => { fetchFacilityBadgeName(status); + fetchLocationBadgeName(status); }, - [fetchFacilityBadgeName] + [fetchFacilityBadgeName, fetchLocationBadgeName] ); + const LastAdmittedToTypeBadges = () => { const badge = (key: string, value: any, id: string) => { return ( @@ -936,6 +964,11 @@ export const PatientManager = () => { "last_consultation_medico_legal_case" ), value("Facility", "facility", facilityBadgeName), + value( + "Location", + "last_consultation_current_bed__location", + locationBadgeName + ), badge("Facility Type", "facility_type"), value("District", "district", districtName), ordering(), diff --git a/src/Components/Patient/PatientFilter.tsx b/src/Components/Patient/PatientFilter.tsx index 97f5a6c6b9c..7bc7f7b861f 100644 --- a/src/Components/Patient/PatientFilter.tsx +++ b/src/Components/Patient/PatientFilter.tsx @@ -34,6 +34,7 @@ import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; import AccordionV2 from "../Common/components/AccordionV2"; import { dateQueryString } from "../../Utils/utils"; import dayjs from "dayjs"; +import { LocationSelect } from "../Common/LocationSelect"; const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); @@ -79,6 +80,8 @@ export default function PatientFilter(props: any) { filter.last_consultation_admitted_bed_type_list ? filter.last_consultation_admitted_bed_type_list.split(",") : [], + last_consultation_current_bed__location: + filter.last_consultation_current_bed__location || "", last_consultation_discharge_reason: filter.last_consultation_discharge_reason || null, srf_id: filter.srf_id || null, @@ -128,6 +131,7 @@ export default function PatientFilter(props: any) { last_consultation_discharge_date_before: "", last_consultation_discharge_date_after: "", last_consultation_admitted_to_list: [], + last_consultation_current_bed__location: "", srf_id: "", number_of_doses: null, covin_id: "", @@ -239,6 +243,7 @@ export default function PatientFilter(props: any) { last_consultation_discharge_date_after, last_consultation_admitted_bed_type_list, last_consultation_discharge_reason, + last_consultation_current_bed__location, number_of_doses, covin_id, srf_id, @@ -256,6 +261,8 @@ export default function PatientFilter(props: any) { district: district || "", lsgBody: lsgBody || "", facility: facility || "", + last_consultation_current_bed__location: + last_consultation_current_bed__location || "", facility_type: facility_type || "", date_declared_positive_before: dateQueryString( date_declared_positive_before @@ -588,13 +595,29 @@ export default function PatientFilter(props: any) { setFacility(obj, "facility")} />
-
+ Location + + setFilterState({ + ...filterState, + last_consultation_current_bed__location: selected, + }) + } + /> +
+
Facility type Date: Tue, 31 Oct 2023 16:52:38 +0530 Subject: [PATCH 08/11] Show only those facilties that aren't linked to user (#6253) * only show unlinked facilties * rename param * fix cypress for updated backend * fix typo --- cypress/e2e/users_spec/user_creation.cy.ts | 9 +-------- src/Components/Common/FacilitySelect.tsx | 3 +++ src/Components/Users/ManageUsers.tsx | 1 + 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/users_spec/user_creation.cy.ts b/cypress/e2e/users_spec/user_creation.cy.ts index 5a1e5d874de..bd5002d386f 100644 --- a/cypress/e2e/users_spec/user_creation.cy.ts +++ b/cypress/e2e/users_spec/user_creation.cy.ts @@ -202,14 +202,7 @@ describe("User Creation", () => { // .click() // .type("Dummy Facility 1") // .wait("@getFacilities"); - // cy.get("li[role='option']").first().click(); - // cy.intercept(/\/api\/v1\/users\/\w+\/add_facility\//).as("addFacility"); - // cy.get("button[id='link-facility']").click(); - // cy.wait("@addFacility") - // // .its("response.statusCode") - // // .should("eq", 201) - // .get("span") - // .contains("Facility - User Already has permission to this facility"); + // cy.get("li[role='option']").should("not.exist"); // }); afterEach(() => { diff --git a/src/Components/Common/FacilitySelect.tsx b/src/Components/Common/FacilitySelect.tsx index 400ce115bc5..19494081aa3 100644 --- a/src/Components/Common/FacilitySelect.tsx +++ b/src/Components/Common/FacilitySelect.tsx @@ -6,6 +6,7 @@ import { FacilityModel } from "../Facility/models"; interface FacilitySelectProps { name: string; + exclude_user: string; errors?: string | undefined; className?: string; searchAll?: boolean; @@ -22,6 +23,7 @@ interface FacilitySelectProps { export const FacilitySelect = (props: FacilitySelectProps) => { const { name, + exclude_user, multiple, selected, setSelected, @@ -45,6 +47,7 @@ export const FacilitySelect = (props: FacilitySelectProps) => { search_text: text, all: searchAll, facility_type: facilityType, + exclude_user: exclude_user, district, }; diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index 3b55c40fc70..1771a6ca22f 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -706,6 +706,7 @@ function UserFacilities(props: { user: any }) { Date: Fri, 3 Nov 2023 17:50:46 +0530 Subject: [PATCH 09/11] Refactor middleware hostname in Feed component (#6538) --- src/Common/hooks/useMSEplayer.ts | 2 ++ .../Assets/AssetType/ONVIFCamera.tsx | 12 +++++---- .../Facility/Consultations/Feed.tsx | 27 ++++++++++++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/Common/hooks/useMSEplayer.ts b/src/Common/hooks/useMSEplayer.ts index fcbf216ed6a..4d1bb36b9ac 100644 --- a/src/Common/hooks/useMSEplayer.ts +++ b/src/Common/hooks/useMSEplayer.ts @@ -20,6 +20,8 @@ interface UseMSEMediaPlayerOption { export interface ICameraAssetState { id: string; accessKey: string; + middleware_address: string; + location_middleware: string; } export enum StreamStatus { diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx index 4a3e475419e..44d4d372d73 100644 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx @@ -53,6 +53,11 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { } }, [facility, facilityId]); + const fallbackMiddleware = + asset?.location_object?.middleware_address || facilityMiddlewareHostname; + + const currentMiddleware = middlewareHostname || fallbackMiddleware; + useEffect(() => { if (asset) { setAssetType(asset?.asset_class); @@ -105,7 +110,7 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { try { setLoadingAddPreset(true); const presetData = await axios.get( - `https://${facilityMiddlewareHostname}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}` + `https://${currentMiddleware}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}` ); const { res } = await request(routes.createAssetBed, { @@ -136,9 +141,6 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { }; if (isLoading || loading || !facility) return ; - const fallbackMiddleware = - asset?.location_object?.middleware_address || facilityMiddlewareHostname; - return (
{["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && ( @@ -223,7 +225,7 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { addPreset={addPreset} isLoading={loadingAddPreset} refreshPresetsHash={refreshPresetsHash} - facilityMiddlewareHostname={facilityMiddlewareHostname} + facilityMiddlewareHostname={currentMiddleware} /> ) : null}
diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx index 31691c736f4..3448d85a6a2 100644 --- a/src/Components/Facility/Consultations/Feed.tsx +++ b/src/Components/Facility/Consultations/Feed.tsx @@ -48,8 +48,11 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { const [cameraAsset, setCameraAsset] = useState({ id: "", accessKey: "", + middleware_address: "", + location_middleware: "", }); - const [cameraMiddlewareHostname, setCameraMiddlewareHostname] = useState(""); + const [facilityMiddlewareHostname, setFacilityMiddlewareHostname] = + useState(""); const [cameraConfig, setCameraConfig] = useState({}); const [isLoading, setIsLoading] = useState(true); const [bedPresets, setBedPresets] = useState([]); @@ -66,13 +69,19 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { const res = await dispatch(getPermittedFacility(facilityId)); if (res.status === 200 && res.data) { - setCameraMiddlewareHostname(res.data.middleware_address); + setFacilityMiddlewareHostname(res.data.middleware_address); } }; if (facilityId) fetchFacility(); }, [dispatch, facilityId]); + const fallbackMiddleware = + cameraAsset.location_middleware || facilityMiddlewareHostname; + + const currentMiddleware = + cameraAsset.middleware_address || fallbackMiddleware; + useEffect(() => { if (cameraState) { setCameraState({ @@ -130,6 +139,12 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { setCameraAsset({ id: bedAssets.data.results[0].asset_object.id, accessKey: config[2] || "", + middleware_address: + bedAssets.data.results[0].asset_object?.meta + ?.middleware_hostname, + location_middleware: + bedAssets.data.results[0].asset_object.location_object + ?.middleware_address, }); setCameraConfig(bedAssets.data.results[0].meta); setCameraState({ @@ -170,8 +185,8 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { ); const url = !isIOS - ? `wss://${cameraMiddlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0` - : `https://${cameraMiddlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0`; + ? `wss://${currentMiddleware}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0` + : `https://${currentMiddleware}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0`; const { startStream, @@ -182,7 +197,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { : // eslint-disable-next-line react-hooks/rules-of-hooks useMSEMediaPlayer({ config: { - middlewareHostname: cameraMiddlewareHostname, + middlewareHostname: currentMiddleware, ...cameraAsset, }, url, @@ -229,7 +244,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { }); getBedPresets(cameraAsset); } - }, [cameraAsset, cameraMiddlewareHostname]); + }, [cameraAsset, currentMiddleware]); useEffect(() => { let tId: any; From ac405ce4e9c51293d3d0cfb1fc4a6d8322417769 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Fri, 3 Nov 2023 20:33:34 +0530 Subject: [PATCH 10/11] Show camera feed button only for specific roles (#6540) --- .../Facility/ConsultationDetails/index.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 202c9200b0a..6fda874e3b3 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -335,14 +335,17 @@ export const ConsultationDetails = (props: any) => { > Doctor Connect - {patientData.last_consultation?.id && ( - - Camera Feed - - )} + {patientData.last_consultation?.id && + ["DistrictAdmin", "StateAdmin", "Doctor"].includes( + authUser.user_type + ) && ( + + Camera Feed + + )} )} Date: Fri, 3 Nov 2023 21:43:50 +0530 Subject: [PATCH 11/11] add auto deployment for staging gcp deployment (#6519) --- .github/workflows/deploy.yaml | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 06b1cf8eff0..e5b3c768c49 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -180,6 +180,48 @@ jobs: rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache + deploy-staging-gcp: + needs: build-production + name: Deploy to staging GCP cluster + runs-on: ubuntu-latest + environment: + name: Staging-GCP + url: https://care-staging.ohc.network/ + steps: + - name: Checkout Kube Config + uses: actions/checkout@v3 + with: + repository: coronasafe/care-staging-gcp + token: ${{ secrets.GIT_ACCESS_TOKEN }} + path: kube + ref: main + + # Setup gcloud CLI + - uses: google-github-actions/setup-gcloud@94337306dda8180d967a56932ceb4ddcf01edae7 + with: + service_account_key: ${{ secrets.GKE_SA_KEY }} + project_id: ${{ secrets.GKE_PROJECT }} + + # Get the GKE credentials so we can deploy to the cluster + - uses: google-github-actions/get-gke-credentials@fb08709ba27618c31c09e014e1d8364b02e5042e + with: + cluster_name: ${{ secrets.GKE_CLUSTER }} + location: ${{ secrets.GKE_ZONE }} + credentials: ${{ secrets.GKE_SA_KEY }} + + - name: install kubectl + uses: azure/setup-kubectl@v3.0 + with: + version: "v1.23.6" + id: install + + - name: Deploy Care Fe Production + run: | + mkdir -p $HOME/.kube/ + cd kube/deployments/ + sed -i -e "s/_BUILD_NUMBER_/${GITHUB_RUN_NUMBER}/g" care-fe.yaml + kubectl apply -f care-fe.yaml + deploy-production-manipur: needs: build-production name: Deploy to GKE Manipur