diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 4fe94183a2f..40e5259b1c6 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -26,7 +26,7 @@
"cpus": 4
},
"waitFor": "onCreateCommand",
- "postCreateCommand": "npm install",
+ "postCreateCommand": "npm run install-all",
"postAttachCommand": {
"server": "npm run dev"
},
diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml
index 500dbd92be0..0f588568bbc 100644
--- a/.github/workflows/cypress.yaml
+++ b/.github/workflows/cypress.yaml
@@ -70,7 +70,7 @@ jobs:
node-version: "20"
- name: Install dependencies 📦
- run: npm install
+ run: npm run install-all
- name: Build ⚙️
run: npm run build
diff --git a/Dockerfile b/Dockerfile
index 5061a977585..3a96ab3c28a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,6 +13,8 @@ RUN npm install
COPY . .
+RUN npm run setup
+
RUN npm run build
diff --git a/README.md b/README.md
index 7ea8fe1a221..2504bf47fb7 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
diff --git a/cypress/e2e/auth_spec/auth.cy.ts b/cypress/e2e/auth_spec/auth.cy.ts
index a535668ef01..b2bd7b634c0 100644
--- a/cypress/e2e/auth_spec/auth.cy.ts
+++ b/cypress/e2e/auth_spec/auth.cy.ts
@@ -6,6 +6,7 @@ describe("Authorisation/Authentication", () => {
it("Try login as admin with correct password", () => {
cy.loginByApi("devdistrictadmin", "Coronasafe@123");
cy.awaitUrl("/facility");
+ cy.get("#user-profile-name").click();
cy.get("#sign-out-button").contains("Sign Out").click();
cy.url().should("include", "/");
});
diff --git a/cypress/e2e/patient_spec/PatientBedManagement.cy.ts b/cypress/e2e/patient_spec/PatientBedManagement.cy.ts
index d9453806c9f..1964f913a03 100644
--- a/cypress/e2e/patient_spec/PatientBedManagement.cy.ts
+++ b/cypress/e2e/patient_spec/PatientBedManagement.cy.ts
@@ -10,7 +10,7 @@ describe("Patient swtich bed functionality", () => {
const patientConsultationPage = new PatientConsultationPage();
const switchBedOne = "Dummy Bed 4";
const switchBedTwo = "Dummy Bed 1";
- const switchBedThree = "Dummy Bed 3";
+ const switchBedThree = "Dummy Bed 7";
const switchPatientOne = "Dummy Patient 6";
const switchPatientTwo = "Dummy Patient 7";
diff --git a/cypress/e2e/patient_spec/PatientDoctorConnect.cy.ts b/cypress/e2e/patient_spec/PatientDoctorConnect.cy.ts
index 64d47db2cf1..c1362c328c0 100644
--- a/cypress/e2e/patient_spec/PatientDoctorConnect.cy.ts
+++ b/cypress/e2e/patient_spec/PatientDoctorConnect.cy.ts
@@ -8,7 +8,6 @@ describe("Patient Doctor Connect in consultation page", () => {
const doctorconnect = new DoctorConnect();
const patientName = "Dummy Patient 11";
const doctorUser = "Dev Doctor";
- const doctorUserNumber = "+919876543219";
const nurseUser = "Dev Staff";
const teleIcuUser = "Dev Doctor Two";
@@ -37,7 +36,7 @@ describe("Patient Doctor Connect in consultation page", () => {
"#doctor-connect-home-doctor",
doctorUser,
);
- doctorconnect.verifyCopiedContent(doctorUserNumber);
+ doctorconnect.verifyCopiedContent();
// verify the whatsapp and phone number icon presence
doctorconnect.verifyIconVisible("#whatsapp-icon");
doctorconnect.verifyIconVisible("#phone-icon");
diff --git a/cypress/e2e/patient_spec/PatientDoctorNotes.cy.ts b/cypress/e2e/patient_spec/PatientDoctorNotes.cy.ts
index 94300cdb5c6..9a21eaea51f 100644
--- a/cypress/e2e/patient_spec/PatientDoctorNotes.cy.ts
+++ b/cypress/e2e/patient_spec/PatientDoctorNotes.cy.ts
@@ -37,7 +37,7 @@ describe("Patient Discussion notes in the consultation page", () => {
cy.verifyNotification(discussionNotesSuccessMessage);
cy.closeNotification();
// verify the auto-switching of tab to nurse notes if the user is a nurse
- cy.get("#sign-out-button").contains("Sign Out").click();
+ patientDoctorNotes.signout();
loginPage.loginManuallyAsNurse();
loginPage.ensureLoggedIn();
cy.visit("/patients");
diff --git a/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts
index 7faaeed5a9f..3907784b4b7 100644
--- a/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts
+++ b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts
@@ -302,9 +302,9 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => {
patientRhythm,
]);
patientLogupdate.clickUpdateDetail();
- patientLogupdate.clickClearButtonInElement("#systolic");
+ patientLogupdate.clearIntoElementById("#systolic");
patientLogupdate.typeSystolic(patientModifiedSystolic);
- patientLogupdate.clickClearButtonInElement("#diastolic");
+ patientLogupdate.clearIntoElementById("#diastolic");
patientLogupdate.typeDiastolic(patientModifiedDiastolic);
cy.submitButton("Continue");
cy.verifyNotification("Brief Update updated successfully");
diff --git a/cypress/e2e/users_spec/UsersCreation.cy.ts b/cypress/e2e/users_spec/UsersCreation.cy.ts
index 0f592318888..2c5797fefa7 100644
--- a/cypress/e2e/users_spec/UsersCreation.cy.ts
+++ b/cypress/e2e/users_spec/UsersCreation.cy.ts
@@ -67,6 +67,7 @@ describe("User Creation", () => {
it("Update the existing user profile and verify its reflection", () => {
userCreationPage.clickElementById("user-profile-name");
+ userCreationPage.clickElementById("profile-button");
userCreationPage.verifyElementContainsText(
"username-profile-details",
"devdistrictadmin",
@@ -129,6 +130,7 @@ describe("User Creation", () => {
it("Update the existing user profile Form Mandatory File Error", () => {
userCreationPage.clickElementById("user-profile-name");
+ userCreationPage.clickElementById("profile-button");
userCreationPage.clickElementById("edit-cancel-profile-button");
userCreationPage.clearIntoElementById("firstName");
userCreationPage.clearIntoElementById("lastName");
diff --git a/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts
index d6991e82ff0..f0b2f1b74eb 100644
--- a/cypress/pageobject/Asset/AssetCreation.ts
+++ b/cypress/pageobject/Asset/AssetCreation.ts
@@ -154,7 +154,7 @@ export class AssetPage {
}
clickConfigureAsset() {
- cy.get("#submit").contains("Set Configuration").click();
+ cy.get("#submit").contains("Update").click();
}
clickConfigureVital() {
diff --git a/cypress/pageobject/Facility/FacilityHome.ts b/cypress/pageobject/Facility/FacilityHome.ts
index e021171ff0a..dea7de0e7b6 100644
--- a/cypress/pageobject/Facility/FacilityHome.ts
+++ b/cypress/pageobject/Facility/FacilityHome.ts
@@ -72,7 +72,7 @@ class FacilityHome {
}
verifyOccupancyBadgeVisibility() {
- cy.get("#occupany-badge").should("be.visible");
+ cy.get('[data-test-id="occupancy-badge"]').should("be.visible");
}
verifyAndCloseNotifyModal() {
diff --git a/cypress/pageobject/Login/LoginPage.ts b/cypress/pageobject/Login/LoginPage.ts
index 7ea94d54737..06bd165c9ac 100644
--- a/cypress/pageobject/Login/LoginPage.ts
+++ b/cypress/pageobject/Login/LoginPage.ts
@@ -30,6 +30,8 @@ class LoginPage {
}
ensureLoggedIn(): void {
+ cy.get("#user-profile-name").click();
+ cy.get("#sign-out-button").scrollIntoView();
cy.get("#sign-out-button").contains("Sign Out").should("exist");
}
}
diff --git a/cypress/pageobject/Patient/PatientDoctorConnect.ts b/cypress/pageobject/Patient/PatientDoctorConnect.ts
index b8c33bb4b24..9933c3a1dec 100644
--- a/cypress/pageobject/Patient/PatientDoctorConnect.ts
+++ b/cypress/pageobject/Patient/PatientDoctorConnect.ts
@@ -10,8 +10,8 @@ export class DoctorConnect {
});
}
- verifyCopiedContent(text: string) {
- cy.get("@clipboardStub").should("be.calledWith", text);
+ verifyCopiedContent() {
+ cy.get("@clipboardStub").should("be.calledWithMatch", /^\+91\d{10}$/);
}
verifyIconVisible(selector: string) {
diff --git a/cypress/pageobject/Patient/PatientDoctorNotes.ts b/cypress/pageobject/Patient/PatientDoctorNotes.ts
index 9538b0eed3b..157f35d47d9 100644
--- a/cypress/pageobject/Patient/PatientDoctorNotes.ts
+++ b/cypress/pageobject/Patient/PatientDoctorNotes.ts
@@ -26,4 +26,10 @@ export class PatientDoctorNotes {
.its("response.statusCode")
.should("eq", 201);
}
+
+ signout() {
+ cy.get("#user-profile-name").click();
+ cy.get("#sign-out-button").scrollIntoView();
+ cy.get("#sign-out-button").contains("Sign Out").click();
+ }
}
diff --git a/cypress/pageobject/Patient/PatientLogupdate.ts b/cypress/pageobject/Patient/PatientLogupdate.ts
index 857fe7dd972..d7b49fde05e 100644
--- a/cypress/pageobject/Patient/PatientLogupdate.ts
+++ b/cypress/pageobject/Patient/PatientLogupdate.ts
@@ -43,11 +43,11 @@ class PatientLogupdate {
}
typeSystolic(systolic: string) {
- cy.typeAndSelectOption("#systolic", systolic);
+ cy.get("#systolic").click().type(systolic);
}
typeDiastolic(diastolic: string) {
- cy.typeAndSelectOption("#diastolic", diastolic);
+ cy.get("#diastolic").click().type(diastolic);
}
typePulse(pulse: string) {
@@ -55,7 +55,7 @@ class PatientLogupdate {
}
typeTemperature(temperature: string) {
- cy.typeAndSelectOption("#temperature", temperature);
+ cy.get("#temperature").click().type(temperature);
}
typeRespiratory(respiratory: string) {
@@ -93,8 +93,8 @@ class PatientLogupdate {
cy.wait(3000);
}
- clickClearButtonInElement(elementId: string) {
- cy.get(elementId).find("#clear-button").click();
+ clearIntoElementById(elementId) {
+ cy.get(elementId).click().clear();
}
clickVitals() {
diff --git a/cypress/pageobject/Users/ManageUserPage.ts b/cypress/pageobject/Users/ManageUserPage.ts
index efa0d90142c..470862693a8 100644
--- a/cypress/pageobject/Users/ManageUserPage.ts
+++ b/cypress/pageobject/Users/ManageUserPage.ts
@@ -75,6 +75,7 @@ export class ManageUserPage {
navigateToProfile() {
cy.intercept("GET", "**/api/v1/users/**").as("getUsers");
cy.get("#user-profile-name").click();
+ cy.get("#profile-button").click();
cy.wait("@getUsers").its("response.statusCode").should("eq", 200);
}
diff --git a/package-lock.json b/package-lock.json
index 732d56802f5..9215300570b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,9 +19,11 @@
"@hello-pangea/dnd": "^17.0.0",
"@pnotify/core": "^5.2.0",
"@pnotify/mobile": "^5.2.0",
+ "@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.2",
+ "@radix-ui/react-tooltip": "^1.1.3",
"@sentry/browser": "^8.33.0",
"@yudiel/react-qr-scanner": "^2.0.8",
"axios": "^1.7.7",
@@ -3059,6 +3061,28 @@
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
},
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
+ "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
@@ -3126,6 +3150,20 @@
}
}
},
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+ "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
@@ -3152,6 +3190,72 @@
}
}
},
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz",
+ "integrity": "sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-menu": "2.1.2",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
+ "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
+ "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-icons": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
@@ -3160,6 +3264,107 @@
"react": "^16.x || ^17.x || ^18.x"
}
},
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
+ "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz",
+ "integrity": "sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-collection": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.1",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.0",
+ "@radix-ui/react-portal": "1.1.2",
+ "@radix-ui/react-presence": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-roving-focus": "1.1.0",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.6.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
+ "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-rect": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0",
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-portal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
@@ -3228,6 +3433,50 @@
}
}
},
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
+ "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-collection": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
@@ -3278,6 +3527,39 @@
}
}
},
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.3.tgz",
+ "integrity": "sha512-Z4w1FIS0BqVFI2c1jZvb/uDVJijJjJ2ZMuPV81oVgTZ7g3BZxobplnMVvXtFWgtozdvYJ+MFWtwkM5S2HnAong==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.0",
+ "@radix-ui/react-portal": "1.1.2",
+ "@radix-ui/react-presence": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-visually-hidden": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
@@ -3340,6 +3622,40 @@
}
}
},
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
+ "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
+ "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
@@ -3362,6 +3678,11 @@
}
}
},
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
+ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
+ },
"node_modules/@react-aria/focus": {
"version": "3.18.4",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz",
@@ -5210,6 +5531,17 @@
"node": ">=14.0.0"
}
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
+ "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/array-buffer-byte-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
@@ -6671,6 +7003,11 @@
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
"node_modules/detective-amd": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-6.0.0.tgz",
@@ -8343,6 +8680,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-own-enumerable-property-symbols": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
@@ -9163,6 +9508,14 @@
"node": ">= 0.10"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
@@ -14576,6 +14929,73 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
+ "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.6",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
+ "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
+ "dependencies": {
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
+ "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "invariant": "^2.2.4",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-webcam": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-webcam/-/react-webcam-7.2.0.tgz",
@@ -17440,6 +17860,26 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
+ "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-keyboard-shortcut": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.1.6.tgz",
@@ -17457,6 +17897,27 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/use-sidecar": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
+ "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
diff --git a/package.json b/package.json
index 7749374be44..57d0c3d3405 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,8 @@
"prepare": "husky install",
"lint": "eslint ./src",
"lint-fix": "eslint ./src --fix",
- "format": "prettier ./src --write"
+ "format": "prettier ./src --write",
+ "sort-locales": "node ./scripts/sort-locales.js"
},
"dependencies": {
"@fontsource/figtree": "^5.1.0",
@@ -57,9 +58,11 @@
"@hello-pangea/dnd": "^17.0.0",
"@pnotify/core": "^5.2.0",
"@pnotify/mobile": "^5.2.0",
+ "@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.2",
+ "@radix-ui/react-tooltip": "^1.1.3",
"@sentry/browser": "^8.33.0",
"@yudiel/react-qr-scanner": "^2.0.8",
"axios": "^1.7.7",
@@ -161,10 +164,13 @@
"prettier --write --ignore-unknown --plugin prettier-plugin-tailwindcss",
"eslint --fix",
"git update-index --again"
+ ],
+ "src/Locale/*.json": [
+ "npm run sort-locales"
]
},
"engines": {
"node": ">=20.12.0"
},
"packageManager": "npm@10.5.0"
-}
+}
\ No newline at end of file
diff --git a/public/favicon.ico b/public/favicon.ico
index 67a5686e32f..51fe5064a91 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/scripts/sort-locales.js b/scripts/sort-locales.js
new file mode 100644
index 00000000000..adff586a93b
--- /dev/null
+++ b/scripts/sort-locales.js
@@ -0,0 +1,15 @@
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const fs = require("fs");
+
+const file = "src/Locale/en.json";
+
+const data = JSON.parse(fs.readFileSync(file, "utf8"));
+
+const sortedData = Object.keys(data)
+ .sort()
+ .reduce((acc, key) => {
+ acc[key] = data[key];
+ return acc;
+ }, {});
+
+fs.writeFileSync(file, JSON.stringify(sortedData, null, 2) + "\n");
diff --git a/src/CAREUI/display/Count.tsx b/src/CAREUI/display/Count.tsx
index 997b58794eb..6b28ca4f962 100644
--- a/src/CAREUI/display/Count.tsx
+++ b/src/CAREUI/display/Count.tsx
@@ -11,15 +11,13 @@ interface Props {
export default function CountBlock(props: Props) {
return (
-
-
-
+
+
+
-
-
+
-
{props.text}
{props.loading ? (
diff --git a/src/CAREUI/icons/UniconPaths.json b/src/CAREUI/icons/UniconPaths.json
index 1a76c51570d..64f97d72d65 100644
--- a/src/CAREUI/icons/UniconPaths.json
+++ b/src/CAREUI/icons/UniconPaths.json
@@ -247,6 +247,10 @@
24,
"M7,11H17a1,1,0,0,0,0-2H13V5.41l.79.8a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42l-2.5-2.5a1,1,0,0,0-.33-.21,1,1,0,0,0-.76,0,1,1,0,0,0-.33.21l-2.5,2.5a1,1,0,0,0,1.42,1.42l.79-.8V9H7a1,1,0,0,0,0,2Zm10,2H7a1,1,0,0,0,0,2h4v3.59l-.79-.8a1,1,0,0,0-1.42,1.42l2.5,2.5a1,1,0,0,0,.33.21.94.94,0,0,0,.76,0,1,1,0,0,0,.33-.21l2.5-2.5a1,1,0,0,0-1.42-1.42l-.79.8V15h4a1,1,0,0,0,0-2Z"
],
+ "l-arrow-bar-right": [
+ 24,
+ "M8.4 12c0 .4.3.7.7.7h8.3l-3.1 3.1c-.3.3-.3.7 0 1s.7.3 1 0l4.3-4.3c.3-.3.3-.7 0-1 0 0 0 0 0 0l-4.3-4.3c-.3-.3-.7-.3-1 0s-.3.7 0 1l3.1 3.1h-8.3c-.4 0-.7.3-.7.7M4.9 22c-.4 0-.7-.3-.7-.7V2.7c0-.4.3-.7.7-.7s.7.3.7.7v18.6c0 .4-.3.7-.7.7"
+ ],
"l-arrow-circle-down": [
24,
"M11.29,15.71a1,1,0,0,0,.33.21,1,1,0,0,0,.76,0,1,1,0,0,0,.33-.21l3-3a1,1,0,0,0-1.42-1.42L13,12.59V9a1,1,0,0,0-2,0v3.59l-1.29-1.3a1,1,0,0,0-1.42,0,1,1,0,0,0,0,1.42ZM12,22A10,10,0,1,0,2,12,10,10,0,0,0,12,22ZM12,4a8,8,0,1,1-8,8A8,8,0,0,1,12,4Z"
@@ -2795,6 +2799,14 @@
24,
"M2.5,10.56l9,5.2a1,1,0,0,0,1,0l9-5.2a1,1,0,0,0,0-1.73l-9-5.2a1,1,0,0,0-1,0l-9,5.2a1,1,0,0,0,0,1.73ZM12,5.65l7,4-7,4.05L5,9.69Zm8.5,7.79L12,18.35,3.5,13.44a1,1,0,0,0-1.37.36,1,1,0,0,0,.37,1.37l9,5.2a1,1,0,0,0,1,0l9-5.2a1,1,0,0,0,.37-1.37A1,1,0,0,0,20.5,13.44Z"
],
+ "l-layout-sidebar": [
+ 24,
+ "M0 4.5C0 2.8 1.3 1.5 3 1.5h18c1.7 0 3 1.3 3 3v15c0 1.7-1.3 3-3 3H3c-1.7 0-3-1.3-3-3V4.5ZM7.5 3v18h13.5c.8 0 1.5-.7 1.5-1.5V4.5c0-.8-.7-1.5-1.5-1.5H7.5ZM6 3h-3c-.8 0-1.5.7-1.5 1.5v15c0 .8.7 1.5 1.5 1.5h3V3Z"
+ ],
+ "l-layout-sidebar-alt": [
+ 24,
+ "M21 1.5H3C1.3 1.5 0 2.8 0 4.5v15c0 1.7 1.3 3 3 3h18c1.7 0 3-1.3 3-3V4.5c0-1.7-1.3-3-3-3ZM22.5 19.5c0 .8-.7 1.5-1.5 1.5H3c-.8 0-1.5-.7-1.5-1.5V4.5c0-.8.7-1.5 1.5-1.5h18c.8 0 1.5.7 1.5 1.5v15ZM10.5 6v12c0 .8-.7 1.5-1.5 1.5h-3c-.8 0-1.5-.7-1.5-1.5V6c0-.8.7-1.5 1.5-1.5h3c.8 0 1.5.7 1.5 1.5Z"
+ ],
"l-left-arrow-from-left": [
24,
"M17,11H5.41l2.3-2.29A1,1,0,1,0,6.29,7.29l-4,4a1,1,0,0,0-.21.33,1,1,0,0,0,0,.76,1,1,0,0,0,.21.33l4,4a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42L5.41,13H17a1,1,0,0,0,0-2Zm4-7a1,1,0,0,0-1,1V19a1,1,0,0,0,2,0V5A1,1,0,0,0,21,4Z"
diff --git a/src/CAREUI/interactive/KeyboardShortcut.tsx b/src/CAREUI/interactive/KeyboardShortcut.tsx
index 1d2bebeb316..4ad46c99387 100644
--- a/src/CAREUI/interactive/KeyboardShortcut.tsx
+++ b/src/CAREUI/interactive/KeyboardShortcut.tsx
@@ -31,7 +31,10 @@ export default function KeyboardShortcut(props: Props) {
)}
{(props.altShortcuts || [props.shortcut]).map((shortcut, idx, arr) => (
<>
-
+
{shortcut.map((key, idx, keys) => (
<>
{SHORTCUT_KEY_MAP[key] || key}
@@ -42,7 +45,12 @@ export default function KeyboardShortcut(props: Props) {
))}
{idx !== arr.length - 1 && (
- or
+
+ or
+
)}
>
))}
diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx
index d46473bfd2d..08d21dc1edc 100644
--- a/src/Common/constants.tsx
+++ b/src/Common/constants.tsx
@@ -839,20 +839,6 @@ export const LOCATION_BED_TYPES = [
{ id: "REGULAR", name: "Regular" },
] as const;
-export const ASSET_META_TYPE = [
- { id: "CAMERA", text: "Camera(ONVIF)" },
- { id: "HL7MONITOR", text: "Vitals Monitor(HL7)" },
-];
-
-export const CAMERA_TYPE = [
- { id: "HIKVISION", text: "ONVIF Camera (HIKVISION)" },
-];
-
-export const GENDER: { [key: number]: string } = GENDER_TYPES.reduce(
- (acc, curr) => ({ ...acc, [curr.id]: curr.text }),
- {},
-);
-
export type CameraPTZ = {
icon?: IconName;
label: string;
diff --git a/src/Components/Assets/AssetConfigure.tsx b/src/Components/Assets/AssetConfigure.tsx
index aa6b7c9221e..9f5f73f296d 100644
--- a/src/Components/Assets/AssetConfigure.tsx
+++ b/src/Components/Assets/AssetConfigure.tsx
@@ -1,6 +1,6 @@
import Loading from "../Common/Loading";
import HL7Monitor from "./AssetType/HL7Monitor";
-import ONVIFCamera from "./AssetType/ONVIFCamera";
+import ConfigureCamera from "../CameraFeed/ConfigureCamera";
import Page from "../Common/components/Page";
import useQuery from "../../Utils/request/useQuery";
import routes from "../../Redux/api";
@@ -11,13 +11,11 @@ interface AssetConfigureProps {
}
const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => {
- const {
- data: asset,
- loading,
- refetch,
- } = useQuery(routes.getAsset, { pathParams: { external_id: assetId } });
+ const { data: asset, refetch } = useQuery(routes.getAsset, {
+ pathParams: { external_id: assetId },
+ });
- if (loading || !asset) {
+ if (!asset) {
return ;
}
@@ -63,12 +61,7 @@ const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => {
}}
backUrl={`/facility/${facilityId}/assets/${assetId}`}
>
- refetch()}
- />
+ refetch()} />
);
};
diff --git a/src/Components/Assets/AssetType/HL7Monitor.tsx b/src/Components/Assets/AssetType/HL7Monitor.tsx
index 6583157018d..383af38fa28 100644
--- a/src/Components/Assets/AssetType/HL7Monitor.tsx
+++ b/src/Components/Assets/AssetType/HL7Monitor.tsx
@@ -1,7 +1,6 @@
import { SyntheticEvent, useEffect, useState } from "react";
-import { AssetData, ResolvedMiddleware } from "../AssetTypes";
+import { AssetClass, AssetData, ResolvedMiddleware } from "../AssetTypes";
import * as Notification from "../../../Utils/Notifications.js";
-import MonitorConfigure from "../configure/MonitorConfigure";
import Loading from "../../Common/Loading";
import { checkIfValidIP } from "../../../Common/validation";
import Card from "../../../CAREUI/display/Card";
@@ -13,6 +12,10 @@ import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatien
import useAuthUser from "../../../Common/hooks/useAuthUser";
import request from "../../../Utils/request/request";
import routes from "../../../Redux/api";
+import { BedModel } from "../../Facility/models";
+import useQuery from "../../../Utils/request/useQuery";
+import { FieldLabel } from "../../Form/FormFields/FormField";
+import { BedSelect } from "../../Common/BedSelect";
interface HL7MonitorProps {
assetId: string;
@@ -151,3 +154,79 @@ const HL7Monitor = (props: HL7MonitorProps) => {
);
};
export default HL7Monitor;
+
+const saveLink = async (assetId: string, bedId: string) => {
+ await request(routes.createAssetBed, {
+ body: {
+ asset: assetId,
+ bed: bedId,
+ },
+ });
+ Notification.Success({ msg: "AssetBed Link created successfully" });
+};
+const updateLink = async (
+ assetbedId: string,
+ assetId: string,
+ bed: BedModel,
+) => {
+ await request(routes.partialUpdateAssetBed, {
+ pathParams: { external_id: assetbedId },
+ body: {
+ asset: assetId,
+ bed: bed.id ?? "",
+ },
+ });
+ Notification.Success({ msg: "AssetBed Link updated successfully" });
+};
+
+function MonitorConfigure({ asset }: { asset: AssetData }) {
+ const [bed, setBed] = useState({});
+ const [shouldUpdateLink, setShouldUpdateLink] = useState(false);
+ const { data: assetBed } = useQuery(routes.listAssetBeds, {
+ query: { asset: asset.id },
+ onResponse: ({ res, data }) => {
+ if (res?.status === 200 && data && data.results.length > 0) {
+ setBed(data.results[0].bed_object);
+ setShouldUpdateLink(true);
+ }
+ },
+ });
+
+ return (
+
+ );
+}
diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx
deleted file mode 100644
index f79894d089f..00000000000
--- a/src/Components/Assets/AssetType/ONVIFCamera.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import { useEffect, useState } from "react";
-import { AssetData, ResolvedMiddleware } from "../AssetTypes";
-import * as Notification from "../../../Utils/Notifications.js";
-import { BedModel } from "../../Facility/models";
-import { getCameraConfig } from "../../../Utils/transformUtils";
-import CameraConfigure from "../configure/CameraConfigure";
-import Loading from "../../Common/Loading";
-import { checkIfValidIP } from "../../../Common/validation";
-import TextFormField from "../../Form/FormFields/TextFormField";
-import { Submit } from "../../Common/components/ButtonV2";
-import { SyntheticEvent } from "react";
-import useAuthUser from "../../../Common/hooks/useAuthUser";
-
-import request from "../../../Utils/request/request";
-import routes from "../../../Redux/api";
-import useQuery from "../../../Utils/request/useQuery";
-
-import CareIcon from "../../../CAREUI/icons/CareIcon";
-import useOperateCamera, {
- PTZPayload,
-} from "../../CameraFeed/useOperateCamera";
-
-interface Props {
- assetId: string;
- facilityId: string;
- asset: any;
- onUpdated?: () => void;
-}
-
-const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => {
- const [isLoading, setIsLoading] = useState(true);
- const [assetType, setAssetType] = useState("");
- const [middlewareHostname, setMiddlewareHostname] = useState("");
- const [resolvedMiddleware, setResolvedMiddleware] =
- useState();
- const [cameraAddress, setCameraAddress] = useState("");
- const [ipadrdress_error, setIpAddress_error] = useState("");
- const [username, setUsername] = useState("");
- const [password, setPassword] = useState("");
- const [streamUuid, setStreamUuid] = useState("");
- const [bed, setBed] = useState({});
- const [newPreset, setNewPreset] = useState("");
- const [loadingAddPreset, setLoadingAddPreset] = useState(false);
- const [loadingSetConfiguration, setLoadingSetConfiguration] = useState(false);
- const [refreshPresetsHash, setRefreshPresetsHash] = useState(
- Number(new Date()),
- );
- const { data: facility, loading } = useQuery(routes.getPermittedFacility, {
- pathParams: { id: facilityId },
- });
- const authUser = useAuthUser();
-
- const { operate } = useOperateCamera(assetId ?? "", true);
-
- useEffect(() => {
- if (asset) {
- setAssetType(asset?.asset_class);
- setResolvedMiddleware(asset?.resolved_middleware);
- const cameraConfig = getCameraConfig(asset);
- setMiddlewareHostname(cameraConfig.middleware_hostname);
- setCameraAddress(cameraConfig.hostname);
- setUsername(cameraConfig.username);
- setPassword(cameraConfig.password);
- setStreamUuid(cameraConfig.accessKey);
- }
- setIsLoading(false);
- }, [asset]);
-
- const handleSubmit = async (e: SyntheticEvent) => {
- e.preventDefault();
- if (checkIfValidIP(cameraAddress)) {
- setLoadingSetConfiguration(true);
- setIpAddress_error("");
- const data = {
- meta: {
- asset_type: "CAMERA",
- middleware_hostname: middlewareHostname,
- local_ip_address: cameraAddress,
- camera_access_key: `${username}:${password}:${streamUuid}`,
- },
- };
- const { res } = await request(routes.partialUpdateAsset, {
- pathParams: { external_id: assetId },
- body: data,
- });
- if (res?.status === 200) {
- Notification.Success({ msg: "Asset Configured Successfully" });
- onUpdated?.();
- } else {
- Notification.Error({ msg: "Something went wrong!" });
- }
- setLoadingSetConfiguration(false);
- } else {
- setIpAddress_error("IP address is invalid");
- }
- };
-
- const addPreset = async (e: SyntheticEvent) => {
- e.preventDefault();
- const meta = {
- bed_id: bed.id,
- preset_name: newPreset,
- };
- try {
- setLoadingAddPreset(true);
-
- const { data } = await operate({ type: "get_status" });
- const { position } = (data as { result: { position: PTZPayload } })
- .result;
-
- const { res } = await request(routes.createAssetBed, {
- body: {
- meta: { ...meta, position },
- asset: assetId,
- bed: bed?.id as string,
- },
- });
- if (res?.status === 201) {
- Notification.Success({
- msg: "Preset Added Successfully",
- });
- setBed({});
- setNewPreset("");
- setRefreshPresetsHash(Number(new Date()));
- } else {
- Notification.Error({
- msg: "Something went wrong..!",
- });
- }
- } catch (e) {
- Notification.Error({
- msg: "Something went wrong..!",
- });
- }
- setLoadingAddPreset(false);
- };
- if (isLoading || loading || !facility) return ;
-
- return (
-
- {["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && (
-
-
-
-
-
- )}
-
- {assetType === "ONVIF" ? (
-
- ) : null}
-
- );
-};
-export default ONVIFCamera;
diff --git a/src/Components/Assets/AssetTypes.tsx b/src/Components/Assets/AssetTypes.tsx
index e3dee31bb7f..2f8e086a813 100644
--- a/src/Components/Assets/AssetTypes.tsx
+++ b/src/Components/Assets/AssetTypes.tsx
@@ -109,6 +109,9 @@ export interface AssetData {
latest_status: string;
last_service: AssetService;
meta?: {
+ middleware_hostname?: string;
+ local_ip_address?: string;
+ camera_access_key?: string;
[key: string]: any;
};
}
diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx
deleted file mode 100644
index e5e017db8dc..00000000000
--- a/src/Components/Assets/configure/CameraConfigure.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { SyntheticEvent } from "react";
-import { AssetData } from "../AssetTypes";
-import CameraFeedOld from "../../CameraFeed/CameraFeedOld";
-import { BedSelect } from "../../Common/BedSelect";
-import { BedModel } from "../../Facility/models";
-import { getCameraConfig } from "../../../Utils/transformUtils";
-import { Submit } from "../../Common/components/ButtonV2";
-import TextFormField from "../../Form/FormFields/TextFormField";
-import Card from "../../../CAREUI/display/Card";
-import { FieldErrorText } from "../../Form/FormFields/FormField";
-
-interface CameraConfigureProps {
- asset: AssetData;
- addPreset(e: SyntheticEvent): void;
- setBed(bed: BedModel): void;
- bed: BedModel;
- newPreset: string;
- setNewPreset(preset: string): void;
- refreshPresetsHash: number;
- facilityMiddlewareHostname: string;
- isLoading: boolean;
-}
-export default function CameraConfigure(props: CameraConfigureProps) {
- const {
- asset,
- addPreset,
- setBed,
- bed,
- isLoading,
- newPreset,
- setNewPreset,
- refreshPresetsHash,
- facilityMiddlewareHostname,
- } = props;
-
- return (
-
- );
-}
diff --git a/src/Components/Assets/configure/MonitorConfigure.tsx b/src/Components/Assets/configure/MonitorConfigure.tsx
deleted file mode 100644
index 785b82873de..00000000000
--- a/src/Components/Assets/configure/MonitorConfigure.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import { useState } from "react";
-import { BedSelect } from "../../Common/BedSelect";
-import { BedModel } from "../../Facility/models";
-import { AssetClass, AssetData } from "../AssetTypes";
-import * as Notification from "../../../Utils/Notifications.js";
-import { Submit } from "../../Common/components/ButtonV2";
-import { FieldLabel } from "../../Form/FormFields/FormField";
-import request from "../../../Utils/request/request";
-import routes from "../../../Redux/api";
-import useQuery from "../../../Utils/request/useQuery";
-import CareIcon from "../../../CAREUI/icons/CareIcon";
-
-const saveLink = async (assetId: string, bedId: string) => {
- await request(routes.createAssetBed, {
- body: {
- asset: assetId,
- bed: bedId,
- },
- });
- Notification.Success({ msg: "AssetBed Link created successfully" });
-};
-const update_Link = async (
- assetbedId: string,
- assetId: string,
- bed: BedModel,
-) => {
- await request(routes.partialUpdateAssetBed, {
- pathParams: { external_id: assetbedId },
- body: {
- asset: assetId,
- bed: bed.id ?? "",
- },
- });
- Notification.Success({ msg: "AssetBed Link updated successfully" });
-};
-
-export default function MonitorConfigure({ asset }: { asset: AssetData }) {
- const [bed, setBed] = useState({});
- const [updateLink, setUpdateLink] = useState(false);
- const { data: assetBed } = useQuery(routes.listAssetBeds, {
- query: { asset: asset.id },
- onResponse: ({ res, data }) => {
- if (res?.status === 200 && data && data.results.length > 0) {
- setBed(data.results[0].bed_object);
- setUpdateLink(true);
- }
- },
- });
-
- return (
-
- );
-}
diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx
index e49a63f7028..3c9b25b6fc9 100644
--- a/src/Components/CameraFeed/CameraFeed.tsx
+++ b/src/Components/CameraFeed/CameraFeed.tsx
@@ -78,7 +78,7 @@ export default function CameraFeed(props: Props) {
.operate({ type: "get_stream_token" })
.then(({ res, data }) => {
if (res?.status != 200) {
- setState("authentication_error");
+ setState("host_unreachable");
return props.onStreamError?.();
}
const result = data?.result as { token: string };
@@ -183,13 +183,15 @@ export default function CameraFeed(props: Props) {
{props.children}
-
- {props.asset.name}
-
-
+
+
+ {props.asset.name}
+
+
+
{!isIOS && (
);
- case "authentication_error":
- return (
-
- );
case "offline":
return (
{
- const { cameraPTZ } = props;
- return (
-
- );
-};
-
-const CameraFeedOld = (props: any) => {
- const middlewareHostname = props.middlewareHostname;
- const [presetsPage, setPresetsPage] = useState(0);
- const cameraAsset = props.asset;
- const [presets, setPresets] = useState([]);
- const [bedPresets, setBedPresets] = useState([]);
- const [showDefaultPresets, setShowDefaultPresets] = useState(false);
- const [precision, setPrecision] = useState(1);
- const [streamStatus, setStreamStatus] = useState(
- StreamStatus.Offline,
- );
- const [videoStartTime, setVideoStartTime] = useState(null);
- const [bed, setBed] = useState({});
- const [presetName, setPresetName] = useState("");
- const [loading, setLoading] = useState();
- const dispatch: any = useDispatch();
- const [page, setPage] = useState({
- count: 0,
- limit: 8,
- offset: 0,
- });
- const [toDelete, setToDelete] = useState(null);
- const [toUpdate, setToUpdate] = useState(null);
- const [_isFullscreen, setFullscreen] = useFullscreen();
-
- const { width } = useWindowDimensions();
- const extremeSmallScreenBreakpoint = 320;
- const isExtremeSmallScreen =
- width <= extremeSmallScreenBreakpoint ? true : false;
- const liveFeedPlayerRef = useRef(null);
- const [streamUrl, setStreamUrl] = useState("");
-
- const refreshPresetsHash = props.refreshPresetsHash;
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [currentPreset, setCurrentPreset] = useState();
- const {
- absoluteMove,
- getCameraStatus,
- getStreamToken,
- getPTZPayload,
- getPresets,
- gotoPreset,
- relativeMove,
- } = useFeedPTZ({
- config: {
- middlewareHostname,
- ...cameraAsset,
- },
- dispatch,
- });
-
- const fetchCameraPresets = () =>
- getPresets({
- onSuccess: (resp) => {
- setPresets(resp);
- },
- onError: (resp) => {
- resp instanceof AxiosError &&
- Notification.Error({
- msg: "Camera is offline",
- });
- },
- });
-
- const calculateVideoLiveDelay = () => {
- const video = liveFeedPlayerRef.current as HTMLVideoElement;
- if (!video || !videoStartTime) return 0;
-
- const timeDifference =
- (new Date().getTime() - videoStartTime.getTime()) / 1000;
-
- return timeDifference - video.currentTime;
- };
-
- const getBedPresets = async (id: any) => {
- const bedAssets = await dispatch(
- listAssetBeds({
- asset: id,
- limit: page.limit,
- offset: page.offset,
- }),
- );
- setBedPresets(bedAssets?.data?.results);
- setPage({
- ...page,
- count: bedAssets?.data?.count,
- });
- };
-
- const deletePreset = async (id: any) => {
- const res = await dispatch(deleteAssetBed(id));
- if (res?.status === 204) {
- Notification.Success({ msg: "Preset deleted successfully" });
- getBedPresets(cameraAsset.id);
- } else {
- Notification.Error({
- msg: "Error while deleting Preset: " + (res?.data?.detail || ""),
- });
- }
- setToDelete(null);
- };
-
- const updatePreset = async (currentPreset: any) => {
- const data = {
- bed_id: bed.id,
- preset_name: presetName,
- };
- const response = await dispatch(
- partialUpdateAssetBed(
- {
- asset: currentPreset.asset_object.id,
- bed: bed.id,
- meta: {
- ...currentPreset.meta,
- ...data,
- },
- },
- currentPreset?.id,
- ),
- );
- if (response && response.status === 200) {
- Notification.Success({ msg: "Preset Updated" });
- } else {
- Notification.Error({ msg: "Something Went Wrong" });
- }
- getBedPresets(cameraAsset?.id);
- fetchCameraPresets();
- setToUpdate(null);
- };
-
- const gotoBedPreset = (preset: any) => {
- setLoading("Moving");
- absoluteMove(preset.meta.position, {
- onSuccess: () => setLoading(undefined),
- });
- };
-
- useEffect(() => {
- if (cameraAsset?.hostname) {
- fetchCameraPresets();
- setTimeout(() => {
- startStreamFeed();
- }, 1000);
- }
- }, []);
-
- useEffect(() => {
- setPresetName(toUpdate?.meta?.preset_name);
- setBed(toUpdate?.bed_object);
- }, [toUpdate]);
-
- useEffect(() => {
- getBedPresets(cameraAsset.id);
- if (bedPresets?.[0]?.position) {
- absoluteMove(bedPresets[0]?.position, {});
- }
- }, [page.offset, cameraAsset.id, refreshPresetsHash]);
-
- const startStreamFeed = useCallback(async () => {
- if (!liveFeedPlayerRef.current) return;
-
- await getStreamToken({
- onSuccess: (data) => {
- setStreamUrl(
- `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0&token=${data.token}`,
- );
- },
- onError: () => {
- setStreamStatus(StreamStatus.Offline);
- },
- });
- }, [liveFeedPlayerRef.current]);
-
- const viewOptions = (page: number) => {
- return presets
- ? Object.entries(presets)
- .map(([key, value]) => ({ label: key, value }))
- .slice(page, page + 10)
- : Array.from(Array(10), (_, i) => ({
- label: "Monitor " + (i + 1),
- value: i + 1,
- }));
- };
- useEffect(() => {
- let tId: any;
- if (streamStatus !== StreamStatus.Playing) {
- setStreamStatus(StreamStatus.Loading);
- tId = setTimeout(() => {
- startStreamFeed();
- }, 5000);
- }
-
- return () => {
- clearTimeout(tId);
- };
- }, [startStreamFeed, streamStatus]);
-
- const handlePagination = (cOffset: number) => {
- setPage({
- ...page,
- offset: cOffset,
- });
- };
-
- const cameraPTZActionCBs: { [key: string]: (option: any) => void } = {
- precision: () => {
- setPrecision((precision: number) =>
- precision === 16 ? 1 : precision * 2,
- );
- },
- reset: async () => {
- setStreamStatus(StreamStatus.Loading);
- setVideoStartTime(null);
- await startStreamFeed();
- },
- fullScreen: () => {
- if (!liveFeedPlayerRef.current) return;
- setFullscreen(true, liveFeedPlayerRef.current);
- },
- updatePreset: (option) => {
- getCameraStatus({
- onSuccess: async (data) => {
- if (currentPreset?.asset_object?.id && data?.position) {
- setLoading(option.loadingLabel);
- console.log("Updating Preset");
- const response = await dispatch(
- partialUpdateAssetBed(
- {
- asset: currentPreset.asset_object.id,
- bed: currentPreset.bed_object.id,
- meta: {
- ...currentPreset.meta,
- position: data?.position,
- },
- },
- currentPreset?.id,
- ),
- );
- if (response && response.status === 200) {
- Notification.Success({ msg: "Preset Updated" });
- getBedPresets(cameraAsset?.id);
- fetchCameraPresets();
- }
- setLoading(undefined);
- }
- },
- });
- },
- other: (option) => {
- setLoading(option.loadingLabel);
- relativeMove(getPTZPayload(option.action, precision), {
- onSuccess: () => setLoading(undefined),
- });
- },
- };
-
- const cameraPTZ = getCameraPTZ(precision).map((option) => {
- const cb =
- cameraPTZActionCBs[
- cameraPTZActionCBs[option.action] ? option.action : "other"
- ];
- return { ...option, callback: () => cb(option) };
- });
-
- // Voluntarily disabling eslint, since length of `cameraPTZ` is constant and
- // hence shall not cause issues. (https://news.ycombinator.com/item?id=24363703)
- for (const option of cameraPTZ) {
- if (!option.shortcutKey) continue;
- // eslint-disable-next-line react-hooks/rules-of-hooks
- useKeyboardShortcut(option.shortcutKey, option.callback);
- }
-
- return (
-
- {toDelete && (
-
-
- Preset: {toDelete.meta.preset_name}
-
-
- Bed: {toDelete.bed_object.name}
-
-
- }
- action="Delete"
- variant="danger"
- onClose={() => setToDelete(null)}
- onConfirm={() => deletePreset(toDelete.id)}
- />
- )}
- {toUpdate && (
- setToUpdate(null)}
- onConfirm={() => updatePreset(toUpdate)}
- >
-
-
setPresetName(value)}
- />
-
- Bed
- setBed(selected as BedModel)}
- selected={bed}
- error=""
- multiple={false}
- location={cameraAsset.location_id}
- facility={cameraAsset.facility_id}
- />
-
-
-
- )}
-
-
-
-
-
{
- setVideoStartTime(() => new Date());
- }}
- onWaiting={() => {
- const delay = calculateVideoLiveDelay();
- if (delay > 5) {
- setStreamStatus(StreamStatus.Loading);
- }
- }}
- onSuccess={() => setStreamStatus(StreamStatus.Playing)}
- onError={() => setStreamStatus(StreamStatus.Offline)}
- />
-
- {streamStatus === StreamStatus.Playing &&
- calculateVideoLiveDelay() > 3 && (
-
-
- Slow Network Detected
-
- )}
-
- {loading && (
-
- )}
- {/* { streamStatus > 0 && */}
-
- {streamStatus === StreamStatus.Offline && (
-
-
- STATUS: OFFLINE
-
-
- Feed is currently not live.
-
-
- Click refresh button to try again.
-
-
- )}
- {streamStatus === StreamStatus.Stop && (
-
-
- STATUS: STOPPED
-
-
Feed is Stooped.
-
- Click refresh button to start feed.
-
-
- )}
- {streamStatus === StreamStatus.Loading && (
-
-
- STATUS: LOADING
-
-
- Fetching latest feed.
-
-
- )}
-
-
-
- {cameraPTZ.map((option) => {
- const shortcutKeyDescription =
- option.shortcutKey &&
- option.shortcutKey
- .join(" + ")
- .replace("Control", "Ctrl")
- .replace("ArrowUp", "↑")
- .replace("ArrowDown", "↓")
- .replace("ArrowLeft", "←")
- .replace("ArrowRight", "→");
-
- return (
-
- );
- })}
-
-
-
-
-
-
-
-
-
-
- {showDefaultPresets ? (
- <>
- {viewOptions(presetsPage)?.map((option: any, i) => (
-
- ))}
- >
- ) : (
- <>
- {bedPresets?.map((preset: any, index: number) => (
-
-
-
-
-
-
-
- ))}
- >
- )}
-
- {/* Page Number Next and Prev buttons */}
- {showDefaultPresets ? (
-
-
-
-
- ) : (
-
-
-
-
- )}
- {props?.showRefreshButton && (
-
- )}
-
-
-
-
-
- );
-};
-
-export default CameraFeedOld;
diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx
index 7268397b81a..3e447e34e21 100644
--- a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx
+++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx
@@ -1,32 +1,29 @@
import { useState } from "react";
-import { AssetBedModel, AssetData } from "../Assets/AssetTypes";
+import { AssetData } from "../Assets/AssetTypes";
import CameraFeed from "./CameraFeed";
import useQuery from "../../Utils/request/useQuery";
-import routes from "../../Redux/api";
-import useSlug from "../../Common/hooks/useSlug";
-import { CameraPresetDropdown } from "./AssetBedSelect";
+import { CameraPresetDropdown } from "./CameraPresetSelect";
import useOperateCamera from "./useOperateCamera";
import { classNames } from "../../Utils/utils";
+import { CameraPreset, FeedRoutes } from "./routes";
interface Props {
asset: AssetData;
}
export default function LocationFeedTile(props: Props) {
- const facility = useSlug("facility");
- const [preset, setPreset] = useState();
-
- const { data, loading } = useQuery(routes.listAssetBeds, {
- query: { limit: 100, facility, asset: props.asset?.id },
+ const [preset, setPreset] = useState();
+ const { operate, key } = useOperateCamera(props.asset.id);
+ const { data, loading } = useQuery(FeedRoutes.listAssetPresets, {
+ pathParams: { asset_id: props.asset.id },
+ query: { limit: 100 },
});
- const { operate, key } = useOperateCamera(props.asset.id, true);
-
return (
string;
- onChange?: (value: AssetBedModel) => void;
+ options: CameraPreset[];
+ value?: CameraPreset;
+ label?: (value: CameraPreset) => string;
+ onChange?: (value: CameraPreset) => void;
}
export default function CameraPresetSelect(props: Props) {
@@ -71,16 +70,13 @@ export const CameraPresetDropdown = (
props: Props & { placeholder: string },
) => {
const selected = props.value;
-
- const options = props.options.filter(({ meta }) => meta.type !== "boundary");
-
const label = props.label ?? defaultLabel;
return (
- {options.length === 0
+ {props.options.length === 0
? "No presets"
: selected
? label(selected)
@@ -113,7 +109,7 @@ export const CameraPresetDropdown = (
as="ul"
className="absolute z-20 max-h-48 w-full overflow-auto rounded-b-lg bg-white py-1 text-base shadow-lg ring-1 ring-secondary-500 focus:outline-none md:max-h-60"
>
- {options?.map((obj) => (
+ {props.options.map((obj) => (
{
- return `${bed_object.name}: ${meta.preset_name}`;
+const defaultLabel = (preset: CameraPreset) => {
+ return `${preset.asset_bed.bed_object.name}: ${preset.name}`;
};
diff --git a/src/Components/CameraFeed/ConfigureCamera.tsx b/src/Components/CameraFeed/ConfigureCamera.tsx
new file mode 100644
index 00000000000..052f93a9b05
--- /dev/null
+++ b/src/Components/CameraFeed/ConfigureCamera.tsx
@@ -0,0 +1,643 @@
+import { useEffect, useState } from "react";
+import { AssetData } from "../Assets/AssetTypes";
+import { getCameraConfig, makeAccessKey } from "../../Utils/transformUtils";
+import TextFormField from "../Form/FormFields/TextFormField";
+import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2";
+import useAuthUser from "../../Common/hooks/useAuthUser";
+import CareIcon from "../../CAREUI/icons/CareIcon";
+import useOperateCamera from "./useOperateCamera";
+import CameraFeed from "./CameraFeed";
+import { useTranslation } from "react-i18next";
+import request from "../../Utils/request/request";
+import routes from "../../Redux/api";
+import { Error, Success } from "../../Utils/Notifications";
+import { useQueryParams } from "raviger";
+import useQuery from "../../Utils/request/useQuery";
+import { classNames, compareBy } from "../../Utils/utils";
+import RecordMeta from "../../CAREUI/display/RecordMeta";
+import { CameraPreset, FeedRoutes, GetStatusResponse } from "./routes";
+import DialogModal from "../Common/Dialog";
+import {
+ Listbox,
+ ListboxButton,
+ ListboxOption,
+ ListboxOptions,
+} from "@headlessui/react";
+import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2";
+import Loading from "../Common/Loading";
+import ConfirmDialog from "../Common/ConfirmDialog";
+import { FieldLabel } from "../Form/FormFields/FormField";
+import { checkIfValidIP } from "../../Common/validation";
+import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField";
+
+interface Props {
+ asset: AssetData;
+ onUpdated: () => void;
+}
+
+type OnvifPreset = { name: string; value: number };
+
+export default function ConfigureCamera(props: Props) {
+ const { t } = useTranslation();
+ const authUser = useAuthUser();
+
+ const presetNameSuggestions = [
+ t("patient_face"),
+ t("patient_body"),
+ t("vitals_monitor"),
+ ];
+
+ const [query, setQuery] = useQueryParams<{ bed?: string }>();
+ const [meta, setMeta] = useState(props.asset.meta);
+ const [onvifPresets, setOnvifPresets] = useState();
+ const [currentOnvifPreset, setCurrentOnvifPreset] = useState();
+ const [createPreset, setCreatePreset] = useState();
+ const [editPreset, setEditPreset] = useState<{
+ preset: CameraPreset["id"];
+ position?: CameraPreset["position"];
+ }>();
+ const [presetName, setPresetName] = useState("");
+ const [showUnlinkConfirmation, setShowUnlinkConfirmation] = useState(false);
+
+ const assetBedsQuery = useQuery(routes.listAssetBeds, {
+ query: { asset: props.asset.id, limit: 50 },
+ });
+
+ const bedsQuery = useQuery(routes.listFacilityBeds, {
+ query: { location: props.asset.location_object.id, limit: 50 },
+ });
+
+ const linkedAssetBeds = assetBedsQuery.data?.results.sort(
+ compareBy("created_date"),
+ );
+
+ const linkedBedIDs = linkedAssetBeds?.map((a) => a.bed_object.id!);
+ const unlinkedBeds =
+ linkedBedIDs &&
+ bedsQuery.data?.results
+ .filter((bed) => !linkedBedIDs.includes(bed.id!))
+ .sort(compareBy("created_date"));
+
+ const firstBedId =
+ linkedAssetBeds?.[0]?.bed_object.id ?? unlinkedBeds?.[0]?.id;
+ useEffect(() => {
+ if (!query.bed && firstBedId) {
+ setQuery({ bed: firstBedId });
+ }
+ }, [query.bed, firstBedId]);
+
+ const selectedAssetBed = linkedAssetBeds?.find(
+ (a) => a.bed_object.id === query.bed,
+ );
+ const selectedUnlinkedBed = unlinkedBeds?.find((bed) => bed.id === query.bed);
+
+ const cameraPresetsQuery = useQuery(FeedRoutes.listAssetBedPresets, {
+ pathParams: { assetbed_id: selectedAssetBed?.id ?? "" },
+ query: { position: true, limit: 50 },
+ prefetch: !!selectedAssetBed?.id,
+ });
+
+ useEffect(() => setMeta(props.asset.meta), [props.asset]);
+
+ const accessKeyAttributes = getCameraConfig(meta);
+
+ const { operate, key } = useOperateCamera(props.asset.id);
+
+ if (!["DistrictAdmin", "StateAdmin"].includes(authUser.user_type)) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{
+ if (!onvifPresets) {
+ setOnvifPresets(
+ Object.entries(presets).map(([name, value]) => ({
+ name,
+ value,
+ })),
+ );
+ }
+ }}
+ >
+
+
{
+ setCurrentOnvifPreset(preset);
+ operate({
+ type: "goto_preset",
+ data: {
+ preset: preset.value,
+ },
+ });
+ }}
+ disabled={!onvifPresets?.length}
+ >
+
+
+
+ {!onvifPresets?.length
+ ? t("no_presets")
+ : (currentOnvifPreset?.name ??
+ t("move_to_onvif_preset"))}
+
+
+
+
+
+
+ {onvifPresets?.map((obj) => (
+
+ classNames(
+ dropdownOptionClassNames(args),
+ "px-2 py-1.5",
+ )
+ }
+ value={obj}
+ >
+ {obj.name}
+
+ ))}
+
+
+
+
+
+
+
+
+ {!linkedAssetBeds?.length && !unlinkedBeds?.length ? (
+
+
+ {t("location_beds_empty")}
+ {t("add_beds_to_configure_presets")}
+
+
+ ) : (
+
+
{t("manage_bed_presets")}
+
+
+ {cameraPresetsQuery.loading &&
}
+ {selectedAssetBed && (
+ <>
+
setShowUnlinkConfirmation(false)}
+ onConfirm={async () => {
+ const { res } = await request(routes.deleteAssetBed, {
+ pathParams: { external_id: selectedAssetBed.id },
+ });
+
+ if (res?.ok) {
+ Success({
+ msg: `${selectedAssetBed.bed_object.name} was unlinked from ${selectedAssetBed.asset_object.name}.`,
+ });
+ setShowUnlinkConfirmation(false);
+ assetBedsQuery.refetch();
+ }
+ }}
+ />
+ {
+ setCreatePreset(undefined);
+ setPresetName("");
+ }}
+ >
+ setPresetName(value)}
+ errorClassName="hidden"
+ placeholder={t("preset_name_placeholder")}
+ suggestions={presetNameSuggestions}
+ />
+
+ {
+ const { res } = await request(FeedRoutes.createPreset, {
+ pathParams: { assetbed_id: selectedAssetBed.id },
+ body: {
+ name: presetName,
+ position: createPreset!,
+ },
+ });
+ if (!res?.ok) {
+ return;
+ }
+ setCreatePreset(undefined);
+ setPresetName("");
+ Success({ msg: "Preset created" });
+ cameraPresetsQuery.refetch();
+ }}
+ disabled={!presetName}
+ />
+
+
+
+
+ - {
+ const { data } = await operate({ type: "get_status" });
+ if (!data) {
+ Error({ msg: t("unable_to_get_current_position") });
+ return;
+ }
+ setCreatePreset(
+ (data as GetStatusResponse).result.position,
+ );
+ }}
+ >
+
+ {t("add_preset")}
+
+ {cameraPresetsQuery.data?.results.map((preset) => (
+ -
+ {
+ setEditPreset(undefined);
+ setPresetName("");
+ }}
+ >
+
+ setPresetName(value)}
+ placeholder={t("preset_name_placeholder")}
+ suggestions={presetNameSuggestions}
+ />
+ {t("position")}
+ {
+ if (!value) {
+ setEditPreset({
+ ...editPreset!,
+ position: undefined,
+ });
+ return;
+ }
+
+ const { data } = await operate({
+ type: "get_status",
+ });
+ if (!data) {
+ Error({
+ msg: t("unable_to_get_current_position"),
+ });
+ return;
+ }
+ setEditPreset({
+ ...editPreset!,
+ position: (data as GetStatusResponse).result
+ .position!,
+ });
+ }}
+ />
+
+ {
+ setEditPreset(undefined);
+ setPresetName("");
+ }}
+ />
+ {
+ const { res } = await request(
+ FeedRoutes.deletePreset,
+ {
+ pathParams: {
+ assetbed_id: selectedAssetBed.id,
+ id: preset.id,
+ },
+ },
+ );
+ if (!res?.ok) {
+ return;
+ }
+ Success({ msg: t("preset_deleted") });
+ cameraPresetsQuery.refetch();
+ setEditPreset(undefined);
+ setPresetName("");
+ }}
+ variant="danger"
+ >
+
+ {t("delete")}
+
+ {
+ const { res } = await request(
+ FeedRoutes.updatePreset,
+ {
+ pathParams: {
+ assetbed_id: selectedAssetBed.id,
+ id: preset.id,
+ },
+ body: {
+ name: presetName || undefined,
+ position: editPreset?.position,
+ },
+ },
+ );
+ if (!res?.ok) {
+ return;
+ }
+ Success({ msg: t("preset_updated") });
+ setEditPreset(undefined);
+ setPresetName("");
+ cameraPresetsQuery.refetch();
+ }}
+ />
+
+
+
+
{preset.name}
+
+
+
+
+
+ operate({
+ type: "absolute_move",
+ data: preset.position!,
+ })
+ }
+ >
+
+ {t("view")}
+
+ {
+ setEditPreset({ preset: preset.id });
+ }}
+ >
+
+ {t("update")}
+
+
+
+
+ ))}
+
+
+
+ setShowUnlinkConfirmation(true)}
+ >
+ {t("unlink_camera_and_bed")}
+
+
+
+ >
+ )}
+ {selectedUnlinkedBed && (
+
+
+ {t("bed_not_linked_to_camera")}
+ {t("create_preset_prerequisite")}
+ {
+ const { res } = await request(routes.createAssetBed, {
+ body: {
+ asset: props.asset.id,
+ bed: selectedUnlinkedBed.id,
+ },
+ });
+ if (res?.ok) {
+ Success({ msg: t("camera_bed_link_success") });
+ assetBedsQuery.refetch();
+ }
+ }}
+ className="mt-6"
+ >
+ {t("link_camera_and_bed")}
+
+
+
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/src/Components/CameraFeed/FeedAlert.tsx b/src/Components/CameraFeed/FeedAlert.tsx
index 09f3b21ae42..86c5468decf 100644
--- a/src/Components/CameraFeed/FeedAlert.tsx
+++ b/src/Components/CameraFeed/FeedAlert.tsx
@@ -9,8 +9,7 @@ export type FeedAlertState =
| "moving"
| "zooming"
| "saving_preset"
- | "host_unreachable"
- | "authentication_error";
+ | "host_unreachable";
interface Props {
state?: FeedAlertState;
@@ -25,7 +24,6 @@ const ALERT_ICON_MAP: Partial> = {
zooming: "l-search",
saving_preset: "l-save",
host_unreachable: "l-exclamation-triangle",
- authentication_error: "l-exclamation-triangle",
};
export default function FeedAlert({ state }: Props) {
diff --git a/src/Components/CameraFeed/FeedControls.tsx b/src/Components/CameraFeed/FeedControls.tsx
index 1bacd6c1cfc..36d3aa96cd2 100644
--- a/src/Components/CameraFeed/FeedControls.tsx
+++ b/src/Components/CameraFeed/FeedControls.tsx
@@ -15,8 +15,6 @@ const Actions = {
const metaKey = isAppleDevice ? "Meta" : "Control";
-export type PTZAction = keyof typeof Actions;
-
/**
* Returns the PTZ payload for the given action
*
diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts
index aecbdc655fa..db983d6a383 100644
--- a/src/Components/CameraFeed/routes.ts
+++ b/src/Components/CameraFeed/routes.ts
@@ -1,4 +1,8 @@
import { Type } from "../../Redux/api";
+import { PaginatedResponse } from "../../Utils/request/types";
+import { WritableOnly } from "../../Utils/types";
+import { AssetBedModel } from "../Assets/AssetTypes";
+import { PerformedByModel } from "../HCX/misc";
import { OperationAction, PTZPayload } from "./useOperateCamera";
export type GetStatusResponse = {
@@ -23,6 +27,18 @@ export type GetPresetsResponse = {
result: Record;
};
+export type CameraPreset = {
+ readonly id: string;
+ name: string;
+ readonly asset_bed: AssetBedModel;
+ position: PTZPayload;
+ readonly created_by: PerformedByModel;
+ readonly updated_by: PerformedByModel;
+ readonly created_date: string;
+ readonly modified_date: string;
+ readonly is_migrated: boolean;
+};
+
export const FeedRoutes = {
operateAsset: {
path: "/api/v1/asset/{id}/operate_assets/",
@@ -32,4 +48,37 @@ export const FeedRoutes = {
>(),
TBody: Type<{ action: OperationAction }>(),
},
+
+ listAssetBedPresets: {
+ path: "/api/v1/assetbed/{assetbed_id}/camera_presets/",
+ method: "GET",
+ TRes: Type>(),
+ },
+ listAssetPresets: {
+ path: "/api/v1/asset/{asset_id}/camera_presets/",
+ method: "GET",
+ TRes: Type>(),
+ },
+ listBedPresets: {
+ path: "/api/v1/bed/{bed_id}/camera_presets/",
+ method: "GET",
+ TRes: Type>(),
+ },
+ createPreset: {
+ path: "/api/v1/assetbed/{assetbed_id}/camera_presets/",
+ method: "POST",
+ TRes: Type(),
+ TBody: Type>(),
+ },
+ updatePreset: {
+ path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/",
+ method: "PATCH",
+ TRes: Type(),
+ TBody: Type>>(),
+ },
+ deletePreset: {
+ path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/",
+ method: "DELETE",
+ TRes: Type(),
+ },
} as const;
diff --git a/src/Components/CameraFeed/useFeedPTZ.ts b/src/Components/CameraFeed/useFeedPTZ.ts
deleted file mode 100644
index fb704baf972..00000000000
--- a/src/Components/CameraFeed/useFeedPTZ.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-/**
- * Deprecated. Use `useOperateAsset` instead.
- *
- * Preserving for backwards compatibility and preventing merge conflict with a
- * co-related PR. Will be removed in the future.
- */
-
-import { operateAsset } from "../../Redux/actions";
-
-export interface IAsset {
- id: string;
-}
-
-interface PTZPayload {
- x: number;
- y: number;
- zoom: number;
-}
-
-interface UseMSEMediaPlayerOption {
- config: IAsset;
- dispatch: any;
-}
-
-interface UseMSEMediaPlayerReturnType {
- absoluteMove: (payload: PTZPayload, options: IOptions) => void;
- relativeMove: (payload: PTZPayload, options: IOptions) => void;
- getPTZPayload: (
- action: PTZ,
- precision?: number,
- value?: number,
- ) => PTZPayload;
- getCameraStatus: (options: IOptions) => void;
- getStreamToken: (options: IOptions) => void;
- getPresets: (options: IOptions) => void;
- gotoPreset: (payload: IGotoPresetPayload, options: IOptions) => void;
-}
-
-interface IOptions {
- onSuccess?: (resp: Record) => void;
- onError?: (resp: Record) => void;
-}
-
-export enum PTZ {
- Up = "up",
- Down = "down",
- Left = "left",
- Right = "right",
- ZoomIn = "zoomIn",
- ZoomOut = "zoomOut",
-}
-
-const getCameraStatus =
- (config: IAsset, dispatch: any) =>
- async (options: IOptions = {}) => {
- if (!config.id) return;
- const resp = await dispatch(
- operateAsset(config.id, {
- action: {
- type: "get_status",
- },
- }),
- );
- resp &&
- (resp.status === 200
- ? options?.onSuccess && options.onSuccess(resp.data.result)
- : options?.onError && options.onError(resp));
- };
-
-const getStreamToken =
- (config: IAsset, dispatch: any) =>
- async (options: IOptions = {}) => {
- if (!config.id) return;
- const resp = await dispatch(
- operateAsset(config.id, {
- action: {
- type: "get_stream_token",
- },
- }),
- );
- resp &&
- (resp.status === 200
- ? options?.onSuccess && options.onSuccess(resp.data.result)
- : options?.onError && options.onError(resp));
- };
-
-const getPresets =
- (config: IAsset, dispatch: any) =>
- async (options: IOptions = {}) => {
- if (!config.id) return;
- const resp = await dispatch(
- operateAsset(config.id, {
- action: {
- type: "get_presets",
- },
- }),
- );
- resp &&
- (resp.status === 200
- ? options?.onSuccess && options.onSuccess(resp.data.result)
- : options?.onError && options.onError(resp));
- };
-
-interface IGotoPresetPayload {
- preset: string;
-}
-
-const gotoPreset =
- (config: IAsset, dispatch: any) =>
- async (payload: IGotoPresetPayload, options: IOptions = {}) => {
- if (!config.id) return;
- const resp = await dispatch(
- operateAsset(config.id, {
- action: {
- type: "goto_preset",
- data: payload,
- },
- }),
- );
- resp &&
- (resp.status === 200
- ? options?.onSuccess && options.onSuccess(resp.data.result)
- : options?.onError && options.onError(resp));
- };
-
-const absoluteMove =
- (config: IAsset, dispatch: any) =>
- async (payload: PTZPayload, options: IOptions = {}) => {
- if (!config.id) return;
- const resp = await dispatch(
- operateAsset(config.id, {
- action: {
- type: "absolute_move",
- data: payload,
- },
- }),
- );
- resp &&
- (resp.status === 200
- ? options?.onSuccess && options.onSuccess(resp.data.result)
- : options?.onError && options.onError(resp));
- };
-
-const relativeMove =
- (config: IAsset, dispatch: any) =>
- async (payload: PTZPayload, options: IOptions = {}) => {
- if (!config.id) return;
- const resp = await dispatch(
- operateAsset(config.id, {
- action: {
- type: "relative_move",
- data: payload,
- },
- }),
- );
- resp &&
- (resp.status === 200
- ? options?.onSuccess && options.onSuccess(resp.data.result)
- : options?.onError && options.onError(resp));
- };
-
-export const getPTZPayload = (
- action: PTZ,
- precision = 1,
- value?: number,
-): PTZPayload => {
- let x = 0;
- let y = 0;
- let zoom = 0;
- const delta = !value ? 0.1 / Math.max(1, precision) : value;
- switch (action) {
- case PTZ.Up:
- y = delta;
- break;
- case PTZ.Down:
- y = -delta;
- break;
- case PTZ.Left:
- x = -delta;
- break;
- case PTZ.Right:
- x = delta;
- break;
- case PTZ.ZoomIn:
- zoom = delta;
- break;
- case PTZ.ZoomOut:
- zoom = -delta;
- break;
- }
-
- return { x, y, zoom };
-};
-
-export const useFeedPTZ = ({
- config,
- dispatch,
-}: UseMSEMediaPlayerOption): UseMSEMediaPlayerReturnType => {
- return {
- absoluteMove: absoluteMove(config, dispatch),
- relativeMove: relativeMove(config, dispatch),
- getPTZPayload,
- getCameraStatus: getCameraStatus(config, dispatch),
- getStreamToken: getStreamToken(config, dispatch),
- getPresets: getPresets(config, dispatch),
- gotoPreset: gotoPreset(config, dispatch),
- };
-};
diff --git a/src/Components/CameraFeed/useOperateCamera.ts b/src/Components/CameraFeed/useOperateCamera.ts
index bfddbf5b887..0e65fb0130c 100644
--- a/src/Components/CameraFeed/useOperateCamera.ts
+++ b/src/Components/CameraFeed/useOperateCamera.ts
@@ -54,7 +54,7 @@ export type OperationAction =
* This hook is used to control the PTZ of a camera asset and retrieve other related information.
* @param id The external id of the camera asset
*/
-export default function useOperateCamera(id: string, silent = false) {
+export default function useOperateCamera(id: string) {
const [key, setKey] = useState(0);
return {
@@ -70,14 +70,14 @@ export default function useOperateCamera(id: string, silent = false) {
type: "get_status",
},
},
- silent,
+ silent: true,
});
}
return request(FeedRoutes.operateAsset, {
pathParams: { id },
body: { action },
- silent,
+ silent: true,
});
},
};
diff --git a/src/Components/CameraFeed/utils.ts b/src/Components/CameraFeed/utils.ts
index 5556237d579..1cb721ebcc3 100644
--- a/src/Components/CameraFeed/utils.ts
+++ b/src/Components/CameraFeed/utils.ts
@@ -21,7 +21,7 @@ export const getStreamUrl = (asset: AssetData, token?: string) => {
throw "getStreamUrl can be invoked only for ONVIF Assets";
}
- const config = getCameraConfig(asset);
+ const config = getCameraConfig(asset.meta);
const host = asset.resolved_middleware?.hostname;
const uuid = config.accessKey;
diff --git a/src/Components/Common/Avatar.tsx b/src/Components/Common/Avatar.tsx
index a519901216b..e4d42dd72ae 100644
--- a/src/Components/Common/Avatar.tsx
+++ b/src/Components/Common/Avatar.tsx
@@ -1,4 +1,5 @@
-import React from "react";
+import { cn } from "@/lib/utils";
+import React, { useEffect, useRef, useState } from "react";
const colors: string[] = [
"#E6F3FF", // Light Blue
@@ -44,43 +45,54 @@ const initials = (name: string): string => {
interface AvatarProps {
colors?: [string, string];
name: string;
+ imageUrl?: string;
className?: string;
- square?: boolean; // New prop to determine if the avatar should be square
}
const Avatar: React.FC = ({
colors: propColors,
name,
+ imageUrl,
className,
- square = false, // Default to false for backwards compatibility
}) => {
- const [bgColor, fgColor] = propColors || toColor(name);
+ const [bgColor] = propColors || toColor(name);
+ const [width, setWidth] = useState(0);
+ const avatarRef = useRef(null);
+
+ useEffect(() => {
+ const updateWidth = () => {
+ const avatarRect = avatarRef.current?.getBoundingClientRect();
+ const width = avatarRect?.width || 0;
+ setWidth(width);
+ };
+ updateWidth();
+ document.addEventListener("resize", updateWidth);
+ return () => document.removeEventListener("resize", updateWidth);
+ }, []);
return (
-
+
);
};
diff --git a/src/Components/Common/BloodPressureFormField.tsx b/src/Components/Common/BloodPressureFormField.tsx
index e6fff756d0f..beddd34cf38 100644
--- a/src/Components/Common/BloodPressureFormField.tsx
+++ b/src/Components/Common/BloodPressureFormField.tsx
@@ -1,13 +1,13 @@
import { useTranslation } from "react-i18next";
import { FieldValidator } from "../Form/FieldValidators";
import FormField from "../Form/FormFields/FormField";
-import RangeAutocompleteFormField from "../Form/FormFields/RangeAutocompleteFormField";
import {
FieldChangeEvent,
FormFieldBaseProps,
useFormFieldPropsResolver,
} from "../Form/FormFields/Utils";
import { BloodPressure } from "../Patient/models";
+import TextFormField from "../Form/FormFields/TextFormField";
type Props = FormFieldBaseProps
;
@@ -16,12 +16,13 @@ export default function BloodPressureFormField(props: Props) {
const field = useFormFieldPropsResolver(props);
const map = meanArterialPressure(props.value)?.toFixed();
- const handleChange = (event: FieldChangeEvent) => {
+ const handleChange = (event: FieldChangeEvent) => {
+ const value = event.value ? parseInt(event.value, 10) : "";
const bp = {
systolic: field.value?.systolic,
diastolic: field.value?.diastolic,
};
- bp[event.name as keyof BloodPressure] = event.value;
+ bp[event.name as keyof BloodPressure] = value || undefined;
field.handleChange(Object.values(bp).filter(Boolean).length ? bp : null);
};
@@ -32,63 +33,35 @@ export default function BloodPressureFormField(props: Props) {
labelSuffix: map && MAP: {map},
}}
>
-
-
+
- /
- /
+
@@ -102,14 +75,20 @@ export const meanArterialPressure = (bp?: BloodPressure | null) => {
return (2 * bp.diastolic + bp.systolic) / 3;
};
-export const BloodPressureValidator: FieldValidator = (bp) => {
+export const BloodPressureValidator: FieldValidator = (
+ bp,
+ t,
+) => {
if (Object.values(bp).every((v) => v == null)) {
return;
}
- if (bp.diastolic == null) {
- return "Diastolic is missing. Either specify both or clear both.";
+ if (bp.systolic == null || bp.diastolic == null) {
+ return t("blood_pressure_error.missing");
+ }
+ if (bp.systolic > 250 || bp.diastolic > 250) {
+ return t("blood_pressure_error.exceed");
}
- if (bp.systolic == null) {
- return "Systolic is missing. Either specify both or clear both.";
+ if (bp.systolic < bp.diastolic) {
+ return t("blood_pressure_error.systolic_less_than_diastolic");
}
};
diff --git a/src/Components/Common/Breadcrumbs.tsx b/src/Components/Common/Breadcrumbs.tsx
index aaee7ac0b64..be35cf6862d 100644
--- a/src/Components/Common/Breadcrumbs.tsx
+++ b/src/Components/Common/Breadcrumbs.tsx
@@ -63,7 +63,7 @@ export default function Breadcrumbs({
return (
-
@@ -90,7 +90,7 @@ export default function Breadcrumbs({
-