diff --git a/.gitignore b/.gitignore index d02d753ca3d..d8e2615c727 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ public/build-meta.json !.vscode/launch.json src/supportedBrowsers.ts -# Reason React +# Reason React /lib/bs/** .merlin *.bs.js diff --git a/crowdin.yml b/crowdin.yml index 3bb120df6fc..0ab1a042711 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,3 +1,5 @@ files: - source: /src/Locale/en/*.json translation: /src/Locale/%two_letters_code%/%original_file_name% +bundles: + - 2 diff --git a/cypress/e2e/assets_spec/asset_homepage.cy.ts b/cypress/e2e/assets_spec/AssetHomepage.cy.ts similarity index 100% rename from cypress/e2e/assets_spec/asset_homepage.cy.ts rename to cypress/e2e/assets_spec/AssetHomepage.cy.ts diff --git a/cypress/e2e/assets_spec/assets_creation.cy.ts b/cypress/e2e/assets_spec/AssetsCreation.cy.ts similarity index 100% rename from cypress/e2e/assets_spec/assets_creation.cy.ts rename to cypress/e2e/assets_spec/AssetsCreation.cy.ts diff --git a/cypress/e2e/assets_spec/assets_manage.cy.ts b/cypress/e2e/assets_spec/AssetsManage.cy.ts similarity index 100% rename from cypress/e2e/assets_spec/assets_manage.cy.ts rename to cypress/e2e/assets_spec/AssetsManage.cy.ts diff --git a/cypress/e2e/auth_spec/forget_password.cy.ts b/cypress/e2e/auth_spec/ForgotPassword.cy.ts similarity index 100% rename from cypress/e2e/auth_spec/forget_password.cy.ts rename to cypress/e2e/auth_spec/ForgotPassword.cy.ts diff --git a/cypress/e2e/external_results_spec/filter.cy.ts b/cypress/e2e/external_results_spec/ExternalResultsAdvanceFilters.ts similarity index 100% rename from cypress/e2e/external_results_spec/filter.cy.ts rename to cypress/e2e/external_results_spec/ExternalResultsAdvanceFilters.ts diff --git a/cypress/e2e/external_results_spec/external_result.cy.ts b/cypress/e2e/external_results_spec/ExternalResultsHomepage.cy.ts similarity index 95% rename from cypress/e2e/external_results_spec/external_result.cy.ts rename to cypress/e2e/external_results_spec/ExternalResultsHomepage.cy.ts index d0d78e5f5f6..6a37d8fb2d0 100644 --- a/cypress/e2e/external_results_spec/external_result.cy.ts +++ b/cypress/e2e/external_results_spec/ExternalResultsHomepage.cy.ts @@ -33,7 +33,9 @@ describe("Edit Profile Testing", () => { cy.get("div").contains("Import/Export").click(); cy.get("div").contains("Import Results").click(); cy.get("[data-testid=import-file]") - .selectFile("cypress/fixtures/externalresultsample.csv", { force: true }) + .selectFile("cypress/fixtures/external-result-sample.csv", { + force: true, + }) .wait(100); cy.submitButton("Import"); cy.wait("@import").then((interception) => { diff --git a/cypress/e2e/facility_spec/facility_creation.cy.ts b/cypress/e2e/facility_spec/FacilityCreation.cy.ts similarity index 100% rename from cypress/e2e/facility_spec/facility_creation.cy.ts rename to cypress/e2e/facility_spec/FacilityCreation.cy.ts diff --git a/cypress/e2e/facility_spec/facility_homepage.cy.ts b/cypress/e2e/facility_spec/FacilityHomepage.cy.ts similarity index 100% rename from cypress/e2e/facility_spec/facility_homepage.cy.ts rename to cypress/e2e/facility_spec/FacilityHomepage.cy.ts diff --git a/cypress/e2e/facility_spec/inventory.cy.ts b/cypress/e2e/facility_spec/FacilityInventory.cy.ts similarity index 100% rename from cypress/e2e/facility_spec/inventory.cy.ts rename to cypress/e2e/facility_spec/FacilityInventory.cy.ts diff --git a/cypress/e2e/facility_spec/locations.cy.ts b/cypress/e2e/facility_spec/FacilityLocation.cy.ts similarity index 100% rename from cypress/e2e/facility_spec/locations.cy.ts rename to cypress/e2e/facility_spec/FacilityLocation.cy.ts diff --git a/cypress/e2e/facility_spec/facility_manage.cy.ts b/cypress/e2e/facility_spec/FacilityManage.cy.ts similarity index 99% rename from cypress/e2e/facility_spec/facility_manage.cy.ts rename to cypress/e2e/facility_spec/FacilityManage.cy.ts index 868a26de022..e4f4ba40ff1 100644 --- a/cypress/e2e/facility_spec/facility_manage.cy.ts +++ b/cypress/e2e/facility_spec/FacilityManage.cy.ts @@ -44,7 +44,7 @@ describe("Facility Manage Functions", () => { // It's only button functionality because we can't access S3 bucket in local facilityManage.clickCoverImage(); facilityManage.verifyUploadButtonVisible(); - facilityManage.uploadCoverImage("facilitycoverimage.jpg"); + facilityManage.uploadCoverImage("facility-cover-image.jpg"); facilityManage.clickSaveCoverImage(); }); diff --git a/cypress/e2e/patient_spec/patient_consultation.cy.ts b/cypress/e2e/patient_spec/PatientConsultationCreation.cy.ts similarity index 100% rename from cypress/e2e/patient_spec/patient_consultation.cy.ts rename to cypress/e2e/patient_spec/PatientConsultationCreation.cy.ts diff --git a/cypress/e2e/patient_spec/patient_discharge.cy.ts b/cypress/e2e/patient_spec/PatientConsultationDischarge.cy.ts similarity index 100% rename from cypress/e2e/patient_spec/patient_discharge.cy.ts rename to cypress/e2e/patient_spec/PatientConsultationDischarge.cy.ts diff --git a/cypress/e2e/patient_spec/PatientDoctorNotes.cy.ts b/cypress/e2e/patient_spec/PatientDoctorNotes.cy.ts new file mode 100644 index 00000000000..5b0ceac0c8e --- /dev/null +++ b/cypress/e2e/patient_spec/PatientDoctorNotes.cy.ts @@ -0,0 +1,61 @@ +import LoginPage from "../../pageobject/Login/LoginPage"; +import { PatientPage } from "../../pageobject/Patient/PatientCreation"; +import { PatientDoctorNotes } from "../../pageobject/Patient/PatientDoctorNotes"; + +describe("Patient Discussion notes in the consultation page", () => { + const loginPage = new LoginPage(); + const patientPage = new PatientPage(); + const patientDoctorNotes = new PatientDoctorNotes(); + const patientName = "Dummy Patient 4"; + const patientNurseNote = "Test nurse Notes"; + const patientNurseReplyNote = "Test nurse reply Notes"; + const discussionNotesSubscribeWarning = + "Please subscribe to notifications to get live updates on discussion notes."; + const discussionNotesSuccessMessage = "Note added successfully"; + + before(() => { + loginPage.loginAsDisctrictAdmin(); + cy.saveLocalStorage(); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.clearLocalStorage(/filters--.+/); + cy.awaitUrl("/patients"); + }); + + it("Create a nurse note for a patient and verify both ID received the messages", () => { + // Create a doctor notes a with a district admin + patientPage.visitPatient(patientName); + patientDoctorNotes.visitDiscussionNotesPage(); + cy.verifyNotification(discussionNotesSubscribeWarning); + cy.closeNotification(); + // switch the switch to nurse note, as the bydefault is doctornotes + patientDoctorNotes.selectNurseDiscussion(); + patientDoctorNotes.addDiscussionNotes(patientNurseNote); + patientDoctorNotes.postDiscussionNotes(); + cy.verifyNotification(discussionNotesSuccessMessage); + cy.closeNotification(); + // verify the auto-switching of tab to nurse notes if the user is a nurse + cy.get("p").contains("Sign Out").click(); + loginPage.loginManuallyAsNurse(); + loginPage.ensureLoggedIn(); + cy.visit("/patients"); + patientPage.visitPatient(patientName); + patientDoctorNotes.visitDiscussionNotesPage(); + // verify the message is received from admin + cy.verifyNotification(discussionNotesSubscribeWarning); + cy.closeNotification(); + patientDoctorNotes.verifyDiscussionMessage(patientNurseNote); + // Post a reply comment to the message + patientDoctorNotes.addDiscussionNotes(patientNurseReplyNote); + patientDoctorNotes.postDiscussionNotes(); + cy.verifyNotification(discussionNotesSuccessMessage); + cy.closeNotification(); + patientDoctorNotes.verifyDiscussionMessage(patientNurseReplyNote); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); +}); diff --git a/cypress/e2e/patient_spec/patient_fileupload.cy.ts b/cypress/e2e/patient_spec/PatientFileUpload.ts similarity index 100% rename from cypress/e2e/patient_spec/patient_fileupload.cy.ts rename to cypress/e2e/patient_spec/PatientFileUpload.ts diff --git a/cypress/e2e/patient_spec/patient_logupdate.cy.ts b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts similarity index 70% rename from cypress/e2e/patient_spec/patient_logupdate.cy.ts rename to cypress/e2e/patient_spec/PatientLogUpdate.cy.ts index 13e69e33211..923d9410657 100644 --- a/cypress/e2e/patient_spec/patient_logupdate.cy.ts +++ b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts @@ -27,6 +27,12 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { const patientSpo2 = "15"; const patientRhythmType = "Regular"; const patientRhythm = "Normal Rhythm"; + const patientEtco2 = "50"; + const patientOxygenFlowRate = "40"; + const patientBloodSugar = "52"; + const patientInsulinDosage = "56"; + const patientFluidBalance = "500"; + const patientNetBalance = "1000"; before(() => { loginPage.loginAsDisctrictAdmin(); @@ -39,6 +45,88 @@ 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"); + patientLogupdate.clickLogupdate(); + cy.verifyNotification("Please assign a bed to the patient"); + patientLogupdate.selectBed("Dummy Bed 2"); + cy.closeNotification(); + patientLogupdate.clickLogupdate(); + patientLogupdate.selectPatientCategory(patientCategory); + patientLogupdate.selectRoundType("Detailed Update"); + cy.submitButton("Save and Continue"); + cy.verifyNotification("Detailed Update created successfully"); + cy.closeNotification(); + // Select two Section - First One is Respiratory Support + patientLogupdate.selectCriticalCareSection("Respiratory Support"); + patientLogupdate.selectNoBilateralAirFlow(); + patientLogupdate.typeEtco2(patientEtco2); + patientLogupdate.selectOxygenSupport(); + patientLogupdate.typeOxygenFlowRate(patientOxygenFlowRate); + patientLogupdate.typeVentilatorSpo2(patientSpo2); + cy.submitButton("Update Details"); + cy.verifyNotification("Respiratory Support details succesfully updated."); + cy.closeNotification(); + // Second Section will be Blood Sugar + patientLogupdate.selectCriticalCareSection("Blood Sugar"); + patientLogupdate.typeBloodSugar(patientBloodSugar); + patientLogupdate.typeInsulinDosage(patientInsulinDosage); + cy.get("#insulin_intake_frequency-option-BD").click(); + cy.submitButton("Update Details"); + cy.verifyNotification("Blood Sugar details succesfully updated."); + // Submit the form and verify the details + cy.submitButton("Complete"); + cy.verifyNotification("Detailed Log Update filed successfully"); + cy.closeNotification(); + cy.contains("button", "Daily Rounds").click(); + patientLogupdate.clickLogUpdateViewDetails( + "#dailyround-entry", + patientCategory, + ); + cy.verifyContentPresence("#respiratory-support", [ + patientEtco2, + patientOxygenFlowRate, + ]); + cy.verifyContentPresence("#blood-sugar", [ + patientBloodSugar, + patientInsulinDosage, + ]); + // Go back and edit the data on a third section + patientLogupdate.clickGoBackConsultation(); + cy.contains("button", "Daily Rounds").click(); + patientLogupdate.clickLogUpdateUpdateLog( + "#dailyround-entry", + patientCategory, + ); + patientLogupdate.selectCriticalCareSection("Dialysis"); + patientLogupdate.typeFluidBalance(patientFluidBalance); + patientLogupdate.typeNetBalance(patientNetBalance); + cy.submitButton("Update Details"); + cy.verifyNotification("Dialysis details succesfully updated."); + cy.closeNotification(); + cy.submitButton("Complete"); + cy.verifyNotification("Detailed Log Update filed successfully"); + cy.closeNotification(); + //Reverify the editted and newly added data + cy.contains("button", "Daily Rounds").click(); + patientLogupdate.clickLogUpdateViewDetails( + "#dailyround-entry", + patientCategory, + ); + cy.verifyContentPresence("#respiratory-support", [ + patientEtco2, + patientOxygenFlowRate, + ]); + cy.verifyContentPresence("#blood-sugar", [ + patientBloodSugar, + patientInsulinDosage, + ]); + cy.verifyContentPresence("#dialysis", [ + patientFluidBalance, + patientNetBalance, + ]); + }); + it("Create a new Progress log update for a admitted patient and edit it", () => { patientPage.visitPatient("Dummy Patient 12"); patientLogupdate.clickLogupdate(); @@ -74,10 +162,10 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { cy.closeNotification(); // Submit the doctors log update cy.submitButton("Save and Continue"); - cy.verifyNotification("Progress Note log created successfully"); + cy.verifyNotification("Progress Note created successfully"); cy.closeNotification(); // modify the relevant critical care log update - cy.contains("button", "Neurological Monitoring").click(); + 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"); @@ -91,7 +179,10 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { cy.closeNotification(); // Verify the data reflection cy.contains("button", "Daily Rounds").click(); - patientLogupdate.clickLogupdateCard("#dailyround-entry", patientCategory); + patientLogupdate.clickLogUpdateViewDetails( + "#dailyround-entry", + patientCategory, + ); cy.verifyContentPresence("#consultation-preview", [ patientCategory, patientTemperature, @@ -101,7 +192,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeSystolic(patientModifiedSystolic); patientLogupdate.typeDiastolic(patientModifiedDiastolic); cy.submitButton("Continue"); - cy.verifyNotification("Progress Note log updated successfully"); + cy.verifyNotification("Progress Note updated successfully"); }); it("Create a new TeleIcu log update for a domicilary care patient", () => { @@ -113,7 +204,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { cy.closeNotification(); patientLogupdate.clickLogupdate(); patientLogupdate.typePhysicalExamination(physicalExamination); - patientLogupdate.selectRoundType("Telemedicine"); + patientLogupdate.selectRoundType("Tele-medicine Log"); patientLogupdate.typeOtherDetails(otherExamination); patientLogupdate.selectSymptomsDate("01012024"); patientLogupdate.typeAndMultiSelectSymptoms("fe", ["Fever"]); @@ -128,7 +219,7 @@ 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("Telemedicine log created successfully"); + cy.verifyNotification("Tele-medicine Log created successfully"); }); it("Create a new Normal Log update for a domicilary care patient and edit it", () => { @@ -154,11 +245,14 @@ 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("Brief Update log created successfully"); + cy.verifyNotification("Brief Update created successfully"); cy.closeNotification(); // edit the card and verify the data. cy.contains("button", "Daily Rounds").click(); - patientLogupdate.clickLogupdateCard("#dailyround-entry", patientCategory); + patientLogupdate.clickLogUpdateViewDetails( + "#dailyround-entry", + patientCategory, + ); cy.verifyContentPresence("#consultation-preview", [ patientCategory, patientDiastolic, @@ -177,9 +271,12 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.clickClearButtonInElement("#diastolic"); patientLogupdate.typeDiastolic(patientModifiedDiastolic); cy.submitButton("Continue"); - cy.verifyNotification("Brief Update log updated successfully"); + cy.verifyNotification("Brief Update updated successfully"); cy.contains("button", "Daily Rounds").click(); - patientLogupdate.clickLogupdateCard("#dailyround-entry", patientCategory); + patientLogupdate.clickLogUpdateViewDetails( + "#dailyround-entry", + patientCategory, + ); cy.verifyContentPresence("#consultation-preview", [ patientModifiedDiastolic, patientModifiedSystolic, @@ -210,7 +307,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { cy.get("#consciousness_level-option-RESPONDS_TO_PAIN").click(); cy.submitButton("Save"); cy.wait(2000); - cy.verifyNotification("Brief Update log created successfully"); + cy.verifyNotification("Brief Update created successfully"); // Verify the card content cy.get("#basic-information").scrollIntoView(); cy.verifyContentPresence("#encounter-symptoms", [additionalSymptoms]); @@ -233,7 +330,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeRespiratory(patientRespiratory); cy.get("#consciousness_level-option-RESPONDS_TO_PAIN").click(); cy.submitButton("Save"); - cy.verifyNotification("Brief Update log created successfully"); + cy.verifyNotification("Brief Update created successfully"); cy.closeNotification(); cy.verifyContentPresence("#consultation-buttons", ["9"]); // Verify the Incomplete data will give blank info @@ -243,7 +340,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeDiastolic(patientDiastolic); patientLogupdate.typePulse(patientPulse); cy.submitButton("Save"); - cy.verifyNotification("Brief Update log created successfully"); + cy.verifyNotification("Brief 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/PatientPrescription.cy.ts similarity index 100% rename from cypress/e2e/patient_spec/patient_prescription.cy.ts rename to cypress/e2e/patient_spec/PatientPrescription.cy.ts diff --git a/cypress/e2e/patient_spec/patient_registration.cy.ts b/cypress/e2e/patient_spec/PatientRegistration.cy.ts similarity index 98% rename from cypress/e2e/patient_spec/patient_registration.cy.ts rename to cypress/e2e/patient_spec/PatientRegistration.cy.ts index 2e7ce853fc5..ef92c2e9bd1 100644 --- a/cypress/e2e/patient_spec/patient_registration.cy.ts +++ b/cypress/e2e/patient_spec/PatientRegistration.cy.ts @@ -101,6 +101,8 @@ describe("Patient Creation with consultation", () => { facilityPage.selectLocalBody(patientOneLocalbody); facilityPage.selectWard(patientOneWard); patientPage.selectPatientOccupation(patientOccupation); + patientPage.selectSocioeconomicStatus("MIDDLE_CLASS"); + patientPage.selectDomesticHealthcareSupport("FAMILY_MEMBER"); // Patient Medical History patientMedicalHistory.typePatientPresentHealth(patientOnePresentHealth); patientMedicalHistory.typePatientOngoingMedication( @@ -130,6 +132,8 @@ describe("Patient Creation with consultation", () => { yearOfBirth, patientOneBloodGroup, patientOccupation, + "Middle Class", + "Family member", ); patientMedicalHistory.verifyPatientMedicalDetails( patientOnePresentHealth, diff --git a/cypress/e2e/patient_spec/patient_manage.cy.ts b/cypress/e2e/patient_spec/patient_manage.cy.ts deleted file mode 100644 index 22d77aa2fda..00000000000 --- a/cypress/e2e/patient_spec/patient_manage.cy.ts +++ /dev/null @@ -1,48 +0,0 @@ -import LoginPage from "../../pageobject/Login/LoginPage"; -import { PatientConsultationPage } from "../../pageobject/Patient/PatientConsultation"; -import { PatientPage } from "../../pageobject/Patient/PatientCreation"; - -describe("Patient", () => { - const loginPage = new LoginPage(); - const patientPage = new PatientPage(); - const patientConsultationPage = new PatientConsultationPage(); - - before(() => { - loginPage.loginAsDisctrictAdmin(); - cy.saveLocalStorage(); - }); - - beforeEach(() => { - cy.restoreLocalStorage(); - cy.clearLocalStorage(/filters--.+/); - cy.awaitUrl("/patients"); - }); - - // it("Create Patient shift requests.", () => { - // patientPage.visitPatient(); - // patientConsultationPage.visitShiftRequestPage(); - // patientConsultationPage.enterPatientShiftDetails( - // "Test User", - // phone_number, - // "Dummy Shifting", - // "Reason" - // ); - // patientConsultationPage.createShiftRequest(); - // patientConsultationPage.verifySuccessNotification( - // "Shift request created successfully" - // ); - // }); - // commented out the shifting request, as logic need to be re-visited - - it("Post discussion notes for an already created patient", () => { - patientPage.visitPatient("Dummy Patient 3"); - patientConsultationPage.visitDoctorNotesPage(); - patientConsultationPage.addDoctorsNotes("Test Doctor Notes"); - patientConsultationPage.postDoctorNotes(); - cy.verifyNotification("Note added successfully"); - }); - - afterEach(() => { - cy.saveLocalStorage(); - }); -}); diff --git a/cypress/e2e/resource_spec/filter.cy.ts b/cypress/e2e/resource_spec/ResourcesAdvanceFilters.cy.ts similarity index 100% rename from cypress/e2e/resource_spec/filter.cy.ts rename to cypress/e2e/resource_spec/ResourcesAdvanceFilters.cy.ts diff --git a/cypress/e2e/resource_spec/resources.cy.ts b/cypress/e2e/resource_spec/ResourcesHomepage.cy.ts similarity index 100% rename from cypress/e2e/resource_spec/resources.cy.ts rename to cypress/e2e/resource_spec/ResourcesHomepage.cy.ts diff --git a/cypress/e2e/sample_test_spec/filter.cy.ts b/cypress/e2e/sample_test_spec/SampleTestAdvanceFilters.cy.ts similarity index 100% rename from cypress/e2e/sample_test_spec/filter.cy.ts rename to cypress/e2e/sample_test_spec/SampleTestAdvanceFilters.cy.ts diff --git a/cypress/e2e/sample_test_spec/sample_test.cy.ts b/cypress/e2e/sample_test_spec/SampleTestHomepage.cy.ts similarity index 100% rename from cypress/e2e/sample_test_spec/sample_test.cy.ts rename to cypress/e2e/sample_test_spec/SampleTestHomepage.cy.ts diff --git a/cypress/e2e/shifting_spec/filter.cy.ts b/cypress/e2e/shifting_spec/ShiftingAdvanceFilters.cy.ts similarity index 100% rename from cypress/e2e/shifting_spec/filter.cy.ts rename to cypress/e2e/shifting_spec/ShiftingAdvanceFilters.cy.ts diff --git a/cypress/e2e/shifting_spec/shifting.cy.ts b/cypress/e2e/shifting_spec/ShiftingHomepage.cy.ts similarity index 100% rename from cypress/e2e/shifting_spec/shifting.cy.ts rename to cypress/e2e/shifting_spec/ShiftingHomepage.cy.ts diff --git a/cypress/e2e/users_spec/user_creation.cy.ts b/cypress/e2e/users_spec/UsersCreation.cy.ts similarity index 100% rename from cypress/e2e/users_spec/user_creation.cy.ts rename to cypress/e2e/users_spec/UsersCreation.cy.ts diff --git a/cypress/e2e/users_spec/user_homepage.cy.ts b/cypress/e2e/users_spec/UsersHomepage.cy.ts similarity index 57% rename from cypress/e2e/users_spec/user_homepage.cy.ts rename to cypress/e2e/users_spec/UsersHomepage.cy.ts index b1ecd567d86..0cf25cacfc8 100644 --- a/cypress/e2e/users_spec/user_homepage.cy.ts +++ b/cypress/e2e/users_spec/UsersHomepage.cy.ts @@ -3,11 +3,18 @@ import { UserPage } from "../../pageobject/Users/UserSearch"; describe("User Homepage", () => { const userPage = new UserPage(); - const usernameToTest = "devdoctor"; - const currentuser = "devdistrictadmin"; const loginPage = new LoginPage(); - const phone_number = "9876543219"; - const alt_phone_number = "9876543219"; + const currentuser = "devdistrictadmin"; + const firstName = "Dummy"; + const lastName = "Nurse"; + const role = "Nurse"; + const state = "Kerala"; + const district = "Ernakulam"; + const phoneNumber = "8878825662"; + const altPhoneNumber = "8878825662"; + const homeFacility = "Dummy Facility 40"; + const nurseUserName = "dummynurse1"; + const doctorUserName = "devdoctor"; before(() => { loginPage.loginAsDisctrictAdmin(); @@ -22,33 +29,32 @@ describe("User Homepage", () => { it("User advance filter functionality", () => { userPage.clickAdvancedFilters(); - userPage.typeInFirstName("Dev"); - userPage.typeInLastName("Doctor"); - userPage.selectRole("Doctor"); - userPage.selectState("Kerala"); - userPage.selectDistrict("Ernakulam"); - userPage.typeInPhoneNumber(phone_number); - userPage.typeInAltPhoneNumber(alt_phone_number); - userPage.selectHomeFacility("Dummy Facility 40"); + userPage.typeInFirstName(firstName); + userPage.typeInLastName(lastName); + userPage.selectRole(role); + userPage.selectState(state); + userPage.selectDistrict(district); + userPage.typeInPhoneNumber(phoneNumber); + userPage.typeInAltPhoneNumber(altPhoneNumber); + userPage.selectHomeFacility(homeFacility); userPage.applyFilter(); - userPage.verifyUrlafteradvancefilter(); - userPage.checkUsernameText(usernameToTest); - userPage.verifyDataTestIdText("First Name", "First Name: Dev"); - userPage.verifyDataTestIdText("Last Name", "Last Name: Doctor"); + userPage.checkUsernameText(nurseUserName); + // Verify the badges related to the data + userPage.verifyDataTestIdText("First Name", `First Name: ${firstName}`); + userPage.verifyDataTestIdText("Last Name", `Last Name: ${lastName}`); userPage.verifyDataTestIdText( "Phone Number", - "Phone Number: +919876543219", + `Phone Number: +91${phoneNumber}`, ); userPage.verifyDataTestIdText( "WhatsApp no.", - "WhatsApp no.: +919876543219", + `WhatsApp no.: +91${altPhoneNumber}`, ); - userPage.verifyDataTestIdText("Role", "Role: Doctor"); + userPage.verifyDataTestIdText("Role", `Role: ${role}`); userPage.verifyDataTestIdText( "Home Facility", - "Home Facility: Dummy Facility 40", + `Home Facility: ${homeFacility}`, ); - userPage.verifyDataTestIdText("District", "District: Ernakulam"); userPage.clearFilters(); userPage.verifyDataTestIdNotVisible("First Name"); userPage.verifyDataTestIdNotVisible("Last Name"); @@ -61,14 +67,14 @@ describe("User Homepage", () => { it("Search by username", () => { userPage.checkSearchInputVisibility(); - userPage.typeInSearchInput(usernameToTest); - userPage.checkUrlForUsername(usernameToTest); - userPage.checkUsernameText(usernameToTest); + userPage.typeInSearchInput(doctorUserName); + userPage.checkUrlForUsername(doctorUserName); + userPage.checkUsernameText(doctorUserName); userPage.checkUsernameBadgeVisibility(true); userPage.clearSearchInput(); userPage.checkUsernameBadgeVisibility(false); - userPage.typeInSearchInput(usernameToTest); - userPage.checkUsernameText(usernameToTest); + userPage.typeInSearchInput(doctorUserName); + userPage.checkUsernameText(doctorUserName); userPage.clickRemoveIcon(); userPage.checkUsernameBadgeVisibility(false); userPage.checkUsernameText(currentuser); diff --git a/cypress/e2e/users_spec/user_manage.cy.ts b/cypress/e2e/users_spec/UsersManage.cy.ts similarity index 100% rename from cypress/e2e/users_spec/user_manage.cy.ts rename to cypress/e2e/users_spec/UsersManage.cy.ts diff --git a/cypress/e2e/users_spec/user_profile.cy.ts b/cypress/e2e/users_spec/UsersProfile.cy.ts similarity index 100% rename from cypress/e2e/users_spec/user_profile.cy.ts rename to cypress/e2e/users_spec/UsersProfile.cy.ts diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json deleted file mode 100644 index 02e4254378e..00000000000 --- a/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/cypress/fixtures/externalresultsample.csv b/cypress/fixtures/external-result-sample.csv similarity index 100% rename from cypress/fixtures/externalresultsample.csv rename to cypress/fixtures/external-result-sample.csv diff --git a/cypress/fixtures/facilitycoverimage.jpg b/cypress/fixtures/facility-cover-image.jpg similarity index 100% rename from cypress/fixtures/facilitycoverimage.jpg rename to cypress/fixtures/facility-cover-image.jpg diff --git a/cypress/fixtures/sampleAsset.xlsx b/cypress/fixtures/sample-asset.xlsx similarity index 100% rename from cypress/fixtures/sampleAsset.xlsx rename to cypress/fixtures/sample-asset.xlsx diff --git a/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts index 2de13561a0b..41d3c4ca8d0 100644 --- a/cypress/pageobject/Asset/AssetCreation.ts +++ b/cypress/pageobject/Asset/AssetCreation.ts @@ -246,7 +246,7 @@ export class AssetPage { importAssetFile() { cy.get("[data-testid=import-file]") - .selectFile("cypress/fixtures/sampleAsset.xlsx", { force: true }) + .selectFile("cypress/fixtures/sample-asset.xlsx", { force: true }) .wait(100); } diff --git a/cypress/pageobject/Asset/AssetFilters.ts b/cypress/pageobject/Asset/AssetFilters.ts index 33363f2d161..ac83ca97c0b 100644 --- a/cypress/pageobject/Asset/AssetFilters.ts +++ b/cypress/pageobject/Asset/AssetFilters.ts @@ -3,7 +3,7 @@ export class AssetFilters { facilityName: string, assetStatus: string, assetClass: string, - assetLocation: string + assetLocation: string, ) { cy.contains("Advanced Filters").click(); cy.get("input[name='Facilities']") diff --git a/cypress/pageobject/Asset/AssetSearch.ts b/cypress/pageobject/Asset/AssetSearch.ts index b1ccb2f71c9..653c7bdd89c 100644 --- a/cypress/pageobject/Asset/AssetSearch.ts +++ b/cypress/pageobject/Asset/AssetSearch.ts @@ -17,7 +17,7 @@ export class AssetSearchPage { verifyBadgeContent(expectedText: string) { cy.get("[data-testid='Name/Serial No./QR ID']").should( "contain", - expectedText + expectedText, ); } @@ -54,7 +54,7 @@ export class AssetSearchPage { verifyAssetListContains(dummyCameraText: string) { cy.get("[data-testid='created-asset-list']").should( "contain", - dummyCameraText + dummyCameraText, ); } } diff --git a/cypress/pageobject/Login/LoginPage.ts b/cypress/pageobject/Login/LoginPage.ts index c75a024ae03..3dfd267fab2 100644 --- a/cypress/pageobject/Login/LoginPage.ts +++ b/cypress/pageobject/Login/LoginPage.ts @@ -19,6 +19,12 @@ class LoginPage { cy.get("button").contains("Login").click(); } + loginManuallyAsNurse(): void { + cy.get("input[id='username']").click().type("dummynurse1"); + cy.get("input[id='password']").click().type("Coronasafe@123"); + cy.get("button").contains("Login").click(); + } + login(username: string, password: string): void { cy.loginByApi(username, password); } diff --git a/cypress/pageobject/Patient/PatientConsultation.ts b/cypress/pageobject/Patient/PatientConsultation.ts index e4a9810141e..cc5b8e19465 100644 --- a/cypress/pageobject/Patient/PatientConsultation.ts +++ b/cypress/pageobject/Patient/PatientConsultation.ts @@ -110,30 +110,4 @@ export class PatientConsultationPage { ); cy.wait(3000); } - - visitShiftRequestPage() { - cy.get("#create_shift_request").click(); - } - - createShiftRequest() { - cy.intercept("POST", "**/api/v1/shift/").as("createShiftRequest"); - cy.get("#submit").click(); - cy.wait("@createShiftRequest").its("response.statusCode").should("eq", 201); - } - - visitDoctorNotesPage() { - cy.get("#patient_doctor_notes").scrollIntoView(); - cy.get("#patient_doctor_notes").click(); - } - - addDoctorsNotes(notes: string) { - cy.get("#doctor_notes_textarea").scrollIntoView(); - cy.get("#doctor_notes_textarea").click().type(notes); - } - - postDoctorNotes() { - cy.intercept("POST", "**/api/v1/patient/*/notes").as("postDoctorNotes"); - cy.get("#add_doctor_note_button").click(); - cy.wait("@postDoctorNotes").its("response.statusCode").should("eq", 201); - } } diff --git a/cypress/pageobject/Patient/PatientCreation.ts b/cypress/pageobject/Patient/PatientCreation.ts index 739601d5b56..e037f0888f0 100644 --- a/cypress/pageobject/Patient/PatientCreation.ts +++ b/cypress/pageobject/Patient/PatientCreation.ts @@ -14,6 +14,7 @@ export class PatientPage { cy.get("#name").click().type(patientName); cy.intercept("GET", "**/api/v1/consultation/**").as("getPatient"); cy.get("#patient-name-list").contains(patientName).click(); + cy.wait(2000); cy.wait("@getPatient").its("response.statusCode").should("eq", 200); cy.get("#patient-name-consultation") .should("be.visible") @@ -116,6 +117,14 @@ export class PatientPage { cy.searchAndSelectOption("#occupation", occupation); } + selectSocioeconomicStatus(value: string) { + cy.selectRadioOption("socioeconomic_status", value); + } + + selectDomesticHealthcareSupport(value: string) { + cy.selectRadioOption("domestic_healthcare_support", value); + } + clickCreatePatient() { cy.intercept("POST", "**/api/v1/patient/").as("createPatient"); cy.get("button[data-testid='submit-button']").click(); @@ -164,6 +173,8 @@ export class PatientPage { yearOfBirth, bloodGroup, occupation, + socioeconomicStatus = null, + domesticHealthcareSupport = null, isAntenatal = false, isPostPartum = false, ) { @@ -177,6 +188,9 @@ export class PatientPage { expect($dashboard).to.contain(yearOfBirth); expect($dashboard).to.contain(bloodGroup); expect($dashboard).to.contain(occupation); + socioeconomicStatus && expect($dashboard).to.contain(socioeconomicStatus); + domesticHealthcareSupport && + expect($dashboard).to.contain(domesticHealthcareSupport); if (isAntenatal) { expect($dashboard).to.contain("Antenatal"); diff --git a/cypress/pageobject/Patient/PatientDoctorNotes.ts b/cypress/pageobject/Patient/PatientDoctorNotes.ts new file mode 100644 index 00000000000..9538b0eed3b --- /dev/null +++ b/cypress/pageobject/Patient/PatientDoctorNotes.ts @@ -0,0 +1,29 @@ +export class PatientDoctorNotes { + visitDiscussionNotesPage() { + cy.get("#patient_discussion_notes").scrollIntoView(); + cy.get("#patient_discussion_notes").click(); + } + + addDiscussionNotes(notes: string) { + cy.wait(2000); + cy.get("#discussion_notes_textarea").scrollIntoView(); + cy.get("#discussion_notes_textarea").click().type(notes); + } + + selectNurseDiscussion() { + cy.get("#patient-note-tab-Nurses").scrollIntoView(); + cy.get("#patient-note-tab-Nurses").click(); + } + + verifyDiscussionMessage(text: string) { + cy.get("#patient-notes-list").contains(text); + } + + postDiscussionNotes() { + cy.intercept("POST", "**/api/v1/patient/*/notes").as("postDiscussionNotes"); + cy.get("#add_doctor_note_button").click(); + cy.wait("@postDiscussionNotes") + .its("response.statusCode") + .should("eq", 201); + } +} diff --git a/cypress/pageobject/Patient/PatientFileupload.ts b/cypress/pageobject/Patient/PatientFileupload.ts index 3f353cb8807..0616a19729c 100644 --- a/cypress/pageobject/Patient/PatientFileupload.ts +++ b/cypress/pageobject/Patient/PatientFileupload.ts @@ -40,7 +40,7 @@ export class PatientFileUpload { uploadFile() { cy.get("#file_upload_patient").selectFile( - "cypress/fixtures/sampleAsset.xlsx", + "cypress/fixtures/sample-asset.xlsx", { force: true }, ); } diff --git a/cypress/pageobject/Patient/PatientInsurance.ts b/cypress/pageobject/Patient/PatientInsurance.ts index be4c25c5535..60eaefffa44 100644 --- a/cypress/pageobject/Patient/PatientInsurance.ts +++ b/cypress/pageobject/Patient/PatientInsurance.ts @@ -2,7 +2,7 @@ class PatientInsurance { typePatientInsuranceDetail( containerId: string, fieldId: string, - value: string + value: string, ) { cy.get(`#${containerId}`).within(() => { cy.get(`#${fieldId}`).click().type(value); diff --git a/cypress/pageobject/Patient/PatientLogupdate.ts b/cypress/pageobject/Patient/PatientLogupdate.ts index ca238fe115c..8a4c11ab25b 100644 --- a/cypress/pageobject/Patient/PatientLogupdate.ts +++ b/cypress/pageobject/Patient/PatientLogupdate.ts @@ -70,13 +70,20 @@ class PatientLogupdate { cy.get("#rhythm_detail").click().type(rhythm); } - clickLogupdateCard(element, patientCategory) { + clickLogUpdateViewDetails(element, patientCategory) { cy.get(element).scrollIntoView(); cy.verifyContentPresence(element, [patientCategory]); cy.get(element).first().contains("View Details").click(); cy.wait(3000); } + clickLogUpdateUpdateLog(element, patientCategory) { + cy.get(element).scrollIntoView(); + cy.verifyContentPresence(element, [patientCategory]); + cy.get(element).first().contains("Update Log").click(); + cy.wait(3000); + } + clickUpdateDetail() { cy.verifyAndClickElement("#consultation-preview", "Update Details"); cy.wait(3000); @@ -90,5 +97,53 @@ class PatientLogupdate { cy.get("#consultation_tab_nav").scrollIntoView(); cy.verifyAndClickElement("#consultation_tab_nav", "Vitals"); } + + selectNoBilateralAirFlow() { + cy.get("#bilateral_air_entry-option-false").click(); + } + + typeEtco2(etco2: string) { + cy.get("#etco2-range-input").type(etco2); + } + + selectOxygenSupport() { + cy.get("#respiratory_support-option-OXYGEN_SUPPORT").click(); + } + + selectNonBreathingModality() { + cy.get("#ventilator_oxygen_modality-option-NON_REBREATHING_MASK").click(); + } + + typeOxygenFlowRate(flowRate: string) { + cy.get("#oxygen_flow_rate-range-input").type(flowRate); + } + + typeVentilatorSpo2(spo2: string) { + cy.get("#ventilator_spo2-range-input").type(spo2); + } + + selectCriticalCareSection(sectionName: string) { + cy.contains("button", sectionName).click(); + } + + typeBloodSugar(bloodSugar: string) { + cy.get("#blood_sugar_level-range-input").type(bloodSugar); + } + + typeInsulinDosage(insulinDosage: string) { + cy.get("#insulin_intake_dose-range-input").type(insulinDosage); + } + + clickGoBackConsultation() { + cy.get("#back-to-consultation").click(); + } + + typeFluidBalance(fluid: string) { + cy.get("#dialysis_fluid_balance-range-input").type(fluid); + } + + typeNetBalance(netBalance: string) { + cy.get("#dialysis_net_balance-range-input").type(netBalance); + } } export default PatientLogupdate; diff --git a/cypress/pageobject/Patient/PatientMedicalHistory.ts b/cypress/pageobject/Patient/PatientMedicalHistory.ts index 1c9b733f3ba..a0dbdd0131a 100644 --- a/cypress/pageobject/Patient/PatientMedicalHistory.ts +++ b/cypress/pageobject/Patient/PatientMedicalHistory.ts @@ -31,7 +31,7 @@ class PatientMedicalHistory { patientSymptoms4, patientSymptoms5, patientSymptoms6, - patientSymptoms7 + patientSymptoms7, ) { cy.get("[data-testid=patient-details]").then(($dashboard) => { cy.url().should("include", "/facility/"); @@ -51,7 +51,7 @@ class PatientMedicalHistory { verifyNoSymptosPresent(patientSymptoms1: string) { cy.get("[data-testid=patient-details]").should( "not.contain", - patientSymptoms1 + patientSymptoms1, ); } } diff --git a/cypress/pageobject/Users/UserSearch.ts b/cypress/pageobject/Users/UserSearch.ts index 56d1a81395d..0c214e92706 100644 --- a/cypress/pageobject/Users/UserSearch.ts +++ b/cypress/pageobject/Users/UserSearch.ts @@ -22,16 +22,6 @@ export class UserPage { cy.url().should("include", `username=${username}`); } - verifyUrlafteradvancefilter() { - cy.url() - .should("include", "first_name=Dev") - .and("include", "last_name=Doctor") - .and("include", "phone_number=%2B919876543219") - .and("include", "alt_phone_number=%2B919876543219") - .and("include", "user_type=Doctor") - .and("include", "district=7"); - } - checkUsernameText(username: string) { cy.get(this.usernameText).should("have.text", username); } diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index c6437505349..86d048e5f41 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -169,6 +169,10 @@ Cypress.Commands.add( }, ); +Cypress.Commands.add("selectRadioOption", (name: string, value: string) => { + cy.get(`input[type='radio'][name='${name}'][value=${value}]`).click(); +}); + Cypress.Commands.add("clickAndTypeDate", (selector: string, date: string) => { cy.get(selector).scrollIntoView(); cy.get(selector).click(); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 9ddfd0c819a..d660246324f 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -23,6 +23,7 @@ declare global { selector: string, symptoms: string | string[], ): Chainable; + selectRadioOption(name: string, value: string): Chainable; typeAndMultiSelectOption( selector: string, input: string, diff --git a/package-lock.json b/package-lock.json index 4615e263405..553224ffc27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,15 @@ "version": "2.5.4", "license": "MIT", "dependencies": { - "@fontsource/inter": "^5.0.18", + "@fontsource/inter": "^5.0.21", "@googlemaps/react-wrapper": "^1.1.35", "@googlemaps/typescript-guards": "^2.0.3", "@headlessui/react": "^2.1.2", "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", - "@sentry/browser": "^8.12.0", + "@sentry/browser": "^8.29.0", "@yudiel/react-qr-scanner": "^2.0.0-beta.3", - "axios": "^1.6.8", + "axios": "^1.7.7", "bowser": "^2.11.0", "browser-image-compression": "^2.0.2", "browserslist-useragent-regexp": "^4.1.3", @@ -44,7 +44,6 @@ "react-infinite-scroll-component": "^6.1.0", "react-markdown": "^8.0.7", "react-pdf": "^9.1.0", - "react-player": "^2.16.0", "react-redux": "^8.1.1", "react-webcam": "^7.2.0", "redux": "^4.2.1", @@ -2550,9 +2549,9 @@ "license": "MIT" }, "node_modules/@fontsource/inter": { - "version": "5.0.18", - "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.0.18.tgz", - "integrity": "sha512-YCsoYPTcs713sI7tLtxaPrIhXAXvEetGg5Ry02ivA8qUOb3fQHojbK/X9HLD5OOKvFUNR2Ynkwb1kR1hVKQHpw==" + "version": "5.0.21", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.0.21.tgz", + "integrity": "sha512-Y4Ph0bYD5IfW8r9PSeiNjLvM6D/Bh5+YT9jOKiBqW3YS+AEBlmy8v74TpTNeZAvL3zHmikHCnzaKw7ob8LZiKQ==" }, "node_modules/@googlemaps/js-api-loader": { "version": "1.16.6", @@ -3765,178 +3764,178 @@ ] }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.12.0.tgz", - "integrity": "sha512-h7HRqED15Qa+DRt8iZGna24Z331nglgjPzdFn4+u+jvnZrehUjH0vjsfuj7qhwSUNZu8Rxi1ZlUYFURjLDTKCA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.29.0.tgz", + "integrity": "sha512-6HpyQkaqPvK6Lnigjlarq/LDYgXT2OBNf24RK7z0ipJSxSIpmtelfzHbnwWYnypNDXfTDdPm97fZEenQHryYJA==", "dependencies": { - "@sentry/core": "8.12.0", - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/browser-utils/node_modules/@sentry/core": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.12.0.tgz", - "integrity": "sha512-y+5Hlf/E45nj2adJy4aUCNBefQbyWIX66Z9bOM6JjnVB0hxCm5H0sYqrFKldYqaeZx6/Q2cgAcGs61krUxNerQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.29.0.tgz", + "integrity": "sha512-scMbZaJ0Ov8NPgWn86EdjhyTLrhvRVbTxjg0imJAvhIvRbblH3xyqye/17Qnk2fOp8TNDOl7TBZHi0NCFQ5HUw==", "dependencies": { - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/browser-utils/node_modules/@sentry/types": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.12.0.tgz", - "integrity": "sha512-pKuW64IjgcklWAOHzPJ02Ej480hyL25TLnYCAfl2TDMrYc+N0bbbH1N7ZxqJpTSVK9IxZPY/t2TRxpQBiyPEcg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.29.0.tgz", + "integrity": "sha512-j4gX3ctzgD4xVWllXAhm6M+kHFEvrFoUPFq60X/pgkjsWCocGuhtNfB0rW43ICG8hCnlz8IYl7O7b8V8qY7SPg==", "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/browser-utils/node_modules/@sentry/utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.12.0.tgz", - "integrity": "sha512-pwYMoOmexz3vsNSOJGPvD2qwp/fsPcr8mkFk67wMM37Y+30KQ8pF4Aq1cc+HBRIn1tKmenzFDPTczSdVPFxm3Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-nb93/m3SjQChQJFqJj3oNW3Rz/12yrT7jypTCire3c2hpYWG2uR5n8VY9UUMTA6HLNvdom6tckK7p3bXGXlF0w==", "dependencies": { - "@sentry/types": "8.12.0" + "@sentry/types": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.12.0.tgz", - "integrity": "sha512-PvQ14wVOPmzRdYdmXD791CqERZZC4jZa5hnyBKBuF6ZpifIQ4Uk7spPu6ZO+Ympx3GtRlpYjk4dbjHyNSfYTwA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.29.0.tgz", + "integrity": "sha512-yAL5YMEFk4XaeVRUGEguydahRzaQrNPAaWRv6k+XRzCv9CGBhxb14KXQc9X/penlauMFcDfgelCPKcTqcf6wDw==", "dependencies": { - "@sentry/core": "8.12.0", - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback/node_modules/@sentry/core": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.12.0.tgz", - "integrity": "sha512-y+5Hlf/E45nj2adJy4aUCNBefQbyWIX66Z9bOM6JjnVB0hxCm5H0sYqrFKldYqaeZx6/Q2cgAcGs61krUxNerQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.29.0.tgz", + "integrity": "sha512-scMbZaJ0Ov8NPgWn86EdjhyTLrhvRVbTxjg0imJAvhIvRbblH3xyqye/17Qnk2fOp8TNDOl7TBZHi0NCFQ5HUw==", "dependencies": { - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback/node_modules/@sentry/types": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.12.0.tgz", - "integrity": "sha512-pKuW64IjgcklWAOHzPJ02Ej480hyL25TLnYCAfl2TDMrYc+N0bbbH1N7ZxqJpTSVK9IxZPY/t2TRxpQBiyPEcg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.29.0.tgz", + "integrity": "sha512-j4gX3ctzgD4xVWllXAhm6M+kHFEvrFoUPFq60X/pgkjsWCocGuhtNfB0rW43ICG8hCnlz8IYl7O7b8V8qY7SPg==", "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback/node_modules/@sentry/utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.12.0.tgz", - "integrity": "sha512-pwYMoOmexz3vsNSOJGPvD2qwp/fsPcr8mkFk67wMM37Y+30KQ8pF4Aq1cc+HBRIn1tKmenzFDPTczSdVPFxm3Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-nb93/m3SjQChQJFqJj3oNW3Rz/12yrT7jypTCire3c2hpYWG2uR5n8VY9UUMTA6HLNvdom6tckK7p3bXGXlF0w==", "dependencies": { - "@sentry/types": "8.12.0" + "@sentry/types": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.12.0.tgz", - "integrity": "sha512-TJceMtzRnY3SCvt3nFDu9rlT00Le7SaV2RL3D7SyDuijvJbWvIw3DRk7yutpF8c9YKO9j6FMa4NlkCJ+YAnnKQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.29.0.tgz", + "integrity": "sha512-Xgv/eYucsm7GaGKms2ClQ02NpD07MxjoTjp1/vYZm0H4Q08dVphVZrQp7hL1oX/VD9mb5SFyyKuuIRqIu7S8RA==", "dependencies": { - "@sentry-internal/browser-utils": "8.12.0", - "@sentry/core": "8.12.0", - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry-internal/browser-utils": "8.29.0", + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.12.0.tgz", - "integrity": "sha512-0slfHZ3TD3MKeBu5NEGuKuecxStX23gts5L3mGFJd/zwsd04A31fhVmo6agIkxnZbOU4GPX/7HPWIeevkvy3ig==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.29.0.tgz", + "integrity": "sha512-W2YbZRvp2lYC50V51fNLcnoIiK1Km4vSc+v6SL7c//lv2qpyumoUAAIDKY+14s8Lgt1RsR6rfZhfheD4O/6WSQ==", "dependencies": { - "@sentry-internal/replay": "8.12.0", - "@sentry/core": "8.12.0", - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry-internal/replay": "8.29.0", + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.12.0.tgz", - "integrity": "sha512-y+5Hlf/E45nj2adJy4aUCNBefQbyWIX66Z9bOM6JjnVB0hxCm5H0sYqrFKldYqaeZx6/Q2cgAcGs61krUxNerQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.29.0.tgz", + "integrity": "sha512-scMbZaJ0Ov8NPgWn86EdjhyTLrhvRVbTxjg0imJAvhIvRbblH3xyqye/17Qnk2fOp8TNDOl7TBZHi0NCFQ5HUw==", "dependencies": { - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/types": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.12.0.tgz", - "integrity": "sha512-pKuW64IjgcklWAOHzPJ02Ej480hyL25TLnYCAfl2TDMrYc+N0bbbH1N7ZxqJpTSVK9IxZPY/t2TRxpQBiyPEcg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.29.0.tgz", + "integrity": "sha512-j4gX3ctzgD4xVWllXAhm6M+kHFEvrFoUPFq60X/pgkjsWCocGuhtNfB0rW43ICG8hCnlz8IYl7O7b8V8qY7SPg==", "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.12.0.tgz", - "integrity": "sha512-pwYMoOmexz3vsNSOJGPvD2qwp/fsPcr8mkFk67wMM37Y+30KQ8pF4Aq1cc+HBRIn1tKmenzFDPTczSdVPFxm3Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-nb93/m3SjQChQJFqJj3oNW3Rz/12yrT7jypTCire3c2hpYWG2uR5n8VY9UUMTA6HLNvdom6tckK7p3bXGXlF0w==", "dependencies": { - "@sentry/types": "8.12.0" + "@sentry/types": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay/node_modules/@sentry/core": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.12.0.tgz", - "integrity": "sha512-y+5Hlf/E45nj2adJy4aUCNBefQbyWIX66Z9bOM6JjnVB0hxCm5H0sYqrFKldYqaeZx6/Q2cgAcGs61krUxNerQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.29.0.tgz", + "integrity": "sha512-scMbZaJ0Ov8NPgWn86EdjhyTLrhvRVbTxjg0imJAvhIvRbblH3xyqye/17Qnk2fOp8TNDOl7TBZHi0NCFQ5HUw==", "dependencies": { - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay/node_modules/@sentry/types": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.12.0.tgz", - "integrity": "sha512-pKuW64IjgcklWAOHzPJ02Ej480hyL25TLnYCAfl2TDMrYc+N0bbbH1N7ZxqJpTSVK9IxZPY/t2TRxpQBiyPEcg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.29.0.tgz", + "integrity": "sha512-j4gX3ctzgD4xVWllXAhm6M+kHFEvrFoUPFq60X/pgkjsWCocGuhtNfB0rW43ICG8hCnlz8IYl7O7b8V8qY7SPg==", "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay/node_modules/@sentry/utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.12.0.tgz", - "integrity": "sha512-pwYMoOmexz3vsNSOJGPvD2qwp/fsPcr8mkFk67wMM37Y+30KQ8pF4Aq1cc+HBRIn1tKmenzFDPTczSdVPFxm3Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-nb93/m3SjQChQJFqJj3oNW3Rz/12yrT7jypTCire3c2hpYWG2uR5n8VY9UUMTA6HLNvdom6tckK7p3bXGXlF0w==", "dependencies": { - "@sentry/types": "8.12.0" + "@sentry/types": "8.29.0" }, "engines": { "node": ">=14.18" @@ -3957,48 +3956,48 @@ } }, "node_modules/@sentry/browser": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.12.0.tgz", - "integrity": "sha512-H82dmr7KQWoS2DQc5dJko5wNepltcEro1EM4mBeL2YmVbNRtoZzD3HQTpbxJJuFsTvEMZevvez5HFlpUgxmIwQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.29.0.tgz", + "integrity": "sha512-aKTy4H/3RI0q9LIeepesjWGlGNeh4HGFfwQjzHME8gcWCQ5LSlzYX4U+hu2yp7r1Jfd9MUTFfOuuLih2HGLGsQ==", "dependencies": { - "@sentry-internal/browser-utils": "8.12.0", - "@sentry-internal/feedback": "8.12.0", - "@sentry-internal/replay": "8.12.0", - "@sentry-internal/replay-canvas": "8.12.0", - "@sentry/core": "8.12.0", - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry-internal/browser-utils": "8.29.0", + "@sentry-internal/feedback": "8.29.0", + "@sentry-internal/replay": "8.29.0", + "@sentry-internal/replay-canvas": "8.29.0", + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/browser/node_modules/@sentry/core": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.12.0.tgz", - "integrity": "sha512-y+5Hlf/E45nj2adJy4aUCNBefQbyWIX66Z9bOM6JjnVB0hxCm5H0sYqrFKldYqaeZx6/Q2cgAcGs61krUxNerQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.29.0.tgz", + "integrity": "sha512-scMbZaJ0Ov8NPgWn86EdjhyTLrhvRVbTxjg0imJAvhIvRbblH3xyqye/17Qnk2fOp8TNDOl7TBZHi0NCFQ5HUw==", "dependencies": { - "@sentry/types": "8.12.0", - "@sentry/utils": "8.12.0" + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/browser/node_modules/@sentry/types": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.12.0.tgz", - "integrity": "sha512-pKuW64IjgcklWAOHzPJ02Ej480hyL25TLnYCAfl2TDMrYc+N0bbbH1N7ZxqJpTSVK9IxZPY/t2TRxpQBiyPEcg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.29.0.tgz", + "integrity": "sha512-j4gX3ctzgD4xVWllXAhm6M+kHFEvrFoUPFq60X/pgkjsWCocGuhtNfB0rW43ICG8hCnlz8IYl7O7b8V8qY7SPg==", "engines": { "node": ">=14.18" } }, "node_modules/@sentry/browser/node_modules/@sentry/utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.12.0.tgz", - "integrity": "sha512-pwYMoOmexz3vsNSOJGPvD2qwp/fsPcr8mkFk67wMM37Y+30KQ8pF4Aq1cc+HBRIn1tKmenzFDPTczSdVPFxm3Q==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-nb93/m3SjQChQJFqJj3oNW3Rz/12yrT7jypTCire3c2hpYWG2uR5n8VY9UUMTA6HLNvdom6tckK7p3bXGXlF0w==", "dependencies": { - "@sentry/types": "8.12.0" + "@sentry/types": "8.29.0" }, "engines": { "node": ">=14.18" @@ -5798,9 +5797,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -5937,12 +5936,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -6947,6 +6946,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -8377,9 +8377,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -10942,11 +10942,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/load-script": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", - "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" - }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -12076,11 +12071,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" - }, "node_modules/merge-refs": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", @@ -14330,11 +14320,6 @@ "react": "^18.3.1" } }, - "node_modules/react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" - }, "node_modules/react-google-recaptcha": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz", @@ -14992,21 +14977,6 @@ } } }, - "node_modules/react-player": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz", - "integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==", - "dependencies": { - "deepmerge": "^4.0.0", - "load-script": "^1.0.0", - "memoize-one": "^5.1.1", - "prop-types": "^15.7.2", - "react-fast-compare": "^3.0.1" - }, - "peerDependencies": { - "react": ">=16.6.0" - } - }, "node_modules/react-redux": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", @@ -15652,9 +15622,9 @@ } }, "node_modules/requirejs": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", - "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz", + "integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==", "dev": true, "bin": { "r_js": "bin/r.js", @@ -19258,4 +19228,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 95cea780b72..cc2c7b955c2 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "scripts": { "build:react": "cross-env NODE_ENV=production vite build", "build": "npm run generate-build-meta && npm run supported-browsers && npm run build:react", - "dev": "vite", + "dev": "npm run supported-browsers && vite", "preview": "cross-env NODE_ENV=production vite preview", "generate-build-meta": "node ./scripts/generate-build-version.js", "test": "snyk test", @@ -41,18 +41,18 @@ "lint": "eslint ./src", "lint-fix": "eslint ./src --fix", "format": "prettier ./src --write", - "supported-browsers": "echo \"export default $(browserslist-useragent-regexp --allowHigherVersions --ignorePatch --ignoreMinor);\" | sed 's/\\x1b\\[[0-9;]*m//g' > src/supportedBrowsers.ts" + "supported-browsers": "node ./scripts/generate-supported-browsers.mjs" }, "dependencies": { - "@fontsource/inter": "^5.0.18", + "@fontsource/inter": "^5.0.21", "@googlemaps/react-wrapper": "^1.1.35", "@googlemaps/typescript-guards": "^2.0.3", "@headlessui/react": "^2.1.2", "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", - "@sentry/browser": "^8.12.0", + "@sentry/browser": "^8.29.0", "@yudiel/react-qr-scanner": "^2.0.0-beta.3", - "axios": "^1.6.8", + "axios": "^1.7.7", "bowser": "^2.11.0", "browser-image-compression": "^2.0.2", "browserslist-useragent-regexp": "^4.1.3", @@ -79,7 +79,6 @@ "react-infinite-scroll-component": "^6.1.0", "react-markdown": "^8.0.7", "react-pdf": "^9.1.0", - "react-player": "^2.16.0", "react-redux": "^8.1.1", "react-webcam": "^7.2.0", "redux": "^4.2.1", diff --git a/scripts/generate-build-version.js b/scripts/generate-build-version.js index 36e7ea1162a..84b8e0e5928 100644 --- a/scripts/generate-build-version.js +++ b/scripts/generate-build-version.js @@ -20,5 +20,5 @@ fs.writeFile( return console.log(err); } return null; - } + }, ); diff --git a/scripts/generate-supported-browsers.mjs b/scripts/generate-supported-browsers.mjs new file mode 100644 index 00000000000..2f8b627c57d --- /dev/null +++ b/scripts/generate-supported-browsers.mjs @@ -0,0 +1,17 @@ +import { getUserAgentRegex } from 'browserslist-useragent-regexp'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const regex = getUserAgentRegex({ + ignoreMinor: true, + ignorePatch: true, + allowZeroSubversions: false, + allowHigherVersions: true, +}); + +const supportedBrowsersPath = path.resolve(__dirname, '../src/supportedBrowsers.ts'); +fs.writeFileSync(supportedBrowsersPath, `export default ${regex};`); diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 681fe874475..8149a144ed9 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -735,10 +735,23 @@ export const HEARTBEAT_RHYTHM_CHOICES = [ ] as const; export const NURSING_CARE_PROCEDURES = [ + "oral_care", + "hair_care", + "bed_bath", + "eye_care", + "perineal_care", + "skin_care", + "pre_enema", + "wound_dressing", + "lymphedema_care", + "ascitic_tapping", + "colostomy_care", + "colostomy_change", "personal_hygiene", "positioning", "suctioning", "ryles_tube_care", + "ryles_tube_change", "iv_sitecare", "nubulisation", "dressing", @@ -746,8 +759,10 @@ export const NURSING_CARE_PROCEDURES = [ "restrain", "chest_tube_care", "tracheostomy_care", + "tracheostomy_change", "stoma_care", "catheter_care", + "catheter_change", ] as const; export const EYE_OPEN_SCALE = [ @@ -773,28 +788,71 @@ export const MOTOR_RESPONSE_SCALE = [ { value: 5, text: "Moves to localized pain" }, { value: 6, text: "Obeying commands/Normal acrivity" }, ]; -export const CONSULTATION_TABS = [ - { text: "UPDATES", desc: "Overview" }, - { text: "FEED", desc: "Feed" }, - { text: "SUMMARY", desc: "Vitals" }, - { text: "ABG", desc: "ABG" }, - { text: "MEDICINES", desc: "Medicines" }, - { text: "FILES", desc: "Files" }, - { text: "INVESTIGATIONS", desc: "Investigations" }, - { text: "NEUROLOGICAL_MONITORING", desc: "Neuro" }, - { text: "VENTILATOR", desc: "Ventilation" }, - { text: "NUTRITION", desc: "Nutrition" }, - { text: "PRESSURE_SORE", desc: "Pressure Sore" }, - { text: "NURSING", desc: "Nursing" }, - { text: "DIALYSIS", desc: "Dialysis" }, - { text: "ABDM", desc: "ABDM Records" }, -]; export const RHYTHM_CHOICES = [ { id: 5, text: "REGULAR", desc: "Regular" }, { id: 10, text: "IRREGULAR", desc: "Irregular" }, ] as const; +export const BOWEL_ISSUE_CHOICES = [ + "NO_DIFFICULTY", + "CONSTIPATION", + "DIARRHOEA", +] as const; + +export const BLADDER_DRAINAGE_CHOICES = [ + "NORMAL", + "CONDOM_CATHETER", + "DIAPER", + "INTERMITTENT_CATHETER", + "CONTINUOUS_INDWELLING_CATHETER", + "CONTINUOUS_SUPRAPUBIC_CATHETER", + "UROSTOMY", +] as const; + +export const BLADDER_ISSUE_CHOICES = [ + "NO_ISSUES", + "INCONTINENCE", + "RETENTION", + "HESITANCY", +] as const; + +export const URINATION_FREQUENCY_CHOICES = [ + "NORMAL", + "DECREASED", + "INCREASED", +] as const; + +export const SLEEP_CHOICES = [ + "EXCESSIVE", + "SATISFACTORY", + "UNSATISFACTORY", + "NO_SLEEP", +] as const; + +export const NUTRITION_ROUTE_CHOICES = [ + "ORAL", + "RYLES_TUBE", + "GASTROSTOMY_OR_JEJUNOSTOMY", + "PEG", + "PARENTERAL_TUBING_FLUID", + "PARENTERAL_TUBING_TPN", +] as const; + +export const ORAL_ISSUE_CHOICES = [ + "NO_ISSUE", + "DYSPHAGIA", + "ODYNOPHAGIA", +] as const; + +export const APPETITE_CHOICES = [ + "INCREASED", + "SATISFACTORY", + "REDUCED", + "NO_TASTE_FOR_FOOD", + "CANNOT_BE_ASSESSED", +] as const; + export const LOCATION_BED_TYPES = [ { id: "ISOLATION", name: "Isolation" }, { id: "ICU", name: "ICU" }, @@ -1357,6 +1415,20 @@ export const CONSENT_PATIENT_CODE_STATUS_CHOICES = [ { id: 3, text: "Comfort Care Only" }, { id: 4, text: "Active treatment" }, ]; + +export const SOCIOECONOMIC_STATUS_CHOICES = [ + "MIDDLE_CLASS", + "POOR", + "VERY_POOR", + "WELL_OFF", +] as const; + +export const DOMESTIC_HEALTHCARE_SUPPORT_CHOICES = [ + "FAMILY_MEMBER", + "PAID_CAREGIVER", + "NO_SUPPORT", +] as const; + export const OCCUPATION_TYPES = [ { id: 27, diff --git a/src/Common/hooks/useHLSPlayer.ts b/src/Common/hooks/useHLSPlayer.ts deleted file mode 100644 index 32a52043fe7..00000000000 --- a/src/Common/hooks/useHLSPlayer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import ReactPlayer from "react-player"; -import { IOptions } from "./useMSEplayer"; - -export const useHLSPLayer = (ref: ReactPlayer | null) => { - const startStream = ({ onSuccess, onError }: IOptions = {}) => { - try { - ref?.setState({ url: ref?.props.url + "&t=" + Date.now() }); - onSuccess && onSuccess(undefined); - } catch (err) { - onError && onError(err); - } - }; - return { - startStream, - stopStream: undefined, - }; -}; diff --git a/src/Common/hooks/useMSEplayer.ts b/src/Common/hooks/useMSEplayer.ts deleted file mode 100644 index 5271c08fd56..00000000000 --- a/src/Common/hooks/useMSEplayer.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { useEffect, useRef } from "react"; - -export interface IAsset { - middlewareHostname: string; -} - -interface UseMSEMediaPlayerOption { - config: IAsset; - url?: string; - videoEl: HTMLVideoElement | null; -} - -export interface ICameraAssetState { - id: string; - accessKey: string; - middleware_address: string; - location_middleware: string; -} - -export enum StreamStatus { - Playing, - Stop, - Loading, - Offline, -} - -interface UseMSEMediaPlayerReturnType { - stopStream: (config: { id: string }, options: IOptions) => void; - startStream: (options?: IOptions) => void; -} - -export interface IOptions { - onSuccess?: (resp: any) => void; - onError?: (err: any) => void; -} -const stopStream = - ({ - middlewareHostname, - ws, - }: { - middlewareHostname: string; - ws?: WebSocket; - }) => - (payload: { id: string }, options: IOptions) => { - const { id } = payload; - ws?.close(); - fetch(`https://${middlewareHostname}/stop`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ id }), - }) - .then((res) => { - if (!res.ok) { - throw new Error("network response was not ok"); - } - return res.json(); - }) - .then((res) => options?.onSuccess && options.onSuccess(res)) - .catch((err) => options.onError && options.onError(err)); - }; - -/** - * MSE player utility - */ -const Utf8ArrayToStr = (array: string | any[] | Uint8Array) => { - let out, i, c; - let char2, char3; - out = ""; - const len = array.length; - i = 0; - while (i < len) { - c = array[i++]; - switch (c >> 4) { - case 7: - out += String.fromCharCode(c); - break; - case 13: - char2 = array[i++]; - out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f)); - break; - case 14: - char2 = array[i++]; - char3 = array[i++]; - out += String.fromCharCode( - ((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0), - ); - break; - } - } - return out; -}; - -export const useMSEMediaPlayer = ({ - config, - url, - videoEl, -}: UseMSEMediaPlayerOption): UseMSEMediaPlayerReturnType => { - const mseQueue: any[] = []; - let mseStreamingStarted = false; - const wsRef = useRef(); - let mseSourceBuffer: any; - - const pushPacket = () => { - if (!mseSourceBuffer.updating) { - if (mseQueue.length > 0) { - const packet = mseQueue.shift(); - // Check if SourceBuffer has been removed before appending buffer - if (mseSourceBuffer.removed) { - console.error("Attempted to append to a removed SourceBuffer."); - return; - } - mseSourceBuffer.appendBuffer(packet); - } else { - mseStreamingStarted = false; - } - } - if (videoEl && videoEl.buffered.length > 0) { - if (typeof document.hidden !== "undefined" && document.hidden) { - //no sound, browser paused video without sound in background - videoEl.currentTime = - videoEl.buffered.end(videoEl.buffered.length - 1) - 0.5; - } - } - }; - - const readPacket = (packet: any) => { - if (!mseStreamingStarted) { - // Check if SourceBuffer has been removed before appending buffer - if (mseSourceBuffer.removed) { - console.error("Attempted to append to a removed SourceBuffer."); - return; - } - mseSourceBuffer.appendBuffer(packet); - mseStreamingStarted = true; - return; - } - mseQueue.push(packet); - if (!mseSourceBuffer.updating) { - pushPacket(); - } - }; - - const startStream = ({ onError, onSuccess }: IOptions = {}) => { - // location.protocol == 'https:' ? protocol = 'wss' : protocol = 'ws'; - try { - wsRef.current?.close(); - const mse = new MediaSource(); - if (videoEl) { - videoEl.src = window.URL.createObjectURL(mse); - } - - if (url) { - mse.addEventListener( - "sourceopen", - function () { - wsRef.current = new WebSocket(url); - const ws = wsRef.current; - ws.binaryType = "arraybuffer"; - ws.onopen = function (_event) { - onSuccess && onSuccess(undefined); - }; - ws.onmessage = function (event) { - const data = new Uint8Array(event.data); - if (+data[0] === 9) { - const decoded_arr = data.slice(1); - let mimeCodec; - if (window.TextDecoder) { - mimeCodec = new TextDecoder("utf-8").decode(decoded_arr); - } else { - mimeCodec = Utf8ArrayToStr(decoded_arr); - } - try { - mseSourceBuffer = mse.addSourceBuffer( - `video/mp4; codecs="${mimeCodec}"`, - ); - } catch (error) { - onError?.(error); - return; - } - mseSourceBuffer.mode = "segments"; - if (mseQueue.length > 0 && !mseSourceBuffer.updating) { - mseSourceBuffer.addEventListener("updateend", pushPacket); - } - } else { - readPacket(event.data); - } - }; - ws.onerror = function (event) { - onError && onError(event); - }; - }, - false, - ); - } - } catch (e) { - onError && onError(e); - } - }; - - document.addEventListener("DOMContentLoaded", function () { - if (videoEl) { - videoEl.addEventListener("loadeddata", () => { - videoEl.play(); - }); - - //fix stalled video in safari - videoEl.addEventListener("pause", () => { - if ( - videoEl.currentTime > - videoEl.buffered.end(videoEl.buffered.length - 1) - ) { - videoEl.currentTime = - videoEl.buffered.end(videoEl.buffered.length - 1) - 0.1; - videoEl.play(); - } - }); - - videoEl.addEventListener("error", (e) => { - console.log("video_error", e); - }); - } - }); - - useEffect(() => { - return () => { - wsRef.current?.close(); - }; - }, []); - - return { - startStream: startStream, - stopStream: stopStream({ ...config, ws: wsRef.current }), - }; -}; diff --git a/src/Components/ABDM/ABHAProfileModal.tsx b/src/Components/ABDM/ABHAProfileModal.tsx index 4853626f1ef..99b888cc1ef 100644 --- a/src/Components/ABDM/ABHAProfileModal.tsx +++ b/src/Components/ABDM/ABHAProfileModal.tsx @@ -1,6 +1,5 @@ import * as Notify from "../../Utils/Notifications"; -import { AbhaObject } from "../Patient/models"; import CareIcon from "../../CAREUI/icons/CareIcon"; import DialogModal from "../Common/Dialog"; import QRCode from "qrcode.react"; @@ -8,10 +7,11 @@ import { formatDateTime } from "../../Utils/utils"; import { useRef } from "react"; import request from "../../Utils/request/request"; import routes from "../../Redux/api"; +import { AbhaNumberModel } from "./types/abha"; interface IProps { patientId?: string; - abha?: AbhaObject; + abha?: AbhaNumberModel; show: boolean; onClose: () => void; } diff --git a/src/Components/ABDM/FetchRecordsModal.tsx b/src/Components/ABDM/FetchRecordsModal.tsx index b457ccc453a..cc7c72474d1 100644 --- a/src/Components/ABDM/FetchRecordsModal.tsx +++ b/src/Components/ABDM/FetchRecordsModal.tsx @@ -2,7 +2,6 @@ import * as Notification from "../../Utils/Notifications.js"; import ButtonV2 from "../Common/components/ButtonV2"; import DialogModal from "../Common/Dialog"; -import { PatientModel } from "../Patient/models"; import TextFormField from "../Form/FormFields/TextFormField"; import { useState } from "react"; import { @@ -20,6 +19,7 @@ import { useMessageListener } from "../../Common/hooks/useMessageListener.js"; import CircularProgress from "../Common/components/CircularProgress.js"; import CareIcon from "../../CAREUI/icons/CareIcon.js"; import { classNames } from "../../Utils/utils.js"; +import { AbhaNumberModel } from "./types/abha.js"; import { ConsentHIType, ConsentPurpose } from "./types/consent.js"; import useNotificationSubscriptionState from "../../Common/hooks/useNotificationSubscriptionState.js"; @@ -27,12 +27,12 @@ const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); interface IProps { - patient: PatientModel; + abha?: AbhaNumberModel; show: boolean; onClose: () => void; } -export default function FetchRecordsModal({ patient, show, onClose }: IProps) { +export default function FetchRecordsModal({ abha, show, onClose }: IProps) { const [idVerificationStatus, setIdVerificationStatus] = useState< "pending" | "in-progress" | "verified" | "failed" >("pending"); @@ -53,9 +53,7 @@ export default function FetchRecordsModal({ patient, show, onClose }: IProps) { useMessageListener((data) => { if (data.type === "MESSAGE" && data.from === "patients/on_find") { - if ( - data.message?.patient?.id === patient?.abha_number_object?.health_id - ) { + if (data.message?.patient?.id === abha?.health_id) { setIdVerificationStatus("verified"); setErrors({ ...errors, @@ -85,7 +83,7 @@ export default function FetchRecordsModal({ patient, show, onClose }: IProps) {
null} disabled label="Patient Identifier" @@ -98,7 +96,7 @@ export default function FetchRecordsModal({ patient, show, onClose }: IProps) { onClick={async () => { const { res } = await request(routes.abha.findPatient, { body: { - id: patient?.abha_number_object?.health_id, + id: abha?.health_id, }, reattempts: 0, }); @@ -214,7 +212,7 @@ export default function FetchRecordsModal({ patient, show, onClose }: IProps) { setIsMakingConsentRequest(true); const { res } = await request(routes.abha.createConsent, { body: { - patient_abha: patient?.abha_number_object?.health_id as string, + patient_abha: abha?.health_id as string, hi_types: hiTypes, purpose, from_time: fromDate, @@ -229,8 +227,8 @@ export default function FetchRecordsModal({ patient, show, onClose }: IProps) { }); navigate( - `/facility/${patient.facility}/abdm` ?? - `/facility/${patient.facility}/patient/${patient.id}/consultation/${patient.last_consultation?.id}/abdm`, + `/facility/${abha?.patient_object?.facility}/abdm` ?? + `/facility/${abha?.patient_object?.facility}/patient/${abha?.patient_object?.id}/consultation/${abha?.patient_object?.last_consultation?.id}/abdm`, ); } else { Notification.Error({ diff --git a/src/Components/ABDM/LinkABHANumberModal.tsx b/src/Components/ABDM/LinkABHANumberModal.tsx index ea996ddd1da..8d6365387cd 100644 --- a/src/Components/ABDM/LinkABHANumberModal.tsx +++ b/src/Components/ABDM/LinkABHANumberModal.tsx @@ -202,7 +202,7 @@ const ScanABHAQRSection = ({ dob: abha?.dob.replace(/\//g, "-"), address: abha?.address, "dist name": abha?.["dist name"] ?? abha?.district_name, - "state name": abha?.["state name"], + "state name": abha?.["state name"] ?? abha?.state_name, }, }); diff --git a/src/Components/ABDM/LinkCareContextModal.tsx b/src/Components/ABDM/LinkCareContextModal.tsx index b22189c9f2f..9ec9885bbca 100644 --- a/src/Components/ABDM/LinkCareContextModal.tsx +++ b/src/Components/ABDM/LinkCareContextModal.tsx @@ -3,22 +3,22 @@ import * as Notification from "../../Utils/Notifications.js"; import ButtonV2 from "../Common/components/ButtonV2"; import DateFormField from "../Form/FormFields/DateFormField"; import DialogModal from "../Common/Dialog"; -import { PatientModel } from "../Patient/models"; import TextFormField from "../Form/FormFields/TextFormField"; import { useState } from "react"; import routes from "../../Redux/api.js"; import request from "../../Utils/request/request.js"; +import { AbhaNumberModel } from "./types/abha.js"; interface IProps { consultationId: string; - patient: PatientModel; + abha?: AbhaNumberModel; show: boolean; onClose: () => void; } const LinkCareContextModal = ({ consultationId, - patient, + abha, show, onClose, }: IProps) => { @@ -33,7 +33,7 @@ const LinkCareContextModal = ({ >
null} disabled label="Name" @@ -41,7 +41,7 @@ const LinkCareContextModal = ({ error="" /> null} disabled label="Gender" @@ -52,11 +52,7 @@ const LinkCareContextModal = ({ null} disabled required @@ -84,9 +80,9 @@ const LinkCareContextModal = ({ const { res } = await request(routes.abha.linkCareContext, { body: { consultation: consultationId, - name: patient?.abha_number_object?.name, - gender: patient?.abha_number_object?.gender, - dob: patient?.abha_number_object?.date_of_birth, + name: abha?.name, + gender: abha?.gender, + dob: abha?.date_of_birth, }, reattempts: 0, }); @@ -94,10 +90,6 @@ const LinkCareContextModal = ({ Notification.Success({ msg: "Care Context sucessfully linked!", }); - } else { - Notification.Error({ - msg: "Error in linking Care Context!", - }); } setIsLinkingCareContext(false); onClose(); diff --git a/src/Components/ABDM/models.ts b/src/Components/ABDM/models.ts index 9dc362f5cac..899ec948777 100644 --- a/src/Components/ABDM/models.ts +++ b/src/Components/ABDM/models.ts @@ -124,6 +124,7 @@ export interface ABHAQRContent { address: string; distlgd: string; district_name?: string; + state_name?: string; dob: string; gender: "M" | "F" | "O"; hid?: string; @@ -132,6 +133,6 @@ export interface ABHAQRContent { hidn: string; mobile: string; name: string; - "state name": string; + "state name"?: string; statelgd: string; } diff --git a/src/Components/ABDM/types/abha.ts b/src/Components/ABDM/types/abha.ts index d45986e8f8f..fd03b30cdc4 100644 --- a/src/Components/ABDM/types/abha.ts +++ b/src/Components/ABDM/types/abha.ts @@ -1,3 +1,5 @@ +import { PatientModel } from "../../Patient/models"; + export type AbhaNumberModel = { id: number; external_id: string; @@ -18,4 +20,6 @@ export type AbhaNumberModel = { email: string | null; profile_photo: string | null; new: boolean; + patient: string | null; + patient_object: PatientModel | null; }; diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx index 21beb5f7bf8..e49fcad549d 100644 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx @@ -16,6 +16,7 @@ import routes from "../../../Redux/api"; import useQuery from "../../../Utils/request/useQuery"; import CareIcon from "../../../CAREUI/icons/CareIcon"; +import useOperateCamera from "../../CameraFeed/useOperateCamera"; interface Props { assetId: string; @@ -47,6 +48,8 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { }); const authUser = useAuthUser(); + const { operate } = useOperateCamera(assetId ?? "", true); + useEffect(() => { if (asset) { setAssetType(asset?.asset_class); @@ -92,7 +95,6 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { const addPreset = async (e: SyntheticEvent) => { e.preventDefault(); - const config = getCameraConfig(asset as AssetData); const data = { bed_id: bed.id, preset_name: newPreset, @@ -100,13 +102,7 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { try { setLoadingAddPreset(true); - const response = await fetch( - `https://${resolvedMiddleware?.hostname}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}`, - ); - if (!response.ok) { - throw new Error("Network error"); - } - const presetData = await response.json(); + const { data: presetData } = await operate({ type: "get_status" }); const { res } = await request(routes.createAssetBed, { body: { diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx index 5a8ccd5c184..e5e017db8dc 100644 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ b/src/Components/Assets/configure/CameraConfigure.tsx @@ -1,6 +1,6 @@ import { SyntheticEvent } from "react"; import { AssetData } from "../AssetTypes"; -import LiveFeed from "../../Facility/Consultations/LiveFeed"; +import CameraFeedOld from "../../CameraFeed/CameraFeedOld"; import { BedSelect } from "../../Common/BedSelect"; import { BedModel } from "../../Facility/models"; import { getCameraConfig } from "../../../Utils/transformUtils"; @@ -76,7 +76,7 @@ export default function CameraConfigure(props: CameraConfigureProps) { - (null); + const playerRef = useRef(null); const playerWrapperRef = useRef(null); - const streamUrl = getStreamUrl(props.asset); + const [streamUrl, setStreamUrl] = useState(""); const inlineControls = useBreakpoints({ default: false, sm: true }); - const player = usePlayer(streamUrl, playerRef); - const [isFullscreen, setFullscreen] = useFullscreen(); const [state, setState] = useState(); - useEffect(() => setState(player.status), [player.status, setState]); - + const [playedOn, setPlayedOn] = useState(); + const [playerStatus, setPlayerStatus] = useState("stop"); // Move camera when selected preset has changed useEffect(() => { async function move(preset: PTZPayload) { @@ -73,21 +70,29 @@ export default function CameraFeed(props: Props) { getPresets(props.onCameraPresetsObtained); }, [props.operate, props.onCameraPresetsObtained]); - const initializeStream = useCallback(() => { - player.initializeStream({ - onSuccess: async () => { - props.onStreamSuccess?.(); - const { res } = await props.operate({ type: "get_status" }); - if (res?.status === 500) { - setState("host_unreachable"); + const initializeStream = useCallback(async () => { + if (!playerRef.current) return; + setPlayerStatus("loading"); + await props + .operate({ type: "get_stream_token" }) + .then(({ res, data }) => { + if (res?.status != 200) { + setState("authentication_error"); + return props.onStreamError?.(); } - }, - onError: props.onStreamError, - }); - }, [player.initializeStream]); + const result = data?.result as { token: string }; + return setStreamUrl(getStreamUrl(props.asset, result.token)); + }) + .catch(() => { + setState("host_unreachable"); + return props.onStreamError?.(); + }); + }, []); // Start stream on mount - useEffect(() => initializeStream(), [initializeStream]); + useEffect(() => { + initializeStream(); + }, []); const resetStream = () => { setState("loading"); @@ -154,7 +159,7 @@ export default function CameraFeed(props: Props) { isFullscreen ? "hidden lg:flex" : "flex", "items-center justify-between px-4 py-0.5 transition-all duration-500 ease-in-out lg:py-1", (() => { - if (player.status !== "playing") { + if (playerStatus !== "playing") { return "bg-black text-zinc-400"; } @@ -168,7 +173,7 @@ export default function CameraFeed(props: Props) { >
@@ -200,75 +205,74 @@ export default function CameraFeed(props: Props) {
{/* Notifications */} - {player.status === "playing" && } + {playerStatus === "playing" && } {/* No Feed informations */} - {state === "host_unreachable" && ( - - )} - {player.status === "offline" && ( - - )} + {(() => { + switch (state) { + case "host_unreachable": + return ( + + ); + case "authentication_error": + return ( + + ); + case "offline": + return ( + + ); + } + })()} {/* Video Player */} - {isIOS ? ( -
- } - controls={false} - pip={false} - playsinline - playing - muted - width="100%" - height="100%" - onPlay={player.onPlayCB} - onEnded={() => player.setStatus("stop")} - onError={(e, _, hlsInstance) => { - if (e === "hlsError") { - const recovered = hlsInstance.recoverMediaError(); - console.info(recovered); - } - }} - /> -
- ) : ( -
{!inlineControls && (
{ +import { BedSelect } from "../Common/BedSelect.js"; +import { BedModel } from "../Facility/models.js"; +import useWindowDimensions from "../../Common/hooks/useWindowDimensions.js"; +import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon.js"; +import Page from "../Common/components/Page.js"; +import ConfirmDialog from "../Common/ConfirmDialog.js"; +import { FieldLabel } from "../Form/FormFields/FormField.js"; +import useFullscreen from "../../Common/hooks/useFullscreen.js"; +import TextFormField from "../Form/FormFields/TextFormField.js"; +import VideoPlayer from "./videoPlayer.js"; + +export enum StreamStatus { + Playing, + Stop, + Loading, + Offline, +} + +export const FeedCameraPTZHelpButton = (props: { cameraPTZ: CameraPTZ[] }) => { + const { cameraPTZ } = props; + return ( + + ); +}; + +const CameraFeedOld = (props: any) => { const middlewareHostname = props.middlewareHostname; const [presetsPage, setPresetsPage] = useState(0); const cameraAsset = props.asset; @@ -57,21 +110,7 @@ const LiveFeed = (props: any) => { const isExtremeSmallScreen = width <= extremeSmallScreenBreakpoint ? true : false; const liveFeedPlayerRef = useRef(null); - - const videoEl = liveFeedPlayerRef.current as HTMLVideoElement; - - const streamUrl = isIOS - ? `https://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0` - : `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0`; - - const { startStream } = useMSEMediaPlayer({ - config: { - middlewareHostname, - ...cameraAsset, - }, - url: streamUrl, - videoEl, - }); + const [streamUrl, setStreamUrl] = useState(""); const refreshPresetsHash = props.refreshPresetsHash; @@ -80,6 +119,7 @@ const LiveFeed = (props: any) => { const { absoluteMove, getCameraStatus, + getStreamToken, getPTZPayload, getPresets, gotoPreset, @@ -199,12 +239,20 @@ const LiveFeed = (props: any) => { } }, [page.offset, cameraAsset.id, refreshPresetsHash]); - const startStreamFeed = () => { - startStream({ - onSuccess: () => setStreamStatus(StreamStatus.Playing), - onError: () => setStreamStatus(StreamStatus.Offline), + const startStreamFeed = useCallback(async () => { + if (!liveFeedPlayerRef.current) return; + + await getStreamToken({ + onSuccess: (data) => { + setStreamUrl( + `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0&token=${data.token}`, + ); + }, + onError: () => { + setStreamStatus(StreamStatus.Offline); + }, }); - }; + }, [liveFeedPlayerRef.current]); const viewOptions = (page: number) => { return presets @@ -228,7 +276,7 @@ const LiveFeed = (props: any) => { return () => { clearTimeout(tId); }; - }, [startStream, streamStatus]); + }, [startStreamFeed, streamStatus]); const handlePagination = (cOffset: number) => { setPage({ @@ -243,13 +291,10 @@ const LiveFeed = (props: any) => { precision === 16 ? 1 : precision * 2, ); }, - reset: () => { + reset: async () => { setStreamStatus(StreamStatus.Loading); setVideoStartTime(null); - startStream({ - onSuccess: () => setStreamStatus(StreamStatus.Playing), - onError: () => setStreamStatus(StreamStatus.Offline), - }); + await startStreamFeed(); }, fullScreen: () => { if (!liveFeedPlayerRef.current) return; @@ -258,7 +303,6 @@ const LiveFeed = (props: any) => { updatePreset: (option) => { getCameraStatus({ onSuccess: async (data) => { - console.log({ currentPreset, data }); if (currentPreset?.asset_object?.id && data?.position) { setLoading(option.loadingLabel); console.log("Updating Preset"); @@ -365,56 +409,23 @@ const LiveFeed = (props: any) => {
- {/* ADD VIDEO PLAYER HERE */}
- {isIOS ? ( -
- } - controls={false} - playsinline - playing - muted - width="100%" - height="100%" - onPlay={() => { - setVideoStartTime(() => new Date()); - setStreamStatus(StreamStatus.Playing); - }} - onWaiting={() => { - const delay = calculateVideoLiveDelay(); - if (delay > 5) { - setStreamStatus(StreamStatus.Loading); - } - }} - onError={(e, _, hlsInstance) => { - if (e === "hlsError") { - const recovered = hlsInstance.recoverMediaError(); - console.info(recovered); - } - }} - /> -
- ) : ( - - )} + { + setVideoStartTime(() => new Date()); + }} + onWaiting={() => { + const delay = calculateVideoLiveDelay(); + if (delay > 5) { + setStreamStatus(StreamStatus.Loading); + } + }} + onSuccess={() => setStreamStatus(StreamStatus.Playing)} + onError={() => setStreamStatus(StreamStatus.Offline)} + /> {streamStatus === StreamStatus.Playing && calculateVideoLiveDelay() > 3 && ( @@ -671,4 +682,4 @@ const LiveFeed = (props: any) => { ); }; -export default LiveFeed; +export default CameraFeedOld; diff --git a/src/Components/CameraFeed/FeedAlert.tsx b/src/Components/CameraFeed/FeedAlert.tsx index b907ce72ad2..09f3b21ae42 100644 --- a/src/Components/CameraFeed/FeedAlert.tsx +++ b/src/Components/CameraFeed/FeedAlert.tsx @@ -2,14 +2,15 @@ import { Transition } from "@headlessui/react"; import { useEffect, useState } from "react"; import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon"; import { classNames } from "../../Utils/utils"; -import { StreamStatus } from "./usePlayer"; +export type StreamStatus = "playing" | "stop" | "loading" | "offline"; export type FeedAlertState = | StreamStatus | "moving" | "zooming" | "saving_preset" - | "host_unreachable"; + | "host_unreachable" + | "authentication_error"; interface Props { state?: FeedAlertState; @@ -24,6 +25,7 @@ const ALERT_ICON_MAP: Partial> = { zooming: "l-search", saving_preset: "l-save", host_unreachable: "l-exclamation-triangle", + authentication_error: "l-exclamation-triangle", }; export default function FeedAlert({ state }: Props) { diff --git a/src/Components/CameraFeed/FeedNetworkSignal.tsx b/src/Components/CameraFeed/FeedNetworkSignal.tsx index 68df86bb4d5..4b1e6c7fbe3 100644 --- a/src/Components/CameraFeed/FeedNetworkSignal.tsx +++ b/src/Components/CameraFeed/FeedNetworkSignal.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { calculateVideoDelay } from "./utils"; import NetworkSignal from "../../CAREUI/display/NetworkSignal"; -import { StreamStatus } from "./usePlayer"; +import { StreamStatus } from "./FeedAlert"; interface Props { playerRef: React.RefObject; @@ -23,7 +23,9 @@ export default function FeedNetworkSignal(props: Props) { // 2. This value may become negative when the web-socket stream // disconnects while the tab was not in focus. if (-5 > delay || delay > 5) { - props.onReset(); + if (document.hasFocus() && props.status !== "loading") { + props.onReset(); + } } }, 1000); diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts index 482dc515eb3..aecbdc655fa 100644 --- a/src/Components/CameraFeed/routes.ts +++ b/src/Components/CameraFeed/routes.ts @@ -13,6 +13,12 @@ export type GetStatusResponse = { }; }; +export type GetStreamTokenResponse = { + result: { + token: string; + }; +}; + export type GetPresetsResponse = { result: Record; }; @@ -21,7 +27,9 @@ export const FeedRoutes = { operateAsset: { path: "/api/v1/asset/{id}/operate_assets/", method: "POST", - TRes: Type(), + TRes: Type< + GetStreamTokenResponse | GetStatusResponse | GetPresetsResponse + >(), TBody: Type<{ action: OperationAction }>(), }, } as const; diff --git a/src/Common/hooks/useFeedPTZ.ts b/src/Components/CameraFeed/useFeedPTZ.ts similarity index 89% rename from src/Common/hooks/useFeedPTZ.ts rename to src/Components/CameraFeed/useFeedPTZ.ts index a393edc5922..fb704baf972 100644 --- a/src/Common/hooks/useFeedPTZ.ts +++ b/src/Components/CameraFeed/useFeedPTZ.ts @@ -17,33 +17,11 @@ interface PTZPayload { zoom: number; } -export interface PTZState { - x: number; - y: number; - zoom: number; - precision: number; -} - interface UseMSEMediaPlayerOption { config: IAsset; dispatch: any; } -export interface ICameraAssetState { - id: string; - username: string; - password: string; - hostname: string; - port: number; -} - -export enum StreamStatus { - Playing, - Stop, - Loading, - Offline, -} - interface UseMSEMediaPlayerReturnType { absoluteMove: (payload: PTZPayload, options: IOptions) => void; relativeMove: (payload: PTZPayload, options: IOptions) => void; @@ -53,6 +31,7 @@ interface UseMSEMediaPlayerReturnType { value?: number, ) => PTZPayload; getCameraStatus: (options: IOptions) => void; + getStreamToken: (options: IOptions) => void; getPresets: (options: IOptions) => void; gotoPreset: (payload: IGotoPresetPayload, options: IOptions) => void; } @@ -88,6 +67,23 @@ const getCameraStatus = : options?.onError && options.onError(resp)); }; +const getStreamToken = + (config: IAsset, dispatch: any) => + async (options: IOptions = {}) => { + if (!config.id) return; + const resp = await dispatch( + operateAsset(config.id, { + action: { + type: "get_stream_token", + }, + }), + ); + resp && + (resp.status === 200 + ? options?.onSuccess && options.onSuccess(resp.data.result) + : options?.onError && options.onError(resp)); + }; + const getPresets = (config: IAsset, dispatch: any) => async (options: IOptions = {}) => { @@ -205,6 +201,7 @@ export const useFeedPTZ = ({ relativeMove: relativeMove(config, dispatch), getPTZPayload, getCameraStatus: getCameraStatus(config, dispatch), + getStreamToken: getStreamToken(config, dispatch), getPresets: getPresets(config, dispatch), gotoPreset: gotoPreset(config, dispatch), }; diff --git a/src/Components/CameraFeed/useOperateCamera.ts b/src/Components/CameraFeed/useOperateCamera.ts index c0e35f90bf2..bfddbf5b887 100644 --- a/src/Components/CameraFeed/useOperateCamera.ts +++ b/src/Components/CameraFeed/useOperateCamera.ts @@ -33,6 +33,10 @@ interface RelativeMoveOperation { data: PTZPayload; } +interface GetStreamToken { + type: "get_stream_token"; +} + interface ResetFeedOperation { type: "reset"; } @@ -43,6 +47,7 @@ export type OperationAction = | GoToPresetOperation | AbsoluteMoveOperation | RelativeMoveOperation + | GetStreamToken | ResetFeedOperation; /** diff --git a/src/Components/CameraFeed/usePlayer.tsx b/src/Components/CameraFeed/usePlayer.tsx deleted file mode 100644 index 7f2dc088739..00000000000 --- a/src/Components/CameraFeed/usePlayer.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { MutableRefObject, useCallback, useState } from "react"; -import ReactPlayer from "react-player"; -import { isIOS } from "../../Utils/utils"; -import { useHLSPLayer } from "../../Common/hooks/useHLSPlayer"; -import { IOptions, useMSEMediaPlayer } from "../../Common/hooks/useMSEplayer"; - -export type StreamStatus = "playing" | "stop" | "loading" | "offline"; - -export default function usePlayer( - streamUrl: string, - ref: MutableRefObject, -) { - const [playedOn, setPlayedOn] = useState(); - const [status, setStatus] = useState("stop"); - - // Voluntarily disabling react-hooks/rules-of-hooks for this line as order of - // hooks is maintained (since platform won't change in runtime) - const _start = isIOS - ? // eslint-disable-next-line react-hooks/rules-of-hooks - useHLSPLayer(ref.current as ReactPlayer).startStream - : // eslint-disable-next-line react-hooks/rules-of-hooks - useMSEMediaPlayer({ - // Voluntarily set to "" as it's used by `stopStream` only (which is not - // used by this hook) - config: { middlewareHostname: "" }, - url: streamUrl, - videoEl: ref.current as HTMLVideoElement, - }).startStream; - - const initializeStream = useCallback( - ({ onSuccess, onError }: IOptions) => { - setPlayedOn(undefined); - setStatus("loading"); - _start({ - onSuccess, - onError: (args) => { - setStatus("offline"); - onError?.(args); - }, - }); - }, - [ref.current, streamUrl], - ); - - const onPlayCB = () => { - // Voluntarily updating only if previously undefined (as this method may be invoked by the HTML video element on tab re-focus) - setPlayedOn((prev) => (prev === undefined ? new Date() : prev)); - setStatus("playing"); - }; - - return { - status, - setStatus, - initializeStream, - playedOn, - onPlayCB, - }; -} diff --git a/src/Components/CameraFeed/utils.ts b/src/Components/CameraFeed/utils.ts index f4c55f315ad..5556237d579 100644 --- a/src/Components/CameraFeed/utils.ts +++ b/src/Components/CameraFeed/utils.ts @@ -1,7 +1,6 @@ import { MutableRefObject } from "react"; import { AssetClass, AssetData } from "../Assets/AssetTypes"; import { getCameraConfig } from "../../Utils/transformUtils"; -import { isIOS } from "../../Utils/utils"; export const calculateVideoDelay = ( ref: MutableRefObject, @@ -17,7 +16,7 @@ export const calculateVideoDelay = ( return playedDuration - video.currentTime; }; -export const getStreamUrl = (asset: AssetData) => { +export const getStreamUrl = (asset: AssetData, token?: string) => { if (asset.asset_class !== AssetClass.ONVIF) { throw "getStreamUrl can be invoked only for ONVIF Assets"; } @@ -26,7 +25,5 @@ export const getStreamUrl = (asset: AssetData) => { const host = asset.resolved_middleware?.hostname; const uuid = config.accessKey; - return isIOS - ? `https://${host}/stream/${uuid}/channel/0/hls/live/index.m3u8?uuid=${uuid}&channel=0` - : `wss://${host}/stream/${uuid}/channel/0/mse?uuid=${uuid}&channel=0`; + return `wss://${host}/stream/${uuid}/channel/0/mse?uuid=${uuid}&channel=0${token ? `&token=${token}` : ""}`; }; diff --git a/src/Components/CameraFeed/videoPlayer.tsx b/src/Components/CameraFeed/videoPlayer.tsx new file mode 100644 index 00000000000..9b6ad378d9f --- /dev/null +++ b/src/Components/CameraFeed/videoPlayer.tsx @@ -0,0 +1,198 @@ +import { useEffect, useRef } from "react"; + +declare const ManagedMediaSource: typeof MediaSource; + +function isIOSVersionLessThan18() { + const ua = navigator.userAgent; + if (/iPad|iPhone|iPod/.test(ua)) { + const iOSVersionMatch = ua.match(/OS (\d+)_?(\d+)?/); + if (iOSVersionMatch && parseInt(iOSVersionMatch[1], 10) < 18) { + return true; + } + } + return false; +} + +function isSafariVersionLessThan17() { + const ua = navigator.userAgent; + if (/^((?!chrome|android).)*safari/i.test(ua)) { + const safariVersionMatch = ua.match(/Version\/(\d+)\.(\d+)/); + if (safariVersionMatch && parseInt(safariVersionMatch[1], 10) < 17) { + return true; + } + } + return false; +} + +interface VideoPlayerProps { + playerRef: React.RefObject; + streamUrl: string; + className?: string; + onPlay?: () => void; + onEnded?: () => void; + onWaiting?: () => void; + onSuccess?: (resp: any) => void; + onError?: (err: any) => void; +} + +export default function VideoPlayer(props: VideoPlayerProps) { + const wsRef = useRef(); + const playerRef = props.playerRef; + let mediaSource: MediaSource; + let mseSourceBuffer: SourceBuffer; + let buf: Uint8Array; + let bufLen = 0; + + const pushPacket = () => { + if (mseSourceBuffer.updating) return; + + try { + if (bufLen > 0) { + // If there's data in the buffer to append + const data = buf.slice(0, bufLen); + bufLen = 0; // Reset buffer length + mseSourceBuffer.appendBuffer(data); // Append data to SourceBuffer + } else if (mseSourceBuffer.buffered && mseSourceBuffer.buffered.length) { + // If no new data to append, check if there's buffered data in SourceBuffer + const end = + mseSourceBuffer.buffered.end(mseSourceBuffer.buffered.length - 1) - + 15; + const start = mseSourceBuffer.buffered.start(0); + if (end > start) { + // Remove older data from the SourceBuffer to free up space + mseSourceBuffer.remove(start, end); + mediaSource.setLiveSeekableRange(end, end + 15); + } + } + } catch (e) { + console.debug(e); + props.onError?.(e); + } + }; + + const readPacket = (event: MessageEvent) => { + if (mseSourceBuffer.updating || bufLen > 0) { + // Buffer data if SourceBuffer is updating or buffer has data + const b = new Uint8Array(event.data); + buf.set(b, bufLen); + bufLen += b.byteLength; + } else { + try { + // Append data directly if SourceBuffer is ready + mseSourceBuffer.appendBuffer(event.data); + } catch (e) { + console.debug(e); + props.onError?.(e); + } + } + }; + + const cleanup = () => { + console.debug("Cleaning up video player"); + if (wsRef.current) { + wsRef.current.close(); + } + if (mseSourceBuffer) { + mseSourceBuffer.abort(); + } + if (playerRef.current) { + playerRef.current.pause(); + playerRef.current.src = ""; + playerRef.current.srcObject = null; + } + }; + + const startHLS = () => { + console.debug("Broken os/browser, falling back to hls"); + try { + if (!playerRef.current || !props.streamUrl) return; + const url = new URL(props.streamUrl); + if (url.protocol === "wss:") { + url.protocol = "https:"; + } + url.pathname = url.pathname.replace("mse", "hls/live/index.m3u8"); + playerRef.current.src = url.toString(); + playerRef.current.onplaying = () => { + props.onSuccess?.(undefined); + }; + } catch (err) { + console.debug(err); + props.onError?.(err); + } + }; + + const startMSE = () => { + try { + if (!playerRef.current || !props.streamUrl) return; + if (typeof ManagedMediaSource !== "undefined") { + mediaSource = new ManagedMediaSource(); + playerRef.current.disableRemotePlayback = true; + playerRef.current.srcObject = mediaSource; + } else { + mediaSource = new MediaSource(); + playerRef.current.src = URL.createObjectURL(mediaSource); + } + mediaSource.onsourceopen = function () { + const ws = new WebSocket(props.streamUrl); + wsRef.current = ws; + ws.binaryType = "arraybuffer"; + ws.onopen = (_) => props.onSuccess?.(undefined); + ws.onerror = (event) => props.onError?.(event); + ws.onmessage = function (event) { + const data = new Uint8Array(event.data); + // First packet is the codec type + if (+data[0] === 9) { + const mimeCodec = new TextDecoder("utf-8").decode(data.slice(1)); + try { + mseSourceBuffer = mediaSource.addSourceBuffer( + `video/mp4; codecs="${mimeCodec}"`, + ); + } catch (error) { + props.onError?.(error); + return; + } + buf = new Uint8Array(2 * 1024 * 1024); + mseSourceBuffer.mode = "segments"; + mseSourceBuffer.onupdateend = pushPacket; + // switch to readPacket after creating SourceBuffer + ws.onmessage = readPacket; + } else { + readPacket(event); + } + }; + }; + } catch (err) { + console.debug(err); + } + }; + + useEffect(() => { + // if the device is ios < 18 or safari < 17 then fallback to hls + if (isIOSVersionLessThan18() || isSafariVersionLessThan17()) { + startHLS(); + } else { + startMSE(); + } + return () => { + cleanup(); + }; + }, [props.streamUrl]); + + return ( + <> +