diff --git a/cypress/e2e/patient_spec/patient_consultation.cy.ts b/cypress/e2e/patient_spec/patient_consultation.cy.ts index d5732a6619d..eef98a5e3ce 100644 --- a/cypress/e2e/patient_spec/patient_consultation.cy.ts +++ b/cypress/e2e/patient_spec/patient_consultation.cy.ts @@ -59,7 +59,7 @@ describe("Patient Consultation in multiple combination", () => { patientConsultationPage.selectConsultationStatus( "Outpatient/Emergency Room", ); - cy.searchAndSelectOption("#symptoms", "ASYMPTOMATIC"); + cy.get("#is_asymptomatic").click(); patientConsultationPage.typePatientIllnessHistory(patientIllnessHistory); patientConsultationPage.typePatientExaminationHistory( patientExaminationHistory, @@ -175,7 +175,7 @@ describe("Patient Consultation in multiple combination", () => { "Outpatient/Emergency Room", ); // Asymptomatic - cy.searchAndSelectOption("#symptoms", "ASYMPTOMATIC"); + cy.get("#is_asymptomatic").click(); // CRITICAL category patientConsultationPage.selectPatientCategory("Critical"); patientConsultationPage.selectPatientSuggestion("Declare Death"); @@ -234,7 +234,7 @@ describe("Patient Consultation in multiple combination", () => { ); patientConsultationPage.selectPatientWard("Dummy Location 1"); // Asymptomatic - cy.searchAndSelectOption("#symptoms", "ASYMPTOMATIC"); + cy.get("#is_asymptomatic").click(); // Abnormal category patientConsultationPage.selectPatientCategory("Moderate"); patientConsultationPage.selectPatientSuggestion("Domiciliary Care"); @@ -293,18 +293,14 @@ describe("Patient Consultation in multiple combination", () => { ); // verify the free text in referring facility name patientConsultationPage.typeReferringFacility("Life Care Hospital"); - // Vomiting and Nausea symptoms + patientConsultationPage.selectSymptomsDate("01012024"); patientConsultationPage.typeAndMultiSelectSymptoms("s", [ - "SPUTUM", - "SORE THROAT", + "Sore throat", + "Sputum", ]); + patientConsultationPage.clickAddSymptom(); // Stable category patientConsultationPage.selectPatientCategory("Mild"); - // Date of symptoms - patientConsultationPage.selectSymptomsDate( - "#symptoms_onset_date", - "01012024", - ); // OP Consultation patientConsultationPage.selectPatientSuggestion("OP Consultation"); // one ICD-11 and no principal @@ -341,18 +337,16 @@ describe("Patient Consultation in multiple combination", () => { patientConsultationPage.selectConsultationStatus( "Outpatient/Emergency Room", ); - // Select the Symptoms - Sore throat and fever symptoms + // Select the Symptoms - Breathlessness and Bleeding symptoms + patientConsultationPage.selectSymptomsDate("01012024"); patientConsultationPage.typeAndMultiSelectSymptoms("b", [ - "BREATHLESSNESS", - "BLEEDING", + "Breathlessness", + "Bleeding", ]); + patientConsultationPage.clickAddSymptom(); // Comfort Care category patientConsultationPage.selectPatientCategory("Comfort Care"); // Date of symptoms - patientConsultationPage.selectSymptomsDate( - "#symptoms_onset_date", - "01012024", - ); // Decision after consultation - Referred to Facility patientConsultationPage.selectPatientSuggestion( "Refer to another Hospital", diff --git a/cypress/e2e/patient_spec/patient_logupdate.cy.ts b/cypress/e2e/patient_spec/patient_logupdate.cy.ts index a55b86e464b..562e430a9ad 100644 --- a/cypress/e2e/patient_spec/patient_logupdate.cy.ts +++ b/cypress/e2e/patient_spec/patient_logupdate.cy.ts @@ -11,7 +11,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { const patientLogupdate = new PatientLogupdate(); const domicilaryPatient = "Dummy Patient 11"; const patientCategory = "Moderate"; - const additionalSymptoms = "ASYMPTOMATIC"; + const additionalSymptoms = "Fever"; const physicalExamination = "physical examination details"; const otherExamination = "Other"; const patientSystolic = "119"; @@ -59,9 +59,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeRhythm(patientRhythm); cy.get("#consciousness_level-2").click(); cy.submitButton("Save"); - cy.verifyNotification( - "Telemedicine Log Updates details created successfully", - ); + cy.verifyNotification("Tele-medicine log update created successfully"); }); it("Create a new log normal update for a domicilary care patient and edit it", () => { @@ -86,7 +84,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeRhythm(patientRhythm); cy.get("#consciousness_level-2").click(); cy.submitButton("Save"); - cy.verifyNotification("Normal Log Updates details created successfully"); + cy.verifyNotification("Normal log update created successfully"); cy.closeNotification(); // edit the card and verify the data. cy.contains("Daily Rounds").click(); @@ -109,7 +107,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.clickClearButtonInElement("#diastolic"); patientLogupdate.typeDiastolic(patientModifiedDiastolic); cy.submitButton("Continue"); - cy.verifyNotification("Normal Log Updates details updated successfully"); + cy.verifyNotification("Normal log update details updated successfully"); cy.contains("Daily Rounds").click(); patientLogupdate.clickLogupdateCard("#dailyround-entry", patientCategory); cy.verifyContentPresence("#consultation-preview", [ @@ -127,7 +125,9 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.clickLogupdate(); patientLogupdate.typePhysicalExamination(physicalExamination); patientLogupdate.typeOtherDetails(otherExamination); - patientLogupdate.typeAdditionalSymptoms(additionalSymptoms); + patientLogupdate.selectSymptomsDate("01012024"); + patientLogupdate.typeAndMultiSelectSymptoms("fe", ["Fever"]); + patientLogupdate.clickAddSymptom(); patientLogupdate.selectPatientCategory(patientCategory); patientLogupdate.typeSystolic(patientSystolic); patientLogupdate.typeDiastolic(patientDiastolic); @@ -140,10 +140,10 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { cy.get("#consciousness_level-2").click(); cy.submitButton("Save"); cy.wait(2000); - cy.verifyNotification("Normal Log Updates details created successfully"); + cy.verifyNotification("Normal log update created successfully"); // Verify the card content cy.get("#basic-information").scrollIntoView(); - cy.verifyContentPresence("#basic-information", [additionalSymptoms]); + cy.verifyContentPresence("#encounter-symptoms", [additionalSymptoms]); }); it("Create a normal log update to verify MEWS Score Functionality", () => { @@ -163,7 +163,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeRespiratory(patientRespiratory); cy.get("#consciousness_level-2").click(); cy.submitButton("Save"); - cy.verifyNotification("Normal Log Updates details created successfully"); + cy.verifyNotification("Normal log update created successfully"); cy.closeNotification(); cy.verifyContentPresence("#consultation-buttons", ["9"]); // Verify the Incomplete data will give blank info @@ -173,7 +173,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => { patientLogupdate.typeDiastolic(patientDiastolic); patientLogupdate.typePulse(patientPulse); cy.submitButton("Save"); - cy.verifyNotification("Normal Log Updates details created successfully"); + cy.verifyNotification("Normal log update created successfully"); cy.closeNotification(); cy.verifyContentPresence("#consultation-buttons", ["-"]); }); diff --git a/cypress/e2e/users_spec/user_homepage.cy.ts b/cypress/e2e/users_spec/user_homepage.cy.ts index a006fe77569..3ac07dd9d9c 100644 --- a/cypress/e2e/users_spec/user_homepage.cy.ts +++ b/cypress/e2e/users_spec/user_homepage.cy.ts @@ -32,6 +32,7 @@ describe("User Homepage", () => { userPage.selectDistrict("Ernakulam"); userPage.typeInPhoneNumber(phone_number); userPage.typeInAltPhoneNumber(alt_phone_number); + userPage.selectHomeFacility("Dummy Facility 40"); userPage.applyFilter(); userPage.verifyUrlafteradvancefilter(); userPage.checkUsernameText(usernameToTest); @@ -46,6 +47,10 @@ describe("User Homepage", () => { "WhatsApp no.: +919876543219", ); userPage.verifyDataTestIdText("Role", "Role: Doctor"); + userPage.verifyDataTestIdText( + "Home Facility", + "Home Facility: Dummy Facility 40", + ); userPage.verifyDataTestIdText("District", "District: Ernakulam"); userPage.clearFilters(); userPage.verifyDataTestIdNotVisible("First Name"); @@ -53,6 +58,7 @@ describe("User Homepage", () => { userPage.verifyDataTestIdNotVisible("Phone Number"); userPage.verifyDataTestIdNotVisible("WhatsApp no."); userPage.verifyDataTestIdNotVisible("Role"); + userPage.verifyDataTestIdNotVisible("Home Facility"); userPage.verifyDataTestIdNotVisible("District"); }); diff --git a/cypress/pageobject/Patient/PatientConsultation.ts b/cypress/pageobject/Patient/PatientConsultation.ts index 4400d9a524c..31b1fd6cb68 100644 --- a/cypress/pageobject/Patient/PatientConsultation.ts +++ b/cypress/pageobject/Patient/PatientConsultation.ts @@ -6,14 +6,14 @@ export class PatientConsultationPage { cy.clickAndSelectOption("#route_to_facility", status); } - selectSymptoms(symptoms) { - cy.clickAndMultiSelectOption("#symptoms", symptoms); - } typeAndMultiSelectSymptoms(input, symptoms) { - cy.typeAndMultiSelectOption("#symptoms", input, symptoms); + cy.typeAndMultiSelectOption("#additional_symptoms", input, symptoms); + } + selectSymptomsDate(date: string) { + cy.clickAndTypeDate("#symptoms_onset_date", date); } - selectSymptomsDate(selector: string, date: string) { - cy.clickAndTypeDate(selector, date); + clickAddSymptom() { + cy.get("#add-symptom").click(); } verifyConsultationPatientName(patientName: string) { diff --git a/cypress/pageobject/Patient/PatientLogupdate.ts b/cypress/pageobject/Patient/PatientLogupdate.ts index 3511f0241bb..92ea02a1417 100644 --- a/cypress/pageobject/Patient/PatientLogupdate.ts +++ b/cypress/pageobject/Patient/PatientLogupdate.ts @@ -32,6 +32,16 @@ class PatientLogupdate { cy.searchAndSelectOption("#additional_symptoms", symptoms); } + typeAndMultiSelectSymptoms(input, symptoms) { + cy.typeAndMultiSelectOption("#additional_symptoms", input, symptoms); + } + selectSymptomsDate(date: string) { + cy.clickAndTypeDate("#symptoms_onset_date", date); + } + clickAddSymptom() { + cy.get("#add-symptom").click(); + } + typeSystolic(systolic: string) { cy.searchAndSelectOption("#systolic", systolic); } diff --git a/cypress/pageobject/Users/UserSearch.ts b/cypress/pageobject/Users/UserSearch.ts index 7d85563d62c..56d1a81395d 100644 --- a/cypress/pageobject/Users/UserSearch.ts +++ b/cypress/pageobject/Users/UserSearch.ts @@ -78,6 +78,10 @@ export class UserPage { cy.get("#alt_phone_number").click().type(altPhone); } + selectHomeFacility(facility: string) { + cy.searchAndSelectOption("input[name='home_facility']", facility); + } + applyFilter() { cy.get("#apply-filter").click(); } diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 2dbdfcbff00..d63c3bebf31 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -343,42 +343,6 @@ export const REVIEW_AT_CHOICES: Array = [ { id: 30 * 24 * 60, text: "1 month" }, ]; -export const SYMPTOM_CHOICES = [ - { id: 1, text: "ASYMPTOMATIC", isSingleSelect: true }, - { id: 2, text: "FEVER" }, - { id: 3, text: "SORE THROAT" }, - { id: 4, text: "COUGH" }, - { id: 5, text: "BREATHLESSNESS" }, - { id: 6, text: "MYALGIA" }, - { id: 7, text: "ABDOMINAL DISCOMFORT" }, - { id: 8, text: "VOMITING" }, - { id: 11, text: "SPUTUM" }, - { id: 12, text: "NAUSEA" }, - { id: 13, text: "CHEST PAIN" }, - { id: 14, text: "HEMOPTYSIS" }, - { id: 15, text: "NASAL DISCHARGE" }, - { id: 16, text: "BODY ACHE" }, - { id: 17, text: "DIARRHOEA" }, - { id: 18, text: "PAIN" }, - { id: 19, text: "PEDAL EDEMA" }, - { id: 20, text: "WOUND" }, - { id: 21, text: "CONSTIPATION" }, - { id: 22, text: "HEAD ACHE" }, - { id: 23, text: "BLEEDING" }, - { id: 24, text: "DIZZINESS" }, - { id: 25, text: "CHILLS" }, - { id: 26, text: "GENERAL WEAKNESS" }, - { id: 27, text: "IRRITABILITY" }, - { id: 28, text: "CONFUSION" }, - { id: 29, text: "ABDOMINAL PAIN" }, - { id: 30, text: "JOINT PAIN" }, - { id: 31, text: "REDNESS OF EYES" }, - { id: 32, text: "ANOREXIA" }, - { id: 33, text: "NEW LOSS OF TASTE" }, - { id: 34, text: "NEW LOSS OF SMELL" }, - { id: 9, text: "OTHERS" }, -]; - export const DISCHARGE_REASONS = [ { id: 1, text: "Recovered" }, { id: 2, text: "Referred" }, @@ -1333,7 +1297,7 @@ export const CONSENT_PATIENT_CODE_STATUS_CHOICES = [ { id: 1, text: "Do Not Hospitalise (DNH)" }, { id: 2, text: "Do Not Resuscitate (DNR)" }, { id: 3, text: "Comfort Care Only" }, - { id: 4, text: "Active treatment (Default)" }, + { id: 4, text: "Active treatment" }, ]; export const OCCUPATION_TYPES = [ { @@ -1429,3 +1393,5 @@ export const PATIENT_NOTES_THREADS = { Doctors: 10, Nurses: 20, } as const; + +export const RATION_CARD_CATEGORY = ["BPL", "APL", "NO_CARD"] as const; diff --git a/src/Components/CameraFeed/AssetBedSelect.tsx b/src/Components/CameraFeed/AssetBedSelect.tsx index c8a2d5451bc..715c326c35d 100644 --- a/src/Components/CameraFeed/AssetBedSelect.tsx +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -2,6 +2,7 @@ import { Fragment } from "react"; import { AssetBedModel } from "../Assets/AssetTypes"; import { Listbox, Transition } from "@headlessui/react"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import { classNames } from "../../Utils/utils"; interface Props { options: AssetBedModel[]; @@ -10,7 +11,40 @@ interface Props { onChange?: (value: AssetBedModel) => void; } -export default function AssetBedSelect(props: Props) { +export default function CameraPresetSelect(props: Props) { + const label = props.label ?? defaultLabel; + return ( + <> +
+ {/* Desktop View */} + {props.options + .slice(0, props.options.length > 5 ? 4 : 5) + .map((option) => ( + + ))} + {props.options.length > 5 && ( + + )} +
+
+ {/* Mobile View */} + +
+ + ); +} + +export const CameraPresetDropdown = (props: Props) => { const selected = props.value; const options = props.options.filter(({ meta }) => meta.type !== "boundary"); @@ -20,9 +54,14 @@ export default function AssetBedSelect(props: Props) { return (
- - - {selected ? label(selected) : "No Preset"} + + + {selected ? label(selected) : "Select preset"} @@ -63,7 +102,7 @@ export default function AssetBedSelect(props: Props) {
); -} +}; const defaultLabel = ({ bed_object, meta }: AssetBedModel) => { return `${bed_object.name}: ${meta.preset_name}`; diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index 8f7659cf730..81b526363b9 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -10,6 +10,8 @@ import FeedNetworkSignal from "./FeedNetworkSignal"; import NoFeedAvailable from "./NoFeedAvailable"; import FeedControls from "./FeedControls"; import Fullscreen from "../../CAREUI/misc/Fullscreen"; +import FeedWatermark from "./FeedWatermark"; +import CareIcon from "../../CAREUI/icons/CareIcon"; interface Props { children?: React.ReactNode; @@ -86,7 +88,6 @@ export default function CameraFeed(props: Props) { setState("loading"); initializeStream(); }; - return ( setFullscreen(false)}>
-
+ {props.children} +
+ {props.asset.name}
@@ -109,12 +115,12 @@ export default function CameraFeed(props: Props) { />
- {props.children}
{/* Notifications */} + {player.status === "playing" && } {/* No Feed informations */} {state === "host_unreachable" && ( @@ -145,6 +151,7 @@ export default function CameraFeed(props: Props) { url={streamUrl} ref={playerRef.current as LegacyRef} controls={false} + pip={false} playsinline playing muted @@ -162,10 +169,12 @@ export default function CameraFeed(props: Props) {
) : (
diff --git a/src/Components/Facility/ConsultationDetails/Events/types.ts b/src/Components/Facility/ConsultationDetails/Events/types.ts index f5cf3c9abec..053450ea346 100644 --- a/src/Components/Facility/ConsultationDetails/Events/types.ts +++ b/src/Components/Facility/ConsultationDetails/Events/types.ts @@ -1,3 +1,5 @@ +import routes from "../../../../Redux/api"; +import request from "../../../../Utils/request/request"; import { UserBareMinimum } from "../../../Users/models"; export type Type = { @@ -28,3 +30,19 @@ export type EventGeneric = { }; // TODO: Once event types are finalized, define specific types for each event + +let cachedEventTypes: Type[] | null = null; + +export const fetchEventTypeByName = async (name: Type["name"]) => { + if (!cachedEventTypes) { + const { data } = await request(routes.listEventTypes, { + query: { limit: 100 }, + }); + + if (data?.results) { + cachedEventTypes = data.results; + } + } + + return cachedEventTypes?.find((t) => t.name === name); +}; diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 38882c66f33..ee1f7bda7ff 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -1,8 +1,4 @@ -import { - CONSULTATION_TABS, - GENDER_TYPES, - SYMPTOM_CHOICES, -} from "../../../Common/constants"; +import { CONSULTATION_TABS, GENDER_TYPES } from "../../../Common/constants"; import { ConsultationModel } from "../models"; import { getConsultation, @@ -44,7 +40,6 @@ import { CameraFeedPermittedUserTypes } from "../../../Utils/permissions"; const Loading = lazy(() => import("../../Common/Loading")); const PageTitle = lazy(() => import("../../Common/PageTitle")); -const symptomChoices = [...SYMPTOM_CHOICES]; export interface ConsultationTabProps { consultationId: string; @@ -114,15 +109,15 @@ export const ConsultationDetails = (props: any) => { ...res.data, symptoms_text: "", }; - if (res.data.symptoms?.length) { - const symptoms = res.data.symptoms - .filter((symptom: number) => symptom !== 9) - .map((symptom: number) => { - const option = symptomChoices.find((i) => i.id === symptom); - return option ? option.text.toLowerCase() : symptom; - }); - data.symptoms_text = symptoms.join(", "); - } + // if (res.data.symptoms?.length) { + // const symptoms = res.data.symptoms + // .filter((symptom: number) => symptom !== 9) + // .map((symptom: number) => { + // const option = symptomChoices.find((i) => i.id === symptom); + // return option ? option.text.toLowerCase() : symptom; + // }); + // data.symptoms_text = symptoms.join(", "); + // } if (facilityId != data.facility || patientId != data.patient) { navigate( `/facility/${data.facility}/patient/${data.patient}/consultation/${data?.id}`, diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 7e6fe345f7c..d362abe1ecc 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -1,6 +1,6 @@ import * as Notification from "../../Utils/Notifications.js"; -import { BedModel, FacilityModel } from "./models"; +import { BedModel, ConsentRecord, FacilityModel } from "./models"; import { CONSULTATION_SUGGESTION, DISCHARGE_REASONS, @@ -8,8 +8,6 @@ import { PATIENT_CATEGORIES, REVIEW_AT_CHOICES, TELEMEDICINE_ACTIONS, - CONSENT_TYPE_CHOICES, - CONSENT_PATIENT_CODE_STATUS_CHOICES, } from "../../Common/constants"; import { Cancel, Submit } from "../Common/components/ButtonV2"; import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave"; @@ -25,7 +23,6 @@ import { BedSelect } from "../Common/BedSelect"; import Beds from "./Consultations/Beds"; import CareIcon from "../../CAREUI/icons/CareIcon"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; -import DateFormField from "../Form/FormFields/DateFormField"; import { FacilitySelect } from "../Common/FacilitySelect"; import { FieldChangeEvent, @@ -34,7 +31,6 @@ import { import { FormAction } from "../Form/Utils"; import PatientCategorySelect from "../Patient/PatientCategorySelect"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; -import { SymptomsSelect } from "../Common/SymptomsSelect"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import TextFormField from "../Form/FormFields/TextFormField"; import UserAutocompleteFormField from "../Common/UserAutocompleteFormField"; @@ -59,29 +55,24 @@ import { CreateDiagnosesBuilder, EditDiagnosesBuilder, } from "../Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder.js"; -import { FileUpload } from "../Patient/FileUpload.js"; -import ConfirmDialog from "../Common/ConfirmDialog.js"; import request from "../../Utils/request/request.js"; import routes from "../../Redux/api.js"; import useQuery from "../../Utils/request/useQuery.js"; import { t } from "i18next"; +import { Writable } from "../../Utils/types.js"; +import { EncounterSymptom } from "../Symptoms/types.js"; +import { + EncounterSymptomsBuilder, + CreateSymptomsBuilder, +} from "../Symptoms/SymptomsBuilder.js"; const Loading = lazy(() => import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); type BooleanStrings = "true" | "false"; -export type ConsentRecord = { - id: string; - type: (typeof CONSENT_TYPE_CHOICES)[number]["id"]; - patient_code_status?: (typeof CONSENT_PATIENT_CODE_STATUS_CHOICES)[number]["id"]; - deleted?: boolean; -}; - type FormDetails = { - symptoms: number[]; - other_symptoms: string; - symptoms_onset_date?: Date; + is_asymptomatic: boolean; suggestion: ConsultationSuggestionValue; route_to_facility?: RouteToFacility; patient: string; @@ -102,6 +93,8 @@ type FormDetails = { treating_physician_object: UserModel | null; create_diagnoses: CreateDiagnosis[]; diagnoses: ConsultationDiagnosis[]; + symptoms: EncounterSymptom[]; + create_symptoms: Writable[]; is_kasp: BooleanStrings; kasp_enabled_date: null; examination_details: string; @@ -125,13 +118,14 @@ type FormDetails = { death_datetime: string; death_confirmed_doctor: string; InvestigationAdvice: InvestigationType[]; + procedures: ProcedureType[]; consent_records: ConsentRecord[]; }; const initForm: FormDetails = { + is_asymptomatic: false, + create_symptoms: [], symptoms: [], - other_symptoms: "", - symptoms_onset_date: undefined, suggestion: "A", route_to_facility: undefined, patient: "", @@ -175,6 +169,7 @@ const initForm: FormDetails = { death_datetime: "", death_confirmed_doctor: "", InvestigationAdvice: [], + procedures: [], consent_records: [], }; @@ -226,7 +221,6 @@ type ConsultationFormSection = | "Consultation Details" | "Diagnosis" | "Treatment Plan" - | "Consent Records" | "Bed Status"; type Props = { @@ -259,14 +253,8 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { const [diagnosisVisible, diagnosisRef] = useVisibility(-300); const [treatmentPlanVisible, treatmentPlanRef] = useVisibility(-300); const [bedStatusVisible, bedStatusRef] = useVisibility(-300); - const [consentRecordsVisible, consentRecordsRef] = useVisibility(-300); + const [disabledFields, setDisabledFields] = useState([]); - const [collapsedConsentRecords, setCollapsedConsentRecords] = useState< - number[] - >([]); - const [showDeleteConsent, setShowDeleteConsent] = useState( - null, - ); const { min_encounter_date } = useConfig(); @@ -286,11 +274,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { visible: treatmentPlanVisible, ref: treatmentPlanRef, }, - "Consent Records": { - iconClass: "l-file-alt", - visible: consentRecordsVisible, - ref: consentRecordsRef, - }, "Bed Status": { iconClass: "l-bed", visible: bedStatusVisible, @@ -303,7 +286,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { if (consultationDetailsVisible) return "Consultation Details"; if (diagnosisVisible) return "Diagnosis"; if (treatmentPlanVisible) return "Treatment Plan"; - if (consentRecordsVisible) return "Consent Records"; if (bedStatusVisible) return "Bed Status"; return prev; }); @@ -311,7 +293,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { consultationDetailsVisible, diagnosisVisible, treatmentPlanVisible, - consentRecordsVisible, bedStatusVisible, ]); @@ -338,10 +319,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { }); }, []); - const hasSymptoms = - !!state.form.symptoms.length && !state.form.symptoms.includes(1); - const isOtherSymptomsSelected = state.form.symptoms.includes(9); - const handleFormFieldChange: FieldChangeEventHandler = (event) => { if (event.name === "suggestion" && event.value === "DD") { dispatch({ @@ -352,12 +329,21 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { consultation_notes: "Patient declared dead", }, }); - } else { + return; + } + + if (event.name === "is_asymptomatic" && event.value === true) { dispatch({ type: "set_form", - form: { ...state.form, [event.name]: event.value }, + form: { ...state.form, [event.name]: event.value, create_symptoms: [] }, }); + return; } + + dispatch({ + type: "set_form", + form: { ...state.form, [event.name]: event.value }, + }); }; const { loading: consultationLoading, refetch } = useQuery( @@ -395,9 +381,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { if (data) { const formData = { ...data, - symptoms_onset_date: - data.symptoms_onset_date && - isoStringToDate(data.symptoms_onset_date), encounter_date: isoStringToDate(data.encounter_date), icu_admission_date: data.icu_admission_date && @@ -458,12 +441,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { Object.keys(state.form).forEach((field) => { switch (field) { - case "symptoms": - if (!state.form[field] || !state.form[field].length) { - errors[field] = "Please select the symptoms"; - invalidForm = true; - } - return; case "category": if (!state.form[field]) { errors[field] = "Please select a category"; @@ -492,18 +469,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { invalidForm = true; } return; - case "other_symptoms": - if (isOtherSymptomsSelected && !state.form[field]) { - errors[field] = "Please enter the other symptom details"; - invalidForm = true; - } - return; - case "symptoms_onset_date": - if (hasSymptoms && !state.form[field]) { - errors[field] = "Please enter date of onset of the above symptoms"; - invalidForm = true; - } - return; case "encounter_date": if (!state.form[field]) { errors[field] = "Field is required"; @@ -524,6 +489,17 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { invalidForm = true; } return; + case "create_symptoms": + if ( + !isUpdate && + !state.form.is_asymptomatic && + state.form[field].length === 0 + ) { + errors[field] = + "Symptoms needs to be added as the patient is symptomatic"; + invalidForm = true; + } + return; case "death_datetime": if (state.form.suggestion === "DD" && !state.form[field]) { errors[field] = "Please enter the date & time of death"; @@ -702,13 +678,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { if (validated) { setIsLoading(true); const data: any = { - symptoms: state.form.symptoms, - other_symptoms: isOtherSymptomsSelected - ? state.form.other_symptoms - : undefined, - symptoms_onset_date: hasSymptoms - ? state.form.symptoms_onset_date - : undefined, suggestion: state.form.suggestion, route_to_facility: state.form.route_to_facility, admitted: state.form.suggestion === "A", @@ -721,6 +690,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { treatment_plan: state.form.treatment_plan, discharge_date: state.form.discharge_date, create_diagnoses: isUpdate ? undefined : state.form.create_diagnoses, + create_symptoms: isUpdate ? undefined : state.form.create_symptoms, treating_physician: state.form.treating_physician, investigation: state.form.InvestigationAdvice, procedure: state.form.procedure, @@ -769,7 +739,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { height: Number(state.form.height), bed: bed && bed instanceof Array ? bed[0]?.id : bed?.id, patient_no: state.form.patient_no || null, - consent_records: state.form.consent_records || [], }; const { data: obj } = await request( @@ -917,64 +886,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { }; }; - const handleConsentTypeChange: FieldChangeEventHandler = async ( - event, - ) => { - if (!id) return; - const consentRecords = [...state.form.consent_records]; - if ( - consentRecords - .filter((cr) => cr.deleted !== true) - .map((cr) => cr.type) - .includes(event.value) - ) { - return; - } else { - const randomId = "consent-" + new Date().getTime().toString(); - const newRecords = [ - ...consentRecords, - { id: randomId, type: event.value }, - ]; - await request(routes.partialUpdateConsultation, { - pathParams: { id }, - body: { consent_records: newRecords }, - }); - dispatch({ - type: "set_form", - form: { ...state.form, consent_records: newRecords }, - }); - } - }; - - const handleConsentPCSChange: FieldChangeEventHandler = (event) => { - dispatch({ - type: "set_form", - form: { - ...state.form, - consent_records: state.form.consent_records.map((cr) => - cr.type === 2 ? { ...cr, patient_code_status: event.value } : cr, - ), - }, - }); - }; - - const handleDeleteConsent = async () => { - const consent_id = showDeleteConsent; - if (!consent_id || !id) return; - const newRecords = state.form.consent_records.map((cr) => - cr.id === consent_id ? { ...cr, deleted: true } : cr, - ); - await request(routes.partialUpdateConsultation, { - pathParams: { id }, - body: { consent_records: newRecords }, - }); - dispatch({ - type: "set_form", - form: { ...state.form, consent_records: newRecords }, - }); - setShowDeleteConsent(null); - }; - return (
{ />
-
+
{Object.keys(sections).map((sectionTitle) => { - if ( - !isUpdate && - ["Bed Status", "Consent Records"].includes(sectionTitle) - ) { + if (!isUpdate && ["Bed Status"].includes(sectionTitle)) { + return null; + } + + if (isUpdate && sectionTitle === "Bed Status") { return null; } + const isCurrent = currentSection === sectionTitle; const section = sections[sectionTitle as ConsultationFormSection]; return ( @@ -1105,41 +1018,48 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
)} -
- -
- {isOtherSymptomsSelected && ( -
- -
- )} +
+
+ Symptoms - {hasSymptoms && ( -
- + {!isUpdate && ( + + )} + +
+ {isUpdate ? ( + + ) : ( + { + handleFormFieldChange({ + name: "create_symptoms", + value: symptoms, + }); + }} + /> + )} + +
- )} +
+
{ placeholder="Weight" trailingPadding=" " trailing={ -

+

Weight (kg)

} @@ -1196,7 +1116,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { placeholder="Height" trailingPadding=" " trailing={ -

+

Height (cm)

} @@ -1350,7 +1270,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
)} - {["A", "DC"].includes(state.form.suggestion) && !isUpdate && ( + {state.form.suggestion === "A" && !isUpdate && (
Bed { )}
- {id && ( - <> -
- {sectionTitle("Consent Records", true)} -
- setShowDeleteConsent(null)} - onConfirm={handleDeleteConsent} - action="Delete" - variant="danger" - description={ - "Are you sure you want to delete this consent record?" - } - title="Delete Consent" - className="w-auto" - /> - - !state.form.consent_records - .filter((r) => r.deleted !== true) - .map((record) => record.type) - .includes(c.id), - )} - /> -
- {state.form.consent_records - .filter((record) => record.deleted !== true) - .map((record, index) => ( -
-
- - -
-
-
- {record.type === 2 && ( - - )} -
- -
-
- ))} -
- - )}
@@ -1678,7 +1486,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
- {isUpdate && ( + {state.form.suggestion === "A" && isUpdate && ( <>
{sectionTitle("Bed Status")} diff --git a/src/Components/Facility/Consultations/Beds.res b/src/Components/Facility/Consultations/Beds.res new file mode 100644 index 00000000000..6e356172147 --- /dev/null +++ b/src/Components/Facility/Consultations/Beds.res @@ -0,0 +1,18 @@ +type reactClass +module Beds = { + @module("./Beds.tsx") @react.component + external make: ( + ~facilityId: string, + ~patientId: string, + ~consultationId: string, + ~setState: unit => unit, + ) => React.element = "default" +} + +@react.component +let make = ( + ~facilityId: string, + ~patientId: string, + ~consultationId: string, + ~setState: unit => unit, +) => diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index 1fd51940f8f..383a0646958 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -1001,5 +1001,5 @@ export const FacilityCreate = (props: FacilityProps) => { }; const FieldUnit = ({ unit }: { unit: string }) => { - return

{unit}

; + return

{unit}

; }; diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 49a2f1346af..8e54f0896bc 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -1,4 +1,6 @@ import { + CONSENT_PATIENT_CODE_STATUS_CHOICES, + CONSENT_TYPE_CHOICES, ConsultationSuggestionValue, DISCHARGE_REASONS, PATIENT_NOTES_THREADS, @@ -11,8 +13,8 @@ import { ProcedureType } from "../Common/prescription-builder/ProcedureBuilder"; import { ConsultationDiagnosis, CreateDiagnosis } from "../Diagnosis/types"; import { NormalPrescription, PRNPrescription } from "../Medicine/models"; import { AssignedToObjectModel, DailyRoundsModel } from "../Patient/models"; +import { EncounterSymptom } from "../Symptoms/types"; import { UserBareMinimum } from "../Users/models"; -import { ConsentRecord } from "./ConsultationForm"; export interface LocalBodyModel { id: number; @@ -97,6 +99,13 @@ export interface OptionsType { export type PatientCategory = "Comfort Care" | "Mild" | "Moderate" | "Critical"; +export type ConsentRecord = { + id: string; + type: (typeof CONSENT_TYPE_CHOICES)[number]["id"]; + patient_code_status?: (typeof CONSENT_PATIENT_CODE_STATUS_CHOICES)[number]["id"]; + deleted?: boolean; +}; + export interface ConsultationModel { encounter_date: string; icu_admission_date?: string; @@ -116,7 +125,6 @@ export interface ConsultationModel { facility_name?: string; id: string; modified_date?: string; - other_symptoms?: string; patient: string; treatment_plan?: string; referred_to?: FacilityModel["id"]; @@ -135,13 +143,12 @@ export interface ConsultationModel { kasp_enabled_date?: string; readonly diagnoses?: ConsultationDiagnosis[]; create_diagnoses?: CreateDiagnosis[]; // Used for bulk creating diagnoses upon consultation creation + readonly symptoms?: EncounterSymptom[]; + create_symptoms?: CreateDiagnosis[]; // Used for bulk creating symptoms upon consultation creation deprecated_verified_by?: string; - treating_physician?: UserBareMinimum["id"]; + readonly treating_physician?: UserBareMinimum["id"]; treating_physician_object?: UserBareMinimum; suggestion_text?: string; - symptoms?: Array; - symptoms_text?: string; - symptoms_onset_date?: string; consultation_notes?: string; is_telemedicine?: boolean; procedure?: ProcedureType[]; diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx index bed6a41f3c0..3bdbffdc6cb 100644 --- a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx +++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx @@ -84,7 +84,7 @@ export const AutocompleteMutliSelect = ( return { option, label, - description: props.optionDescription && props.optionDescription(option), + description: props.optionDescription?.(option), search: label.toLowerCase(), value: (props.optionValue ? props.optionValue(option) : option) as V, }; diff --git a/src/Components/Form/SearchInput.tsx b/src/Components/Form/SearchInput.tsx index e64aad207d1..2d146837ebf 100644 --- a/src/Components/Form/SearchInput.tsx +++ b/src/Components/Form/SearchInput.tsx @@ -80,7 +80,7 @@ const SearchInput = ({ props.trailing || (!props.secondary && (
- + {shortcutKeyIcon}
@@ -88,7 +88,7 @@ const SearchInput = ({ } trailingFocused={
- + Esc
diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx index 98fa1021f9f..49a071b744b 100644 --- a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx @@ -24,7 +24,7 @@ export default function MedicineAdministrationTable({ return (
- +
@@ -51,7 +51,7 @@ export default function MedicineAdministrationTable({ disabled={!pagination.hasPrevious} onClick={pagination.previous} tooltip="Previous 24 hours" - tooltipClassName="tooltip-bottom -translate-x-1/2 text-xs" + tooltipClassName="tooltip-bottom text-xs" > diff --git a/src/Components/Medicine/MedicinePrescriptionSummary.tsx b/src/Components/Medicine/MedicinePrescriptionSummary.tsx new file mode 100644 index 00000000000..b77acc090c4 --- /dev/null +++ b/src/Components/Medicine/MedicinePrescriptionSummary.tsx @@ -0,0 +1,372 @@ +import MedicineRoutes from "../Medicine/routes"; +import useQuery from "../../Utils/request/useQuery"; +import DialogModal from "../Common/Dialog"; +import { useState } from "react"; +import { lazy } from "react"; +import Timeline, { TimelineNode } from "../../CAREUI/display/Timeline"; +import { MedibaseMedicine, Prescription } from "../Medicine/models"; +import { useTranslation } from "react-i18next"; + +const Loading = lazy(() => import("../Common/Loading")); + +interface MedicinePrescriptionSummaryProps { + consultation: string; +} + +export const MedicinePrescriptionSummary = ({ + consultation, +}: MedicinePrescriptionSummaryProps) => { + const { t } = useTranslation(); + const [showMedicineModal, setShowMedicineModal] = useState({ + open: false, + name: "", + medicineId: "", + }); + const { data } = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation }, + query: { limit: 100 }, + }); + + const closeMedicineModal = () => { + setShowMedicineModal({ ...showMedicineModal, open: false }); + }; + + function extractUniqueMedicineObjects( + prescriptions: Prescription[], + ): MedibaseMedicine[] { + const uniqueMedicineObjects: Set = new Set(); + const uniqueMedicines: MedibaseMedicine[] = []; + + prescriptions.forEach((prescription: Prescription) => { + if (prescription?.medicine_object) { + const medicineId = prescription?.medicine_object.id; + + if (!uniqueMedicineObjects.has(medicineId)) { + uniqueMedicineObjects.add(medicineId); + uniqueMedicines.push(prescription?.medicine_object); + } + } + }); + + return uniqueMedicines; + } + + const medicinesList: MedibaseMedicine[] = extractUniqueMedicineObjects( + data?.results ?? [], + ); + + return ( +
+

{t("summary")}

+
+ {medicinesList && medicinesList.length > 0 ? ( + medicinesList?.map((med: MedibaseMedicine) => ( +
+
{med.name}
+ +
+ )) + ) : ( +
+
+

{"No Medicine Summary"}

+
+
+ )} +
+ + + {showMedicineModal.name}: {t("prescription_logs")} +

+ } + show={showMedicineModal.open} + onClose={closeMedicineModal} + fixedWidth={false} + className="md:w-3/4" + > + +
+
+ ); +}; + +interface ConsultationMedicineLogsProps { + consultationId: string; + medicineId: string; +} + +export default function ConsultationMedicineLogs({ + consultationId, + medicineId, +}: ConsultationMedicineLogsProps) { + const { data, loading } = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation: consultationId }, + query: { + medicine: medicineId, + }, + }); + + if (loading) { + return ; + } + + const getDetailsMessage = (prescription: Prescription) => { + const message = `Details: ${ + prescription.base_dosage != null + ? `${prescription.dosage_type === "TITRATED" ? "Start Dosage" : "Dosage"}: ${prescription.base_dosage}, ` + : "" + }${prescription.route != null ? `Route: ${prescription.route}, ` : ""}${ + prescription.target_dosage != null + ? `Target Dosage: ${prescription.target_dosage}, ` + : "" + }${ + prescription.instruction_on_titration != null + ? `Instruction on Titration: ${prescription.instruction_on_titration}, ` + : "" + }${ + prescription.frequency != null + ? `Frequency: ${prescription.frequency}, ` + : "" + }${prescription.days != null ? `Days: ${prescription.days}, ` : ""}${ + prescription.indicator != null + ? `Indicator: ${prescription.indicator}, ` + : "" + }${ + prescription.max_dosage != null + ? `Max Dosage: ${prescription.max_dosage}, ` + : "" + }${ + prescription.min_hours_between_doses != null + ? `Min Hours Between Doses: ${prescription.min_hours_between_doses}, ` + : "" + }${prescription.discontinued ? "Discontinued: Yes, " : ""}${ + prescription.dosage_type + ? `Prescription Type: ${prescription.dosage_type}, ` + : "" + }`.replace(/, $/, ""); + + return message; + }; + + const calculateChanges = (prescriptions: Prescription[]) => { + prescriptions = prescriptions.reverse(); + const changes = []; + + const message = getDetailsMessage(prescriptions[0]); + + changes.push({ + prescriptionId: prescriptions[0].id, + changeMessage: message, + prescribed_by: prescriptions[0].prescribed_by, + created_date: prescriptions[0].created_date, + }); + + if (prescriptions[0].discontinued) { + changes.push({ + prescriptionId: prescriptions[0].id, + changeMessage: "This prescription has been discontinued", + prescribed_by: prescriptions[0].prescribed_by, + created_date: prescriptions[0].discontinued_date, + }); + } + + for (let i = 1; i < prescriptions.length; i++) { + const prevPrescription = prescriptions[i - 1]; + const currentPrescription = prescriptions[i]; + + const changesForPrescription: string[] = []; + + // Check for changes in base dosage + if (prevPrescription.base_dosage !== currentPrescription.base_dosage) { + changesForPrescription.push( + `Base dosage changed to ${currentPrescription.base_dosage} from ${prevPrescription.base_dosage}`, + ); + } + + // Check for changes in route + if (prevPrescription.route !== currentPrescription.route) { + changesForPrescription.push( + `Route changed to ${ + currentPrescription.route ?? "Not specified" + } from ${prevPrescription.route ?? "Not specified"}`, + ); + } + + // Check for changes in dosage type + if (prevPrescription.dosage_type !== currentPrescription.dosage_type) { + changesForPrescription.push( + `Dosage type changed to ${ + currentPrescription.dosage_type ?? "Not specified" + } from ${prevPrescription.dosage_type ?? "Not specified"}`, + ); + } + + // Check for changes in target dosage + if ( + prevPrescription.target_dosage !== currentPrescription.target_dosage + ) { + changesForPrescription.push( + `Target dosage changed to ${ + currentPrescription.target_dosage ?? "Not specified" + } from ${prevPrescription.target_dosage ?? "Not specified"}`, + ); + } + + // Check for changes in instruction on titration + if ( + prevPrescription.instruction_on_titration !== + currentPrescription.instruction_on_titration + ) { + changesForPrescription.push( + `Instruction on titration changed to ${ + currentPrescription.instruction_on_titration ?? "Not specified" + } from ${ + prevPrescription.instruction_on_titration ?? "Not specified" + }`, + ); + } + + // Check for changes in frequency + if (prevPrescription.frequency !== currentPrescription.frequency) { + changesForPrescription.push( + `Frequency changed to ${ + currentPrescription.frequency ?? "Not specified" + } from ${prevPrescription.frequency ?? "Not specified"}`, + ); + } + + // Check for changes in days + if (prevPrescription.days !== currentPrescription.days) { + changesForPrescription.push( + `Days changed to ${ + currentPrescription.days ?? "Not specified" + } from ${prevPrescription.days ?? "Not specified"}`, + ); + } + + // Check for changes in indicator + if (prevPrescription.indicator !== currentPrescription.indicator) { + changesForPrescription.push( + `Indicator changed to ${ + currentPrescription.indicator ?? "Not specified" + } from ${prevPrescription.indicator ?? "Not specified"}`, + ); + } + + // Check for changes in max dosage + if (prevPrescription.max_dosage !== currentPrescription.max_dosage) { + changesForPrescription.push( + `Max dosage changed to ${ + currentPrescription.max_dosage ?? "Not specified" + } from ${prevPrescription.max_dosage ?? "Not specified"}`, + ); + } + + // Check for changes in min hours between doses + if ( + prevPrescription.min_hours_between_doses !== + currentPrescription.min_hours_between_doses + ) { + changesForPrescription.push( + `Min hours between doses changed to ${ + currentPrescription.min_hours_between_doses ?? "Not specified" + } from ${prevPrescription.min_hours_between_doses ?? "Not specified"}`, + ); + } + + // Check if discontinued + if (currentPrescription.discontinued && !prevPrescription.discontinued) { + changesForPrescription.push("Prescription was discontinued"); + } + + // Check if prescription type is changed + if ( + prevPrescription.prescription_type !== + currentPrescription.prescription_type + ) { + changesForPrescription.push( + `Prescription Type changed from ${prevPrescription.prescription_type} to ${currentPrescription.prescription_type}`, + ); + } + + // If there are changes, add them to the changes array + if (changesForPrescription.length > 0 && !prevPrescription.discontinued) { + const message = `Changes: ${changesForPrescription.join(", ")}`; + changes.push({ + prescriptionId: currentPrescription.id, + changeMessage: message, + prescribed_by: currentPrescription.prescribed_by, + created_date: currentPrescription.created_date, + }); + } else { + // If no changes, just list out the details of the prescription + const message = getDetailsMessage(currentPrescription); + + changes.push({ + prescriptionId: currentPrescription.id, + changeMessage: message, + prescribed_by: currentPrescription.prescribed_by, + created_date: currentPrescription.created_date, + }); + } + + if (currentPrescription.discontinued) { + changes.push({ + prescriptionId: currentPrescription.id, + changeMessage: "This prescription has been discontinued", + prescribed_by: currentPrescription.prescribed_by, + created_date: currentPrescription.discontinued_date, + }); + } + } + + return changes.reverse(); + }; + + return ( +
+ + {data?.results && + (() => { + const changesArray = calculateChanges(data?.results); + return changesArray.map((changes, index) => ( + +

{changes?.changeMessage}

+
+ )); + })()} +
+
+ ); +} diff --git a/src/Components/Medicine/PrescriptionBuilder.tsx b/src/Components/Medicine/PrescriptionBuilder.tsx index 972a74159d2..7833245135c 100644 --- a/src/Components/Medicine/PrescriptionBuilder.tsx +++ b/src/Components/Medicine/PrescriptionBuilder.tsx @@ -12,6 +12,7 @@ import useQuery from "../../Utils/request/useQuery"; import MedicineRoutes from "./routes"; import useSlug from "../../Common/hooks/useSlug"; import { AuthorizedForConsultationRelatedActions } from "../../CAREUI/misc/AuthorizedChild"; +import { compareBy } from "../../Utils/utils"; interface Props { prescription_type?: Prescription["prescription_type"]; @@ -66,15 +67,18 @@ export default function PrescriptionBuilder({ /> )}
- {data?.results.map((obj, index) => ( - setShowDiscontinueFor(obj)} - onAdministerClick={() => setShowAdministerFor(obj)} - readonly={disabled} - /> - ))} + {data?.results + .sort(compareBy("discontinued")) + ?.map((obj) => ( + setShowDiscontinueFor(obj)} + onAdministerClick={() => setShowAdministerFor(obj)} + readonly={disabled} + /> + ))}
void; onAdministerClick?: () => void; selected?: boolean; -}) { + collapsible?: boolean; +} + +export default function PrescriptionDetailCard({ + prescription, + collapsible = false, + ...props +}: Props) { const { t } = useTranslation(); + const [isCollapsed, setIsCollapsed] = useState( + collapsible && prescription.discontinued, + ); + return (
-
+
{ + if (collapsible) { + setIsCollapsed(!isCollapsed); + } + }} + >
@@ -39,14 +56,21 @@ export default function PrescriptionDetailCard({ props.selected ? "text-black" : "text-gray-700", )} > - {prescription.prescription_type === "DISCHARGE" && - `${t("discharge")} `} - {t( - prescription.dosage_type === "PRN" - ? "prn_prescription" - : "prescription", + {isCollapsed ? ( + prescription.medicine_object?.name ?? + prescription.medicine_old + ) : ( + <> + {prescription.prescription_type === "DISCHARGE" && + `${t("discharge")} `} + {t( + prescription.dosage_type === "PRN" + ? "prn_prescription" + : "prescription", + )} + {` #${prescription.id?.slice(-5)}`} + )} - {` #${prescription.id?.slice(-5)}`} {prescription.discontinued && ( @@ -62,7 +86,10 @@ export default function PrescriptionDetailCard({ { + e.stopPropagation(); + props.onAdministerClick?.(); + }} type="button" size="small" variant="secondary" @@ -79,7 +106,10 @@ export default function PrescriptionDetailCard({ variant="danger" ghost border - onClick={props.onDiscontinueClick} + onClick={(e) => { + e.stopPropagation(); + props.onDiscontinueClick?.(); + }} > {t("discontinue")} @@ -89,114 +119,114 @@ export default function PrescriptionDetailCard({ )}
- -
- - {prescription.medicine_object?.name ?? prescription.medicine_old} - - - {prescription.route && - t("PRESCRIPTION_ROUTE_" + prescription.route)} - - {prescription.dosage_type === "TITRATED" ? ( - <> - - {prescription.base_dosage} - - - {prescription.target_dosage} - - - ) : ( + {!isCollapsed && ( +
- {prescription.base_dosage} + {prescription.medicine_object?.name ?? prescription.medicine_old} - )} - - {prescription.dosage_type === "PRN" ? ( - <> + + {prescription.route && + t("PRESCRIPTION_ROUTE_" + prescription.route)} + + {prescription.dosage_type === "TITRATED" ? ( + <> + + {prescription.base_dosage} + + + {prescription.target_dosage} + + + ) : ( - {prescription.indicator} + {prescription.base_dosage} + )} + + {prescription.dosage_type === "PRN" ? ( + <> + + {prescription.indicator} + + + {prescription.max_dosage} + + + {prescription.min_hours_between_doses && + prescription.min_hours_between_doses + " hrs."} + + + ) : ( + <> + + {prescription.frequency && + t( + "PRESCRIPTION_FREQUENCY_" + + prescription.frequency.toUpperCase(), + )} + + + {prescription.days} + + + )} + + {prescription.instruction_on_titration && ( - {prescription.max_dosage} + + + )} + + {prescription.notes && ( + + + )} + + {prescription.discontinued && ( - {prescription.min_hours_between_doses && - prescription.min_hours_between_doses + " hrs."} + {prescription.discontinued_reason} - - ) : ( - <> - - {prescription.frequency && - t( - "PRESCRIPTION_FREQUENCY_" + - prescription.frequency.toUpperCase(), - )} - - - {prescription.days} - - - )} - - {prescription.instruction_on_titration && ( - - - - )} - - {prescription.notes && ( - - - - )} - - {prescription.discontinued && ( - - {prescription.discontinued_reason} - - )} -
- + )} +
+ )}
Prescribed diff --git a/src/Components/Patient/DailyRoundListDetails.tsx b/src/Components/Patient/DailyRoundListDetails.tsx index 3c5e70044ed..66536c986ca 100644 --- a/src/Components/Patient/DailyRoundListDetails.tsx +++ b/src/Components/Patient/DailyRoundListDetails.tsx @@ -1,5 +1,5 @@ import { lazy, useState } from "react"; -import { CONSCIOUSNESS_LEVEL, SYMPTOM_CHOICES } from "../../Common/constants"; +import { CONSCIOUSNESS_LEVEL } from "../../Common/constants"; import { DailyRoundsModel } from "./models"; import Page from "../Common/components/Page"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -7,7 +7,6 @@ import { formatDateTime } from "../../Utils/utils"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; const Loading = lazy(() => import("../Common/Loading")); -const symptomChoices = [...SYMPTOM_CHOICES]; export const DailyRoundListDetails = (props: any) => { const { facilityId, patientId, consultationId, id } = props; @@ -21,16 +20,8 @@ export const DailyRoundListDetails = (props: any) => { const tdata: DailyRoundsModel = { ...data, temperature: Number(data.temperature) ? data.temperature : "", - additional_symptoms_text: "", medication_given: data.medication_given ?? [], }; - if (data.additional_symptoms?.length) { - const symptoms = data.additional_symptoms.map((symptom: number) => { - const option = symptomChoices.find((i) => i.id === symptom); - return option ? option.text.toLowerCase() : symptom; - }); - tdata.additional_symptoms_text = symptoms.join(", "); - } setDailyRoundListDetails(tdata); } }, @@ -85,12 +76,6 @@ export const DailyRoundListDetails = (props: any) => { SpO2: {dailyRoundListDetailsData.ventilator_spo2 ?? "-"}
-
- - Additional Symptoms:{" "} - - {dailyRoundListDetailsData.additional_symptoms_text ?? "-"} -
Admitted To *:{" "} @@ -103,12 +88,6 @@ export const DailyRoundListDetails = (props: any) => { {dailyRoundListDetailsData.physical_examination_info ?? "-"}
-
- - Other Symptoms:{" "} - - {dailyRoundListDetailsData.other_symptoms ?? "-"} -
Other Details:{" "} diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index dd15d44facc..d0b2e321898 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -17,7 +17,6 @@ import { capitalize } from "lodash-es"; import BloodPressureFormField, { BloodPressureValidator, } from "../Common/BloodPressureFormField"; -import { SymptomsSelect } from "../Common/SymptomsSelect"; import TemperatureFormField from "../Common/TemperatureFormField"; import { Cancel, Submit } from "../Common/components/ButtonV2"; import Page from "../Common/components/Page"; @@ -33,11 +32,23 @@ import routes from "../../Redux/api"; import { Scribe } from "../Scribe/Scribe"; import { DAILY_ROUND_FORM_SCRIBE_DATA } from "../Scribe/formDetails"; import { DailyRoundsModel } from "./models"; +import { fetchEventTypeByName } from "../Facility/ConsultationDetails/Events/types"; +import InvestigationBuilder from "../Common/prescription-builder/InvestigationBuilder"; +import { FieldErrorText } from "../Form/FormFields/FormField"; +import { error } from "@pnotify/core"; +import { useTranslation } from "react-i18next"; +import PrescriptionBuilder from "../Medicine/PrescriptionBuilder"; +import { EditDiagnosesBuilder } from "../Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder"; +import { + ConditionVerificationStatuses, + ConsultationDiagnosis, +} from "../Diagnosis/types"; +import { EncounterSymptomsBuilder } from "../Symptoms/SymptomsBuilder"; +import { FieldLabel } from "../Form/FormFields/FormField"; + const Loading = lazy(() => import("../Common/Loading")); const initForm: any = { - additional_symptoms: [], - other_symptoms: "", physical_examination_info: "", other_details: "", patient_category: "", @@ -48,6 +59,8 @@ const initForm: any = { taken_at: null, rounds_type: "NORMAL", systolic: null, + investigations: [], + investigations_dirty: false, diastolic: null, pulse: null, resp: null, @@ -98,6 +111,7 @@ const DailyRoundsFormReducer = (state = initialState, action: any) => { }; export const DailyRounds = (props: any) => { + const { t } = useTranslation(); const { goBack } = useAppHistory(); const { facilityId, patientId, consultationId, id } = props; const [state, dispatch] = useAutoSaveReducer( @@ -114,18 +128,19 @@ export const DailyRounds = (props: any) => { ...initForm, action: "", }); + const [diagnoses, setDiagnoses] = useState(); const headerText = !id ? "Add Consultation Update" : "Info"; const buttonText = !id ? "Save" : "Continue"; const formFields = [ "physical_examination_info", "other_details", - "additional_symptoms", "action", "review_interval", "bp", "pulse", "resp", + "investigations", "ventilator_spo2", "rhythm", "rhythm_detail", @@ -134,6 +149,7 @@ export const DailyRounds = (props: any) => { const fetchRoundDetails = useCallback(async () => { setIsLoading(true); + fetchEventTypeByName(""); let formData: any = initialData; if (id) { const { data } = await request(routes.getDailyReport, { @@ -165,6 +181,13 @@ export const DailyRounds = (props: any) => { setPatientName(data.name!); setFacilityName(data.facility_object!.name); setConsultationSuggestion(data.last_consultation?.suggestion); + setDiagnoses( + data.last_consultation?.diagnoses?.sort( + (a: ConsultationDiagnosis, b: ConsultationDiagnosis) => + ConditionVerificationStatuses.indexOf(a.verification_status) - + ConditionVerificationStatuses.indexOf(b.verification_status), + ), + ); setPreviousReviewInterval( Number(data.last_consultation?.review_interval), ); @@ -176,7 +199,11 @@ export const DailyRounds = (props: any) => { ...initialData, action: getAction, }); - formData = { ...formData, ...{ action: getAction } }; + formData = { + ...formData, + action: getAction, + investigations: data.last_consultation?.investigation ?? [], + }; } } else { setPatientName(""); @@ -201,15 +228,6 @@ export const DailyRounds = (props: any) => { invalidForm = true; } return; - case "other_symptoms": - if ( - state.form.additional_symptoms?.includes(9) && - !state.form[field] - ) { - errors[field] = "Please enter the other symptom details"; - invalidForm = true; - } - return; case "bp": { const error = BloodPressureValidator(state.form.bp); if (error) { @@ -218,6 +236,33 @@ export const DailyRounds = (props: any) => { } return; } + + case "investigations": { + for (const investigation of state.form.investigations) { + if (!investigation.type?.length) { + errors[field] = "Investigation field can not be empty"; + invalidForm = true; + break; + } + if ( + investigation.repetitive && + !investigation.frequency?.replace(/\s/g, "").length + ) { + errors[field] = "Frequency field cannot be empty"; + invalidForm = true; + break; + } + if ( + !investigation.repetitive && + !investigation.time?.replace(/\s/g, "").length + ) { + errors[field] = "Time field cannot be empty"; + invalidForm = true; + break; + } + } + return; + } default: return; } @@ -231,17 +276,31 @@ export const DailyRounds = (props: any) => { const validForm = validateForm(); if (validForm) { setIsLoading(true); + + if ( + state.form.rounds_type === "DOCTORS_LOG" && + state.form.investigations_dirty + ) { + const { error: investigationError } = await request( + routes.partialUpdateConsultation, + { + body: { investigation: state.form.investigations }, + pathParams: { id: consultationId }, + }, + ); + + if (investigationError) { + Notification.Error({ msg: error }); + return; + } + } + let data: DailyRoundsModel = { rounds_type: state.form.rounds_type, patient_category: state.form.patient_category, taken_at: state.form.taken_at ? state.form.taken_at : new Date().toISOString(), - - additional_symptoms: state.form.additional_symptoms, - other_symptoms: state.form.additional_symptoms?.includes(9) - ? state.form.other_symptoms - : undefined, admitted_to: (state.form.admitted === "Select" ? undefined @@ -278,7 +337,7 @@ export const DailyRounds = (props: any) => { if (obj) { dispatch({ type: "set_form", form: initForm }); Notification.Success({ - msg: `${obj.rounds_type === "VENTILATOR" ? "Critical Care" : capitalize(obj.rounds_type)} Log Updates details updated successfully`, + msg: `${obj.rounds_type === "VENTILATOR" ? "Critical Care" : capitalize(obj.rounds_type)} log update details updated successfully`, }); if (["NORMAL", "TELEMEDICINE"].includes(state.form.rounds_type)) { navigate( @@ -298,14 +357,24 @@ export const DailyRounds = (props: any) => { setIsLoading(false); if (obj) { dispatch({ type: "set_form", form: initForm }); - Notification.Success({ - msg: `${obj.rounds_type === "VENTILATOR" ? "Critical Care" : capitalize(obj.rounds_type)} Log Updates details created successfully`, - }); if (["NORMAL", "TELEMEDICINE"].includes(state.form.rounds_type)) { + Notification.Success({ + msg: `${state.form.rounds_type === "NORMAL" ? "Normal" : "Tele-medicine"} log update created successfully`, + }); + navigate( + `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}`, + ); + } else if (state.form.rounds_type === "DOCTORS_LOG") { + Notification.Success({ + msg: "Doctors log update created successfully", + }); navigate( `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}`, ); } else { + Notification.Success({ + msg: "Critical Care log update created successfully", + }); navigate( `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/daily_rounds/${obj.id}/update`, ); @@ -316,10 +385,16 @@ export const DailyRounds = (props: any) => { }; const handleFormFieldChange = (event: FieldChangeEvent) => { - dispatch({ - type: "set_form", - form: { ...state.form, [event.name]: event.value }, - }); + const form = { + ...state.form, + [event.name]: event.value, + }; + + if (event.name === "investigations") { + form["investigations_dirty"] = true; + } + + dispatch({ type: "set_form", form }); }; const field = (name: string) => { @@ -406,6 +481,7 @@ export const DailyRounds = (props: any) => { options={[ ...[ { id: "NORMAL", text: "Normal" }, + { id: "DOCTORS_LOG", text: "Doctor's Log Update" }, { id: "VENTILATOR", text: "Critical Care" }, ], ...(consultationSuggestion == "DC" @@ -436,51 +512,46 @@ export const DailyRounds = (props: any) => { label="Other Details" rows={5} /> - - {state.form.additional_symptoms?.includes(9) && ( -
- -
- )} +
+ Symptoms + +
- option.desc} - optionValue={(option) => option.text} - value={prevAction} - onChange={(event) => { - handleFormFieldChange(event); - setPreviousAction(event.value); - }} - /> + {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)); - }} - /> + option.text} + optionValue={(option) => option.id} + value={prevReviewInterval} + onChange={(event) => { + handleFormFieldChange(event); + setPreviousReviewInterval(Number(event.value)); + }} + /> + + )} - {["NORMAL", "TELEMEDICINE"].includes(state.form.rounds_type) && ( + {["NORMAL", "TELEMEDICINE", "DOCTORS_LOG"].includes( + state.form.rounds_type, + ) && ( <>

Vitals

@@ -599,6 +670,53 @@ export const DailyRounds = (props: any) => { /> )} + + {state.form.rounds_type === "DOCTORS_LOG" && ( + <> +
+
+

+ {t("investigations")} +

+ { + handleFormFieldChange({ + name: "investigations", + value: investigations, + }); + }} + /> + +
+
+

+ {t("prescription_medications")} +

+ +
+
+

+ {t("prn_prescriptions")} +

+ +
+
+

+ {t("diagnosis")} +

+ {/* */} + {diagnoses ? ( + + ) : ( +
+ Fetching existing diagnosis of patient... +
+ )} +
+
+ + )}
@@ -607,11 +725,14 @@ export const DailyRounds = (props: any) => { disabled={ buttonText === "Save" && formFields.every( - (field: string) => state.form[field] == initialData[field], + (field: string) => + JSON.stringify(state.form[field]) === + JSON.stringify(initialData[field]), ) && (state.form.temperature == initialData.temperature || isNaN(state.form.temperature)) && - state.form.rounds_type !== "VENTILATOR" + state.form.rounds_type !== "VENTILATOR" && + state.form.rounds_type !== "DOCTORS_LOG" } onClick={(e) => handleSubmit(e)} label={buttonText} diff --git a/src/Components/Patient/FileUpload.tsx b/src/Components/Patient/FileUpload.tsx index 81e92efdf8b..6f63a697229 100644 --- a/src/Components/Patient/FileUpload.tsx +++ b/src/Components/Patient/FileUpload.tsx @@ -57,7 +57,7 @@ export const header_content_type: URLS = { }; // Array of image extensions -const ExtImage: string[] = [ +export const ExtImage: string[] = [ "jpeg", "jpg", "png", @@ -119,12 +119,13 @@ interface URLS { [id: string]: string; } -interface ModalDetails { +export interface ModalDetails { name?: string; id?: string; reason?: string; userArchived?: string; archiveTime?: any; + associatedId?: string; } export interface StateInterface { diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index ac716f7bf45..a99d12c09fc 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -181,6 +181,7 @@ export const PatientManager = () => { qParams.date_declared_positive_before || undefined, date_declared_positive_after: qParams.date_declared_positive_after || undefined, + ration_card_category: qParams.ration_card_category || undefined, last_consultation_medico_legal_case: qParams.last_consultation_medico_legal_case || undefined, last_consultation_encounter_date_before: @@ -960,6 +961,13 @@ export const PatientManager = () => { "Is Medico-Legal Case", "last_consultation_medico_legal_case", ), + value( + "Ration Card Category", + "ration_card_category", + qParams.ration_card_category + ? t(`ration_card__${qParams.ration_card_category}`) + : "", + ), value( "Facility", "facility", diff --git a/src/Components/Patient/PatientConsentRecordBlock.tsx b/src/Components/Patient/PatientConsentRecordBlock.tsx new file mode 100644 index 00000000000..9c1969ae5a3 --- /dev/null +++ b/src/Components/Patient/PatientConsentRecordBlock.tsx @@ -0,0 +1,187 @@ +import dayjs from "dayjs"; +import { + CONSENT_PATIENT_CODE_STATUS_CHOICES, + CONSENT_TYPE_CHOICES, +} from "../../Common/constants"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; +import { ConsentRecord } from "../Facility/models"; +import { FileUploadModel } from "./models"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import ButtonV2 from "../Common/components/ButtonV2"; +import { useEffect } from "react"; +import useAuthUser from "../../Common/hooks/useAuthUser"; + +export default function PatientConsentRecordBlockGroup(props: { + consentRecord: ConsentRecord; + previewFile: (file: FileUploadModel, file_associating_id: string) => void; + archiveFile: ( + file: FileUploadModel, + file_associating_id: string, + skipPrompt?: { reason: string }, + ) => void; + onDelete: (consentRecord: ConsentRecord) => void; + refreshTrigger: any; + showArchive: boolean; + onFilesFound: () => void; +}) { + const { + consentRecord, + previewFile, + archiveFile, + refreshTrigger, + showArchive, + } = props; + + const authUser = useAuthUser(); + + const filesQuery = useQuery(routes.viewUpload, { + query: { + file_type: "CONSENT_RECORD", + associating_id: consentRecord.id, + is_archived: false, + limit: 100, + offset: 0, + }, + onResponse: (response) => { + /* + if (consentRecord.deleted === true && response.data?.results) { + const unarchivedFiles = response.data.results; + console.log("checking for unarchived files on this deleted consent record") + for (const file of unarchivedFiles) { + console.log("archiving file", file) + archiveFile(file, consentRecord.id, { + reason: "Consent Record Archived", + }); + } + } + */ + + if ((response.data?.results?.length || 0) > 0) { + props.onFilesFound(); + } + }, + }); + + const archivedFilesQuery = useQuery(routes.viewUpload, { + query: { + file_type: "CONSENT_RECORD", + associating_id: consentRecord.id, + is_archived: true, + limit: 100, + offset: 0, + }, + prefetch: showArchive, + onResponse: (response) => { + if ((response.data?.results?.length || 0) > 0) { + props.onFilesFound(); + } + }, + }); + + const consent = CONSENT_TYPE_CHOICES.find((c) => c.id === consentRecord.type); + const consentPCS = CONSENT_PATIENT_CODE_STATUS_CHOICES.find( + (c) => c.id === consentRecord.patient_code_status, + ); + + const data = showArchive + ? [ + ...(archivedFilesQuery.data?.results || []), + ...(consentRecord.deleted ? filesQuery.data?.results || [] : []), + ] + : filesQuery.data?.results; + + const loading = archivedFilesQuery.loading || filesQuery.loading; + + useEffect(() => { + if (!showArchive) { + filesQuery.refetch(); + } else { + archivedFilesQuery.refetch(); + } + }, [showArchive, refreshTrigger]); + + return ( +
+
+
+

+ {consent?.text} {consentPCS?.text && `(${consentPCS.text})`} +

+ {consentRecord.deleted && ( +
+
+ + Archived +
+
+ )} +
+ {/* + {!consentRecord.deleted && !showArchive && ( + + )} + */} +
+ {loading ? ( +
+ ) : ( + data?.map((file: FileUploadModel, i: number) => ( +
+
+
+ +
+
+
+ {file.name} + {file.extension} {file.is_archived && "(Archived)"} +
+
+ {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} +
+
+
+
+ {!file.is_archived && ( + previewFile(file, consentRecord.id)} + className="" + > + + View + + )} + {(file.is_archived || + file?.uploaded_by?.username === authUser.username || + authUser.user_type === "DistrictAdmin" || + authUser.user_type === "StateAdmin") && ( + archiveFile(file, consentRecord.id)} + className="" + > + + {file.is_archived ? "More Info" : "Archive"} + + )} +
+
+ )) + )} +
+ ); +} diff --git a/src/Components/Patient/PatientConsentRecords.tsx b/src/Components/Patient/PatientConsentRecords.tsx new file mode 100644 index 00000000000..ca26b270d80 --- /dev/null +++ b/src/Components/Patient/PatientConsentRecords.tsx @@ -0,0 +1,312 @@ +import { useEffect, useState } from "react"; +import { + CONSENT_PATIENT_CODE_STATUS_CHOICES, + CONSENT_TYPE_CHOICES, +} from "../../Common/constants"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; +import Page from "../Common/components/Page"; +import { ConsentRecord } from "../Facility/models"; +import request from "../../Utils/request/request"; +import ConfirmDialog from "../Common/ConfirmDialog"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { formatDateTime } from "../../Utils/utils"; +import TextFormField from "../Form/FormFields/TextFormField"; +import ButtonV2 from "../Common/components/ButtonV2"; +import useFileUpload from "../../Utils/useFileUpload"; +import PatientConsentRecordBlockGroup from "./PatientConsentRecordBlock"; +import SwitchTabs from "../Common/components/SwitchTabs"; +import useFileManager from "../../Utils/useFileManager"; + +export default function PatientConsentRecords(props: { + facilityId: string; + patientId: string; + consultationId: string; +}) { + const { facilityId, patientId, consultationId } = props; + const [showArchived, setShowArchived] = useState(false); + const [filesFound, setFilesFound] = useState(false); + const [showPCSChangeModal, setShowPCSChangeModal] = useState( + null, + ); + const [newConsent, setNewConsent] = useState({ + type: 0, + patient_code_status: 4, + }); + + const fileUpload = useFileUpload({ + type: "CONSENT_RECORD", + }); + + const fileManager = useFileManager({ + type: "CONSENT_RECORD", + onArchive: async () => { + refetch(); + }, + }); + + const { data: patient } = useQuery(routes.getPatient, { + pathParams: { + id: patientId, + }, + }); + const { + data: consultation, + refetch, + loading, + } = useQuery(routes.getConsultation, { + pathParams: { id: consultationId! }, + onResponse: (data) => { + if (data.data && data.data.consent_records) { + setConsentRecords(data.data.consent_records); + } + }, + }); + + const [showDeleteConsent, setShowDeleteConsent] = useState( + null, + ); + + const [consentRecords, setConsentRecords] = useState( + null, + ); + + const handleDeleteConsent = async () => { + const consent_id = showDeleteConsent; + if (!consent_id || !consultationId || !consentRecords) return; + const newRecords = consentRecords.map((cr) => + cr.id === consent_id ? { ...cr, deleted: true } : cr, + ); + setConsentRecords(newRecords); + setShowDeleteConsent(null); + }; + + const selectField = (name: string) => { + return { + name, + optionValue: (option: any) => option.id, + optionLabel: (option: any) => option.text, + optionDescription: (option: any) => option.desc, + }; + }; + + const handleUpload = async (diffPCS?: ConsentRecord) => { + if (newConsent.type === 0) return; + const consentTypeExists = consentRecords?.find( + (record) => record.type === newConsent.type && record.deleted !== true, + ); + if (consentTypeExists && !diffPCS) { + await fileUpload.handleFileUpload(consentTypeExists.id); + } else { + const randomId = "consent-" + new Date().getTime().toString(); + const newRecords = [ + ...(consentRecords?.map((r) => + r.id === diffPCS?.id ? { ...r, deleted: true } : r, + ) || []), + { + id: randomId, + type: newConsent.type, + patient_code_status: + newConsent.type === 2 ? newConsent.patient_code_status : undefined, + }, + ]; + await request(routes.partialUpdateConsultation, { + pathParams: { id: consultationId }, + body: { consent_records: newRecords }, + }); + await fileUpload.handleFileUpload(randomId); + setConsentRecords(newRecords); + } + + refetch(); + }; + + useEffect(() => { + const timeout = setTimeout(async () => { + if (consentRecords) { + await request(routes.partialUpdateConsultation, { + pathParams: { id: consultationId }, + body: { consent_records: consentRecords }, + }); + } + }, 1000); + return () => clearTimeout(timeout); + }, [consentRecords]); + + const tabConsents = consentRecords?.filter((c) => showArchived || !c.deleted); + + useEffect(() => { + setFilesFound(false); + }, [showArchived]); + + return ( + + {fileUpload.Dialogues} + {fileManager.Dialogues} + setShowDeleteConsent(null)} + onConfirm={handleDeleteConsent} + action="Archive" + variant="danger" + description={ + "Are you sure you want to archive this consent record? You can find it in the archive section." + } + title="Archive Consent" + className="w-auto" + /> + setShowPCSChangeModal(null)} + onConfirm={() => { + if (showPCSChangeModal !== null) { + handleUpload( + consentRecords?.find( + (record) => + record.type === 2 && + !record.deleted && + record.patient_code_status !== showPCSChangeModal, + ), + ); + } + setShowPCSChangeModal(null); + }} + action="Change Patient Code Status" + variant="danger" + description={`Consent records exist with the "${CONSENT_PATIENT_CODE_STATUS_CHOICES.find((c) => consentRecords?.find((c) => c.type === 2 && !c.deleted)?.patient_code_status === c.id)?.text}" patient code status. Adding a new record for a different type will archive the existing records. Are you sure you want to proceed?`} + title="Archive Previous Records" + className="w-auto" + /> + setShowArchived(false)} + onClickTab2={() => setShowArchived(true)} + isTab2Active={showArchived} + /> +
+
+

Add New Record

+ { + setNewConsent({ ...newConsent, type: e.value }); + }} + value={newConsent.type} + label="Consent Type" + options={CONSENT_TYPE_CHOICES} + required + /> + {newConsent.type === 2 && ( + { + setNewConsent({ + ...newConsent, + patient_code_status: e.value, + }); + }} + label="Patient Code Status" + value={newConsent.patient_code_status} + options={CONSENT_PATIENT_CODE_STATUS_CHOICES} + required + /> + )} + fileUpload.setFileName(e.value)} + /> +
+ {fileUpload.file ? ( + <> + { + const diffPCS = consentRecords?.find( + (record) => + record.type === 2 && + newConsent.type === 2 && + record.patient_code_status !== + newConsent.patient_code_status && + record.deleted !== true, + ); + if (diffPCS) { + setShowPCSChangeModal(newConsent.patient_code_status); + } else { + handleUpload(); + } + }} + loading={!!fileUpload.progress} + className="flex-1" + > + + Upload + + + + + + ) : ( + <> + + + + )} +
+
+
+
+ {loading ? ( +
+ ) : tabConsents?.length === 0 || !filesFound ? ( +
+ No records found +
+ ) : null} + {!loading && + tabConsents?.map((record, index) => ( + setShowDeleteConsent(record.id)} + refreshTrigger={consultation} + showArchive={showArchived} + onFilesFound={() => setFilesFound(true)} + /> + ))} +
+
+
+ + ); +} diff --git a/src/Components/Patient/PatientFilter.tsx b/src/Components/Patient/PatientFilter.tsx index 0d8ebaae2b0..f153f9aa93d 100644 --- a/src/Components/Patient/PatientFilter.tsx +++ b/src/Components/Patient/PatientFilter.tsx @@ -7,6 +7,7 @@ import { FACILITY_TYPES, GENDER_TYPES, PATIENT_FILTER_CATEGORIES, + RATION_CARD_CATEGORY, } from "../../Common/constants"; import useConfig from "../../Common/hooks/useConfig"; import useMergeState from "../../Common/hooks/useMergeState"; @@ -31,11 +32,14 @@ import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import useAuthUser from "../../Common/hooks/useAuthUser"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import { useTranslation } from "react-i18next"; const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); export default function PatientFilter(props: any) { + const { t } = useTranslation(); const authUser = useAuthUser(); const { kasp_enabled, kasp_string } = useConfig(); const { filter, onChange, closeFilter, removeFilters } = props; @@ -59,6 +63,7 @@ export default function PatientFilter(props: any) { age_min: filter.age_min || null, age_max: filter.age_max || null, date_declared_positive: filter.date_declared_positive || null, + ration_card_category: filter.ration_card_category || null, last_consultation_medico_legal_case: filter.last_consultation_medico_legal_case || null, last_consultation_encounter_date_before: @@ -171,6 +176,7 @@ export default function PatientFilter(props: any) { gender, age_min, age_max, + ration_card_category, last_consultation_medico_legal_case, last_consultation_encounter_date_before, last_consultation_encounter_date_after, @@ -214,6 +220,7 @@ export default function PatientFilter(props: any) { created_date_after: dateQueryString(created_date_after), modified_date_before: dateQueryString(modified_date_before), modified_date_after: dateQueryString(modified_date_after), + ration_card_category, last_consultation_medico_legal_case: last_consultation_medico_legal_case || "", last_consultation_encounter_date_before: dateQueryString( @@ -467,6 +474,21 @@ export default function PatientFilter(props: any) { } />
+ t(`ration_card__${o}`)} + optionValue={(o) => o} + value={filterState.ration_card_category} + onChange={(e) => + setFilterState({ + ...filterState, + [e.name]: e.value, + }) + } + />
{ {patientData.facility_object?.name || "-"}

- {patientGender} | {patientData.blood_group || "-"} + {patientGender} | {patientData.blood_group || "-"} | Born on{" "} + {patientData.date_of_birth + ? formatDate(patientData.date_of_birth) + : patientData.year_of_birth}

-
-
- {patientData.date_of_birth - ? "Date of Birth" - : "Year of Birth"} -
-
- {patientData.date_of_birth - ? formatDate(patientData.date_of_birth) - : patientData.year_of_birth} -
-
Phone @@ -547,6 +538,16 @@ export const PatientHome = (props: any) => { {parseOccupation(patientData.meta_info?.occupation) || "-"}
+
+
+ Ration Card Category +
+
+ {patientData.ration_card_category + ? t(`ration_card__${patientData.ration_card_category}`) + : "-"} +
+
@@ -562,7 +563,7 @@ export const PatientHome = (props: any) => { 0 && (
)} -
+
{/* Can support for patient picture in the future */} @@ -234,13 +234,15 @@ export default function PatientInfoCard(props: { {category.toUpperCase()}
)} - setOpen(true)} - className="mt-1 px-[10px] py-1" - > - {bedDialogTitle} - + {consultation?.admitted && ( + setOpen(true)} + className="mt-1 px-[10px] py-1" + > + {bedDialogTitle} + + )}
-
+
{medicoLegalCase && ( - + MLC )} @@ -299,7 +301,7 @@ export default function PatientInfoCard(props: {
{consultation?.patient_no && ( @@ -352,6 +354,18 @@ export default function PatientInfoCard(props: {
)} + {( + consultation?.consent_records?.filter((c) => !c.deleted) || + [] + ).length < 1 && ( +
+
+ + Consent Records Missing + +
+
+ )} {consultation?.suggestion === "DC" && (
@@ -506,12 +520,12 @@ export default function PatientInfoCard(props: {
{consultation?.suggestion === "A" && (
-
+
)} {consultation?.last_daily_round && ( -
+
)} @@ -619,7 +633,7 @@ export default function PatientInfoCard(props: { } className="xl:justify-center" containerClassName="w-full lg:w-auto mt-2 2xl:mt-0 flex justify-center z-20" @@ -634,6 +648,12 @@ export default function PatientInfoCard(props: { consultation?.id && !consultation?.discharge_date, ], + [ + `/facility/${patient.facility}/patient/${patient.id}/consultation/${consultation?.id}/consent-records`, + "Consent Records", + "l-file-medical", + patient.is_active, + ], [ `/patient/${patient.id}/investigation_reports`, "Investigation Summary", @@ -667,7 +687,10 @@ export default function PatientInfoCard(props: { key={i} className="dropdown-item-primary pointer-events-auto m-2 flex cursor-pointer items-center justify-start gap-2 rounded border-0 p-2 text-sm font-normal transition-all duration-200 ease-in-out" href={ - action[1] !== "Treatment Summary" && + ![ + "Treatment Summary", + "Consent Records", + ].includes(action[1]) && consultation?.admitted && !consultation?.current_bed && i === 1 @@ -676,7 +699,10 @@ export default function PatientInfoCard(props: { } onClick={() => { if ( - action[1] !== "Treatment Summary" && + ![ + "Treatment Summary", + "Consent Records", + ].includes(action[1]) && consultation?.admitted && !consultation?.current_bed && i === 1 diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index 37a239b8098..4a891f66bde 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -6,6 +6,7 @@ import { GENDER_TYPES, MEDICAL_HISTORY_CHOICES, OCCUPATION_TYPES, + RATION_CARD_CATEGORY, VACCINES, } from "../../Common/constants"; import { @@ -66,6 +67,7 @@ import SelectMenuV2 from "../Form/SelectMenuV2.js"; import Checkbox from "../Common/components/CheckBox.js"; import _ from "lodash"; import { ILocalBodies } from "../ExternalResult/models.js"; +import { useTranslation } from "react-i18next"; const Loading = lazy(() => import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); @@ -134,6 +136,7 @@ const initForm: any = { last_vaccinated_date: null, abha_number: null, ...medicalHistoryChoices, + ration_card_category: null, }; const initError = Object.assign( @@ -173,6 +176,7 @@ export const parseOccupationFromExt = (occupation: Occupation) => { export const PatientRegister = (props: PatientRegisterProps) => { const authUser = useAuthUser(); + const { t } = useTranslation(); const { goBack } = useAppHistory(); const { gov_data_api_key, enable_hcx, enable_abdm } = useConfig(); const { facilityId, id } = props; @@ -785,6 +789,7 @@ export const PatientRegister = (props: PatientRegisterProps) => { blood_group: formData.blood_group ? formData.blood_group : undefined, medical_history, is_active: true, + ration_card_category: formData.ration_card_category, }; const { res, data: requestData } = id ? await request(routes.updatePatient, { @@ -1424,8 +1429,9 @@ export const PatientRegister = (props: PatientRegisterProps) => { +

{field("age").value !== "" && ( <> @@ -1443,7 +1449,6 @@ export const PatientRegister = (props: PatientRegisterProps) => {

} placeholder="Enter the age" - className="col-span-6 sm:col-span-3" type="number" min={0} /> @@ -1786,6 +1791,14 @@ export const PatientRegister = (props: PatientRegisterProps) => { optionLabel={(o) => o.text} optionValue={(o) => o.id} /> + t(`ration_card__${o}`)} + optionValue={(o) => o} + /> ) : (
diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index 72af6c07f90..ff8446c600a 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -3,6 +3,7 @@ import { PerformedByModel } from "../HCX/misc"; import { CONSCIOUSNESS_LEVEL, OCCUPATION_TYPES, + RATION_CARD_CATEGORY, RHYTHM_CHOICES, } from "../../Common/constants"; @@ -109,6 +110,7 @@ export interface PatientModel { state?: number; nationality?: string; passport_no?: string; + ration_card_category?: (typeof RATION_CARD_CATEGORY)[number] | null; date_of_test?: string; date_of_result?: string; // keeping this to avoid errors in Death report covin_id?: string; @@ -282,6 +284,7 @@ export interface DailyRoundsOutput { export const DailyRoundTypes = [ "NORMAL", + "DOCTORS_LOG", "VENTILATOR", "AUTOMATED", "TELEMEDICINE", @@ -310,13 +313,10 @@ export interface DailyRoundsModel { physical_examination_info?: string; other_details?: string; consultation?: number; - additional_symptoms?: Array; medication_given?: Array; - additional_symptoms_text?: string; action?: string; review_interval?: number; id?: string; - other_symptoms?: string; admitted_to?: string; patient_category?: PatientCategory; output?: DailyRoundsOutput[]; @@ -340,7 +340,7 @@ export interface FacilityNameModel { // File Upload Models -type FileCategory = "UNSPECIFIED" | "XRAY" | "AUDIO" | "IDENTITY_PROOF"; +export type FileCategory = "UNSPECIFIED" | "XRAY" | "AUDIO" | "IDENTITY_PROOF"; export interface CreateFileRequest { file_type: string | number; diff --git a/src/Components/Scribe/formDetails.ts b/src/Components/Scribe/formDetails.ts index 2b56ce90df5..74673adea70 100644 --- a/src/Components/Scribe/formDetails.ts +++ b/src/Components/Scribe/formDetails.ts @@ -3,9 +3,9 @@ import { PATIENT_CATEGORIES, REVIEW_AT_CHOICES, RHYTHM_CHOICES, - SYMPTOM_CHOICES, TELEMEDICINE_ACTIONS, } from "../../Common/constants"; +import { SYMPTOM_CHOICES } from "../Symptoms/types"; import { Field } from "./Scribe"; export const DAILY_ROUND_FORM_SCRIBE_DATA: Field[] = [ diff --git a/src/Components/Symptoms/SymptomsBuilder.tsx b/src/Components/Symptoms/SymptomsBuilder.tsx new file mode 100644 index 00000000000..de6901d8384 --- /dev/null +++ b/src/Components/Symptoms/SymptomsBuilder.tsx @@ -0,0 +1,381 @@ +import { useState } from "react"; +import { Writable } from "../../Utils/types"; +import { + EncounterSymptom, + OTHER_SYMPTOM_CHOICE, + SYMPTOM_CHOICES, +} from "./types"; +import AutocompleteMultiSelectFormField from "../Form/FormFields/AutocompleteMultiselect"; +import DateFormField from "../Form/FormFields/DateFormField"; +import ButtonV2 from "../Common/components/ButtonV2"; +import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import { classNames, dateQueryString } from "../../Utils/utils"; +import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import SymptomsApi from "./api"; +import request from "../../Utils/request/request"; +import { Success } from "../../Utils/Notifications"; +import { sortByOnsetDate } from "./utils"; + +export const CreateSymptomsBuilder = (props: { + value: Writable[]; + onChange: (value: Writable[]) => void; +}) => { + return ( +
+
    + {props.value.map((obj, index, arr) => { + const handleUpdate = (event: FieldChangeEvent) => { + const updated = { ...obj, [event.name]: event.value }; + props.onChange(arr.map((old, i) => (i === index ? updated : old))); + }; + + const handleRemove = () => { + props.onChange(arr.filter((_, i) => i !== index)); + }; + + return ( +
  • + +
  • + ); + })} +
+ + {props.value.length === 0 && ( +
+ No symptoms added +
+ )} + +
+ props.onChange([...props.value, ...objects])} + /> +
+
+ ); +}; + +export const EncounterSymptomsBuilder = (props: { showAll?: boolean }) => { + const consultationId = useSlug("consultation"); + + const [isProcessing, setIsProcessing] = useState(false); + const { data, loading, refetch } = useQuery(SymptomsApi.list, { + pathParams: { consultationId }, + query: { limit: 100 }, + }); + + if (!data) { + return ( +
+ + Fetching symptom records... +
+ ); + } + + let items = sortByOnsetDate(data.results); + if (!props.showAll) { + items = items.filter( + (i) => i.clinical_impression_status !== "entered-in-error", + ); + } + + return ( +
+
    + {items.map((symptom) => { + const handleUpdate = async (event: FieldChangeEvent) => { + setIsProcessing(true); + await request(SymptomsApi.partialUpdate, { + pathParams: { consultationId, external_id: symptom.id }, + body: { [event.name]: event.value }, + }); + await refetch(); + setIsProcessing(false); + }; + + const handleMarkAsEnteredInError = async () => { + setIsProcessing(true); + await request(SymptomsApi.markAsEnteredInError, { + pathParams: { consultationId, external_id: symptom.id }, + }); + await refetch(); + setIsProcessing(false); + }; + + return ( +
  • + +
  • + ); + })} +
+ + {items.length === 0 && ( +
+ Patient is Asymptomatic +
+ )} + +
+ refetch()} + /> +
+
+ ); +}; + +const SymptomEntry = (props: { + disabled?: boolean; + value: Writable | EncounterSymptom; + onChange: (event: FieldChangeEvent) => void; + onRemove: () => void; +}) => { + const symptom = props.value; + const disabled = + props.disabled || symptom.clinical_impression_status === "entered-in-error"; + return ( +
+ + +
+
+ + + + {symptom.clinical_impression_status === "entered-in-error" && ( + + Entered in Error + + )} +
+ + + +
+
+ ); +}; + +const AddSymptom = (props: { + disabled?: boolean; + existing: (Writable | EncounterSymptom)[]; + onAdd?: (value: Writable[]) => void; + consultationId?: string; +}) => { + const [processing, setProcessing] = useState(false); + const [selected, setSelected] = useState([]); + const [otherSymptom, setOtherSymptom] = useState(""); + const [onsetDate, setOnsetDate] = useState(); + + const activeSymptomIds = props.existing + .filter((o) => o.symptom !== OTHER_SYMPTOM_CHOICE.id && !o.cure_date) + .map((o) => o.symptom); + + const handleAdd = async () => { + const objects = selected.map((symptom) => { + return { + symptom, + onset_date: dateQueryString(onsetDate), + other_symptom: + symptom === OTHER_SYMPTOM_CHOICE.id ? otherSymptom : undefined, + }; + }); + + if (props.consultationId) { + const responses = await Promise.all( + objects.map((body) => + request(SymptomsApi.add, { + body, + pathParams: { consultationId: props.consultationId! }, + }), + ), + ); + + if (responses.every(({ res }) => !!res?.ok)) { + Success({ msg: "Symptoms records updated successfully" }); + } + } + props.onAdd?.(objects); + + setSelected([]); + setOtherSymptom(""); + }; + + const hasSymptoms = !!selected.length; + const otherSymptomValid = selected.includes(OTHER_SYMPTOM_CHOICE.id) + ? !!otherSymptom.trim() + : true; + + return ( +
+ setOnsetDate(value)} + errorClassName="hidden" + /> +
+ setSelected(e.value)} + options={SYMPTOM_CHOICES.filter( + ({ id }) => !activeSymptomIds.includes(id), + )} + optionLabel={(option) => option.text} + optionValue={(option) => option.id} + errorClassName="hidden" + /> + {selected.includes(OTHER_SYMPTOM_CHOICE.id) && ( + setOtherSymptom(value)} + errorClassName="hidden" + /> + )} +
+ { + setProcessing(true); + await handleAdd(); + setProcessing(false); + }} + > + {processing ? ( + <> + + Adding... + + ) : ( + Add Symptom(s) + )} + +
+ ); +}; + +export const SymptomText = (props: { + value: Writable | EncounterSymptom; +}) => { + const symptom = + SYMPTOM_CHOICES.find(({ id }) => props.value.symptom === id) || + OTHER_SYMPTOM_CHOICE; + + const isOtherSymptom = symptom.id === OTHER_SYMPTOM_CHOICE.id; + + return isOtherSymptom ? ( + <> + Other: + + {props.value.other_symptom || "Not specified"} + + + ) : ( + symptom.text + ); +}; diff --git a/src/Components/Symptoms/SymptomsCard.tsx b/src/Components/Symptoms/SymptomsCard.tsx new file mode 100644 index 00000000000..7dbf920a7b6 --- /dev/null +++ b/src/Components/Symptoms/SymptomsCard.tsx @@ -0,0 +1,84 @@ +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import { SymptomText } from "./SymptomsBuilder"; +import SymptomsApi from "./api"; +import { type EncounterSymptom } from "./types"; +import { groupAndSortSymptoms } from "./utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +// TODO: switch to list from events as timeline view instead once filter event by event type name is done +const EncounterSymptomsCard = () => { + const consultationId = useSlug("consultation"); + + const { data } = useQuery(SymptomsApi.list, { + pathParams: { consultationId }, + query: { limit: 100 }, + }); + + if (!data) { + return ( +
+ + Fetching symptom records... +
+ ); + } + + const records = groupAndSortSymptoms(data.results); + + return ( +
+

+ Symptoms +

+ +
+ +
+ +
+
+
+ ); +}; + +const SymptomsSection = (props: { + title: string; + symptoms: EncounterSymptom[]; +}) => { + return ( +
+

+ {props.title} +

+
    + {props.symptoms.map((record) => ( +
  • +
    + +
    + + + Onset + + {record.cure_date && ( + + {"; Cured"} + + )} + +
    +
  • + ))} +
+ {!props.symptoms.length && ( +
+ No symptoms +
+ )} +
+ ); +}; + +export default EncounterSymptomsCard; diff --git a/src/Components/Symptoms/api.ts b/src/Components/Symptoms/api.ts new file mode 100644 index 00000000000..1cce062cb1e --- /dev/null +++ b/src/Components/Symptoms/api.ts @@ -0,0 +1,47 @@ +import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { WritableOnly } from "../../Utils/types"; +import { EncounterSymptom } from "./types"; + +const SymptomsApi = { + list: { + method: "GET", + path: "/api/v1/consultation/{consultationId}/symptoms/", + TRes: Type>(), + }, + + add: { + path: "/api/v1/consultation/{consultationId}/symptoms/", + method: "POST", + TRes: Type(), + TBody: Type>(), + }, + + retrieve: { + method: "GET", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TRes: Type(), + }, + + update: { + method: "PUT", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TBody: Type>(), + TRes: Type(), + }, + + partialUpdate: { + method: "PATCH", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TBody: Type>>(), + TRes: Type(), + }, + + markAsEnteredInError: { + method: "DELETE", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TRes: Type(), + }, +} as const; + +export default SymptomsApi; diff --git a/src/Components/Symptoms/types.ts b/src/Components/Symptoms/types.ts new file mode 100644 index 00000000000..78e769ce9a9 --- /dev/null +++ b/src/Components/Symptoms/types.ts @@ -0,0 +1,52 @@ +import { BaseModel } from "../../Utils/types"; + +export const OTHER_SYMPTOM_CHOICE = { id: 9, text: "Other Symptom" } as const; + +export const SYMPTOM_CHOICES = [ + { id: 2, text: "Fever" }, + { id: 3, text: "Sore throat" }, + { id: 4, text: "Cough" }, + { id: 5, text: "Breathlessness" }, + { id: 6, text: "Myalgia" }, + { id: 7, text: "Abdominal discomfort" }, + { id: 8, text: "Vomiting" }, + { id: 11, text: "Sputum" }, + { id: 12, text: "Nausea" }, + { id: 13, text: "Chest pain" }, + { id: 14, text: "Hemoptysis" }, + { id: 15, text: "Nasal discharge" }, + { id: 16, text: "Body ache" }, + { id: 17, text: "Diarrhoea" }, + { id: 18, text: "Pain" }, + { id: 19, text: "Pedal Edema" }, + { id: 20, text: "Wound" }, + { id: 21, text: "Constipation" }, + { id: 22, text: "Head ache" }, + { id: 23, text: "Bleeding" }, + { id: 24, text: "Dizziness" }, + { id: 25, text: "Chills" }, + { id: 26, text: "General weakness" }, + { id: 27, text: "Irritability" }, + { id: 28, text: "Confusion" }, + { id: 29, text: "Abdominal pain" }, + { id: 30, text: "Join pain" }, + { id: 31, text: "Redness of eyes" }, + { id: 32, text: "Anorexia" }, + { id: 33, text: "New loss of taste" }, + { id: 34, text: "New loss of smell" }, + OTHER_SYMPTOM_CHOICE, +] as const; + +type ClinicalImpressionStatus = + | "in-progress" + | "completed" + | "entered-in-error"; + +export interface EncounterSymptom extends BaseModel { + symptom: (typeof SYMPTOM_CHOICES)[number]["id"]; + other_symptom?: string | null; + onset_date: string; + cure_date?: string | null; + readonly clinical_impression_status: ClinicalImpressionStatus; + readonly is_migrated: boolean; +} diff --git a/src/Components/Symptoms/utils.ts b/src/Components/Symptoms/utils.ts new file mode 100644 index 00000000000..997acfc914b --- /dev/null +++ b/src/Components/Symptoms/utils.ts @@ -0,0 +1,37 @@ +import { Writable } from "../../Utils/types"; +import { compareByDateString } from "../../Utils/utils"; +import { EncounterSymptom } from "./types"; + +// TODO: switch to using Object.groupBy(...) instead once upgraded to node v22 +export const groupAndSortSymptoms = < + T extends Writable | EncounterSymptom, +>( + records: T[], +) => { + const result: Record = { + "entered-in-error": [], + "in-progress": [], + completed: [], + }; + + for (const record of records) { + const status = + record.clinical_impression_status || + (record.cure_date ? "completed" : "in-progress"); + result[status].push(record); + } + + result["completed"] = sortByOnsetDate(result["completed"]); + result["in-progress"] = sortByOnsetDate(result["in-progress"]); + result["entered-in-error"] = sortByOnsetDate(result["entered-in-error"]); + + return result; +}; + +export const sortByOnsetDate = < + T extends Writable | EncounterSymptom, +>( + records: T[], +) => { + return records.sort(compareByDateString("onset_date")); +}; diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index e2556587e96..019a51232ba 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -163,5 +163,8 @@ "clear_all_filters": "Clear All Filters", "summary": "Summary", "report": "Report", - "treating_doctor":"Treating Doctor" + "treating_doctor": "Treating Doctor", + "ration_card__NO_CARD": "Non-card holder", + "ration_card__BPL": "BPL", + "ration_card__APL": "APL" } \ No newline at end of file diff --git a/src/Locale/en/Medicine.json b/src/Locale/en/Medicine.json index 36adc259514..4a5a5785047 100644 --- a/src/Locale/en/Medicine.json +++ b/src/Locale/en/Medicine.json @@ -36,6 +36,7 @@ "prescription_discontinued": "Prescription discontinued", "administration_notes": "Administration Notes", "last_administered": "Last administered", + "prescription_logs": "Prescription Logs", "modification_caution_note": "No modifications possible once added", "discontinue_caution_note": "Are you sure you want to discontinue this prescription?", "edit_caution_note": "A new prescription will be added to the consultation with the edited details and the current prescription will be discontinued.", diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 58147f68943..f4a9732a40c 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -97,7 +97,10 @@ import { import { PaginatedResponse } from "../Utils/request/types"; import { ICD11DiagnosisModel } from "../Components/Diagnosis/types"; -import { EventGeneric } from "../Components/Facility/ConsultationDetails/Events/types"; +import { + EventGeneric, + type Type, +} from "../Components/Facility/ConsultationDetails/Events/types"; import { InvestigationGroup, InvestigationType, @@ -628,6 +631,14 @@ const routes = { TRes: Type(), }, + // Event Types + + listEventTypes: { + path: "/api/v1/event_types/", + method: "GET", + TRes: Type>(), + }, + // Hospital Beds createCapacity: { path: "/api/v1/facility/{facilityId}/capacity/", diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx index 6dc5fa9c05d..8b75e3f147f 100644 --- a/src/Routers/routes/ConsultationRoutes.tsx +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -9,6 +9,7 @@ import { make as CriticalCareRecording } from "../../Components/CriticalCareReco import { ConsultationDetails } from "../../Components/Facility/ConsultationDetails"; import TreatmentSummary from "../../Components/Facility/TreatmentSummary"; import ConsultationDoctorNotes from "../../Components/Facility/ConsultationDoctorNotes"; +import PatientConsentRecords from "../../Components/Patient/PatientConsentRecords"; export default { "/facility/:facilityId/patient/:patientId/consultation": ({ @@ -22,6 +23,14 @@ export default { }: any) => ( ), + "/facility/:facilityId/patient/:patientId/consultation/:id/consent-records": + ({ facilityId, patientId, id }: any) => ( + + ), "/facility/:facilityId/patient/:patientId/consultation/:id/files/": ({ facilityId, patientId, diff --git a/src/Utils/types.ts b/src/Utils/types.ts new file mode 100644 index 00000000000..519ede36ffc --- /dev/null +++ b/src/Utils/types.ts @@ -0,0 +1,36 @@ +import { PerformedByModel } from "../Components/HCX/misc"; + +export interface BaseModel { + readonly id: string; + readonly modified_date: string; + readonly created_date: string; + readonly created_by: PerformedByModel; + readonly updated_by: PerformedByModel; +} + +export type Writable = { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + never, + P + >]?: undefined; +} & { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + P, + never + >]: T[P]; +}; + +export type WritableOnly = { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + P + >]: T[P]; +}; + +type IfEquals = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B; diff --git a/src/Utils/useFileManager.tsx b/src/Utils/useFileManager.tsx new file mode 100644 index 00000000000..133b4d2533d --- /dev/null +++ b/src/Utils/useFileManager.tsx @@ -0,0 +1,274 @@ +import { useState } from "react"; +import FilePreviewDialog from "../Components/Common/FilePreviewDialog"; +import { FileUploadModel } from "../Components/Patient/models"; +import { ExtImage, StateInterface } from "../Components/Patient/FileUpload"; +import request from "./request/request"; +import routes from "../Redux/api"; +import DialogModal from "../Components/Common/Dialog"; +import CareIcon from "../CAREUI/icons/CareIcon"; +import TextAreaFormField from "../Components/Form/FormFields/TextAreaFormField"; +import { Cancel, Submit } from "../Components/Common/components/ButtonV2"; +import { formatDateTime } from "./utils"; +import * as Notification from "./Notifications.js"; + +export interface FileManagerOptions { + type: string; + onArchive?: () => void; +} + +export interface FileManagerResult { + viewFile: (file: FileUploadModel, associating_id: string) => void; + archiveFile: ( + file: FileUploadModel, + associating_id: string, + skipPrompt?: { reason: string }, + ) => void; + Dialogues: React.ReactNode; +} + +export default function useFileManager( + options: FileManagerOptions, +): FileManagerResult { + const { type: fileType, onArchive } = options; + + const [file_state, setFileState] = useState({ + open: false, + isImage: false, + name: "", + extension: "", + zoom: 4, + isZoomInDisabled: false, + isZoomOutDisabled: false, + rotation: 0, + }); + const [fileUrl, setFileUrl] = useState(""); + const [downloadURL, setDownloadURL] = useState(""); + const [archiveDialogueOpen, setArchiveDialogueOpen] = useState< + (FileUploadModel & { associating_id: string }) | null + >(null); + const [archiveReason, setArchiveReason] = useState(""); + const [archiveReasonError, setArchiveReasonError] = useState(""); + const [archiving, setArchiving] = useState(false); + + const getExtension = (url: string) => { + const div1 = url.split("?")[0].split("."); + const ext: string = div1[div1.length - 1].toLowerCase(); + return ext; + }; + + const viewFile = async (file: FileUploadModel, associating_id: string) => { + setFileUrl(""); + setFileState({ ...file_state, open: true }); + const { data } = await request(routes.retrieveUpload, { + query: { + file_type: fileType, + associating_id, + }, + pathParams: { id: file.id || "" }, + }); + + if (!data) return; + + const signedUrl = data.read_signed_url as string; + const extension = getExtension(signedUrl); + + const downloadFileUrl = (url: string) => { + fetch(url) + .then((res) => res.blob()) + .then((blob) => { + setDownloadURL(URL.createObjectURL(blob)); + }); + }; + + setFileState({ + ...file_state, + open: true, + name: data.name as string, + extension, + isImage: ExtImage.includes(extension), + }); + downloadFileUrl(signedUrl); + setFileUrl(signedUrl); + }; + + const validateArchiveReason = (name: any) => { + if (name.trim() === "") { + setArchiveReasonError("Please enter a valid reason!"); + return false; + } else { + setArchiveReasonError(""); + return true; + } + }; + + const handleFileArchive = async (archiveFile: typeof archiveDialogueOpen) => { + if (!validateArchiveReason(archiveReason)) { + setArchiving(false); + return; + } + + const { res } = await request(routes.editUpload, { + body: { is_archived: true, archive_reason: archiveReason }, + pathParams: { + id: archiveFile?.id || "", + fileType, + associatingId: archiveFile?.associating_id || "", + }, + }); + + if (res?.ok) { + Notification.Success({ msg: "File archived successfully" }); + } + + setArchiveDialogueOpen(null); + setArchiving(false); + setArchiveReason(""); + onArchive && onArchive(); + return res; + }; + + const archiveFile = ( + file: FileUploadModel, + associating_id: string, + skipPrompt?: { reason: string }, + ) => { + if (skipPrompt) { + setArchiving(true); + setArchiveReason(skipPrompt.reason); + handleFileArchive({ + ...file, + associating_id, + }); + return; + } + setArchiveDialogueOpen({ ...file, associating_id }); + }; + + const handleFilePreviewClose = () => { + setDownloadURL(""); + setFileState({ + ...file_state, + open: false, + zoom: 4, + isZoomInDisabled: false, + isZoomOutDisabled: false, + }); + }; + + const Dialogues = ( + <> + + +
+ +
+
+

Archive File

+ This action is irreversible. Once a file is archived it cannot be + unarchived. +
+
+ } + onClose={() => setArchiveDialogueOpen(null)} + > +
{ + event.preventDefault(); + handleFileArchive(archiveDialogueOpen); + }} + className="mx-2 my-4 flex w-full flex-col" + > +
+ + State the reason for archiving{" "} + {archiveDialogueOpen?.name} file? + + } + rows={6} + required + placeholder="Type the reason..." + value={archiveReason} + onChange={(e) => setArchiveReason(e.value)} + error={archiveReasonError} + /> +
+
+ setArchiveDialogueOpen(null)} /> + +
+
+ + +
+ +
+
+

File Details

+ This file is archived. Once a file is archived it cannot be + unarchived. +
+
+ } + onClose={() => setArchiveDialogueOpen(null)} + > +
+
+
+ {archiveDialogueOpen?.name} file is + archived. +
+
+ Reason: {archiveDialogueOpen?.archive_reason} +
+
+ Archived by: {archiveDialogueOpen?.archived_by?.username} +
+
+ Time of Archive: + {formatDateTime(archiveDialogueOpen?.archived_datetime)} +
+
+
+ setArchiveDialogueOpen(null)} /> +
+
+ + + ); + + return { + viewFile, + archiveFile, + Dialogues, + }; +} diff --git a/src/Utils/useFileUpload.tsx b/src/Utils/useFileUpload.tsx new file mode 100644 index 00000000000..e28e8c9b1b5 --- /dev/null +++ b/src/Utils/useFileUpload.tsx @@ -0,0 +1,448 @@ +import { ChangeEvent, useCallback, useRef, useState } from "react"; +import { + CreateFileResponse, + FileCategory, + FileUploadModel, +} from "../Components/Patient/models"; +import DialogModal from "../Components/Common/Dialog"; +import CareIcon, { IconName } from "../CAREUI/icons/CareIcon"; +import Webcam from "react-webcam"; +import ButtonV2, { Submit } from "../Components/Common/components/ButtonV2"; +import { t } from "i18next"; +import useWindowDimensions from "../Common/hooks/useWindowDimensions"; +import { classNames } from "./utils"; +import request from "./request/request"; +import routes from "../Redux/api"; +import uploadFile from "./request/uploadFile"; +import * as Notification from "./Notifications.js"; +import imageCompression from "browser-image-compression"; + +export type FileUploadOptions = { + type: string; + category?: FileCategory; + onUpload?: (file: FileUploadModel) => void; +} & ( + | { + allowAllExtensions?: boolean; + } + | { + allowedExtensions?: string[]; + } +); + +export type FileUploadButtonProps = { + icon?: IconName; + content?: string; + className?: string; +}; + +export type FileUploadReturn = { + progress: null | number; + error: null | string; + handleCameraCapture: () => void; + handleAudioCapture: () => void; + handleFileUpload: (associating_id: string) => Promise; + Dialogues: JSX.Element; + UploadButton: (_: FileUploadButtonProps) => JSX.Element; + fileName: string; + file: File | null; + setFileName: (name: string) => void; + clearFile: () => void; +}; + +const videoConstraints = { + width: { ideal: 4096 }, + height: { ideal: 2160 }, + facingMode: "user", +}; + +// Array of image extensions +const ExtImage: string[] = [ + "jpeg", + "jpg", + "png", + "gif", + "svg", + "bmp", + "webp", + "jfif", +]; + +export default function useFileUpload( + options: FileUploadOptions, +): FileUploadReturn { + const { type, onUpload, category = "UNSPECIFIED" } = options; + + const [uploadFileName, setUploadFileName] = useState(""); + const [error, setError] = useState(null); + const [progress, setProgress] = useState(null); + const [cameraModalOpen, setCameraModalOpen] = useState(false); + const [cameraFacingFront, setCameraFacingFront] = useState(true); + const webRef = useRef(null); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + + const handleSwitchCamera = useCallback(() => { + setCameraFacingFront((prevState) => !prevState); + }, []); + + const { width } = useWindowDimensions(); + const LaptopScreenBreakpoint = 640; + const isLaptopScreen = width >= LaptopScreenBreakpoint ? true : false; + + const captureImage = () => { + setPreviewImage(webRef.current.getScreenshot()); + const canvas = webRef.current.getCanvas(); + canvas?.toBlob((blob: Blob) => { + const extension = blob.type.split("/").pop(); + const myFile = new File([blob], `capture.${extension}`, { + type: blob.type, + }); + setUploadFileName(uploadFileName || ""); + setFile(myFile); + }); + }; + + const onFileChange = (e: ChangeEvent): any => { + if (!e.target.files?.length) { + return; + } + const f = e.target.files[0]; + const fileName = f.name; + setFile(e.target.files[0]); + setUploadFileName( + uploadFileName || + fileName.substring(0, fileName.lastIndexOf(".")) || + fileName, + ); + + const ext: string = fileName.split(".")[1]; + + if (ExtImage.includes(ext)) { + const options = { + initialQuality: 0.6, + alwaysKeepResolution: true, + }; + imageCompression(f, options).then((compressedFile: File) => { + setFile(compressedFile); + }); + return; + } + setFile(f); + }; + + const validateFileUpload = () => { + const filenameLength = uploadFileName.trim().length; + const f = file; + if (f === undefined || f === null) { + setError("Please choose a file to upload"); + return false; + } + if (filenameLength === 0) { + setError("Please give a name !!"); + return false; + } + if (f.size > 10e7) { + setError("Maximum size of files is 100 MB"); + return false; + } + return true; + }; + const markUploadComplete = ( + data: CreateFileResponse, + associatingId: string, + ) => { + return request(routes.editUpload, { + body: { upload_completed: true }, + pathParams: { + id: data.id, + fileType: type, + associatingId, + }, + }); + }; + + const uploadfile = async (data: CreateFileResponse) => { + const url = data.signed_url; + const internal_name = data.internal_name; + const f = file; + if (!f) return; + const newFile = new File([f], `${internal_name}`); + console.log("filetype: ", newFile.type); + return new Promise((resolve, reject) => { + uploadFile( + url, + newFile, + "PUT", + { "Content-Type": file?.type }, + (xhr: XMLHttpRequest) => { + if (xhr.status >= 200 && xhr.status < 300) { + setProgress(null); + setFile(null); + setUploadFileName(""); + Notification.Success({ + msg: "File Uploaded Successfully", + }); + setError(null); + onUpload && onUpload(data); + resolve(); + } else { + Notification.Error({ + msg: "Error Uploading File: " + xhr.statusText, + }); + setProgress(null); + reject(); + } + }, + setProgress as any, + () => { + Notification.Error({ + msg: "Error Uploading File: Network Error", + }); + setProgress(null); + reject(); + }, + ); + }); + }; + + const handleUpload = async (associating_id: string) => { + if (!validateFileUpload()) return; + const f = file; + + const filename = uploadFileName === "" && f ? f.name : uploadFileName; + const name = f?.name; + setProgress(0); + + const { data } = await request(routes.createUpload, { + body: { + original_name: name ?? "", + file_type: type, + name: filename, + associating_id, + file_category: category, + mime_type: f?.type ?? "", + }, + }); + + if (data) { + await uploadfile(data); + await markUploadComplete(data, associating_id); + } + }; + + const cameraFacingMode = cameraFacingFront + ? "user" + : { exact: "environment" }; + + const Dialogues = ( + +
+ +
+
+

Camera

+
+
+ } + className="max-w-2xl" + onClose={() => setCameraModalOpen(false)} + > +
+ {!previewImage ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ + {/* buttons for mobile screens */} +
+
+ {!previewImage ? ( + + {t("switch")} + + ) : ( + <> + )} +
+
+ {!previewImage ? ( + <> +
+ { + captureImage(); + }} + className="m-2" + > + {t("capture")} + +
+ + ) : ( + <> +
+ { + setPreviewImage(null); + }} + className="m-2" + > + {t("retake")} + + { + setPreviewImage(null); + setCameraModalOpen(false); + }} + className="m-2" + > + {t("submit")} + +
+ + )} +
+
+ { + setPreviewImage(null); + setCameraModalOpen(false); + }} + className="m-2" + > + {t("close")} + +
+
+ {/* buttons for laptop screens */} +