From a24b9cada35a87d9b15d5fb0c42ab0f34cb9a078 Mon Sep 17 00:00:00 2001 From: eat_code_sleep <141677292+i0am0arunava@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:23:51 +0530 Subject: [PATCH] Fixed Add a better search UI for patients index page #8691 (#8834) Co-authored-by: Bodhish Thomas Co-authored-by: Shivank Kacker --- .../e2e/patient_spec/PatientHomepage.cy.ts | 3 +- cypress/pageobject/Facility/FacilityHome.ts | 3 +- cypress/pageobject/Patient/PatientCreation.ts | 6 +- package-lock.json | 496 +++++++++++++++++- package.json | 5 + public/locale/en.json | 6 + src/components/Common/Page.tsx | 6 +- .../Common/SearchByMultipleFields.tsx | 289 ++++++++++ src/components/Facility/FacilityList.tsx | 85 +-- .../Form/FormFields/PhoneNumberFormField.tsx | 241 +++++---- src/components/Patient/ManagePatients.tsx | 224 ++++---- src/components/ui/command.tsx | 154 ++++++ src/components/ui/dialog.tsx | 120 +++++ src/components/ui/input.tsx | 26 + src/components/ui/label.tsx | 24 + src/components/ui/popover.tsx | 31 ++ src/components/ui/scroll-area.tsx | 46 ++ src/hooks/useFilters.tsx | 9 +- 18 files changed, 1489 insertions(+), 285 deletions(-) create mode 100644 src/components/Common/SearchByMultipleFields.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/scroll-area.tsx diff --git a/cypress/e2e/patient_spec/PatientHomepage.cy.ts b/cypress/e2e/patient_spec/PatientHomepage.cy.ts index 2d8d20572bc..a120e282a2d 100644 --- a/cypress/e2e/patient_spec/PatientHomepage.cy.ts +++ b/cypress/e2e/patient_spec/PatientHomepage.cy.ts @@ -157,7 +157,7 @@ describe("Patient Homepage present functionalities", () => { patientHome.verifyPatientExportRequest(); }); - it("Verify the functionality of the patient tab pagination", () => { + it("Test Pagination on Patient List Page", () => { let firstPatientPageOne: string; cy.get('[data-cy="patient"]') .first() @@ -165,6 +165,7 @@ describe("Patient Homepage present functionalities", () => { .then((patientOne: string) => { firstPatientPageOne = patientOne.trim(); pageNavigation.navigateToNextPage(); + cy.wait(2000); pageNavigation.verifyCurrentPageNumber(2); cy.get('[data-cy="patient"]') .first() diff --git a/cypress/pageobject/Facility/FacilityHome.ts b/cypress/pageobject/Facility/FacilityHome.ts index cf68b05306c..fe8585b48be 100644 --- a/cypress/pageobject/Facility/FacilityHome.ts +++ b/cypress/pageobject/Facility/FacilityHome.ts @@ -26,8 +26,7 @@ class FacilityHome { } typeFacilitySearch(facilityName: string) { - cy.get("#search-by-facility").click().clear(); - cy.get("#search-by-facility").click().type(facilityName); + cy.get("#facility-search").click().clear().type(facilityName); } clickMenuItem(itemName: string) { diff --git a/cypress/pageobject/Patient/PatientCreation.ts b/cypress/pageobject/Patient/PatientCreation.ts index 1f915f5474e..febf31b33b5 100644 --- a/cypress/pageobject/Patient/PatientCreation.ts +++ b/cypress/pageobject/Patient/PatientCreation.ts @@ -11,7 +11,8 @@ export class PatientPage { } visitPatient(patientName: string) { - cy.get("#name").click().type(patientName); + cy.get('[data-test-id="patient-search__name"]').click(); + cy.get("#patient-search").click().type(patientName); // Type the patient name cy.intercept("GET", "**/api/v1/consultation/**").as("getPatient"); cy.get("#patient-name-list").contains(patientName).click(); cy.wait(2000); @@ -66,7 +67,8 @@ export class PatientPage { } typePatientNameList(patientName: string) { - cy.get("#name").click().type(patientName); + cy.get('[data-test-id="patient-search__name"]').click(); + cy.get("#patient-search").click().type(patientName); } typePatientAddress(address: string) { diff --git a/package-lock.json b/package-lock.json index 5aafc454c63..dfa66181bd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,11 @@ "@hello-pangea/dnd": "^17.0.0", "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.2", @@ -32,6 +36,7 @@ "browserslist-useragent-regexp": "^4.1.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "cross-env": "^7.0.3", "cypress": "^13.16.1", "dayjs": "^1.11.13", @@ -3607,6 +3612,11 @@ "validator": "^13.9.0" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", @@ -3707,6 +3717,41 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", + "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", + "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-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.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", + "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-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -3845,6 +3890,28 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", + "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", + "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-menu": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz", @@ -3885,6 +3952,42 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz", + "integrity": "sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==", + "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-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-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "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", @@ -4049,6 +4152,36 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.0.tgz", + "integrity": "sha512-q2jMBdsJ9zB7QG6ngQNzNwlvxLQqONyL58QbEGwuyRZZb/ARQwk3uQVbCF7GvQVOtV6EU/pDxAw3zRzJZI3rpQ==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "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-presence": "1.1.1", + "@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" + }, + "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-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -7338,6 +7471,366 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "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/codepage": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", @@ -12281,8 +12774,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/lodash.isplainobject": { "version": "4.0.6", diff --git a/package.json b/package.json index 2bea7ad70af..e07e45edaeb 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,11 @@ "@hello-pangea/dnd": "^17.0.0", "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.2", @@ -71,6 +75,7 @@ "browserslist-useragent-regexp": "^4.1.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "cross-env": "^7.0.3", "cypress": "^13.16.1", "dayjs": "^1.11.13", diff --git a/public/locale/en.json b/public/locale/en.json index b5f46dd7308..f86d78fa3aa 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -700,6 +700,7 @@ "facilities": "Facilities", "facility": "Facility", "facility_consent_requests_page_title": "Patient Consent List", + "facility_district_name": "Facility/District Name", "facility_name": "Facility Name", "facility_preference": "Facility preference", "facility_search_placeholder": "Search by Facility / District Name", @@ -1214,6 +1215,10 @@ "scan_asset_qr": "Scan Asset QR!", "scribe__reviewing_field": "Reviewing field {{currentField}} / {{totalFields}}", "scribe_error": "Could not autofill fields", + "search_by_emergency_contact_phone_number": "Search by Emergency Contact Phone Number", + "search_by_patient_name": "Search by Patient Name", + "search_by_patient_no": "Search by Patient Number", + "search_by_phone_number": "Search by Phone Number", "search_by_username": "Search by username", "search_for_facility": "Search for Facility", "search_icd11_placeholder": "Search for ICD-11 Diagnoses", @@ -1318,6 +1323,7 @@ "to_be_conducted": "To be conducted", "total_amount": "Total Amount", "total_beds": "Total Beds", + "total_patients": "Total Patients", "total_staff": "Total Staff", "total_users": "Total Users", "transcript_edit_info": "You can update this if we made an error", diff --git a/src/components/Common/Page.tsx b/src/components/Common/Page.tsx index 699f3d47204..32e763c45b9 100644 --- a/src/components/Common/Page.tsx +++ b/src/components/Common/Page.tsx @@ -1,10 +1,10 @@ import { RefObject, useContext, useEffect } from "react"; +import { cn } from "@/lib/utils"; + import PageTitle, { PageTitleProps } from "@/components/Common/PageTitle"; import { SidebarShrinkContext } from "@/components/Common/Sidebar/Sidebar"; -import { classNames } from "@/Utils/utils"; - interface PageProps extends PageTitleProps { children: React.ReactNode | React.ReactNode[]; options?: React.ReactNode | React.ReactNode[]; @@ -38,7 +38,7 @@ export default function Page(props: PageProps) { } return ( -
+
; +} + +interface SearchByMultipleFieldsProps { + id: string; + options: SearchOption[]; + onSearch: (key: string, value: string) => void; + initialOptionIndex?: number; + className?: string; + inputClassName?: string; + buttonClassName?: string; + clearSearch?: { value: boolean; params?: string[] }; +} + +type EventType = { + value: string; + target?: { value: string }; +}; + +const SearchByMultipleFields: React.FC = ({ + id, + options, + onSearch, + initialOptionIndex, + className, + inputClassName, + buttonClassName, + clearSearch, +}) => { + const { t } = useTranslation(); + const [selectedOptionIndex, setSelectedOptionIndex] = useState( + initialOptionIndex || 0, + ); + const selectedOption = options[selectedOptionIndex]; + const [searchValue, setSearchValue] = useState( + options[selectedOptionIndex].value || "", + ); + const [open, setOpen] = useState(false); + const inputRef = useRef(null); + const [focusedIndex, setFocusedIndex] = useState(0); + const [error, setError] = useState(); + + useEffect(() => { + if (clearSearch?.value) { + const clearinput = options + .map((op) => op.key) + .some((element) => clearSearch.params?.includes(element)); + clearinput ? setSearchValue("") : null; + inputRef.current?.focus(); + } + }, [clearSearch]); + + const handleOptionChange = useCallback( + (index: number) => { + setSelectedOptionIndex(index); + const option = options[index]; + setSearchValue(option.value || ""); + setFocusedIndex(options.findIndex((op) => op.key === option.key)); + setOpen(false); + inputRef.current?.focus(); + setError(false); + onSearch(option.key, option.value); + }, + [onSearch], + ); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ( + e.key === "/" && + !(document.activeElement instanceof HTMLInputElement) + ) { + e.preventDefault(); + setOpen(true); + } + if (open) { + if (e.key === "ArrowDown") { + setFocusedIndex((prevIndex) => + prevIndex === options.length - 1 ? 0 : prevIndex + 1, + ); + } else if (e.key === "ArrowUp") { + setFocusedIndex((prevIndex) => + prevIndex === 0 ? options.length - 1 : prevIndex - 1, + ); + } else if (e.key === "Enter") { + handleOptionChange(focusedIndex); + } + + if (e.key === "Escape") { + inputRef.current?.focus(); + setOpen(false); + } + + options.forEach((option, i) => { + if ( + e.key.toLocaleLowerCase() === + option.shortcutKey.toLocaleLowerCase() && + open + ) { + e.preventDefault(); + handleOptionChange(i); + } + }); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [focusedIndex, open, handleOptionChange, options]); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, [selectedOptionIndex]); + + useEffect(() => { + const timeout = setTimeout( + () => + selectedOption.value !== searchValue && + onSearch(selectedOption.key, searchValue), + 1000, + ); + return () => clearTimeout(timeout); + }, [searchValue]); + + const handleSearchChange = useCallback( + (value: string) => { + setSearchValue(value); + }, + [selectedOption, onSearch], + ); + + const renderSearchInput = useMemo(() => { + const commonProps = { + ref: inputRef, + value: searchValue, + onChange: (e: EventType) => + handleSearchChange(e.target ? e.target.value : e.value), + className: cn( + "flex-grow border-none shadow-none focus-visible:ring-0 h-10", + inputClassName, + ), + }; + + switch (selectedOption.type) { + case "phone": + return ( + setError(error)} + /> + ); + default: + return ( + + ); + } + }, [selectedOption, searchValue, handleSearchChange, t, inputClassName]); + + return ( +
+
+ + + + + + + + + {options.map((option, index) => ( + handleOptionChange(index)} + className={cn({ + "bg-gray-100": focusedIndex === index, + "hover:bg-secondary-100": true, + })} + > + + {t(option.key)} + + {option.label.charAt(0).toUpperCase()} + + + ))} + + + + + + {renderSearchInput} +
+ {error && ( +
+ {t("invalid_phone_number")} +
+ )} +
+ {options.map((option, i) => ( + + ))} +
+
+ ); +}; + +export default SearchByMultipleFields; diff --git a/src/components/Facility/FacilityList.tsx b/src/components/Facility/FacilityList.tsx index adcb35720f8..164632e1601 100644 --- a/src/components/Facility/FacilityList.tsx +++ b/src/components/Facility/FacilityList.tsx @@ -12,7 +12,6 @@ import Page from "@/components/Common/Page"; import { FacilityCard } from "@/components/Facility/FacilityCard"; import FacilityFilter from "@/components/Facility/FacilityFilter"; import { FacilityModel } from "@/components/Facility/models"; -import SearchInput from "@/components/Form/SearchInput"; import useAuthUser from "@/hooks/useAuthUser"; import useFilters from "@/hooks/useFilters"; @@ -22,6 +21,8 @@ import { FACILITY_TYPES } from "@/common/constants"; import routes from "@/Utils/request/api"; import useQuery from "@/Utils/request/useQuery"; +import SearchByMultipleFields from "../Common/SearchByMultipleFields"; + export const FacilityList = () => { const { qParams, @@ -30,6 +31,7 @@ export const FacilityList = () => { FilterBadges, advancedFilter, resultsPerPage, + clearSearch, } = useFilters({ limit: 14, cacheBlacklist: ["search"], @@ -153,50 +155,59 @@ export const FacilityList = () => { breadcrumbs={false} hideBack options={ - +
+ advancedFilter.setShow(true)} /> + +
} > -
+
+ updateQuery({ search: value })} + clearSearch={clearSearch} /> -
- updateQuery({ [e.name]: e.value })} - placeholder={t("facility_search_placeholder")} - /> - advancedFilter.setShow(true)} /> -
diff --git a/src/components/Form/FormFields/PhoneNumberFormField.tsx b/src/components/Form/FormFields/PhoneNumberFormField.tsx index f96854f4a4a..0fd6f914035 100644 --- a/src/components/Form/FormFields/PhoneNumberFormField.tsx +++ b/src/components/Form/FormFields/PhoneNumberFormField.tsx @@ -1,5 +1,6 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { useCallback, useEffect, useMemo, useState } from "react"; +import React from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -29,136 +30,144 @@ import { const phoneCodes: Record = phoneCodesJson; interface Props extends FormFieldBaseProps { + onError?: (error: FieldError) => void; + hideHelp?: boolean; types: PhoneNumberType[]; placeholder?: string; autoComplete?: string; disableValidation?: boolean; } -export default function PhoneNumberFormField(props: Props) { - const field = useFormFieldPropsResolver(props); - const [error, setError] = useState(); - const [country, setCountry] = useState({ - flag: "🇮🇳", - name: "India", - code: "91", - }); - const validator = useMemo( - () => PhoneNumberValidator(props.types), - [props.types], - ); +const PhoneNumberFormField = React.forwardRef( + (props, ref) => { + const field = useFormFieldPropsResolver(props); + const [error, setError] = useState(); + const [country, setCountry] = useState({ + flag: "🇮🇳", + name: "India", + code: "91", + }); + const validator = useMemo( + () => PhoneNumberValidator(props.types), + [props.types], + ); - const validate = useMemo( - () => (value: string | undefined, event: "blur" | "change") => { - if (!value || props.disableValidation) { - return; - } + const validate = useMemo( + () => (value: string | undefined, event: "blur" | "change") => { + if (!value || props.disableValidation) { + return; + } - const newError = validator(value); + const newError = validator(value); - if (!newError) { - return; - } else if (event === "blur") { - return newError; - } - }, - [props.disableValidation], - ); + if (!newError) { + return; + } else if (event === "blur") { + return newError; + } + }, + [props.disableValidation], + ); - const setValue = useCallback( - (value: string) => { - value = value.replaceAll(/[^0-9+]/g, ""); - if (value.length > 12 && value.startsWith("+910")) { - value = "+91" + value.slice(4); - } + const setValue = useCallback( + (value: string) => { + value = value.replaceAll(/[^0-9+]/g, ""); + if (value.length > 12 && value.startsWith("+910")) { + value = "+91" + value.slice(4); + } - const error = validate(value, "change"); - field.handleChange(value); + const error = validate(value, "change"); + field.handleChange(value); - setError(error); - }, - [field, validate], - ); - - const handleCountryChange = (value: CountryData): void => { - setCountry(value); - setValue(conditionPhoneCode(value.code)); - }; - - useEffect(() => { - if (field.value && field.value.length > 0) { - if (field.value.startsWith("1800")) { - setCountry({ flag: "📞", name: "Support", code: "1800" }); - return; - } - if (field.value === "+") { - setCountry({ flag: "🌍", name: "Other", code: "+" }); - return; + setError(error); + }, + [field, validate, error], + ); + useEffect(() => { + if (props.onError) { + props.onError(error); } - setCountry(phoneCodes[getCountryCode(field.value)!]); - } - }, [setValue]); + }, [error]); + const handleCountryChange = (value: CountryData): void => { + setCountry(value); + setValue(conditionPhoneCode(value.code)); + }; - return ( - - ), - }} - > -
- - {({ open }: { open: boolean }) => { - return ( - <> - -
- - {country?.flag ?? "🇮🇳"} - - -
-
- setValue(e.target.value)} - disabled={field.disabled} - onBlur={() => setError(validate(field.value, "blur"))} - /> - - {({ close }) => ( - - )} - - - ); - }} -
-
-
- ); -} + useEffect(() => { + if (field.value && field.value.length > 0) { + if (field.value.startsWith("1800")) { + setCountry({ flag: "📞", name: "Support", code: "1800" }); + return; + } + if (field.value === "+") { + setCountry({ flag: "🌍", name: "Other", code: "+" }); + return; + } + setCountry(phoneCodes[getCountryCode(field.value)!]); + } + }, [setValue]); + return ( + + )), + }} + > +
+ + {({ open }: { open: boolean }) => { + return ( + <> + +
+ {country?.flag} + +
+
+ setValue(e.target.value)} + disabled={field.disabled} + onBlur={() => setError(validate(field.value, "blur"))} + ref={ref} + /> + + {({ close }) => ( + + )} + + + ); + }} +
+
+
+ ); + }, +); const PhoneNumberTypesHelp = (props: { types: PhoneNumberType[] }) => { const { t } = useTranslation(); @@ -175,7 +184,6 @@ const PhoneNumberTypesHelp = (props: { types: PhoneNumberType[] }) => {
); }; - const conditionPhoneCode = (code: string) => { code = code.split(" ")[0]; return code.startsWith("+") ? code : "+" + code; @@ -280,3 +288,4 @@ const CountryCodesList = ({
); }; +export default PhoneNumberFormField; diff --git a/src/components/Patient/ManagePatients.tsx b/src/components/Patient/ManagePatients.tsx index 53c4fbceeed..6fba4d6e734 100644 --- a/src/components/Patient/ManagePatients.tsx +++ b/src/components/Patient/ManagePatients.tsx @@ -1,38 +1,16 @@ import dayjs from "dayjs"; import { Link, navigate } from "raviger"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import Chip from "@/CAREUI/display/Chip"; -import CountBlock from "@/CAREUI/display/Count"; -import FilterBadge from "@/CAREUI/display/FilterBadge"; -import RecordMeta from "@/CAREUI/display/RecordMeta"; -import CareIcon from "@/CAREUI/icons/CareIcon"; -import { AdvancedFilterButton } from "@/CAREUI/interactive/FiltersSlideover"; - import { Avatar } from "@/components/Common/Avatar"; import ButtonV2 from "@/components/Common/ButtonV2"; import { ExportMenu } from "@/components/Common/Export"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; +import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields"; import SortDropdownMenu from "@/components/Common/SortDropdown"; import Tabs from "@/components/Common/Tabs"; -import { ICD11DiagnosisModel } from "@/components/Diagnosis/types"; -import { getDiagnosesByIds } from "@/components/Diagnosis/utils"; -import FacilitiesSelectDialogue from "@/components/ExternalResult/FacilitiesSelectDialogue"; -import DoctorVideoSlideover from "@/components/Facility/DoctorVideoSlideover"; -import { FacilityModel, PatientCategory } from "@/components/Facility/models"; -import { PhoneNumberValidator } from "@/components/Form/FieldValidators"; -import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField"; -import { FieldChangeEvent } from "@/components/Form/FormFields/Utils"; -import SearchInput from "@/components/Form/SearchInput"; -import { - DIAGNOSES_FILTER_LABELS, - DiagnosesFilterKey, - FILTER_BY_DIAGNOSES_KEYS, -} from "@/components/Patient/DiagnosesFilter"; -import PatientFilter from "@/components/Patient/PatientFilter"; -import { isPatientMandatoryDataFilled } from "@/components/Patient/Utils"; import useAuthUser from "@/hooks/useAuthUser"; import useFilters from "@/hooks/useFilters"; @@ -49,17 +27,36 @@ import { } from "@/common/constants"; import { parseOptionId } from "@/common/utils"; -import { triggerGoal } from "@/Integrations/Plausible"; -import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; -import useQuery from "@/Utils/request/useQuery"; + +import Chip from "../../CAREUI/display/Chip"; +import CountBlock from "../../CAREUI/display/Count"; +import FilterBadge from "../../CAREUI/display/FilterBadge"; +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; +import { triggerGoal } from "../../Integrations/Plausible"; +import * as Notification from "../../Utils/Notifications"; +import request from "../../Utils/request/request"; +import useQuery from "../../Utils/request/useQuery"; import { formatPatientAge, humanizeStrings, isAntenatal, parsePhoneNumber, -} from "@/Utils/utils"; +} from "../../Utils/utils"; +import { ICD11DiagnosisModel } from "../Diagnosis/types"; +import { getDiagnosesByIds } from "../Diagnosis/utils"; +import FacilitiesSelectDialogue from "../ExternalResult/FacilitiesSelectDialogue"; +import DoctorVideoSlideover from "../Facility/DoctorVideoSlideover"; +import { FacilityModel, PatientCategory } from "../Facility/models"; +import { + DIAGNOSES_FILTER_LABELS, + DiagnosesFilterKey, + FILTER_BY_DIAGNOSES_KEYS, +} from "./DiagnosesFilter"; +import PatientFilter from "./PatientFilter"; +import { isPatientMandatoryDataFilled } from "./Utils"; interface TabPanelProps { children?: ReactNode; @@ -93,6 +90,7 @@ export const PatientManager = () => { Pagination, FilterBadges, resultsPerPage, + clearSearch, } = useFilters({ limit: 12, cacheBlacklist: [ @@ -109,30 +107,6 @@ export const PatientManager = () => { const [diagnoses, setDiagnoses] = useState([]); const [showDialog, setShowDialog] = useState<"create" | "list-discharged">(); const [showDoctors, setShowDoctors] = useState(false); - const [phoneNumber, _setPhoneNumber] = useState(""); - const [emergencyPhoneNumber, _setEmergencyPhoneNumber] = useState(""); - - const setPhoneNumber = (value: string) => { - _setPhoneNumber(value); - const error = PhoneNumberValidator()(value); - if (!error) { - updateQuery({ phone_number: value }); - } - if ((value === "+91" || value === "") && qParams.phone_number) { - updateQuery({ phone_number: null }); - } - }; - - const setEmergencyPhoneNumber = (value: string) => { - _setEmergencyPhoneNumber(value); - const error = PhoneNumberValidator()(value); - if (!error) { - updateQuery({ emergency_phone_number: value }); - } - if ((value === "+91" || value === "") && qParams.emergency_phone_number) { - updateQuery({ emergency_phone_number: null }); - } - }; const tabValue = qParams.last_consultation__new_discharge_reason || @@ -319,14 +293,6 @@ export const PatientManager = () => { const { loading: isLoading, data } = useQuery(routes.patientList, { query: params, - onResponse: () => { - if (!params.phone_number) { - _setPhoneNumber("+91"); - } - if (!params.emergency_phone_number) { - _setEmergencyPhoneNumber("+91"); - } - }, }); const getTheCategoryFromId = () => { @@ -779,22 +745,74 @@ export const PatientManager = () => { ); } - const queryField = (name: string, defaultValue?: T) => { - return { - name, - value: qParams[name] || defaultValue, - onChange: (e: FieldChangeEvent) => updateQuery({ [e.name]: e.value }), - }; - }; - const onlyAccessibleFacility = permittedFacilities?.count === 1 ? permittedFacilities.results[0] : null; + const searchOptions = [ + { + key: "phone_number", + label: "Phone Number", + type: "phone" as const, + placeholder: "Search_by_phone_number", + value: qParams.phone_number || "", + shortcutKey: "p", + }, + { + key: "name", + label: "Name", + type: "text" as const, + placeholder: "search_by_patient_name", + value: qParams.name || "", + shortcutKey: "n", + }, + { + key: "patient_no", + label: "IP/OP No", + type: "text" as const, + placeholder: "search_by_patient_no", + value: qParams.patient_no || "", + shortcutKey: "u", + }, + { + key: "emergency_contact_number", + label: "Emergency Contact Phone Number", + type: "phone" as const, + placeholder: "search_by_emergency_phone_number", + value: qParams.emergency_phone_number || "", + shortcutKey: "e", + }, + ]; + + const handleSearch = useCallback( + (key: string, value: string) => { + const updatedQuery = { + phone_number: + key === "phone_number" + ? value.length >= 13 || value === "" + ? value + : undefined + : undefined, + name: key === "name" ? value : undefined, + patient_no: key === "patient_no" ? value : undefined, + emergency_phone_number: + key === "emergency_contact_number" + ? value.length >= 13 || value === "" + ? value + : undefined + : undefined, + }; + + updateQuery(updatedQuery); + }, + [updateQuery], + ); + return (
@@ -973,59 +991,23 @@ export const PatientManager = () => { }} /> -
-
-
- -
-
-
-
-
- - -
-
- setPhoneNumber(e.value)} - types={["mobile", "landline"]} - className="w-full grow" - error={((phoneNumber || "+91") === "+91" && "") || undefined} - /> - setEmergencyPhoneNumber(e.value)} - types={["mobile", "landline"]} - className="w-full" - error={ - ((emergencyPhoneNumber || "+91") === "+91" && "") || undefined - } - /> -
-
+
+
+
+ +
, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 00000000000..94698f9b5e7 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 00000000000..dade8c7d2ee --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface InputProps + extends Omit, "onChange"> {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 00000000000..a115d28af1e --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as LabelPrimitive from "@radix-ui/react-label"; +import { type VariantProps, cva } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 00000000000..466e7226e36 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverAnchor = PopoverPrimitive.Anchor; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 00000000000..d0c4d3ac63b --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/src/hooks/useFilters.tsx b/src/hooks/useFilters.tsx index b67474045ef..63ffa58822e 100644 --- a/src/hooks/useFilters.tsx +++ b/src/hooks/useFilters.tsx @@ -34,6 +34,10 @@ export default function useFilters({ const hasPagination = limit > 0; const [showFilters, setShowFilters] = useState(false); const [qParams, _setQueryParams] = useQueryParams(); + const [clearSearch, setClearSearch] = useState<{ + value: boolean; + params?: string[]; + }>({ value: false }); const updateCache = (query: QueryParam) => { const blacklist = FILTERS_CACHE_BLACKLIST.concat(cacheBlacklist); @@ -63,6 +67,7 @@ export default function useFilters({ const updateQuery = (filter: FilterState) => { filter = hasPagination ? { page: 1, limit, ...filter } : filter; setQueryParams(Object.assign({}, qParams, filter), { replace: true }); + setClearSearch({ value: false }); }; const updatePage = (page: number) => { if (!hasPagination) return; @@ -71,6 +76,7 @@ export default function useFilters({ const removeFilters = (params?: string[]) => { params ??= Object.keys(qParams); setQueryParams(removeFromQuery(qParams, params)); + setClearSearch({ value: true, params: params }); }; const removeFilter = (param: string) => removeFilters([param]); @@ -203,7 +209,7 @@ export default function useFilters({ return (
{compiledBadges.map((props) => ( @@ -268,6 +274,7 @@ export default function useFilters({ * @param param is the key of the filter to be removed. */ removeFilter, + clearSearch, /** * Removes multiple filters from query param