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 5fbff1b48cb..4eb3b51d012 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -318,42 +318,6 @@ export const REVIEW_AT_CHOICES: Array<OptionsType> = [ { 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" }, @@ -1404,3 +1368,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 0bb40dffb98..715c326c35d 100644 --- a/src/Components/CameraFeed/AssetBedSelect.tsx +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -16,6 +16,7 @@ export default function CameraPresetSelect(props: Props) { return ( <> <div className="hidden gap-2 whitespace-nowrap pr-2 md:flex"> + {/* Desktop View */} {props.options .slice(0, props.options.length > 5 ? 4 : 5) .map((option) => ( @@ -31,20 +32,19 @@ export default function CameraPresetSelect(props: Props) { {label(option)} </button> ))} - {/* Desktop View */} {props.options.length > 5 && ( - <ShowMoreDropdown {...props} options={props.options.slice(4)} /> + <CameraPresetDropdown {...props} options={props.options.slice(4)} /> )} </div> <div className="md:hidden"> {/* Mobile View */} - <ShowMoreDropdown {...props} /> + <CameraPresetDropdown {...props} /> </div> </> ); } -const ShowMoreDropdown = (props: Props) => { +export const CameraPresetDropdown = (props: Props) => { const selected = props.value; const options = props.options.filter(({ meta }) => meta.type !== "boundary"); @@ -54,9 +54,14 @@ const ShowMoreDropdown = (props: Props) => { return ( <Listbox value={selected} onChange={props.onChange}> <div className="relative flex-1"> - <Listbox.Button className="relative w-full cursor-default pr-6 text-right text-xs text-white focus:outline-none disabled:cursor-not-allowed disabled:bg-transparent disabled:text-zinc-700 sm:text-sm md:pl-2"> - <span className="block truncate"> - {selected ? label(selected) : "No Preset"} + <Listbox.Button className="relative w-full cursor-default pr-6 text-left text-xs text-white focus:outline-none disabled:cursor-not-allowed disabled:bg-transparent disabled:text-zinc-700 sm:text-sm md:pl-2"> + <span + className={classNames( + "block truncate", + !selected && "text-gray-500", + )} + > + {selected ? label(selected) : "Select preset"} </span> <span className="pointer-events-none absolute inset-y-0 right-0 mt-1 flex items-center"> <CareIcon icon="l-angle-down" className="text-lg text-zinc-500" /> diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index f40aed384c8..81b526363b9 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -10,6 +10,7 @@ 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 { @@ -87,7 +88,6 @@ export default function CameraFeed(props: Props) { setState("loading"); initializeStream(); }; - return ( <Fullscreen fullscreen={isFullscreen} onExit={() => setFullscreen(false)}> <div @@ -120,6 +120,7 @@ export default function CameraFeed(props: Props) { <div className="group relative aspect-video"> {/* Notifications */} <FeedAlert state={state} /> + {player.status === "playing" && <FeedWatermark />} {/* No Feed informations */} {state === "host_unreachable" && ( @@ -150,6 +151,7 @@ export default function CameraFeed(props: Props) { url={streamUrl} ref={playerRef.current as LegacyRef<ReactPlayer>} controls={false} + pip={false} playsinline playing muted @@ -167,10 +169,12 @@ export default function CameraFeed(props: Props) { </div> ) : ( <video + onContextMenu={(e) => e.preventDefault()} className="absolute inset-0 w-full" id="mse-video" autoPlay muted + disablePictureInPicture playsInline onPlay={player.onPlayCB} onEnded={() => player.setStatus("stop")} diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx index 4c205c0e9c6..e3fc2ab2129 100644 --- a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx +++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx @@ -1,10 +1,10 @@ import { useState } from "react"; import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; import CameraFeed from "./CameraFeed"; -import AssetBedSelect from "./AssetBedSelect"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import useSlug from "../../Common/hooks/useSlug"; +import { CameraPresetDropdown } from "./AssetBedSelect"; interface Props { asset: AssetData; @@ -29,7 +29,7 @@ export default function LocationFeedTile(props: Props) { {loading ? ( <span>loading presets...</span> ) : ( - <AssetBedSelect + <CameraPresetDropdown options={data?.results ?? []} value={preset} onChange={setPreset} diff --git a/src/Components/CameraFeed/FeedWatermark.tsx b/src/Components/CameraFeed/FeedWatermark.tsx new file mode 100644 index 00000000000..e80c756ba3f --- /dev/null +++ b/src/Components/CameraFeed/FeedWatermark.tsx @@ -0,0 +1,55 @@ +import { useEffect, useRef } from "react"; +import useAuthUser from "../../Common/hooks/useAuthUser"; + +export default function FeedWatermark() { + const me = useAuthUser(); + return ( + <> + <Watermark className="left-1/3 top-1/3 -translate-x-1/2 -translate-y-1/2"> + {me.username} + </Watermark> + <Watermark className="right-1/3 top-1/3 -translate-y-1/2 translate-x-1/2"> + {me.username} + </Watermark> + <Watermark className="bottom-1/3 left-1/3 -translate-x-1/2 translate-y-1/2"> + {me.username} + </Watermark> + <Watermark className="bottom-1/3 right-1/3 translate-x-1/2 translate-y-1/2"> + {me.username} + </Watermark> + </> + ); +} + +const Watermark = (props: { children: string; className: string }) => { + const ref = useRef<HTMLSpanElement>(null); + const parentRef = useRef<HTMLElement | null>(null); + + // This adds the element back if the element was removed from the DOM + useEffect(() => { + parentRef.current = ref.current?.parentElement || null; + let animationFrameId: number; + + const checkWatermark = () => { + const watermark = ref.current; + const parent = parentRef.current; + if (watermark && parent && !parent.contains(watermark)) { + parent.appendChild(watermark); + } + animationFrameId = requestAnimationFrame(checkWatermark); + }; + + animationFrameId = requestAnimationFrame(checkWatermark); + + return () => cancelAnimationFrame(animationFrameId); + }, []); + + return ( + <span + ref={ref} + className={`absolute z-10 text-2xl font-bold text-white/30 ${props.className}`} + > + {props.children} + </span> + ); +}; diff --git a/src/Components/Common/DialogModal.res b/src/Components/Common/DialogModal.res new file mode 100644 index 00000000000..ae03ad8139c --- /dev/null +++ b/src/Components/Common/DialogModal.res @@ -0,0 +1,20 @@ +type reactClass +module DialogModal = { + @module("./Dialog.tsx") @react.component + external make: ( + ~title: React.element, + ~show: bool, + ~onClose: unit => unit, + ~className: string, + ~children: React.element, + ) => React.element = "default" +} + +@react.component +let make = ( + ~title: React.element, + ~show: bool, + ~onClose: unit => unit, + ~className: string, + ~children: React.element, +) => <DialogModal title show onClose className> {children} </DialogModal> diff --git a/src/Components/Common/SkillSelect.tsx b/src/Components/Common/SkillSelect.tsx index 2d50a92d063..117df4f536d 100644 --- a/src/Components/Common/SkillSelect.tsx +++ b/src/Components/Common/SkillSelect.tsx @@ -44,7 +44,10 @@ export const SkillSelect = (props: SkillSelectProps) => { const { data } = await request(routes.getAllSkills, { query }); return data?.results.filter( - (skill) => !userSkills?.some((userSkill) => userSkill.id === skill.id), + (skill) => + !userSkills?.some( + (userSkill) => userSkill.skill_object.id === skill.id, + ), ); }, [searchAll, userSkills], diff --git a/src/Components/Common/SymptomsSelect.tsx b/src/Components/Common/SymptomsSelect.tsx deleted file mode 100644 index cdca3fe60dc..00000000000 --- a/src/Components/Common/SymptomsSelect.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import CareIcon from "../../CAREUI/icons/CareIcon"; -import { SYMPTOM_CHOICES } from "../../Common/constants"; -import { AutocompleteMutliSelect } from "../Form/FormFields/AutocompleteMultiselect"; -import FormField from "../Form/FormFields/FormField"; -import { - FormFieldBaseProps, - useFormFieldPropsResolver, -} from "../Form/FormFields/Utils"; - -const ASYMPTOMATIC_ID = 1; - -/** - * A `FormField` component to select symptoms. - * - * - If "Asymptomatic" is selected, every other selections are unselected. - * - If any non "Asymptomatic" value is selected, ensures "Asymptomatic" is - * unselected. - * - For other scenarios, this simply works like a `MultiSelect`. - */ -export const SymptomsSelect = (props: FormFieldBaseProps<number[]>) => { - const field = useFormFieldPropsResolver(props); - - const updateSelection = (value: number[]) => { - // Skip the complexities if no initial value was present - if (!props.value?.length) return field.handleChange(value); - - const initialValue = props.value || []; - - if (initialValue.includes(ASYMPTOMATIC_ID) && value.length > 1) { - // If asym. already selected, and new selections have more than one value - const asymptomaticIndex = value.indexOf(1); - if (asymptomaticIndex > -1) { - // unselect asym. - value.splice(asymptomaticIndex, 1); - return field.handleChange(value); - } - } - - if (!initialValue.includes(ASYMPTOMATIC_ID) && value.includes(1)) { - // If new selections have asym., unselect everything else - return field.handleChange([ASYMPTOMATIC_ID]); - } - - field.handleChange(value); - }; - - const getDescription = ({ id }: { id: number }) => { - const value = props.value || []; - if (!value.length) return; - - if (value.includes(ASYMPTOMATIC_ID) && id !== ASYMPTOMATIC_ID) - return ( - <div className="items-center"> - <CareIcon icon="l-exclamation-triangle" className="mr-2" /> - <span> - also unselects <b className="font-medium">Asymptomatic</b> - </span> - </div> - ); - - if (!value.includes(ASYMPTOMATIC_ID) && id === ASYMPTOMATIC_ID) - return ( - <span> - <CareIcon icon="l-exclamation-triangle" className="mr-2" /> - {`also unselects the other ${value.length} option(s)`} - </span> - ); - }; - - return ( - <FormField field={field}> - <AutocompleteMutliSelect - id={field.id} - options={SYMPTOM_CHOICES} - disabled={props.disabled} - placeholder="Select symptoms" - optionLabel={(option) => option.text} - optionValue={(option) => option.id} - value={props.value || []} - optionDescription={getDescription} - onChange={updateSelection} - /> - </FormField> - ); -}; diff --git a/src/Components/CriticalCareRecording/CriticalCare__API.tsx b/src/Components/CriticalCareRecording/CriticalCare__API.tsx index 108acfda05e..7bddbf9eaee 100644 --- a/src/Components/CriticalCareRecording/CriticalCare__API.tsx +++ b/src/Components/CriticalCareRecording/CriticalCare__API.tsx @@ -1,4 +1,6 @@ import { fireRequestV2 } from "../../Redux/fireRequest"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; export const loadDailyRound = ( consultationId: string, @@ -24,3 +26,18 @@ export const updateDailyRound = ( id, }); }; + +export const getAsset = ( + consultationId: string, + setAsset: React.Dispatch<React.SetStateAction<number>>, +) => { + request(routes.listConsultationBeds, { + query: { consultation: consultationId, limit: 1 }, + }).then(({ data }) => { + // here its fetching the ventilator type assets + const assets = data?.results[0].assets_objects?.filter( + (asset) => asset.asset_class == "VENTILATOR", + ); + setAsset(assets?.length || 0); + }); +}; diff --git a/src/Components/CriticalCareRecording/Recording/CriticalCare__Recording.res b/src/Components/CriticalCareRecording/Recording/CriticalCare__Recording.res index 2903ab3e406..e8bfb91e21c 100644 --- a/src/Components/CriticalCareRecording/Recording/CriticalCare__Recording.res +++ b/src/Components/CriticalCareRecording/Recording/CriticalCare__Recording.res @@ -164,6 +164,8 @@ let make = (~id, ~facilityId, ~patientId, ~consultationId, ~dailyRound) => { updateCB={updateDailyRound(send, VentilatorParametersEditor)} id consultationId + patientId + facilityId /> | ArterialBloodGasAnalysisEditor => <CriticalCare__ABGAnalysisEditor diff --git a/src/Components/CriticalCareRecording/VentilatorParametersEditor/CriticalCare__VentilatorParametersEditor.res b/src/Components/CriticalCareRecording/VentilatorParametersEditor/CriticalCare__VentilatorParametersEditor.res index 0e928e57f38..c06780aad75 100644 --- a/src/Components/CriticalCareRecording/VentilatorParametersEditor/CriticalCare__VentilatorParametersEditor.res +++ b/src/Components/CriticalCareRecording/VentilatorParametersEditor/CriticalCare__VentilatorParametersEditor.res @@ -5,6 +5,9 @@ open CriticalCare__Types external updateDailyRound: (string, string, Js.Json.t, _ => unit, _ => unit) => unit = "updateDailyRound" +@module("../CriticalCare__API") +external getAsset: (string, (int => int) => unit) => option<unit => unit> = "getAsset" + open VentilatorParameters let string_of_int = data => Belt.Option.mapWithDefault(data, "", Js.Int.toString) @@ -14,19 +17,19 @@ let reducer = (state: VentilatorParameters.state, action: VentilatorParameters.a switch action { | SetBilateralAirEntry(bilateral_air_entry) => { ...state, - bilateral_air_entry: bilateral_air_entry, + bilateral_air_entry, } | SetETCO2(etco2) => { ...state, - etco2: etco2, + etco2, } | SetVentilatorInterface(ventilator_interface) => { ...state, - ventilator_interface: ventilator_interface, + ventilator_interface, } | SetVentilatorMode(ventilator_mode) => { ...state, - ventilator_mode: ventilator_mode, + ventilator_mode, } | SetOxygenModality(oxygen_modality) => { @@ -59,7 +62,7 @@ let reducer = (state: VentilatorParameters.state, action: VentilatorParameters.a } | SetOxygenModalityOxygenRate(ventilator_oxygen_modality_oxygen_rate) => { ...state, - ventilator_oxygen_modality_oxygen_rate: ventilator_oxygen_modality_oxygen_rate, + ventilator_oxygen_modality_oxygen_rate, } | SetOxygenModalityFlowRate(oxygen_modality_flow_rate) => { ...state, @@ -204,8 +207,22 @@ let initialState: VentilatorParameters.t => VentilatorParameters.state = ventila } @react.component -let make = (~ventilatorParameters: VentilatorParameters.t, ~id, ~consultationId, ~updateCB) => { +let make = ( + ~ventilatorParameters: VentilatorParameters.t, + ~id, + ~consultationId, + ~updateCB, + ~facilityId, + ~patientId, +) => { let (state, send) = React.useReducer(reducer, initialState(ventilatorParameters)) + let (isOpen, setIsOpen) = React.useState(() => false) + let toggleOpen = () => setIsOpen(prevState => !prevState) + let (asset, setAsset) = React.useState(() => 0) + + React.useEffect1(() => { + getAsset(consultationId, setAsset) + }, [isOpen]) let editor = switch state.ventilator_interface { | INVASIVE => <CriticalCare__VentilatorParametersEditor__Invasive state send /> @@ -216,7 +233,7 @@ let make = (~ventilatorParameters: VentilatorParameters.t, ~id, ~consultationId, <div> <CriticalCare__PageTitle title="Respiratory Support" /> - <div> + <div> <div className="px-5 my-10"> <div className=" text-xl font-bold my-2"> {str("Bilateral Air Entry")} </div> <div className="flex md:flex-row flex-col md:space-y-0 space-y-2 space-x-0 md:space-x-4"> @@ -225,19 +242,18 @@ let make = (~ventilatorParameters: VentilatorParameters.t, ~id, ~consultationId, id="bilateral-air-entry-yes" label="Yes" checked={switch state.bilateral_air_entry { - | Some(bae) => bae - | None => false + | Some(bae) => bae + | None => false }} onChange={_ => send(SetBilateralAirEntry(Some(true)))} /> - <Radio key="bilateral-air-entry-no" id="bilateral-air-entry-no" label="No" checked={switch state.bilateral_air_entry { - | Some(bae) => !bae - | None => false + | Some(bae) => !bae + | None => false }} onChange={_ => send(SetBilateralAirEntry(Some(false)))} /> @@ -255,7 +271,6 @@ let make = (~ventilatorParameters: VentilatorParameters.t, ~id, ~consultationId, hasError={ValidationUtils.isInputInRangeInt(0, 200, state.etco2)} /> </div> - <div className="py-6"> <div className="mb-6"> <h4> {str("Respiratory Support")} </h4> @@ -282,10 +297,43 @@ let make = (~ventilatorParameters: VentilatorParameters.t, ~id, ~consultationId, </div> <button disabled={state.saving} - onClick={_ => saveData(id, consultationId, state, send, updateCB)} + onClick={_ => { + // here checking if any asset linked or not before proceeding + if ( + asset == 0 && + (state.ventilator_interface != + CriticalCare__VentilatorParameters.decodeVentilatorInterfaceType( + ventilatorInterfaceOptions[0].value, + ) || + switch state.bilateral_air_entry { + | Some(true) => true + | _ => false + } || + switch (state.etco2) { + | Some(intValue) => true + | None => false + }) + ) { + toggleOpen() + } else { + saveData(id, consultationId, state, send, updateCB) + } + }} className="btn btn-primary btn-large w-full"> {str("Update Details")} </button> + <DialogModal + title={str("Link an asset to proceed")} + show={isOpen} + onClose={_ => toggleOpen()} + className="md:max-w-3xl"> + <Beds + facilityId={facilityId} + patientId={patientId} + consultationId={consultationId} + setState={_ => toggleOpen()} + /> + </DialogModal> </div> </div> } diff --git a/src/Components/Diagnosis/utils.ts b/src/Components/Diagnosis/utils.ts index c53f9b81bc1..1cac3cecbca 100644 --- a/src/Components/Diagnosis/utils.ts +++ b/src/Components/Diagnosis/utils.ts @@ -2,8 +2,6 @@ import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import { ICD11DiagnosisModel } from "./types"; -// TODO: cache ICD11 responses and hit the cache if present instead of making an API call. - export const getDiagnosisById = async (id: ICD11DiagnosisModel["id"]) => { return (await request(routes.getICD11Diagnosis, { pathParams: { id } })).data; }; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx index e1e72c2f936..27af4bb6480 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationMedicinesTab.tsx @@ -1,6 +1,7 @@ import { ConsultationTabProps } from "./index"; import PageTitle from "../../Common/PageHeadTitle"; import MedicineAdministrationSheet from "../../Medicine/MedicineAdministrationSheet"; +import { MedicinePrescriptionSummary } from "../../Medicine/MedicinePrescriptionSummary"; export const ConsultationMedicinesTab = (props: ConsultationTabProps) => { return ( @@ -15,6 +16,7 @@ export const ConsultationMedicinesTab = (props: ConsultationTabProps) => { is_prn={true} readonly={!!props.consultationData.discharge_date} /> + <MedicinePrescriptionSummary consultation={props.consultationId} /> </div> ); }; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index a7e52257e28..be2751f0033 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -5,12 +5,7 @@ import { BedModel } from "../models"; import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor"; import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor"; import useVitalsAspectRatioConfig from "../../VitalsMonitor/useVitalsAspectRatioConfig"; -import { - CONSENT_PATIENT_CODE_STATUS_CHOICES, - CONSENT_TYPE_CHOICES, - DISCHARGE_REASONS, - SYMPTOM_CHOICES, -} from "../../../Common/constants"; +import { DISCHARGE_REASONS } from "../../../Common/constants"; import PrescriptionsTable from "../../Medicine/PrescriptionsTable"; import Chip from "../../../CAREUI/display/Chip"; import { @@ -25,10 +20,10 @@ import DailyRoundsList from "../Consultations/DailyRoundsList"; import EventsList from "./Events/EventsList"; import SwitchTabs from "../../Common/components/SwitchTabs"; import { getVitalsMonitorSocketUrl } from "../../VitalsMonitor/utils"; -import { FileUpload } from "../../Patient/FileUpload"; import useQuery from "../../../Utils/request/useQuery"; import routes from "../../../Redux/api"; import CareIcon from "../../../CAREUI/icons/CareIcon"; +import EncounterSymptomsCard from "../../Symptoms/SymptomsCard"; const PageTitle = lazy(() => import("../../Common/PageTitle")); @@ -369,91 +364,10 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { )} </div> )} - {props.consultationData.symptoms_text && ( - <div className="overflow-hidden rounded-lg bg-white shadow"> - <div className="px-4 py-5 sm:p-6"> - <h3 className="mb-4 text-lg font-semibold leading-relaxed text-gray-900"> - Symptoms - </h3> - <div className=""> - <div className="text-sm font-semibold uppercase"> - Last Daily Update - </div> - {props.consultationData.last_daily_round - ?.additional_symptoms && ( - <> - <div className="my-4 flex flex-wrap items-center gap-2"> - {props.consultationData.last_daily_round?.additional_symptoms.map( - (symptom: any, index: number) => ( - <Chip - key={index} - text={ - SYMPTOM_CHOICES.find( - (choice) => choice.id === symptom, - )?.text ?? "Err. Unknown" - } - size="small" - /> - ), - )} - </div> - {props.consultationData.last_daily_round - ?.other_symptoms && ( - <div className="capitalize"> - <div className="text-xs font-semibold"> - Other Symptoms: - </div> - { - props.consultationData.last_daily_round - ?.other_symptoms - } - </div> - )} - <span className="text-xs font-semibold leading-relaxed text-gray-800"> - from{" "} - {formatDate( - props.consultationData.last_daily_round.taken_at, - )} - </span> - </> - )} - <hr className="my-4 border border-gray-300" /> - <div className="text-sm font-semibold uppercase"> - Consultation Update - </div> - <div className="my-4 flex flex-wrap items-center gap-2"> - {props.consultationData.symptoms?.map( - (symptom, index) => ( - <Chip - key={index} - text={ - SYMPTOM_CHOICES.find( - (choice) => choice.id === symptom, - )?.text ?? "Err. Unknown" - } - size="small" - /> - ), - )} - </div> - {props.consultationData.other_symptoms && ( - <div className="capitalize"> - <div className="text-xs font-semibold"> - Other Symptoms: - </div> - {props.consultationData.other_symptoms} - </div> - )} - <span className="text-xs font-semibold leading-relaxed text-gray-800"> - from{" "} - {props.consultationData.symptoms_onset_date - ? formatDate(props.consultationData.symptoms_onset_date) - : "--/--/----"} - </span> - </div> - </div> - </div> - )} + + <div className="rounded-lg bg-white px-4 py-5 shadow sm:p-6 md:col-span-2"> + <EncounterSymptomsCard /> + </div> {props.consultationData.history_of_present_illness && ( <div className="overflow-hidden rounded-lg bg-white shadow"> @@ -723,47 +637,6 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { </div> </div> </div> - {( - props.consultationData.consent_records?.filter( - (record) => record.deleted !== true, - ) || [] - ).length > 0 && ( - <> - <div className="col-span-1 overflow-hidden rounded-lg bg-white p-4 shadow md:col-span-2"> - <h3 className="text-lg font-semibold leading-relaxed text-gray-900"> - Consent Records - </h3> - {props.consultationData.consent_records - ?.filter((record) => record.deleted !== true) - ?.map((record, i) => ( - <div className="mt-4 border-b" key={i}> - <div className="font-bold"> - { - CONSENT_TYPE_CHOICES.find( - (c) => c.id === record.type, - )?.text - }{" "} - {record.patient_code_status && - `( ${ - CONSENT_PATIENT_CODE_STATUS_CHOICES.find( - (c) => c.id === record.patient_code_status, - )?.text - } )`} - </div> - <FileUpload - changePageMetadata={false} - type="CONSENT_RECORD" - hideBack - unspecified - className="w-full" - consentId={record.id} - hideUpload - /> - </div> - ))} - </div> - </> - )} </div> </div> <div className="w-full pl-0 md:pl-4 xl:w-1/3"> 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 8a9a19db2bc..d362abe1ecc 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -23,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, @@ -32,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"; @@ -61,6 +59,12 @@ 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")); @@ -68,9 +72,7 @@ const PageTitle = lazy(() => import("../Common/PageTitle")); type BooleanStrings = "true" | "false"; type FormDetails = { - symptoms: number[]; - other_symptoms: string; - symptoms_onset_date?: Date; + is_asymptomatic: boolean; suggestion: ConsultationSuggestionValue; route_to_facility?: RouteToFacility; patient: string; @@ -91,6 +93,8 @@ type FormDetails = { treating_physician_object: UserModel | null; create_diagnoses: CreateDiagnosis[]; diagnoses: ConsultationDiagnosis[]; + symptoms: EncounterSymptom[]; + create_symptoms: Writable<EncounterSymptom>[]; is_kasp: BooleanStrings; kasp_enabled_date: null; examination_details: string; @@ -119,9 +123,9 @@ type FormDetails = { }; const initForm: FormDetails = { + is_asymptomatic: false, + create_symptoms: [], symptoms: [], - other_symptoms: "", - symptoms_onset_date: undefined, suggestion: "A", route_to_facility: undefined, patient: "", @@ -315,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<unknown> = (event) => { if (event.name === "suggestion" && event.value === "DD") { dispatch({ @@ -329,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( @@ -372,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 && @@ -435,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"; @@ -469,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"; @@ -501,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"; @@ -679,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", @@ -698,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, @@ -910,11 +903,16 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { /> <div className="top-0 mt-5 flex grow-0 sm:mx-12"> - <div className="fixed hidden h-full w-72 flex-col xl:flex"> + <div className="fixed hidden w-72 flex-col xl:flex"> {Object.keys(sections).map((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 ( @@ -1020,41 +1018,48 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { </div> )} - <div className="col-span-6" ref={fieldRef["symptoms"]}> - <SymptomsSelect - required - label="Symptoms" - {...field("symptoms")} - /> - </div> - {isOtherSymptomsSelected && ( - <div - className="col-span-6" - ref={fieldRef["other_symptoms"]} - > - <TextAreaFormField - {...field("other_symptoms")} - label="Other symptom details" - required - placeholder="Enter details of other symptoms here" - /> - </div> - )} + <div + className="col-span-6" + id="symptoms" + ref={fieldRef["create_symptoms"]} + > + <div className="mb-4 flex flex-col gap-4"> + <FieldLabel required>Symptoms</FieldLabel> - {hasSymptoms && ( - <div - className="col-span-6" - ref={fieldRef["symptoms_onset_date"]} - > - <DateFormField - {...field("symptoms_onset_date")} - disableFuture - required - label="Date of onset of the symptoms" - position="LEFT" - /> + {!isUpdate && ( + <CheckBoxFormField + className="-mt-2 ml-1" + {...field("is_asymptomatic")} + value={state.form.is_asymptomatic} + label="Is the patient Asymptomatic?" + errorClassName="hidden" + /> + )} + + <div + className={classNames( + state.form.is_asymptomatic && + "pointer-events-none opacity-50", + )} + > + {isUpdate ? ( + <EncounterSymptomsBuilder /> + ) : ( + <CreateSymptomsBuilder + value={state.form.create_symptoms} + onChange={(symptoms) => { + handleFormFieldChange({ + name: "create_symptoms", + value: symptoms, + }); + }} + /> + )} + <FieldErrorText error={state.errors.create_symptoms} /> + </div> </div> - )} + </div> + <div className="col-span-6" ref={fieldRef["history_of_present_illness"]} @@ -1098,7 +1103,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { placeholder="Weight" trailingPadding=" " trailing={ - <p className="mr-8 text-sm text-gray-700"> + <p className="absolute right-10 whitespace-nowrap text-sm text-gray-700"> Weight (kg) </p> } @@ -1111,7 +1116,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { placeholder="Height" trailingPadding=" " trailing={ - <p className="mr-8 text-sm text-gray-700"> + <p className="absolute right-10 whitespace-nowrap text-sm text-gray-700"> Height (cm) </p> } @@ -1265,7 +1270,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { </div> )} - {["A", "DC"].includes(state.form.suggestion) && !isUpdate && ( + {state.form.suggestion === "A" && !isUpdate && ( <div className="col-span-6 mb-6" ref={fieldRef["bed"]}> <FieldLabel>Bed</FieldLabel> <BedSelect @@ -1481,7 +1486,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { </div> </div> </form> - {isUpdate && ( + {state.form.suggestion === "A" && isUpdate && ( <> <div className="mx-auto mt-4 max-w-4xl rounded bg-white px-11 py-8"> {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, +) => <Beds facilityId patientId consultationId setState /> 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 <p className="mr-8 text-xs text-gray-700">{unit}</p>; + return <p className="absolute right-10 text-xs text-gray-700">{unit}</p>; }; diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 252603ab89f..8e54f0896bc 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -13,6 +13,7 @@ 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"; export interface LocalBodyModel { @@ -124,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"]; @@ -143,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<number>; - 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 = <T, V>( 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/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 ( <div className="overflow-x-auto"> <table className="w-full whitespace-nowrap"> - <thead className="sticky top-0 z-20 bg-gray-50 text-xs font-medium text-black"> + <thead className="sticky top-0 z-10 bg-gray-50 text-xs font-medium text-black"> <tr> <th className="sticky left-0 z-20 bg-gray-50 py-3 pl-4 text-left"> <div className="flex justify-between gap-2"> @@ -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" > <CareIcon icon="l-angle-left-b" className="text-base" /> </ButtonV2> 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<string> = 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 ( + <div className="pt-6"> + <p className="text-xl font-bold text-gray-700">{t("summary")}</p> + <div className="flex flex-col gap-2 pt-4"> + {medicinesList && medicinesList.length > 0 ? ( + medicinesList?.map((med: MedibaseMedicine) => ( + <div + key={med.id} + className="flex cursor-pointer items-center justify-between rounded-lg border bg-white p-4 shadow hover:bg-gray-200" + > + <div>{med.name}</div> + <button + onClick={() => + setShowMedicineModal({ + open: true, + name: med.name, + medicineId: med.id, + }) + } + className="btn btn-default" + > + View + </button> + </div> + )) + ) : ( + <div className="rounded-lg border shadow"> + <div className="my-16 flex w-full flex-col items-center justify-center gap-4 text-gray-500"> + <h3 className="text-lg font-medium">{"No Medicine Summary"}</h3> + </div> + </div> + )} + </div> + + <DialogModal + title={ + <p> + {showMedicineModal.name}: {t("prescription_logs")} + </p> + } + show={showMedicineModal.open} + onClose={closeMedicineModal} + fixedWidth={false} + className="md:w-3/4" + > + <ConsultationMedicineLogs + consultationId={consultation} + medicineId={showMedicineModal.medicineId} + /> + </DialogModal> + </div> + ); +}; + +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 <Loading />; + } + + 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 ( + <div> + <Timeline + className="rounded-lg bg-white p-2 shadow" + name={data?.results[0].medicine_object?.name ?? ""} + > + {data?.results && + (() => { + const changesArray = calculateChanges(data?.results); + return changesArray.map((changes, index) => ( + <TimelineNode + key={changes.prescriptionId} + event={{ + type: "prescribed", + timestamp: changes.created_date, + by: changes.prescribed_by, + icon: "l-syringe", + }} + isLast={index === changesArray.length - 1} + > + <p>{changes?.changeMessage}</p> + </TimelineNode> + )); + })()} + </Timeline> + </div> + ); +} 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({ /> )} <div className="flex flex-col gap-3"> - {data?.results.map((obj, index) => ( - <PrescriptionDetailCard - key={index} - prescription={obj} - onDiscontinueClick={() => setShowDiscontinueFor(obj)} - onAdministerClick={() => setShowAdministerFor(obj)} - readonly={disabled} - /> - ))} + {data?.results + .sort(compareBy("discontinued")) + ?.map((obj) => ( + <PrescriptionDetailCard + key={obj.id} + prescription={obj} + collapsible + onDiscontinueClick={() => setShowDiscontinueFor(obj)} + onAdministerClick={() => setShowAdministerFor(obj)} + readonly={disabled} + /> + ))} </div> <AuthorizedForConsultationRelatedActions> <ButtonV2 diff --git a/src/Components/Medicine/PrescriptionDetailCard.tsx b/src/Components/Medicine/PrescriptionDetailCard.tsx index 630ab324662..e70acc9b87a 100644 --- a/src/Components/Medicine/PrescriptionDetailCard.tsx +++ b/src/Components/Medicine/PrescriptionDetailCard.tsx @@ -5,20 +5,29 @@ import ReadMore from "../Common/components/Readmore"; import ButtonV2 from "../Common/components/ButtonV2"; import { useTranslation } from "react-i18next"; import RecordMeta from "../../CAREUI/display/RecordMeta"; +import { useState } from "react"; import { AuthorizedForConsultationRelatedActions } from "../../CAREUI/misc/AuthorizedChild"; -export default function PrescriptionDetailCard({ - prescription, - ...props -}: { +interface Props { prescription: Prescription; readonly?: boolean; children?: React.ReactNode; onDiscontinueClick?: () => 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 ( <div className={classNames( @@ -27,9 +36,17 @@ export default function PrescriptionDetailCard({ ? "border-primary-500" : "border-spacing-2 border-dashed border-gray-500", prescription.discontinued && "bg-gray-200 opacity-80", + collapsible && "cursor-pointer hover:border-gray-900", )} > - <div className="flex flex-1 flex-col"> + <div + className="flex flex-1 flex-col" + onClick={() => { + if (collapsible) { + setIsCollapsed(!isCollapsed); + } + }} + > <div> <div className="flex items-center justify-between"> <div className="flex items-center gap-4"> @@ -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)}`} </h3> {prescription.discontinued && ( <span className="rounded-full bg-gray-700 px-2 py-1 text-xs font-semibold uppercase text-white"> @@ -62,7 +86,10 @@ export default function PrescriptionDetailCard({ <ButtonV2 id="administer-medicine" disabled={prescription.discontinued} - onClick={props.onAdministerClick} + onClick={(e) => { + 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?.(); + }} > <CareIcon icon="l-ban" className="text-base" /> {t("discontinue")} @@ -89,114 +119,114 @@ export default function PrescriptionDetailCard({ )} </div> </div> - - <div className="mt-4 grid grid-cols-10 items-center gap-2"> - <Detail - className={ - prescription.dosage_type === "TITRATED" - ? "col-span-10" - : "col-span-10 md:col-span-4" - } - label={t("medicine")} - > - {prescription.medicine_object?.name ?? prescription.medicine_old} - </Detail> - <Detail - className="col-span-10 break-all sm:col-span-4" - label={t("route")} - > - {prescription.route && - t("PRESCRIPTION_ROUTE_" + prescription.route)} - </Detail> - {prescription.dosage_type === "TITRATED" ? ( - <> - <Detail - className="col-span-5 sm:col-span-3" - label={t("start_dosage")} - > - {prescription.base_dosage} - </Detail> - <Detail - className="col-span-5 sm:col-span-3" - label={t("target_dosage")} - > - {prescription.target_dosage} - </Detail> - </> - ) : ( + {!isCollapsed && ( + <div className="mt-4 grid grid-cols-10 items-center gap-2"> <Detail - className="col-span-10 sm:col-span-6 md:col-span-2" - label={t("dosage")} + className={ + prescription.dosage_type === "TITRATED" + ? "col-span-10" + : "col-span-10 md:col-span-4" + } + label={t("medicine")} > - {prescription.base_dosage} + {prescription.medicine_object?.name ?? prescription.medicine_old} </Detail> - )} - - {prescription.dosage_type === "PRN" ? ( - <> + <Detail + className="col-span-10 break-all sm:col-span-4" + label={t("route")} + > + {prescription.route && + t("PRESCRIPTION_ROUTE_" + prescription.route)} + </Detail> + {prescription.dosage_type === "TITRATED" ? ( + <> + <Detail + className="col-span-5 sm:col-span-3" + label={t("start_dosage")} + > + {prescription.base_dosage} + </Detail> + <Detail + className="col-span-5 sm:col-span-3" + label={t("target_dosage")} + > + {prescription.target_dosage} + </Detail> + </> + ) : ( <Detail - className="col-span-10 md:col-span-6" - label={t("indicator")} + className="col-span-10 sm:col-span-6 md:col-span-2" + label={t("dosage")} > - {prescription.indicator} + {prescription.base_dosage} </Detail> + )} + + {prescription.dosage_type === "PRN" ? ( + <> + <Detail + className="col-span-10 md:col-span-6" + label={t("indicator")} + > + {prescription.indicator} + </Detail> + <Detail + className="col-span-10 md:col-span-2" + label={t("max_dosage_24_hrs")} + > + {prescription.max_dosage} + </Detail> + <Detail + className="col-span-10 md:col-span-2" + label={t("min_time_bw_doses")} + > + {prescription.min_hours_between_doses && + prescription.min_hours_between_doses + " hrs."} + </Detail> + </> + ) : ( + <> + <Detail className="col-span-5" label={t("frequency")}> + {prescription.frequency && + t( + "PRESCRIPTION_FREQUENCY_" + + prescription.frequency.toUpperCase(), + )} + </Detail> + <Detail className="col-span-5" label={t("days")}> + {prescription.days} + </Detail> + </> + )} + + {prescription.instruction_on_titration && ( <Detail - className="col-span-10 md:col-span-2" - label={t("max_dosage_24_hrs")} + className="col-span-10" + label={t("instruction_on_titration")} > - {prescription.max_dosage} + <ReadMore + text={prescription.instruction_on_titration} + minChars={120} + /> + </Detail> + )} + + {prescription.notes && ( + <Detail className="col-span-10" label={t("notes")}> + <ReadMore text={prescription.notes} minChars={120} /> </Detail> + )} + + {prescription.discontinued && ( <Detail - className="col-span-10 md:col-span-2" - label={t("min_time_bw_doses")} + className="col-span-10" + label={t("reason_for_discontinuation")} > - {prescription.min_hours_between_doses && - prescription.min_hours_between_doses + " hrs."} + {prescription.discontinued_reason} </Detail> - </> - ) : ( - <> - <Detail className="col-span-5" label={t("frequency")}> - {prescription.frequency && - t( - "PRESCRIPTION_FREQUENCY_" + - prescription.frequency.toUpperCase(), - )} - </Detail> - <Detail className="col-span-5" label={t("days")}> - {prescription.days} - </Detail> - </> - )} - - {prescription.instruction_on_titration && ( - <Detail - className="col-span-10" - label={t("instruction_on_titration")} - > - <ReadMore - text={prescription.instruction_on_titration} - minChars={120} - /> - </Detail> - )} - - {prescription.notes && ( - <Detail className="col-span-10" label={t("notes")}> - <ReadMore text={prescription.notes} minChars={120} /> - </Detail> - )} - - {prescription.discontinued && ( - <Detail - className="col-span-10" - label={t("reason_for_discontinuation")} - > - {prescription.discontinued_reason} - </Detail> - )} - </div> - + )} + </div> + )} <div className="flex flex-col gap-1 text-xs text-gray-600 md:mt-3 md:flex-row md:items-center"> <span className="flex gap-1 font-medium"> 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) => { <span className="font-semibold leading-relaxed">SpO2: </span> {dailyRoundListDetailsData.ventilator_spo2 ?? "-"} </div> - <div className="capitalize md:col-span-2"> - <span className="font-semibold leading-relaxed"> - Additional Symptoms:{" "} - </span> - {dailyRoundListDetailsData.additional_symptoms_text ?? "-"} - </div> <div className="capitalize md:col-span-2"> <span className="font-semibold leading-relaxed"> Admitted To *:{" "} @@ -103,12 +88,6 @@ export const DailyRoundListDetails = (props: any) => { </span> {dailyRoundListDetailsData.physical_examination_info ?? "-"} </div> - <div className="md:col-span-2"> - <span className="font-semibold leading-relaxed"> - Other Symptoms:{" "} - </span> - {dailyRoundListDetailsData.other_symptoms ?? "-"} - </div> <div className="md:col-span-2"> <span className="font-semibold leading-relaxed"> 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<any>( @@ -114,18 +128,19 @@ export const DailyRounds = (props: any) => { ...initForm, action: "", }); + const [diagnoses, setDiagnoses] = useState<ConsultationDiagnosis[]>(); 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<unknown>) => { - 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} /> - <SymptomsSelect - {...field("additional_symptoms")} - label="Symptoms" - className="md:col-span-2" - /> - {state.form.additional_symptoms?.includes(9) && ( - <div className="md:col-span-2"> - <TextAreaFormField - {...field("other_symptoms")} - required - label="Other Symptoms Details" - placeholder="Enter the other symptoms here" - /> - </div> - )} + <div className="pb-6 md:col-span-2"> + <FieldLabel>Symptoms</FieldLabel> + <EncounterSymptomsBuilder /> + </div> - <SelectFormField - {...field("action")} - label="Action" - options={TELEMEDICINE_ACTIONS} - optionLabel={(option) => option.desc} - optionValue={(option) => option.text} - value={prevAction} - onChange={(event) => { - handleFormFieldChange(event); - setPreviousAction(event.value); - }} - /> + {state.form.rounds_type !== "DOCTORS_LOG" && ( + <> + <SelectFormField + {...field("action")} + label="Action" + options={TELEMEDICINE_ACTIONS} + optionLabel={(option) => option.desc} + optionValue={(option) => option.text} + value={prevAction} + onChange={(event) => { + handleFormFieldChange(event); + setPreviousAction(event.value); + }} + /> - <SelectFormField - {...field("review_interval")} - label="Review After" - labelSuffix={getExpectedReviewTime()} - options={REVIEW_AT_CHOICES} - optionLabel={(option) => option.text} - optionValue={(option) => option.id} - value={prevReviewInterval} - onChange={(event) => { - handleFormFieldChange(event); - setPreviousReviewInterval(Number(event.value)); - }} - /> + <SelectFormField + {...field("review_interval")} + label="Review After" + labelSuffix={getExpectedReviewTime()} + options={REVIEW_AT_CHOICES} + optionLabel={(option) => 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, + ) && ( <> <h3 className="mb-6 md:col-span-2">Vitals</h3> @@ -599,6 +670,53 @@ export const DailyRounds = (props: any) => { /> </> )} + + {state.form.rounds_type === "DOCTORS_LOG" && ( + <> + <div className="flex flex-col gap-10 divide-y-2 divide-dashed divide-gray-600 border-t-2 border-dashed border-gray-600 pt-6 md:col-span-2"> + <div> + <h3 className="my-4 text-lg font-semibold"> + {t("investigations")} + </h3> + <InvestigationBuilder + investigations={state.form.investigations} + setInvestigations={(investigations) => { + handleFormFieldChange({ + name: "investigations", + value: investigations, + }); + }} + /> + <FieldErrorText error={state.errors.investigation} /> + </div> + <div> + <h3 className="mb-4 mt-8 text-lg font-semibold"> + {t("prescription_medications")} + </h3> + <PrescriptionBuilder /> + </div> + <div> + <h3 className="mb-4 mt-8 text-lg font-semibold"> + {t("prn_prescriptions")} + </h3> + <PrescriptionBuilder is_prn /> + </div> + <div> + <h3 className="mb-4 mt-8 text-lg font-semibold"> + {t("diagnosis")} + </h3> + {/* */} + {diagnoses ? ( + <EditDiagnosesBuilder value={diagnoses} /> + ) : ( + <div className="flex animate-pulse justify-center py-4 text-center font-medium text-gray-800"> + Fetching existing diagnosis of patient... + </div> + )} + </div> + </div> + </> + )} </div> <div className="mt-4 flex flex-col-reverse justify-end gap-2 md:flex-row"> @@ -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/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/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) { } /> </div> + <SelectFormField + name="ration_card_category" + label="Ration Card Category" + placeholder="Select" + options={RATION_CARD_CATEGORY} + optionLabel={(o) => t(`ration_card__${o}`)} + optionValue={(o) => o} + value={filterState.ration_card_category} + onChange={(e) => + setFilterState({ + ...filterState, + [e.name]: e.value, + }) + } + /> </div> </AccordionV2> <AccordionV2 diff --git a/src/Components/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index 6e5b39cc1b2..e92d043dff4 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -411,22 +411,13 @@ export const PatientHome = (props: any) => { {patientData.facility_object?.name || "-"} </h3> <p className="mb-7 mt-4 text-sm font-medium text-zinc-500"> - {patientGender} | {patientData.blood_group || "-"} + {patientGender} | {patientData.blood_group || "-"} | Born on{" "} + {patientData.date_of_birth + ? formatDate(patientData.date_of_birth) + : patientData.year_of_birth} </p> </div> <div className="mb-8 mt-2 grid grid-cols-1 gap-x-4 gap-y-2 md:grid-cols-2 md:gap-y-8 lg:grid-cols-4"> - <div className="sm:col-span-1"> - <div className="text-sm font-semibold leading-5 text-zinc-400"> - {patientData.date_of_birth - ? "Date of Birth" - : "Year of Birth"} - </div> - <div className="mt-1 text-sm font-medium leading-5"> - {patientData.date_of_birth - ? formatDate(patientData.date_of_birth) - : patientData.year_of_birth} - </div> - </div> <div className="sm:col-span-1"> <div className="text-sm font-semibold leading-5 text-zinc-400"> Phone @@ -537,6 +528,16 @@ export const PatientHome = (props: any) => { {parseOccupation(patientData.meta_info?.occupation) || "-"} </div> </div> + <div className="sm:col-span-1"> + <div className="text-sm font-semibold leading-5 text-zinc-400"> + Ration Card Category + </div> + <div className="mt-1 text-sm font-medium leading-5 "> + {patientData.ration_card_category + ? t(`ration_card__${patientData.ration_card_category}`) + : "-"} + </div> + </div> </div> </div> </div> diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index a2e6dbaf01c..247d9312d6e 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -185,9 +185,9 @@ export default function PatientInfoCard(props: { </> )} - <section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> + <section className="flex flex-col lg:flex-row"> <div - className="col-span-2 flex w-full flex-col bg-white px-4 pt-2 lg:flex-row xl:min-w-fit" + className="flex w-full flex-col bg-white px-4 pt-2 lg:flex-row" id="patient-infobadges" > {/* Can support for patient picture in the future */} @@ -234,13 +234,15 @@ export default function PatientInfoCard(props: { {category.toUpperCase()} </div> )} - <ButtonV2 - ghost - onClick={() => setOpen(true)} - className="mt-1 px-[10px] py-1" - > - {bedDialogTitle} - </ButtonV2> + {consultation?.admitted && ( + <ButtonV2 + ghost + onClick={() => setOpen(true)} + className="mt-1 px-[10px] py-1" + > + {bedDialogTitle} + </ButtonV2> + )} </div> <div className="flex items-center justify-center"> <div @@ -267,7 +269,7 @@ export default function PatientInfoCard(props: { </div> </div> </div> - <div className="flex w-full flex-col items-center gap-4 space-y-2 lg:items-start lg:gap-0 lg:pl-2 xl:w-full"> + <div className="flex w-full flex-col items-center gap-4 space-y-2 lg:items-start lg:gap-0 lg:pl-2"> <div className="flex flex-col items-center gap-2 sm:flex-row"> <Link href={`/facility/${consultation?.facility}`} @@ -282,7 +284,7 @@ export default function PatientInfoCard(props: { </Link> {medicoLegalCase && ( - <span className="flex pl-2 capitalize md:col-span-2"> + <span className="flex pl-2 capitalize"> <span className="badge badge-pill badge-danger">MLC</span> </span> )} @@ -299,7 +301,7 @@ export default function PatientInfoCard(props: { </div> <div className="flex flex-wrap items-center gap-2 text-sm sm:flex-row"> <div - className="flex flex-wrap items-center justify-center gap-2 text-sm text-gray-900 sm:flex-row sm:text-sm lg:justify-normal" + className="flex w-full flex-wrap items-center justify-center gap-2 text-sm text-gray-900 sm:flex-row sm:text-sm md:pr-10 lg:justify-normal" id="patient-consultationbadges" > {consultation?.patient_no && ( @@ -518,12 +520,12 @@ export default function PatientInfoCard(props: { </div> </div> <div - className="col-span-2 flex w-full flex-col items-center justify-end gap-2 px-4 py-1 lg:col-span-1 2xl:flex-row" + className="flex flex-col items-center justify-end gap-4 px-4 py-1 2xl:flex-row" id="consultation-buttons" > {consultation?.suggestion === "A" && ( <div className="flex flex-col items-center"> - <div className="col-span-1 flex w-full justify-center bg-white px-4 lg:flex-row"> + <div className="flex w-full justify-center bg-white px-4 lg:flex-row"> <div className={ "flex h-7 w-7 items-center justify-center rounded-full border-2" @@ -543,7 +545,7 @@ export default function PatientInfoCard(props: { </div> )} {consultation?.last_daily_round && ( - <div className="col-span-1 flex w-full justify-center bg-white px-4 lg:flex-row"> + <div className="flex w-full justify-center bg-white px-4 lg:flex-row"> <Mews dailyRound={consultation?.last_daily_round} /> </div> )} @@ -631,7 +633,7 @@ export default function PatientInfoCard(props: { <DropdownMenu id="show-more" itemClassName="min-w-0 sm:min-w-[225px]" - title={"Manage Patient"} + title="Manage Patient" icon={<CareIcon icon="l-setting" className="text-xl" />} className="xl:justify-center" containerClassName="w-full lg:w-auto mt-2 2xl:mt-0 flex justify-center z-20" diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index 07e682c137f..7d219514267 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -5,6 +5,7 @@ import { GENDER_TYPES, MEDICAL_HISTORY_CHOICES, OCCUPATION_TYPES, + RATION_CARD_CATEGORY, VACCINES, } from "../../Common/constants"; import { @@ -65,6 +66,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")); @@ -130,6 +132,7 @@ const initForm: any = { last_vaccinated_date: null, abha_number: null, ...medicalHistoryChoices, + ration_card_category: null, }; const initError = Object.assign( @@ -169,6 +172,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; @@ -750,6 +754,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, { @@ -1340,8 +1345,9 @@ export const PatientRegister = (props: PatientRegisterProps) => { <TextFormField {...field("age")} errorClassName="hidden" + trailingPadding="pr-4" trailing={ - <p className="relative right-4 space-x-1 text-xs text-gray-700 sm:right-14 sm:text-sm md:right-4 lg:right-14"> + <p className="absolute right-10 space-x-1 whitespace-nowrap text-xs text-gray-700 sm:text-sm"> {field("age").value !== "" && ( <> <span className="hidden sm:inline md:hidden lg:inline"> @@ -1359,7 +1365,6 @@ export const PatientRegister = (props: PatientRegisterProps) => { </p> } placeholder="Enter the age" - className="col-span-6 sm:col-span-3" type="number" min={0} /> @@ -1702,6 +1707,14 @@ export const PatientRegister = (props: PatientRegisterProps) => { optionLabel={(o) => o.text} optionValue={(o) => o.id} /> + <SelectFormField + {...field("ration_card_category")} + label="Ration Card Category" + placeholder="Select" + options={RATION_CARD_CATEGORY} + optionLabel={(o) => t(`ration_card__${o}`)} + optionValue={(o) => o} + /> </> ) : ( <div id="passport_no-div"> diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index 6d6e0b3c979..f64d71146c4 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"; @@ -101,6 +102,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; @@ -274,6 +276,7 @@ export interface DailyRoundsOutput { export const DailyRoundTypes = [ "NORMAL", + "DOCTORS_LOG", "VENTILATOR", "AUTOMATED", "TELEMEDICINE", @@ -302,13 +305,10 @@ export interface DailyRoundsModel { physical_examination_info?: string; other_details?: string; consultation?: number; - additional_symptoms?: Array<number>; medication_given?: Array<any>; - additional_symptoms_text?: string; action?: string; review_interval?: number; id?: string; - other_symptoms?: string; admitted_to?: string; patient_category?: PatientCategory; output?: DailyRoundsOutput[]; 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<EncounterSymptom>[]; + onChange: (value: Writable<EncounterSymptom>[]) => void; +}) => { + return ( + <div className="flex w-full flex-col items-start rounded-lg border border-gray-400"> + <ul className="flex w-full flex-col gap-2 p-4"> + {props.value.map((obj, index, arr) => { + const handleUpdate = (event: FieldChangeEvent<unknown>) => { + 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 ( + <li + key={index} + id={`symptom-${index}`} + className="border-b-2 border-dashed border-gray-400 py-4 last:border-b-0 last:pb-0 md:border-b-0 md:py-2" + > + <SymptomEntry + value={obj} + onChange={handleUpdate} + onRemove={handleRemove} + /> + </li> + ); + })} + </ul> + + {props.value.length === 0 && ( + <div className="flex w-full justify-center gap-2 pb-8 text-center font-medium text-gray-700"> + No symptoms added + </div> + )} + + <div className="w-full rounded-b-lg border-t-2 border-dashed border-gray-400 bg-gray-100 p-4"> + <AddSymptom + existing={props.value} + onAdd={(objects) => props.onChange([...props.value, ...objects])} + /> + </div> + </div> + ); +}; + +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 ( + <div className="flex w-full animate-pulse justify-center gap-2 rounded-lg bg-gray-200 py-8 text-center font-medium text-gray-700"> + <CareIcon icon="l-spinner-alt" className="animate-spin text-lg" /> + <span>Fetching symptom records...</span> + </div> + ); + } + + let items = sortByOnsetDate(data.results); + if (!props.showAll) { + items = items.filter( + (i) => i.clinical_impression_status !== "entered-in-error", + ); + } + + return ( + <div className="flex w-full flex-col items-start rounded-lg border border-gray-400"> + <ul + className={classNames( + "flex w-full flex-col p-4", + (loading || isProcessing) && "pointer-events-none animate-pulse", + )} + > + {items.map((symptom) => { + const handleUpdate = async (event: FieldChangeEvent<unknown>) => { + 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 ( + <li + key={symptom.id} + className="border-b-2 border-dashed border-gray-400 py-4 last:border-b-0 last:pb-0 md:border-b-0 md:py-2" + > + <SymptomEntry + value={symptom} + disabled={isProcessing} + onChange={handleUpdate} + onRemove={handleMarkAsEnteredInError} + /> + </li> + ); + })} + </ul> + + {items.length === 0 && ( + <div className="flex w-full justify-center gap-2 pb-8 text-center font-medium text-gray-700"> + Patient is Asymptomatic + </div> + )} + + <div className="w-full rounded-b-lg border-t-2 border-dashed border-gray-400 bg-gray-100 p-4"> + <AddSymptom + existing={data.results} + consultationId={consultationId} + onAdd={() => refetch()} + /> + </div> + </div> + ); +}; + +const SymptomEntry = (props: { + disabled?: boolean; + value: Writable<EncounterSymptom> | EncounterSymptom; + onChange: (event: FieldChangeEvent<unknown>) => void; + onRemove: () => void; +}) => { + const symptom = props.value; + const disabled = + props.disabled || symptom.clinical_impression_status === "entered-in-error"; + return ( + <div className="grid grid-cols-6 items-center gap-2 md:grid-cols-5"> + <DateFormField + className="col-span-3 w-full md:col-span-1" + name="onset_date" + value={new Date(symptom.onset_date)} + disableFuture + disabled={disabled} + onChange={props.onChange} + errorClassName="hidden" + /> + <DateFormField + className="col-span-3 w-full md:col-span-1" + name="cure_date" + value={symptom.cure_date ? new Date(symptom.cure_date) : undefined} + disableFuture + position="CENTER" + placeholder="Date of cure" + min={new Date(symptom.onset_date)} + disabled={disabled} + onChange={props.onChange} + errorClassName="hidden" + /> + <div className="col-span-6 flex items-center gap-2 md:col-span-3"> + <div + className={classNames( + "cui-input-base w-full font-medium", + disabled && "bg-gray-200", + )} + > + <span + className={classNames( + "whitespace-pre-wrap", + symptom.clinical_impression_status === "entered-in-error" && + "line-through decoration-red-500 decoration-2", + )} + > + <SymptomText value={symptom} /> + </span> + {symptom.clinical_impression_status === "entered-in-error" && ( + <span className="pl-2 text-red-500 no-underline"> + Entered in Error + </span> + )} + </div> + <ButtonV2 + type="button" + variant="danger" + className="p-1" + size="small" + circle + ghost + border + onClick={props.onRemove} + disabled={disabled} + tooltip="Mark as entered in error" + tooltipClassName="tooltip-bottom -translate-x-2/3 md:-translate-x-1/2 translate-y-1 text-xs" + > + <CareIcon icon="l-times" className="text-base md:text-lg" /> + </ButtonV2> + </div> + </div> + ); +}; + +const AddSymptom = (props: { + disabled?: boolean; + existing: (Writable<EncounterSymptom> | EncounterSymptom)[]; + onAdd?: (value: Writable<EncounterSymptom>[]) => void; + consultationId?: string; +}) => { + const [processing, setProcessing] = useState(false); + const [selected, setSelected] = useState<EncounterSymptom["symptom"][]>([]); + const [otherSymptom, setOtherSymptom] = useState(""); + const [onsetDate, setOnsetDate] = useState<Date>(); + + 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 ( + <div className="flex w-full flex-wrap items-start gap-4 md:flex-nowrap"> + <DateFormField + className="w-full md:w-36" + name="onset_date" + id="symptoms_onset_date" + placeholder="Date of onset" + disableFuture + value={onsetDate} + onChange={({ value }) => setOnsetDate(value)} + errorClassName="hidden" + /> + <div className="flex w-full flex-col gap-2"> + <AutocompleteMultiSelectFormField + id="additional_symptoms" + name="symptom" + className="w-full" + disabled={props.disabled || processing} + placeholder="Search for symptoms" + value={selected} + onChange={(e) => 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) && ( + <TextAreaFormField + id="other_symptoms" + label="Other symptom details" + labelClassName="text-sm" + name="other_symptom" + placeholder="Describe the other symptom" + value={otherSymptom} + onChange={({ value }) => setOtherSymptom(value)} + errorClassName="hidden" + /> + )} + </div> + <ButtonV2 + id="add-symptom" + type="button" + className="w-full py-3 md:w-auto" + disabled={ + processing || !hasSymptoms || !otherSymptomValid || !onsetDate + } + tooltip={ + !hasSymptoms + ? "No symptoms selected to be added" + : !otherSymptomValid + ? "Other symptom details not specified" + : !onsetDate + ? "No date of onset specified" + : undefined + } + tooltipClassName="tooltip-bottom -translate-x-1/2 text-xs translate-y-1 w-full max-w-96 whitespace-pre-wrap" + onClick={async () => { + setProcessing(true); + await handleAdd(); + setProcessing(false); + }} + > + {processing ? ( + <> + <CareIcon icon="l-spinner-alt" className="animate-spin text-lg" /> + <span>Adding...</span> + </> + ) : ( + <span>Add Symptom(s)</span> + )} + </ButtonV2> + </div> + ); +}; + +export const SymptomText = (props: { + value: Writable<EncounterSymptom> | EncounterSymptom; +}) => { + const symptom = + SYMPTOM_CHOICES.find(({ id }) => props.value.symptom === id) || + OTHER_SYMPTOM_CHOICE; + + const isOtherSymptom = symptom.id === OTHER_SYMPTOM_CHOICE.id; + + return isOtherSymptom ? ( + <> + <span className="font-normal">Other: </span> + <span + className={classNames( + !props.value.other_symptom?.trim() && "italic text-gray-700", + )} + > + {props.value.other_symptom || "Not specified"} + </span> + </> + ) : ( + 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 ( + <div className="flex w-full animate-pulse justify-center gap-2 rounded-lg bg-gray-200 py-8 text-center font-medium text-gray-700"> + <CareIcon icon="l-spinner-alt" className="animate-spin text-lg" /> + <span>Fetching symptom records...</span> + </div> + ); + } + + const records = groupAndSortSymptoms(data.results); + + return ( + <div id="encounter-symptoms"> + <h3 className="mb-2 text-lg font-semibold leading-relaxed text-gray-900"> + Symptoms + </h3> + + <div className="grid gap-4 divide-y-2 divide-dashed divide-gray-400 md:grid-cols-2 md:divide-x-2 md:divide-y-0"> + <SymptomsSection title="Active" symptoms={records["in-progress"]} /> + <div className="pt-4 md:pl-6 md:pt-0"> + <SymptomsSection title="Cured" symptoms={records["completed"]} /> + </div> + </div> + </div> + ); +}; + +const SymptomsSection = (props: { + title: string; + symptoms: EncounterSymptom[]; +}) => { + return ( + <div> + <h4 className="text-base font-semibold leading-relaxed text-gray-900"> + {props.title} + </h4> + <ul className="flex list-disc flex-col px-4"> + {props.symptoms.map((record) => ( + <li key={record.id}> + <div> + <SymptomText value={record} /> + <br /> + <span className="flex text-sm text-gray-800"> + <span className="flex gap-1"> + Onset <RecordMeta time={record.onset_date} /> + </span> + {record.cure_date && ( + <span className="flex gap-1"> + {"; Cured"} <RecordMeta time={record.cure_date} /> + </span> + )} + </span> + </div> + </li> + ))} + </ul> + {!props.symptoms.length && ( + <div className="flex h-full w-full gap-2 font-medium text-gray-700"> + No symptoms + </div> + )} + </div> + ); +}; + +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<PaginatedResponse<EncounterSymptom>>(), + }, + + add: { + path: "/api/v1/consultation/{consultationId}/symptoms/", + method: "POST", + TRes: Type<EncounterSymptom>(), + TBody: Type<WritableOnly<EncounterSymptom>>(), + }, + + retrieve: { + method: "GET", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TRes: Type<EncounterSymptom>(), + }, + + update: { + method: "PUT", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TBody: Type<WritableOnly<EncounterSymptom>>(), + TRes: Type<EncounterSymptom>(), + }, + + partialUpdate: { + method: "PATCH", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TBody: Type<Partial<WritableOnly<EncounterSymptom>>>(), + TRes: Type<EncounterSymptom>(), + }, + + markAsEnteredInError: { + method: "DELETE", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TRes: Type<unknown>(), + }, +} 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> | EncounterSymptom, +>( + records: T[], +) => { + const result: Record<EncounterSymptom["clinical_impression_status"], T[]> = { + "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> | 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<DailyRoundsRes>(), }, + // Event Types + + listEventTypes: { + path: "/api/v1/event_types/", + method: "GET", + TRes: Type<PaginatedResponse<Type>>(), + }, + // Hospital Beds createCapacity: { path: "/api/v1/facility/{facilityId}/capacity/", 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<T> = { + [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<T> = { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + P + >]: T[P]; +}; + +type IfEquals<X, Y, A = X, B = never> = + (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B; diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 38ffc2e1c66..d599a494b1c 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -409,6 +409,14 @@ export const compareBy = <T extends object>(key: keyof T) => { }; }; +export const compareByDateString = <T extends object>(key: keyof T) => { + return (a: T, b: T) => { + const aV = new Date(a[key] as string); + const bV = new Date(b[key] as string); + return aV < bV ? -1 : aV > bV ? 1 : 0; + }; +}; + export const isValidUrl = (url?: string) => { try { new URL(url ?? "");