diff --git a/SECURITY.md b/SECURITY.md index 0b3730ed9ad..03d8dec4837 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,4 +23,4 @@ ## Reporting a Vulnerability -Please create a ticket at https://support.coronasafe.network +Please create an issue at https://github.com/coronasafe/care_fe/issues/new diff --git a/cypress/e2e/patient_spec/patient_consultation.cy.ts b/cypress/e2e/patient_spec/patient_consultation.cy.ts index 8caea111ca1..eef98a5e3ce 100644 --- a/cypress/e2e/patient_spec/patient_consultation.cy.ts +++ b/cypress/e2e/patient_spec/patient_consultation.cy.ts @@ -59,14 +59,14 @@ describe("Patient Consultation in multiple combination", () => { patientConsultationPage.selectConsultationStatus( "Outpatient/Emergency Room", ); - cy.searchAndSelectOption("#symptoms", "ASYMPTOMATIC"); + cy.get("#is_asymptomatic").click(); patientConsultationPage.typePatientIllnessHistory(patientIllnessHistory); patientConsultationPage.typePatientExaminationHistory( patientExaminationHistory, ); patientConsultationPage.typePatientWeight(patientWeight); patientConsultationPage.typePatientHeight(patientHeight); - patientConsultationPage.selectPatientCategory("Stable"); + patientConsultationPage.selectPatientCategory("Mild"); // icd 11 - 4 diagnosis with one principal patientConsultationPage.selectPatientDiagnosis( diagnosis1, @@ -175,7 +175,7 @@ describe("Patient Consultation in multiple combination", () => { "Outpatient/Emergency Room", ); // Asymptomatic - cy.searchAndSelectOption("#symptoms", "ASYMPTOMATIC"); + cy.get("#is_asymptomatic").click(); // CRITICAL category patientConsultationPage.selectPatientCategory("Critical"); patientConsultationPage.selectPatientSuggestion("Declare Death"); @@ -234,9 +234,9 @@ describe("Patient Consultation in multiple combination", () => { ); patientConsultationPage.selectPatientWard("Dummy Location 1"); // Asymptomatic - cy.searchAndSelectOption("#symptoms", "ASYMPTOMATIC"); + cy.get("#is_asymptomatic").click(); // Abnormal category - patientConsultationPage.selectPatientCategory("Abnormal"); + patientConsultationPage.selectPatientCategory("Moderate"); patientConsultationPage.selectPatientSuggestion("Domiciliary Care"); // one ICD-11 diagnosis patientConsultationPage.selectPatientDiagnosis( @@ -293,18 +293,14 @@ describe("Patient Consultation in multiple combination", () => { ); // verify the free text in referring facility name patientConsultationPage.typeReferringFacility("Life Care Hospital"); - // Vomiting and Nausea symptoms + patientConsultationPage.selectSymptomsDate("01012024"); patientConsultationPage.typeAndMultiSelectSymptoms("s", [ - "SPUTUM", - "SORE THROAT", + "Sore throat", + "Sputum", ]); + patientConsultationPage.clickAddSymptom(); // Stable category - patientConsultationPage.selectPatientCategory("Stable"); - // Date of symptoms - patientConsultationPage.selectSymptomsDate( - "#symptoms_onset_date", - "01012024", - ); + patientConsultationPage.selectPatientCategory("Mild"); // OP Consultation patientConsultationPage.selectPatientSuggestion("OP Consultation"); // one ICD-11 and no principal @@ -341,18 +337,16 @@ describe("Patient Consultation in multiple combination", () => { patientConsultationPage.selectConsultationStatus( "Outpatient/Emergency Room", ); - // Select the Symptoms - Sore throat and fever symptoms + // Select the Symptoms - Breathlessness and Bleeding symptoms + patientConsultationPage.selectSymptomsDate("01012024"); patientConsultationPage.typeAndMultiSelectSymptoms("b", [ - "BREATHLESSNESS", - "BLEEDING", + "Breathlessness", + "Bleeding", ]); + patientConsultationPage.clickAddSymptom(); // Comfort Care category patientConsultationPage.selectPatientCategory("Comfort Care"); // Date of symptoms - patientConsultationPage.selectSymptomsDate( - "#symptoms_onset_date", - "01012024", - ); // Decision after consultation - Referred to Facility patientConsultationPage.selectPatientSuggestion( "Refer to another Hospital", diff --git a/cypress/e2e/patient_spec/patient_logupdate.cy.ts b/cypress/e2e/patient_spec/patient_logupdate.cy.ts index 8cf83b1c5a8..562e430a9ad 100644 --- a/cypress/e2e/patient_spec/patient_logupdate.cy.ts +++ b/cypress/e2e/patient_spec/patient_logupdate.cy.ts @@ -10,8 +10,8 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { const patientPage = new PatientPage(); const patientLogupdate = new PatientLogupdate(); const domicilaryPatient = "Dummy Patient 11"; - const patientCategory = "Abnormal"; - const additionalSymptoms = "ASYMPTOMATIC"; + const patientCategory = "Moderate"; + const additionalSymptoms = "Fever"; const physicalExamination = "physical examination details"; const otherExamination = "Other"; const patientSystolic = "119"; @@ -59,9 +59,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeRhythm(patientRhythm); cy.get("#consciousness_level-2").click(); cy.submitButton("Save"); - cy.verifyNotification( - "Telemedicine Log Updates details created successfully", - ); + cy.verifyNotification("Tele-medicine log update created successfully"); }); it("Create a new log normal update for a domicilary care patient and edit it", () => { @@ -86,7 +84,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeRhythm(patientRhythm); cy.get("#consciousness_level-2").click(); cy.submitButton("Save"); - cy.verifyNotification("Normal Log Updates details created successfully"); + cy.verifyNotification("Normal log update created successfully"); cy.closeNotification(); // edit the card and verify the data. cy.contains("Daily Rounds").click(); @@ -109,7 +107,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.clickClearButtonInElement("#diastolic"); patientLogupdate.typeDiastolic(patientModifiedDiastolic); cy.submitButton("Continue"); - cy.verifyNotification("Normal Log Updates details updated successfully"); + cy.verifyNotification("Normal log update details updated successfully"); cy.contains("Daily Rounds").click(); patientLogupdate.clickLogupdateCard("#dailyround-entry", patientCategory); cy.verifyContentPresence("#consultation-preview", [ @@ -127,7 +125,9 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.clickLogupdate(); patientLogupdate.typePhysicalExamination(physicalExamination); patientLogupdate.typeOtherDetails(otherExamination); - patientLogupdate.typeAdditionalSymptoms(additionalSymptoms); + patientLogupdate.selectSymptomsDate("01012024"); + patientLogupdate.typeAndMultiSelectSymptoms("fe", ["Fever"]); + patientLogupdate.clickAddSymptom(); patientLogupdate.selectPatientCategory(patientCategory); patientLogupdate.typeSystolic(patientSystolic); patientLogupdate.typeDiastolic(patientDiastolic); @@ -140,10 +140,10 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { cy.get("#consciousness_level-2").click(); cy.submitButton("Save"); cy.wait(2000); - cy.verifyNotification("Normal Log Updates details created successfully"); + cy.verifyNotification("Normal log update created successfully"); // Verify the card content cy.get("#basic-information").scrollIntoView(); - cy.verifyContentPresence("#basic-information", [additionalSymptoms]); + cy.verifyContentPresence("#encounter-symptoms", [additionalSymptoms]); }); it("Create a normal log update to verify MEWS Score Functionality", () => { @@ -163,7 +163,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeRespiratory(patientRespiratory); cy.get("#consciousness_level-2").click(); cy.submitButton("Save"); - cy.verifyNotification("Normal Log Updates details created successfully"); + cy.verifyNotification("Normal log update created successfully"); cy.closeNotification(); cy.verifyContentPresence("#consultation-buttons", ["9"]); // Verify the Incomplete data will give blank info @@ -173,7 +173,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeDiastolic(patientDiastolic); patientLogupdate.typePulse(patientPulse); cy.submitButton("Save"); - cy.verifyNotification("Normal Log Updates details created successfully"); + cy.verifyNotification("Normal log update created successfully"); cy.closeNotification(); cy.verifyContentPresence("#consultation-buttons", ["-"]); }); diff --git a/cypress/e2e/patient_spec/patient_prescription.cy.ts b/cypress/e2e/patient_spec/patient_prescription.cy.ts index 3cf5a1ba5aa..d9a7bf7a080 100644 --- a/cypress/e2e/patient_spec/patient_prescription.cy.ts +++ b/cypress/e2e/patient_spec/patient_prescription.cy.ts @@ -5,6 +5,11 @@ import { PatientPage } from "../../pageobject/Patient/PatientCreation"; const patientPrescription = new PatientPrescription(); const loginPage = new LoginPage(); const patientPage = new PatientPage(); +const medicineName = "DOLO"; +const medicineBaseDosage = "4"; +const medicineTargetDosage = "9"; +const medicineFrequency = "Twice daily"; +const medicineAdministerNote = "Medicine Administration Note"; describe("Patient Medicine Administration", () => { before(() => { @@ -18,15 +23,54 @@ describe("Patient Medicine Administration", () => { cy.awaitUrl("/patients"); }); + it("Add a new titrated medicine for a patient | Individual Administeration |", () => { + patientPage.visitPatient("Dummy Patient 5"); + patientPrescription.visitMedicineTab(); + patientPrescription.visitEditPrescription(); + patientPrescription.clickAddPrescription(); + patientPrescription.interceptMedibase(); + patientPrescription.selectMedicinebox(); + patientPrescription.selectMedicine(medicineName); + patientPrescription.clickTitratedDosage(); + patientPrescription.enterDosage(medicineBaseDosage); + patientPrescription.enterTargetDosage(medicineTargetDosage); + patientPrescription.selectDosageFrequency(medicineFrequency); + cy.submitButton("Submit"); + cy.verifyNotification("Medicine prescribed"); + cy.closeNotification(); + // Administer the medicine in edit form + patientPrescription.clickAdministerButton(); + patientPrescription.enterAdministerDosage(medicineBaseDosage); + patientPrescription.enterAdministerNotes(medicineAdministerNote); + cy.submitButton("Administer Medicine"); + cy.verifyNotification("Medicine(s) administered"); + cy.closeNotification(); + // Verify the Reflection on the Medicine + cy.verifyContentPresence("#medicine-preview", [ + medicineName, + medicineBaseDosage, + medicineTargetDosage, + ]); + patientPrescription.clickReturnToDashboard(); + // Go to medicine tab and administer it again + patientPrescription.visitMedicineTab(); + cy.verifyAndClickElement("#0", medicineName); + cy.submitButton("Administer"); + patientPrescription.enterAdministerDosage(medicineBaseDosage); + cy.submitButton("Administer Medicine"); + cy.verifyNotification("Medicine(s) administered"); + }); + it("Add a new medicine for a patient and verify the duplicate medicine validation", () => { patientPage.visitPatient("Dummy Patient 4"); patientPrescription.visitMedicineTab(); + patientPrescription.visitEditPrescription(); patientPrescription.clickAddPrescription(); patientPrescription.interceptMedibase(); patientPrescription.selectMedicinebox(); - patientPrescription.selectMedicine("DOLO"); - patientPrescription.enterDosage("4"); - patientPrescription.selectDosageFrequency("Twice daily"); + patientPrescription.selectMedicine(medicineName); + patientPrescription.enterDosage(medicineBaseDosage); + patientPrescription.selectDosageFrequency(medicineFrequency); cy.submitButton("Submit"); cy.verifyNotification("Medicine prescribed"); cy.closeNotification(); @@ -34,9 +78,9 @@ describe("Patient Medicine Administration", () => { patientPrescription.clickAddPrescription(); patientPrescription.interceptMedibase(); patientPrescription.selectMedicinebox(); - patientPrescription.selectMedicine("DOLO"); - patientPrescription.enterDosage("4"); - patientPrescription.selectDosageFrequency("Twice daily"); + patientPrescription.selectMedicine(medicineName); + patientPrescription.enterDosage(medicineBaseDosage); + patientPrescription.selectDosageFrequency(medicineFrequency); cy.submitButton("Submit"); cy.verifyNotification( "Medicine - This medicine is already prescribed to this patient. Please discontinue the existing prescription to prescribe again.", diff --git a/cypress/e2e/shifting_spec/filter.cy.ts b/cypress/e2e/shifting_spec/filter.cy.ts index a142a657013..82fad9d99e4 100644 --- a/cypress/e2e/shifting_spec/filter.cy.ts +++ b/cypress/e2e/shifting_spec/filter.cy.ts @@ -20,7 +20,7 @@ describe("Shifting section filter", () => { shiftingPage.filterByFacility( "Dummy Shifting", "Dummy Shifting", - "District" + "District", ); shiftingPage.facilityAssignedBadge().should("exist"); @@ -32,13 +32,10 @@ describe("Shifting section filter", () => { "ASC Created Date", "yes", "yes", - "POSITIVE", "no", "MODERATE", - "9999999999" + "9999999999", ); - - shiftingPage.diseaseStatusBadge().should("exist"); shiftingPage.orderingBadge().should("exist"); shiftingPage.breathlessnessLevelBadge().should("exist"); shiftingPage.phoneNumberBadge().should("exist"); diff --git a/cypress/e2e/users_spec/user_homepage.cy.ts b/cypress/e2e/users_spec/user_homepage.cy.ts index a006fe77569..3ac07dd9d9c 100644 --- a/cypress/e2e/users_spec/user_homepage.cy.ts +++ b/cypress/e2e/users_spec/user_homepage.cy.ts @@ -32,6 +32,7 @@ describe("User Homepage", () => { userPage.selectDistrict("Ernakulam"); userPage.typeInPhoneNumber(phone_number); userPage.typeInAltPhoneNumber(alt_phone_number); + userPage.selectHomeFacility("Dummy Facility 40"); userPage.applyFilter(); userPage.verifyUrlafteradvancefilter(); userPage.checkUsernameText(usernameToTest); @@ -46,6 +47,10 @@ describe("User Homepage", () => { "WhatsApp no.: +919876543219", ); userPage.verifyDataTestIdText("Role", "Role: Doctor"); + userPage.verifyDataTestIdText( + "Home Facility", + "Home Facility: Dummy Facility 40", + ); userPage.verifyDataTestIdText("District", "District: Ernakulam"); userPage.clearFilters(); userPage.verifyDataTestIdNotVisible("First Name"); @@ -53,6 +58,7 @@ describe("User Homepage", () => { userPage.verifyDataTestIdNotVisible("Phone Number"); userPage.verifyDataTestIdNotVisible("WhatsApp no."); userPage.verifyDataTestIdNotVisible("Role"); + userPage.verifyDataTestIdNotVisible("Home Facility"); userPage.verifyDataTestIdNotVisible("District"); }); diff --git a/cypress/pageobject/Patient/PatientConsultation.ts b/cypress/pageobject/Patient/PatientConsultation.ts index 4400d9a524c..31b1fd6cb68 100644 --- a/cypress/pageobject/Patient/PatientConsultation.ts +++ b/cypress/pageobject/Patient/PatientConsultation.ts @@ -6,14 +6,14 @@ export class PatientConsultationPage { cy.clickAndSelectOption("#route_to_facility", status); } - selectSymptoms(symptoms) { - cy.clickAndMultiSelectOption("#symptoms", symptoms); - } typeAndMultiSelectSymptoms(input, symptoms) { - cy.typeAndMultiSelectOption("#symptoms", input, symptoms); + cy.typeAndMultiSelectOption("#additional_symptoms", input, symptoms); + } + selectSymptomsDate(date: string) { + cy.clickAndTypeDate("#symptoms_onset_date", date); } - selectSymptomsDate(selector: string, date: string) { - cy.clickAndTypeDate(selector, date); + clickAddSymptom() { + cy.get("#add-symptom").click(); } verifyConsultationPatientName(patientName: string) { diff --git a/cypress/pageobject/Patient/PatientLogupdate.ts b/cypress/pageobject/Patient/PatientLogupdate.ts index 3511f0241bb..92ea02a1417 100644 --- a/cypress/pageobject/Patient/PatientLogupdate.ts +++ b/cypress/pageobject/Patient/PatientLogupdate.ts @@ -32,6 +32,16 @@ class PatientLogupdate { cy.searchAndSelectOption("#additional_symptoms", symptoms); } + typeAndMultiSelectSymptoms(input, symptoms) { + cy.typeAndMultiSelectOption("#additional_symptoms", input, symptoms); + } + selectSymptomsDate(date: string) { + cy.clickAndTypeDate("#symptoms_onset_date", date); + } + clickAddSymptom() { + cy.get("#add-symptom").click(); + } + typeSystolic(systolic: string) { cy.searchAndSelectOption("#systolic", systolic); } diff --git a/cypress/pageobject/Patient/PatientPrescription.ts b/cypress/pageobject/Patient/PatientPrescription.ts index dc4163e4823..a4b92b0a5fa 100644 --- a/cypress/pageobject/Patient/PatientPrescription.ts +++ b/cypress/pageobject/Patient/PatientPrescription.ts @@ -1,8 +1,10 @@ export class PatientPrescription { clickAddPrescription() { - cy.contains("button", "Add Prescription Medication") - .should("be.visible") - .click(); + cy.get("#add-prescription").scrollIntoView(); + cy.verifyAndClickElement( + "#add-prescription", + "Add Prescription Medication", + ); } interceptMedibase() { @@ -16,6 +18,15 @@ export class PatientPrescription { ); } + clickTitratedDosage() { + cy.get("#titrated-dosage").click(); + } + + clickAdministerButton() { + cy.get("#administer-medicine").should("be.visible"); + cy.verifyAndClickElement("#administer-medicine", "Administer"); + } + selectMedicinebox() { cy.get( "div#medicine_object input[placeholder='Select'][role='combobox']", @@ -30,6 +41,18 @@ export class PatientPrescription { cy.get("#base_dosage").type(doseAmount, { force: true }); } + enterAdministerDosage(dosage: string) { + cy.get("#dosage").type(dosage); + } + + enterAdministerNotes(notes: string) { + cy.get("#administration_notes").type(notes); + } + + enterTargetDosage(targetDosage: string) { + cy.get("#target_dosage").type(targetDosage, { force: true }); + } + selectDosageFrequency(frequency: string) { cy.clickAndSelectOption("#frequency", frequency); } @@ -54,7 +77,10 @@ export class PatientPrescription { visitMedicineTab() { cy.get("#consultation_tab_nav").scrollIntoView(); cy.get("#consultation_tab_nav").contains("Medicines").click(); - cy.get("a[href='prescriptions']").first().click(); + } + + visitEditPrescription() { + cy.get("#edit-prescription").click(); } } export default PatientPrescription; diff --git a/cypress/pageobject/Shift/ShiftFilters.ts b/cypress/pageobject/Shift/ShiftFilters.ts index 87d3f33c37e..1f824cebbb3 100644 --- a/cypress/pageobject/Shift/ShiftFilters.ts +++ b/cypress/pageobject/Shift/ShiftFilters.ts @@ -27,10 +27,6 @@ class ShiftingPage { return cy.get("#is-up-shift"); } - diseaseStatusInput() { - return cy.get("#disease-status"); - } - isAntenatalInput() { return cy.get("#is-antenatal"); } @@ -63,10 +59,6 @@ class ShiftingPage { return cy.get("[data-testid='Current facility']"); } - diseaseStatusBadge() { - return cy.get("[data-testid='Disease status']"); - } - orderingBadge() { return cy.get("[data-testid='Ordering']"); } @@ -98,7 +90,7 @@ class ShiftingPage { filterByFacility( origin_facility: string, assigned_facility: string, - assigned_to: string + assigned_to: string, ) { this.originFacilityInput().click().type(origin_facility); cy.get("[role='option']").contains(origin_facility).click(); @@ -116,10 +108,9 @@ class ShiftingPage { ordering: string, emergency: string, is_up_shift: string, - disease_status: string, is_antenatal: string, breathlessness_level: string, - patient_phone_number: string + patient_phone_number: string, ) { this.orderingInput().click(); cy.get("[role='option']").contains(ordering).click(); @@ -130,9 +121,6 @@ class ShiftingPage { this.isUpShiftInput().click(); cy.get("[role='option']").contains(is_up_shift).click(); - this.diseaseStatusInput().click(); - cy.get("[role='option']").contains(disease_status).click(); - this.isAntenatalInput().click(); cy.get("[role='option']").contains(is_antenatal).click(); @@ -148,7 +136,7 @@ class ShiftingPage { created_date_start: string, created_date_end: string, modified_date_start: string, - modified_date_end: string + modified_date_end: string, ) { this.createdDateStartInput().click(); cy.get("[id^='headlessui-popover-panel-'] .care-l-angle-left-b") diff --git a/cypress/pageobject/Users/UserSearch.ts b/cypress/pageobject/Users/UserSearch.ts index 7d85563d62c..56d1a81395d 100644 --- a/cypress/pageobject/Users/UserSearch.ts +++ b/cypress/pageobject/Users/UserSearch.ts @@ -78,6 +78,10 @@ export class UserPage { cy.get("#alt_phone_number").click().type(altPhone); } + selectHomeFacility(facility: string) { + cy.searchAndSelectOption("input[name='home_facility']", facility); + } + applyFilter() { cy.get("#apply-filter").click(); } diff --git a/src/CAREUI/display/NetworkSignal.tsx b/src/CAREUI/display/NetworkSignal.tsx index 2bcd2744acb..1d5f2d49623 100644 --- a/src/CAREUI/display/NetworkSignal.tsx +++ b/src/CAREUI/display/NetworkSignal.tsx @@ -19,7 +19,7 @@ export default function NetworkSignal({ strength, children }: Props) { return (
)) )} + {!!strength && strength < 2 && ( + + )}
{children} diff --git a/src/CAREUI/display/RecordMeta.tsx b/src/CAREUI/display/RecordMeta.tsx index 662c61fd73f..2363ddbd551 100644 --- a/src/CAREUI/display/RecordMeta.tsx +++ b/src/CAREUI/display/RecordMeta.tsx @@ -1,8 +1,7 @@ import CareIcon from "../icons/CareIcon"; import { - formatDate, + formatDateTime, formatName, - formatTime, isUserOnline, relativeTime, } from "../../Utils/utils"; @@ -39,8 +38,9 @@ const RecordMeta = ({
{relativeTime(time)} - {formatTime(time)}
- {formatDate(time)} + + {formatDateTime(time).replace(";", "")} + {user && !inlineUser && ( by diff --git a/src/CAREUI/misc/AuthorizedChild.tsx b/src/CAREUI/misc/AuthorizedChild.tsx index ce9ec69d546..935f0c51f3c 100644 --- a/src/CAREUI/misc/AuthorizedChild.tsx +++ b/src/CAREUI/misc/AuthorizedChild.tsx @@ -1,4 +1,7 @@ +import { ReactNode } from "react"; +import useAuthUser from "../../Common/hooks/useAuthUser"; import { useIsAuthorized } from "../../Common/hooks/useIsAuthorized"; +import useSlug from "../../Common/hooks/useSlug"; import { AuthorizedForCB } from "../../Utils/AuthorizeFor"; interface Props { @@ -12,3 +15,20 @@ const AuthorizedChild = (props: Props) => { }; export default AuthorizedChild; + +export const AuthorizedForConsultationRelatedActions = (props: { + children: ReactNode; +}) => { + const me = useAuthUser(); + const facilityId = useSlug("facility"); + + if ( + me.home_facility_object?.id === facilityId || + me.user_type === "DistrictAdmin" || + me.user_type === "StateAdmin" + ) { + return props.children; + } + + return null; +}; diff --git a/src/CAREUI/misc/PaginatedList.tsx b/src/CAREUI/misc/PaginatedList.tsx index db68c6f042c..ec270bf8134 100644 --- a/src/CAREUI/misc/PaginatedList.tsx +++ b/src/CAREUI/misc/PaginatedList.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; import { PaginatedResponse, QueryRoute } from "../../Utils/request/types"; import useQuery, { QueryOptions } from "../../Utils/request/useQuery"; import ButtonV2, { @@ -33,6 +33,9 @@ function useContextualized() { interface Props extends QueryOptions> { route: QueryRoute>; perPage?: number; + queryCB?: ( + query: ReturnType>>, + ) => void; children: ( ctx: PaginatedListContext, query: ReturnType>>, @@ -43,6 +46,7 @@ export default function PaginatedList({ children, route, perPage = DEFAULT_PER_PAGE_LIMIT, + queryCB, ...queryOptions }: Props) { const [currentPage, setPage] = useState(1); @@ -57,6 +61,12 @@ export default function PaginatedList({ const items = query.data?.results ?? []; + useEffect(() => { + if (queryCB) { + queryCB(query); + } + }, [query]); + return ( = [ { id: 30 * 24 * 60, text: "1 month" }, ]; -export const SYMPTOM_CHOICES = [ - { id: 1, text: "ASYMPTOMATIC", isSingleSelect: true }, - { id: 2, text: "FEVER" }, - { id: 3, text: "SORE THROAT" }, - { id: 4, text: "COUGH" }, - { id: 5, text: "BREATHLESSNESS" }, - { id: 6, text: "MYALGIA" }, - { id: 7, text: "ABDOMINAL DISCOMFORT" }, - { id: 8, text: "VOMITING" }, - { id: 11, text: "SPUTUM" }, - { id: 12, text: "NAUSEA" }, - { id: 13, text: "CHEST PAIN" }, - { id: 14, text: "HEMOPTYSIS" }, - { id: 15, text: "NASAL DISCHARGE" }, - { id: 16, text: "BODY ACHE" }, - { id: 17, text: "DIARRHOEA" }, - { id: 18, text: "PAIN" }, - { id: 19, text: "PEDAL EDEMA" }, - { id: 20, text: "WOUND" }, - { id: 21, text: "CONSTIPATION" }, - { id: 22, text: "HEAD ACHE" }, - { id: 23, text: "BLEEDING" }, - { id: 24, text: "DIZZINESS" }, - { id: 25, text: "CHILLS" }, - { id: 26, text: "GENERAL WEAKNESS" }, - { id: 27, text: "IRRITABILITY" }, - { id: 28, text: "CONFUSION" }, - { id: 29, text: "ABDOMINAL PAIN" }, - { id: 30, text: "JOINT PAIN" }, - { id: 31, text: "REDNESS OF EYES" }, - { id: 32, text: "ANOREXIA" }, - { id: 33, text: "NEW LOSS OF TASTE" }, - { id: 34, text: "NEW LOSS OF SMELL" }, - { id: 9, text: "OTHERS" }, -]; - export const DISCHARGE_REASONS = [ { id: 1, text: "Recovered" }, { id: 2, text: "Referred" }, @@ -406,10 +370,10 @@ export const SAMPLE_TEST_RESULT = [ 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" }, + { id: "R", text: "Refer to another Hospital", editDisabled: true }, { id: "OP", text: "OP Consultation" }, { id: "DC", text: "Domiciliary Care" }, - { id: "DD", text: "Declare Death" }, + { id: "DD", text: "Declare Death", editDisabled: true }, ] as const; export type ConsultationSuggestionValue = @@ -438,8 +402,8 @@ export const PATIENT_CATEGORIES: { twClass: string; }[] = [ { id: "Comfort", text: "Comfort Care", twClass: "patient-comfort" }, - { id: "Stable", text: "Stable", twClass: "patient-stable" }, - { id: "Moderate", text: "Abnormal", twClass: "patient-abnormal" }, + { id: "Stable", text: "Mild", twClass: "patient-stable" }, + { id: "Moderate", text: "Moderate", twClass: "patient-abnormal" }, { id: "Critical", text: "Critical", twClass: "patient-critical" }, ]; @@ -477,13 +441,6 @@ export const SAMPLE_FLOW_RULES = { RECEIVED_AT_LAB: ["COMPLETED"], }; -export const DISEASE_STATUS = [ - "POSITIVE", - "SUSPECTED", - "NEGATIVE", - "RECOVERED", -]; - export const TEST_TYPE = [ "UNK", "ANTIGEN", @@ -1322,7 +1279,7 @@ export const CONSENT_PATIENT_CODE_STATUS_CHOICES = [ { id: 1, text: "Do Not Hospitalise (DNH)" }, { id: 2, text: "Do Not Resuscitate (DNR)" }, { id: 3, text: "Comfort Care Only" }, - { id: 4, text: "Active treatment (Default)" }, + { id: 4, text: "Active treatment" }, ]; export const OCCUPATION_TYPES = [ { @@ -1418,3 +1375,5 @@ export const PATIENT_NOTES_THREADS = { Doctors: 10, Nurses: 20, } as const; + +export const RATION_CARD_CATEGORY = ["BPL", "APL", "NO_CARD"] as const; diff --git a/src/Common/hooks/useMSEplayer.ts b/src/Common/hooks/useMSEplayer.ts index 482f991bc25..39e4d356910 100644 --- a/src/Common/hooks/useMSEplayer.ts +++ b/src/Common/hooks/useMSEplayer.ts @@ -149,7 +149,6 @@ export const useMSEMediaPlayer = ({ const ws = wsRef.current; ws.binaryType = "arraybuffer"; ws.onopen = function (_event) { - console.log("Connected to ws"); onSuccess && onSuccess(undefined); }; ws.onmessage = function (event) { diff --git a/src/Common/hooks/useNotificationSubscriptionState.ts b/src/Common/hooks/useNotificationSubscriptionState.ts new file mode 100644 index 00000000000..b7927e0cc70 --- /dev/null +++ b/src/Common/hooks/useNotificationSubscriptionState.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; +import useAuthUser from "./useAuthUser"; +import * as Sentry from "@sentry/browser"; + +export type NotificationSubscriptionState = + | "unsubscribed" + | "subscribed" + | "subscribed_on_other_device" + | "subscribed" + | "pending" + | "error"; + +/** + * This is a temporary hook and will be removed in the future. + * + * This hook is used to get the initial notification subscription state of the user. + * @returns NotificationSubscriptionState + */ +export default function useNotificationSubscriptionState( + dependencies: any[] = [], +) { + const { username } = useAuthUser(); + const [subscriptionState, setSubscriptionState] = + useState("pending"); + + const getSubscriptionState = async () => { + try { + const res = await request(routes.getUserPnconfig, { + pathParams: { username }, + }); + + const reg = await navigator.serviceWorker.ready; + const subscription = await reg.pushManager.getSubscription(); + + if (!subscription && !res.data?.pf_endpoint) { + setSubscriptionState("unsubscribed"); + } else if (subscription?.endpoint !== res.data?.pf_endpoint) { + setSubscriptionState("subscribed_on_other_device"); + } else { + setSubscriptionState("subscribed"); + } + } catch (error) { + setSubscriptionState("error"); + Sentry.captureException(error); + } + }; + + useEffect(() => { + getSubscriptionState(); + }, [username, ...dependencies]); + + return subscriptionState; +} diff --git a/src/Components/ABDM/ABDMFacilityRecords.tsx b/src/Components/ABDM/ABDMFacilityRecords.tsx index 8178e037a7f..96040905d6c 100644 --- a/src/Components/ABDM/ABDMFacilityRecords.tsx +++ b/src/Components/ABDM/ABDMFacilityRecords.tsx @@ -4,6 +4,8 @@ import useQuery from "../../Utils/request/useQuery"; import { formatDateTime } from "../../Utils/utils"; import Loading from "../Common/Loading"; import Page from "../Common/components/Page"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import ButtonV2 from "../Common/components/ButtonV2"; interface IProps { facilityId: string; @@ -21,7 +23,11 @@ const TableHeads = [ ]; export default function ABDMFacilityRecords({ facilityId }: IProps) { - const { data: consentsResult, loading } = useQuery(routes.abha.listConsents, { + const { + data: consentsResult, + loading, + refetch, + } = useQuery(routes.abha.listConsents, { query: { facility: facilityId, ordering: "-created_date" }, }); @@ -53,6 +59,13 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) { scope="col" className="sticky right-0 top-0 py-3.5 pl-3 pr-4 sm:pr-6" > + refetch()} + ghost + className="max-w-2xl text-sm text-gray-700 hover:text-gray-900" + > + Refresh + View diff --git a/src/Components/ABDM/FetchRecordsModal.tsx b/src/Components/ABDM/FetchRecordsModal.tsx index 8273a9763f1..950b69611c4 100644 --- a/src/Components/ABDM/FetchRecordsModal.tsx +++ b/src/Components/ABDM/FetchRecordsModal.tsx @@ -21,6 +21,7 @@ import CircularProgress from "../Common/components/CircularProgress.js"; import CareIcon from "../../CAREUI/icons/CareIcon.js"; import { classNames } from "../../Utils/utils.js"; import { ConsentHIType, ConsentPurpose } from "./types/consent.js"; +import useNotificationSubscriptionState from "../../Common/hooks/useNotificationSubscriptionState.js"; const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); @@ -46,6 +47,9 @@ export default function FetchRecordsModal({ patient, show, onClose }: IProps) { dayjs().add(30, "day").toDate(), ); const [errors, setErrors] = useState({}); + const notificationSubscriptionState = useNotificationSubscriptionState([ + show, + ]); useMessageListener((data) => { if (data.type === "MESSAGE" && data.from === "patients/on_find") { @@ -62,7 +66,23 @@ export default function FetchRecordsModal({ patient, show, onClose }: IProps) { }); return ( - + + {["unsubscribed", "subscribed_on_other_device"].includes( + notificationSubscriptionState, + ) && ( +

+ {" "} + Notifications needs to be enabled on this device to verify the + patient. +

+ )} +
Request Consent diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx index c3ba434ef3f..5a8ccd5c184 100644 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ b/src/Components/Assets/configure/CameraConfigure.tsx @@ -7,6 +7,7 @@ import { getCameraConfig } from "../../../Utils/transformUtils"; import { Submit } from "../../Common/components/ButtonV2"; import TextFormField from "../../Form/FormFields/TextFormField"; import Card from "../../../CAREUI/display/Card"; +import { FieldErrorText } from "../../Form/FormFields/FormField"; interface CameraConfigureProps { asset: AssetData; @@ -59,8 +60,14 @@ export default function CameraConfigure(props: CameraConfigureProps) { value={newPreset} className="mt-1" onChange={(e) => setNewPreset(e.value)} - error="" + errorClassName="hidden" /> + {newPreset.length > 12 && ( + + )}
diff --git a/src/Components/Assets/configure/MonitorConfigure.tsx b/src/Components/Assets/configure/MonitorConfigure.tsx index 5053b5b7d02..785b82873de 100644 --- a/src/Components/Assets/configure/MonitorConfigure.tsx +++ b/src/Components/Assets/configure/MonitorConfigure.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { BedSelect } from "../../Common/BedSelect"; import { BedModel } from "../../Facility/models"; -import { AssetData } from "../AssetTypes"; +import { AssetClass, AssetData } from "../AssetTypes"; import * as Notification from "../../../Utils/Notifications.js"; import { Submit } from "../../Common/components/ButtonV2"; import { FieldLabel } from "../../Form/FormFields/FormField"; @@ -73,6 +73,7 @@ export default function MonitorConfigure({ asset }: { asset: AssetData }) { multiple={false} location={asset?.location_object?.id} facility={asset?.location_object?.facility?.id} + not_occupied_by_asset_type={AssetClass.HL7MONITOR} className="w-full" />
diff --git a/src/Components/CameraFeed/AssetBedSelect.tsx b/src/Components/CameraFeed/AssetBedSelect.tsx index 17701dccbde..56def3d41c9 100644 --- a/src/Components/CameraFeed/AssetBedSelect.tsx +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -1,42 +1,76 @@ import { Fragment } from "react"; -import useSlug from "../../Common/hooks/useSlug"; -import routes from "../../Redux/api"; -import useQuery from "../../Utils/request/useQuery"; -import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; -import { BedModel } from "../Facility/models"; +import { AssetBedModel } from "../Assets/AssetTypes"; import { Listbox, Transition } from "@headlessui/react"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import { classNames } from "../../Utils/utils"; interface Props { - asset?: AssetData; - bed?: BedModel; + options: AssetBedModel[]; value?: AssetBedModel; + label?: (value: AssetBedModel) => string; onChange?: (value: AssetBedModel) => void; } -export default function AssetBedSelect(props: Props) { - const facility = useSlug("facility"); - - const { data, loading } = useQuery(routes.listAssetBeds, { - query: { - limit: 100, - facility, - asset: props.asset?.id, - bed: props.bed?.id, - }, - }); +export default function CameraPresetSelect(props: Props) { + const label = props.label ?? defaultLabel; + return ( + <> +
+ {/* Desktop View */} + {props.options + .slice(0, props.options.length > 5 ? 4 : 5) + .map((option) => ( + + ))} + {props.options.length > 5 && ( + o.id === props.value?.id)} + /> + )} +
+
+ {/* Mobile View */} + +
+ + ); +} +export const CameraPresetDropdown = (props: Props) => { const selected = props.value; + const options = props.options.filter(({ meta }) => meta.type !== "boundary"); + + const label = props.label ?? defaultLabel; + return ( - -
- + +
+ - {selected?.bed_object.name ?? "No Preset"} + {selected ? label(selected) : "Select preset"} - - + + - - {data?.results.map((obj) => ( + + {options?.map((obj) => ( @@ -59,11 +93,11 @@ export default function AssetBedSelect(props: Props) { {({ selected }) => ( <> - {obj.bed_object.name}: {obj.meta.preset_name} + {label(obj)} )} @@ -74,4 +108,8 @@ export default function AssetBedSelect(props: Props) {
); -} +}; + +const defaultLabel = ({ bed_object, meta }: AssetBedModel) => { + return `${bed_object.name}: ${meta.preset_name}`; +}; diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index d5ea120e126..a5f0a3d80e0 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -10,6 +10,8 @@ import FeedNetworkSignal from "./FeedNetworkSignal"; import NoFeedAvailable from "./NoFeedAvailable"; import FeedControls from "./FeedControls"; import Fullscreen from "../../CAREUI/misc/Fullscreen"; +import FeedWatermark from "./FeedWatermark"; +import CareIcon from "../../CAREUI/icons/CareIcon"; interface Props { children?: React.ReactNode; @@ -24,6 +26,7 @@ interface Props { // Controls constrolsDisabled?: boolean; shortcutsDisabled?: boolean; + onMove?: () => void; } export default function CameraFeed(props: Props) { @@ -76,7 +79,7 @@ export default function CameraFeed(props: Props) { }, onError: props.onStreamError, }); - }, [player.initializeStream, props.onStreamSuccess, props.onStreamError]); + }, [player.initializeStream]); // Start stream on mount useEffect(() => initializeStream(), [initializeStream]); @@ -85,18 +88,22 @@ export default function CameraFeed(props: Props) { setState("loading"); initializeStream(); }; - return ( setFullscreen(false)}>
-
-
- +
+ {props.children} +
+ + {props.asset.name}
@@ -108,12 +115,12 @@ export default function CameraFeed(props: Props) { />
- {props.children}
{/* Notifications */} + {player.status === "playing" && } {/* No Feed informations */} {state === "host_unreachable" && ( @@ -144,6 +151,7 @@ export default function CameraFeed(props: Props) { url={streamUrl} ref={playerRef.current as LegacyRef} controls={false} + pip={false} playsinline playing muted @@ -161,10 +169,12 @@ export default function CameraFeed(props: Props) {
) : (