From b54bcc5c6f1b73d45b02dd13d0a2cf9515bf7463 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:49:01 +0300 Subject: [PATCH 1/6] New Cypress test for the patient discussion notes (#8554) * doctor notes cypress test without tab switching * doctor notes cypress test without tab switching * Adds utility fn. to extract keys of obj. in type-safe manner. * i18n for patient notes thread titles and add id for tabs * discussion note tab switching functionality test * made success notification as variables * corrected the test description --------- Co-authored-by: rithviknishad --- .../e2e/patient_spec/PatientDoctorNotes.cy.ts | 61 +++++++++++++++++++ cypress/e2e/patient_spec/PatientManage.cy.ts | 48 --------------- cypress/pageobject/Login/LoginPage.ts | 6 ++ .../pageobject/Patient/PatientConsultation.ts | 26 -------- cypress/pageobject/Patient/PatientCreation.ts | 1 + .../pageobject/Patient/PatientDoctorNotes.ts | 29 +++++++++ .../Facility/ConsultationDetails/index.tsx | 2 +- .../ConsultationDoctorNotes/index.tsx | 16 ++--- .../Facility/PatientNotesSlideover.tsx | 18 +++--- src/Components/Patient/PatientNotes.tsx | 16 ++--- src/Locale/en/Consultation.json | 6 +- src/Utils/utils.ts | 7 +++ 12 files changed, 128 insertions(+), 108 deletions(-) create mode 100644 cypress/e2e/patient_spec/PatientDoctorNotes.cy.ts delete mode 100644 cypress/e2e/patient_spec/PatientManage.cy.ts create mode 100644 cypress/pageobject/Patient/PatientDoctorNotes.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/PatientManage.cy.ts b/cypress/e2e/patient_spec/PatientManage.cy.ts deleted file mode 100644 index 22d77aa2fda..00000000000 --- a/cypress/e2e/patient_spec/PatientManage.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/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..db3c10fdcb5 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") 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/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index f2c2644afda..78f7022b683 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -288,7 +288,7 @@ export const ConsultationDetails = (props: any) => { Patient Details showPatientNotesPopup ? navigate( diff --git a/src/Components/Facility/ConsultationDoctorNotes/index.tsx b/src/Components/Facility/ConsultationDoctorNotes/index.tsx index ccf71cdb0b6..cf7d9c61ef9 100644 --- a/src/Components/Facility/ConsultationDoctorNotes/index.tsx +++ b/src/Components/Facility/ConsultationDoctorNotes/index.tsx @@ -11,7 +11,7 @@ import routes from "../../../Redux/api.js"; import request from "../../../Utils/request/request.js"; import useQuery from "../../../Utils/request/useQuery.js"; import useKeyboardShortcut from "use-keyboard-shortcut"; -import { classNames, isAppleDevice } from "../../../Utils/utils.js"; +import { classNames, isAppleDevice, keysOf } from "../../../Utils/utils.js"; import AutoExpandingTextInputFormField from "../../Form/FormFields/AutoExpandingTextInputFormField.js"; import { PATIENT_NOTES_THREADS } from "../../../Common/constants.js"; import useAuthUser from "../../../Common/hooks/useAuthUser.js"; @@ -123,23 +123,19 @@ const ConsultationDoctorNotes = (props: ConsultationDoctorNotesProps) => { >
- {Object.values(PATIENT_NOTES_THREADS).map((current) => ( + {keysOf(PATIENT_NOTES_THREADS).map((current) => ( ))}
diff --git a/src/Components/Facility/PatientNotesSlideover.tsx b/src/Components/Facility/PatientNotesSlideover.tsx index 9b1f7e2424d..d7847c3add7 100644 --- a/src/Components/Facility/PatientNotesSlideover.tsx +++ b/src/Components/Facility/PatientNotesSlideover.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, Dispatch, SetStateAction } from "react"; import * as Notification from "../../Utils/Notifications.js"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import CareIcon from "../../CAREUI/icons/CareIcon"; -import { classNames, isAppleDevice } from "../../Utils/utils"; +import { classNames, isAppleDevice, keysOf } from "../../Utils/utils"; import ButtonV2 from "../Common/components/ButtonV2"; import { useMessageListener } from "../../Common/hooks/useMessageListener"; import PatientConsultationNotesList from "./PatientConsultationNotesList"; @@ -193,23 +193,19 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { {notesActionIcons}
- {Object.values(PATIENT_NOTES_THREADS).map((current) => ( + {keysOf(PATIENT_NOTES_THREADS).map((current) => ( ))}
@@ -223,7 +219,7 @@ export default function PatientNotesSlideover(props: PatientNotesProps) { />
{ >
- {Object.values(PATIENT_NOTES_THREADS).map((current) => ( + {keysOf(PATIENT_NOTES_THREADS).map((current) => ( ))}
diff --git a/src/Locale/en/Consultation.json b/src/Locale/en/Consultation.json index 65ea63e875c..8282656b560 100644 --- a/src/Locale/en/Consultation.json +++ b/src/Locale/en/Consultation.json @@ -55,5 +55,7 @@ "back_dated_encounter_date_caution": "You are creating an encounter for", "encounter_duration_confirmation": "The duration of this encounter would be", "consultation_notes": "General Instructions (Advice)", - "procedure_suggestions": "Procedure Suggestions" -} + "procedure_suggestions": "Procedure Suggestions", + "patient_notes_thread__Doctors": "Doctor's Discussions", + "patient_notes_thread__Nurses": "Nurse's Discussions" +} \ No newline at end of file diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 716f0851464..46832e76228 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -525,3 +525,10 @@ export const celsiusToFahrenheit = (celsius: number) => { export const fahrenheitToCelsius = (fahrenheit: number) => { return ((fahrenheit - 32) * 5) / 9; }; + +/** + * Although same as `Objects.keys(...)`, this provides better type-safety. + */ +export const keysOf = (obj: T) => { + return Object.keys(obj) as (keyof T)[]; +}; From 775a8e5e6009bb8c314314ec6a9e1e501716da47 Mon Sep 17 00:00:00 2001 From: Kunal Dubey <21157775+xakep8@users.noreply.github.com> Date: Wed, 18 Sep 2024 07:22:57 +0530 Subject: [PATCH 2/6] RadioFormField: changed optionDisplay to optionLabel in RadioFormField (#8533) --- package-lock.json | 2 +- src/Components/ExternalResult/ResultUpdate.tsx | 2 +- src/Components/Facility/FacilityCreate.tsx | 2 +- src/Components/Form/FormFields/RadioFormField.tsx | 4 ++-- src/Components/LogUpdate/Sections/BloodSugar.tsx | 2 +- .../LogUpdate/Sections/NeurologicalMonitoring.tsx | 12 ++++++------ .../Sections/RespiratorySupport/OxygenSupport.tsx | 2 +- .../LogUpdate/Sections/RespiratorySupport/index.tsx | 4 ++-- src/Components/LogUpdate/Sections/Vitals.tsx | 2 +- src/Components/Patient/DailyRounds.tsx | 2 +- src/Components/Patient/PatientRegister.tsx | 10 +++++----- src/Components/Resource/ResourceCreate.tsx | 2 +- src/Components/Resource/ResourceDetailsUpdate.tsx | 2 +- src/Components/Shifting/ShiftDetailsUpdate.tsx | 6 +++--- 14 files changed, 27 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index dec462f006f..14fd0262607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19258,4 +19258,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Components/ExternalResult/ResultUpdate.tsx b/src/Components/ExternalResult/ResultUpdate.tsx index 482c320457e..3e892940ac8 100644 --- a/src/Components/ExternalResult/ResultUpdate.tsx +++ b/src/Components/ExternalResult/ResultUpdate.tsx @@ -289,7 +289,7 @@ export default function UpdateResult(props: any) { ]} value={state.form.patient_created} onChange={handleChange} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} optionValue={(option) => option.value} error={state.errors.patient_created} /> diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index 2f14bcc26bf..9c76e6e0ea0 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -928,7 +928,7 @@ export const FacilityCreate = (props: FacilityProps) => { {...field("kasp_empanelled")} label={`Is this facility ${careConfig.kasp.string} empanelled?`} options={[true, false]} - optionDisplay={(o) => (o ? "Yes" : "No")} + optionLabel={(o) => (o ? "Yes" : "No")} optionValue={(o) => String(o)} /> )} diff --git a/src/Components/Form/FormFields/RadioFormField.tsx b/src/Components/Form/FormFields/RadioFormField.tsx index c881bc66f94..ddb9178c6e9 100644 --- a/src/Components/Form/FormFields/RadioFormField.tsx +++ b/src/Components/Form/FormFields/RadioFormField.tsx @@ -5,7 +5,7 @@ import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils"; type Props = FormFieldBaseProps & { options: readonly T[]; - optionDisplay: (option: T) => React.ReactNode; + optionLabel: (option: T) => React.ReactNode; optionValue: (option: T) => V; containerClassName?: string; unselectLabel?: string; @@ -47,7 +47,7 @@ const RadioFormField = (props: Props) => { { label="Frequency" name="insulin_intake_frequency" options={INSULIN_INTAKE_FREQUENCY_OPTIONS} - optionDisplay={(c) => t(`INSULIN_INTAKE_FREQUENCY__${c}`)} + optionLabel={(c) => t(`INSULIN_INTAKE_FREQUENCY__${c}`)} optionValue={(c) => c} value={log.insulin_intake_frequency} onChange={(c) => diff --git a/src/Components/LogUpdate/Sections/NeurologicalMonitoring.tsx b/src/Components/LogUpdate/Sections/NeurologicalMonitoring.tsx index 0d237aafd49..7ef98c4010b 100644 --- a/src/Components/LogUpdate/Sections/NeurologicalMonitoring.tsx +++ b/src/Components/LogUpdate/Sections/NeurologicalMonitoring.tsx @@ -31,7 +31,7 @@ const NeurologicalMonitoring = ({ log, onChange }: LogUpdateSectionProps) => { t(`CONSCIOUSNESS_LEVEL__${c.value}`)} + optionLabel={(c) => t(`CONSCIOUSNESS_LEVEL__${c.value}`)} optionValue={(c) => c.value} value={log.consciousness_level} onChange={(c) => @@ -60,7 +60,7 @@ const NeurologicalMonitoring = ({ log, onChange }: LogUpdateSectionProps) => { (o) => o.value !== "UNKNOWN", )} id={`${d}_reaction`} - optionDisplay={(c) => t(`PUPIL_REACTION__${c.value}`)} + optionLabel={(c) => t(`PUPIL_REACTION__${c.value}`)} optionValue={(c) => c.value} name={`${d}_pupil_light_reaction`} value={log[`${d}_pupil_light_reaction`]} @@ -98,7 +98,7 @@ const NeurologicalMonitoring = ({ log, onChange }: LogUpdateSectionProps) => { Eye Opening Response} options={EYE_OPEN_SCALE} - optionDisplay={(c) => c.value + " - " + c.text} + optionLabel={(c) => c.value + " - " + c.text} optionValue={(c) => `${c.value}`} name="eye_opening_response" value={`${log.glasgow_eye_open}`} @@ -110,7 +110,7 @@ const NeurologicalMonitoring = ({ log, onChange }: LogUpdateSectionProps) => { Verbal Response} options={VERBAL_RESPONSE_SCALE} - optionDisplay={(c) => c.value + " - " + c.text} + optionLabel={(c) => c.value + " - " + c.text} optionValue={(c) => `${c.value}`} name="verbal_response" value={`${log.glasgow_verbal_response}`} @@ -124,7 +124,7 @@ const NeurologicalMonitoring = ({ log, onChange }: LogUpdateSectionProps) => { Motor Response} options={MOTOR_RESPONSE_SCALE} - optionDisplay={(c) => c.value + " - " + c.text} + optionLabel={(c) => c.value + " - " + c.text} optionValue={(c) => `${c.value}`} name="motor_response" value={`${log.glasgow_motor_response}`} @@ -164,7 +164,7 @@ const NeurologicalMonitoring = ({ log, onChange }: LogUpdateSectionProps) => { } options={LIMB_RESPONSE_OPTIONS.filter((o) => o.value !== "UNKNOWN")} - optionDisplay={(c) => t(`LIMB_RESPONSE__${c.value}`)} + optionLabel={(c) => t(`LIMB_RESPONSE__${c.value}`)} optionValue={(c) => c.value} name={key} value={log[key]} diff --git a/src/Components/LogUpdate/Sections/RespiratorySupport/OxygenSupport.tsx b/src/Components/LogUpdate/Sections/RespiratorySupport/OxygenSupport.tsx index cefcf2ddb0e..758b49613ce 100644 --- a/src/Components/LogUpdate/Sections/RespiratorySupport/OxygenSupport.tsx +++ b/src/Components/LogUpdate/Sections/RespiratorySupport/OxygenSupport.tsx @@ -13,7 +13,7 @@ const OxygenRespiratorySupport = ({ log, onChange }: LogUpdateSectionProps) => { Oxygen Modality} options={OXYGEN_MODALITY_OPTIONS} - optionDisplay={(c) => t(`OXYGEN_MODALITY__${c.value}`)} + optionLabel={(c) => t(`OXYGEN_MODALITY__${c.value}`)} optionValue={(c) => c.value} name="ventilator_oxygen_modality" value={log.ventilator_oxygen_modality} diff --git a/src/Components/LogUpdate/Sections/RespiratorySupport/index.tsx b/src/Components/LogUpdate/Sections/RespiratorySupport/index.tsx index 8d558820843..6c58d0bfa72 100644 --- a/src/Components/LogUpdate/Sections/RespiratorySupport/index.tsx +++ b/src/Components/LogUpdate/Sections/RespiratorySupport/index.tsx @@ -57,7 +57,7 @@ const RespiratorySupport = ({ log, onChange }: LogUpdateSectionProps) => { label="Bilateral Air Entry" labelClassName="text-lg sm:font-bold" options={[true, false]} - optionDisplay={(c) => (c ? "Yes" : "No")} + optionLabel={(c) => (c ? "Yes" : "No")} optionValue={(c) => JSON.stringify(c)} name="bilateral_air_entry" value={ @@ -90,7 +90,7 @@ const RespiratorySupport = ({ log, onChange }: LogUpdateSectionProps) => { Respiratory Support} options={RESPIRATORY_SUPPORT} - optionDisplay={(c) => t(`RESPIRATORY_SUPPORT__${c.value}`)} + optionLabel={(c) => t(`RESPIRATORY_SUPPORT__${c.value}`)} optionValue={(c) => c.value} name="respiratory_support" value={log.ventilator_interface} diff --git a/src/Components/LogUpdate/Sections/Vitals.tsx b/src/Components/LogUpdate/Sections/Vitals.tsx index 1fa4b3f3aac..8592f004370 100644 --- a/src/Components/LogUpdate/Sections/Vitals.tsx +++ b/src/Components/LogUpdate/Sections/Vitals.tsx @@ -140,7 +140,7 @@ const Vitals = ({ log, onChange }: LogUpdateSectionProps) => { label={t("heartbeat_rhythm")} name="heartbeat-rythm" options={HEARTBEAT_RHYTHM_CHOICES} - optionDisplay={(c) => t(`HEARTBEAT_RHYTHM__${c}`)} + optionLabel={(c) => t(`HEARTBEAT_RHYTHM__${c}`)} optionValue={(c) => c} value={log.rhythm} onChange={(c) => onChange({ rhythm: c.value ?? undefined })} diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index 0f0c7396f5e..7335e5c6983 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -770,7 +770,7 @@ export const DailyRounds = (props: any) => { label: t(`CONSCIOUSNESS_LEVEL__${level.value}`), value: level.value, }))} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} optionValue={(option) => option.value} unselectLabel="Unknown" layout="vertical" diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index ab20ff91e59..17eb745496b 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -1495,7 +1495,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { { label: "Yes", value: "true" }, { label: "No", value: "false" }, ]} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} optionValue={(option) => option.value} />
@@ -1528,7 +1528,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { { label: "Yes", value: "true" }, { label: "No", value: "false" }, ]} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} optionValue={(option) => option.value} /> @@ -1808,7 +1808,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { { label: "Yes", value: "true" }, { label: "No", value: "false" }, ]} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} optionValue={(option) => option.value} />
@@ -1842,7 +1842,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { value: "3", }, ]} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} optionValue={(option) => option.value} /> @@ -1877,7 +1877,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { { label: "Yes", value: "true" }, { label: "No", value: "false" }, ]} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} optionValue={(option) => option.value} /> (o ? t("yes") : t("no"))} + optionLabel={(o) => (o ? t("yes") : t("no"))} optionValue={(o) => String(o)} value={state.form.emergency} onChange={handleChange} diff --git a/src/Components/Resource/ResourceDetailsUpdate.tsx b/src/Components/Resource/ResourceDetailsUpdate.tsx index 178ab596148..6d293f05df5 100644 --- a/src/Components/Resource/ResourceDetailsUpdate.tsx +++ b/src/Components/Resource/ResourceDetailsUpdate.tsx @@ -298,7 +298,7 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { onChange={handleChange} label={"Is this an emergency?"} options={[true, false]} - optionDisplay={(o) => (o ? "Yes" : "No")} + optionLabel={(o) => (o ? "Yes" : "No")} optionValue={(o) => String(o)} value={String(state.form.emergency)} error={state.errors.emergency} diff --git a/src/Components/Shifting/ShiftDetailsUpdate.tsx b/src/Components/Shifting/ShiftDetailsUpdate.tsx index 8e6fb582906..ccf808ecda9 100644 --- a/src/Components/Shifting/ShiftDetailsUpdate.tsx +++ b/src/Components/Shifting/ShiftDetailsUpdate.tsx @@ -420,7 +420,7 @@ export const ShiftDetailsUpdate = (props: patientShiftProps) => { { label: t("yes"), value: "true" }, { label: t("no"), value: "false" }, ]} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} optionValue={(option) => option.value} /> @@ -434,7 +434,7 @@ export const ShiftDetailsUpdate = (props: patientShiftProps) => { { label: t("no"), value: "false" }, ]} optionValue={(option) => option.value} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} onChange={handleFormFieldChange} /> )} @@ -448,7 +448,7 @@ export const ShiftDetailsUpdate = (props: patientShiftProps) => { { label: t("no"), value: "false" }, ]} optionValue={(option) => option.value} - optionDisplay={(option) => option.label} + optionLabel={(option) => option.label} onChange={handleFormFieldChange} /> From c479e4602d10af48bb7c5ffdc988c41d44a4561b Mon Sep 17 00:00:00 2001 From: Hitish Rao P <140695309+HitishRaoP@users.noreply.github.com> Date: Wed, 18 Sep 2024 07:23:44 +0530 Subject: [PATCH 3/6] Fixes Facility cover image edit button malfunctioning (#8531) --- src/Components/Facility/FacilityHome.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index f1322e4d20e..22dd2bc830f 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -113,7 +113,7 @@ export const FacilityHome = ({ facilityId }: Props) => {
setEditCoverImage(true)} > From f7bda46d59a61acc7f7adf898024796febde5dc7 Mon Sep 17 00:00:00 2001 From: Zeeshan <95434634+Zeshanxviii@users.noreply.github.com> Date: Wed, 18 Sep 2024 07:26:54 +0530 Subject: [PATCH 4/6] Updated TreatmentSummary component by integrating it with PrintPreview Component #8430 (#8441) --- cypress/pageobject/Asset/AssetFilters.ts | 2 +- cypress/pageobject/Asset/AssetSearch.ts | 4 +- .../pageobject/Patient/PatientInsurance.ts | 2 +- .../Patient/PatientMedicalHistory.ts | 4 +- scripts/generate-build-version.js | 2 +- .../Investigations/InvestigationTable.tsx | 36 +++++----- .../Investigations/ShowInvestigation.tsx | 38 +++++----- src/Components/Facility/TreatmentSummary.tsx | 69 ++++++++----------- src/Locale/en/Consultation.json | 3 + src/style/index.css | 3 +- 10 files changed, 76 insertions(+), 87 deletions(-) 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/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/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/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/src/Components/Facility/Investigations/InvestigationTable.tsx b/src/Components/Facility/Investigations/InvestigationTable.tsx index 6984b0baedb..18c64108b6f 100644 --- a/src/Components/Facility/Investigations/InvestigationTable.tsx +++ b/src/Components/Facility/Investigations/InvestigationTable.tsx @@ -4,6 +4,7 @@ import { SelectFormField } from "../../Form/FormFields/SelectFormField"; import TextFormField from "../../Form/FormFields/TextFormField"; import { classNames } from "../../../Utils/utils"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; const TestRow = ({ data, i, onChange, showForm, value, isChanged }: any) => { return ( @@ -70,6 +71,7 @@ export const InvestigationTable = ({ handleUpdateCancel, handleSave, }: any) => { + const { t } = useTranslation(); const [searchFilter, setSearchFilter] = useState(""); const [showForm, setShowForm] = useState(false); const filterTests = Object.values(data).filter((i: any) => { @@ -84,16 +86,10 @@ export const InvestigationTable = ({ return (
- {title &&
{title}
} -
- window.print()} - className="my-2 mr-2" - disabled={showForm} - > - Print Report - + {title && ( +
{title}
+ )} +
setSearchFilter(e.value)} />
-
-
- - +
+
+
+ {["Name", "Value", "Unit", "Min", "Max", "Ideal"].map( (heading) => ( @@ -143,7 +140,7 @@ export const InvestigationTable = ({ )} - + {filterTests.length > 0 ? ( filterTests.map((t: any, i) => { const value = @@ -173,12 +170,13 @@ export const InvestigationTable = ({ }; handleValueChange(value, target); }} + className="print:text-black" /> ); }) ) : ( - - No tests taken + + )} diff --git a/src/Components/Facility/Investigations/ShowInvestigation.tsx b/src/Components/Facility/Investigations/ShowInvestigation.tsx index a7f74b98fde..7477c64f899 100644 --- a/src/Components/Facility/Investigations/ShowInvestigation.tsx +++ b/src/Components/Facility/Investigations/ShowInvestigation.tsx @@ -1,14 +1,13 @@ import _, { set } from "lodash-es"; import { navigate } from "raviger"; import { lazy, useCallback, useReducer } from "react"; -import { useTranslation } from "react-i18next"; import routes from "../../../Redux/api"; import * as Notification from "../../../Utils/Notifications.js"; import request from "../../../Utils/request/request"; import useQuery from "../../../Utils/request/useQuery"; -import PageTitle from "../../Common/PageTitle"; import InvestigationTable from "./InvestigationTable"; - +import PrintPreview from "../../../CAREUI/misc/PrintPreview"; +import { useTranslation } from "react-i18next"; const Loading = lazy(() => import("../../Common/Loading")); const initialState = { @@ -35,10 +34,15 @@ const updateFormReducer = (state = initialState, action: any) => { } }; -export default function ShowInvestigation(props: any) { +interface ShowInvestigationProps { + consultationId: string; + patientId: string; + sessionId: string; + facilityId: string; +} +export default function ShowInvestigation(props: ShowInvestigationProps) { + const { consultationId, patientId, sessionId } = props; const { t } = useTranslation(); - const { consultationId, patientId, facilityId, sessionId } = props; - const [state, dispatch] = useReducer(updateFormReducer, initialState); const { loading: investigationLoading } = useQuery(routes.getInvestigation, { pathParams: { @@ -141,20 +145,16 @@ export default function ShowInvestigation(props: any) { if (patientLoading || investigationLoading) { return ; } - return ( -
- + -
+ ); } diff --git a/src/Components/Facility/TreatmentSummary.tsx b/src/Components/Facility/TreatmentSummary.tsx index e719e859a4b..610e7dc189d 100644 --- a/src/Components/Facility/TreatmentSummary.tsx +++ b/src/Components/Facility/TreatmentSummary.tsx @@ -4,10 +4,8 @@ import { formatDateTime, formatPatientAge, } from "../../Utils/utils"; -import useAppHistory from "../../Common/hooks/useAppHistory"; import routes from "../../Redux/api"; import useQuery from "../../Utils/request/useQuery"; -import CareIcon from "../../CAREUI/icons/CareIcon"; import { ConsultationModel } from "./models"; import { useMemo } from "react"; import { @@ -18,6 +16,8 @@ import PageHeadTitle from "../Common/PageHeadTitle"; import { useTranslation } from "react-i18next"; import { PatientModel } from "../Patient/models"; import MedicineRoutes from "../Medicine/routes"; +import PrintPreview from "../../CAREUI/misc/PrintPreview"; +import careConfig from "@careConfig"; export interface ITreatmentSummaryProps { consultationId: string; @@ -28,12 +28,9 @@ export interface ITreatmentSummaryProps { export default function TreatmentSummary({ consultationId, patientId, - facilityId, }: ITreatmentSummaryProps) { const { t } = useTranslation(); const date = new Date(); - const { goBack } = useAppHistory(); - const url = `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}`; const { data: patientData } = useQuery(routes.getPatient, { pathParams: { id: patientId }, @@ -49,50 +46,42 @@ export default function TreatmentSummary({
-
- - -
- -
-

- {consultationData?.facility_name ?? ""} -

+ +
+
+

{consultationData?.facility_name}

+ care logo +
+

+ {t("treatment_summary__heading")} +

-

- {t("treatment_summary__heading")} -

+
{formatDate(date)}
-
{formatDate(date)}
+
+ -
- + - + - + - + - + - - - + +
-
+
); diff --git a/src/Locale/en/Consultation.json b/src/Locale/en/Consultation.json index 8282656b560..3d101bcb2bd 100644 --- a/src/Locale/en/Consultation.json +++ b/src/Locale/en/Consultation.json @@ -22,9 +22,12 @@ "investigations": "Investigations", "search_investigation_placeholder": "Search Investigation & Groups", "save_investigation": "Save Investigation", + "investigation_report_for_{{name}}": "Investigation Report for {{name}}", + "investigation_report_of_{{name}}":"Investigation Report of : {{name}}", "investigation_reports": "Investigation Reports", "no_investigation": "No investigation Reports found", "investigations_suggested": "Investigations Suggested", + "no_tests_taken":"No tests taken", "to_be_conducted": "To be conducted", "log_report": "Log Report", "no_investigation_suggestions": "No Investigation Suggestions", diff --git a/src/style/index.css b/src/style/index.css index 327cc03d047..254bf4e9b6e 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -435,7 +435,6 @@ button:disabled, background-size: 1000px 100%; } - @media print { body * { visibility: hidden; @@ -450,7 +449,7 @@ button:disabled, left: 0; top: 0; } -} +} .header-section .appBar { z-index: 1201; From 14814085c93a7e1e11abf342c80abafae7913ca8 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 18 Sep 2024 17:27:41 +0530 Subject: [PATCH 5/6] Adds Community Nurse's Log; improved Nursing Care section UI (#8522) --- .../e2e/patient_spec/PatientLogUpdate.cy.ts | 20 +- src/Common/constants.tsx | 75 ++++- .../ConsultationNursingTab.tsx | 224 ++++++++++++- .../Facility/ConsultationDetails/index.tsx | 21 +- .../Facility/Consultations/ABGPlots.tsx | 16 +- .../DailyRounds/DefaultLogUpdateCard.tsx | 31 +- .../Consultations/DailyRoundsList.tsx | 13 - .../Facility/Consultations/DialysisPlots.tsx | 6 +- .../Consultations/NeurologicalTables.tsx | 24 +- .../Facility/Consultations/NursingPlot.tsx | 8 +- .../Facility/Consultations/NutritionPlots.tsx | 13 +- .../Facility/Consultations/PainDiagrams.tsx | 5 +- .../Consultations/PressureSoreDiagrams.tsx | 7 +- .../Consultations/PrimaryParametersPlot.tsx | 15 +- .../Facility/Consultations/VentilatorPlot.tsx | 19 +- src/Components/Facility/models.tsx | 178 +++++----- .../Form/FormFields/SelectFormField.tsx | 2 +- src/Components/Form/MultiSelectMenuV2.tsx | 2 +- .../LogUpdate/CriticalCarePreview.tsx | 212 +++++++++--- .../LogUpdate/Sections/NursingCare.tsx | 110 +++---- src/Components/LogUpdate/Sections/Vitals.tsx | 14 +- src/Components/LogUpdate/Sections/index.tsx | 1 + src/Components/Patient/DailyRounds.tsx | 309 +++++++++++++----- src/Components/Patient/models.tsx | 20 ++ src/Locale/en/Common.json | 6 +- src/Locale/en/Consultation.json | 15 + src/Locale/en/LogUpdate.json | 82 ++++- src/Locale/hi/LogUpdate.json | 8 +- src/Locale/kn/LogUpdate.json | 8 +- src/Locale/ml/LogUpdate.json | 8 +- src/Locale/ta/LogUpdate.json | 8 +- 31 files changed, 1022 insertions(+), 458 deletions(-) diff --git a/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts index 773b0087d80..923d9410657 100644 --- a/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts +++ b/cypress/e2e/patient_spec/PatientLogUpdate.cy.ts @@ -55,7 +55,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.selectPatientCategory(patientCategory); patientLogupdate.selectRoundType("Detailed Update"); cy.submitButton("Save and Continue"); - cy.verifyNotification("Detailed Update log created successfully"); + cy.verifyNotification("Detailed Update created successfully"); cy.closeNotification(); // Select two Section - First One is Respiratory Support patientLogupdate.selectCriticalCareSection("Respiratory Support"); @@ -162,7 +162,7 @@ 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 patientLogupdate.selectCriticalCareSection("Neurological Monitoring"); @@ -192,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", () => { @@ -204,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"]); @@ -219,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", () => { @@ -245,7 +245,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("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(); @@ -271,7 +271,7 @@ 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.clickLogUpdateViewDetails( "#dailyround-entry", @@ -307,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]); @@ -330,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 @@ -340,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/src/Common/constants.tsx b/src/Common/constants.tsx index 681fe874475..74150a53e4f 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -773,28 +773,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" }, diff --git a/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx index f8d38b2adb3..2f689c82d19 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationNursingTab.tsx @@ -1,18 +1,228 @@ -import { lazy } from "react"; +import { lazy, useEffect, useState } from "react"; import { ConsultationTabProps } from "./index"; import { NursingPlot } from "../Consultations/NursingPlot"; +import { useTranslation } from "react-i18next"; +import request from "../../../Utils/request/request"; +import routes from "../../../Redux/api"; +import { RoutineAnalysisRes, RoutineFields } from "../models"; +import Loading from "../../Common/Loading"; +import { classNames, formatDate, formatTime } from "../../../Utils/utils"; +import Pagination from "../../Common/Pagination"; +import { PAGINATION_LIMIT } from "../../../Common/constants"; const PageTitle = lazy(() => import("../../Common/PageTitle")); -export const ConsultationNursingTab = (props: ConsultationTabProps) => { +export default function ConsultationNursingTab(props: ConsultationTabProps) { + const { t } = useTranslation(); return (
- - +
+

{t("routine")}

+ +
+
+

{t("nursing_care")}

+ +
+
+ ); +} + +const REVERSE_CHOICES = { + appetite: { + 1: "INCREASED", + 2: "SATISFACTORY", + 3: "REDUCED", + 4: "NO_TASTE_FOR_FOOD", + 5: "CANNOT_BE_ASSESSED", + }, + bladder_drainage: { + 1: "NORMAL", + 2: "CONDOM_CATHETER", + 3: "DIAPER", + 4: "INTERMITTENT_CATHETER", + 5: "CONTINUOUS_INDWELLING_CATHETER", + 6: "CONTINUOUS_SUPRAPUBIC_CATHETER", + 7: "UROSTOMY", + }, + bladder_issue: { + 0: "NO_ISSUES", + 1: "INCONTINENCE", + 2: "RETENTION", + 3: "HESITANCY", + }, + bowel_issue: { + 0: "NO_DIFFICULTY", + 1: "CONSTIPATION", + 2: "DIARRHOEA", + }, + nutrition_route: { + 1: "ORAL", + 2: "RYLES_TUBE", + 3: "GASTROSTOMY_OR_JEJUNOSTOMY", + 4: "PEG", + 5: "PARENTERAL_TUBING_FLUID", + 6: "PARENTERAL_TUBING_TPN", + }, + oral_issue: { + 0: "NO_ISSUE", + 1: "DYSPHAGIA", + 2: "ODYNOPHAGIA", + }, + is_experiencing_dysuria: { + true: "yes", + false: "no", + }, + urination_frequency: { + 1: "NORMAL", + 2: "DECREASED", + 3: "INCREASED", + }, + sleep: { + 1: "EXCESSIVE", + 2: "SATISFACTORY", + 3: "UNSATISFACTORY", + 4: "NO_SLEEP", + }, +} as const; + +const ROUTINE_ROWS = [ + { field: "sleep" } as const, + { field: "bowel_issue" } as const, + { title: "Bladder" } as const, + { subField: true, field: "bladder_drainage" } as const, + { subField: true, field: "bladder_issue" } as const, + { subField: true, field: "is_experiencing_dysuria" } as const, + { subField: true, field: "urination_frequency" } as const, + { title: "Nutrition" } as const, + { subField: true, field: "nutrition_route" } as const, + { subField: true, field: "oral_issue" } as const, + { subField: true, field: "appetite" } as const, +]; + +const RoutineSection = ({ consultationId }: ConsultationTabProps) => { + const { t } = useTranslation(); + const [page, setPage] = useState(1); + const [totalCount, setTotalCount] = useState(); + const [results, setResults] = useState>(); + + useEffect(() => { + const getData = async () => { + const { data } = await request(routes.dailyRoundsAnalyse, { + body: { fields: RoutineFields, page }, + pathParams: { consultationId }, + }); + if (!data) { + return; + } + setTotalCount(data.count); + setResults( + Object.fromEntries( + Object.entries(data.results).filter(([_, value]) => + Object.entries(value).some(([k, v]) => k !== "id" && v != null), + ), + ) as typeof results, + ); + }; + + getData(); + }, [page, consultationId]); + + if (results == null) { + return ; + } + + if (Object.keys(results).length === 0) { + return ( +
+
+ {t("no_data_found")} +
+
+ ); + } + + return ( +
+
+
{heading}
{t("no_tests_taken")}
+ + + + ))} + + + + {ROUTINE_ROWS.map((row) => ( + + + {row.field && + Object.values(results).map((obj, idx) => ( + + ))} + + ))} + +
+ {Object.keys(results).map((date) => ( + +

{formatDate(date)}

+

{formatTime(date)}

+
+ {row.title ?? t(`LOG_UPDATE_FIELD_LABEL__${row.field!}`)} + + {(() => { + const value = obj[row.field]; + if (value == null) { + return "-"; + } + if (typeof value === "boolean") { + return t(value ? "yes" : "no"); + } + const choices = REVERSE_CHOICES[row.field]; + const choice = `${row.field.toUpperCase()}__${choices[value as keyof typeof choices]}`; + return t(choice); + })()} +
+
+ + {totalCount != null && totalCount > PAGINATION_LIMIT && ( +
+ +
+ )}
); }; diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 78f7022b683..911e62d5627 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -1,4 +1,4 @@ -import { CONSULTATION_TABS, GENDER_TYPES } from "../../../Common/constants"; +import { GENDER_TYPES } from "../../../Common/constants"; import { ConsultationModel } from "../models"; import { getConsultation, @@ -13,6 +13,7 @@ import { PatientModel } from "../../Patient/models"; import { formatDateTime, humanizeStrings, + keysOf, relativeTime, } from "../../../Utils/utils"; @@ -22,7 +23,7 @@ import { triggerGoal } from "../../../Integrations/Plausible"; import useAuthUser from "../../../Common/hooks/useAuthUser"; import { ConsultationUpdatesTab } from "./ConsultationUpdatesTab"; import { ConsultationABGTab } from "./ConsultationABGTab"; -import { ConsultationNursingTab } from "./ConsultationNursingTab"; +import ConsultationNursingTab from "./ConsultationNursingTab"; import { ConsultationFeedTab } from "./ConsultationFeedTab"; import { ConsultationSummaryTab } from "./ConsultationSummaryTab"; import { ConsultationFilesTab } from "./ConsultationFilesTab"; @@ -44,6 +45,7 @@ import routes from "../../../Redux/api"; import request from "../../../Utils/request/request"; import { CameraFeedPermittedUserTypes } from "../../../Utils/permissions"; import Error404 from "../../ErrorPages/404"; +import { useTranslation } from "react-i18next"; const Loading = lazy(() => import("../../Common/Loading")); const PageTitle = lazy(() => import("../../Common/PageTitle")); @@ -75,6 +77,7 @@ const TABS = { export const ConsultationDetails = (props: any) => { const { facilityId, patientId, consultationId } = props; + const { t } = useTranslation(); let tab = undefined; if (Object.keys(TABS).includes(props.tab.toUpperCase())) { tab = props.tab.toUpperCase() as keyof typeof TABS; @@ -388,8 +391,8 @@ export const ConsultationDetails = (props: any) => { className="flex space-x-6 overflow-x-auto pb-2 pl-2" id="consultation_tab_nav" > - {CONSULTATION_TABS.map((p) => { - if (p.text === "FEED") { + {keysOf(TABS).map((p) => { + if (p === "FEED") { if ( isCameraAttached === false || // No camera attached consultationData?.discharge_date || // Discharged @@ -399,17 +402,17 @@ export const ConsultationDetails = (props: any) => { return null; // Hide feed tab } - if (p.text === "ABDM" && !abhaNumberData?.abha_number) { + if (p === "ABDM" && !abhaNumberData?.abha_number) { return null; } return ( - {p.desc} + {t(`CONSULTATION_TAB__${p}`)} ); })} diff --git a/src/Components/Facility/Consultations/ABGPlots.tsx b/src/Components/Facility/Consultations/ABGPlots.tsx index 7b1809730bf..10415920ceb 100644 --- a/src/Components/Facility/Consultations/ABGPlots.tsx +++ b/src/Components/Facility/Consultations/ABGPlots.tsx @@ -5,6 +5,7 @@ import { PAGINATION_LIMIT } from "../../../Common/constants"; import { formatDateTime } from "../../../Utils/utils"; import routes from "../../../Redux/api"; import request from "../../../Utils/request/request"; +import { ABGPlotsFields } from "../models"; export const ABGPlots = (props: any) => { const { consultationId } = props; @@ -15,20 +16,7 @@ export const ABGPlots = (props: any) => { useEffect(() => { const fetchDailyRounds = async (currentPage: number) => { const { res, data } = await request(routes.dailyRoundsAnalyse, { - body: { - page: currentPage, - fields: [ - "ph", - "pco2", - "po2", - "hco3", - "base_excess", - "lactate", - "sodium", - "potassium", - "ventilator_fio2", - ], - }, + body: { page: currentPage, fields: ABGPlotsFields }, pathParams: { consultationId, }, diff --git a/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx b/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx index 9096e66b69b..fa589c89710 100644 --- a/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx +++ b/src/Components/Facility/Consultations/DailyRounds/DefaultLogUpdateCard.tsx @@ -4,16 +4,22 @@ import ButtonV2 from "../../../Common/components/ButtonV2"; import { DailyRoundsModel } from "../../../Patient/models"; import LogUpdateCardAttribute from "./LogUpdateCardAttribute"; import { ConsultationModel } from "../../models"; +import { useSlugs } from "../../../../Common/hooks/useSlug"; interface Props { round: DailyRoundsModel; consultationData: ConsultationModel; - onViewDetails: () => void; - onUpdateLog?: () => void; } const DefaultLogUpdateCard = ({ round, ...props }: Props) => { + const [facilityId, patientId, consultationId] = useSlugs( + "facility", + "patient", + "consultation", + ); const { t } = useTranslation(); + const consultationUrl = `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}`; + return (
{ > { ghost size="small" className="w-full" - onClick={props.onViewDetails} + href={ + ["NORMAL", "TELEMEDICINE", "DOCTORS_LOG"].includes( + round.rounds_type!, + ) + ? `${consultationUrl}/daily-rounds/${round.id}` + : `${consultationUrl}/daily_rounds/${round.id}` + } > {t("view_details")} @@ -55,7 +67,16 @@ const DefaultLogUpdateCard = ({ round, ...props }: Props) => { ghost size="small" className="w-full" - onClick={props.onUpdateLog} + href={ + [ + "NORMAL", + "TELEMEDICINE", + "DOCTORS_LOG", + "COMMUNITY_NURSES_LOG", + ].includes(round.rounds_type!) + ? `${consultationUrl}/daily-rounds/${round.id}/update` + : `${consultationUrl}/daily_rounds/${round.id}/update` + } > {t("update_log")} diff --git a/src/Components/Facility/Consultations/DailyRoundsList.tsx b/src/Components/Facility/Consultations/DailyRoundsList.tsx index ddf5b28fd8f..561a4f1a958 100644 --- a/src/Components/Facility/Consultations/DailyRoundsList.tsx +++ b/src/Components/Facility/Consultations/DailyRoundsList.tsx @@ -1,4 +1,3 @@ -import { navigate } from "raviger"; import { DailyRoundsModel } from "../../Patient/models"; import VirtualNursingAssistantLogUpdateCard from "./DailyRounds/VirtualNursingAssistantLogUpdateCard"; import DefaultLogUpdateCard from "./DailyRounds/DefaultLogUpdateCard"; @@ -24,8 +23,6 @@ export default function DailyRoundsList({ consultation }: Props) { const { t } = useTranslation(); const [query, setQuery] = useState(); - const consultationUrl = `/facility/${consultation.facility}/patient/${consultation.patient}/consultation/${consultation.id}`; - return ( navigate(itemUrl)} - onUpdateLog={() => navigate(`${itemUrl}/update`)} /> ); diff --git a/src/Components/Facility/Consultations/DialysisPlots.tsx b/src/Components/Facility/Consultations/DialysisPlots.tsx index 50bbf911208..54c2dab60b9 100644 --- a/src/Components/Facility/Consultations/DialysisPlots.tsx +++ b/src/Components/Facility/Consultations/DialysisPlots.tsx @@ -5,6 +5,7 @@ import { LinePlot } from "./components/LinePlot"; import Pagination from "../../Common/Pagination"; import { PAGINATION_LIMIT } from "../../../Common/constants"; import { formatDateTime } from "../../../Utils/utils"; +import { DialysisPlotsFields } from "../models"; export const DialysisPlots = (props: any) => { const { consultationId } = props; @@ -15,10 +16,7 @@ export const DialysisPlots = (props: any) => { useEffect(() => { const fetchDailyRounds = async (currentPage: number) => { const { res, data } = await request(routes.dailyRoundsAnalyse, { - body: { - page: currentPage, - fields: ["dialysis_fluid_balance", "dialysis_net_balance"], - }, + body: { page: currentPage, fields: DialysisPlotsFields }, pathParams: { consultationId, }, diff --git a/src/Components/Facility/Consultations/NeurologicalTables.tsx b/src/Components/Facility/Consultations/NeurologicalTables.tsx index 5a1abe3e189..6ee363e341f 100644 --- a/src/Components/Facility/Consultations/NeurologicalTables.tsx +++ b/src/Components/Facility/Consultations/NeurologicalTables.tsx @@ -15,6 +15,7 @@ import { } from "../../../Common/constants"; import { formatDateTime } from "../../../Utils/utils"; import { useTranslation } from "react-i18next"; +import { NeurologicalTablesFields } from "../models"; const DataTable = (props: any) => { const { title, data } = props; @@ -113,28 +114,7 @@ export const NeurologicalTable = (props: any) => { consultationId: string, ) => { const { res, data } = await request(routes.dailyRoundsAnalyse, { - body: { - page: currentPage, - fields: [ - "consciousness_level", - "left_pupil_size", - "left_pupil_size_detail", - "right_pupil_size", - "right_pupil_size_detail", - "left_pupil_light_reaction", - "left_pupil_light_reaction_detail", - "right_pupil_light_reaction", - "right_pupil_light_reaction_detail", - "limb_response_upper_extremity_right", - "limb_response_upper_extremity_left", - "limb_response_lower_extremity_left", - "limb_response_lower_extremity_right", - "glasgow_eye_open", - "glasgow_verbal_response", - "glasgow_motor_response", - "glasgow_total_calculated", - ], - }, + body: { page: currentPage, fields: NeurologicalTablesFields }, pathParams: { consultationId, }, diff --git a/src/Components/Facility/Consultations/NursingPlot.tsx b/src/Components/Facility/Consultations/NursingPlot.tsx index fcae4bb43db..c747ec60d26 100644 --- a/src/Components/Facility/Consultations/NursingPlot.tsx +++ b/src/Components/Facility/Consultations/NursingPlot.tsx @@ -9,6 +9,7 @@ import { import Pagination from "../../Common/Pagination"; import { formatDateTime } from "../../../Utils/utils"; import { useTranslation } from "react-i18next"; +import { NursingPlotFields } from "../models"; export const NursingPlot = ({ consultationId }: any) => { const { t } = useTranslation(); @@ -22,10 +23,7 @@ export const NursingPlot = ({ consultationId }: any) => { consultationId: string, ) => { const { res, data } = await request(routes.dailyRoundsAnalyse, { - body: { - page: currentPage, - fields: ["nursing"], - }, + body: { page: currentPage, fields: NursingPlotFields }, pathParams: { consultationId, }, @@ -81,7 +79,7 @@ export const NursingPlot = ({ consultationId }: any) => { {areFieldsEmpty() && (
- No data available + {t("no_data_found")}
)} diff --git a/src/Components/Facility/Consultations/NutritionPlots.tsx b/src/Components/Facility/Consultations/NutritionPlots.tsx index 835a1c2a28e..b489b257184 100644 --- a/src/Components/Facility/Consultations/NutritionPlots.tsx +++ b/src/Components/Facility/Consultations/NutritionPlots.tsx @@ -8,6 +8,7 @@ import Pagination from "../../Common/Pagination"; import { PAGINATION_LIMIT } from "../../../Common/constants"; import { formatDateTime } from "../../../Utils/utils"; import CareIcon from "../../../CAREUI/icons/CareIcon"; +import { NutritionPlotsFields } from "../models"; export const NutritionPlots = (props: any) => { const { consultationId } = props; @@ -24,17 +25,7 @@ export const NutritionPlots = (props: any) => { consultationId: string, ) => { const { res, data } = await request(routes.dailyRoundsAnalyse, { - body: { - page: currentPage, - fields: [ - "infusions", - "iv_fluids", - "feeds", - "total_intake_calculated", - "total_output_calculated", - "output", - ], - }, + body: { page: currentPage, fields: NutritionPlotsFields }, pathParams: { consultationId, }, diff --git a/src/Components/Facility/Consultations/PainDiagrams.tsx b/src/Components/Facility/Consultations/PainDiagrams.tsx index baf7308d03d..2b73fe978ad 100644 --- a/src/Components/Facility/Consultations/PainDiagrams.tsx +++ b/src/Components/Facility/Consultations/PainDiagrams.tsx @@ -3,6 +3,7 @@ import routes from "../../../Redux/api"; import request from "../../../Utils/request/request"; import { formatDateTime } from "../../../Utils/utils"; import PainChart from "../../LogUpdate/components/PainChart"; +import { PainDiagramsFields } from "../models"; export const PainDiagrams = (props: any) => { const { consultationId } = props; @@ -19,9 +20,7 @@ export const PainDiagrams = (props: any) => { const { res, data: dailyRound } = await request( routes.dailyRoundsAnalyse, { - body: { - fields: ["pain_scale_enhanced"], - }, + body: { fields: PainDiagramsFields }, pathParams: { consultationId, }, diff --git a/src/Components/Facility/Consultations/PressureSoreDiagrams.tsx b/src/Components/Facility/Consultations/PressureSoreDiagrams.tsx index a0d559a0eca..8113a81e99b 100644 --- a/src/Components/Facility/Consultations/PressureSoreDiagrams.tsx +++ b/src/Components/Facility/Consultations/PressureSoreDiagrams.tsx @@ -5,7 +5,7 @@ import Pagination from "../../Common/Pagination"; import { PAGINATION_LIMIT } from "../../../Common/constants"; import { formatDateTime } from "../../../Utils/utils"; -import { PressureSoreDiagramsRes } from "../models"; +import { PressureSoreDiagramsFields, PressureSoreDiagramsRes } from "../models"; import PressureSore from "../../LogUpdate/Sections/PressureSore/PressureSore"; export const PressureSoreDiagrams = (props: any) => { @@ -28,10 +28,7 @@ export const PressureSoreDiagrams = (props: any) => { const { res, data: dailyRounds } = await request( routes.dailyRoundsAnalyse, { - body: { - page: currentPage, - fields: ["pressure_sore"], - }, + body: { page: currentPage, fields: PressureSoreDiagramsFields }, pathParams: { consultationId, }, diff --git a/src/Components/Facility/Consultations/PrimaryParametersPlot.tsx b/src/Components/Facility/Consultations/PrimaryParametersPlot.tsx index ca01ac1b113..0fb0844c576 100644 --- a/src/Components/Facility/Consultations/PrimaryParametersPlot.tsx +++ b/src/Components/Facility/Consultations/PrimaryParametersPlot.tsx @@ -10,6 +10,7 @@ import CareIcon from "../../../CAREUI/icons/CareIcon"; import { PainDiagrams } from "./PainDiagrams"; import PageTitle from "../../Common/PageTitle"; import dayjs from "../../../Utils/dayjs"; +import { PrimaryParametersPlotFields } from "../models"; interface PrimaryParametersPlotProps { facilityId: string; @@ -43,19 +44,7 @@ export const PrimaryParametersPlot = ({ const { res, data } = await request(routes.dailyRoundsAnalyse, { body: { page: currentPage, - fields: [ - "bp", - "pulse", - "temperature", - "resp", - "blood_sugar_level", - "insulin_intake_frequency", - "insulin_intake_dose", - "ventilator_spo2", - "ventilator_fio2", - "rhythm", - "rhythm_detail", - ], + fields: PrimaryParametersPlotFields, }, pathParams: { consultationId, diff --git a/src/Components/Facility/Consultations/VentilatorPlot.tsx b/src/Components/Facility/Consultations/VentilatorPlot.tsx index aa07b639f88..778f274918c 100644 --- a/src/Components/Facility/Consultations/VentilatorPlot.tsx +++ b/src/Components/Facility/Consultations/VentilatorPlot.tsx @@ -6,6 +6,7 @@ import Pagination from "../../Common/Pagination"; import { PAGINATION_LIMIT } from "../../../Common/constants"; import { formatDateTime } from "../../../Utils/utils"; import BinaryChronologicalChart from "./components/BinaryChronologicalChart"; +import { VentilatorPlotFields } from "../models"; /* interface ModalityType { @@ -39,23 +40,7 @@ export const VentilatorPlot = (props: any) => { consultationId: string, ) => { const { res, data } = await request(routes.dailyRoundsAnalyse, { - body: { - page: currentPage, - fields: [ - "ventilator_pip", - "ventilator_mean_airway_pressure", - "ventilator_resp_rate", - "ventilator_pressure_support", - "ventilator_tidal_volume", - "ventilator_peep", - "ventilator_fio2", - "ventilator_spo2", - "etco2", - "bilateral_air_entry", - "ventilator_oxygen_modality_oxygen_rate", - "ventilator_oxygen_modality_flow_rate", - ], - }, + body: { page: currentPage, fields: VentilatorPlotFields }, pathParams: { consultationId, }, diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 3f060240ee7..92a55ae6ae7 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -282,16 +282,17 @@ export type ICD11DiagnosisModel = { label: string; }; -export type ABGPlotsFields = - | "ph" - | "pco2" - | "po2" - | "hco3" - | "base_excess" - | "lactate" - | "sodium" - | "potassium" - | "ventilator_fio2"; +export const ABGPlotsFields = [ + "ph", + "pco2", + "po2", + "hco3", + "base_excess", + "lactate", + "sodium", + "potassium", + "ventilator_fio2", +] as const satisfies (keyof DailyRoundsModel)[]; export type ABGPlotsRes = { ph: string; @@ -305,33 +306,35 @@ export type ABGPlotsRes = { ventilator_fio2: number; }; -export type DialysisPlotsFields = - | "dialysis_fluid_balance" - | "dialysis_net_balance"; +export const DialysisPlotsFields = [ + "dialysis_fluid_balance", + "dialysis_net_balance", +] as const satisfies (keyof DailyRoundsModel)[]; export type DialysisPlotsRes = { dialysis_fluid_balance: number; dialysis_net_balance: number; }; -export type NeurologicalTablesFields = - | "consciousness_level" - | "left_pupil_size" - | "left_pupil_size_detail" - | "right_pupil_size" - | "right_pupil_size_detail" - | "left_pupil_light_reaction" - | "left_pupil_light_reaction_detail" - | "right_pupil_light_reaction" - | "right_pupil_light_reaction_detail" - | "limb_response_upper_extremity_right" - | "limb_response_upper_extremity_left" - | "limb_response_lower_extremity_left" - | "limb_response_lower_extremity_right" - | "glasgow_eye_open" - | "glasgow_verbal_response" - | "glasgow_motor_response" - | "glasgow_total_calculated"; +export const NeurologicalTablesFields = [ + "consciousness_level", + "left_pupil_size", + "left_pupil_size_detail", + "right_pupil_size", + "right_pupil_size_detail", + "left_pupil_light_reaction", + "left_pupil_light_reaction_detail", + "right_pupil_light_reaction", + "right_pupil_light_reaction_detail", + "limb_response_upper_extremity_right", + "limb_response_upper_extremity_left", + "limb_response_lower_extremity_left", + "limb_response_lower_extremity_right", + "glasgow_eye_open", + "glasgow_verbal_response", + "glasgow_motor_response", + "glasgow_total_calculated", +] as const satisfies (keyof DailyRoundsModel)[]; export type NeurologicalTablesRes = { consciousness_level: number; @@ -353,19 +356,36 @@ export type NeurologicalTablesRes = { glasgow_total_calculated: number; }; -export type NursingPlotFields = "nursing"; +export const NursingPlotFields = [ + "nursing", +] as const satisfies (keyof DailyRoundsModel)[]; export type NursingPlotRes = { nursing: any[]; }; -export type NutritionPlotsFields = - | "infusions" - | "iv_fluids" - | "feeds" - | "total_intake_calculated" - | "total_output_calculated" - | "output"; +export const RoutineFields = [ + "sleep", + "bowel_issue", + "bladder_drainage", + "bladder_issue", + "is_experiencing_dysuria", + "urination_frequency", + "nutrition_route", + "oral_issue", + "appetite", +] as const satisfies (keyof DailyRoundsModel)[]; + +export type RoutineAnalysisRes = Record<(typeof RoutineFields)[number], any>; + +export const NutritionPlotsFields = [ + "infusions", + "iv_fluids", + "feeds", + "total_intake_calculated", + "total_output_calculated", + "output", +] as const satisfies (keyof DailyRoundsModel)[]; export type NutritionPlotsRes = { infusions: any[]; @@ -376,30 +396,35 @@ export type NutritionPlotsRes = { output: any[]; }; -export type PainDiagramsFields = "pain_scale_enhanced"; +export const PainDiagramsFields = [ + "pain_scale_enhanced", +] as const satisfies (keyof DailyRoundsModel)[]; export type PainDiagramsRes = { pain_scale_enhanced: any[]; }; -export type PressureSoreDiagramsFields = "pressure_sore"; +export const PressureSoreDiagramsFields = [ + "pressure_sore", +] as const satisfies (keyof DailyRoundsModel)[]; export type PressureSoreDiagramsRes = { pressure_sore: any[]; }; -export type PrimaryParametersPlotFields = - | "bp" - | "pulse" - | "temperature" - | "resp" - | "blood_sugar_level" - | "insulin_intake_frequency" - | "insulin_intake_dose" - | "ventilator_spo2" - | "ventilator_fio2" - | "rhythm" - | "rhythm_detail"; +export const PrimaryParametersPlotFields = [ + "bp", + "pulse", + "temperature", + "resp", + "blood_sugar_level", + "insulin_intake_frequency", + "insulin_intake_dose", + "ventilator_spo2", + "ventilator_fio2", + "rhythm", + "rhythm_detail", +] as const satisfies (keyof DailyRoundsModel)[]; export type PrimaryParametersPlotRes = { bp: { @@ -419,19 +444,20 @@ export type PrimaryParametersPlotRes = { rhythm_detail: string; }; -export type VentilatorPlotFields = - | "ventilator_pip" - | "ventilator_mean_airway_pressure" - | "ventilator_resp_rate" - | "ventilator_pressure_support" - | "ventilator_tidal_volume" - | "ventilator_peep" - | "ventilator_fio2" - | "ventilator_spo2" - | "etco2" - | "bilateral_air_entry" - | "ventilator_oxygen_modality_oxygen_rate" - | "ventilator_oxygen_modality_flow_rate"; +export const VentilatorPlotFields = [ + "ventilator_pip", + "ventilator_mean_airway_pressure", + "ventilator_resp_rate", + "ventilator_pressure_support", + "ventilator_tidal_volume", + "ventilator_peep", + "ventilator_fio2", + "ventilator_spo2", + "etco2", + "bilateral_air_entry", + "ventilator_oxygen_modality_oxygen_rate", + "ventilator_oxygen_modality_flow_rate", +] as const satisfies (keyof DailyRoundsModel)[]; export type VentilatorPlotRes = { ventilator_pip: number; @@ -451,15 +477,16 @@ export type VentilatorPlotRes = { export interface DailyRoundsBody { page?: number; fields: - | ABGPlotsFields[] - | DialysisPlotsFields[] - | NeurologicalTablesFields[] - | NursingPlotFields[] - | NutritionPlotsFields[] - | PainDiagramsFields[] - | PressureSoreDiagramsFields[] - | PrimaryParametersPlotFields[] - | VentilatorPlotFields[]; + | typeof ABGPlotsFields + | typeof DialysisPlotsFields + | typeof NeurologicalTablesFields + | typeof NursingPlotFields + | typeof RoutineFields + | typeof NutritionPlotsFields + | typeof PainDiagramsFields + | typeof PressureSoreDiagramsFields + | typeof PrimaryParametersPlotFields + | typeof VentilatorPlotFields; } export interface DailyRoundsRes { @@ -472,6 +499,7 @@ export interface DailyRoundsRes { | DialysisPlotsRes | NeurologicalTablesRes | NursingPlotRes + | RoutineAnalysisRes | NutritionPlotsRes | PainDiagramsRes | PrimaryParametersPlotRes diff --git a/src/Components/Form/FormFields/SelectFormField.tsx b/src/Components/Form/FormFields/SelectFormField.tsx index 75c9c5741f9..9607e26bbdf 100644 --- a/src/Components/Form/FormFields/SelectFormField.tsx +++ b/src/Components/Form/FormFields/SelectFormField.tsx @@ -46,7 +46,7 @@ export const SelectFormField = (props: SelectFormFieldProps) => { type MultiSelectFormFieldProps = FormFieldBaseProps & { placeholder?: React.ReactNode; - options: T[]; + options: readonly T[]; optionLabel: OptionCallback; optionSelectedLabel?: OptionCallback; optionDescription?: OptionCallback; diff --git a/src/Components/Form/MultiSelectMenuV2.tsx b/src/Components/Form/MultiSelectMenuV2.tsx index d3a46cdb1f9..2cf47584dda 100644 --- a/src/Components/Form/MultiSelectMenuV2.tsx +++ b/src/Components/Form/MultiSelectMenuV2.tsx @@ -14,7 +14,7 @@ type OptionCallback = (option: T) => R; type Props = { id?: string; - options: T[]; + options: readonly T[]; value: V[] | undefined; placeholder?: ReactNode; optionLabel: OptionCallback; diff --git a/src/Components/LogUpdate/CriticalCarePreview.tsx b/src/Components/LogUpdate/CriticalCarePreview.tsx index 61f4f2a3567..838887decf9 100644 --- a/src/Components/LogUpdate/CriticalCarePreview.tsx +++ b/src/Components/LogUpdate/CriticalCarePreview.tsx @@ -4,7 +4,7 @@ import useQuery from "../../Utils/request/useQuery"; import ButtonV2 from "../Common/components/ButtonV2"; import Loading from "../Common/Loading"; import Card from "../../CAREUI/display/Card"; -import React from "react"; +import React, { useEffect } from "react"; import { ABGAnalysisFields } from "./Sections/ABGAnalysis"; import { classNames, @@ -16,6 +16,7 @@ import { VentilatorFields } from "./Sections/RespiratorySupport/Ventilator"; import PressureSore from "./Sections/PressureSore/PressureSore"; import { IOBalanceSections } from "./Sections/IOBalance"; import PainChart from "./components/PainChart"; +import { DailyRoundsModel } from "../Patient/models"; type Props = { facilityId: string; @@ -55,10 +56,15 @@ export default function CriticalCarePreview(props: Props) {
-

Consultation Updates

+

+ Consultation Updates +
+ {t(`ROUNDS_TYPE__${data.rounds_type}`)} +
+

- {/* */} +
+
+ + +
+ + + + +
+
+ + + +
+
+
-
- {(["left", "right"] as const).map((dir) => ( -
-
{dir} Pupil
- - {data[`${dir}_pupil_size`] === 0 && ( + {(data.left_pupil_light_reaction || + data.left_pupil_light_reaction_detail || + data.left_pupil_size || + data.left_pupil_size_detail || + data.right_pupil_light_reaction || + data.right_pupil_light_reaction_detail || + data.right_pupil_size || + data.right_pupil_size_detail) && ( +
+ {(["left", "right"] as const).map((dir) => ( +
+
{dir} Pupil
- )} - )} - /> - -
- ))} -
+ + +
+ ))} +
+ )}
-
+
{data.bp && (
Blood Pressure
@@ -270,21 +307,30 @@ export default function CriticalCarePreview(props: Props) { ]} /> -

Pain Scale

- + {!!data.pain_scale_enhanced?.length && ( + <> +

Pain Scale

+ + + )}
{!!IOBalanceSections.flatMap((s) => s.fields.flatMap((f) => data[f.key] ?? []), ).length && ( -
+
+ s.fields.map((f) => f.key), + ).some((field) => data[field]?.length)} + >
{IOBalanceSections.map(({ name, fields }) => (
@@ -351,7 +397,7 @@ export default function CriticalCarePreview(props: Props) {
  • ))} @@ -359,7 +405,10 @@ export default function CriticalCarePreview(props: Props) {
    )} -
    +
    { +type SectionContextType = { + hasValue: () => void; +}; + +const sectionContext = React.createContext(null); + +const Section = (props: { + title: string; + children: React.ReactNode; + subSection?: boolean; + show?: boolean; +}) => { + const parentContext = React.useContext(sectionContext); + const [hasValue, setHasValue] = React.useState(props.show ?? false); + + useEffect(() => { + if (parentContext && hasValue) { + parentContext.hasValue(); + } + }, [parentContext, hasValue]); + return ( -
    setHasValue(true), + }} > -

    {props.title}

    - {props.children} -
    +
    + {props.subSection ? ( +
    {props.title}
    + ) : ( +

    {props.title}

    + )} + {props.children} +
    + ); }; const Detail = (props: { label: React.ReactNode; - value?: string | number | boolean; + value?: string | number | boolean | null; suffix?: React.ReactNode; }) => { + const context = React.useContext(sectionContext); + let value = props.value; - value = value === "" ? undefined : value; + value = value === "" ? null : value; value = value === true ? "Yes" : value; value = value === false ? "No" : value; + React.useEffect(() => { + if (context && value != null) { + context.hasValue(); + } + }, [context, value]); + + if (value == null) { + // Skip showing detail if attribute not filled. + return null; + } + value = typeof value === "string" ? parseFloat(value) || value : value; value = typeof value === "number" ? properRoundOf(value) : value; @@ -516,6 +611,25 @@ const Detail = (props: { ); }; +const ChoiceDetail = (props: { + name: keyof DailyRoundsModel; + data: DailyRoundsModel; +}) => { + const { t } = useTranslation(); + const value = props.data[props.name]; + + if (value == null) { + return; + } + + return ( + + ); +}; + const RangeDetail = (props: { label: React.ReactNode; value?: number; diff --git a/src/Components/LogUpdate/Sections/NursingCare.tsx b/src/Components/LogUpdate/Sections/NursingCare.tsx index ffb4a445125..48037d6c842 100644 --- a/src/Components/LogUpdate/Sections/NursingCare.tsx +++ b/src/Components/LogUpdate/Sections/NursingCare.tsx @@ -1,9 +1,8 @@ import { useTranslation } from "react-i18next"; import { NURSING_CARE_PROCEDURES } from "../../../Common/constants"; -import { classNames } from "../../../Utils/utils"; -import CheckBoxFormField from "../../Form/FormFields/CheckBoxFormField"; -import TextAreaFormField from "../../Form/FormFields/TextAreaFormField"; import { LogUpdateSectionMeta, LogUpdateSectionProps } from "../utils"; +import { MultiSelectFormField } from "../../Form/FormFields/SelectFormField"; +import AutoExpandingTextInputFormField from "../../Form/FormFields/AutoExpandingTextInputFormField"; const NursingCare = ({ log, onChange }: LogUpdateSectionProps) => { const { t } = useTranslation(); @@ -11,59 +10,58 @@ const NursingCare = ({ log, onChange }: LogUpdateSectionProps) => { return (
    - {NURSING_CARE_PROCEDURES.map((procedure, i) => { - const obj = nursing.find((n) => n.procedure === procedure); - - return ( -
    -
    - { - if (e.value) { - onChange({ - nursing: [...nursing, { procedure, description: "" }], - }); - } else { - onChange({ - nursing: nursing.filter((n) => n.procedure !== procedure), - }); - } - }} - errorClassName="hidden" - /> -
    - {obj && ( -
    - - onChange({ - nursing: nursing.map((n) => - n.procedure === procedure - ? { ...n, description: val.value } - : n, - ), - }) - } - placeholder="Description" - errorClassName="hidden" - /> -
    - )} -
    - ); - })} + p.procedure)} + onChange={({ value }) => { + onChange({ + nursing: value.map((procedure) => ({ + procedure, + description: + nursing.find((p) => p.procedure === procedure)?.description ?? + "", + })), + }); + }} + options={NURSING_CARE_PROCEDURES} + optionLabel={(procedure) => t(`NURSING_CARE_PROCEDURE__${procedure}`)} + optionValue={(o) => o} + errorClassName="hidden" + /> + {!!nursing.length && ( + + + {nursing.map((obj) => ( + + + + + ))} + +
    + {t(`NURSING_CARE_PROCEDURE__${obj.procedure}`)} + + + onChange({ + nursing: nursing.map((n) => + n.procedure === obj.procedure + ? { ...n, description: val.value } + : n, + ), + }) + } + rows={1} + maxHeight={160} + placeholder={t("add_remarks")} + errorClassName="hidden" + /> +
    + )}
    ); }; diff --git a/src/Components/LogUpdate/Sections/Vitals.tsx b/src/Components/LogUpdate/Sections/Vitals.tsx index 8592f004370..0561debc88c 100644 --- a/src/Components/LogUpdate/Sections/Vitals.tsx +++ b/src/Components/LogUpdate/Sections/Vitals.tsx @@ -59,8 +59,8 @@ const Vitals = ({ log, onChange }: LogUpdateSectionProps) => { />
    onChange({ ventilator_spo2: c.value })} value={log.ventilator_spo2} min={0} @@ -70,7 +70,7 @@ const Vitals = ({ log, onChange }: LogUpdateSectionProps) => { valueDescriptions={rangeValueDescription({ low: 89 })} /> onChange({ temperature: c.value })} value={log.temperature} @@ -88,7 +88,7 @@ const Vitals = ({ log, onChange }: LogUpdateSectionProps) => { ]} /> onChange({ resp: c.value })} value={log.resp} @@ -111,7 +111,7 @@ const Vitals = ({ log, onChange }: LogUpdateSectionProps) => { />
    onChange({ pulse: c.value })} value={log.pulse} @@ -137,7 +137,7 @@ const Vitals = ({ log, onChange }: LogUpdateSectionProps) => { ]} /> t(`HEARTBEAT_RHYTHM__${c}`)} @@ -146,7 +146,7 @@ const Vitals = ({ log, onChange }: LogUpdateSectionProps) => { onChange={(c) => onChange({ rhythm: c.value ?? undefined })} /> onChange({ rhythm_detail: c.value })} diff --git a/src/Components/LogUpdate/Sections/index.tsx b/src/Components/LogUpdate/Sections/index.tsx index bccc0578095..97328f74c92 100644 --- a/src/Components/LogUpdate/Sections/index.tsx +++ b/src/Components/LogUpdate/Sections/index.tsx @@ -44,6 +44,7 @@ export const RoundTypeSections = { "NursingCare", ], DOCTORS_LOG: ["NeurologicalMonitoring", "RespiratorySupport"], + COMMUNITY_NURSES_LOG: [], } as const satisfies Record< (typeof DailyRoundTypes)[number], (keyof typeof LogUpdateSections)[] diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index 7335e5c6983..f949bd9e404 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -3,11 +3,19 @@ import { navigate } from "raviger"; import dayjs from "dayjs"; import { lazy, useCallback, useEffect, useState } from "react"; import { + APPETITE_CHOICES, + BLADDER_DRAINAGE_CHOICES, + BLADDER_ISSUE_CHOICES, + BOWEL_ISSUE_CHOICES, CONSCIOUSNESS_LEVEL, + NUTRITION_ROUTE_CHOICES, + ORAL_ISSUE_CHOICES, PATIENT_CATEGORIES, REVIEW_AT_CHOICES, RHYTHM_CHOICES, + SLEEP_CHOICES, TELEMEDICINE_ACTIONS, + URINATION_FREQUENCY_CHOICES, } from "../../Common/constants"; import useAppHistory from "../../Common/hooks/useAppHistory"; import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave"; @@ -30,7 +38,7 @@ import request from "../../Utils/request/request"; import routes from "../../Redux/api"; import { Scribe } from "../Scribe/Scribe"; import { SCRIBE_FORMS } from "../Scribe/formDetails"; -import { DailyRoundsModel } from "./models"; +import { DailyRoundsModel, DailyRoundTypes } from "./models"; import InvestigationBuilder from "../Common/prescription-builder/InvestigationBuilder"; import { FieldErrorText } from "../Form/FormFields/FormField"; import { error } from "@pnotify/core"; @@ -48,6 +56,7 @@ import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import SymptomsApi from "../Symptoms/api"; import { scrollTo } from "../../Utils/utils"; import { ICD11DiagnosisModel } from "../Facility/models"; +import NursingCare from "../LogUpdate/Sections/NursingCare"; const Loading = lazy(() => import("../Common/Loading")); @@ -281,6 +290,16 @@ export const DailyRounds = (props: any) => { } return; } + + case "oral_issue": { + if (state.form.nutrition_route !== "ORAL" && state.form[field]) { + errors[field] = t("oral_issue_for_non_oral_nutrition_route_error"); + invalidForm = true; + break; + } + return; + } + default: return; } @@ -329,7 +348,7 @@ export const DailyRounds = (props: any) => { review_interval: Number(prevReviewInterval), }; - if (!["VENTILATOR"].includes(state.form.rounds_type)) { + if (state.form.rounds_type !== "VENTILATOR") { data = { ...data, bp: state.form.bp ?? {}, @@ -340,6 +359,17 @@ export const DailyRounds = (props: any) => { rhythm_detail: state.form.rhythm_detail, ventilator_spo2: state.form.ventilator_spo2 ?? null, consciousness_level: state.form.consciousness_level || undefined, + bowel_issue: state.form.bowel_issue ?? undefined, + bladder_drainage: state.form.bladder_drainage ?? undefined, + bladder_issue: state.form.bladder_issue ?? undefined, + is_experiencing_dysuria: state.form.is_experiencing_dysuria, + urination_frequency: state.form.urination_frequency ?? undefined, + sleep: state.form.sleep ?? undefined, + nutrition_route: state.form.nutrition_route ?? undefined, + oral_issue: state.form.oral_issue ?? undefined, + appetite: state.form.appetite ?? undefined, + blood_sugar_level: state.form.blood_sugar_level, + nursing: state.form.nursing, }; } @@ -354,9 +384,15 @@ export const DailyRounds = (props: any) => { if (obj) { dispatch({ type: "set_form", form: initForm }); Notification.Success({ - msg: `${t(obj.rounds_type as string)} log updated successfully`, + msg: t("LOG_UPDATE_UPDATED_NOTIFICATION", { + roundType: t(`ROUNDS_TYPE__${state.form.rounds_type}`), + }), }); - if (["NORMAL", "TELEMEDICINE"].includes(state.form.rounds_type)) { + if ( + ["NORMAL", "TELEMEDICINE", "COMMUNITY_NURSES_LOG"].includes( + state.form.rounds_type, + ) + ) { navigate( `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}`, ); @@ -375,10 +411,16 @@ export const DailyRounds = (props: any) => { if (obj) { dispatch({ type: "set_form", form: initForm }); Notification.Success({ - msg: `${t(state.form.rounds_type)} log created successfully`, + msg: t("LOG_UPDATE_CREATED_NOTIFICATION", { + roundType: t(`ROUNDS_TYPE__${state.form.rounds_type}`), + }), }); - if (["NORMAL", "TELEMEDICINE"].includes(state.form.rounds_type)) { + if ( + ["NORMAL", "TELEMEDICINE", "COMMUNITY_NURSES_LOG"].includes( + state.form.rounds_type, + ) + ) { navigate( `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}`, ); @@ -408,6 +450,7 @@ export const DailyRounds = (props: any) => { const field = (name: string) => { return { id: name, + label: t(`LOG_UPDATE_FIELD_LABEL__${name}`), name, value: state.form[name], error: state.errors[name], @@ -415,6 +458,18 @@ export const DailyRounds = (props: any) => { }; }; + const selectField = ( + name: keyof DailyRoundsModel, + options: readonly T[], + ) => { + return { + ...field(name), + options, + optionLabel: (option: T) => t(`${name.toUpperCase()}__${option}`), + optionValue: (option: T) => option, + }; + }; + const getExpectedReviewTime = () => { const nextReviewTime = Number( state.form.review_interval || prevReviewInterval, @@ -428,30 +483,30 @@ export const DailyRounds = (props: any) => { return ; } - const roundTypes: { id: string; text: string }[] = []; + const roundTypes: (typeof DailyRoundTypes)[number][] = []; if ( ["Doctor", "Staff", "DistrictAdmin", "StateAdmin"].includes( authUser.user_type, ) ) { - roundTypes.push({ id: "DOCTORS_LOG", text: t("DOCTORS_LOG") }); + roundTypes.push("DOCTORS_LOG"); } - - roundTypes.push( - { id: "NORMAL", text: t("NORMAL") }, - { id: "VENTILATOR", text: t("VENTILATOR") }, - ); - + roundTypes.push("NORMAL", "COMMUNITY_NURSES_LOG", "VENTILATOR"); if (consultationSuggestion === "DC") { - roundTypes.push({ id: "TELEMEDICINE", text: t("TELEMEDICINE") }); + roundTypes.push("TELEMEDICINE"); } + const submitButtonDisabled = (() => { if (buttonText !== "Save") { return false; } - if (["VENTILATOR", "DOCTORS_LOG"].includes(state.form.rounds_type)) { + if ( + ["VENTILATOR", "DOCTORS_LOG", "COMMUNITY_NURSES_LOG"].includes( + state.form.rounds_type, + ) + ) { return false; } @@ -539,7 +594,7 @@ export const DailyRounds = (props: any) => { "additional_symptoms", ].includes(f), ) && - roundTypes.some((t) => t.id === "DOCTORS_LOG") + roundTypes.some((t) => t === "DOCTORS_LOG") ) { rounds_type = "DOCTORS_LOG"; } @@ -577,20 +632,15 @@ export const DailyRounds = (props: any) => {
    option.text} - optionValue={(option) => option.id} />
    @@ -610,63 +660,67 @@ export const DailyRounds = (props: any) => { />
    - - + + - {state.form.rounds_type !== "DOCTORS_LOG" && ( + {state.form.rounds_type === "COMMUNITY_NURSES_LOG" && ( <> +
    +

    {t("routine")}

    + option.desc} - optionValue={(option) => option.text} - value={prevAction} - onChange={(event) => { - handleFormFieldChange(event); - setPreviousAction(event.value); - }} - /> - - option.text} - optionValue={(option) => option.id} - value={prevReviewInterval} - onChange={(event) => { - handleFormFieldChange(event); - setPreviousReviewInterval(Number(event.value)); - }} + {...selectField("bowel_issue", BOWEL_ISSUE_CHOICES)} /> +
    +
    {t("bladder")}
    + + + t(c ? "yes" : "no")} + /> + +
    +
    +
    {t("nutrition")}
    + + + +
    )} - {["NORMAL", "TELEMEDICINE", "DOCTORS_LOG"].includes( - state.form.rounds_type, - ) && ( + {[ + "NORMAL", + "TELEMEDICINE", + "DOCTORS_LOG", + "COMMUNITY_NURSES_LOG", + ].includes(state.form.rounds_type) && ( <> -

    Vitals

    +
    +

    {t("vitals")}

    - + { }, ]} /> + + )} - + + + )} + + {["NORMAL", "TELEMEDICINE", "DOCTORS_LOG"].includes( + state.form.rounds_type, + ) && ( + <> + { { option.desc} optionValue={(option) => option.id} @@ -759,13 +842,14 @@ export const DailyRounds = (props: any) => { a.value), + )} options={CONSCIOUSNESS_LEVEL.map((level) => ({ label: t(`CONSCIOUSNESS_LEVEL__${level.value}`), value: level.value, @@ -778,6 +862,45 @@ export const DailyRounds = (props: any) => { )} + {state.form.rounds_type === "COMMUNITY_NURSES_LOG" && ( +
    +
    +
    +

    + {t("prescription_medications")} +

    + + setShowDiscontinuedPrescriptions(value) + } + errorClassName="hidden" + /> +
    + +
    + )} + + {state.form.rounds_type === "COMMUNITY_NURSES_LOG" && ( +
    +
    +
    +

    {t("nursing_care")}

    +
    + + handleFormFieldChange({ name: "nursing", value: log.nursing }) + } + /> +
    + )} + {state.form.rounds_type === "DOCTORS_LOG" && ( <>
    @@ -860,6 +983,36 @@ export const DailyRounds = (props: any) => {
    )} + + {state.form.rounds_type !== "DOCTORS_LOG" && ( + <> +
    + option.desc} + optionValue={(option) => option.text} + value={prevAction} + onChange={(event) => { + handleFormFieldChange(event); + setPreviousAction(event.value); + }} + /> + + option.text} + optionValue={(option) => option.id} + value={prevReviewInterval} + onChange={(event) => { + handleFormFieldChange(event); + setPreviousReviewInterval(Number(event.value)); + }} + /> + + )}
    diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index 343c80a60bf..8002a86442f 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -1,18 +1,26 @@ import { ConsultationModel, PatientCategory } from "../Facility/models"; import { PerformedByModel } from "../HCX/misc"; import { + APPETITE_CHOICES, + BLADDER_DRAINAGE_CHOICES, + BLADDER_ISSUE_CHOICES, + BOWEL_ISSUE_CHOICES, CONSCIOUSNESS_LEVEL, HEARTBEAT_RHYTHM_CHOICES, HumanBodyRegion, INSULIN_INTAKE_FREQUENCY_OPTIONS, LIMB_RESPONSE_OPTIONS, NURSING_CARE_PROCEDURES, + NUTRITION_ROUTE_CHOICES, OCCUPATION_TYPES, + ORAL_ISSUE_CHOICES, OXYGEN_MODALITY_OPTIONS, PressureSoreExudateAmountOptions, PressureSoreTissueTypeOptions, RATION_CARD_CATEGORY, RESPIRATORY_SUPPORT, + SLEEP_CHOICES, + URINATION_FREQUENCY_CHOICES, VENTILATOR_MODE_OPTIONS, } from "../../Common/constants"; @@ -257,6 +265,7 @@ export interface SampleListModel { export const DailyRoundTypes = [ "NORMAL", + "COMMUNITY_NURSES_LOG", "DOCTORS_LOG", "VENTILATOR", "AUTOMATED", @@ -355,6 +364,8 @@ export interface DailyRoundsModel { infusions?: NameQuantity[]; iv_fluids?: NameQuantity[]; output?: NameQuantity[]; + total_intake_calculated?: number; + total_output_calculated?: number; ventilator_spo2?: number; ventilator_interface?: (typeof RESPIRATORY_SUPPORT)[number]["value"]; ventilator_oxygen_modality?: (typeof OXYGEN_MODALITY_OPTIONS)[number]["value"]; @@ -370,6 +381,15 @@ export interface DailyRoundsModel { ventilator_tidal_volume?: number; pressure_sore?: IPressureSore[]; + bowel_issue?: (typeof BOWEL_ISSUE_CHOICES)[number]; + bladder_drainage?: (typeof BLADDER_DRAINAGE_CHOICES)[number]; + bladder_issue?: (typeof BLADDER_ISSUE_CHOICES)[number]; + is_experiencing_dysuria?: boolean; + urination_frequency?: (typeof URINATION_FREQUENCY_CHOICES)[number]; + sleep?: (typeof SLEEP_CHOICES)[number]; + nutrition_route?: (typeof NUTRITION_ROUTE_CHOICES)[number]; + oral_issue?: (typeof ORAL_ISSUE_CHOICES)[number]; + appetite?: (typeof APPETITE_CHOICES)[number]; } export interface FacilityNameModel { diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index 432d5979bed..b0316e4d98c 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -166,6 +166,8 @@ "not_specified": "Not Specified", "all_changes_have_been_saved": "All changes have been saved", "no_data_found": "No data found", + "other_details": "Other details", + "no_remarks": "No remarks", "edit": "Edit", "clear_selection": "Clear selection", "select_date": "Select date", @@ -203,6 +205,6 @@ "deleted_successfully": "{{name}} deleted successfully", "delete_item": "Delete {{name}}", "unsupported_browser": "Unsupported Browser", - "unsupported_browser_description": "Your browser ({{name}} version {{version}}) is not supported. Please update your browser to the latest version or switch to a supported browser for the best experience." - + "unsupported_browser_description": "Your browser ({{name}} version {{version}}) is not supported. Please update your browser to the latest version or switch to a supported browser for the best experience.", + "add_remarks": "Add remarks" } \ No newline at end of file diff --git a/src/Locale/en/Consultation.json b/src/Locale/en/Consultation.json index 3d101bcb2bd..d12a6cb16ac 100644 --- a/src/Locale/en/Consultation.json +++ b/src/Locale/en/Consultation.json @@ -1,4 +1,19 @@ { + "CONSULTATION_TAB__UPDATES": "Overview", + "CONSULTATION_TAB__FEED": "Feed", + "CONSULTATION_TAB__SUMMARY": "Vitals", + "CONSULTATION_TAB__ABG": "ABG", + "CONSULTATION_TAB__MEDICINES": "Medicines", + "CONSULTATION_TAB__FILES": "Files", + "CONSULTATION_TAB__INVESTIGATIONS": "Investigations", + "CONSULTATION_TAB__NEUROLOGICAL_MONITORING": "Neuro", + "CONSULTATION_TAB__VENTILATOR": "Ventilation", + "CONSULTATION_TAB__NUTRITION": "Nutrition", + "CONSULTATION_TAB__PRESSURE_SORE": "Pressure Sore", + "CONSULTATION_TAB__NURSING": "Nursing", + "CONSULTATION_TAB__DIALYSIS": "Dialysis", + "CONSULTATION_TAB__ABDM": "ABDM Records", + "nursing_information": "Nursing Information", "no_consultation_updates": "No consultation updates", "consultation_updates": "Consultation updates", "update_log": "Update Log", diff --git a/src/Locale/en/LogUpdate.json b/src/Locale/en/LogUpdate.json index 080e2fc979a..f057c8c4235 100644 --- a/src/Locale/en/LogUpdate.json +++ b/src/Locale/en/LogUpdate.json @@ -1,4 +1,36 @@ { + "LOG_UPDATE_CREATED_NOTIFICATION": "{{ roundType }} created successfully", + "LOG_UPDATE_UPDATED_NOTIFICATION": "{{ roundType }} updated successfully", + "LOG_UPDATE_FIELD_LABEL__rounds_type": "Rounds Type", + "LOG_UPDATE_FIELD_LABEL__patient_category": "Category", + "LOG_UPDATE_FIELD_LABEL__consciousness_level": "Level of Consciousness", + "LOG_UPDATE_FIELD_LABEL__sleep": "Sleep", + "LOG_UPDATE_FIELD_LABEL__bowel_issue": "Bowel", + "LOG_UPDATE_FIELD_LABEL__bladder_drainage": "Drainage", + "LOG_UPDATE_FIELD_LABEL__bladder_issue": "Issues", + "LOG_UPDATE_FIELD_LABEL__is_experiencing_dysuria": "Experiences Dysuria?", + "LOG_UPDATE_FIELD_LABEL__urination_frequency": "Frequency of Urination", + "LOG_UPDATE_FIELD_LABEL__nutrition_route": "Nutrition Route", + "LOG_UPDATE_FIELD_LABEL__oral_issue": "Oral issues", + "LOG_UPDATE_FIELD_LABEL__appetite": "Appetite", + "LOG_UPDATE_FIELD_LABEL__physical_examination_info": "Physical Examination Info", + "LOG_UPDATE_FIELD_LABEL__bp": "Blood Pressure", + "LOG_UPDATE_FIELD_LABEL__blood_sugar_level": "Blood Sugar Level", + "LOG_UPDATE_FIELD_LABEL__action": "Action", + "LOG_UPDATE_FIELD_LABEL__review_interval": "Review after", + "LOG_UPDATE_FIELD_LABEL__rhythm": "Heartbeat Rhythm", + "LOG_UPDATE_FIELD_LABEL__rhythm_detail": "Rhythm Description", + "LOG_UPDATE_FIELD_LABEL__ventilator_spo2": "SpO₂", + "LOG_UPDATE_FIELD_LABEL__resp": "Respiratory Rate", + "LOG_UPDATE_FIELD_LABEL__temperature": "Temperature", + "LOG_UPDATE_FIELD_LABEL__other_details": "Other details", + "LOG_UPDATE_FIELD_LABEL__pulse": "Pulse", + "ROUNDS_TYPE__NORMAL": "Brief Update", + "ROUNDS_TYPE__COMMUNITY_NURSES_LOG": "Community Nurse's Log", + "ROUNDS_TYPE__VENTILATOR": "Detailed Update", + "ROUNDS_TYPE__DOCTORS_LOG": "Progress Note", + "ROUNDS_TYPE__AUTOMATED": "Virtual Nursing Assistant", + "ROUNDS_TYPE__TELEMEDICINE": "Tele-medicine Log", "RESPIRATORY_SUPPORT_SHORT__UNKNOWN": "None", "RESPIRATORY_SUPPORT_SHORT__OXYGEN_SUPPORT": "O2 Support", "RESPIRATORY_SUPPORT_SHORT__NON_INVASIVE": "NIV", @@ -20,11 +52,46 @@ "CONSCIOUSNESS_LEVEL__ALERT": "Alert", "CONSCIOUSNESS_LEVEL__AGITATED_OR_CONFUSED": "Agitated or Confused", "CONSCIOUSNESS_LEVEL__ONSET_OF_AGITATION_AND_CONFUSION": "Onset of Agitation and Confusion", + "BOWEL_ISSUE__NO_DIFFICULTY": "No difficulty", + "BOWEL_ISSUE__CONSTIPATION": "Constipation", + "BOWEL_ISSUE__DIARRHOEA": "Diarrhoea", + "BLADDER_DRAINAGE__NORMAL": "Normal", + "BLADDER_DRAINAGE__CONDOM_CATHETER": "Condom Catheter", + "BLADDER_DRAINAGE__DIAPER": "Diaper", + "BLADDER_DRAINAGE__INTERMITTENT_CATHETER": "Intermittent Catheter", + "BLADDER_DRAINAGE__CONTINUOUS_INDWELLING_CATHETER": "Continuous Indwelling Catheter", + "BLADDER_DRAINAGE__CONTINUOUS_SUPRAPUBIC_CATHETER": "Continuous Suprapubic Catheter", + "BLADDER_DRAINAGE__UROSTOMY": "Urostomy", + "BLADDER_ISSUE__NO_ISSUES": "No issues", + "BLADDER_ISSUE__INCONTINENCE": "Incontinence", + "BLADDER_ISSUE__RETENTION": "Retention", + "BLADDER_ISSUE__HESITANCY": "Hesitancy", + "URINATION_FREQUENCY__NORMAL": "Normal", + "URINATION_FREQUENCY__DECREASED": "Decreased", + "URINATION_FREQUENCY__INCREASED": "Increased", + "SLEEP__EXCESSIVE": "Excessive", + "SLEEP__SATISFACTORY": "Satisfactory", + "SLEEP__UNSATISFACTORY": "Unsatisfactory", + "SLEEP__NO_SLEEP": "No sleep", + "NUTRITION_ROUTE__ORAL": "Oral", + "NUTRITION_ROUTE__RYLES_TUBE": "Ryle's Tube", + "NUTRITION_ROUTE__GASTROSTOMY_OR_JEJUNOSTOMY": "Gastrostomy / Jejunostomy", + "NUTRITION_ROUTE__PEG": "PEG", + "NUTRITION_ROUTE__PARENTERAL_TUBING_FLUID": "Parenteral Tubing (Fluid)", + "NUTRITION_ROUTE__PARENTERAL_TUBING_TPN": "Parenteral Tubing (TPN)", + "ORAL_ISSUE__NO_ISSUE": "No issues", + "ORAL_ISSUE__DYSPHAGIA": "Dysphagia", + "ORAL_ISSUE__ODYNOPHAGIA": "Odynophagia", + "APPETITE__INCREASED": "Increased", + "APPETITE__SATISFACTORY": "Satisfactory", + "APPETITE__REDUCED": "Reduced", + "APPETITE__NO_TASTE_FOR_FOOD": "No taste for food", + "APPETITE__CANNOT_BE_ASSESSED": "Cannot be assessed", "PUPIL_REACTION__UNKNOWN": "Unknown", "PUPIL_REACTION__BRISK": "Brisk", "PUPIL_REACTION__SLUGGISH": "Sluggish", "PUPIL_REACTION__FIXED": "Fixed", - "PUPIL_REACTION__CANNOT_BE_ASSESSED": "Cannot Be Assessed", + "PUPIL_REACTION__CANNOT_BE_ASSESSED": "Cannot be assessed", "LIMB_RESPONSE__UNKNOWN": "Unknown", "LIMB_RESPONSE__STRONG": "Strong", "LIMB_RESPONSE__MODERATE": "Moderate", @@ -56,18 +123,19 @@ "HEARTBEAT_RHYTHM__REGULAR": "Regular", "HEARTBEAT_RHYTHM__IRREGULAR": "Irregular", "HEARTBEAT_RHYTHM__UNKNOWN": "Unknown", - "heartbeat_rhythm": "Heartbeat Rhythm", - "heartbeat_description": "Heartbeat Description", "blood_pressure": "Blood Pressure", "map_acronym": "M.A.P.", "systolic": "Systolic", "diastolic": "Diastolic", - "temperature": "Temperature", - "resipiratory_rate": "Respiratory Rate", "pain": "Pain", "pain_chart_description": "Mark region and intensity of pain", - "pulse": "Pulse", "bradycardia": "Bradycardia", "tachycardia": "Tachycardia", - "spo2": "SpO₂" + "procedures_select_placeholder": "Select procedures to add details", + "oral_issue_for_non_oral_nutrition_route_error": "Can be specified only if nutrition route is set to Oral", + "routine": "Routine", + "bladder": "Bladder", + "nutrition": "Nutrition", + "vitals": "Vitals", + "nursing_care": "Nursing Care" } \ No newline at end of file diff --git a/src/Locale/hi/LogUpdate.json b/src/Locale/hi/LogUpdate.json index 3026eccce57..176886c855b 100644 --- a/src/Locale/hi/LogUpdate.json +++ b/src/Locale/hi/LogUpdate.json @@ -56,18 +56,12 @@ "HEARTBEAT_RHYTHM__REGULAR": "नियमित", "HEARTBEAT_RHYTHM__IRREGULAR": "अनियमित", "HEARTBEAT_RHYTHM__UNKNOWN": "अज्ञात", - "heartbeat_rhythm": "दिल की धड़कन की लय", - "heartbeat_description": "दिल की धड़कन का विवरण", "blood_pressure": "रक्तचाप", "map_acronym": "मानचित्र", "systolic": "सिस्टोलिक", "diastolic": "डायस्टोलिक", - "temperature": "तापमान", - "resipiratory_rate": "श्वसन दर", "pain": "दर्द", "pain_chart_description": "दर्द का क्षेत्र और तीव्रता चिह्नित करें", - "pulse": "नाड़ी", "bradycardia": "मंदनाड़ी", - "tachycardia": "tachycardia", - "spo2": "SpO₂" + "tachycardia": "tachycardia" } \ No newline at end of file diff --git a/src/Locale/kn/LogUpdate.json b/src/Locale/kn/LogUpdate.json index e1d8104ad95..25e4ee4623e 100644 --- a/src/Locale/kn/LogUpdate.json +++ b/src/Locale/kn/LogUpdate.json @@ -56,18 +56,12 @@ "HEARTBEAT_RHYTHM__REGULAR": "ನಿಯಮಿತ", "HEARTBEAT_RHYTHM__IRREGULAR": "ಅನಿಯಮಿತ", "HEARTBEAT_RHYTHM__UNKNOWN": "ಅಜ್ಞಾತ", - "heartbeat_rhythm": "ಹೃದಯ ಬಡಿತದ ಲಯ", - "heartbeat_description": "ಹೃದಯ ಬಡಿತದ ವಿವರಣೆ", "blood_pressure": "ರಕ್ತದೊತ್ತಡ", "map_acronym": "ನಕ್ಷೆ", "systolic": "ಸಿಸ್ಟೊಲಿಕ್", "diastolic": "ಡಯಾಸ್ಟೊಲಿಕ್", - "temperature": "ತಾಪಮಾನ", - "resipiratory_rate": "ಉಸಿರಾಟದ ದರ", "pain": "ನೋವು", "pain_chart_description": "ನೋವಿನ ಪ್ರದೇಶ ಮತ್ತು ತೀವ್ರತೆಯನ್ನು ಗುರುತಿಸಿ", - "pulse": "ನಾಡಿ", "bradycardia": "ಬ್ರಾಡಿಕಾರ್ಡಿಯಾ", - "tachycardia": "ಟಾಕಿಕಾರ್ಡಿಯಾ", - "spo2": "SpO₂" + "tachycardia": "ಟಾಕಿಕಾರ್ಡಿಯಾ" } \ No newline at end of file diff --git a/src/Locale/ml/LogUpdate.json b/src/Locale/ml/LogUpdate.json index 1a3304b88ca..d2503cd8d34 100644 --- a/src/Locale/ml/LogUpdate.json +++ b/src/Locale/ml/LogUpdate.json @@ -56,18 +56,12 @@ "HEARTBEAT_RHYTHM__REGULAR": "പതിവ്", "HEARTBEAT_RHYTHM__IRREGULAR": "ക്രമരഹിതം", "HEARTBEAT_RHYTHM__UNKNOWN": "അജ്ഞാതം", - "heartbeat_rhythm": "ഹൃദയമിടിപ്പ് താളം", - "heartbeat_description": "ഹൃദയമിടിപ്പ് വിവരണം", "blood_pressure": "രക്തസമ്മർദ്ദം", "map_acronym": "മാപ്പ്", "systolic": "സിസ്റ്റോളിക്", "diastolic": "ഡയസ്റ്റോളിക്", - "temperature": "താപനില", - "resipiratory_rate": "ശ്വസന നിരക്ക്", "pain": "വേദന", "pain_chart_description": "വേദനയുടെ പ്രദേശവും തീവ്രതയും അടയാളപ്പെടുത്തുക", - "pulse": "പൾസ്", "bradycardia": "ബ്രാഡികാർഡിയ", - "tachycardia": "ടാക്കിക്കാർഡിയ", - "spo2": "SpO₂" + "tachycardia": "ടാക്കിക്കാർഡിയ" } \ No newline at end of file diff --git a/src/Locale/ta/LogUpdate.json b/src/Locale/ta/LogUpdate.json index fa5308cde39..61a52f69d48 100644 --- a/src/Locale/ta/LogUpdate.json +++ b/src/Locale/ta/LogUpdate.json @@ -56,18 +56,12 @@ "HEARTBEAT_RHYTHM__REGULAR": "வழக்கமான", "HEARTBEAT_RHYTHM__IRREGULAR": "ஒழுங்கற்ற", "HEARTBEAT_RHYTHM__UNKNOWN": "தெரியவில்லை", - "heartbeat_rhythm": "இதயத் துடிப்பு தாளம்", - "heartbeat_description": "இதய துடிப்பு விளக்கம்", "blood_pressure": "இரத்த அழுத்தம்", "map_acronym": "வரைபடம்", "systolic": "சிஸ்டாலிக்", "diastolic": "டயஸ்டாலிக்", - "temperature": "வெப்பநிலை", - "resipiratory_rate": "சுவாச விகிதம்", "pain": "வலி", "pain_chart_description": "வலியின் பகுதி மற்றும் தீவிரத்தை குறிக்கவும்", - "pulse": "துடிப்பு", "bradycardia": "பிராடி கார்டியா", - "tachycardia": "டாக்ரிக்கார்டியா", - "spo2": "SpO₂" + "tachycardia": "டாக்ரிக்கார்டியா" } \ No newline at end of file From 383dfbc899220a22e30e7530c40ce010b31314e4 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 18 Sep 2024 18:01:40 +0530 Subject: [PATCH 6/6] Revamp media player (#8437) --- package-lock.json | 52 +- package.json | 1 - src/Common/hooks/useHLSPlayer.ts | 17 - src/Common/hooks/useMSEplayer.ts | 236 ------ .../Assets/AssetType/ONVIFCamera.tsx | 12 +- .../Assets/configure/CameraConfigure.tsx | 4 +- src/Components/CameraFeed/CameraFeed.tsx | 176 ++--- .../CameraFeedOld.tsx} | 210 +++--- src/Components/CameraFeed/FeedAlert.tsx | 6 +- .../CameraFeed/FeedNetworkSignal.tsx | 6 +- src/Components/CameraFeed/routes.ts | 10 +- .../CameraFeed}/useFeedPTZ.ts | 41 +- src/Components/CameraFeed/useOperateCamera.ts | 5 + src/Components/CameraFeed/usePlayer.tsx | 58 -- src/Components/CameraFeed/utils.ts | 7 +- src/Components/CameraFeed/videoPlayer.tsx | 198 +++++ .../Facility/Consultations/Feed.tsx | 704 ------------------ .../Facility/Consultations/FeedButton.tsx | 63 -- 18 files changed, 459 insertions(+), 1347 deletions(-) delete mode 100644 src/Common/hooks/useHLSPlayer.ts delete mode 100644 src/Common/hooks/useMSEplayer.ts rename src/Components/{Facility/Consultations/LiveFeed.tsx => CameraFeed/CameraFeedOld.tsx} (83%) rename src/{Common/hooks => Components/CameraFeed}/useFeedPTZ.ts (89%) delete mode 100644 src/Components/CameraFeed/usePlayer.tsx create mode 100644 src/Components/CameraFeed/videoPlayer.tsx delete mode 100644 src/Components/Facility/Consultations/Feed.tsx delete mode 100644 src/Components/Facility/Consultations/FeedButton.tsx diff --git a/package-lock.json b/package-lock.json index 14fd0262607..553224ffc27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -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", diff --git a/package.json b/package.json index 8dcd4e13954..cfc0b3389a2 100644 --- a/package.json +++ b/package.json @@ -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/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/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; @@ -364,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 && ( @@ -670,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 ( + <> +