From effeaf930ee357861cb0112c7fcda20393ad54cc Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 24 Sep 2024 11:34:53 +0530 Subject: [PATCH 01/13] update caniuse-lite (#8599) --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 553224ffc27..99f4f706f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6121,9 +6121,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001603", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001603.tgz", - "integrity": "sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==", + "version": "1.0.30001663", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", + "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", "funding": [ { "type": "opencollective", @@ -19228,4 +19228,4 @@ } } } -} \ No newline at end of file +} From 214b373d6c5d804ef6e0994771afb745576b7f2b Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Tue, 24 Sep 2024 08:09:39 +0200 Subject: [PATCH 02/13] Enhance the multi-select component to immediately open onFocus (#8598) --------- Co-authored-by: Khavin Shankar Co-authored-by: rithviknishad --- .../e2e/facility_spec/FacilityHomepage.cy.ts | 21 +++++++------------ .../e2e/patient_spec/PatientLogUpdate.cy.ts | 8 +++---- cypress/pageobject/Facility/FacilityHome.ts | 5 ++++- src/Components/Form/AutoCompleteAsync.tsx | 1 + .../FormFields/AutocompleteMultiselect.tsx | 1 + 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cypress/e2e/facility_spec/FacilityHomepage.cy.ts b/cypress/e2e/facility_spec/FacilityHomepage.cy.ts index 11b3ec6f37f..e2a12b01a05 100644 --- a/cypress/e2e/facility_spec/FacilityHomepage.cy.ts +++ b/cypress/e2e/facility_spec/FacilityHomepage.cy.ts @@ -15,7 +15,6 @@ describe("Facility Homepage Function", () => { const userPage = new UserPage(); const assetPagination = new AssetPagination(); const facilitiesAlias = "downloadFacilitiesCSV"; - const capacitiesAlias = "downloadCapacitiesCSV"; const doctorsAlias = "downloadDoctorsCSV"; const triagesAlias = "downloadTriagesCSV"; const facilityName = "Dummy Facility 40"; @@ -91,30 +90,26 @@ describe("Facility Homepage Function", () => { }); it("Verify Facility Export Functionality", () => { - // Download the Facilities CSV + // Verify Facility Export facilityHome.csvDownloadIntercept(facilitiesAlias, ""); facilityHome.clickExportButton(); facilityHome.clickMenuItem("Facilities"); facilityHome.verifyDownload(facilitiesAlias); - facilityHome.clickSearchButton(); // to avoid flaky test, as sometimes, the test is unable to focus on the object - // Download the Capacities CSV - facilityHome.csvDownloadIntercept(capacitiesAlias, "&capacity"); - facilityHome.clickExportButton(); - facilityHome.clickMenuItem("Capacities"); - facilityHome.verifyDownload(capacitiesAlias); - facilityHome.clickSearchButton(); - // Download the Doctors CSV + // Verify Doctor Export facilityHome.csvDownloadIntercept(doctorsAlias, "&doctors"); facilityHome.clickExportButton(); facilityHome.clickMenuItem("Doctors"); facilityHome.verifyDownload(doctorsAlias); - facilityHome.clickSearchButton(); - // Download the Triages CSV + // Verify Triage Export facilityHome.csvDownloadIntercept(triagesAlias, "&triage"); facilityHome.clickExportButton(); facilityHome.clickMenuItem("Triages"); facilityHome.verifyDownload(triagesAlias); - facilityHome.clickSearchButton(); + }); + + it("Verify Capacity Export Functionality", () => { + facilityHome.clickExportButton(); + facilityHome.clickMenuItem("Capacities"); }); it("Verify Facility Detail page redirection to CNS and Live Minitoring ", () => { diff --git a/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts index 923d9410657..99d4170fde5 100644 --- a/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts +++ b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts @@ -17,10 +17,10 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { const additionalSymptoms = "Fever"; const physicalExamination = "physical examination details"; const otherExamination = "Other"; - const patientSystolic = "119"; - const patientDiastolic = "150"; - const patientModifiedSystolic = "120"; - const patientModifiedDiastolic = "145"; + const patientSystolic = "149"; + const patientDiastolic = "119"; + const patientModifiedSystolic = "145"; + const patientModifiedDiastolic = "120"; const patientPulse = "152"; const patientTemperature = "96.6"; const patientRespiratory = "140"; diff --git a/cypress/pageobject/Facility/FacilityHome.ts b/cypress/pageobject/Facility/FacilityHome.ts index b10368717a6..956c374ada4 100644 --- a/cypress/pageobject/Facility/FacilityHome.ts +++ b/cypress/pageobject/Facility/FacilityHome.ts @@ -6,6 +6,7 @@ class FacilityHome { // Operations clickExportButton() { + cy.get(this.exportButton).scrollIntoView(); cy.get(this.exportButton).click(); } @@ -89,7 +90,9 @@ class FacilityHome { } verifyDownload(alias: string) { - cy.wait(`@${alias}`).its("response.statusCode").should("eq", 200); + cy.wait(`@${alias}`, { timeout: 60000 }) + .its("response.statusCode") + .should("eq", 200); } getURL() { diff --git a/src/Components/Form/AutoCompleteAsync.tsx b/src/Components/Form/AutoCompleteAsync.tsx index 1cd07a93df1..18bffb0e11c 100644 --- a/src/Components/Form/AutoCompleteAsync.tsx +++ b/src/Components/Form/AutoCompleteAsync.tsx @@ -93,6 +93,7 @@ const AutoCompleteAsync = (props: Props) => { onChange={onChange} by={compareBy} multiple={multiple as any} + immediate >
diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx index a727db8b749..d8bde2dba03 100644 --- a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx +++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx @@ -110,6 +110,7 @@ export const AutocompleteMutliSelect = ( return (
Date: Tue, 24 Sep 2024 15:18:17 +0530 Subject: [PATCH 03/13] Show IV/NIV Badge only for active consultations (#8603) Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> --- .../e2e/patient_spec/PatientLogUpdate.cy.ts | 224 +++++++++--------- package-lock.json | 19 +- package.json | 3 +- src/Components/Patient/ManagePatients.tsx | 3 +- 4 files changed, 134 insertions(+), 115 deletions(-) diff --git a/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts index 99d4170fde5..7faaeed5a9f 100644 --- a/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts +++ b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts @@ -12,7 +12,6 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { const patientLogupdate = new PatientLogupdate(); const patientInvestigation = new PatientInvestigation(); const patientPrescription = new PatientPrescription(); - const domicilaryPatient = "Dummy Patient 11"; const patientCategory = "Moderate"; const additionalSymptoms = "Fever"; const physicalExamination = "physical examination details"; @@ -33,6 +32,13 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { const patientInsulinDosage = "56"; const patientFluidBalance = "500"; const patientNetBalance = "1000"; + const patientOne = "Dummy Patient 9"; + const bedOne = "Dummy Bed 5"; + const patientTwo = "Dummy Patient 10"; + const bedTwo = "Dummy Bed 2"; + const patientThree = "Dummy Patient 8"; + const bedThree = "Dummy Bed 3"; + const domicilaryPatient = "Dummy Patient 11"; before(() => { loginPage.loginAsDisctrictAdmin(); @@ -45,15 +51,110 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { cy.awaitUrl("/patients"); }); - it("Create a basic critical care log update for a admitted patient and edit it", () => { - patientPage.visitPatient("Dummy Patient 10"); + it("Create a new TeleIcu log update for a domicilary care patient", () => { + patientPage.visitPatient(domicilaryPatient); + patientConsultationPage.clickEditConsultationButton(); + patientConsultationPage.selectPatientSuggestion("Domiciliary Care"); + cy.submitButton("Update Consultation"); + cy.verifyNotification("Consultation updated successfully"); + cy.closeNotification(); + patientLogupdate.clickLogupdate(); + patientLogupdate.typePhysicalExamination(physicalExamination); + patientLogupdate.selectRoundType("Tele-medicine Log"); + patientLogupdate.selectPatientCategory(patientCategory); + patientLogupdate.typeOtherDetails(otherExamination); + patientLogupdate.selectSymptomsDate("01012024"); + patientLogupdate.typeAndMultiSelectSymptoms("fe", ["Fever"]); + patientLogupdate.typeSystolic(patientSystolic); + patientLogupdate.typeDiastolic(patientDiastolic); + patientLogupdate.typePulse(patientPulse); + patientLogupdate.typeTemperature(patientTemperature); + patientLogupdate.typeRespiratory(patientRespiratory); + patientLogupdate.typeSpo2(patientSpo2); + patientLogupdate.selectRhythm(patientRhythmType); + patientLogupdate.typeRhythm(patientRhythm); + cy.get("#consciousness_level-option-RESPONDS_TO_PAIN").click(); + cy.submitButton("Save"); + cy.verifyNotification("Tele-medicine Log created successfully"); + }); + + it("Create a new Progress log update for a admitted patient and edit it", () => { + patientPage.visitPatient(patientOne); patientLogupdate.clickLogupdate(); cy.verifyNotification("Please assign a bed to the patient"); - patientLogupdate.selectBed("Dummy Bed 2"); + patientLogupdate.selectBed(bedOne); cy.closeNotification(); patientLogupdate.clickLogupdate(); + // Only will be using random non-unique progress note fields + patientLogupdate.selectRoundType("Progress Note"); patientLogupdate.selectPatientCategory(patientCategory); + patientLogupdate.selectSymptomsDate("01012024"); + patientLogupdate.typeAndMultiSelectSymptoms("fe", ["Fever"]); + patientLogupdate.typeTemperature(patientTemperature); + // add diagnosis + patientConsultationPage.selectPatientDiagnosis( + "1A06", + "add-icd11-diagnosis-as-differential", + ); + // add a investigation for the patient + patientInvestigation.clickAddInvestigation(); + patientInvestigation.selectInvestigation("Vitals (GROUP)"); + patientInvestigation.clickInvestigationCheckbox(); + patientInvestigation.selectInvestigationFrequency("6"); + // add a medicine for the patient + patientPrescription.clickAddPrescription(); + patientPrescription.interceptMedibase(); + patientPrescription.selectMedicinebox(); + patientPrescription.selectMedicine("DOLO"); + patientPrescription.enterDosage("4"); + patientPrescription.selectDosageFrequency("Twice daily"); + cy.submitButton("Submit"); + cy.verifyNotification("Medicine prescribed"); + cy.closeNotification(); + // Submit the doctors log update + cy.submitButton("Save and Continue"); + cy.verifyNotification("Progress Note created successfully"); + cy.closeNotification(); + // modify the relevant critical care log update + patientLogupdate.selectCriticalCareSection("Neurological Monitoring"); + cy.get("#consciousness_level-option-RESPONDS_TO_PAIN").click(); + cy.get("#left_pupil_light_reaction-option-FIXED").click(); + cy.submitButton("Update Details"); + cy.verifyNotification( + "Neurological Monitoring details succesfully updated.", + ); + cy.closeNotification(); + // Final Submission of the form + cy.submitButton("Complete"); + cy.verifyNotification("Progress Note Log Update filed successfully"); + cy.closeNotification(); + // Verify the data reflection + cy.contains("button", "Daily Rounds").click(); + patientLogupdate.clickLogUpdateViewDetails( + "#dailyround-entry", + patientCategory, + ); + cy.verifyContentPresence("#consultation-preview", [ + patientCategory, + patientTemperature, + ]); + // verify the edit functionality + patientLogupdate.clickUpdateDetail(); + patientLogupdate.typeSystolic(patientModifiedSystolic); + patientLogupdate.typeDiastolic(patientModifiedDiastolic); + cy.submitButton("Continue"); + cy.verifyNotification("Progress Note updated successfully"); + }); + + it("Create a basic critical care log update for a admitted patient and edit it", () => { + patientPage.visitPatient(patientTwo); + patientLogupdate.clickLogupdate(); + cy.verifyNotification("Please assign a bed to the patient"); + patientLogupdate.selectBed(bedTwo); + cy.closeNotification(); + patientLogupdate.clickLogupdate(); patientLogupdate.selectRoundType("Detailed Update"); + patientLogupdate.selectPatientCategory(patientCategory); cy.submitButton("Save and Continue"); cy.verifyNotification("Detailed Update created successfully"); cy.closeNotification(); @@ -127,88 +228,19 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { ]); }); - it("Create a new Progress log update for a admitted patient and edit it", () => { - patientPage.visitPatient("Dummy Patient 12"); + it("Create a new Normal update for a admission patient and verify its reflection in cards", () => { + patientPage.visitPatient(patientThree); patientLogupdate.clickLogupdate(); cy.verifyNotification("Please assign a bed to the patient"); - patientLogupdate.selectBed("Dummy Bed 4"); - cy.closeNotification(); - patientLogupdate.clickLogupdate(); - // Only will be using random non-unique progress note fields - patientLogupdate.selectPatientCategory(patientCategory); - patientLogupdate.selectRoundType("Progress Note"); - patientLogupdate.selectSymptomsDate("01012024"); - patientLogupdate.typeAndMultiSelectSymptoms("fe", ["Fever"]); - patientLogupdate.typeTemperature(patientTemperature); - // add diagnosis - patientConsultationPage.selectPatientDiagnosis( - "1A06", - "add-icd11-diagnosis-as-differential", - ); - // add a investigation for the patient - patientInvestigation.clickAddInvestigation(); - patientInvestigation.selectInvestigation("Vitals (GROUP)"); - patientInvestigation.clickInvestigationCheckbox(); - patientInvestigation.selectInvestigationFrequency("6"); - // add a medicine for the patient - patientPrescription.clickAddPrescription(); - patientPrescription.interceptMedibase(); - patientPrescription.selectMedicinebox(); - patientPrescription.selectMedicine("DOLO"); - patientPrescription.enterDosage("4"); - patientPrescription.selectDosageFrequency("Twice daily"); - cy.submitButton("Submit"); - cy.verifyNotification("Medicine prescribed"); - cy.closeNotification(); - // Submit the doctors log update - cy.submitButton("Save and Continue"); - cy.verifyNotification("Progress Note created successfully"); - cy.closeNotification(); - // modify the relevant critical care log update - patientLogupdate.selectCriticalCareSection("Neurological Monitoring"); - cy.get("#consciousness_level-option-RESPONDS_TO_PAIN").click(); - cy.get("#left_pupil_light_reaction-option-FIXED").click(); - cy.submitButton("Update Details"); - cy.verifyNotification( - "Neurological Monitoring details succesfully updated.", - ); - cy.closeNotification(); - // Final Submission of the form - cy.submitButton("Complete"); - cy.verifyNotification("Progress Note Log Update filed successfully"); - cy.closeNotification(); - // Verify the data reflection - cy.contains("button", "Daily Rounds").click(); - patientLogupdate.clickLogUpdateViewDetails( - "#dailyround-entry", - patientCategory, - ); - cy.verifyContentPresence("#consultation-preview", [ - patientCategory, - patientTemperature, - ]); - // verify the edit functionality - patientLogupdate.clickUpdateDetail(); - patientLogupdate.typeSystolic(patientModifiedSystolic); - patientLogupdate.typeDiastolic(patientModifiedDiastolic); - cy.submitButton("Continue"); - cy.verifyNotification("Progress Note updated successfully"); - }); - - it("Create a new TeleIcu log update for a domicilary care patient", () => { - patientPage.visitPatient("Dummy Patient 11"); - patientConsultationPage.clickEditConsultationButton(); - patientConsultationPage.selectPatientSuggestion("Domiciliary Care"); - cy.submitButton("Update Consultation"); - cy.verifyNotification("Consultation updated successfully"); + patientLogupdate.selectBed(bedThree); cy.closeNotification(); patientLogupdate.clickLogupdate(); patientLogupdate.typePhysicalExamination(physicalExamination); - patientLogupdate.selectRoundType("Tele-medicine Log"); + patientLogupdate.selectPatientCategory(patientCategory); patientLogupdate.typeOtherDetails(otherExamination); patientLogupdate.selectSymptomsDate("01012024"); patientLogupdate.typeAndMultiSelectSymptoms("fe", ["Fever"]); - patientLogupdate.selectPatientCategory(patientCategory); + patientLogupdate.clickAddSymptom(); patientLogupdate.typeSystolic(patientSystolic); patientLogupdate.typeDiastolic(patientDiastolic); patientLogupdate.typePulse(patientPulse); @@ -219,7 +251,11 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeRhythm(patientRhythm); cy.get("#consciousness_level-option-RESPONDS_TO_PAIN").click(); cy.submitButton("Save"); - cy.verifyNotification("Tele-medicine Log created successfully"); + cy.wait(2000); + cy.verifyNotification("Brief Update created successfully"); + // Verify the card content + cy.get("#basic-information").scrollIntoView(); + cy.verifyContentPresence("#encounter-symptoms", [additionalSymptoms]); }); it("Create a new Normal Log update for a domicilary care patient and edit it", () => { @@ -283,36 +319,6 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { ]); }); - it("Create a new Normal update for a admission patient and verify its reflection in cards", () => { - patientPage.visitPatient("Dummy Patient 13"); - patientLogupdate.clickLogupdate(); - cy.verifyNotification("Please assign a bed to the patient"); - patientLogupdate.selectBed("Dummy Bed 6"); - cy.closeNotification(); - patientLogupdate.clickLogupdate(); - patientLogupdate.typePhysicalExamination(physicalExamination); - patientLogupdate.typeOtherDetails(otherExamination); - patientLogupdate.selectSymptomsDate("01012024"); - patientLogupdate.typeAndMultiSelectSymptoms("fe", ["Fever"]); - patientLogupdate.clickAddSymptom(); - patientLogupdate.selectPatientCategory(patientCategory); - patientLogupdate.typeSystolic(patientSystolic); - patientLogupdate.typeDiastolic(patientDiastolic); - patientLogupdate.typePulse(patientPulse); - patientLogupdate.typeTemperature(patientTemperature); - patientLogupdate.typeRespiratory(patientRespiratory); - patientLogupdate.typeSpo2(patientSpo2); - patientLogupdate.selectRhythm(patientRhythmType); - patientLogupdate.typeRhythm(patientRhythm); - cy.get("#consciousness_level-option-RESPONDS_TO_PAIN").click(); - cy.submitButton("Save"); - cy.wait(2000); - cy.verifyNotification("Brief Update created successfully"); - // Verify the card content - cy.get("#basic-information").scrollIntoView(); - cy.verifyContentPresence("#encounter-symptoms", [additionalSymptoms]); - }); - it("Create a Normal Log update to verify MEWS Score Functionality", () => { patientPage.visitPatient(domicilaryPatient); patientConsultationPage.clickEditConsultationButton(); diff --git a/package-lock.json b/package-lock.json index 99f4f706f58..051c12bd2b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", + "@types/cypress": "^1.1.3", "@types/events": "^3.0.3", "@types/google.maps": "^3.55.8", "@types/lodash-es": "^4.17.12", @@ -68,7 +69,7 @@ "@typescript-eslint/eslint-plugin": "^7.18.0", "@vitejs/plugin-react-swc": "^3.6.0", "autoprefixer": "^10.4.19", - "cypress": "^13.14.1", + "cypress": "^13.14.2", "cypress-localstorage-commands": "^2.2.5", "cypress-split": "^1.23.2", "eslint-config-prettier": "^9.1.0", @@ -4425,6 +4426,16 @@ "@types/node": "*" } }, + "node_modules/@types/cypress": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/cypress/-/cypress-1.1.3.tgz", + "integrity": "sha512-OXe0Gw8LeCflkG1oPgFpyrYWJmEKqYncBsD/J0r17r0ETx/TnIGDNLwXt/pFYSYuYTpzcq1q3g62M9DrfsBL4g==", + "deprecated": "This is a stub types definition for cypress (https://cypress.io). cypress provides its own type definitions, so you don't need @types/cypress installed!", + "dev": true, + "dependencies": { + "cypress": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -6629,9 +6640,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cypress": { - "version": "13.14.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.1.tgz", - "integrity": "sha512-Wo+byPmjps66hACEH5udhXINEiN3qS3jWNGRzJOjrRJF3D0+YrcP2LVB1T7oYaVQM/S+eanqEvBWYc8cf7Vcbg==", + "version": "13.14.2", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.2.tgz", + "integrity": "sha512-lsiQrN17vHMB2fnvxIrKLAjOr9bPwsNbPZNrWf99s4u+DVmCY6U+w7O3GGG9FvP4EUVYaDu+guWeNLiUzBrqvA==", "dev": true, "hasInstallScript": true, "dependencies": { diff --git a/package.json b/package.json index cc2c7b955c2..62cdc8a0fb9 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", + "@types/cypress": "^1.1.3", "@types/events": "^3.0.3", "@types/google.maps": "^3.55.8", "@types/lodash-es": "^4.17.12", @@ -103,7 +104,7 @@ "@typescript-eslint/eslint-plugin": "^7.18.0", "@vitejs/plugin-react-swc": "^3.6.0", "autoprefixer": "^10.4.19", - "cypress": "^13.14.1", + "cypress": "^13.14.2", "cypress-localstorage-commands": "^2.2.5", "cypress-split": "^1.23.2", "eslint-config-prettier": "^9.1.0", diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 944a5163c96..7a5b0d28a38 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -721,7 +721,8 @@ export const PatientManager = () => { {patient.last_consultation?.last_daily_round ?.ventilator_interface && patient.last_consultation?.last_daily_round - ?.ventilator_interface !== "UNKNOWN" && ( + ?.ventilator_interface !== "UNKNOWN" && + !patient.last_consultation?.discharge_date && (
{ RESPIRATORY_SUPPORT.find( From 5accddc9be467b72a58dc91dda54b53da4da3d72 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 24 Sep 2024 15:54:44 +0530 Subject: [PATCH 04/13] fix i18n for sort options (#8602) --- src/Common/hooks/useFilters.tsx | 2 +- src/Components/Common/SortDropdown.tsx | 2 +- .../Facility/CentralNursingStation.tsx | 2 +- src/Locale/en/Common.json | 20 ++++++++++++-- src/Locale/en/SortOptions.json | 18 ------------- src/Locale/en/index.js | 2 -- src/Locale/hi/Common.json | 18 ++++++++++++- src/Locale/hi/SortOptions.json | 18 ------------- src/Locale/kn/Common.json | 18 ++++++++++++- src/Locale/kn/SortOptions.json | 18 ------------- src/Locale/kn/index.js | 26 +++++++++++++++++++ src/Locale/ml/Common.json | 18 ++++++++++++- src/Locale/ml/SortOptions.json | 18 ------------- src/Locale/ta/Common.json | 18 ++++++++++++- src/Locale/ta/SortOptions.json | 18 ------------- 15 files changed, 115 insertions(+), 101 deletions(-) delete mode 100644 src/Locale/en/SortOptions.json delete mode 100644 src/Locale/hi/SortOptions.json delete mode 100644 src/Locale/kn/SortOptions.json delete mode 100644 src/Locale/ml/SortOptions.json delete mode 100644 src/Locale/ta/SortOptions.json diff --git a/src/Common/hooks/useFilters.tsx b/src/Common/hooks/useFilters.tsx index a4b924edcdb..188dec2229d 100644 --- a/src/Common/hooks/useFilters.tsx +++ b/src/Common/hooks/useFilters.tsx @@ -109,7 +109,7 @@ export default function useFilters({ return { name, paramKey, - value: qParams[paramKey] && t("SortOptions." + qParams[paramKey]), + value: qParams[paramKey] && t("SORT_OPTIONS__" + qParams[paramKey]), }; }, value(name: string, paramKey: FilterBadgeProps["paramKey"], value: string) { diff --git a/src/Components/Common/SortDropdown.tsx b/src/Components/Common/SortDropdown.tsx index 8ffd09ba269..b54edffa595 100644 --- a/src/Components/Common/SortDropdown.tsx +++ b/src/Components/Common/SortDropdown.tsx @@ -41,7 +41,7 @@ export default function SortDropdownMenu(props: Props) { /> } > - {t("SortOptions." + value)} + {t("SORT_OPTIONS__" + value)} ))} diff --git a/src/Components/Facility/CentralNursingStation.tsx b/src/Components/Facility/CentralNursingStation.tsx index 178bd2bc25c..fbbdf24d7f2 100644 --- a/src/Components/Facility/CentralNursingStation.tsx +++ b/src/Components/Facility/CentralNursingStation.tsx @@ -145,7 +145,7 @@ export default function CentralNursingStation({ facilityId }: Props) { value={qParams.ordering || "bed__name"} onChange={({ value }) => updateQuery({ ordering: value })} options={SORT_OPTIONS} - optionLabel={({ value }) => t("SortOptions." + value)} + optionLabel={({ value }) => t("SORT_OPTIONS__" + value)} optionIcon={({ isAscending }) => ( Date: Tue, 24 Sep 2024 15:14:04 +0200 Subject: [PATCH 05/13] switch bed functionality test (#8613) --- .../patient_spec/PatientBedManagement.cy.ts | 56 +++++++++++++++++++ .../pageobject/Patient/PatientLogupdate.ts | 6 +- src/CAREUI/display/Timeline.tsx | 2 +- .../Facility/Consultations/Beds.tsx | 2 +- src/Components/Patient/PatientInfoCard.tsx | 1 + 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 cypress/e2e/patient_spec/PatientBedManagement.cy.ts diff --git a/cypress/e2e/patient_spec/PatientBedManagement.cy.ts b/cypress/e2e/patient_spec/PatientBedManagement.cy.ts new file mode 100644 index 00000000000..d9453806c9f --- /dev/null +++ b/cypress/e2e/patient_spec/PatientBedManagement.cy.ts @@ -0,0 +1,56 @@ +import LoginPage from "../../pageobject/Login/LoginPage"; +import { PatientPage } from "../../pageobject/Patient/PatientCreation"; +import PatientLogupdate from "../../pageobject/Patient/PatientLogupdate"; +import { PatientConsultationPage } from "../../pageobject/Patient/PatientConsultation"; + +describe("Patient swtich bed functionality", () => { + const loginPage = new LoginPage(); + const patientPage = new PatientPage(); + const patientLogupdate = new PatientLogupdate(); + const patientConsultationPage = new PatientConsultationPage(); + const switchBedOne = "Dummy Bed 4"; + const switchBedTwo = "Dummy Bed 1"; + const switchBedThree = "Dummy Bed 3"; + const switchPatientOne = "Dummy Patient 6"; + const switchPatientTwo = "Dummy Patient 7"; + + before(() => { + loginPage.loginAsDisctrictAdmin(); + cy.saveLocalStorage(); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); + cy.awaitUrl("/patients"); + }); + + it("Assign a bed for a admitted patient from update consultation page", () => { + // Open the update consultation page and assign a bed + patientPage.visitPatient(switchPatientTwo); + patientConsultationPage.clickEditConsultationButton(); + patientLogupdate.selectBed(switchBedThree); + // verify the notification + cy.verifyNotification("Bed allocated successfully"); + }); + + it("Assign a bed for a admitted patient from patient dashboard", () => { + // Assign a bed to a patient + patientPage.visitPatient(switchPatientOne); + patientLogupdate.clickSwitchBed(); + patientLogupdate.selectBed(switchBedOne); + cy.verifyNotification("Bed allocated successfully"); + // Clear the bed and reassign + patientLogupdate.clickSwitchBed(); + cy.get("#clear-button").click(); + patientLogupdate.selectBed(switchBedTwo); + cy.verifyNotification("Bed allocated successfully"); + // verify the card is reflected + patientLogupdate.clickSwitchBed(); + cy.verifyContentPresence("#previousbed-list", [switchBedOne, switchBedTwo]); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); +}); diff --git a/cypress/pageobject/Patient/PatientLogupdate.ts b/cypress/pageobject/Patient/PatientLogupdate.ts index 8a4c11ab25b..add3fbb0590 100644 --- a/cypress/pageobject/Patient/PatientLogupdate.ts +++ b/cypress/pageobject/Patient/PatientLogupdate.ts @@ -5,13 +5,17 @@ class PatientLogupdate { cy.wait(2000); } + clickSwitchBed() { + cy.get("#switch-bed").click(); + } + selectRoundType(roundType: string) { cy.clickAndSelectOption("#rounds_type", roundType); } selectBed(bed: string) { cy.searchAndSelectOption("input[name='bed']", bed); - cy.submitButton("Update"); + cy.get("#update-switchbed").click(); cy.wait(2000); } diff --git a/src/CAREUI/display/Timeline.tsx b/src/CAREUI/display/Timeline.tsx index 7fcb57f56ea..ee21b337724 100644 --- a/src/CAREUI/display/Timeline.tsx +++ b/src/CAREUI/display/Timeline.tsx @@ -26,7 +26,7 @@ const TimelineContext = createContext(""); export default function Timeline({ className, children, name }: TimelineProps) { return ( -
+
    {children} diff --git a/src/Components/Facility/Consultations/Beds.tsx b/src/Components/Facility/Consultations/Beds.tsx index 0333ec40f63..0b62af9fdcb 100644 --- a/src/Components/Facility/Consultations/Beds.tsx +++ b/src/Components/Facility/Consultations/Beds.tsx @@ -231,7 +231,7 @@ const Beds = (props: BedsProps) => {
- + Update diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 300c2190a60..1f581b9b0e7 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -230,6 +230,7 @@ export default function PatientInfoCard(props: { )} {consultation?.admitted && ( setOpen(true)} className="mt-1 px-[10px] py-1" From ea578313545013649da6a6bd3c0e609811c47eb3 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:12:50 +0200 Subject: [PATCH 06/13] patient pagination test (#8614) --- .../e2e/patient_spec/PatientHomepage.cy.ts | 42 +++++++++++++++++++ cypress/pageobject/Patient/PatientHome.ts | 14 +++++++ 2 files changed, 56 insertions(+) create mode 100644 cypress/e2e/patient_spec/PatientHomepage.cy.ts create mode 100644 cypress/pageobject/Patient/PatientHome.ts diff --git a/cypress/e2e/patient_spec/PatientHomepage.cy.ts b/cypress/e2e/patient_spec/PatientHomepage.cy.ts new file mode 100644 index 00000000000..c1575057fe4 --- /dev/null +++ b/cypress/e2e/patient_spec/PatientHomepage.cy.ts @@ -0,0 +1,42 @@ +import LoginPage from "../../pageobject/Login/LoginPage"; +import PatientHome from "../../pageobject/Patient/PatientHome"; + +describe("Patient Homepage present functionalities", () => { + const loginPage = new LoginPage(); + const patientHome = new PatientHome(); + + before(() => { + loginPage.loginAsDisctrictAdmin(); + cy.saveLocalStorage(); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); + cy.awaitUrl("/patients"); + }); + + it("Verify the functionality of the patient tab pagination", () => { + let firstPatientPageOne: string; + cy.get('[data-cy="patient"]') + .first() + .invoke("text") + .then((patientOne: string) => { + firstPatientPageOne = patientOne.trim(); + patientHome.clickNextPage(); + patientHome.verifySecondPageUrl(); + cy.get('[data-cy="patient"]') + .first() + .invoke("text") + .then((patientTwo: string) => { + const firstPatientPageTwo = patientTwo.trim(); + expect(firstPatientPageOne).not.to.eq(firstPatientPageTwo); + patientHome.clickPreviousPage(); + }); + }); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); +}); diff --git a/cypress/pageobject/Patient/PatientHome.ts b/cypress/pageobject/Patient/PatientHome.ts new file mode 100644 index 00000000000..94801cd4bb8 --- /dev/null +++ b/cypress/pageobject/Patient/PatientHome.ts @@ -0,0 +1,14 @@ +class PatientHome { + clickNextPage() { + cy.get("#next-pages").click(); + } + + verifySecondPageUrl() { + cy.url().should("include", "/patients?page=2"); + } + + clickPreviousPage() { + cy.get("#prev-pages").click(); + } +} +export default PatientHome; From a0f5c523f8248be6f7924dbfde7af5f308803c50 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Tue, 24 Sep 2024 20:13:10 +0530 Subject: [PATCH 07/13] enabled filter (#7933) Co-authored-by: Rithvik Nishad --- src/Components/Patient/PatientFilter.tsx | 40 +++++++++++------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/Components/Patient/PatientFilter.tsx b/src/Components/Patient/PatientFilter.tsx index 773a3f333b7..f496a59df6d 100644 --- a/src/Components/Patient/PatientFilter.tsx +++ b/src/Components/Patient/PatientFilter.tsx @@ -343,27 +343,25 @@ export default function PatientFilter(props: any) { />
- {props.dischargePage || ( -
- - {props.dischargePage && "Last "}Admitted to (Bed Types) - - o.id} - optionLabel={(o) => o.text} - onChange={(o) => - setFilterState({ - ...filterState, - last_consultation_admitted_bed_type_list: o, - }) - } - /> -
- )} +
+ + {props.dischargePage && "Last "}Admitted to (Bed Types) + + o.id} + optionLabel={(o) => o.text} + onChange={(o) => + setFilterState({ + ...filterState, + last_consultation_admitted_bed_type_list: o, + }) + } + /> +
); diff --git a/src/Components/Facility/DoctorNote.tsx b/src/Components/Facility/DoctorNote.tsx index 91c361cc82e..c2bf8155cf2 100644 --- a/src/Components/Facility/DoctorNote.tsx +++ b/src/Components/Facility/DoctorNote.tsx @@ -1,17 +1,19 @@ import InfiniteScroll from "react-infinite-scroll-component"; import CircularProgress from "../Common/components/CircularProgress"; import PatientNoteCard from "./PatientNoteCard"; -import { PatientNoteStateType } from "./models"; +import { PatientNoteStateType, PatientNotesModel } from "./models"; +import DoctorNoteReplyPreviewCard from "./DoctorNoteReplyPreviewCard"; interface DoctorNoteProps { state: PatientNoteStateType; setReload: any; handleNext: () => void; disableEdit?: boolean; + setReplyTo?: (reply_to: PatientNotesModel | undefined) => void; } const DoctorNote = (props: DoctorNoteProps) => { - const { state, handleNext, setReload, disableEdit } = props; + const { state, handleNext, setReload, disableEdit, setReplyTo } = props; return (
{ scrollableTarget="patient-notes-list" > {state.notes.map((note) => ( - + parentNote={note.reply_to_object} + > + + ))} ) : ( diff --git a/src/Components/Facility/DoctorNoteReplyPreviewCard.tsx b/src/Components/Facility/DoctorNoteReplyPreviewCard.tsx new file mode 100644 index 00000000000..470f05f2bfe --- /dev/null +++ b/src/Components/Facility/DoctorNoteReplyPreviewCard.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { PaitentNotesReplyModel } from "./models"; +import { USER_TYPES_MAP } from "../../Common/constants"; +import { formatDateTime, relativeDate } from "../../Utils/utils"; + +interface Props { + parentNote: PaitentNotesReplyModel | undefined; + children: React.ReactNode; + cancelReply?: () => void; +} + +const DoctorNoteReplyPreviewCard = ({ + parentNote, + children, + cancelReply, +}: Props) => { + return ( +
+ {parentNote ? ( +
+
+
+
+
+ + {parentNote.created_by_object?.first_name || "Unknown"}{" "} + {parentNote.created_by_object?.last_name} + + {parentNote.user_type && ( + + {`(${USER_TYPES_MAP[parentNote.user_type]})`} + + )} +
+
+
+ + {formatDateTime(parentNote.created_date)} + + Created {relativeDate(parentNote.created_date, true)} +
+
+
+ {cancelReply && ( +
+ Cancel +
+ )} +
+
{parentNote.note}
+
+
{children}
+
+ ) : ( +
{children}
+ )} +
+ ); +}; + +export default DoctorNoteReplyPreviewCard; diff --git a/src/Components/Facility/PatientConsultationNotesList.tsx b/src/Components/Facility/PatientConsultationNotesList.tsx index f81ef122f6c..15238ff189f 100644 --- a/src/Components/Facility/PatientConsultationNotesList.tsx +++ b/src/Components/Facility/PatientConsultationNotesList.tsx @@ -14,12 +14,21 @@ interface PatientNotesProps { setReload?: (value: boolean) => void; disableEdit?: boolean; thread: PatientNotesModel["thread"]; + setReplyTo?: (value: PatientNotesModel | undefined) => void; } const pageSize = RESULTS_PER_PAGE_LIMIT; const PatientConsultationNotesList = (props: PatientNotesProps) => { - const { state, setState, reload, setReload, disableEdit, thread } = props; + const { + state, + setState, + reload, + setReload, + disableEdit, + thread, + setReplyTo, + } = props; const consultationId = useSlug("consultation") ?? ""; const [isLoading, setIsLoading] = useState(true); @@ -95,6 +104,7 @@ const PatientConsultationNotesList = (props: PatientNotesProps) => { handleNext={handleNext} setReload={setReload} disableEdit={disableEdit} + setReplyTo={setReplyTo} /> ); }; diff --git a/src/Components/Facility/PatientNoteCard.tsx b/src/Components/Facility/PatientNoteCard.tsx index 7d2a8c6eb70..9ddfbc009e6 100644 --- a/src/Components/Facility/PatientNoteCard.tsx +++ b/src/Components/Facility/PatientNoteCard.tsx @@ -23,10 +23,12 @@ const PatientNoteCard = ({ note, setReload, disableEdit, + setReplyTo, }: { note: PatientNotesModel; setReload: any; disableEdit?: boolean; + setReplyTo?: (reply_to: PatientNotesModel | undefined) => void; }) => { const patientId = useSlug("patient"); const [isEditing, setIsEditing] = useState(false); @@ -128,19 +130,28 @@ const PatientNoteCard = ({ ) }
- - {!disableEdit && - note.created_by_object.id === authUser.id && - !isEditing && ( - { - setIsEditing(true); - }} - > - - - )} +
+ {!disableEdit && + note.created_by_object.id === authUser.id && + !isEditing && ( + { + setIsEditing(true); + }} + > + + + )} + { + setReplyTo && setReplyTo(note); + }} + > + + +
{
diff --git a/src/Components/Facility/PatientNotesList.tsx b/src/Components/Facility/PatientNotesList.tsx index 4e1d0a5a7a6..bbd037e866c 100644 --- a/src/Components/Facility/PatientNotesList.tsx +++ b/src/Components/Facility/PatientNotesList.tsx @@ -14,12 +14,13 @@ interface PatientNotesProps { reload?: boolean; setReload?: any; thread: PatientNotesModel["thread"]; + setReplyTo?: (reply_to: PatientNotesModel | undefined) => void; } const pageSize = RESULTS_PER_PAGE_LIMIT; const PatientNotesList = (props: PatientNotesProps) => { - const { state, setState, reload, setReload, thread } = props; + const { state, setState, reload, setReload, thread, setReplyTo } = props; const [isLoading, setIsLoading] = useState(true); @@ -83,7 +84,12 @@ const PatientNotesList = (props: PatientNotesProps) => { } return ( - + ); }; diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index d7847c3add7..52f99aee763 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -8,11 +8,12 @@ import { useMessageListener } from "../../Common/hooks/useMessageListener"; import PatientConsultationNotesList from "./PatientConsultationNotesList"; import request from "../../Utils/request/request"; import routes from "../../Redux/api"; -import { PatientNoteStateType } from "./models"; +import { PatientNoteStateType, PaitentNotesReplyModel } from "./models"; import useKeyboardShortcut from "use-keyboard-shortcut"; import AutoExpandingTextInputFormField from "../Form/FormFields/AutoExpandingTextInputFormField.js"; import useAuthUser from "../../Common/hooks/useAuthUser"; import { PATIENT_NOTES_THREADS } from "../../Common/constants.js"; +import DoctorNoteReplyPreviewCard from "./DoctorNoteReplyPreviewCard.js"; import useNotificationSubscriptionState from "../../Common/hooks/useNotificationSubscriptionState.js"; import { Link } from "raviger"; import { t } from "i18next"; @@ -36,6 +37,9 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { const [patientActive, setPatientActive] = useState(true); const [reload, setReload] = useState(false); const [focused, setFocused] = useState(false); + const [reply_to, setReplyTo] = useState( + undefined, + ); useEffect(() => { if (notificationSubscriptionState === "unsubscribed") { @@ -79,6 +83,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { note: noteField, consultation: consultationId, thread, + reply_to: reply_to?.id, }, }); if (res?.status === 201) { @@ -86,6 +91,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { setNoteField(""); setState({ ...state, cPage: 1 }); setReload(true); + setReplyTo(undefined); } }; @@ -216,36 +222,42 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { setReload={setReload} disableEdit={!patientActive} thread={thread} + setReplyTo={setReplyTo} /> -
- setNoteField(e.value)} - className="w-full grow" - errorClassName="hidden" - innerClassName="pr-10" - placeholder={t("notes_placeholder")} - disabled={!patientActive} - onFocus={() => setFocused(true)} - onBlur={() => setFocused(false)} - /> - - - -
+ setReplyTo(undefined)} + > +
+ setNoteField(e.value)} + className="w-full grow" + errorClassName="hidden" + innerClassName="pr-10" + placeholder={t("notes_placeholder")} + disabled={!patientActive} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} + /> + + + +
+
)}
diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index cdb78a5f712..7756bf73f69 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -546,6 +546,14 @@ export interface PatientNotesEditModel { note: string; } +export interface PaitentNotesReplyModel { + id: string; + note: string; + user_type?: UserRole | "RemoteSpecialist"; + created_by_object: BaseUserModel; + created_date: string; +} + export interface PatientNotesModel { id: string; note: string; @@ -556,6 +564,7 @@ export interface PatientNotesModel { created_date: string; last_edited_by?: BaseUserModel; last_edited_date?: string; + reply_to_object?: PaitentNotesReplyModel; } export interface PatientNoteStateType { diff --git a/src/Components/Patient/PatientNotes.tsx b/src/Components/Patient/PatientNotes.tsx index 7138f4df8e5..da97e5d3f4f 100644 --- a/src/Components/Patient/PatientNotes.tsx +++ b/src/Components/Patient/PatientNotes.tsx @@ -6,11 +6,12 @@ import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import PatientNotesList from "../Facility/PatientNotesList"; import Page from "../Common/components/Page"; import { useMessageListener } from "../../Common/hooks/useMessageListener"; -import { PatientNoteStateType } from "../Facility/models"; +import { PatientNoteStateType, PatientNotesModel } from "../Facility/models"; import request from "../../Utils/request/request"; import routes from "../../Redux/api"; import { PATIENT_NOTES_THREADS } from "../../Common/constants.js"; import useAuthUser from "../../Common/hooks/useAuthUser.js"; +import DoctorNoteReplyPreviewCard from "../Facility/DoctorNoteReplyPreviewCard.js"; import { classNames, keysOf } from "../../Utils/utils.js"; import AutoExpandingTextInputFormField from "../Form/FormFields/AutoExpandingTextInputFormField.js"; import { t } from "i18next"; @@ -35,6 +36,9 @@ const PatientNotes = (props: PatientNotesProps) => { const [reload, setReload] = useState(false); const [facilityName, setFacilityName] = useState(""); const [patientName, setPatientName] = useState(""); + const [reply_to, setReplyTo] = useState( + undefined, + ); const initialData: PatientNoteStateType = { notes: [], @@ -56,6 +60,7 @@ const PatientNotes = (props: PatientNotesProps) => { body: { note: noteField, thread, + reply_to: reply_to?.id, }, }); if (res?.status === 201) { @@ -63,6 +68,7 @@ const PatientNotes = (props: PatientNotesProps) => { setNoteField(""); setReload(!reload); setState({ ...state, cPage: 1 }); + setReplyTo(undefined); } }; @@ -130,33 +136,38 @@ const PatientNotes = (props: PatientNotesProps) => { reload={reload} setReload={setReload} thread={thread} + setReplyTo={setReplyTo} /> - -
- setNoteField(e.value)} - className="w-full grow" - errorClassName="hidden" - innerClassName="pr-10" - placeholder={t("notes_placeholder")} - disabled={!patientActive} - /> - - - -
+ setReplyTo(undefined)} + > +
+ setNoteField(e.value)} + className="w-full grow" + errorClassName="hidden" + innerClassName="pr-10" + placeholder={t("notes_placeholder")} + disabled={!patientActive} + /> + + + +
+
); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index e53342e3d7c..0243db57df7 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -774,7 +774,10 @@ const routes = { method: "POST", TRes: Type(), TBody: Type< - Pick & { consultation?: string } + Pick & { + consultation?: string; + reply_to?: string; + } >(), }, updatePatientNote: { From 7878cd6ed70e5c648f31401246bc5f65d90627dc Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Tue, 24 Sep 2024 20:57:01 +0530 Subject: [PATCH 10/13] Added facility hubs (#7772) --- src/Common/constants.tsx | 16 +- src/Components/Common/FacilitySelect.tsx | 6 + src/Components/Facility/FacilityBlock.tsx | 34 ++++ src/Components/Facility/FacilityCreate.tsx | 11 +- src/Components/Facility/FacilityHome.tsx | 22 +++ .../Facility/SpokeFacilityEditor.tsx | 154 ++++++++++++++++++ src/Components/Facility/models.tsx | 33 +++- src/Components/Form/AutoCompleteAsync.tsx | 6 +- src/Components/Form/ModelCrudEditor.tsx | 153 +++++++++++++++++ src/Locale/en/Common.json | 3 + src/Redux/api.tsx | 36 +++- 11 files changed, 463 insertions(+), 11 deletions(-) create mode 100644 src/Components/Facility/FacilityBlock.tsx create mode 100644 src/Components/Facility/SpokeFacilityEditor.tsx create mode 100644 src/Components/Form/ModelCrudEditor.tsx diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 307e912844f..70b5bc78e23 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -1,4 +1,7 @@ -import { PatientCategory } from "../Components/Facility/models"; +import { + PatientCategory, + SpokeRelationship, +} from "../Components/Facility/models"; import { SortOption } from "../Components/Common/SortDropdown"; import { dateQueryString } from "../Utils/utils"; import { IconName } from "../CAREUI/icons/CareIcon"; @@ -1517,6 +1520,17 @@ export const DEFAULT_ALLOWED_EXTENSIONS = [ "application/vnd.oasis.opendocument.spreadsheet,application/pdf", ]; +export const SPOKE_RELATION_TYPES = [ + { + text: "Regular", + value: SpokeRelationship.REGULAR, + }, + { + text: "Tele ICU", + value: SpokeRelationship.TELE_ICU, + }, +]; + export const HumanBodyPaths = { anterior: [ { diff --git a/src/Components/Common/FacilitySelect.tsx b/src/Components/Common/FacilitySelect.tsx index d718ef3e781..d91b9a7f8fc 100644 --- a/src/Components/Common/FacilitySelect.tsx +++ b/src/Components/Common/FacilitySelect.tsx @@ -23,6 +23,8 @@ interface FacilitySelectProps { selected?: FacilityModel | FacilityModel[] | null; setSelected: (selected: FacilityModel | FacilityModel[] | null) => void; allowNone?: boolean; + placeholder?: string; + filter?: (facilities: FacilityModel) => boolean; } export const FacilitySelect = (props: FacilitySelectProps) => { @@ -44,6 +46,8 @@ export const FacilitySelect = (props: FacilitySelectProps) => { allowNone = false, freeText = false, errors = "", + placeholder, + filter, } = props; const facilitySearch = useCallback( @@ -82,6 +86,7 @@ export const FacilitySelect = (props: FacilitySelectProps) => { return ( { compareBy="id" className={className} error={errors} + filter={filter} /> ); }; diff --git a/src/Components/Facility/FacilityBlock.tsx b/src/Components/Facility/FacilityBlock.tsx new file mode 100644 index 00000000000..64c0a24d78d --- /dev/null +++ b/src/Components/Facility/FacilityBlock.tsx @@ -0,0 +1,34 @@ +import { Link } from "raviger"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { FacilityModel } from "./models"; + +export default function FacilityBlock(props: { facility: FacilityModel }) { + const { facility } = props; + + return ( + +
+ {facility.read_cover_image_url ? ( + + ) : ( + <> + + + )} +
+
+ {facility.name} +

+ {facility.address} {facility.local_body_object?.name} +

+
+ + ); +} diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index 1fe9d2e207e..e9e95fbcdfc 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -62,6 +62,7 @@ import routes from "../../Redux/api.js"; import useQuery from "../../Utils/request/useQuery.js"; import { RequestResult } from "../../Utils/request/types.js"; import useAuthUser from "../../Common/hooks/useAuthUser"; +import SpokeFacilityEditor from "./SpokeFacilityEditor.js"; import careConfig from "@careConfig"; const Loading = lazy(() => import("../Common/Loading")); @@ -247,7 +248,7 @@ export const FacilityCreate = (props: FacilityProps) => { }, ); - useQuery(routes.getPermittedFacility, { + const facilityQuery = useQuery(routes.getPermittedFacility, { pathParams: { id: facilityId!, }, @@ -850,6 +851,14 @@ export const FacilityCreate = (props: FacilityProps) => { required types={["mobile", "landline"]} /> +
+

{t("spokes")}

+ {facilityId && ( + + )} +
{ }); }; + const spokesQuery = useQuery(routes.getFacilitySpokes, { + pathParams: { + id: facilityId, + }, + silent: true, + }); + if (isLoading) { return ; } @@ -289,6 +297,20 @@ export const FacilityHome = ({ facilityId }: Props) => { />
+ {!!spokesQuery.data?.results.length && ( +
+
+

+ {t("spokes")} +

+
+ {spokesQuery.data?.results.map((spoke) => ( + + ))} +
+
+
+ )}
diff --git a/src/Components/Facility/SpokeFacilityEditor.tsx b/src/Components/Facility/SpokeFacilityEditor.tsx new file mode 100644 index 00000000000..197d68da2bf --- /dev/null +++ b/src/Components/Facility/SpokeFacilityEditor.tsx @@ -0,0 +1,154 @@ +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; +import useQuery from "../../Utils/request/useQuery"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import { + FacilityModel, + FacilitySpokeErrors, + FacilitySpokeModel, + FacilitySpokeRequest, + SpokeRelationship, +} from "./models"; +import ModelCrudEditor from "../Form/ModelCrudEditor"; +import { FacilitySelect } from "../Common/FacilitySelect"; +import { useEffect, useState } from "react"; +import { SPOKE_RELATION_TYPES } from "../../Common/constants"; +import FacilityBlock from "./FacilityBlock"; +import { useTranslation } from "react-i18next"; + +export interface SpokeFacilityEditorProps { + facility: Omit & { id: string }; +} + +export default function SpokeFacilityEditor(props: SpokeFacilityEditorProps) { + const { facility } = props; + + const { t } = useTranslation(); + + const spokesQuery = useQuery(routes.getFacilitySpokes, { + pathParams: { + id: facility.id, + }, + }); + + const spokes = spokesQuery.data?.results; + + const createSpoke = (body: FacilitySpokeRequest) => + request(routes.createFacilitySpoke, { + body, + pathParams: { + id: facility.id, + }, + onResponse: ({ res }) => { + if (res?.ok) { + spokesQuery.refetch(); + } + }, + }); + + const deleteSpoke = (spokeFacilityId: string) => + request(routes.deleteFacilitySpoke, { + pathParams: { + id: facility.id, + spoke_id: spokeFacilityId, + }, + onResponse: ({ res }) => { + if (res?.ok) { + spokesQuery.refetch(); + } + }, + }); + + const updateSpoke = (spokeFacilityId: string, body: FacilitySpokeRequest) => + request(routes.updateFacilitySpokes, { + pathParams: { + id: facility.id, + spoke_id: spokeFacilityId, + }, + body, + onResponse: ({ res }) => { + if (res?.ok) { + spokesQuery.refetch(); + } + }, + }); + + const FormRender = ( + item: FacilitySpokeModel | FacilitySpokeRequest, + setItem: (item: FacilitySpokeModel | FacilitySpokeRequest) => void, + processing: boolean, + ) => { + const [selectedFacility, setSelectedFacility] = useState(); + + useEffect(() => { + setItem({ ...item, spoke: selectedFacility?.id }); + }, [selectedFacility]); + + return ( +
+ {"id" in item ? ( +
+ +
+ ) : ( + + v && !Array.isArray(v) && setSelectedFacility(v) + } + errors="" + className="w-full" + disabled={processing} + filter={(f) => + !!f.id && + facility.id !== f.id && + !spokes?.flatMap((s) => s.spoke_object.id).includes(f.id) + } + /> + )} + v.text} + optionValue={(v) => v.value} + value={item.relationship} + onChange={(v) => setItem({ ...item, relationship: v.value })} + errorClassName="hidden" + className="w-full shrink-0 md:w-auto" + disabled={processing} + /> +
+ ); + }; + + return ( + <> + + items={spokes} + onCreate={createSpoke} + onUpdate={updateSpoke} + onDelete={deleteSpoke} + loading={spokesQuery.loading} + errors={{}} + emptyText={"No Spokes"} + empty={{ + spoke: "", + relationship: SpokeRelationship.REGULAR, + }} + createText="Add Spoke" + allowCreate={(item) => !item.relationship || !item.spoke} + > + {FormRender} + + + ); +} diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 7756bf73f69..97d81674658 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -12,6 +12,7 @@ import { AssignedToObjectModel, BloodPressure, DailyRoundsModel, + FacilityNameModel, FileUploadModel, } from "../Patient/models"; import { EncounterSymptom } from "../Symptoms/types"; @@ -79,8 +80,32 @@ export interface FacilityModel { local_body?: number; ward?: number; pincode?: string; + latitude?: string; + longitude?: string; + kasp_empanelled?: boolean; + patient_count?: string; + bed_count?: string; +} + +export enum SpokeRelationship { + REGULAR = 1, + TELE_ICU = 2, +} + +export interface FacilitySpokeModel { + id: string; + hub_object: FacilityNameModel; + spoke_object: FacilityNameModel; + relationship: SpokeRelationship; } +export interface FacilitySpokeRequest { + spoke?: string; + relationship?: SpokeRelationship; +} + +export interface FacilitySpokeErrors {} + export interface CapacityModal { id?: number; room_type?: number; @@ -588,13 +613,7 @@ export type IUserFacilityRequest = { facility: string; }; -export type FacilityRequest = Omit & { - latitude?: string; - longitude?: string; - kasp_empanelled?: boolean; - patient_count?: string; - bed_count?: string; -}; +export type FacilityRequest = Omit; export type InventorySummaryResponse = { id: string; diff --git a/src/Components/Form/AutoCompleteAsync.tsx b/src/Components/Form/AutoCompleteAsync.tsx index 18bffb0e11c..f362918dfc2 100644 --- a/src/Components/Form/AutoCompleteAsync.tsx +++ b/src/Components/Form/AutoCompleteAsync.tsx @@ -36,6 +36,7 @@ interface Props { required?: boolean; onBlur?: () => void; onFocus?: () => void; + filter?: (data: any) => boolean; } const AutoCompleteAsync = (props: Props) => { @@ -56,6 +57,7 @@ const AutoCompleteAsync = (props: Props) => { disabled = false, required = false, error, + filter, } = props; const [data, setData] = useState([]); const [query, setQuery] = useState(""); @@ -69,7 +71,9 @@ const AutoCompleteAsync = (props: Props) => { () => debounce(async (query: string) => { setLoading(true); - const data = (await fetchData(query)) || []; + const data = ((await fetchData(query)) || [])?.filter((d: any) => + filter ? filter(d) : true, + ); if (showNOptions !== undefined) { setData(data.slice(0, showNOptions)); diff --git a/src/Components/Form/ModelCrudEditor.tsx b/src/Components/Form/ModelCrudEditor.tsx new file mode 100644 index 00000000000..3ce4b15eba3 --- /dev/null +++ b/src/Components/Form/ModelCrudEditor.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from "react"; +import { classNames } from "../../Utils/utils"; +import ButtonV2 from "../Common/components/ButtonV2"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { useTranslation } from "react-i18next"; + +interface Identifier { + id: string; +} + +export interface ModelCrudEditorProps { + onCreate?: (req: TReq) => Promise; + onUpdate?: (itemId: string, req: TReq) => Promise; + onDelete?: (itemId: string) => Promise; + items?: TRes[]; + children: ( + item: TRes | TReq, + setItem: (item: TRes | TReq) => void, + processing: boolean, + errors?: TErr, + ) => React.ReactNode; + loading: boolean; + errors: TErr; + emptyText?: React.ReactNode; + empty: TReq; + createText?: React.ReactNode; + allowCreate?: (item: TReq) => boolean; +} + +export default function ModelCrudEditor( + props: ModelCrudEditorProps, +) { + const { t } = useTranslation(); + + const { + onCreate, + onUpdate, + onDelete, + items, + children, + loading, + errors, + emptyText, + empty, + createText, + allowCreate, + } = props; + const [creating, setCreating] = useState(false); + const [updating, setUpdating] = useState(null); + + const handleUpdate = async (itemId: string, item: TReq) => { + if (!onUpdate) return; + setUpdating(itemId); + await onUpdate(itemId, item); + setUpdating(null); + }; + + const handleDelete = async (itemId: string) => { + if (!onDelete) return; + setUpdating(itemId); + await onDelete(itemId); + setUpdating(null); + }; + + const handleCreate = async (item: TReq) => { + if (!onCreate) return; + setCreating(true); + await onCreate(item); + setCreating(false); + }; + + type FormProps = + | { + type: "creating"; + item: TReq; + } + | { + type: "updating"; + item: TRes; + }; + + const Form = (props: FormProps) => { + const [item, setItem] = useState(props.item); + const processing = + props.type === "creating" ? creating : props.item.id === updating; + + useEffect(() => { + if ( + props.type === "updating" && + JSON.stringify(item) !== JSON.stringify(props.item) + ) { + const timeout = setTimeout(() => { + handleUpdate(props.item.id, item as TReq); + }, 1000); + return () => clearTimeout(timeout); + } + }, [item]); + + return ( +
+ {children(item, setItem, processing, errors)} + {props.type === "creating" && ( + handleCreate(item as TReq)} + > + {createText || "Create"} + + )} + {props.type === "updating" && onDelete && ( + + )} +
+ ); + }; + + return ( +
+
    + {items?.map((item, i) => ( +
  • +
    +
  • + ))} + + {items?.length === 0 && ( +
    + {emptyText} +
    + )} +
+
+ +
+
+ ); +} diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index 661a1872cca..7f0b9f51802 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -175,6 +175,9 @@ "summary": "Summary", "report": "Report", "treating_doctor": "Treating Doctor", + "hubs": "Hub Facilities", + "spokes": "Spoke Facilities", + "add_spoke" : "Add Spoke Facility", "ration_card__NO_CARD": "Non-card holder", "ration_card__BPL": "BPL", "ration_card__APL": "APL", diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 0243db57df7..0bedb12dca5 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -26,6 +26,8 @@ import { DoctorModal, FacilityModel, FacilityRequest, + FacilitySpokeModel, + FacilitySpokeRequest, IFacilityNotificationRequest, IFacilityNotificationResponse, IUserFacilityRequest, @@ -366,7 +368,7 @@ const routes = { getPermittedFacility: { path: "/api/v1/facility/{id}/", method: "GET", - TRes: Type(), + TRes: Type(), }, getAnyFacility: { @@ -389,6 +391,38 @@ const routes = { TBody: Type>(), }, + getFacilitySpokes: { + path: "/api/v1/facility/{id}/spokes/", + method: "GET", + TRes: Type>(), + }, + + updateFacilitySpokes: { + path: "/api/v1/facility/{id}/spokes/{spoke_id}/", + method: "PATCH", + TRes: Type(), + TBody: Type(), + }, + + getFacilitySpoke: { + path: "/api/v1/facility/{id}/spokes/{spoke_id}/", + method: "GET", + TRes: Type(), + }, + + createFacilitySpoke: { + path: "/api/v1/facility/{id}/spokes/", + method: "POST", + TRes: Type(), + TBody: Type>(), + }, + + deleteFacilitySpoke: { + path: "/api/v1/facility/{id}/spokes/{spoke_id}/", + method: "DELETE", + TRes: Type>(), + }, + deleteFacilityCoverImage: { path: "/api/v1/facility/{id}/cover_image/", method: "DELETE", From 67c524068d079bc17500bc2388d7667d8957c21e Mon Sep 17 00:00:00 2001 From: vishwansh01 <165544401+vishwansh01@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:51:14 +0530 Subject: [PATCH 11/13] Adding cache control headers (#8575) Co-authored-by: Aakash Singh --- nginx/nginx.conf | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 40a878bcee2..e9ece31ea0e 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,17 +1,13 @@ server { - listen 80; + root /usr/share/nginx/html; - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; + location = /index.html { + expires 6h; } - error_page 500 502 503 504 /50x.html; - - location = /50x.html { - root /usr/share/nginx/html; + location / { + expires 7d; + try_files $uri $uri/ /index.html; } - } From 60068ad187ab45c1baecddcda08333902180f439 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 25 Sep 2024 14:37:16 +0530 Subject: [PATCH 12/13] resolve conflicts (#8619) --- nginx/nginx.conf | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index e9ece31ea0e..5ad797a13ec 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,13 +1,21 @@ server { - listen 80; - root /usr/share/nginx/html; - location = /index.html { - expires 6h; + include mime.types; + types { + text/javascript js mjs; } + listen 80; + location / { - expires 7d; + root /usr/share/nginx/html; + index index.html index.htm; try_files $uri $uri/ /index.html; } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } } From f5152fca3e1b11665ea77ba7bf9a2fc65a030f5b Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 25 Sep 2024 14:43:30 +0530 Subject: [PATCH 13/13] Adding cache control headers (#8622) --- nginx/nginx.conf | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 5ad797a13ec..595873a4be2 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,21 +1,18 @@ server { + listen 80; + root /usr/share/nginx/html; include mime.types; types { text/javascript js mjs; } - listen 80; + location = /index.html { + expires 6h; + } location / { - root /usr/share/nginx/html; - index index.html index.htm; + expires 7d; try_files $uri $uri/ /index.html; } - - error_page 500 502 503 504 /50x.html; - - location = /50x.html { - root /usr/share/nginx/html; - } }