diff --git a/.env b/.env index 2c8b73fb65a..3c4b26fdad4 100644 --- a/.env +++ b/.env @@ -5,6 +5,8 @@ REACT_APP_META_DESCRIPTION="Revolutionizing EMR with AI: Open Healthcare Network REACT_APP_COVER_IMAGE=https://cdn.ohc.network/care_logo.svg REACT_APP_COVER_IMAGE_ALT=https://cdn.ohc.network/care_logo.svg REACT_PUBLIC_URL=https://care.ohc.network +HEADERS="/*\n Strict-Transport-Security: max-age=63072000; includeSubDomains; preload\n X-XSS-Protection: 1; mode=block\n X-Frame-Options: SAMEORIGIN\n X-Content-Type-Options: nosniff\n Referrer-Policy: strict-origin-when-cross-origin\n Permissions-Policy: geolocation=(self), microphone=()" +REACT_RECAPTCHA_SITE_KEY=6LcedK8qAAAAAM2PpuqlqhZUxQpmIqHqluL74dDs # Care API URL without the /api prefix REACT_CARE_API_URL=https://careapi.ohc.network diff --git a/.example.env b/.example.env index 04055c21c5e..862f05eb5c4 100644 --- a/.example.env +++ b/.example.env @@ -35,9 +35,6 @@ REACT_CUSTOM_DESCRIPTION= # Google Maps API key REACT_GMAPS_API_KEY= -# Government data API key -REACT_GOV_DATA_API_KEY= - # reCAPTCHA site key REACT_RECAPTCHA_SITE_KEY= @@ -47,16 +44,9 @@ REACT_SENTRY_DSN= # Sentry environment (default: staging) REACT_SENTRY_ENVIRONMENT= -# Camera feed, still watching idle timeout (in seconds; default: 180) -REACT_STILL_WATCHING_IDLE_TIMEOUT= - -# Camera feed, still watching prompt duration (in seconds; default: 30) -REACT_STILL_WATCHING_PROMPT_DURATION= - # Feature flags REACT_ENABLE_HCX=true REACT_ENABLE_ABDM=true -REACT_WARTIME_SHIFTING=true # JWT token refresh interval (in milliseconds) (default: 5 minutes) REACT_JWT_TOKEN_REFRESH_INTERVAL= diff --git a/.gitignore b/.gitignore index 17e2f8f71d7..fb30d613042 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ src/pluginMap.ts # Federation Temp files /.__mf__temp public/sbom/* +public/_headers diff --git a/care.config.ts b/care.config.ts index 6813a60b331..d99d6fe73cd 100644 --- a/care.config.ts +++ b/care.config.ts @@ -58,22 +58,7 @@ const careConfig = { gmapsApiKey: env.REACT_GMAPS_API_KEY || "AIzaSyDsBAc3y7deI5ZO3NtK5GuzKwtUzQNJNUk", - govDataApiKey: - env.REACT_GOV_DATA_API_KEY || - "579b464db66ec23bdd000001cdd3946e44ce4aad7209ff7b23ac571b", - reCaptchaSiteKey: - env.REACT_RECAPTCHA_SITE_KEY || "6LdvxuQUAAAAADDWVflgBqyHGfq-xmvNJaToM0pN", - - wartimeShifting: boolean("REACT_WARTIME_SHIFTING"), - - stillWatching: { - idleTimeout: env.REACT_STILL_WATCHING_IDLE_TIMEOUT - ? parseInt(env.REACT_STILL_WATCHING_IDLE_TIMEOUT) - : 3 * 60, - promptDuration: env.REACT_STILL_WATCHING_PROMPT_DURATION - ? parseInt(env.REACT_STILL_WATCHING_PROMPT_DURATION) - : 30, - }, + reCaptchaSiteKey: env.REACT_RECAPTCHA_SITE_KEY, auth: { tokenRefreshInterval: env.REACT_JWT_TOKEN_REFRESH_INTERVAL diff --git a/cypress/e2e/patient_spec/patient_creation.cy.ts b/cypress/e2e/patient_spec/patient_creation.cy.ts index 8f2c1606c38..dc237fdd1bf 100644 --- a/cypress/e2e/patient_spec/patient_creation.cy.ts +++ b/cypress/e2e/patient_spec/patient_creation.cy.ts @@ -106,7 +106,7 @@ describe("Patient Management", () => { patientTestCases.forEach(({ description, data }) => { it(`creates a new ${description} and verifies registration`, () => { - facilityCreation.selectFacility("GHC Trikaripur"); + facilityCreation.selectFacility("GHC payyanur"); patientCreation .clickSearchPatients() .clickCreateNewPatient() @@ -134,7 +134,7 @@ describe("Patient Management", () => { }); it("Search patient with phone number and create a new encounter", () => { - facilityCreation.selectFacility("GHC Trikaripur"); + facilityCreation.selectFacility("GHC payyanur"); patientCreation .clickSearchPatients() .searchPatient(TEST_PHONE) @@ -167,7 +167,7 @@ describe("Patient Management", () => { address: generateAddress(true), }; - facilityCreation.selectFacility("GHC Trikaripur"); + facilityCreation.selectFacility("GHC payyanur"); patientEncounter .navigateToEncounters() .openFirstEncounterDetails() diff --git a/cypress/e2e/patient_spec/patient_details.cy.ts b/cypress/e2e/patient_spec/patient_details.cy.ts index 35732dbecd0..d96baf9bfef 100644 --- a/cypress/e2e/patient_spec/patient_details.cy.ts +++ b/cypress/e2e/patient_spec/patient_details.cy.ts @@ -15,7 +15,7 @@ describe("Patient Management", () => { it("Assign users to a patient", () => { const userName = "nihal-nurse"; const userRole = "Nurse"; - facilityCreation.selectFacility("GHC Trikaripur"); + facilityCreation.selectFacility("GHC payyanur"); patientEncounter .navigateToEncounters() .openFirstEncounterDetails() diff --git a/cypress/e2e/patient_spec/patient_encounter.cy.ts b/cypress/e2e/patient_spec/patient_encounter.cy.ts index 6fd08494d92..2569b9567bc 100644 --- a/cypress/e2e/patient_spec/patient_encounter.cy.ts +++ b/cypress/e2e/patient_spec/patient_encounter.cy.ts @@ -15,7 +15,7 @@ describe("Patient Encounter Questionnaire", () => { pco2: "120", po2: "80", }; - facilityCreation.selectFacility("GHC Trikaripur"); + facilityCreation.selectFacility("GHC payyanur"); // Chain the methods instead of multiple separate calls patientEncounter diff --git a/cypress/pageObject/Patients/PatientEncounter.ts b/cypress/pageObject/Patients/PatientEncounter.ts index 95e3b8c1fe0..e38abe91b40 100644 --- a/cypress/pageObject/Patients/PatientEncounter.ts +++ b/cypress/pageObject/Patients/PatientEncounter.ts @@ -14,7 +14,6 @@ export class PatientEncounter { } clickUpdateEncounter() { - cy.verifyAndClickElement('[data-cy="update-encounter-button"]', "Update"); cy.verifyAndClickElement( '[data-cy="update-encounter-option"]', "Update Encounter", @@ -76,10 +75,10 @@ export class PatientEncounter { } clickPatientDetailsButton() { - cy.verifyAndClickElement( - '[data-cy="patient-details-button"]', - "Patient Details", - ); + cy.get('[data-cy="patient-details-button"]') + .filter(":visible") + .first() + .click(); return this; } diff --git a/cypress/pageObject/Users/UserCreation.ts b/cypress/pageObject/Users/UserCreation.ts index fb2a2a180f0..016dc948560 100644 --- a/cypress/pageObject/Users/UserCreation.ts +++ b/cypress/pageObject/Users/UserCreation.ts @@ -72,7 +72,7 @@ export class UserCreation { { label: "Confirm Password", message: "Required" }, { label: "Phone Number", - message: "Invalid input", + message: "This field is required", }, { label: "State", message: "Required" }, ]); diff --git a/cypress/pageObject/facility/FacilityCreation.ts b/cypress/pageObject/facility/FacilityCreation.ts index 054cb3539e9..c7182aa5f1d 100644 --- a/cypress/pageObject/facility/FacilityCreation.ts +++ b/cypress/pageObject/facility/FacilityCreation.ts @@ -94,7 +94,7 @@ export class FacilityCreation { { label: "Address", message: "Address is required" }, { label: "Phone Number", - message: "Invalid input", + message: "This field is required", }, { label: "Pincode", message: "Invalid Pincode" }, ]); diff --git a/package-lock.json b/package-lock.json index fef9044fed8..bc5266174fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "care_fe", - "version": "2.10.0", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "care_fe", - "version": "2.10.0", + "version": "3.2.0", "hasInstallScript": true, "license": "MIT", "dependencies": { "@fontsource/figtree": "^5.1.1", "@headlessui/react": "^2.2.0", - "@hookform/resolvers": "^3.10.0", + "@hookform/resolvers": "^4.0.0", "@originjs/vite-plugin-federation": "^1.3.7", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.2", @@ -35,7 +35,7 @@ "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.6", - "@sentry/browser": "^8.51.0", + "@sentry/browser": "^9.0.0", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.64.2", "@vitejs/plugin-react": "^4.3.4", @@ -58,7 +58,7 @@ "input-otp": "^1.4.2", "jspdf": "^2.5.2", "libphonenumber-js": "^1.11.18", - "lucide-react": "^0.474.0", + "lucide-react": "^0.475.0", "markdown-it": "^14.1.0", "next-themes": "^0.4.3", "postcss-loader": "^8.1.1", @@ -78,8 +78,7 @@ "sonner": "^1.7.2", "tailwind-merge": "^3.0.0", "tailwindcss-animate": "^1.0.7", - "use-keyboard-shortcut": "^1.1.6", - "zod": "^3.23.8" + "use-keyboard-shortcut": "^1.1.6" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -88,7 +87,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", - "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@trivago/prettier-plugin-sort-imports": "^5.0.0", "@types/dompurify": "^3.0.5", "@types/events": "^3.0.3", "@types/google.maps": "^3.58.1", @@ -144,8 +143,8 @@ "optionalDependencies": { "@esbuild/linux-arm64": "latest", "@esbuild/linux-x64": "latest", - "@rollup/rollup-linux-arm64-gnu": "4.34.4", - "@rollup/rollup-linux-x64-gnu": "4.34.4" + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8" } }, "node_modules/@actions/core": { @@ -296,74 +295,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/core/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -374,15 +305,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", - "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", - "dev": true, + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" @@ -401,20 +333,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", @@ -462,79 +380,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -611,181 +456,157 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-environment-visitor/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { + "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, "engines": { - "node": ">=6" + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-imports": { + "node_modules/@babel/helper-wrap-function": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, "license": "MIT", "dependencies": { + "@babel/template": "^7.25.9", "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" }, @@ -793,606 +614,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-module-transforms/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-replace-supers/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/helpers": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", @@ -1406,26 +627,13 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.26.9" }, "bin": { "parser": "bin/babel-parser.js" @@ -1434,19 +642,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/parser/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", @@ -1464,79 +659,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", @@ -1604,79 +726,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -1789,79 +838,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-async-generator-functions/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", @@ -1964,57 +940,7 @@ "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-classes/node_modules/globals": { @@ -2027,19 +953,6 @@ "node": ">=4" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", @@ -2206,79 +1119,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-function-name/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", @@ -2396,79 +1236,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", @@ -3036,116 +1803,54 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/traverse/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/@babel/traverse/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -3920,9 +2625,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", - "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", "dev": true, "license": "MIT", "engines": { @@ -4042,10 +2747,13 @@ } }, "node_modules/@hookform/resolvers": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", - "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.0.tgz", + "integrity": "sha512-fX/uHKb+OOCpACLc6enuTQsf0ZpRrKbeBBPETg5PCPLCIYV6osP2Bw6ezuclM61lH+wBF9eXcuC0+BFh9XOEnQ==", "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001698" + }, "peerDependencies": { "react-hook-form": "^7.0.0" } @@ -5877,9 +4585,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.4.tgz", - "integrity": "sha512-7CwSJW+sEhM9sESEk+pEREF2JL0BmyCro8UyTq0Kyh0nu1v0QPNY3yfLPFKChzVoUmaKj8zbdgBxUhBRR+xGxg==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", "cpu": [ "arm64" ], @@ -5955,9 +4663,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.4.tgz", - "integrity": "sha512-JGejzEfVzqc/XNiCKZj14eb6s5w8DdWlnQ5tWUbs99kkdvfq9btxxVX97AaxiUX7xJTKFA0LwoS0KU8C2faZRg==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", "cpu": [ "x64" ], @@ -6020,53 +4728,53 @@ ] }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.54.0.tgz", - "integrity": "sha512-DKWCqb4YQosKn6aD45fhKyzhkdG7N6goGFDeyTaJFREJDFVDXiNDsYZu30nJ6BxMM7uQIaARhPAC5BXfoED3pQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.1.0.tgz", + "integrity": "sha512-S1uT+kkFlstWpwnaBTIJSwwAID8PS3aA0fIidOjNezeoUE5gOvpsjDATo9q+sl6FbGWynxMz6EnYSrq/5tuaBQ==", "license": "MIT", "dependencies": { - "@sentry/core": "8.54.0" + "@sentry/core": "9.1.0" }, "engines": { - "node": ">=14.18" + "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.54.0.tgz", - "integrity": "sha512-nQqRacOXoElpE0L0ADxUUII0I3A94niqG9Z4Fmsw6057QvyrV/LvTiMQBop6r5qLjwMqK+T33iR4/NQI5RhsXQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.1.0.tgz", + "integrity": "sha512-jTDCqkqH3QDC8m9WO4mB06hqnBRsl3p7ozoh0E774UvNB6blOEZjShhSGMMEy5jbbJajPWsOivCofUtFAwbfGw==", "license": "MIT", "dependencies": { - "@sentry/core": "8.54.0" + "@sentry/core": "9.1.0" }, "engines": { - "node": ">=14.18" + "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.54.0.tgz", - "integrity": "sha512-8xuBe06IaYIGJec53wUC12tY2q4z2Z0RPS2s1sLtbA00EvK1YDGuXp96IDD+HB9mnDMrQ/jW5f97g9TvPsPQUg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.1.0.tgz", + "integrity": "sha512-E2xrUoms90qvm0BVOuaZ8QfkMoTUEgoIW/35uOeaqNcL7uOIj8c5cSEQQKit2Dr7CL6W+Ci5c6Khdyd5C0NL5w==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.54.0", - "@sentry/core": "8.54.0" + "@sentry-internal/browser-utils": "9.1.0", + "@sentry/core": "9.1.0" }, "engines": { - "node": ">=14.18" + "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.54.0.tgz", - "integrity": "sha512-K/On3OAUBeq/TV2n+1EvObKC+WMV9npVXpVyJqCCyn8HYMm8FUGzuxeajzm0mlW4wDTPCQor6mK9/IgOquUzCw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.1.0.tgz", + "integrity": "sha512-gxredVe+mOgfNqDJ3dTLiRON3FK1rZ8d0LHp7TICK/umLkWFkuso0DbNeyKU+3XCEjCr9VM7ZRqTDMzmY6zyVg==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.54.0", - "@sentry/core": "8.54.0" + "@sentry-internal/replay": "9.1.0", + "@sentry/core": "9.1.0" }, "engines": { - "node": ">=14.18" + "node": ">=18" } }, "node_modules/@sentry-internal/tracing": { @@ -6099,28 +4807,28 @@ } }, "node_modules/@sentry/browser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.54.0.tgz", - "integrity": "sha512-BgUtvxFHin0fS0CmJVKTLXXZcke0Av729IVfi+2fJ4COX8HO7/HAP02RKaSQGmL2HmvWYTfNZ7529AnUtrM4Rg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.1.0.tgz", + "integrity": "sha512-G55e5j77DqRW3LkalJLAjRRfuyKrjHaKTnwIYXa6ycO+Q1+l14pEUxu+eK5Abu2rtSdViwRSb5/G6a/miSUlYA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.54.0", - "@sentry-internal/feedback": "8.54.0", - "@sentry-internal/replay": "8.54.0", - "@sentry-internal/replay-canvas": "8.54.0", - "@sentry/core": "8.54.0" + "@sentry-internal/browser-utils": "9.1.0", + "@sentry-internal/feedback": "9.1.0", + "@sentry-internal/replay": "9.1.0", + "@sentry-internal/replay-canvas": "9.1.0", + "@sentry/core": "9.1.0" }, "engines": { - "node": ">=14.18" + "node": ">=18" } }, "node_modules/@sentry/core": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.54.0.tgz", - "integrity": "sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.1.0.tgz", + "integrity": "sha512-djWEzSBpMgqdF3GQuxO+kXCUX+Mgq42G4Uah/HSUBvPDHKipMmyWlutGRoFyVPPOnCDgpHu3wCt83wbpEyVmDw==", "license": "MIT", "engines": { - "node": ">=14.18" + "node": ">=18" } }, "node_modules/@sentry/integrations": { @@ -6279,9 +4987,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.66.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.0.tgz", - "integrity": "sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw==", + "version": "5.66.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz", + "integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==", "license": "MIT", "funding": { "type": "github", @@ -6299,12 +5007,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.66.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.0.tgz", - "integrity": "sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw==", + "version": "5.66.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.5.tgz", + "integrity": "sha512-D9aABj3/aFeNmifsdllh5O3hPyA8gUnZ1jAV8MjODQ7blirfAyGed9NjAnm8rgEdr1wChyjTT738ij3vY0EREQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.66.0" + "@tanstack/query-core": "5.66.4" }, "funding": { "type": "github", @@ -6315,9 +5023,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.66.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.0.tgz", - "integrity": "sha512-uB57wA2YZaQ2fPcFW0E9O1zAGDGSbRKRx84uMk/86VyU9jWVxvJ3Uzp+zNm+nZJYsuekCIo2opTdgNuvM3cKgA==", + "version": "5.66.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.5.tgz", + "integrity": "sha512-aj/+IuLdt3fbNweKo2XSK6OMUpOULDUO1MRDUtggRl8W6/EvD7FVvm0ydRrOMvS9KUqC25V88WPYaPj8SEVGXg==", "license": "MIT", "dependencies": { "@tanstack/query-devtools": "5.65.0" @@ -6327,7 +5035,7 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query": "^5.66.5", "react": "^18 || ^19" } }, @@ -6359,26 +5067,37 @@ } }, "node_modules/@trivago/prettier-plugin-sort-imports": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", - "integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", + "integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@babel/generator": "7.17.7", - "@babel/parser": "^7.20.5", - "@babel/traverse": "7.23.2", - "@babel/types": "7.17.0", - "javascript-natural-sort": "0.7.1", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, + "engines": { + "node": ">18.12" + }, "peerDependencies": { "@vue/compiler-sfc": "3.x", - "prettier": "2.x - 3.x" + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" }, "peerDependenciesMeta": { "@vue/compiler-sfc": { "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true } } }, @@ -6423,19 +5142,6 @@ "@types/babel__traverse": "*" } }, - "node_modules/@types/babel__core/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@types/babel__generator": { "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", @@ -6464,19 +5170,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/babel__traverse/node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -6616,9 +5309,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", - "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", + "version": "22.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", + "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -6748,17 +5441,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", + "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/type-utils": "8.24.1", + "@typescript-eslint/utils": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6778,9 +5471,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -6792,13 +5485,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -6836,16 +5529,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", + "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "engines": { @@ -6861,9 +5554,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -6875,14 +5568,14 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6902,13 +5595,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -6946,14 +5639,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", + "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6964,9 +5657,9 @@ } }, "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -6978,13 +5671,13 @@ } }, "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -7009,14 +5702,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", + "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -7033,9 +5726,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -7047,14 +5740,14 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7074,13 +5767,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -7161,16 +5854,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", + "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7185,9 +5878,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -7199,14 +5892,14 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7226,13 +5919,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -8268,9 +6961,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001700", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", + "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "funding": [ { "type": "opencollective", @@ -8900,13 +7593,13 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.0.2.tgz", - "integrity": "sha512-3qqTU2JoVY262qkYg9I2nohwxcfsJk0dSVp/LXAjD94Jz2y6411Mf/l5uHEHiaANrOmMcHbzYgOd/ueDsZlS7A==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.0.3.tgz", + "integrity": "sha512-yIdvobANw3kS+KF/t5vwjjPNufBA8ux7iQHaWxPTkUw2yCKI72m9mKM24eOwE84Wk4ALPsSvEcGbDrwgmhr4RA==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.6", + "@cypress/request": "^3.0.7", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -8971,9 +7664,9 @@ } }, "node_modules/cypress-split": { - "version": "1.24.8", - "resolved": "https://registry.npmjs.org/cypress-split/-/cypress-split-1.24.8.tgz", - "integrity": "sha512-gxsZg7Se3iy6eQuYP0w6munfRFBWP81OKc3WWTKQoxxafFWEp9gu173Nx3zoAEDUIEeYxUS1kwmZ7q5hhMN9og==", + "version": "1.24.13", + "resolved": "https://registry.npmjs.org/cypress-split/-/cypress-split-1.24.13.tgz", + "integrity": "sha512-vu3CHfnXFLUKFWvpEeVo2IQNJe8661ZdtAr9EYySLxaqs1H3I3yNDmQklEv3wF/ZDdQKa0r1crka8Umy2w8BsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8982,7 +7675,7 @@ "console.table": "^0.10.0", "debug": "^4.3.4", "fast-shuffle": "^6.1.0", - "find-cypress-specs": "1.46.2", + "find-cypress-specs": "1.47.7", "globby": "^11.1.0", "humanize-duration": "^3.28.0" }, @@ -10134,18 +8827,18 @@ } }, "node_modules/eslint": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", - "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.19.0", + "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -10368,6 +9061,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -10833,9 +9539,9 @@ } }, "node_modules/find-cypress-specs": { - "version": "1.46.2", - "resolved": "https://registry.npmjs.org/find-cypress-specs/-/find-cypress-specs-1.46.2.tgz", - "integrity": "sha512-eXvGQMftYDTbQflVX6TzJj7hA0ypO5+FfUrnMdRf1+yCu5xVY5wPv721AWp/xPBOpQXwx5jdoZRH9m95U/1c7A==", + "version": "1.47.7", + "resolved": "https://registry.npmjs.org/find-cypress-specs/-/find-cypress-specs-1.47.7.tgz", + "integrity": "sha512-l44j22oQRBcgP79Fj9MMMGUQ3T0950sHRB1yOncGdZAqZKjykBLf/sn0u7sGcUf76+pgknsA5n2ehVIHoMqOuA==", "dev": true, "license": "MIT", "dependencies": { @@ -10843,7 +9549,7 @@ "arg": "^5.0.1", "console.table": "^0.10.0", "debug": "^4.3.3", - "find-test-names": "1.28.30", + "find-test-names": "1.29.5", "globby": "^11.1.0", "minimatch": "^3.0.4", "pluralize": "^8.0.0", @@ -10884,9 +9590,9 @@ } }, "node_modules/find-test-names": { - "version": "1.28.30", - "resolved": "https://registry.npmjs.org/find-test-names/-/find-test-names-1.28.30.tgz", - "integrity": "sha512-b5PLJ5WnskdaYHBf+38FN/4TKh5lqwrltITkqxuARsN2bW6civrhqOXbVA+4727YNowYLt/jtIC9Dsn7eJSP6A==", + "version": "1.29.5", + "resolved": "https://registry.npmjs.org/find-test-names/-/find-test-names-1.29.5.tgz", + "integrity": "sha512-PsD3qx5miZKhT/766UGAgmApU8NwGAqUKPwIRVOuYgjKHOvWzL5OYbHJIhKC0nVvhVedIOvH1SwJG/Df8EN/8w==", "dev": true, "license": "MIT", "dependencies": { @@ -11384,9 +10090,9 @@ } }, "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", "engines": { @@ -11605,9 +10311,9 @@ } }, "node_modules/html-to-image": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", - "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==", + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", "license": "MIT" }, "node_modules/html2canvas": { @@ -11730,9 +10436,9 @@ } }, "node_modules/i18next-browser-languagedetector": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz", - "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.3.tgz", + "integrity": "sha512-beOOLArattPBc2YZG5IXGJytdYFgUR7cS8Wd6HT4IczIoWKgmTspOQ2yasaGklelVo5seLPmnEKvLHR+E/MdWQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2" @@ -12681,16 +11387,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -12876,9 +11581,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.19.tgz", - "integrity": "sha512-bW/Yp/9dod6fmyR+XqSUL1N5JE7QRxQ3KrBIbYS1FTv32e5i3SEtQVX+71CYNv8maWNSOgnlCoNp9X78f/cKiA==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.20.tgz", + "integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==", "license": "MIT" }, "node_modules/lie": { @@ -13716,9 +12421,9 @@ } }, "node_modules/lucide-react": { - "version": "0.474.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz", - "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==", + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -13779,9 +12484,9 @@ } }, "node_modules/marked": { - "version": "15.0.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz", - "integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==", + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz", + "integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==", "dev": true, "license": "MIT", "bin": { @@ -14803,9 +13508,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", "funding": [ { "type": "opencollective", @@ -15161,9 +13866,9 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", "dev": true, "license": "MIT", "bin": { @@ -16957,9 +15662,9 @@ "license": "MIT" }, "node_modules/snyk": { - "version": "1.1295.2", - "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1295.2.tgz", - "integrity": "sha512-Unl6HD/XF2DfDEj4pySiQF5bUkc6yS/tljtO9LHgjJjfj4CRGZdNs2HmtfEJHsnda3pqBEUO71L7PXhgouzYGw==", + "version": "1.1295.3", + "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1295.3.tgz", + "integrity": "sha512-PFPetFXVMx5kt49WraLbr1Lw36jikktxQeOjaK3y/0Bjgm1e1rRjTsKYI83lINhkxUuSRBf9MCm8QUcOEYZiaw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -16984,16 +15689,6 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -17967,15 +16662,6 @@ "node": ">=14.14" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -20846,9 +19532,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index a84562ddae6..d4d8f582882 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "care_fe", - "version": "2.10.0", + "version": "3.2.0", "description": "Care is a Digital Public Good enabling TeleICU & Decentralised Administration of Healthcare Capacity across States.", "private": true, "repository": { @@ -35,7 +35,7 @@ "supported-browsers": "node ./scripts/generate-supported-browsers.mjs", "build": "npm run setup && npm run build:meta && npm run supported-browsers && npm run build:react", "setup": "tsx scripts/setup-care-apps.ts", - "postinstall": "tsx scripts/install-platform-deps.ts && tsx scripts/generate-sbom-data.ts", + "postinstall": "tsx scripts/install-platform-deps.ts && tsx scripts/generate-sbom-data.ts && tsx scripts/generate-headers.ts", "test": "snyk test", "cypress:open": "cross-env NODE_ENV=development cypress open", "cypress:run": "cross-env NODE_ENV=development cypress run", @@ -51,7 +51,7 @@ "dependencies": { "@fontsource/figtree": "^5.1.1", "@headlessui/react": "^2.2.0", - "@hookform/resolvers": "^3.10.0", + "@hookform/resolvers": "^4.0.0", "@originjs/vite-plugin-federation": "^1.3.7", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.2", @@ -74,7 +74,7 @@ "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.6", - "@sentry/browser": "^8.51.0", + "@sentry/browser": "^9.0.0", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.64.2", "@vitejs/plugin-react": "^4.3.4", @@ -97,7 +97,7 @@ "input-otp": "^1.4.2", "jspdf": "^2.5.2", "libphonenumber-js": "^1.11.18", - "lucide-react": "^0.474.0", + "lucide-react": "^0.475.0", "markdown-it": "^14.1.0", "next-themes": "^0.4.3", "postcss-loader": "^8.1.1", @@ -127,7 +127,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", - "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@trivago/prettier-plugin-sort-imports": "^5.0.0", "@types/dompurify": "^3.0.5", "@types/events": "^3.0.3", "@types/google.maps": "^3.58.1", @@ -180,8 +180,8 @@ "optionalDependencies": { "@esbuild/linux-arm64": "latest", "@esbuild/linux-x64": "latest", - "@rollup/rollup-linux-arm64-gnu": "4.34.4", - "@rollup/rollup-linux-x64-gnu": "4.34.4" + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8" }, "browserslist": { "production": [ @@ -199,7 +199,6 @@ "*.{ts,tsx,js,jsx}": [ "prettier --write --ignore-unknown --plugin prettier-plugin-tailwindcss --plugin @trivago/prettier-plugin-sort-imports", "eslint --fix", - "unimported", "git update-index --again" ], "public/locale/*.json": [ @@ -210,4 +209,4 @@ "node": ">=22.8.0" }, "packageManager": "npm@10.9.2" -} \ No newline at end of file +} diff --git a/public/locale/en.json b/public/locale/en.json index 7dc753cf5b3..af153bb4806 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -48,6 +48,7 @@ "DOMESTIC_HEALTHCARE_SUPPORT__NO_SUPPORT": "No support", "DOMESTIC_HEALTHCARE_SUPPORT__PAID_CAREGIVER": "Paid caregiver", "ENCOUNTER_TAB__abdm": "ABDM Records", + "ENCOUNTER_TAB__claims": "Insurance Claims", "ENCOUNTER_TAB__feed": "Feed", "ENCOUNTER_TAB__files": "Files", "ENCOUNTER_TAB__medicines": "Medicines", @@ -290,6 +291,7 @@ "add_beds_to_configure_presets": "Add beds to this location to configure presets for them.", "add_consultation": "Add consultation", "add_consultation_update": "Add Consultation Update", + "add_department_team": "Add Department/Team", "add_details_of_patient": "Add Details of Patient", "add_exception": "Add Exception", "add_facility": "Add Facility", @@ -333,6 +335,7 @@ "administered_on": "Administered on", "administration_dosage_range_error": "Dosage should be between start and target dosage", "administration_notes": "Administration Notes", + "admission_source": "Admission Source", "admit_source": "Admit Source", "admitted": "Admitted", "admitted_on": "Admitted On", @@ -382,13 +385,19 @@ "approved_by_district_covid_control_room": "Approved by District COVID Control Room", "approving_facility": "Name of Approving Facility", "archive": "Archive", + "archive_file": "Archive File", "archived": "Archived", + "archived_at": "Archived at", + "archived_by": "Archived by", + "archived_file": "Archived file", "archived_files": "Archived Files", + "archived_reason": "Archived reason", "are_non_editable_fields": "are non-editable fields", "are_you_still_watching": "Are you still watching?", "are_you_sure_want_to_delete": "Are you sure you want to delete {{name}}?", "are_you_sure_want_to_delete_this_record": "Are you sure want to delete this record?", - "are_you_sure_want_to_remove": "Are you sure you want to remove {{name}} from the patient? This action cannot be undone", + "are_you_sure_want_to_remove": "Are you sure you want to remove {{name}} from the patient? This action cannot be undone", + "are_you_sure_you_want_to_delete_user": "Are you sure you want to delete this user?", "ari": "ARI - Acute Respiratory illness", "arrived": "Arrived", "as_needed_prn": "As Needed / PRN", @@ -410,6 +419,7 @@ "assigned_facility": "Facility assigned", "assigned_to": "Assigned to", "assigned_volunteer": "Assigned Volunteer", + "at_time": "at {{time}}", "atypical_presentation_details": "Atypical presentation details", "audio__allow_permission": "Please allow microphone permission in site settings", "audio__allow_permission_button": "Click here to know how to allow", @@ -478,7 +488,7 @@ "booked_by": "Booked by", "bradycardia": "Bradycardia", "breathlessness_level": "Breathlessness level", - "by": "By", + "by_name": "by {{by}}", "camera": "Camera", "camera_bed_link_success": "Camera linked to bed successfully.", "camera_permission_denied": "Camera Permission denied", @@ -498,7 +508,7 @@ "care_backend": "Care Backend", "care_frontend": "Care Frontend", "category": "Category", - "category_description": "Choose the category that best describes the resource needed.", + "category_description": "Choose the category ", "caution": "Caution", "central_nursing_station": "Central Nursing Station", "change_avatar": "Change Avatar", @@ -557,6 +567,7 @@ "claim__use__claim": "Claim", "claim__use__preauthorization": "Pre Authorization", "claims": "Claims", + "class_history": "Class History", "clear": "Clear", "clear_all_filters": "Clear all filters", "clear_home_facility": "Clear Home Facility", @@ -631,12 +642,16 @@ "create_add_more": "Create & Add More", "create_asset": "Create Asset", "create_consultation": "Create Consultation", + "create_department_team": "Create Department/Team", + "create_department_team_description": "Create a new department/team in this facility.", "create_encounter": "Create Encounter", "create_facility": "Create Facility", + "create_new": "Create New", "create_new_asset": "Create New Asset", "create_new_encounter": "Create a new encounter to get started", "create_new_facility": "Create a new facility and add it to the organization.", "create_new_tag": "Create New Tag", + "create_organization": "Create Organization", "create_position_preset": "Create a new position preset", "create_position_preset_description": "Creates a new position preset in Care from the current position of the camera for the given name", "create_preset_prerequisite": "To create presets for this bed, you'll need to link the camera to the bed first.", @@ -660,6 +675,7 @@ "current_password": "Current Password", "current_role": "Current Role", "current_status": "Current Status", + "custom_options": "Custom Options", "customer_support_email": "Customer Support Email", "customer_support_name": "Customer Support Name", "customer_support_number": "Customer support number", @@ -688,13 +704,17 @@ "delete_account_btn": "Yes, delete this account", "delete_account_note": "Deleting this account will remove all associated data and cannot be undone.", "delete_facility": "Delete Facility", + "delete_facility_confirmation": "Are you sure you want to delete {{name}}? This action cannot be undone.", "delete_item": "Delete {{name}}", "delete_record": "Delete Record", + "delete_user": "Delete User", "deleting": "Deleting...", "demography": "Demography", "denied_on": "Denied On", + "department": "Department", "departments": "Departments", "departments_and_teams": "Departments and Teams", + "departments_or_teams": "Departments/Teams", "describe_why_the_asset_is_not_working": "Describe why the asset is not working", "description": "Description", "details_about_the_equipment": "Details about the equipment", @@ -712,6 +732,8 @@ "diagnosis_already_added": "This diagnosis was already added", "diagnosis_at_discharge": "Diagnosis at Discharge", "diagnosis_empty_message": "No diagnoses recorded", + "diagnosis_status_placeholder": "Select diagnosis status", + "diagnosis_verification_placeholder": "Select verification status", "diastolic": "Diastolic", "didnt_receive_a_message": "Didn't receive a message?", "diet_preference": "Diet Preference", @@ -723,7 +745,10 @@ "discharge_disposition": "Discharge Disposition", "discharge_from_care": "Discharge from CARE", "discharge_prescription": "Discharge Prescription", + "discharge_summaries": "Discharge Summaries", "discharge_summary": "Discharge Summary", + "discharge_summary_emailed": "Discharged Summary emailed", + "discharge_summary_generated": "Discharge Summary generated", "discharge_summary_not_ready": "Discharge summary is not ready yet.", "discharged": "Discharged", "discharged_on": "Discharged On", @@ -731,6 +756,7 @@ "discharged_patients_empty": "No discharged patients present in this facility", "discharged_to": "Discharged to", "disclaimer": "Disclaimer", + "disclaimer_computer_generated_summary": "Disclaimer: This is a computer generated summary and may not be 100% accurate. Please verify the details before using it.", "discontinue": "Discontinue", "discontinue_caution_note": "Are you sure you want to discontinue this prescription?", "discontinued": "Discontinued", @@ -756,7 +782,7 @@ "dosage_instructions": "Dosage Instructions", "down": "Down", "download": "Download", - "download_discharge_summary": "Download discharge summary", + "download_discharge_summary": "Download Discharge Summary", "download_type": "Download Type", "downloading": "Downloading", "downloading_abha_card": "Generating ABHA Card, Please hold on", @@ -871,6 +897,7 @@ "encounter_notes__failed_send_message": "Failed to send message", "encounter_notes__new": "New", "encounter_notes__no_discussions": "No discussions yet", + "encounter_notes__no_unused_threads": "Please enter a custom title for thread", "encounter_notes__select_create_thread": "Select or create a thread to start messaging", "encounter_notes__start_conversation": "Start the Conversation", "encounter_notes__start_new_discussion": "Start New Discussion", @@ -912,13 +939,17 @@ "encounter_suggestion__OP": "Out-patient visit", "encounter_suggestion__R": "Consultation", "encounter_suggestion_edit_disallowed": "Not allowed to switch to this option in edit consultation", + "encounter_type": "Encounter Type", "encounters": "Encounters", + "end_date": "End date", "end_datetime": "End Date/Time", "end_dose": "End Dose", "end_time": "End Time", "end_time_before_start_error": "End time cannot be before start time", "end_time_future_error": "End time cannot be in the future", "ended": "Ended", + "enter_department_team_description": "Enter department/team description (optional)", + "enter_department_team_name": "Enter department/team name", "enter_dosage_instructions": "Enter Dosage Instructions", "enter_file_name": "Enter File Name", "enter_message": "Start typing...", @@ -935,6 +966,7 @@ "enter_valid_dob": "Enter a valid date of birth", "enter_valid_dob_age": "Please enter an age greater than 15 years", "enter_year_of_birth_to_verify": "Enter year of birth to verify", + "enter_your_valid_email_address_to_receive_the_discharge_summary": "Enter your valid email address to receive the discharge summary", "entered_in_error": "Entered in Error", "entered_in_error_warning": "This action cannot be undone. The appointment will be marked as entered in error and removed from the system.", "entity_count_one": "{{count}} {{entity}}", @@ -948,6 +980,8 @@ "error_fetching_slots_data": "Error while fetching slots data", "error_fetching_user_data": "Error while fetching user data", "error_fetching_user_details": "Error while fetching user details: ", + "error_fetching_users_data": "Failed to load user data. Please try again later.", + "error_generating_discharge_summary": "Error generating discharge summary", "error_loading_questionnaire_response": "Error loading questionnaire response", "error_slug_already_in_use": "This slug is already in use. Please choose a different one.", "error_updating_encounter": "Error to Updating Encounter", @@ -958,6 +992,7 @@ "etiology_identified": "Etiology identified", "evening_slots": "Evening Slots", "events": "Events", + "example_email_address": "example@email.com", "exception": "Exception", "exception_created": "Exception created successfully", "exception_deleted": "Exception deleted", @@ -970,20 +1005,27 @@ "export": "Export", "export_live_patients": "Export Live Patients", "exporting": "Exporting", + "external_id": "External ID", "external_identifier": "External Identifier", "facilities": "Facilities", "facility": "Facility", "facility_actions_menu": "Facility action menu", "facility_added_successfully": "Facility created successfully", + "facility_assign_request": "What facility would you like to assign the request to?", "facility_consent_requests_page_title": "Patient Consent List", "facility_count_one": "{{count}} Facility", "facility_count_other": "{{count}} Facilities ", + "facility_deleted_successfully": "{{name}} has been deleted successfully.", "facility_district_name": "Facility/District Name", "facility_district_pincode": "Facility/District/Pincode", "facility_for_care_support": "Facility for Care Support", "facility_linked_success": "Facility linked successfully", "facility_name": "Facility Name", "facility_not_found": "Facility Not Found", + "facility_organization_type__dept": "Department", + "facility_organization_type__other": "Other", + "facility_organization_type__root": "Root", + "facility_organization_type__team": "Team", "facility_organizations": "Facility Organizations", "facility_preference": "Facility preference", "facility_search_placeholder_text": "Search by Facility name", @@ -993,6 +1035,7 @@ "facility_updated_successfully": "Facility updated successfully", "failed_to_create_appointment": "Failed to create an appointment", "failed_to_link_abha_number": "Failed to link ABHA Number. Please try again later.", + "failed_to_send_message": "Failed to send message", "false": "False", "fast_track_testing_reason": "Fast track testing reason", "features": "Features", @@ -1030,6 +1073,7 @@ "file_upload_error": "Error uploading file", "file_upload_success": "File uploaded successfully", "file_uploaded": "File Uploaded Successfully", + "filed": "filed {{title}}", "files": "Files", "fill_my_details": "Fill My Details", "filter": "Filter", @@ -1054,6 +1098,8 @@ "gender_is_required": "Gender is required", "general": "General", "general_info_detail": "Provide the patient's personal details, including name, date of birth, gender, and contact information for accurate identification and communication.", + "generate": "Generate", + "generate_discharge_summary": "Generate Discharge Summary", "generate_link_abha": "Generate/Link ABHA Number", "generate_report": "Generate Report", "generated_on": "Generated on:", @@ -1087,6 +1133,7 @@ "hide_notes": "Hide notes", "high": "High", "history": "History", + "home": "Home", "home_facility": "Home Facility", "home_facility_cleared_success": "Home Facility cleared successfully", "home_facility_updated_error": "Error while updating Home Facility", @@ -1232,6 +1279,7 @@ "location_created": "Location Created", "location_details": "Location Details", "location_form": "Location Form", + "location_history": "Location History", "location_management": "Location Management", "location_updated": "Location Updated", "location_updated_successfully": "Location updated successfully", @@ -1251,6 +1299,7 @@ "make_facility_public": "Make this facility public", "make_facility_public_description": "When enabled, this facility will be visible to the public and can be discovered by anyone using the platform", "make_multiple_beds_label": "Do you want to make multiple beds?", + "manage_and_view_questionnaires": "Manage and view questionnaires", "manage_bed_presets": "Manage Presets of Bed", "manage_facility_users": "Manage encounters", "manage_my_schedule": "Manage my schedule", @@ -1271,6 +1320,7 @@ "mark_as_noshow": "Mark as no-show", "mark_as_read": "Mark as Read", "mark_as_unread": "Mark as Unread", + "mark_encounter_as_complete_confirmation": "Are you sure you would like to mark this encounter as complete?", "mark_inactive": "Mark Inactive", "mark_resolved": "Mark Resolved", "mark_this_transfer_as_complete_question": "Are you sure you want to mark this transfer as complete? The Origin facility will no longer have access to this patient", @@ -1288,7 +1338,9 @@ "medical_worker": "Medical Worker", "medication": "Medication", "medication_administration_saved": "Medicine Administration saved", + "medication_already_marked_as_error": "Medication already marked as entered in error", "medication_taken_between": "Medication Taken Between", + "medications": "Medications", "medicine": "Medicine", "medicine_administration": "Medicine Administration", "medicine_administration_history": "Medicine Administration History", @@ -1296,6 +1348,7 @@ "medicines_administered": "Medicine(s) administered", "medicines_administered_error": "Error administering medicine(s)", "member_id_required": "Member Id is required", + "messages": "Messages", "method": "Method", "middleware_hostname": "Middleware Hostname", "middleware_hostname_example": "e.g. example.ohc.network", @@ -1344,6 +1397,8 @@ "new_password_same_as_old": "Your new password must not match the old password ", "new_password_validation": "New password is not valid.", "new_session": "New Session", + "next": "Next", + "next_file": "Next file", "next_month": "Next month", "next_sessions": "Next Sessions", "next_week_short": "Next wk", @@ -1368,6 +1423,7 @@ "no_data_found": "No data found", "no_departments_teams_found": "No Departments or Teams found", "no_diagnoses_recorded": "No diagnoses recorded", + "no_discharge_summaries_found": "No Discharge Summaries found", "no_doctors_found": "No Doctors Found", "no_duplicate_facility": "You should not create duplicate facilities", "no_encounters_found": "No encounters found", @@ -1380,6 +1436,7 @@ "no_investigation": "No investigation Reports found", "no_investigation_suggestions": "No Investigation Suggestions", "no_linked_facilities": "No Linked Facilities", + "no_locations_available": "No locations available", "no_locations_found": "No locations found", "no_log_update_delta": "No changes since previous log update", "no_log_updates": "No log updates found", @@ -1438,6 +1495,7 @@ "not_eligible": "Not Eligible", "not_found": "Not Found", "not_specified": "Not Specified", + "not_started": "Not started", "not_taken": "Not Taken", "note": "Note", "notes": "Notes", @@ -1468,7 +1526,9 @@ "on": "on", "on_emergency_basis": " on emergency basis", "on_hold": "On Hold", + "ongoing": "Ongoing", "ongoing_medications": "Ongoing Medications", + "online": "Online", "only_indian_mobile_numbers_supported": "Currently only Indian numbers are supported", "onset": "Onset", "op_encounter": "OP Encounter", @@ -1482,9 +1542,11 @@ "ordering": "Ordering", "organization": "Organization", "organization_access_help": "Organizations help you manage facilities, users, and resources efficiently. Contact your administrator to get access.", + "organization_created_successfully": "Organization created successfully", "organization_for_care_support": "Organization for Care Support", "organization_forbidden": "You don't have access to any organizations yet.", "organization_not_found": "No Organizations Found", + "organization_required": "Organization is required", "organizations": "Organizations", "organizations_fetch_error": "Error while fetching organizations", "origin_facility": "Current facility", @@ -1500,6 +1562,7 @@ "page_not_found": "Page Not Found", "pain": "Pain", "pain_chart_description": "Mark region and intensity of pain", + "participants": "Participants", "passport_number": "Passport Number", "password": "Password", "password_length_validation": "Use at least 8 characters", @@ -1542,6 +1605,7 @@ "patient_details": "Patient Details", "patient_details_incomplete": "Patient Details Incomplete", "patient_face": "Patient Face", + "patient_files": "Patient Files", "patient_information": "Patient Information", "patient_name": "Patient Name", "patient_name_uhid": "Patient Name/UHID", @@ -1585,7 +1649,6 @@ "phone_number": "Phone Number", "phone_number_at_current_facility": "Phone Number of Contact person at current Facility", "phone_number_min_error": "Phone number must be at least 10 characters long", - "phone_number_must_be_10_digits": "Phone number must be a 10-digit mobile number", "phone_number_not_found": "Phone number not found", "phone_number_validation_error": "Entered phone number is not valid", "phone_number_verified": "Phone Number Verified", @@ -1606,7 +1669,9 @@ "please_enter_confirm_password": "Please confirm your new password", "please_enter_correct_birth_year": "Please enter the correct birth year to verify the patient details.", "please_enter_current_password": "Please enter your current password.", + "please_enter_email_address": "Please enter email address", "please_enter_new_password": "Please enter your new password.", + "please_enter_organization_name": "Please enter an organization name", "please_enter_username": "Please enter the username", "please_fix_errors": "Please fix the errors in the highlighted fields and try submitting again.", "please_select_a_facility": "Please select a facility", @@ -1660,6 +1725,7 @@ "preset_name_placeholder": "Specify an identifiable name for the new preset", "preset_updated": "Preset updated", "prev_sessions": "Prev Sessions", + "previous": "Previous", "primary_ph_no": "Primary Ph No.", "primary_phone_no": "Primary ph. no.", "principal": "Principal", @@ -1692,6 +1758,7 @@ "questionnaire_not_exist": "The questionnaire you tried to access does not exist.", "questionnaire_submission_failed": "Failed to submit questionnaire", "questionnaire_submitted_successfully": "Questionnaire submitted successfully", + "questionnaires": "Questionnaires", "quick_access": "Quick Access", "quick_actions": "Quick Actions", "quick_actions_description": "Schedule an appointment or create a new encounter", @@ -1724,7 +1791,9 @@ "referred_to": "Referred to", "refresh": "Refresh", "refresh_list": "Refresh List", + "refreshed": "Refreshed", "refuted": "Refuted", + "regenerate_discharge_summary": "Regenerate Discharge Summary", "register_hospital": "Register Hospital", "register_page_title": "Register As Hospital Administrator", "register_patient": "Register Patient", @@ -1803,6 +1872,7 @@ "resource_status__transportation_to_be_arranged": "Transportation to be arranged", "resource_title": "Resource Title", "resource_type": "Request Type", + "resource_updated_successfully": "Resource updated successfully", "respiratory_status": "Respiratory Status", "result": "Result", "result_date": "Result Date", @@ -1821,6 +1891,8 @@ "right": "Right", "role": "Role", "room_apt": "Room/Apt & Time", + "rotate_left": "Rotate Left", + "rotate_right": "Rotate Right", "route": "Route", "routine": "Routine", "sample_collection_date": "Sample Collection Date", @@ -1867,6 +1939,7 @@ "scribe_error": "Could not autofill fields", "search": "Search", "search_by": "Search by", + "search_by_department_team_name": "Search by department/team name", "search_by_emergency_contact_phone_number": "Search by Emergency Contact Phone Number", "search_by_emergency_phone_number": "Search by Emergency Phone Number", "search_by_name": "Search by Name", @@ -1874,17 +1947,19 @@ "search_by_patient_no": "Search by Patient Number", "search_by_phone_number": "Search by Phone Number", "search_by_resource_title": "Search by resource title", + "search_by_user_name": "Search by user name", "search_by_username": "Search by username", "search_country": "Search country...", "search_encounters": "Search Encounters", "search_for_allergies_to_add": "Search for allergies to add", "search_for_diagnoses_to_add": "Search for diagnoses to add", "search_for_facility": "Search for Facility", + "search_for_medications_to_add": "Search for medications to add", "search_for_symptoms_to_add": "Search for symptoms to add", "search_icd11_placeholder": "Search for ICD-11 Diagnoses", "search_investigation_placeholder": "Search Investigation & Groups", "search_medication": "Search Medication", - "search_medications": "Search for medications to add", + "search_medications": "Search Medications", "search_medicine": "Search Medicine", "search_patient_page_text": "Search for existing patients using their phone number or create a new patient record", "search_patients": "Search Patients", @@ -1892,10 +1967,13 @@ "search_tags": "Search tags...", "search_user": "Search User", "search_user_description": "Search for a user and assign a role to add them to the patient.", + "search_users": "Search users...", "searching": "Searching...", "see_attachments": "See Attachments", + "see_details": "See Details", "see_note": "See Note", "select": "Select", + "select_a_value_set": "Select a Value Set", "select_additional_instructions": "Select additional instructions", "select_admit_source": "Select Admit Source", "select_all": "Select All", @@ -1925,6 +2003,7 @@ "select_method": "Select method", "select_new_role": "Select New Role", "select_organization": "Select Organization", + "select_organization_type": "Select organization type", "select_patient": "Select Patient", "select_policy": "Select an Insurance Policy", "select_policy_to_add_items": "Select a Policy to Add Items", @@ -1958,6 +2037,7 @@ "send_reset_link": "Send Reset Link", "send_sample_to_collection_centre_description": "Are you sure you want to send the sample to Collection Centre?", "send_sample_to_collection_centre_title": "Send sample to collection centre", + "sending": "Sending", "serial_number": "Serial Number", "serviced_on": "Serviced on", "session_capacity": "Session Capacity", @@ -2005,6 +2085,7 @@ "skills": "Skills", "slot_configuration": "Slot Configuration", "slots_left": "slots left", + "slug": "Slug", "social_profile": "Social Profile", "social_profile_detail": "Include occupation, ration card category, socioeconomic status, and domestic healthcare support for a complete profile.", "socioeconomic_status": "Socioeconomic status", @@ -2013,6 +2094,7 @@ "something_wrong": "Something went wrong! Try again later!", "sort_by": "Sort By", "source": "Source", + "source_file": "Source Files", "spdx_sbom_version": "SPDX SBOM Version", "spokes": "Spoke Facilities", "srf_id": "SRF ID", @@ -2032,9 +2114,11 @@ "start_time_before_authored_error": "Start time cannot be before the medication was prescribed", "start_time_future_error": "Start time cannot be in the future", "start_time_must_be_before_end_time": "Start time must be before end time", + "start_typing_to_search": "Start typing to search...", "state": "State", "state_reason_for_archiving": "State reason for archiving {{name}} file?", "status": "Status", + "status_history": "Status History", "stop": "Stop", "stop_recording": "Stop Recording", "stopped": "Stopped", @@ -2061,6 +2145,7 @@ "symptom": "Symptom", "symptoms": "Symptoms", "symptoms_empty_message": "No symptoms recorded", + "system": "System", "systolic": "Systolic", "tachycardia": "Tachycardia", "tag_name": "Tag Name", @@ -2077,6 +2162,7 @@ "third_party_software_licenses": "Third Party Software Licenses", "this_action_is_irreversible": "This action is irreversible. Once a file is archived it cannot be unarchived.", "this_file_has_been_archived": "This file has been archived and cannot be unarchived.", + "thread_already_exists": "Thread with this title already exists", "time": "Time", "time_slot": "Time Slot", "title": "Title", @@ -2110,6 +2196,7 @@ "true": "True", "try_again_later": "Try again later!", "try_different_abha_linking_option": "Want to try a different linking option, here are some more:", + "type": "Type", "type_any_extra_comments_here": "type any extra comments here", "type_b_cylinders": "B Type Cylinders", "type_c_cylinders": "C Type Cylinders", @@ -2175,11 +2262,13 @@ "update_available": "Update Available", "update_bed": "Update Bed", "update_department": "Update Department", + "update_encounter": "Update Encounter", "update_encounter_details": "Update Encounter Details", "update_existing_facility": "Update the details of the existing facility.", "update_facility": "Update Facility", "update_facility_middleware_success": "Facility middleware updated successfully", "update_hospitalisation_details": "Update Hospitalisation Details", + "update_location": "Update Location", "update_log": "Update Log", "update_password": "Update Password", "update_patient_details": "Update Patient Details", @@ -2187,6 +2276,8 @@ "update_preset_position_to_current": "Update preset's position to camera's current position", "update_record": "Update Record", "update_record_for_asset": "Update record for asset", + "update_request": "Update Request", + "update_resource_request": "Update Resource Request", "update_role": "Update Role", "update_shift_request": "Update Shift Request", "update_status": "Update Status", @@ -2249,6 +2340,8 @@ "valid_otp_found": "Valid OTP found, Navigating to Appointments", "valid_to": "Valid Till", "valid_year_of_birth": "Please enter a valid year of birth (YYYY)", + "value_set": "Value Set", + "valuesets": "Valuesets", "vehicle_preference": "Vehicle preference", "vendor_name": "Vendor Name", "ventilator_interface": "Respiratory Support Type", diff --git a/scripts/generate-headers.ts b/scripts/generate-headers.ts new file mode 100644 index 00000000000..5dbc803370c --- /dev/null +++ b/scripts/generate-headers.ts @@ -0,0 +1,27 @@ +import { writeFile } from "fs/promises"; +import path from "path"; + +const headers = process.env.HEADERS; +const header_folder = path.join(__dirname, "..", "public"); + +async function writeHeaders() { + if (!headers) { + console.warn("HEADERS environment variable is not set."); + process.exit(0); + } + + console.log("HEADERS environment variable is set."); + const headersPath = path.join(header_folder, "_headers"); + console.log(`Writing headers to file at path: ${headersPath}`); + + try { + await writeFile(headersPath, headers, "utf-8"); + console.log("Headers written to file successfully."); + process.exit(0); + } catch (error) { + console.error("Error writing headers to file:", error); + process.exit(0); + } +} + +writeHeaders(); diff --git a/src/CAREUI/display/Card.tsx b/src/CAREUI/display/Card.tsx deleted file mode 100644 index 3772fa0b75b..00000000000 --- a/src/CAREUI/display/Card.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { HTMLAttributes, ReactNode } from "react"; - -export default function Card( - props: { - children?: ReactNode; - } & HTMLAttributes, -) { - const { children, ...rest } = props; - return ( - - {children} - - ); -} diff --git a/src/CAREUI/icons/Index.tsx b/src/CAREUI/icons/Index.tsx index ca8c315a194..59919021f8c 100644 --- a/src/CAREUI/icons/Index.tsx +++ b/src/CAREUI/icons/Index.tsx @@ -22,7 +22,7 @@ const IconIndex: React.FC = () => { return ( - + , "/not-found": () => , @@ -69,6 +68,10 @@ const Routes: AppRoutes = { "/login": () => , }; +const AdminRouter: AppRoutes = { + ...AdminRoutes, +}; + export default function AppRouter() { const pluginRoutes = usePluginRoutes(); let routes = Routes; @@ -81,7 +84,13 @@ export default function AppRouter() { ...routes, }; - const pages = useRoutes(routes) || ; + const appPages = useRoutes(routes); + const adminPages = useRoutes(AdminRouter); + + const sidebarFor = appPages ? SidebarFor.FACILITY : SidebarFor.ADMIN; + + const pages = appPages || adminPages || ; + const user = useAuthUser(); const currentPath = window.location.pathname; const shouldShowSidebar = !PATHS_WITHOUT_SIDEBAR.includes(currentPath); @@ -92,7 +101,9 @@ export default function AppRouter() { userPermissions={user?.permissions || []} isSuperAdmin={user?.is_superuser || false} > - {shouldShowSidebar && } + {shouldShowSidebar && ( + + )} ( + + ), + "/facility/:facilityId/patient/:patientId/encounter/:encounterId/treatment_summary": + ({ facilityId, encounterId }) => ( + + ), + "/facility/:facilityId/encounter/:encounterId/:tab": ({ facilityId, encounterId, - }) => , - "/facility/:facilityId/encounter/:encounterId/:tab": ({ + tab, + }) => ( + + ), + "/facility/:facilityId/encounter/:encounterId/:tab/:subPage": ({ facilityId, encounterId, tab, + subPage, }) => ( ), "/facility/:facilityId/patient/:patientId/consultation": ({ @@ -63,18 +84,6 @@ const consultationRoutes: AppRoutes = { ({ patientId, id }) => ( ), - "/facility/:facilityId/patient/:patientId/encounterId/:id/files/": ({ - facilityId, - patientId, - id, - }) => ( - - ), }; export default consultationRoutes; diff --git a/src/Routers/routes/FacilityRoutes.tsx b/src/Routers/routes/FacilityRoutes.tsx index bf716284545..875d3e32ebd 100644 --- a/src/Routers/routes/FacilityRoutes.tsx +++ b/src/Routers/routes/FacilityRoutes.tsx @@ -1,7 +1,7 @@ import { Redirect } from "raviger"; import FacilityUsers from "@/components/Facility/FacilityUsers"; -import ResourceCreate from "@/components/Resource/ResourceCreate"; +import ResourceCreate from "@/components/Resource/ResourceForm"; import { AppRoutes } from "@/Routers/AppRouter"; import { FacilityOverview } from "@/pages/Facility/overview"; diff --git a/src/Routers/routes/PatientRoutes.tsx b/src/Routers/routes/PatientRoutes.tsx index 77230f3ee76..26b2ff4aa38 100644 --- a/src/Routers/routes/PatientRoutes.tsx +++ b/src/Routers/routes/PatientRoutes.tsx @@ -1,4 +1,3 @@ -import FileUploadPage from "@/components/Patient/FileUploadPage"; import { facilityPatientTabs, patientTabs, @@ -45,16 +44,6 @@ const PatientRoutes: AppRoutes = { "/facility/:facilityId/patient/:id/update": ({ facilityId, id }) => ( ), - "/facility/:facilityId/patient/:patientId/files": ({ - facilityId, - patientId, - }) => ( - - ), }; export default PatientRoutes; diff --git a/src/Routers/routes/ResourceRoutes.tsx b/src/Routers/routes/ResourceRoutes.tsx index 390e566325b..d82b17d1672 100644 --- a/src/Routers/routes/ResourceRoutes.tsx +++ b/src/Routers/routes/ResourceRoutes.tsx @@ -1,6 +1,6 @@ import PrintResourceLetter from "@/components/Resource/PrintResourceLetter"; import ResourceDetails from "@/components/Resource/ResourceDetails"; -import { ResourceDetailsUpdate } from "@/components/Resource/ResourceDetailsUpdate"; +import ResourceForm from "@/components/Resource/ResourceForm"; import ResourceList from "@/components/Resource/ResourceList"; import { AppRoutes } from "@/Routers/AppRouter"; @@ -13,7 +13,7 @@ const ResourceRoutes: AppRoutes = { ), "/facility/:facilityId/resource/:id/update": ({ facilityId, id }) => ( - + ), "/facility/:facilityId/resource/:id/print": ({ id }) => ( diff --git a/src/Routers/routes/adminRoutes.tsx b/src/Routers/routes/adminRoutes.tsx new file mode 100644 index 00000000000..abbc7bef952 --- /dev/null +++ b/src/Routers/routes/adminRoutes.tsx @@ -0,0 +1,19 @@ +import QuestionnaireEditor from "@/components/Questionnaire/QuestionnaireEditor"; +import { QuestionnaireList } from "@/components/Questionnaire/QuestionnaireList"; +import { QuestionnaireShow } from "@/components/Questionnaire/show"; +import { ValueSetEditor } from "@/components/ValueSet/ValueSetEditor"; +import { ValueSetList } from "@/components/ValueSet/ValueSetList"; + +import { AppRoutes } from "@/Routers/AppRouter"; + +const AdminRoutes: AppRoutes = { + "/admin/questionnaire": () => , + "/admin/questionnaire/create": () => , + "/admin/questionnaire/:id": ({ id }) => , + "/admin/questionnaire/:id/edit": ({ id }) => , + "/admin/valuesets": () => , + "/admin/valuesets/create": () => , + "/admin/valuesets/:slug/edit": ({ slug }) => , +}; + +export default AdminRoutes; diff --git a/src/Routers/routes/questionnaireRoutes.tsx b/src/Routers/routes/questionnaireRoutes.tsx deleted file mode 100644 index 670861016a7..00000000000 --- a/src/Routers/routes/questionnaireRoutes.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { QuestionnaireList } from "@/components/Questionnaire"; -import QuestionnaireEditor from "@/components/Questionnaire/QuestionnaireEditor"; -import { QuestionnaireShow } from "@/components/Questionnaire/show"; - -import { AppRoutes } from "@/Routers/AppRouter"; - -const QuestionnaireRoutes: AppRoutes = { - "/questionnaire": () => , - "/questionnaire/create": () => , - "/questionnaire/:id": ({ id }) => , - "/questionnaire/:id/edit": ({ id }) => , -}; - -export default QuestionnaireRoutes; diff --git a/src/Utils/request/README.md b/src/Utils/request/README.md index 77b4e2f99e4..4277593fe5c 100644 --- a/src/Utils/request/README.md +++ b/src/Utils/request/README.md @@ -1,6 +1,6 @@ # CARE's data fetching utilities -CARE now uses TanStack Query (formerly React Query) as its data fetching solution. For backward compatibility, we maintain a wrapper `useTanStackQueryInstead` that provides the same API as our previous `useQuery` hook. +CARE now uses TanStack Query (formerly React Query) as its data fetching solution. ## Using TanStack Query (Recommended for new code) @@ -178,265 +178,3 @@ function UpdatePatient({ patientId }: { patientId: string }) { return ; } ``` - -### mutate - -`mutate` is our wrapper around the API call functionality that works with TanStack Query's `useMutation`. It: - -- Handles request body serialization -- Sets appropriate headers -- Integrates with our global error handling -- Provides TypeScript type safety for your mutation payload - -```typescript -interface APICallOptions { - pathParams?: Record; // URL parameters - queryParams?: QueryParams; // Query string parameters - body?: TBody; // Request body - silent?: boolean; // Suppress error notifications - headers?: HeadersInit; // Additional headers -} - -// Basic usage -useMutation({ - mutationFn: mutate(routes.users.create), -}); - -// With parameters -useMutation({ - mutationFn: mutate(routes.users.update, { - pathParams: { id }, - silent: true, // Optional: suppress error notifications - }), -}); -``` - -## Migration Guide & Reference - -### Understanding the Transition - -Our codebase contains two patterns for data fetching: - -1. Legacy pattern using `useTanStackQueryInstead` (wrapper around TanStack Query) -2. Modern pattern using TanStack Query directly - -### Pattern Comparison - -Here's the same API call implemented both ways: - -```tsx -// Legacy Pattern (existing code) -function LegacyComponent({ id }) { - const { data, loading, error, refetch } = useTanStackQueryInstead( - UserRoutes.getUser, - { - pathParams: { id }, - prefetch: true, - refetchOnWindowFocus: false, - }, - ); -} - -// Modern Pattern (new code) -function ModernComponent({ id }) { - const { data, isLoading, error, refetch } = useQuery({ - queryKey: [UserRoutes.getUser.path, id], - queryFn: query(UserRoutes.getUser, { - pathParams: { id }, - }), - enabled: true, - refetchOnWindowFocus: false, - }); -} -``` - -### Migration Mapping - -When migrating from `useTanStackQueryInstead` to direct TanStack Query usage: - -```typescript -// Legacy options -> TanStack Query options -{ - prefetch: true -> enabled: true - loading -> isLoading - refetchOnWindowFocus: false -> refetchOnWindowFocus: false - - // Response structure - data -> data (direct access, no .data property) - res.status -> Use error handling or onError callback - error -> error -} -``` - -### Common Patterns - -1. **Conditional Fetching**: - -```tsx -// Legacy -useTanStackQueryInstead(route, { prefetch: shouldFetch }); - -// Modern -useQuery({ - queryKey: [route.path], - queryFn: query(route), - enabled: shouldFetch, -}); -``` - -2. **With Parameters**: - -```tsx -// Legacy -useTanStackQueryInstead(route, { - pathParams: { id }, - query: { filter }, -}); - -// Modern -useQuery({ - queryKey: [route.path, id, filter], - queryFn: query(route, { - pathParams: { id }, - queryParams: { filter }, - }), -}); -``` - -3. **Error Handling**: - -```tsx -// Legacy -const { error, res } = useTanStackQueryInstead(route); -if (res?.status === 403) handleForbidden(); - -// Modern -useQuery({ - queryKey: [route.path], - queryFn: query(route, { - silent: true, // Optional: suppress error notifications - }), - // Error handling is now done globally -}); -``` - -## Legacy Support: `useTanStackQueryInstead` - -For existing code or maintaining consistency with older patterns, use our wrapper around TanStack Query: - -```jsx -import { useTanStackQueryInstead } from "@care/request"; -import FooRoutes from "@foo/routes"; - -export default function FooDetails({ children, id }) { - const { res, data, loading, error } = useTanStackQueryInstead( - FooRoutes.getFoo, - { - pathParams: { id }, - }, - ); - - if (loading) return ; - - if (res.status === 403) { - navigate("/forbidden"); - return null; - } - - if (error) { - return ; - } - - return ( - - {data.id} - {data.name} - - ); -} -``` - -### API - -```ts -useTanStackQueryInstead(route: Route, options?: QueryOptions): ReturnType; -``` - -#### `route` - -A route object that specifies the endpoint to fetch data from. - -```ts -const FooRoutes = { - getFoo: { - path: "/api/v1/foo/{id}/", // 👈 The path to the endpoint. Slug parameters can be specified using curly braces. - method: "GET", // 👈 The HTTP method to use. Optional; defaults to "GET". - TRes: Type(), // 👈 The type of the response body (for type inference). - TBody: Type(), // 👈 The type of the request body (for type inference). - noAuth: true, // 👈 Whether to skip adding the Authorization header to the request. - }, -} as const; // 👈 This is important for type inference to work properly. -``` - -#### `options` - -An object that specifies options for the request. - -```ts -const options = { - prefetch: true, // 👈 Whether to prefetch the data when the component mounts. - refetchOnWindowFocus: true, // 👈 Whether to refetch the data when the window regains focus. - - // The following options are passed directly to the underlying `request` function. - pathParams: { id: "123" }, // 👈 The slug parameters to use in the path. - query: { limit: 10 }, // 👈 The query parameters to be added to the request URL. - body: { name: "foo" }, // 👈 The body to be sent with the request. - headers: { "X-Foo": "bar" }, // 👈 Additional headers to be sent with the request. - silent: true, // 👈 Whether to suppress notifications for this request. - - onResponse: (res) => { - if (res.status === 403) { - navigate("/forbidden"); - } - }, -}; -``` - -#### Return Type - -The hook returns an object with the following properties: - -```ts -{ - res: Type | undefined; // 👈 The response object. `undefined` if the request has not been made yet. - data: TRes | null; // 👈 The response body. `null` if the request has not been made yet. - error: any; // 👈 The error that occurred while making the request if any. - loading: boolean; // 👈 Whether the request is currently in progress. - refetch: () => void; // 👈 A function that can be called to refetch the data. -} -``` - -## `request` - -`request` is a function that allows you to fetch data. It is a wrapper around `fetch` that adds some useful features. It can be used in both React components and non-React code. For fetching data in React components, prefer using TanStack Query or `useTanStackQueryInstead`. For mutations, use `request`. - -### `request` usage - -```ts -import { request } from "@care/request"; -import FooRoutes from "@foo/routes"; - -export default async function updateFoo(id: string, object: Foo) { - const { res, data } = await request(FooRoutes.updateFoo, { - pathParams: { id }, - body: object, // 👈 The body is automatically serialized to JSON. - }); - - if (res.status === 403) { - navigate("/forbidden"); - return null; - } - - return data; -} -``` diff --git a/src/Utils/request/api.tsx b/src/Utils/request/api.tsx index fac0ede9be3..d6084393f41 100644 --- a/src/Utils/request/api.tsx +++ b/src/Utils/request/api.tsx @@ -17,8 +17,6 @@ import { import { PaginatedResponse } from "@/Utils/request/types"; import { AppointmentPatientRegister } from "@/pages/Patient/Utils"; import { Encounter, EncounterEditRequest } from "@/types/emr/encounter"; -import { MedicationAdministration } from "@/types/emr/medicationAdministration/medicationAdministration"; -import { MedicationStatement } from "@/types/emr/medicationStatement"; import { PartialPatientModel, Patient } from "@/types/emr/newPatient"; import { Observation, @@ -116,11 +114,6 @@ const routes = { TBody: Type<{ refresh: JwtTokenObtainPair["refresh"] }>(), }, - token_verify: { - path: "/api/v1/auth/token/verify/", - method: "POST", - }, - checkResetToken: { path: "/api/v1/password_reset/check/", method: "POST", @@ -164,12 +157,6 @@ const routes = { TRes: Type(), }, - userList: { - path: "/api/v1/users/", - method: "GET", - TRes: Type>(), - }, - deleteProfilePicture: { path: "/api/v1/users/{username}/profile_picture/", method: "DELETE", @@ -184,20 +171,6 @@ const routes = { TBody: Type(), }, - // Facility Endpoints - - getPermittedFacilities: { - path: "/api/v1/facility/", - TRes: Type>(), - }, - - createFacility: { - path: "/api/v1/facility/", - method: "POST", - TRes: Type(), - TBody: Type(), - }, - getPermittedFacility: { path: "/api/v1/facility/{id}/", method: "GET", @@ -224,11 +197,6 @@ const routes = { TBody: Type(), }, - getFacilityUsers: { - path: "/api/v1/facility/{facility_id}/get_users/", - TRes: Type>(), - }, - getScheduleAbleFacilityUser: { path: "/api/v1/facility/{facility_id}/schedulable_users/{user_id}/", TRes: Type(), @@ -338,11 +306,6 @@ const routes = { method: "GET", TRes: Type(), }, - downloadResourceRequests: { - path: "/api/v1/resource/", - method: "GET", - TRes: Type(), - }, getResourceComments: { path: "/api/v1/resource/{id}/comment/", method: "GET", @@ -562,6 +525,11 @@ const routes = { TRes: Type(), TBody: Type<{ organization: string }>(), }, + generateDischargeSummary: { + path: "/api/v1/encounter/{encounterId}/generate_discharge_summary/", + method: "POST", + TRes: Type<{ detail: string }>(), + }, }, // New Patient Routes @@ -650,22 +618,6 @@ const routes = { }, }, }, - - medicationStatement: { - list: { - path: "/api/v1/patient/{patientId}/medication/statement/", - method: "GET", - TRes: Type>(), - }, - }, - - medicationAdministration: { - list: { - path: "/api/v1/patient/{patientId}/medication/administration/", - method: "GET", - TRes: Type>(), - }, - }, } as const; export default routes; diff --git a/src/Utils/request/handleResponse.ts b/src/Utils/request/handleResponse.ts deleted file mode 100644 index 1a97a5b8b24..00000000000 --- a/src/Utils/request/handleResponse.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { t } from "i18next"; -import { navigate } from "raviger"; -import { toast } from "sonner"; - -import * as Notifications from "@/Utils/Notifications"; -import { RequestResult } from "@/Utils/request/types"; - -/** - * @deprecated in favor of useQuery/useMutation/callApi - */ -export default function handleResponse( - { res, error }: RequestResult, - silent?: boolean, -) { - const notify = silent ? undefined : Notifications; - - if (res === undefined) { - return; - } - - // 404 Not Found - if (res.status === 404) { - toast.error(t("not_found")); - return; - } - - // 400/406 Bad Request - if (res.status === 400 || res.status === 406) { - notify?.BadRequest({ errs: error }); - return; - } - - // Other Errors between 400-599 (inclusive) - if (res.status >= 400 && res.status < 600) { - // Handle invalid token / session expiry - if ( - !silent && - (error?.code === "token_not_valid" || - error?.detail === "Authentication credentials were not provided.") - ) { - if (!location.pathname.startsWith("/session-expired")) { - navigate(`/session-expired?redirect=${window.location.href}`); - } - return; - } - - toast.error((error?.detail as string) || t("something_went_wrong")); - return; - } -} diff --git a/src/Utils/request/query.ts b/src/Utils/request/query.ts index 68a4e1b0bba..ef32bb1f5d4 100644 --- a/src/Utils/request/query.ts +++ b/src/Utils/request/query.ts @@ -1,8 +1,14 @@ import careConfig from "@careConfig"; -import { getResponseBody } from "@/Utils/request/request"; -import { ApiCallOptions, ApiRoute, HTTPError } from "@/Utils/request/types"; -import { makeHeaders, makeUrl } from "@/Utils/request/utils"; +import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants"; + +import { + ApiCallOptions, + ApiRoute, + HTTPError, + PaginatedResponse, +} from "@/Utils/request/types"; +import { getResponseBody, makeHeaders, makeUrl } from "@/Utils/request/utils"; import { sleep } from "@/Utils/utils"; export async function callApi>( @@ -113,3 +119,69 @@ const debouncedQuery = >( }; }; query.debounced = debouncedQuery; + +/** + * Creates a TanStack Query compatible paginated query function. + * + * This function is useful for fetching paginated data from an API. + * It will fetch all pages of data and return a single array of results. + * + * To disable pagination, set the `maxPages` option to `1`. + * Leaving it unset will fetch all pages. + * + * Example: + * ```tsx + * const { data, isLoading } = useQuery({ + * queryKey: ["patient-search", facilityId, search], + * queryFn: query.paginated(patientsApi.search, { + * pathParams: { facilityId }, + * queryParams: { limit: 10, offset: 0, search }, + * }), + * }); + * ``` + */ +const paginatedQuery = < + Route extends ApiRoute, unknown>, +>( + route: Route, + options?: ApiCallOptions & { pageSize?: number; maxPages?: number }, +) => { + return async ({ signal }: { signal: AbortSignal }) => { + const items: Route["TRes"]["results"] = []; + let hasNextPage = true; + let page = 0; + let count = 0; + + const pageSize = options?.pageSize ?? RESULTS_PER_PAGE_LIMIT; + + while (hasNextPage) { + const res = await query(route, { + ...options, + queryParams: { + limit: pageSize, + offset: page * pageSize, + ...options?.queryParams, + }, + })({ signal }); + + count = res.count; + items.push(...res.results); + + if (options?.maxPages && page >= options.maxPages - 1) { + hasNextPage = false; + } + + if (items.length >= res.count) { + hasNextPage = false; + } + + page++; + } + + return { + count, + results: items, + }; + }; +}; +query.paginated = paginatedQuery; diff --git a/src/Utils/request/request.ts b/src/Utils/request/request.ts deleted file mode 100644 index 7bbfebb2614..00000000000 --- a/src/Utils/request/request.ts +++ /dev/null @@ -1,91 +0,0 @@ -import careConfig from "@careConfig"; - -import handleResponse from "@/Utils/request/handleResponse"; -import { ApiRoute, RequestOptions, RequestResult } from "@/Utils/request/types"; -import { makeHeaders, makeUrl } from "@/Utils/request/utils"; - -type Options = RequestOptions & { - signal?: AbortSignal; -}; - -/** - * @deprecated use useQuery/useMutation/callApi instead - * - * This no longer ensures that the path params are provided correctly during runtime. - * Usages so far works as path params were passed correctly, but this should not be used anymore. - */ -export default async function request( - { path, method, noAuth }: ApiRoute, - { - query, - body, - pathParams, - onResponse, - silent, - signal, - }: Options = {}, -): Promise> { - const url = `${careConfig.apiUrl}${makeUrl(path, query, pathParams)}`; - - const options: RequestInit = { method, signal }; - - if (body) { - options.body = JSON.stringify(body); - } - - let result: RequestResult = { - res: undefined, - data: undefined, - error: undefined, - }; - - options.headers = makeHeaders(noAuth ?? false); - - try { - const res = await fetch(url, options); - - const data = await getResponseBody(res); - - result = { - res, - data: res.ok ? data : undefined, - error: res.ok ? undefined : (data as Record), - }; - - onResponse?.(result); - handleResponse(result, silent); - - return result; - } catch (error: any) { - result = { error, res: undefined, data: undefined }; - if (error.name === "AbortError") { - return result; - } - } - - console.error(`Request failed `, result.error); - return result; -} - -export async function getResponseBody(res: Response): Promise { - if (!(res.headers.get("content-length") !== "0")) { - return null as TData; - } - - const isJson = res.headers.get("content-type")?.includes("application/json"); - const isImage = res.headers.get("content-type")?.includes("image"); - - if (isImage) { - return (await res.blob()) as TData; - } - - if (!isJson) { - return (await res.text()) as TData; - } - - try { - return await res.json(); - } catch { - return (await res.text()) as TData; - } -} diff --git a/src/Utils/request/types.ts b/src/Utils/request/types.ts index fee73ae52b7..e9506b6cda9 100644 --- a/src/Utils/request/types.ts +++ b/src/Utils/request/types.ts @@ -16,26 +16,6 @@ export interface ApiRoute { noAuth?: boolean; } -/** - * @deprecated in favor of useQuery/useMutation/callApi - */ -export interface RequestResult { - res: Response | undefined; - data: TData | undefined; - error: undefined | Record; -} - -/** - * @deprecated in favor of ApiCallOptions used by useQuery/useMutation/callApi - */ -export interface RequestOptions { - query?: QueryParams; - body?: TBody; - pathParams?: Record; - onResponse?: (res: RequestResult) => void; - silent?: boolean; -} - type ExtractRouteParams = T extends `${infer _Start}{${infer Param}}${infer Rest}` ? Param | ExtractRouteParams @@ -98,7 +78,5 @@ declare module "@tanstack/react-query" { export interface PaginatedResponse { count: number; - next: string | null; - previous: string | null; results: TItem[]; } diff --git a/src/Utils/request/useQuery.ts b/src/Utils/request/useQuery.ts deleted file mode 100644 index 422e5f96868..00000000000 --- a/src/Utils/request/useQuery.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useMemo, useRef } from "react"; - -import request from "@/Utils/request/request"; -import { ApiRoute, RequestOptions } from "@/Utils/request/types"; - -import { mergeRequestOptions } from "./utils"; - -export interface QueryOptions extends RequestOptions { - prefetch?: boolean; - key?: string; -} - -/** - * @deprecated use `useQuery` from `@tanstack/react-query` instead. - */ -export default function useTanStackQueryInstead( - route: ApiRoute, - options?: QueryOptions, -) { - const overridesRef = useRef>(); - - // Ensure unique key for each usage of the hook unless explicitly provided - // (hack to opt-out of tanstack query's caching between usages) - const key = useMemo(() => options?.key ?? Math.random(), [options?.key]); - - const { - data: response, - refetch, - isFetching: isLoading, - } = useQuery({ - queryKey: [route.path, options?.pathParams, options?.query, key], - queryFn: async ({ signal }) => { - const resolvedOptions = overridesRef.current - ? mergeRequestOptions(options || {}, overridesRef.current) - : options; - - return await request(route, { ...resolvedOptions, signal }); - }, - enabled: options?.prefetch ?? true, - refetchOnWindowFocus: false, - }); - - return { - data: response?.data, - loading: isLoading, - error: response?.error, - res: response?.res, - /** - * Refetch function that applies new options and fetches fresh data. - */ - refetch: async (overrides?: QueryOptions) => { - overridesRef.current = overrides; - await refetch(); - return response!; - }, - }; -} diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts index 8edb710f32d..86199a01bbe 100644 --- a/src/Utils/request/utils.ts +++ b/src/Utils/request/utils.ts @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction } from "react"; import { LocalStorageKeys } from "@/common/constants"; -import { QueryParams, RequestOptions } from "@/Utils/request/types"; +import { QueryParams } from "@/Utils/request/types"; export function makeUrl( path: string, @@ -64,27 +64,27 @@ export function getAuthorizationHeader() { return null; } -export function mergeRequestOptions( - options: RequestOptions, - overrides: RequestOptions, -): RequestOptions { - return { - ...options, - ...overrides, - - query: { ...options.query, ...overrides.query }, - body: (options.body || overrides.body) && { - ...(options.body ?? {}), - ...(overrides.body ?? {}), - }, - pathParams: { ...options.pathParams, ...overrides.pathParams }, - - onResponse: (res) => { - options.onResponse?.(res); - overrides.onResponse?.(res); - }, - silent: overrides.silent ?? options.silent, - }; +export async function getResponseBody(res: Response): Promise { + if (!(res.headers.get("content-length") !== "0")) { + return null as TData; + } + + const isJson = res.headers.get("content-type")?.includes("application/json"); + const isImage = res.headers.get("content-type")?.includes("image"); + + if (isImage) { + return (await res.blob()) as TData; + } + + if (!isJson) { + return (await res.text()) as TData; + } + + try { + return await res.json(); + } catch { + return (await res.text()) as TData; + } } export function handleUploadPercentage( diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index add2677343b..3198b10bcd3 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -5,11 +5,6 @@ import dayjs from "@/Utils/dayjs"; import { Time } from "@/Utils/types"; import { Patient } from "@/types/emr/newPatient"; import { PatientModel } from "@/types/emr/patient"; -import { - Organization, - OrganizationParent, -} from "@/types/organization/organization"; -import { Quantity } from "@/types/questionnaire/quantity"; const DATE_FORMAT = "DD/MM/YYYY"; const TIME_FORMAT = "hh:mm A"; @@ -93,21 +88,6 @@ function _isAppleDevice() { */ export const isAppleDevice = _isAppleDevice(); -/** - * Conditionally concatenate classes. An alternate replacement for `clsx`. - * - * **Example Usage:** - * ```tsx - * - * // "md:flex p-0" - * ``` - * - * @deprecated Use `cn` from `@/lib/utils` instead. - */ -export const classNames = (...classes: (string | boolean | undefined)[]) => { - return classes.filter(Boolean).join(" "); -}; - export const isUserOnline = (user: { last_login: DateLike }) => { return user.last_login ? dayjs().subtract(5, "minutes").isBefore(user.last_login) @@ -158,20 +138,6 @@ export const formatPatientAge = ( return `${day}${suffixes.day}`; }; -export const mergeQueryOptions = ( - selected: T[], - queryOptions: T[], - compareBy: (obj: T) => T[keyof T], -) => { - if (!selected.length) return queryOptions; - return [ - ...selected, - ...queryOptions.filter( - (option) => !selected.find((s) => compareBy(s) === compareBy(option)), - ), - ]; -}; - /** * A utility method to format an array of string to human readable format. * @@ -212,12 +178,6 @@ export const getMonthStartAndEnd = (date: Date) => { }; }; -export const displayQuantity = (quantity?: Quantity) => { - if (!quantity) return "N/A"; - - return [quantity.value ?? "N/A", quantity.unit].join(" "); -}; - /** * Returns hours and minutes between two dates. * @@ -273,14 +233,28 @@ export const conditionalArrayAttribute = ( return condition ? attributes : []; }; -export const stringifyGeoOrganization = (org: Organization) => { +export const stringifyNestedObject = < + T extends { name: string; parent?: Partial }, +>( + obj: T, + separator = ", ", +) => { const levels: string[] = []; - let current: OrganizationParent | undefined = org; + let current: Partial | undefined = obj; while (current?.name) { levels.push(current.name); current = current.parent; } - return levels.join(", "); + return levels.join(separator); +}; + +export const mergeAutocompleteOptions = ( + options: { label: string; value: string }[], + value?: { label: string; value: string }, +) => { + if (!value) return options; + if (options.find((o) => o.value === value.value)) return options; + return [value, ...options]; }; diff --git a/src/Utils/validators.ts b/src/Utils/validators.ts index 8fc37b39f55..9a9d453b8a8 100644 --- a/src/Utils/validators.ts +++ b/src/Utils/validators.ts @@ -2,7 +2,7 @@ import { t } from "i18next"; import { isValidPhoneNumber } from "react-phone-number-input"; import { z } from "zod"; -export default { +export default () => ({ phoneNumber: { optional: z .string() @@ -10,9 +10,13 @@ export default { .refine((val) => !val || isValidPhoneNumber(val), { message: t("phone_number_validation_error"), }), - required: z.string().refine((val) => isValidPhoneNumber(val), { - message: t("phone_number_validation_error"), - }), + + required: z + .string() + .min(1, { message: t("field_required") }) + .refine((val) => isValidPhoneNumber(val), { + message: t("phone_number_validation_error"), + }), }, coordinates: { @@ -20,9 +24,10 @@ export default { .number() .min(-90, t("invalid_latitude")) .max(90, t("invalid_latitude")), + longitude: z .number() .min(-180, t("invalid_longitude")) .max(180, t("invalid_longitude")), }, -}; +}); diff --git a/src/common/constants.tsx b/src/common/constants.tsx index 8fda21a820f..1ce2a011725 100644 --- a/src/common/constants.tsx +++ b/src/common/constants.tsx @@ -62,42 +62,6 @@ export const FACILITY_TYPES: Array = [ { id: 4000, text: "Community Based Organization" }, ]; -export const SHIFTING_FILTER_ORDER: Array = [ - { id: 1, text: "created_date", desc: "ASC Created Date" }, - { id: 2, text: "-created_date", desc: "DESC Created Date" }, - { id: 3, text: "modified_date", desc: "ASC Modified Date" }, - { id: 4, text: "-modified_date", desc: "DESC Modified Date" }, -]; - -export const CONSCIOUSNESS_LEVEL = [ - { id: 20, value: "UNRESPONSIVE" }, - { id: 15, value: "RESPONDS_TO_PAIN" }, - { id: 10, value: "RESPONDS_TO_VOICE" }, - { id: 5, value: "ALERT" }, - { id: 25, value: "AGITATED_OR_CONFUSED" }, - { - id: 30, - value: "ONSET_OF_AGITATION_AND_CONFUSION", - }, -] as const; - -export const LIMB_RESPONSE_OPTIONS = [ - { id: 0, value: "UNKNOWN" }, - { id: 5, value: "STRONG" }, - { id: 10, value: "MODERATE" }, - { id: 15, value: "WEAK" }, - { id: 20, value: "FLEXION" }, - { id: 25, value: "EXTENSION" }, - { id: 30, value: "NONE" }, -] as const; - -export const OXYGEN_MODALITY_OPTIONS = [ - { value: "NASAL_PRONGS" }, - { value: "SIMPLE_FACE_MASK" }, - { value: "NON_REBREATHING_MASK" }, - { value: "HIGH_FLOW_NASAL_CANNULA" }, -] as const; - export const GENDER_TYPES = [ { id: "male", text: "Male", icon: "M" }, { id: "female", text: "Female", icon: "F" }, @@ -109,41 +73,6 @@ export const GENDERS = GENDER_TYPES.map((gender) => gender.id) as [ (typeof GENDER_TYPES)[number]["id"], ]; -export const CONSULTATION_SUGGESTION = [ - { id: "HI", text: "Home Isolation", deprecated: true }, // # Deprecated. Preserving option for backward compatibility (use only for readonly operations) - { id: "A", text: "Admission" }, - { id: "R", text: "Refer to another Hospital", editDisabled: true }, - { id: "OP", text: "OP Consultation" }, - { id: "DC", text: "Domiciliary Care" }, - { id: "DD", text: "Declare Death", editDisabled: true }, -] as const; - -export const RESPIRATORY_SUPPORT = [ - { id: "NIV", value: "NON_INVASIVE" }, - { id: "IV", value: "INVASIVE" }, - { id: "O2", value: "OXYGEN_SUPPORT" }, - { id: "NONE", value: "UNKNOWN" }, -] as const; - -export const VENTILATOR_MODE_OPTIONS = [ - "VCV", - "PCV", - "PRVC", - "APRV", - "VC_SIMV", - "PC_SIMV", - "PRVC_SIMV", - "ASV", - "PSV", -] as const; - -export const INSULIN_INTAKE_FREQUENCY_OPTIONS = [ - "UNKNOWN", - "OD", - "BD", - "TD", -] as const; - export const BLOOD_GROUP_CHOICES = [ { id: "unknown", text: "Unknown" }, { id: "A_positive", text: "A+" }, @@ -173,109 +102,6 @@ export const RESOURCE_STATUS_CHOICES = [ { icon: "l-check-circle", text: "completed" }, ] as const; -export const RESOURCE_FILTER_ORDER: Array = [ - { id: 1, text: "created_date", desc: "ASC Created Date" }, - { id: 2, text: "-created_date", desc: "DESC Created Date" }, - { id: 3, text: "modified_date", desc: "ASC Modified Date" }, - { id: 4, text: "-modified_date", desc: "DESC Modified Date" }, -]; - -export const HEARTBEAT_RHYTHM_CHOICES = [ - "REGULAR", - "IRREGULAR", - "UNKNOWN", -] as const; - -export const NURSING_CARE_PROCEDURES = [ - "oral_care", - "hair_care", - "bed_bath", - "eye_care", - "perineal_care", - "skin_care", - "pre_enema", - "wound_dressing", - "lymphedema_care", - "ascitic_tapping", - "colostomy_care", - "colostomy_change", - "personal_hygiene", - "positioning", - "suctioning", - "ryles_tube_care", - "ryles_tube_change", - "iv_sitecare", - "nubulisation", - "dressing", - "dvt_pump_stocking", - "restrain", - "chest_tube_care", - "tracheostomy_care", - "tracheostomy_tube_change", - "stoma_care", - "catheter_care", - "catheter_change", -] as const; - -export const BOWEL_ISSUE_CHOICES = [ - "NO_DIFFICULTY", - "CONSTIPATION", - "DIARRHOEA", -] as const; - -export const BLADDER_DRAINAGE_CHOICES = [ - "NORMAL", - "CONDOM_CATHETER", - "DIAPER", - "INTERMITTENT_CATHETER", - "CONTINUOUS_INDWELLING_CATHETER", - "CONTINUOUS_SUPRAPUBIC_CATHETER", - "UROSTOMY", -] as const; - -export const BLADDER_ISSUE_CHOICES = [ - "NO_ISSUES", - "INCONTINENCE", - "RETENTION", - "HESITANCY", -] as const; - -export const URINATION_FREQUENCY_CHOICES = [ - "NORMAL", - "DECREASED", - "INCREASED", -] as const; - -export const SLEEP_CHOICES = [ - "EXCESSIVE", - "SATISFACTORY", - "UNSATISFACTORY", - "NO_SLEEP", -] as const; - -export const NUTRITION_ROUTE_CHOICES = [ - "ORAL", - "RYLES_TUBE", - "GASTROSTOMY_OR_JEJUNOSTOMY", - "PEG", - "PARENTERAL_TUBING_FLUID", - "PARENTERAL_TUBING_TPN", -] as const; - -export const ORAL_ISSUE_CHOICES = [ - "NO_ISSUE", - "DYSPHAGIA", - "ODYNOPHAGIA", -] as const; - -export const APPETITE_CHOICES = [ - "INCREASED", - "SATISFACTORY", - "REDUCED", - "NO_TASTE_FOR_FOOD", - "CANNOT_BE_ASSESSED", -] as const; - export const FACILITY_FEATURE_TYPES: { id: number; name: string; @@ -505,233 +331,6 @@ export const DEFAULT_ALLOWED_EXTENSIONS = [ "application/vnd.oasis.opendocument.spreadsheet,application/pdf", ]; -export const HumanBodyPaths = { - anterior: [ - { - d: "M535.244,212.572c32.253.43,32.684-31.823,32.684-31.823,9.891-.215,14.191-19.783,13.331-23.653s-7.526-1.5-7.526-1.5c3.656-30.1-9.676-48.38-17.847-53.756S535.244,95.6,535.244,95.6h.43s-12.472.86-20.643,6.236-21.5,23.653-17.846,53.756c0,0-6.666-2.365-7.526,1.5s3.44,23.438,13.331,23.653c0,0,.43,32.253,32.684,31.823Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorHead", - }, - { - d: "M512.129,213.97s31.608,4.954,47.574-1.394v14.456s-26.287,4.355-47.574,0Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorNeck", - }, - { - d: "M505.355,231.279s-56.766,25.8-69.452,34.4c0,0,15.7,20.857,21.072,66.872C456.975,332.555,469.417,246.838,505.355,231.279Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorRightShoulder", - }, - { - d: "M526.482,232.838l.806,137.346s-46.607-22.2-67.745,18.762C459.543,388.946,455.685,234.612,526.482,232.838Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorRightChest", - }, - { - d: "M433.108,269.768s34.728,55.552,18.279,141.992c0,0-19.57-9.107-33.761-7.333,0,0-1.613-106.276,0-110.952S429.721,271.058,433.108,269.768Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorRightArm", - }, - { - d: "M415.207,408.781s27.254-.968,35.963,11.45c0,0-7.58,59.024-13.547,77.57s-19.03,56.766-19.03,56.766l-22.254-2.742s1.451-34.672,1.29-45.477,5-49.993,9.514-62.249S415.207,408.781,415.207,408.781Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorRightForearm", - }, - { - d: "M396.6,556.524l18.245,2.606a1.808,1.808,0,0,1,1.565,1.776c.049,6.373.053,30.692-2.6,41.987-2.568,10.951-16.244,28.022-26.205,35.726a4.126,4.126,0,0,1-6.575-2.7c-.192-1.322-.39-2.923-.584-4.855a1.828,1.828,0,0,0-2.054-1.637l-4.174.551a1.818,1.818,0,0,1-2.026-2.171c.631-3.043,1.887-8.187,3.72-11.529,2.591-4.724,5.9-18.948,5.442-26.76a1.79,1.79,0,0,0-1.514-1.635,7.118,7.118,0,0,0-5.448,1c-1.364,1.043-3.83,4.558-5.963,7.825-1.941,2.973-6.715.452-5.152-2.736.018-.037.037-.074.056-.111,1.936-3.71,13.063-18.708,16.288-24.513,2.9-5.221,13.627-8.747,15.171-11.984A1.706,1.706,0,0,1,396.6,556.524Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorRightHand", - }, - { - d: "M674.037,556.2l-18.244,2.606a1.808,1.808,0,0,0-1.566,1.776c-.049,6.373-.052,30.692,2.6,41.988,2.569,10.951,16.244,28.021,26.205,35.726a4.126,4.126,0,0,0,6.576-2.7c.191-1.322.389-2.922.584-4.855a1.827,1.827,0,0,1,2.053-1.637l4.174.551a1.818,1.818,0,0,0,2.027-2.17c-.632-3.043-1.888-8.188-3.721-11.53-2.59-4.723-5.9-18.948-5.442-26.76a1.79,1.79,0,0,1,1.515-1.634,7.114,7.114,0,0,1,5.447,1c1.364,1.043,3.83,4.558,5.964,7.826,1.94,2.973,6.715.451,5.151-2.736-.018-.038-.037-.075-.056-.112-1.935-3.709-13.063-18.707-16.288-24.513-2.9-5.221-13.627-8.746-15.171-11.984A1.707,1.707,0,0,0,674.037,556.2Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLeftHand", - }, - { - d: "M544.705,232.838h19.137s18.062,15.643,20,19.513,29.888,42.79,26.878,128.154c0,0-16.557-16.556-31.178-15.051,0,0,2.365-33.114-34.834-34.619Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLeftChest", - }, - { - d: "M569.432,231.279s61.927,31.824,65.153,35.694c0,0-12.9,9.752-18.707,73.791C615.878,340.764,610.072,268.048,569.432,231.279Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLeftShoulder", - }, - { - d: "M638.455,271.058s14.407,18.923,14.837,23.223-1.291,105.362.86,108.8c0,0-26.233,1.29-34.834,9.891,0,0-4.3-51.176.86-78.484S633.079,279.659,638.455,271.058Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLeftArm", - }, - { - d: "M621.038,419s16.342-12.257,33.974-10.537c0,0,7.741,26.233,8.816,34.189s10.321,49.241,9.246,66.658.087,41.069.087,41.069-16.214,3.44-20.084,4.731c0,0-17.2-46.661-18.062-52.036S620.982,426.52,621.038,419Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLeftForearm", - }, - { - d: "M510.758,934.272s-20.723,1.451-24.973,1.5a56.32,56.32,0,0,0-1.556,10.672c0,4.355.484,25.481-.645,28.061s-21.771,27.254-23.383,30.641.645,8.386,1.935,9.192,2.1,4.757,4.193,5.644c1.807.765,3.064,3.709,5.644,4.032s10.482-.645,12.418.726c0,0,.887,3.144,2.58,3.306.864.082,5.644,1.774,10.644-5.967s13.04-35.019,13.439-37.791c.249-1.732-1.183-2.125-1.506-5.189a112.484,112.484,0,0,1,1.855-20.64C513.419,948.3,510.758,934.272,510.758,934.272Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorRightFoot", - }, - { - d: "M563.251,934.191s20.756.564,25.006.616c0,0,.151,7.125.151,11.479s.162,24.351,1.29,26.932,22.531,27.576,24.144,30.963-.645,8.386-1.935,9.192-2.1,4.758-4.193,5.645c-1.807.764-3.064,3.709-5.645,4.031s-10.482-.645-12.417.726c0,0-.887,3.145-2.581,3.306-.864.082-5.644,1.774-10.643-5.967s-13.04-35.018-13.439-37.79c-.25-1.733,1.182-2.126,1.5-5.19a112.484,112.484,0,0,0-1.855-20.64C560.623,947.334,563.251,934.191,563.251,934.191Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLeftFoot", - }, - { - d: "M485.2,932.363l24.513-1.4s1.666-37.2,2.526-41.285,4.731-85.149,4.086-99.771c0,0-30,2.527-49.348-3.924,0,0-6.451,44.026-1.828,62.841C467.775,859.527,484.874,929.6,485.2,932.363Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorRightLeg", - }, - { - d: "M469.231,420.715s-5.966-30.318-4.515-34.834a115.141,115.141,0,0,1,16.772-10.966c10.428-5.483,29.727-6.773,36.339-3.548,5.81,2.834,4.972,2.548,13.439,4.73l.054-142.13h9.192V334.92s32.415,1.291,31.931,33.06a72.9,72.9,0,0,0,8.869,2.419c5.322,1.129,23.062,9.031,25.642,19.675,0,0-4.945,22.2-3.655,32.684,0,0-39.4-29.835-47.306-31.609s-12.959,2.31-16.933,2.8c-4.483.547-11.71-.628-18.142-2.9C514.306,388.7,475.2,414.909,469.231,420.715Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLowerChest", - }, - { - d: "M461.813,481.665c2.43-11.313,8.042-43.207,7.1-55.467,0,0,48.3-30.56,50.88-30.4s12.122,5.564,23.841,2.338c0,0,6.719-3.225,13.331.162s34.874,24.149,46.324,28.987c0,0-.524,28.746,1.573,37.777s10.159,42.091,10.966,46.123,0,.806,0,.806-58.057,50.155-59.669,52.574c0,0-6.451-6.29-20.481-6.774s-20.643,6.774-20.643,6.774l-60.152-51.122S460.965,485.617,461.813,481.665Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorAbdomen", - }, - { - d: "M554.381,790.77s30.748,3.226,51.39-4.945c0,0,3.441,40.424,0,63.432s-16.449,76.871-17.094,81.816c0,0-23.33.108-25.91-1.4,0,0-3.011-33.328-3.871-43S550.08,810.982,554.381,790.77Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLeftLeg", - }, - { - d: "M454.072,520.056s-4.515,29.35-6.128,48.7.323,59.346,6.128,81.278,21.288,89.343,14.514,131.272c0,0,20.464,8.064,47.808,4.516,0,0,7.2-74.822,6.7-87.73,0,0,3.333-50.745,3.333-58.7s1.72-27.738,1.72-27.738-20.642-10.106-14.837-44.08C513.311,567.576,462.351,524.571,454.072,520.056Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorRightThigh", - }, - { - d: "M553.114,785.825s37.713,2.741,50.615-3.548c0,0-6.451-35.8-1.129-63.325s19.943-77.408,20.8-92.03,2.33-87.3-7.221-108.371l-56.426,49.455s3.441,37.629-18.922,44.725c0,0,7.741,68.807,7.741,78.913S554.857,777.654,553.114,785.825Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorLeftThigh", - }, - { - d: "M535.624,610.466s16.722-10.1,18.818-27.355-3.386-17.578-5.805-18.545-13.063-1.291-13.063-1.291h.1s-10.644.323-13.063,1.291-7.9,1.29-5.806,18.545S535.624,610.466,535.624,610.466Z", - transform: "translate(-362.967 -95.599)", - region: "AnteriorGroin", - }, - ], - - posterior: [ - { - d: "M 506.9838 158.0121 C 509.6029 173.1336 512.1258 187.9477 521.5039 184.4407 C 517.7283 191.6346 525.6919 202.9266 528.0919 210.8841 C 544.9623 208.3461 562.3174 208.3461 579.1878 210.8841 C 581.5893 202.9236 589.5363 191.6662 585.7863 184.4511 C 595.6744 187.4586 596.8188 174.3021 600.3813 158.5926 C 600.1173 156.4611 595.9999 158.5806 594.7788 159.0816 C 597.7384 128.3122 591.2088 97.1811 553.7104 97.22 C 516.1444 97.1497 509.5249 128.2116 512.5008 159.0891 C 511.0564 158.4651 508.4914 157.0971 506.9838 158.0121 Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorHead", - }, - { - d: "M 503.129 213.97 s 30.871 -1.97 46.871 0.03 v 12.456 s -26 -2.456 -47.574 0 Z", - transform: "translate(-362.967 -95.599)", - region: "PosteriorNeck", - }, - { - d: "M545.584,228.037V361.6s-13.6,10.828-25.282,13.145c-10.077,2-36.162,3.374-36.766-.857S478.9,239.117,545.584,228.037Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorLeftChest", - }, - { - d: "M563.865,228.037V361.6s13.6,10.828,25.282,13.145c10.076,2,36.161,3.374,36.766-.857S630.546,239.117,563.865,228.037Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorRightChest", - }, - { - d: "M550.973,228.188h8.914l.151,136.435s20.7,17.828,59.681,16.317c0,0-4.684,38.528-1.057,56.508s9.216,41.248,9.216,41.248-77.812,30.218-145.954-.151c0,0,9.67-35.96,9.972-58.321a167.6,167.6,0,0,0-4.23-39.888s37.924,5.439,62.1-15.713C549.764,364.623,550.52,228.188,550.973,228.188Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorAbdomen", - }, - { - d: "M523.223,230.857s-40.694,20.548-50.968,25.182-11.08,5.439-11.08,5.439,15.512,18.735,18.533,70.509C479.708,331.987,489.58,244.354,523.223,230.857Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorLeftShoulder", - }, - { - d: "M587.084,230.857s40.693,20.548,50.968,25.182,11.08,5.439,11.08,5.439S633.62,280.213,630.6,331.987C630.6,331.987,620.726,244.354,587.084,230.857Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorRightShoulder", - }, - { - d: "M457.951,265.306s-12.49,14.706-13.5,29.613,1.813,82.194.6,95.691c0,0,15.512-1.209,22.16,3.022s9.872,4.23,9.872,4.23,3.223-32.232,1.41-53.385S467.823,277.393,457.951,265.306Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorLeftArm", - }, - { - d: "M444.655,394.639s3.627-1.209,8.864,1.612,21.153,4.835,21.153,4.835a241.987,241.987,0,0,1-6.245,50.968c-6.446,27.8-23.167,79.977-22.966,81.992,0,0-17.325-4.03-20.951-3.828,0,0,1.209-21.354,1.612-31.427s.2-42.91,6.648-63.659S444.454,396.049,444.655,394.639Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorLeftForearm", - }, - { - d: "M423.5,533.844s-4.029,2.82-7.454,5.036-12.49,13.1-15.311,18.131-11.482,15.915-10.274,16.923,5.44.2,7.454-2.216,7.051-8.663,10.476-7.253c0,0,1.007,12.087-3.224,22.966s-4.633,13.7-4.633,13.7,2.591,2.22,7.063.809q.291-.091.592-.2s1.612,4.835.806,8.864,3.022,3.425,7.655,1.007,21.959-22.562,24.175-35.053,1.611-40.895,1.611-40.895S427.33,534.65,423.5,533.844Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorLeftHand", - }, - { - d: "M650.678,265.306s12.49,14.706,13.5,29.613-1.813,82.194-.6,95.691c0,0-15.512-1.209-22.16,3.022s-9.871,4.23-9.871,4.23-3.224-32.232-1.41-53.385S640.807,277.393,650.678,265.306Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorRightArm", - }, - { - d: "M663.974,394.639s-3.626-1.209-8.864,1.612-21.153,4.835-21.153,4.835a242.066,242.066,0,0,0,6.245,50.968c6.447,27.8,23.168,79.977,22.966,81.992,0,0,17.325-4.03,20.951-3.828,0,0-1.208-21.354-1.611-31.427s-.2-42.91-6.648-63.659S664.175,396.049,663.974,394.639Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorRightForearm", - }, - { - d: "M685.127,533.844s4.029,2.82,7.453,5.036,12.491,13.1,15.311,18.131,11.483,15.915,10.274,16.923-5.439.2-7.454-2.216-7.051-8.663-10.475-7.253c0,0-1.008,12.087,3.223,22.966s4.633,13.7,4.633,13.7-2.59,2.22-7.062.809q-.291-.091-.593-.2s-1.612,4.835-.806,8.864-3.022,3.425-7.655,1.007-21.958-22.562-24.174-35.053-1.612-40.895-1.612-40.895S681.3,534.65,685.127,533.844Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorRightHand", - }, - { - d: "M552.635,495.366s0,66.279-.6,69.9c-.051.277-.126.982-.2,2.065-5.691,6.673-27.473,28.254-58.673,9.04a10.164,10.164,0,0,1-1.738-1.309c-23.066-21.783-7.076-50.968-6.371-52.2l-2.216-1.234c-.176.327-17.652,32.107,6.849,55.249a14.16,14.16,0,0,0,2.166,1.662c9.519,5.842,18.232,8.033,25.988,8.033,16.116,0,27.977-9.519,33.642-15.235-1.661,20.07-6.144,82.369-6.5,86-.4,4.231-7.605,77.51-7.605,80.935,0,0-36.111-4.785-45.579-2.972,0,0,.2-37.672-2.821-59.63s-14.5-65.473-15.914-101.936-1.411-65.473,7.453-91.46C480.514,482.272,499.048,497.582,552.635,495.366Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorLeftThighAndButtock", - }, - { - d: "M555.471,495.366s0,66.279.6,69.9c.051.277.126.982.2,2.065,5.691,6.673,27.473,28.254,58.673,9.04a10.164,10.164,0,0,0,1.738-1.309c23.066-21.783,7.076-50.968,6.371-52.2l2.216-1.234c.176.327,17.652,32.107-6.85,55.249a14.151,14.151,0,0,1-2.165,1.662c-9.519,5.842-18.232,8.033-25.988,8.033-16.116,0-27.977-9.519-33.643-15.235,1.662,20.07,6.145,82.369,6.5,86,.4,4.231,7.605,77.51,7.605,80.935,0,0,36.111-4.785,45.579-2.972,0,0-.2-37.672,2.82-59.63s14.5-65.473,15.915-101.936,1.41-65.473-7.453-91.46C627.592,482.272,609.058,497.582,555.471,495.366Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorRightThighAndButtock", - }, - { - d: "M492.2,739.529s21.354-2.418,42.909,3.425c0,0,3.627,43.312,1.612,61.846s-7.655,75.445-6.849,80.078c0,0-19.944.907-25.988,2.518,0,0-2.619-29.009-9.267-49.154S486.961,754.839,492.2,739.529Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorLeftLeg", - }, - { - d: "M617.088,739.529s-21.354-2.418-42.909,3.425c0,0-3.626,43.312-1.612,61.846s7.655,75.445,6.85,80.078c0,0,19.944.907,25.987,2.518,0,0,2.619-29.009,9.267-49.154S622.326,754.839,617.088,739.529Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorRightLeg", - }, - { - d: "M504.387,891.023s17.728-.806,24.879-2.619c0,0,2.015,6.245,1.209,18.131s-1.007,21.555-.6,23.771,1.813,9.67-1.209,15.512S520,967.172,516.978,972.007s-10.275,5.439-11.886-1.611c0,0-1.813,3.424-7.857,1.41s-9.67-1.209-11.483-5.44-4.835-11.684-1.41-16.922,18.937-18.534,20.145-25.182S505.6,895.455,504.387,891.023Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorLeftFoot", - }, - { - d: "M604.752,891.023s-17.728-.806-24.88-2.619c0,0-2.014,6.245-1.209,18.131s1.008,21.555.605,23.771-1.813,9.67,1.209,15.512,8.662,21.354,11.684,26.189,10.274,5.439,11.886-1.611c0,0,1.813,3.424,7.856,1.41s9.67-1.209,11.483-5.44,4.835-11.684,1.41-16.922-18.936-18.534-20.145-25.182S603.543,895.455,604.752,891.023Z", - transform: "translate(-390.349 -94.472)", - region: "PosteriorRightFoot", - }, - ], -} as const; - -export type HumanBodyRegion = (typeof HumanBodyPaths)[ - | "anterior" - | "posterior"][number]["region"]; - -export const PressureSoreExudateAmountOptions = [ - "None", - "Light", - "Moderate", - "Heavy", -] as const; - -export const PressureSoreTissueTypeOptions = [ - "Closed", - "Epithelial", - "Granulation", - "Slough", - "Necrotic", -] as const; - export const FILE_EXTENSIONS = { IMAGE: ["jpeg", "jpg", "png", "gif", "svg", "bmp", "webp", "jfif"], AUDIO: ["mp3", "wav"], diff --git a/src/components/Auth/Login.tsx b/src/components/Auth/Login.tsx index 591e162b4ff..320bc352011 100644 --- a/src/components/Auth/Login.tsx +++ b/src/components/Auth/Login.tsx @@ -494,7 +494,7 @@ const Login = (props: LoginProps) => { )} - {isCaptchaEnabled && ( + {isCaptchaEnabled && reCaptchaSiteKey && ( { name="otp" type="text" value={otp} + autoComplete="one-time-code" onChange={(e) => { setOtp(e.target.value); setOtpValidationError(""); diff --git a/src/components/Common/AvatarEditModal.tsx b/src/components/Common/AvatarEditModal.tsx index 31d0f772a60..3c278ea80b0 100644 --- a/src/components/Common/AvatarEditModal.tsx +++ b/src/components/Common/AvatarEditModal.tsx @@ -12,18 +12,22 @@ import { toast } from "sonner"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; - -import DialogModal from "@/components/Common/Dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import useDragAndDrop from "@/hooks/useDragAndDrop"; interface Props { title: string; open: boolean; + onOpenChange: (open: boolean) => void; imageUrl?: string; handleUpload: (file: File, onError: () => void) => Promise; handleDelete: (onError: () => void) => Promise; - onClose?: () => void; hint?: React.ReactNode; } @@ -48,10 +52,10 @@ type IVideoConstraint = const AvatarEditModal = ({ title, open, + onOpenChange, imageUrl, handleUpload, handleDelete, - onClose, hint, }: Props) => { const [isProcessing, setIsProcessing] = useState(false); @@ -91,7 +95,7 @@ const AvatarEditModal = ({ setPreview(undefined); setIsProcessing(false); setSelectedFile(undefined); - onClose?.(); + onOpenChange(false); }; useEffect(() => { @@ -179,246 +183,246 @@ const AvatarEditModal = ({ const hintMessage = hint || defaultHint; return ( - - - - {!isCameraOpen ? ( - <> - {preview || imageUrl ? ( - <> - - - - - {hintMessage} - - > - ) : ( - - + + + {title} + + + + {!isCameraOpen ? ( + <> + {preview || imageUrl ? ( + <> + + + + + {hintMessage} + + > + ) : ( + - - - - {dragProps.fileDropError !== "" - ? dragProps.fileDropError - : `${t("drag_drop_image_to_upload")}`} - - - {t("no_image_found")}. {hintMessage} - - - )} + + + + + {dragProps.fileDropError !== "" + ? dragProps.fileDropError + : `${t("drag_drop_image_to_upload")}`} + + + {t("no_image_found")}. {hintMessage} + + + )} - - + + + + + + {t("upload_an_image")} + + + + { + setConstraint(() => VideoConstraints.user); + setIsCameraOpen(true); + }} > - - - {t("upload_an_image")} - - + {`${t("open_camera")}`} - - { - setConstraint(() => VideoConstraints.user); - setIsCameraOpen(true); - }} - > - {`${t("open")} ${t("camera")}`} - - - { - e.stopPropagation(); - closeModal(); - dragProps.setFileDropError(""); - }} - disabled={isProcessing} - > - {t("cancel")} - - {imageUrl && ( + { + e.stopPropagation(); + closeModal(); + dragProps.setFileDropError(""); + }} disabled={isProcessing} > - {t("delete")} + {t("cancel")} - )} - - {isProcessing ? ( - - ) : ( - - )} - - {isProcessing ? `${t("uploading")}...` : `${t("save")}`} - - - - > - ) : ( - <> - - {!previewImage ? ( - <> - { - setIsCameraOpen(false); - toast.warning(t("camera_permission_denied")); - }} - /> - > - ) : ( - <> - - > - )} - - {/* buttons for mobile screens */} - - {!previewImage ? ( - <> - - - {`${t("switch")} ${t("camera")}`} - - { - captureImage(); - }} - > - - {t("capture")} - - > - ) : ( - <> + {imageUrl && ( { - setPreviewImage(null); - }} - > - {t("retake")} - - - {isCaptureImgBeingUploaded ? ( - <> - - {`${t("submitting")}...`} - > - ) : ( - <> {t("submit")}> - )} + {t("delete")} - > - )} - - { - setPreviewImage(null); - setIsCameraOpen(false); - webRef.current.stopCamera(); - }} - disabled={isProcessing} - > - {t("close")} - - - > - )} + )} + + {isProcessing ? ( + + ) : ( + + )} + + {isProcessing ? `${t("uploading")}...` : `${t("save")}`} + + + + > + ) : ( + <> + + {!previewImage ? ( + <> + { + setIsCameraOpen(false); + toast.warning(t("camera_permission_denied")); + }} + /> + > + ) : ( + <> + + > + )} + + {/* buttons for mobile screens */} + + {!previewImage ? ( + <> + + + {`${t("switch")} ${t("camera")}`} + + { + captureImage(); + }} + > + + {t("capture")} + + > + ) : ( + <> + { + setPreviewImage(null); + }} + > + {t("retake")} + + + {isCaptureImgBeingUploaded ? ( + <> + + {`${t("submitting")}...`} + > + ) : ( + <> {t("submit")}> + )} + + > + )} + + { + setPreviewImage(null); + setIsCameraOpen(false); + webRef.current.stopCamera(); + }} + disabled={isProcessing} + > + {t("close")} + + + > + )} + - - + + ); }; diff --git a/src/components/Common/Breadcrumbs.tsx b/src/components/Common/Breadcrumbs.tsx deleted file mode 100644 index 72951c5a98e..00000000000 --- a/src/components/Common/Breadcrumbs.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { Link, usePath } from "raviger"; -import { useState } from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, -} from "@/components/ui/breadcrumb"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -import useAppHistory from "@/hooks/useAppHistory"; - -import { classNames } from "@/Utils/utils"; - -const MENU_TAGS: { [key: string]: string } = { - facility: "Facilities", - patients: "Patients", - assets: "Assets", - shifting: "Shiftings", - resource: "Resources", - users: "Users", - notice_board: "Notice Board", -}; - -const capitalize = (string: string) => - string - .replace(/[_-]/g, " ") - .split(" ") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); - -interface BreadcrumbsProps { - replacements?: { - [key: string]: { - name?: string; - uri?: string; - style?: string; - hide?: boolean; - }; - }; - className?: string; - hideBack?: boolean; - backUrl?: string; - onBackClick?: () => boolean | void; -} - -export default function Breadcrumbs({ - replacements = {}, - className = "", - hideBack = false, - backUrl, - onBackClick, -}: BreadcrumbsProps) { - const { goBack } = useAppHistory(); - const path = usePath(); - const [showFullPath, setShowFullPath] = useState(false); - - const crumbs = path - ?.slice(1) - .split("/") - .filter((field) => replacements[field]?.hide !== true) - .map((field, i) => ({ - name: replacements[field]?.name || MENU_TAGS[field] || capitalize(field), - uri: - replacements[field]?.uri || - path - .split("/") - .slice(0, i + 2) - .join("/"), - style: replacements[field]?.style || "", - })); - - const renderCrumb = (crumb: any, index: number) => { - const isLastItem = index === crumbs!.length - 1; - return ( - - - {!isLastItem ? ( - - {crumb.name} - - ) : ( - {crumb.name} - )} - - - ); - }; - - return ( - - - - {!hideBack && ( - - { - if (onBackClick && onBackClick() === false) return; - goBack(backUrl); - }} - > - - Back - - - )} - - - Home - - - {crumbs && crumbs.length > 1 && ( - <> - {!showFullPath && ( - - - - setShowFullPath(true)} - > - ••• - - - - {crumbs.slice(0, -1).map((crumb, index) => ( - - - {crumb.name} - - - ))} - - - - )} - {showFullPath && crumbs.slice(0, -1).map(renderCrumb)} - > - )} - {crumbs?.length && - renderCrumb(crumbs[crumbs.length - 1], crumbs.length - 1)} - - - - ); -} diff --git a/src/components/Common/ConfirmDialog.tsx b/src/components/Common/ConfirmDialog.tsx deleted file mode 100644 index 78cebe23f8e..00000000000 --- a/src/components/Common/ConfirmDialog.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Button, ButtonVariant } from "@/components/ui/button"; - -import DialogModal from "@/components/Common/Dialog"; - -type ConfirmDialogProps = { - className?: string; - title: React.ReactNode; - description?: React.ReactNode; - disabled?: boolean; - show: boolean; - action: React.ReactNode; - variant?: ButtonVariant; - onClose: () => void; - onConfirm: () => void; - children?: React.ReactNode; - cancelLabel?: string; - name?: string; -}; - -const ConfirmDialog = ({ - disabled, - variant, - action, - onConfirm, - cancelLabel, - children, - name, - ...props -}: ConfirmDialogProps) => { - return ( - - {children} - - - {cancelLabel} - - - {action} - - - - ); -}; - -export default ConfirmDialog; diff --git a/src/components/Common/DebugPreview.tsx b/src/components/Common/DebugPreview.tsx new file mode 100644 index 00000000000..ef0e794ab4d --- /dev/null +++ b/src/components/Common/DebugPreview.tsx @@ -0,0 +1,28 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface DebugPreviewProps { + data: unknown; + title?: string; + className?: string; +} + +export function DebugPreview({ data, title, className }: DebugPreviewProps) { + if (!import.meta.env.DEV) { + return null; + } + + return ( + + + + {title || "Debug Preview"} + + + + + {JSON.stringify(data, null, 2)} + + + + ); +} diff --git a/src/components/Common/Dialog.tsx b/src/components/Common/Dialog.tsx deleted file mode 100644 index a67981dd3f8..00000000000 --- a/src/components/Common/Dialog.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { - Dialog, - DialogPanel, - DialogTitle, - Transition, - TransitionChild, -} from "@headlessui/react"; - -import { classNames } from "@/Utils/utils"; - -type DialogProps = { - title: React.ReactNode; - description?: React.ReactNode; - show: boolean; - onClose: () => void; - children: React.ReactNode; - className?: string; - titleAction?: React.ReactNode; - fixedWidth?: boolean; -}; - -const DialogModal = (props: DialogProps) => { - const { - title, - description, - show, - onClose, - children, - className, - fixedWidth = true, - } = props; - return ( - - - - - - - - - - - - - - {title} - - {description} - - - {props.titleAction} - - {children} - - - - - - - - ); -}; - -export default DialogModal; diff --git a/src/components/Common/FacilitySelect.tsx b/src/components/Common/FacilitySelect.tsx deleted file mode 100644 index 4209de3fc89..00000000000 --- a/src/components/Common/FacilitySelect.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { t } from "i18next"; -import { useCallback } from "react"; - -import { FacilityModel } from "@/components/Facility/models"; -import AutoCompleteAsync from "@/components/Form/AutoCompleteAsync"; - -import request from "@/Utils/request/request"; -import facilityApi from "@/types/facility/facilityApi"; - -interface BaseFacilitySelectProps { - name: string; - exclude_user?: string; - errors?: string | undefined; - className?: string; - required?: boolean; - searchAll?: boolean; - disabled?: boolean; - multiple?: boolean; - facilityType?: number; - showAll?: boolean; - showNOptions?: number | undefined; - freeText?: boolean; - allowNone?: boolean; - placeholder?: string; - filter?: (facilities: FacilityModel) => boolean; - id?: string; -} - -interface SingleFacilitySelectProps extends BaseFacilitySelectProps { - multiple?: false; - selected: FacilityModel | null; - setSelected: (selected: FacilityModel | null) => void; -} - -interface MultipleFacilitySelectProps extends BaseFacilitySelectProps { - multiple: true; - selected: FacilityModel[]; - setSelected: (selected: FacilityModel[] | null) => void; -} - -type FacilitySelectProps = - | SingleFacilitySelectProps - | MultipleFacilitySelectProps; - -export const FacilitySelect = ({ - name, - exclude_user, - required, - multiple, - selected, - setSelected, - searchAll, - disabled = false, - showAll = true, - showNOptions, - className = "", - facilityType, - allowNone = false, - freeText = false, - errors = "", - placeholder, - filter, - id, -}: FacilitySelectProps) => { - const facilitySearch = useCallback( - async (text: string) => { - const query = { - limit: 50, - offset: 0, - search_text: text, - all: searchAll, - facility_type: facilityType, - exclude_user: exclude_user, - }; - - const { data } = await request(facilityApi.getAllFacilities, { query }); - - if (allowNone) - return [ - { name: t("no_home_facility"), id: "NONE" }, - ...(data?.results || []), - ]; - - return data?.results; - }, - [searchAll, showAll, facilityType, exclude_user, freeText], - ); - - return ( - option.name} - compareBy="id" - className={className} - error={errors} - filter={filter} - /> - ); -}; diff --git a/src/components/Common/FilePreviewDialog.tsx b/src/components/Common/FilePreviewDialog.tsx index fbcaf9ae69a..f5c44865549 100644 --- a/src/components/Common/FilePreviewDialog.tsx +++ b/src/components/Common/FilePreviewDialog.tsx @@ -16,10 +16,14 @@ import useKeyboardShortcut from "use-keyboard-shortcut"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import CircularProgress from "@/components/Common/CircularProgress"; -import DialogModal from "@/components/Common/Dialog"; -import { StateInterface } from "@/components/Files/FileUpload"; import { FileUploadModel } from "@/components/Patient/models"; const PDFViewer = lazy(() => import("@/components/Common/PDFViewer")); @@ -35,6 +39,19 @@ export const zoom_values = [ "scale-200", ]; +export interface StateInterface { + open: boolean; + isImage: boolean; + name: string; + extension: string; + zoom: number; + isZoomInDisabled: boolean; + isZoomOutDisabled: boolean; + rotation: number; + id?: string; + associating_id?: string; +} + type FilePreviewProps = { title?: ReactNode; description?: ReactNode; @@ -148,11 +165,11 @@ const FilePreviewDialog = (props: FilePreviewProps) => { }; function getRotationClass(rotation: number) { - let normalizedRotation = ((rotation % 360) + 360) % 360; // Normalize rotation to be within [0, 360) + let normalizedRotation = ((rotation % 360) + 360) % 360; if (normalizedRotation > 180) { - normalizedRotation -= 360; // Adjust to be within [-180, 180) + normalizedRotation -= 360; } - return normalizedRotation === -90 // Special case for -90 rotation since tailwind doesn't support 270deg + return normalizedRotation === -90 ? "-rotate-90" : `rotate-${normalizedRotation}`; } @@ -164,232 +181,236 @@ const FilePreviewDialog = (props: FilePreviewProps) => { ); return ( - { - handleClose(); - }} - title={{t("file_preview")}} - show={show} - > - {fileUrl ? ( - <> - - - - - - - {fileNameTooltip} - - - - - {fileName} + !open && handleClose()}> + + + + {t("file_preview")} + + + + {fileUrl ? ( + <> + + + + + + + {fileNameTooltip} + + + + + {fileName} + + + + + {uploadedFiles && + uploadedFiles[index] && + uploadedFiles[index].created_date && ( + + {t("created_on")}{" "} + {new Date( + uploadedFiles[index].created_date!, + ).toLocaleString("en-US", { + dateStyle: "long", + timeStyle: "short", + })} - - - - {uploadedFiles && - uploadedFiles[index] && - uploadedFiles[index].created_date && ( - - {t("created_on")}{" "} - {new Date( - uploadedFiles[index].created_date!, - ).toLocaleString("en-US", { - dateStyle: "long", - timeStyle: "short", - })} - + )} + + + {downloadURL && downloadURL.length > 0 && ( + + + + {t("download")} + + )} + + {t("close")} + + - - {downloadURL && downloadURL.length > 0 && ( - - - - {t("download")} - + + {uploadedFiles && uploadedFiles.length > 1 && ( + handleNext(index - 1)} + disabled={index <= 0} + aria-label="Previous file" + > + )} - - {t("close")} - - - - - {uploadedFiles && uploadedFiles.length > 1 && ( - handleNext(index - 1)} - disabled={index <= 0} - aria-label="Previous file" - > - - - )} - - {file_state.isImage ? ( - - ) : file_state.extension === "pdf" ? ( - }> - { - setPage(1); - setNumPages(numPages); - }} - pageNumber={page} - scale={scale} + + {file_state.isImage ? ( + - - ) : previewExtensions.includes(file_state.extension) ? ( - - ) : ( - - }> + { + setPage(1); + setNumPages(numPages); + }} + pageNumber={page} + scale={scale} + /> + + ) : previewExtensions.includes(file_state.extension) ? ( + - {t("file_preview_not_supported")} - - )} - + ) : ( + + + {t("file_preview_not_supported")} + + )} + - {uploadedFiles && uploadedFiles.length > 1 && ( - handleNext(index + 1)} - disabled={index >= uploadedFiles.length - 1} - aria-label="Next file" - > - - - )} - - - - {file_state.isImage && ( - <> - {[ - [ - t("Zoom In"), - "l-search-plus", - handleZoomIn, - file_state.zoom === zoom_values.length, - ], - [ - `${25 * file_state.zoom}%`, - false, - () => { - setFileState({ ...file_state, zoom: 4 }); - }, - false, - ], - [ - t("Zoom Out"), - "l-search-minus", - handleZoomOut, - file_state.zoom === 1, - ], - [ - t("Rotate Left"), - "l-corner-up-left", - () => handleRotate(-90), - false, - ], - [ - t("Rotate Right"), - "l-corner-up-right", - () => handleRotate(90), - false, - ], - ].map((button, index) => ( - void} - className="z-50 rounded bg-white/60 px-4 py-2 text-black backdrop-blur transition hover:bg-white/70" - disabled={button[3] as boolean} - > - {button[1] && ( - - )} - {button[0] as string} - - ))} - > - )} - {file_state.extension === "pdf" && ( - <> - {[ - ["Zoom In", "l-search-plus", handleZoomIn, scale >= 2], - [`${Math.round(scale * 100)}%`, false, () => {}, false], - ["Zoom Out", "l-search-minus", handleZoomOut, scale <= 0.5], - [ - "Previous", - "l-arrow-left", - () => setPage((prev) => prev - 1), - page === 1, - ], - [`${page}/${numPages}`, false, () => ({}), false], - [ - "Next", - "l-arrow-right", - () => setPage((prev) => prev + 1), - page === numPages, - ], - ].map((button, index) => ( - void} - className="z-50 rounded bg-white/60 px-4 py-2 text-black backdrop-blur transition hover:bg-white/70" - disabled={button[3] as boolean} - > - {button[1] && ( - - )} - {button[0] as string} - - ))} - > + {uploadedFiles && uploadedFiles.length > 1 && ( + handleNext(index + 1)} + disabled={index >= uploadedFiles.length - 1} + aria-label={t("next_file")} + > + + )} + + + {file_state.isImage && ( + <> + {[ + [ + t("zoom_in"), + "l-search-plus", + handleZoomIn, + file_state.zoom === zoom_values.length, + ], + [ + `${25 * file_state.zoom}%`, + false, + () => { + setFileState({ ...file_state, zoom: 4 }); + }, + false, + ], + [ + t("zoom_out"), + "l-search-minus", + handleZoomOut, + file_state.zoom === 1, + ], + [ + t("rotate_left"), + "l-corner-up-left", + () => handleRotate(-90), + false, + ], + [ + t("rotate_right"), + "l-corner-up-right", + () => handleRotate(90), + false, + ], + ].map((button, index) => ( + void} + className="z-50 rounded bg-white/60 px-4 py-2 text-black backdrop-blur transition hover:bg-white/70" + disabled={button[3] as boolean} + > + {button[1] && ( + + )} + {button[0] as string} + + ))} + > + )} + {file_state.extension === "pdf" && ( + <> + {[ + [t("zoom_in"), "l-search-plus", handleZoomIn, scale >= 2], + [`${Math.round(scale * 100)}%`, false, () => {}, false], + [ + t("zoom_out"), + "l-search-minus", + handleZoomOut, + scale <= 0.5, + ], + [ + t("previous"), + "l-arrow-left", + () => setPage((prev) => prev - 1), + page === 1, + ], + [`${page}/${numPages}`, false, () => ({}), false], + [ + t("next"), + "l-arrow-right", + () => setPage((prev) => prev + 1), + page === numPages, + ], + ].map((button, index) => ( + void} + className="z-50 rounded bg-white/60 px-4 py-2 text-black backdrop-blur transition hover:bg-white/70" + disabled={button[3] as boolean} + > + {button[1] && ( + + )} + {button[0] as string} + + ))} + > + )} + + + > + ) : ( + + - > - ) : ( - - - - )} - + )} + + ); }; diff --git a/src/components/Common/HelperComponents.tsx b/src/components/Common/HelperComponents.tsx deleted file mode 100644 index 5100476284e..00000000000 --- a/src/components/Common/HelperComponents.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Transition, TransitionEvents } from "@headlessui/react"; -import { ReactNode } from "react"; - -type DropdownMenuTransitionProps = { - show?: boolean | undefined; - children: ReactNode; -} & TransitionEvents; - -export const DropdownTransition = ({ - show, - children, - ...transitionEvents -}: DropdownMenuTransitionProps) => ( - - {children} - -); diff --git a/src/components/Common/LanguageSelector.tsx b/src/components/Common/LanguageSelector.tsx index e1f3446eca0..a3b9d5c3d5d 100644 --- a/src/components/Common/LanguageSelector.tsx +++ b/src/components/Common/LanguageSelector.tsx @@ -2,7 +2,9 @@ import careConfig from "@careConfig"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { classNames, keysOf } from "@/Utils/utils"; +import { cn } from "@/lib/utils"; + +import { keysOf } from "@/Utils/utils"; import { LANGUAGES } from "@/i18n"; export const LanguageSelector = (props: any) => { @@ -27,7 +29,7 @@ export const LanguageSelector = (props: any) => { return ( { @@ -32,7 +34,7 @@ export const LanguageSelectorLogin = () => { handleLanguage(e)} - className={classNames( + className={cn( "text-primary-400 hover:text-primary-600", (i18n.language === e || (i18n.language === "en-US" && e === "en")) && diff --git a/src/components/Common/Page.tsx b/src/components/Common/Page.tsx index 24b797d245f..b9079c5089e 100644 --- a/src/components/Common/Page.tsx +++ b/src/components/Common/Page.tsx @@ -33,7 +33,7 @@ export default function Page(props: PageProps) { let padding = ""; if (!props.noImplicitPadding) { - if (!props.hideBack || props.componentRight) padding = "py-0 md:px-6"; + if (props.componentRight) padding = "py-0 md:px-6"; else padding = "px-6 py-0"; } @@ -43,13 +43,8 @@ export default function Page(props: PageProps) { diff --git a/src/components/Common/PageTitle.tsx b/src/components/Common/PageTitle.tsx index cee87a8c5fb..c374124934d 100644 --- a/src/components/Common/PageTitle.tsx +++ b/src/components/Common/PageTitle.tsx @@ -1,46 +1,26 @@ import { ReactNode, useEffect, useRef } from "react"; -import Breadcrumbs from "@/components/Common/Breadcrumbs"; -import PageHeadTitle from "@/components/Common/PageHeadTitle"; +import { cn } from "@/lib/utils"; -import { classNames } from "@/Utils/utils"; +import PageHeadTitle from "@/components/Common/PageHeadTitle"; export interface PageTitleProps { title: string; className?: string; componentRight?: ReactNode; - breadcrumbs?: boolean; - crumbsReplacements?: { - [key: string]: { - name?: string; - uri?: string; - style?: string; - hide?: boolean; - }; - }; focusOnLoad?: boolean; isInsidePage?: boolean; changePageMetadata?: boolean; - // New props for Breadcrumbs - hideBack?: boolean; - backUrl?: string; hideTitleOnPage?: boolean; - onBackClick?: () => boolean | void; } export default function PageTitle({ title, className = "", componentRight = <>>, - breadcrumbs = true, - crumbsReplacements = {}, focusOnLoad = false, isInsidePage = false, changePageMetadata = true, - // New props passed to Breadcrumbs - hideBack = false, - backUrl, - onBackClick, hideTitleOnPage, }: PageTitleProps) { const divRef = useRef(); @@ -54,23 +34,12 @@ export default function PageTitle({ return ( - - {breadcrumbs && ( - - )} - {changePageMetadata && } void; + autoFocus?: boolean; } const KeyboardShortcutHint = ({ open }: { open: boolean }) => { @@ -90,6 +91,7 @@ const SearchByMultipleFields: React.FC = ({ clearSearch, onFieldChange, enableOptionButtons = true, + autoFocus = false, }) => { const { t } = useTranslation(); const [selectedOptionIndex, setSelectedOptionIndex] = @@ -175,12 +177,6 @@ const SearchByMultipleFields: React.FC = ({ return () => document.removeEventListener("keydown", handleKeyDown); }, [focusedIndex, open, handleOptionChange, options]); - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, [selectedOptionIndex]); - useEffect(() => { if (selectedOption.value !== searchValue) { onSearch(selectedOption.key, searchValue); @@ -224,6 +220,12 @@ const SearchByMultipleFields: React.FC = ({ } }, [selectedOption, searchValue, t, inputClassName, open]); + useEffect(() => { + if (autoFocus) { + inputRef.current?.focus(); + } + }, [autoFocus, open, selectedOptionIndex]); + return ( )); + +export const FormSkeleton = ({ rows }: { rows: number }) => ( + + {Array.from({ length: rows }).map((_, index) => ( + + ))} + +); diff --git a/src/components/Common/Tabs.tsx b/src/components/Common/Tabs.tsx deleted file mode 100644 index 9c375db618e..00000000000 --- a/src/components/Common/Tabs.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { type ReactNode, useEffect, useRef } from "react"; - -import useWindowDimensions from "@/hooks/useWindowDimensions"; - -import { classNames } from "@/Utils/utils"; - -export default function Tabs(props: { - className?: string; - currentTab: string | number; - onTabChange: (value: string | number) => void; - tabs: { text: ReactNode; value: string | number; id?: string }[]; -}) { - const { className, currentTab, onTabChange, tabs } = props; - const ref = useRef(null); - const tabSwitcherRef = useRef(null); - - const dimensions = useWindowDimensions(); - - useEffect(() => { - const currentTabIndex = tabs.findIndex((t) => t.value === currentTab); - if ( - typeof currentTabIndex != "number" || - !ref.current || - !tabSwitcherRef.current - ) - return; - const tabButton = ref.current.querySelectorAll("button")[currentTabIndex]; - if (!tabButton) return; - tabSwitcherRef.current.style.width = tabButton.clientWidth + "px"; - tabSwitcherRef.current.style.left = - tabButton.getBoundingClientRect().left - - ref.current.getBoundingClientRect().left + - ref.current.scrollLeft + - "px"; - }, [currentTab, tabSwitcherRef.current, ref.current, dimensions]); - - return ( - - - {/* There has to be a better way of handling this... */} - {tabs.map((tab, i) => ( - - {tab.text} - - ))} - - {tabs.map((tab, i) => ( - onTabChange(tab.value)} - className={`${currentTab === tab.value ? "text-white" : "text-primary-500 hover:text-primary-600"} flex-1 whitespace-nowrap px-6 py-2 text-sm font-semibold transition-all`} - > - {tab.text} - - ))} - - - ); -} diff --git a/src/components/Common/UpdatableApp.tsx b/src/components/Common/UpdatableApp.tsx index 814779a99d3..3a383d73408 100644 --- a/src/components/Common/UpdatableApp.tsx +++ b/src/components/Common/UpdatableApp.tsx @@ -1,12 +1,12 @@ import { Popover, Transition } from "@headlessui/react"; import { ReactNode, useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; -import { classNames } from "@/Utils/utils"; - const META_URL = "/build-meta.json"; const APP_VERSION_KEY = "app-version"; const APP_UPDATED_KEY = "app-updated"; @@ -120,10 +120,7 @@ const UpdateAppPopup = ({ onUpdate }: UpdateAppPopupProps) => { Software Update diff --git a/src/components/Common/UserAutocompleteFormField.tsx b/src/components/Common/UserAutocompleteFormField.tsx deleted file mode 100644 index 90addafb702..00000000000 --- a/src/components/Common/UserAutocompleteFormField.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useEffect, useState } from "react"; - -import { Autocomplete } from "@/components/Form/FormFields/Autocomplete"; -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; -import { UserType } from "@/components/Users/UserFormValidations"; -import { UserBareMinimum } from "@/components/Users/models"; - -import routes from "@/Utils/request/api"; -import useTanStackQueryInstead from "@/Utils/request/useQuery"; -import { - classNames, - formatName, - isUserOnline, - mergeQueryOptions, -} from "@/Utils/utils"; - -import { Avatar } from "./Avatar"; - -type BaseProps = FormFieldBaseProps & { - placeholder?: string; - userType?: UserType; - noResultsError?: string; -}; - -type UserSearchProps = BaseProps & { - facilityId?: undefined; - homeFacility?: string; -}; - -export default function UserAutocomplete(props: UserSearchProps) { - const field = useFormFieldPropsResolver(props); - const [query, setQuery] = useState(""); - const [disabled, setDisabled] = useState(false); - - const { data, loading } = useTanStackQueryInstead(routes.userList, { - query: { - home_facility: props.homeFacility, - user_type: props.userType, - search_text: query, - limit: 50, - offset: 0, - }, - }); - - useEffect(() => { - if ( - loading || - query || - !field.required || - !props.noResultsError || - !data?.results - ) { - return; - } - - if (data.results.length === 0) { - setDisabled(true); - field.handleChange(undefined as unknown as UserBareMinimum); - } - }, [loading, field.required, data?.results, props.noResultsError]); - - const getAvatar = (option: UserBareMinimum) => { - return ( - - ); - }; - - return ( - - obj.username, - )} - optionLabel={formatName} - optionIcon={userOnlineDot} - optionImage={getAvatar} - optionDescription={(option) => - `${option.user_type} - ${option.username}` - } - optionValue={(option) => option} - onQuery={setQuery} - isLoading={loading} - /> - - ); -} - -const userOnlineDot = (user: UserBareMinimum) => ( - -); diff --git a/src/components/Common/UserSelector.tsx b/src/components/Common/UserSelector.tsx index ff2221d5561..5c898e336da 100644 --- a/src/components/Common/UserSelector.tsx +++ b/src/components/Common/UserSelector.tsx @@ -18,6 +18,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { TooltipComponent } from "@/components/ui/tooltip"; import { Avatar } from "@/components/Common/Avatar"; @@ -69,7 +70,11 @@ export default function UserSelector({ name={formatName(selected)} className="size-6 rounded-full" /> - {formatName(selected)} + + + {formatName(selected)} + + ) : ( {placeholder || t("select_user")} diff --git a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx index ea8ccfed412..2514c7bbdfe 100644 --- a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx +++ b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { useQueryParams } from "raviger"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; @@ -25,6 +25,8 @@ import { QuestionnaireResponse } from "@/types/questionnaire/questionnaireRespon interface Props { encounter?: Encounter; patientId: string; + isPrintPreview?: boolean; + onlyUnstructured?: boolean; } interface QuestionResponseProps { @@ -214,19 +216,33 @@ function ResponseCard({ item }: { item: QuestionnaireResponse }) { } /> ) : ( - - {item.questionnaire?.title} {t("filed")} - + }} + /> )} - {t("at")} - {formatDateTime(item.created_date)} - {t("by")} - - {item.created_by?.first_name || ""}{" "} - {item.created_by?.last_name || ""} - {item.created_by?.user_type && ` (${item.created_by?.user_type})`} - + + }} + /> + + + }} + /> + @@ -268,19 +284,26 @@ function ResponseCard({ item }: { item: QuestionnaireResponse }) { export default function QuestionnaireResponsesList({ encounter, patientId, + isPrintPreview = false, + onlyUnstructured, }: Props) { const { t } = useTranslation(); const [qParams, setQueryParams] = useQueryParams<{ page?: number }>(); const { data: questionnarieResponses, isLoading } = useQuery({ queryKey: ["questionnaireResponses", patientId, qParams], - queryFn: query(routes.getQuestionnaireResponses, { + queryFn: query.paginated(routes.getQuestionnaireResponses, { pathParams: { patientId }, queryParams: { + ...(!isPrintPreview && { + limit: RESULTS_PER_PAGE_LIMIT, + offset: ((qParams.page ?? 1) - 1) * RESULTS_PER_PAGE_LIMIT, + }), encounter: encounter?.id, - limit: RESULTS_PER_PAGE_LIMIT, - offset: ((qParams.page ?? 1) - 1) * RESULTS_PER_PAGE_LIMIT, + only_unstructured: onlyUnstructured, }, + maxPages: isPrintPreview ? undefined : 1, + pageSize: isPrintPreview ? 100 : RESULTS_PER_PAGE_LIMIT, }), }); @@ -308,24 +331,28 @@ export default function QuestionnaireResponsesList({ ), )} - - - RESULTS_PER_PAGE_LIMIT - ? "visible" - : "invisible", - )} - > - setQueryParams({ page })} - /> + {!isPrintPreview && ( + + + RESULTS_PER_PAGE_LIMIT + ? "visible" + : "invisible", + )} + > + setQueryParams({ page })} + /> + - + )} )} diff --git a/src/components/Facility/ConsultationDetails/QuickAccess.tsx b/src/components/Facility/ConsultationDetails/QuickAccess.tsx index b9aa70c903d..649bfe6ef8b 100644 --- a/src/components/Facility/ConsultationDetails/QuickAccess.tsx +++ b/src/components/Facility/ConsultationDetails/QuickAccess.tsx @@ -1,4 +1,3 @@ -import { useQuery } from "@tanstack/react-query"; import { Link } from "raviger"; import { useTranslation } from "react-i18next"; @@ -9,9 +8,10 @@ import { Button } from "@/components/ui/button"; import LinkDepartmentsSheet from "@/components/Patient/LinkDepartmentsSheet"; -import query from "@/Utils/request/query"; +import useQuestionnaireOptions from "@/hooks/useQuestionnaireOptions"; + +import { stringifyNestedObject } from "@/Utils/utils"; import { Encounter } from "@/types/emr/encounter"; -import questionnaireApi from "@/types/questionnaire/questionnaireApi"; interface QuickAccessProps { encounter: Encounter; @@ -19,65 +19,35 @@ interface QuickAccessProps { export default function QuickAccess({ encounter }: QuickAccessProps) { const { t } = useTranslation(); - - const { data: response } = useQuery({ - queryKey: ["questionnaires"], - queryFn: query(questionnaireApi.list), - }); - - const questionnaireList = response?.results || []; - - const encounterSettings = [ - { id: "encounter_settings", label: t("encounter_settings") }, - ]; + const questionnaireOptions = useQuestionnaireOptions("encounter_actions"); return ( {/* Questionnaire Section */} - - {t("questionnaire")} - - {questionnaireList.map((item) => ( - - - {item.title} - - ))} - - - - - - {/* Update Encounter Details */} - - - {t("update_encounter_details")} - - - {encounterSettings.map((item) => ( - + {encounter.status !== "completed" && ( + + {t("questionnaire")} + + {questionnaireOptions.map((option) => ( - {item.label} + + {t(option.title)} - - ))} - - - - + ))} + + + + )} {/* Departments and Teams */} - - + + {t("departments_and_teams")} - {org.name} + {stringifyNestedObject(org)} )) : t("no_organizations_added_yet")} diff --git a/src/components/Facility/DuplicatePatientDialog.tsx b/src/components/Facility/DuplicatePatientDialog.tsx index 2e795092042..beac625f852 100644 --- a/src/components/Facility/DuplicatePatientDialog.tsx +++ b/src/components/Facility/DuplicatePatientDialog.tsx @@ -4,6 +4,13 @@ import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Table, TableBody, @@ -13,120 +20,125 @@ import { TableRow, } from "@/components/ui/table"; -import DialogModal from "@/components/Common/Dialog"; - import { PartialPatientModel } from "@/types/emr/newPatient"; interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; patientList: Array; handleOk: (action: string) => void; - handleCancel: () => void; } const DuplicatePatientDialog = (props: Props) => { const { t } = useTranslation(); - const { patientList, handleOk, handleCancel } = props; + const { open, onOpenChange, patientList, handleOk } = props; const [action, setAction] = useState(""); return ( - - - - - {t("patient_records_found_description")}( - {patientList[0].phone_number}) - - - - - - - - {[`${t("patient_name")} / ID`, t("gender")].map( - (heading, i) => ( - {heading} - ), - )} - - - - {patientList.map((patient, i) => { - return ( - - - - {patient.name} - - - ID : {patient.id} - - - {patient.gender} - - ); - })} - - + + + + {t("patient_records_found")} + + + + + {t("patient_records_found_description")}( + {patientList[0].phone_number}) + - - - - - setAction(e.target.value)} - /> - {t("duplicate_patient_record_confirmation")} - + + + + + + {[`${t("patient_name")} / ID`, t("gender")].map( + (heading, i) => ( + {heading} + ), + )} + + + + {patientList.map((patient, i) => { + return ( + + + + {patient.name} + + + ID : {patient.id} + + + {patient.gender} + + ); + })} + + + + + + + setAction(e.target.value)} + /> + {t("duplicate_patient_record_confirmation")} + + - - - setAction(e.target.value)} - /> - {t("duplicate_patient_record_rejection")} - - + + + setAction(e.target.value)} + /> + {t("duplicate_patient_record_rejection")} + + - {t("duplicate_patient_record_birth_unknown")} + {t("duplicate_patient_record_birth_unknown")} + - - - - - {t("close")} - - handleOk(action)} - disabled={!action} - variant={"primary"} - > - - {t("continue")} - - - + + + onOpenChange(false)} + className="gap-1" + variant={"secondary"} + > + + {t("close")} + + handleOk(action)} + disabled={!action} + variant={"primary"} + > + + {t("continue")} + + + + + ); }; diff --git a/src/components/Facility/FacilityForm.tsx b/src/components/Facility/FacilityForm.tsx index b0fed2348eb..c6cabe1de6a 100644 --- a/src/components/Facility/FacilityForm.tsx +++ b/src/components/Facility/FacilityForm.tsx @@ -66,9 +66,9 @@ export default function FacilityForm({ pincode: z.string().refine(validatePincode, t("invalid_pincode")), geo_organization: z.string().min(1, t("field_required")), address: z.string().min(1, t("address_is_required")), - phone_number: validators.phoneNumber.required, - latitude: validators.coordinates.latitude.optional(), - longitude: validators.coordinates.longitude.optional(), + phone_number: validators().phoneNumber.required, + latitude: validators().coordinates.latitude.optional(), + longitude: validators().coordinates.longitude.optional(), is_public: z.boolean().default(false), }); @@ -112,6 +112,9 @@ export default function FacilityForm({ queryClient.invalidateQueries({ queryKey: ["facility"], }); + queryClient.invalidateQueries({ + queryKey: ["currentUser"], + }); form.reset(); onSubmitSuccess?.(); }, diff --git a/src/components/Facility/FacilityHome.tsx b/src/components/Facility/FacilityHome.tsx index 547d1b209d6..195e4d700bf 100644 --- a/src/components/Facility/FacilityHome.tsx +++ b/src/components/Facility/FacilityHome.tsx @@ -7,14 +7,26 @@ import { } from "@radix-ui/react-tooltip"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query"; -import { Hospital, MapPin, MoreVertical, Settings } from "lucide-react"; +import { Edit2, Hospital, MapPin, MoreVertical, Settings } from "lucide-react"; import { navigate } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DropdownMenu, @@ -26,7 +38,6 @@ import { Markdown } from "@/components/ui/markdown"; import { Avatar } from "@/components/Common/Avatar"; import AvatarEditModal from "@/components/Common/AvatarEditModal"; -import ConfirmDialog from "@/components/Common/ConfirmDialog"; import ContactLink from "@/components/Common/ContactLink"; import Loading from "@/components/Common/Loading"; @@ -96,7 +107,6 @@ const renderGeoOrganizations = (geoOrg: Organization) => { export const FacilityHome = ({ facilityId }: Props) => { const { t } = useTranslation(); - const [openDeleteDialog, setOpenDeleteDialog] = useState(false); const [editCoverImage, setEditCoverImage] = useState(false); const queryClient = useQueryClient(); @@ -106,13 +116,14 @@ export const FacilityHome = ({ facilityId }: Props) => { pathParams: { id: facilityId }, }), }); - const { mutate: deleteFacility } = useMutation({ + + const { mutate: deleteFacility, isPending: isDeleting } = useMutation({ mutationFn: mutate(routes.deleteFacility, { pathParams: { id: facilityId }, }), onSuccess: () => { toast.success( - t("entity_deleted_successfully", { name: facilityData?.name }), + t("facility_deleted_successfully", { name: facilityData?.name }), ); navigate("/facility"); }, @@ -184,26 +195,13 @@ export const FacilityHome = ({ facilityId }: Props) => { return ( - - {t("are_you_sure_want_to_delete", { name: facilityData?.name })} - - } - action="Delete" - variant="destructive" - show={openDeleteDialog} - onClose={() => setOpenDeleteDialog(false)} - onConfirm={() => deleteFacility()} - /> setEditCoverImage(false)} + onOpenChange={(open) => setEditCoverImage(open)} hint={coverImageHint} /> @@ -267,7 +265,7 @@ export const FacilityHome = ({ facilityId }: Props) => { className="cursor-pointer" onClick={() => setEditCoverImage(true)} > - + {t("edit_cover_photo")} )} @@ -276,7 +274,7 @@ export const FacilityHome = ({ facilityId }: Props) => { facilityId={facilityId} trigger={ { e.preventDefault(); }} @@ -286,16 +284,45 @@ export const FacilityHome = ({ facilityId }: Props) => { } /> - {/* TODO: get permissions from backend */} - {/* {hasPermissionToDeleteFacility && ( - setOpenDeleteDialog(true)} - > - - {t("delete_facility")} - - )} */} + + {/* TODO: add delete facility */} + {/* + e.preventDefault()} + > + + {t("delete_facility")} + + */} + + + + {t("delete_facility")} + + + {t("delete_facility_confirmation", { + name: facilityData?.name, + })} + + + + + {t("cancel")} + + deleteFacility()} + className={cn( + buttonVariants({ variant: "destructive" }), + )} + disabled={isDeleting} + > + {isDeleting ? t("deleting") : t("delete")} + + + + + ; export interface CommentModel { diff --git a/src/components/Files/ArchivedFileDialog.tsx b/src/components/Files/ArchivedFileDialog.tsx new file mode 100644 index 00000000000..ac3eec81e3e --- /dev/null +++ b/src/components/Files/ArchivedFileDialog.tsx @@ -0,0 +1,77 @@ +import dayjs from "dayjs"; +import { t } from "i18next"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +import { FileUploadModel } from "@/components/Patient/models"; + +export default function ArchivedFileDialog({ + open, + onOpenChange, + file, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + file: FileUploadModel | null; +}) { + if (!file) { + return <>>; + } + const fileName = file?.name ? file.name + file.extension : ""; + return ( + + + + + {t("archived_file")}:{" "} + + + + {fileName} + + + {fileName} + + + + + + + + + {t("archived_reason")}: + + {file?.archive_reason} + + + + {t("archived_by")}: {file.archived_by?.username} + + + {t("archived_at")}:{" "} + {dayjs(file.archived_datetime).format("DD MMM YYYY, hh:mm A")} + + + + + + ); +} diff --git a/src/components/Files/AudioPlayerDialog.tsx b/src/components/Files/AudioPlayerDialog.tsx new file mode 100644 index 00000000000..81e3139ceb4 --- /dev/null +++ b/src/components/Files/AudioPlayerDialog.tsx @@ -0,0 +1,61 @@ +import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +import AudioPlayer from "@/components/Common/AudioPlayer"; +import { FileUploadModel } from "@/components/Patient/models"; + +import routes from "@/Utils/request/api"; +import query from "@/Utils/request/query"; + +export default function AudioPlayerDialog({ + open, + onOpenChange, + file, + type, + associatingId, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + file: FileUploadModel | null; + type: "encounter" | "patient"; + associatingId: string; +}) { + const { data: fileData } = useQuery({ + queryKey: [routes.retrieveUpload, type, file?.id], + queryFn: query(routes.retrieveUpload, { + queryParams: { file_type: type, associating_id: associatingId }, + pathParams: { id: file?.id || "" }, + }), + enabled: !!file?.id, + }); + const { Player, stopPlayback } = AudioPlayer({ + src: fileData?.read_signed_url || "", + }); + return ( + { + stopPlayback(); + onOpenChange(false); + }} + aria-labelledby="audio-player-dialog" + > + + + {t("play_audio")} + + + + + ); +} diff --git a/src/components/Files/CameraCaptureDialog.tsx b/src/components/Files/CameraCaptureDialog.tsx index 54b9debc21e..8ee66a313d6 100644 --- a/src/components/Files/CameraCaptureDialog.tsx +++ b/src/components/Files/CameraCaptureDialog.tsx @@ -6,21 +6,25 @@ import { toast } from "sonner"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; - -import DialogModal from "@/components/Common/Dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import useBreakpoints from "@/hooks/useBreakpoints"; export interface CameraCaptureDialogProps { - show: boolean; - onHide: () => void; + open: boolean; + onOpenChange: (open: boolean) => void; onCapture: (file: File, fileName: string) => void; onResetCapture: () => void; setPreview?: (isPreview: boolean) => void; } export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { - const { show, onHide, onCapture, onResetCapture, setPreview } = props; + const { open, onOpenChange, onCapture, onResetCapture, setPreview } = props; const isLaptopScreen = useBreakpoints({ lg: true, default: false }); const [cameraFacingMode, setCameraFacingMode] = useState( @@ -34,8 +38,9 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { height: { ideal: 2160 }, facingMode: cameraFacingMode, }; + useEffect(() => { - if (!show) return; + if (!open) return; let stream: MediaStream | null = null; navigator.mediaDevices @@ -45,7 +50,7 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { }) .catch(() => { toast.warning(t("camera_permission_denied")); - onHide(); + onOpenChange(false); }); return () => { @@ -55,7 +60,7 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { }); } }; - }, [show, cameraFacingMode, onHide]); + }, [open, cameraFacingMode, onOpenChange]); const handleSwitchCamera = useCallback(async () => { const devices = await navigator.mediaDevices.enumerateDevices(); @@ -87,130 +92,62 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { }; return ( - - - - - - {t("camera")} - - - } - className="max-w-2xl" - onClose={onHide} - > - - {!previewImage ? ( - - - - ) : ( - - - - )} - + + + + + + + + + + + {t("camera")} + + + + - {/* buttons for mobile screens */} - - - {!previewImage ? ( - - {t("switch")} - - ) : ( - <>> - )} - {!previewImage ? ( - <> - - { - captureImage(); - setPreview?.(true); - }} - className="m-2" - > - {t("capture")} - - - > + + + ) : ( - <> - - { - setPreviewImage(null); - onResetCapture(); - setPreview?.(false); - }} - className="m-2" - > - {t("retake")} - - { - setPreviewImage(null); - onHide(); - setPreview?.(false); - }} - className="m-2" - > - {t("submit")} - - - > + + + )} - - { - setPreviewImage(null); - onResetCapture(); - onHide(); - }} - className="m-2" - > - {t("close")} - - - - {/* buttons for laptop screens */} - - - - - {`${t("switch")} ${t("camera")}`} - - - + {/* buttons for mobile and tablet screens */} + + + {!previewImage ? ( + + {t("switch")} + + ) : ( + <>> + )} + {!previewImage ? ( <> @@ -221,8 +158,8 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { captureImage(); setPreview?.(true); }} + className="m-2" > - {t("capture")} @@ -237,16 +174,18 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { onResetCapture(); setPreview?.(false); }} + className="m-2" > {t("retake")} { - onHide(); setPreviewImage(null); + onOpenChange(false); setPreview?.(false); }} + className="m-2" > {t("submit")} @@ -254,20 +193,82 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { > )} - - { - setPreviewImage(null); - onResetCapture(); - onHide(); - setPreview?.(false); - }} - > - {`${t("close")} ${t("camera")}`} - + + { + setPreviewImage(null); + onResetCapture(); + onOpenChange(false); + }} + className="m-2" + > + {t("close")} + + + + + {/* buttons for laptop screens */} + + + + {!previewImage ? ( + <> + + { + captureImage(); + setPreview?.(true); + }} + > + + {t("capture")} + + + > + ) : ( + <> + + { + setPreviewImage(null); + onResetCapture(); + setPreview?.(false); + }} + > + {t("retake")} + + { + onOpenChange(false); + setPreviewImage(null); + setPreview?.(false); + }} + > + {t("submit")} + + + > + )} + + + { + setPreviewImage(null); + onResetCapture(); + onOpenChange(false); + setPreview?.(false); + }} + > + {`${t("close")} ${t("camera")}`} + + - - + + ); } diff --git a/src/components/Files/FileBlock.tsx b/src/components/Files/FileBlock.tsx deleted file mode 100644 index 1b9cb9f8959..00000000000 --- a/src/components/Files/FileBlock.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import dayjs from "dayjs"; -import { t } from "i18next"; - -import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; - -import { Button } from "@/components/ui/button"; - -import { FileUploadModel } from "@/components/Patient/models"; - -import { FileManagerResult } from "@/hooks/useFileManager"; - -import { FILE_EXTENSIONS } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import query from "@/Utils/request/query"; - -export interface FileBlockProps { - file: FileUploadModel; - fileManager: FileManagerResult; - associating_id: string; - editable: boolean; - archivable?: boolean; -} - -export default function FileBlock(props: FileBlockProps) { - const { - file, - fileManager, - associating_id, - editable = false, - archivable = false, - } = props; - - const filetype = fileManager.getFileType(file); - - const { data: fileData } = useQuery({ - queryKey: ["file", { id: file.id, type: fileManager.type, associating_id }], - queryFn: query(routes.retrieveUpload, { - queryParams: { file_type: fileManager.type, associating_id }, - pathParams: { id: file.id || "" }, - }), - enabled: filetype === "AUDIO" && !file.is_archived, - }); - - const icons: Record = { - AUDIO: "l-volume", - IMAGE: "l-image", - PRESENTATION: "l-presentation-play", - VIDEO: "l-video", - UNKNOWN: "l-file-medical", - DOCUMENT: "l-file-medical", - }; - - const archived = file.is_archived; - - return ( - - - - - - - - {file.name} - {file.extension} {file.is_archived && "(Archived)"} - - - {dayjs( - file.is_archived ? file.archived_datetime : file.created_date, - ).format("DD MMM YYYY, hh:mm A")}{" "} - by{" "} - {file.is_archived - ? file.archived_by?.username - : file.uploaded_by?.username} - - - - - {filetype === "AUDIO" && !file.is_archived && ( - - - - )} - {!file.is_archived && - (fileManager.isPreviewable(file) ? ( - fileManager.viewFile(file, associating_id)} - className="w-full md:w-auto" - > - - {t("view")} - - ) : ( - fileManager.downloadFile(file, associating_id)} - className="w-full md:w-auto" - > - - {t("download")} - - ))} - - {!file.is_archived && editable && ( - fileManager.editFile(file, associating_id)} - className="flex-1 md:flex-auto" - > - - {t("rename")} - - )} - {(file.is_archived || editable) && archivable && ( - fileManager.archiveFile(file, associating_id)} - className="flex-1 md:flex-auto" - > - - {file.is_archived ? t("more_info") : t("archive")} - - )} - - - - ); -} diff --git a/src/components/Files/FileUpload.tsx b/src/components/Files/FileUpload.tsx deleted file mode 100644 index 54881c80c0a..00000000000 --- a/src/components/Files/FileUpload.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Loader2 } from "lucide-react"; -import { ReactNode, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; - -import Pagination from "@/components/Common/Pagination"; -import Tabs from "@/components/Common/Tabs"; -import FileBlock from "@/components/Files/FileBlock"; -import { FileUploadModel } from "@/components/Patient/models"; - -import useAuthUser from "@/hooks/useAuthUser"; -import useFileManager from "@/hooks/useFileManager"; -import useFileUpload from "@/hooks/useFileUpload"; - -import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import query from "@/Utils/request/query"; - -export const LinearProgressWithLabel = (props: { value: number }) => { - return ( - - - - - - - - {`${Math.round(props.value)}%`} - - - ); -}; - -interface FileUploadProps { - type: string; - patientId?: string; - encounterId?: string; - consentId?: string; - allowAudio?: boolean; - sampleId?: string; - claimId?: string; - className?: string; - hideUpload?: boolean; -} - -export interface ModalDetails { - name?: string; - id?: string; - reason?: string; - userArchived?: string; - archiveTime?: string; - associatedId?: string; -} - -export interface StateInterface { - open: boolean; - isImage: boolean; - name: string; - extension: string; - zoom: number; - isZoomInDisabled: boolean; - isZoomOutDisabled: boolean; - rotation: number; - id?: string; - associating_id?: string; -} - -export const FileUpload = (props: FileUploadProps) => { - const { t } = useTranslation(); - const { - encounterId, - patientId, - consentId, - type, - sampleId, - claimId, - allowAudio, - hideUpload, - } = props; - const [currentPage, setCurrentPage] = useState(1); - const [offset, setOffset] = useState(0); - const [tab, setTab] = useState("UNARCHIVED"); - const authUser = useAuthUser(); - const queryClient = useQueryClient(); - - const handlePagination = (page: number, limit: number) => { - const offset = (page - 1) * limit; - setCurrentPage(page); - setOffset(offset); - }; - - const UPLOAD_HEADING: { [index: string]: string } = { - PATIENT: t("upload_headings__patient"), - CONSULTATION: t("upload_headings__consultation"), - SAMPLE_MANAGEMENT: t("upload_headings__sample_report"), - CLAIM: t("upload_headings__supporting_info"), - }; - const VIEW_HEADING: { [index: string]: string } = { - PATIENT: t("file_list_headings__patient"), - CONSULTATION: t("file_list_headings__consultation"), - SAMPLE_MANAGEMENT: t("file_list_headings__sample_report"), - CLAIM: t("file_list_headings__supporting_info"), - }; - - const associatedId = - { - PATIENT: patientId, - CONSENT_RECORD: consentId, - ENCOUNTER: encounterId, - SAMPLE_MANAGEMENT: sampleId, - CLAIM: claimId, - }[type] || ""; - - const refetchAll = () => { - queryClient.invalidateQueries({ - queryKey: ["viewUpload", "active", type, associatedId], - }); - queryClient.invalidateQueries({ - queryKey: ["viewUpload", "archived", type, associatedId], - }); - if (type === "consultation") { - queryClient.invalidateQueries({ - queryKey: ["viewUpload", "discharge_summary", associatedId], - }); - } - }; - - const { data: activeFiles, isLoading: activeFilesLoading } = useQuery({ - queryKey: ["viewUpload", "active", type, associatedId, offset], - queryFn: query(routes.viewUpload, { - queryParams: { - file_type: type, - associating_id: associatedId, - is_archived: false, - limit: RESULTS_PER_PAGE_LIMIT, - offset: offset, - }, - }), - }); - - const { data: archivedFiles, isLoading: archivedFilesLoading } = useQuery({ - queryKey: ["viewUpload", "archived", type, associatedId, offset], - queryFn: query(routes.viewUpload, { - queryParams: { - file_type: type, - associating_id: associatedId, - is_archived: true, - limit: RESULTS_PER_PAGE_LIMIT, - offset: offset, - }, - }), - }); - - const { data: dischargeSummary, isLoading: dischargeSummaryLoading } = - useQuery({ - queryKey: ["viewUpload", "discharge_summary", associatedId, offset], - queryFn: query(routes.viewUpload, { - queryParams: { - file_type: "discharge_summary", - associating_id: associatedId, - is_archived: false, - limit: RESULTS_PER_PAGE_LIMIT, - offset: offset, - silent: true, - }, - }), - enabled: type === "consultation", - }); - - const queries = { - UNARCHIVED: { data: activeFiles, isLoading: activeFilesLoading }, - ARCHIVED: { data: archivedFiles, isLoading: archivedFilesLoading }, - DISCHARGE_SUMMARY: { - data: dischargeSummary, - isLoading: dischargeSummaryLoading, - }, - }; - - const loading = Object.values(queries).some((q) => q.isLoading); - const fileQuery = queries[tab as keyof typeof queries]; - - const tabs = [ - { text: "Active Files", value: "UNARCHIVED" }, - { text: "Archived Files", value: "ARCHIVED" }, - ...(dischargeSummary?.results?.length - ? [ - { - text: "Discharge Summary", - value: "DISCHARGE_SUMMARY", - }, - ] - : []), - ]; - - const fileUpload = useFileUpload({ - type, - allowedExtensions: [ - "jpg", - "jpeg", - "png", - "gif", - "bmp", - "tiff", - "mp4", - "mov", - "avi", - "wmv", - "mp3", - "wav", - "ogg", - "txt", - "csv", - "rtf", - "doc", - "odt", - "pdf", - "xls", - "xlsx", - "ods", - "pdf", - ], - allowNameFallback: false, - onUpload: refetchAll, - }); - - const fileManager = useFileManager({ - type, - onArchive: refetchAll, - onEdit: refetchAll, - uploadedFiles: - fileQuery?.data?.results - .slice() - .reverse() - .map((file) => ({ - ...file, - associating_id: associatedId, - })) || [], - }); - const dischargeSummaryFileManager = useFileManager({ - type: "DISCHARGE_SUMMARY", - onArchive: refetchAll, - onEdit: refetchAll, - }); - - const uploadButtons: { - name: string; - icon: IconName; - onClick?: () => void; - children?: ReactNode; - show?: boolean; - id: string; - }[] = [ - { - name: t("choose_file"), - icon: "l-file-upload-alt", - children: , - id: "upload-file", - }, - { - name: t("open_camera"), - icon: "l-camera", - onClick: fileUpload.handleCameraCapture, - id: "open-webcam", - }, - { - name: t("record"), - icon: "l-microphone", - onClick: fileUpload.handleAudioCapture, - show: allowAudio, - id: "record-audio", - }, - ]; - return ( - - {fileUpload.Dialogues} - {fileManager.Dialogues} - {dischargeSummaryFileManager.Dialogues} - {!hideUpload && ( - <> - {UPLOAD_HEADING[type]} - {fileUpload.files[0] ? ( - - - - - {fileUpload.files[0].name} - - - - - - {t("enter_file_name")} - fileUpload.setFileName(e.target.value)} - /> - {fileUpload.error && ( - {fileUpload.error} - )} - - fileUpload.handleFileUpload(associatedId)} - disabled={fileUpload.uploading} // Disable the button when loading - className={`w-full ${fileUpload.uploading ? "opacity-50" : ""}`} - id="upload_file_button" - > - {fileUpload.uploading ? ( - - ) : ( - - )} - {t("upload")} - - - - {t("discard")} - - - {!!fileUpload.progress && ( - - )} - - ) : ( - - {uploadButtons - .filter((b) => b.show !== false) - .map((button, i) => ( - - - {button.name} - {button.children} - - ))} - - )} - > - )} - - {VIEW_HEADING[type]} - setTab(v.toString())} - currentTab={tab} - /> - - - {!(fileQuery?.data?.results || []).length && loading && ( - - )} - {fileQuery?.data?.results.map((item: FileUploadModel) => ( - - ))} - {!(fileQuery?.data?.results || []).length && ( - - - {t("no_files_found", { type: tab.toLowerCase() })} - - - )} - - {(fileQuery?.data?.count ?? 0) > RESULTS_PER_PAGE_LIMIT && ( - - - - )} - - ); -}; diff --git a/src/components/Files/FileUploadDialog.tsx b/src/components/Files/FileUploadDialog.tsx new file mode 100644 index 00000000000..331d9c7266a --- /dev/null +++ b/src/components/Files/FileUploadDialog.tsx @@ -0,0 +1,204 @@ +import { t } from "i18next"; +import { useState } from "react"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Progress } from "@/components/ui/progress"; + +import { FileUploadReturn } from "@/hooks/useFileUpload"; + +export default function FileUploadDialog({ + open, + onOpenChange, + fileUpload, + associatingId, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + fileUpload: FileUploadReturn; + associatingId: string; +}) { + const handleDialogClose = (open: boolean) => { + if (!open) { + setIsPdf(false); + } + onOpenChange(open); + }; + const [isPdf, setIsPdf] = useState(false); + return ( + + + + + {fileUpload.files.length > 1 ? t("upload_files") : t("upload_file")} + + + + {isPdf ? ( + <> + {fileUpload.files.map((file, index) => ( + + + + + + {file.name.length > 40 + ? `${file.name.substring(0, 30)}...` + : file.name} + + + fileUpload.removeFile(index)} + disabled={fileUpload.uploading} + > + + + + + ))} + + + {t("enter_file_name")} + + { + fileUpload.setFileName(e.target.value); + fileUpload.setError(null); + }} + className="ml-0.5 mb-1" + /> + {fileUpload.error && ( + + {fileUpload.error} + + )} + + > + ) : ( + fileUpload.files.map((file, index) => ( + + + + + + {file.name.length > 40 + ? `${file.name.substring(0, 30)}...` + : file.name} + + + fileUpload.removeFile(index)} + disabled={fileUpload.uploading} + > + + + + + + {t("enter_file_name")} + + + { + fileUpload.setFileName(e.target.value, index); + fileUpload.setError(null); + }} + className="ml-0.5 mb-0.5" + /> + {!fileUpload.fileNames[index] && fileUpload.error && ( + + {fileUpload.error} + + )} + + + )) + )} + + {fileUpload.files.length > 1 && ( + + setIsPdf(checked)} + disabled={fileUpload.uploading} + className="cursor-pointer" + /> + + {t("combine_files_pdf")} + + + )} + + + fileUpload.handleFileUpload(associatingId, isPdf)} + disabled={fileUpload.uploading} + className="w-full" + id="upload_file_button" + > + + {t("upload")} + + + + {t("discard")} + + + {!!fileUpload.progress && ( + + )} + + + ); +} diff --git a/src/components/Files/FilesTab.tsx b/src/components/Files/FilesTab.tsx index 0d11046948a..e092adfcae7 100644 --- a/src/components/Files/FilesTab.tsx +++ b/src/components/Files/FilesTab.tsx @@ -1,28 +1,24 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import dayjs from "dayjs"; +import { t } from "i18next"; +import { Link } from "raviger"; import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import { cn } from "@/lib/utils"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; +import { Card, CardContent } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Progress } from "@/components/ui/progress"; import { Table, TableBody, @@ -31,7 +27,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Tabs, TabsContent } from "@/components/ui/tabs"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, @@ -39,64 +35,82 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; -import AudioPlayer from "@/components/Common/AudioPlayer"; import Loading from "@/components/Common/Loading"; +import ArchivedFileDialog from "@/components/Files/ArchivedFileDialog"; +import AudioPlayerDialog from "@/components/Files/AudioPlayerDialog"; +import FileUploadDialog from "@/components/Files/FileUploadDialog"; import { FileUploadModel } from "@/components/Patient/models"; import useFileManager from "@/hooks/useFileManager"; -import useFileUpload, { FileUploadReturn } from "@/hooks/useFileUpload"; +import useFileUpload from "@/hooks/useFileUpload"; import useFilters from "@/hooks/useFilters"; import { FILE_EXTENSIONS } from "@/common/constants"; import routes from "@/Utils/request/api"; +import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; -import { classNames } from "@/Utils/utils"; +import { HTTPError } from "@/Utils/request/types"; import { usePermissions } from "@/context/PermissionContext"; +import { Encounter } from "@/types/emr/encounter"; +import { Patient } from "@/types/emr/newPatient"; export interface FilesTabProps { type: "encounter" | "patient"; facilityId: string; - encounterId?: string; patientId?: string; + encounter?: Encounter; + patient?: Patient; + subPage?: string; } export const FilesTab = (props: FilesTabProps) => { - const { encounterId, patientId, type } = props; + const { patientId, type, encounter, subPage = "all" } = props; const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ limit: 14, }); - const { t } = useTranslation(); const [openUploadDialog, setOpenUploadDialog] = useState(false); + const [openArchivedFileDialog, setOpenArchivedFileDialog] = useState(false); + const [selectedArchivedFile, setSelectedArchivedFile] = + useState(null); const [selectedAudioFile, setSelectedAudioFile] = useState(null); const [openAudioPlayerDialog, setOpenAudioPlayerDialog] = useState(false); const { hasPermission } = usePermissions(); + const queryClient = useQueryClient(); const associatingId = { patient: patientId, - encounter: encounterId, + encounter: encounter?.id, }[type] || ""; const fileCategories = [ { value: "all", label: "All" }, - { value: "imaging", label: "Imaging" }, - { value: "lab_reports", label: "Lab Reports" }, - { value: "documents", label: "Documents" }, { value: "audio", label: "Audio" }, + { value: "xray", label: "X-Ray" }, + { value: "identity_proof", label: "Identity Proof" }, + { value: "unspecified", label: "Unspecified" }, + { value: "discharge_summary", label: "Discharge Summary" }, ] as const; - const handleTabChange = (value: (typeof fileCategories)[number]["value"]) => { - updateQuery({ file_category: value === "all" ? undefined : value }); - }; + const { mutate: generateDischargeSummary, isPending: isGenerating } = + useMutation<{ detail: string }, HTTPError>({ + mutationFn: mutate(routes.encounter.generateDischargeSummary, { + pathParams: { encounterId: encounter?.id || "" }, + }), + onSuccess: (response) => { + toast.success(response.detail); + refetch(); + }, + }); const { data: files, isLoading: filesLoading, refetch, } = useQuery({ - queryKey: ["files", type, associatingId, qParams], + queryKey: ["files", type, associatingId, qParams, subPage], queryFn: query(routes.viewUpload, { queryParams: { file_type: type, @@ -106,7 +120,7 @@ export const FilesTab = (props: FilesTabProps) => { ...(qParams.is_archived !== undefined && { is_archived: qParams.is_archived, }), - //file_category: qParams.file_category, + ...(subPage !== "all" && { file_category: subPage }), }, }), }); @@ -117,6 +131,7 @@ export const FilesTab = (props: FilesTabProps) => { onEdit: refetch, uploadedFiles: files?.results + .filter((file) => !file.is_archived) .slice() .reverse() .map((file) => ({ @@ -193,87 +208,128 @@ export const FilesTab = (props: FilesTabProps) => { DOCUMENT: "l-file-medical", }; - const getArchivedMessage = () => { + const getArchivedMessage = (file: FileUploadModel) => { return ( - + {t("archived")} + { + setSelectedArchivedFile(file); + setOpenArchivedFileDialog(true); + }} + > + + + {t("view")} + + ); }; + const editPermission = () => { + if (type === "encounter") { + return ( + encounter && + ![ + "completed", + "cancelled", + "entered_in_error", + "discontinued", + ].includes(encounter.status) && + hasPermission("can_write_encounter") + ); + } else if (type === "patient") { + return hasPermission("can_write_patient"); + } + return false; + }; + const DetailButtons = ({ file }: { file: FileUploadModel }) => { const filetype = getFileType(file); return ( - - {filetype === "AUDIO" && !file.is_archived && ( - { - setSelectedAudioFile(file); - setOpenAudioPlayerDialog(true); - }} - > - - - {t("play")} - - - )} - {fileManager.isPreviewable(file) && ( - fileManager.viewFile(file, associatingId)} - > - - - {t("view")} - - - )} - { - - - - + <> + {editPermission() && ( + + {filetype === "AUDIO" && !file.is_archived && ( + { + setSelectedAudioFile(file); + setOpenAudioPlayerDialog(true); + }} + > + + + {t("play")} + - - - - fileManager.downloadFile(file, associatingId)} - > - - {t("download")} - - - - fileManager.archiveFile(file, associatingId)} - > - - {t("archive")} - - - {hasPermission( - type === "encounter" - ? "can_write_encounter" - : "can_write_patient", - ) && ( - - fileManager.editFile(file, associatingId)} - > - - {t("rename")} - - - )} - - - } - + )} + {fileManager.isPreviewable(file) && ( + fileManager.viewFile(file, associatingId)} + > + + + {t("view")} + + + )} + { + + + + + + + + + + fileManager.downloadFile(file, associatingId) + } + variant="ghost" + className="w-full flex flex-row justify-stretch items-center" + > + + {t("download")} + + + + + fileManager.archiveFile(file, associatingId) + } + variant="ghost" + className="w-full flex flex-row justify-stretch items-center" + > + + {t("archive")} + + + + fileManager.editFile(file, associatingId)} + variant="ghost" + className="w-full flex flex-row justify-stretch items-center" + > + + {t("rename")} + + + + + } + + )} + > ); }; @@ -332,12 +388,7 @@ export const FilesTab = (props: FilesTabProps) => { }; const FileUploadButtons = (): JSX.Element => { - if ( - !hasPermission( - type === "encounter" ? "can_write_encounter" : "can_write_patient", - ) - ) - return <>>; + if (!editPermission()) return <>>; return ( @@ -362,38 +413,104 @@ export const FilesTab = (props: FilesTabProps) => { > {t("choose_file")} {fileUpload.Input({ className: "hidden" })} - - + fileUpload.handleCameraCapture()} - className="flex flex-row items-center " + className="flex flex-row justify-stretch items-center w-full text-primary-900" > - + {t("open_camera")} - + - - + fileUpload.handleAudioCapture()} - className="flex flex-row items-center" + className="flex flex-row justify-stretch items-center w-full text-primary-900" > - + {t("record")} - + ); }; - const RenderTable = () => { - return ( + const RenderCard = () => ( + + {files?.results && files?.results?.length > 0 ? ( + files.results.map((file) => { + const filetype = getFileType(file); + const fileName = file.name ? file.name + file.extension : ""; + + return ( + + + + + + + + + {fileName} + + {filetype} + + + + + + {t("date")} + + {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} + + + + {t("shared_by")} + + {file.uploaded_by?.username} + + + + + + {file.is_archived ? ( + getArchivedMessage(file) + ) : ( + + )} + + + + ); + }) + ) : ( + + {t("no_files_found")} + + )} + + ); + + const RenderTable = () => ( + @@ -421,12 +538,10 @@ export const FilesTab = (props: FilesTabProps) => { return ( { { {filetype} { {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} { {file.uploaded_by?.username} {file.is_archived ? ( - getArchivedMessage() + getArchivedMessage(file) ) : ( )} @@ -503,8 +618,8 @@ export const FilesTab = (props: FilesTabProps) => { )} - ); - }; + + ); return ( @@ -524,40 +639,79 @@ export const FilesTab = (props: FilesTabProps) => { associatingId={associatingId} /> + - - handleTabChange(value as (typeof fileCategories)[number]["value"]) - } - > - - - {/* - {fileCategories.map((category) => ( - - {category.label} - - ))} - */} - - + + + + + {t("all")} + + + + + {t("discharge_summary")} + + + + + + {subPage === "discharge_summary" && ( + <> + { + await queryClient.invalidateQueries({ + queryKey: ["files"], + }); + toast.success(t("refreshed")); + }} + > + + {t("refresh")} + + > + )} + {subPage === "discharge_summary" && ( + <> + generateDischargeSummary()} + disabled={isGenerating} + > + + + {isGenerating + ? t("generating") + : t("generate_discharge_summary")} + + + > + )} {fileCategories.map((category) => ( + ))} @@ -565,237 +719,3 @@ export const FilesTab = (props: FilesTabProps) => { ); }; - -const FileUploadDialog = ({ - open, - onOpenChange, - fileUpload, - associatingId, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - fileUpload: FileUploadReturn; - associatingId: string; -}) => { - const handleDialogClose = (open: boolean) => { - if (!open) { - setIsPdf(false); - } - onOpenChange(open); - }; - const { t } = useTranslation(); - const [isPdf, setIsPdf] = useState(false); - return ( - - - - - {fileUpload.files.length > 1 ? t("upload_files") : t("upload_file")} - - - - {isPdf ? ( - <> - {fileUpload.files.map((file, index) => ( - - - - - - {file.name.length > 40 - ? `${file.name.substring(0, 30)}...` - : file.name} - - - fileUpload.removeFile(index)} - disabled={fileUpload.uploading} - > - - - - - ))} - - - {t("enter_file_name")} - - { - fileUpload.setFileName(e.target.value); - fileUpload.setError(null); - }} - className="ml-0.5 mb-1" - /> - {fileUpload.error && ( - - {fileUpload.error} - - )} - - > - ) : ( - fileUpload.files.map((file, index) => ( - - - - - - {file.name.length > 40 - ? `${file.name.substring(0, 30)}...` - : file.name} - - - fileUpload.removeFile(index)} - disabled={fileUpload.uploading} - > - - - - - - {t("enter_file_name")} - - - { - fileUpload.setFileName(e.target.value, index); - fileUpload.setError(null); - }} - className="ml-0.5 mb-0.5" - /> - {!fileUpload.fileNames[index] && fileUpload.error && ( - - {fileUpload.error} - - )} - - - )) - )} - - {fileUpload.files.length > 1 && ( - - setIsPdf(checked)} - disabled={fileUpload.uploading} - className="cursor-pointer" - /> - - {t("combine_files_pdf")} - - - )} - - - fileUpload.handleFileUpload(associatingId, isPdf)} - disabled={fileUpload.uploading} - className="w-full" - id="upload_file_button" - > - - {t("upload")} - - - - {t("discard")} - - - {!!fileUpload.progress && ( - - )} - - - ); -}; - -const AudioPlayerDialog = ({ - open, - onOpenChange, - file, - type, - associatingId, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - file: FileUploadModel | null; - type: "encounter" | "patient"; - associatingId: string; -}) => { - const { t } = useTranslation(); - const { data: fileData } = useQuery({ - queryKey: [routes.retrieveUpload, type, file?.id], - queryFn: query(routes.retrieveUpload, { - queryParams: { file_type: type, associating_id: associatingId }, - pathParams: { id: file?.id || "" }, - }), - enabled: !!file?.id, - }); - const { Player, stopPlayback } = AudioPlayer({ - src: fileData?.read_signed_url || "", - }); - return ( - { - stopPlayback(); - onOpenChange(false); - }} - aria-labelledby="audio-player-dialog" - > - - - {t("play_audio")} - - - - - ); -}; diff --git a/src/components/Form/AutoCompleteAsync.tsx b/src/components/Form/AutoCompleteAsync.tsx deleted file mode 100644 index b561b1f0648..00000000000 --- a/src/components/Form/AutoCompleteAsync.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { - Combobox, - ComboboxButton, - ComboboxInput, - ComboboxOption, - ComboboxOptions, -} from "@headlessui/react"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { DropdownTransition } from "@/components/Common/HelperComponents"; -import { - MultiSelectOptionChip, - dropdownOptionClassNames, -} from "@/components/Form/MultiSelectMenuV2"; - -import useDebounce from "@/hooks/useDebounce"; - -import { classNames } from "@/Utils/utils"; - -interface Props { - id?: string; - name?: string; - selected: any | any[]; - fetchData: (search: string) => Promise | undefined; - onChange: (selected: any) => void; - optionLabel?: (option: any) => string; - optionLabelChip?: (option: any) => string; - showNOptions?: number | undefined; - multiple?: boolean; - compareBy?: string; - debounceTime?: number; - className?: string; - placeholder?: string; - disabled?: boolean; - error?: string; - required?: boolean; - onBlur?: () => void; - onFocus?: () => void; - filter?: (data: any) => boolean; -} - -const AutoCompleteAsync = (props: Props) => { - const { - id, - name, - selected, - fetchData, - onChange, - optionLabel = (option: any) => option.label, - optionLabelChip = (option: any) => option.label, - showNOptions, - multiple = false, - compareBy, - debounceTime = 300, - className = "", - placeholder, - disabled = false, - required = false, - error, - filter, - } = props; - const [data, setData] = useState([]); - const [query, setQuery] = useState(""); - const [loading, setLoading] = useState(false); - const { t } = useTranslation(); - - const hasSelection = - (!multiple && selected) || (multiple && selected?.length > 0); - - const fetchDataDebounced = useDebounce(async (searchQuery: string) => { - setLoading(true); - try { - const fetchedData = (await fetchData(searchQuery)) || []; - const filteredData = filter - ? fetchedData.filter((item: any) => filter(item)) - : fetchedData; - - setData( - showNOptions !== undefined - ? filteredData.slice(0, showNOptions) - : filteredData, - ); - } catch (error) { - console.error("Error fetching data:", error); - } finally { - setLoading(false); - } - }, debounceTime); - - useEffect(() => { - fetchDataDebounced(query); - }, [query]); - - return ( - - - - - - hasSelection && !multiple ? optionLabel?.(selected) : "" - } - onChange={({ target }) => setQuery(target.value)} - onFocus={props.onFocus} - onBlur={() => { - props.onBlur?.(); - }} - autoComplete="off" - /> - {!disabled && ( - - - {hasSelection && !loading && !required && ( - - { - e.preventDefault(); - onChange(null); - }} - /> - - {t("clear_selection")} - - - )} - {loading ? ( - - ) : ( - - )} - - - )} - - - - {data?.length === 0 ? ( - - {query !== "" - ? "Nothing found." - : "Start typing to search..."} - - ) : ( - data?.map((item: any) => ( - - {({ selected }) => ( - - - {optionLabel(item)} - {optionLabelChip(item) && ( - - {optionLabelChip(item)} - - )} - - {selected && ( - - )} - - )} - - )) - )} - - - {multiple && selected?.length > 0 && ( - - {selected?.map((option: any, index: number) => ( - - onChange( - selected.filter((item: any) => item.id !== option.id), - ) - } - /> - ))} - - )} - {error && ( - - {error} - - )} - - - - ); -}; - -export default AutoCompleteAsync; diff --git a/src/components/Form/FieldValidators.tsx b/src/components/Form/FieldValidators.tsx deleted file mode 100644 index 989a7aa6688..00000000000 --- a/src/components/Form/FieldValidators.tsx +++ /dev/null @@ -1 +0,0 @@ -export type FieldError = string | undefined; diff --git a/src/components/Form/FormFields/Autocomplete.tsx b/src/components/Form/FormFields/Autocomplete.tsx deleted file mode 100644 index 7e00c89fc7c..00000000000 --- a/src/components/Form/FormFields/Autocomplete.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { - Combobox, - ComboboxButton, - ComboboxInput, - ComboboxOption, - ComboboxOptions, -} from "@headlessui/react"; -import { ReactNode, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { DropdownTransition } from "@/components/Common/HelperComponents"; -import { dropdownOptionClassNames } from "@/components/Form/MultiSelectMenuV2"; - -import { classNames } from "@/Utils/utils"; - -type OptionCallback = (option: T) => R; - -type AutocompleteProps = { - id?: string; - options: readonly T[]; - disabled?: boolean | undefined; - value: V | undefined; - placeholder?: string; - optionLabel: OptionCallback; - optionIcon?: OptionCallback; - optionImage?: OptionCallback; - optionValue?: OptionCallback; - optionDescription?: OptionCallback; - optionDisabled?: OptionCallback; - className?: string; - minQueryLength?: number; - onQuery?: (query: string) => void; - requiredError?: boolean; - isLoading?: boolean; - allowRawInput?: boolean; - error?: string; -} & ( - | { - required?: false; - onChange: OptionCallback; - } - | { - required: true; - onChange: OptionCallback; - } -); - -/** - * Avoid using this component directly. Use `AutocompleteFormField` instead as - * its API is easier to use and compliant with `FormField` based components. - * - * Use this only when you want to hack into the design and get more - * customizability. - */ -export const Autocomplete = (props: AutocompleteProps) => { - const { t } = useTranslation(); - const [query, setQuery] = useState(""); // Ensure lower case - - useEffect(() => { - props.onQuery?.(query); - }, [query]); - - const mappedOptions = props.options.map((option) => { - const label = props.optionLabel(option); - const description = props.optionDescription?.(option); - return { - label, - description, - search: label.toLowerCase(), - icon: props.optionIcon?.(option), - image: props.optionImage?.(option), - value: props.optionValue ? props.optionValue(option) : option, - disabled: props.optionDisabled?.(option), - }; - }); - - const getOptions = () => { - if (!query) return mappedOptions; - - const knownOption = mappedOptions.find( - (o) => o.value == props.value || o.label == props.value, - ); - - if (knownOption) return mappedOptions; - return [ - { - label: query, - description: undefined, - search: query.toLowerCase(), - icon: , - image: undefined, - value: query, - disabled: undefined, - }, - ...mappedOptions, - ]; - }; - - const options = props.allowRawInput ? getOptions() : mappedOptions; - - const value = options.find((o) => props.value == o.value); - - const filteredOptions = - props.onQuery === undefined - ? options.filter((o) => o.search.includes(query)) - : options; - - return ( - - props.onChange(selection?.value)} - > - - - value?.label || ""} - onChange={(event) => setQuery(event.target.value.toLowerCase())} - onBlur={() => value && setQuery("")} - autoComplete="off" - /> - {!props.disabled && ( - - - {value?.icon} - - {value && !props.isLoading && !props.required && ( - - { - e.preventDefault(); - props.onChange(undefined); - }} - /> - - {t("clear_selection")} - - - )} - - {props.isLoading ? ( - - ) : ( - - )} - - - )} - - - - - {props.minQueryLength && query.length < props.minQueryLength ? ( - - {`Please enter at least ${props.minQueryLength} characters to search`} - - ) : filteredOptions.length === 0 ? ( - - No options found - - ) : ( - filteredOptions.map((option, index) => ( - - {({ focus }) => ( - - {option?.image} - - - {option.label} - {option.icon} - - {option.description && ( - - {option.description} - - )} - - - )} - - )) - )} - - - - - - ); -}; diff --git a/src/components/Form/FormFields/FormField.tsx b/src/components/Form/FormFields/FormField.tsx deleted file mode 100644 index f3ad0559e44..00000000000 --- a/src/components/Form/FormFields/FormField.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { FieldError } from "@/components/Form/FieldValidators"; -import { FormFieldBaseProps } from "@/components/Form/FormFields/Utils"; - -import { classNames } from "@/Utils/utils"; - -type LabelProps = { - id?: string | undefined; - required?: boolean; - htmlFor?: string; - children: React.ReactNode; - className?: string | undefined; - noPadding?: boolean; -}; - -export const FieldLabel = (props: LabelProps) => { - return ( - - {props.children} - {props.required && {" *"}} - - ); -}; - -type ErrorProps = { - error: FieldError; - className?: string | undefined; -}; - -export const FieldErrorText = (props: ErrorProps) => { - return ( - - {props.error} - - ); -}; - -/** - * @deprecated use shadcn/ui's solution for form fields instead along with react-hook-form - */ -const FormField = ({ - field, - ...props -}: { - field?: FormFieldBaseProps; - children: React.ReactNode; -}) => { - return ( - - - {field?.label && ( - - {field?.label} - - )} - {field?.labelSuffix && ( - {field?.labelSuffix} - )} - - {props.children} - - - ); -}; - -export default FormField; diff --git a/src/components/Form/FormFields/RadioFormField.tsx b/src/components/Form/FormFields/RadioFormField.tsx deleted file mode 100644 index ca205fcccad..00000000000 --- a/src/components/Form/FormFields/RadioFormField.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { ChangeEventHandler, ReactNode } from "react"; - -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; - -import { classNames } from "@/Utils/utils"; - -type Props = FormFieldBaseProps & { - options: readonly T[]; - optionLabel: (option: T) => React.ReactNode; - optionValue: (option: T) => V; - containerClassName?: string; - unselectLabel?: string; - layout?: "vertical" | "horizontal" | "grid" | "auto"; -}; - -/** - * @deprecated use shadcn/ui's radio-group instead - */ -const RadioFormField = (props: Props) => { - const field = useFormFieldPropsResolver(props); - return ( - - - {props.unselectLabel && ( - - field.handleChange(null)} - /> - {props.unselectLabel} - - )} - {props.options.map((option) => { - const value = props.optionValue(option); - return ( - field.handleChange(e.target.value as V)} - /> - ); - })} - - - ); -}; - -export default RadioFormField; - -export const RadioInput = (props: { - label?: ReactNode; - id?: string; - name?: string; - value?: string; - checked?: boolean; - onChange?: ChangeEventHandler; -}) => { - return ( - - props.onChange?.(e)} - /> - {props.label} - - ); -}; diff --git a/src/components/Form/FormFields/SelectFormField.tsx b/src/components/Form/FormFields/SelectFormField.tsx deleted file mode 100644 index 6b95550299f..00000000000 --- a/src/components/Form/FormFields/SelectFormField.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; -import SelectMenuV2 from "@/components/Form/SelectMenuV2"; - -type OptionCallback = (option: T) => R; - -type SelectFormFieldProps = FormFieldBaseProps & { - placeholder?: React.ReactNode; - options: readonly T[]; - position?: "above" | "below"; - optionLabel: OptionCallback; - optionSelectedLabel?: OptionCallback; - optionDescription?: OptionCallback; - optionIcon?: OptionCallback; - optionValue?: OptionCallback; - optionDisabled?: OptionCallback; - inputClassName?: string; -}; - -/** - * @deprecated use shadcn/ui's select instead - */ -export const SelectFormField = (props: SelectFormFieldProps) => { - const field = useFormFieldPropsResolver(props); - return ( - - field.handleChange(value)} - position={props.position} - placeholder={props.placeholder} - optionLabel={props.optionLabel} - inputClassName={props.inputClassName} - optionSelectedLabel={props.optionSelectedLabel} - optionDescription={props.optionDescription} - optionIcon={props.optionIcon} - optionValue={props.optionValue} - optionDisabled={props.optionDisabled} - requiredError={field.error ? props.required : false} - /> - - ); -}; diff --git a/src/components/Form/FormFields/TextFormField.tsx b/src/components/Form/FormFields/TextFormField.tsx deleted file mode 100644 index 18a4de18341..00000000000 --- a/src/components/Form/FormFields/TextFormField.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { - DetailedHTMLProps, - InputHTMLAttributes, - forwardRef, - useState, -} from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; - -import { classNames } from "@/Utils/utils"; - -export type TextFormFieldProps = FormFieldBaseProps & - Omit< - DetailedHTMLProps, HTMLInputElement>, - "onChange" - > & { - inputClassName?: string | undefined; - removeDefaultClasses?: true | undefined; - leading?: React.ReactNode | undefined; - trailing?: React.ReactNode | undefined; - leadingFocused?: React.ReactNode | undefined; - trailingFocused?: React.ReactNode | undefined; - trailingPadding?: string | undefined; - leadingPadding?: string | undefined; - suggestions?: string[]; - clearable?: boolean | undefined; - }; - -/** - * @deprecated use shadcn/ui's Input instead - */ -const TextFormField = forwardRef((props: TextFormFieldProps, ref) => { - const field = useFormFieldPropsResolver(props); - const { leading, trailing } = props; - const leadingFocused = props.leadingFocused || props.leading; - const trailingFocused = props.trailingFocused || props.trailing; - const hasLeading = !!(leading || leadingFocused); - const hasTrailing = !!(trailing || trailingFocused); - const hasIcon = hasLeading || hasTrailing; - const [showPassword, setShowPassword] = useState(false); - - const getPasswordFieldType = () => { - return showPassword ? "text" : "password"; - }; - - const minError = - typeof props.min !== "undefined" && - typeof field.value !== "undefined" && - parseFloat(`${props.min}`) > parseFloat(`${field.value}`) - ? `Value can not be smaller than ${props.min}` - : undefined; - const maxError = - typeof props.max !== "undefined" && - typeof field.value !== "undefined" && - parseFloat(`${props.max}`) < parseFloat(`${field.value}`) - ? `Value can not be greater than ${props.max}` - : undefined; - - const labelSuffixWithThreshold = ( - - {field.labelSuffix} - - ); - - let child = ( - - } - id={field.id} - className={classNames( - "cui-input-base peer", - hasLeading && (props.leadingPadding || "pl-10"), - hasTrailing && (props.trailingPadding || "pr-10"), - field.error && "border-danger-500", - props.inputClassName, - )} - disabled={field.disabled} - type={props.type === "password" ? getPasswordFieldType() : props.type} - name={field.name} - value={field.value} - required={field.required} - onChange={(e) => field.handleChange(e.target.value)} - /> - {props.clearable && field.value && ( - field.handleChange("")} - aria-label="Clear input" - > - - - )} - - ); - - if (props.type === "password") { - child = ( - - {child} - setShowPassword(!showPassword)} - > - - - - ); - } - - if (hasIcon) { - const _leading = - leading === leadingFocused ? ( - - {leading} - - ) : ( - <> - - {leading} - - - {leadingFocused} - - > - ); - const _trailing = - trailing === trailingFocused ? ( - - {trailing} - - ) : ( - <> - - {trailing} - - - {trailingFocused} - - > - ); - - child = ( - - {(leading || leadingFocused) && _leading} - {child} - {(trailing || trailingFocused) && _trailing} - - ); - } - - if ( - props.suggestions?.length && - !props.suggestions.includes(`${field.value}`) - ) { - child = ( - - {child} - - {props.suggestions.map((suggestion) => ( - field.handleChange(suggestion)} - > - {suggestion} - - ))} - - - ); - } - - return ( - - {child} - - ); -}); -TextFormField.displayName = "TextFormField"; - -export default TextFormField; diff --git a/src/components/Form/FormFields/Utils.ts b/src/components/Form/FormFields/Utils.ts deleted file mode 100644 index 1e88bcbd6a0..00000000000 --- a/src/components/Form/FormFields/Utils.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { FocusEvent } from "react"; - -import { FieldError } from "@/components/Form/FieldValidators"; - -export type FieldChangeEvent = { name: string; value: T }; -export type FieldChangeEventHandler = (event: FieldChangeEvent) => void; - -/** - * The base props for a form field. - * - * If a form context is provided, the field will be registered with the form - * and the onChange, value, and error props will be ignored. - * - * If a form context is not provided, the field will be treated as a standalone - * field. - * - * @template T The type of the field value. - * @template Form The type of the form details. - */ -export type FormFieldBaseProps = { - label?: React.ReactNode; - labelSuffix?: React.ReactNode; - disabled?: boolean; - className?: string; - required?: boolean; - labelClassName?: string; - errorClassName?: string; - name: string; - validate?: undefined; - id?: string; - onChange: FieldChangeEventHandler; - value?: T; - error?: FieldError; - onFocus?: (event: FocusEvent) => void; - onBlur?: (event: FocusEvent) => void; -}; - -/** - * Resolves the props for a form field. - * If a form context is provided, the field will be registered with the form. - * Otherwise, the field will be treated as a standalone field. - * - * @param props The props for the field. - * @returns The resolved props along with a handleChange function. - */ -export const useFormFieldPropsResolver = (props: FormFieldBaseProps) => { - const handleChange = (value: T) => - props.onChange({ name: props.name, value }); - - return { - ...props, - id: props.id ?? props.name, - name: props.name, - onChange: props.onChange, - value: props.value, - error: props.error, - handleChange, - }; -}; diff --git a/src/components/Form/MultiSelectMenuV2.tsx b/src/components/Form/MultiSelectMenuV2.tsx deleted file mode 100644 index b67da3656f4..00000000000 --- a/src/components/Form/MultiSelectMenuV2.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { ReactNode } from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { classNames } from "@/Utils/utils"; - -interface MultiSelectOptionChipProps { - label: ReactNode; - onRemove?: () => void; -} - -export const MultiSelectOptionChip = ({ - label, - onRemove, -}: MultiSelectOptionChipProps) => { - return ( - - {label} - {onRemove && ( - { - e.stopPropagation(); - onRemove(); - }} - > - - - )} - - ); -}; - -interface OptionRenderPropArg { - focus: boolean; - selected: boolean; - disabled: boolean; -} - -export const dropdownOptionClassNames = ({ - focus, - selected, - disabled, -}: OptionRenderPropArg) => { - return classNames( - "group/option relative w-full cursor-default select-none p-4 text-sm transition-colors duration-75 ease-in-out", - !disabled && focus && "bg-primary-500 text-white", - !disabled && !focus && selected && "text-primary-500", - !disabled && !focus && !selected && "text-secondary-900", - disabled && "cursor-not-allowed text-secondary-600", - selected ? "font-semibold" : "font-normal", - ); -}; diff --git a/src/components/Form/SelectMenuV2.tsx b/src/components/Form/SelectMenuV2.tsx deleted file mode 100644 index a13d03607c6..00000000000 --- a/src/components/Form/SelectMenuV2.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { - Label, - Listbox, - ListboxButton, - ListboxOption, - ListboxOptions, -} from "@headlessui/react"; -import { ReactNode } from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { dropdownOptionClassNames } from "@/components/Form/MultiSelectMenuV2"; - -import { classNames } from "@/Utils/utils"; - -type OptionCallback = (option: T) => R; - -type SelectMenuProps = { - id?: string; - options: readonly T[]; - disabled?: boolean | undefined; - value: V | undefined; - placeholder?: ReactNode; - position?: "above" | "below"; - optionLabel: OptionCallback; - optionSelectedLabel?: OptionCallback; - optionDescription?: OptionCallback; - optionIcon?: OptionCallback; - optionValue?: OptionCallback; - optionDisabled?: OptionCallback; - showIconWhenSelected?: boolean; - showChevronIcon?: boolean; - className?: string; - inputClassName?: string; - requiredError?: boolean; - onFocus?: () => void; - onBlur?: () => void; -} & ( - | { - required?: false; - onChange: OptionCallback; - } - | { - required: true; - onChange: OptionCallback; - } -); - -/** - * Avoid using this component directly. Use `SelectFormField` instead as its API - * is easier to use and compliant with `FormField` based components. - * - * Use this only when you want to hack into the design and get more - * customizability. - */ -const SelectMenuV2 = (props: SelectMenuProps) => { - const valueOptions = props.options.map((option) => { - const label = props.optionLabel(option); - return { - label, - selectedLabel: props.optionSelectedLabel - ? props.optionSelectedLabel(option) - : label, - description: props.optionDescription?.(option), - icon: props.optionIcon?.(option), - value: props.optionValue ? props.optionValue(option) : option, - disabled: props.optionDisabled?.(option), - }; - }); - - const showChevronIcon = props.showChevronIcon ?? true; - - const placeholder = - valueOptions?.length > 0 ? (props.placeholder ?? "Select") : "No options"; - const defaultOption = { - label: placeholder, - selectedLabel: ( - {placeholder} - ), - description: undefined, - icon: undefined, - value: undefined, - disabled: undefined, - }; - - const options = props.required - ? valueOptions - : [defaultOption, ...valueOptions]; - - const value = options.find((o) => props.value == o.value) ?? defaultOption; - - return ( - - props.onChange(selection.value)} - > - <> - {props.placeholder} - - - - - {props.showIconWhenSelected && value?.icon && ( - - {value.icon} - - )} - - {value.selectedLabel} - - - {showChevronIcon && ( - - )} - - - - - {options.map((option, index) => ( - - {({ focus, selected }) => ( - - - {option.label} - {props.optionIcon - ? option.icon - : selected && ( - - )} - - {option.description && ( - - {option.description} - - )} - - )} - - ))} - - - - > - - - ); -}; - -export default SelectMenuV2; diff --git a/src/components/Location/LocationHistorySheet.tsx b/src/components/Location/LocationHistorySheet.tsx new file mode 100644 index 00000000000..de0ddb25abb --- /dev/null +++ b/src/components/Location/LocationHistorySheet.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from "react-i18next"; + +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; + +import { LocationHistory } from "@/types/emr/encounter"; + +import { LocationTree } from "./LocationTree"; + +interface LocationHistorySheetProps { + trigger: React.ReactNode; + history: LocationHistory[]; +} + +export function LocationHistorySheet({ + trigger, + history, +}: LocationHistorySheetProps) { + const { t } = useTranslation(); + + return ( + + {trigger} + + + {t("location_history")} + + + {history.map((item, index) => ( + + + + ))} + + + + ); +} diff --git a/src/components/Location/LocationSearch.tsx b/src/components/Location/LocationSearch.tsx index bd21bb26273..a27d8e8d1ec 100644 --- a/src/components/Location/LocationSearch.tsx +++ b/src/components/Location/LocationSearch.tsx @@ -20,7 +20,7 @@ import locationApi from "@/types/location/locationApi"; interface LocationSearchProps { facilityId: string; - mode?: "kind" | "location"; + mode?: "kind" | "instance"; onSelect: (location: LocationList) => void; disabled?: boolean; value?: LocationList | null; @@ -44,7 +44,6 @@ export function LocationSearch({ }), enabled: facilityId !== "preview", }); - return ( @@ -57,7 +56,7 @@ export function LocationSearch({ - + + + + + {location.name} + + + {children} + {isLast && datetime && ( + + {format(new Date(datetime), "MMM d, yyyy h:mm a")} + + )} + + ); + } + + return ( + + + + + + {location.name} + + + {children} + {isLast && datetime && ( + + {format(new Date(datetime), "MMM d, yyyy h:mm a")} + + )} + + + ); +} + +export function LocationTree({ + location, + datetime, + isLatest, + showTimeline = false, +}: LocationPathProps) { + return ( + + {showTimeline && ( + + + + + + {!isLatest && } + + )} + + + + + ); +} diff --git a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx index 39f477f6d65..4a0bc9d63ac 100644 --- a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx +++ b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx @@ -21,7 +21,6 @@ import { EmptyState } from "@/components/Medicine/MedicationRequestTable"; import { getFrequencyDisplay } from "@/components/Medicine/MedicationsTable"; import { formatDosage } from "@/components/Medicine/utils"; -import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { formatName } from "@/Utils/utils"; @@ -29,7 +28,12 @@ import { MedicationAdministration, MedicationAdministrationRequest, } from "@/types/emr/medicationAdministration/medicationAdministration"; -import { MedicationRequestRead } from "@/types/emr/medicationRequest"; +import medicationAdministrationApi from "@/types/emr/medicationAdministration/medicationAdministrationApi"; +import { + ACTIVE_MEDICATION_STATUSES, + INACTIVE_MEDICATION_STATUSES, + MedicationRequestRead, +} from "@/types/emr/medicationRequest"; import medicationRequestApi from "@/types/emr/medicationRequest/medicationRequestApi"; import { MedicineAdminDialog } from "./MedicineAdminDialog"; @@ -40,14 +44,6 @@ import { createMedicationAdministrationRequest, } from "./utils"; -const ACTIVE_STATUSES = ["active", "on-hold", "draft", "unknown"] as const; -const INACTIVE_STATUSES = [ - "ended", - "completed", - "cancelled", - "entered_in_error", -] as const; - // Utility Functions function isTimeInSlot( date: Date, @@ -211,9 +207,13 @@ const MedicationRow: React.FC = ({ onEditAdministration, onDiscontinue, }) => { + const isInactive = INACTIVE_MEDICATION_STATUSES.includes( + medication.status as (typeof INACTIVE_MEDICATION_STATUSES)[number], + ); + return ( - + {medication.medication?.display} @@ -247,7 +247,7 @@ const MedicationRow: React.FC = ({ return ( {administrationRecords?.map((admin) => { const colorClass = @@ -260,16 +260,34 @@ const MedicationRow: React.FC = ({ className={`flex font-medium items-center gap-2 rounded-md p-2 mb-2 cursor-pointer justify-between border ${colorClass}`} onClick={() => onEditAdministration(medication, admin)} > - - - {new Date(admin.occurrence_period_start).toLocaleTimeString( - "en-US", - { + + + + {new Date( + admin.occurrence_period_start, + ).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, - }, - )} + })} + + + {admin.occurrence_period_end && ( + <> + {"- "} + {new Date( + admin.occurrence_period_end, + ).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + })} + > + )} + {admin.note && ( = ({ ); })} - - {ACTIVE_STATUSES.includes( - medication.status as (typeof ACTIVE_STATUSES)[number], + + {ACTIVE_MEDICATION_STATUSES.includes( + medication.status as (typeof ACTIVE_MEDICATION_STATUSES)[number], ) && ( @@ -336,6 +356,7 @@ export const AdministrationTab: React.FC = ({ const currentDate = new Date(); const [endSlotDate, setEndSlotDate] = useState(currentDate); const [showStopped, setShowStopped] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); const [endSlotIndex, setEndSlotIndex] = useState( Math.floor(currentDate.getHours() / 6), ); @@ -369,7 +390,7 @@ export const AdministrationTab: React.FC = ({ queryParams: { encounter: encounterId, limit: 100, - status: ACTIVE_STATUSES.join(","), + status: ACTIVE_MEDICATION_STATUSES.join(","), }, }), enabled: !!patientId, @@ -382,7 +403,7 @@ export const AdministrationTab: React.FC = ({ queryParams: { encounter: encounterId, limit: 100, - status: INACTIVE_STATUSES.join(","), + status: INACTIVE_MEDICATION_STATUSES.join(","), }, }), enabled: !!patientId, @@ -390,7 +411,7 @@ export const AdministrationTab: React.FC = ({ const { data: administrations, refetch: refetchAdministrations } = useQuery({ queryKey: ["medication_administrations", patientId, visibleSlots], - queryFn: query(routes.medicationAdministration.list, { + queryFn: query(medicationAdministrationApi.list, { pathParams: { patientId }, queryParams: { encounter: encounterId, @@ -581,36 +602,33 @@ export const AdministrationTab: React.FC = ({ ] : activeMedications?.results || []; + const filteredMedications = medications.filter( + (med: MedicationRequestRead) => { + if (!searchQuery.trim()) return true; + const searchTerm = searchQuery.toLowerCase().trim(); + const medicationName = med.medication?.display?.toLowerCase() || ""; + return medicationName.includes(searchTerm); + }, + ); + + let content; if (!activeMedications || !stoppedMedications) { - return ( + content = ( ); - } - - if (!medications?.length) { - return ( + } else if (!medications?.length) { + content = ( ); - } - - return ( - - - setIsSheetOpen(true)} - > - - {t("administer_medicine")} - - - + } else if (searchQuery && !filteredMedications.length) { + content = ; + } else { + content = ( @@ -684,7 +702,7 @@ export const AdministrationTab: React.FC = ({ {/* Medication rows */} - {medications?.map((medication) => ( + {filteredMedications?.map((medication) => ( = ({ + ); + } + + return ( + + + + + + setSearchQuery(e.target.value)} + className="flex-1 bg-transparent text-sm outline-none placeholder:text-gray-500" + /> + {searchQuery && ( + setSearchQuery("")} + > + + + )} + + + setIsSheetOpen(true)} + > + + {t("administer_medicine")} + + + + {content} {selectedMedication && administrationRequest && ( { onOpenChange(false); toast.success(t("medication_administration_saved")); diff --git a/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx b/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx index 62b48d65b65..3519619d08a 100644 --- a/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx +++ b/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx @@ -83,29 +83,37 @@ export const MedicineAdminForm: React.FC = ({ // Validate and notify parent whenever times change useEffect(() => { - if ( - !administrationRequest.occurrence_period_start || - !administrationRequest.occurrence_period_end - ) { + if (!administrationRequest.occurrence_period_start) { isValid?.(false); return; } const startDate = new Date(administrationRequest.occurrence_period_start); - const endDate = new Date(administrationRequest.occurrence_period_end); - const startError = validateDateTime(startDate, true); - const endError = validateDateTime(endDate, false); - setStartTimeError(startError); - setEndTimeError(endError); - isValid?.(!startError && !endError); + // Only validate end time if status is completed or if end time is provided + if ( + administrationRequest.status === "completed" || + administrationRequest.occurrence_period_end + ) { + if (!administrationRequest.occurrence_period_end) { + isValid?.(false); + return; + } + const endDate = new Date(administrationRequest.occurrence_period_end); + const endError = validateDateTime(endDate, false); + setEndTimeError(endError); + isValid?.(!startError && !endError); + } else { + setEndTimeError(""); + isValid?.(!startError); + } }, [ administrationRequest.occurrence_period_start, administrationRequest.occurrence_period_end, + administrationRequest.status, isValid, - validateDateTime, ]); const handleDateChange = (newTime: string, isStartTime: boolean) => { @@ -238,9 +246,21 @@ export const MedicineAdminForm: React.FC = ({ {t("status")} - onChange({ ...administrationRequest, status: value }) - } + onValueChange={(value: MedicationAdministrationStatus) => { + const newRequest = { ...administrationRequest, status: value }; + + if (value === "in_progress" || value === "not_done") { + delete newRequest.occurrence_period_end; + } else if ( + value === "completed" && + !administrationRequest.occurrence_period_end + ) { + newRequest.occurrence_period_end = + administrationRequest.occurrence_period_start; + } + + onChange(newRequest); + }} > @@ -275,13 +295,22 @@ export const MedicineAdminForm: React.FC = ({ onValueChange={(newValue) => { setIsPastTime(newValue === "yes"); if (newValue === "no") { - // Set both times to current time const now = new Date().toISOString(); - onChange({ + const newRequest = { ...administrationRequest, occurrence_period_start: now, - occurrence_period_end: now, - }); + }; + + if ( + !( + administrationRequest.status === "in_progress" || + administrationRequest.status === "not_done" + ) + ) { + newRequest.occurrence_period_end = now; + } + + onChange(newRequest); } }} className="flex gap-4" @@ -370,7 +399,8 @@ export const MedicineAdminForm: React.FC = ({ disabled={ !isPastTime || (!!administrationRequest.id && - administrationRequest.status !== "in_progress") + administrationRequest.status !== "in_progress") || + administrationRequest.status === "in_progress" } > @@ -411,7 +441,8 @@ export const MedicineAdminForm: React.FC = ({ disabled={ !isPastTime || (!!administrationRequest.id && - administrationRequest.status !== "in_progress") + administrationRequest.status !== "in_progress") || + administrationRequest.status === "in_progress" } /> diff --git a/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx b/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx index 80b212081c4..8b22e738b59 100644 --- a/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx +++ b/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx @@ -73,7 +73,7 @@ const MedicineListItem = ({ @@ -122,12 +122,9 @@ export function MedicineAdminSheet({ const formRef = useRef(null); const { mutate: upsertAdministrations, isPending } = useMutation({ - mutationFn: mutate( - medicationAdministrationApi.upsertMedicationAdministration, - { - pathParams: { patientId }, - }, - ), + mutationFn: mutate(medicationAdministrationApi.upsert, { + pathParams: { patientId }, + }), onSuccess: () => { toast.success(t("medication_administration_saved")); handleClose(); diff --git a/src/components/Medicine/MedicationRequestTable/index.tsx b/src/components/Medicine/MedicationRequestTable/index.tsx index 389093ae429..2e811025048 100644 --- a/src/components/Medicine/MedicationRequestTable/index.tsx +++ b/src/components/Medicine/MedicationRequestTable/index.tsx @@ -114,7 +114,7 @@ export default function MedicationRequestTable({ - + - + - + @@ -161,9 +166,14 @@ export default function MedicationRequestTable({ {t("edit")} - + {t("print")} @@ -184,7 +194,10 @@ export default function MedicationRequestTable({ - + {!!stoppedMedications?.results?.length && ( { +export const MedicationsTable = ({ + patientId, + encounterId, +}: MedicationsTableProps) => { const { t } = useTranslation(); - if (!medications?.length) { + const { data: medications, isLoading } = useQuery({ + queryKey: ["medication_requests", patientId, encounterId], + queryFn: query(medicationRequestApi.list, { + pathParams: { patientId }, + queryParams: { encounter: encounterId, limit: 50, offset: 0 }, + }), + }); + if (isLoading) { return ( - {t("no_medications_found_for_this_encounter")} + ); } - return ( @@ -59,15 +73,22 @@ export const MedicationsTable = ({ medications }: MedicationsTableProps) => { - {medications.map((medication) => { + {medications?.results.map((medication) => { const instruction = medication.dosage_instruction[0]; const frequency = getFrequencyDisplay(instruction?.timing); const dosage = formatDosage(instruction); const duration = instruction?.timing?.repeat?.bounds_duration; const remarks = formatSig(instruction); const notes = medication.note; + const isInactive = INACTIVE_MEDICATION_STATUSES.includes( + medication.status as (typeof INACTIVE_MEDICATION_STATUSES)[number], + ); + return ( - + {medication.medication?.display} diff --git a/src/components/Patient/EncounterQuestionnaire.tsx b/src/components/Patient/EncounterQuestionnaire.tsx index f31aeb0839a..43eb6bca2bd 100644 --- a/src/components/Patient/EncounterQuestionnaire.tsx +++ b/src/components/Patient/EncounterQuestionnaire.tsx @@ -25,10 +25,7 @@ export default function EncounterQuestionnaire({ }: Props) { const { goBack } = useAppHistory(); return ( - + - - - ); -} diff --git a/src/components/Patient/MedicationStatementList.tsx b/src/components/Patient/MedicationStatementList.tsx index 39c8e334cda..112c42414a4 100644 --- a/src/components/Patient/MedicationStatementList.tsx +++ b/src/components/Patient/MedicationStatementList.tsx @@ -2,9 +2,16 @@ import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, @@ -14,88 +21,99 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import routes from "@/Utils/request/api"; +import { Avatar } from "@/components/Common/Avatar"; + import query from "@/Utils/request/query"; -import { formatDateTime } from "@/Utils/utils"; +import { formatDateTime, formatName } from "@/Utils/utils"; +import { + MEDICATION_STATEMENT_STATUS_STYLES, + MedicationStatementRead, +} from "@/types/emr/medicationStatement"; +import medicationStatementApi from "@/types/emr/medicationStatement/medicationStatementApi"; interface MedicationStatementListProps { patientId: string; + className?: string; + isPrintPreview?: boolean; } interface MedicationRowProps { - statement: any; + statement: MedicationStatementRead; isEnteredInError?: boolean; - index: number; + isPrintPreview?: boolean; } function MedicationRow({ statement, isEnteredInError, - index, + isPrintPreview = false, }: MedicationRowProps) { + const { t } = useTranslation(); + return ( - - - - {statement.medication.display ?? statement.medication.code} - - - {statement.medication.display ?? statement.medication.code} - - - - - - - {statement.dosage_text} - - - {statement.dosage_text} - - + + {statement.medication.display ?? statement.medication.code} + {statement.dosage_text} {statement.status} - + {[statement.effective_period?.start, statement.effective_period?.end] .map((date) => formatDateTime(date)) .join(" - ")} - - - - {statement.reason} - - - {statement.reason} - - + {statement.reason} + + {statement.note ? ( + + {isPrintPreview ? ( + {statement.note} + ) : ( + + + + {t("see_note")} + + + + + {statement.note} + + + + )} + + ) : ( + "-" + )} - - - - {statement.note} - - - {statement.note} - - + + + + {formatName(statement.created_by)} + ); @@ -103,24 +121,28 @@ function MedicationRow({ export function MedicationStatementList({ patientId, + className, + isPrintPreview = false, }: MedicationStatementListProps) { const { t } = useTranslation(); - const [showEnteredInError, setShowEnteredInError] = useState(false); + const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); const { data: medications, isLoading } = useQuery({ queryKey: ["medication_statements", patientId], - queryFn: query(routes.medicationStatement.list, { + queryFn: query(medicationStatementApi.list, { pathParams: { patientId }, }), }); if (isLoading) { return ( - - + + {t("ongoing_medications")} - + @@ -138,11 +160,15 @@ export function MedicationStatementList({ if (!filteredMedications?.length) { return ( - - + + {t("ongoing_medications")} - + {t("no_ongoing_medications")} @@ -150,52 +176,75 @@ export function MedicationStatementList({ } return ( - - + + {t("ongoing_medications")} ({filteredMedications.length}) - - + + - - {t("medication")} - {t("dosage")} - {t("status")} - {t("medication_taken_between")} - {t("reason")} - {t("note")} + + + {t("medication")} + + + {t("dosage")} + + + {t("status")} + + + {t("medication_taken_between")} + + + {t("reason")} + + + {t("notes")} + + + {t("logged_by")} + - {filteredMedications.map((statement, index) => { - const isEnteredInError = statement.status === "entered_in_error"; - - return ( - <> - - > - ); - })} + {[ + ...filteredMedications.filter( + (medication) => medication.status !== "entered_in_error", + ), + ...(showEnteredInError + ? filteredMedications.filter( + (medication) => medication.status === "entered_in_error", + ) + : []), + ].map((statement) => ( + + ))} {hasEnteredInErrorRecords && !showEnteredInError && ( - - setShowEnteredInError(true)} - className="text-xs underline text-gray-500" - > - {t("view_all")} - - + <> + + + setShowEnteredInError(true)} + className="text-xs underline text-gray-500" + > + {t("view_all")} + + + > )} diff --git a/src/components/Patient/PatientDetailsTab/Appointments.tsx b/src/components/Patient/PatientDetailsTab/Appointments.tsx index 603b5aead0a..d0ec294fb48 100644 --- a/src/components/Patient/PatientDetailsTab/Appointments.tsx +++ b/src/components/Patient/PatientDetailsTab/Appointments.tsx @@ -20,7 +20,7 @@ import { PatientProps } from "@/components/Patient/PatientDetailsTab"; import query from "@/Utils/request/query"; import { formatDateTime, formatName } from "@/Utils/utils"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; export const Appointments = (props: PatientProps) => { const { patientData, facilityId, patientId } = props; diff --git a/src/components/Patient/PatientDetailsTab/PatientFiles.tsx b/src/components/Patient/PatientDetailsTab/PatientFiles.tsx index 7f65fd8c00c..df9789b35d7 100644 --- a/src/components/Patient/PatientDetailsTab/PatientFiles.tsx +++ b/src/components/Patient/PatientDetailsTab/PatientFiles.tsx @@ -8,6 +8,7 @@ export const PatientFilesTab = (props: PatientProps) => { type="patient" facilityId={props.facilityId} patientId={props.patientData.id} + patient={props.patientData} /> ); }; diff --git a/src/components/Patient/PatientDetailsTab/PatientUsers.tsx b/src/components/Patient/PatientDetailsTab/PatientUsers.tsx index 0cb118704ad..bbbafddec5b 100644 --- a/src/components/Patient/PatientDetailsTab/PatientUsers.tsx +++ b/src/components/Patient/PatientDetailsTab/PatientUsers.tsx @@ -1,8 +1,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { t } from "i18next"; import { useState } from "react"; +import { Trans } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { @@ -16,7 +19,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Select, SelectContent, @@ -32,6 +35,7 @@ import { SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; +import { TooltipComponent } from "@/components/ui/tooltip"; import { Avatar } from "@/components/Common/Avatar"; import UserSelector from "@/components/Common/UserSelector"; @@ -127,9 +131,11 @@ function AddUserSheet({ patientId }: AddUserSheetProps) { className="h-12 w-12" /> - - {formatDisplayName(selectedUser)} - + + + {formatDisplayName(selectedUser)} + + {selectedUser.email} @@ -225,7 +231,7 @@ export const PatientUsers = (props: PatientProps) => { const ManageUsers = () => { if (!users?.results?.length) { return ( - + {t("no_user_assigned")} ); @@ -235,7 +241,7 @@ export const PatientUsers = (props: PatientProps) => { {users?.results.map((user) => ( @@ -245,10 +251,20 @@ export const PatientUsers = (props: PatientProps) => { imageUrl={user.profile_picture_url} /> - - {formatDisplayName(user)} + + + + {formatDisplayName(user)} + + - {user.username} + + + + {user.username} + + + @@ -257,17 +273,24 @@ export const PatientUsers = (props: PatientProps) => { variant="ghost" size="icon" data-cy="patient-user-remove-button" + className="absolute top-0 right-0" > - + {t("remove_user")} - {t("are_you_sure_want_to_remove", { - name: formatDisplayName(user), - })} + + ), + }} + /> @@ -275,7 +298,7 @@ export const PatientUsers = (props: PatientProps) => { removeUser(user.id)} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + className={cn(buttonVariants({ variant: "destructive" }))} > {t("remove")} diff --git a/src/components/Patient/PatientHome.tsx b/src/components/Patient/PatientHome.tsx index 0807023978a..69df69c4143 100644 --- a/src/components/Patient/PatientHome.tsx +++ b/src/components/Patient/PatientHome.tsx @@ -247,30 +247,6 @@ export const PatientHome = (props: { - - {/* setOpenAssignVolunteerDialog(false)} - description={ - - setAssignedVolunteer(user.value)} - userType={"Volunteer"} - name={"assign_volunteer"} - error={errors.assignedVolunteer} - /> - - } - action={ - assignedVolunteer || !patientData.assigned_to - ? t("assign") - : t("unassign") - } - onConfirm={handleAssignedVolunteer} - /> */} ); }; diff --git a/src/components/Patient/PatientInfoCard.tsx b/src/components/Patient/PatientInfoCard.tsx index 3f2a2234a6f..18e52410b4b 100644 --- a/src/components/Patient/PatientInfoCard.tsx +++ b/src/components/Patient/PatientInfoCard.tsx @@ -12,16 +12,28 @@ import { Link } from "raviger"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { @@ -31,8 +43,8 @@ import { } from "@/components/ui/popover"; import { Avatar } from "@/components/Common/Avatar"; - -import useQuestionnaireOptions from "@/hooks/useQuestionnaireOptions"; +import { LocationHistorySheet } from "@/components/Location/LocationHistorySheet"; +import { LocationTree } from "@/components/Location/LocationTree"; import { PLUGIN_Component } from "@/PluginEngine"; import routes from "@/Utils/request/api"; @@ -53,7 +65,6 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { const { patient, encounter } = props; const { t } = useTranslation(); const queryClient = useQueryClient(); - const questionnaireOptions = useQuestionnaireOptions("encounter_actions"); const { mutate: updateEncounter } = useMutation({ mutationFn: mutate(routes.encounter.update, { @@ -101,7 +112,18 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { className="mb-2 flex flex-col text-xl font-semibold capitalize lg:hidden" id="patient-name-consultation" > - {patient.name} + + {patient.name} + + {formatPatientAge(patient, true)} •{" "} {t(`GENDER__${patient.gender}`)} @@ -115,7 +137,18 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { className="hidden flex-row text-xl font-semibold capitalize lg:flex" id="patient-name-consultation" > - {patient.name} + + {patient.name} + + {formatPatientAge(patient, true)} •{" "} {t(`GENDER__${patient.gender}`)} @@ -124,28 +157,28 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { - Start Date + {t("start_date")} {props.encounter.period.start ? formatDateTime(props.encounter.period.start) - : "Not started"} + : t("not_started")} - End Date + {t("end_date")} {props.encounter.period.end ? formatDateTime(props.encounter.period.end) - : "Ongoing"} + : t("ongoing")} {props.encounter.external_identifier && ( - Hospital Identifier + {t("hospital_identifier")} {props.encounter.external_identifier} @@ -183,7 +216,9 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { - Status History + + {t("status_history")} + {encounter.status_history.history.map( (history, index) => ( - {props.encounter.current_location && ( - - - - - - {props.encounter.current_location.name} - - - - - - - - Current Location - - - {props.encounter.current_location.name} - - - {props.encounter.current_location.description} - - - - - Move Patient - - - - - )} @@ -261,7 +259,9 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { - Class History + + {t(`class_history`)} + {encounter.encounter_class_history.history.map( (history, index) => ( } /> + {props.encounter.current_location ? ( + + + + + + {props.encounter.current_location.name} + + + + + + + + + {t("location")} + + + + + + {t("history")} + + + } + /> + + + + + + + {t("update_location")} + + + + + + ) : ( + + + + {t("add_location")} + + + )} @@ -368,36 +444,69 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { className="flex w-full flex-col gap-3 lg:w-auto 2xl:flex-row" data-cy="update-encounter-button" > - - - - {t("update")} - - - - - {questionnaireOptions.map((option) => ( - + + + + + {t("update")} + + + + + {t("actions")} + - {t(option.title)} + {t("treatment_summary")} - ))} - - {t("actions")} - - {t("mark_as_complete")} - + + + {t("discharge_summary")} + + + + e.preventDefault()}> + {t("mark_as_complete")} + + + + + + + + {t("mark_as_complete")} + + {t("mark_encounter_as_complete_confirmation")} + + + - - + + + {t("cancel")} + + + {t("mark_as_complete")} + + + + )} diff --git a/src/components/Patient/PatientRegistration.tsx b/src/components/Patient/PatientRegistration.tsx index 9acf238d3de..1c1113081ef 100644 --- a/src/components/Patient/PatientRegistration.tsx +++ b/src/components/Patient/PatientRegistration.tsx @@ -42,11 +42,7 @@ import DuplicatePatientDialog from "@/components/Facility/DuplicatePatientDialog import useAppHistory from "@/hooks/useAppHistory"; -import { - BLOOD_GROUP_CHOICES, // DOMESTIC_HEALTHCARE_SUPPORT_CHOICES, - GENDER_TYPES, // OCCUPATION_TYPES, - //RATION_CARD_CATEGORY, // SOCIOECONOMIC_STATUS_CHOICES , -} from "@/common/constants"; +import { BLOOD_GROUP_CHOICES, GENDER_TYPES } from "@/common/constants"; import { GENDERS } from "@/common/constants"; import countryList from "@/common/static/countries.json"; @@ -87,9 +83,9 @@ export default function PatientRegistration( z .object({ name: z.string().nonempty(t("name_is_required")), - phone_number: validators.phoneNumber.required, + phone_number: validators().phoneNumber.required, same_phone_number: z.boolean(), - emergency_phone_number: validators.phoneNumber.required, + emergency_phone_number: validators().phoneNumber.required, gender: z.enum(GENDERS, { required_error: t("gender_is_required") }), blood_group: z.enum(BLOOD_GROUPS, { required_error: t("blood_group_is_required"), @@ -120,7 +116,10 @@ export default function PatientRegistration( .min(100000, t("pincode_must_be_6_digits")) .max(999999, t("pincode_must_be_6_digits")), nationality: z.string().nonempty(t("nationality_is_required")), - geo_organization: z.string().uuid().optional(), + geo_organization: z + .string() + .uuid({ message: t("geo_organization_is_required") }) + .optional(), }) .refine( (data) => (data.age_or_dob === "dob" ? !!data.date_of_birth : true), @@ -175,7 +174,11 @@ export default function PatientRegistration( }, }); - const { mutate: updatePatient, isPending: isUpdatingPatient } = useMutation({ + const { + mutate: updatePatient, + isPending: isUpdatingPatient, + isSuccess: isUpdateSuccess, + } = useMutation({ mutationFn: mutate(routes.updatePatient, { pathParams: { id: patientId || "" }, }), @@ -291,7 +294,7 @@ export default function PatientRegistration( useNavigationPrompt( form.formState.isDirty && !isCreatingPatient && - !isUpdatingPatient && + !(isUpdatingPatient || isUpdateSuccess) && !showDuplicate, t("unsaved_changes"), ); @@ -548,7 +551,7 @@ export default function PatientRegistration( "age", e.target.value ? Number(e.target.value) - : (undefined as unknown as number), // intentionally setting to undefined, when the value is empty to avoid 0 in the input field + : (null as unknown as number), ) } data-cy="age-input" @@ -557,10 +560,16 @@ export default function PatientRegistration( {form.getValues("age") && ( - - {t("year_of_birth")}:{" "} - {new Date().getFullYear() - - Number(form.getValues("age"))} + + {Number(form.getValues("age")) <= 0 ? ( + Invalid age + ) : ( + + {t("year_of_birth")}:{" "} + {new Date().getFullYear() - + Number(form.getValues("age"))} + + )} )} @@ -734,10 +743,13 @@ export default function PatientRegistration( {showDuplicate && ( { - handleDialogClose("close"); + onOpenChange={(open) => { + if (!open) { + handleDialogClose("close"); + } }} /> )} diff --git a/src/components/Patient/TreatmentSummary.tsx b/src/components/Patient/TreatmentSummary.tsx new file mode 100644 index 00000000000..ad87eee91a6 --- /dev/null +++ b/src/components/Patient/TreatmentSummary.tsx @@ -0,0 +1,253 @@ +import careConfig from "@careConfig"; +import { useQuery } from "@tanstack/react-query"; +import { format } from "date-fns"; +import { t } from "i18next"; + +import PrintPreview from "@/CAREUI/misc/PrintPreview"; + +import QuestionnaireResponsesList from "@/components/Facility/ConsultationDetails/QuestionnaireResponsesList"; +import { MedicationsTable } from "@/components/Medicine/MedicationsTable"; +import { AllergyList } from "@/components/Patient/allergy/list"; +import { DiagnosisList } from "@/components/Patient/diagnosis/list"; +import { SymptomsList } from "@/components/Patient/symptoms/list"; + +import api from "@/Utils/request/api"; +import query from "@/Utils/request/query"; +import { formatName, formatPatientAge } from "@/Utils/utils"; + +import { MedicationStatementList } from "./MedicationStatementList"; + +interface TreatmentSummaryProps { + facilityId: string; + encounterId: string; +} + +export default function TreatmentSummary({ + facilityId, + encounterId, +}: TreatmentSummaryProps) { + const { data: encounter } = useQuery({ + queryKey: ["encounter", encounterId], + queryFn: query(api.encounter.get, { + pathParams: { id: encounterId }, + queryParams: { facility: facilityId }, + }), + }); + + if (!encounter) { + return ( + + {t("no_patient_record_found")} + + ); + } + + return ( + + + + {/* Header */} + + + + + {encounter.facility?.name} + + + {t("treatment_summary")} + + + + + + + {/* Patient Details */} + + + + {t("patient")} + : + {encounter.patient.name} + + + {`${t("age")} / ${t("sex")}`} + : + + {`${formatPatientAge(encounter.patient, true)}, ${t(`GENDER__${encounter.patient.gender}`)}`} + + + + {t("encounter_class")} + : + + {t(`encounter_class__${encounter.encounter_class}`)} + + + + {t("priority")} + : + + {t(`encounter_priority__${encounter.priority}`)} + + + {encounter.hospitalization?.admit_source && ( + + {t("admission_source")} + : + + {t( + `encounter_admit_sources__${encounter.hospitalization.admit_source}`, + )} + + + )} + {encounter.hospitalization?.re_admission && ( + + {t("readmission")} + : + {t("yes")} + + )} + {encounter.hospitalization?.diet_preference && ( + + {t("diet_preference")} + : + + {t( + `encounter_diet_preference__${encounter.hospitalization.diet_preference}`, + )} + + + )} + + + + {t("mobile_number")} + : + + {encounter.patient.phone_number} + + + {encounter.period?.start && ( + + {t("encounter_date")} + : + + {format( + new Date(encounter.period.start), + "dd MMM yyyy, EEEE", + )} + + + )} + + {t("status")} + : + + {t(`encounter_status__${encounter.status}`)} + + + + {t("consulting_doctor")} + : + + {formatName(encounter.created_by)} + + + {encounter.external_identifier && ( + + {t("external_id")} + : + + {encounter.external_identifier} + + + )} + {encounter.hospitalization?.discharge_disposition && ( + + + {t("discharge_disposition")} + + : + + {t( + `encounter_discharge_disposition__${encounter.hospitalization.discharge_disposition}`, + )} + + + )} + + + + {/* Medical Information */} + + {/* Allergies */} + + + {/* Symptoms */} + + + {/* Diagnoses */} + + + {/* Medications */} + + + {t("medications")} + + + + + + {/* Medication Statements */} + + + {/* Questionnaire Responses Section */} + + + + + + {/* Footer */} + + + {t("generated_on")} {format(new Date(), "PPP 'at' p")} + + + + + ); +} diff --git a/src/components/Patient/allergy/list.tsx b/src/components/Patient/allergy/list.tsx index ddbcaaab127..62695fe02e8 100644 --- a/src/components/Patient/allergy/list.tsx +++ b/src/components/Patient/allergy/list.tsx @@ -9,6 +9,8 @@ import { import { Link } from "raviger"; import { ReactNode, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Badge } from "@/components/ui/badge"; @@ -47,6 +49,8 @@ interface AllergyListProps { facilityId?: string; patientId: string; encounterId?: string; + className?: string; + isPrintPreview?: boolean; encounterStatus?: Encounter["status"]; } @@ -67,9 +71,11 @@ export function AllergyList({ facilityId, patientId, encounterId, + className, + isPrintPreview = false, encounterStatus, }: AllergyListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(false); + const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); const { data: allergies, isLoading } = useQuery({ queryKey: ["allergies", patientId, encounterId, encounterStatus], @@ -125,13 +131,6 @@ export function AllergyList({ } function AllergyRow({ allergy }: AllergyRowProps) { - const MAX_NOTE_LENGTH = 15; - const note = allergy.note || ""; - const isLongNote = note.length > MAX_NOTE_LENGTH; - const displayNote = isLongNote - ? `${note.slice(0, MAX_NOTE_LENGTH)}..` - : note; - return ( - {note && ( + {allergy.note && ( - {displayNote} - {isLongNote && ( + {isPrintPreview ? ( + + {allergy.note} + + ) : ( - {note} + {allergy.note} @@ -220,6 +222,8 @@ export function AllergyList({ facilityId={facilityId} patientId={patientId} encounterId={encounterId} + className={className} + isPrintPreview={isPrintPreview} > @@ -290,15 +294,25 @@ const AllergyListLayout = ({ patientId, encounterId, children, + className, + isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; + className?: string; + isPrintPreview?: boolean; }) => { return ( - - + + {t("allergies")} {facilityId && encounterId && ( )} - {children} + + {children} + ); }; diff --git a/src/components/Patient/diagnosis/DiagnosisTable.tsx b/src/components/Patient/diagnosis/DiagnosisTable.tsx index 3f821d5d110..ac9d93cc878 100644 --- a/src/components/Patient/diagnosis/DiagnosisTable.tsx +++ b/src/components/Patient/diagnosis/DiagnosisTable.tsx @@ -26,9 +26,13 @@ import { interface DiagnosisTableProps { diagnoses: Diagnosis[]; + isPrintPreview?: boolean; } -export function DiagnosisTable({ diagnoses }: DiagnosisTableProps) { +export function DiagnosisTable({ + diagnoses, + isPrintPreview = false, +}: DiagnosisTableProps) { return ( @@ -54,99 +58,85 @@ export function DiagnosisTable({ diagnoses }: DiagnosisTableProps) { - {diagnoses.map((diagnosis) => { - const note = diagnosis.note || ""; - const MAX_NOTE_LENGTH = 15; - const isLongNote = note.length > MAX_NOTE_LENGTH; - const displayNote = isLongNote - ? `${note.slice(0, MAX_NOTE_LENGTH)}..` - : note; - - return ( - - - {diagnosis.code.display} - - - - {t(diagnosis.clinical_status)} - - - - - {t(diagnosis.verification_status)} - - - - {diagnosis.onset?.onset_datetime - ? new Date( - diagnosis.onset.onset_datetime, - ).toLocaleDateString() - : "-"} - - - {note ? ( - - - {displayNote} - - {isLongNote && ( - - - - {t("see_note")} - - - - - {note} - - - - )} - - ) : ( - "-" - )} - - + {diagnoses.map((diagnosis) => ( + + + {diagnosis.code.display} + + + + {t(diagnosis.clinical_status)} + + + + + {t(diagnosis.verification_status)} + + + + {diagnosis.onset?.onset_datetime + ? new Date(diagnosis.onset.onset_datetime).toLocaleDateString() + : "-"} + + + {diagnosis.note ? ( - - - {diagnosis.created_by.username} - + {isPrintPreview ? ( + {diagnosis.note} + ) : ( + + + + {t("see_note")} + + + + + {diagnosis.note} + + + + )} - - - ); - })} + ) : ( + "-" + )} + + + + + {diagnosis.created_by.username} + + + + ))} ); diff --git a/src/components/Patient/diagnosis/list.tsx b/src/components/Patient/diagnosis/list.tsx index 8e7fe9f1c07..4695f5900f9 100644 --- a/src/components/Patient/diagnosis/list.tsx +++ b/src/components/Patient/diagnosis/list.tsx @@ -3,6 +3,8 @@ import { t } from "i18next"; import { Link } from "raviger"; import { ReactNode, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; @@ -18,14 +20,18 @@ interface DiagnosisListProps { patientId: string; encounterId?: string; facilityId?: string; + className?: string; + isPrintPreview?: boolean; } export function DiagnosisList({ patientId, encounterId, facilityId, + className, + isPrintPreview = false, }: DiagnosisListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(false); + const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); const { data: diagnoses, isLoading } = useQuery({ queryKey: ["diagnosis", patientId, encounterId], @@ -78,6 +84,8 @@ export function DiagnosisList({ facilityId={facilityId} patientId={patientId} encounterId={encounterId} + className={className} + isPrintPreview={isPrintPreview} > {hasEnteredInErrorRecords && !showEnteredInError && ( @@ -117,15 +126,24 @@ const DiagnosisListLayout = ({ patientId, encounterId, children, + className, + isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; + className?: string; + isPrintPreview?: boolean; }) => { return ( - - + + {t("diagnoses")} {facilityId && encounterId && ( )} - {children} + + {children} + ); }; diff --git a/src/components/Patient/models.tsx b/src/components/Patient/models.tsx index 14f048191b0..abc717e6120 100644 --- a/src/components/Patient/models.tsx +++ b/src/components/Patient/models.tsx @@ -1,46 +1,11 @@ -import { PatientCategory } from "@/components/Facility/models"; import { UserBareMinimum } from "@/components/Users/models"; import { - APPETITE_CHOICES, - BLADDER_DRAINAGE_CHOICES, - BLADDER_ISSUE_CHOICES, - BOWEL_ISSUE_CHOICES, - CONSCIOUSNESS_LEVEL, DOMESTIC_HEALTHCARE_SUPPORT_CHOICES, - HEARTBEAT_RHYTHM_CHOICES, - HumanBodyRegion, - INSULIN_INTAKE_FREQUENCY_OPTIONS, - LIMB_RESPONSE_OPTIONS, - NURSING_CARE_PROCEDURES, - NUTRITION_ROUTE_CHOICES, OCCUPATION_TYPES, - ORAL_ISSUE_CHOICES, - OXYGEN_MODALITY_OPTIONS, - PressureSoreExudateAmountOptions, - PressureSoreTissueTypeOptions, - RESPIRATORY_SUPPORT, - SLEEP_CHOICES, SOCIOECONOMIC_STATUS_CHOICES, - URINATION_FREQUENCY_CHOICES, - VENTILATOR_MODE_OPTIONS, } from "@/common/constants"; -export interface FlowModel { - id?: number; - status?: string; - created_date?: string; - modified_date?: string; - deleted?: boolean; - notes?: string; - patient_sample?: number; - created_by?: number; -} - -export interface DischargeSummaryModel { - email?: string; -} - export interface AssignedToObjectModel { first_name: string; last_name: string; @@ -56,138 +21,6 @@ export interface PatientMeta { domestic_healthcare_support?: (typeof DOMESTIC_HEALTHCARE_SUPPORT_CHOICES)[number]; } -export const DailyRoundTypes = [ - "NORMAL", - "COMMUNITY_NURSES_LOG", - "DOCTORS_LOG", - "VENTILATOR", - "AUTOMATED", - "TELEMEDICINE", -] as const; - -export type BloodPressure = { - systolic?: number; - diastolic?: number; -}; - -export interface IPainScale { - description: string; - region: HumanBodyRegion; - scale: number; -} - -export type NameQuantity = { name: string; quantity: number }; - -export type IPressureSore = { - region: HumanBodyRegion; - width: number; - length: number; - description: string; - scale: number; - exudate_amount: (typeof PressureSoreExudateAmountOptions)[number]; - tissue_type: (typeof PressureSoreTissueTypeOptions)[number]; -}; -export interface DailyRoundsModel { - spo2?: number; - rhythm?: (typeof HEARTBEAT_RHYTHM_CHOICES)[number]; - rhythm_detail?: string; - bp?: BloodPressure; - pulse?: number; - resp?: number; - temperature?: number; - physical_examination_info?: string; - other_details?: string; - consultation?: number; - action?: string; - review_interval?: number; - id?: string; - admitted_to?: string; - patient_category?: PatientCategory; - recommend_discharge?: boolean; - created_date?: string; - modified_date?: string; - taken_at?: string; - consciousness_level?: (typeof CONSCIOUSNESS_LEVEL)[number]["value"]; - rounds_type?: (typeof DailyRoundTypes)[number]; - last_updated_by_telemedicine?: boolean; - created_by_telemedicine?: boolean; - created_by?: UserBareMinimum; - last_edited_by?: UserBareMinimum; - bed?: string; - pain_scale_enhanced?: IPainScale[]; - in_prone_position?: boolean; - left_pupil_size?: number; - left_pupil_size_detail?: string; - left_pupil_light_reaction?: string; - left_pupil_light_reaction_detail?: string; - right_pupil_size?: number; - right_pupil_size_detail?: string; - right_pupil_light_reaction?: string; - right_pupil_light_reaction_detail?: string; - glasgow_eye_open?: number; - glasgow_motor_response?: number; - glasgow_verbal_response?: number; - limb_response_upper_extremity_right?: (typeof LIMB_RESPONSE_OPTIONS)[number]["value"]; - limb_response_upper_extremity_left?: (typeof LIMB_RESPONSE_OPTIONS)[number]["value"]; - limb_response_lower_extremity_left?: (typeof LIMB_RESPONSE_OPTIONS)[number]["value"]; - limb_response_lower_extremity_right?: (typeof LIMB_RESPONSE_OPTIONS)[number]["value"]; - glasgow_total_calculated?: number; - bilateral_air_entry?: boolean; - etco2?: number; - po2?: number; - pco2?: number; - ph?: number; - hco3?: number; - base_excess?: number; - lactate?: number; - sodium?: number; - potassium?: number; - blood_sugar_level?: number; - insulin_intake_dose?: number; - insulin_intake_frequency?: (typeof INSULIN_INTAKE_FREQUENCY_OPTIONS)[number]; - dialysis_fluid_balance?: number; - dialysis_net_balance?: number; - nursing?: { - procedure: (typeof NURSING_CARE_PROCEDURES)[number]; - description: string; - }[]; - feeds?: NameQuantity[]; - infusions?: NameQuantity[]; - iv_fluids?: NameQuantity[]; - output?: NameQuantity[]; - total_intake_calculated?: number; - total_output_calculated?: number; - ventilator_spo2?: number; - ventilator_interface?: (typeof RESPIRATORY_SUPPORT)[number]["value"]; - ventilator_oxygen_modality?: (typeof OXYGEN_MODALITY_OPTIONS)[number]["value"]; - ventilator_oxygen_modality_flow_rate?: number; - ventilator_oxygen_modality_oxygen_rate?: number; - ventilator_fio2?: number; - ventilator_mode?: (typeof VENTILATOR_MODE_OPTIONS)[number]; - ventilator_peep?: number; - ventilator_pip?: number; - ventilator_mean_airway_pressure?: number; - ventilator_resp_rate?: number; - ventilator_pressure_support?: number; - - ventilator_tidal_volume?: number; - pressure_sore?: IPressureSore[]; - bowel_issue?: (typeof BOWEL_ISSUE_CHOICES)[number]; - bladder_drainage?: (typeof BLADDER_DRAINAGE_CHOICES)[number]; - bladder_issue?: (typeof BLADDER_ISSUE_CHOICES)[number]; - is_experiencing_dysuria?: boolean; - urination_frequency?: (typeof URINATION_FREQUENCY_CHOICES)[number]; - sleep?: (typeof SLEEP_CHOICES)[number]; - nutrition_route?: (typeof NUTRITION_ROUTE_CHOICES)[number]; - oral_issue?: (typeof ORAL_ISSUE_CHOICES)[number]; - appetite?: (typeof APPETITE_CHOICES)[number]; -} - -export interface FacilityNameModel { - id?: string; - name?: string; -} - // File Upload Models export type FileCategory = "unspecified" | "xray" | "audio" | "identity_proof"; diff --git a/src/components/Patient/symptoms/SymptomTable.tsx b/src/components/Patient/symptoms/SymptomTable.tsx index 8fc626918e9..8b9dd9cb973 100644 --- a/src/components/Patient/symptoms/SymptomTable.tsx +++ b/src/components/Patient/symptoms/SymptomTable.tsx @@ -27,9 +27,13 @@ import { interface SymptomTableProps { symptoms: Symptom[]; + isPrintPreview?: boolean; } -export function SymptomTable({ symptoms }: SymptomTableProps) { +export function SymptomTable({ + symptoms, + isPrintPreview = false, +}: SymptomTableProps) { return ( @@ -55,27 +59,20 @@ export function SymptomTable({ symptoms }: SymptomTableProps) { - {symptoms.map((symptom) => { - const note = symptom.note || ""; - const MAX_NOTE_LENGTH = 15; - const isLongNote = note.length > MAX_NOTE_LENGTH; - const displayNote = isLongNote - ? `${note.slice(0, MAX_NOTE_LENGTH)}..` - : note; - - return ( - - - {symptom.code.display} - - + {symptoms.map((symptom) => ( + + + {symptom.code.display} + + + {symptom.severity ? ( {t(symptom.severity)} - - - - {t(symptom.clinical_status)} - - - - - {t(symptom.verification_status)} - - - - {note ? ( - - - {displayNote} - - {isLongNote && ( - - - - {t("see_note")} - - - - - {note} - - - - )} - - ) : ( - "-" - )} - - + ) : ( + "-" + )} + + + + {t(symptom.clinical_status)} + + + + + {t(symptom.verification_status)} + + + + {symptom.note ? ( - - {symptom.created_by.username} + {isPrintPreview ? ( + {symptom.note} + ) : ( + + + + {t("see_note")} + + + + + {symptom.note} + + + + )} - - - ); - })} + ) : ( + "-" + )} + + + + + {symptom.created_by.username} + + + + ))} ); diff --git a/src/components/Patient/symptoms/list.tsx b/src/components/Patient/symptoms/list.tsx index e815a229e0d..0f4b558d6a9 100644 --- a/src/components/Patient/symptoms/list.tsx +++ b/src/components/Patient/symptoms/list.tsx @@ -3,6 +3,8 @@ import { t } from "i18next"; import { Link } from "raviger"; import { ReactNode, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; @@ -18,14 +20,18 @@ interface SymptomsListProps { patientId: string; encounterId?: string; facilityId?: string; + className?: string; + isPrintPreview?: boolean; } export function SymptomsList({ patientId, encounterId, facilityId, + className, + isPrintPreview = false, }: SymptomsListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(false); + const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); const { data: symptoms, isLoading } = useQuery({ queryKey: ["symptoms", patientId, encounterId], @@ -77,6 +83,8 @@ export function SymptomsList({ facilityId={facilityId} patientId={patientId} encounterId={encounterId} + className={className} + isPrintPreview={isPrintPreview} > {hasEnteredInErrorRecords && !showEnteredInError && ( @@ -115,15 +124,25 @@ const SymptomListLayout = ({ patientId, encounterId, children, + className, + isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; + className?: string; + isPrintPreview?: boolean; }) => { return ( - - + + {t("symptoms")} {facilityId && encounterId && ( )} - {children} + + {children} + ); }; diff --git a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx index c8b41842161..834517a98a3 100644 --- a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx +++ b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx @@ -66,7 +66,7 @@ export default function CloneQuestionnaireSheet({ silent: true, }), onSuccess: async (data: QuestionnaireDetail) => { - navigate(`/questionnaire/${data.slug}`); + navigate(`/admin/questionnaire/${data.slug}`); setOpen(false); }, onError: (error) => { diff --git a/src/components/Questionnaire/CodingEditor.tsx b/src/components/Questionnaire/CodingEditor.tsx new file mode 100644 index 00000000000..6e3fe0aba2c --- /dev/null +++ b/src/components/Questionnaire/CodingEditor.tsx @@ -0,0 +1,165 @@ +import { UpdateIcon } from "@radix-ui/react-icons"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +import mutate from "@/Utils/request/mutate"; +import { Code } from "@/types/questionnaire/code"; +import { + TERMINOLOGY_SYSTEMS, + ValuesetLookupResponse, +} from "@/types/valueset/valueset"; +import valuesetApi from "@/types/valueset/valuesetApi"; + +interface CodingEditorProps { + code?: Code; + onChange: (code: Code | undefined) => void; +} + +export function CodingEditor({ code, onChange }: CodingEditorProps) { + const { mutate: verifyCode, isPending } = useMutation({ + mutationFn: mutate(valuesetApi.lookup), + onSuccess: (response: ValuesetLookupResponse) => { + if (response.metadata && code) { + onChange({ + ...code, + display: response.metadata.display, + }); + toast.success("Code verified successfully"); + } + }, + onError: (error) => { + console.error(error); + toast.error("Failed to verify code"); + }, + }); + + if (!code) { + return ( + + { + onChange({ + system: Object.values(TERMINOLOGY_SYSTEMS)[0], + code: "", + display: "", + }); + }} + > + + Add Coding + + + ); + } + + return ( + + + + Coding Details + { + onChange(undefined); + }} + > + + Remove Coding + + + + + + + System + { + onChange({ + ...code, + system: value, + code: "", + display: "", + }); + }} + > + + + + + {Object.entries(TERMINOLOGY_SYSTEMS).map(([key, value]) => ( + + {key} + + ))} + + + + + + + Code + { + onChange({ + ...code, + code: e.target.value, + display: "", + }); + }} + placeholder="Enter code" + /> + + + Display + + + + { + if (!code.system || !code.code) { + toast.error("Please select a system and enter a code first"); + return; + } + + verifyCode({ + system: code.system, + code: code.code, + }); + }} + > + + + + + + + ); +} diff --git a/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx b/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx index dec289eedf4..4ec261ca4a4 100644 --- a/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx @@ -27,7 +27,7 @@ import { } from "@/types/questionnaire/form"; import { Question } from "@/types/questionnaire/question"; import { CreateAppointmentQuestion } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; import { UserBase } from "@/types/user/user"; interface FollowUpVisitQuestionProps { diff --git a/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx b/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx index f40485d97c0..00379ae671a 100644 --- a/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx @@ -309,7 +309,13 @@ const DiagnosisItem: React.FC = ({ disabled={disabled} > - + + {t("diagnosis_status_placeholder")} + + } + /> {DIAGNOSIS_CLINICAL_STATUS.map((status) => ( @@ -339,7 +345,13 @@ const DiagnosisItem: React.FC = ({ disabled={disabled} > - + + {t("diagnosis_verification_placeholder")} + + } + /> {DIAGNOSIS_VERIFICATION_STATUS.map((status) => ( diff --git a/src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx b/src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx index ed60dbe4b7e..5d2e1391d6c 100644 --- a/src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx @@ -79,7 +79,7 @@ export function LocationQuestion({ Select Location {t("cancel")} {t("remove")} @@ -363,7 +363,7 @@ export function MedicationRequestQuestion({ {t("cancel")} {t("remove")} @@ -356,7 +356,7 @@ export function MedicationStatementQuestion({ ); } - return null; + return ( + + Medication requests cannot be recorded without an active + encounter + + ); case "medication_statement": if (encounterId) { return ( @@ -134,7 +139,12 @@ export function QuestionInput({ /> ); } - return null; + return ( + + Medication statement cannot be recorded without an active + encounter + + ); case "allergy_intolerance": return ; case "symptom": @@ -147,14 +157,20 @@ export function QuestionInput({ /> ); } - return null; + return ( + Symptoms cannot be recorded without an encounter + ); case "diagnosis": if (encounterId) { return ( ); } - return null; + return ( + + Diagnosis cannot be recorded without an active encounter + + ); case "appointment": return ; case "encounter": @@ -167,7 +183,9 @@ export function QuestionInput({ /> ); } - return null; + return ( + Create an encounter first in order to update it + ); case "location_association": if (encounterId) { return ( @@ -179,7 +197,11 @@ export function QuestionInput({ /> ); } - return null; + return ( + + Location cannot be recorded without an active encounter + + ); } return null; diff --git a/src/components/Questionnaire/QuestionnaireEditor.tsx b/src/components/Questionnaire/QuestionnaireEditor.tsx index 2637d5c9880..436dbdbb18a 100644 --- a/src/components/Questionnaire/QuestionnaireEditor.tsx +++ b/src/components/Questionnaire/QuestionnaireEditor.tsx @@ -1,4 +1,5 @@ import { useMutation, useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { ChevronDown, ChevronUp } from "lucide-react"; import { Building, Check, Loader2, X } from "lucide-react"; import { useNavigate } from "raviger"; @@ -44,16 +45,17 @@ import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; +import { DebugPreview } from "@/components/Common/DebugPreview"; import Loading from "@/components/Common/Loading"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import organizationApi from "@/types/organization/organizationApi"; import { - AnswerOption, EnableWhen, Question, QuestionType, + SUPPORTED_QUESTION_TYPES, StructuredQuestionType, } from "@/types/questionnaire/question"; import { @@ -62,7 +64,9 @@ import { SubjectType, } from "@/types/questionnaire/questionnaire"; import questionnaireApi from "@/types/questionnaire/questionnaireApi"; +import valuesetApi from "@/types/valueset/valuesetApi"; +import { CodingEditor } from "./CodingEditor"; import ManageQuestionnaireOrganizationsSheet from "./ManageQuestionnaireOrganizationsSheet"; import { QuestionnaireForm } from "./QuestionnaireForm"; @@ -117,7 +121,7 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { mutationFn: mutate(questionnaireApi.create), onSuccess: (data: QuestionnaireDetail) => { toast.success("Questionnaire created successfully"); - navigate(`/questionnaire/${data.slug}`); + navigate(`/admin/questionnaire/${data.slug}`); }, onError: (_error) => { toast.error("Failed to create questionnaire"); @@ -203,7 +207,7 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { }; const handleCancel = () => { - navigate(id ? `/questionnaire/${id}` : "/questionnaire"); + navigate(id ? `/admin/questionnaire/${id}` : "/admin/questionnaire"); }; const toggleQuestionExpanded = (questionId: string) => { @@ -632,6 +636,11 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { + @@ -690,12 +699,20 @@ function QuestionEditor({ repeats, answer_option, questions, + code, } = question; const [expandedSubQuestions, setExpandedSubQuestions] = useState>( new Set(), ); + const { data: valuesetResponse } = useQuery({ + queryKey: ["valuesets"], + queryFn: query(valuesetApi.list), + }); + + const valuesets = valuesetResponse?.results || []; + const updateField = ( field: K, value: Question[K], @@ -824,136 +841,173 @@ function QuestionEditor({ /> - - - Type - { - if (val !== "group") { - updateField("type", val, { questions: [] }); - } else { - updateField("type", val); - } - }} - > - - - - - Group - Boolean - Decimal - Integer - Date - DateTime - Time - String - Text - URL - Choice - Quantity - Structured - - - - - {type === "structured" && ( + + - Structured Type + Type - updateField("structured_type", val) - } + value={type} + onValueChange={(val: QuestionType) => { + if (val !== "group") { + updateField("type", val, { questions: [] }); + } else { + updateField("type", val); + } + }} > - + - {STRUCTURED_QUESTION_TYPES.map((type) => ( + {SUPPORTED_QUESTION_TYPES.map((type) => ( - {type.label} + {type.name} ))} - )} - - - - updateField("required", val)} - id={`required-${getQuestionPath()}`} - /> - Required + {type === "structured" && ( + + Structured Type + + updateField("structured_type", val) + } + > + + + + + {STRUCTURED_QUESTION_TYPES.map((type) => ( + + {type.label} + + ))} + + + + )} - - updateField("repeats", val)} - id={`repeats-${getQuestionPath()}`} + {type !== "structured" && type !== "group" && ( + updateField("code", newCode)} /> - Repeatable - + )} + - - updateField("collect_time", val)} - id={`collect_time-${getQuestionPath()}`} - /> - - Collect Time - - + + + Question Settings + + Configure the basic behavior: mark as required, allow multiple + entries, or set as read only. + + + + + updateField("required", val)} + id={`required-${getQuestionPath()}`} + /> + + Required + + - - updateField("collect_performer", val)} - id={`collect_performer-${getQuestionPath()}`} - /> - - Collect Performer - - + + updateField("repeats", val)} + id={`repeats-${getQuestionPath()}`} + /> + + Repeatable + + - - updateField("collect_body_site", val)} - id={`collect_body_site-${getQuestionPath()}`} - /> - - Collect Body Site - + + updateField("read_only", val)} + id={`read_only-${getQuestionPath()}`} + /> + + Read Only + + + + - - updateField("collect_method", val)} - id={`collect_method-${getQuestionPath()}`} - /> - - Collect Method - - + + + Data Collection Details + + + Specify key collection info: time, performer, body site, and + method. + + + + + + updateField("collect_time", val) + } + id={`collect_time-${getQuestionPath()}`} + /> + + Collect Time + + - - updateField("read_only", val)} - id={`read_only-${getQuestionPath()}`} - /> - - Read Only - + + + updateField("collect_performer", val) + } + id={`collect_performer-${getQuestionPath()}`} + /> + + Collect Performer + + + + + + updateField("collect_body_site", val) + } + id={`collect_body_site-${getQuestionPath()}`} + /> + + Collect Body Site + + + + + + updateField("collect_method", val) + } + id={`collect_method-${getQuestionPath()}`} + /> + + Collect Method + + + + @@ -988,329 +1042,139 @@ function QuestionEditor({ {type === "choice" && ( - - Answer Options - + + + + + Answer Options + + + Define possible answers for this question + + updateField( "answer_value_set", - val === "custom" ? undefined : val, + val === "custom" ? undefined : "valueset", ) } > - - + + - Custom Options - Yes/No - - Severity Levels + + {t("custom_options")} - - Frequency - - Duration + {t("value_set")} - - {(!question.answer_value_set || - question.answer_value_set === "custom") && ( - + + + {!question.answer_value_set ? ( + {(answer_option || []).map((opt, idx) => ( - - { - const newOptions = [...(answer_option || [])]; - newOptions[idx] = { ...opt, value: e.target.value }; - updateField("answer_option", newOptions); - }} - placeholder="Option value" - /> - - { - const newOptions = [...(answer_option || [])]; - newOptions[idx] = { - ...opt, - display: e.target.value, - }; - updateField("answer_option", newOptions); - }} - placeholder="Display text (optional)" - /> - { - const newOptions = answer_option?.filter( - (_, i) => i !== idx, - ); - updateField("answer_option", newOptions); - }} - > - - + + + + Value + { + const newOptions = answer_option + ? [...answer_option] + : []; + newOptions[idx] = { + ...opt, + value: e.target.value, + }; + updateField("answer_option", newOptions); + }} + placeholder="Option value" + /> + + + + Display Text + { + const newOptions = answer_option + ? [...answer_option] + : []; + newOptions[idx] = { + ...opt, + display: e.target.value, + }; + updateField("answer_option", newOptions); + }} + placeholder="Display text (optional)" + /> + + { + const newOptions = answer_option?.filter( + (_, i) => i !== idx, + ); + updateField("answer_option", newOptions); + }} + > + + + ))} + { - const newOption: AnswerOption = { value: "" }; - updateField("answer_option", [ - ...(answer_option || []), - newOption, - ]); + const newOption = { value: "" }; + const newOptions = answer_option + ? [...answer_option, newOption] + : [newOption]; + updateField("answer_option", newOptions); }} > Add Option - - )} - - - - Enable When Conditions - - {(question.enable_when || []).length > 0 && ( - - Enable Behavior - - updateField("enable_behavior", val) - } - > - - - - - - All conditions must be met - - - Any condition must be met - - - - - )} - {(question.enable_when || []).map((condition, idx) => ( - + ) : ( + + + updateField("answer_value_set", val) + } > - - Question - { - const newConditions = [ - ...(question.enable_when || []), - ]; - newConditions[idx] = { - ...condition, - question: e.target.value, - }; - updateField("enable_when", newConditions); - }} - placeholder="Question Link ID" - /> - - - Operator - { - const newConditions = [ - ...(question.enable_when || []), - ]; - - switch (val) { - case "greater": - case "less": - case "greater_or_equals": - case "less_or_equals": - newConditions[idx] = { - question: condition.question, - operator: val, - answer: 0, - }; - break; - case "exists": - newConditions[idx] = { - question: condition.question, - operator: val, - answer: true, - }; - break; - case "equals": - case "not_equals": - newConditions[idx] = { - question: condition.question, - operator: val, - answer: "", - }; - break; - } - - updateField("enable_when", newConditions); - }} - > - - - - - Equals - - Not Equals - - - Greater Than - - Less Than - - Greater Than or Equal - - - Less Than or Equal - - Exists - - - - - - Answer - {condition.operator === "exists" ? ( - { - const newConditions = [ - ...(question.enable_when || []), - ]; - newConditions[idx] = { - question: condition.question, - operator: "exists" as const, - answer: val === "true", - }; - updateField("enable_when", newConditions); - }} - > - - - - - True - False - - - ) : ( - { - const newConditions = [ - ...(question.enable_when || []), - ]; - const value = e.target.value; - let newCondition; - - if ( - [ - "greater", - "less", - "greater_or_equals", - "less_or_equals", - ].includes(condition.operator) - ) { - newCondition = { - question: condition.question, - operator: condition.operator as - | "greater" - | "less" - | "greater_or_equals" - | "less_or_equals", - answer: Number(value), - }; - } else { - newCondition = { - question: condition.question, - operator: condition.operator as - | "equals" - | "not_equals", - answer: value, - }; - } - - newConditions[idx] = newCondition; - updateField("enable_when", newConditions); - }} - placeholder="Answer value" - /> - )} - - { - const newConditions = question.enable_when?.filter( - (_, i) => i !== idx, - ); - updateField("enable_when", newConditions); - }} - > - - - - - ))} - { - const newCondition: EnableWhen = { - question: "", - operator: "equals", - answer: "", - }; - updateField("enable_when", [ - ...(question.enable_when || []), - newCondition, - ]); - }} - > - - Add Condition - - - + + + + + {valuesets.map((valueset) => ( + + {valueset.name} + + ))} + + + + )} + )} @@ -1391,6 +1255,234 @@ function QuestionEditor({ )} + + + Enable When Conditions + + {(question.enable_when || []).length > 0 && ( + + Enable Behavior + + updateField("enable_behavior", val) + } + > + + + + + + All conditions must be met + + + Any condition must be met + + + + + )} + {(question.enable_when || []).map((condition, idx) => ( + + + Question + { + const newConditions = [...(question.enable_when || [])]; + newConditions[idx] = { + ...condition, + question: e.target.value, + }; + updateField("enable_when", newConditions); + }} + placeholder="Question Link ID" + /> + + + Operator + { + const newConditions = [...(question.enable_when || [])]; + + switch (val) { + case "greater": + case "less": + case "greater_or_equals": + case "less_or_equals": + newConditions[idx] = { + question: condition.question, + operator: val, + answer: 0, + }; + break; + case "exists": + newConditions[idx] = { + question: condition.question, + operator: val, + answer: true, + }; + break; + case "equals": + case "not_equals": + newConditions[idx] = { + question: condition.question, + operator: val, + answer: "", + }; + break; + } + + updateField("enable_when", newConditions); + }} + > + + + + + Equals + Not Equals + Greater Than + Less Than + + Greater Than or Equal + + + Less Than or Equal + + Exists + + + + + + Answer + {condition.operator === "exists" ? ( + { + const newConditions = [ + ...(question.enable_when || []), + ]; + newConditions[idx] = { + question: condition.question, + operator: "exists" as const, + answer: val === "true", + }; + updateField("enable_when", newConditions); + }} + > + + + + + True + False + + + ) : ( + { + const newConditions = [ + ...(question.enable_when || []), + ]; + const value = e.target.value; + let newCondition; + + if ( + [ + "greater", + "less", + "greater_or_equals", + "less_or_equals", + ].includes(condition.operator) + ) { + newCondition = { + question: condition.question, + operator: condition.operator as + | "greater" + | "less" + | "greater_or_equals" + | "less_or_equals", + answer: Number(value), + }; + } else { + newCondition = { + question: condition.question, + operator: condition.operator as + | "equals" + | "not_equals", + answer: value, + }; + } + + newConditions[idx] = newCondition; + updateField("enable_when", newConditions); + }} + placeholder="Answer value" + /> + )} + + { + const newConditions = question.enable_when?.filter( + (_, i) => i !== idx, + ); + updateField("enable_when", newConditions); + }} + > + + + + + ))} + { + const newCondition: EnableWhen = { + question: "", + operator: "equals", + answer: "", + }; + updateField("enable_when", [ + ...(question.enable_when || []), + newCondition, + ]); + }} + > + + Add Condition + + + diff --git a/src/components/Questionnaire/QuestionnaireForm.tsx b/src/components/Questionnaire/QuestionnaireForm.tsx index 7357dd5298d..1b1de03ebc9 100644 --- a/src/components/Questionnaire/QuestionnaireForm.tsx +++ b/src/components/Questionnaire/QuestionnaireForm.tsx @@ -11,6 +11,7 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; +import { DebugPreview } from "@/components/Common/DebugPreview"; import Loading from "@/components/Common/Loading"; import { PLUGIN_Component } from "@/PluginEngine"; @@ -525,15 +526,11 @@ export function QuestionnaireForm({ setFormState={setQuestionnaireForms} /> - {/* Add a Preview of the QuestionnaireForm */} - {import.meta.env.DEV && ( - - QuestionnaireForm - - {JSON.stringify(questionnaireForms, null, 2)} - - - )} + ); diff --git a/src/components/Questionnaire/index.tsx b/src/components/Questionnaire/QuestionnaireList.tsx similarity index 76% rename from src/components/Questionnaire/index.tsx rename to src/components/Questionnaire/QuestionnaireList.tsx index 54353743df9..cb6c29040eb 100644 --- a/src/components/Questionnaire/index.tsx +++ b/src/components/Questionnaire/QuestionnaireList.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { useNavigate } from "raviger"; import { Badge } from "@/components/ui/badge"; @@ -6,15 +7,25 @@ import { Button } from "@/components/ui/button"; import Loading from "@/components/Common/Loading"; +import useFilters from "@/hooks/useFilters"; + import query from "@/Utils/request/query"; import { QuestionnaireDetail } from "@/types/questionnaire/questionnaire"; import questionnaireApi from "@/types/questionnaire/questionnaireApi"; export function QuestionnaireList() { + const { qParams, Pagination, resultsPerPage } = useFilters({ + limit: 15, + }); const navigate = useNavigate(); const { data: response, isLoading } = useQuery({ - queryKey: ["questionnaires"], - queryFn: query(questionnaireApi.list), + queryKey: ["questionnaires", qParams], + queryFn: query(questionnaireApi.list, { + queryParams: { + limit: resultsPerPage, + offset: ((qParams.page ?? 1) - 1) * resultsPerPage, + }, + }), }); if (isLoading) { @@ -27,11 +38,11 @@ export function QuestionnaireList() { - Questionnaires - Manage and view questionnaires + {t("questionnaires")} + {t("manage_and_view_questionnaires")} - navigate("/questionnaire/create")}> - Create New + navigate("/admin/questionnaire/create")}> + {t("create_new")} @@ -40,16 +51,16 @@ export function QuestionnaireList() { - Title + {t("title")} - Description + {t("description")} - Status + {t("status")} - Slug + {t("slug")} @@ -57,7 +68,9 @@ export function QuestionnaireList() { {questionnaireList.map((questionnaire: QuestionnaireDetail) => ( navigate(`/questionnaire/${questionnaire.slug}`)} + onClick={() => + navigate(`/admin/questionnaire/${questionnaire.slug}`) + } className="cursor-pointer hover:bg-gray-50" > @@ -89,6 +102,7 @@ export function QuestionnaireList() { + ); } diff --git a/src/components/Questionnaire/show.tsx b/src/components/Questionnaire/show.tsx index 4534ce5734b..f18ca66f1e0 100644 --- a/src/components/Questionnaire/show.tsx +++ b/src/components/Questionnaire/show.tsx @@ -2,6 +2,8 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { useNavigate } from "raviger"; import { useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -16,7 +18,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DropdownMenu, @@ -103,7 +105,7 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) { pathParams: { id }, }), onSuccess: () => { - navigate("/questionnaire"); + navigate("/admin/questionnaire"); }, }); @@ -147,11 +149,14 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) { {questionnaire.description} - navigate("/questionnaire")}> + navigate("/admin/questionnaire")} + > Back to List - navigate(`/questionnaire/${id}/edit`)}> + navigate(`/admin/questionnaire/${id}/edit`)}> Edit @@ -218,7 +223,7 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) { Cancel {isPending ? "Deleting..." : "Delete"} diff --git a/src/components/Resource/PrintResourceLetter.tsx b/src/components/Resource/PrintResourceLetter.tsx index bc1a80f72a8..5fd6f82e209 100644 --- a/src/components/Resource/PrintResourceLetter.tsx +++ b/src/components/Resource/PrintResourceLetter.tsx @@ -45,7 +45,7 @@ export default function PrintResourceLetter({ id }: { id: string }) { {/* From Address */} - {t("From")}: + {t("from")}: {data.origin_facility.name} diff --git a/src/components/Resource/ResourceCommentSection.tsx b/src/components/Resource/ResourceCommentSection.tsx index 62f94d5d6fa..ddda59be9f1 100644 --- a/src/components/Resource/ResourceCommentSection.tsx +++ b/src/components/Resource/ResourceCommentSection.tsx @@ -7,7 +7,9 @@ import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; +import { Markdown } from "@/components/ui/markdown"; import { Textarea } from "@/components/ui/textarea"; +import { TooltipComponent } from "@/components/ui/tooltip"; import { Avatar } from "@/components/Common/Avatar"; import PaginationComponent from "@/components/Common/Pagination"; @@ -18,7 +20,7 @@ import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants"; import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; -import { formatName, relativeTime } from "@/Utils/utils"; +import { formatDateTime, formatName, relativeTime } from "@/Utils/utils"; import { CommentModel } from "@/types/resourceRequest/resourceRequest"; const CommentSection = (props: { id: string }) => { @@ -91,11 +93,13 @@ const CommentSection = (props: { id: string }) => { ) : ( - {resourceComments?.results?.map((comment) => ( - - - - ))} + {resourceComments?.results + ? [...resourceComments.results].reverse().map((comment) => ( + + + + )) + : null} ( - - - - {comment.replace(/\n+/g, "\n")} - - - - - - - {formatName(created_by)} - + + + + + + + + + + + {formatName(created_by)} + + + {relativeTime(created_date)} + + + + + - {relativeTime(created_date)} ); diff --git a/src/components/Resource/ResourceCreate.tsx b/src/components/Resource/ResourceCreate.tsx deleted file mode 100644 index b21a74d0c96..00000000000 --- a/src/components/Resource/ResourceCreate.tsx +++ /dev/null @@ -1,420 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { navigate, useQueryParams } from "raviger"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; -import * as z from "zod"; - -import Card from "@/CAREUI/display/Card"; -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { PhoneInput } from "@/components/ui/phone-input"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Separator } from "@/components/ui/separator"; -import { Textarea } from "@/components/ui/textarea"; - -import { FacilitySelect } from "@/components/Common/FacilitySelect"; -import Loading from "@/components/Common/Loading"; -import Page from "@/components/Common/Page"; - -import useAppHistory from "@/hooks/useAppHistory"; -import useAuthUser from "@/hooks/useAuthUser"; - -import { RESOURCE_CATEGORY_CHOICES } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import mutate from "@/Utils/request/mutate"; -import query from "@/Utils/request/query"; -import validators from "@/Utils/validators"; -import { ResourceRequest } from "@/types/resourceRequest/resourceRequest"; - -interface ResourceProps { - facilityId: number; -} - -export default function ResourceCreate(props: ResourceProps) { - const { goBack } = useAppHistory(); - const { facilityId } = props; - const { t } = useTranslation(); - const [{ related_patient }] = useQueryParams(); - const authUser = useAuthUser(); - - const resourceFormSchema = z.object({ - category: z.string().min(1, { message: t("field_required") }), - assigned_facility: z - .object({ - id: z.string(), - name: z.string(), - }) - .nullable(), - emergency: z.enum(["true", "false"]), - title: z.string().min(1, { message: t("field_required") }), - reason: z.string().min(1, { message: t("field_required") }), - referring_facility_contact_name: z - .string() - .min(1, { message: t("field_required") }), - referring_facility_contact_number: validators.phoneNumber.required, - priority: z.number().default(1), - }); - - type ResourceFormValues = z.infer; - - const { data: facilityData } = useQuery({ - queryKey: ["facility", facilityId], - queryFn: () => - query(routes.getAnyFacility, { - pathParams: { id: String(facilityId) }, - }), - enabled: !!facilityId, - }); - - const form = useForm({ - resolver: zodResolver(resourceFormSchema), - defaultValues: { - category: "", - assigned_facility: null, - emergency: "false" as const, - title: "", - reason: "", - referring_facility_contact_name: "", - referring_facility_contact_number: "", - priority: 1, - }, - }); - - const { mutate: createResource, isPending } = useMutation({ - mutationFn: mutate(routes.createResource), - onSuccess: (data: ResourceRequest) => { - toast.success(t("resource_created_successfully")); - navigate(`/facility/${facilityId}/resource/${data.id}`); - }, - }); - - const onSubmit = (data: ResourceFormValues) => { - createResource({ - status: "PENDING", - category: data.category, - origin_facility: String(props.facilityId), - assigned_facility: data.assigned_facility?.id || null, - approving_facility: null, - emergency: data.emergency === "true", - title: data.title, - reason: data.reason, - referring_facility_contact_name: data.referring_facility_contact_name, - referring_facility_contact_number: data.referring_facility_contact_number, - related_patient: related_patient, - priority: data.priority, - }); - }; - - const fillMyDetails = () => { - form.setValue( - "referring_facility_contact_name", - `${authUser.first_name} ${authUser.last_name}`.trim(), - ); - if (authUser.phone_number) { - form.setValue("referring_facility_contact_number", authUser.phone_number); - } - }; - - if (isPending) { - return ; - } - - return ( - - - - - - {related_patient && ( - - - - - {t("linked_patient")}:{" "} - {related_patient} - - - - )} - - - - - {t("basic_information")} - - - {t("resource_request_basic_info_description")} - - - - - ( - - - {t("facility_for_care_support")} - - - - - - {t("select_facility_description")} - - - - )} - /> - - ( - - {t("is_this_an_emergency")} - - - - - - - - {t("yes")} - - - - - - - - {t("no")} - - - - - - {t("emergency_description")} - - - - )} - /> - - - ( - - {t("category")} - - - - - - - - {RESOURCE_CATEGORY_CHOICES.map((category) => ( - - {category.text} - - ))} - - - - {t("category_description")} - - - - )} - /> - - - - - - - - {t("request_details")} - - - {t("resource_request_details_description")} - - - - ( - - {t("request_title")} - - field.onChange(value)} - /> - - - {t("request_title_description")} - - - - )} - /> - - ( - - {t("request_reason")} - - field.onChange(value)} - /> - - - {t("request_reason_description")} - - - - )} - /> - - - - - - - - - {t("contact_information")} - - - {t("contact_information_description")} - - - - - {t("fill_my_details")} - - - - - ( - - {t("contact_person")} - - field.onChange(value)} - /> - - - {t("contact_person_description")} - - - - )} - /> - - ( - - {t("contact_phone")} - - field.onChange(value)} - /> - - - {t("contact_phone_description")} - - - - )} - /> - - - - - goBack()} - > - {t("cancel")} - - - {isPending && ( - - )} - {isPending ? t("submitting") : t("submit")} - - - - - - - - ); -} diff --git a/src/components/Resource/ResourceDetails.tsx b/src/components/Resource/ResourceDetails.tsx index f684596c13a..182cd9ab514 100644 --- a/src/components/Resource/ResourceDetails.tsx +++ b/src/components/Resource/ResourceDetails.tsx @@ -123,11 +123,7 @@ export default function ResourceDetails({ } return ( - + {/* Action Buttons */} diff --git a/src/components/Resource/ResourceDetailsUpdate.tsx b/src/components/Resource/ResourceDetailsUpdate.tsx deleted file mode 100644 index f3e48df7af8..00000000000 --- a/src/components/Resource/ResourceDetailsUpdate.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useMutation } from "@tanstack/react-query"; -import { t } from "i18next"; -import { navigate, useQueryParams } from "raviger"; -import { useEffect, useReducer, useState } from "react"; -import { toast } from "sonner"; - -import Card from "@/CAREUI/display/Card"; - -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; - -import CircularProgress from "@/components/Common/CircularProgress"; -import { FacilitySelect } from "@/components/Common/FacilitySelect"; -import Loading from "@/components/Common/Loading"; -import Page from "@/components/Common/Page"; -import UserAutocomplete from "@/components/Common/UserAutocompleteFormField"; -import { FieldLabel } from "@/components/Form/FormFields/FormField"; -import RadioFormField from "@/components/Form/FormFields/RadioFormField"; -import { SelectFormField } from "@/components/Form/FormFields/SelectFormField"; -import TextFormField from "@/components/Form/FormFields/TextFormField"; -import { FieldChangeEvent } from "@/components/Form/FormFields/Utils"; -import { UserModel } from "@/components/Users/models"; - -import useAppHistory from "@/hooks/useAppHistory"; - -import { RESOURCE_STATUS_CHOICES } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import mutate from "@/Utils/request/mutate"; -import query from "@/Utils/request/query"; -import { UpdateResourceRequest } from "@/types/resourceRequest/resourceRequest"; - -interface resourceProps { - id: string; - facilityId: string; -} - -const initForm: Partial = { - assigned_facility: null, - emergency: false, - title: "", - reason: "", - assigned_to: null, -}; - -const requiredFields: any = { - assigned_facility_type: { - errorText: "Please Select Facility Type", - }, -}; - -const initError = Object.assign( - {}, - ...Object.keys(initForm).map((k) => ({ [k]: "" })), -); - -const initialState = { - form: { ...initForm }, - errors: { ...initError }, -}; - -export const ResourceDetailsUpdate = (props: resourceProps) => { - const { goBack } = useAppHistory(); - const [qParams, _] = useQueryParams(); - const [assignedUser, SetAssignedUser] = useState(); - const resourceFormReducer = (state = initialState, action: any) => { - switch (action.type) { - case "set_form": { - return { - ...state, - form: action.form, - }; - } - case "set_error": { - return { - ...state, - errors: action.errors, - }; - } - default: - return state; - } - }; - - const [state, dispatch] = useReducer(resourceFormReducer, initialState); - const { data, isLoading: assignedUserLoading } = useQuery({ - queryKey: ["user", props.facilityId], - queryFn: query(routes.userList), - }); - - useEffect(() => { - if (data) { - SetAssignedUser(data.results[0]); - } - }, [data]); - - const validateForm = () => { - const errors = { ...initError }; - let isInvalidForm = false; - Object.keys(requiredFields).forEach((field) => { - if (!state.form[field] || !state.form[field].length) { - errors[field] = requiredFields[field].errorText; - isInvalidForm = true; - } - }); - - dispatch({ type: "set_error", errors }); - return isInvalidForm; - }; - - const handleChange = (e: FieldChangeEvent) => { - dispatch({ - type: "set_form", - form: { ...state.form, [e.name]: e.value }, - }); - }; - - const handleOnSelect = (user: any) => { - const form = { ...state.form }; - form["assigned_to"] = user?.value?.id; - SetAssignedUser(user.value); - dispatch({ type: "set_form", form }); - }; - - const setFacility = (selected: any, name: string) => { - const form = { ...state.form }; - form[name] = selected; - dispatch({ type: "set_form", form }); - }; - const { data: resourceDetails } = useQuery({ - queryKey: ["resource", props.facilityId, props.id], - queryFn: query(routes.getResourceDetails, { - pathParams: { id: props.id }, - }), - }); - useEffect(() => { - if (resourceDetails) { - dispatch({ - type: "set_form", - form: { - ...resourceDetails, - status: qParams.status || resourceDetails.status.toLowerCase(), - }, - }); - } - }, [resourceDetails]); - - const { mutate: updateResource, isPending: updateResourceLoading } = - useMutation({ - mutationFn: mutate(routes.updateResource, { - pathParams: { id: props.id }, - }), - onSuccess: (data) => { - dispatch({ type: "set_form", form: data }); - toast.success(t("request_updated_successfully")); - navigate(`/facility/${props.facilityId}/resource/${props.id}`); - }, - }); - - const handleSubmit = async () => { - const validForm = validateForm(); - - if (validForm) { - const resourceData: UpdateResourceRequest = { - id: props.id, - status: state.form.status, - origin_facility: state.form.origin_facility?.id, - assigned_facility: state.form?.assigned_facility?.id, - emergency: [true, "true"].includes(state.form.emergency), - title: state.form.title, - reason: state.form.reason, - assigned_to: state.form.assigned_to, - category: state.form.category, - priority: state.form.priority, - referring_facility_contact_number: - state.form.referring_facility_contact_number, - referring_facility_contact_name: - state.form.referring_facility_contact_name, - approving_facility: state.form.approving_facility?.id, - related_patient: state.form.related_patient?.id, - }; - updateResource(resourceData); - } - }; - - if (updateResourceLoading || !resourceDetails) { - return ; - } - - return ( - - - - - - option.text} - onChange={handleChange} - optionLabel={(option) => t(`resource_status__${option.text}`)} - /> - - - - {assignedUserLoading ? ( - - ) : ( - - )} - - - - - - What facility would you like to assign the request to - - setFacility(obj, "assigned_facility")} - errors={state.errors.assigned_facility} - /> - - - - - - - - - {t("request_reason")} - - - handleChange({ name: e.target.name, value: e.target.value }) - } - /> - {state.errors.reason && ( - - {state.errors.emergency} - - )} - - - - (o ? "Yes" : "No")} - optionValue={(o) => String(o)} - value={String(state.form.emergency)} - error={state.errors.emergency} - /> - - - - goBack()}> - {t("cancel")} - - - {t("submit")} - - - - - - - ); -}; diff --git a/src/components/Resource/ResourceForm.tsx b/src/components/Resource/ResourceForm.tsx new file mode 100644 index 00000000000..fb93e88e316 --- /dev/null +++ b/src/components/Resource/ResourceForm.tsx @@ -0,0 +1,571 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Link, navigate, useQueryParams } from "raviger"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import * as z from "zod"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Alert, AlertDescription } from "@/components/ui/alert"; +import Autocomplete from "@/components/ui/autocomplete"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { PhoneInput } from "@/components/ui/phone-input"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; + +import Loading from "@/components/Common/Loading"; +import Page from "@/components/Common/Page"; +import UserSelector from "@/components/Common/UserSelector"; + +import useAppHistory from "@/hooks/useAppHistory"; +import useAuthUser from "@/hooks/useAuthUser"; + +import { RESOURCE_STATUS_CHOICES } from "@/common/constants"; +import { RESOURCE_CATEGORY_CHOICES } from "@/common/constants"; + +import routes from "@/Utils/request/api"; +import mutate from "@/Utils/request/mutate"; +import query from "@/Utils/request/query"; +import { mergeAutocompleteOptions } from "@/Utils/utils"; +import validators from "@/Utils/validators"; +import facilityApi from "@/types/facility/facilityApi"; +import { ResourceRequest } from "@/types/resourceRequest/resourceRequest"; +import { UserBase } from "@/types/user/user"; + +interface ResourceProps { + facilityId: number; + id?: string; +} + +export default function ResourceForm({ facilityId, id }: ResourceProps) { + const [facilitySearch, setFacilitySearch] = useState(""); + const { goBack } = useAppHistory(); + const { t } = useTranslation(); + const [{ related_patient }] = useQueryParams(); + const [assignedToUser, setAssignedToUser] = useState(); + const authUser = useAuthUser(); + + const resourceFormSchema = z.object({ + status: z.string().min(1, { message: t("field_required") }), + category: z.string().min(1, { message: t("field_required") }), + assigned_facility: z + .object({ + id: z.string(), + name: z.string(), + }) + .nullable(), + assigned_to: z.string().min(1, { message: t("field_required") }), + emergency: z.enum(["true", "false"]), + title: z.string().min(1, { message: t("field_required") }), + reason: z.string().min(1, { message: t("field_required") }), + referring_facility_contact_name: z + .string() + .min(1, { message: t("field_required") }), + referring_facility_contact_number: validators().phoneNumber.required, + priority: z.number().default(1), + }); + + type ResourceFormValues = z.infer; + + const { data: patientData } = useQuery({ + queryKey: ["patient", related_patient], + queryFn: query(routes.patient.getPatient, { + pathParams: { id: String(related_patient) }, + }), + enabled: !!related_patient, + }); + + const { data: resourceData } = useQuery({ + queryKey: ["resource_request", id], + queryFn: query(routes.getResourceDetails, { + pathParams: { id: String(id) }, + }), + enabled: !!id, + }); + + const form = useForm({ + resolver: zodResolver(resourceFormSchema), + defaultValues: { + status: "pending", + category: "", + assigned_facility: null, + assigned_to: "", + emergency: "false" as const, + title: "", + reason: "", + referring_facility_contact_name: "", + referring_facility_contact_number: "", + priority: 1, + }, + }); + + useEffect(() => { + if (resourceData) { + form.reset({ + status: resourceData.status, + category: resourceData.category, + assigned_facility: resourceData.assigned_facility, + assigned_to: resourceData.assigned_to?.id, + emergency: resourceData.emergency ? "true" : "false", + title: resourceData.title, + reason: resourceData.reason, + referring_facility_contact_name: + resourceData.referring_facility_contact_name, + referring_facility_contact_number: + resourceData.referring_facility_contact_number, + priority: resourceData.priority, + }); + if (resourceData.assigned_to) { + setAssignedToUser(resourceData.assigned_to); + } else { + setAssignedToUser(undefined); + } + } + }, [resourceData, form]); + + const { mutate: createResource, isPending } = useMutation({ + mutationFn: mutate(routes.createResource), + onSuccess: (data: ResourceRequest) => { + toast.success(t("resource_created_successfully")); + navigate(`/facility/${facilityId}/resource/${data.id}`); + }, + }); + + const { mutate: updateResource, isPending: isUpdatePending } = useMutation({ + mutationFn: mutate(routes.updateResource, { + pathParams: { id: String(id) }, + }), + onSuccess: (data: ResourceRequest) => { + toast.success(t("resource_updated_successfully")); + navigate(`/facility/${facilityId}/resource/${data.id}`); + }, + }); + + const onSubmit = (data: ResourceFormValues) => { + const resourcePayload = { + status: data.status, + category: data.category, + origin_facility: String(facilityId), + assigned_facility: data.assigned_facility?.id || null, + assigned_to: assignedToUser?.id || null, + approving_facility: null, + emergency: data.emergency === "true", + title: data.title, + reason: data.reason, + referring_facility_contact_name: data.referring_facility_contact_name, + referring_facility_contact_number: data.referring_facility_contact_number, + related_patient: related_patient, + priority: data.priority, + }; + + if (id) { + updateResource({ ...resourcePayload, id }); + } else { + createResource(resourcePayload); + } + }; + const { data: facilities } = useQuery({ + queryKey: ["facilities", facilitySearch], + queryFn: query.debounced(facilityApi.getAllFacilities, { + queryParams: { + search_text: facilitySearch, + limit: 50, + }, + }), + }); + + const facilityOptions = facilities?.results.map((facility) => ({ + label: facility.name, + value: facility.id, + })); + + const handleUserChange = (user: UserBase) => { + form.setValue("assigned_to", user.id); + setAssignedToUser(user); + }; + + const fillMyDetails = () => { + form.setValue( + "referring_facility_contact_name", + `${authUser.first_name} ${authUser.last_name}`.trim(), + ); + if (authUser.phone_number) { + form.setValue("referring_facility_contact_number", authUser.phone_number); + } + }; + + if (isPending || isUpdatePending) { + return ; + } + + return ( + + + + + + + {patientData && ( + + + + + + {t("linked_patient")}:{" "} + + {patientData.name} + + + + + + )} + + + + + {t("basic_information")} + + + {t("resource_request_basic_info_description")} + + + + + ( + + + {t("facility_for_care_support")} + + + { + const facility = + facilities?.results.find( + (f) => f.id === value, + ) ?? null; + form.setValue("assigned_facility", facility); + }} + /> + + + {t("select_facility_description")} + + + + )} + /> + + ( + + {t("is_this_an_emergency")} + + + + + + + + {t("yes")} + + + + + + + + {t("no")} + + + + + + {t("emergency_description")} + + + + )} + /> + + ( + + {t("status")} + + + + + + + + {RESOURCE_STATUS_CHOICES.map((option, index) => ( + + {t(`resource_status__${option.text}`)} + + ))} + + + + + + )} + /> + ( + + {t("category")} + + + + + + + + {RESOURCE_CATEGORY_CHOICES.map((category) => ( + + {category.text} + + ))} + + + + + )} + /> + ( + + {t("assigned_to")} + + + + + + )} + /> + + + + + + + + {t("request_details")} + + + {t("resource_request_details_description")} + + + + ( + + {t("request_title")} + + field.onChange(value)} + /> + + + {t("request_title_description")} + + + + )} + /> + + ( + + {t("request_reason")} + + field.onChange(value)} + /> + + + {t("request_reason_description")} + + + + )} + /> + + + + + + + + + {t("contact_information")} + + + {t("contact_information_description")} + + + + + {t("fill_my_details")} + + + + + ( + + {t("contact_person")} + + field.onChange(value)} + /> + + + {t("contact_person_description")} + + + + )} + /> + + ( + + {t("contact_phone")} + + field.onChange(value)} + /> + + + {t("contact_phone_description")} + + + + )} + /> + + + + + goBack()} + > + {t("cancel")} + + + {isPending && ( + + )} + {isPending ? t("submitting") : t("submit")} + + + + + + + + + ); +} diff --git a/src/components/Resource/ResourceList.tsx b/src/components/Resource/ResourceList.tsx index 1ba35b0effe..566a19dedde 100644 --- a/src/components/Resource/ResourceList.tsx +++ b/src/components/Resource/ResourceList.tsx @@ -103,7 +103,7 @@ export default function ResourceList({ facilityId }: { facilityId: string }) { const resources = queryResources?.results || []; return ( - + @@ -154,11 +154,12 @@ export default function ResourceList({ facilityId }: { facilityId: string }) { }) } className="w-full border-none shadow-none" + autoFocus /> - + - + - + - + - + {currentStatuses.map((statusOption) => ( setEditAvatar(false)} + onOpenChange={(open) => setEditAvatar(open)} /> diff --git a/src/components/Users/UserDeleteDialog.tsx b/src/components/Users/UserDeleteDialog.tsx index 5a2becefe3d..a088c9f2512 100644 --- a/src/components/Users/UserDeleteDialog.tsx +++ b/src/components/Users/UserDeleteDialog.tsx @@ -1,26 +1,51 @@ -import ConfirmDialog from "@/components/Common/ConfirmDialog"; +import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { buttonVariants } from "@/components/ui/button"; interface ConfirmDialogProps { name: string; handleCancel: () => void; handleOk: () => void; + show: boolean; } const UserDeleteDialog = (props: ConfirmDialogProps) => { + const { t } = useTranslation(); return ( - - Are you sure you want to delete user {props.name} ? - - } - action="Delete" - variant="destructive" - show - onConfirm={props.handleOk} - onClose={props.handleCancel} - /> + + + + {t("delete_user")} + + {t("are_you_sure_you_want_to_delete_user")} + {props.name}? + + + + + {t("cancel")} + + + {t("delete")} + + + + ); }; diff --git a/src/components/Users/UserForm.tsx b/src/components/Users/UserForm.tsx index ce92df7533b..ee78d887d9f 100644 --- a/src/components/Users/UserForm.tsx +++ b/src/components/Users/UserForm.tsx @@ -90,8 +90,8 @@ export default function UserForm({ first_name: z.string().min(1, t("field_required")), last_name: z.string().min(1, t("field_required")), email: z.string().email(t("invalid_email_address")), - phone_number: validators.phoneNumber.required, - gender: z.enum(GENDERS), + phone_number: validators().phoneNumber.required, + gender: z.enum(GENDERS, { required_error: t("gender_is_required") }), /* TODO: Userbase doesn't currently support these, neither does BE but we will probably need these */ /* qualification: z.string().optional(), @@ -145,7 +145,7 @@ export default function UserForm({ last_name: userData.last_name, email: userData.email, phone_number: userData.phone_number || "", - gender: userData.gender, + gender: userData.gender || undefined, }; form.reset(formData); } @@ -155,7 +155,7 @@ export default function UserForm({ const [isUsernameFieldFocused, setIsUsernameFieldFocused] = useState(false); //const userType = form.watch("user_type"); - const usernameInput = form.watch("username"); + const usernameInput = form.watch("username") || ""; const phoneNumber = form.watch("phone_number"); useEffect(() => { @@ -271,7 +271,7 @@ export default function UserForm({ useEffect(() => { const levels: Organization[] = []; - if (org) levels.push(org); + if (org && org.org_type === "govt") levels.push(org); setSelectedLevels(levels); }, [org, organizationId]); diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx index f07e08a9718..f87017fa479 100644 --- a/src/components/Users/UserFormValidations.tsx +++ b/src/components/Users/UserFormValidations.tsx @@ -1,8 +1,8 @@ import { Trans } from "react-i18next"; -import CareIcon from "@/CAREUI/icons/CareIcon"; +import { cn } from "@/lib/utils"; -import { classNames } from "@/Utils/utils"; +import CareIcon from "@/CAREUI/icons/CareIcon"; export type UserType = "doctor" | "nurse" | "staff" | "volunteer"; @@ -72,7 +72,7 @@ export const validateRule = ( )}{" "} { diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index ee6753c1472..bf8b2b3e96d 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -133,9 +133,7 @@ const UserListHeader = () => { return ( - - {t("name")} - + {t("name")} {t("status")} {t("role")} {t("contact_number")} @@ -151,7 +149,7 @@ const UserListRow = ({ user }: { user: UserBase }) => { id={`usr_${user.id}`} className="hover:bg-gray-50" > - + {showDeleteDialog && ( { @@ -96,6 +97,7 @@ export default function UserSummaryTab({ userData }: { userData?: UserBase }) { }} /> )} + { + toast.success("ValueSet created successfully"); + navigate(`/valuesets/${data.slug}`); + }, + }); + + // Update mutation + const updateMutation = useMutation({ + mutationFn: mutate(valuesetApi.update, { + pathParams: { slug: slug! }, + }), + onSuccess: () => { + toast.success("ValueSet updated successfully"); + navigate(`/admin/valuesets`); + }, + }); + + const handleSubmit = (data: ValuesetFormType) => { + if (slug && existingValueset) { + const updateData: UpdateValuesetModel = { + ...data, + id: existingValueset.id, + }; + updateMutation.mutate(updateData); + } else { + const createData: CreateValuesetModel = data; + createMutation.mutate(createData); + } + }; + + return ( + + + {slug ? "Edit ValueSet" : "Create New ValueSet"} + + + {slug && isLoading ? ( + + ) : ( + + )} + + ); +} diff --git a/src/components/ValueSet/ValueSetForm.tsx b/src/components/ValueSet/ValueSetForm.tsx new file mode 100644 index 00000000000..b3e76eaaa23 --- /dev/null +++ b/src/components/ValueSet/ValueSetForm.tsx @@ -0,0 +1,492 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon, TrashIcon, UpdateIcon } from "@radix-ui/react-icons"; +import { useMutation } from "@tanstack/react-query"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import * as z from "zod"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; + +import mutate from "@/Utils/request/mutate"; +import { + TERMINOLOGY_SYSTEMS, + ValuesetFormType, + ValuesetLookupResponse, +} from "@/types/valueset/valueset"; +import valuesetApi from "@/types/valueset/valuesetApi"; + +// Create a schema for form validation +const valuesetFormSchema = z.object({ + name: z.string().min(1, "Name is required"), + slug: z.string().min(1, "Slug is required"), + description: z.string(), + status: z.enum(["active", "inactive"]), + is_system_defined: z.boolean(), + compose: z.object({ + include: z.array( + z.object({ + system: z.string(), + concept: z + .array( + z.object({ + code: z.string(), + display: z.string(), + }), + ) + .optional(), + filter: z + .array( + z.object({ + property: z.string(), + op: z.string(), + value: z.string(), + }), + ) + .optional(), + }), + ), + exclude: z.array( + z.object({ + system: z.string(), + concept: z + .array( + z.object({ + code: z.string(), + display: z.string(), + }), + ) + .optional(), + filter: z + .array( + z.object({ + property: z.string(), + op: z.string(), + value: z.string(), + }), + ) + .optional(), + }), + ), + }), +}); + +interface ValueSetFormProps { + initialData?: ValuesetFormType; + onSubmit: (data: ValuesetFormType) => void; + isSubmitting?: boolean; +} + +function ConceptFields({ + nestIndex, + type, + parentForm, +}: { + nestIndex: number; + type: "include" | "exclude"; + parentForm: ReturnType>; +}) { + const { fields, append, remove } = useFieldArray({ + control: parentForm.control, + name: `compose.${type}.${nestIndex}.concept`, + }); + + const lookupMutation = useMutation({ + mutationFn: mutate(valuesetApi.lookup, { + silent: true, // Suppress default error handling since we have custom handling + }), + onSuccess: (response: ValuesetLookupResponse) => { + if (response.metadata) { + const concepts = parentForm.getValues( + `compose.${type}.${nestIndex}.concept`, + ); + + const conceptIndex = concepts?.findIndex( + (concept) => concept.code === response.metadata.code, + ); + + if (conceptIndex && conceptIndex !== -1) { + parentForm.setValue( + `compose.${type}.${nestIndex}.concept.${conceptIndex}.display`, + response.metadata.display, + { shouldValidate: true }, + ); + } + toast.success("Code verified successfully"); + } + }, + onError: () => { + toast.error("Failed to verify code"); + }, + }); + + const handleVerify = async (index: number) => { + const system = parentForm.getValues(`compose.${type}.${nestIndex}.system`); + const code = parentForm.getValues( + `compose.${type}.${nestIndex}.concept.${index}.code`, + ); + + if (!system || !code) { + toast.error("Please select a system and enter a code first"); + return; + } + + lookupMutation.mutate({ system, code }); + }; + + return ( + + + Concepts + append({ code: "", display: "" })} + > + + Add Concept + + + {fields.map((field, index) => ( + + ( + + + { + field.onChange(e); + // Clear display and set isVerified to false when code changes + parentForm.setValue( + `compose.${type}.${nestIndex}.concept.${index}.display`, + "", + { shouldValidate: true }, + ); + }} + /> + + + )} + /> + ( + + + + + + )} + /> + handleVerify(index)} + disabled={lookupMutation.isPending} + > + + + remove(index)} + > + + + + ))} + + ); +} + +function FilterFields({ + nestIndex, + type, +}: { + nestIndex: number; + type: "include" | "exclude"; +}) { + const form = useForm(); + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: `compose.${type}.${nestIndex}.filter`, + }); + + return ( + + + Filters + append({ property: "", op: "", value: "" })} + > + + Add Filter + + + {fields.map((field, index) => ( + + ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + + + + + )} + /> + remove(index)} + > + + + + ))} + + ); +} + +function RuleFields({ + type, + form, +}: { + type: "include" | "exclude"; + form: ReturnType>; +}) { + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: `compose.${type}`, + }); + + return ( + + + + {type === "include" ? "Include Rules" : "Exclude Rules"} + + + append({ + system: Object.values(TERMINOLOGY_SYSTEMS)[0], + concept: [], + filter: [], + }) + } + > + + Add Rule + + + + {fields.map((field, index) => ( + + + ( + + System + + + + + + + + {Object.entries(TERMINOLOGY_SYSTEMS).map( + ([key, value]) => ( + + {key} + + ), + )} + + + + )} + /> + remove(index)} + > + + + + + + + ))} + + + ); +} + +export function ValueSetForm({ + initialData, + onSubmit, + isSubmitting, +}: ValueSetFormProps) { + const form = useForm({ + resolver: zodResolver(valuesetFormSchema), + defaultValues: { + name: initialData?.name || "", + slug: initialData?.slug || "", + description: initialData?.description || "", + status: initialData?.status || "active", + is_system_defined: initialData?.is_system_defined || false, + compose: { + include: initialData?.compose?.include || [], + exclude: initialData?.compose?.exclude || [], + }, + }, + }); + + return ( + + + ( + + Name + + + + + + )} + /> + + ( + + Slug + + + + + + )} + /> + + ( + + Description + + + + + + )} + /> + + ( + + Status + + + + + + + + Active + Inactive + + + + + )} + /> + + + + + + + + {isSubmitting ? "Saving..." : "Save ValueSet"} + + + + ); +} diff --git a/src/components/ValueSet/ValueSetList.tsx b/src/components/ValueSet/ValueSetList.tsx new file mode 100644 index 00000000000..f2168758e5a --- /dev/null +++ b/src/components/ValueSet/ValueSetList.tsx @@ -0,0 +1,118 @@ +import { useQuery } from "@tanstack/react-query"; +import { PlusIcon } from "lucide-react"; +import { Link, useNavigate } from "raviger"; +import { useTranslation } from "react-i18next"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; + +import Loading from "@/components/Common/Loading"; + +import query from "@/Utils/request/query"; +import valuesetApi from "@/types/valueset/valuesetApi"; + +export function ValueSetList() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { data: response, isLoading } = useQuery({ + queryKey: ["valuesets"], + queryFn: query(valuesetApi.list), + }); + + if (isLoading) { + return ; + } + + const valuesets = response?.results || []; + + return ( + + + + {t("valuesets")} + {t("manage_valuesets")} + + + + + {t("create_new")} + + + + + + + + + + {t("name")} + + + {t("slug")} + + + {t("status")} + + + {t("description")} + + + {t("system")} + + + {t("actions")} + + + + + {valuesets.map((valueset) => ( + + + + {valueset.name} + + + + {valueset.slug} + + + + {valueset.status} + + + + + {valueset.description} + + + + {valueset.is_system_defined ? t("yes") : t("no")} + + + {!valueset.is_system_defined && ( + + navigate(`/admin/valuesets/${valueset.slug}/edit`) + } + className="hover:bg-primary/5" + > + {t("edit")} + + )} + + + ))} + + + + + ); +} diff --git a/src/components/ui/autocomplete.tsx b/src/components/ui/autocomplete.tsx index 8e960e0db6c..e3ecf950e5a 100644 --- a/src/components/ui/autocomplete.tsx +++ b/src/components/ui/autocomplete.tsx @@ -69,13 +69,12 @@ export default function Autocomplete({ {options.map((option) => ( { const currentValue = - options.find( - (option) => option.label.toLowerCase() === v.toLowerCase(), - )?.value || ""; - onChange(currentValue === value ? "" : currentValue); + options.find((o) => `${o.label} - ${o.value}` === v)?.value || + ""; + onChange(currentValue); setOpen(false); }} > diff --git a/src/components/ui/date-field.tsx b/src/components/ui/date-field.tsx index daeff36be5f..b82ad00c9fc 100644 --- a/src/components/ui/date-field.tsx +++ b/src/components/ui/date-field.tsx @@ -45,20 +45,25 @@ export default function DateField({ const newDay = e.target.value; setDay(newDay); + // Check if change is from spinner (stepUp/stepDown) vs keyboard input + const isFromSpinner = + e.nativeEvent instanceof InputEvent && + (e.nativeEvent as InputEvent).inputType === "insertReplacementText"; + if ( - newDay.length === 2 && + (isFromSpinner || newDay.length === 2) && parseInt(newDay) >= 1 && parseInt(newDay) <= 31 ) { - if (isValidDate(year, month, newDay) && onChange) { + const modifiedDay = isFromSpinner ? newDay.padStart(2, "0") : newDay; + if (isValidDate(year, month, modifiedDay) && onChange) { const updatedDate = new Date( parseInt(year), parseInt(month) - 1, - parseInt(newDay), + parseInt(modifiedDay), ); onChange(updatedDate); } - document.getElementById(`${id}-month-input`)?.focus(); } }; @@ -66,21 +71,27 @@ export default function DateField({ const newMonth = e.target.value; setMonth(newMonth); + // Check if change is from spinner (stepUp/stepDown) vs keyboard input + const isFromSpinner = + e.nativeEvent instanceof InputEvent && + (e.nativeEvent as InputEvent).inputType === "insertReplacementText"; + if ( - newMonth.length === 2 && + (isFromSpinner || newMonth.length === 2) && parseInt(newMonth) >= 1 && parseInt(newMonth) <= 12 ) { - if (isValidDate(year, newMonth, day) && onChange) { + const modifiedMonth = isFromSpinner + ? newMonth.padStart(2, "0") + : newMonth; + if (isValidDate(year, modifiedMonth, day) && onChange) { const updatedDate = new Date( parseInt(year), - parseInt(newMonth) - 1, + parseInt(modifiedMonth) - 1, parseInt(day), ); onChange(updatedDate); } - - document.getElementById(`${id}-year-input`)?.focus(); } }; @@ -100,6 +111,38 @@ export default function DateField({ } }; + // Handle day blur to pad single digit values + const handleDayBlur = () => { + if (day.length === 1 && parseInt(day) >= 1 && parseInt(day) <= 9) { + const paddedDay = day.padStart(2, "0"); + setDay(paddedDay); + if (isValidDate(year, month, paddedDay) && onChange) { + const updatedDate = new Date( + parseInt(year), + parseInt(month) - 1, + parseInt(paddedDay), + ); + onChange(updatedDate); + } + } + }; + + // Handle month blur to pad single digit values + const handleMonthBlur = () => { + if (month.length === 1 && parseInt(month) >= 1) { + const paddedMonth = month.padStart(2, "0"); + setMonth(paddedMonth); + if (isValidDate(year, paddedMonth, day) && onChange) { + const updatedDate = new Date( + parseInt(year), + parseInt(paddedMonth) - 1, + parseInt(day), + ); + onChange(updatedDate); + } + } + }; + return ( @@ -109,6 +152,7 @@ export default function DateField({ placeholder="DD" value={day} onChange={handleDayChange} + onBlur={handleDayBlur} min={1} max={31} id={`${id}-day-input`} @@ -124,6 +168,7 @@ export default function DateField({ placeholder="MM" value={month} onChange={handleMonthChange} + onBlur={handleMonthBlur} min={1} max={12} id={`${id}-month-input`} @@ -140,6 +185,7 @@ export default function DateField({ value={year} onChange={handleYearChange} min={1900} + max={new Date().getFullYear()} id={`${id}-year-input`} data-cy={`${id}-year-input`} disabled={disabled} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index b76d7c5b131..e2514538690 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>( { const Markdown = React.forwardRef( ({ className, content, prose = true, ...props }, ref) => { - const html = React.useMemo(() => md.render(content), [content]); + const html = React.useMemo(() => { + const renderedHtml = md.render(content); + return DOMPurify.sanitize(renderedHtml); + }, [content]); if (prose) { return ( diff --git a/src/components/ui/sidebar/admin-nav.tsx b/src/components/ui/sidebar/admin-nav.tsx new file mode 100644 index 00000000000..e0544ad919b --- /dev/null +++ b/src/components/ui/sidebar/admin-nav.tsx @@ -0,0 +1,33 @@ +import { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; + +import { NavMain } from "@/components/ui/sidebar/nav-main"; + +interface NavigationLink { + name: string; + url: string; + icon?: string; +} + +function generateAdminLinks(t: TFunction) { + const baseUrl = "/admin"; + const links: NavigationLink[] = [ + { + name: t("questionnaire"), + url: `${baseUrl}/questionnaire`, + icon: "d-book-open", + }, + { + name: "Valuesets", + url: `${baseUrl}/valuesets`, + icon: "l-list-ol-alt", + }, + ]; + + return links; +} + +export function AdminNav() { + const { t } = useTranslation(); + return ; +} diff --git a/src/components/ui/sidebar/app-sidebar.tsx b/src/components/ui/sidebar/app-sidebar.tsx index b22136ff928..bc2a66153fc 100644 --- a/src/components/ui/sidebar/app-sidebar.tsx +++ b/src/components/ui/sidebar/app-sidebar.tsx @@ -1,6 +1,7 @@ import { DashboardIcon } from "@radix-ui/react-icons"; import { Link, usePathParams } from "raviger"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import { Sidebar, @@ -12,6 +13,7 @@ import { SidebarMenuItem, SidebarRail, } from "@/components/ui/sidebar"; +import { AdminNav } from "@/components/ui/sidebar/admin-nav"; import { FacilityNav } from "@/components/ui/sidebar/facility-nav"; import { FacilitySwitcher } from "@/components/ui/sidebar/facility-switcher"; import { @@ -33,6 +35,7 @@ interface AppSidebarProps extends React.ComponentProps { export enum SidebarFor { FACILITY = "facility", PATIENT = "patient", + ADMIN = "admin", } export function AppSidebar({ @@ -40,6 +43,7 @@ export function AppSidebar({ sidebarFor = SidebarFor.FACILITY, ...props }: AppSidebarProps) { + const { t } = useTranslation(); const exactMatch = usePathParams("/facility/:facilityId"); const subpathMatch = usePathParams("/facility/:facilityId/*"); const facilityId = exactMatch?.facilityId || subpathMatch?.facilityId; @@ -50,6 +54,7 @@ export function AppSidebar({ const facilitySidebar = sidebarFor === SidebarFor.FACILITY; const patientSidebar = sidebarFor === SidebarFor.PATIENT; + const adminSidebar = sidebarFor === SidebarFor.ADMIN; const [selectedFacility, setSelectedFacility] = React.useState(null); @@ -60,12 +65,13 @@ export function AppSidebar({ }, [user?.organizations, organizationId]); React.useEffect(() => { - if (!user?.facilities || !facilityId || !facilitySidebar) return; - - const facility = user.facilities.find((f) => f.id === facilityId); - if (facility) { - setSelectedFacility(facility); + if (!user?.facilities || !facilityId || !facilitySidebar) { + setSelectedFacility(null); + return; } + + const facility = user.facilities.find((f) => f.id === facilityId) || null; + setSelectedFacility(facility); }, [facilityId, user?.facilities, facilitySidebar]); const hasFacilities = user?.facilities && user.facilities.length > 0; @@ -85,7 +91,7 @@ export function AppSidebar({ selectedOrganization={selectedOrganization} /> )} - {selectedFacility && hasFacilities && ( + {facilityId && selectedFacility && hasFacilities && ( - View Dashboard + {t("view_dashboard")} @@ -123,6 +129,7 @@ export function AppSidebar({ )} {patientSidebar && } + {adminSidebar && } diff --git a/src/components/ui/sidebar/patient-switcher.tsx b/src/components/ui/sidebar/patient-switcher.tsx index 6718146ba71..97810946b03 100644 --- a/src/components/ui/sidebar/patient-switcher.tsx +++ b/src/components/ui/sidebar/patient-switcher.tsx @@ -1,5 +1,7 @@ import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + import { Select, SelectContent, @@ -13,8 +15,6 @@ import { Avatar } from "@/components/Common/Avatar"; import { usePatientContext } from "@/hooks/usePatientUser"; -import { classNames } from "@/Utils/utils"; - interface PatientSwitcherProps { className?: string; } @@ -30,12 +30,7 @@ export function PatientSwitcher({ className }: PatientSwitcherProps) { } return ( - + , React.ComponentPropsWithoutRef ->(({ children, content, sideOffset = 4, className }, ref) => { +>(({ children, content, sideOffset = 4, className, side }, ref) => { const [open, setOpen] = React.useState(false); return ( @@ -45,6 +45,7 @@ const TooltipComponent = React.forwardRef< "z-50 overflow-hidden rounded-md bg-gray-900 px-3 py-1.5 text-xs text-gray-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-gray-50 dark:text-gray-900", className, )} + side={side} > {content} diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts deleted file mode 100644 index 66ee398f962..00000000000 --- a/src/hooks/useDebounce.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useRef } from "react"; - -export default function useDebounce( - callback: (...args: T) => void, - delay: number, -) { - const callbackRef = useRef(callback); - const timeoutRef = useRef | null>(null); - - useEffect(() => { - callbackRef.current = callback; - }, [callback]); - - useEffect(() => { - return () => { - if (timeoutRef.current) clearTimeout(timeoutRef.current); - }; - }, []); - - const debouncedCallback = (...args: T) => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = setTimeout(() => { - callbackRef.current(...args); - }, delay); - }; - return debouncedCallback; -} diff --git a/src/hooks/useFileManager.tsx b/src/hooks/useFileManager.tsx index 6a704b02c77..83e999b97d0 100644 --- a/src/hooks/useFileManager.tsx +++ b/src/hooks/useFileManager.tsx @@ -9,13 +9,18 @@ import { cn } from "@/lib/utils"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; -import DialogModal from "@/components/Common/Dialog"; import FilePreviewDialog from "@/components/Common/FilePreviewDialog"; -import { StateInterface } from "@/components/Files/FileUpload"; +import { StateInterface } from "@/components/Common/FilePreviewDialog"; import { FileUploadModel } from "@/components/Patient/models"; import { @@ -257,67 +262,156 @@ export default function useFileManager( downloadURL={downloadURL} uploadedFiles={uploadedFiles} onClose={handleFilePreviewClose} - fixedWidth={false} className="h-[80vh] w-full md:h-screen" loadFile={viewFile} currentIndex={currentIndex} /> - - - setArchiveDialogueOpen(null)} + > + + + + + + + + + {t("archive_file")} + + {t("this_action_is_irreversible")} + + + + + + + { + event.preventDefault(); + handleFileArchive(archiveDialogueOpen); + }} + className="mx-2 my-4 flex w-full flex-col" + > + + + }} + /> + + setArchiveReason(e.target.value)} + className={cn( + archiveReasonError && + "border-red-500 focus-visible:ring-red-500", + )} /> + {archiveReasonError && ( + {archiveReasonError} + )} - - Archive File - - {t("this_action_is_irreversible")} - + + setArchiveDialogueOpen(null)} + > + {t("cancel")} + + + {t("proceed")} + - + + + + setArchiveDialogueOpen(null)} + onOpenChange={() => setArchiveDialogueOpen(null)} > - { - event.preventDefault(); - handleFileArchive(archiveDialogueOpen); - }} - className="mx-2 my-4 flex w-full flex-col" - > - - - }} - /> - - setArchiveReason(e.target.value)} - className={cn( - archiveReasonError && - "border-red-500 focus-visible:ring-red-500", - )} - /> - {archiveReasonError && ( - {archiveReasonError} - )} + + + + {archiveDialogueOpen?.name} {t("archived")} + + + + + {t("this_file_has_been_archived")} + + + {[ + { + label: "File Name", + content: archiveDialogueOpen?.name, + icon: "l-file", + }, + { + label: "Uploaded By", + content: archiveDialogueOpen?.uploaded_by?.username, + icon: "l-user", + }, + { + label: "Uploaded On", + content: formatDateTime(archiveDialogueOpen?.created_date), + icon: "l-clock", + }, + { + label: "Archive Reason", + content: archiveDialogueOpen?.archive_reason, + icon: "l-archive", + }, + { + label: "Archived By", + content: archiveDialogueOpen?.archived_by?.username, + icon: "l-user", + }, + { + label: "Archived On", + content: formatDateTime(archiveDialogueOpen?.archived_datetime), + icon: "l-clock", + }, + ].map((item, index) => ( + + + + + + + {item.label} + + + {item.content} + + + + ))} - + {t("cancel")} - - {t("proceed")} - - - - - {archiveDialogueOpen?.name} (Archived) - - } - fixedWidth={false} - className="md:w-[700px]" - onClose={() => setArchiveDialogueOpen(null)} + + + setEditDialogueOpen(null)} > - - - {t("this_file_has_been_archived")} - - - {[ - { - label: "File Name", - content: archiveDialogueOpen?.name, - icon: "l-file", - }, - { - label: "Uploaded By", - content: archiveDialogueOpen?.uploaded_by?.username, - icon: "l-user", - }, - { - label: "Uploaded On", - content: formatDateTime(archiveDialogueOpen?.created_date), - icon: "l-clock", - }, - { - label: "Archive Reason", - content: archiveDialogueOpen?.archive_reason, - icon: "l-archive", - }, - { - label: "Archived By", - content: archiveDialogueOpen?.archived_by?.username, - icon: "l-user", - }, - { - label: "Archived On", - content: formatDateTime(archiveDialogueOpen?.archived_datetime), - icon: "l-clock", - }, - ].map((item, index) => ( - - - - - - - {item.label} + + + + + + - - {item.content} + + {t("rename_file")} - - ))} - - - setArchiveDialogueOpen(null)} + + + { + event.preventDefault(); + setEditing(true); + if (editDialogueOpen) partialupdateFileName(editDialogueOpen); + }} + className="flex w-full flex-col" > - {t("cancel")} - - - - - - + {t("enter_the_file_name")} + { + setEditDialogueOpen({ + ...editDialogueOpen, + name: e.target.value, + }); + }} /> + {editError && {editError}} - - {t("rename_file")} + + setEditDialogueOpen(null)} + > + {t("cancel")} + + + {t("proceed")} + - - } - onClose={() => setEditDialogueOpen(null)} - > - { - event.preventDefault(); - setEditing(true); - if (editDialogueOpen) partialupdateFileName(editDialogueOpen); - }} - className="flex w-full flex-col" - > - - {t("enter_the_file_name")} - { - setEditDialogueOpen({ - ...editDialogueOpen, - name: e.target.value, - }); - }} - /> - {editError && {editError}} - - - setEditDialogueOpen(null)} - > - {t("cancel")} - - - {t("proceed")} - - - - + + + > ); diff --git a/src/hooks/useFileUpload.tsx b/src/hooks/useFileUpload.tsx index d53b9497e08..d1b556fbb22 100644 --- a/src/hooks/useFileUpload.tsx +++ b/src/hooks/useFileUpload.tsx @@ -363,8 +363,8 @@ export default function useFileUpload( const Dialogues = ( <> setCameraModalOpen(false)} + open={cameraModalOpen} + onOpenChange={(open) => setCameraModalOpen(open)} onCapture={(file) => { setFiles((prev) => [...prev, file]); }} diff --git a/src/hooks/useFilters.tsx b/src/hooks/useFilters.tsx index b6e9022a675..a0c10a7a24e 100644 --- a/src/hooks/useFilters.tsx +++ b/src/hooks/useFilters.tsx @@ -2,12 +2,14 @@ import { QueryParam, setQueryParamsOptions, useQueryParams } from "raviger"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + import GenericFilterBadge from "@/CAREUI/display/FilterBadge"; import PaginationComponent from "@/components/Common/Pagination"; import FiltersCache from "@/Utils/FiltersCache"; -import { classNames, humanizeStrings } from "@/Utils/utils"; +import { humanizeStrings } from "@/Utils/utils"; export type FilterState = Record; @@ -222,7 +224,7 @@ export default function useFilters({ } return ( limit ? "visible" : "invisible", !noMargin && "mt-4", diff --git a/src/index.tsx b/src/index.tsx index 9154b832187..4f8ac291d51 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ import careConfig from "@careConfig"; import * as Sentry from "@sentry/browser"; +import React from "react"; import { createRoot } from "react-dom/client"; import { registerSW } from "virtual:pwa-register"; @@ -31,4 +32,8 @@ if (import.meta.env.PROD) { } const root = createRoot(document.getElementById("root") as HTMLElement); -root.render(); +root.render( + + + , +); diff --git a/src/pages/Appointments/AppointmentDetail.tsx b/src/pages/Appointments/AppointmentDetail.tsx index c29454bfaf0..624647e5987 100644 --- a/src/pages/Appointments/AppointmentDetail.tsx +++ b/src/pages/Appointments/AppointmentDetail.tsx @@ -54,17 +54,16 @@ import { formatName, getReadableDuration, saveElementAsImage, - stringifyGeoOrganization, + stringifyNestedObject, } from "@/Utils/utils"; import { AppointmentTokenCard } from "@/pages/Appointments/components/AppointmentTokenCard"; -import { formatAppointmentSlotTime } from "@/pages/Appointments/utils"; import { FacilityData } from "@/types/facility/facility"; import { Appointment, AppointmentFinalStatuses, AppointmentUpdateRequest, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; import { AppointmentSlotPicker } from "./components/AppointmentSlotPicker"; @@ -135,17 +134,9 @@ export default function AppointmentDetail(props: Props) { } const { patient } = appointment; - const appointmentDate = formatAppointmentSlotTime(appointment); return ( - + - {stringifyGeoOrganization(appointment.patient.geo_organization)} + {stringifyNestedObject(appointment.patient.geo_organization)} {t("pincode")}: {appointment.patient.pincode} diff --git a/src/pages/Appointments/AppointmentsPage.tsx b/src/pages/Appointments/AppointmentsPage.tsx index 27102edcc59..dfb7c5e1401 100644 --- a/src/pages/Appointments/AppointmentsPage.tsx +++ b/src/pages/Appointments/AppointmentsPage.tsx @@ -82,7 +82,7 @@ import { AppointmentStatuses, TokenSlot, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface QueryParams { practitioner: string | null; @@ -337,8 +337,6 @@ export default function AppointmentsPage(props: { facilityId?: string }) { return ( - + + setQParams({ + ...qParams, + practitioner: null, + slot: null, + }) + } + className="cursor-pointer w-full" + > + + {t("show_all")} + {!qParams.practitioner && ( + + )} + + + {schedulableUsersQuery.data?.users.map((user) => ( setQParams({ ...qParams, - practitioner: null, + practitioner: user.username, slot: null, }) } - className="cursor-pointer" + className="cursor-pointer w-full" > - {t("show_all")} - {!qParams.practitioner && ( + + + {formatName(user)} + + {user.user_type} + + + {qParams.practitioner === user.username && ( )} - - {schedulableUsersQuery.data?.users.map((user) => ( - - - setQParams({ - ...qParams, - practitioner: user.username, - slot: null, - }) - } - className="cursor-pointer" - > - - - {formatName(user)} - - {user.user_type} - - - {qParams.practitioner === user.username && ( - - )} - - ))} diff --git a/src/pages/Appointments/BookAppointment.tsx b/src/pages/Appointments/BookAppointment.tsx index 5a670f81e16..fb61ed487f4 100644 --- a/src/pages/Appointments/BookAppointment.tsx +++ b/src/pages/Appointments/BookAppointment.tsx @@ -25,7 +25,7 @@ import useAppHistory from "@/hooks/useAppHistory"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { formatDisplayName, formatName } from "@/Utils/utils"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; import { AppointmentSlotPicker } from "./components/AppointmentSlotPicker"; diff --git a/src/pages/Appointments/components/AppointmentSlotPicker.tsx b/src/pages/Appointments/components/AppointmentSlotPicker.tsx index 1830c9b423f..0220b47678a 100644 --- a/src/pages/Appointments/components/AppointmentSlotPicker.tsx +++ b/src/pages/Appointments/components/AppointmentSlotPicker.tsx @@ -18,7 +18,7 @@ import { useAvailabilityHeatmap, } from "@/pages/Appointments/utils"; import { TokenSlot } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface AppointmentSlotPickerProps { facilityId: string; diff --git a/src/pages/Appointments/utils.ts b/src/pages/Appointments/utils.ts index 50ab30c5b6f..2144524f130 100644 --- a/src/pages/Appointments/utils.ts +++ b/src/pages/Appointments/utils.ts @@ -15,7 +15,7 @@ import { AvailabilityHeatmapResponse, TokenSlot, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; export const groupSlotsByAvailability = (slots: TokenSlot[]) => { const result: { diff --git a/src/pages/Apps/PlugConfigEdit.tsx b/src/pages/Apps/PlugConfigEdit.tsx index 353a1796b88..f6230b71f15 100644 --- a/src/pages/Apps/PlugConfigEdit.tsx +++ b/src/pages/Apps/PlugConfigEdit.tsx @@ -2,6 +2,8 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { useNavigate } from "raviger"; import { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { @@ -15,7 +17,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; @@ -108,7 +110,7 @@ export function PlugConfigEdit({ slug }: Props) { Cancel Delete diff --git a/src/pages/Encounters/EncounterList.tsx b/src/pages/Encounters/EncounterList.tsx index f6eaf605c80..9af50a0ebca 100644 --- a/src/pages/Encounters/EncounterList.tsx +++ b/src/pages/Encounters/EncounterList.tsx @@ -213,7 +213,7 @@ export function EncounterList({ const { t } = useTranslation(); return ( - + @@ -261,6 +261,7 @@ export function EncounterList({ onFieldChange={handleFieldChange} onSearch={handleSearch} className="w-full border-none shadow-none" + autoFocus /> diff --git a/src/pages/Encounters/EncounterShow.tsx b/src/pages/Encounters/EncounterShow.tsx index 1f065b6e50c..020bd8f636d 100644 --- a/src/pages/Encounters/EncounterShow.tsx +++ b/src/pages/Encounters/EncounterShow.tsx @@ -26,6 +26,7 @@ export interface EncounterTabProps { facilityId: string; encounter: Encounter; patient: Patient; + subPage?: string; } const defaultTabs = { @@ -44,10 +45,11 @@ interface Props { encounterId: string; facilityId: string; tab?: string; + subPage?: string; } export const EncounterShow = (props: Props) => { - const { facilityId, encounterId } = props; + const { facilityId, encounterId, subPage } = props; const { t } = useTranslation(); const pluginTabs = useCareAppConsultationTabs(); @@ -74,6 +76,7 @@ export const EncounterShow = (props: Props) => { const encounterTabProps: EncounterTabProps = { encounter: encounterData, patient: encounterData.patient, + subPage: subPage, facilityId, }; @@ -97,21 +100,7 @@ export const EncounterShow = (props: Props) => { return ( - + { )} > )} */} - - {t("patient_details")} - diff --git a/src/pages/Encounters/PrintPrescription.tsx b/src/pages/Encounters/PrintPrescription.tsx index 607a42eee91..6c4b0f4d332 100644 --- a/src/pages/Encounters/PrintPrescription.tsx +++ b/src/pages/Encounters/PrintPrescription.tsx @@ -17,8 +17,9 @@ import medicationRequestApi from "@/types/emr/medicationRequest/medicationReques export const PrintPrescription = (props: { facilityId: string; encounterId: string; + patientId: string; }) => { - const { facilityId, encounterId } = props; + const { facilityId, encounterId, patientId } = props; const { t } = useTranslation(); const { data: encounter } = useQuery({ @@ -30,12 +31,12 @@ export const PrintPrescription = (props: { }); const { data: medications } = useQuery({ - queryKey: ["medication_requests", encounter?.patient?.id], + queryKey: ["medication_requests", patientId], queryFn: query(medicationRequestApi.list, { - pathParams: { patientId: encounter?.patient?.id || "" }, + pathParams: { patientId }, queryParams: { encounter: encounterId, limit: 50, offset: 0 }, }), - enabled: !!encounter?.patient?.id, + enabled: !!patientId, }); if (!medications?.results?.length) { @@ -60,12 +61,8 @@ export const PrintPrescription = (props: { return ( @@ -91,7 +88,7 @@ export const PrintPrescription = (props: { @@ -125,7 +122,7 @@ export const PrintPrescription = (props: { ℞ {/* Medications Table */} - + {/* Doctor's Signature */} diff --git a/src/pages/Encounters/tabs/EncounterFilesTab.tsx b/src/pages/Encounters/tabs/EncounterFilesTab.tsx index ff270ac9d60..76add034ca5 100644 --- a/src/pages/Encounters/tabs/EncounterFilesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterFilesTab.tsx @@ -7,7 +7,8 @@ export const EncounterFilesTab = (props: EncounterTabProps) => { ); }; diff --git a/src/pages/Encounters/tabs/EncounterNotesTab.tsx b/src/pages/Encounters/tabs/EncounterNotesTab.tsx index 89b535134df..058606392e4 100644 --- a/src/pages/Encounters/tabs/EncounterNotesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterNotesTab.tsx @@ -9,11 +9,13 @@ import { Info, Loader2, MessageCircle, + MessageSquare, MessageSquarePlus, Plus, Send, Users, } from "lucide-react"; +import { Link, usePathParams } from "raviger"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useInView } from "react-intersection-observer"; @@ -25,6 +27,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, + DialogClose, DialogContent, DialogDescription, DialogFooter, @@ -61,6 +64,7 @@ import { Thread } from "@/types/notes/threads"; const MESSAGES_LIMIT = 20; // Thread templates for quick selection + const threadTemplates = [ "Treatment Plan", "Medication Notes", @@ -118,6 +122,7 @@ const ThreadItem = ({ // Message item component const MessageItem = ({ message }: { message: Message }) => { const authUser = useAuthUser(); + const { facilityId } = usePathParams("/facility/:facilityId/*")!; const isCurrentUser = authUser?.external_id === message.created_by.id; return ( @@ -135,15 +140,19 @@ const MessageItem = ({ message }: { message: Message }) => { > - - - - - + + + + + + + {message.created_by.username} @@ -192,11 +201,13 @@ const NewThreadDialog = ({ onClose, onCreate, isCreating, + threadsUnused, }: { isOpen: boolean; onClose: () => void; onCreate: (title: string) => void; isCreating: boolean; + threadsUnused: string[]; }) => { const { t } = useTranslation(); const [title, setTitle] = useState(""); @@ -218,13 +229,15 @@ const NewThreadDialog = ({ - {t("encounter_notes__choose_template")} + {threadsUnused.length === 0 + ? t("encounter_notes__no_unused_threads") + : t("encounter_notes__choose_template")} - {threadTemplates.map((template) => ( + {threadsUnused.map((template) => ( - - {t("Cancel")} - + + {t("cancel")} + + onCreate(title)} disabled={!title.trim() || isCreating} @@ -258,7 +272,7 @@ const NewThreadDialog = ({ ) : ( )} - {t("Create")} + {t("create")} @@ -308,6 +322,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { const [newMessage, setNewMessage] = useState(""); const messagesEndRef = useRef(null); const { ref, inView } = useInView(); + const [commentAdded, setCommentAdded] = useState(false); // Fetch threads const { data: threadsData, isLoading: threadsLoading } = useQuery({ @@ -318,17 +333,11 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { }), }); - // Auto-select first thread - useEffect(() => { - if (threadsData?.results.length && !selectedThread) { - setSelectedThread(threadsData.results[0].id); - } - }, [threadsData, selectedThread]); - // Fetch messages with infinite scroll const { data: messagesData, isLoading: messagesLoading, + isFetching: isFetchingMessages, hasNextPage, fetchNextPage, isFetchingNextPage, @@ -382,28 +391,63 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["messages", selectedThread] }); setNewMessage(""); - setTimeout(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, 100); + setCommentAdded(true); }, }); - // Handle infinite scroll + // handle scrolling to last message when new message is added + useEffect(() => { - if (inView && hasNextPage) { - fetchNextPage(); + if (commentAdded && !isFetchingMessages) { + messagesEndRef.current?.scrollIntoView(); + setCommentAdded(false); } - }, [inView, hasNextPage, fetchNextPage]); + }, [commentAdded, isFetchingMessages]); + + const [threads, setThreads] = useState([...threadTemplates]); + + // Auto-select first thread - // Scroll to bottom on initial load and thread change useEffect(() => { - if (messagesData && !messagesLoading && !isFetchingNextPage) { + if (threadsData?.results.length) { + if (!selectedThread) setSelectedThread(threadsData.results[0].id); + const threadTitles = threadsData.results.map((thread) => thread.title); + setThreads( + threads.filter((template) => !threadTitles.includes(template)), + ); + } + }, [threadsData, selectedThread]); + + // hack to scroll to bottom on initial load + + useEffect(() => { + messagesEndRef.current?.scrollIntoView(); + }, [messagesLoading]); + + // Handle infinite scroll + + useEffect(() => { + if (inView && hasNextPage) { + fetchNextPage(); messagesEndRef.current?.scrollIntoView(); } - }, [selectedThread, messagesData, messagesLoading, isFetchingNextPage]); + }, [ + inView, + hasNextPage, + fetchNextPage, + messagesData, + isFetchingNextPage, + messagesLoading, + ]); const handleCreateThread = (title: string) => { if (title.trim()) { + if ( + threadsData?.results.some((thread) => thread.title === title.trim()) + ) { + toast.error(t("thread_already_exists")); + return; + } createThreadMutation.mutate({ title: title.trim(), encounter: encounter.id, @@ -423,6 +467,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { } const messages = messagesData?.pages.flatMap((page) => page.results) ?? []; + const totalMessages = messagesData?.pages[0]?.count ?? 0; return ( @@ -529,8 +574,8 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { {/* Main Content */} - {/* Mobile Header */} - + {/* Header */} + {selectedThread ? ( @@ -539,10 +584,27 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { ?.title } - - - {messages.length} - + + + + + + {new Set(messages.map((m) => m.created_by.id)).size} + + + {totalMessages} + + + + + {t("participants")}:{" "} + {new Set(messages.map((m) => m.created_by.id)).size} + + + {t("messages")}: {totalMessages} + + + ) : ( @@ -550,13 +612,12 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { )} - {selectedThread ? ( <> {messagesLoading ? ( - + ) : ( @@ -580,17 +641,17 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { )) )} - {isFetchingNextPage && ( + {isFetchingNextPage ? ( + ) : ( + )} - - {/* Message Input */} @@ -662,6 +723,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { onClose={() => setShowNewThreadDialog(false)} onCreate={handleCreateThread} isCreating={createThreadMutation.isPending} + threadsUnused={threads} /> ); diff --git a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx index 5f6eaa151d9..d55cf9b5346 100644 --- a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx @@ -14,7 +14,7 @@ export const EncounterUpdatesTab = ({ return ( {/* Main Content Area */} - + {/* Left Column - Symptoms, Diagnoses, and Questionnaire Responses */} {/* Allergies Section */} @@ -55,7 +55,7 @@ export const EncounterUpdatesTab = ({ {/* Right Column - Observations */} - + diff --git a/src/pages/Facility/overview.tsx b/src/pages/Facility/overview.tsx index 346a34c6c48..cf07db69e65 100644 --- a/src/pages/Facility/overview.tsx +++ b/src/pages/Facility/overview.tsx @@ -27,7 +27,7 @@ export function FacilityOverview({ facilityId }: FacilityOverviewProps) { href: `/facility/${facilityId}/users/${user?.username}/availability`, }, { - title: t("Encounters"), + title: t("encounters"), description: t("manage_facility_users"), icon: Users, href: `/facility/${facilityId}/encounters`, diff --git a/src/pages/Facility/settings/layout.tsx b/src/pages/Facility/settings/layout.tsx index 9ba1d35ec39..a2b7c1c58f3 100644 --- a/src/pages/Facility/settings/layout.tsx +++ b/src/pages/Facility/settings/layout.tsx @@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import ErrorPage from "@/components/ErrorPages/DefaultErrorPage"; + import { GeneralSettings } from "./general/general"; import LocationList from "./locations/LocationList"; import LocationView from "./locations/LocationView"; @@ -28,7 +30,7 @@ const getRoutes = (facilityId: string) => ({ "/location/:id": ({ id }: { id: string }) => ( ), - "*": () => 404, + "*": () => , }); export function SettingsLayout({ facilityId }: SettingsLayoutProps) { diff --git a/src/pages/Facility/settings/locations/LocationForm.tsx b/src/pages/Facility/settings/locations/LocationForm.tsx index dc9b3f96bfd..6f82f64228a 100644 --- a/src/pages/Facility/settings/locations/LocationForm.tsx +++ b/src/pages/Facility/settings/locations/LocationForm.tsx @@ -223,7 +223,7 @@ export default function LocationForm({ - + {locationFormOptions.map((option) => ( {option.label} diff --git a/src/pages/Facility/settings/locations/LocationView.tsx b/src/pages/Facility/settings/locations/LocationView.tsx index d5527d5fe4f..9bab726b813 100644 --- a/src/pages/Facility/settings/locations/LocationView.tsx +++ b/src/pages/Facility/settings/locations/LocationView.tsx @@ -1,10 +1,18 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Link } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Badge } from "@/components/ui/badge"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -44,7 +52,6 @@ export default function LocationView({ id, facilityId }: Props) { pathParams: { facility_id: facilityId, id }, }), }); - const { data: locationOrganizations } = useQuery({ queryKey: ["location", id, "organizations"], queryFn: query(locationApi.getOrganizations, { @@ -92,110 +99,157 @@ export default function LocationView({ id, facilityId }: Props) { ); + const generateBreadcrumbs = (location: any) => { + const breadcrumbs = []; + let current = location; + while (current) { + breadcrumbs.unshift({ + name: current.name, + id: current.id, + }); + current = current.parent; + } + return breadcrumbs; + }; + const breadcrumbs = location ? generateBreadcrumbs(location) : []; return ( - - - - - - {t("locations")} - - {getLocationFormLabel(location?.form)} - - - {location?.status} - - {location && "mode" in location && location.mode === "kind" && ( - - - {t("add_location")} - + <> + + + + + {t("home")} + + + {breadcrumbs.map((breadcrumb, index) => ( + + {index === breadcrumbs.length - 1 ? ( + + {breadcrumb.name} + + ) : ( + <> + + {breadcrumb.name} + + + > )} + + ))} + + + + + + + + + {t("locations")} + + {getLocationFormLabel(location?.form)} + + + {location?.status} + + {location && "mode" in location && location.mode === "kind" && ( + + + {t("add_location")} + + )} + + + { + setSearchQuery(e.target.value); + setPage(1); + }} + className="w-full" + /> + - - { - setSearchQuery(e.target.value); - setPage(1); + {locationOrganizations && ( + + + {t("manage_organizations")} + + } + onUpdate={() => { + queryClient.invalidateQueries({ + queryKey: ["location", facilityId, id], + }); }} - className="w-full" /> - + )} - {locationOrganizations && ( - - - {t("manage_organizations")} - - } - onUpdate={() => { - queryClient.invalidateQueries({ - queryKey: ["location", facilityId, id], - }); - }} - /> - )} - - {isLoading ? ( - - - - ) : ( - + {isLoading ? ( - {children?.results?.length ? ( - children.results.map((childLocation: LocationList) => ( - + + ) : ( + + + {children?.results?.length ? ( + children.results.map((childLocation: LocationList) => ( + + )) + ) : ( + + + {searchQuery + ? t("no_locations_found") + : t("no_child_locations_found")} + + + )} + + {children && children.count > limit && ( + + setPage(page)} + defaultPerPage={limit} + cPage={page} /> - )) - ) : ( - - - {searchQuery - ? t("no_locations_found") - : t("no_child_locations_found")} - - + )} - {children && children.count > limit && ( - - setPage(page)} - defaultPerPage={limit} - cPage={page} - /> - - )} - - )} - + )} + - - + + + > ); } diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx index 571d898bd10..da1e2eae284 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx @@ -55,12 +55,7 @@ export default function FacilityOrganizationIndex({ if (!data?.results?.length) { return ( - + @@ -87,12 +82,7 @@ export default function FacilityOrganizationIndex({ } return ( - + diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx index 509f451a592..d0bde50e2ce 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx @@ -1,19 +1,19 @@ import { useQuery } from "@tanstack/react-query"; -import { useQueryParams } from "raviger"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/Common/Avatar"; -import { - CardGridSkeleton, - CardListSkeleton, -} from "@/components/Common/SkeletonLoading"; +import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import { UserStatusIndicator } from "@/components/Users/UserListAndCard"; +import useFilters from "@/hooks/useFilters"; + import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; import AddUserSheet from "@/pages/Organization/components/AddUserSheet"; @@ -29,19 +29,30 @@ interface Props { } export default function FacilityOrganizationUsers({ id, facilityId }: Props) { - const [qParams, setQueryParams] = useQueryParams<{ + const [sheetState, setSheetState] = useState<{ sheet: string; username: string; - }>(); + }>({ + sheet: "", + username: "", + }); + const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ + limit: 12, + }); const { t } = useTranslation(); - const openAddUserSheet = qParams.sheet === "add"; - const openLinkUserSheet = qParams.sheet === "link"; + const openAddUserSheet = sheetState.sheet === "add"; + const openLinkUserSheet = sheetState.sheet === "link"; const { data: users, isLoading: isLoadingUsers } = useQuery({ - queryKey: ["facilityOrganizationUsers", facilityId, id], - queryFn: query(routes.facilityOrganization.listUsers, { + queryKey: ["facilityOrganizationUsers", facilityId, id, qParams], + queryFn: query.debounced(routes.facilityOrganization.listUsers, { pathParams: { facilityId, organizationId: id }, + queryParams: { + search_text: qParams.search || undefined, + limit: resultsPerPage, + offset: ((qParams.page || 1) - 1) * resultsPerPage, + }, }), enabled: !!id, }); @@ -50,32 +61,32 @@ export default function FacilityOrganizationUsers({ id, facilityId }: Props) { return null; } - if (isLoadingUsers) { - return ( - - - - - - - - - ); - } - return ( - - {t("users")} - + + + + { + updateQuery({ search: e.target.value || undefined }); + }} + className="w-full pl-8" + /> + + { - setQueryParams({ sheet: open ? "add" : "", username: "" }); + setSheetState({ sheet: open ? "add" : "", username: "" }); }} onUserCreated={(user) => { - setQueryParams({ sheet: "link", username: user.username }); + setSheetState({ sheet: "link", username: user.username }); }} /> { - setQueryParams({ sheet: open ? "link" : "", username: "" }); + setSheetState({ sheet: open ? "link" : "", username: "" }); }} - preSelectedUsername={qParams.username} + preSelectedUsername={sheetState.username} /> - - {users?.results?.length === 0 ? ( - - - {t("no_users_found")} - - - ) : ( - users?.results?.map((userRole: OrganizationUserRole) => ( - - - - - - - - - - {userRole.user.first_name} {userRole.user.last_name} - - - + {isLoadingUsers ? ( + + + + ) : ( + + + {!users?.results?.length ? ( + + + {t("no_users_found")} + + + ) : ( + users.results.map((userRole: OrganizationUserRole) => ( + + + + + + + + + + {userRole.user.first_name}{" "} + {userRole.user.last_name} + + + + + + {userRole.user.username} - - + + + + {t("role")} + + {userRole.role.name} + + + + + {t("phone_number")} + + + {userRole.user.phone_number} + + + - - - - {t("role")} - - {userRole.role.name} - + + + {t("see_details")} + + } + /> - - {t("phone_number")} - - {userRole.user.phone_number} - - - - - - - - {t("more_details")} - - } - /> - - - - - )) - )} - + + + )) + )} + + + {(users?.results || []).length > 0 && + users?.count && + users.count > resultsPerPage && ( + + + + )} + + )} ); diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx index 1ce58eaa786..5efc8fc4076 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx @@ -10,11 +10,13 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import Pagination from "@/components/Common/Pagination"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; +import useFilters from "@/hooks/useFilters"; + import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; +import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; import CreateFacilityOrganizationSheet from "./components/CreateFacilityOrganizationSheet"; import FacilityOrganizationLayout from "./components/FacilityOrganizationLayout"; @@ -24,12 +26,47 @@ interface Props { facilityId: string; } -export default function FacilityOrganizationView({ id, facilityId }: Props) { +function OrganizationCard({ + org, +}: { + org: FacilityOrganization; + facilityId: string; +}) { const { t } = useTranslation(); - const [page, setPage] = useState(1); + return ( + + + + + + {org.name} + + + {org.org_type} + + + + + {t("see_details")} + + + + + + ); +} + +export default function FacilityOrganizationView({ id, facilityId }: Props) { + const { t } = useTranslation(); + const { qParams, Pagination, resultsPerPage } = useFilters({ + limit: 12, + cacheBlacklist: ["username"], + }); const [searchQuery, setSearchQuery] = useState(""); - const limit = 12; // 3x4 grid const { data: children, isLoading } = useQuery({ queryKey: [ @@ -37,16 +74,16 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { "list", facilityId, id, - page, - limit, + qParams.page, + resultsPerPage, searchQuery, ], queryFn: query.debounced(routes.facilityOrganization.list, { pathParams: { facilityId }, queryParams: { parent: id, - offset: (page - 1) * limit, - limit, + offset: ((qParams.page || 1) - 1) * resultsPerPage, + limit: resultsPerPage, name: searchQuery || undefined, }, }), @@ -54,20 +91,24 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { return ( - + - {t("departments")} - - - { - setSearchQuery(e.target.value); - setPage(1); // Reset to first page on search - }} - className="w-full" - /> + + + + + { + setSearchQuery(e.target.value); + }} + className="w-full pl-8" + /> + ) : ( - + {children?.results?.length ? ( children.results.map((org) => ( - - - - - - - {org.name} - - - {org.org_type} - {org.org_type} - - - - - {t("view_details")} - - - - - {org.description && ( - - {org.description} - - )} - - - + )) ) : ( @@ -127,14 +142,9 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { )} - {children && children.count > limit && ( + {children && children.count > resultsPerPage && ( - setPage(page)} - defaultPerPage={limit} - cPage={page} - /> + )} diff --git a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx index aa0a1774d58..feca583e80d 100644 --- a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx @@ -1,4 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { t } from "i18next"; import { useState } from "react"; import { toast } from "sonner"; @@ -62,7 +63,7 @@ export default function CreateFacilityOrganizationSheet({ queryClient.invalidateQueries({ queryKey: ["getCurrentUser"], }); - toast.success("Organization created successfully"); + toast.success(t("organization_created_successfully")); setOpen(false); setName(""); setDescription(""); @@ -72,7 +73,7 @@ export default function CreateFacilityOrganizationSheet({ const handleSubmit = () => { if (!name.trim()) { - toast.error("Please enter an organization name"); + toast.error(t("please_enter_organization_name")); return; } @@ -87,16 +88,16 @@ export default function CreateFacilityOrganizationSheet({ return ( - + - Create Department/Team + {t("add_department_team")} - Create Department/Team + {t("create_department_team")} - Create a new department/team in this facility. + {t("create_department_team_description")} @@ -105,18 +106,18 @@ export default function CreateFacilityOrganizationSheet({ setName(e.target.value)} - placeholder="Enter department/team name" + placeholder={t("enter_department_team_name")} /> - Type + {t(`type`)} setOrgType(value)} > - + {ORG_TYPES.map((type) => ( @@ -133,7 +134,7 @@ export default function CreateFacilityOrganizationSheet({ setDescription(e.target.value)} - placeholder="Enter department/team description (optional)" + placeholder={t("enter_department_team_description")} /> @@ -142,7 +143,7 @@ export default function CreateFacilityOrganizationSheet({ onClick={handleSubmit} disabled={isPending || !name.trim()} > - {isPending ? "Creating..." : "Create Organization"} + {isPending ? t("creating") : t("create_organization")} diff --git a/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx b/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx index 90ab2e20ca2..6628bfac56b 100644 --- a/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import { AlertDialog, AlertDialogAction, @@ -14,7 +16,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, @@ -218,7 +220,7 @@ export default function EditUserRoleSheet({ {t("cancel")} removeRole()} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + className={cn(buttonVariants({ variant: "destructive" }))} > {t("remove")} diff --git a/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx b/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx index c706c2b38d9..de12da5794e 100644 --- a/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx +++ b/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx @@ -1,8 +1,8 @@ import { useQuery } from "@tanstack/react-query"; import { Link, usePath } from "raviger"; +import { useTranslation } from "react-i18next"; -import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; - +import { Badge } from "@/components/ui/badge"; import { Breadcrumb, BreadcrumbItem, @@ -10,8 +10,8 @@ import { BreadcrumbList, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; -import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar"; import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Page from "@/components/Common/Page"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; @@ -32,7 +32,7 @@ interface Props { interface NavItem { path: string; title: string; - icon: IconName; + value: string; } export default function FacilityOrganizationLayout({ @@ -41,20 +41,24 @@ export default function FacilityOrganizationLayout({ children, }: Props) { const path = usePath() || ""; + const { t } = useTranslation(); const navItems: NavItem[] = [ { path: `/departments/${id}`, - title: "Departments", - icon: "d-hospital", + title: t("departments_or_teams"), + value: "departments", }, { path: `/departments/${id}/users`, - title: "Users", - icon: "d-people", + title: t("users"), + value: "users", }, ]; + const currentTab = + navItems.find((item) => item.path === path)?.value || "departments"; + const { data: org, isLoading } = useQuery({ queryKey: ["facilityOrganization", id], queryFn: query(routes.facilityOrganization.get, { @@ -74,9 +78,9 @@ export default function FacilityOrganizationLayout({ ); } - // add loading state + if (!org) { - return Not found; + return {t("not_found")}; } const orgParents: FacilityOrganizationParent[] = []; @@ -89,55 +93,76 @@ export default function FacilityOrganizationLayout({ } return ( - - {/* Since we have links to all parent organizations, we can show the breadcrumb here */} - - - {/* Org has parent and each parent may have another parent, so we need to show all the parents */} - - {orgParents.reverse().map((parent) => ( - <> - - - {parent.name} - + <> + {orgParents.length > 0 && ( + + + + {orgParents.reverse().map((parent) => ( + <> + + + + {parent.name} + + + + + + + > + ))} + + + {org.name} + - - - - > - ))} - - - {org.name} - - - - - {/* Navigation */} - - - {navItems.map((item) => ( - - - - - {item.title} + + + + )} + + {t(`facility_organization_type__${org.org_type}`)} + + } + className="mx-auto max-w-4xl" + > + + {org.description && ( + + {org.description} + + )} + + + {navItems.map((item) => ( + + + {item.title} + - - - ))} - - - {/* Page Content */} - {children} - + ))} + + + + {children} + + > ); } diff --git a/src/pages/Organization/OrganizationUsers.tsx b/src/pages/Organization/OrganizationUsers.tsx index 479059fa9a3..e05e6374097 100644 --- a/src/pages/Organization/OrganizationUsers.tsx +++ b/src/pages/Organization/OrganizationUsers.tsx @@ -1,13 +1,15 @@ import { useQuery } from "@tanstack/react-query"; +import { useCallback } from "react"; import { useTranslation } from "react-i18next"; +import { isValidPhoneNumber } from "react-phone-number-input"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/Common/Avatar"; +import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import { UserStatusIndicator } from "@/components/Users/UserListAndCard"; @@ -30,19 +32,62 @@ interface Props { export default function OrganizationUsers({ id, navOrganizationId }: Props) { const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ limit: 15, - cacheBlacklist: ["search"], + cacheBlacklist: ["name", "phone_number"], }); const { t } = useTranslation(); + const searchOptions = [ + { + key: "username", + type: "text" as const, + placeholder: "Search by username", + value: qParams.name || "", + }, + { + key: "phone_number", + type: "phone" as const, + placeholder: "Search by phone number", + value: qParams.phone_number || "", + }, + ]; + + const handleSearch = useCallback((key: string, value: string) => { + const searchParams = { + name: key === "username" ? value : "", + phone_number: + key === "phone_number" + ? isValidPhoneNumber(value) + ? value + : undefined + : undefined, + }; + updateQuery(searchParams); + }, []); + + const handleFieldChange = () => { + updateQuery({ + name: undefined, + phone_number: undefined, + }); + }; + const openAddUserSheet = qParams.sheet === "add"; const openLinkUserSheet = qParams.sheet === "link"; const { data: users, isFetching: isFetchingUsers } = useQuery({ - queryKey: ["organizationUsers", id, qParams.search, qParams.page], + queryKey: [ + "organizationUsers", + id, + qParams.name, + qParams.phone_number, + qParams.page, + ], queryFn: query.debounced(organizationApi.listUsers, { pathParams: { id }, queryParams: { - username: qParams.search, + username: qParams.name, + phone_number: qParams.phone_number, + page: qParams.page, limit: resultsPerPage, offset: ((qParams.page ?? 1) - 1) * resultsPerPage, }, @@ -88,16 +133,16 @@ export default function OrganizationUsers({ id, navOrganizationId }: Props) { - - updateQuery({ - search: e.target.value as string, - }) - } - className="max-w-sm" + option.value !== ""), + 0, + )} + onSearch={handleSearch} + onFieldChange={handleFieldChange} + className="w-full" data-cy="search-user" /> diff --git a/src/pages/Organization/components/AddUserSheet.tsx b/src/pages/Organization/components/AddUserSheet.tsx index 1efba82ca81..b6054eb4a56 100644 --- a/src/pages/Organization/components/AddUserSheet.tsx +++ b/src/pages/Organization/components/AddUserSheet.tsx @@ -33,7 +33,7 @@ export default function AddUserSheet({ return ( - + {t("add_user")} diff --git a/src/pages/Organization/components/EditUserRoleSheet.tsx b/src/pages/Organization/components/EditUserRoleSheet.tsx index 2903aff574b..ca8c6d796ab 100644 --- a/src/pages/Organization/components/EditUserRoleSheet.tsx +++ b/src/pages/Organization/components/EditUserRoleSheet.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import { AlertDialog, AlertDialogAction, @@ -14,7 +16,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, @@ -226,7 +228,7 @@ export default function EditUserRoleSheet({ {t("cancel")} removeRole()} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + className={cn(buttonVariants({ variant: "destructive" }))} > {t("remove")} diff --git a/src/pages/Organization/components/OrganizationLayout.tsx b/src/pages/Organization/components/OrganizationLayout.tsx index e10168c8257..2b3a3da33a4 100644 --- a/src/pages/Organization/components/OrganizationLayout.tsx +++ b/src/pages/Organization/components/OrganizationLayout.tsx @@ -5,13 +5,6 @@ import { useTranslation } from "react-i18next"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar"; import Page from "@/components/Common/Page"; @@ -113,31 +106,7 @@ export default function OrganizationLayout({ } return ( - - {/* Since we have links to all parent organizations, we can show the breadcrumb here */} - - - {/* Org has parent and each parent may have another parent, so we need to show all the parents */} - - {orgParents.reverse().map((parent) => ( - <> - - - {parent.name} - - - - - - > - ))} - - - {org.name} - - - - + {/* Navigation */} diff --git a/src/pages/Scheduling/ScheduleExceptions.tsx b/src/pages/Scheduling/ScheduleExceptions.tsx index 7480c49a323..96e97fa177e 100644 --- a/src/pages/Scheduling/ScheduleExceptions.tsx +++ b/src/pages/Scheduling/ScheduleExceptions.tsx @@ -15,7 +15,7 @@ import Loading from "@/components/Common/Loading"; import mutate from "@/Utils/request/mutate"; import { formatTimeShort } from "@/Utils/utils"; import { ScheduleException } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { items?: ScheduleException[]; diff --git a/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx b/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx index 1d050aea3cd..22883d6f75b 100644 --- a/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx +++ b/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx @@ -34,7 +34,7 @@ import { import mutate from "@/Utils/request/mutate"; import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { facilityId: string; diff --git a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx index d4081a965fe..daee952132e 100644 --- a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx +++ b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx @@ -44,7 +44,7 @@ import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; import { getSlotsPerSession, getTokenDuration } from "@/pages/Scheduling/utils"; import { ScheduleAvailabilityCreateRequest } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { facilityId: string; diff --git a/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx index 5cdd6790bcb..16d62abc1dc 100644 --- a/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx +++ b/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx @@ -47,7 +47,7 @@ import { ScheduleAvailabilityCreateRequest, ScheduleTemplate, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; export default function EditScheduleTemplateSheet({ template, diff --git a/src/pages/UserDashboard.tsx b/src/pages/UserDashboard.tsx index ab7e8430033..573d51c0032 100644 --- a/src/pages/UserDashboard.tsx +++ b/src/pages/UserDashboard.tsx @@ -1,4 +1,4 @@ -import { ChevronRight, ClipboardList, LogOut, Settings } from "lucide-react"; +import { ChevronRight, LogOut, Settings, User2Icon } from "lucide-react"; import { Link } from "raviger"; import { Button } from "@/components/ui/button"; @@ -8,6 +8,7 @@ import { Avatar } from "@/components/Common/Avatar"; import useAuthUser, { useAuthContext } from "@/hooks/useAuthUser"; +import { formatDisplayName } from "@/Utils/utils"; import { getOrgLabel } from "@/types/organization/organization"; export default function UserDashboard() { @@ -22,7 +23,7 @@ export default function UserDashboard() { @@ -62,9 +63,9 @@ export default function UserDashboard() { className="w-full sm:w-auto" asChild > - - - Questionnaires + + + Admin Dashboard )} diff --git a/src/pluginTypes.ts b/src/pluginTypes.ts index f25c3aa7eb3..180df1502a2 100644 --- a/src/pluginTypes.ts +++ b/src/pluginTypes.ts @@ -31,6 +31,10 @@ export type PatientInfoCardActionsComponentType = React.FC<{ className?: string; }>; +export type PatientInfoCardMarkAsCompleteComponentType = React.FC<{ + encounter: Encounter; +}>; + export type FacilityHomeActionsComponentType = React.FC<{ facility: FacilityData; className?: string; @@ -53,6 +57,7 @@ export type SupportedPluginComponents = { Scribe: ScribeComponentType; PatientHomeActions: PatientHomeActionsComponentType; PatientInfoCardActions: PatientInfoCardActionsComponentType; + PatientInfoCardMarkAsComplete: PatientInfoCardMarkAsCompleteComponentType; FacilityHomeActions: FacilityHomeActionsComponentType; PatientRegistrationForm: PatientRegistrationFormComponentType; PatientDetailsTabDemographyGeneralInfo: PatientDetailsTabDemographyGeneralInfoComponentType; diff --git a/src/service-worker.ts b/src/service-worker.ts index 078e4237c28..973ff051736 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -1,6 +1,4 @@ /// - -/* eslint-disable no-restricted-globals */ // This service worker can be customized! // See https://developers.google.com/web/tools/workbox/modules // for the list of available Workbox modules, or add any other @@ -13,7 +11,6 @@ import { precacheAndRoute } from "workbox-precaching"; declare const self: ServiceWorkerGlobalScope; -// eslint-disable-next-line no-restricted-globals const _ignored = self.__WB_MANIFEST.map((_) => { return _; }); diff --git a/src/style/CAREUI.css b/src/style/CAREUI.css index 893d16c2e4e..0e01efa2853 100644 --- a/src/style/CAREUI.css +++ b/src/style/CAREUI.css @@ -1,34 +1,3 @@ -.cui-input-legend::before { - content: " "; - position: absolute; - left: -4px; - right: -4px; - top: calc(50% - 3px); - height: 6px; - background: #fff; - z-index: -1; -} - -.cui-label-required::after { - content: "*"; - color: rgb(255, 81, 0); - font-size: 1.2em; - font-weight: bold; - margin-left: 4px; -} - -.cui-input-base { - @apply text-sm block w-full py-3 px-4 text-black placeholder:text-secondary-600 bg-white disabled:bg-secondary-200 border border-secondary-400 focus:border-primary-400 ring-0 focus:ring-1 ring-primary-400 outline-none focus:outline-none shadow-none rounded transition-colors duration-300 !important -} - -.cui-dropdown-base { - @apply z-40 w-full rounded-b-md xl:rounded-b-lg shadow-lg overflow-auto max-h-96 bg-secondary-100 divide-y divide-secondary-300 ring-1 ring-secondary-400 focus:outline-none -} - -.cui-card { - @apply bg-white p-5 rounded-lg shadow -} - .tooltip { @apply relative } @@ -83,41 +52,6 @@ transform: translateY(0px); } -.cui-input:-webkit-autofill, -.cui-input:-webkit-autofill:hover, -.cui-input:-webkit-autofill:focus, -.cui-input:-webkit-autofill:active { - box-shadow: 0 0 0 40px #f9fafb inset !important; - -webkit-box-shadow: 0 0 0 40px #f9fafb inset !important; -} - -.cui-slideover-x { - @apply w-full md:w-[300px] h-full -} - -.cui-slideover-y { - @apply h-full md:h-[300px] w-full -} - -.dropdown-item-primary { @apply accent-primary-500 hover:bg-primary-100 text-black hover:text-primary-500 } -.dropdown-item-secondary { @apply accent-secondary-200 hover:bg-secondary-200 text-secondary-700 } -.dropdown-item-danger { @apply accent-danger-500 hover:bg-danger-100 text-danger-500 } -.dropdown-item-warning { @apply accent-warning-500 hover:bg-warning-100 text-warning-500 } -.dropdown-item-alert { @apply accent-alert-600 hover:bg-alert-100 text-alert-600 } - -.cui-form-button-group { - @apply flex flex-col-reverse md:flex-row md:justify-end gap-2 w-full md:w-auto -} - -.cui-range-slider { - @apply outline-none bg-black/10 h-2 w-full appearance-none transition-all -} -.cui-range-slider::-webkit-slider-thumb { - @apply appearance-none w-5 aspect-square bg-black border border-black rounded-full cursor-pointer hover:scale-125 transition-all -} -.cui-range-slider::-moz-range-thumb { - @apply appearance-none w-5 aspect-square bg-black border border-black rounded-full cursor-pointer -} hr { @apply border border-secondary-300 } diff --git a/src/style/index.css b/src/style/index.css index 2f14f673a18..4c7410e732b 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -10,23 +10,7 @@ @tailwind base; @tailwind components; - -@keyframes indeterminateAnimation { - 0% { - transform: translateX(0) scaleX(0); - } - 40% { - transform: translateX(0) scaleX(0.4); - } - 100% { - transform: translateX(100%) scaleX(0.5); - } -} - -.indeterminate { - animation: indeterminateAnimation 1s infinite linear; - transform-origin: 0% 50%; -} +@tailwind utilities; html { @@ -72,363 +56,12 @@ h6 { font-size: 0.875rem; } -.btn { - @apply inline-flex items-center justify-center whitespace-nowrap text-sm font-semibold py-2 px-4 rounded cursor-pointer; -} - -.btn:focus { - @apply outline-none shadow-inner; -} - -[type="text"]:focus { - @apply border-black ring-white; -} - - -/* Button Styles */ - -.btn-large { - @apply py-2 px-5 text-base h-10; -} - -a { - @apply text-blue-800; -} - -a:hover { - @apply text-blue-600; -} - -.bg-primary { - background-image: linear-gradient( 135deg, rgba(56, 161, 105, 1) 0%, rgba(47, 133, 90, 1) 100%); -} - - -/* Button */ - -button:focus { - outline: none; -} - -.btn { - @apply inline-flex items-center justify-center whitespace-nowrap text-sm font-semibold py-2 px-4 rounded cursor-pointer; -} - -.btn:focus { - @apply outline-none shadow-inner; -} - - -/* Button Styles */ - -.btn-default { - @apply border text-secondary-800; -} - -.btn-default:hover { - @apply bg-secondary-100 text-secondary-600; -} - -.btn-default:focus { - @apply bg-secondary-400 text-secondary-700; -} - -.btn-subtle { - @apply bg-secondary-200 text-secondary-800; -} - -.btn-subtle:hover { - @apply bg-secondary-300 text-secondary-900; -} - -.btn-subtle:focus { - @apply bg-secondary-400 text-secondary-900; -} - -.btn-primary-ghost { - @apply bg-white text-primary-500 border border-primary-500; -} - -.btn-primary-ghost:hover { - @apply bg-primary-100 text-primary-600 border-primary-400; -} - -.btn-primary-ghost:focus { - @apply bg-primary-800 text-white border-primary-400; -} - -.btn-primary { - @apply bg-primary-500 text-white; -} - -.btn-primary:hover { - @apply bg-primary-600; -} - -.btn-primary:focus { - @apply bg-primary-800; - background-image: none; -} - -.btn-warning { - @apply bg-yellow-500 text-white; - background-image: linear-gradient( 135deg, rgba(237, 137, 54, 1) 0%, rgba(221, 107, 32, 1) 100%); -} - -.btn-warning:hover { - @apply bg-yellow-600; - background-image: linear-gradient( 135deg, rgba(221, 107, 32, 1) 0%, rgba(192, 86, 33, 1) 100%); -} - -.btn-warning:focus { - @apply bg-yellow-800; - background-image: none; -} - -.btn-danger { - @apply bg-red-500 text-white; - background-image: linear-gradient( 135deg, rgba(245, 101, 101, 1) 0%, rgba(229, 62, 62, 1) 100%); -} - -.btn-danger:hover { - @apply bg-red-600; - background-image: linear-gradient( 135deg, rgba(229, 62, 62, 1) 0%, rgba(197, 48, 48, 1) 100%); -} - -.btn-danger:focus { - @apply bg-red-800; - background-image: none; -} - - -.primary-button { - @apply focus:outline-none text-white bg-primary-500 hover:bg-primary-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-6 py-2.5 mr-2 mb-2 transition-all ease-in-out duration-200 !important; -} - -.secondary-button { - @apply text-secondary-900 bg-white border border-secondary-300 focus:outline-none hover:bg-secondary-100 focus:ring-4 focus:ring-secondary-200 font-medium rounded-lg text-sm px-6 py-2.5 mr-2 mb-2 transition-all ease-in-out duration-200 !important; -} - -button:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-300 text-secondary-500 shadow-none border-transparent; - background-image: none; -} - -button:disabled:hover, -.disabled:hover, -button:disabled:focus, -.disabled:focus { - @apply bg-secondary-300 text-secondary-500 border-transparent shadow-none; - background-image: none; -} - -.btn-small { - @apply py-1 px-3 text-xs h-7 leading-normal; -} - -.btn-normal { - @apply py-1 px-4 text-sm h-8; -} - -.btn-large { - @apply py-2 px-5 text-base h-10; -} - -.button-xl { - @apply py-2 px-6 text-base h-12; -} - -@tailwind utilities; -.multiselect-dropdown__search-dropdown { - max-height: 20rem; - overflow: auto; -} - -.max-height-dropdown { - max-height: 20rem; - overflow: auto; -} - - -/* Radio */ - -.radio-label { - @apply select-none cursor-pointer; -} - -.radio-label span { - @apply inline-block align-middle; - transform: translate3d(0, 0, 0); -} - -.radio-label span:first-child { - @apply relative rounded-full align-middle border border-secondary-500 bg-white mr-3; - width: 1.125rem; - height: 1.125rem; - transform: scale(1); - transition: all 0.2s ease; -} - -.radio-label span:first-child svg { - position: absolute; - top: 0; - left: 0; - fill: none; - stroke: #ffffff; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - transition: all 0.3s ease; - transition-delay: 0.1s; -} - -.radio-label span:first-child:before { - content: ""; - @apply bg-primary-500 w-full h-full block opacity-100 rounded-full; - transform: scale(0); -} - -.radio-label:hover span:first-child { - @apply bg-primary-100 border-primary-500; -} - -.radio-input:checked+.radio-label span:first-child { - @apply bg-primary-500 border-primary-500; - animation: wave 0.4s ease; -} - -.radio-input:checked+.radio-label span:first-child:before { - transform: scale(3.5); - opacity: 0; - transition: all 0.6s ease; -} - - -/* Checkbox */ - -.checkbox__label { - @apply select-none cursor-pointer; -} - -.checkbox__label span { - @apply inline-block align-middle; - transform: translate3d(0, 0, 0); -} - -.checkbox__label span:first-child { - @apply relative rounded align-middle border border-secondary-500 bg-white w-4 h-4 mr-3; - transform: scale(1); - transition: all 0.2s ease; -} - -.checkbox__label span:first-child svg { - position: absolute; - top: 3px; - left: 2px; - fill: none; - stroke: #ffffff; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - stroke-dasharray: 16px; - stroke-dashoffset: 16px; - transition: all 0.3s ease; - transition-delay: 0.1s; - transform: translate3d(0, 0, 0); -} - -.checkbox__label span:first-child:before { - content: ""; - @apply bg-primary-500 w-full h-full block opacity-100 rounded-full; - transform: scale(0); -} - -.checkbox__label:hover span:first-child { - @apply bg-primary-100 border-primary-500; -} - -.checkbox__input:checked+.checkbox__label span:first-child { - @apply bg-primary-500 border-primary-500; - animation: wave 0.4s ease; -} - -.checkbox__input:checked+.checkbox__label span:first-child svg { - stroke-dashoffset: 0; -} - -.checkbox__input:checked+.checkbox__label span:first-child:before { - transform: scale(3.5); - opacity: 0; - transition: all 0.6s ease; -} - @keyframes wave { 50% { transform: scale(0.9); } } -label { - @apply block text-secondary-700 text-sm font-medium; -} - -input { - @apply border-secondary-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 transition-colors duration-300 !important; -} - -input:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-200 border-secondary-400 text-secondary-700; -} - -textarea { - @apply border-secondary-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 transition-colors duration-300; -} - -button:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-300 text-secondary-500 shadow-none border-transparent; - background-image: none; -} - - -/* Styling skeleton loading */ - -.skeleton-placeholder__line-sm { - @apply rounded-full bg-secondary-100; - height: 0.625rem; -} - -.skeleton-placeholder__line-md { - @apply rounded-full bg-secondary-100; - height: 1rem; -} - -.skeleton-placeholder__image { - @apply mt-5 h-48 rounded-lg bg-secondary-100; -} - -.skeleton-animate { - animation: skeletonShimmer 3s infinite linear; - background: linear-gradient(to right, #f5f3f7 10%, #fbfafc 40%, #f5f3f7 70%); - background-size: 1000px 100%; -} - -@keyframes skeletonShimmer { - 0% { - background-position: -1000px 0; - } - 100% { - background-position: 1000px 0; - } -} - -.skeleton-animate-alpha { - animation: skeletonShimmer 3s infinite linear; - background: linear-gradient(to right, rgba(0, 0, 0, 0.1) 10%, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.1) 70%); - background-size: 1000px 100%; -} @media print { @page { @@ -449,20 +82,6 @@ button:disabled, } } -.header-section .appBar { - z-index: 1201; -} - -.header-section .toolbar { - padding-top: 72px; -} - -@media only screen and (min-width: 601px) { - .header-section .menuButton { - display: none; - } -} - .App { text-align: center; } @@ -497,87 +116,10 @@ button:disabled, 100% {opacity: 0;} } -.badge { - display: inline-block; - padding: 0.25em 0.4em; - font-size: 12px; - font-weight: 700; - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: 0.25rem; - transition: color 0.15s; -} - -.badge-pill { - padding-right: 0.6em; - padding-left: 0.6em; - border-radius: 10rem; -} - -.badge-primary { - color: #fff; - background-color: #007bff; -} - -.badge-secondary { - color: #fff; - background-color: #6c757d; -} - -.badge-success { - color: #fff; - background-color: #28a745; -} - -.badge-warning { - color: #212529; - background-color: #ffc107; -} - -.badge-danger { - color: #fff; - background-color: #dc3545; -} - -.badge-dark { - color: #fff; - background-color: #343a40; -} - .dropdown:hover .dropdown-menu { display: block; } -.border-primary { - border-color: #28a745; -} - -.form-input { - padding-top: 6px; - padding-bottom: 6px; - width: 100%; - border-radius: 4px; - height: 38px; -} - -.form-input:focus { - border-color: #28a745 !important; - box-shadow: 0 0 0 1px #28a745 !important; -} - -.copied-to-cb { - color: #28a745; - font-size: 13px; - padding-left: 5px; - font-style: italic; -} - -.copy-to-cb { - padding-left: 5px; -} - ::-webkit-scrollbar { width: 10px; height: 8px; @@ -596,166 +138,6 @@ button:disabled, background: rgba(0, 0, 0, 0.4); } -@media (min-width:1000px) { - .manualGrid { - display: grid !important - } -} - -.scrollbar-hide::-webkit-scrollbar { - width: 0px; - height: 0px; -} - -/* - Range Sliders for Camera Feed -*/ - -#feed-range { - -webkit-appearance: none; - appearance: none; - margin: 18px 0; - width: 100%; -} - -#feed-range:focus { - outline: none; -} - -#feed-range::-webkit-slider-runnable-track { - width: 100%; - height: 1px; - cursor: pointer; - background: transparent; - border-radius: 5px; -} - -#feed-range::-webkit-slider-thumb { - height: 16px; - width: 16px; - border-radius: 3px; - border: 1px solid white; - background: black; - cursor: pointer; - -webkit-appearance: none; - margin-top: -8px; - border-radius: 50%; -} - -@media (min-width:471px) { - .csv-input { - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin: auto; - width: 50%; - } -} - -@media (max-width:470px) { - .csv-input { - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin: auto; - width: 100%; - } -} - -[type="checkbox"] { - @apply text-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none; -} - - - -.csv-input::file-selector-button:hover { - background-color: #84e1bc; -} - - - -/* for gmaps search dropdown */ -.pac-container { - z-index: 100000 !important; -} - -.csv-input::file-selector-button { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; - font-weight: 600; - border-width: 0; - background-color: #def7ec; - color: #046c4e; -} - -.csv-input::file-selector-button:hover { - background-color: #84e1bc; -} - -/* Patient Category Styles */ - -.patient-comfort { - @apply bg-patient-comfort text-patient-comfort-fore -} -.patient-stable { - @apply bg-patient-stable text-patient-stable-fore -} -.patient-abnormal { - @apply bg-patient-abnormal text-patient-abnormal-fore -} -.patient-critical { - @apply bg-patient-critical text-patient-critical-fore -} -.patient-unknown { - @apply bg-patient-unknown text-patient-unknown-fore -} - -.patient-activelydying { - @apply bg-patient-activelydying text-patient-activelydying-fore -} - -.patient-comfort-ring { - @apply ring-patient-comfort -} -.patient-stable-ring { - @apply ring-patient-stable -} -.patient-abnormal-ring { - @apply ring-patient-abnormal -} -.patient-critical-ring { - @apply ring-patient-critical -} -.patient-unknown-ring { - @apply ring-patient-unknown -} -.patient-activelydying-ring { - @apply ring-patient-activelydying -} - -.patient-comfort-profile { - @apply border-2 border-patient-comfort rounded-t -} -.patient-stable-profile { - @apply border-2 border-patient-stable rounded-t -} -.patient-abnormal-profile { - @apply border-2 border-patient-abnormal rounded-t -} -.patient-critical-profile { - @apply border-2 border-patient-critical rounded-t -} -.patient-unknown-profile { - @apply border border-patient-unknown rounded -} -.patient-activelydying-profile { - @apply border-2 border-patient-activelydying rounded-t -} - /* for gmaps search dropdown */ .pac-container { z-index: 100000 !important; @@ -855,468 +237,6 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-decoration { display: none; } -.service-panel{ - @apply flex-row -} - -@media screen and (max-width: 920px) { - .service-panel{ - @apply flex-col - } -} -/* styles for files in src/Components/CriticalCareRecording/PressureSore */ - -input[type="checkbox"]:checked~.dot { - transform: translateX(100%); -} - - -/* styles for files in src/Components/CriticalCareRecording/components */ - -.range { - position: relative; - width: 644px; - height: 5px; -} - -.range input { - width: 100%; - position: absolute; - top: 2px; - height: 0; - -webkit-appearance: none; -} - -.range input::-webkit-slider-thumb { - -webkit-appearance: none; - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-moz-range-thumb { - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-ms-thumb { - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-webkit-slider-runnable-track { - width: 100%; - height: 2px; - cursor: pointer; - background: #b2b2b2; - z-index: 1; -} - -.range input::-moz-range-track { - width: 100%; - height: 2px; - cursor: pointer; - background: black; - z-index: 1; -} - -.range input::-ms-track { - width: 100%; - height: 2px; - cursor: pointer; - background: #b2b2b2; - z-index: 1; -} - -.range input:focus { - background: none; - outline: none; -} - -.range input::-ms-track { - width: 100%; - cursor: pointer; - background: transparent; - border-color: transparent; - color: transparent; -} - -.range-labels { - margin: 18px -41px 0; - padding: 0; - list-style: none; -} - -.range-labels li { - position: relative; - float: left; - width: 90.25px; - text-align: center; - color: black; - font-size: 14px; - cursor: pointer; -} - -.range-labels .label::before { - position: absolute; - top: -25px; - right: 0; - left: 0; - content: ""; - margin: 0 auto; - width: 9px; - height: 9px; - background: #b2b2b2; - border-radius: 50%; -} - -.double-range-labels { - margin: 18px -41px 0; - padding: 0; - list-style: none; -} - -.double-range-labels li { - position: relative; - float: left; - width: 45.25px; - text-align: center; - color: black; - font-size: 14px; - cursor: pointer; -} - -.double-range-labels .label::before { - position: absolute; - top: -25px; - right: 0; - left: 0; - content: ""; - margin: 0 auto; - width: 9px; - height: 9px; - background: #b2b2b2; - border-radius: 50%; -} - -.upper-label { - height: 10px; - margin-bottom: 10px; -} - -.pupil1 { - bottom: 0; - margin: 0 auto; - width: 9px; - height: 9px; - border-radius: 50%; - background-color: black; -} - -.pupil2 { - bottom: 0; - margin: 0 auto; - width: 15px; - height: 15px; - border-radius: 50%; - background-color: black; -} - -.pupil3 { - bottom: 0; - margin: 0 auto; - width: 21px; - height: 21px; - border-radius: 50%; - background-color: black; -} - -.pupil4 { - bottom: 0; - margin: 0 auto; - width: 25px; - height: 25px; - border-radius: 50%; - background-color: black; -} - -.pupil5 { - bottom: 0; - margin: 0 auto; - width: 30px; - height: 30px; - border-radius: 50%; - background-color: black; -} - -.pupil6 { - bottom: 0; - margin: 0 auto; - width: 35px; - height: 35px; - border-radius: 50%; - background-color: black; -} - -.pupil7 { - bottom: 0; - margin: 0 auto; - width: 38px; - height: 38px; - border-radius: 50%; - background-color: black; -} - -.pupil8 { - width: 41px; - height: 41px; - border-radius: 50%; - background-color: black; -} - -.range-labels .active { - color: rgb(7, 109, 243); -} - -.range-labels .selected::before { - background: rgb(7, 109, 243); -} - -.range-labels .active.selected::before { - display: none; -} - -.align-circles { - position: relative; - display: flex; - justify-content: center; - height: 41px; - vertical-align: baseline; -} - -input[type="number"] { - background-color: #fbfafc; -} - - -/* Copied CSS */ - -.slider { - position: relative; - width: 200px; -} - -.slider__track, -.slider__range, -.slider__left-value, -.slider__right-value { - position: absolute; -} - -.slider__track, -.slider__range { - border-radius: 3px; - height: 5px; -} - -.slider__track { - background-color: #1476d8; - width: 100%; - z-index: 1; -} - -.slider__range { - background-color: #19ebe0; - z-index: 2; -} - -.slider__left-value, -.slider__right-value { - color: #1c85ee; - font-size: 12px; - margin-top: 20px; -} - -.slider__left-value { - left: 6px; -} - -.slider__right-value { - right: -4px; -} - - -/* Removing the default appearance */ - -.thumb input { - pointer-events: none; - position: absolute; - height: 0; - width: 200px; - outline: none; -} - -.thumb--left { - z-index: 3; -} - -.thumb--right { - z-index: 4; -} - - -/* For Chrome browsers */ - -.thumb input::-webkit-slider-thumb { - background-color: #242525; - border: none; - border-radius: 50%; - box-shadow: 0 0 1px 1px #0b2846; - cursor: pointer; - height: 18px; - width: 18px; - margin-top: 4px; - pointer-events: all; - position: relative; -} - - -/* For Firefox browsers */ - -.thumb input::-moz-range-thumb { - background-color: #272b2c; - border: none; - border-radius: 50%; - box-shadow: 0 0 1px 1px #0d2d4d; - cursor: pointer; - height: 18px; - width: 18px; - margin-top: 4px; - pointer-events: all; - position: relative; -} - -.slider { - appearance: none; - background-color: #dee0e4; - width: 100%; - height: 4px; - margin: 0; - padding: 0; - z-index: 2; -} - -.slider::-webkit-slider-thumb { - cursor: pointer; -} - -.slider-container { - position: relative; - width: 100%; - /* max-width: 700px; */ - display: flex; - justify-content: center; -} - -.indicators { - position: absolute; - top: -200%; - width: 100%; - height: 20px; - border-left: 2px solid #dee0e4; -} - -.tick { - content: ""; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: linear-gradient(to left, #dee0e4 2px, transparent 2px); -} - -.slider-box { - width: 100%; - /* max-width: 700px; */ - padding: 20px 10px 60px 10px; - border-radius: 12px; -} - -.slider-head { - font-family: sans-serif; - margin-bottom: 2rem; -} - -.slider-head>div>h1 { - font-size: 1rem; - font-weight: bold; -} - -.slider-head input { - padding: 0.5rem 0.7rem; - border-radius: 8px; - border: 1px solid #979aa0; - margin-left: 12px; - font-size: 1.1rem; - color: #383a3e; - letter-spacing: 1px; -} - -.slider-head label { - font-weight: 700; - color: #2856ff; - font-size: 1rem; -} - - -.grid-2-col, .grid-3-col, .grid-2-1-col, .grid-3-1-col, .grid-1-2-col { - display: grid; - grid-template-columns: 1fr; - gap: 1.5rem; -} - -@media (min-width: 768px) { - .grid-2-col { - grid-template-columns: repeat(2, 1fr); - } - .grid-3-col { - grid-template-columns: repeat(3, 1fr); - } - .grid-2-1-col { - grid-template-columns: 2fr 1fr; - } - .grid-1-2-col { - grid-template-columns: 1fr 2fr; - } -} - -.input-field-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} -.col-span-all { - grid-column: 1 / -1; -} - @layer base { :root { --radius: 0.5rem; diff --git a/src/types/emr/encounter.ts b/src/types/emr/encounter.ts index f63a118a83f..45c6602b763 100644 --- a/src/types/emr/encounter.ts +++ b/src/types/emr/encounter.ts @@ -117,6 +117,12 @@ export type StatusHistory = { history: History[]; }; +export type LocationHistory = { + start_datetime: string; + location: LocationList; + status: string; +}; + export interface Encounter { id: string; patient: Patient; @@ -138,6 +144,7 @@ export interface Encounter { status_history: StatusHistory; organizations: FacilityOrganization[]; current_location: LocationList; + location_history: LocationHistory[]; } export interface EncounterEditRequest { diff --git a/src/types/emr/medicationAdministration/medicationAdministration.ts b/src/types/emr/medicationAdministration/medicationAdministration.ts index a736846de4a..caf6f254711 100644 --- a/src/types/emr/medicationAdministration/medicationAdministration.ts +++ b/src/types/emr/medicationAdministration/medicationAdministration.ts @@ -11,7 +11,6 @@ export const MEDICATION_ADMINISTRATION_STATUS = [ "stopped", "in_progress", "on_hold", - "unknown", "cancelled", ] as const; @@ -93,4 +92,6 @@ export interface MedicationAdministrationRead { dose?: DosageQuantity; rate?: Quantity; }; + created_by: UserBareMinimum; + updated_by: UserBareMinimum; } diff --git a/src/types/emr/medicationAdministration/medicationAdministrationApi.ts b/src/types/emr/medicationAdministration/medicationAdministrationApi.ts index 77e37c391e2..35b980b0349 100644 --- a/src/types/emr/medicationAdministration/medicationAdministrationApi.ts +++ b/src/types/emr/medicationAdministration/medicationAdministrationApi.ts @@ -7,12 +7,12 @@ import { } from "./medicationAdministration"; export default { - listMedicationAdministrations: { + list: { path: "/api/v1/patient/{patientId}/medication/administration/", method: HttpMethod.GET, TRes: Type>(), }, - upsertMedicationAdministration: { + upsert: { path: "/api/v1/patient/{patientId}/medication/administration/upsert/", method: HttpMethod.POST, TRes: Type, diff --git a/src/types/emr/medicationStatement.ts b/src/types/emr/medicationStatement.ts index a357ddab63c..00e20ae150a 100644 --- a/src/types/emr/medicationStatement.ts +++ b/src/types/emr/medicationStatement.ts @@ -23,6 +23,17 @@ export const MEDICATION_STATEMENT_STATUS = [ export type MedicationStatementStatus = (typeof MEDICATION_STATEMENT_STATUS)[number]; +export const MEDICATION_STATEMENT_STATUS_STYLES = { + active: "bg-green-100 text-green-800 border-green-200", + completed: "bg-blue-100 text-blue-800 border-blue-200", + stopped: "bg-red-100 text-red-800 border-red-200", + on_hold: "bg-yellow-100 text-yellow-800 border-yellow-200", + intended: "bg-purple-100 text-purple-800 border-purple-200", + not_taken: "bg-gray-100 text-gray-800 border-gray-200", + unknown: "bg-gray-100 text-gray-800 border-gray-200", + entered_in_error: "bg-red-100 text-red-800 border-red-200", +} as const; + export type MedicationStatement = { readonly id: string; status: MedicationStatementStatus; diff --git a/src/types/emr/medicationStatement/medicationStatementApi.ts b/src/types/emr/medicationStatement/medicationStatementApi.ts index 68736e4ab13..7f65dc5f3c8 100644 --- a/src/types/emr/medicationStatement/medicationStatementApi.ts +++ b/src/types/emr/medicationStatement/medicationStatementApi.ts @@ -1,12 +1,12 @@ import { Type } from "@/Utils/request/api"; import { PaginatedResponse } from "@/Utils/request/types"; -import { MedicationStatement } from "@/types/emr/medicationStatement"; +import { MedicationStatementRead } from "@/types/emr/medicationStatement"; const medicationStatementApi = { list: { path: "/api/v1/patient/{patientId}/medication/statement/", method: "GET", - TRes: Type>(), + TRes: Type>(), }, } as const; diff --git a/src/types/location/location.ts b/src/types/location/location.ts index cfb16dd13c3..3738dce63f7 100644 --- a/src/types/location/location.ts +++ b/src/types/location/location.ts @@ -45,6 +45,7 @@ export interface LocationDetail extends LocationBase { export interface LocationList extends LocationBase { id: string; has_children: boolean; + parent?: LocationList; } export interface LocationWrite extends LocationBase { diff --git a/src/types/questionnaire/question.ts b/src/types/questionnaire/question.ts index cc16b086bc7..f0199d63890 100644 --- a/src/types/questionnaire/question.ts +++ b/src/types/questionnaire/question.ts @@ -16,6 +16,23 @@ export type QuestionType = | "quantity" | "structured"; +export const SUPPORTED_QUESTION_TYPES = [ + { name: "Group", value: "group" }, + { name: "Display", value: "display" }, + { name: "Boolean", value: "boolean" }, + { name: "Decimal", value: "decimal" }, + { name: "Integer", value: "integer" }, + { name: "Date", value: "date" }, + { name: "DateTime", value: "dateTime" }, + { name: "Time", value: "time" }, + { name: "String", value: "string" }, + { name: "Text", value: "text" }, + { name: "URL", value: "url" }, + { name: "Choice", value: "choice" }, + { name: "Quantity", value: "quantity" }, + { name: "Structured", value: "structured" }, +]; + export type StructuredQuestionType = | "allergy_intolerance" | "medication_request" @@ -49,6 +66,13 @@ export interface AnswerOption { value: string; display?: string; initialSelected?: boolean; + code?: Code; +} + +export interface ObservationType { + system: string; + code: string; + display: string; } export interface Question { diff --git a/src/types/resourceRequest/resourceRequest.ts b/src/types/resourceRequest/resourceRequest.ts index 7594766017c..07bfdbd2479 100644 --- a/src/types/resourceRequest/resourceRequest.ts +++ b/src/types/resourceRequest/resourceRequest.ts @@ -2,6 +2,7 @@ import { FacilityModel } from "@/components/Facility/models"; import { UserBareMinimum } from "@/components/Users/models"; import { PatientModel } from "@/types/emr/patient"; +import { UserBase } from "@/types/user/user"; export interface ResourceRequest { approving_facility: FacilityModel | null; @@ -17,9 +18,9 @@ export interface ResourceRequest { requested_quantity: number; status: string; title: string; - assigned_to: UserBareMinimum | null; - created_by: UserBareMinimum; - updated_by: UserBareMinimum; + assigned_to: UserBase | null; + created_by: UserBase; + updated_by: UserBase; created_date: string; modified_date: string; related_patient: PatientModel | null; @@ -32,6 +33,7 @@ export interface CreateResourceRequest { referring_facility_contact_name: string; referring_facility_contact_number: string; approving_facility: string | null; + assigned_to: string | null; assigned_facility: string | null; origin_facility: string; related_patient: string; diff --git a/src/types/scheduling/scheduleApis.ts b/src/types/scheduling/scheduleApi.ts similarity index 100% rename from src/types/scheduling/scheduleApis.ts rename to src/types/scheduling/scheduleApi.ts diff --git a/src/types/user/userApi.ts b/src/types/user/userApi.ts index f1f776cece0..dd11f7e2ae2 100644 --- a/src/types/user/userApi.ts +++ b/src/types/user/userApi.ts @@ -30,4 +30,4 @@ export default { TRes: Type(), TBody: Type>(), }, -}; +} as const; diff --git a/src/types/valueset/valueset.ts b/src/types/valueset/valueset.ts new file mode 100644 index 00000000000..01ad2e9f8c1 --- /dev/null +++ b/src/types/valueset/valueset.ts @@ -0,0 +1,70 @@ +export interface ValuesetFilter { + op: string; + value: string; + property: string; +} + +export interface ValuesetConcept { + code: string; + display: string; +} + +export interface ValuesetInclude { + filter?: ValuesetFilter[]; + system: string; + concept?: ValuesetConcept[]; +} + +interface ValuesetCompose { + exclude: ValuesetInclude[]; + include: ValuesetInclude[]; +} + +export interface ValuesetBase { + id: string; + slug: string; + name: string; + description: string; + compose: ValuesetCompose; + status: "active" | "inactive"; + is_system_defined: boolean; + created_by: string | null; + updated_by: string | null; +} + +export type CreateValuesetModel = Omit< + ValuesetBase, + "id" | "created_by" | "updated_by" +>; + +export type UpdateValuesetModel = CreateValuesetModel & { + id: string; +}; + +export type ValuesetFormType = CreateValuesetModel; + +export interface ValuesetCodeMetadata { + code: string; + display: string; + name: string; + system: string; + version: string; + inactive: boolean; +} + +export interface ValuesetLookupResponse { + metadata: ValuesetCodeMetadata; +} + +export interface ValuesetLookupRequest { + system: string; + code: string; +} + +export const TERMINOLOGY_SYSTEMS = { + LOINC: "http://loinc.org", + SNOMED: "http://snomed.info/sct", + UCUM: "http://unitsofmeasure.org", +} as const; + +export type TerminologySystem = keyof typeof TERMINOLOGY_SYSTEMS; diff --git a/src/types/valueset/valuesetApi.ts b/src/types/valueset/valuesetApi.ts new file mode 100644 index 00000000000..3398cd18432 --- /dev/null +++ b/src/types/valueset/valuesetApi.ts @@ -0,0 +1,49 @@ +import { HttpMethod, Type } from "@/Utils/request/api"; +import { PaginatedResponse } from "@/Utils/request/types"; + +import { + CreateValuesetModel, + UpdateValuesetModel, + ValuesetBase, + ValuesetLookupRequest, + ValuesetLookupResponse, +} from "./valueset"; + +export default { + list: { + path: "/api/v1/valueset/", + method: HttpMethod.GET, + TRes: Type>(), + }, + create: { + path: "/api/v1/valueset/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, + get: { + path: "/api/v1/valueset/{slug}/", + method: HttpMethod.GET, + TRes: Type(), + }, + update: { + path: "/api/v1/valueset/{slug}/", + method: HttpMethod.PUT, + TRes: Type(), + TBody: Type(), + }, + lookup: { + path: "/api/v1/valueset/lookup_code/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, + expand: { + path: "/api/v1/valueset/expand/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type<{ + search: string; + }>(), + }, +} as const; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index defba258b80..5e2066155a8 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -19,11 +19,7 @@ interface ImportMetaEnv { readonly REACT_CUSTOM_LOGO_ALT?: string; readonly REACT_CUSTOM_DESCRIPTION?: string; readonly REACT_GMAPS_API_KEY?: string; - readonly REACT_GOV_DATA_API_KEY?: string; readonly REACT_RECAPTCHA_SITE_KEY?: string; - readonly REACT_WARTIME_SHIFTING?: string; - readonly REACT_STILL_WATCHING_IDLE_TIMEOUT?: string; - readonly REACT_STILL_WATCHING_PROMPT_DURATION?: string; readonly REACT_JWT_TOKEN_REFRESH_INTERVAL?: string; readonly REACT_MIN_ENCOUNTER_DATE?: string; readonly REACT_DEFAULT_ENCOUNTER_TYPE?: string; diff --git a/vite.config.mts b/vite.config.mts index 749f188002c..308c942b7ad 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -288,6 +288,7 @@ export default defineConfig(({ mode }) => { server: { port: 4000, host: "0.0.0.0", + allowedHosts: true, }, preview: { headers: {
- {hintMessage} -
+ {hintMessage} +
- {dragProps.fileDropError !== "" - ? dragProps.fileDropError - : `${t("drag_drop_image_to_upload")}`} -
- {t("no_image_found")}. {hintMessage} -
+ {dragProps.fileDropError !== "" + ? dragProps.fileDropError + : `${t("drag_drop_image_to_upload")}`} +
+ {t("no_image_found")}. {hintMessage} +
+ {JSON.stringify(data, null, 2)} +
- {description} -
- {fileNameTooltip} -
- {fileName} + !open && handleClose()}> + + + + {t("file_preview")} + + + + {fileUrl ? ( + <> + + + + + + + {fileNameTooltip} + + + + + {fileName} + + + + + {uploadedFiles && + uploadedFiles[index] && + uploadedFiles[index].created_date && ( + + {t("created_on")}{" "} + {new Date( + uploadedFiles[index].created_date!, + ).toLocaleString("en-US", { + dateStyle: "long", + timeStyle: "short", + })} - - - - {uploadedFiles && - uploadedFiles[index] && - uploadedFiles[index].created_date && ( - - {t("created_on")}{" "} - {new Date( - uploadedFiles[index].created_date!, - ).toLocaleString("en-US", { - dateStyle: "long", - timeStyle: "short", - })} - + )} + + + {downloadURL && downloadURL.length > 0 && ( + + + + {t("download")} + + )} + + {t("close")} + + - - {downloadURL && downloadURL.length > 0 && ( - - - - {t("download")} - + + {uploadedFiles && uploadedFiles.length > 1 && ( + handleNext(index - 1)} + disabled={index <= 0} + aria-label="Previous file" + > + )} - - {t("close")} - - - - - {uploadedFiles && uploadedFiles.length > 1 && ( - handleNext(index - 1)} - disabled={index <= 0} - aria-label="Previous file" - > - - - )} - - {file_state.isImage ? ( - - ) : file_state.extension === "pdf" ? ( - }> - { - setPage(1); - setNumPages(numPages); - }} - pageNumber={page} - scale={scale} + + {file_state.isImage ? ( + - - ) : previewExtensions.includes(file_state.extension) ? ( - - ) : ( - - }> + { + setPage(1); + setNumPages(numPages); + }} + pageNumber={page} + scale={scale} + /> + + ) : previewExtensions.includes(file_state.extension) ? ( + - {t("file_preview_not_supported")} - - )} - + ) : ( + + + {t("file_preview_not_supported")} + + )} + - {uploadedFiles && uploadedFiles.length > 1 && ( - handleNext(index + 1)} - disabled={index >= uploadedFiles.length - 1} - aria-label="Next file" - > - - - )} - - - - {file_state.isImage && ( - <> - {[ - [ - t("Zoom In"), - "l-search-plus", - handleZoomIn, - file_state.zoom === zoom_values.length, - ], - [ - `${25 * file_state.zoom}%`, - false, - () => { - setFileState({ ...file_state, zoom: 4 }); - }, - false, - ], - [ - t("Zoom Out"), - "l-search-minus", - handleZoomOut, - file_state.zoom === 1, - ], - [ - t("Rotate Left"), - "l-corner-up-left", - () => handleRotate(-90), - false, - ], - [ - t("Rotate Right"), - "l-corner-up-right", - () => handleRotate(90), - false, - ], - ].map((button, index) => ( - void} - className="z-50 rounded bg-white/60 px-4 py-2 text-black backdrop-blur transition hover:bg-white/70" - disabled={button[3] as boolean} - > - {button[1] && ( - - )} - {button[0] as string} - - ))} - > - )} - {file_state.extension === "pdf" && ( - <> - {[ - ["Zoom In", "l-search-plus", handleZoomIn, scale >= 2], - [`${Math.round(scale * 100)}%`, false, () => {}, false], - ["Zoom Out", "l-search-minus", handleZoomOut, scale <= 0.5], - [ - "Previous", - "l-arrow-left", - () => setPage((prev) => prev - 1), - page === 1, - ], - [`${page}/${numPages}`, false, () => ({}), false], - [ - "Next", - "l-arrow-right", - () => setPage((prev) => prev + 1), - page === numPages, - ], - ].map((button, index) => ( - void} - className="z-50 rounded bg-white/60 px-4 py-2 text-black backdrop-blur transition hover:bg-white/70" - disabled={button[3] as boolean} - > - {button[1] && ( - - )} - {button[0] as string} - - ))} - > + {uploadedFiles && uploadedFiles.length > 1 && ( + handleNext(index + 1)} + disabled={index >= uploadedFiles.length - 1} + aria-label={t("next_file")} + > + + )} + + + {file_state.isImage && ( + <> + {[ + [ + t("zoom_in"), + "l-search-plus", + handleZoomIn, + file_state.zoom === zoom_values.length, + ], + [ + `${25 * file_state.zoom}%`, + false, + () => { + setFileState({ ...file_state, zoom: 4 }); + }, + false, + ], + [ + t("zoom_out"), + "l-search-minus", + handleZoomOut, + file_state.zoom === 1, + ], + [ + t("rotate_left"), + "l-corner-up-left", + () => handleRotate(-90), + false, + ], + [ + t("rotate_right"), + "l-corner-up-right", + () => handleRotate(90), + false, + ], + ].map((button, index) => ( + void} + className="z-50 rounded bg-white/60 px-4 py-2 text-black backdrop-blur transition hover:bg-white/70" + disabled={button[3] as boolean} + > + {button[1] && ( + + )} + {button[0] as string} + + ))} + > + )} + {file_state.extension === "pdf" && ( + <> + {[ + [t("zoom_in"), "l-search-plus", handleZoomIn, scale >= 2], + [`${Math.round(scale * 100)}%`, false, () => {}, false], + [ + t("zoom_out"), + "l-search-minus", + handleZoomOut, + scale <= 0.5, + ], + [ + t("previous"), + "l-arrow-left", + () => setPage((prev) => prev - 1), + page === 1, + ], + [`${page}/${numPages}`, false, () => ({}), false], + [ + t("next"), + "l-arrow-right", + () => setPage((prev) => prev + 1), + page === numPages, + ], + ].map((button, index) => ( + void} + className="z-50 rounded bg-white/60 px-4 py-2 text-black backdrop-blur transition hover:bg-white/70" + disabled={button[3] as boolean} + > + {button[1] && ( + + )} + {button[0] as string} + + ))} + > + )} + + + > + ) : ( + + - > - ) : ( - - - - )} - + )} + + ); }; diff --git a/src/components/Common/HelperComponents.tsx b/src/components/Common/HelperComponents.tsx deleted file mode 100644 index 5100476284e..00000000000 --- a/src/components/Common/HelperComponents.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Transition, TransitionEvents } from "@headlessui/react"; -import { ReactNode } from "react"; - -type DropdownMenuTransitionProps = { - show?: boolean | undefined; - children: ReactNode; -} & TransitionEvents; - -export const DropdownTransition = ({ - show, - children, - ...transitionEvents -}: DropdownMenuTransitionProps) => ( - - {children} - -); diff --git a/src/components/Common/LanguageSelector.tsx b/src/components/Common/LanguageSelector.tsx index e1f3446eca0..a3b9d5c3d5d 100644 --- a/src/components/Common/LanguageSelector.tsx +++ b/src/components/Common/LanguageSelector.tsx @@ -2,7 +2,9 @@ import careConfig from "@careConfig"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { classNames, keysOf } from "@/Utils/utils"; +import { cn } from "@/lib/utils"; + +import { keysOf } from "@/Utils/utils"; import { LANGUAGES } from "@/i18n"; export const LanguageSelector = (props: any) => { @@ -27,7 +29,7 @@ export const LanguageSelector = (props: any) => { return ( { @@ -32,7 +34,7 @@ export const LanguageSelectorLogin = () => { handleLanguage(e)} - className={classNames( + className={cn( "text-primary-400 hover:text-primary-600", (i18n.language === e || (i18n.language === "en-US" && e === "en")) && diff --git a/src/components/Common/Page.tsx b/src/components/Common/Page.tsx index 24b797d245f..b9079c5089e 100644 --- a/src/components/Common/Page.tsx +++ b/src/components/Common/Page.tsx @@ -33,7 +33,7 @@ export default function Page(props: PageProps) { let padding = ""; if (!props.noImplicitPadding) { - if (!props.hideBack || props.componentRight) padding = "py-0 md:px-6"; + if (props.componentRight) padding = "py-0 md:px-6"; else padding = "px-6 py-0"; } @@ -43,13 +43,8 @@ export default function Page(props: PageProps) { diff --git a/src/components/Common/PageTitle.tsx b/src/components/Common/PageTitle.tsx index cee87a8c5fb..c374124934d 100644 --- a/src/components/Common/PageTitle.tsx +++ b/src/components/Common/PageTitle.tsx @@ -1,46 +1,26 @@ import { ReactNode, useEffect, useRef } from "react"; -import Breadcrumbs from "@/components/Common/Breadcrumbs"; -import PageHeadTitle from "@/components/Common/PageHeadTitle"; +import { cn } from "@/lib/utils"; -import { classNames } from "@/Utils/utils"; +import PageHeadTitle from "@/components/Common/PageHeadTitle"; export interface PageTitleProps { title: string; className?: string; componentRight?: ReactNode; - breadcrumbs?: boolean; - crumbsReplacements?: { - [key: string]: { - name?: string; - uri?: string; - style?: string; - hide?: boolean; - }; - }; focusOnLoad?: boolean; isInsidePage?: boolean; changePageMetadata?: boolean; - // New props for Breadcrumbs - hideBack?: boolean; - backUrl?: string; hideTitleOnPage?: boolean; - onBackClick?: () => boolean | void; } export default function PageTitle({ title, className = "", componentRight = <>>, - breadcrumbs = true, - crumbsReplacements = {}, focusOnLoad = false, isInsidePage = false, changePageMetadata = true, - // New props passed to Breadcrumbs - hideBack = false, - backUrl, - onBackClick, hideTitleOnPage, }: PageTitleProps) { const divRef = useRef(); @@ -54,23 +34,12 @@ export default function PageTitle({ return ( - - {breadcrumbs && ( - - )} - {changePageMetadata && } void; + autoFocus?: boolean; } const KeyboardShortcutHint = ({ open }: { open: boolean }) => { @@ -90,6 +91,7 @@ const SearchByMultipleFields: React.FC = ({ clearSearch, onFieldChange, enableOptionButtons = true, + autoFocus = false, }) => { const { t } = useTranslation(); const [selectedOptionIndex, setSelectedOptionIndex] = @@ -175,12 +177,6 @@ const SearchByMultipleFields: React.FC = ({ return () => document.removeEventListener("keydown", handleKeyDown); }, [focusedIndex, open, handleOptionChange, options]); - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, [selectedOptionIndex]); - useEffect(() => { if (selectedOption.value !== searchValue) { onSearch(selectedOption.key, searchValue); @@ -224,6 +220,12 @@ const SearchByMultipleFields: React.FC = ({ } }, [selectedOption, searchValue, t, inputClassName, open]); + useEffect(() => { + if (autoFocus) { + inputRef.current?.focus(); + } + }, [autoFocus, open, selectedOptionIndex]); + return ( )); + +export const FormSkeleton = ({ rows }: { rows: number }) => ( + + {Array.from({ length: rows }).map((_, index) => ( + + ))} + +); diff --git a/src/components/Common/Tabs.tsx b/src/components/Common/Tabs.tsx deleted file mode 100644 index 9c375db618e..00000000000 --- a/src/components/Common/Tabs.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { type ReactNode, useEffect, useRef } from "react"; - -import useWindowDimensions from "@/hooks/useWindowDimensions"; - -import { classNames } from "@/Utils/utils"; - -export default function Tabs(props: { - className?: string; - currentTab: string | number; - onTabChange: (value: string | number) => void; - tabs: { text: ReactNode; value: string | number; id?: string }[]; -}) { - const { className, currentTab, onTabChange, tabs } = props; - const ref = useRef(null); - const tabSwitcherRef = useRef(null); - - const dimensions = useWindowDimensions(); - - useEffect(() => { - const currentTabIndex = tabs.findIndex((t) => t.value === currentTab); - if ( - typeof currentTabIndex != "number" || - !ref.current || - !tabSwitcherRef.current - ) - return; - const tabButton = ref.current.querySelectorAll("button")[currentTabIndex]; - if (!tabButton) return; - tabSwitcherRef.current.style.width = tabButton.clientWidth + "px"; - tabSwitcherRef.current.style.left = - tabButton.getBoundingClientRect().left - - ref.current.getBoundingClientRect().left + - ref.current.scrollLeft + - "px"; - }, [currentTab, tabSwitcherRef.current, ref.current, dimensions]); - - return ( - - - {/* There has to be a better way of handling this... */} - {tabs.map((tab, i) => ( - - {tab.text} - - ))} - - {tabs.map((tab, i) => ( - onTabChange(tab.value)} - className={`${currentTab === tab.value ? "text-white" : "text-primary-500 hover:text-primary-600"} flex-1 whitespace-nowrap px-6 py-2 text-sm font-semibold transition-all`} - > - {tab.text} - - ))} - - - ); -} diff --git a/src/components/Common/UpdatableApp.tsx b/src/components/Common/UpdatableApp.tsx index 814779a99d3..3a383d73408 100644 --- a/src/components/Common/UpdatableApp.tsx +++ b/src/components/Common/UpdatableApp.tsx @@ -1,12 +1,12 @@ import { Popover, Transition } from "@headlessui/react"; import { ReactNode, useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; -import { classNames } from "@/Utils/utils"; - const META_URL = "/build-meta.json"; const APP_VERSION_KEY = "app-version"; const APP_UPDATED_KEY = "app-updated"; @@ -120,10 +120,7 @@ const UpdateAppPopup = ({ onUpdate }: UpdateAppPopupProps) => { Software Update diff --git a/src/components/Common/UserAutocompleteFormField.tsx b/src/components/Common/UserAutocompleteFormField.tsx deleted file mode 100644 index 90addafb702..00000000000 --- a/src/components/Common/UserAutocompleteFormField.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useEffect, useState } from "react"; - -import { Autocomplete } from "@/components/Form/FormFields/Autocomplete"; -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; -import { UserType } from "@/components/Users/UserFormValidations"; -import { UserBareMinimum } from "@/components/Users/models"; - -import routes from "@/Utils/request/api"; -import useTanStackQueryInstead from "@/Utils/request/useQuery"; -import { - classNames, - formatName, - isUserOnline, - mergeQueryOptions, -} from "@/Utils/utils"; - -import { Avatar } from "./Avatar"; - -type BaseProps = FormFieldBaseProps & { - placeholder?: string; - userType?: UserType; - noResultsError?: string; -}; - -type UserSearchProps = BaseProps & { - facilityId?: undefined; - homeFacility?: string; -}; - -export default function UserAutocomplete(props: UserSearchProps) { - const field = useFormFieldPropsResolver(props); - const [query, setQuery] = useState(""); - const [disabled, setDisabled] = useState(false); - - const { data, loading } = useTanStackQueryInstead(routes.userList, { - query: { - home_facility: props.homeFacility, - user_type: props.userType, - search_text: query, - limit: 50, - offset: 0, - }, - }); - - useEffect(() => { - if ( - loading || - query || - !field.required || - !props.noResultsError || - !data?.results - ) { - return; - } - - if (data.results.length === 0) { - setDisabled(true); - field.handleChange(undefined as unknown as UserBareMinimum); - } - }, [loading, field.required, data?.results, props.noResultsError]); - - const getAvatar = (option: UserBareMinimum) => { - return ( - - ); - }; - - return ( - - obj.username, - )} - optionLabel={formatName} - optionIcon={userOnlineDot} - optionImage={getAvatar} - optionDescription={(option) => - `${option.user_type} - ${option.username}` - } - optionValue={(option) => option} - onQuery={setQuery} - isLoading={loading} - /> - - ); -} - -const userOnlineDot = (user: UserBareMinimum) => ( - -); diff --git a/src/components/Common/UserSelector.tsx b/src/components/Common/UserSelector.tsx index ff2221d5561..5c898e336da 100644 --- a/src/components/Common/UserSelector.tsx +++ b/src/components/Common/UserSelector.tsx @@ -18,6 +18,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { TooltipComponent } from "@/components/ui/tooltip"; import { Avatar } from "@/components/Common/Avatar"; @@ -69,7 +70,11 @@ export default function UserSelector({ name={formatName(selected)} className="size-6 rounded-full" /> - {formatName(selected)} + + + {formatName(selected)} + + ) : ( {placeholder || t("select_user")} diff --git a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx index ea8ccfed412..2514c7bbdfe 100644 --- a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx +++ b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { useQueryParams } from "raviger"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; @@ -25,6 +25,8 @@ import { QuestionnaireResponse } from "@/types/questionnaire/questionnaireRespon interface Props { encounter?: Encounter; patientId: string; + isPrintPreview?: boolean; + onlyUnstructured?: boolean; } interface QuestionResponseProps { @@ -214,19 +216,33 @@ function ResponseCard({ item }: { item: QuestionnaireResponse }) { } /> ) : ( - - {item.questionnaire?.title} {t("filed")} - + }} + /> )} - {t("at")} - {formatDateTime(item.created_date)} - {t("by")} - - {item.created_by?.first_name || ""}{" "} - {item.created_by?.last_name || ""} - {item.created_by?.user_type && ` (${item.created_by?.user_type})`} - + + }} + /> + + + }} + /> + @@ -268,19 +284,26 @@ function ResponseCard({ item }: { item: QuestionnaireResponse }) { export default function QuestionnaireResponsesList({ encounter, patientId, + isPrintPreview = false, + onlyUnstructured, }: Props) { const { t } = useTranslation(); const [qParams, setQueryParams] = useQueryParams<{ page?: number }>(); const { data: questionnarieResponses, isLoading } = useQuery({ queryKey: ["questionnaireResponses", patientId, qParams], - queryFn: query(routes.getQuestionnaireResponses, { + queryFn: query.paginated(routes.getQuestionnaireResponses, { pathParams: { patientId }, queryParams: { + ...(!isPrintPreview && { + limit: RESULTS_PER_PAGE_LIMIT, + offset: ((qParams.page ?? 1) - 1) * RESULTS_PER_PAGE_LIMIT, + }), encounter: encounter?.id, - limit: RESULTS_PER_PAGE_LIMIT, - offset: ((qParams.page ?? 1) - 1) * RESULTS_PER_PAGE_LIMIT, + only_unstructured: onlyUnstructured, }, + maxPages: isPrintPreview ? undefined : 1, + pageSize: isPrintPreview ? 100 : RESULTS_PER_PAGE_LIMIT, }), }); @@ -308,24 +331,28 @@ export default function QuestionnaireResponsesList({ ), )} - - - RESULTS_PER_PAGE_LIMIT - ? "visible" - : "invisible", - )} - > - setQueryParams({ page })} - /> + {!isPrintPreview && ( + + + RESULTS_PER_PAGE_LIMIT + ? "visible" + : "invisible", + )} + > + setQueryParams({ page })} + /> + - + )} )} diff --git a/src/components/Facility/ConsultationDetails/QuickAccess.tsx b/src/components/Facility/ConsultationDetails/QuickAccess.tsx index b9aa70c903d..649bfe6ef8b 100644 --- a/src/components/Facility/ConsultationDetails/QuickAccess.tsx +++ b/src/components/Facility/ConsultationDetails/QuickAccess.tsx @@ -1,4 +1,3 @@ -import { useQuery } from "@tanstack/react-query"; import { Link } from "raviger"; import { useTranslation } from "react-i18next"; @@ -9,9 +8,10 @@ import { Button } from "@/components/ui/button"; import LinkDepartmentsSheet from "@/components/Patient/LinkDepartmentsSheet"; -import query from "@/Utils/request/query"; +import useQuestionnaireOptions from "@/hooks/useQuestionnaireOptions"; + +import { stringifyNestedObject } from "@/Utils/utils"; import { Encounter } from "@/types/emr/encounter"; -import questionnaireApi from "@/types/questionnaire/questionnaireApi"; interface QuickAccessProps { encounter: Encounter; @@ -19,65 +19,35 @@ interface QuickAccessProps { export default function QuickAccess({ encounter }: QuickAccessProps) { const { t } = useTranslation(); - - const { data: response } = useQuery({ - queryKey: ["questionnaires"], - queryFn: query(questionnaireApi.list), - }); - - const questionnaireList = response?.results || []; - - const encounterSettings = [ - { id: "encounter_settings", label: t("encounter_settings") }, - ]; + const questionnaireOptions = useQuestionnaireOptions("encounter_actions"); return ( {/* Questionnaire Section */} - - {t("questionnaire")} - - {questionnaireList.map((item) => ( - - - {item.title} - - ))} - - - - - - {/* Update Encounter Details */} - - - {t("update_encounter_details")} - - - {encounterSettings.map((item) => ( - + {encounter.status !== "completed" && ( + + {t("questionnaire")} + + {questionnaireOptions.map((option) => ( - {item.label} + + {t(option.title)} - - ))} - - - - + ))} + + + + )} {/* Departments and Teams */} - - + + {t("departments_and_teams")} - {org.name} + {stringifyNestedObject(org)} )) : t("no_organizations_added_yet")} diff --git a/src/components/Facility/DuplicatePatientDialog.tsx b/src/components/Facility/DuplicatePatientDialog.tsx index 2e795092042..beac625f852 100644 --- a/src/components/Facility/DuplicatePatientDialog.tsx +++ b/src/components/Facility/DuplicatePatientDialog.tsx @@ -4,6 +4,13 @@ import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Table, TableBody, @@ -13,120 +20,125 @@ import { TableRow, } from "@/components/ui/table"; -import DialogModal from "@/components/Common/Dialog"; - import { PartialPatientModel } from "@/types/emr/newPatient"; interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; patientList: Array; handleOk: (action: string) => void; - handleCancel: () => void; } const DuplicatePatientDialog = (props: Props) => { const { t } = useTranslation(); - const { patientList, handleOk, handleCancel } = props; + const { open, onOpenChange, patientList, handleOk } = props; const [action, setAction] = useState(""); return ( - - - - - {t("patient_records_found_description")}( - {patientList[0].phone_number}) - - - - - - - - {[`${t("patient_name")} / ID`, t("gender")].map( - (heading, i) => ( - {heading} - ), - )} - - - - {patientList.map((patient, i) => { - return ( - - - - {patient.name} - - - ID : {patient.id} - - - {patient.gender} - - ); - })} - - + + + + {t("patient_records_found")} + + + + + {t("patient_records_found_description")}( + {patientList[0].phone_number}) + - - - - - setAction(e.target.value)} - /> - {t("duplicate_patient_record_confirmation")} - + + + + + + {[`${t("patient_name")} / ID`, t("gender")].map( + (heading, i) => ( + {heading} + ), + )} + + + + {patientList.map((patient, i) => { + return ( + + + + {patient.name} + + + ID : {patient.id} + + + {patient.gender} + + ); + })} + + + + + + + setAction(e.target.value)} + /> + {t("duplicate_patient_record_confirmation")} + + - - - setAction(e.target.value)} - /> - {t("duplicate_patient_record_rejection")} - - + + + setAction(e.target.value)} + /> + {t("duplicate_patient_record_rejection")} + + - {t("duplicate_patient_record_birth_unknown")} + {t("duplicate_patient_record_birth_unknown")} + - - - - - {t("close")} - - handleOk(action)} - disabled={!action} - variant={"primary"} - > - - {t("continue")} - - - + + + onOpenChange(false)} + className="gap-1" + variant={"secondary"} + > + + {t("close")} + + handleOk(action)} + disabled={!action} + variant={"primary"} + > + + {t("continue")} + + + + + ); }; diff --git a/src/components/Facility/FacilityForm.tsx b/src/components/Facility/FacilityForm.tsx index b0fed2348eb..c6cabe1de6a 100644 --- a/src/components/Facility/FacilityForm.tsx +++ b/src/components/Facility/FacilityForm.tsx @@ -66,9 +66,9 @@ export default function FacilityForm({ pincode: z.string().refine(validatePincode, t("invalid_pincode")), geo_organization: z.string().min(1, t("field_required")), address: z.string().min(1, t("address_is_required")), - phone_number: validators.phoneNumber.required, - latitude: validators.coordinates.latitude.optional(), - longitude: validators.coordinates.longitude.optional(), + phone_number: validators().phoneNumber.required, + latitude: validators().coordinates.latitude.optional(), + longitude: validators().coordinates.longitude.optional(), is_public: z.boolean().default(false), }); @@ -112,6 +112,9 @@ export default function FacilityForm({ queryClient.invalidateQueries({ queryKey: ["facility"], }); + queryClient.invalidateQueries({ + queryKey: ["currentUser"], + }); form.reset(); onSubmitSuccess?.(); }, diff --git a/src/components/Facility/FacilityHome.tsx b/src/components/Facility/FacilityHome.tsx index 547d1b209d6..195e4d700bf 100644 --- a/src/components/Facility/FacilityHome.tsx +++ b/src/components/Facility/FacilityHome.tsx @@ -7,14 +7,26 @@ import { } from "@radix-ui/react-tooltip"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query"; -import { Hospital, MapPin, MoreVertical, Settings } from "lucide-react"; +import { Edit2, Hospital, MapPin, MoreVertical, Settings } from "lucide-react"; import { navigate } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DropdownMenu, @@ -26,7 +38,6 @@ import { Markdown } from "@/components/ui/markdown"; import { Avatar } from "@/components/Common/Avatar"; import AvatarEditModal from "@/components/Common/AvatarEditModal"; -import ConfirmDialog from "@/components/Common/ConfirmDialog"; import ContactLink from "@/components/Common/ContactLink"; import Loading from "@/components/Common/Loading"; @@ -96,7 +107,6 @@ const renderGeoOrganizations = (geoOrg: Organization) => { export const FacilityHome = ({ facilityId }: Props) => { const { t } = useTranslation(); - const [openDeleteDialog, setOpenDeleteDialog] = useState(false); const [editCoverImage, setEditCoverImage] = useState(false); const queryClient = useQueryClient(); @@ -106,13 +116,14 @@ export const FacilityHome = ({ facilityId }: Props) => { pathParams: { id: facilityId }, }), }); - const { mutate: deleteFacility } = useMutation({ + + const { mutate: deleteFacility, isPending: isDeleting } = useMutation({ mutationFn: mutate(routes.deleteFacility, { pathParams: { id: facilityId }, }), onSuccess: () => { toast.success( - t("entity_deleted_successfully", { name: facilityData?.name }), + t("facility_deleted_successfully", { name: facilityData?.name }), ); navigate("/facility"); }, @@ -184,26 +195,13 @@ export const FacilityHome = ({ facilityId }: Props) => { return ( - - {t("are_you_sure_want_to_delete", { name: facilityData?.name })} - - } - action="Delete" - variant="destructive" - show={openDeleteDialog} - onClose={() => setOpenDeleteDialog(false)} - onConfirm={() => deleteFacility()} - /> setEditCoverImage(false)} + onOpenChange={(open) => setEditCoverImage(open)} hint={coverImageHint} /> @@ -267,7 +265,7 @@ export const FacilityHome = ({ facilityId }: Props) => { className="cursor-pointer" onClick={() => setEditCoverImage(true)} > - + {t("edit_cover_photo")} )} @@ -276,7 +274,7 @@ export const FacilityHome = ({ facilityId }: Props) => { facilityId={facilityId} trigger={ { e.preventDefault(); }} @@ -286,16 +284,45 @@ export const FacilityHome = ({ facilityId }: Props) => { } /> - {/* TODO: get permissions from backend */} - {/* {hasPermissionToDeleteFacility && ( - setOpenDeleteDialog(true)} - > - - {t("delete_facility")} - - )} */} + + {/* TODO: add delete facility */} + {/* + e.preventDefault()} + > + + {t("delete_facility")} + + */} + + + + {t("delete_facility")} + + + {t("delete_facility_confirmation", { + name: facilityData?.name, + })} + + + + + {t("cancel")} + + deleteFacility()} + className={cn( + buttonVariants({ variant: "destructive" }), + )} + disabled={isDeleting} + > + {isDeleting ? t("deleting") : t("delete")} + + + + + ; export interface CommentModel { diff --git a/src/components/Files/ArchivedFileDialog.tsx b/src/components/Files/ArchivedFileDialog.tsx new file mode 100644 index 00000000000..ac3eec81e3e --- /dev/null +++ b/src/components/Files/ArchivedFileDialog.tsx @@ -0,0 +1,77 @@ +import dayjs from "dayjs"; +import { t } from "i18next"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +import { FileUploadModel } from "@/components/Patient/models"; + +export default function ArchivedFileDialog({ + open, + onOpenChange, + file, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + file: FileUploadModel | null; +}) { + if (!file) { + return <>>; + } + const fileName = file?.name ? file.name + file.extension : ""; + return ( + + + + + {t("archived_file")}:{" "} + + + + {fileName} + + + {fileName} + + + + + + + + + {t("archived_reason")}: + + {file?.archive_reason} + + + + {t("archived_by")}: {file.archived_by?.username} + + + {t("archived_at")}:{" "} + {dayjs(file.archived_datetime).format("DD MMM YYYY, hh:mm A")} + + + + + + ); +} diff --git a/src/components/Files/AudioPlayerDialog.tsx b/src/components/Files/AudioPlayerDialog.tsx new file mode 100644 index 00000000000..81e3139ceb4 --- /dev/null +++ b/src/components/Files/AudioPlayerDialog.tsx @@ -0,0 +1,61 @@ +import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +import AudioPlayer from "@/components/Common/AudioPlayer"; +import { FileUploadModel } from "@/components/Patient/models"; + +import routes from "@/Utils/request/api"; +import query from "@/Utils/request/query"; + +export default function AudioPlayerDialog({ + open, + onOpenChange, + file, + type, + associatingId, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + file: FileUploadModel | null; + type: "encounter" | "patient"; + associatingId: string; +}) { + const { data: fileData } = useQuery({ + queryKey: [routes.retrieveUpload, type, file?.id], + queryFn: query(routes.retrieveUpload, { + queryParams: { file_type: type, associating_id: associatingId }, + pathParams: { id: file?.id || "" }, + }), + enabled: !!file?.id, + }); + const { Player, stopPlayback } = AudioPlayer({ + src: fileData?.read_signed_url || "", + }); + return ( + { + stopPlayback(); + onOpenChange(false); + }} + aria-labelledby="audio-player-dialog" + > + + + {t("play_audio")} + + + + + ); +} diff --git a/src/components/Files/CameraCaptureDialog.tsx b/src/components/Files/CameraCaptureDialog.tsx index 54b9debc21e..8ee66a313d6 100644 --- a/src/components/Files/CameraCaptureDialog.tsx +++ b/src/components/Files/CameraCaptureDialog.tsx @@ -6,21 +6,25 @@ import { toast } from "sonner"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; - -import DialogModal from "@/components/Common/Dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import useBreakpoints from "@/hooks/useBreakpoints"; export interface CameraCaptureDialogProps { - show: boolean; - onHide: () => void; + open: boolean; + onOpenChange: (open: boolean) => void; onCapture: (file: File, fileName: string) => void; onResetCapture: () => void; setPreview?: (isPreview: boolean) => void; } export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { - const { show, onHide, onCapture, onResetCapture, setPreview } = props; + const { open, onOpenChange, onCapture, onResetCapture, setPreview } = props; const isLaptopScreen = useBreakpoints({ lg: true, default: false }); const [cameraFacingMode, setCameraFacingMode] = useState( @@ -34,8 +38,9 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { height: { ideal: 2160 }, facingMode: cameraFacingMode, }; + useEffect(() => { - if (!show) return; + if (!open) return; let stream: MediaStream | null = null; navigator.mediaDevices @@ -45,7 +50,7 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { }) .catch(() => { toast.warning(t("camera_permission_denied")); - onHide(); + onOpenChange(false); }); return () => { @@ -55,7 +60,7 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { }); } }; - }, [show, cameraFacingMode, onHide]); + }, [open, cameraFacingMode, onOpenChange]); const handleSwitchCamera = useCallback(async () => { const devices = await navigator.mediaDevices.enumerateDevices(); @@ -87,130 +92,62 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { }; return ( - - - - - - {t("camera")} - - - } - className="max-w-2xl" - onClose={onHide} - > - - {!previewImage ? ( - - - - ) : ( - - - - )} - + + + + + + + + + + + {t("camera")} + + + + - {/* buttons for mobile screens */} - - - {!previewImage ? ( - - {t("switch")} - - ) : ( - <>> - )} - {!previewImage ? ( - <> - - { - captureImage(); - setPreview?.(true); - }} - className="m-2" - > - {t("capture")} - - - > + + + ) : ( - <> - - { - setPreviewImage(null); - onResetCapture(); - setPreview?.(false); - }} - className="m-2" - > - {t("retake")} - - { - setPreviewImage(null); - onHide(); - setPreview?.(false); - }} - className="m-2" - > - {t("submit")} - - - > + + + )} - - { - setPreviewImage(null); - onResetCapture(); - onHide(); - }} - className="m-2" - > - {t("close")} - - - - {/* buttons for laptop screens */} - - - - - {`${t("switch")} ${t("camera")}`} - - - + {/* buttons for mobile and tablet screens */} + + + {!previewImage ? ( + + {t("switch")} + + ) : ( + <>> + )} + {!previewImage ? ( <> @@ -221,8 +158,8 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { captureImage(); setPreview?.(true); }} + className="m-2" > - {t("capture")} @@ -237,16 +174,18 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { onResetCapture(); setPreview?.(false); }} + className="m-2" > {t("retake")} { - onHide(); setPreviewImage(null); + onOpenChange(false); setPreview?.(false); }} + className="m-2" > {t("submit")} @@ -254,20 +193,82 @@ export default function CameraCaptureDialog(props: CameraCaptureDialogProps) { > )} - - { - setPreviewImage(null); - onResetCapture(); - onHide(); - setPreview?.(false); - }} - > - {`${t("close")} ${t("camera")}`} - + + { + setPreviewImage(null); + onResetCapture(); + onOpenChange(false); + }} + className="m-2" + > + {t("close")} + + + + + {/* buttons for laptop screens */} + + + + {!previewImage ? ( + <> + + { + captureImage(); + setPreview?.(true); + }} + > + + {t("capture")} + + + > + ) : ( + <> + + { + setPreviewImage(null); + onResetCapture(); + setPreview?.(false); + }} + > + {t("retake")} + + { + onOpenChange(false); + setPreviewImage(null); + setPreview?.(false); + }} + > + {t("submit")} + + + > + )} + + + { + setPreviewImage(null); + onResetCapture(); + onOpenChange(false); + setPreview?.(false); + }} + > + {`${t("close")} ${t("camera")}`} + + - - + + ); } diff --git a/src/components/Files/FileBlock.tsx b/src/components/Files/FileBlock.tsx deleted file mode 100644 index 1b9cb9f8959..00000000000 --- a/src/components/Files/FileBlock.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import dayjs from "dayjs"; -import { t } from "i18next"; - -import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; - -import { Button } from "@/components/ui/button"; - -import { FileUploadModel } from "@/components/Patient/models"; - -import { FileManagerResult } from "@/hooks/useFileManager"; - -import { FILE_EXTENSIONS } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import query from "@/Utils/request/query"; - -export interface FileBlockProps { - file: FileUploadModel; - fileManager: FileManagerResult; - associating_id: string; - editable: boolean; - archivable?: boolean; -} - -export default function FileBlock(props: FileBlockProps) { - const { - file, - fileManager, - associating_id, - editable = false, - archivable = false, - } = props; - - const filetype = fileManager.getFileType(file); - - const { data: fileData } = useQuery({ - queryKey: ["file", { id: file.id, type: fileManager.type, associating_id }], - queryFn: query(routes.retrieveUpload, { - queryParams: { file_type: fileManager.type, associating_id }, - pathParams: { id: file.id || "" }, - }), - enabled: filetype === "AUDIO" && !file.is_archived, - }); - - const icons: Record = { - AUDIO: "l-volume", - IMAGE: "l-image", - PRESENTATION: "l-presentation-play", - VIDEO: "l-video", - UNKNOWN: "l-file-medical", - DOCUMENT: "l-file-medical", - }; - - const archived = file.is_archived; - - return ( - - - - - - - - {file.name} - {file.extension} {file.is_archived && "(Archived)"} - - - {dayjs( - file.is_archived ? file.archived_datetime : file.created_date, - ).format("DD MMM YYYY, hh:mm A")}{" "} - by{" "} - {file.is_archived - ? file.archived_by?.username - : file.uploaded_by?.username} - - - - - {filetype === "AUDIO" && !file.is_archived && ( - - - - )} - {!file.is_archived && - (fileManager.isPreviewable(file) ? ( - fileManager.viewFile(file, associating_id)} - className="w-full md:w-auto" - > - - {t("view")} - - ) : ( - fileManager.downloadFile(file, associating_id)} - className="w-full md:w-auto" - > - - {t("download")} - - ))} - - {!file.is_archived && editable && ( - fileManager.editFile(file, associating_id)} - className="flex-1 md:flex-auto" - > - - {t("rename")} - - )} - {(file.is_archived || editable) && archivable && ( - fileManager.archiveFile(file, associating_id)} - className="flex-1 md:flex-auto" - > - - {file.is_archived ? t("more_info") : t("archive")} - - )} - - - - ); -} diff --git a/src/components/Files/FileUpload.tsx b/src/components/Files/FileUpload.tsx deleted file mode 100644 index 54881c80c0a..00000000000 --- a/src/components/Files/FileUpload.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Loader2 } from "lucide-react"; -import { ReactNode, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; - -import Pagination from "@/components/Common/Pagination"; -import Tabs from "@/components/Common/Tabs"; -import FileBlock from "@/components/Files/FileBlock"; -import { FileUploadModel } from "@/components/Patient/models"; - -import useAuthUser from "@/hooks/useAuthUser"; -import useFileManager from "@/hooks/useFileManager"; -import useFileUpload from "@/hooks/useFileUpload"; - -import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import query from "@/Utils/request/query"; - -export const LinearProgressWithLabel = (props: { value: number }) => { - return ( - - - - - - - - {`${Math.round(props.value)}%`} - - - ); -}; - -interface FileUploadProps { - type: string; - patientId?: string; - encounterId?: string; - consentId?: string; - allowAudio?: boolean; - sampleId?: string; - claimId?: string; - className?: string; - hideUpload?: boolean; -} - -export interface ModalDetails { - name?: string; - id?: string; - reason?: string; - userArchived?: string; - archiveTime?: string; - associatedId?: string; -} - -export interface StateInterface { - open: boolean; - isImage: boolean; - name: string; - extension: string; - zoom: number; - isZoomInDisabled: boolean; - isZoomOutDisabled: boolean; - rotation: number; - id?: string; - associating_id?: string; -} - -export const FileUpload = (props: FileUploadProps) => { - const { t } = useTranslation(); - const { - encounterId, - patientId, - consentId, - type, - sampleId, - claimId, - allowAudio, - hideUpload, - } = props; - const [currentPage, setCurrentPage] = useState(1); - const [offset, setOffset] = useState(0); - const [tab, setTab] = useState("UNARCHIVED"); - const authUser = useAuthUser(); - const queryClient = useQueryClient(); - - const handlePagination = (page: number, limit: number) => { - const offset = (page - 1) * limit; - setCurrentPage(page); - setOffset(offset); - }; - - const UPLOAD_HEADING: { [index: string]: string } = { - PATIENT: t("upload_headings__patient"), - CONSULTATION: t("upload_headings__consultation"), - SAMPLE_MANAGEMENT: t("upload_headings__sample_report"), - CLAIM: t("upload_headings__supporting_info"), - }; - const VIEW_HEADING: { [index: string]: string } = { - PATIENT: t("file_list_headings__patient"), - CONSULTATION: t("file_list_headings__consultation"), - SAMPLE_MANAGEMENT: t("file_list_headings__sample_report"), - CLAIM: t("file_list_headings__supporting_info"), - }; - - const associatedId = - { - PATIENT: patientId, - CONSENT_RECORD: consentId, - ENCOUNTER: encounterId, - SAMPLE_MANAGEMENT: sampleId, - CLAIM: claimId, - }[type] || ""; - - const refetchAll = () => { - queryClient.invalidateQueries({ - queryKey: ["viewUpload", "active", type, associatedId], - }); - queryClient.invalidateQueries({ - queryKey: ["viewUpload", "archived", type, associatedId], - }); - if (type === "consultation") { - queryClient.invalidateQueries({ - queryKey: ["viewUpload", "discharge_summary", associatedId], - }); - } - }; - - const { data: activeFiles, isLoading: activeFilesLoading } = useQuery({ - queryKey: ["viewUpload", "active", type, associatedId, offset], - queryFn: query(routes.viewUpload, { - queryParams: { - file_type: type, - associating_id: associatedId, - is_archived: false, - limit: RESULTS_PER_PAGE_LIMIT, - offset: offset, - }, - }), - }); - - const { data: archivedFiles, isLoading: archivedFilesLoading } = useQuery({ - queryKey: ["viewUpload", "archived", type, associatedId, offset], - queryFn: query(routes.viewUpload, { - queryParams: { - file_type: type, - associating_id: associatedId, - is_archived: true, - limit: RESULTS_PER_PAGE_LIMIT, - offset: offset, - }, - }), - }); - - const { data: dischargeSummary, isLoading: dischargeSummaryLoading } = - useQuery({ - queryKey: ["viewUpload", "discharge_summary", associatedId, offset], - queryFn: query(routes.viewUpload, { - queryParams: { - file_type: "discharge_summary", - associating_id: associatedId, - is_archived: false, - limit: RESULTS_PER_PAGE_LIMIT, - offset: offset, - silent: true, - }, - }), - enabled: type === "consultation", - }); - - const queries = { - UNARCHIVED: { data: activeFiles, isLoading: activeFilesLoading }, - ARCHIVED: { data: archivedFiles, isLoading: archivedFilesLoading }, - DISCHARGE_SUMMARY: { - data: dischargeSummary, - isLoading: dischargeSummaryLoading, - }, - }; - - const loading = Object.values(queries).some((q) => q.isLoading); - const fileQuery = queries[tab as keyof typeof queries]; - - const tabs = [ - { text: "Active Files", value: "UNARCHIVED" }, - { text: "Archived Files", value: "ARCHIVED" }, - ...(dischargeSummary?.results?.length - ? [ - { - text: "Discharge Summary", - value: "DISCHARGE_SUMMARY", - }, - ] - : []), - ]; - - const fileUpload = useFileUpload({ - type, - allowedExtensions: [ - "jpg", - "jpeg", - "png", - "gif", - "bmp", - "tiff", - "mp4", - "mov", - "avi", - "wmv", - "mp3", - "wav", - "ogg", - "txt", - "csv", - "rtf", - "doc", - "odt", - "pdf", - "xls", - "xlsx", - "ods", - "pdf", - ], - allowNameFallback: false, - onUpload: refetchAll, - }); - - const fileManager = useFileManager({ - type, - onArchive: refetchAll, - onEdit: refetchAll, - uploadedFiles: - fileQuery?.data?.results - .slice() - .reverse() - .map((file) => ({ - ...file, - associating_id: associatedId, - })) || [], - }); - const dischargeSummaryFileManager = useFileManager({ - type: "DISCHARGE_SUMMARY", - onArchive: refetchAll, - onEdit: refetchAll, - }); - - const uploadButtons: { - name: string; - icon: IconName; - onClick?: () => void; - children?: ReactNode; - show?: boolean; - id: string; - }[] = [ - { - name: t("choose_file"), - icon: "l-file-upload-alt", - children: , - id: "upload-file", - }, - { - name: t("open_camera"), - icon: "l-camera", - onClick: fileUpload.handleCameraCapture, - id: "open-webcam", - }, - { - name: t("record"), - icon: "l-microphone", - onClick: fileUpload.handleAudioCapture, - show: allowAudio, - id: "record-audio", - }, - ]; - return ( - - {fileUpload.Dialogues} - {fileManager.Dialogues} - {dischargeSummaryFileManager.Dialogues} - {!hideUpload && ( - <> - {UPLOAD_HEADING[type]} - {fileUpload.files[0] ? ( - - - - - {fileUpload.files[0].name} - - - - - - {t("enter_file_name")} - fileUpload.setFileName(e.target.value)} - /> - {fileUpload.error && ( - {fileUpload.error} - )} - - fileUpload.handleFileUpload(associatedId)} - disabled={fileUpload.uploading} // Disable the button when loading - className={`w-full ${fileUpload.uploading ? "opacity-50" : ""}`} - id="upload_file_button" - > - {fileUpload.uploading ? ( - - ) : ( - - )} - {t("upload")} - - - - {t("discard")} - - - {!!fileUpload.progress && ( - - )} - - ) : ( - - {uploadButtons - .filter((b) => b.show !== false) - .map((button, i) => ( - - - {button.name} - {button.children} - - ))} - - )} - > - )} - - {VIEW_HEADING[type]} - setTab(v.toString())} - currentTab={tab} - /> - - - {!(fileQuery?.data?.results || []).length && loading && ( - - )} - {fileQuery?.data?.results.map((item: FileUploadModel) => ( - - ))} - {!(fileQuery?.data?.results || []).length && ( - - - {t("no_files_found", { type: tab.toLowerCase() })} - - - )} - - {(fileQuery?.data?.count ?? 0) > RESULTS_PER_PAGE_LIMIT && ( - - - - )} - - ); -}; diff --git a/src/components/Files/FileUploadDialog.tsx b/src/components/Files/FileUploadDialog.tsx new file mode 100644 index 00000000000..331d9c7266a --- /dev/null +++ b/src/components/Files/FileUploadDialog.tsx @@ -0,0 +1,204 @@ +import { t } from "i18next"; +import { useState } from "react"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Progress } from "@/components/ui/progress"; + +import { FileUploadReturn } from "@/hooks/useFileUpload"; + +export default function FileUploadDialog({ + open, + onOpenChange, + fileUpload, + associatingId, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + fileUpload: FileUploadReturn; + associatingId: string; +}) { + const handleDialogClose = (open: boolean) => { + if (!open) { + setIsPdf(false); + } + onOpenChange(open); + }; + const [isPdf, setIsPdf] = useState(false); + return ( + + + + + {fileUpload.files.length > 1 ? t("upload_files") : t("upload_file")} + + + + {isPdf ? ( + <> + {fileUpload.files.map((file, index) => ( + + + + + + {file.name.length > 40 + ? `${file.name.substring(0, 30)}...` + : file.name} + + + fileUpload.removeFile(index)} + disabled={fileUpload.uploading} + > + + + + + ))} + + + {t("enter_file_name")} + + { + fileUpload.setFileName(e.target.value); + fileUpload.setError(null); + }} + className="ml-0.5 mb-1" + /> + {fileUpload.error && ( + + {fileUpload.error} + + )} + + > + ) : ( + fileUpload.files.map((file, index) => ( + + + + + + {file.name.length > 40 + ? `${file.name.substring(0, 30)}...` + : file.name} + + + fileUpload.removeFile(index)} + disabled={fileUpload.uploading} + > + + + + + + {t("enter_file_name")} + + + { + fileUpload.setFileName(e.target.value, index); + fileUpload.setError(null); + }} + className="ml-0.5 mb-0.5" + /> + {!fileUpload.fileNames[index] && fileUpload.error && ( + + {fileUpload.error} + + )} + + + )) + )} + + {fileUpload.files.length > 1 && ( + + setIsPdf(checked)} + disabled={fileUpload.uploading} + className="cursor-pointer" + /> + + {t("combine_files_pdf")} + + + )} + + + fileUpload.handleFileUpload(associatingId, isPdf)} + disabled={fileUpload.uploading} + className="w-full" + id="upload_file_button" + > + + {t("upload")} + + + + {t("discard")} + + + {!!fileUpload.progress && ( + + )} + + + ); +} diff --git a/src/components/Files/FilesTab.tsx b/src/components/Files/FilesTab.tsx index 0d11046948a..e092adfcae7 100644 --- a/src/components/Files/FilesTab.tsx +++ b/src/components/Files/FilesTab.tsx @@ -1,28 +1,24 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import dayjs from "dayjs"; +import { t } from "i18next"; +import { Link } from "raviger"; import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import { cn } from "@/lib/utils"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; +import { Card, CardContent } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Progress } from "@/components/ui/progress"; import { Table, TableBody, @@ -31,7 +27,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Tabs, TabsContent } from "@/components/ui/tabs"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, @@ -39,64 +35,82 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; -import AudioPlayer from "@/components/Common/AudioPlayer"; import Loading from "@/components/Common/Loading"; +import ArchivedFileDialog from "@/components/Files/ArchivedFileDialog"; +import AudioPlayerDialog from "@/components/Files/AudioPlayerDialog"; +import FileUploadDialog from "@/components/Files/FileUploadDialog"; import { FileUploadModel } from "@/components/Patient/models"; import useFileManager from "@/hooks/useFileManager"; -import useFileUpload, { FileUploadReturn } from "@/hooks/useFileUpload"; +import useFileUpload from "@/hooks/useFileUpload"; import useFilters from "@/hooks/useFilters"; import { FILE_EXTENSIONS } from "@/common/constants"; import routes from "@/Utils/request/api"; +import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; -import { classNames } from "@/Utils/utils"; +import { HTTPError } from "@/Utils/request/types"; import { usePermissions } from "@/context/PermissionContext"; +import { Encounter } from "@/types/emr/encounter"; +import { Patient } from "@/types/emr/newPatient"; export interface FilesTabProps { type: "encounter" | "patient"; facilityId: string; - encounterId?: string; patientId?: string; + encounter?: Encounter; + patient?: Patient; + subPage?: string; } export const FilesTab = (props: FilesTabProps) => { - const { encounterId, patientId, type } = props; + const { patientId, type, encounter, subPage = "all" } = props; const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ limit: 14, }); - const { t } = useTranslation(); const [openUploadDialog, setOpenUploadDialog] = useState(false); + const [openArchivedFileDialog, setOpenArchivedFileDialog] = useState(false); + const [selectedArchivedFile, setSelectedArchivedFile] = + useState(null); const [selectedAudioFile, setSelectedAudioFile] = useState(null); const [openAudioPlayerDialog, setOpenAudioPlayerDialog] = useState(false); const { hasPermission } = usePermissions(); + const queryClient = useQueryClient(); const associatingId = { patient: patientId, - encounter: encounterId, + encounter: encounter?.id, }[type] || ""; const fileCategories = [ { value: "all", label: "All" }, - { value: "imaging", label: "Imaging" }, - { value: "lab_reports", label: "Lab Reports" }, - { value: "documents", label: "Documents" }, { value: "audio", label: "Audio" }, + { value: "xray", label: "X-Ray" }, + { value: "identity_proof", label: "Identity Proof" }, + { value: "unspecified", label: "Unspecified" }, + { value: "discharge_summary", label: "Discharge Summary" }, ] as const; - const handleTabChange = (value: (typeof fileCategories)[number]["value"]) => { - updateQuery({ file_category: value === "all" ? undefined : value }); - }; + const { mutate: generateDischargeSummary, isPending: isGenerating } = + useMutation<{ detail: string }, HTTPError>({ + mutationFn: mutate(routes.encounter.generateDischargeSummary, { + pathParams: { encounterId: encounter?.id || "" }, + }), + onSuccess: (response) => { + toast.success(response.detail); + refetch(); + }, + }); const { data: files, isLoading: filesLoading, refetch, } = useQuery({ - queryKey: ["files", type, associatingId, qParams], + queryKey: ["files", type, associatingId, qParams, subPage], queryFn: query(routes.viewUpload, { queryParams: { file_type: type, @@ -106,7 +120,7 @@ export const FilesTab = (props: FilesTabProps) => { ...(qParams.is_archived !== undefined && { is_archived: qParams.is_archived, }), - //file_category: qParams.file_category, + ...(subPage !== "all" && { file_category: subPage }), }, }), }); @@ -117,6 +131,7 @@ export const FilesTab = (props: FilesTabProps) => { onEdit: refetch, uploadedFiles: files?.results + .filter((file) => !file.is_archived) .slice() .reverse() .map((file) => ({ @@ -193,87 +208,128 @@ export const FilesTab = (props: FilesTabProps) => { DOCUMENT: "l-file-medical", }; - const getArchivedMessage = () => { + const getArchivedMessage = (file: FileUploadModel) => { return ( - + {t("archived")} + { + setSelectedArchivedFile(file); + setOpenArchivedFileDialog(true); + }} + > + + + {t("view")} + + ); }; + const editPermission = () => { + if (type === "encounter") { + return ( + encounter && + ![ + "completed", + "cancelled", + "entered_in_error", + "discontinued", + ].includes(encounter.status) && + hasPermission("can_write_encounter") + ); + } else if (type === "patient") { + return hasPermission("can_write_patient"); + } + return false; + }; + const DetailButtons = ({ file }: { file: FileUploadModel }) => { const filetype = getFileType(file); return ( - - {filetype === "AUDIO" && !file.is_archived && ( - { - setSelectedAudioFile(file); - setOpenAudioPlayerDialog(true); - }} - > - - - {t("play")} - - - )} - {fileManager.isPreviewable(file) && ( - fileManager.viewFile(file, associatingId)} - > - - - {t("view")} - - - )} - { - - - - + <> + {editPermission() && ( + + {filetype === "AUDIO" && !file.is_archived && ( + { + setSelectedAudioFile(file); + setOpenAudioPlayerDialog(true); + }} + > + + + {t("play")} + - - - - fileManager.downloadFile(file, associatingId)} - > - - {t("download")} - - - - fileManager.archiveFile(file, associatingId)} - > - - {t("archive")} - - - {hasPermission( - type === "encounter" - ? "can_write_encounter" - : "can_write_patient", - ) && ( - - fileManager.editFile(file, associatingId)} - > - - {t("rename")} - - - )} - - - } - + )} + {fileManager.isPreviewable(file) && ( + fileManager.viewFile(file, associatingId)} + > + + + {t("view")} + + + )} + { + + + + + + + + + + fileManager.downloadFile(file, associatingId) + } + variant="ghost" + className="w-full flex flex-row justify-stretch items-center" + > + + {t("download")} + + + + + fileManager.archiveFile(file, associatingId) + } + variant="ghost" + className="w-full flex flex-row justify-stretch items-center" + > + + {t("archive")} + + + + fileManager.editFile(file, associatingId)} + variant="ghost" + className="w-full flex flex-row justify-stretch items-center" + > + + {t("rename")} + + + + + } + + )} + > ); }; @@ -332,12 +388,7 @@ export const FilesTab = (props: FilesTabProps) => { }; const FileUploadButtons = (): JSX.Element => { - if ( - !hasPermission( - type === "encounter" ? "can_write_encounter" : "can_write_patient", - ) - ) - return <>>; + if (!editPermission()) return <>>; return ( @@ -362,38 +413,104 @@ export const FilesTab = (props: FilesTabProps) => { > {t("choose_file")} {fileUpload.Input({ className: "hidden" })} - - + fileUpload.handleCameraCapture()} - className="flex flex-row items-center " + className="flex flex-row justify-stretch items-center w-full text-primary-900" > - + {t("open_camera")} - + - - + fileUpload.handleAudioCapture()} - className="flex flex-row items-center" + className="flex flex-row justify-stretch items-center w-full text-primary-900" > - + {t("record")} - + ); }; - const RenderTable = () => { - return ( + const RenderCard = () => ( + + {files?.results && files?.results?.length > 0 ? ( + files.results.map((file) => { + const filetype = getFileType(file); + const fileName = file.name ? file.name + file.extension : ""; + + return ( + + + + + + + + + {fileName} + + {filetype} + + + + + + {t("date")} + + {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} + + + + {t("shared_by")} + + {file.uploaded_by?.username} + + + + + + {file.is_archived ? ( + getArchivedMessage(file) + ) : ( + + )} + + + + ); + }) + ) : ( + + {t("no_files_found")} + + )} + + ); + + const RenderTable = () => ( + @@ -421,12 +538,10 @@ export const FilesTab = (props: FilesTabProps) => { return ( { { {filetype} { {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} { {file.uploaded_by?.username} {file.is_archived ? ( - getArchivedMessage() + getArchivedMessage(file) ) : ( )} @@ -503,8 +618,8 @@ export const FilesTab = (props: FilesTabProps) => { )} - ); - }; + + ); return ( @@ -524,40 +639,79 @@ export const FilesTab = (props: FilesTabProps) => { associatingId={associatingId} /> + - - handleTabChange(value as (typeof fileCategories)[number]["value"]) - } - > - - - {/* - {fileCategories.map((category) => ( - - {category.label} - - ))} - */} - - + + + + + {t("all")} + + + + + {t("discharge_summary")} + + + + + + {subPage === "discharge_summary" && ( + <> + { + await queryClient.invalidateQueries({ + queryKey: ["files"], + }); + toast.success(t("refreshed")); + }} + > + + {t("refresh")} + + > + )} + {subPage === "discharge_summary" && ( + <> + generateDischargeSummary()} + disabled={isGenerating} + > + + + {isGenerating + ? t("generating") + : t("generate_discharge_summary")} + + + > + )} {fileCategories.map((category) => ( + ))} @@ -565,237 +719,3 @@ export const FilesTab = (props: FilesTabProps) => { ); }; - -const FileUploadDialog = ({ - open, - onOpenChange, - fileUpload, - associatingId, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - fileUpload: FileUploadReturn; - associatingId: string; -}) => { - const handleDialogClose = (open: boolean) => { - if (!open) { - setIsPdf(false); - } - onOpenChange(open); - }; - const { t } = useTranslation(); - const [isPdf, setIsPdf] = useState(false); - return ( - - - - - {fileUpload.files.length > 1 ? t("upload_files") : t("upload_file")} - - - - {isPdf ? ( - <> - {fileUpload.files.map((file, index) => ( - - - - - - {file.name.length > 40 - ? `${file.name.substring(0, 30)}...` - : file.name} - - - fileUpload.removeFile(index)} - disabled={fileUpload.uploading} - > - - - - - ))} - - - {t("enter_file_name")} - - { - fileUpload.setFileName(e.target.value); - fileUpload.setError(null); - }} - className="ml-0.5 mb-1" - /> - {fileUpload.error && ( - - {fileUpload.error} - - )} - - > - ) : ( - fileUpload.files.map((file, index) => ( - - - - - - {file.name.length > 40 - ? `${file.name.substring(0, 30)}...` - : file.name} - - - fileUpload.removeFile(index)} - disabled={fileUpload.uploading} - > - - - - - - {t("enter_file_name")} - - - { - fileUpload.setFileName(e.target.value, index); - fileUpload.setError(null); - }} - className="ml-0.5 mb-0.5" - /> - {!fileUpload.fileNames[index] && fileUpload.error && ( - - {fileUpload.error} - - )} - - - )) - )} - - {fileUpload.files.length > 1 && ( - - setIsPdf(checked)} - disabled={fileUpload.uploading} - className="cursor-pointer" - /> - - {t("combine_files_pdf")} - - - )} - - - fileUpload.handleFileUpload(associatingId, isPdf)} - disabled={fileUpload.uploading} - className="w-full" - id="upload_file_button" - > - - {t("upload")} - - - - {t("discard")} - - - {!!fileUpload.progress && ( - - )} - - - ); -}; - -const AudioPlayerDialog = ({ - open, - onOpenChange, - file, - type, - associatingId, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - file: FileUploadModel | null; - type: "encounter" | "patient"; - associatingId: string; -}) => { - const { t } = useTranslation(); - const { data: fileData } = useQuery({ - queryKey: [routes.retrieveUpload, type, file?.id], - queryFn: query(routes.retrieveUpload, { - queryParams: { file_type: type, associating_id: associatingId }, - pathParams: { id: file?.id || "" }, - }), - enabled: !!file?.id, - }); - const { Player, stopPlayback } = AudioPlayer({ - src: fileData?.read_signed_url || "", - }); - return ( - { - stopPlayback(); - onOpenChange(false); - }} - aria-labelledby="audio-player-dialog" - > - - - {t("play_audio")} - - - - - ); -}; diff --git a/src/components/Form/AutoCompleteAsync.tsx b/src/components/Form/AutoCompleteAsync.tsx deleted file mode 100644 index b561b1f0648..00000000000 --- a/src/components/Form/AutoCompleteAsync.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { - Combobox, - ComboboxButton, - ComboboxInput, - ComboboxOption, - ComboboxOptions, -} from "@headlessui/react"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { DropdownTransition } from "@/components/Common/HelperComponents"; -import { - MultiSelectOptionChip, - dropdownOptionClassNames, -} from "@/components/Form/MultiSelectMenuV2"; - -import useDebounce from "@/hooks/useDebounce"; - -import { classNames } from "@/Utils/utils"; - -interface Props { - id?: string; - name?: string; - selected: any | any[]; - fetchData: (search: string) => Promise | undefined; - onChange: (selected: any) => void; - optionLabel?: (option: any) => string; - optionLabelChip?: (option: any) => string; - showNOptions?: number | undefined; - multiple?: boolean; - compareBy?: string; - debounceTime?: number; - className?: string; - placeholder?: string; - disabled?: boolean; - error?: string; - required?: boolean; - onBlur?: () => void; - onFocus?: () => void; - filter?: (data: any) => boolean; -} - -const AutoCompleteAsync = (props: Props) => { - const { - id, - name, - selected, - fetchData, - onChange, - optionLabel = (option: any) => option.label, - optionLabelChip = (option: any) => option.label, - showNOptions, - multiple = false, - compareBy, - debounceTime = 300, - className = "", - placeholder, - disabled = false, - required = false, - error, - filter, - } = props; - const [data, setData] = useState([]); - const [query, setQuery] = useState(""); - const [loading, setLoading] = useState(false); - const { t } = useTranslation(); - - const hasSelection = - (!multiple && selected) || (multiple && selected?.length > 0); - - const fetchDataDebounced = useDebounce(async (searchQuery: string) => { - setLoading(true); - try { - const fetchedData = (await fetchData(searchQuery)) || []; - const filteredData = filter - ? fetchedData.filter((item: any) => filter(item)) - : fetchedData; - - setData( - showNOptions !== undefined - ? filteredData.slice(0, showNOptions) - : filteredData, - ); - } catch (error) { - console.error("Error fetching data:", error); - } finally { - setLoading(false); - } - }, debounceTime); - - useEffect(() => { - fetchDataDebounced(query); - }, [query]); - - return ( - - - - - - hasSelection && !multiple ? optionLabel?.(selected) : "" - } - onChange={({ target }) => setQuery(target.value)} - onFocus={props.onFocus} - onBlur={() => { - props.onBlur?.(); - }} - autoComplete="off" - /> - {!disabled && ( - - - {hasSelection && !loading && !required && ( - - { - e.preventDefault(); - onChange(null); - }} - /> - - {t("clear_selection")} - - - )} - {loading ? ( - - ) : ( - - )} - - - )} - - - - {data?.length === 0 ? ( - - {query !== "" - ? "Nothing found." - : "Start typing to search..."} - - ) : ( - data?.map((item: any) => ( - - {({ selected }) => ( - - - {optionLabel(item)} - {optionLabelChip(item) && ( - - {optionLabelChip(item)} - - )} - - {selected && ( - - )} - - )} - - )) - )} - - - {multiple && selected?.length > 0 && ( - - {selected?.map((option: any, index: number) => ( - - onChange( - selected.filter((item: any) => item.id !== option.id), - ) - } - /> - ))} - - )} - {error && ( - - {error} - - )} - - - - ); -}; - -export default AutoCompleteAsync; diff --git a/src/components/Form/FieldValidators.tsx b/src/components/Form/FieldValidators.tsx deleted file mode 100644 index 989a7aa6688..00000000000 --- a/src/components/Form/FieldValidators.tsx +++ /dev/null @@ -1 +0,0 @@ -export type FieldError = string | undefined; diff --git a/src/components/Form/FormFields/Autocomplete.tsx b/src/components/Form/FormFields/Autocomplete.tsx deleted file mode 100644 index 7e00c89fc7c..00000000000 --- a/src/components/Form/FormFields/Autocomplete.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { - Combobox, - ComboboxButton, - ComboboxInput, - ComboboxOption, - ComboboxOptions, -} from "@headlessui/react"; -import { ReactNode, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { DropdownTransition } from "@/components/Common/HelperComponents"; -import { dropdownOptionClassNames } from "@/components/Form/MultiSelectMenuV2"; - -import { classNames } from "@/Utils/utils"; - -type OptionCallback = (option: T) => R; - -type AutocompleteProps = { - id?: string; - options: readonly T[]; - disabled?: boolean | undefined; - value: V | undefined; - placeholder?: string; - optionLabel: OptionCallback; - optionIcon?: OptionCallback; - optionImage?: OptionCallback; - optionValue?: OptionCallback; - optionDescription?: OptionCallback; - optionDisabled?: OptionCallback; - className?: string; - minQueryLength?: number; - onQuery?: (query: string) => void; - requiredError?: boolean; - isLoading?: boolean; - allowRawInput?: boolean; - error?: string; -} & ( - | { - required?: false; - onChange: OptionCallback; - } - | { - required: true; - onChange: OptionCallback; - } -); - -/** - * Avoid using this component directly. Use `AutocompleteFormField` instead as - * its API is easier to use and compliant with `FormField` based components. - * - * Use this only when you want to hack into the design and get more - * customizability. - */ -export const Autocomplete = (props: AutocompleteProps) => { - const { t } = useTranslation(); - const [query, setQuery] = useState(""); // Ensure lower case - - useEffect(() => { - props.onQuery?.(query); - }, [query]); - - const mappedOptions = props.options.map((option) => { - const label = props.optionLabel(option); - const description = props.optionDescription?.(option); - return { - label, - description, - search: label.toLowerCase(), - icon: props.optionIcon?.(option), - image: props.optionImage?.(option), - value: props.optionValue ? props.optionValue(option) : option, - disabled: props.optionDisabled?.(option), - }; - }); - - const getOptions = () => { - if (!query) return mappedOptions; - - const knownOption = mappedOptions.find( - (o) => o.value == props.value || o.label == props.value, - ); - - if (knownOption) return mappedOptions; - return [ - { - label: query, - description: undefined, - search: query.toLowerCase(), - icon: , - image: undefined, - value: query, - disabled: undefined, - }, - ...mappedOptions, - ]; - }; - - const options = props.allowRawInput ? getOptions() : mappedOptions; - - const value = options.find((o) => props.value == o.value); - - const filteredOptions = - props.onQuery === undefined - ? options.filter((o) => o.search.includes(query)) - : options; - - return ( - - props.onChange(selection?.value)} - > - - - value?.label || ""} - onChange={(event) => setQuery(event.target.value.toLowerCase())} - onBlur={() => value && setQuery("")} - autoComplete="off" - /> - {!props.disabled && ( - - - {value?.icon} - - {value && !props.isLoading && !props.required && ( - - { - e.preventDefault(); - props.onChange(undefined); - }} - /> - - {t("clear_selection")} - - - )} - - {props.isLoading ? ( - - ) : ( - - )} - - - )} - - - - - {props.minQueryLength && query.length < props.minQueryLength ? ( - - {`Please enter at least ${props.minQueryLength} characters to search`} - - ) : filteredOptions.length === 0 ? ( - - No options found - - ) : ( - filteredOptions.map((option, index) => ( - - {({ focus }) => ( - - {option?.image} - - - {option.label} - {option.icon} - - {option.description && ( - - {option.description} - - )} - - - )} - - )) - )} - - - - - - ); -}; diff --git a/src/components/Form/FormFields/FormField.tsx b/src/components/Form/FormFields/FormField.tsx deleted file mode 100644 index f3ad0559e44..00000000000 --- a/src/components/Form/FormFields/FormField.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { FieldError } from "@/components/Form/FieldValidators"; -import { FormFieldBaseProps } from "@/components/Form/FormFields/Utils"; - -import { classNames } from "@/Utils/utils"; - -type LabelProps = { - id?: string | undefined; - required?: boolean; - htmlFor?: string; - children: React.ReactNode; - className?: string | undefined; - noPadding?: boolean; -}; - -export const FieldLabel = (props: LabelProps) => { - return ( - - {props.children} - {props.required && {" *"}} - - ); -}; - -type ErrorProps = { - error: FieldError; - className?: string | undefined; -}; - -export const FieldErrorText = (props: ErrorProps) => { - return ( - - {props.error} - - ); -}; - -/** - * @deprecated use shadcn/ui's solution for form fields instead along with react-hook-form - */ -const FormField = ({ - field, - ...props -}: { - field?: FormFieldBaseProps; - children: React.ReactNode; -}) => { - return ( - - - {field?.label && ( - - {field?.label} - - )} - {field?.labelSuffix && ( - {field?.labelSuffix} - )} - - {props.children} - - - ); -}; - -export default FormField; diff --git a/src/components/Form/FormFields/RadioFormField.tsx b/src/components/Form/FormFields/RadioFormField.tsx deleted file mode 100644 index ca205fcccad..00000000000 --- a/src/components/Form/FormFields/RadioFormField.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { ChangeEventHandler, ReactNode } from "react"; - -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; - -import { classNames } from "@/Utils/utils"; - -type Props = FormFieldBaseProps & { - options: readonly T[]; - optionLabel: (option: T) => React.ReactNode; - optionValue: (option: T) => V; - containerClassName?: string; - unselectLabel?: string; - layout?: "vertical" | "horizontal" | "grid" | "auto"; -}; - -/** - * @deprecated use shadcn/ui's radio-group instead - */ -const RadioFormField = (props: Props) => { - const field = useFormFieldPropsResolver(props); - return ( - - - {props.unselectLabel && ( - - field.handleChange(null)} - /> - {props.unselectLabel} - - )} - {props.options.map((option) => { - const value = props.optionValue(option); - return ( - field.handleChange(e.target.value as V)} - /> - ); - })} - - - ); -}; - -export default RadioFormField; - -export const RadioInput = (props: { - label?: ReactNode; - id?: string; - name?: string; - value?: string; - checked?: boolean; - onChange?: ChangeEventHandler; -}) => { - return ( - - props.onChange?.(e)} - /> - {props.label} - - ); -}; diff --git a/src/components/Form/FormFields/SelectFormField.tsx b/src/components/Form/FormFields/SelectFormField.tsx deleted file mode 100644 index 6b95550299f..00000000000 --- a/src/components/Form/FormFields/SelectFormField.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; -import SelectMenuV2 from "@/components/Form/SelectMenuV2"; - -type OptionCallback = (option: T) => R; - -type SelectFormFieldProps = FormFieldBaseProps & { - placeholder?: React.ReactNode; - options: readonly T[]; - position?: "above" | "below"; - optionLabel: OptionCallback; - optionSelectedLabel?: OptionCallback; - optionDescription?: OptionCallback; - optionIcon?: OptionCallback; - optionValue?: OptionCallback; - optionDisabled?: OptionCallback; - inputClassName?: string; -}; - -/** - * @deprecated use shadcn/ui's select instead - */ -export const SelectFormField = (props: SelectFormFieldProps) => { - const field = useFormFieldPropsResolver(props); - return ( - - field.handleChange(value)} - position={props.position} - placeholder={props.placeholder} - optionLabel={props.optionLabel} - inputClassName={props.inputClassName} - optionSelectedLabel={props.optionSelectedLabel} - optionDescription={props.optionDescription} - optionIcon={props.optionIcon} - optionValue={props.optionValue} - optionDisabled={props.optionDisabled} - requiredError={field.error ? props.required : false} - /> - - ); -}; diff --git a/src/components/Form/FormFields/TextFormField.tsx b/src/components/Form/FormFields/TextFormField.tsx deleted file mode 100644 index 18a4de18341..00000000000 --- a/src/components/Form/FormFields/TextFormField.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { - DetailedHTMLProps, - InputHTMLAttributes, - forwardRef, - useState, -} from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import FormField from "@/components/Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "@/components/Form/FormFields/Utils"; - -import { classNames } from "@/Utils/utils"; - -export type TextFormFieldProps = FormFieldBaseProps & - Omit< - DetailedHTMLProps, HTMLInputElement>, - "onChange" - > & { - inputClassName?: string | undefined; - removeDefaultClasses?: true | undefined; - leading?: React.ReactNode | undefined; - trailing?: React.ReactNode | undefined; - leadingFocused?: React.ReactNode | undefined; - trailingFocused?: React.ReactNode | undefined; - trailingPadding?: string | undefined; - leadingPadding?: string | undefined; - suggestions?: string[]; - clearable?: boolean | undefined; - }; - -/** - * @deprecated use shadcn/ui's Input instead - */ -const TextFormField = forwardRef((props: TextFormFieldProps, ref) => { - const field = useFormFieldPropsResolver(props); - const { leading, trailing } = props; - const leadingFocused = props.leadingFocused || props.leading; - const trailingFocused = props.trailingFocused || props.trailing; - const hasLeading = !!(leading || leadingFocused); - const hasTrailing = !!(trailing || trailingFocused); - const hasIcon = hasLeading || hasTrailing; - const [showPassword, setShowPassword] = useState(false); - - const getPasswordFieldType = () => { - return showPassword ? "text" : "password"; - }; - - const minError = - typeof props.min !== "undefined" && - typeof field.value !== "undefined" && - parseFloat(`${props.min}`) > parseFloat(`${field.value}`) - ? `Value can not be smaller than ${props.min}` - : undefined; - const maxError = - typeof props.max !== "undefined" && - typeof field.value !== "undefined" && - parseFloat(`${props.max}`) < parseFloat(`${field.value}`) - ? `Value can not be greater than ${props.max}` - : undefined; - - const labelSuffixWithThreshold = ( - - {field.labelSuffix} - - ); - - let child = ( - - } - id={field.id} - className={classNames( - "cui-input-base peer", - hasLeading && (props.leadingPadding || "pl-10"), - hasTrailing && (props.trailingPadding || "pr-10"), - field.error && "border-danger-500", - props.inputClassName, - )} - disabled={field.disabled} - type={props.type === "password" ? getPasswordFieldType() : props.type} - name={field.name} - value={field.value} - required={field.required} - onChange={(e) => field.handleChange(e.target.value)} - /> - {props.clearable && field.value && ( - field.handleChange("")} - aria-label="Clear input" - > - - - )} - - ); - - if (props.type === "password") { - child = ( - - {child} - setShowPassword(!showPassword)} - > - - - - ); - } - - if (hasIcon) { - const _leading = - leading === leadingFocused ? ( - - {leading} - - ) : ( - <> - - {leading} - - - {leadingFocused} - - > - ); - const _trailing = - trailing === trailingFocused ? ( - - {trailing} - - ) : ( - <> - - {trailing} - - - {trailingFocused} - - > - ); - - child = ( - - {(leading || leadingFocused) && _leading} - {child} - {(trailing || trailingFocused) && _trailing} - - ); - } - - if ( - props.suggestions?.length && - !props.suggestions.includes(`${field.value}`) - ) { - child = ( - - {child} - - {props.suggestions.map((suggestion) => ( - field.handleChange(suggestion)} - > - {suggestion} - - ))} - - - ); - } - - return ( - - {child} - - ); -}); -TextFormField.displayName = "TextFormField"; - -export default TextFormField; diff --git a/src/components/Form/FormFields/Utils.ts b/src/components/Form/FormFields/Utils.ts deleted file mode 100644 index 1e88bcbd6a0..00000000000 --- a/src/components/Form/FormFields/Utils.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { FocusEvent } from "react"; - -import { FieldError } from "@/components/Form/FieldValidators"; - -export type FieldChangeEvent = { name: string; value: T }; -export type FieldChangeEventHandler = (event: FieldChangeEvent) => void; - -/** - * The base props for a form field. - * - * If a form context is provided, the field will be registered with the form - * and the onChange, value, and error props will be ignored. - * - * If a form context is not provided, the field will be treated as a standalone - * field. - * - * @template T The type of the field value. - * @template Form The type of the form details. - */ -export type FormFieldBaseProps = { - label?: React.ReactNode; - labelSuffix?: React.ReactNode; - disabled?: boolean; - className?: string; - required?: boolean; - labelClassName?: string; - errorClassName?: string; - name: string; - validate?: undefined; - id?: string; - onChange: FieldChangeEventHandler; - value?: T; - error?: FieldError; - onFocus?: (event: FocusEvent) => void; - onBlur?: (event: FocusEvent) => void; -}; - -/** - * Resolves the props for a form field. - * If a form context is provided, the field will be registered with the form. - * Otherwise, the field will be treated as a standalone field. - * - * @param props The props for the field. - * @returns The resolved props along with a handleChange function. - */ -export const useFormFieldPropsResolver = (props: FormFieldBaseProps) => { - const handleChange = (value: T) => - props.onChange({ name: props.name, value }); - - return { - ...props, - id: props.id ?? props.name, - name: props.name, - onChange: props.onChange, - value: props.value, - error: props.error, - handleChange, - }; -}; diff --git a/src/components/Form/MultiSelectMenuV2.tsx b/src/components/Form/MultiSelectMenuV2.tsx deleted file mode 100644 index b67da3656f4..00000000000 --- a/src/components/Form/MultiSelectMenuV2.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { ReactNode } from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { classNames } from "@/Utils/utils"; - -interface MultiSelectOptionChipProps { - label: ReactNode; - onRemove?: () => void; -} - -export const MultiSelectOptionChip = ({ - label, - onRemove, -}: MultiSelectOptionChipProps) => { - return ( - - {label} - {onRemove && ( - { - e.stopPropagation(); - onRemove(); - }} - > - - - )} - - ); -}; - -interface OptionRenderPropArg { - focus: boolean; - selected: boolean; - disabled: boolean; -} - -export const dropdownOptionClassNames = ({ - focus, - selected, - disabled, -}: OptionRenderPropArg) => { - return classNames( - "group/option relative w-full cursor-default select-none p-4 text-sm transition-colors duration-75 ease-in-out", - !disabled && focus && "bg-primary-500 text-white", - !disabled && !focus && selected && "text-primary-500", - !disabled && !focus && !selected && "text-secondary-900", - disabled && "cursor-not-allowed text-secondary-600", - selected ? "font-semibold" : "font-normal", - ); -}; diff --git a/src/components/Form/SelectMenuV2.tsx b/src/components/Form/SelectMenuV2.tsx deleted file mode 100644 index a13d03607c6..00000000000 --- a/src/components/Form/SelectMenuV2.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { - Label, - Listbox, - ListboxButton, - ListboxOption, - ListboxOptions, -} from "@headlessui/react"; -import { ReactNode } from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { dropdownOptionClassNames } from "@/components/Form/MultiSelectMenuV2"; - -import { classNames } from "@/Utils/utils"; - -type OptionCallback = (option: T) => R; - -type SelectMenuProps = { - id?: string; - options: readonly T[]; - disabled?: boolean | undefined; - value: V | undefined; - placeholder?: ReactNode; - position?: "above" | "below"; - optionLabel: OptionCallback; - optionSelectedLabel?: OptionCallback; - optionDescription?: OptionCallback; - optionIcon?: OptionCallback; - optionValue?: OptionCallback; - optionDisabled?: OptionCallback; - showIconWhenSelected?: boolean; - showChevronIcon?: boolean; - className?: string; - inputClassName?: string; - requiredError?: boolean; - onFocus?: () => void; - onBlur?: () => void; -} & ( - | { - required?: false; - onChange: OptionCallback; - } - | { - required: true; - onChange: OptionCallback; - } -); - -/** - * Avoid using this component directly. Use `SelectFormField` instead as its API - * is easier to use and compliant with `FormField` based components. - * - * Use this only when you want to hack into the design and get more - * customizability. - */ -const SelectMenuV2 = (props: SelectMenuProps) => { - const valueOptions = props.options.map((option) => { - const label = props.optionLabel(option); - return { - label, - selectedLabel: props.optionSelectedLabel - ? props.optionSelectedLabel(option) - : label, - description: props.optionDescription?.(option), - icon: props.optionIcon?.(option), - value: props.optionValue ? props.optionValue(option) : option, - disabled: props.optionDisabled?.(option), - }; - }); - - const showChevronIcon = props.showChevronIcon ?? true; - - const placeholder = - valueOptions?.length > 0 ? (props.placeholder ?? "Select") : "No options"; - const defaultOption = { - label: placeholder, - selectedLabel: ( - {placeholder} - ), - description: undefined, - icon: undefined, - value: undefined, - disabled: undefined, - }; - - const options = props.required - ? valueOptions - : [defaultOption, ...valueOptions]; - - const value = options.find((o) => props.value == o.value) ?? defaultOption; - - return ( - - props.onChange(selection.value)} - > - <> - {props.placeholder} - - - - - {props.showIconWhenSelected && value?.icon && ( - - {value.icon} - - )} - - {value.selectedLabel} - - - {showChevronIcon && ( - - )} - - - - - {options.map((option, index) => ( - - {({ focus, selected }) => ( - - - {option.label} - {props.optionIcon - ? option.icon - : selected && ( - - )} - - {option.description && ( - - {option.description} - - )} - - )} - - ))} - - - - > - - - ); -}; - -export default SelectMenuV2; diff --git a/src/components/Location/LocationHistorySheet.tsx b/src/components/Location/LocationHistorySheet.tsx new file mode 100644 index 00000000000..de0ddb25abb --- /dev/null +++ b/src/components/Location/LocationHistorySheet.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from "react-i18next"; + +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; + +import { LocationHistory } from "@/types/emr/encounter"; + +import { LocationTree } from "./LocationTree"; + +interface LocationHistorySheetProps { + trigger: React.ReactNode; + history: LocationHistory[]; +} + +export function LocationHistorySheet({ + trigger, + history, +}: LocationHistorySheetProps) { + const { t } = useTranslation(); + + return ( + + {trigger} + + + {t("location_history")} + + + {history.map((item, index) => ( + + + + ))} + + + + ); +} diff --git a/src/components/Location/LocationSearch.tsx b/src/components/Location/LocationSearch.tsx index bd21bb26273..a27d8e8d1ec 100644 --- a/src/components/Location/LocationSearch.tsx +++ b/src/components/Location/LocationSearch.tsx @@ -20,7 +20,7 @@ import locationApi from "@/types/location/locationApi"; interface LocationSearchProps { facilityId: string; - mode?: "kind" | "location"; + mode?: "kind" | "instance"; onSelect: (location: LocationList) => void; disabled?: boolean; value?: LocationList | null; @@ -44,7 +44,6 @@ export function LocationSearch({ }), enabled: facilityId !== "preview", }); - return ( @@ -57,7 +56,7 @@ export function LocationSearch({ - + + + + + {location.name} + + + {children} + {isLast && datetime && ( + + {format(new Date(datetime), "MMM d, yyyy h:mm a")} + + )} + + ); + } + + return ( + + + + + + {location.name} + + + {children} + {isLast && datetime && ( + + {format(new Date(datetime), "MMM d, yyyy h:mm a")} + + )} + + + ); +} + +export function LocationTree({ + location, + datetime, + isLatest, + showTimeline = false, +}: LocationPathProps) { + return ( + + {showTimeline && ( + + + + + + {!isLatest && } + + )} + + + + + ); +} diff --git a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx index 39f477f6d65..4a0bc9d63ac 100644 --- a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx +++ b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx @@ -21,7 +21,6 @@ import { EmptyState } from "@/components/Medicine/MedicationRequestTable"; import { getFrequencyDisplay } from "@/components/Medicine/MedicationsTable"; import { formatDosage } from "@/components/Medicine/utils"; -import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { formatName } from "@/Utils/utils"; @@ -29,7 +28,12 @@ import { MedicationAdministration, MedicationAdministrationRequest, } from "@/types/emr/medicationAdministration/medicationAdministration"; -import { MedicationRequestRead } from "@/types/emr/medicationRequest"; +import medicationAdministrationApi from "@/types/emr/medicationAdministration/medicationAdministrationApi"; +import { + ACTIVE_MEDICATION_STATUSES, + INACTIVE_MEDICATION_STATUSES, + MedicationRequestRead, +} from "@/types/emr/medicationRequest"; import medicationRequestApi from "@/types/emr/medicationRequest/medicationRequestApi"; import { MedicineAdminDialog } from "./MedicineAdminDialog"; @@ -40,14 +44,6 @@ import { createMedicationAdministrationRequest, } from "./utils"; -const ACTIVE_STATUSES = ["active", "on-hold", "draft", "unknown"] as const; -const INACTIVE_STATUSES = [ - "ended", - "completed", - "cancelled", - "entered_in_error", -] as const; - // Utility Functions function isTimeInSlot( date: Date, @@ -211,9 +207,13 @@ const MedicationRow: React.FC = ({ onEditAdministration, onDiscontinue, }) => { + const isInactive = INACTIVE_MEDICATION_STATUSES.includes( + medication.status as (typeof INACTIVE_MEDICATION_STATUSES)[number], + ); + return ( - + {medication.medication?.display} @@ -247,7 +247,7 @@ const MedicationRow: React.FC = ({ return ( {administrationRecords?.map((admin) => { const colorClass = @@ -260,16 +260,34 @@ const MedicationRow: React.FC = ({ className={`flex font-medium items-center gap-2 rounded-md p-2 mb-2 cursor-pointer justify-between border ${colorClass}`} onClick={() => onEditAdministration(medication, admin)} > - - - {new Date(admin.occurrence_period_start).toLocaleTimeString( - "en-US", - { + + + + {new Date( + admin.occurrence_period_start, + ).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, - }, - )} + })} + + + {admin.occurrence_period_end && ( + <> + {"- "} + {new Date( + admin.occurrence_period_end, + ).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + })} + > + )} + {admin.note && ( = ({ ); })} - - {ACTIVE_STATUSES.includes( - medication.status as (typeof ACTIVE_STATUSES)[number], + + {ACTIVE_MEDICATION_STATUSES.includes( + medication.status as (typeof ACTIVE_MEDICATION_STATUSES)[number], ) && ( @@ -336,6 +356,7 @@ export const AdministrationTab: React.FC = ({ const currentDate = new Date(); const [endSlotDate, setEndSlotDate] = useState(currentDate); const [showStopped, setShowStopped] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); const [endSlotIndex, setEndSlotIndex] = useState( Math.floor(currentDate.getHours() / 6), ); @@ -369,7 +390,7 @@ export const AdministrationTab: React.FC = ({ queryParams: { encounter: encounterId, limit: 100, - status: ACTIVE_STATUSES.join(","), + status: ACTIVE_MEDICATION_STATUSES.join(","), }, }), enabled: !!patientId, @@ -382,7 +403,7 @@ export const AdministrationTab: React.FC = ({ queryParams: { encounter: encounterId, limit: 100, - status: INACTIVE_STATUSES.join(","), + status: INACTIVE_MEDICATION_STATUSES.join(","), }, }), enabled: !!patientId, @@ -390,7 +411,7 @@ export const AdministrationTab: React.FC = ({ const { data: administrations, refetch: refetchAdministrations } = useQuery({ queryKey: ["medication_administrations", patientId, visibleSlots], - queryFn: query(routes.medicationAdministration.list, { + queryFn: query(medicationAdministrationApi.list, { pathParams: { patientId }, queryParams: { encounter: encounterId, @@ -581,36 +602,33 @@ export const AdministrationTab: React.FC = ({ ] : activeMedications?.results || []; + const filteredMedications = medications.filter( + (med: MedicationRequestRead) => { + if (!searchQuery.trim()) return true; + const searchTerm = searchQuery.toLowerCase().trim(); + const medicationName = med.medication?.display?.toLowerCase() || ""; + return medicationName.includes(searchTerm); + }, + ); + + let content; if (!activeMedications || !stoppedMedications) { - return ( + content = ( ); - } - - if (!medications?.length) { - return ( + } else if (!medications?.length) { + content = ( ); - } - - return ( - - - setIsSheetOpen(true)} - > - - {t("administer_medicine")} - - - + } else if (searchQuery && !filteredMedications.length) { + content = ; + } else { + content = ( @@ -684,7 +702,7 @@ export const AdministrationTab: React.FC = ({ {/* Medication rows */} - {medications?.map((medication) => ( + {filteredMedications?.map((medication) => ( = ({ + ); + } + + return ( + + + + + + setSearchQuery(e.target.value)} + className="flex-1 bg-transparent text-sm outline-none placeholder:text-gray-500" + /> + {searchQuery && ( + setSearchQuery("")} + > + + + )} + + + setIsSheetOpen(true)} + > + + {t("administer_medicine")} + + + + {content} {selectedMedication && administrationRequest && ( { onOpenChange(false); toast.success(t("medication_administration_saved")); diff --git a/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx b/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx index 62b48d65b65..3519619d08a 100644 --- a/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx +++ b/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx @@ -83,29 +83,37 @@ export const MedicineAdminForm: React.FC = ({ // Validate and notify parent whenever times change useEffect(() => { - if ( - !administrationRequest.occurrence_period_start || - !administrationRequest.occurrence_period_end - ) { + if (!administrationRequest.occurrence_period_start) { isValid?.(false); return; } const startDate = new Date(administrationRequest.occurrence_period_start); - const endDate = new Date(administrationRequest.occurrence_period_end); - const startError = validateDateTime(startDate, true); - const endError = validateDateTime(endDate, false); - setStartTimeError(startError); - setEndTimeError(endError); - isValid?.(!startError && !endError); + // Only validate end time if status is completed or if end time is provided + if ( + administrationRequest.status === "completed" || + administrationRequest.occurrence_period_end + ) { + if (!administrationRequest.occurrence_period_end) { + isValid?.(false); + return; + } + const endDate = new Date(administrationRequest.occurrence_period_end); + const endError = validateDateTime(endDate, false); + setEndTimeError(endError); + isValid?.(!startError && !endError); + } else { + setEndTimeError(""); + isValid?.(!startError); + } }, [ administrationRequest.occurrence_period_start, administrationRequest.occurrence_period_end, + administrationRequest.status, isValid, - validateDateTime, ]); const handleDateChange = (newTime: string, isStartTime: boolean) => { @@ -238,9 +246,21 @@ export const MedicineAdminForm: React.FC = ({ {t("status")} - onChange({ ...administrationRequest, status: value }) - } + onValueChange={(value: MedicationAdministrationStatus) => { + const newRequest = { ...administrationRequest, status: value }; + + if (value === "in_progress" || value === "not_done") { + delete newRequest.occurrence_period_end; + } else if ( + value === "completed" && + !administrationRequest.occurrence_period_end + ) { + newRequest.occurrence_period_end = + administrationRequest.occurrence_period_start; + } + + onChange(newRequest); + }} > @@ -275,13 +295,22 @@ export const MedicineAdminForm: React.FC = ({ onValueChange={(newValue) => { setIsPastTime(newValue === "yes"); if (newValue === "no") { - // Set both times to current time const now = new Date().toISOString(); - onChange({ + const newRequest = { ...administrationRequest, occurrence_period_start: now, - occurrence_period_end: now, - }); + }; + + if ( + !( + administrationRequest.status === "in_progress" || + administrationRequest.status === "not_done" + ) + ) { + newRequest.occurrence_period_end = now; + } + + onChange(newRequest); } }} className="flex gap-4" @@ -370,7 +399,8 @@ export const MedicineAdminForm: React.FC = ({ disabled={ !isPastTime || (!!administrationRequest.id && - administrationRequest.status !== "in_progress") + administrationRequest.status !== "in_progress") || + administrationRequest.status === "in_progress" } > @@ -411,7 +441,8 @@ export const MedicineAdminForm: React.FC = ({ disabled={ !isPastTime || (!!administrationRequest.id && - administrationRequest.status !== "in_progress") + administrationRequest.status !== "in_progress") || + administrationRequest.status === "in_progress" } /> diff --git a/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx b/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx index 80b212081c4..8b22e738b59 100644 --- a/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx +++ b/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx @@ -73,7 +73,7 @@ const MedicineListItem = ({ @@ -122,12 +122,9 @@ export function MedicineAdminSheet({ const formRef = useRef(null); const { mutate: upsertAdministrations, isPending } = useMutation({ - mutationFn: mutate( - medicationAdministrationApi.upsertMedicationAdministration, - { - pathParams: { patientId }, - }, - ), + mutationFn: mutate(medicationAdministrationApi.upsert, { + pathParams: { patientId }, + }), onSuccess: () => { toast.success(t("medication_administration_saved")); handleClose(); diff --git a/src/components/Medicine/MedicationRequestTable/index.tsx b/src/components/Medicine/MedicationRequestTable/index.tsx index 389093ae429..2e811025048 100644 --- a/src/components/Medicine/MedicationRequestTable/index.tsx +++ b/src/components/Medicine/MedicationRequestTable/index.tsx @@ -114,7 +114,7 @@ export default function MedicationRequestTable({ - + - + - + @@ -161,9 +166,14 @@ export default function MedicationRequestTable({ {t("edit")} - + {t("print")} @@ -184,7 +194,10 @@ export default function MedicationRequestTable({ - + {!!stoppedMedications?.results?.length && ( { +export const MedicationsTable = ({ + patientId, + encounterId, +}: MedicationsTableProps) => { const { t } = useTranslation(); - if (!medications?.length) { + const { data: medications, isLoading } = useQuery({ + queryKey: ["medication_requests", patientId, encounterId], + queryFn: query(medicationRequestApi.list, { + pathParams: { patientId }, + queryParams: { encounter: encounterId, limit: 50, offset: 0 }, + }), + }); + if (isLoading) { return ( - {t("no_medications_found_for_this_encounter")} + ); } - return ( @@ -59,15 +73,22 @@ export const MedicationsTable = ({ medications }: MedicationsTableProps) => { - {medications.map((medication) => { + {medications?.results.map((medication) => { const instruction = medication.dosage_instruction[0]; const frequency = getFrequencyDisplay(instruction?.timing); const dosage = formatDosage(instruction); const duration = instruction?.timing?.repeat?.bounds_duration; const remarks = formatSig(instruction); const notes = medication.note; + const isInactive = INACTIVE_MEDICATION_STATUSES.includes( + medication.status as (typeof INACTIVE_MEDICATION_STATUSES)[number], + ); + return ( - + {medication.medication?.display} diff --git a/src/components/Patient/EncounterQuestionnaire.tsx b/src/components/Patient/EncounterQuestionnaire.tsx index f31aeb0839a..43eb6bca2bd 100644 --- a/src/components/Patient/EncounterQuestionnaire.tsx +++ b/src/components/Patient/EncounterQuestionnaire.tsx @@ -25,10 +25,7 @@ export default function EncounterQuestionnaire({ }: Props) { const { goBack } = useAppHistory(); return ( - + - - - ); -} diff --git a/src/components/Patient/MedicationStatementList.tsx b/src/components/Patient/MedicationStatementList.tsx index 39c8e334cda..112c42414a4 100644 --- a/src/components/Patient/MedicationStatementList.tsx +++ b/src/components/Patient/MedicationStatementList.tsx @@ -2,9 +2,16 @@ import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, @@ -14,88 +21,99 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import routes from "@/Utils/request/api"; +import { Avatar } from "@/components/Common/Avatar"; + import query from "@/Utils/request/query"; -import { formatDateTime } from "@/Utils/utils"; +import { formatDateTime, formatName } from "@/Utils/utils"; +import { + MEDICATION_STATEMENT_STATUS_STYLES, + MedicationStatementRead, +} from "@/types/emr/medicationStatement"; +import medicationStatementApi from "@/types/emr/medicationStatement/medicationStatementApi"; interface MedicationStatementListProps { patientId: string; + className?: string; + isPrintPreview?: boolean; } interface MedicationRowProps { - statement: any; + statement: MedicationStatementRead; isEnteredInError?: boolean; - index: number; + isPrintPreview?: boolean; } function MedicationRow({ statement, isEnteredInError, - index, + isPrintPreview = false, }: MedicationRowProps) { + const { t } = useTranslation(); + return ( - - - - {statement.medication.display ?? statement.medication.code} - - - {statement.medication.display ?? statement.medication.code} - - - - - - - {statement.dosage_text} - - - {statement.dosage_text} - - + + {statement.medication.display ?? statement.medication.code} + {statement.dosage_text} {statement.status} - + {[statement.effective_period?.start, statement.effective_period?.end] .map((date) => formatDateTime(date)) .join(" - ")} - - - - {statement.reason} - - - {statement.reason} - - + {statement.reason} + + {statement.note ? ( + + {isPrintPreview ? ( + {statement.note} + ) : ( + + + + {t("see_note")} + + + + + {statement.note} + + + + )} + + ) : ( + "-" + )} - - - - {statement.note} - - - {statement.note} - - + + + + {formatName(statement.created_by)} + ); @@ -103,24 +121,28 @@ function MedicationRow({ export function MedicationStatementList({ patientId, + className, + isPrintPreview = false, }: MedicationStatementListProps) { const { t } = useTranslation(); - const [showEnteredInError, setShowEnteredInError] = useState(false); + const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); const { data: medications, isLoading } = useQuery({ queryKey: ["medication_statements", patientId], - queryFn: query(routes.medicationStatement.list, { + queryFn: query(medicationStatementApi.list, { pathParams: { patientId }, }), }); if (isLoading) { return ( - - + + {t("ongoing_medications")} - + @@ -138,11 +160,15 @@ export function MedicationStatementList({ if (!filteredMedications?.length) { return ( - - + + {t("ongoing_medications")} - + {t("no_ongoing_medications")} @@ -150,52 +176,75 @@ export function MedicationStatementList({ } return ( - - + + {t("ongoing_medications")} ({filteredMedications.length}) - - + + - - {t("medication")} - {t("dosage")} - {t("status")} - {t("medication_taken_between")} - {t("reason")} - {t("note")} + + + {t("medication")} + + + {t("dosage")} + + + {t("status")} + + + {t("medication_taken_between")} + + + {t("reason")} + + + {t("notes")} + + + {t("logged_by")} + - {filteredMedications.map((statement, index) => { - const isEnteredInError = statement.status === "entered_in_error"; - - return ( - <> - - > - ); - })} + {[ + ...filteredMedications.filter( + (medication) => medication.status !== "entered_in_error", + ), + ...(showEnteredInError + ? filteredMedications.filter( + (medication) => medication.status === "entered_in_error", + ) + : []), + ].map((statement) => ( + + ))} {hasEnteredInErrorRecords && !showEnteredInError && ( - - setShowEnteredInError(true)} - className="text-xs underline text-gray-500" - > - {t("view_all")} - - + <> + + + setShowEnteredInError(true)} + className="text-xs underline text-gray-500" + > + {t("view_all")} + + + > )} diff --git a/src/components/Patient/PatientDetailsTab/Appointments.tsx b/src/components/Patient/PatientDetailsTab/Appointments.tsx index 603b5aead0a..d0ec294fb48 100644 --- a/src/components/Patient/PatientDetailsTab/Appointments.tsx +++ b/src/components/Patient/PatientDetailsTab/Appointments.tsx @@ -20,7 +20,7 @@ import { PatientProps } from "@/components/Patient/PatientDetailsTab"; import query from "@/Utils/request/query"; import { formatDateTime, formatName } from "@/Utils/utils"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; export const Appointments = (props: PatientProps) => { const { patientData, facilityId, patientId } = props; diff --git a/src/components/Patient/PatientDetailsTab/PatientFiles.tsx b/src/components/Patient/PatientDetailsTab/PatientFiles.tsx index 7f65fd8c00c..df9789b35d7 100644 --- a/src/components/Patient/PatientDetailsTab/PatientFiles.tsx +++ b/src/components/Patient/PatientDetailsTab/PatientFiles.tsx @@ -8,6 +8,7 @@ export const PatientFilesTab = (props: PatientProps) => { type="patient" facilityId={props.facilityId} patientId={props.patientData.id} + patient={props.patientData} /> ); }; diff --git a/src/components/Patient/PatientDetailsTab/PatientUsers.tsx b/src/components/Patient/PatientDetailsTab/PatientUsers.tsx index 0cb118704ad..bbbafddec5b 100644 --- a/src/components/Patient/PatientDetailsTab/PatientUsers.tsx +++ b/src/components/Patient/PatientDetailsTab/PatientUsers.tsx @@ -1,8 +1,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { t } from "i18next"; import { useState } from "react"; +import { Trans } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { @@ -16,7 +19,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Select, SelectContent, @@ -32,6 +35,7 @@ import { SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; +import { TooltipComponent } from "@/components/ui/tooltip"; import { Avatar } from "@/components/Common/Avatar"; import UserSelector from "@/components/Common/UserSelector"; @@ -127,9 +131,11 @@ function AddUserSheet({ patientId }: AddUserSheetProps) { className="h-12 w-12" /> - - {formatDisplayName(selectedUser)} - + + + {formatDisplayName(selectedUser)} + + {selectedUser.email} @@ -225,7 +231,7 @@ export const PatientUsers = (props: PatientProps) => { const ManageUsers = () => { if (!users?.results?.length) { return ( - + {t("no_user_assigned")} ); @@ -235,7 +241,7 @@ export const PatientUsers = (props: PatientProps) => { {users?.results.map((user) => ( @@ -245,10 +251,20 @@ export const PatientUsers = (props: PatientProps) => { imageUrl={user.profile_picture_url} /> - - {formatDisplayName(user)} + + + + {formatDisplayName(user)} + + - {user.username} + + + + {user.username} + + + @@ -257,17 +273,24 @@ export const PatientUsers = (props: PatientProps) => { variant="ghost" size="icon" data-cy="patient-user-remove-button" + className="absolute top-0 right-0" > - + {t("remove_user")} - {t("are_you_sure_want_to_remove", { - name: formatDisplayName(user), - })} + + ), + }} + /> @@ -275,7 +298,7 @@ export const PatientUsers = (props: PatientProps) => { removeUser(user.id)} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + className={cn(buttonVariants({ variant: "destructive" }))} > {t("remove")} diff --git a/src/components/Patient/PatientHome.tsx b/src/components/Patient/PatientHome.tsx index 0807023978a..69df69c4143 100644 --- a/src/components/Patient/PatientHome.tsx +++ b/src/components/Patient/PatientHome.tsx @@ -247,30 +247,6 @@ export const PatientHome = (props: { - - {/* setOpenAssignVolunteerDialog(false)} - description={ - - setAssignedVolunteer(user.value)} - userType={"Volunteer"} - name={"assign_volunteer"} - error={errors.assignedVolunteer} - /> - - } - action={ - assignedVolunteer || !patientData.assigned_to - ? t("assign") - : t("unassign") - } - onConfirm={handleAssignedVolunteer} - /> */} ); }; diff --git a/src/components/Patient/PatientInfoCard.tsx b/src/components/Patient/PatientInfoCard.tsx index 3f2a2234a6f..18e52410b4b 100644 --- a/src/components/Patient/PatientInfoCard.tsx +++ b/src/components/Patient/PatientInfoCard.tsx @@ -12,16 +12,28 @@ import { Link } from "raviger"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { @@ -31,8 +43,8 @@ import { } from "@/components/ui/popover"; import { Avatar } from "@/components/Common/Avatar"; - -import useQuestionnaireOptions from "@/hooks/useQuestionnaireOptions"; +import { LocationHistorySheet } from "@/components/Location/LocationHistorySheet"; +import { LocationTree } from "@/components/Location/LocationTree"; import { PLUGIN_Component } from "@/PluginEngine"; import routes from "@/Utils/request/api"; @@ -53,7 +65,6 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { const { patient, encounter } = props; const { t } = useTranslation(); const queryClient = useQueryClient(); - const questionnaireOptions = useQuestionnaireOptions("encounter_actions"); const { mutate: updateEncounter } = useMutation({ mutationFn: mutate(routes.encounter.update, { @@ -101,7 +112,18 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { className="mb-2 flex flex-col text-xl font-semibold capitalize lg:hidden" id="patient-name-consultation" > - {patient.name} + + {patient.name} + + {formatPatientAge(patient, true)} •{" "} {t(`GENDER__${patient.gender}`)} @@ -115,7 +137,18 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { className="hidden flex-row text-xl font-semibold capitalize lg:flex" id="patient-name-consultation" > - {patient.name} + + {patient.name} + + {formatPatientAge(patient, true)} •{" "} {t(`GENDER__${patient.gender}`)} @@ -124,28 +157,28 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { - Start Date + {t("start_date")} {props.encounter.period.start ? formatDateTime(props.encounter.period.start) - : "Not started"} + : t("not_started")} - End Date + {t("end_date")} {props.encounter.period.end ? formatDateTime(props.encounter.period.end) - : "Ongoing"} + : t("ongoing")} {props.encounter.external_identifier && ( - Hospital Identifier + {t("hospital_identifier")} {props.encounter.external_identifier} @@ -183,7 +216,9 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { - Status History + + {t("status_history")} + {encounter.status_history.history.map( (history, index) => ( - {props.encounter.current_location && ( - - - - - - {props.encounter.current_location.name} - - - - - - - - Current Location - - - {props.encounter.current_location.name} - - - {props.encounter.current_location.description} - - - - - Move Patient - - - - - )} @@ -261,7 +259,9 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { - Class History + + {t(`class_history`)} + {encounter.encounter_class_history.history.map( (history, index) => ( } /> + {props.encounter.current_location ? ( + + + + + + {props.encounter.current_location.name} + + + + + + + + + {t("location")} + + + + + + {t("history")} + + + } + /> + + + + + + + {t("update_location")} + + + + + + ) : ( + + + + {t("add_location")} + + + )} @@ -368,36 +444,69 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { className="flex w-full flex-col gap-3 lg:w-auto 2xl:flex-row" data-cy="update-encounter-button" > - - - - {t("update")} - - - - - {questionnaireOptions.map((option) => ( - + + + + + {t("update")} + + + + + {t("actions")} + - {t(option.title)} + {t("treatment_summary")} - ))} - - {t("actions")} - - {t("mark_as_complete")} - + + + {t("discharge_summary")} + + + + e.preventDefault()}> + {t("mark_as_complete")} + + + + + + + + {t("mark_as_complete")} + + {t("mark_encounter_as_complete_confirmation")} + + + - - + + + {t("cancel")} + + + {t("mark_as_complete")} + + + + )} diff --git a/src/components/Patient/PatientRegistration.tsx b/src/components/Patient/PatientRegistration.tsx index 9acf238d3de..1c1113081ef 100644 --- a/src/components/Patient/PatientRegistration.tsx +++ b/src/components/Patient/PatientRegistration.tsx @@ -42,11 +42,7 @@ import DuplicatePatientDialog from "@/components/Facility/DuplicatePatientDialog import useAppHistory from "@/hooks/useAppHistory"; -import { - BLOOD_GROUP_CHOICES, // DOMESTIC_HEALTHCARE_SUPPORT_CHOICES, - GENDER_TYPES, // OCCUPATION_TYPES, - //RATION_CARD_CATEGORY, // SOCIOECONOMIC_STATUS_CHOICES , -} from "@/common/constants"; +import { BLOOD_GROUP_CHOICES, GENDER_TYPES } from "@/common/constants"; import { GENDERS } from "@/common/constants"; import countryList from "@/common/static/countries.json"; @@ -87,9 +83,9 @@ export default function PatientRegistration( z .object({ name: z.string().nonempty(t("name_is_required")), - phone_number: validators.phoneNumber.required, + phone_number: validators().phoneNumber.required, same_phone_number: z.boolean(), - emergency_phone_number: validators.phoneNumber.required, + emergency_phone_number: validators().phoneNumber.required, gender: z.enum(GENDERS, { required_error: t("gender_is_required") }), blood_group: z.enum(BLOOD_GROUPS, { required_error: t("blood_group_is_required"), @@ -120,7 +116,10 @@ export default function PatientRegistration( .min(100000, t("pincode_must_be_6_digits")) .max(999999, t("pincode_must_be_6_digits")), nationality: z.string().nonempty(t("nationality_is_required")), - geo_organization: z.string().uuid().optional(), + geo_organization: z + .string() + .uuid({ message: t("geo_organization_is_required") }) + .optional(), }) .refine( (data) => (data.age_or_dob === "dob" ? !!data.date_of_birth : true), @@ -175,7 +174,11 @@ export default function PatientRegistration( }, }); - const { mutate: updatePatient, isPending: isUpdatingPatient } = useMutation({ + const { + mutate: updatePatient, + isPending: isUpdatingPatient, + isSuccess: isUpdateSuccess, + } = useMutation({ mutationFn: mutate(routes.updatePatient, { pathParams: { id: patientId || "" }, }), @@ -291,7 +294,7 @@ export default function PatientRegistration( useNavigationPrompt( form.formState.isDirty && !isCreatingPatient && - !isUpdatingPatient && + !(isUpdatingPatient || isUpdateSuccess) && !showDuplicate, t("unsaved_changes"), ); @@ -548,7 +551,7 @@ export default function PatientRegistration( "age", e.target.value ? Number(e.target.value) - : (undefined as unknown as number), // intentionally setting to undefined, when the value is empty to avoid 0 in the input field + : (null as unknown as number), ) } data-cy="age-input" @@ -557,10 +560,16 @@ export default function PatientRegistration( {form.getValues("age") && ( - - {t("year_of_birth")}:{" "} - {new Date().getFullYear() - - Number(form.getValues("age"))} + + {Number(form.getValues("age")) <= 0 ? ( + Invalid age + ) : ( + + {t("year_of_birth")}:{" "} + {new Date().getFullYear() - + Number(form.getValues("age"))} + + )} )} @@ -734,10 +743,13 @@ export default function PatientRegistration( {showDuplicate && ( { - handleDialogClose("close"); + onOpenChange={(open) => { + if (!open) { + handleDialogClose("close"); + } }} /> )} diff --git a/src/components/Patient/TreatmentSummary.tsx b/src/components/Patient/TreatmentSummary.tsx new file mode 100644 index 00000000000..ad87eee91a6 --- /dev/null +++ b/src/components/Patient/TreatmentSummary.tsx @@ -0,0 +1,253 @@ +import careConfig from "@careConfig"; +import { useQuery } from "@tanstack/react-query"; +import { format } from "date-fns"; +import { t } from "i18next"; + +import PrintPreview from "@/CAREUI/misc/PrintPreview"; + +import QuestionnaireResponsesList from "@/components/Facility/ConsultationDetails/QuestionnaireResponsesList"; +import { MedicationsTable } from "@/components/Medicine/MedicationsTable"; +import { AllergyList } from "@/components/Patient/allergy/list"; +import { DiagnosisList } from "@/components/Patient/diagnosis/list"; +import { SymptomsList } from "@/components/Patient/symptoms/list"; + +import api from "@/Utils/request/api"; +import query from "@/Utils/request/query"; +import { formatName, formatPatientAge } from "@/Utils/utils"; + +import { MedicationStatementList } from "./MedicationStatementList"; + +interface TreatmentSummaryProps { + facilityId: string; + encounterId: string; +} + +export default function TreatmentSummary({ + facilityId, + encounterId, +}: TreatmentSummaryProps) { + const { data: encounter } = useQuery({ + queryKey: ["encounter", encounterId], + queryFn: query(api.encounter.get, { + pathParams: { id: encounterId }, + queryParams: { facility: facilityId }, + }), + }); + + if (!encounter) { + return ( + + {t("no_patient_record_found")} + + ); + } + + return ( + + + + {/* Header */} + + + + + {encounter.facility?.name} + + + {t("treatment_summary")} + + + + + + + {/* Patient Details */} + + + + {t("patient")} + : + {encounter.patient.name} + + + {`${t("age")} / ${t("sex")}`} + : + + {`${formatPatientAge(encounter.patient, true)}, ${t(`GENDER__${encounter.patient.gender}`)}`} + + + + {t("encounter_class")} + : + + {t(`encounter_class__${encounter.encounter_class}`)} + + + + {t("priority")} + : + + {t(`encounter_priority__${encounter.priority}`)} + + + {encounter.hospitalization?.admit_source && ( + + {t("admission_source")} + : + + {t( + `encounter_admit_sources__${encounter.hospitalization.admit_source}`, + )} + + + )} + {encounter.hospitalization?.re_admission && ( + + {t("readmission")} + : + {t("yes")} + + )} + {encounter.hospitalization?.diet_preference && ( + + {t("diet_preference")} + : + + {t( + `encounter_diet_preference__${encounter.hospitalization.diet_preference}`, + )} + + + )} + + + + {t("mobile_number")} + : + + {encounter.patient.phone_number} + + + {encounter.period?.start && ( + + {t("encounter_date")} + : + + {format( + new Date(encounter.period.start), + "dd MMM yyyy, EEEE", + )} + + + )} + + {t("status")} + : + + {t(`encounter_status__${encounter.status}`)} + + + + {t("consulting_doctor")} + : + + {formatName(encounter.created_by)} + + + {encounter.external_identifier && ( + + {t("external_id")} + : + + {encounter.external_identifier} + + + )} + {encounter.hospitalization?.discharge_disposition && ( + + + {t("discharge_disposition")} + + : + + {t( + `encounter_discharge_disposition__${encounter.hospitalization.discharge_disposition}`, + )} + + + )} + + + + {/* Medical Information */} + + {/* Allergies */} + + + {/* Symptoms */} + + + {/* Diagnoses */} + + + {/* Medications */} + + + {t("medications")} + + + + + + {/* Medication Statements */} + + + {/* Questionnaire Responses Section */} + + + + + + {/* Footer */} + + + {t("generated_on")} {format(new Date(), "PPP 'at' p")} + + + + + ); +} diff --git a/src/components/Patient/allergy/list.tsx b/src/components/Patient/allergy/list.tsx index ddbcaaab127..62695fe02e8 100644 --- a/src/components/Patient/allergy/list.tsx +++ b/src/components/Patient/allergy/list.tsx @@ -9,6 +9,8 @@ import { import { Link } from "raviger"; import { ReactNode, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Badge } from "@/components/ui/badge"; @@ -47,6 +49,8 @@ interface AllergyListProps { facilityId?: string; patientId: string; encounterId?: string; + className?: string; + isPrintPreview?: boolean; encounterStatus?: Encounter["status"]; } @@ -67,9 +71,11 @@ export function AllergyList({ facilityId, patientId, encounterId, + className, + isPrintPreview = false, encounterStatus, }: AllergyListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(false); + const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); const { data: allergies, isLoading } = useQuery({ queryKey: ["allergies", patientId, encounterId, encounterStatus], @@ -125,13 +131,6 @@ export function AllergyList({ } function AllergyRow({ allergy }: AllergyRowProps) { - const MAX_NOTE_LENGTH = 15; - const note = allergy.note || ""; - const isLongNote = note.length > MAX_NOTE_LENGTH; - const displayNote = isLongNote - ? `${note.slice(0, MAX_NOTE_LENGTH)}..` - : note; - return ( - {note && ( + {allergy.note && ( - {displayNote} - {isLongNote && ( + {isPrintPreview ? ( + + {allergy.note} + + ) : ( - {note} + {allergy.note} @@ -220,6 +222,8 @@ export function AllergyList({ facilityId={facilityId} patientId={patientId} encounterId={encounterId} + className={className} + isPrintPreview={isPrintPreview} > @@ -290,15 +294,25 @@ const AllergyListLayout = ({ patientId, encounterId, children, + className, + isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; + className?: string; + isPrintPreview?: boolean; }) => { return ( - - + + {t("allergies")} {facilityId && encounterId && ( )} - {children} + + {children} + ); }; diff --git a/src/components/Patient/diagnosis/DiagnosisTable.tsx b/src/components/Patient/diagnosis/DiagnosisTable.tsx index 3f821d5d110..ac9d93cc878 100644 --- a/src/components/Patient/diagnosis/DiagnosisTable.tsx +++ b/src/components/Patient/diagnosis/DiagnosisTable.tsx @@ -26,9 +26,13 @@ import { interface DiagnosisTableProps { diagnoses: Diagnosis[]; + isPrintPreview?: boolean; } -export function DiagnosisTable({ diagnoses }: DiagnosisTableProps) { +export function DiagnosisTable({ + diagnoses, + isPrintPreview = false, +}: DiagnosisTableProps) { return ( @@ -54,99 +58,85 @@ export function DiagnosisTable({ diagnoses }: DiagnosisTableProps) { - {diagnoses.map((diagnosis) => { - const note = diagnosis.note || ""; - const MAX_NOTE_LENGTH = 15; - const isLongNote = note.length > MAX_NOTE_LENGTH; - const displayNote = isLongNote - ? `${note.slice(0, MAX_NOTE_LENGTH)}..` - : note; - - return ( - - - {diagnosis.code.display} - - - - {t(diagnosis.clinical_status)} - - - - - {t(diagnosis.verification_status)} - - - - {diagnosis.onset?.onset_datetime - ? new Date( - diagnosis.onset.onset_datetime, - ).toLocaleDateString() - : "-"} - - - {note ? ( - - - {displayNote} - - {isLongNote && ( - - - - {t("see_note")} - - - - - {note} - - - - )} - - ) : ( - "-" - )} - - + {diagnoses.map((diagnosis) => ( + + + {diagnosis.code.display} + + + + {t(diagnosis.clinical_status)} + + + + + {t(diagnosis.verification_status)} + + + + {diagnosis.onset?.onset_datetime + ? new Date(diagnosis.onset.onset_datetime).toLocaleDateString() + : "-"} + + + {diagnosis.note ? ( - - - {diagnosis.created_by.username} - + {isPrintPreview ? ( + {diagnosis.note} + ) : ( + + + + {t("see_note")} + + + + + {diagnosis.note} + + + + )} - - - ); - })} + ) : ( + "-" + )} + + + + + {diagnosis.created_by.username} + + + + ))} ); diff --git a/src/components/Patient/diagnosis/list.tsx b/src/components/Patient/diagnosis/list.tsx index 8e7fe9f1c07..4695f5900f9 100644 --- a/src/components/Patient/diagnosis/list.tsx +++ b/src/components/Patient/diagnosis/list.tsx @@ -3,6 +3,8 @@ import { t } from "i18next"; import { Link } from "raviger"; import { ReactNode, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; @@ -18,14 +20,18 @@ interface DiagnosisListProps { patientId: string; encounterId?: string; facilityId?: string; + className?: string; + isPrintPreview?: boolean; } export function DiagnosisList({ patientId, encounterId, facilityId, + className, + isPrintPreview = false, }: DiagnosisListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(false); + const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); const { data: diagnoses, isLoading } = useQuery({ queryKey: ["diagnosis", patientId, encounterId], @@ -78,6 +84,8 @@ export function DiagnosisList({ facilityId={facilityId} patientId={patientId} encounterId={encounterId} + className={className} + isPrintPreview={isPrintPreview} > {hasEnteredInErrorRecords && !showEnteredInError && ( @@ -117,15 +126,24 @@ const DiagnosisListLayout = ({ patientId, encounterId, children, + className, + isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; + className?: string; + isPrintPreview?: boolean; }) => { return ( - - + + {t("diagnoses")} {facilityId && encounterId && ( )} - {children} + + {children} + ); }; diff --git a/src/components/Patient/models.tsx b/src/components/Patient/models.tsx index 14f048191b0..abc717e6120 100644 --- a/src/components/Patient/models.tsx +++ b/src/components/Patient/models.tsx @@ -1,46 +1,11 @@ -import { PatientCategory } from "@/components/Facility/models"; import { UserBareMinimum } from "@/components/Users/models"; import { - APPETITE_CHOICES, - BLADDER_DRAINAGE_CHOICES, - BLADDER_ISSUE_CHOICES, - BOWEL_ISSUE_CHOICES, - CONSCIOUSNESS_LEVEL, DOMESTIC_HEALTHCARE_SUPPORT_CHOICES, - HEARTBEAT_RHYTHM_CHOICES, - HumanBodyRegion, - INSULIN_INTAKE_FREQUENCY_OPTIONS, - LIMB_RESPONSE_OPTIONS, - NURSING_CARE_PROCEDURES, - NUTRITION_ROUTE_CHOICES, OCCUPATION_TYPES, - ORAL_ISSUE_CHOICES, - OXYGEN_MODALITY_OPTIONS, - PressureSoreExudateAmountOptions, - PressureSoreTissueTypeOptions, - RESPIRATORY_SUPPORT, - SLEEP_CHOICES, SOCIOECONOMIC_STATUS_CHOICES, - URINATION_FREQUENCY_CHOICES, - VENTILATOR_MODE_OPTIONS, } from "@/common/constants"; -export interface FlowModel { - id?: number; - status?: string; - created_date?: string; - modified_date?: string; - deleted?: boolean; - notes?: string; - patient_sample?: number; - created_by?: number; -} - -export interface DischargeSummaryModel { - email?: string; -} - export interface AssignedToObjectModel { first_name: string; last_name: string; @@ -56,138 +21,6 @@ export interface PatientMeta { domestic_healthcare_support?: (typeof DOMESTIC_HEALTHCARE_SUPPORT_CHOICES)[number]; } -export const DailyRoundTypes = [ - "NORMAL", - "COMMUNITY_NURSES_LOG", - "DOCTORS_LOG", - "VENTILATOR", - "AUTOMATED", - "TELEMEDICINE", -] as const; - -export type BloodPressure = { - systolic?: number; - diastolic?: number; -}; - -export interface IPainScale { - description: string; - region: HumanBodyRegion; - scale: number; -} - -export type NameQuantity = { name: string; quantity: number }; - -export type IPressureSore = { - region: HumanBodyRegion; - width: number; - length: number; - description: string; - scale: number; - exudate_amount: (typeof PressureSoreExudateAmountOptions)[number]; - tissue_type: (typeof PressureSoreTissueTypeOptions)[number]; -}; -export interface DailyRoundsModel { - spo2?: number; - rhythm?: (typeof HEARTBEAT_RHYTHM_CHOICES)[number]; - rhythm_detail?: string; - bp?: BloodPressure; - pulse?: number; - resp?: number; - temperature?: number; - physical_examination_info?: string; - other_details?: string; - consultation?: number; - action?: string; - review_interval?: number; - id?: string; - admitted_to?: string; - patient_category?: PatientCategory; - recommend_discharge?: boolean; - created_date?: string; - modified_date?: string; - taken_at?: string; - consciousness_level?: (typeof CONSCIOUSNESS_LEVEL)[number]["value"]; - rounds_type?: (typeof DailyRoundTypes)[number]; - last_updated_by_telemedicine?: boolean; - created_by_telemedicine?: boolean; - created_by?: UserBareMinimum; - last_edited_by?: UserBareMinimum; - bed?: string; - pain_scale_enhanced?: IPainScale[]; - in_prone_position?: boolean; - left_pupil_size?: number; - left_pupil_size_detail?: string; - left_pupil_light_reaction?: string; - left_pupil_light_reaction_detail?: string; - right_pupil_size?: number; - right_pupil_size_detail?: string; - right_pupil_light_reaction?: string; - right_pupil_light_reaction_detail?: string; - glasgow_eye_open?: number; - glasgow_motor_response?: number; - glasgow_verbal_response?: number; - limb_response_upper_extremity_right?: (typeof LIMB_RESPONSE_OPTIONS)[number]["value"]; - limb_response_upper_extremity_left?: (typeof LIMB_RESPONSE_OPTIONS)[number]["value"]; - limb_response_lower_extremity_left?: (typeof LIMB_RESPONSE_OPTIONS)[number]["value"]; - limb_response_lower_extremity_right?: (typeof LIMB_RESPONSE_OPTIONS)[number]["value"]; - glasgow_total_calculated?: number; - bilateral_air_entry?: boolean; - etco2?: number; - po2?: number; - pco2?: number; - ph?: number; - hco3?: number; - base_excess?: number; - lactate?: number; - sodium?: number; - potassium?: number; - blood_sugar_level?: number; - insulin_intake_dose?: number; - insulin_intake_frequency?: (typeof INSULIN_INTAKE_FREQUENCY_OPTIONS)[number]; - dialysis_fluid_balance?: number; - dialysis_net_balance?: number; - nursing?: { - procedure: (typeof NURSING_CARE_PROCEDURES)[number]; - description: string; - }[]; - feeds?: NameQuantity[]; - infusions?: NameQuantity[]; - iv_fluids?: NameQuantity[]; - output?: NameQuantity[]; - total_intake_calculated?: number; - total_output_calculated?: number; - ventilator_spo2?: number; - ventilator_interface?: (typeof RESPIRATORY_SUPPORT)[number]["value"]; - ventilator_oxygen_modality?: (typeof OXYGEN_MODALITY_OPTIONS)[number]["value"]; - ventilator_oxygen_modality_flow_rate?: number; - ventilator_oxygen_modality_oxygen_rate?: number; - ventilator_fio2?: number; - ventilator_mode?: (typeof VENTILATOR_MODE_OPTIONS)[number]; - ventilator_peep?: number; - ventilator_pip?: number; - ventilator_mean_airway_pressure?: number; - ventilator_resp_rate?: number; - ventilator_pressure_support?: number; - - ventilator_tidal_volume?: number; - pressure_sore?: IPressureSore[]; - bowel_issue?: (typeof BOWEL_ISSUE_CHOICES)[number]; - bladder_drainage?: (typeof BLADDER_DRAINAGE_CHOICES)[number]; - bladder_issue?: (typeof BLADDER_ISSUE_CHOICES)[number]; - is_experiencing_dysuria?: boolean; - urination_frequency?: (typeof URINATION_FREQUENCY_CHOICES)[number]; - sleep?: (typeof SLEEP_CHOICES)[number]; - nutrition_route?: (typeof NUTRITION_ROUTE_CHOICES)[number]; - oral_issue?: (typeof ORAL_ISSUE_CHOICES)[number]; - appetite?: (typeof APPETITE_CHOICES)[number]; -} - -export interface FacilityNameModel { - id?: string; - name?: string; -} - // File Upload Models export type FileCategory = "unspecified" | "xray" | "audio" | "identity_proof"; diff --git a/src/components/Patient/symptoms/SymptomTable.tsx b/src/components/Patient/symptoms/SymptomTable.tsx index 8fc626918e9..8b9dd9cb973 100644 --- a/src/components/Patient/symptoms/SymptomTable.tsx +++ b/src/components/Patient/symptoms/SymptomTable.tsx @@ -27,9 +27,13 @@ import { interface SymptomTableProps { symptoms: Symptom[]; + isPrintPreview?: boolean; } -export function SymptomTable({ symptoms }: SymptomTableProps) { +export function SymptomTable({ + symptoms, + isPrintPreview = false, +}: SymptomTableProps) { return ( @@ -55,27 +59,20 @@ export function SymptomTable({ symptoms }: SymptomTableProps) { - {symptoms.map((symptom) => { - const note = symptom.note || ""; - const MAX_NOTE_LENGTH = 15; - const isLongNote = note.length > MAX_NOTE_LENGTH; - const displayNote = isLongNote - ? `${note.slice(0, MAX_NOTE_LENGTH)}..` - : note; - - return ( - - - {symptom.code.display} - - + {symptoms.map((symptom) => ( + + + {symptom.code.display} + + + {symptom.severity ? ( {t(symptom.severity)} - - - - {t(symptom.clinical_status)} - - - - - {t(symptom.verification_status)} - - - - {note ? ( - - - {displayNote} - - {isLongNote && ( - - - - {t("see_note")} - - - - - {note} - - - - )} - - ) : ( - "-" - )} - - + ) : ( + "-" + )} + + + + {t(symptom.clinical_status)} + + + + + {t(symptom.verification_status)} + + + + {symptom.note ? ( - - {symptom.created_by.username} + {isPrintPreview ? ( + {symptom.note} + ) : ( + + + + {t("see_note")} + + + + + {symptom.note} + + + + )} - - - ); - })} + ) : ( + "-" + )} + + + + + {symptom.created_by.username} + + + + ))} ); diff --git a/src/components/Patient/symptoms/list.tsx b/src/components/Patient/symptoms/list.tsx index e815a229e0d..0f4b558d6a9 100644 --- a/src/components/Patient/symptoms/list.tsx +++ b/src/components/Patient/symptoms/list.tsx @@ -3,6 +3,8 @@ import { t } from "i18next"; import { Link } from "raviger"; import { ReactNode, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; @@ -18,14 +20,18 @@ interface SymptomsListProps { patientId: string; encounterId?: string; facilityId?: string; + className?: string; + isPrintPreview?: boolean; } export function SymptomsList({ patientId, encounterId, facilityId, + className, + isPrintPreview = false, }: SymptomsListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(false); + const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); const { data: symptoms, isLoading } = useQuery({ queryKey: ["symptoms", patientId, encounterId], @@ -77,6 +83,8 @@ export function SymptomsList({ facilityId={facilityId} patientId={patientId} encounterId={encounterId} + className={className} + isPrintPreview={isPrintPreview} > {hasEnteredInErrorRecords && !showEnteredInError && ( @@ -115,15 +124,25 @@ const SymptomListLayout = ({ patientId, encounterId, children, + className, + isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; + className?: string; + isPrintPreview?: boolean; }) => { return ( - - + + {t("symptoms")} {facilityId && encounterId && ( )} - {children} + + {children} + ); }; diff --git a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx index c8b41842161..834517a98a3 100644 --- a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx +++ b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx @@ -66,7 +66,7 @@ export default function CloneQuestionnaireSheet({ silent: true, }), onSuccess: async (data: QuestionnaireDetail) => { - navigate(`/questionnaire/${data.slug}`); + navigate(`/admin/questionnaire/${data.slug}`); setOpen(false); }, onError: (error) => { diff --git a/src/components/Questionnaire/CodingEditor.tsx b/src/components/Questionnaire/CodingEditor.tsx new file mode 100644 index 00000000000..6e3fe0aba2c --- /dev/null +++ b/src/components/Questionnaire/CodingEditor.tsx @@ -0,0 +1,165 @@ +import { UpdateIcon } from "@radix-ui/react-icons"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +import mutate from "@/Utils/request/mutate"; +import { Code } from "@/types/questionnaire/code"; +import { + TERMINOLOGY_SYSTEMS, + ValuesetLookupResponse, +} from "@/types/valueset/valueset"; +import valuesetApi from "@/types/valueset/valuesetApi"; + +interface CodingEditorProps { + code?: Code; + onChange: (code: Code | undefined) => void; +} + +export function CodingEditor({ code, onChange }: CodingEditorProps) { + const { mutate: verifyCode, isPending } = useMutation({ + mutationFn: mutate(valuesetApi.lookup), + onSuccess: (response: ValuesetLookupResponse) => { + if (response.metadata && code) { + onChange({ + ...code, + display: response.metadata.display, + }); + toast.success("Code verified successfully"); + } + }, + onError: (error) => { + console.error(error); + toast.error("Failed to verify code"); + }, + }); + + if (!code) { + return ( + + { + onChange({ + system: Object.values(TERMINOLOGY_SYSTEMS)[0], + code: "", + display: "", + }); + }} + > + + Add Coding + + + ); + } + + return ( + + + + Coding Details + { + onChange(undefined); + }} + > + + Remove Coding + + + + + + + System + { + onChange({ + ...code, + system: value, + code: "", + display: "", + }); + }} + > + + + + + {Object.entries(TERMINOLOGY_SYSTEMS).map(([key, value]) => ( + + {key} + + ))} + + + + + + + Code + { + onChange({ + ...code, + code: e.target.value, + display: "", + }); + }} + placeholder="Enter code" + /> + + + Display + + + + { + if (!code.system || !code.code) { + toast.error("Please select a system and enter a code first"); + return; + } + + verifyCode({ + system: code.system, + code: code.code, + }); + }} + > + + + + + + + ); +} diff --git a/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx b/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx index dec289eedf4..4ec261ca4a4 100644 --- a/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx @@ -27,7 +27,7 @@ import { } from "@/types/questionnaire/form"; import { Question } from "@/types/questionnaire/question"; import { CreateAppointmentQuestion } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; import { UserBase } from "@/types/user/user"; interface FollowUpVisitQuestionProps { diff --git a/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx b/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx index f40485d97c0..00379ae671a 100644 --- a/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx @@ -309,7 +309,13 @@ const DiagnosisItem: React.FC = ({ disabled={disabled} > - + + {t("diagnosis_status_placeholder")} + + } + /> {DIAGNOSIS_CLINICAL_STATUS.map((status) => ( @@ -339,7 +345,13 @@ const DiagnosisItem: React.FC = ({ disabled={disabled} > - + + {t("diagnosis_verification_placeholder")} + + } + /> {DIAGNOSIS_VERIFICATION_STATUS.map((status) => ( diff --git a/src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx b/src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx index ed60dbe4b7e..5d2e1391d6c 100644 --- a/src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx @@ -79,7 +79,7 @@ export function LocationQuestion({ Select Location {t("cancel")} {t("remove")} @@ -363,7 +363,7 @@ export function MedicationRequestQuestion({ {t("cancel")} {t("remove")} @@ -356,7 +356,7 @@ export function MedicationStatementQuestion({ ); } - return null; + return ( + + Medication requests cannot be recorded without an active + encounter + + ); case "medication_statement": if (encounterId) { return ( @@ -134,7 +139,12 @@ export function QuestionInput({ /> ); } - return null; + return ( + + Medication statement cannot be recorded without an active + encounter + + ); case "allergy_intolerance": return ; case "symptom": @@ -147,14 +157,20 @@ export function QuestionInput({ /> ); } - return null; + return ( + Symptoms cannot be recorded without an encounter + ); case "diagnosis": if (encounterId) { return ( ); } - return null; + return ( + + Diagnosis cannot be recorded without an active encounter + + ); case "appointment": return ; case "encounter": @@ -167,7 +183,9 @@ export function QuestionInput({ /> ); } - return null; + return ( + Create an encounter first in order to update it + ); case "location_association": if (encounterId) { return ( @@ -179,7 +197,11 @@ export function QuestionInput({ /> ); } - return null; + return ( + + Location cannot be recorded without an active encounter + + ); } return null; diff --git a/src/components/Questionnaire/QuestionnaireEditor.tsx b/src/components/Questionnaire/QuestionnaireEditor.tsx index 2637d5c9880..436dbdbb18a 100644 --- a/src/components/Questionnaire/QuestionnaireEditor.tsx +++ b/src/components/Questionnaire/QuestionnaireEditor.tsx @@ -1,4 +1,5 @@ import { useMutation, useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { ChevronDown, ChevronUp } from "lucide-react"; import { Building, Check, Loader2, X } from "lucide-react"; import { useNavigate } from "raviger"; @@ -44,16 +45,17 @@ import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; +import { DebugPreview } from "@/components/Common/DebugPreview"; import Loading from "@/components/Common/Loading"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import organizationApi from "@/types/organization/organizationApi"; import { - AnswerOption, EnableWhen, Question, QuestionType, + SUPPORTED_QUESTION_TYPES, StructuredQuestionType, } from "@/types/questionnaire/question"; import { @@ -62,7 +64,9 @@ import { SubjectType, } from "@/types/questionnaire/questionnaire"; import questionnaireApi from "@/types/questionnaire/questionnaireApi"; +import valuesetApi from "@/types/valueset/valuesetApi"; +import { CodingEditor } from "./CodingEditor"; import ManageQuestionnaireOrganizationsSheet from "./ManageQuestionnaireOrganizationsSheet"; import { QuestionnaireForm } from "./QuestionnaireForm"; @@ -117,7 +121,7 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { mutationFn: mutate(questionnaireApi.create), onSuccess: (data: QuestionnaireDetail) => { toast.success("Questionnaire created successfully"); - navigate(`/questionnaire/${data.slug}`); + navigate(`/admin/questionnaire/${data.slug}`); }, onError: (_error) => { toast.error("Failed to create questionnaire"); @@ -203,7 +207,7 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { }; const handleCancel = () => { - navigate(id ? `/questionnaire/${id}` : "/questionnaire"); + navigate(id ? `/admin/questionnaire/${id}` : "/admin/questionnaire"); }; const toggleQuestionExpanded = (questionId: string) => { @@ -632,6 +636,11 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { + @@ -690,12 +699,20 @@ function QuestionEditor({ repeats, answer_option, questions, + code, } = question; const [expandedSubQuestions, setExpandedSubQuestions] = useState>( new Set(), ); + const { data: valuesetResponse } = useQuery({ + queryKey: ["valuesets"], + queryFn: query(valuesetApi.list), + }); + + const valuesets = valuesetResponse?.results || []; + const updateField = ( field: K, value: Question[K], @@ -824,136 +841,173 @@ function QuestionEditor({ /> - - - Type - { - if (val !== "group") { - updateField("type", val, { questions: [] }); - } else { - updateField("type", val); - } - }} - > - - - - - Group - Boolean - Decimal - Integer - Date - DateTime - Time - String - Text - URL - Choice - Quantity - Structured - - - - - {type === "structured" && ( + + - Structured Type + Type - updateField("structured_type", val) - } + value={type} + onValueChange={(val: QuestionType) => { + if (val !== "group") { + updateField("type", val, { questions: [] }); + } else { + updateField("type", val); + } + }} > - + - {STRUCTURED_QUESTION_TYPES.map((type) => ( + {SUPPORTED_QUESTION_TYPES.map((type) => ( - {type.label} + {type.name} ))} - )} - - - - updateField("required", val)} - id={`required-${getQuestionPath()}`} - /> - Required + {type === "structured" && ( + + Structured Type + + updateField("structured_type", val) + } + > + + + + + {STRUCTURED_QUESTION_TYPES.map((type) => ( + + {type.label} + + ))} + + + + )} - - updateField("repeats", val)} - id={`repeats-${getQuestionPath()}`} + {type !== "structured" && type !== "group" && ( + updateField("code", newCode)} /> - Repeatable - + )} + - - updateField("collect_time", val)} - id={`collect_time-${getQuestionPath()}`} - /> - - Collect Time - - + + + Question Settings + + Configure the basic behavior: mark as required, allow multiple + entries, or set as read only. + + + + + updateField("required", val)} + id={`required-${getQuestionPath()}`} + /> + + Required + + - - updateField("collect_performer", val)} - id={`collect_performer-${getQuestionPath()}`} - /> - - Collect Performer - - + + updateField("repeats", val)} + id={`repeats-${getQuestionPath()}`} + /> + + Repeatable + + - - updateField("collect_body_site", val)} - id={`collect_body_site-${getQuestionPath()}`} - /> - - Collect Body Site - + + updateField("read_only", val)} + id={`read_only-${getQuestionPath()}`} + /> + + Read Only + + + + - - updateField("collect_method", val)} - id={`collect_method-${getQuestionPath()}`} - /> - - Collect Method - - + + + Data Collection Details + + + Specify key collection info: time, performer, body site, and + method. + + + + + + updateField("collect_time", val) + } + id={`collect_time-${getQuestionPath()}`} + /> + + Collect Time + + - - updateField("read_only", val)} - id={`read_only-${getQuestionPath()}`} - /> - - Read Only - + + + updateField("collect_performer", val) + } + id={`collect_performer-${getQuestionPath()}`} + /> + + Collect Performer + + + + + + updateField("collect_body_site", val) + } + id={`collect_body_site-${getQuestionPath()}`} + /> + + Collect Body Site + + + + + + updateField("collect_method", val) + } + id={`collect_method-${getQuestionPath()}`} + /> + + Collect Method + + + + @@ -988,329 +1042,139 @@ function QuestionEditor({ {type === "choice" && ( - - Answer Options - + + + + + Answer Options + + + Define possible answers for this question + + updateField( "answer_value_set", - val === "custom" ? undefined : val, + val === "custom" ? undefined : "valueset", ) } > - - + + - Custom Options - Yes/No - - Severity Levels + + {t("custom_options")} - - Frequency - - Duration + {t("value_set")} - - {(!question.answer_value_set || - question.answer_value_set === "custom") && ( - + + + {!question.answer_value_set ? ( + {(answer_option || []).map((opt, idx) => ( - - { - const newOptions = [...(answer_option || [])]; - newOptions[idx] = { ...opt, value: e.target.value }; - updateField("answer_option", newOptions); - }} - placeholder="Option value" - /> - - { - const newOptions = [...(answer_option || [])]; - newOptions[idx] = { - ...opt, - display: e.target.value, - }; - updateField("answer_option", newOptions); - }} - placeholder="Display text (optional)" - /> - { - const newOptions = answer_option?.filter( - (_, i) => i !== idx, - ); - updateField("answer_option", newOptions); - }} - > - - + + + + Value + { + const newOptions = answer_option + ? [...answer_option] + : []; + newOptions[idx] = { + ...opt, + value: e.target.value, + }; + updateField("answer_option", newOptions); + }} + placeholder="Option value" + /> + + + + Display Text + { + const newOptions = answer_option + ? [...answer_option] + : []; + newOptions[idx] = { + ...opt, + display: e.target.value, + }; + updateField("answer_option", newOptions); + }} + placeholder="Display text (optional)" + /> + + { + const newOptions = answer_option?.filter( + (_, i) => i !== idx, + ); + updateField("answer_option", newOptions); + }} + > + + + ))} + { - const newOption: AnswerOption = { value: "" }; - updateField("answer_option", [ - ...(answer_option || []), - newOption, - ]); + const newOption = { value: "" }; + const newOptions = answer_option + ? [...answer_option, newOption] + : [newOption]; + updateField("answer_option", newOptions); }} > Add Option - - )} - - - - Enable When Conditions - - {(question.enable_when || []).length > 0 && ( - - Enable Behavior - - updateField("enable_behavior", val) - } - > - - - - - - All conditions must be met - - - Any condition must be met - - - - - )} - {(question.enable_when || []).map((condition, idx) => ( - + ) : ( + + + updateField("answer_value_set", val) + } > - - Question - { - const newConditions = [ - ...(question.enable_when || []), - ]; - newConditions[idx] = { - ...condition, - question: e.target.value, - }; - updateField("enable_when", newConditions); - }} - placeholder="Question Link ID" - /> - - - Operator - { - const newConditions = [ - ...(question.enable_when || []), - ]; - - switch (val) { - case "greater": - case "less": - case "greater_or_equals": - case "less_or_equals": - newConditions[idx] = { - question: condition.question, - operator: val, - answer: 0, - }; - break; - case "exists": - newConditions[idx] = { - question: condition.question, - operator: val, - answer: true, - }; - break; - case "equals": - case "not_equals": - newConditions[idx] = { - question: condition.question, - operator: val, - answer: "", - }; - break; - } - - updateField("enable_when", newConditions); - }} - > - - - - - Equals - - Not Equals - - - Greater Than - - Less Than - - Greater Than or Equal - - - Less Than or Equal - - Exists - - - - - - Answer - {condition.operator === "exists" ? ( - { - const newConditions = [ - ...(question.enable_when || []), - ]; - newConditions[idx] = { - question: condition.question, - operator: "exists" as const, - answer: val === "true", - }; - updateField("enable_when", newConditions); - }} - > - - - - - True - False - - - ) : ( - { - const newConditions = [ - ...(question.enable_when || []), - ]; - const value = e.target.value; - let newCondition; - - if ( - [ - "greater", - "less", - "greater_or_equals", - "less_or_equals", - ].includes(condition.operator) - ) { - newCondition = { - question: condition.question, - operator: condition.operator as - | "greater" - | "less" - | "greater_or_equals" - | "less_or_equals", - answer: Number(value), - }; - } else { - newCondition = { - question: condition.question, - operator: condition.operator as - | "equals" - | "not_equals", - answer: value, - }; - } - - newConditions[idx] = newCondition; - updateField("enable_when", newConditions); - }} - placeholder="Answer value" - /> - )} - - { - const newConditions = question.enable_when?.filter( - (_, i) => i !== idx, - ); - updateField("enable_when", newConditions); - }} - > - - - - - ))} - { - const newCondition: EnableWhen = { - question: "", - operator: "equals", - answer: "", - }; - updateField("enable_when", [ - ...(question.enable_when || []), - newCondition, - ]); - }} - > - - Add Condition - - - + + + + + {valuesets.map((valueset) => ( + + {valueset.name} + + ))} + + + + )} + )} @@ -1391,6 +1255,234 @@ function QuestionEditor({ )} + + + Enable When Conditions + + {(question.enable_when || []).length > 0 && ( + + Enable Behavior + + updateField("enable_behavior", val) + } + > + + + + + + All conditions must be met + + + Any condition must be met + + + + + )} + {(question.enable_when || []).map((condition, idx) => ( + + + Question + { + const newConditions = [...(question.enable_when || [])]; + newConditions[idx] = { + ...condition, + question: e.target.value, + }; + updateField("enable_when", newConditions); + }} + placeholder="Question Link ID" + /> + + + Operator + { + const newConditions = [...(question.enable_when || [])]; + + switch (val) { + case "greater": + case "less": + case "greater_or_equals": + case "less_or_equals": + newConditions[idx] = { + question: condition.question, + operator: val, + answer: 0, + }; + break; + case "exists": + newConditions[idx] = { + question: condition.question, + operator: val, + answer: true, + }; + break; + case "equals": + case "not_equals": + newConditions[idx] = { + question: condition.question, + operator: val, + answer: "", + }; + break; + } + + updateField("enable_when", newConditions); + }} + > + + + + + Equals + Not Equals + Greater Than + Less Than + + Greater Than or Equal + + + Less Than or Equal + + Exists + + + + + + Answer + {condition.operator === "exists" ? ( + { + const newConditions = [ + ...(question.enable_when || []), + ]; + newConditions[idx] = { + question: condition.question, + operator: "exists" as const, + answer: val === "true", + }; + updateField("enable_when", newConditions); + }} + > + + + + + True + False + + + ) : ( + { + const newConditions = [ + ...(question.enable_when || []), + ]; + const value = e.target.value; + let newCondition; + + if ( + [ + "greater", + "less", + "greater_or_equals", + "less_or_equals", + ].includes(condition.operator) + ) { + newCondition = { + question: condition.question, + operator: condition.operator as + | "greater" + | "less" + | "greater_or_equals" + | "less_or_equals", + answer: Number(value), + }; + } else { + newCondition = { + question: condition.question, + operator: condition.operator as + | "equals" + | "not_equals", + answer: value, + }; + } + + newConditions[idx] = newCondition; + updateField("enable_when", newConditions); + }} + placeholder="Answer value" + /> + )} + + { + const newConditions = question.enable_when?.filter( + (_, i) => i !== idx, + ); + updateField("enable_when", newConditions); + }} + > + + + + + ))} + { + const newCondition: EnableWhen = { + question: "", + operator: "equals", + answer: "", + }; + updateField("enable_when", [ + ...(question.enable_when || []), + newCondition, + ]); + }} + > + + Add Condition + + + diff --git a/src/components/Questionnaire/QuestionnaireForm.tsx b/src/components/Questionnaire/QuestionnaireForm.tsx index 7357dd5298d..1b1de03ebc9 100644 --- a/src/components/Questionnaire/QuestionnaireForm.tsx +++ b/src/components/Questionnaire/QuestionnaireForm.tsx @@ -11,6 +11,7 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; +import { DebugPreview } from "@/components/Common/DebugPreview"; import Loading from "@/components/Common/Loading"; import { PLUGIN_Component } from "@/PluginEngine"; @@ -525,15 +526,11 @@ export function QuestionnaireForm({ setFormState={setQuestionnaireForms} /> - {/* Add a Preview of the QuestionnaireForm */} - {import.meta.env.DEV && ( - - QuestionnaireForm - - {JSON.stringify(questionnaireForms, null, 2)} - - - )} + ); diff --git a/src/components/Questionnaire/index.tsx b/src/components/Questionnaire/QuestionnaireList.tsx similarity index 76% rename from src/components/Questionnaire/index.tsx rename to src/components/Questionnaire/QuestionnaireList.tsx index 54353743df9..cb6c29040eb 100644 --- a/src/components/Questionnaire/index.tsx +++ b/src/components/Questionnaire/QuestionnaireList.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { useNavigate } from "raviger"; import { Badge } from "@/components/ui/badge"; @@ -6,15 +7,25 @@ import { Button } from "@/components/ui/button"; import Loading from "@/components/Common/Loading"; +import useFilters from "@/hooks/useFilters"; + import query from "@/Utils/request/query"; import { QuestionnaireDetail } from "@/types/questionnaire/questionnaire"; import questionnaireApi from "@/types/questionnaire/questionnaireApi"; export function QuestionnaireList() { + const { qParams, Pagination, resultsPerPage } = useFilters({ + limit: 15, + }); const navigate = useNavigate(); const { data: response, isLoading } = useQuery({ - queryKey: ["questionnaires"], - queryFn: query(questionnaireApi.list), + queryKey: ["questionnaires", qParams], + queryFn: query(questionnaireApi.list, { + queryParams: { + limit: resultsPerPage, + offset: ((qParams.page ?? 1) - 1) * resultsPerPage, + }, + }), }); if (isLoading) { @@ -27,11 +38,11 @@ export function QuestionnaireList() { - Questionnaires - Manage and view questionnaires + {t("questionnaires")} + {t("manage_and_view_questionnaires")} - navigate("/questionnaire/create")}> - Create New + navigate("/admin/questionnaire/create")}> + {t("create_new")} @@ -40,16 +51,16 @@ export function QuestionnaireList() { - Title + {t("title")} - Description + {t("description")} - Status + {t("status")} - Slug + {t("slug")} @@ -57,7 +68,9 @@ export function QuestionnaireList() { {questionnaireList.map((questionnaire: QuestionnaireDetail) => ( navigate(`/questionnaire/${questionnaire.slug}`)} + onClick={() => + navigate(`/admin/questionnaire/${questionnaire.slug}`) + } className="cursor-pointer hover:bg-gray-50" > @@ -89,6 +102,7 @@ export function QuestionnaireList() { + ); } diff --git a/src/components/Questionnaire/show.tsx b/src/components/Questionnaire/show.tsx index 4534ce5734b..f18ca66f1e0 100644 --- a/src/components/Questionnaire/show.tsx +++ b/src/components/Questionnaire/show.tsx @@ -2,6 +2,8 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { useNavigate } from "raviger"; import { useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -16,7 +18,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DropdownMenu, @@ -103,7 +105,7 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) { pathParams: { id }, }), onSuccess: () => { - navigate("/questionnaire"); + navigate("/admin/questionnaire"); }, }); @@ -147,11 +149,14 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) { {questionnaire.description} - navigate("/questionnaire")}> + navigate("/admin/questionnaire")} + > Back to List - navigate(`/questionnaire/${id}/edit`)}> + navigate(`/admin/questionnaire/${id}/edit`)}> Edit @@ -218,7 +223,7 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) { Cancel {isPending ? "Deleting..." : "Delete"} diff --git a/src/components/Resource/PrintResourceLetter.tsx b/src/components/Resource/PrintResourceLetter.tsx index bc1a80f72a8..5fd6f82e209 100644 --- a/src/components/Resource/PrintResourceLetter.tsx +++ b/src/components/Resource/PrintResourceLetter.tsx @@ -45,7 +45,7 @@ export default function PrintResourceLetter({ id }: { id: string }) { {/* From Address */} - {t("From")}: + {t("from")}: {data.origin_facility.name} diff --git a/src/components/Resource/ResourceCommentSection.tsx b/src/components/Resource/ResourceCommentSection.tsx index 62f94d5d6fa..ddda59be9f1 100644 --- a/src/components/Resource/ResourceCommentSection.tsx +++ b/src/components/Resource/ResourceCommentSection.tsx @@ -7,7 +7,9 @@ import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; +import { Markdown } from "@/components/ui/markdown"; import { Textarea } from "@/components/ui/textarea"; +import { TooltipComponent } from "@/components/ui/tooltip"; import { Avatar } from "@/components/Common/Avatar"; import PaginationComponent from "@/components/Common/Pagination"; @@ -18,7 +20,7 @@ import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants"; import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; -import { formatName, relativeTime } from "@/Utils/utils"; +import { formatDateTime, formatName, relativeTime } from "@/Utils/utils"; import { CommentModel } from "@/types/resourceRequest/resourceRequest"; const CommentSection = (props: { id: string }) => { @@ -91,11 +93,13 @@ const CommentSection = (props: { id: string }) => { ) : ( - {resourceComments?.results?.map((comment) => ( - - - - ))} + {resourceComments?.results + ? [...resourceComments.results].reverse().map((comment) => ( + + + + )) + : null} ( - - - - {comment.replace(/\n+/g, "\n")} - - - - - - - {formatName(created_by)} - + + + + + + + + + + + {formatName(created_by)} + + + {relativeTime(created_date)} + + + + + - {relativeTime(created_date)} ); diff --git a/src/components/Resource/ResourceCreate.tsx b/src/components/Resource/ResourceCreate.tsx deleted file mode 100644 index b21a74d0c96..00000000000 --- a/src/components/Resource/ResourceCreate.tsx +++ /dev/null @@ -1,420 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { navigate, useQueryParams } from "raviger"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; -import * as z from "zod"; - -import Card from "@/CAREUI/display/Card"; -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { PhoneInput } from "@/components/ui/phone-input"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Separator } from "@/components/ui/separator"; -import { Textarea } from "@/components/ui/textarea"; - -import { FacilitySelect } from "@/components/Common/FacilitySelect"; -import Loading from "@/components/Common/Loading"; -import Page from "@/components/Common/Page"; - -import useAppHistory from "@/hooks/useAppHistory"; -import useAuthUser from "@/hooks/useAuthUser"; - -import { RESOURCE_CATEGORY_CHOICES } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import mutate from "@/Utils/request/mutate"; -import query from "@/Utils/request/query"; -import validators from "@/Utils/validators"; -import { ResourceRequest } from "@/types/resourceRequest/resourceRequest"; - -interface ResourceProps { - facilityId: number; -} - -export default function ResourceCreate(props: ResourceProps) { - const { goBack } = useAppHistory(); - const { facilityId } = props; - const { t } = useTranslation(); - const [{ related_patient }] = useQueryParams(); - const authUser = useAuthUser(); - - const resourceFormSchema = z.object({ - category: z.string().min(1, { message: t("field_required") }), - assigned_facility: z - .object({ - id: z.string(), - name: z.string(), - }) - .nullable(), - emergency: z.enum(["true", "false"]), - title: z.string().min(1, { message: t("field_required") }), - reason: z.string().min(1, { message: t("field_required") }), - referring_facility_contact_name: z - .string() - .min(1, { message: t("field_required") }), - referring_facility_contact_number: validators.phoneNumber.required, - priority: z.number().default(1), - }); - - type ResourceFormValues = z.infer; - - const { data: facilityData } = useQuery({ - queryKey: ["facility", facilityId], - queryFn: () => - query(routes.getAnyFacility, { - pathParams: { id: String(facilityId) }, - }), - enabled: !!facilityId, - }); - - const form = useForm({ - resolver: zodResolver(resourceFormSchema), - defaultValues: { - category: "", - assigned_facility: null, - emergency: "false" as const, - title: "", - reason: "", - referring_facility_contact_name: "", - referring_facility_contact_number: "", - priority: 1, - }, - }); - - const { mutate: createResource, isPending } = useMutation({ - mutationFn: mutate(routes.createResource), - onSuccess: (data: ResourceRequest) => { - toast.success(t("resource_created_successfully")); - navigate(`/facility/${facilityId}/resource/${data.id}`); - }, - }); - - const onSubmit = (data: ResourceFormValues) => { - createResource({ - status: "PENDING", - category: data.category, - origin_facility: String(props.facilityId), - assigned_facility: data.assigned_facility?.id || null, - approving_facility: null, - emergency: data.emergency === "true", - title: data.title, - reason: data.reason, - referring_facility_contact_name: data.referring_facility_contact_name, - referring_facility_contact_number: data.referring_facility_contact_number, - related_patient: related_patient, - priority: data.priority, - }); - }; - - const fillMyDetails = () => { - form.setValue( - "referring_facility_contact_name", - `${authUser.first_name} ${authUser.last_name}`.trim(), - ); - if (authUser.phone_number) { - form.setValue("referring_facility_contact_number", authUser.phone_number); - } - }; - - if (isPending) { - return ; - } - - return ( - - - - - - {related_patient && ( - - - - - {t("linked_patient")}:{" "} - {related_patient} - - - - )} - - - - - {t("basic_information")} - - - {t("resource_request_basic_info_description")} - - - - - ( - - - {t("facility_for_care_support")} - - - - - - {t("select_facility_description")} - - - - )} - /> - - ( - - {t("is_this_an_emergency")} - - - - - - - - {t("yes")} - - - - - - - - {t("no")} - - - - - - {t("emergency_description")} - - - - )} - /> - - - ( - - {t("category")} - - - - - - - - {RESOURCE_CATEGORY_CHOICES.map((category) => ( - - {category.text} - - ))} - - - - {t("category_description")} - - - - )} - /> - - - - - - - - {t("request_details")} - - - {t("resource_request_details_description")} - - - - ( - - {t("request_title")} - - field.onChange(value)} - /> - - - {t("request_title_description")} - - - - )} - /> - - ( - - {t("request_reason")} - - field.onChange(value)} - /> - - - {t("request_reason_description")} - - - - )} - /> - - - - - - - - - {t("contact_information")} - - - {t("contact_information_description")} - - - - - {t("fill_my_details")} - - - - - ( - - {t("contact_person")} - - field.onChange(value)} - /> - - - {t("contact_person_description")} - - - - )} - /> - - ( - - {t("contact_phone")} - - field.onChange(value)} - /> - - - {t("contact_phone_description")} - - - - )} - /> - - - - - goBack()} - > - {t("cancel")} - - - {isPending && ( - - )} - {isPending ? t("submitting") : t("submit")} - - - - - - - - ); -} diff --git a/src/components/Resource/ResourceDetails.tsx b/src/components/Resource/ResourceDetails.tsx index f684596c13a..182cd9ab514 100644 --- a/src/components/Resource/ResourceDetails.tsx +++ b/src/components/Resource/ResourceDetails.tsx @@ -123,11 +123,7 @@ export default function ResourceDetails({ } return ( - + {/* Action Buttons */} diff --git a/src/components/Resource/ResourceDetailsUpdate.tsx b/src/components/Resource/ResourceDetailsUpdate.tsx deleted file mode 100644 index f3e48df7af8..00000000000 --- a/src/components/Resource/ResourceDetailsUpdate.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useMutation } from "@tanstack/react-query"; -import { t } from "i18next"; -import { navigate, useQueryParams } from "raviger"; -import { useEffect, useReducer, useState } from "react"; -import { toast } from "sonner"; - -import Card from "@/CAREUI/display/Card"; - -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; - -import CircularProgress from "@/components/Common/CircularProgress"; -import { FacilitySelect } from "@/components/Common/FacilitySelect"; -import Loading from "@/components/Common/Loading"; -import Page from "@/components/Common/Page"; -import UserAutocomplete from "@/components/Common/UserAutocompleteFormField"; -import { FieldLabel } from "@/components/Form/FormFields/FormField"; -import RadioFormField from "@/components/Form/FormFields/RadioFormField"; -import { SelectFormField } from "@/components/Form/FormFields/SelectFormField"; -import TextFormField from "@/components/Form/FormFields/TextFormField"; -import { FieldChangeEvent } from "@/components/Form/FormFields/Utils"; -import { UserModel } from "@/components/Users/models"; - -import useAppHistory from "@/hooks/useAppHistory"; - -import { RESOURCE_STATUS_CHOICES } from "@/common/constants"; - -import routes from "@/Utils/request/api"; -import mutate from "@/Utils/request/mutate"; -import query from "@/Utils/request/query"; -import { UpdateResourceRequest } from "@/types/resourceRequest/resourceRequest"; - -interface resourceProps { - id: string; - facilityId: string; -} - -const initForm: Partial = { - assigned_facility: null, - emergency: false, - title: "", - reason: "", - assigned_to: null, -}; - -const requiredFields: any = { - assigned_facility_type: { - errorText: "Please Select Facility Type", - }, -}; - -const initError = Object.assign( - {}, - ...Object.keys(initForm).map((k) => ({ [k]: "" })), -); - -const initialState = { - form: { ...initForm }, - errors: { ...initError }, -}; - -export const ResourceDetailsUpdate = (props: resourceProps) => { - const { goBack } = useAppHistory(); - const [qParams, _] = useQueryParams(); - const [assignedUser, SetAssignedUser] = useState(); - const resourceFormReducer = (state = initialState, action: any) => { - switch (action.type) { - case "set_form": { - return { - ...state, - form: action.form, - }; - } - case "set_error": { - return { - ...state, - errors: action.errors, - }; - } - default: - return state; - } - }; - - const [state, dispatch] = useReducer(resourceFormReducer, initialState); - const { data, isLoading: assignedUserLoading } = useQuery({ - queryKey: ["user", props.facilityId], - queryFn: query(routes.userList), - }); - - useEffect(() => { - if (data) { - SetAssignedUser(data.results[0]); - } - }, [data]); - - const validateForm = () => { - const errors = { ...initError }; - let isInvalidForm = false; - Object.keys(requiredFields).forEach((field) => { - if (!state.form[field] || !state.form[field].length) { - errors[field] = requiredFields[field].errorText; - isInvalidForm = true; - } - }); - - dispatch({ type: "set_error", errors }); - return isInvalidForm; - }; - - const handleChange = (e: FieldChangeEvent) => { - dispatch({ - type: "set_form", - form: { ...state.form, [e.name]: e.value }, - }); - }; - - const handleOnSelect = (user: any) => { - const form = { ...state.form }; - form["assigned_to"] = user?.value?.id; - SetAssignedUser(user.value); - dispatch({ type: "set_form", form }); - }; - - const setFacility = (selected: any, name: string) => { - const form = { ...state.form }; - form[name] = selected; - dispatch({ type: "set_form", form }); - }; - const { data: resourceDetails } = useQuery({ - queryKey: ["resource", props.facilityId, props.id], - queryFn: query(routes.getResourceDetails, { - pathParams: { id: props.id }, - }), - }); - useEffect(() => { - if (resourceDetails) { - dispatch({ - type: "set_form", - form: { - ...resourceDetails, - status: qParams.status || resourceDetails.status.toLowerCase(), - }, - }); - } - }, [resourceDetails]); - - const { mutate: updateResource, isPending: updateResourceLoading } = - useMutation({ - mutationFn: mutate(routes.updateResource, { - pathParams: { id: props.id }, - }), - onSuccess: (data) => { - dispatch({ type: "set_form", form: data }); - toast.success(t("request_updated_successfully")); - navigate(`/facility/${props.facilityId}/resource/${props.id}`); - }, - }); - - const handleSubmit = async () => { - const validForm = validateForm(); - - if (validForm) { - const resourceData: UpdateResourceRequest = { - id: props.id, - status: state.form.status, - origin_facility: state.form.origin_facility?.id, - assigned_facility: state.form?.assigned_facility?.id, - emergency: [true, "true"].includes(state.form.emergency), - title: state.form.title, - reason: state.form.reason, - assigned_to: state.form.assigned_to, - category: state.form.category, - priority: state.form.priority, - referring_facility_contact_number: - state.form.referring_facility_contact_number, - referring_facility_contact_name: - state.form.referring_facility_contact_name, - approving_facility: state.form.approving_facility?.id, - related_patient: state.form.related_patient?.id, - }; - updateResource(resourceData); - } - }; - - if (updateResourceLoading || !resourceDetails) { - return ; - } - - return ( - - - - - - option.text} - onChange={handleChange} - optionLabel={(option) => t(`resource_status__${option.text}`)} - /> - - - - {assignedUserLoading ? ( - - ) : ( - - )} - - - - - - What facility would you like to assign the request to - - setFacility(obj, "assigned_facility")} - errors={state.errors.assigned_facility} - /> - - - - - - - - - {t("request_reason")} - - - handleChange({ name: e.target.name, value: e.target.value }) - } - /> - {state.errors.reason && ( - - {state.errors.emergency} - - )} - - - - (o ? "Yes" : "No")} - optionValue={(o) => String(o)} - value={String(state.form.emergency)} - error={state.errors.emergency} - /> - - - - goBack()}> - {t("cancel")} - - - {t("submit")} - - - - - - - ); -}; diff --git a/src/components/Resource/ResourceForm.tsx b/src/components/Resource/ResourceForm.tsx new file mode 100644 index 00000000000..fb93e88e316 --- /dev/null +++ b/src/components/Resource/ResourceForm.tsx @@ -0,0 +1,571 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Link, navigate, useQueryParams } from "raviger"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import * as z from "zod"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Alert, AlertDescription } from "@/components/ui/alert"; +import Autocomplete from "@/components/ui/autocomplete"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { PhoneInput } from "@/components/ui/phone-input"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; + +import Loading from "@/components/Common/Loading"; +import Page from "@/components/Common/Page"; +import UserSelector from "@/components/Common/UserSelector"; + +import useAppHistory from "@/hooks/useAppHistory"; +import useAuthUser from "@/hooks/useAuthUser"; + +import { RESOURCE_STATUS_CHOICES } from "@/common/constants"; +import { RESOURCE_CATEGORY_CHOICES } from "@/common/constants"; + +import routes from "@/Utils/request/api"; +import mutate from "@/Utils/request/mutate"; +import query from "@/Utils/request/query"; +import { mergeAutocompleteOptions } from "@/Utils/utils"; +import validators from "@/Utils/validators"; +import facilityApi from "@/types/facility/facilityApi"; +import { ResourceRequest } from "@/types/resourceRequest/resourceRequest"; +import { UserBase } from "@/types/user/user"; + +interface ResourceProps { + facilityId: number; + id?: string; +} + +export default function ResourceForm({ facilityId, id }: ResourceProps) { + const [facilitySearch, setFacilitySearch] = useState(""); + const { goBack } = useAppHistory(); + const { t } = useTranslation(); + const [{ related_patient }] = useQueryParams(); + const [assignedToUser, setAssignedToUser] = useState(); + const authUser = useAuthUser(); + + const resourceFormSchema = z.object({ + status: z.string().min(1, { message: t("field_required") }), + category: z.string().min(1, { message: t("field_required") }), + assigned_facility: z + .object({ + id: z.string(), + name: z.string(), + }) + .nullable(), + assigned_to: z.string().min(1, { message: t("field_required") }), + emergency: z.enum(["true", "false"]), + title: z.string().min(1, { message: t("field_required") }), + reason: z.string().min(1, { message: t("field_required") }), + referring_facility_contact_name: z + .string() + .min(1, { message: t("field_required") }), + referring_facility_contact_number: validators().phoneNumber.required, + priority: z.number().default(1), + }); + + type ResourceFormValues = z.infer; + + const { data: patientData } = useQuery({ + queryKey: ["patient", related_patient], + queryFn: query(routes.patient.getPatient, { + pathParams: { id: String(related_patient) }, + }), + enabled: !!related_patient, + }); + + const { data: resourceData } = useQuery({ + queryKey: ["resource_request", id], + queryFn: query(routes.getResourceDetails, { + pathParams: { id: String(id) }, + }), + enabled: !!id, + }); + + const form = useForm({ + resolver: zodResolver(resourceFormSchema), + defaultValues: { + status: "pending", + category: "", + assigned_facility: null, + assigned_to: "", + emergency: "false" as const, + title: "", + reason: "", + referring_facility_contact_name: "", + referring_facility_contact_number: "", + priority: 1, + }, + }); + + useEffect(() => { + if (resourceData) { + form.reset({ + status: resourceData.status, + category: resourceData.category, + assigned_facility: resourceData.assigned_facility, + assigned_to: resourceData.assigned_to?.id, + emergency: resourceData.emergency ? "true" : "false", + title: resourceData.title, + reason: resourceData.reason, + referring_facility_contact_name: + resourceData.referring_facility_contact_name, + referring_facility_contact_number: + resourceData.referring_facility_contact_number, + priority: resourceData.priority, + }); + if (resourceData.assigned_to) { + setAssignedToUser(resourceData.assigned_to); + } else { + setAssignedToUser(undefined); + } + } + }, [resourceData, form]); + + const { mutate: createResource, isPending } = useMutation({ + mutationFn: mutate(routes.createResource), + onSuccess: (data: ResourceRequest) => { + toast.success(t("resource_created_successfully")); + navigate(`/facility/${facilityId}/resource/${data.id}`); + }, + }); + + const { mutate: updateResource, isPending: isUpdatePending } = useMutation({ + mutationFn: mutate(routes.updateResource, { + pathParams: { id: String(id) }, + }), + onSuccess: (data: ResourceRequest) => { + toast.success(t("resource_updated_successfully")); + navigate(`/facility/${facilityId}/resource/${data.id}`); + }, + }); + + const onSubmit = (data: ResourceFormValues) => { + const resourcePayload = { + status: data.status, + category: data.category, + origin_facility: String(facilityId), + assigned_facility: data.assigned_facility?.id || null, + assigned_to: assignedToUser?.id || null, + approving_facility: null, + emergency: data.emergency === "true", + title: data.title, + reason: data.reason, + referring_facility_contact_name: data.referring_facility_contact_name, + referring_facility_contact_number: data.referring_facility_contact_number, + related_patient: related_patient, + priority: data.priority, + }; + + if (id) { + updateResource({ ...resourcePayload, id }); + } else { + createResource(resourcePayload); + } + }; + const { data: facilities } = useQuery({ + queryKey: ["facilities", facilitySearch], + queryFn: query.debounced(facilityApi.getAllFacilities, { + queryParams: { + search_text: facilitySearch, + limit: 50, + }, + }), + }); + + const facilityOptions = facilities?.results.map((facility) => ({ + label: facility.name, + value: facility.id, + })); + + const handleUserChange = (user: UserBase) => { + form.setValue("assigned_to", user.id); + setAssignedToUser(user); + }; + + const fillMyDetails = () => { + form.setValue( + "referring_facility_contact_name", + `${authUser.first_name} ${authUser.last_name}`.trim(), + ); + if (authUser.phone_number) { + form.setValue("referring_facility_contact_number", authUser.phone_number); + } + }; + + if (isPending || isUpdatePending) { + return ; + } + + return ( + + + + + + + {patientData && ( + + + + + + {t("linked_patient")}:{" "} + + {patientData.name} + + + + + + )} + + + + + {t("basic_information")} + + + {t("resource_request_basic_info_description")} + + + + + ( + + + {t("facility_for_care_support")} + + + { + const facility = + facilities?.results.find( + (f) => f.id === value, + ) ?? null; + form.setValue("assigned_facility", facility); + }} + /> + + + {t("select_facility_description")} + + + + )} + /> + + ( + + {t("is_this_an_emergency")} + + + + + + + + {t("yes")} + + + + + + + + {t("no")} + + + + + + {t("emergency_description")} + + + + )} + /> + + ( + + {t("status")} + + + + + + + + {RESOURCE_STATUS_CHOICES.map((option, index) => ( + + {t(`resource_status__${option.text}`)} + + ))} + + + + + + )} + /> + ( + + {t("category")} + + + + + + + + {RESOURCE_CATEGORY_CHOICES.map((category) => ( + + {category.text} + + ))} + + + + + )} + /> + ( + + {t("assigned_to")} + + + + + + )} + /> + + + + + + + + {t("request_details")} + + + {t("resource_request_details_description")} + + + + ( + + {t("request_title")} + + field.onChange(value)} + /> + + + {t("request_title_description")} + + + + )} + /> + + ( + + {t("request_reason")} + + field.onChange(value)} + /> + + + {t("request_reason_description")} + + + + )} + /> + + + + + + + + + {t("contact_information")} + + + {t("contact_information_description")} + + + + + {t("fill_my_details")} + + + + + ( + + {t("contact_person")} + + field.onChange(value)} + /> + + + {t("contact_person_description")} + + + + )} + /> + + ( + + {t("contact_phone")} + + field.onChange(value)} + /> + + + {t("contact_phone_description")} + + + + )} + /> + + + + + goBack()} + > + {t("cancel")} + + + {isPending && ( + + )} + {isPending ? t("submitting") : t("submit")} + + + + + + + + + ); +} diff --git a/src/components/Resource/ResourceList.tsx b/src/components/Resource/ResourceList.tsx index 1ba35b0effe..566a19dedde 100644 --- a/src/components/Resource/ResourceList.tsx +++ b/src/components/Resource/ResourceList.tsx @@ -103,7 +103,7 @@ export default function ResourceList({ facilityId }: { facilityId: string }) { const resources = queryResources?.results || []; return ( - + @@ -154,11 +154,12 @@ export default function ResourceList({ facilityId }: { facilityId: string }) { }) } className="w-full border-none shadow-none" + autoFocus /> - + - + - + - + - + {currentStatuses.map((statusOption) => ( setEditAvatar(false)} + onOpenChange={(open) => setEditAvatar(open)} /> diff --git a/src/components/Users/UserDeleteDialog.tsx b/src/components/Users/UserDeleteDialog.tsx index 5a2becefe3d..a088c9f2512 100644 --- a/src/components/Users/UserDeleteDialog.tsx +++ b/src/components/Users/UserDeleteDialog.tsx @@ -1,26 +1,51 @@ -import ConfirmDialog from "@/components/Common/ConfirmDialog"; +import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { buttonVariants } from "@/components/ui/button"; interface ConfirmDialogProps { name: string; handleCancel: () => void; handleOk: () => void; + show: boolean; } const UserDeleteDialog = (props: ConfirmDialogProps) => { + const { t } = useTranslation(); return ( - - Are you sure you want to delete user {props.name} ? - - } - action="Delete" - variant="destructive" - show - onConfirm={props.handleOk} - onClose={props.handleCancel} - /> + + + + {t("delete_user")} + + {t("are_you_sure_you_want_to_delete_user")} + {props.name}? + + + + + {t("cancel")} + + + {t("delete")} + + + + ); }; diff --git a/src/components/Users/UserForm.tsx b/src/components/Users/UserForm.tsx index ce92df7533b..ee78d887d9f 100644 --- a/src/components/Users/UserForm.tsx +++ b/src/components/Users/UserForm.tsx @@ -90,8 +90,8 @@ export default function UserForm({ first_name: z.string().min(1, t("field_required")), last_name: z.string().min(1, t("field_required")), email: z.string().email(t("invalid_email_address")), - phone_number: validators.phoneNumber.required, - gender: z.enum(GENDERS), + phone_number: validators().phoneNumber.required, + gender: z.enum(GENDERS, { required_error: t("gender_is_required") }), /* TODO: Userbase doesn't currently support these, neither does BE but we will probably need these */ /* qualification: z.string().optional(), @@ -145,7 +145,7 @@ export default function UserForm({ last_name: userData.last_name, email: userData.email, phone_number: userData.phone_number || "", - gender: userData.gender, + gender: userData.gender || undefined, }; form.reset(formData); } @@ -155,7 +155,7 @@ export default function UserForm({ const [isUsernameFieldFocused, setIsUsernameFieldFocused] = useState(false); //const userType = form.watch("user_type"); - const usernameInput = form.watch("username"); + const usernameInput = form.watch("username") || ""; const phoneNumber = form.watch("phone_number"); useEffect(() => { @@ -271,7 +271,7 @@ export default function UserForm({ useEffect(() => { const levels: Organization[] = []; - if (org) levels.push(org); + if (org && org.org_type === "govt") levels.push(org); setSelectedLevels(levels); }, [org, organizationId]); diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx index f07e08a9718..f87017fa479 100644 --- a/src/components/Users/UserFormValidations.tsx +++ b/src/components/Users/UserFormValidations.tsx @@ -1,8 +1,8 @@ import { Trans } from "react-i18next"; -import CareIcon from "@/CAREUI/icons/CareIcon"; +import { cn } from "@/lib/utils"; -import { classNames } from "@/Utils/utils"; +import CareIcon from "@/CAREUI/icons/CareIcon"; export type UserType = "doctor" | "nurse" | "staff" | "volunteer"; @@ -72,7 +72,7 @@ export const validateRule = ( )}{" "} { diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index ee6753c1472..bf8b2b3e96d 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -133,9 +133,7 @@ const UserListHeader = () => { return ( - - {t("name")} - + {t("name")} {t("status")} {t("role")} {t("contact_number")} @@ -151,7 +149,7 @@ const UserListRow = ({ user }: { user: UserBase }) => { id={`usr_${user.id}`} className="hover:bg-gray-50" > - + {showDeleteDialog && ( { @@ -96,6 +97,7 @@ export default function UserSummaryTab({ userData }: { userData?: UserBase }) { }} /> )} + { + toast.success("ValueSet created successfully"); + navigate(`/valuesets/${data.slug}`); + }, + }); + + // Update mutation + const updateMutation = useMutation({ + mutationFn: mutate(valuesetApi.update, { + pathParams: { slug: slug! }, + }), + onSuccess: () => { + toast.success("ValueSet updated successfully"); + navigate(`/admin/valuesets`); + }, + }); + + const handleSubmit = (data: ValuesetFormType) => { + if (slug && existingValueset) { + const updateData: UpdateValuesetModel = { + ...data, + id: existingValueset.id, + }; + updateMutation.mutate(updateData); + } else { + const createData: CreateValuesetModel = data; + createMutation.mutate(createData); + } + }; + + return ( + + + {slug ? "Edit ValueSet" : "Create New ValueSet"} + + + {slug && isLoading ? ( + + ) : ( + + )} + + ); +} diff --git a/src/components/ValueSet/ValueSetForm.tsx b/src/components/ValueSet/ValueSetForm.tsx new file mode 100644 index 00000000000..b3e76eaaa23 --- /dev/null +++ b/src/components/ValueSet/ValueSetForm.tsx @@ -0,0 +1,492 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon, TrashIcon, UpdateIcon } from "@radix-ui/react-icons"; +import { useMutation } from "@tanstack/react-query"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import * as z from "zod"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; + +import mutate from "@/Utils/request/mutate"; +import { + TERMINOLOGY_SYSTEMS, + ValuesetFormType, + ValuesetLookupResponse, +} from "@/types/valueset/valueset"; +import valuesetApi from "@/types/valueset/valuesetApi"; + +// Create a schema for form validation +const valuesetFormSchema = z.object({ + name: z.string().min(1, "Name is required"), + slug: z.string().min(1, "Slug is required"), + description: z.string(), + status: z.enum(["active", "inactive"]), + is_system_defined: z.boolean(), + compose: z.object({ + include: z.array( + z.object({ + system: z.string(), + concept: z + .array( + z.object({ + code: z.string(), + display: z.string(), + }), + ) + .optional(), + filter: z + .array( + z.object({ + property: z.string(), + op: z.string(), + value: z.string(), + }), + ) + .optional(), + }), + ), + exclude: z.array( + z.object({ + system: z.string(), + concept: z + .array( + z.object({ + code: z.string(), + display: z.string(), + }), + ) + .optional(), + filter: z + .array( + z.object({ + property: z.string(), + op: z.string(), + value: z.string(), + }), + ) + .optional(), + }), + ), + }), +}); + +interface ValueSetFormProps { + initialData?: ValuesetFormType; + onSubmit: (data: ValuesetFormType) => void; + isSubmitting?: boolean; +} + +function ConceptFields({ + nestIndex, + type, + parentForm, +}: { + nestIndex: number; + type: "include" | "exclude"; + parentForm: ReturnType>; +}) { + const { fields, append, remove } = useFieldArray({ + control: parentForm.control, + name: `compose.${type}.${nestIndex}.concept`, + }); + + const lookupMutation = useMutation({ + mutationFn: mutate(valuesetApi.lookup, { + silent: true, // Suppress default error handling since we have custom handling + }), + onSuccess: (response: ValuesetLookupResponse) => { + if (response.metadata) { + const concepts = parentForm.getValues( + `compose.${type}.${nestIndex}.concept`, + ); + + const conceptIndex = concepts?.findIndex( + (concept) => concept.code === response.metadata.code, + ); + + if (conceptIndex && conceptIndex !== -1) { + parentForm.setValue( + `compose.${type}.${nestIndex}.concept.${conceptIndex}.display`, + response.metadata.display, + { shouldValidate: true }, + ); + } + toast.success("Code verified successfully"); + } + }, + onError: () => { + toast.error("Failed to verify code"); + }, + }); + + const handleVerify = async (index: number) => { + const system = parentForm.getValues(`compose.${type}.${nestIndex}.system`); + const code = parentForm.getValues( + `compose.${type}.${nestIndex}.concept.${index}.code`, + ); + + if (!system || !code) { + toast.error("Please select a system and enter a code first"); + return; + } + + lookupMutation.mutate({ system, code }); + }; + + return ( + + + Concepts + append({ code: "", display: "" })} + > + + Add Concept + + + {fields.map((field, index) => ( + + ( + + + { + field.onChange(e); + // Clear display and set isVerified to false when code changes + parentForm.setValue( + `compose.${type}.${nestIndex}.concept.${index}.display`, + "", + { shouldValidate: true }, + ); + }} + /> + + + )} + /> + ( + + + + + + )} + /> + handleVerify(index)} + disabled={lookupMutation.isPending} + > + + + remove(index)} + > + + + + ))} + + ); +} + +function FilterFields({ + nestIndex, + type, +}: { + nestIndex: number; + type: "include" | "exclude"; +}) { + const form = useForm(); + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: `compose.${type}.${nestIndex}.filter`, + }); + + return ( + + + Filters + append({ property: "", op: "", value: "" })} + > + + Add Filter + + + {fields.map((field, index) => ( + + ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + + + + + )} + /> + remove(index)} + > + + + + ))} + + ); +} + +function RuleFields({ + type, + form, +}: { + type: "include" | "exclude"; + form: ReturnType>; +}) { + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: `compose.${type}`, + }); + + return ( + + + + {type === "include" ? "Include Rules" : "Exclude Rules"} + + + append({ + system: Object.values(TERMINOLOGY_SYSTEMS)[0], + concept: [], + filter: [], + }) + } + > + + Add Rule + + + + {fields.map((field, index) => ( + + + ( + + System + + + + + + + + {Object.entries(TERMINOLOGY_SYSTEMS).map( + ([key, value]) => ( + + {key} + + ), + )} + + + + )} + /> + remove(index)} + > + + + + + + + ))} + + + ); +} + +export function ValueSetForm({ + initialData, + onSubmit, + isSubmitting, +}: ValueSetFormProps) { + const form = useForm({ + resolver: zodResolver(valuesetFormSchema), + defaultValues: { + name: initialData?.name || "", + slug: initialData?.slug || "", + description: initialData?.description || "", + status: initialData?.status || "active", + is_system_defined: initialData?.is_system_defined || false, + compose: { + include: initialData?.compose?.include || [], + exclude: initialData?.compose?.exclude || [], + }, + }, + }); + + return ( + + + ( + + Name + + + + + + )} + /> + + ( + + Slug + + + + + + )} + /> + + ( + + Description + + + + + + )} + /> + + ( + + Status + + + + + + + + Active + Inactive + + + + + )} + /> + + + + + + + + {isSubmitting ? "Saving..." : "Save ValueSet"} + + + + ); +} diff --git a/src/components/ValueSet/ValueSetList.tsx b/src/components/ValueSet/ValueSetList.tsx new file mode 100644 index 00000000000..f2168758e5a --- /dev/null +++ b/src/components/ValueSet/ValueSetList.tsx @@ -0,0 +1,118 @@ +import { useQuery } from "@tanstack/react-query"; +import { PlusIcon } from "lucide-react"; +import { Link, useNavigate } from "raviger"; +import { useTranslation } from "react-i18next"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; + +import Loading from "@/components/Common/Loading"; + +import query from "@/Utils/request/query"; +import valuesetApi from "@/types/valueset/valuesetApi"; + +export function ValueSetList() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { data: response, isLoading } = useQuery({ + queryKey: ["valuesets"], + queryFn: query(valuesetApi.list), + }); + + if (isLoading) { + return ; + } + + const valuesets = response?.results || []; + + return ( + + + + {t("valuesets")} + {t("manage_valuesets")} + + + + + {t("create_new")} + + + + + + + + + + {t("name")} + + + {t("slug")} + + + {t("status")} + + + {t("description")} + + + {t("system")} + + + {t("actions")} + + + + + {valuesets.map((valueset) => ( + + + + {valueset.name} + + + + {valueset.slug} + + + + {valueset.status} + + + + + {valueset.description} + + + + {valueset.is_system_defined ? t("yes") : t("no")} + + + {!valueset.is_system_defined && ( + + navigate(`/admin/valuesets/${valueset.slug}/edit`) + } + className="hover:bg-primary/5" + > + {t("edit")} + + )} + + + ))} + + + + + ); +} diff --git a/src/components/ui/autocomplete.tsx b/src/components/ui/autocomplete.tsx index 8e960e0db6c..e3ecf950e5a 100644 --- a/src/components/ui/autocomplete.tsx +++ b/src/components/ui/autocomplete.tsx @@ -69,13 +69,12 @@ export default function Autocomplete({ {options.map((option) => ( { const currentValue = - options.find( - (option) => option.label.toLowerCase() === v.toLowerCase(), - )?.value || ""; - onChange(currentValue === value ? "" : currentValue); + options.find((o) => `${o.label} - ${o.value}` === v)?.value || + ""; + onChange(currentValue); setOpen(false); }} > diff --git a/src/components/ui/date-field.tsx b/src/components/ui/date-field.tsx index daeff36be5f..b82ad00c9fc 100644 --- a/src/components/ui/date-field.tsx +++ b/src/components/ui/date-field.tsx @@ -45,20 +45,25 @@ export default function DateField({ const newDay = e.target.value; setDay(newDay); + // Check if change is from spinner (stepUp/stepDown) vs keyboard input + const isFromSpinner = + e.nativeEvent instanceof InputEvent && + (e.nativeEvent as InputEvent).inputType === "insertReplacementText"; + if ( - newDay.length === 2 && + (isFromSpinner || newDay.length === 2) && parseInt(newDay) >= 1 && parseInt(newDay) <= 31 ) { - if (isValidDate(year, month, newDay) && onChange) { + const modifiedDay = isFromSpinner ? newDay.padStart(2, "0") : newDay; + if (isValidDate(year, month, modifiedDay) && onChange) { const updatedDate = new Date( parseInt(year), parseInt(month) - 1, - parseInt(newDay), + parseInt(modifiedDay), ); onChange(updatedDate); } - document.getElementById(`${id}-month-input`)?.focus(); } }; @@ -66,21 +71,27 @@ export default function DateField({ const newMonth = e.target.value; setMonth(newMonth); + // Check if change is from spinner (stepUp/stepDown) vs keyboard input + const isFromSpinner = + e.nativeEvent instanceof InputEvent && + (e.nativeEvent as InputEvent).inputType === "insertReplacementText"; + if ( - newMonth.length === 2 && + (isFromSpinner || newMonth.length === 2) && parseInt(newMonth) >= 1 && parseInt(newMonth) <= 12 ) { - if (isValidDate(year, newMonth, day) && onChange) { + const modifiedMonth = isFromSpinner + ? newMonth.padStart(2, "0") + : newMonth; + if (isValidDate(year, modifiedMonth, day) && onChange) { const updatedDate = new Date( parseInt(year), - parseInt(newMonth) - 1, + parseInt(modifiedMonth) - 1, parseInt(day), ); onChange(updatedDate); } - - document.getElementById(`${id}-year-input`)?.focus(); } }; @@ -100,6 +111,38 @@ export default function DateField({ } }; + // Handle day blur to pad single digit values + const handleDayBlur = () => { + if (day.length === 1 && parseInt(day) >= 1 && parseInt(day) <= 9) { + const paddedDay = day.padStart(2, "0"); + setDay(paddedDay); + if (isValidDate(year, month, paddedDay) && onChange) { + const updatedDate = new Date( + parseInt(year), + parseInt(month) - 1, + parseInt(paddedDay), + ); + onChange(updatedDate); + } + } + }; + + // Handle month blur to pad single digit values + const handleMonthBlur = () => { + if (month.length === 1 && parseInt(month) >= 1) { + const paddedMonth = month.padStart(2, "0"); + setMonth(paddedMonth); + if (isValidDate(year, paddedMonth, day) && onChange) { + const updatedDate = new Date( + parseInt(year), + parseInt(paddedMonth) - 1, + parseInt(day), + ); + onChange(updatedDate); + } + } + }; + return ( @@ -109,6 +152,7 @@ export default function DateField({ placeholder="DD" value={day} onChange={handleDayChange} + onBlur={handleDayBlur} min={1} max={31} id={`${id}-day-input`} @@ -124,6 +168,7 @@ export default function DateField({ placeholder="MM" value={month} onChange={handleMonthChange} + onBlur={handleMonthBlur} min={1} max={12} id={`${id}-month-input`} @@ -140,6 +185,7 @@ export default function DateField({ value={year} onChange={handleYearChange} min={1900} + max={new Date().getFullYear()} id={`${id}-year-input`} data-cy={`${id}-year-input`} disabled={disabled} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index b76d7c5b131..e2514538690 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>( { const Markdown = React.forwardRef( ({ className, content, prose = true, ...props }, ref) => { - const html = React.useMemo(() => md.render(content), [content]); + const html = React.useMemo(() => { + const renderedHtml = md.render(content); + return DOMPurify.sanitize(renderedHtml); + }, [content]); if (prose) { return ( diff --git a/src/components/ui/sidebar/admin-nav.tsx b/src/components/ui/sidebar/admin-nav.tsx new file mode 100644 index 00000000000..e0544ad919b --- /dev/null +++ b/src/components/ui/sidebar/admin-nav.tsx @@ -0,0 +1,33 @@ +import { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; + +import { NavMain } from "@/components/ui/sidebar/nav-main"; + +interface NavigationLink { + name: string; + url: string; + icon?: string; +} + +function generateAdminLinks(t: TFunction) { + const baseUrl = "/admin"; + const links: NavigationLink[] = [ + { + name: t("questionnaire"), + url: `${baseUrl}/questionnaire`, + icon: "d-book-open", + }, + { + name: "Valuesets", + url: `${baseUrl}/valuesets`, + icon: "l-list-ol-alt", + }, + ]; + + return links; +} + +export function AdminNav() { + const { t } = useTranslation(); + return ; +} diff --git a/src/components/ui/sidebar/app-sidebar.tsx b/src/components/ui/sidebar/app-sidebar.tsx index b22136ff928..bc2a66153fc 100644 --- a/src/components/ui/sidebar/app-sidebar.tsx +++ b/src/components/ui/sidebar/app-sidebar.tsx @@ -1,6 +1,7 @@ import { DashboardIcon } from "@radix-ui/react-icons"; import { Link, usePathParams } from "raviger"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import { Sidebar, @@ -12,6 +13,7 @@ import { SidebarMenuItem, SidebarRail, } from "@/components/ui/sidebar"; +import { AdminNav } from "@/components/ui/sidebar/admin-nav"; import { FacilityNav } from "@/components/ui/sidebar/facility-nav"; import { FacilitySwitcher } from "@/components/ui/sidebar/facility-switcher"; import { @@ -33,6 +35,7 @@ interface AppSidebarProps extends React.ComponentProps { export enum SidebarFor { FACILITY = "facility", PATIENT = "patient", + ADMIN = "admin", } export function AppSidebar({ @@ -40,6 +43,7 @@ export function AppSidebar({ sidebarFor = SidebarFor.FACILITY, ...props }: AppSidebarProps) { + const { t } = useTranslation(); const exactMatch = usePathParams("/facility/:facilityId"); const subpathMatch = usePathParams("/facility/:facilityId/*"); const facilityId = exactMatch?.facilityId || subpathMatch?.facilityId; @@ -50,6 +54,7 @@ export function AppSidebar({ const facilitySidebar = sidebarFor === SidebarFor.FACILITY; const patientSidebar = sidebarFor === SidebarFor.PATIENT; + const adminSidebar = sidebarFor === SidebarFor.ADMIN; const [selectedFacility, setSelectedFacility] = React.useState(null); @@ -60,12 +65,13 @@ export function AppSidebar({ }, [user?.organizations, organizationId]); React.useEffect(() => { - if (!user?.facilities || !facilityId || !facilitySidebar) return; - - const facility = user.facilities.find((f) => f.id === facilityId); - if (facility) { - setSelectedFacility(facility); + if (!user?.facilities || !facilityId || !facilitySidebar) { + setSelectedFacility(null); + return; } + + const facility = user.facilities.find((f) => f.id === facilityId) || null; + setSelectedFacility(facility); }, [facilityId, user?.facilities, facilitySidebar]); const hasFacilities = user?.facilities && user.facilities.length > 0; @@ -85,7 +91,7 @@ export function AppSidebar({ selectedOrganization={selectedOrganization} /> )} - {selectedFacility && hasFacilities && ( + {facilityId && selectedFacility && hasFacilities && ( - View Dashboard + {t("view_dashboard")} @@ -123,6 +129,7 @@ export function AppSidebar({ )} {patientSidebar && } + {adminSidebar && } diff --git a/src/components/ui/sidebar/patient-switcher.tsx b/src/components/ui/sidebar/patient-switcher.tsx index 6718146ba71..97810946b03 100644 --- a/src/components/ui/sidebar/patient-switcher.tsx +++ b/src/components/ui/sidebar/patient-switcher.tsx @@ -1,5 +1,7 @@ import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + import { Select, SelectContent, @@ -13,8 +15,6 @@ import { Avatar } from "@/components/Common/Avatar"; import { usePatientContext } from "@/hooks/usePatientUser"; -import { classNames } from "@/Utils/utils"; - interface PatientSwitcherProps { className?: string; } @@ -30,12 +30,7 @@ export function PatientSwitcher({ className }: PatientSwitcherProps) { } return ( - + , React.ComponentPropsWithoutRef ->(({ children, content, sideOffset = 4, className }, ref) => { +>(({ children, content, sideOffset = 4, className, side }, ref) => { const [open, setOpen] = React.useState(false); return ( @@ -45,6 +45,7 @@ const TooltipComponent = React.forwardRef< "z-50 overflow-hidden rounded-md bg-gray-900 px-3 py-1.5 text-xs text-gray-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-gray-50 dark:text-gray-900", className, )} + side={side} > {content} diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts deleted file mode 100644 index 66ee398f962..00000000000 --- a/src/hooks/useDebounce.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useRef } from "react"; - -export default function useDebounce( - callback: (...args: T) => void, - delay: number, -) { - const callbackRef = useRef(callback); - const timeoutRef = useRef | null>(null); - - useEffect(() => { - callbackRef.current = callback; - }, [callback]); - - useEffect(() => { - return () => { - if (timeoutRef.current) clearTimeout(timeoutRef.current); - }; - }, []); - - const debouncedCallback = (...args: T) => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = setTimeout(() => { - callbackRef.current(...args); - }, delay); - }; - return debouncedCallback; -} diff --git a/src/hooks/useFileManager.tsx b/src/hooks/useFileManager.tsx index 6a704b02c77..83e999b97d0 100644 --- a/src/hooks/useFileManager.tsx +++ b/src/hooks/useFileManager.tsx @@ -9,13 +9,18 @@ import { cn } from "@/lib/utils"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; -import DialogModal from "@/components/Common/Dialog"; import FilePreviewDialog from "@/components/Common/FilePreviewDialog"; -import { StateInterface } from "@/components/Files/FileUpload"; +import { StateInterface } from "@/components/Common/FilePreviewDialog"; import { FileUploadModel } from "@/components/Patient/models"; import { @@ -257,67 +262,156 @@ export default function useFileManager( downloadURL={downloadURL} uploadedFiles={uploadedFiles} onClose={handleFilePreviewClose} - fixedWidth={false} className="h-[80vh] w-full md:h-screen" loadFile={viewFile} currentIndex={currentIndex} /> - - - setArchiveDialogueOpen(null)} + > + + + + + + + + + {t("archive_file")} + + {t("this_action_is_irreversible")} + + + + + + + { + event.preventDefault(); + handleFileArchive(archiveDialogueOpen); + }} + className="mx-2 my-4 flex w-full flex-col" + > + + + }} + /> + + setArchiveReason(e.target.value)} + className={cn( + archiveReasonError && + "border-red-500 focus-visible:ring-red-500", + )} /> + {archiveReasonError && ( + {archiveReasonError} + )} - - Archive File - - {t("this_action_is_irreversible")} - + + setArchiveDialogueOpen(null)} + > + {t("cancel")} + + + {t("proceed")} + - + + + + setArchiveDialogueOpen(null)} + onOpenChange={() => setArchiveDialogueOpen(null)} > - { - event.preventDefault(); - handleFileArchive(archiveDialogueOpen); - }} - className="mx-2 my-4 flex w-full flex-col" - > - - - }} - /> - - setArchiveReason(e.target.value)} - className={cn( - archiveReasonError && - "border-red-500 focus-visible:ring-red-500", - )} - /> - {archiveReasonError && ( - {archiveReasonError} - )} + + + + {archiveDialogueOpen?.name} {t("archived")} + + + + + {t("this_file_has_been_archived")} + + + {[ + { + label: "File Name", + content: archiveDialogueOpen?.name, + icon: "l-file", + }, + { + label: "Uploaded By", + content: archiveDialogueOpen?.uploaded_by?.username, + icon: "l-user", + }, + { + label: "Uploaded On", + content: formatDateTime(archiveDialogueOpen?.created_date), + icon: "l-clock", + }, + { + label: "Archive Reason", + content: archiveDialogueOpen?.archive_reason, + icon: "l-archive", + }, + { + label: "Archived By", + content: archiveDialogueOpen?.archived_by?.username, + icon: "l-user", + }, + { + label: "Archived On", + content: formatDateTime(archiveDialogueOpen?.archived_datetime), + icon: "l-clock", + }, + ].map((item, index) => ( + + + + + + + {item.label} + + + {item.content} + + + + ))} - + {t("cancel")} - - {t("proceed")} - - - - - {archiveDialogueOpen?.name} (Archived) - - } - fixedWidth={false} - className="md:w-[700px]" - onClose={() => setArchiveDialogueOpen(null)} + + + setEditDialogueOpen(null)} > - - - {t("this_file_has_been_archived")} - - - {[ - { - label: "File Name", - content: archiveDialogueOpen?.name, - icon: "l-file", - }, - { - label: "Uploaded By", - content: archiveDialogueOpen?.uploaded_by?.username, - icon: "l-user", - }, - { - label: "Uploaded On", - content: formatDateTime(archiveDialogueOpen?.created_date), - icon: "l-clock", - }, - { - label: "Archive Reason", - content: archiveDialogueOpen?.archive_reason, - icon: "l-archive", - }, - { - label: "Archived By", - content: archiveDialogueOpen?.archived_by?.username, - icon: "l-user", - }, - { - label: "Archived On", - content: formatDateTime(archiveDialogueOpen?.archived_datetime), - icon: "l-clock", - }, - ].map((item, index) => ( - - - - - - - {item.label} + + + + + + - - {item.content} + + {t("rename_file")} - - ))} - - - setArchiveDialogueOpen(null)} + + + { + event.preventDefault(); + setEditing(true); + if (editDialogueOpen) partialupdateFileName(editDialogueOpen); + }} + className="flex w-full flex-col" > - {t("cancel")} - - - - - - + {t("enter_the_file_name")} + { + setEditDialogueOpen({ + ...editDialogueOpen, + name: e.target.value, + }); + }} /> + {editError && {editError}} - - {t("rename_file")} + + setEditDialogueOpen(null)} + > + {t("cancel")} + + + {t("proceed")} + - - } - onClose={() => setEditDialogueOpen(null)} - > - { - event.preventDefault(); - setEditing(true); - if (editDialogueOpen) partialupdateFileName(editDialogueOpen); - }} - className="flex w-full flex-col" - > - - {t("enter_the_file_name")} - { - setEditDialogueOpen({ - ...editDialogueOpen, - name: e.target.value, - }); - }} - /> - {editError && {editError}} - - - setEditDialogueOpen(null)} - > - {t("cancel")} - - - {t("proceed")} - - - - + + + > ); diff --git a/src/hooks/useFileUpload.tsx b/src/hooks/useFileUpload.tsx index d53b9497e08..d1b556fbb22 100644 --- a/src/hooks/useFileUpload.tsx +++ b/src/hooks/useFileUpload.tsx @@ -363,8 +363,8 @@ export default function useFileUpload( const Dialogues = ( <> setCameraModalOpen(false)} + open={cameraModalOpen} + onOpenChange={(open) => setCameraModalOpen(open)} onCapture={(file) => { setFiles((prev) => [...prev, file]); }} diff --git a/src/hooks/useFilters.tsx b/src/hooks/useFilters.tsx index b6e9022a675..a0c10a7a24e 100644 --- a/src/hooks/useFilters.tsx +++ b/src/hooks/useFilters.tsx @@ -2,12 +2,14 @@ import { QueryParam, setQueryParamsOptions, useQueryParams } from "raviger"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + import GenericFilterBadge from "@/CAREUI/display/FilterBadge"; import PaginationComponent from "@/components/Common/Pagination"; import FiltersCache from "@/Utils/FiltersCache"; -import { classNames, humanizeStrings } from "@/Utils/utils"; +import { humanizeStrings } from "@/Utils/utils"; export type FilterState = Record; @@ -222,7 +224,7 @@ export default function useFilters({ } return ( limit ? "visible" : "invisible", !noMargin && "mt-4", diff --git a/src/index.tsx b/src/index.tsx index 9154b832187..4f8ac291d51 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ import careConfig from "@careConfig"; import * as Sentry from "@sentry/browser"; +import React from "react"; import { createRoot } from "react-dom/client"; import { registerSW } from "virtual:pwa-register"; @@ -31,4 +32,8 @@ if (import.meta.env.PROD) { } const root = createRoot(document.getElementById("root") as HTMLElement); -root.render(); +root.render( + + + , +); diff --git a/src/pages/Appointments/AppointmentDetail.tsx b/src/pages/Appointments/AppointmentDetail.tsx index c29454bfaf0..624647e5987 100644 --- a/src/pages/Appointments/AppointmentDetail.tsx +++ b/src/pages/Appointments/AppointmentDetail.tsx @@ -54,17 +54,16 @@ import { formatName, getReadableDuration, saveElementAsImage, - stringifyGeoOrganization, + stringifyNestedObject, } from "@/Utils/utils"; import { AppointmentTokenCard } from "@/pages/Appointments/components/AppointmentTokenCard"; -import { formatAppointmentSlotTime } from "@/pages/Appointments/utils"; import { FacilityData } from "@/types/facility/facility"; import { Appointment, AppointmentFinalStatuses, AppointmentUpdateRequest, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; import { AppointmentSlotPicker } from "./components/AppointmentSlotPicker"; @@ -135,17 +134,9 @@ export default function AppointmentDetail(props: Props) { } const { patient } = appointment; - const appointmentDate = formatAppointmentSlotTime(appointment); return ( - + - {stringifyGeoOrganization(appointment.patient.geo_organization)} + {stringifyNestedObject(appointment.patient.geo_organization)} {t("pincode")}: {appointment.patient.pincode} diff --git a/src/pages/Appointments/AppointmentsPage.tsx b/src/pages/Appointments/AppointmentsPage.tsx index 27102edcc59..dfb7c5e1401 100644 --- a/src/pages/Appointments/AppointmentsPage.tsx +++ b/src/pages/Appointments/AppointmentsPage.tsx @@ -82,7 +82,7 @@ import { AppointmentStatuses, TokenSlot, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface QueryParams { practitioner: string | null; @@ -337,8 +337,6 @@ export default function AppointmentsPage(props: { facilityId?: string }) { return ( - + + setQParams({ + ...qParams, + practitioner: null, + slot: null, + }) + } + className="cursor-pointer w-full" + > + + {t("show_all")} + {!qParams.practitioner && ( + + )} + + + {schedulableUsersQuery.data?.users.map((user) => ( setQParams({ ...qParams, - practitioner: null, + practitioner: user.username, slot: null, }) } - className="cursor-pointer" + className="cursor-pointer w-full" > - {t("show_all")} - {!qParams.practitioner && ( + + + {formatName(user)} + + {user.user_type} + + + {qParams.practitioner === user.username && ( )} - - {schedulableUsersQuery.data?.users.map((user) => ( - - - setQParams({ - ...qParams, - practitioner: user.username, - slot: null, - }) - } - className="cursor-pointer" - > - - - {formatName(user)} - - {user.user_type} - - - {qParams.practitioner === user.username && ( - - )} - - ))} diff --git a/src/pages/Appointments/BookAppointment.tsx b/src/pages/Appointments/BookAppointment.tsx index 5a670f81e16..fb61ed487f4 100644 --- a/src/pages/Appointments/BookAppointment.tsx +++ b/src/pages/Appointments/BookAppointment.tsx @@ -25,7 +25,7 @@ import useAppHistory from "@/hooks/useAppHistory"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { formatDisplayName, formatName } from "@/Utils/utils"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; import { AppointmentSlotPicker } from "./components/AppointmentSlotPicker"; diff --git a/src/pages/Appointments/components/AppointmentSlotPicker.tsx b/src/pages/Appointments/components/AppointmentSlotPicker.tsx index 1830c9b423f..0220b47678a 100644 --- a/src/pages/Appointments/components/AppointmentSlotPicker.tsx +++ b/src/pages/Appointments/components/AppointmentSlotPicker.tsx @@ -18,7 +18,7 @@ import { useAvailabilityHeatmap, } from "@/pages/Appointments/utils"; import { TokenSlot } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface AppointmentSlotPickerProps { facilityId: string; diff --git a/src/pages/Appointments/utils.ts b/src/pages/Appointments/utils.ts index 50ab30c5b6f..2144524f130 100644 --- a/src/pages/Appointments/utils.ts +++ b/src/pages/Appointments/utils.ts @@ -15,7 +15,7 @@ import { AvailabilityHeatmapResponse, TokenSlot, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; export const groupSlotsByAvailability = (slots: TokenSlot[]) => { const result: { diff --git a/src/pages/Apps/PlugConfigEdit.tsx b/src/pages/Apps/PlugConfigEdit.tsx index 353a1796b88..f6230b71f15 100644 --- a/src/pages/Apps/PlugConfigEdit.tsx +++ b/src/pages/Apps/PlugConfigEdit.tsx @@ -2,6 +2,8 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { useNavigate } from "raviger"; import { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { @@ -15,7 +17,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; @@ -108,7 +110,7 @@ export function PlugConfigEdit({ slug }: Props) { Cancel Delete diff --git a/src/pages/Encounters/EncounterList.tsx b/src/pages/Encounters/EncounterList.tsx index f6eaf605c80..9af50a0ebca 100644 --- a/src/pages/Encounters/EncounterList.tsx +++ b/src/pages/Encounters/EncounterList.tsx @@ -213,7 +213,7 @@ export function EncounterList({ const { t } = useTranslation(); return ( - + @@ -261,6 +261,7 @@ export function EncounterList({ onFieldChange={handleFieldChange} onSearch={handleSearch} className="w-full border-none shadow-none" + autoFocus /> diff --git a/src/pages/Encounters/EncounterShow.tsx b/src/pages/Encounters/EncounterShow.tsx index 1f065b6e50c..020bd8f636d 100644 --- a/src/pages/Encounters/EncounterShow.tsx +++ b/src/pages/Encounters/EncounterShow.tsx @@ -26,6 +26,7 @@ export interface EncounterTabProps { facilityId: string; encounter: Encounter; patient: Patient; + subPage?: string; } const defaultTabs = { @@ -44,10 +45,11 @@ interface Props { encounterId: string; facilityId: string; tab?: string; + subPage?: string; } export const EncounterShow = (props: Props) => { - const { facilityId, encounterId } = props; + const { facilityId, encounterId, subPage } = props; const { t } = useTranslation(); const pluginTabs = useCareAppConsultationTabs(); @@ -74,6 +76,7 @@ export const EncounterShow = (props: Props) => { const encounterTabProps: EncounterTabProps = { encounter: encounterData, patient: encounterData.patient, + subPage: subPage, facilityId, }; @@ -97,21 +100,7 @@ export const EncounterShow = (props: Props) => { return ( - + { )} > )} */} - - {t("patient_details")} - diff --git a/src/pages/Encounters/PrintPrescription.tsx b/src/pages/Encounters/PrintPrescription.tsx index 607a42eee91..6c4b0f4d332 100644 --- a/src/pages/Encounters/PrintPrescription.tsx +++ b/src/pages/Encounters/PrintPrescription.tsx @@ -17,8 +17,9 @@ import medicationRequestApi from "@/types/emr/medicationRequest/medicationReques export const PrintPrescription = (props: { facilityId: string; encounterId: string; + patientId: string; }) => { - const { facilityId, encounterId } = props; + const { facilityId, encounterId, patientId } = props; const { t } = useTranslation(); const { data: encounter } = useQuery({ @@ -30,12 +31,12 @@ export const PrintPrescription = (props: { }); const { data: medications } = useQuery({ - queryKey: ["medication_requests", encounter?.patient?.id], + queryKey: ["medication_requests", patientId], queryFn: query(medicationRequestApi.list, { - pathParams: { patientId: encounter?.patient?.id || "" }, + pathParams: { patientId }, queryParams: { encounter: encounterId, limit: 50, offset: 0 }, }), - enabled: !!encounter?.patient?.id, + enabled: !!patientId, }); if (!medications?.results?.length) { @@ -60,12 +61,8 @@ export const PrintPrescription = (props: { return ( @@ -91,7 +88,7 @@ export const PrintPrescription = (props: { @@ -125,7 +122,7 @@ export const PrintPrescription = (props: { ℞ {/* Medications Table */} - + {/* Doctor's Signature */} diff --git a/src/pages/Encounters/tabs/EncounterFilesTab.tsx b/src/pages/Encounters/tabs/EncounterFilesTab.tsx index ff270ac9d60..76add034ca5 100644 --- a/src/pages/Encounters/tabs/EncounterFilesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterFilesTab.tsx @@ -7,7 +7,8 @@ export const EncounterFilesTab = (props: EncounterTabProps) => { ); }; diff --git a/src/pages/Encounters/tabs/EncounterNotesTab.tsx b/src/pages/Encounters/tabs/EncounterNotesTab.tsx index 89b535134df..058606392e4 100644 --- a/src/pages/Encounters/tabs/EncounterNotesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterNotesTab.tsx @@ -9,11 +9,13 @@ import { Info, Loader2, MessageCircle, + MessageSquare, MessageSquarePlus, Plus, Send, Users, } from "lucide-react"; +import { Link, usePathParams } from "raviger"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useInView } from "react-intersection-observer"; @@ -25,6 +27,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, + DialogClose, DialogContent, DialogDescription, DialogFooter, @@ -61,6 +64,7 @@ import { Thread } from "@/types/notes/threads"; const MESSAGES_LIMIT = 20; // Thread templates for quick selection + const threadTemplates = [ "Treatment Plan", "Medication Notes", @@ -118,6 +122,7 @@ const ThreadItem = ({ // Message item component const MessageItem = ({ message }: { message: Message }) => { const authUser = useAuthUser(); + const { facilityId } = usePathParams("/facility/:facilityId/*")!; const isCurrentUser = authUser?.external_id === message.created_by.id; return ( @@ -135,15 +140,19 @@ const MessageItem = ({ message }: { message: Message }) => { > - - - - - + + + + + + + {message.created_by.username} @@ -192,11 +201,13 @@ const NewThreadDialog = ({ onClose, onCreate, isCreating, + threadsUnused, }: { isOpen: boolean; onClose: () => void; onCreate: (title: string) => void; isCreating: boolean; + threadsUnused: string[]; }) => { const { t } = useTranslation(); const [title, setTitle] = useState(""); @@ -218,13 +229,15 @@ const NewThreadDialog = ({ - {t("encounter_notes__choose_template")} + {threadsUnused.length === 0 + ? t("encounter_notes__no_unused_threads") + : t("encounter_notes__choose_template")} - {threadTemplates.map((template) => ( + {threadsUnused.map((template) => ( - - {t("Cancel")} - + + {t("cancel")} + + onCreate(title)} disabled={!title.trim() || isCreating} @@ -258,7 +272,7 @@ const NewThreadDialog = ({ ) : ( )} - {t("Create")} + {t("create")} @@ -308,6 +322,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { const [newMessage, setNewMessage] = useState(""); const messagesEndRef = useRef(null); const { ref, inView } = useInView(); + const [commentAdded, setCommentAdded] = useState(false); // Fetch threads const { data: threadsData, isLoading: threadsLoading } = useQuery({ @@ -318,17 +333,11 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { }), }); - // Auto-select first thread - useEffect(() => { - if (threadsData?.results.length && !selectedThread) { - setSelectedThread(threadsData.results[0].id); - } - }, [threadsData, selectedThread]); - // Fetch messages with infinite scroll const { data: messagesData, isLoading: messagesLoading, + isFetching: isFetchingMessages, hasNextPage, fetchNextPage, isFetchingNextPage, @@ -382,28 +391,63 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["messages", selectedThread] }); setNewMessage(""); - setTimeout(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, 100); + setCommentAdded(true); }, }); - // Handle infinite scroll + // handle scrolling to last message when new message is added + useEffect(() => { - if (inView && hasNextPage) { - fetchNextPage(); + if (commentAdded && !isFetchingMessages) { + messagesEndRef.current?.scrollIntoView(); + setCommentAdded(false); } - }, [inView, hasNextPage, fetchNextPage]); + }, [commentAdded, isFetchingMessages]); + + const [threads, setThreads] = useState([...threadTemplates]); + + // Auto-select first thread - // Scroll to bottom on initial load and thread change useEffect(() => { - if (messagesData && !messagesLoading && !isFetchingNextPage) { + if (threadsData?.results.length) { + if (!selectedThread) setSelectedThread(threadsData.results[0].id); + const threadTitles = threadsData.results.map((thread) => thread.title); + setThreads( + threads.filter((template) => !threadTitles.includes(template)), + ); + } + }, [threadsData, selectedThread]); + + // hack to scroll to bottom on initial load + + useEffect(() => { + messagesEndRef.current?.scrollIntoView(); + }, [messagesLoading]); + + // Handle infinite scroll + + useEffect(() => { + if (inView && hasNextPage) { + fetchNextPage(); messagesEndRef.current?.scrollIntoView(); } - }, [selectedThread, messagesData, messagesLoading, isFetchingNextPage]); + }, [ + inView, + hasNextPage, + fetchNextPage, + messagesData, + isFetchingNextPage, + messagesLoading, + ]); const handleCreateThread = (title: string) => { if (title.trim()) { + if ( + threadsData?.results.some((thread) => thread.title === title.trim()) + ) { + toast.error(t("thread_already_exists")); + return; + } createThreadMutation.mutate({ title: title.trim(), encounter: encounter.id, @@ -423,6 +467,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { } const messages = messagesData?.pages.flatMap((page) => page.results) ?? []; + const totalMessages = messagesData?.pages[0]?.count ?? 0; return ( @@ -529,8 +574,8 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { {/* Main Content */} - {/* Mobile Header */} - + {/* Header */} + {selectedThread ? ( @@ -539,10 +584,27 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { ?.title } - - - {messages.length} - + + + + + + {new Set(messages.map((m) => m.created_by.id)).size} + + + {totalMessages} + + + + + {t("participants")}:{" "} + {new Set(messages.map((m) => m.created_by.id)).size} + + + {t("messages")}: {totalMessages} + + + ) : ( @@ -550,13 +612,12 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { )} - {selectedThread ? ( <> {messagesLoading ? ( - + ) : ( @@ -580,17 +641,17 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { )) )} - {isFetchingNextPage && ( + {isFetchingNextPage ? ( + ) : ( + )} - - {/* Message Input */} @@ -662,6 +723,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { onClose={() => setShowNewThreadDialog(false)} onCreate={handleCreateThread} isCreating={createThreadMutation.isPending} + threadsUnused={threads} /> ); diff --git a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx index 5f6eaa151d9..d55cf9b5346 100644 --- a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx @@ -14,7 +14,7 @@ export const EncounterUpdatesTab = ({ return ( {/* Main Content Area */} - + {/* Left Column - Symptoms, Diagnoses, and Questionnaire Responses */} {/* Allergies Section */} @@ -55,7 +55,7 @@ export const EncounterUpdatesTab = ({ {/* Right Column - Observations */} - + diff --git a/src/pages/Facility/overview.tsx b/src/pages/Facility/overview.tsx index 346a34c6c48..cf07db69e65 100644 --- a/src/pages/Facility/overview.tsx +++ b/src/pages/Facility/overview.tsx @@ -27,7 +27,7 @@ export function FacilityOverview({ facilityId }: FacilityOverviewProps) { href: `/facility/${facilityId}/users/${user?.username}/availability`, }, { - title: t("Encounters"), + title: t("encounters"), description: t("manage_facility_users"), icon: Users, href: `/facility/${facilityId}/encounters`, diff --git a/src/pages/Facility/settings/layout.tsx b/src/pages/Facility/settings/layout.tsx index 9ba1d35ec39..a2b7c1c58f3 100644 --- a/src/pages/Facility/settings/layout.tsx +++ b/src/pages/Facility/settings/layout.tsx @@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import ErrorPage from "@/components/ErrorPages/DefaultErrorPage"; + import { GeneralSettings } from "./general/general"; import LocationList from "./locations/LocationList"; import LocationView from "./locations/LocationView"; @@ -28,7 +30,7 @@ const getRoutes = (facilityId: string) => ({ "/location/:id": ({ id }: { id: string }) => ( ), - "*": () => 404, + "*": () => , }); export function SettingsLayout({ facilityId }: SettingsLayoutProps) { diff --git a/src/pages/Facility/settings/locations/LocationForm.tsx b/src/pages/Facility/settings/locations/LocationForm.tsx index dc9b3f96bfd..6f82f64228a 100644 --- a/src/pages/Facility/settings/locations/LocationForm.tsx +++ b/src/pages/Facility/settings/locations/LocationForm.tsx @@ -223,7 +223,7 @@ export default function LocationForm({ - + {locationFormOptions.map((option) => ( {option.label} diff --git a/src/pages/Facility/settings/locations/LocationView.tsx b/src/pages/Facility/settings/locations/LocationView.tsx index d5527d5fe4f..9bab726b813 100644 --- a/src/pages/Facility/settings/locations/LocationView.tsx +++ b/src/pages/Facility/settings/locations/LocationView.tsx @@ -1,10 +1,18 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Link } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Badge } from "@/components/ui/badge"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -44,7 +52,6 @@ export default function LocationView({ id, facilityId }: Props) { pathParams: { facility_id: facilityId, id }, }), }); - const { data: locationOrganizations } = useQuery({ queryKey: ["location", id, "organizations"], queryFn: query(locationApi.getOrganizations, { @@ -92,110 +99,157 @@ export default function LocationView({ id, facilityId }: Props) { ); + const generateBreadcrumbs = (location: any) => { + const breadcrumbs = []; + let current = location; + while (current) { + breadcrumbs.unshift({ + name: current.name, + id: current.id, + }); + current = current.parent; + } + return breadcrumbs; + }; + const breadcrumbs = location ? generateBreadcrumbs(location) : []; return ( - - - - - - {t("locations")} - - {getLocationFormLabel(location?.form)} - - - {location?.status} - - {location && "mode" in location && location.mode === "kind" && ( - - - {t("add_location")} - + <> + + + + + {t("home")} + + + {breadcrumbs.map((breadcrumb, index) => ( + + {index === breadcrumbs.length - 1 ? ( + + {breadcrumb.name} + + ) : ( + <> + + {breadcrumb.name} + + + > )} + + ))} + + + + + + + + + {t("locations")} + + {getLocationFormLabel(location?.form)} + + + {location?.status} + + {location && "mode" in location && location.mode === "kind" && ( + + + {t("add_location")} + + )} + + + { + setSearchQuery(e.target.value); + setPage(1); + }} + className="w-full" + /> + - - { - setSearchQuery(e.target.value); - setPage(1); + {locationOrganizations && ( + + + {t("manage_organizations")} + + } + onUpdate={() => { + queryClient.invalidateQueries({ + queryKey: ["location", facilityId, id], + }); }} - className="w-full" /> - + )} - {locationOrganizations && ( - - - {t("manage_organizations")} - - } - onUpdate={() => { - queryClient.invalidateQueries({ - queryKey: ["location", facilityId, id], - }); - }} - /> - )} - - {isLoading ? ( - - - - ) : ( - + {isLoading ? ( - {children?.results?.length ? ( - children.results.map((childLocation: LocationList) => ( - + + ) : ( + + + {children?.results?.length ? ( + children.results.map((childLocation: LocationList) => ( + + )) + ) : ( + + + {searchQuery + ? t("no_locations_found") + : t("no_child_locations_found")} + + + )} + + {children && children.count > limit && ( + + setPage(page)} + defaultPerPage={limit} + cPage={page} /> - )) - ) : ( - - - {searchQuery - ? t("no_locations_found") - : t("no_child_locations_found")} - - + )} - {children && children.count > limit && ( - - setPage(page)} - defaultPerPage={limit} - cPage={page} - /> - - )} - - )} - + )} + - - + + + > ); } diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx index 571d898bd10..da1e2eae284 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx @@ -55,12 +55,7 @@ export default function FacilityOrganizationIndex({ if (!data?.results?.length) { return ( - + @@ -87,12 +82,7 @@ export default function FacilityOrganizationIndex({ } return ( - + diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx index 509f451a592..d0bde50e2ce 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx @@ -1,19 +1,19 @@ import { useQuery } from "@tanstack/react-query"; -import { useQueryParams } from "raviger"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/Common/Avatar"; -import { - CardGridSkeleton, - CardListSkeleton, -} from "@/components/Common/SkeletonLoading"; +import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import { UserStatusIndicator } from "@/components/Users/UserListAndCard"; +import useFilters from "@/hooks/useFilters"; + import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; import AddUserSheet from "@/pages/Organization/components/AddUserSheet"; @@ -29,19 +29,30 @@ interface Props { } export default function FacilityOrganizationUsers({ id, facilityId }: Props) { - const [qParams, setQueryParams] = useQueryParams<{ + const [sheetState, setSheetState] = useState<{ sheet: string; username: string; - }>(); + }>({ + sheet: "", + username: "", + }); + const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ + limit: 12, + }); const { t } = useTranslation(); - const openAddUserSheet = qParams.sheet === "add"; - const openLinkUserSheet = qParams.sheet === "link"; + const openAddUserSheet = sheetState.sheet === "add"; + const openLinkUserSheet = sheetState.sheet === "link"; const { data: users, isLoading: isLoadingUsers } = useQuery({ - queryKey: ["facilityOrganizationUsers", facilityId, id], - queryFn: query(routes.facilityOrganization.listUsers, { + queryKey: ["facilityOrganizationUsers", facilityId, id, qParams], + queryFn: query.debounced(routes.facilityOrganization.listUsers, { pathParams: { facilityId, organizationId: id }, + queryParams: { + search_text: qParams.search || undefined, + limit: resultsPerPage, + offset: ((qParams.page || 1) - 1) * resultsPerPage, + }, }), enabled: !!id, }); @@ -50,32 +61,32 @@ export default function FacilityOrganizationUsers({ id, facilityId }: Props) { return null; } - if (isLoadingUsers) { - return ( - - - - - - - - - ); - } - return ( - - {t("users")} - + + + + { + updateQuery({ search: e.target.value || undefined }); + }} + className="w-full pl-8" + /> + + { - setQueryParams({ sheet: open ? "add" : "", username: "" }); + setSheetState({ sheet: open ? "add" : "", username: "" }); }} onUserCreated={(user) => { - setQueryParams({ sheet: "link", username: user.username }); + setSheetState({ sheet: "link", username: user.username }); }} /> { - setQueryParams({ sheet: open ? "link" : "", username: "" }); + setSheetState({ sheet: open ? "link" : "", username: "" }); }} - preSelectedUsername={qParams.username} + preSelectedUsername={sheetState.username} /> - - {users?.results?.length === 0 ? ( - - - {t("no_users_found")} - - - ) : ( - users?.results?.map((userRole: OrganizationUserRole) => ( - - - - - - - - - - {userRole.user.first_name} {userRole.user.last_name} - - - + {isLoadingUsers ? ( + + + + ) : ( + + + {!users?.results?.length ? ( + + + {t("no_users_found")} + + + ) : ( + users.results.map((userRole: OrganizationUserRole) => ( + + + + + + + + + + {userRole.user.first_name}{" "} + {userRole.user.last_name} + + + + + + {userRole.user.username} - - + + + + {t("role")} + + {userRole.role.name} + + + + + {t("phone_number")} + + + {userRole.user.phone_number} + + + - - - - {t("role")} - - {userRole.role.name} - + + + {t("see_details")} + + } + /> - - {t("phone_number")} - - {userRole.user.phone_number} - - - - - - - - {t("more_details")} - - } - /> - - - - - )) - )} - + + + )) + )} + + + {(users?.results || []).length > 0 && + users?.count && + users.count > resultsPerPage && ( + + + + )} + + )} ); diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx index 1ce58eaa786..5efc8fc4076 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx @@ -10,11 +10,13 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import Pagination from "@/components/Common/Pagination"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; +import useFilters from "@/hooks/useFilters"; + import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; +import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; import CreateFacilityOrganizationSheet from "./components/CreateFacilityOrganizationSheet"; import FacilityOrganizationLayout from "./components/FacilityOrganizationLayout"; @@ -24,12 +26,47 @@ interface Props { facilityId: string; } -export default function FacilityOrganizationView({ id, facilityId }: Props) { +function OrganizationCard({ + org, +}: { + org: FacilityOrganization; + facilityId: string; +}) { const { t } = useTranslation(); - const [page, setPage] = useState(1); + return ( + + + + + + {org.name} + + + {org.org_type} + + + + + {t("see_details")} + + + + + + ); +} + +export default function FacilityOrganizationView({ id, facilityId }: Props) { + const { t } = useTranslation(); + const { qParams, Pagination, resultsPerPage } = useFilters({ + limit: 12, + cacheBlacklist: ["username"], + }); const [searchQuery, setSearchQuery] = useState(""); - const limit = 12; // 3x4 grid const { data: children, isLoading } = useQuery({ queryKey: [ @@ -37,16 +74,16 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { "list", facilityId, id, - page, - limit, + qParams.page, + resultsPerPage, searchQuery, ], queryFn: query.debounced(routes.facilityOrganization.list, { pathParams: { facilityId }, queryParams: { parent: id, - offset: (page - 1) * limit, - limit, + offset: ((qParams.page || 1) - 1) * resultsPerPage, + limit: resultsPerPage, name: searchQuery || undefined, }, }), @@ -54,20 +91,24 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { return ( - + - {t("departments")} - - - { - setSearchQuery(e.target.value); - setPage(1); // Reset to first page on search - }} - className="w-full" - /> + + + + + { + setSearchQuery(e.target.value); + }} + className="w-full pl-8" + /> + ) : ( - + {children?.results?.length ? ( children.results.map((org) => ( - - - - - - - {org.name} - - - {org.org_type} - {org.org_type} - - - - - {t("view_details")} - - - - - {org.description && ( - - {org.description} - - )} - - - + )) ) : ( @@ -127,14 +142,9 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { )} - {children && children.count > limit && ( + {children && children.count > resultsPerPage && ( - setPage(page)} - defaultPerPage={limit} - cPage={page} - /> + )} diff --git a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx index aa0a1774d58..feca583e80d 100644 --- a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx @@ -1,4 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { t } from "i18next"; import { useState } from "react"; import { toast } from "sonner"; @@ -62,7 +63,7 @@ export default function CreateFacilityOrganizationSheet({ queryClient.invalidateQueries({ queryKey: ["getCurrentUser"], }); - toast.success("Organization created successfully"); + toast.success(t("organization_created_successfully")); setOpen(false); setName(""); setDescription(""); @@ -72,7 +73,7 @@ export default function CreateFacilityOrganizationSheet({ const handleSubmit = () => { if (!name.trim()) { - toast.error("Please enter an organization name"); + toast.error(t("please_enter_organization_name")); return; } @@ -87,16 +88,16 @@ export default function CreateFacilityOrganizationSheet({ return ( - + - Create Department/Team + {t("add_department_team")} - Create Department/Team + {t("create_department_team")} - Create a new department/team in this facility. + {t("create_department_team_description")} @@ -105,18 +106,18 @@ export default function CreateFacilityOrganizationSheet({ setName(e.target.value)} - placeholder="Enter department/team name" + placeholder={t("enter_department_team_name")} /> - Type + {t(`type`)} setOrgType(value)} > - + {ORG_TYPES.map((type) => ( @@ -133,7 +134,7 @@ export default function CreateFacilityOrganizationSheet({ setDescription(e.target.value)} - placeholder="Enter department/team description (optional)" + placeholder={t("enter_department_team_description")} /> @@ -142,7 +143,7 @@ export default function CreateFacilityOrganizationSheet({ onClick={handleSubmit} disabled={isPending || !name.trim()} > - {isPending ? "Creating..." : "Create Organization"} + {isPending ? t("creating") : t("create_organization")} diff --git a/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx b/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx index 90ab2e20ca2..6628bfac56b 100644 --- a/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import { AlertDialog, AlertDialogAction, @@ -14,7 +16,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, @@ -218,7 +220,7 @@ export default function EditUserRoleSheet({ {t("cancel")} removeRole()} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + className={cn(buttonVariants({ variant: "destructive" }))} > {t("remove")} diff --git a/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx b/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx index c706c2b38d9..de12da5794e 100644 --- a/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx +++ b/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx @@ -1,8 +1,8 @@ import { useQuery } from "@tanstack/react-query"; import { Link, usePath } from "raviger"; +import { useTranslation } from "react-i18next"; -import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; - +import { Badge } from "@/components/ui/badge"; import { Breadcrumb, BreadcrumbItem, @@ -10,8 +10,8 @@ import { BreadcrumbList, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; -import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar"; import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Page from "@/components/Common/Page"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; @@ -32,7 +32,7 @@ interface Props { interface NavItem { path: string; title: string; - icon: IconName; + value: string; } export default function FacilityOrganizationLayout({ @@ -41,20 +41,24 @@ export default function FacilityOrganizationLayout({ children, }: Props) { const path = usePath() || ""; + const { t } = useTranslation(); const navItems: NavItem[] = [ { path: `/departments/${id}`, - title: "Departments", - icon: "d-hospital", + title: t("departments_or_teams"), + value: "departments", }, { path: `/departments/${id}/users`, - title: "Users", - icon: "d-people", + title: t("users"), + value: "users", }, ]; + const currentTab = + navItems.find((item) => item.path === path)?.value || "departments"; + const { data: org, isLoading } = useQuery({ queryKey: ["facilityOrganization", id], queryFn: query(routes.facilityOrganization.get, { @@ -74,9 +78,9 @@ export default function FacilityOrganizationLayout({ ); } - // add loading state + if (!org) { - return Not found; + return {t("not_found")}; } const orgParents: FacilityOrganizationParent[] = []; @@ -89,55 +93,76 @@ export default function FacilityOrganizationLayout({ } return ( - - {/* Since we have links to all parent organizations, we can show the breadcrumb here */} - - - {/* Org has parent and each parent may have another parent, so we need to show all the parents */} - - {orgParents.reverse().map((parent) => ( - <> - - - {parent.name} - + <> + {orgParents.length > 0 && ( + + + + {orgParents.reverse().map((parent) => ( + <> + + + + {parent.name} + + + + + + + > + ))} + + + {org.name} + - - - - > - ))} - - - {org.name} - - - - - {/* Navigation */} - - - {navItems.map((item) => ( - - - - - {item.title} + + + + )} + + {t(`facility_organization_type__${org.org_type}`)} + + } + className="mx-auto max-w-4xl" + > + + {org.description && ( + + {org.description} + + )} + + + {navItems.map((item) => ( + + + {item.title} + - - - ))} - - - {/* Page Content */} - {children} - + ))} + + + + {children} + + > ); } diff --git a/src/pages/Organization/OrganizationUsers.tsx b/src/pages/Organization/OrganizationUsers.tsx index 479059fa9a3..e05e6374097 100644 --- a/src/pages/Organization/OrganizationUsers.tsx +++ b/src/pages/Organization/OrganizationUsers.tsx @@ -1,13 +1,15 @@ import { useQuery } from "@tanstack/react-query"; +import { useCallback } from "react"; import { useTranslation } from "react-i18next"; +import { isValidPhoneNumber } from "react-phone-number-input"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/Common/Avatar"; +import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import { UserStatusIndicator } from "@/components/Users/UserListAndCard"; @@ -30,19 +32,62 @@ interface Props { export default function OrganizationUsers({ id, navOrganizationId }: Props) { const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ limit: 15, - cacheBlacklist: ["search"], + cacheBlacklist: ["name", "phone_number"], }); const { t } = useTranslation(); + const searchOptions = [ + { + key: "username", + type: "text" as const, + placeholder: "Search by username", + value: qParams.name || "", + }, + { + key: "phone_number", + type: "phone" as const, + placeholder: "Search by phone number", + value: qParams.phone_number || "", + }, + ]; + + const handleSearch = useCallback((key: string, value: string) => { + const searchParams = { + name: key === "username" ? value : "", + phone_number: + key === "phone_number" + ? isValidPhoneNumber(value) + ? value + : undefined + : undefined, + }; + updateQuery(searchParams); + }, []); + + const handleFieldChange = () => { + updateQuery({ + name: undefined, + phone_number: undefined, + }); + }; + const openAddUserSheet = qParams.sheet === "add"; const openLinkUserSheet = qParams.sheet === "link"; const { data: users, isFetching: isFetchingUsers } = useQuery({ - queryKey: ["organizationUsers", id, qParams.search, qParams.page], + queryKey: [ + "organizationUsers", + id, + qParams.name, + qParams.phone_number, + qParams.page, + ], queryFn: query.debounced(organizationApi.listUsers, { pathParams: { id }, queryParams: { - username: qParams.search, + username: qParams.name, + phone_number: qParams.phone_number, + page: qParams.page, limit: resultsPerPage, offset: ((qParams.page ?? 1) - 1) * resultsPerPage, }, @@ -88,16 +133,16 @@ export default function OrganizationUsers({ id, navOrganizationId }: Props) { - - updateQuery({ - search: e.target.value as string, - }) - } - className="max-w-sm" + option.value !== ""), + 0, + )} + onSearch={handleSearch} + onFieldChange={handleFieldChange} + className="w-full" data-cy="search-user" /> diff --git a/src/pages/Organization/components/AddUserSheet.tsx b/src/pages/Organization/components/AddUserSheet.tsx index 1efba82ca81..b6054eb4a56 100644 --- a/src/pages/Organization/components/AddUserSheet.tsx +++ b/src/pages/Organization/components/AddUserSheet.tsx @@ -33,7 +33,7 @@ export default function AddUserSheet({ return ( - + {t("add_user")} diff --git a/src/pages/Organization/components/EditUserRoleSheet.tsx b/src/pages/Organization/components/EditUserRoleSheet.tsx index 2903aff574b..ca8c6d796ab 100644 --- a/src/pages/Organization/components/EditUserRoleSheet.tsx +++ b/src/pages/Organization/components/EditUserRoleSheet.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import { AlertDialog, AlertDialogAction, @@ -14,7 +16,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, @@ -226,7 +228,7 @@ export default function EditUserRoleSheet({ {t("cancel")} removeRole()} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + className={cn(buttonVariants({ variant: "destructive" }))} > {t("remove")} diff --git a/src/pages/Organization/components/OrganizationLayout.tsx b/src/pages/Organization/components/OrganizationLayout.tsx index e10168c8257..2b3a3da33a4 100644 --- a/src/pages/Organization/components/OrganizationLayout.tsx +++ b/src/pages/Organization/components/OrganizationLayout.tsx @@ -5,13 +5,6 @@ import { useTranslation } from "react-i18next"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar"; import Page from "@/components/Common/Page"; @@ -113,31 +106,7 @@ export default function OrganizationLayout({ } return ( - - {/* Since we have links to all parent organizations, we can show the breadcrumb here */} - - - {/* Org has parent and each parent may have another parent, so we need to show all the parents */} - - {orgParents.reverse().map((parent) => ( - <> - - - {parent.name} - - - - - - > - ))} - - - {org.name} - - - - + {/* Navigation */} diff --git a/src/pages/Scheduling/ScheduleExceptions.tsx b/src/pages/Scheduling/ScheduleExceptions.tsx index 7480c49a323..96e97fa177e 100644 --- a/src/pages/Scheduling/ScheduleExceptions.tsx +++ b/src/pages/Scheduling/ScheduleExceptions.tsx @@ -15,7 +15,7 @@ import Loading from "@/components/Common/Loading"; import mutate from "@/Utils/request/mutate"; import { formatTimeShort } from "@/Utils/utils"; import { ScheduleException } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { items?: ScheduleException[]; diff --git a/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx b/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx index 1d050aea3cd..22883d6f75b 100644 --- a/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx +++ b/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx @@ -34,7 +34,7 @@ import { import mutate from "@/Utils/request/mutate"; import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { facilityId: string; diff --git a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx index d4081a965fe..daee952132e 100644 --- a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx +++ b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx @@ -44,7 +44,7 @@ import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; import { getSlotsPerSession, getTokenDuration } from "@/pages/Scheduling/utils"; import { ScheduleAvailabilityCreateRequest } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { facilityId: string; diff --git a/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx index 5cdd6790bcb..16d62abc1dc 100644 --- a/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx +++ b/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx @@ -47,7 +47,7 @@ import { ScheduleAvailabilityCreateRequest, ScheduleTemplate, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; export default function EditScheduleTemplateSheet({ template, diff --git a/src/pages/UserDashboard.tsx b/src/pages/UserDashboard.tsx index ab7e8430033..573d51c0032 100644 --- a/src/pages/UserDashboard.tsx +++ b/src/pages/UserDashboard.tsx @@ -1,4 +1,4 @@ -import { ChevronRight, ClipboardList, LogOut, Settings } from "lucide-react"; +import { ChevronRight, LogOut, Settings, User2Icon } from "lucide-react"; import { Link } from "raviger"; import { Button } from "@/components/ui/button"; @@ -8,6 +8,7 @@ import { Avatar } from "@/components/Common/Avatar"; import useAuthUser, { useAuthContext } from "@/hooks/useAuthUser"; +import { formatDisplayName } from "@/Utils/utils"; import { getOrgLabel } from "@/types/organization/organization"; export default function UserDashboard() { @@ -22,7 +23,7 @@ export default function UserDashboard() { @@ -62,9 +63,9 @@ export default function UserDashboard() { className="w-full sm:w-auto" asChild > - - - Questionnaires + + + Admin Dashboard )} diff --git a/src/pluginTypes.ts b/src/pluginTypes.ts index f25c3aa7eb3..180df1502a2 100644 --- a/src/pluginTypes.ts +++ b/src/pluginTypes.ts @@ -31,6 +31,10 @@ export type PatientInfoCardActionsComponentType = React.FC<{ className?: string; }>; +export type PatientInfoCardMarkAsCompleteComponentType = React.FC<{ + encounter: Encounter; +}>; + export type FacilityHomeActionsComponentType = React.FC<{ facility: FacilityData; className?: string; @@ -53,6 +57,7 @@ export type SupportedPluginComponents = { Scribe: ScribeComponentType; PatientHomeActions: PatientHomeActionsComponentType; PatientInfoCardActions: PatientInfoCardActionsComponentType; + PatientInfoCardMarkAsComplete: PatientInfoCardMarkAsCompleteComponentType; FacilityHomeActions: FacilityHomeActionsComponentType; PatientRegistrationForm: PatientRegistrationFormComponentType; PatientDetailsTabDemographyGeneralInfo: PatientDetailsTabDemographyGeneralInfoComponentType; diff --git a/src/service-worker.ts b/src/service-worker.ts index 078e4237c28..973ff051736 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -1,6 +1,4 @@ /// - -/* eslint-disable no-restricted-globals */ // This service worker can be customized! // See https://developers.google.com/web/tools/workbox/modules // for the list of available Workbox modules, or add any other @@ -13,7 +11,6 @@ import { precacheAndRoute } from "workbox-precaching"; declare const self: ServiceWorkerGlobalScope; -// eslint-disable-next-line no-restricted-globals const _ignored = self.__WB_MANIFEST.map((_) => { return _; }); diff --git a/src/style/CAREUI.css b/src/style/CAREUI.css index 893d16c2e4e..0e01efa2853 100644 --- a/src/style/CAREUI.css +++ b/src/style/CAREUI.css @@ -1,34 +1,3 @@ -.cui-input-legend::before { - content: " "; - position: absolute; - left: -4px; - right: -4px; - top: calc(50% - 3px); - height: 6px; - background: #fff; - z-index: -1; -} - -.cui-label-required::after { - content: "*"; - color: rgb(255, 81, 0); - font-size: 1.2em; - font-weight: bold; - margin-left: 4px; -} - -.cui-input-base { - @apply text-sm block w-full py-3 px-4 text-black placeholder:text-secondary-600 bg-white disabled:bg-secondary-200 border border-secondary-400 focus:border-primary-400 ring-0 focus:ring-1 ring-primary-400 outline-none focus:outline-none shadow-none rounded transition-colors duration-300 !important -} - -.cui-dropdown-base { - @apply z-40 w-full rounded-b-md xl:rounded-b-lg shadow-lg overflow-auto max-h-96 bg-secondary-100 divide-y divide-secondary-300 ring-1 ring-secondary-400 focus:outline-none -} - -.cui-card { - @apply bg-white p-5 rounded-lg shadow -} - .tooltip { @apply relative } @@ -83,41 +52,6 @@ transform: translateY(0px); } -.cui-input:-webkit-autofill, -.cui-input:-webkit-autofill:hover, -.cui-input:-webkit-autofill:focus, -.cui-input:-webkit-autofill:active { - box-shadow: 0 0 0 40px #f9fafb inset !important; - -webkit-box-shadow: 0 0 0 40px #f9fafb inset !important; -} - -.cui-slideover-x { - @apply w-full md:w-[300px] h-full -} - -.cui-slideover-y { - @apply h-full md:h-[300px] w-full -} - -.dropdown-item-primary { @apply accent-primary-500 hover:bg-primary-100 text-black hover:text-primary-500 } -.dropdown-item-secondary { @apply accent-secondary-200 hover:bg-secondary-200 text-secondary-700 } -.dropdown-item-danger { @apply accent-danger-500 hover:bg-danger-100 text-danger-500 } -.dropdown-item-warning { @apply accent-warning-500 hover:bg-warning-100 text-warning-500 } -.dropdown-item-alert { @apply accent-alert-600 hover:bg-alert-100 text-alert-600 } - -.cui-form-button-group { - @apply flex flex-col-reverse md:flex-row md:justify-end gap-2 w-full md:w-auto -} - -.cui-range-slider { - @apply outline-none bg-black/10 h-2 w-full appearance-none transition-all -} -.cui-range-slider::-webkit-slider-thumb { - @apply appearance-none w-5 aspect-square bg-black border border-black rounded-full cursor-pointer hover:scale-125 transition-all -} -.cui-range-slider::-moz-range-thumb { - @apply appearance-none w-5 aspect-square bg-black border border-black rounded-full cursor-pointer -} hr { @apply border border-secondary-300 } diff --git a/src/style/index.css b/src/style/index.css index 2f14f673a18..4c7410e732b 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -10,23 +10,7 @@ @tailwind base; @tailwind components; - -@keyframes indeterminateAnimation { - 0% { - transform: translateX(0) scaleX(0); - } - 40% { - transform: translateX(0) scaleX(0.4); - } - 100% { - transform: translateX(100%) scaleX(0.5); - } -} - -.indeterminate { - animation: indeterminateAnimation 1s infinite linear; - transform-origin: 0% 50%; -} +@tailwind utilities; html { @@ -72,363 +56,12 @@ h6 { font-size: 0.875rem; } -.btn { - @apply inline-flex items-center justify-center whitespace-nowrap text-sm font-semibold py-2 px-4 rounded cursor-pointer; -} - -.btn:focus { - @apply outline-none shadow-inner; -} - -[type="text"]:focus { - @apply border-black ring-white; -} - - -/* Button Styles */ - -.btn-large { - @apply py-2 px-5 text-base h-10; -} - -a { - @apply text-blue-800; -} - -a:hover { - @apply text-blue-600; -} - -.bg-primary { - background-image: linear-gradient( 135deg, rgba(56, 161, 105, 1) 0%, rgba(47, 133, 90, 1) 100%); -} - - -/* Button */ - -button:focus { - outline: none; -} - -.btn { - @apply inline-flex items-center justify-center whitespace-nowrap text-sm font-semibold py-2 px-4 rounded cursor-pointer; -} - -.btn:focus { - @apply outline-none shadow-inner; -} - - -/* Button Styles */ - -.btn-default { - @apply border text-secondary-800; -} - -.btn-default:hover { - @apply bg-secondary-100 text-secondary-600; -} - -.btn-default:focus { - @apply bg-secondary-400 text-secondary-700; -} - -.btn-subtle { - @apply bg-secondary-200 text-secondary-800; -} - -.btn-subtle:hover { - @apply bg-secondary-300 text-secondary-900; -} - -.btn-subtle:focus { - @apply bg-secondary-400 text-secondary-900; -} - -.btn-primary-ghost { - @apply bg-white text-primary-500 border border-primary-500; -} - -.btn-primary-ghost:hover { - @apply bg-primary-100 text-primary-600 border-primary-400; -} - -.btn-primary-ghost:focus { - @apply bg-primary-800 text-white border-primary-400; -} - -.btn-primary { - @apply bg-primary-500 text-white; -} - -.btn-primary:hover { - @apply bg-primary-600; -} - -.btn-primary:focus { - @apply bg-primary-800; - background-image: none; -} - -.btn-warning { - @apply bg-yellow-500 text-white; - background-image: linear-gradient( 135deg, rgba(237, 137, 54, 1) 0%, rgba(221, 107, 32, 1) 100%); -} - -.btn-warning:hover { - @apply bg-yellow-600; - background-image: linear-gradient( 135deg, rgba(221, 107, 32, 1) 0%, rgba(192, 86, 33, 1) 100%); -} - -.btn-warning:focus { - @apply bg-yellow-800; - background-image: none; -} - -.btn-danger { - @apply bg-red-500 text-white; - background-image: linear-gradient( 135deg, rgba(245, 101, 101, 1) 0%, rgba(229, 62, 62, 1) 100%); -} - -.btn-danger:hover { - @apply bg-red-600; - background-image: linear-gradient( 135deg, rgba(229, 62, 62, 1) 0%, rgba(197, 48, 48, 1) 100%); -} - -.btn-danger:focus { - @apply bg-red-800; - background-image: none; -} - - -.primary-button { - @apply focus:outline-none text-white bg-primary-500 hover:bg-primary-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-6 py-2.5 mr-2 mb-2 transition-all ease-in-out duration-200 !important; -} - -.secondary-button { - @apply text-secondary-900 bg-white border border-secondary-300 focus:outline-none hover:bg-secondary-100 focus:ring-4 focus:ring-secondary-200 font-medium rounded-lg text-sm px-6 py-2.5 mr-2 mb-2 transition-all ease-in-out duration-200 !important; -} - -button:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-300 text-secondary-500 shadow-none border-transparent; - background-image: none; -} - -button:disabled:hover, -.disabled:hover, -button:disabled:focus, -.disabled:focus { - @apply bg-secondary-300 text-secondary-500 border-transparent shadow-none; - background-image: none; -} - -.btn-small { - @apply py-1 px-3 text-xs h-7 leading-normal; -} - -.btn-normal { - @apply py-1 px-4 text-sm h-8; -} - -.btn-large { - @apply py-2 px-5 text-base h-10; -} - -.button-xl { - @apply py-2 px-6 text-base h-12; -} - -@tailwind utilities; -.multiselect-dropdown__search-dropdown { - max-height: 20rem; - overflow: auto; -} - -.max-height-dropdown { - max-height: 20rem; - overflow: auto; -} - - -/* Radio */ - -.radio-label { - @apply select-none cursor-pointer; -} - -.radio-label span { - @apply inline-block align-middle; - transform: translate3d(0, 0, 0); -} - -.radio-label span:first-child { - @apply relative rounded-full align-middle border border-secondary-500 bg-white mr-3; - width: 1.125rem; - height: 1.125rem; - transform: scale(1); - transition: all 0.2s ease; -} - -.radio-label span:first-child svg { - position: absolute; - top: 0; - left: 0; - fill: none; - stroke: #ffffff; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - transition: all 0.3s ease; - transition-delay: 0.1s; -} - -.radio-label span:first-child:before { - content: ""; - @apply bg-primary-500 w-full h-full block opacity-100 rounded-full; - transform: scale(0); -} - -.radio-label:hover span:first-child { - @apply bg-primary-100 border-primary-500; -} - -.radio-input:checked+.radio-label span:first-child { - @apply bg-primary-500 border-primary-500; - animation: wave 0.4s ease; -} - -.radio-input:checked+.radio-label span:first-child:before { - transform: scale(3.5); - opacity: 0; - transition: all 0.6s ease; -} - - -/* Checkbox */ - -.checkbox__label { - @apply select-none cursor-pointer; -} - -.checkbox__label span { - @apply inline-block align-middle; - transform: translate3d(0, 0, 0); -} - -.checkbox__label span:first-child { - @apply relative rounded align-middle border border-secondary-500 bg-white w-4 h-4 mr-3; - transform: scale(1); - transition: all 0.2s ease; -} - -.checkbox__label span:first-child svg { - position: absolute; - top: 3px; - left: 2px; - fill: none; - stroke: #ffffff; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - stroke-dasharray: 16px; - stroke-dashoffset: 16px; - transition: all 0.3s ease; - transition-delay: 0.1s; - transform: translate3d(0, 0, 0); -} - -.checkbox__label span:first-child:before { - content: ""; - @apply bg-primary-500 w-full h-full block opacity-100 rounded-full; - transform: scale(0); -} - -.checkbox__label:hover span:first-child { - @apply bg-primary-100 border-primary-500; -} - -.checkbox__input:checked+.checkbox__label span:first-child { - @apply bg-primary-500 border-primary-500; - animation: wave 0.4s ease; -} - -.checkbox__input:checked+.checkbox__label span:first-child svg { - stroke-dashoffset: 0; -} - -.checkbox__input:checked+.checkbox__label span:first-child:before { - transform: scale(3.5); - opacity: 0; - transition: all 0.6s ease; -} - @keyframes wave { 50% { transform: scale(0.9); } } -label { - @apply block text-secondary-700 text-sm font-medium; -} - -input { - @apply border-secondary-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 transition-colors duration-300 !important; -} - -input:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-200 border-secondary-400 text-secondary-700; -} - -textarea { - @apply border-secondary-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 transition-colors duration-300; -} - -button:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-300 text-secondary-500 shadow-none border-transparent; - background-image: none; -} - - -/* Styling skeleton loading */ - -.skeleton-placeholder__line-sm { - @apply rounded-full bg-secondary-100; - height: 0.625rem; -} - -.skeleton-placeholder__line-md { - @apply rounded-full bg-secondary-100; - height: 1rem; -} - -.skeleton-placeholder__image { - @apply mt-5 h-48 rounded-lg bg-secondary-100; -} - -.skeleton-animate { - animation: skeletonShimmer 3s infinite linear; - background: linear-gradient(to right, #f5f3f7 10%, #fbfafc 40%, #f5f3f7 70%); - background-size: 1000px 100%; -} - -@keyframes skeletonShimmer { - 0% { - background-position: -1000px 0; - } - 100% { - background-position: 1000px 0; - } -} - -.skeleton-animate-alpha { - animation: skeletonShimmer 3s infinite linear; - background: linear-gradient(to right, rgba(0, 0, 0, 0.1) 10%, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.1) 70%); - background-size: 1000px 100%; -} @media print { @page { @@ -449,20 +82,6 @@ button:disabled, } } -.header-section .appBar { - z-index: 1201; -} - -.header-section .toolbar { - padding-top: 72px; -} - -@media only screen and (min-width: 601px) { - .header-section .menuButton { - display: none; - } -} - .App { text-align: center; } @@ -497,87 +116,10 @@ button:disabled, 100% {opacity: 0;} } -.badge { - display: inline-block; - padding: 0.25em 0.4em; - font-size: 12px; - font-weight: 700; - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: 0.25rem; - transition: color 0.15s; -} - -.badge-pill { - padding-right: 0.6em; - padding-left: 0.6em; - border-radius: 10rem; -} - -.badge-primary { - color: #fff; - background-color: #007bff; -} - -.badge-secondary { - color: #fff; - background-color: #6c757d; -} - -.badge-success { - color: #fff; - background-color: #28a745; -} - -.badge-warning { - color: #212529; - background-color: #ffc107; -} - -.badge-danger { - color: #fff; - background-color: #dc3545; -} - -.badge-dark { - color: #fff; - background-color: #343a40; -} - .dropdown:hover .dropdown-menu { display: block; } -.border-primary { - border-color: #28a745; -} - -.form-input { - padding-top: 6px; - padding-bottom: 6px; - width: 100%; - border-radius: 4px; - height: 38px; -} - -.form-input:focus { - border-color: #28a745 !important; - box-shadow: 0 0 0 1px #28a745 !important; -} - -.copied-to-cb { - color: #28a745; - font-size: 13px; - padding-left: 5px; - font-style: italic; -} - -.copy-to-cb { - padding-left: 5px; -} - ::-webkit-scrollbar { width: 10px; height: 8px; @@ -596,166 +138,6 @@ button:disabled, background: rgba(0, 0, 0, 0.4); } -@media (min-width:1000px) { - .manualGrid { - display: grid !important - } -} - -.scrollbar-hide::-webkit-scrollbar { - width: 0px; - height: 0px; -} - -/* - Range Sliders for Camera Feed -*/ - -#feed-range { - -webkit-appearance: none; - appearance: none; - margin: 18px 0; - width: 100%; -} - -#feed-range:focus { - outline: none; -} - -#feed-range::-webkit-slider-runnable-track { - width: 100%; - height: 1px; - cursor: pointer; - background: transparent; - border-radius: 5px; -} - -#feed-range::-webkit-slider-thumb { - height: 16px; - width: 16px; - border-radius: 3px; - border: 1px solid white; - background: black; - cursor: pointer; - -webkit-appearance: none; - margin-top: -8px; - border-radius: 50%; -} - -@media (min-width:471px) { - .csv-input { - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin: auto; - width: 50%; - } -} - -@media (max-width:470px) { - .csv-input { - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin: auto; - width: 100%; - } -} - -[type="checkbox"] { - @apply text-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none; -} - - - -.csv-input::file-selector-button:hover { - background-color: #84e1bc; -} - - - -/* for gmaps search dropdown */ -.pac-container { - z-index: 100000 !important; -} - -.csv-input::file-selector-button { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; - font-weight: 600; - border-width: 0; - background-color: #def7ec; - color: #046c4e; -} - -.csv-input::file-selector-button:hover { - background-color: #84e1bc; -} - -/* Patient Category Styles */ - -.patient-comfort { - @apply bg-patient-comfort text-patient-comfort-fore -} -.patient-stable { - @apply bg-patient-stable text-patient-stable-fore -} -.patient-abnormal { - @apply bg-patient-abnormal text-patient-abnormal-fore -} -.patient-critical { - @apply bg-patient-critical text-patient-critical-fore -} -.patient-unknown { - @apply bg-patient-unknown text-patient-unknown-fore -} - -.patient-activelydying { - @apply bg-patient-activelydying text-patient-activelydying-fore -} - -.patient-comfort-ring { - @apply ring-patient-comfort -} -.patient-stable-ring { - @apply ring-patient-stable -} -.patient-abnormal-ring { - @apply ring-patient-abnormal -} -.patient-critical-ring { - @apply ring-patient-critical -} -.patient-unknown-ring { - @apply ring-patient-unknown -} -.patient-activelydying-ring { - @apply ring-patient-activelydying -} - -.patient-comfort-profile { - @apply border-2 border-patient-comfort rounded-t -} -.patient-stable-profile { - @apply border-2 border-patient-stable rounded-t -} -.patient-abnormal-profile { - @apply border-2 border-patient-abnormal rounded-t -} -.patient-critical-profile { - @apply border-2 border-patient-critical rounded-t -} -.patient-unknown-profile { - @apply border border-patient-unknown rounded -} -.patient-activelydying-profile { - @apply border-2 border-patient-activelydying rounded-t -} - /* for gmaps search dropdown */ .pac-container { z-index: 100000 !important; @@ -855,468 +237,6 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-decoration { display: none; } -.service-panel{ - @apply flex-row -} - -@media screen and (max-width: 920px) { - .service-panel{ - @apply flex-col - } -} -/* styles for files in src/Components/CriticalCareRecording/PressureSore */ - -input[type="checkbox"]:checked~.dot { - transform: translateX(100%); -} - - -/* styles for files in src/Components/CriticalCareRecording/components */ - -.range { - position: relative; - width: 644px; - height: 5px; -} - -.range input { - width: 100%; - position: absolute; - top: 2px; - height: 0; - -webkit-appearance: none; -} - -.range input::-webkit-slider-thumb { - -webkit-appearance: none; - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-moz-range-thumb { - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-ms-thumb { - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-webkit-slider-runnable-track { - width: 100%; - height: 2px; - cursor: pointer; - background: #b2b2b2; - z-index: 1; -} - -.range input::-moz-range-track { - width: 100%; - height: 2px; - cursor: pointer; - background: black; - z-index: 1; -} - -.range input::-ms-track { - width: 100%; - height: 2px; - cursor: pointer; - background: #b2b2b2; - z-index: 1; -} - -.range input:focus { - background: none; - outline: none; -} - -.range input::-ms-track { - width: 100%; - cursor: pointer; - background: transparent; - border-color: transparent; - color: transparent; -} - -.range-labels { - margin: 18px -41px 0; - padding: 0; - list-style: none; -} - -.range-labels li { - position: relative; - float: left; - width: 90.25px; - text-align: center; - color: black; - font-size: 14px; - cursor: pointer; -} - -.range-labels .label::before { - position: absolute; - top: -25px; - right: 0; - left: 0; - content: ""; - margin: 0 auto; - width: 9px; - height: 9px; - background: #b2b2b2; - border-radius: 50%; -} - -.double-range-labels { - margin: 18px -41px 0; - padding: 0; - list-style: none; -} - -.double-range-labels li { - position: relative; - float: left; - width: 45.25px; - text-align: center; - color: black; - font-size: 14px; - cursor: pointer; -} - -.double-range-labels .label::before { - position: absolute; - top: -25px; - right: 0; - left: 0; - content: ""; - margin: 0 auto; - width: 9px; - height: 9px; - background: #b2b2b2; - border-radius: 50%; -} - -.upper-label { - height: 10px; - margin-bottom: 10px; -} - -.pupil1 { - bottom: 0; - margin: 0 auto; - width: 9px; - height: 9px; - border-radius: 50%; - background-color: black; -} - -.pupil2 { - bottom: 0; - margin: 0 auto; - width: 15px; - height: 15px; - border-radius: 50%; - background-color: black; -} - -.pupil3 { - bottom: 0; - margin: 0 auto; - width: 21px; - height: 21px; - border-radius: 50%; - background-color: black; -} - -.pupil4 { - bottom: 0; - margin: 0 auto; - width: 25px; - height: 25px; - border-radius: 50%; - background-color: black; -} - -.pupil5 { - bottom: 0; - margin: 0 auto; - width: 30px; - height: 30px; - border-radius: 50%; - background-color: black; -} - -.pupil6 { - bottom: 0; - margin: 0 auto; - width: 35px; - height: 35px; - border-radius: 50%; - background-color: black; -} - -.pupil7 { - bottom: 0; - margin: 0 auto; - width: 38px; - height: 38px; - border-radius: 50%; - background-color: black; -} - -.pupil8 { - width: 41px; - height: 41px; - border-radius: 50%; - background-color: black; -} - -.range-labels .active { - color: rgb(7, 109, 243); -} - -.range-labels .selected::before { - background: rgb(7, 109, 243); -} - -.range-labels .active.selected::before { - display: none; -} - -.align-circles { - position: relative; - display: flex; - justify-content: center; - height: 41px; - vertical-align: baseline; -} - -input[type="number"] { - background-color: #fbfafc; -} - - -/* Copied CSS */ - -.slider { - position: relative; - width: 200px; -} - -.slider__track, -.slider__range, -.slider__left-value, -.slider__right-value { - position: absolute; -} - -.slider__track, -.slider__range { - border-radius: 3px; - height: 5px; -} - -.slider__track { - background-color: #1476d8; - width: 100%; - z-index: 1; -} - -.slider__range { - background-color: #19ebe0; - z-index: 2; -} - -.slider__left-value, -.slider__right-value { - color: #1c85ee; - font-size: 12px; - margin-top: 20px; -} - -.slider__left-value { - left: 6px; -} - -.slider__right-value { - right: -4px; -} - - -/* Removing the default appearance */ - -.thumb input { - pointer-events: none; - position: absolute; - height: 0; - width: 200px; - outline: none; -} - -.thumb--left { - z-index: 3; -} - -.thumb--right { - z-index: 4; -} - - -/* For Chrome browsers */ - -.thumb input::-webkit-slider-thumb { - background-color: #242525; - border: none; - border-radius: 50%; - box-shadow: 0 0 1px 1px #0b2846; - cursor: pointer; - height: 18px; - width: 18px; - margin-top: 4px; - pointer-events: all; - position: relative; -} - - -/* For Firefox browsers */ - -.thumb input::-moz-range-thumb { - background-color: #272b2c; - border: none; - border-radius: 50%; - box-shadow: 0 0 1px 1px #0d2d4d; - cursor: pointer; - height: 18px; - width: 18px; - margin-top: 4px; - pointer-events: all; - position: relative; -} - -.slider { - appearance: none; - background-color: #dee0e4; - width: 100%; - height: 4px; - margin: 0; - padding: 0; - z-index: 2; -} - -.slider::-webkit-slider-thumb { - cursor: pointer; -} - -.slider-container { - position: relative; - width: 100%; - /* max-width: 700px; */ - display: flex; - justify-content: center; -} - -.indicators { - position: absolute; - top: -200%; - width: 100%; - height: 20px; - border-left: 2px solid #dee0e4; -} - -.tick { - content: ""; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: linear-gradient(to left, #dee0e4 2px, transparent 2px); -} - -.slider-box { - width: 100%; - /* max-width: 700px; */ - padding: 20px 10px 60px 10px; - border-radius: 12px; -} - -.slider-head { - font-family: sans-serif; - margin-bottom: 2rem; -} - -.slider-head>div>h1 { - font-size: 1rem; - font-weight: bold; -} - -.slider-head input { - padding: 0.5rem 0.7rem; - border-radius: 8px; - border: 1px solid #979aa0; - margin-left: 12px; - font-size: 1.1rem; - color: #383a3e; - letter-spacing: 1px; -} - -.slider-head label { - font-weight: 700; - color: #2856ff; - font-size: 1rem; -} - - -.grid-2-col, .grid-3-col, .grid-2-1-col, .grid-3-1-col, .grid-1-2-col { - display: grid; - grid-template-columns: 1fr; - gap: 1.5rem; -} - -@media (min-width: 768px) { - .grid-2-col { - grid-template-columns: repeat(2, 1fr); - } - .grid-3-col { - grid-template-columns: repeat(3, 1fr); - } - .grid-2-1-col { - grid-template-columns: 2fr 1fr; - } - .grid-1-2-col { - grid-template-columns: 1fr 2fr; - } -} - -.input-field-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} -.col-span-all { - grid-column: 1 / -1; -} - @layer base { :root { --radius: 0.5rem; diff --git a/src/types/emr/encounter.ts b/src/types/emr/encounter.ts index f63a118a83f..45c6602b763 100644 --- a/src/types/emr/encounter.ts +++ b/src/types/emr/encounter.ts @@ -117,6 +117,12 @@ export type StatusHistory = { history: History[]; }; +export type LocationHistory = { + start_datetime: string; + location: LocationList; + status: string; +}; + export interface Encounter { id: string; patient: Patient; @@ -138,6 +144,7 @@ export interface Encounter { status_history: StatusHistory; organizations: FacilityOrganization[]; current_location: LocationList; + location_history: LocationHistory[]; } export interface EncounterEditRequest { diff --git a/src/types/emr/medicationAdministration/medicationAdministration.ts b/src/types/emr/medicationAdministration/medicationAdministration.ts index a736846de4a..caf6f254711 100644 --- a/src/types/emr/medicationAdministration/medicationAdministration.ts +++ b/src/types/emr/medicationAdministration/medicationAdministration.ts @@ -11,7 +11,6 @@ export const MEDICATION_ADMINISTRATION_STATUS = [ "stopped", "in_progress", "on_hold", - "unknown", "cancelled", ] as const; @@ -93,4 +92,6 @@ export interface MedicationAdministrationRead { dose?: DosageQuantity; rate?: Quantity; }; + created_by: UserBareMinimum; + updated_by: UserBareMinimum; } diff --git a/src/types/emr/medicationAdministration/medicationAdministrationApi.ts b/src/types/emr/medicationAdministration/medicationAdministrationApi.ts index 77e37c391e2..35b980b0349 100644 --- a/src/types/emr/medicationAdministration/medicationAdministrationApi.ts +++ b/src/types/emr/medicationAdministration/medicationAdministrationApi.ts @@ -7,12 +7,12 @@ import { } from "./medicationAdministration"; export default { - listMedicationAdministrations: { + list: { path: "/api/v1/patient/{patientId}/medication/administration/", method: HttpMethod.GET, TRes: Type>(), }, - upsertMedicationAdministration: { + upsert: { path: "/api/v1/patient/{patientId}/medication/administration/upsert/", method: HttpMethod.POST, TRes: Type, diff --git a/src/types/emr/medicationStatement.ts b/src/types/emr/medicationStatement.ts index a357ddab63c..00e20ae150a 100644 --- a/src/types/emr/medicationStatement.ts +++ b/src/types/emr/medicationStatement.ts @@ -23,6 +23,17 @@ export const MEDICATION_STATEMENT_STATUS = [ export type MedicationStatementStatus = (typeof MEDICATION_STATEMENT_STATUS)[number]; +export const MEDICATION_STATEMENT_STATUS_STYLES = { + active: "bg-green-100 text-green-800 border-green-200", + completed: "bg-blue-100 text-blue-800 border-blue-200", + stopped: "bg-red-100 text-red-800 border-red-200", + on_hold: "bg-yellow-100 text-yellow-800 border-yellow-200", + intended: "bg-purple-100 text-purple-800 border-purple-200", + not_taken: "bg-gray-100 text-gray-800 border-gray-200", + unknown: "bg-gray-100 text-gray-800 border-gray-200", + entered_in_error: "bg-red-100 text-red-800 border-red-200", +} as const; + export type MedicationStatement = { readonly id: string; status: MedicationStatementStatus; diff --git a/src/types/emr/medicationStatement/medicationStatementApi.ts b/src/types/emr/medicationStatement/medicationStatementApi.ts index 68736e4ab13..7f65dc5f3c8 100644 --- a/src/types/emr/medicationStatement/medicationStatementApi.ts +++ b/src/types/emr/medicationStatement/medicationStatementApi.ts @@ -1,12 +1,12 @@ import { Type } from "@/Utils/request/api"; import { PaginatedResponse } from "@/Utils/request/types"; -import { MedicationStatement } from "@/types/emr/medicationStatement"; +import { MedicationStatementRead } from "@/types/emr/medicationStatement"; const medicationStatementApi = { list: { path: "/api/v1/patient/{patientId}/medication/statement/", method: "GET", - TRes: Type>(), + TRes: Type>(), }, } as const; diff --git a/src/types/location/location.ts b/src/types/location/location.ts index cfb16dd13c3..3738dce63f7 100644 --- a/src/types/location/location.ts +++ b/src/types/location/location.ts @@ -45,6 +45,7 @@ export interface LocationDetail extends LocationBase { export interface LocationList extends LocationBase { id: string; has_children: boolean; + parent?: LocationList; } export interface LocationWrite extends LocationBase { diff --git a/src/types/questionnaire/question.ts b/src/types/questionnaire/question.ts index cc16b086bc7..f0199d63890 100644 --- a/src/types/questionnaire/question.ts +++ b/src/types/questionnaire/question.ts @@ -16,6 +16,23 @@ export type QuestionType = | "quantity" | "structured"; +export const SUPPORTED_QUESTION_TYPES = [ + { name: "Group", value: "group" }, + { name: "Display", value: "display" }, + { name: "Boolean", value: "boolean" }, + { name: "Decimal", value: "decimal" }, + { name: "Integer", value: "integer" }, + { name: "Date", value: "date" }, + { name: "DateTime", value: "dateTime" }, + { name: "Time", value: "time" }, + { name: "String", value: "string" }, + { name: "Text", value: "text" }, + { name: "URL", value: "url" }, + { name: "Choice", value: "choice" }, + { name: "Quantity", value: "quantity" }, + { name: "Structured", value: "structured" }, +]; + export type StructuredQuestionType = | "allergy_intolerance" | "medication_request" @@ -49,6 +66,13 @@ export interface AnswerOption { value: string; display?: string; initialSelected?: boolean; + code?: Code; +} + +export interface ObservationType { + system: string; + code: string; + display: string; } export interface Question { diff --git a/src/types/resourceRequest/resourceRequest.ts b/src/types/resourceRequest/resourceRequest.ts index 7594766017c..07bfdbd2479 100644 --- a/src/types/resourceRequest/resourceRequest.ts +++ b/src/types/resourceRequest/resourceRequest.ts @@ -2,6 +2,7 @@ import { FacilityModel } from "@/components/Facility/models"; import { UserBareMinimum } from "@/components/Users/models"; import { PatientModel } from "@/types/emr/patient"; +import { UserBase } from "@/types/user/user"; export interface ResourceRequest { approving_facility: FacilityModel | null; @@ -17,9 +18,9 @@ export interface ResourceRequest { requested_quantity: number; status: string; title: string; - assigned_to: UserBareMinimum | null; - created_by: UserBareMinimum; - updated_by: UserBareMinimum; + assigned_to: UserBase | null; + created_by: UserBase; + updated_by: UserBase; created_date: string; modified_date: string; related_patient: PatientModel | null; @@ -32,6 +33,7 @@ export interface CreateResourceRequest { referring_facility_contact_name: string; referring_facility_contact_number: string; approving_facility: string | null; + assigned_to: string | null; assigned_facility: string | null; origin_facility: string; related_patient: string; diff --git a/src/types/scheduling/scheduleApis.ts b/src/types/scheduling/scheduleApi.ts similarity index 100% rename from src/types/scheduling/scheduleApis.ts rename to src/types/scheduling/scheduleApi.ts diff --git a/src/types/user/userApi.ts b/src/types/user/userApi.ts index f1f776cece0..dd11f7e2ae2 100644 --- a/src/types/user/userApi.ts +++ b/src/types/user/userApi.ts @@ -30,4 +30,4 @@ export default { TRes: Type(), TBody: Type>(), }, -}; +} as const; diff --git a/src/types/valueset/valueset.ts b/src/types/valueset/valueset.ts new file mode 100644 index 00000000000..01ad2e9f8c1 --- /dev/null +++ b/src/types/valueset/valueset.ts @@ -0,0 +1,70 @@ +export interface ValuesetFilter { + op: string; + value: string; + property: string; +} + +export interface ValuesetConcept { + code: string; + display: string; +} + +export interface ValuesetInclude { + filter?: ValuesetFilter[]; + system: string; + concept?: ValuesetConcept[]; +} + +interface ValuesetCompose { + exclude: ValuesetInclude[]; + include: ValuesetInclude[]; +} + +export interface ValuesetBase { + id: string; + slug: string; + name: string; + description: string; + compose: ValuesetCompose; + status: "active" | "inactive"; + is_system_defined: boolean; + created_by: string | null; + updated_by: string | null; +} + +export type CreateValuesetModel = Omit< + ValuesetBase, + "id" | "created_by" | "updated_by" +>; + +export type UpdateValuesetModel = CreateValuesetModel & { + id: string; +}; + +export type ValuesetFormType = CreateValuesetModel; + +export interface ValuesetCodeMetadata { + code: string; + display: string; + name: string; + system: string; + version: string; + inactive: boolean; +} + +export interface ValuesetLookupResponse { + metadata: ValuesetCodeMetadata; +} + +export interface ValuesetLookupRequest { + system: string; + code: string; +} + +export const TERMINOLOGY_SYSTEMS = { + LOINC: "http://loinc.org", + SNOMED: "http://snomed.info/sct", + UCUM: "http://unitsofmeasure.org", +} as const; + +export type TerminologySystem = keyof typeof TERMINOLOGY_SYSTEMS; diff --git a/src/types/valueset/valuesetApi.ts b/src/types/valueset/valuesetApi.ts new file mode 100644 index 00000000000..3398cd18432 --- /dev/null +++ b/src/types/valueset/valuesetApi.ts @@ -0,0 +1,49 @@ +import { HttpMethod, Type } from "@/Utils/request/api"; +import { PaginatedResponse } from "@/Utils/request/types"; + +import { + CreateValuesetModel, + UpdateValuesetModel, + ValuesetBase, + ValuesetLookupRequest, + ValuesetLookupResponse, +} from "./valueset"; + +export default { + list: { + path: "/api/v1/valueset/", + method: HttpMethod.GET, + TRes: Type>(), + }, + create: { + path: "/api/v1/valueset/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, + get: { + path: "/api/v1/valueset/{slug}/", + method: HttpMethod.GET, + TRes: Type(), + }, + update: { + path: "/api/v1/valueset/{slug}/", + method: HttpMethod.PUT, + TRes: Type(), + TBody: Type(), + }, + lookup: { + path: "/api/v1/valueset/lookup_code/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, + expand: { + path: "/api/v1/valueset/expand/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type<{ + search: string; + }>(), + }, +} as const; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index defba258b80..5e2066155a8 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -19,11 +19,7 @@ interface ImportMetaEnv { readonly REACT_CUSTOM_LOGO_ALT?: string; readonly REACT_CUSTOM_DESCRIPTION?: string; readonly REACT_GMAPS_API_KEY?: string; - readonly REACT_GOV_DATA_API_KEY?: string; readonly REACT_RECAPTCHA_SITE_KEY?: string; - readonly REACT_WARTIME_SHIFTING?: string; - readonly REACT_STILL_WATCHING_IDLE_TIMEOUT?: string; - readonly REACT_STILL_WATCHING_PROMPT_DURATION?: string; readonly REACT_JWT_TOKEN_REFRESH_INTERVAL?: string; readonly REACT_MIN_ENCOUNTER_DATE?: string; readonly REACT_DEFAULT_ENCOUNTER_TYPE?: string; diff --git a/vite.config.mts b/vite.config.mts index 749f188002c..308c942b7ad 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -288,6 +288,7 @@ export default defineConfig(({ mode }) => { server: { port: 4000, host: "0.0.0.0", + allowedHosts: true, }, preview: { headers: {
+ {fileNameTooltip} +
+ {fileName} +
+ {t("created_on")}{" "} + {new Date( + uploadedFiles[index].created_date!, + ).toLocaleString("en-US", { + dateStyle: "long", + timeStyle: "short", + })}
- {t("created_on")}{" "} - {new Date( - uploadedFiles[index].created_date!, - ).toLocaleString("en-US", { - dateStyle: "long", - timeStyle: "short", - })} -
Software Update
+ {formatName(selected)} +
- {t("patient_records_found_description")}( - {patientList[0].phone_number}) -
+ {t("patient_records_found_description")}( + {patientList[0].phone_number}) +
{t("duplicate_patient_record_confirmation")}
{t("duplicate_patient_record_rejection")}
{t("duplicate_patient_record_birth_unknown")}
{fileName}
{`${Math.round(props.value)}%`}
{fileUpload.error}
+ {fileUpload.error} +
- {fileUpload.error} -
{label}
{ - e.stopPropagation(); - onRemove(); - }} - > - -
- {value.selectedLabel} -
{statement.medication.display ?? statement.medication.code}
{statement.dosage_text}
{statement.reason}
+ {statement.note} +
{statement.note}
{t("no_ongoing_medications")}
+ {formatDisplayName(selectedUser)} +
+ {formatDisplayName(user)} +
{user.username}
+ + + {user.username} + + +
+ {user.username} +
- {props.encounter.current_location.name} -
- {props.encounter.current_location.description} -
+ {t("medications")} +
+ {t("generated_on")} {format(new Date(), "PPP 'at' p")} +
- {note} + {allergy.note}
- {note} -
+ {diagnosis.note} +
+ {symptom.note} +
+ Configure the basic behavior: mark as required, allow multiple + entries, or set as read only. +
+ Specify key collection info: time, performer, body site, and + method. +
+ Define possible answers for this question +
- {JSON.stringify(questionnaireForms, null, 2)} -
Manage and view questionnaires
{t("manage_and_view_questionnaires")}
{questionnaire.description}
- {comment.replace(/\n+/g, "\n")} -
- {t("resource_request_basic_info_description")} -
- {t("resource_request_details_description")} -
- {t("contact_information_description")} -
- {state.errors.emergency} -
+ {t("resource_request_basic_info_description")} +
+ {t("resource_request_details_description")} +
+ {t("contact_information_description")} +
{t("manage_valuesets")}
{archiveReasonError}
{editError}
- {stringifyGeoOrganization(appointment.patient.geo_organization)} + {stringifyNestedObject(appointment.patient.geo_organization)}
{t("pincode")}: {appointment.patient.pincode} diff --git a/src/pages/Appointments/AppointmentsPage.tsx b/src/pages/Appointments/AppointmentsPage.tsx index 27102edcc59..dfb7c5e1401 100644 --- a/src/pages/Appointments/AppointmentsPage.tsx +++ b/src/pages/Appointments/AppointmentsPage.tsx @@ -82,7 +82,7 @@ import { AppointmentStatuses, TokenSlot, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface QueryParams { practitioner: string | null; @@ -337,8 +337,6 @@ export default function AppointmentsPage(props: { facilityId?: string }) { return ( - + + setQParams({ + ...qParams, + practitioner: null, + slot: null, + }) + } + className="cursor-pointer w-full" + > + + {t("show_all")} + {!qParams.practitioner && ( + + )} + + + {schedulableUsersQuery.data?.users.map((user) => ( setQParams({ ...qParams, - practitioner: null, + practitioner: user.username, slot: null, }) } - className="cursor-pointer" + className="cursor-pointer w-full" > - {t("show_all")} - {!qParams.practitioner && ( + + + {formatName(user)} + + {user.user_type} + + + {qParams.practitioner === user.username && ( )} - - {schedulableUsersQuery.data?.users.map((user) => ( - - - setQParams({ - ...qParams, - practitioner: user.username, - slot: null, - }) - } - className="cursor-pointer" - > - - - {formatName(user)} - - {user.user_type} - - - {qParams.practitioner === user.username && ( - - )} - - ))} diff --git a/src/pages/Appointments/BookAppointment.tsx b/src/pages/Appointments/BookAppointment.tsx index 5a670f81e16..fb61ed487f4 100644 --- a/src/pages/Appointments/BookAppointment.tsx +++ b/src/pages/Appointments/BookAppointment.tsx @@ -25,7 +25,7 @@ import useAppHistory from "@/hooks/useAppHistory"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { formatDisplayName, formatName } from "@/Utils/utils"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; import { AppointmentSlotPicker } from "./components/AppointmentSlotPicker"; diff --git a/src/pages/Appointments/components/AppointmentSlotPicker.tsx b/src/pages/Appointments/components/AppointmentSlotPicker.tsx index 1830c9b423f..0220b47678a 100644 --- a/src/pages/Appointments/components/AppointmentSlotPicker.tsx +++ b/src/pages/Appointments/components/AppointmentSlotPicker.tsx @@ -18,7 +18,7 @@ import { useAvailabilityHeatmap, } from "@/pages/Appointments/utils"; import { TokenSlot } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface AppointmentSlotPickerProps { facilityId: string; diff --git a/src/pages/Appointments/utils.ts b/src/pages/Appointments/utils.ts index 50ab30c5b6f..2144524f130 100644 --- a/src/pages/Appointments/utils.ts +++ b/src/pages/Appointments/utils.ts @@ -15,7 +15,7 @@ import { AvailabilityHeatmapResponse, TokenSlot, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; export const groupSlotsByAvailability = (slots: TokenSlot[]) => { const result: { diff --git a/src/pages/Apps/PlugConfigEdit.tsx b/src/pages/Apps/PlugConfigEdit.tsx index 353a1796b88..f6230b71f15 100644 --- a/src/pages/Apps/PlugConfigEdit.tsx +++ b/src/pages/Apps/PlugConfigEdit.tsx @@ -2,6 +2,8 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { useNavigate } from "raviger"; import { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { @@ -15,7 +17,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; @@ -108,7 +110,7 @@ export function PlugConfigEdit({ slug }: Props) { Cancel Delete diff --git a/src/pages/Encounters/EncounterList.tsx b/src/pages/Encounters/EncounterList.tsx index f6eaf605c80..9af50a0ebca 100644 --- a/src/pages/Encounters/EncounterList.tsx +++ b/src/pages/Encounters/EncounterList.tsx @@ -213,7 +213,7 @@ export function EncounterList({ const { t } = useTranslation(); return ( - + @@ -261,6 +261,7 @@ export function EncounterList({ onFieldChange={handleFieldChange} onSearch={handleSearch} className="w-full border-none shadow-none" + autoFocus /> diff --git a/src/pages/Encounters/EncounterShow.tsx b/src/pages/Encounters/EncounterShow.tsx index 1f065b6e50c..020bd8f636d 100644 --- a/src/pages/Encounters/EncounterShow.tsx +++ b/src/pages/Encounters/EncounterShow.tsx @@ -26,6 +26,7 @@ export interface EncounterTabProps { facilityId: string; encounter: Encounter; patient: Patient; + subPage?: string; } const defaultTabs = { @@ -44,10 +45,11 @@ interface Props { encounterId: string; facilityId: string; tab?: string; + subPage?: string; } export const EncounterShow = (props: Props) => { - const { facilityId, encounterId } = props; + const { facilityId, encounterId, subPage } = props; const { t } = useTranslation(); const pluginTabs = useCareAppConsultationTabs(); @@ -74,6 +76,7 @@ export const EncounterShow = (props: Props) => { const encounterTabProps: EncounterTabProps = { encounter: encounterData, patient: encounterData.patient, + subPage: subPage, facilityId, }; @@ -97,21 +100,7 @@ export const EncounterShow = (props: Props) => { return ( - + { )} > )} */} - - {t("patient_details")} - diff --git a/src/pages/Encounters/PrintPrescription.tsx b/src/pages/Encounters/PrintPrescription.tsx index 607a42eee91..6c4b0f4d332 100644 --- a/src/pages/Encounters/PrintPrescription.tsx +++ b/src/pages/Encounters/PrintPrescription.tsx @@ -17,8 +17,9 @@ import medicationRequestApi from "@/types/emr/medicationRequest/medicationReques export const PrintPrescription = (props: { facilityId: string; encounterId: string; + patientId: string; }) => { - const { facilityId, encounterId } = props; + const { facilityId, encounterId, patientId } = props; const { t } = useTranslation(); const { data: encounter } = useQuery({ @@ -30,12 +31,12 @@ export const PrintPrescription = (props: { }); const { data: medications } = useQuery({ - queryKey: ["medication_requests", encounter?.patient?.id], + queryKey: ["medication_requests", patientId], queryFn: query(medicationRequestApi.list, { - pathParams: { patientId: encounter?.patient?.id || "" }, + pathParams: { patientId }, queryParams: { encounter: encounterId, limit: 50, offset: 0 }, }), - enabled: !!encounter?.patient?.id, + enabled: !!patientId, }); if (!medications?.results?.length) { @@ -60,12 +61,8 @@ export const PrintPrescription = (props: { return ( @@ -91,7 +88,7 @@ export const PrintPrescription = (props: { @@ -125,7 +122,7 @@ export const PrintPrescription = (props: { â„ž {/* Medications Table */} - + {/* Doctor's Signature */} diff --git a/src/pages/Encounters/tabs/EncounterFilesTab.tsx b/src/pages/Encounters/tabs/EncounterFilesTab.tsx index ff270ac9d60..76add034ca5 100644 --- a/src/pages/Encounters/tabs/EncounterFilesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterFilesTab.tsx @@ -7,7 +7,8 @@ export const EncounterFilesTab = (props: EncounterTabProps) => { ); }; diff --git a/src/pages/Encounters/tabs/EncounterNotesTab.tsx b/src/pages/Encounters/tabs/EncounterNotesTab.tsx index 89b535134df..058606392e4 100644 --- a/src/pages/Encounters/tabs/EncounterNotesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterNotesTab.tsx @@ -9,11 +9,13 @@ import { Info, Loader2, MessageCircle, + MessageSquare, MessageSquarePlus, Plus, Send, Users, } from "lucide-react"; +import { Link, usePathParams } from "raviger"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useInView } from "react-intersection-observer"; @@ -25,6 +27,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, + DialogClose, DialogContent, DialogDescription, DialogFooter, @@ -61,6 +64,7 @@ import { Thread } from "@/types/notes/threads"; const MESSAGES_LIMIT = 20; // Thread templates for quick selection + const threadTemplates = [ "Treatment Plan", "Medication Notes", @@ -118,6 +122,7 @@ const ThreadItem = ({ // Message item component const MessageItem = ({ message }: { message: Message }) => { const authUser = useAuthUser(); + const { facilityId } = usePathParams("/facility/:facilityId/*")!; const isCurrentUser = authUser?.external_id === message.created_by.id; return ( @@ -135,15 +140,19 @@ const MessageItem = ({ message }: { message: Message }) => { > - - - - - + + + + + + + {message.created_by.username} @@ -192,11 +201,13 @@ const NewThreadDialog = ({ onClose, onCreate, isCreating, + threadsUnused, }: { isOpen: boolean; onClose: () => void; onCreate: (title: string) => void; isCreating: boolean; + threadsUnused: string[]; }) => { const { t } = useTranslation(); const [title, setTitle] = useState(""); @@ -218,13 +229,15 @@ const NewThreadDialog = ({ - {t("encounter_notes__choose_template")} + {threadsUnused.length === 0 + ? t("encounter_notes__no_unused_threads") + : t("encounter_notes__choose_template")} - {threadTemplates.map((template) => ( + {threadsUnused.map((template) => ( - - {t("Cancel")} - + + {t("cancel")} + + onCreate(title)} disabled={!title.trim() || isCreating} @@ -258,7 +272,7 @@ const NewThreadDialog = ({ ) : ( )} - {t("Create")} + {t("create")} @@ -308,6 +322,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { const [newMessage, setNewMessage] = useState(""); const messagesEndRef = useRef(null); const { ref, inView } = useInView(); + const [commentAdded, setCommentAdded] = useState(false); // Fetch threads const { data: threadsData, isLoading: threadsLoading } = useQuery({ @@ -318,17 +333,11 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { }), }); - // Auto-select first thread - useEffect(() => { - if (threadsData?.results.length && !selectedThread) { - setSelectedThread(threadsData.results[0].id); - } - }, [threadsData, selectedThread]); - // Fetch messages with infinite scroll const { data: messagesData, isLoading: messagesLoading, + isFetching: isFetchingMessages, hasNextPage, fetchNextPage, isFetchingNextPage, @@ -382,28 +391,63 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["messages", selectedThread] }); setNewMessage(""); - setTimeout(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, 100); + setCommentAdded(true); }, }); - // Handle infinite scroll + // handle scrolling to last message when new message is added + useEffect(() => { - if (inView && hasNextPage) { - fetchNextPage(); + if (commentAdded && !isFetchingMessages) { + messagesEndRef.current?.scrollIntoView(); + setCommentAdded(false); } - }, [inView, hasNextPage, fetchNextPage]); + }, [commentAdded, isFetchingMessages]); + + const [threads, setThreads] = useState([...threadTemplates]); + + // Auto-select first thread - // Scroll to bottom on initial load and thread change useEffect(() => { - if (messagesData && !messagesLoading && !isFetchingNextPage) { + if (threadsData?.results.length) { + if (!selectedThread) setSelectedThread(threadsData.results[0].id); + const threadTitles = threadsData.results.map((thread) => thread.title); + setThreads( + threads.filter((template) => !threadTitles.includes(template)), + ); + } + }, [threadsData, selectedThread]); + + // hack to scroll to bottom on initial load + + useEffect(() => { + messagesEndRef.current?.scrollIntoView(); + }, [messagesLoading]); + + // Handle infinite scroll + + useEffect(() => { + if (inView && hasNextPage) { + fetchNextPage(); messagesEndRef.current?.scrollIntoView(); } - }, [selectedThread, messagesData, messagesLoading, isFetchingNextPage]); + }, [ + inView, + hasNextPage, + fetchNextPage, + messagesData, + isFetchingNextPage, + messagesLoading, + ]); const handleCreateThread = (title: string) => { if (title.trim()) { + if ( + threadsData?.results.some((thread) => thread.title === title.trim()) + ) { + toast.error(t("thread_already_exists")); + return; + } createThreadMutation.mutate({ title: title.trim(), encounter: encounter.id, @@ -423,6 +467,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { } const messages = messagesData?.pages.flatMap((page) => page.results) ?? []; + const totalMessages = messagesData?.pages[0]?.count ?? 0; return ( @@ -529,8 +574,8 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { {/* Main Content */} - {/* Mobile Header */} - + {/* Header */} + {selectedThread ? ( @@ -539,10 +584,27 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { ?.title } - - - {messages.length} - + + + + + + {new Set(messages.map((m) => m.created_by.id)).size} + + + {totalMessages} + + + + + {t("participants")}:{" "} + {new Set(messages.map((m) => m.created_by.id)).size} + + + {t("messages")}: {totalMessages} + + + ) : ( @@ -550,13 +612,12 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { )} - {selectedThread ? ( <> {messagesLoading ? ( - + ) : ( @@ -580,17 +641,17 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { )) )} - {isFetchingNextPage && ( + {isFetchingNextPage ? ( + ) : ( + )} - - {/* Message Input */} @@ -662,6 +723,7 @@ export const EncounterNotesTab = ({ encounter }: EncounterTabProps) => { onClose={() => setShowNewThreadDialog(false)} onCreate={handleCreateThread} isCreating={createThreadMutation.isPending} + threadsUnused={threads} /> ); diff --git a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx index 5f6eaa151d9..d55cf9b5346 100644 --- a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx @@ -14,7 +14,7 @@ export const EncounterUpdatesTab = ({ return ( {/* Main Content Area */} - + {/* Left Column - Symptoms, Diagnoses, and Questionnaire Responses */} {/* Allergies Section */} @@ -55,7 +55,7 @@ export const EncounterUpdatesTab = ({ {/* Right Column - Observations */} - + diff --git a/src/pages/Facility/overview.tsx b/src/pages/Facility/overview.tsx index 346a34c6c48..cf07db69e65 100644 --- a/src/pages/Facility/overview.tsx +++ b/src/pages/Facility/overview.tsx @@ -27,7 +27,7 @@ export function FacilityOverview({ facilityId }: FacilityOverviewProps) { href: `/facility/${facilityId}/users/${user?.username}/availability`, }, { - title: t("Encounters"), + title: t("encounters"), description: t("manage_facility_users"), icon: Users, href: `/facility/${facilityId}/encounters`, diff --git a/src/pages/Facility/settings/layout.tsx b/src/pages/Facility/settings/layout.tsx index 9ba1d35ec39..a2b7c1c58f3 100644 --- a/src/pages/Facility/settings/layout.tsx +++ b/src/pages/Facility/settings/layout.tsx @@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import ErrorPage from "@/components/ErrorPages/DefaultErrorPage"; + import { GeneralSettings } from "./general/general"; import LocationList from "./locations/LocationList"; import LocationView from "./locations/LocationView"; @@ -28,7 +30,7 @@ const getRoutes = (facilityId: string) => ({ "/location/:id": ({ id }: { id: string }) => ( ), - "*": () => 404, + "*": () => , }); export function SettingsLayout({ facilityId }: SettingsLayoutProps) { diff --git a/src/pages/Facility/settings/locations/LocationForm.tsx b/src/pages/Facility/settings/locations/LocationForm.tsx index dc9b3f96bfd..6f82f64228a 100644 --- a/src/pages/Facility/settings/locations/LocationForm.tsx +++ b/src/pages/Facility/settings/locations/LocationForm.tsx @@ -223,7 +223,7 @@ export default function LocationForm({ - + {locationFormOptions.map((option) => ( {option.label} diff --git a/src/pages/Facility/settings/locations/LocationView.tsx b/src/pages/Facility/settings/locations/LocationView.tsx index d5527d5fe4f..9bab726b813 100644 --- a/src/pages/Facility/settings/locations/LocationView.tsx +++ b/src/pages/Facility/settings/locations/LocationView.tsx @@ -1,10 +1,18 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Link } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Badge } from "@/components/ui/badge"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -44,7 +52,6 @@ export default function LocationView({ id, facilityId }: Props) { pathParams: { facility_id: facilityId, id }, }), }); - const { data: locationOrganizations } = useQuery({ queryKey: ["location", id, "organizations"], queryFn: query(locationApi.getOrganizations, { @@ -92,110 +99,157 @@ export default function LocationView({ id, facilityId }: Props) { ); + const generateBreadcrumbs = (location: any) => { + const breadcrumbs = []; + let current = location; + while (current) { + breadcrumbs.unshift({ + name: current.name, + id: current.id, + }); + current = current.parent; + } + return breadcrumbs; + }; + const breadcrumbs = location ? generateBreadcrumbs(location) : []; return ( - - - - - - {t("locations")} - - {getLocationFormLabel(location?.form)} - - - {location?.status} - - {location && "mode" in location && location.mode === "kind" && ( - - - {t("add_location")} - + <> + + + + + {t("home")} + + + {breadcrumbs.map((breadcrumb, index) => ( + + {index === breadcrumbs.length - 1 ? ( + + {breadcrumb.name} + + ) : ( + <> + + {breadcrumb.name} + + + > )} + + ))} + + + + + + + + + {t("locations")} + + {getLocationFormLabel(location?.form)} + + + {location?.status} + + {location && "mode" in location && location.mode === "kind" && ( + + + {t("add_location")} + + )} + + + { + setSearchQuery(e.target.value); + setPage(1); + }} + className="w-full" + /> + - - { - setSearchQuery(e.target.value); - setPage(1); + {locationOrganizations && ( + + + {t("manage_organizations")} + + } + onUpdate={() => { + queryClient.invalidateQueries({ + queryKey: ["location", facilityId, id], + }); }} - className="w-full" /> - + )} - {locationOrganizations && ( - - - {t("manage_organizations")} - - } - onUpdate={() => { - queryClient.invalidateQueries({ - queryKey: ["location", facilityId, id], - }); - }} - /> - )} - - {isLoading ? ( - - - - ) : ( - + {isLoading ? ( - {children?.results?.length ? ( - children.results.map((childLocation: LocationList) => ( - + + ) : ( + + + {children?.results?.length ? ( + children.results.map((childLocation: LocationList) => ( + + )) + ) : ( + + + {searchQuery + ? t("no_locations_found") + : t("no_child_locations_found")} + + + )} + + {children && children.count > limit && ( + + setPage(page)} + defaultPerPage={limit} + cPage={page} /> - )) - ) : ( - - - {searchQuery - ? t("no_locations_found") - : t("no_child_locations_found")} - - + )} - {children && children.count > limit && ( - - setPage(page)} - defaultPerPage={limit} - cPage={page} - /> - - )} - - )} - + )} + - - + + + > ); } diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx index 571d898bd10..da1e2eae284 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx @@ -55,12 +55,7 @@ export default function FacilityOrganizationIndex({ if (!data?.results?.length) { return ( - + @@ -87,12 +82,7 @@ export default function FacilityOrganizationIndex({ } return ( - + diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx index 509f451a592..d0bde50e2ce 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationUsers.tsx @@ -1,19 +1,19 @@ import { useQuery } from "@tanstack/react-query"; -import { useQueryParams } from "raviger"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/Common/Avatar"; -import { - CardGridSkeleton, - CardListSkeleton, -} from "@/components/Common/SkeletonLoading"; +import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import { UserStatusIndicator } from "@/components/Users/UserListAndCard"; +import useFilters from "@/hooks/useFilters"; + import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; import AddUserSheet from "@/pages/Organization/components/AddUserSheet"; @@ -29,19 +29,30 @@ interface Props { } export default function FacilityOrganizationUsers({ id, facilityId }: Props) { - const [qParams, setQueryParams] = useQueryParams<{ + const [sheetState, setSheetState] = useState<{ sheet: string; username: string; - }>(); + }>({ + sheet: "", + username: "", + }); + const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ + limit: 12, + }); const { t } = useTranslation(); - const openAddUserSheet = qParams.sheet === "add"; - const openLinkUserSheet = qParams.sheet === "link"; + const openAddUserSheet = sheetState.sheet === "add"; + const openLinkUserSheet = sheetState.sheet === "link"; const { data: users, isLoading: isLoadingUsers } = useQuery({ - queryKey: ["facilityOrganizationUsers", facilityId, id], - queryFn: query(routes.facilityOrganization.listUsers, { + queryKey: ["facilityOrganizationUsers", facilityId, id, qParams], + queryFn: query.debounced(routes.facilityOrganization.listUsers, { pathParams: { facilityId, organizationId: id }, + queryParams: { + search_text: qParams.search || undefined, + limit: resultsPerPage, + offset: ((qParams.page || 1) - 1) * resultsPerPage, + }, }), enabled: !!id, }); @@ -50,32 +61,32 @@ export default function FacilityOrganizationUsers({ id, facilityId }: Props) { return null; } - if (isLoadingUsers) { - return ( - - - - - - - - - ); - } - return ( - - {t("users")} - + + + + { + updateQuery({ search: e.target.value || undefined }); + }} + className="w-full pl-8" + /> + + { - setQueryParams({ sheet: open ? "add" : "", username: "" }); + setSheetState({ sheet: open ? "add" : "", username: "" }); }} onUserCreated={(user) => { - setQueryParams({ sheet: "link", username: user.username }); + setSheetState({ sheet: "link", username: user.username }); }} /> { - setQueryParams({ sheet: open ? "link" : "", username: "" }); + setSheetState({ sheet: open ? "link" : "", username: "" }); }} - preSelectedUsername={qParams.username} + preSelectedUsername={sheetState.username} /> - - {users?.results?.length === 0 ? ( - - - {t("no_users_found")} - - - ) : ( - users?.results?.map((userRole: OrganizationUserRole) => ( - - - - - - - - - - {userRole.user.first_name} {userRole.user.last_name} - - - + {isLoadingUsers ? ( + + + + ) : ( + + + {!users?.results?.length ? ( + + + {t("no_users_found")} + + + ) : ( + users.results.map((userRole: OrganizationUserRole) => ( + + + + + + + + + + {userRole.user.first_name}{" "} + {userRole.user.last_name} + + + + + + {userRole.user.username} - - + + + + {t("role")} + + {userRole.role.name} + + + + + {t("phone_number")} + + + {userRole.user.phone_number} + + + - - - - {t("role")} - - {userRole.role.name} - + + + {t("see_details")} + + } + /> - - {t("phone_number")} - - {userRole.user.phone_number} - - - - - - - - {t("more_details")} - - } - /> - - - - - )) - )} - + + + )) + )} + + + {(users?.results || []).length > 0 && + users?.count && + users.count > resultsPerPage && ( + + + + )} + + )} ); diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx index 1ce58eaa786..5efc8fc4076 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationView.tsx @@ -10,11 +10,13 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import Pagination from "@/components/Common/Pagination"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; +import useFilters from "@/hooks/useFilters"; + import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; +import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; import CreateFacilityOrganizationSheet from "./components/CreateFacilityOrganizationSheet"; import FacilityOrganizationLayout from "./components/FacilityOrganizationLayout"; @@ -24,12 +26,47 @@ interface Props { facilityId: string; } -export default function FacilityOrganizationView({ id, facilityId }: Props) { +function OrganizationCard({ + org, +}: { + org: FacilityOrganization; + facilityId: string; +}) { const { t } = useTranslation(); - const [page, setPage] = useState(1); + return ( + + + + + + {org.name} + + + {org.org_type} + + + + + {t("see_details")} + + + + + + ); +} + +export default function FacilityOrganizationView({ id, facilityId }: Props) { + const { t } = useTranslation(); + const { qParams, Pagination, resultsPerPage } = useFilters({ + limit: 12, + cacheBlacklist: ["username"], + }); const [searchQuery, setSearchQuery] = useState(""); - const limit = 12; // 3x4 grid const { data: children, isLoading } = useQuery({ queryKey: [ @@ -37,16 +74,16 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { "list", facilityId, id, - page, - limit, + qParams.page, + resultsPerPage, searchQuery, ], queryFn: query.debounced(routes.facilityOrganization.list, { pathParams: { facilityId }, queryParams: { parent: id, - offset: (page - 1) * limit, - limit, + offset: ((qParams.page || 1) - 1) * resultsPerPage, + limit: resultsPerPage, name: searchQuery || undefined, }, }), @@ -54,20 +91,24 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { return ( - + - {t("departments")} - - - { - setSearchQuery(e.target.value); - setPage(1); // Reset to first page on search - }} - className="w-full" - /> + + + + + { + setSearchQuery(e.target.value); + }} + className="w-full pl-8" + /> + ) : ( - + {children?.results?.length ? ( children.results.map((org) => ( - - - - - - - {org.name} - - - {org.org_type} - {org.org_type} - - - - - {t("view_details")} - - - - - {org.description && ( - - {org.description} - - )} - - - + )) ) : ( @@ -127,14 +142,9 @@ export default function FacilityOrganizationView({ id, facilityId }: Props) { )} - {children && children.count > limit && ( + {children && children.count > resultsPerPage && ( - setPage(page)} - defaultPerPage={limit} - cPage={page} - /> + )} diff --git a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx index aa0a1774d58..feca583e80d 100644 --- a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx @@ -1,4 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { t } from "i18next"; import { useState } from "react"; import { toast } from "sonner"; @@ -62,7 +63,7 @@ export default function CreateFacilityOrganizationSheet({ queryClient.invalidateQueries({ queryKey: ["getCurrentUser"], }); - toast.success("Organization created successfully"); + toast.success(t("organization_created_successfully")); setOpen(false); setName(""); setDescription(""); @@ -72,7 +73,7 @@ export default function CreateFacilityOrganizationSheet({ const handleSubmit = () => { if (!name.trim()) { - toast.error("Please enter an organization name"); + toast.error(t("please_enter_organization_name")); return; } @@ -87,16 +88,16 @@ export default function CreateFacilityOrganizationSheet({ return ( - + - Create Department/Team + {t("add_department_team")} - Create Department/Team + {t("create_department_team")} - Create a new department/team in this facility. + {t("create_department_team_description")} @@ -105,18 +106,18 @@ export default function CreateFacilityOrganizationSheet({ setName(e.target.value)} - placeholder="Enter department/team name" + placeholder={t("enter_department_team_name")} /> - Type + {t(`type`)} setOrgType(value)} > - + {ORG_TYPES.map((type) => ( @@ -133,7 +134,7 @@ export default function CreateFacilityOrganizationSheet({ setDescription(e.target.value)} - placeholder="Enter department/team description (optional)" + placeholder={t("enter_department_team_description")} /> @@ -142,7 +143,7 @@ export default function CreateFacilityOrganizationSheet({ onClick={handleSubmit} disabled={isPending || !name.trim()} > - {isPending ? "Creating..." : "Create Organization"} + {isPending ? t("creating") : t("create_organization")} diff --git a/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx b/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx index 90ab2e20ca2..6628bfac56b 100644 --- a/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import { AlertDialog, AlertDialogAction, @@ -14,7 +16,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, @@ -218,7 +220,7 @@ export default function EditUserRoleSheet({ {t("cancel")} removeRole()} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + className={cn(buttonVariants({ variant: "destructive" }))} > {t("remove")} diff --git a/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx b/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx index c706c2b38d9..de12da5794e 100644 --- a/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx +++ b/src/pages/Facility/settings/organizations/components/FacilityOrganizationLayout.tsx @@ -1,8 +1,8 @@ import { useQuery } from "@tanstack/react-query"; import { Link, usePath } from "raviger"; +import { useTranslation } from "react-i18next"; -import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; - +import { Badge } from "@/components/ui/badge"; import { Breadcrumb, BreadcrumbItem, @@ -10,8 +10,8 @@ import { BreadcrumbList, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; -import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar"; import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Page from "@/components/Common/Page"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; @@ -32,7 +32,7 @@ interface Props { interface NavItem { path: string; title: string; - icon: IconName; + value: string; } export default function FacilityOrganizationLayout({ @@ -41,20 +41,24 @@ export default function FacilityOrganizationLayout({ children, }: Props) { const path = usePath() || ""; + const { t } = useTranslation(); const navItems: NavItem[] = [ { path: `/departments/${id}`, - title: "Departments", - icon: "d-hospital", + title: t("departments_or_teams"), + value: "departments", }, { path: `/departments/${id}/users`, - title: "Users", - icon: "d-people", + title: t("users"), + value: "users", }, ]; + const currentTab = + navItems.find((item) => item.path === path)?.value || "departments"; + const { data: org, isLoading } = useQuery({ queryKey: ["facilityOrganization", id], queryFn: query(routes.facilityOrganization.get, { @@ -74,9 +78,9 @@ export default function FacilityOrganizationLayout({ ); } - // add loading state + if (!org) { - return Not found; + return {t("not_found")}; } const orgParents: FacilityOrganizationParent[] = []; @@ -89,55 +93,76 @@ export default function FacilityOrganizationLayout({ } return ( - - {/* Since we have links to all parent organizations, we can show the breadcrumb here */} - - - {/* Org has parent and each parent may have another parent, so we need to show all the parents */} - - {orgParents.reverse().map((parent) => ( - <> - - - {parent.name} - + <> + {orgParents.length > 0 && ( + + + + {orgParents.reverse().map((parent) => ( + <> + + + + {parent.name} + + + + + + + > + ))} + + + {org.name} + - - - - > - ))} - - - {org.name} - - - - - {/* Navigation */} - - - {navItems.map((item) => ( - - - - - {item.title} + + + + )} + + {t(`facility_organization_type__${org.org_type}`)} + + } + className="mx-auto max-w-4xl" + > + + {org.description && ( + + {org.description} + + )} + + + {navItems.map((item) => ( + + + {item.title} + - - - ))} - - - {/* Page Content */} - {children} - + ))} + + + + {children} + + > ); } diff --git a/src/pages/Organization/OrganizationUsers.tsx b/src/pages/Organization/OrganizationUsers.tsx index 479059fa9a3..e05e6374097 100644 --- a/src/pages/Organization/OrganizationUsers.tsx +++ b/src/pages/Organization/OrganizationUsers.tsx @@ -1,13 +1,15 @@ import { useQuery } from "@tanstack/react-query"; +import { useCallback } from "react"; import { useTranslation } from "react-i18next"; +import { isValidPhoneNumber } from "react-phone-number-input"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/Common/Avatar"; +import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import { UserStatusIndicator } from "@/components/Users/UserListAndCard"; @@ -30,19 +32,62 @@ interface Props { export default function OrganizationUsers({ id, navOrganizationId }: Props) { const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ limit: 15, - cacheBlacklist: ["search"], + cacheBlacklist: ["name", "phone_number"], }); const { t } = useTranslation(); + const searchOptions = [ + { + key: "username", + type: "text" as const, + placeholder: "Search by username", + value: qParams.name || "", + }, + { + key: "phone_number", + type: "phone" as const, + placeholder: "Search by phone number", + value: qParams.phone_number || "", + }, + ]; + + const handleSearch = useCallback((key: string, value: string) => { + const searchParams = { + name: key === "username" ? value : "", + phone_number: + key === "phone_number" + ? isValidPhoneNumber(value) + ? value + : undefined + : undefined, + }; + updateQuery(searchParams); + }, []); + + const handleFieldChange = () => { + updateQuery({ + name: undefined, + phone_number: undefined, + }); + }; + const openAddUserSheet = qParams.sheet === "add"; const openLinkUserSheet = qParams.sheet === "link"; const { data: users, isFetching: isFetchingUsers } = useQuery({ - queryKey: ["organizationUsers", id, qParams.search, qParams.page], + queryKey: [ + "organizationUsers", + id, + qParams.name, + qParams.phone_number, + qParams.page, + ], queryFn: query.debounced(organizationApi.listUsers, { pathParams: { id }, queryParams: { - username: qParams.search, + username: qParams.name, + phone_number: qParams.phone_number, + page: qParams.page, limit: resultsPerPage, offset: ((qParams.page ?? 1) - 1) * resultsPerPage, }, @@ -88,16 +133,16 @@ export default function OrganizationUsers({ id, navOrganizationId }: Props) { - - updateQuery({ - search: e.target.value as string, - }) - } - className="max-w-sm" + option.value !== ""), + 0, + )} + onSearch={handleSearch} + onFieldChange={handleFieldChange} + className="w-full" data-cy="search-user" /> diff --git a/src/pages/Organization/components/AddUserSheet.tsx b/src/pages/Organization/components/AddUserSheet.tsx index 1efba82ca81..b6054eb4a56 100644 --- a/src/pages/Organization/components/AddUserSheet.tsx +++ b/src/pages/Organization/components/AddUserSheet.tsx @@ -33,7 +33,7 @@ export default function AddUserSheet({ return ( - + {t("add_user")} diff --git a/src/pages/Organization/components/EditUserRoleSheet.tsx b/src/pages/Organization/components/EditUserRoleSheet.tsx index 2903aff574b..ca8c6d796ab 100644 --- a/src/pages/Organization/components/EditUserRoleSheet.tsx +++ b/src/pages/Organization/components/EditUserRoleSheet.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + import { AlertDialog, AlertDialogAction, @@ -14,7 +16,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, @@ -226,7 +228,7 @@ export default function EditUserRoleSheet({ {t("cancel")} removeRole()} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + className={cn(buttonVariants({ variant: "destructive" }))} > {t("remove")} diff --git a/src/pages/Organization/components/OrganizationLayout.tsx b/src/pages/Organization/components/OrganizationLayout.tsx index e10168c8257..2b3a3da33a4 100644 --- a/src/pages/Organization/components/OrganizationLayout.tsx +++ b/src/pages/Organization/components/OrganizationLayout.tsx @@ -5,13 +5,6 @@ import { useTranslation } from "react-i18next"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar"; import Page from "@/components/Common/Page"; @@ -113,31 +106,7 @@ export default function OrganizationLayout({ } return ( - - {/* Since we have links to all parent organizations, we can show the breadcrumb here */} - - - {/* Org has parent and each parent may have another parent, so we need to show all the parents */} - - {orgParents.reverse().map((parent) => ( - <> - - - {parent.name} - - - - - - > - ))} - - - {org.name} - - - - + {/* Navigation */} diff --git a/src/pages/Scheduling/ScheduleExceptions.tsx b/src/pages/Scheduling/ScheduleExceptions.tsx index 7480c49a323..96e97fa177e 100644 --- a/src/pages/Scheduling/ScheduleExceptions.tsx +++ b/src/pages/Scheduling/ScheduleExceptions.tsx @@ -15,7 +15,7 @@ import Loading from "@/components/Common/Loading"; import mutate from "@/Utils/request/mutate"; import { formatTimeShort } from "@/Utils/utils"; import { ScheduleException } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { items?: ScheduleException[]; diff --git a/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx b/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx index 1d050aea3cd..22883d6f75b 100644 --- a/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx +++ b/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx @@ -34,7 +34,7 @@ import { import mutate from "@/Utils/request/mutate"; import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { facilityId: string; diff --git a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx index d4081a965fe..daee952132e 100644 --- a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx +++ b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx @@ -44,7 +44,7 @@ import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; import { getSlotsPerSession, getTokenDuration } from "@/pages/Scheduling/utils"; import { ScheduleAvailabilityCreateRequest } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; interface Props { facilityId: string; diff --git a/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx index 5cdd6790bcb..16d62abc1dc 100644 --- a/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx +++ b/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx @@ -47,7 +47,7 @@ import { ScheduleAvailabilityCreateRequest, ScheduleTemplate, } from "@/types/scheduling/schedule"; -import scheduleApis from "@/types/scheduling/scheduleApis"; +import scheduleApis from "@/types/scheduling/scheduleApi"; export default function EditScheduleTemplateSheet({ template, diff --git a/src/pages/UserDashboard.tsx b/src/pages/UserDashboard.tsx index ab7e8430033..573d51c0032 100644 --- a/src/pages/UserDashboard.tsx +++ b/src/pages/UserDashboard.tsx @@ -1,4 +1,4 @@ -import { ChevronRight, ClipboardList, LogOut, Settings } from "lucide-react"; +import { ChevronRight, LogOut, Settings, User2Icon } from "lucide-react"; import { Link } from "raviger"; import { Button } from "@/components/ui/button"; @@ -8,6 +8,7 @@ import { Avatar } from "@/components/Common/Avatar"; import useAuthUser, { useAuthContext } from "@/hooks/useAuthUser"; +import { formatDisplayName } from "@/Utils/utils"; import { getOrgLabel } from "@/types/organization/organization"; export default function UserDashboard() { @@ -22,7 +23,7 @@ export default function UserDashboard() { @@ -62,9 +63,9 @@ export default function UserDashboard() { className="w-full sm:w-auto" asChild > - - - Questionnaires + + + Admin Dashboard )} diff --git a/src/pluginTypes.ts b/src/pluginTypes.ts index f25c3aa7eb3..180df1502a2 100644 --- a/src/pluginTypes.ts +++ b/src/pluginTypes.ts @@ -31,6 +31,10 @@ export type PatientInfoCardActionsComponentType = React.FC<{ className?: string; }>; +export type PatientInfoCardMarkAsCompleteComponentType = React.FC<{ + encounter: Encounter; +}>; + export type FacilityHomeActionsComponentType = React.FC<{ facility: FacilityData; className?: string; @@ -53,6 +57,7 @@ export type SupportedPluginComponents = { Scribe: ScribeComponentType; PatientHomeActions: PatientHomeActionsComponentType; PatientInfoCardActions: PatientInfoCardActionsComponentType; + PatientInfoCardMarkAsComplete: PatientInfoCardMarkAsCompleteComponentType; FacilityHomeActions: FacilityHomeActionsComponentType; PatientRegistrationForm: PatientRegistrationFormComponentType; PatientDetailsTabDemographyGeneralInfo: PatientDetailsTabDemographyGeneralInfoComponentType; diff --git a/src/service-worker.ts b/src/service-worker.ts index 078e4237c28..973ff051736 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -1,6 +1,4 @@ /// - -/* eslint-disable no-restricted-globals */ // This service worker can be customized! // See https://developers.google.com/web/tools/workbox/modules // for the list of available Workbox modules, or add any other @@ -13,7 +11,6 @@ import { precacheAndRoute } from "workbox-precaching"; declare const self: ServiceWorkerGlobalScope; -// eslint-disable-next-line no-restricted-globals const _ignored = self.__WB_MANIFEST.map((_) => { return _; }); diff --git a/src/style/CAREUI.css b/src/style/CAREUI.css index 893d16c2e4e..0e01efa2853 100644 --- a/src/style/CAREUI.css +++ b/src/style/CAREUI.css @@ -1,34 +1,3 @@ -.cui-input-legend::before { - content: " "; - position: absolute; - left: -4px; - right: -4px; - top: calc(50% - 3px); - height: 6px; - background: #fff; - z-index: -1; -} - -.cui-label-required::after { - content: "*"; - color: rgb(255, 81, 0); - font-size: 1.2em; - font-weight: bold; - margin-left: 4px; -} - -.cui-input-base { - @apply text-sm block w-full py-3 px-4 text-black placeholder:text-secondary-600 bg-white disabled:bg-secondary-200 border border-secondary-400 focus:border-primary-400 ring-0 focus:ring-1 ring-primary-400 outline-none focus:outline-none shadow-none rounded transition-colors duration-300 !important -} - -.cui-dropdown-base { - @apply z-40 w-full rounded-b-md xl:rounded-b-lg shadow-lg overflow-auto max-h-96 bg-secondary-100 divide-y divide-secondary-300 ring-1 ring-secondary-400 focus:outline-none -} - -.cui-card { - @apply bg-white p-5 rounded-lg shadow -} - .tooltip { @apply relative } @@ -83,41 +52,6 @@ transform: translateY(0px); } -.cui-input:-webkit-autofill, -.cui-input:-webkit-autofill:hover, -.cui-input:-webkit-autofill:focus, -.cui-input:-webkit-autofill:active { - box-shadow: 0 0 0 40px #f9fafb inset !important; - -webkit-box-shadow: 0 0 0 40px #f9fafb inset !important; -} - -.cui-slideover-x { - @apply w-full md:w-[300px] h-full -} - -.cui-slideover-y { - @apply h-full md:h-[300px] w-full -} - -.dropdown-item-primary { @apply accent-primary-500 hover:bg-primary-100 text-black hover:text-primary-500 } -.dropdown-item-secondary { @apply accent-secondary-200 hover:bg-secondary-200 text-secondary-700 } -.dropdown-item-danger { @apply accent-danger-500 hover:bg-danger-100 text-danger-500 } -.dropdown-item-warning { @apply accent-warning-500 hover:bg-warning-100 text-warning-500 } -.dropdown-item-alert { @apply accent-alert-600 hover:bg-alert-100 text-alert-600 } - -.cui-form-button-group { - @apply flex flex-col-reverse md:flex-row md:justify-end gap-2 w-full md:w-auto -} - -.cui-range-slider { - @apply outline-none bg-black/10 h-2 w-full appearance-none transition-all -} -.cui-range-slider::-webkit-slider-thumb { - @apply appearance-none w-5 aspect-square bg-black border border-black rounded-full cursor-pointer hover:scale-125 transition-all -} -.cui-range-slider::-moz-range-thumb { - @apply appearance-none w-5 aspect-square bg-black border border-black rounded-full cursor-pointer -} hr { @apply border border-secondary-300 } diff --git a/src/style/index.css b/src/style/index.css index 2f14f673a18..4c7410e732b 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -10,23 +10,7 @@ @tailwind base; @tailwind components; - -@keyframes indeterminateAnimation { - 0% { - transform: translateX(0) scaleX(0); - } - 40% { - transform: translateX(0) scaleX(0.4); - } - 100% { - transform: translateX(100%) scaleX(0.5); - } -} - -.indeterminate { - animation: indeterminateAnimation 1s infinite linear; - transform-origin: 0% 50%; -} +@tailwind utilities; html { @@ -72,363 +56,12 @@ h6 { font-size: 0.875rem; } -.btn { - @apply inline-flex items-center justify-center whitespace-nowrap text-sm font-semibold py-2 px-4 rounded cursor-pointer; -} - -.btn:focus { - @apply outline-none shadow-inner; -} - -[type="text"]:focus { - @apply border-black ring-white; -} - - -/* Button Styles */ - -.btn-large { - @apply py-2 px-5 text-base h-10; -} - -a { - @apply text-blue-800; -} - -a:hover { - @apply text-blue-600; -} - -.bg-primary { - background-image: linear-gradient( 135deg, rgba(56, 161, 105, 1) 0%, rgba(47, 133, 90, 1) 100%); -} - - -/* Button */ - -button:focus { - outline: none; -} - -.btn { - @apply inline-flex items-center justify-center whitespace-nowrap text-sm font-semibold py-2 px-4 rounded cursor-pointer; -} - -.btn:focus { - @apply outline-none shadow-inner; -} - - -/* Button Styles */ - -.btn-default { - @apply border text-secondary-800; -} - -.btn-default:hover { - @apply bg-secondary-100 text-secondary-600; -} - -.btn-default:focus { - @apply bg-secondary-400 text-secondary-700; -} - -.btn-subtle { - @apply bg-secondary-200 text-secondary-800; -} - -.btn-subtle:hover { - @apply bg-secondary-300 text-secondary-900; -} - -.btn-subtle:focus { - @apply bg-secondary-400 text-secondary-900; -} - -.btn-primary-ghost { - @apply bg-white text-primary-500 border border-primary-500; -} - -.btn-primary-ghost:hover { - @apply bg-primary-100 text-primary-600 border-primary-400; -} - -.btn-primary-ghost:focus { - @apply bg-primary-800 text-white border-primary-400; -} - -.btn-primary { - @apply bg-primary-500 text-white; -} - -.btn-primary:hover { - @apply bg-primary-600; -} - -.btn-primary:focus { - @apply bg-primary-800; - background-image: none; -} - -.btn-warning { - @apply bg-yellow-500 text-white; - background-image: linear-gradient( 135deg, rgba(237, 137, 54, 1) 0%, rgba(221, 107, 32, 1) 100%); -} - -.btn-warning:hover { - @apply bg-yellow-600; - background-image: linear-gradient( 135deg, rgba(221, 107, 32, 1) 0%, rgba(192, 86, 33, 1) 100%); -} - -.btn-warning:focus { - @apply bg-yellow-800; - background-image: none; -} - -.btn-danger { - @apply bg-red-500 text-white; - background-image: linear-gradient( 135deg, rgba(245, 101, 101, 1) 0%, rgba(229, 62, 62, 1) 100%); -} - -.btn-danger:hover { - @apply bg-red-600; - background-image: linear-gradient( 135deg, rgba(229, 62, 62, 1) 0%, rgba(197, 48, 48, 1) 100%); -} - -.btn-danger:focus { - @apply bg-red-800; - background-image: none; -} - - -.primary-button { - @apply focus:outline-none text-white bg-primary-500 hover:bg-primary-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-6 py-2.5 mr-2 mb-2 transition-all ease-in-out duration-200 !important; -} - -.secondary-button { - @apply text-secondary-900 bg-white border border-secondary-300 focus:outline-none hover:bg-secondary-100 focus:ring-4 focus:ring-secondary-200 font-medium rounded-lg text-sm px-6 py-2.5 mr-2 mb-2 transition-all ease-in-out duration-200 !important; -} - -button:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-300 text-secondary-500 shadow-none border-transparent; - background-image: none; -} - -button:disabled:hover, -.disabled:hover, -button:disabled:focus, -.disabled:focus { - @apply bg-secondary-300 text-secondary-500 border-transparent shadow-none; - background-image: none; -} - -.btn-small { - @apply py-1 px-3 text-xs h-7 leading-normal; -} - -.btn-normal { - @apply py-1 px-4 text-sm h-8; -} - -.btn-large { - @apply py-2 px-5 text-base h-10; -} - -.button-xl { - @apply py-2 px-6 text-base h-12; -} - -@tailwind utilities; -.multiselect-dropdown__search-dropdown { - max-height: 20rem; - overflow: auto; -} - -.max-height-dropdown { - max-height: 20rem; - overflow: auto; -} - - -/* Radio */ - -.radio-label { - @apply select-none cursor-pointer; -} - -.radio-label span { - @apply inline-block align-middle; - transform: translate3d(0, 0, 0); -} - -.radio-label span:first-child { - @apply relative rounded-full align-middle border border-secondary-500 bg-white mr-3; - width: 1.125rem; - height: 1.125rem; - transform: scale(1); - transition: all 0.2s ease; -} - -.radio-label span:first-child svg { - position: absolute; - top: 0; - left: 0; - fill: none; - stroke: #ffffff; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - transition: all 0.3s ease; - transition-delay: 0.1s; -} - -.radio-label span:first-child:before { - content: ""; - @apply bg-primary-500 w-full h-full block opacity-100 rounded-full; - transform: scale(0); -} - -.radio-label:hover span:first-child { - @apply bg-primary-100 border-primary-500; -} - -.radio-input:checked+.radio-label span:first-child { - @apply bg-primary-500 border-primary-500; - animation: wave 0.4s ease; -} - -.radio-input:checked+.radio-label span:first-child:before { - transform: scale(3.5); - opacity: 0; - transition: all 0.6s ease; -} - - -/* Checkbox */ - -.checkbox__label { - @apply select-none cursor-pointer; -} - -.checkbox__label span { - @apply inline-block align-middle; - transform: translate3d(0, 0, 0); -} - -.checkbox__label span:first-child { - @apply relative rounded align-middle border border-secondary-500 bg-white w-4 h-4 mr-3; - transform: scale(1); - transition: all 0.2s ease; -} - -.checkbox__label span:first-child svg { - position: absolute; - top: 3px; - left: 2px; - fill: none; - stroke: #ffffff; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - stroke-dasharray: 16px; - stroke-dashoffset: 16px; - transition: all 0.3s ease; - transition-delay: 0.1s; - transform: translate3d(0, 0, 0); -} - -.checkbox__label span:first-child:before { - content: ""; - @apply bg-primary-500 w-full h-full block opacity-100 rounded-full; - transform: scale(0); -} - -.checkbox__label:hover span:first-child { - @apply bg-primary-100 border-primary-500; -} - -.checkbox__input:checked+.checkbox__label span:first-child { - @apply bg-primary-500 border-primary-500; - animation: wave 0.4s ease; -} - -.checkbox__input:checked+.checkbox__label span:first-child svg { - stroke-dashoffset: 0; -} - -.checkbox__input:checked+.checkbox__label span:first-child:before { - transform: scale(3.5); - opacity: 0; - transition: all 0.6s ease; -} - @keyframes wave { 50% { transform: scale(0.9); } } -label { - @apply block text-secondary-700 text-sm font-medium; -} - -input { - @apply border-secondary-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 transition-colors duration-300 !important; -} - -input:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-200 border-secondary-400 text-secondary-700; -} - -textarea { - @apply border-secondary-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 transition-colors duration-300; -} - -button:disabled, -.disabled { - @apply cursor-not-allowed bg-secondary-300 text-secondary-500 shadow-none border-transparent; - background-image: none; -} - - -/* Styling skeleton loading */ - -.skeleton-placeholder__line-sm { - @apply rounded-full bg-secondary-100; - height: 0.625rem; -} - -.skeleton-placeholder__line-md { - @apply rounded-full bg-secondary-100; - height: 1rem; -} - -.skeleton-placeholder__image { - @apply mt-5 h-48 rounded-lg bg-secondary-100; -} - -.skeleton-animate { - animation: skeletonShimmer 3s infinite linear; - background: linear-gradient(to right, #f5f3f7 10%, #fbfafc 40%, #f5f3f7 70%); - background-size: 1000px 100%; -} - -@keyframes skeletonShimmer { - 0% { - background-position: -1000px 0; - } - 100% { - background-position: 1000px 0; - } -} - -.skeleton-animate-alpha { - animation: skeletonShimmer 3s infinite linear; - background: linear-gradient(to right, rgba(0, 0, 0, 0.1) 10%, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.1) 70%); - background-size: 1000px 100%; -} @media print { @page { @@ -449,20 +82,6 @@ button:disabled, } } -.header-section .appBar { - z-index: 1201; -} - -.header-section .toolbar { - padding-top: 72px; -} - -@media only screen and (min-width: 601px) { - .header-section .menuButton { - display: none; - } -} - .App { text-align: center; } @@ -497,87 +116,10 @@ button:disabled, 100% {opacity: 0;} } -.badge { - display: inline-block; - padding: 0.25em 0.4em; - font-size: 12px; - font-weight: 700; - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: 0.25rem; - transition: color 0.15s; -} - -.badge-pill { - padding-right: 0.6em; - padding-left: 0.6em; - border-radius: 10rem; -} - -.badge-primary { - color: #fff; - background-color: #007bff; -} - -.badge-secondary { - color: #fff; - background-color: #6c757d; -} - -.badge-success { - color: #fff; - background-color: #28a745; -} - -.badge-warning { - color: #212529; - background-color: #ffc107; -} - -.badge-danger { - color: #fff; - background-color: #dc3545; -} - -.badge-dark { - color: #fff; - background-color: #343a40; -} - .dropdown:hover .dropdown-menu { display: block; } -.border-primary { - border-color: #28a745; -} - -.form-input { - padding-top: 6px; - padding-bottom: 6px; - width: 100%; - border-radius: 4px; - height: 38px; -} - -.form-input:focus { - border-color: #28a745 !important; - box-shadow: 0 0 0 1px #28a745 !important; -} - -.copied-to-cb { - color: #28a745; - font-size: 13px; - padding-left: 5px; - font-style: italic; -} - -.copy-to-cb { - padding-left: 5px; -} - ::-webkit-scrollbar { width: 10px; height: 8px; @@ -596,166 +138,6 @@ button:disabled, background: rgba(0, 0, 0, 0.4); } -@media (min-width:1000px) { - .manualGrid { - display: grid !important - } -} - -.scrollbar-hide::-webkit-scrollbar { - width: 0px; - height: 0px; -} - -/* - Range Sliders for Camera Feed -*/ - -#feed-range { - -webkit-appearance: none; - appearance: none; - margin: 18px 0; - width: 100%; -} - -#feed-range:focus { - outline: none; -} - -#feed-range::-webkit-slider-runnable-track { - width: 100%; - height: 1px; - cursor: pointer; - background: transparent; - border-radius: 5px; -} - -#feed-range::-webkit-slider-thumb { - height: 16px; - width: 16px; - border-radius: 3px; - border: 1px solid white; - background: black; - cursor: pointer; - -webkit-appearance: none; - margin-top: -8px; - border-radius: 50%; -} - -@media (min-width:471px) { - .csv-input { - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin: auto; - width: 50%; - } -} - -@media (max-width:470px) { - .csv-input { - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin: auto; - width: 100%; - } -} - -[type="checkbox"] { - @apply text-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none; -} - - - -.csv-input::file-selector-button:hover { - background-color: #84e1bc; -} - - - -/* for gmaps search dropdown */ -.pac-container { - z-index: 100000 !important; -} - -.csv-input::file-selector-button { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; - font-weight: 600; - border-width: 0; - background-color: #def7ec; - color: #046c4e; -} - -.csv-input::file-selector-button:hover { - background-color: #84e1bc; -} - -/* Patient Category Styles */ - -.patient-comfort { - @apply bg-patient-comfort text-patient-comfort-fore -} -.patient-stable { - @apply bg-patient-stable text-patient-stable-fore -} -.patient-abnormal { - @apply bg-patient-abnormal text-patient-abnormal-fore -} -.patient-critical { - @apply bg-patient-critical text-patient-critical-fore -} -.patient-unknown { - @apply bg-patient-unknown text-patient-unknown-fore -} - -.patient-activelydying { - @apply bg-patient-activelydying text-patient-activelydying-fore -} - -.patient-comfort-ring { - @apply ring-patient-comfort -} -.patient-stable-ring { - @apply ring-patient-stable -} -.patient-abnormal-ring { - @apply ring-patient-abnormal -} -.patient-critical-ring { - @apply ring-patient-critical -} -.patient-unknown-ring { - @apply ring-patient-unknown -} -.patient-activelydying-ring { - @apply ring-patient-activelydying -} - -.patient-comfort-profile { - @apply border-2 border-patient-comfort rounded-t -} -.patient-stable-profile { - @apply border-2 border-patient-stable rounded-t -} -.patient-abnormal-profile { - @apply border-2 border-patient-abnormal rounded-t -} -.patient-critical-profile { - @apply border-2 border-patient-critical rounded-t -} -.patient-unknown-profile { - @apply border border-patient-unknown rounded -} -.patient-activelydying-profile { - @apply border-2 border-patient-activelydying rounded-t -} - /* for gmaps search dropdown */ .pac-container { z-index: 100000 !important; @@ -855,468 +237,6 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-decoration { display: none; } -.service-panel{ - @apply flex-row -} - -@media screen and (max-width: 920px) { - .service-panel{ - @apply flex-col - } -} -/* styles for files in src/Components/CriticalCareRecording/PressureSore */ - -input[type="checkbox"]:checked~.dot { - transform: translateX(100%); -} - - -/* styles for files in src/Components/CriticalCareRecording/components */ - -.range { - position: relative; - width: 644px; - height: 5px; -} - -.range input { - width: 100%; - position: absolute; - top: 2px; - height: 0; - -webkit-appearance: none; -} - -.range input::-webkit-slider-thumb { - -webkit-appearance: none; - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-moz-range-thumb { - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-ms-thumb { - width: 18px; - height: 18px; - margin: -8px 0 0; - border-radius: 50%; - background: rgb(7, 109, 243); - cursor: pointer; - border: 0 !important; - z-index: 10; -} - -.range input::-webkit-slider-runnable-track { - width: 100%; - height: 2px; - cursor: pointer; - background: #b2b2b2; - z-index: 1; -} - -.range input::-moz-range-track { - width: 100%; - height: 2px; - cursor: pointer; - background: black; - z-index: 1; -} - -.range input::-ms-track { - width: 100%; - height: 2px; - cursor: pointer; - background: #b2b2b2; - z-index: 1; -} - -.range input:focus { - background: none; - outline: none; -} - -.range input::-ms-track { - width: 100%; - cursor: pointer; - background: transparent; - border-color: transparent; - color: transparent; -} - -.range-labels { - margin: 18px -41px 0; - padding: 0; - list-style: none; -} - -.range-labels li { - position: relative; - float: left; - width: 90.25px; - text-align: center; - color: black; - font-size: 14px; - cursor: pointer; -} - -.range-labels .label::before { - position: absolute; - top: -25px; - right: 0; - left: 0; - content: ""; - margin: 0 auto; - width: 9px; - height: 9px; - background: #b2b2b2; - border-radius: 50%; -} - -.double-range-labels { - margin: 18px -41px 0; - padding: 0; - list-style: none; -} - -.double-range-labels li { - position: relative; - float: left; - width: 45.25px; - text-align: center; - color: black; - font-size: 14px; - cursor: pointer; -} - -.double-range-labels .label::before { - position: absolute; - top: -25px; - right: 0; - left: 0; - content: ""; - margin: 0 auto; - width: 9px; - height: 9px; - background: #b2b2b2; - border-radius: 50%; -} - -.upper-label { - height: 10px; - margin-bottom: 10px; -} - -.pupil1 { - bottom: 0; - margin: 0 auto; - width: 9px; - height: 9px; - border-radius: 50%; - background-color: black; -} - -.pupil2 { - bottom: 0; - margin: 0 auto; - width: 15px; - height: 15px; - border-radius: 50%; - background-color: black; -} - -.pupil3 { - bottom: 0; - margin: 0 auto; - width: 21px; - height: 21px; - border-radius: 50%; - background-color: black; -} - -.pupil4 { - bottom: 0; - margin: 0 auto; - width: 25px; - height: 25px; - border-radius: 50%; - background-color: black; -} - -.pupil5 { - bottom: 0; - margin: 0 auto; - width: 30px; - height: 30px; - border-radius: 50%; - background-color: black; -} - -.pupil6 { - bottom: 0; - margin: 0 auto; - width: 35px; - height: 35px; - border-radius: 50%; - background-color: black; -} - -.pupil7 { - bottom: 0; - margin: 0 auto; - width: 38px; - height: 38px; - border-radius: 50%; - background-color: black; -} - -.pupil8 { - width: 41px; - height: 41px; - border-radius: 50%; - background-color: black; -} - -.range-labels .active { - color: rgb(7, 109, 243); -} - -.range-labels .selected::before { - background: rgb(7, 109, 243); -} - -.range-labels .active.selected::before { - display: none; -} - -.align-circles { - position: relative; - display: flex; - justify-content: center; - height: 41px; - vertical-align: baseline; -} - -input[type="number"] { - background-color: #fbfafc; -} - - -/* Copied CSS */ - -.slider { - position: relative; - width: 200px; -} - -.slider__track, -.slider__range, -.slider__left-value, -.slider__right-value { - position: absolute; -} - -.slider__track, -.slider__range { - border-radius: 3px; - height: 5px; -} - -.slider__track { - background-color: #1476d8; - width: 100%; - z-index: 1; -} - -.slider__range { - background-color: #19ebe0; - z-index: 2; -} - -.slider__left-value, -.slider__right-value { - color: #1c85ee; - font-size: 12px; - margin-top: 20px; -} - -.slider__left-value { - left: 6px; -} - -.slider__right-value { - right: -4px; -} - - -/* Removing the default appearance */ - -.thumb input { - pointer-events: none; - position: absolute; - height: 0; - width: 200px; - outline: none; -} - -.thumb--left { - z-index: 3; -} - -.thumb--right { - z-index: 4; -} - - -/* For Chrome browsers */ - -.thumb input::-webkit-slider-thumb { - background-color: #242525; - border: none; - border-radius: 50%; - box-shadow: 0 0 1px 1px #0b2846; - cursor: pointer; - height: 18px; - width: 18px; - margin-top: 4px; - pointer-events: all; - position: relative; -} - - -/* For Firefox browsers */ - -.thumb input::-moz-range-thumb { - background-color: #272b2c; - border: none; - border-radius: 50%; - box-shadow: 0 0 1px 1px #0d2d4d; - cursor: pointer; - height: 18px; - width: 18px; - margin-top: 4px; - pointer-events: all; - position: relative; -} - -.slider { - appearance: none; - background-color: #dee0e4; - width: 100%; - height: 4px; - margin: 0; - padding: 0; - z-index: 2; -} - -.slider::-webkit-slider-thumb { - cursor: pointer; -} - -.slider-container { - position: relative; - width: 100%; - /* max-width: 700px; */ - display: flex; - justify-content: center; -} - -.indicators { - position: absolute; - top: -200%; - width: 100%; - height: 20px; - border-left: 2px solid #dee0e4; -} - -.tick { - content: ""; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: linear-gradient(to left, #dee0e4 2px, transparent 2px); -} - -.slider-box { - width: 100%; - /* max-width: 700px; */ - padding: 20px 10px 60px 10px; - border-radius: 12px; -} - -.slider-head { - font-family: sans-serif; - margin-bottom: 2rem; -} - -.slider-head>div>h1 { - font-size: 1rem; - font-weight: bold; -} - -.slider-head input { - padding: 0.5rem 0.7rem; - border-radius: 8px; - border: 1px solid #979aa0; - margin-left: 12px; - font-size: 1.1rem; - color: #383a3e; - letter-spacing: 1px; -} - -.slider-head label { - font-weight: 700; - color: #2856ff; - font-size: 1rem; -} - - -.grid-2-col, .grid-3-col, .grid-2-1-col, .grid-3-1-col, .grid-1-2-col { - display: grid; - grid-template-columns: 1fr; - gap: 1.5rem; -} - -@media (min-width: 768px) { - .grid-2-col { - grid-template-columns: repeat(2, 1fr); - } - .grid-3-col { - grid-template-columns: repeat(3, 1fr); - } - .grid-2-1-col { - grid-template-columns: 2fr 1fr; - } - .grid-1-2-col { - grid-template-columns: 1fr 2fr; - } -} - -.input-field-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} -.col-span-all { - grid-column: 1 / -1; -} - @layer base { :root { --radius: 0.5rem; diff --git a/src/types/emr/encounter.ts b/src/types/emr/encounter.ts index f63a118a83f..45c6602b763 100644 --- a/src/types/emr/encounter.ts +++ b/src/types/emr/encounter.ts @@ -117,6 +117,12 @@ export type StatusHistory = { history: History[]; }; +export type LocationHistory = { + start_datetime: string; + location: LocationList; + status: string; +}; + export interface Encounter { id: string; patient: Patient; @@ -138,6 +144,7 @@ export interface Encounter { status_history: StatusHistory; organizations: FacilityOrganization[]; current_location: LocationList; + location_history: LocationHistory[]; } export interface EncounterEditRequest { diff --git a/src/types/emr/medicationAdministration/medicationAdministration.ts b/src/types/emr/medicationAdministration/medicationAdministration.ts index a736846de4a..caf6f254711 100644 --- a/src/types/emr/medicationAdministration/medicationAdministration.ts +++ b/src/types/emr/medicationAdministration/medicationAdministration.ts @@ -11,7 +11,6 @@ export const MEDICATION_ADMINISTRATION_STATUS = [ "stopped", "in_progress", "on_hold", - "unknown", "cancelled", ] as const; @@ -93,4 +92,6 @@ export interface MedicationAdministrationRead { dose?: DosageQuantity; rate?: Quantity; }; + created_by: UserBareMinimum; + updated_by: UserBareMinimum; } diff --git a/src/types/emr/medicationAdministration/medicationAdministrationApi.ts b/src/types/emr/medicationAdministration/medicationAdministrationApi.ts index 77e37c391e2..35b980b0349 100644 --- a/src/types/emr/medicationAdministration/medicationAdministrationApi.ts +++ b/src/types/emr/medicationAdministration/medicationAdministrationApi.ts @@ -7,12 +7,12 @@ import { } from "./medicationAdministration"; export default { - listMedicationAdministrations: { + list: { path: "/api/v1/patient/{patientId}/medication/administration/", method: HttpMethod.GET, TRes: Type>(), }, - upsertMedicationAdministration: { + upsert: { path: "/api/v1/patient/{patientId}/medication/administration/upsert/", method: HttpMethod.POST, TRes: Type, diff --git a/src/types/emr/medicationStatement.ts b/src/types/emr/medicationStatement.ts index a357ddab63c..00e20ae150a 100644 --- a/src/types/emr/medicationStatement.ts +++ b/src/types/emr/medicationStatement.ts @@ -23,6 +23,17 @@ export const MEDICATION_STATEMENT_STATUS = [ export type MedicationStatementStatus = (typeof MEDICATION_STATEMENT_STATUS)[number]; +export const MEDICATION_STATEMENT_STATUS_STYLES = { + active: "bg-green-100 text-green-800 border-green-200", + completed: "bg-blue-100 text-blue-800 border-blue-200", + stopped: "bg-red-100 text-red-800 border-red-200", + on_hold: "bg-yellow-100 text-yellow-800 border-yellow-200", + intended: "bg-purple-100 text-purple-800 border-purple-200", + not_taken: "bg-gray-100 text-gray-800 border-gray-200", + unknown: "bg-gray-100 text-gray-800 border-gray-200", + entered_in_error: "bg-red-100 text-red-800 border-red-200", +} as const; + export type MedicationStatement = { readonly id: string; status: MedicationStatementStatus; diff --git a/src/types/emr/medicationStatement/medicationStatementApi.ts b/src/types/emr/medicationStatement/medicationStatementApi.ts index 68736e4ab13..7f65dc5f3c8 100644 --- a/src/types/emr/medicationStatement/medicationStatementApi.ts +++ b/src/types/emr/medicationStatement/medicationStatementApi.ts @@ -1,12 +1,12 @@ import { Type } from "@/Utils/request/api"; import { PaginatedResponse } from "@/Utils/request/types"; -import { MedicationStatement } from "@/types/emr/medicationStatement"; +import { MedicationStatementRead } from "@/types/emr/medicationStatement"; const medicationStatementApi = { list: { path: "/api/v1/patient/{patientId}/medication/statement/", method: "GET", - TRes: Type>(), + TRes: Type>(), }, } as const; diff --git a/src/types/location/location.ts b/src/types/location/location.ts index cfb16dd13c3..3738dce63f7 100644 --- a/src/types/location/location.ts +++ b/src/types/location/location.ts @@ -45,6 +45,7 @@ export interface LocationDetail extends LocationBase { export interface LocationList extends LocationBase { id: string; has_children: boolean; + parent?: LocationList; } export interface LocationWrite extends LocationBase { diff --git a/src/types/questionnaire/question.ts b/src/types/questionnaire/question.ts index cc16b086bc7..f0199d63890 100644 --- a/src/types/questionnaire/question.ts +++ b/src/types/questionnaire/question.ts @@ -16,6 +16,23 @@ export type QuestionType = | "quantity" | "structured"; +export const SUPPORTED_QUESTION_TYPES = [ + { name: "Group", value: "group" }, + { name: "Display", value: "display" }, + { name: "Boolean", value: "boolean" }, + { name: "Decimal", value: "decimal" }, + { name: "Integer", value: "integer" }, + { name: "Date", value: "date" }, + { name: "DateTime", value: "dateTime" }, + { name: "Time", value: "time" }, + { name: "String", value: "string" }, + { name: "Text", value: "text" }, + { name: "URL", value: "url" }, + { name: "Choice", value: "choice" }, + { name: "Quantity", value: "quantity" }, + { name: "Structured", value: "structured" }, +]; + export type StructuredQuestionType = | "allergy_intolerance" | "medication_request" @@ -49,6 +66,13 @@ export interface AnswerOption { value: string; display?: string; initialSelected?: boolean; + code?: Code; +} + +export interface ObservationType { + system: string; + code: string; + display: string; } export interface Question { diff --git a/src/types/resourceRequest/resourceRequest.ts b/src/types/resourceRequest/resourceRequest.ts index 7594766017c..07bfdbd2479 100644 --- a/src/types/resourceRequest/resourceRequest.ts +++ b/src/types/resourceRequest/resourceRequest.ts @@ -2,6 +2,7 @@ import { FacilityModel } from "@/components/Facility/models"; import { UserBareMinimum } from "@/components/Users/models"; import { PatientModel } from "@/types/emr/patient"; +import { UserBase } from "@/types/user/user"; export interface ResourceRequest { approving_facility: FacilityModel | null; @@ -17,9 +18,9 @@ export interface ResourceRequest { requested_quantity: number; status: string; title: string; - assigned_to: UserBareMinimum | null; - created_by: UserBareMinimum; - updated_by: UserBareMinimum; + assigned_to: UserBase | null; + created_by: UserBase; + updated_by: UserBase; created_date: string; modified_date: string; related_patient: PatientModel | null; @@ -32,6 +33,7 @@ export interface CreateResourceRequest { referring_facility_contact_name: string; referring_facility_contact_number: string; approving_facility: string | null; + assigned_to: string | null; assigned_facility: string | null; origin_facility: string; related_patient: string; diff --git a/src/types/scheduling/scheduleApis.ts b/src/types/scheduling/scheduleApi.ts similarity index 100% rename from src/types/scheduling/scheduleApis.ts rename to src/types/scheduling/scheduleApi.ts diff --git a/src/types/user/userApi.ts b/src/types/user/userApi.ts index f1f776cece0..dd11f7e2ae2 100644 --- a/src/types/user/userApi.ts +++ b/src/types/user/userApi.ts @@ -30,4 +30,4 @@ export default { TRes: Type(), TBody: Type>(), }, -}; +} as const; diff --git a/src/types/valueset/valueset.ts b/src/types/valueset/valueset.ts new file mode 100644 index 00000000000..01ad2e9f8c1 --- /dev/null +++ b/src/types/valueset/valueset.ts @@ -0,0 +1,70 @@ +export interface ValuesetFilter { + op: string; + value: string; + property: string; +} + +export interface ValuesetConcept { + code: string; + display: string; +} + +export interface ValuesetInclude { + filter?: ValuesetFilter[]; + system: string; + concept?: ValuesetConcept[]; +} + +interface ValuesetCompose { + exclude: ValuesetInclude[]; + include: ValuesetInclude[]; +} + +export interface ValuesetBase { + id: string; + slug: string; + name: string; + description: string; + compose: ValuesetCompose; + status: "active" | "inactive"; + is_system_defined: boolean; + created_by: string | null; + updated_by: string | null; +} + +export type CreateValuesetModel = Omit< + ValuesetBase, + "id" | "created_by" | "updated_by" +>; + +export type UpdateValuesetModel = CreateValuesetModel & { + id: string; +}; + +export type ValuesetFormType = CreateValuesetModel; + +export interface ValuesetCodeMetadata { + code: string; + display: string; + name: string; + system: string; + version: string; + inactive: boolean; +} + +export interface ValuesetLookupResponse { + metadata: ValuesetCodeMetadata; +} + +export interface ValuesetLookupRequest { + system: string; + code: string; +} + +export const TERMINOLOGY_SYSTEMS = { + LOINC: "http://loinc.org", + SNOMED: "http://snomed.info/sct", + UCUM: "http://unitsofmeasure.org", +} as const; + +export type TerminologySystem = keyof typeof TERMINOLOGY_SYSTEMS; diff --git a/src/types/valueset/valuesetApi.ts b/src/types/valueset/valuesetApi.ts new file mode 100644 index 00000000000..3398cd18432 --- /dev/null +++ b/src/types/valueset/valuesetApi.ts @@ -0,0 +1,49 @@ +import { HttpMethod, Type } from "@/Utils/request/api"; +import { PaginatedResponse } from "@/Utils/request/types"; + +import { + CreateValuesetModel, + UpdateValuesetModel, + ValuesetBase, + ValuesetLookupRequest, + ValuesetLookupResponse, +} from "./valueset"; + +export default { + list: { + path: "/api/v1/valueset/", + method: HttpMethod.GET, + TRes: Type>(), + }, + create: { + path: "/api/v1/valueset/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, + get: { + path: "/api/v1/valueset/{slug}/", + method: HttpMethod.GET, + TRes: Type(), + }, + update: { + path: "/api/v1/valueset/{slug}/", + method: HttpMethod.PUT, + TRes: Type(), + TBody: Type(), + }, + lookup: { + path: "/api/v1/valueset/lookup_code/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, + expand: { + path: "/api/v1/valueset/expand/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type<{ + search: string; + }>(), + }, +} as const; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index defba258b80..5e2066155a8 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -19,11 +19,7 @@ interface ImportMetaEnv { readonly REACT_CUSTOM_LOGO_ALT?: string; readonly REACT_CUSTOM_DESCRIPTION?: string; readonly REACT_GMAPS_API_KEY?: string; - readonly REACT_GOV_DATA_API_KEY?: string; readonly REACT_RECAPTCHA_SITE_KEY?: string; - readonly REACT_WARTIME_SHIFTING?: string; - readonly REACT_STILL_WATCHING_IDLE_TIMEOUT?: string; - readonly REACT_STILL_WATCHING_PROMPT_DURATION?: string; readonly REACT_JWT_TOKEN_REFRESH_INTERVAL?: string; readonly REACT_MIN_ENCOUNTER_DATE?: string; readonly REACT_DEFAULT_ENCOUNTER_TYPE?: string; diff --git a/vite.config.mts b/vite.config.mts index 749f188002c..308c942b7ad 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -288,6 +288,7 @@ export default defineConfig(({ mode }) => { server: { port: 4000, host: "0.0.0.0", + allowedHosts: true, }, preview: { headers: {
{message.created_by.username}
+ {t("participants")}:{" "} + {new Set(messages.map((m) => m.created_by.id)).size} +
+ {t("messages")}: {totalMessages} +
- {org.description} -
+ {org.description} +