From cd6ce2c313453a09aa799dea341435f8df87dd2b Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 23 May 2024 14:50:01 +0530 Subject: [PATCH 01/24] fix placeholder overlapping in search input (#7865) --- src/Components/Form/SearchInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/Form/SearchInput.tsx b/src/Components/Form/SearchInput.tsx index e64aad207d1..2d146837ebf 100644 --- a/src/Components/Form/SearchInput.tsx +++ b/src/Components/Form/SearchInput.tsx @@ -80,7 +80,7 @@ const SearchInput = ({ props.trailing || (!props.secondary && (
- + {shortcutKeyIcon}
@@ -88,7 +88,7 @@ const SearchInput = ({ } trailingFocused={
- + Esc
From 63a2cdb7e2378a412835605ca96cd70b8802766d Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Fri, 24 May 2024 23:41:06 +0530 Subject: [PATCH 02/24] Separate page for patient consent records (#7882) * . * completed patient consent records * Update src/Components/Patient/PatientInfoCard.tsx Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> * removed delete in favour of archiving * fixed error * abstracted to file manager * refetch * removed group unarchive and changed layout * fixed archive refresh and fixed bed popup * lint fix * fixed archiving * added no records found and hidden archive button * added no records found and hidden archive button --- src/Common/constants.tsx | 2 +- src/Components/Facility/ConsultationForm.tsx | 207 +------- src/Components/Facility/models.tsx | 10 +- src/Components/Patient/FileUpload.tsx | 5 +- .../Patient/PatientConsentRecordBlock.tsx | 157 ++++++ .../Patient/PatientConsentRecords.tsx | 307 ++++++++++++ src/Components/Patient/PatientHome.tsx | 11 +- src/Components/Patient/PatientInfoCard.tsx | 28 +- src/Components/Patient/models.tsx | 2 +- src/Routers/routes/ConsultationRoutes.tsx | 9 + src/Utils/useFileManager.tsx | 256 ++++++++++ src/Utils/useFileUpload.tsx | 448 ++++++++++++++++++ 12 files changed, 1232 insertions(+), 210 deletions(-) create mode 100644 src/Components/Patient/PatientConsentRecordBlock.tsx create mode 100644 src/Components/Patient/PatientConsentRecords.tsx create mode 100644 src/Utils/useFileManager.tsx create mode 100644 src/Utils/useFileUpload.tsx diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index de0ab305f48..5fbff1b48cb 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -1308,7 +1308,7 @@ export const CONSENT_PATIENT_CODE_STATUS_CHOICES = [ { id: 1, text: "Do Not Hospitalise (DNH)" }, { id: 2, text: "Do Not Resuscitate (DNR)" }, { id: 3, text: "Comfort Care Only" }, - { id: 4, text: "Active treatment (Default)" }, + { id: 4, text: "Active treatment" }, ]; export const OCCUPATION_TYPES = [ { diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 7e6fe345f7c..8a9a19db2bc 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -1,6 +1,6 @@ import * as Notification from "../../Utils/Notifications.js"; -import { BedModel, FacilityModel } from "./models"; +import { BedModel, ConsentRecord, FacilityModel } from "./models"; import { CONSULTATION_SUGGESTION, DISCHARGE_REASONS, @@ -8,8 +8,6 @@ import { PATIENT_CATEGORIES, REVIEW_AT_CHOICES, TELEMEDICINE_ACTIONS, - CONSENT_TYPE_CHOICES, - CONSENT_PATIENT_CODE_STATUS_CHOICES, } from "../../Common/constants"; import { Cancel, Submit } from "../Common/components/ButtonV2"; import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave"; @@ -59,8 +57,6 @@ import { CreateDiagnosesBuilder, EditDiagnosesBuilder, } from "../Diagnosis/ConsultationDiagnosisBuilder/ConsultationDiagnosisBuilder.js"; -import { FileUpload } from "../Patient/FileUpload.js"; -import ConfirmDialog from "../Common/ConfirmDialog.js"; import request from "../../Utils/request/request.js"; import routes from "../../Redux/api.js"; import useQuery from "../../Utils/request/useQuery.js"; @@ -71,13 +67,6 @@ const PageTitle = lazy(() => import("../Common/PageTitle")); type BooleanStrings = "true" | "false"; -export type ConsentRecord = { - id: string; - type: (typeof CONSENT_TYPE_CHOICES)[number]["id"]; - patient_code_status?: (typeof CONSENT_PATIENT_CODE_STATUS_CHOICES)[number]["id"]; - deleted?: boolean; -}; - type FormDetails = { symptoms: number[]; other_symptoms: string; @@ -125,6 +114,7 @@ type FormDetails = { death_datetime: string; death_confirmed_doctor: string; InvestigationAdvice: InvestigationType[]; + procedures: ProcedureType[]; consent_records: ConsentRecord[]; }; @@ -175,6 +165,7 @@ const initForm: FormDetails = { death_datetime: "", death_confirmed_doctor: "", InvestigationAdvice: [], + procedures: [], consent_records: [], }; @@ -226,7 +217,6 @@ type ConsultationFormSection = | "Consultation Details" | "Diagnosis" | "Treatment Plan" - | "Consent Records" | "Bed Status"; type Props = { @@ -259,14 +249,8 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { const [diagnosisVisible, diagnosisRef] = useVisibility(-300); const [treatmentPlanVisible, treatmentPlanRef] = useVisibility(-300); const [bedStatusVisible, bedStatusRef] = useVisibility(-300); - const [consentRecordsVisible, consentRecordsRef] = useVisibility(-300); + const [disabledFields, setDisabledFields] = useState([]); - const [collapsedConsentRecords, setCollapsedConsentRecords] = useState< - number[] - >([]); - const [showDeleteConsent, setShowDeleteConsent] = useState( - null, - ); const { min_encounter_date } = useConfig(); @@ -286,11 +270,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { visible: treatmentPlanVisible, ref: treatmentPlanRef, }, - "Consent Records": { - iconClass: "l-file-alt", - visible: consentRecordsVisible, - ref: consentRecordsRef, - }, "Bed Status": { iconClass: "l-bed", visible: bedStatusVisible, @@ -303,7 +282,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { if (consultationDetailsVisible) return "Consultation Details"; if (diagnosisVisible) return "Diagnosis"; if (treatmentPlanVisible) return "Treatment Plan"; - if (consentRecordsVisible) return "Consent Records"; if (bedStatusVisible) return "Bed Status"; return prev; }); @@ -311,7 +289,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { consultationDetailsVisible, diagnosisVisible, treatmentPlanVisible, - consentRecordsVisible, bedStatusVisible, ]); @@ -769,7 +746,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { height: Number(state.form.height), bed: bed && bed instanceof Array ? bed[0]?.id : bed?.id, patient_no: state.form.patient_no || null, - consent_records: state.form.consent_records || [], }; const { data: obj } = await request( @@ -917,64 +893,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { }; }; - const handleConsentTypeChange: FieldChangeEventHandler = async ( - event, - ) => { - if (!id) return; - const consentRecords = [...state.form.consent_records]; - if ( - consentRecords - .filter((cr) => cr.deleted !== true) - .map((cr) => cr.type) - .includes(event.value) - ) { - return; - } else { - const randomId = "consent-" + new Date().getTime().toString(); - const newRecords = [ - ...consentRecords, - { id: randomId, type: event.value }, - ]; - await request(routes.partialUpdateConsultation, { - pathParams: { id }, - body: { consent_records: newRecords }, - }); - dispatch({ - type: "set_form", - form: { ...state.form, consent_records: newRecords }, - }); - } - }; - - const handleConsentPCSChange: FieldChangeEventHandler = (event) => { - dispatch({ - type: "set_form", - form: { - ...state.form, - consent_records: state.form.consent_records.map((cr) => - cr.type === 2 ? { ...cr, patient_code_status: event.value } : cr, - ), - }, - }); - }; - - const handleDeleteConsent = async () => { - const consent_id = showDeleteConsent; - if (!consent_id || !id) return; - const newRecords = state.form.consent_records.map((cr) => - cr.id === consent_id ? { ...cr, deleted: true } : cr, - ); - await request(routes.partialUpdateConsultation, { - pathParams: { id }, - body: { consent_records: newRecords }, - }); - dispatch({ - type: "set_form", - form: { ...state.form, consent_records: newRecords }, - }); - setShowDeleteConsent(null); - }; - return (
{
{Object.keys(sections).map((sectionTitle) => { - if ( - !isUpdate && - ["Bed Status", "Consent Records"].includes(sectionTitle) - ) { + if (!isUpdate && ["Bed Status"].includes(sectionTitle)) { return null; } const isCurrent = currentSection === sectionTitle; @@ -1551,118 +1466,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { )}
- {id && ( - <> -
- {sectionTitle("Consent Records", true)} -
- setShowDeleteConsent(null)} - onConfirm={handleDeleteConsent} - action="Delete" - variant="danger" - description={ - "Are you sure you want to delete this consent record?" - } - title="Delete Consent" - className="w-auto" - /> - - !state.form.consent_records - .filter((r) => r.deleted !== true) - .map((record) => record.type) - .includes(c.id), - )} - /> -
- {state.form.consent_records - .filter((record) => record.deleted !== true) - .map((record, index) => ( -
-
- - -
-
-
- {record.type === 2 && ( - - )} -
- -
-
- ))} -
- - )}
diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 49a2f1346af..252603ab89f 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -1,4 +1,6 @@ import { + CONSENT_PATIENT_CODE_STATUS_CHOICES, + CONSENT_TYPE_CHOICES, ConsultationSuggestionValue, DISCHARGE_REASONS, PATIENT_NOTES_THREADS, @@ -12,7 +14,6 @@ import { ConsultationDiagnosis, CreateDiagnosis } from "../Diagnosis/types"; import { NormalPrescription, PRNPrescription } from "../Medicine/models"; import { AssignedToObjectModel, DailyRoundsModel } from "../Patient/models"; import { UserBareMinimum } from "../Users/models"; -import { ConsentRecord } from "./ConsultationForm"; export interface LocalBodyModel { id: number; @@ -97,6 +98,13 @@ export interface OptionsType { export type PatientCategory = "Comfort Care" | "Mild" | "Moderate" | "Critical"; +export type ConsentRecord = { + id: string; + type: (typeof CONSENT_TYPE_CHOICES)[number]["id"]; + patient_code_status?: (typeof CONSENT_PATIENT_CODE_STATUS_CHOICES)[number]["id"]; + deleted?: boolean; +}; + export interface ConsultationModel { encounter_date: string; icu_admission_date?: string; diff --git a/src/Components/Patient/FileUpload.tsx b/src/Components/Patient/FileUpload.tsx index 81e92efdf8b..6f63a697229 100644 --- a/src/Components/Patient/FileUpload.tsx +++ b/src/Components/Patient/FileUpload.tsx @@ -57,7 +57,7 @@ export const header_content_type: URLS = { }; // Array of image extensions -const ExtImage: string[] = [ +export const ExtImage: string[] = [ "jpeg", "jpg", "png", @@ -119,12 +119,13 @@ interface URLS { [id: string]: string; } -interface ModalDetails { +export interface ModalDetails { name?: string; id?: string; reason?: string; userArchived?: string; archiveTime?: any; + associatedId?: string; } export interface StateInterface { diff --git a/src/Components/Patient/PatientConsentRecordBlock.tsx b/src/Components/Patient/PatientConsentRecordBlock.tsx new file mode 100644 index 00000000000..8f1d3715e8e --- /dev/null +++ b/src/Components/Patient/PatientConsentRecordBlock.tsx @@ -0,0 +1,157 @@ +import dayjs from "dayjs"; +import { + CONSENT_PATIENT_CODE_STATUS_CHOICES, + CONSENT_TYPE_CHOICES, +} from "../../Common/constants"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; +import { ConsentRecord } from "../Facility/models"; +import { FileUploadModel } from "./models"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import ButtonV2 from "../Common/components/ButtonV2"; +import { useEffect } from "react"; +import useAuthUser from "../../Common/hooks/useAuthUser"; + +export default function PatientConsentRecordBlockGroup(props: { + consentRecord: ConsentRecord; + previewFile: (file: FileUploadModel, file_associating_id: string) => void; + archiveFile: (file: FileUploadModel, file_associating_id: string) => void; + onDelete: (consentRecord: ConsentRecord) => void; + refreshTrigger: any; + showArchive: boolean; + onFilesFound: () => void; +}) { + const { + consentRecord, + previewFile, + archiveFile, + refreshTrigger, + showArchive, + } = props; + + const authUser = useAuthUser(); + + const filesQuery = useQuery(routes.viewUpload, { + query: { + file_type: "CONSENT_RECORD", + associating_id: consentRecord.id, + is_archived: false, + limit: 100, + offset: 0, + }, + }); + + const archivedFilesQuery = useQuery(routes.viewUpload, { + query: { + file_type: "CONSENT_RECORD", + associating_id: consentRecord.id, + is_archived: true, + limit: 100, + offset: 0, + }, + }); + + const consent = CONSENT_TYPE_CHOICES.find((c) => c.id === consentRecord.type); + const consentPCS = CONSENT_PATIENT_CODE_STATUS_CHOICES.find( + (c) => c.id === consentRecord.patient_code_status, + ); + + const data = showArchive + ? [ + ...(archivedFilesQuery.data?.results || []), + ...(filesQuery.data?.results || []), + ] + : filesQuery.data?.results; + + useEffect(() => { + filesQuery.refetch(); + archivedFilesQuery.refetch(); + }, [refreshTrigger]); + + useEffect(() => { + if ((data?.length || 0) > 1) { + props.onFilesFound(); + } + }, [data]); + + return ( +
+
+
+

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

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

Add New Record

+ { + setNewConsent({ ...newConsent, type: e.value }); + }} + value={newConsent.type} + label="Consent Type" + options={CONSENT_TYPE_CHOICES} + required + /> + {newConsent.type === 2 && ( + { + setNewConsent({ + ...newConsent, + patient_code_status: e.value, + }); + }} + label="Patient Code Status" + value={newConsent.patient_code_status} + options={CONSENT_PATIENT_CODE_STATUS_CHOICES} + required + /> + )} + fileUpload.setFileName(e.value)} + /> +
+ {fileUpload.file ? ( + <> + { + const diffPCS = consentRecords?.find( + (record) => + record.type === 2 && + record.patient_code_status !== + newConsent.patient_code_status && + record.deleted !== true, + ); + if (diffPCS) { + setShowPCSChangeModal(newConsent.patient_code_status); + } else { + handleUpload(); + } + }} + loading={!!fileUpload.progress} + className="flex-1" + > + + Upload + + + + + + ) : ( + <> + + + + )} +
+
+
+ {tabConsents?.length === 0 || + (!filesFound && ( +
+ No records found +
+ ))} +
+ {tabConsents?.map((record, index) => ( + setShowDeleteConsent(record.id)} + refreshTrigger={consultation} + showArchive={showArchived} + onFilesFound={() => setFilesFound(true)} + /> + ))} +
+
+
+
+ ); +} diff --git a/src/Components/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index 50fc5819314..991e5636106 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -552,7 +552,7 @@ export const PatientHome = (props: any) => { 0 && (
{

)} + {( + patientData.last_consultation?.consent_records?.filter( + (c) => !c.deleted, + ) || [] + ).length < 1 && ( +
+ Consent Records Missing +
+ )}
diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index 7a88c478bba..a2e6dbaf01c 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -352,6 +352,18 @@ export default function PatientInfoCard(props: {
)} + {( + consultation?.consent_records?.filter((c) => !c.deleted) || + [] + ).length < 1 && ( +
+
+ + Consent Records Missing + +
+
+ )} {consultation?.suggestion === "DC" && (
@@ -634,6 +646,12 @@ export default function PatientInfoCard(props: { consultation?.id && !consultation?.discharge_date, ], + [ + `/facility/${patient.facility}/patient/${patient.id}/consultation/${consultation?.id}/consent-records`, + "Consent Records", + "l-file-medical", + patient.is_active, + ], [ `/patient/${patient.id}/investigation_reports`, "Investigation Summary", @@ -667,7 +685,10 @@ export default function PatientInfoCard(props: { key={i} className="dropdown-item-primary pointer-events-auto m-2 flex cursor-pointer items-center justify-start gap-2 rounded border-0 p-2 text-sm font-normal transition-all duration-200 ease-in-out" href={ - action[1] !== "Treatment Summary" && + ![ + "Treatment Summary", + "Consent Records", + ].includes(action[1]) && consultation?.admitted && !consultation?.current_bed && i === 1 @@ -676,7 +697,10 @@ export default function PatientInfoCard(props: { } onClick={() => { if ( - action[1] !== "Treatment Summary" && + ![ + "Treatment Summary", + "Consent Records", + ].includes(action[1]) && consultation?.admitted && !consultation?.current_bed && i === 1 diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index b6f6fae7ea2..6d6e0b3c979 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -332,7 +332,7 @@ export interface FacilityNameModel { // File Upload Models -type FileCategory = "UNSPECIFIED" | "XRAY" | "AUDIO" | "IDENTITY_PROOF"; +export type FileCategory = "UNSPECIFIED" | "XRAY" | "AUDIO" | "IDENTITY_PROOF"; export interface CreateFileRequest { file_type: string | number; diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx index 6dc5fa9c05d..8b75e3f147f 100644 --- a/src/Routers/routes/ConsultationRoutes.tsx +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -9,6 +9,7 @@ import { make as CriticalCareRecording } from "../../Components/CriticalCareReco import { ConsultationDetails } from "../../Components/Facility/ConsultationDetails"; import TreatmentSummary from "../../Components/Facility/TreatmentSummary"; import ConsultationDoctorNotes from "../../Components/Facility/ConsultationDoctorNotes"; +import PatientConsentRecords from "../../Components/Patient/PatientConsentRecords"; export default { "/facility/:facilityId/patient/:patientId/consultation": ({ @@ -22,6 +23,14 @@ export default { }: any) => ( ), + "/facility/:facilityId/patient/:patientId/consultation/:id/consent-records": + ({ facilityId, patientId, id }: any) => ( + + ), "/facility/:facilityId/patient/:patientId/consultation/:id/files/": ({ facilityId, patientId, diff --git a/src/Utils/useFileManager.tsx b/src/Utils/useFileManager.tsx new file mode 100644 index 00000000000..aa57e1918a8 --- /dev/null +++ b/src/Utils/useFileManager.tsx @@ -0,0 +1,256 @@ +import { useState } from "react"; +import FilePreviewDialog from "../Components/Common/FilePreviewDialog"; +import { FileUploadModel } from "../Components/Patient/models"; +import { ExtImage, StateInterface } from "../Components/Patient/FileUpload"; +import request from "./request/request"; +import routes from "../Redux/api"; +import DialogModal from "../Components/Common/Dialog"; +import CareIcon from "../CAREUI/icons/CareIcon"; +import TextAreaFormField from "../Components/Form/FormFields/TextAreaFormField"; +import { Cancel, Submit } from "../Components/Common/components/ButtonV2"; +import { formatDateTime } from "./utils"; +import * as Notification from "./Notifications.js"; + +export interface FileManagerOptions { + type: string; + onArchive?: () => void; +} + +export interface FileManagerResult { + viewFile: (file: FileUploadModel, associating_id: string) => void; + archiveFile: (file: FileUploadModel, associating_id: string) => void; + Dialogues: React.ReactNode; +} + +export default function useFileManager( + options: FileManagerOptions, +): FileManagerResult { + const { type: fileType, onArchive } = options; + + const [file_state, setFileState] = useState({ + open: false, + isImage: false, + name: "", + extension: "", + zoom: 4, + isZoomInDisabled: false, + isZoomOutDisabled: false, + rotation: 0, + }); + const [fileUrl, setFileUrl] = useState(""); + const [downloadURL, setDownloadURL] = useState(""); + const [archiveDialogueOpen, setArchiveDialogueOpen] = useState< + (FileUploadModel & { associating_id: string }) | null + >(null); + const [archiveReason, setArchiveReason] = useState(""); + const [archiveReasonError, setArchiveReasonError] = useState(""); + const [archiving, setArchiving] = useState(false); + + const getExtension = (url: string) => { + const div1 = url.split("?")[0].split("."); + const ext: string = div1[div1.length - 1].toLowerCase(); + return ext; + }; + + const viewFile = async (file: FileUploadModel, associating_id: string) => { + setFileUrl(""); + setFileState({ ...file_state, open: true }); + const { data } = await request(routes.retrieveUpload, { + query: { + file_type: fileType, + associating_id, + }, + pathParams: { id: file.id || "" }, + }); + + if (!data) return; + + const signedUrl = data.read_signed_url as string; + const extension = getExtension(signedUrl); + + const downloadFileUrl = (url: string) => { + fetch(url) + .then((res) => res.blob()) + .then((blob) => { + setDownloadURL(URL.createObjectURL(blob)); + }); + }; + + setFileState({ + ...file_state, + open: true, + name: data.name as string, + extension, + isImage: ExtImage.includes(extension), + }); + downloadFileUrl(signedUrl); + setFileUrl(signedUrl); + }; + + const validateArchiveReason = (name: any) => { + if (name.trim() === "") { + setArchiveReasonError("Please enter a valid reason!"); + return false; + } else { + setArchiveReasonError(""); + return true; + } + }; + + const handleFileArchive = async () => { + if (!validateArchiveReason(archiveReason)) { + setArchiving(false); + return; + } + + const { res } = await request(routes.editUpload, { + body: { is_archived: true, archive_reason: archiveReason }, + pathParams: { + id: archiveDialogueOpen?.id || "", + fileType, + associatingId: archiveDialogueOpen?.associating_id || "", + }, + }); + + if (res?.ok) { + Notification.Success({ msg: "File archived successfully" }); + } + + setArchiveDialogueOpen(null); + setArchiving(false); + onArchive && onArchive(); + return res; + }; + + const archiveFile = (file: FileUploadModel, associating_id: string) => { + setArchiveDialogueOpen({ ...file, associating_id }); + }; + + const handleFilePreviewClose = () => { + setDownloadURL(""); + setFileState({ + ...file_state, + open: false, + zoom: 4, + isZoomInDisabled: false, + isZoomOutDisabled: false, + }); + }; + + const Dialogues = ( + <> + + +
+ +
+
+

Archive File

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

File Details

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

Camera

+
+
+ } + className="max-w-2xl" + onClose={() => setCameraModalOpen(false)} + > +
+ {!previewImage ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ + {/* buttons for mobile screens */} +
+
+ {!previewImage ? ( + + {t("switch")} + + ) : ( + <> + )} +
+
+ {!previewImage ? ( + <> +
+ { + captureImage(); + }} + className="m-2" + > + {t("capture")} + +
+ + ) : ( + <> +
+ { + setPreviewImage(null); + }} + className="m-2" + > + {t("retake")} + + { + setPreviewImage(null); + setCameraModalOpen(false); + }} + className="m-2" + > + {t("submit")} + +
+ + )} +
+
+ { + setPreviewImage(null); + setCameraModalOpen(false); + }} + className="m-2" + > + {t("close")} + +
+
+ {/* buttons for laptop screens */} + )} - {( - patientData.last_consultation?.consent_records?.filter( - (c) => !c.deleted, - ) || [] - ).length < 1 && ( -
- Consent Records Missing -
- )}
From 643d00fd71a858b28872cef94ba79d921cdb0b1b Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 27 May 2024 17:03:19 +0530 Subject: [PATCH 04/24] fixed camera overlay bug and changed default name (#7907) Co-authored-by: Shivank Kacker --- src/Components/Patient/PatientConsentRecords.tsx | 2 +- src/Utils/useFileUpload.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/Patient/PatientConsentRecords.tsx b/src/Components/Patient/PatientConsentRecords.tsx index 7b81bb562c0..16178ef5c68 100644 --- a/src/Components/Patient/PatientConsentRecords.tsx +++ b/src/Components/Patient/PatientConsentRecords.tsx @@ -155,7 +155,7 @@ export default function PatientConsentRecords(props: { }} backUrl={`/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/`} > - + {fileUpload.Dialogues} {fileManager.Dialogues} void; handleAudioCapture: () => void; handleFileUpload: (associating_id: string) => Promise; - Dialogues: () => JSX.Element; + Dialogues: JSX.Element; UploadButton: (_: FileUploadButtonProps) => JSX.Element; fileName: string; file: File | null; @@ -98,7 +98,7 @@ export default function useFileUpload( const myFile = new File([blob], `capture.${extension}`, { type: blob.type, }); - setUploadFileName(uploadFileName || "capture"); + setUploadFileName(uploadFileName || ""); setFile(myFile); }); }; @@ -235,7 +235,7 @@ export default function useFileUpload( ? "user" : { exact: "environment" }; - const Dialogues = () => ( + const Dialogues = ( Date: Mon, 27 May 2024 22:14:30 +0530 Subject: [PATCH 05/24] Improve camera preset selection UI in camera feed (#7915) * Improve camera preset selection in camera feed * switch to gray shade --- src/Components/CameraFeed/AssetBedSelect.tsx | 40 +++++++++++++++++-- src/Components/CameraFeed/CameraFeed.tsx | 9 ++++- .../ConsultationFeedTab.tsx | 21 +++++----- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/Components/CameraFeed/AssetBedSelect.tsx b/src/Components/CameraFeed/AssetBedSelect.tsx index c8a2d5451bc..0bb40dffb98 100644 --- a/src/Components/CameraFeed/AssetBedSelect.tsx +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -2,6 +2,7 @@ import { Fragment } from "react"; import { AssetBedModel } from "../Assets/AssetTypes"; import { Listbox, Transition } from "@headlessui/react"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import { classNames } from "../../Utils/utils"; interface Props { options: AssetBedModel[]; @@ -10,7 +11,40 @@ interface Props { onChange?: (value: AssetBedModel) => void; } -export default function AssetBedSelect(props: Props) { +export default function CameraPresetSelect(props: Props) { + const label = props.label ?? defaultLabel; + return ( + <> +
+ {props.options + .slice(0, props.options.length > 5 ? 4 : 5) + .map((option) => ( + + ))} + {/* Desktop View */} + {props.options.length > 5 && ( + + )} +
+
+ {/* Mobile View */} + +
+ + ); +} + +const ShowMoreDropdown = (props: Props) => { const selected = props.value; const options = props.options.filter(({ meta }) => meta.type !== "boundary"); @@ -20,7 +54,7 @@ export default function AssetBedSelect(props: Props) { return (
- + {selected ? label(selected) : "No Preset"} @@ -63,7 +97,7 @@ export default function AssetBedSelect(props: Props) {
); -} +}; const defaultLabel = ({ bed_object, meta }: AssetBedModel) => { return `${bed_object.name}: ${meta.preset_name}`; diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index 8f7659cf730..f40aed384c8 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 CareIcon from "../../CAREUI/icons/CareIcon"; interface Props { children?: React.ReactNode; @@ -96,8 +97,13 @@ export default function CameraFeed(props: Props) { )} >
-
+ {props.children} +
+ {props.asset.name}
@@ -109,7 +115,6 @@ export default function CameraFeed(props: Props) { />
- {props.children}
diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index 64ba39ae5aa..40e5ffdc610 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -120,7 +120,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { }); }} > -
+
{presets ? ( <> { {isUpdatingPreset ? ( ) : ( - + )} From 828cd503875e64a8328065f7a3c6aa5bf9f843b9 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 27 May 2024 22:36:18 +0530 Subject: [PATCH 06/24] added skeleton loader for consent records (#7905) * added skeleton loader for consent records * fixed consents * minor fix * fixed * treating deleted and archived files differently * fixed responsiveness --- .../Patient/PatientConsentRecordBlock.tsx | 136 +++++++++++------- .../Patient/PatientConsentRecords.tsx | 51 ++++--- src/Utils/useFileManager.tsx | 30 +++- src/style/index.css | 6 + 4 files changed, 141 insertions(+), 82 deletions(-) diff --git a/src/Components/Patient/PatientConsentRecordBlock.tsx b/src/Components/Patient/PatientConsentRecordBlock.tsx index 8f1d3715e8e..9c1969ae5a3 100644 --- a/src/Components/Patient/PatientConsentRecordBlock.tsx +++ b/src/Components/Patient/PatientConsentRecordBlock.tsx @@ -15,7 +15,11 @@ import useAuthUser from "../../Common/hooks/useAuthUser"; export default function PatientConsentRecordBlockGroup(props: { consentRecord: ConsentRecord; previewFile: (file: FileUploadModel, file_associating_id: string) => void; - archiveFile: (file: FileUploadModel, file_associating_id: string) => void; + archiveFile: ( + file: FileUploadModel, + file_associating_id: string, + skipPrompt?: { reason: string }, + ) => void; onDelete: (consentRecord: ConsentRecord) => void; refreshTrigger: any; showArchive: boolean; @@ -39,6 +43,24 @@ export default function PatientConsentRecordBlockGroup(props: { limit: 100, offset: 0, }, + onResponse: (response) => { + /* + if (consentRecord.deleted === true && response.data?.results) { + const unarchivedFiles = response.data.results; + console.log("checking for unarchived files on this deleted consent record") + for (const file of unarchivedFiles) { + console.log("archiving file", file) + archiveFile(file, consentRecord.id, { + reason: "Consent Record Archived", + }); + } + } + */ + + if ((response.data?.results?.length || 0) > 0) { + props.onFilesFound(); + } + }, }); const archivedFilesQuery = useQuery(routes.viewUpload, { @@ -49,6 +71,12 @@ export default function PatientConsentRecordBlockGroup(props: { limit: 100, offset: 0, }, + prefetch: showArchive, + onResponse: (response) => { + if ((response.data?.results?.length || 0) > 0) { + props.onFilesFound(); + } + }, }); const consent = CONSENT_TYPE_CHOICES.find((c) => c.id === consentRecord.type); @@ -59,20 +87,19 @@ export default function PatientConsentRecordBlockGroup(props: { const data = showArchive ? [ ...(archivedFilesQuery.data?.results || []), - ...(filesQuery.data?.results || []), + ...(consentRecord.deleted ? filesQuery.data?.results || [] : []), ] : filesQuery.data?.results; - useEffect(() => { - filesQuery.refetch(); - archivedFilesQuery.refetch(); - }, [refreshTrigger]); + const loading = archivedFilesQuery.loading || filesQuery.loading; useEffect(() => { - if ((data?.length || 0) > 1) { - props.onFilesFound(); + if (!showArchive) { + filesQuery.refetch(); + } else { + archivedFilesQuery.refetch(); } - }, [data]); + }, [showArchive, refreshTrigger]); return (
- - {data?.map((file: FileUploadModel, i: number) => ( -
-
-
- -
-
-
- {file.name} - {file.extension} {file.is_archived && "(Archived)"} + {loading ? ( +
+ ) : ( + data?.map((file: FileUploadModel, i: number) => ( +
+
+
+
-
- {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} +
+
+ {file.name} + {file.extension} {file.is_archived && "(Archived)"} +
+
+ {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} +
+
+ {!file.is_archived && ( + previewFile(file, consentRecord.id)} + className="" + > + + View + + )} + {(file.is_archived || + file?.uploaded_by?.username === authUser.username || + authUser.user_type === "DistrictAdmin" || + authUser.user_type === "StateAdmin") && ( + archiveFile(file, consentRecord.id)} + className="" + > + + {file.is_archived ? "More Info" : "Archive"} + + )} +
-
- {!file.is_archived && ( - previewFile(file, consentRecord.id)} - className="" - > - - View - - )} - {(file.is_archived || - file?.uploaded_by?.username === authUser.username || - authUser.user_type === "DistrictAdmin" || - authUser.user_type === "StateAdmin") && ( - archiveFile(file, consentRecord.id)} - className="" - > - - {file.is_archived ? "More Info" : "Archive"} - - )} -
-
- ))} + )) + )}
); } diff --git a/src/Components/Patient/PatientConsentRecords.tsx b/src/Components/Patient/PatientConsentRecords.tsx index 16178ef5c68..ca26b270d80 100644 --- a/src/Components/Patient/PatientConsentRecords.tsx +++ b/src/Components/Patient/PatientConsentRecords.tsx @@ -51,7 +51,11 @@ export default function PatientConsentRecords(props: { id: patientId, }, }); - const { data: consultation, refetch } = useQuery(routes.getConsultation, { + const { + data: consultation, + refetch, + loading, + } = useQuery(routes.getConsultation, { pathParams: { id: consultationId! }, onResponse: (data) => { if (data.data && data.data.consent_records) { @@ -130,9 +134,7 @@ export default function PatientConsentRecords(props: { return () => clearTimeout(timeout); }, [consentRecords]); - const tabConsents = consentRecords?.filter( - (record) => showArchived || record.deleted !== true, - ); + const tabConsents = consentRecords?.filter((c) => showArchived || !c.deleted); useEffect(() => { setFilesFound(false); @@ -192,15 +194,15 @@ export default function PatientConsentRecords(props: { className="w-auto" /> setShowArchived(false)} onClickTab2={() => setShowArchived(true)} isTab2Active={showArchived} /> -
-
+
+

Add New Record

record.type === 2 && + newConsent.type === 2 && record.patient_code_status !== newConsent.patient_code_status && record.deleted !== true, @@ -280,25 +283,27 @@ export default function PatientConsentRecords(props: {
- {tabConsents?.length === 0 || - (!filesFound && ( +
+ {loading ? ( +
+ ) : tabConsents?.length === 0 || !filesFound ? (
No records found
- ))} -
- {tabConsents?.map((record, index) => ( - setShowDeleteConsent(record.id)} - refreshTrigger={consultation} - showArchive={showArchived} - onFilesFound={() => setFilesFound(true)} - /> - ))} + ) : null} + {!loading && + tabConsents?.map((record, index) => ( + setShowDeleteConsent(record.id)} + refreshTrigger={consultation} + showArchive={showArchived} + onFilesFound={() => setFilesFound(true)} + /> + ))}
diff --git a/src/Utils/useFileManager.tsx b/src/Utils/useFileManager.tsx index aa57e1918a8..133b4d2533d 100644 --- a/src/Utils/useFileManager.tsx +++ b/src/Utils/useFileManager.tsx @@ -18,7 +18,11 @@ export interface FileManagerOptions { export interface FileManagerResult { viewFile: (file: FileUploadModel, associating_id: string) => void; - archiveFile: (file: FileUploadModel, associating_id: string) => void; + archiveFile: ( + file: FileUploadModel, + associating_id: string, + skipPrompt?: { reason: string }, + ) => void; Dialogues: React.ReactNode; } @@ -97,7 +101,7 @@ export default function useFileManager( } }; - const handleFileArchive = async () => { + const handleFileArchive = async (archiveFile: typeof archiveDialogueOpen) => { if (!validateArchiveReason(archiveReason)) { setArchiving(false); return; @@ -106,9 +110,9 @@ export default function useFileManager( const { res } = await request(routes.editUpload, { body: { is_archived: true, archive_reason: archiveReason }, pathParams: { - id: archiveDialogueOpen?.id || "", + id: archiveFile?.id || "", fileType, - associatingId: archiveDialogueOpen?.associating_id || "", + associatingId: archiveFile?.associating_id || "", }, }); @@ -118,11 +122,25 @@ export default function useFileManager( setArchiveDialogueOpen(null); setArchiving(false); + setArchiveReason(""); onArchive && onArchive(); return res; }; - const archiveFile = (file: FileUploadModel, associating_id: string) => { + const archiveFile = ( + file: FileUploadModel, + associating_id: string, + skipPrompt?: { reason: string }, + ) => { + if (skipPrompt) { + setArchiving(true); + setArchiveReason(skipPrompt.reason); + handleFileArchive({ + ...file, + associating_id, + }); + return; + } setArchiveDialogueOpen({ ...file, associating_id }); }; @@ -174,7 +192,7 @@ export default function useFileManager(
{ event.preventDefault(); - handleFileArchive(); + handleFileArchive(archiveDialogueOpen); }} className="mx-2 my-4 flex w-full flex-col" > diff --git a/src/style/index.css b/src/style/index.css index fe2c9c8edb1..b05417bf912 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -429,6 +429,12 @@ button:disabled, } } +.skeleton-animate-alpha { + animation: skeletonShimmer 3s infinite linear; + background: linear-gradient(to right, rgba(0, 0, 0, 0.1) 10%, rgba(0, 0, 0, 0.05) 40%, rgba(0, 0, 0, 0.1) 70%); + background-size: 1000px 100%; +} + @media print { body * { From 3e12e19b1b10d529164dca809c2324a28c0c8780 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 28 May 2024 11:24:04 +0530 Subject: [PATCH 07/24] Adds watermark to Camera Feed and disables access to context menu. (#7885) * Adds watermark and disable context menu access in camera feed * Update src/Components/CameraFeed/CameraFeed.tsx --- src/Components/CameraFeed/CameraFeed.tsx | 6 ++- src/Components/CameraFeed/FeedWatermark.tsx | 55 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/Components/CameraFeed/FeedWatermark.tsx 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 ( setFullscreen(false)}>
{/* Notifications */} + {player.status === "playing" && } {/* No Feed informations */} {state === "host_unreachable" && ( @@ -150,6 +151,7 @@ export default function CameraFeed(props: Props) { url={streamUrl} ref={playerRef.current as LegacyRef} controls={false} + pip={false} playsinline playing muted @@ -167,10 +169,12 @@ export default function CameraFeed(props: Props) {
) : (
From ce573b0e201400d09d5dd103bf80f496540df3b3 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 28 May 2024 11:27:00 +0530 Subject: [PATCH 12/24] fixes badges overlapping with IP days in patient consultation page (#7924) --- src/Components/Patient/PatientInfoCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index a2e6dbaf01c..c378a5cc697 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -299,7 +299,7 @@ export default function PatientInfoCard(props: {
{consultation?.patient_no && ( From e42982eb875e3c4ecc3b4600c2017515a6dbac4d Mon Sep 17 00:00:00 2001 From: Sulochan Khadka <122200551+Sulochan-khadka@users.noreply.github.com> Date: Tue, 28 May 2024 11:29:59 +0530 Subject: [PATCH 13/24] Prescriptions: Default Discontinued Prescriptions Collapsed (#7833) * fixes: Prescriptions: Default Discontinued Prescriptions Collapsed #7767 * make necessary changes and fix issues * show medicine name when collapsed * fix key used for list item --------- Co-authored-by: rithviknishad --- .../Medicine/PrescriptionBuilder.tsx | 5 +- .../Medicine/PrescriptionDetailCard.tsx | 252 ++++++++++-------- 2 files changed, 144 insertions(+), 113 deletions(-) diff --git a/src/Components/Medicine/PrescriptionBuilder.tsx b/src/Components/Medicine/PrescriptionBuilder.tsx index 972a74159d2..4fe5bed9e03 100644 --- a/src/Components/Medicine/PrescriptionBuilder.tsx +++ b/src/Components/Medicine/PrescriptionBuilder.tsx @@ -66,10 +66,11 @@ export default function PrescriptionBuilder({ /> )}
- {data?.results.map((obj, index) => ( + {data?.results.map((obj) => ( setShowDiscontinueFor(obj)} onAdministerClick={() => setShowAdministerFor(obj)} readonly={disabled} 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 (
-
+
{ + if (collapsible) { + setIsCollapsed(!isCollapsed); + } + }} + >
@@ -39,14 +56,21 @@ export default function PrescriptionDetailCard({ props.selected ? "text-black" : "text-gray-700", )} > - {prescription.prescription_type === "DISCHARGE" && - `${t("discharge")} `} - {t( - prescription.dosage_type === "PRN" - ? "prn_prescription" - : "prescription", + {isCollapsed ? ( + prescription.medicine_object?.name ?? + prescription.medicine_old + ) : ( + <> + {prescription.prescription_type === "DISCHARGE" && + `${t("discharge")} `} + {t( + prescription.dosage_type === "PRN" + ? "prn_prescription" + : "prescription", + )} + {` #${prescription.id?.slice(-5)}`} + )} - {` #${prescription.id?.slice(-5)}`} {prescription.discontinued && ( @@ -62,7 +86,10 @@ export default function PrescriptionDetailCard({ { + e.stopPropagation(); + props.onAdministerClick?.(); + }} type="button" size="small" variant="secondary" @@ -79,7 +106,10 @@ export default function PrescriptionDetailCard({ variant="danger" ghost border - onClick={props.onDiscontinueClick} + onClick={(e) => { + e.stopPropagation(); + props.onDiscontinueClick?.(); + }} > {t("discontinue")} @@ -89,114 +119,114 @@ export default function PrescriptionDetailCard({ )}
- -
- - {prescription.medicine_object?.name ?? prescription.medicine_old} - - - {prescription.route && - t("PRESCRIPTION_ROUTE_" + prescription.route)} - - {prescription.dosage_type === "TITRATED" ? ( - <> - - {prescription.base_dosage} - - - {prescription.target_dosage} - - - ) : ( + {!isCollapsed && ( +
- {prescription.base_dosage} + {prescription.medicine_object?.name ?? prescription.medicine_old} - )} - - {prescription.dosage_type === "PRN" ? ( - <> + + {prescription.route && + t("PRESCRIPTION_ROUTE_" + prescription.route)} + + {prescription.dosage_type === "TITRATED" ? ( + <> + + {prescription.base_dosage} + + + {prescription.target_dosage} + + + ) : ( - {prescription.indicator} + {prescription.base_dosage} + )} + + {prescription.dosage_type === "PRN" ? ( + <> + + {prescription.indicator} + + + {prescription.max_dosage} + + + {prescription.min_hours_between_doses && + prescription.min_hours_between_doses + " hrs."} + + + ) : ( + <> + + {prescription.frequency && + t( + "PRESCRIPTION_FREQUENCY_" + + prescription.frequency.toUpperCase(), + )} + + + {prescription.days} + + + )} + + {prescription.instruction_on_titration && ( - {prescription.max_dosage} + + + )} + + {prescription.notes && ( + + + )} + + {prescription.discontinued && ( - {prescription.min_hours_between_doses && - prescription.min_hours_between_doses + " hrs."} + {prescription.discontinued_reason} - - ) : ( - <> - - {prescription.frequency && - t( - "PRESCRIPTION_FREQUENCY_" + - prescription.frequency.toUpperCase(), - )} - - - {prescription.days} - - - )} - - {prescription.instruction_on_titration && ( - - - - )} - - {prescription.notes && ( - - - - )} - - {prescription.discontinued && ( - - {prescription.discontinued_reason} - - )} -
- + )} +
+ )}
Prescribed From 71fb32ed3b15243adde307d7cbc9153cd3c88550 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 28 May 2024 11:31:03 +0530 Subject: [PATCH 14/24] Removes "Bed Status" for Domiciliary Care encounters (#7889) --- src/Components/Facility/ConsultationForm.tsx | 9 +++++++-- src/Components/Patient/PatientInfoCard.tsx | 16 +++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 487326e5bdd..b7b99d85448 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -915,6 +915,11 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { 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 ( @@ -1265,7 +1270,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
)} - {["A", "DC"].includes(state.form.suggestion) && !isUpdate && ( + {state.form.suggestion === "A" && !isUpdate && (
Bed {
- {isUpdate && ( + {state.form.suggestion === "A" && isUpdate && ( <>
{sectionTitle("Bed Status")} diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index c378a5cc697..ba9280e903d 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -234,13 +234,15 @@ export default function PatientInfoCard(props: { {category.toUpperCase()}
)} - setOpen(true)} - className="mt-1 px-[10px] py-1" - > - {bedDialogTitle} - + {consultation?.admitted && ( + setOpen(true)} + className="mt-1 px-[10px] py-1" + > + {bedDialogTitle} + + )}
Date: Tue, 28 May 2024 16:11:15 +0530 Subject: [PATCH 15/24] fixes overflow issue in live monitoring (#7928) --- src/Components/CameraFeed/AssetBedSelect.tsx | 19 ++++++++++++------- .../CameraFeed/CameraFeedWithBedPresets.tsx | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) 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 ( <>
+ {/* 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)} ))} - {/* Desktop View */} {props.options.length > 5 && ( - + )}
{/* Mobile View */} - +
); } -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 (
- - - {selected ? label(selected) : "No Preset"} + + + {selected ? label(selected) : "Select preset"} 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 ? ( loading presets... ) : ( - Date: Tue, 28 May 2024 16:36:39 +0530 Subject: [PATCH 16/24] Adds support specifying and filtering by ration card category for patients (#7926) * Adds support specifying and filtering by ration card category for patients * update ui --- src/Common/constants.tsx | 2 ++ src/Components/Patient/ManagePatients.tsx | 8 +++++++ src/Components/Patient/PatientFilter.tsx | 22 ++++++++++++++++++ src/Components/Patient/PatientHome.tsx | 27 +++++++++++----------- src/Components/Patient/PatientRegister.tsx | 13 +++++++++++ src/Components/Patient/models.tsx | 2 ++ src/Locale/en/Common.json | 5 +++- 7 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 5fbff1b48cb..99cb3a7988f 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -1404,3 +1404,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/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) { } />
+ t(`ration_card__${o}`)} + optionValue={(o) => o} + value={filterState.ration_card_category} + onChange={(e) => + setFilterState({ + ...filterState, + [e.name]: e.value, + }) + } + />
{ {patientData.facility_object?.name || "-"}

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

-
-
- {patientData.date_of_birth - ? "Date of Birth" - : "Year of Birth"} -
-
- {patientData.date_of_birth - ? formatDate(patientData.date_of_birth) - : patientData.year_of_birth} -
-
Phone @@ -537,6 +528,16 @@ export const PatientHome = (props: any) => { {parseOccupation(patientData.meta_info?.occupation) || "-"}
+
+
+ Ration Card Category +
+
+ {patientData.ration_card_category + ? t(`ration_card__${patientData.ration_card_category}`) + : "-"} +
+
diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index 07e682c137f..34d2ac66387 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, { @@ -1702,6 +1707,14 @@ export const PatientRegister = (props: PatientRegisterProps) => { optionLabel={(o) => o.text} optionValue={(o) => o.id} /> + t(`ration_card__${o}`)} + optionValue={(o) => o} + /> ) : (
diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index 6d6e0b3c979..1a79411ebd5 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; 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 From 53164025619ecc7fcbbac6b928f8344900b6d199 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 28 May 2024 16:36:57 +0530 Subject: [PATCH 17/24] Sort discontinued medicines to the last (#7925) * Sort discontinued medicines to the last * Update src/Components/Medicine/PrescriptionBuilder.tsx * Update src/Components/Medicine/PrescriptionBuilder.tsx --- .../Medicine/PrescriptionBuilder.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Components/Medicine/PrescriptionBuilder.tsx b/src/Components/Medicine/PrescriptionBuilder.tsx index 4fe5bed9e03..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,16 +67,18 @@ export default function PrescriptionBuilder({ /> )}
- {data?.results.map((obj) => ( - setShowDiscontinueFor(obj)} - onAdministerClick={() => setShowAdministerFor(obj)} - readonly={disabled} - /> - ))} + {data?.results + .sort(compareBy("discontinued")) + ?.map((obj) => ( + setShowDiscontinueFor(obj)} + onAdministerClick={() => setShowAdministerFor(obj)} + readonly={disabled} + /> + ))}
Date: Tue, 28 May 2024 16:40:37 +0530 Subject: [PATCH 18/24] Add Medicine Prescription Log Summary Section (#7584) * Add Medicine Summary Section * add timeline view and show only changes * switch to modal and use timeline view * fix width of modal * show clean message * refactor files * dont show mg twice and show discontinued * show prescription type * add disconitnue and no medicine card * show discontinued in the summary * update changes --- .../ConsultationMedicinesTab.tsx | 2 + .../Medicine/MedicinePrescriptionSummary.tsx | 372 ++++++++++++++++++ src/Locale/en/Medicine.json | 1 + 3 files changed, 375 insertions(+) create mode 100644 src/Components/Medicine/MedicinePrescriptionSummary.tsx 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} /> +
); }; diff --git a/src/Components/Medicine/MedicinePrescriptionSummary.tsx b/src/Components/Medicine/MedicinePrescriptionSummary.tsx new file mode 100644 index 00000000000..b77acc090c4 --- /dev/null +++ b/src/Components/Medicine/MedicinePrescriptionSummary.tsx @@ -0,0 +1,372 @@ +import MedicineRoutes from "../Medicine/routes"; +import useQuery from "../../Utils/request/useQuery"; +import DialogModal from "../Common/Dialog"; +import { useState } from "react"; +import { lazy } from "react"; +import Timeline, { TimelineNode } from "../../CAREUI/display/Timeline"; +import { MedibaseMedicine, Prescription } from "../Medicine/models"; +import { useTranslation } from "react-i18next"; + +const Loading = lazy(() => import("../Common/Loading")); + +interface MedicinePrescriptionSummaryProps { + consultation: string; +} + +export const MedicinePrescriptionSummary = ({ + consultation, +}: MedicinePrescriptionSummaryProps) => { + const { t } = useTranslation(); + const [showMedicineModal, setShowMedicineModal] = useState({ + open: false, + name: "", + medicineId: "", + }); + const { data } = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation }, + query: { limit: 100 }, + }); + + const closeMedicineModal = () => { + setShowMedicineModal({ ...showMedicineModal, open: false }); + }; + + function extractUniqueMedicineObjects( + prescriptions: Prescription[], + ): MedibaseMedicine[] { + const uniqueMedicineObjects: Set = new Set(); + const uniqueMedicines: MedibaseMedicine[] = []; + + prescriptions.forEach((prescription: Prescription) => { + if (prescription?.medicine_object) { + const medicineId = prescription?.medicine_object.id; + + if (!uniqueMedicineObjects.has(medicineId)) { + uniqueMedicineObjects.add(medicineId); + uniqueMedicines.push(prescription?.medicine_object); + } + } + }); + + return uniqueMedicines; + } + + const medicinesList: MedibaseMedicine[] = extractUniqueMedicineObjects( + data?.results ?? [], + ); + + return ( +
+

{t("summary")}

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

{"No Medicine Summary"}

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

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

{changes?.changeMessage}

+
+ )); + })()} +
+
+ ); +} diff --git a/src/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.", From f2384750451e2f3fc8195cfa4d3204ce86621ceb Mon Sep 17 00:00:00 2001 From: Manasvi Gaur <120303997+manasvi-gaur@users.noreply.github.com> Date: Tue, 28 May 2024 16:43:03 +0530 Subject: [PATCH 19/24] [Enhancement] Ventilator Parameters Input - Validate Consultation Bed for Linked Ventilator Asset is resolved (#7522) * Ventilator Parameters Input - Validate Consultation Bed for Linked Ventilator Asset is getting resolved * done * requested changes done * lint error solved * clarifying solution * solved that one condition that failed --- src/Components/Common/DialogModal.res | 20 +++++ .../CriticalCare__API.tsx | 17 +++++ .../Recording/CriticalCare__Recording.res | 2 + ...iticalCare__VentilatorParametersEditor.res | 76 +++++++++++++++---- .../Facility/Consultations/Beds.res | 18 +++++ 5 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 src/Components/Common/DialogModal.res create mode 100644 src/Components/Facility/Consultations/Beds.res 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, +) => {children} 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>, +) => { + 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 => unit, _ => unit) => unit = "updateDailyRound" +@module("../CriticalCare__API") +external getAsset: (string, (int => int) => unit) => option 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 => @@ -216,7 +233,7 @@ let make = (~ventilatorParameters: VentilatorParameters.t, ~id, ~consultationId,
-
+
{str("Bilateral Air Entry")}
@@ -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)))} /> - !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)} />
-

{str("Respiratory Support")}

@@ -282,10 +297,43 @@ let make = (~ventilatorParameters: VentilatorParameters.t, ~id, ~consultationId,
+ toggleOpen()} + className="md:max-w-3xl"> + toggleOpen()} + /> +
} 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, +) => From 54a32440907269b18e30d346804d10dd9b56d082 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 28 May 2024 16:53:00 +0530 Subject: [PATCH 20/24] Adds support for specifying onset and cure date in Encounter Symptoms (#7874) * update types and required utilities * Implements necessary reusable components for composing the Symptoms Builder * Migrate in Consultation Form * Migrate in daily rounds form * improve reusability * patch types, remove unused references, switch to new choices * rename consultation symptom to encounter symptom * Show symptoms in consultation dashboard * add loading state * update sorting * add loading state * fixes during QA * fix responsiveness of symptoms card * update button design * Make symptoms builder entries responsive * make add symptom block responsive * correct separator for for last element * remove unused imports --- src/Common/constants.tsx | 36 -- src/Components/Common/SymptomsSelect.tsx | 85 ---- src/Components/Diagnosis/utils.ts | 2 - .../ConsultationUpdatesTab.tsx | 92 +---- .../Facility/ConsultationDetails/index.tsx | 25 +- src/Components/Facility/ConsultationForm.tsx | 148 +++---- src/Components/Facility/models.tsx | 9 +- .../FormFields/AutocompleteMultiselect.tsx | 2 +- .../Patient/DailyRoundListDetails.tsx | 23 +- src/Components/Patient/DailyRounds.tsx | 39 +- src/Components/Patient/models.tsx | 3 - src/Components/Scribe/formDetails.ts | 2 +- src/Components/Symptoms/SymptomsBuilder.tsx | 379 ++++++++++++++++++ src/Components/Symptoms/SymptomsCard.tsx | 84 ++++ src/Components/Symptoms/api.ts | 47 +++ src/Components/Symptoms/types.ts | 52 +++ src/Components/Symptoms/utils.ts | 37 ++ src/Utils/types.ts | 36 ++ src/Utils/utils.ts | 8 + 19 files changed, 746 insertions(+), 363 deletions(-) delete mode 100644 src/Components/Common/SymptomsSelect.tsx create mode 100644 src/Components/Symptoms/SymptomsBuilder.tsx create mode 100644 src/Components/Symptoms/SymptomsCard.tsx create mode 100644 src/Components/Symptoms/api.ts create mode 100644 src/Components/Symptoms/types.ts create mode 100644 src/Components/Symptoms/utils.ts create mode 100644 src/Utils/types.ts diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 99cb3a7988f..4eb3b51d012 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -318,42 +318,6 @@ export const REVIEW_AT_CHOICES: Array = [ { id: 30 * 24 * 60, text: "1 month" }, ]; -export const SYMPTOM_CHOICES = [ - { id: 1, text: "ASYMPTOMATIC", isSingleSelect: true }, - { id: 2, text: "FEVER" }, - { id: 3, text: "SORE THROAT" }, - { id: 4, text: "COUGH" }, - { id: 5, text: "BREATHLESSNESS" }, - { id: 6, text: "MYALGIA" }, - { id: 7, text: "ABDOMINAL DISCOMFORT" }, - { id: 8, text: "VOMITING" }, - { id: 11, text: "SPUTUM" }, - { id: 12, text: "NAUSEA" }, - { id: 13, text: "CHEST PAIN" }, - { id: 14, text: "HEMOPTYSIS" }, - { id: 15, text: "NASAL DISCHARGE" }, - { id: 16, text: "BODY ACHE" }, - { id: 17, text: "DIARRHOEA" }, - { id: 18, text: "PAIN" }, - { id: 19, text: "PEDAL EDEMA" }, - { id: 20, text: "WOUND" }, - { id: 21, text: "CONSTIPATION" }, - { id: 22, text: "HEAD ACHE" }, - { id: 23, text: "BLEEDING" }, - { id: 24, text: "DIZZINESS" }, - { id: 25, text: "CHILLS" }, - { id: 26, text: "GENERAL WEAKNESS" }, - { id: 27, text: "IRRITABILITY" }, - { id: 28, text: "CONFUSION" }, - { id: 29, text: "ABDOMINAL PAIN" }, - { id: 30, text: "JOINT PAIN" }, - { id: 31, text: "REDNESS OF EYES" }, - { id: 32, text: "ANOREXIA" }, - { id: 33, text: "NEW LOSS OF TASTE" }, - { id: 34, text: "NEW LOSS OF SMELL" }, - { id: 9, text: "OTHERS" }, -]; - export const DISCHARGE_REASONS = [ { id: 1, text: "Recovered" }, { id: 2, text: "Referred" }, 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) => { - 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 ( -
- - - also unselects Asymptomatic - -
- ); - - if (!value.includes(ASYMPTOMATIC_ID) && id === ASYMPTOMATIC_ID) - return ( - - - {`also unselects the other ${value.length} option(s)`} - - ); - }; - - return ( - - option.text} - optionValue={(option) => option.id} - value={props.value || []} - optionDescription={getDescription} - onChange={updateSelection} - /> - - ); -}; 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/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index 2ba4f7929ae..be2751f0033 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -5,7 +5,7 @@ import { BedModel } from "../models"; import HL7PatientVitalsMonitor from "../../VitalsMonitor/HL7PatientVitalsMonitor"; import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatientVitalsMonitor"; import useVitalsAspectRatioConfig from "../../VitalsMonitor/useVitalsAspectRatioConfig"; -import { 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 { @@ -23,6 +23,7 @@ import { getVitalsMonitorSocketUrl } from "../../VitalsMonitor/utils"; 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")); @@ -363,91 +364,10 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => { )}
)} - {props.consultationData.symptoms_text && ( -
-
-

- Symptoms -

-
-
- Last Daily Update -
- {props.consultationData.last_daily_round - ?.additional_symptoms && ( - <> -
- {props.consultationData.last_daily_round?.additional_symptoms.map( - (symptom: any, index: number) => ( - choice.id === symptom, - )?.text ?? "Err. Unknown" - } - size="small" - /> - ), - )} -
- {props.consultationData.last_daily_round - ?.other_symptoms && ( -
-
- Other Symptoms: -
- { - props.consultationData.last_daily_round - ?.other_symptoms - } -
- )} - - from{" "} - {formatDate( - props.consultationData.last_daily_round.taken_at, - )} - - - )} -
-
- Consultation Update -
-
- {props.consultationData.symptoms?.map( - (symptom, index) => ( - choice.id === symptom, - )?.text ?? "Err. Unknown" - } - size="small" - /> - ), - )} -
- {props.consultationData.other_symptoms && ( -
-
- Other Symptoms: -
- {props.consultationData.other_symptoms} -
- )} - - from{" "} - {props.consultationData.symptoms_onset_date - ? formatDate(props.consultationData.symptoms_onset_date) - : "--/--/----"} - -
-
-
- )} + +
+ +
{props.consultationData.history_of_present_illness && (
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 b7b99d85448..594769ec1c4 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[]; 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 = (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, @@ -1025,41 +1018,48 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
)} -
- -
- {isOtherSymptomsSelected && ( -
- -
- )} +
+
+ Symptoms - {hasSymptoms && ( -
- + {!isUpdate && ( + + )} + +
+ {isUpdate ? ( + + ) : ( + { + handleFormFieldChange({ + name: "create_symptoms", + value: symptoms, + }); + }} + /> + )} + +
- )} +
+
; - symptoms_text?: string; - symptoms_onset_date?: string; consultation_notes?: string; is_telemedicine?: boolean; procedure?: ProcedureType[]; diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx index bed6a41f3c0..3bdbffdc6cb 100644 --- a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx +++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx @@ -84,7 +84,7 @@ export const AutocompleteMutliSelect = ( return { option, label, - description: props.optionDescription && props.optionDescription(option), + description: props.optionDescription?.(option), search: label.toLowerCase(), value: (props.optionValue ? props.optionValue(option) : option) as V, }; diff --git a/src/Components/Patient/DailyRoundListDetails.tsx b/src/Components/Patient/DailyRoundListDetails.tsx index 3c5e70044ed..66536c986ca 100644 --- a/src/Components/Patient/DailyRoundListDetails.tsx +++ b/src/Components/Patient/DailyRoundListDetails.tsx @@ -1,5 +1,5 @@ import { lazy, useState } from "react"; -import { CONSCIOUSNESS_LEVEL, SYMPTOM_CHOICES } from "../../Common/constants"; +import { CONSCIOUSNESS_LEVEL } from "../../Common/constants"; import { DailyRoundsModel } from "./models"; import Page from "../Common/components/Page"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -7,7 +7,6 @@ import { formatDateTime } from "../../Utils/utils"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; const Loading = lazy(() => import("../Common/Loading")); -const symptomChoices = [...SYMPTOM_CHOICES]; export const DailyRoundListDetails = (props: any) => { const { facilityId, patientId, consultationId, id } = props; @@ -21,16 +20,8 @@ export const DailyRoundListDetails = (props: any) => { const tdata: DailyRoundsModel = { ...data, temperature: Number(data.temperature) ? data.temperature : "", - additional_symptoms_text: "", medication_given: data.medication_given ?? [], }; - if (data.additional_symptoms?.length) { - const symptoms = data.additional_symptoms.map((symptom: number) => { - const option = symptomChoices.find((i) => i.id === symptom); - return option ? option.text.toLowerCase() : symptom; - }); - tdata.additional_symptoms_text = symptoms.join(", "); - } setDailyRoundListDetails(tdata); } }, @@ -85,12 +76,6 @@ export const DailyRoundListDetails = (props: any) => { SpO2: {dailyRoundListDetailsData.ventilator_spo2 ?? "-"}
-
- - Additional Symptoms:{" "} - - {dailyRoundListDetailsData.additional_symptoms_text ?? "-"} -
Admitted To *:{" "} @@ -103,12 +88,6 @@ export const DailyRoundListDetails = (props: any) => { {dailyRoundListDetailsData.physical_examination_info ?? "-"}
-
- - Other Symptoms:{" "} - - {dailyRoundListDetailsData.other_symptoms ?? "-"} -
Other Details:{" "} diff --git a/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index dd15d44facc..66ebdca4fd2 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,11 @@ import routes from "../../Redux/api"; import { Scribe } from "../Scribe/Scribe"; import { DAILY_ROUND_FORM_SCRIBE_DATA } from "../Scribe/formDetails"; import { DailyRoundsModel } from "./models"; +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: "", @@ -120,7 +119,6 @@ export const DailyRounds = (props: any) => { const formFields = [ "physical_examination_info", "other_details", - "additional_symptoms", "action", "review_interval", "bp", @@ -201,15 +199,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) { @@ -237,11 +226,6 @@ export const DailyRounds = (props: any) => { 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 @@ -436,22 +420,11 @@ export const DailyRounds = (props: any) => { label="Other Details" rows={5} /> - - {state.form.additional_symptoms?.includes(9) && ( -
- -
- )} +
+ Symptoms + +
; medication_given?: Array; - additional_symptoms_text?: string; action?: string; review_interval?: number; id?: string; - other_symptoms?: string; admitted_to?: string; patient_category?: PatientCategory; output?: DailyRoundsOutput[]; 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..4d142a67841 --- /dev/null +++ b/src/Components/Symptoms/SymptomsBuilder.tsx @@ -0,0 +1,379 @@ +import { useState } from "react"; +import { Writable } from "../../Utils/types"; +import { + EncounterSymptom, + OTHER_SYMPTOM_CHOICE, + SYMPTOM_CHOICES, +} from "./types"; +import AutocompleteMultiSelectFormField from "../Form/FormFields/AutocompleteMultiselect"; +import DateFormField from "../Form/FormFields/DateFormField"; +import ButtonV2 from "../Common/components/ButtonV2"; +import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import { classNames, dateQueryString } from "../../Utils/utils"; +import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import SymptomsApi from "./api"; +import request from "../../Utils/request/request"; +import { Success } from "../../Utils/Notifications"; +import { sortByOnsetDate } from "./utils"; + +export const CreateSymptomsBuilder = (props: { + value: Writable[]; + onChange: (value: Writable[]) => void; +}) => { + return ( +
+
    + {props.value.map((obj, index, arr) => { + const handleUpdate = (event: FieldChangeEvent) => { + const updated = { ...obj, [event.name]: event.value }; + props.onChange(arr.map((old, i) => (i === index ? updated : old))); + }; + + const handleRemove = () => { + props.onChange(arr.filter((_, i) => i !== index)); + }; + + return ( +
  • + +
  • + ); + })} +
+ + {props.value.length === 0 && ( +
+ No symptoms added +
+ )} + +
+ props.onChange([...props.value, ...objects])} + /> +
+
+ ); +}; + +export const EncounterSymptomsBuilder = (props: { showAll?: boolean }) => { + const consultationId = useSlug("consultation"); + + const [isProcessing, setIsProcessing] = useState(false); + const { data, loading, refetch } = useQuery(SymptomsApi.list, { + pathParams: { consultationId }, + query: { limit: 100 }, + }); + + if (!data) { + return ( +
+ + Fetching symptom records... +
+ ); + } + + let items = sortByOnsetDate(data.results); + if (!props.showAll) { + items = items.filter( + (i) => i.clinical_impression_status !== "entered-in-error", + ); + } + + return ( +
+
    + {items.map((symptom) => { + const handleUpdate = async (event: FieldChangeEvent) => { + setIsProcessing(true); + await request(SymptomsApi.partialUpdate, { + pathParams: { consultationId, external_id: symptom.id }, + body: { [event.name]: event.value }, + }); + await refetch(); + setIsProcessing(false); + }; + + const handleMarkAsEnteredInError = async () => { + setIsProcessing(true); + await request(SymptomsApi.markAsEnteredInError, { + pathParams: { consultationId, external_id: symptom.id }, + }); + await refetch(); + setIsProcessing(false); + }; + + return ( +
  • + +
  • + ); + })} +
+ + {items.length === 0 && ( +
+ Patient is Asymptomatic +
+ )} + +
+ refetch()} + /> +
+
+ ); +}; + +const SymptomEntry = (props: { + disabled?: boolean; + value: Writable | EncounterSymptom; + onChange: (event: FieldChangeEvent) => void; + onRemove: () => void; +}) => { + const symptom = props.value; + const disabled = + props.disabled || symptom.clinical_impression_status === "entered-in-error"; + return ( +
+ + +
+
+ + + + {symptom.clinical_impression_status === "entered-in-error" && ( + + Entered in Error + + )} +
+ + + +
+
+ ); +}; + +const AddSymptom = (props: { + disabled?: boolean; + existing: (Writable | EncounterSymptom)[]; + onAdd?: (value: Writable[]) => void; + consultationId?: string; +}) => { + const [processing, setProcessing] = useState(false); + const [selected, setSelected] = useState([]); + const [otherSymptom, setOtherSymptom] = useState(""); + const [onsetDate, setOnsetDate] = useState(); + + const activeSymptomIds = props.existing + .filter((o) => o.symptom !== OTHER_SYMPTOM_CHOICE.id && !o.cure_date) + .map((o) => o.symptom); + + const handleAdd = async () => { + const objects = selected.map((symptom) => { + return { + symptom, + onset_date: dateQueryString(onsetDate), + other_symptom: + symptom === OTHER_SYMPTOM_CHOICE.id ? otherSymptom : undefined, + }; + }); + + if (props.consultationId) { + const responses = await Promise.all( + objects.map((body) => + request(SymptomsApi.add, { + body, + pathParams: { consultationId: props.consultationId! }, + }), + ), + ); + + if (responses.every(({ res }) => !!res?.ok)) { + Success({ msg: "Symptoms records updated successfully" }); + } + } + props.onAdd?.(objects); + + setSelected([]); + setOtherSymptom(""); + }; + + const hasSymptoms = !!selected.length; + const otherSymptomValid = selected.includes(OTHER_SYMPTOM_CHOICE.id) + ? !!otherSymptom.trim() + : true; + + return ( +
+ setOnsetDate(value)} + errorClassName="hidden" + /> +
+ setSelected(e.value)} + options={SYMPTOM_CHOICES.filter( + ({ id }) => !activeSymptomIds.includes(id), + )} + optionLabel={(option) => option.text} + optionValue={(option) => option.id} + errorClassName="hidden" + /> + {selected.includes(OTHER_SYMPTOM_CHOICE.id) && ( + setOtherSymptom(value)} + errorClassName="hidden" + /> + )} +
+ { + setProcessing(true); + await handleAdd(); + setProcessing(false); + }} + > + {processing ? ( + <> + + Adding... + + ) : ( + Add Symptom(s) + )} + +
+ ); +}; + +export const SymptomText = (props: { + value: Writable | EncounterSymptom; +}) => { + const symptom = + SYMPTOM_CHOICES.find(({ id }) => props.value.symptom === id) || + OTHER_SYMPTOM_CHOICE; + + const isOtherSymptom = symptom.id === OTHER_SYMPTOM_CHOICE.id; + + return isOtherSymptom ? ( + <> + Other: + + {props.value.other_symptom || "Not specified"} + + + ) : ( + symptom.text + ); +}; diff --git a/src/Components/Symptoms/SymptomsCard.tsx b/src/Components/Symptoms/SymptomsCard.tsx new file mode 100644 index 00000000000..3ea124c17b2 --- /dev/null +++ b/src/Components/Symptoms/SymptomsCard.tsx @@ -0,0 +1,84 @@ +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import { SymptomText } from "./SymptomsBuilder"; +import SymptomsApi from "./api"; +import { type EncounterSymptom } from "./types"; +import { groupAndSortSymptoms } from "./utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +// TODO: switch to list from events as timeline view instead once filter event by event type name is done +const EncounterSymptomsCard = () => { + const consultationId = useSlug("consultation"); + + const { data } = useQuery(SymptomsApi.list, { + pathParams: { consultationId }, + query: { limit: 100 }, + }); + + if (!data) { + return ( +
+ + Fetching symptom records... +
+ ); + } + + const records = groupAndSortSymptoms(data.results); + + return ( +
+

+ Symptoms +

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

+ {props.title} +

+
    + {props.symptoms.map((record) => ( +
  • +
    + +
    + + + Onset + + {record.cure_date && ( + + {"; Cured"} + + )} + +
    +
  • + ))} +
+ {!props.symptoms.length && ( +
+ No symptoms +
+ )} +
+ ); +}; + +export default EncounterSymptomsCard; diff --git a/src/Components/Symptoms/api.ts b/src/Components/Symptoms/api.ts new file mode 100644 index 00000000000..1cce062cb1e --- /dev/null +++ b/src/Components/Symptoms/api.ts @@ -0,0 +1,47 @@ +import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { WritableOnly } from "../../Utils/types"; +import { EncounterSymptom } from "./types"; + +const SymptomsApi = { + list: { + method: "GET", + path: "/api/v1/consultation/{consultationId}/symptoms/", + TRes: Type>(), + }, + + add: { + path: "/api/v1/consultation/{consultationId}/symptoms/", + method: "POST", + TRes: Type(), + TBody: Type>(), + }, + + retrieve: { + method: "GET", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TRes: Type(), + }, + + update: { + method: "PUT", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TBody: Type>(), + TRes: Type(), + }, + + partialUpdate: { + method: "PATCH", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TBody: Type>>(), + TRes: Type(), + }, + + markAsEnteredInError: { + method: "DELETE", + path: "/api/v1/consultation/{consultationId}/symptoms/{external_id}/", + TRes: Type(), + }, +} as const; + +export default SymptomsApi; diff --git a/src/Components/Symptoms/types.ts b/src/Components/Symptoms/types.ts new file mode 100644 index 00000000000..78e769ce9a9 --- /dev/null +++ b/src/Components/Symptoms/types.ts @@ -0,0 +1,52 @@ +import { BaseModel } from "../../Utils/types"; + +export const OTHER_SYMPTOM_CHOICE = { id: 9, text: "Other Symptom" } as const; + +export const SYMPTOM_CHOICES = [ + { id: 2, text: "Fever" }, + { id: 3, text: "Sore throat" }, + { id: 4, text: "Cough" }, + { id: 5, text: "Breathlessness" }, + { id: 6, text: "Myalgia" }, + { id: 7, text: "Abdominal discomfort" }, + { id: 8, text: "Vomiting" }, + { id: 11, text: "Sputum" }, + { id: 12, text: "Nausea" }, + { id: 13, text: "Chest pain" }, + { id: 14, text: "Hemoptysis" }, + { id: 15, text: "Nasal discharge" }, + { id: 16, text: "Body ache" }, + { id: 17, text: "Diarrhoea" }, + { id: 18, text: "Pain" }, + { id: 19, text: "Pedal Edema" }, + { id: 20, text: "Wound" }, + { id: 21, text: "Constipation" }, + { id: 22, text: "Head ache" }, + { id: 23, text: "Bleeding" }, + { id: 24, text: "Dizziness" }, + { id: 25, text: "Chills" }, + { id: 26, text: "General weakness" }, + { id: 27, text: "Irritability" }, + { id: 28, text: "Confusion" }, + { id: 29, text: "Abdominal pain" }, + { id: 30, text: "Join pain" }, + { id: 31, text: "Redness of eyes" }, + { id: 32, text: "Anorexia" }, + { id: 33, text: "New loss of taste" }, + { id: 34, text: "New loss of smell" }, + OTHER_SYMPTOM_CHOICE, +] as const; + +type ClinicalImpressionStatus = + | "in-progress" + | "completed" + | "entered-in-error"; + +export interface EncounterSymptom extends BaseModel { + symptom: (typeof SYMPTOM_CHOICES)[number]["id"]; + other_symptom?: string | null; + onset_date: string; + cure_date?: string | null; + readonly clinical_impression_status: ClinicalImpressionStatus; + readonly is_migrated: boolean; +} diff --git a/src/Components/Symptoms/utils.ts b/src/Components/Symptoms/utils.ts new file mode 100644 index 00000000000..997acfc914b --- /dev/null +++ b/src/Components/Symptoms/utils.ts @@ -0,0 +1,37 @@ +import { Writable } from "../../Utils/types"; +import { compareByDateString } from "../../Utils/utils"; +import { EncounterSymptom } from "./types"; + +// TODO: switch to using Object.groupBy(...) instead once upgraded to node v22 +export const groupAndSortSymptoms = < + T extends Writable | EncounterSymptom, +>( + records: T[], +) => { + const result: Record = { + "entered-in-error": [], + "in-progress": [], + completed: [], + }; + + for (const record of records) { + const status = + record.clinical_impression_status || + (record.cure_date ? "completed" : "in-progress"); + result[status].push(record); + } + + result["completed"] = sortByOnsetDate(result["completed"]); + result["in-progress"] = sortByOnsetDate(result["in-progress"]); + result["entered-in-error"] = sortByOnsetDate(result["entered-in-error"]); + + return result; +}; + +export const sortByOnsetDate = < + T extends Writable | EncounterSymptom, +>( + records: T[], +) => { + return records.sort(compareByDateString("onset_date")); +}; diff --git a/src/Utils/types.ts b/src/Utils/types.ts new file mode 100644 index 00000000000..519ede36ffc --- /dev/null +++ b/src/Utils/types.ts @@ -0,0 +1,36 @@ +import { PerformedByModel } from "../Components/HCX/misc"; + +export interface BaseModel { + readonly id: string; + readonly modified_date: string; + readonly created_date: string; + readonly created_by: PerformedByModel; + readonly updated_by: PerformedByModel; +} + +export type Writable = { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + never, + P + >]?: undefined; +} & { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + P, + never + >]: T[P]; +}; + +export type WritableOnly = { + [P in keyof T as IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + P + >]: T[P]; +}; + +type IfEquals = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B; diff --git a/src/Utils/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 = (key: keyof T) => { }; }; +export const compareByDateString = (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 ?? ""); From fb9ba0e3949d2bd6c29c03b8aebfb5bc45f5df37 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 28 May 2024 17:01:54 +0530 Subject: [PATCH 21/24] Adds support for Doctors Log Update round type (v1) (#7839) * Adds support for Doctors Log Update round type (except syptoms) * Adds Prescription and Diagnosis * fix investigations not working * fix investigations not working * fixes during QA * disable disable save for doctors log --------- Co-authored-by: Khavin Shankar --- .../ConsultationDetails/Events/types.ts | 18 ++ src/Components/Patient/DailyRounds.tsx | 219 +++++++++++++++--- src/Components/Patient/models.tsx | 1 + src/Redux/api.tsx | 13 +- 4 files changed, 214 insertions(+), 37 deletions(-) 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/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index 66ebdca4fd2..5675b56da6e 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -32,6 +32,17 @@ 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")); @@ -47,6 +58,8 @@ const initForm: any = { taken_at: null, rounds_type: "NORMAL", systolic: null, + investigations: [], + investigations_dirty: false, diastolic: null, pulse: null, resp: null, @@ -97,6 +110,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( @@ -113,6 +127,7 @@ export const DailyRounds = (props: any) => { ...initForm, action: "", }); + const [diagnoses, setDiagnoses] = useState(); const headerText = !id ? "Add Consultation Update" : "Info"; const buttonText = !id ? "Save" : "Continue"; @@ -124,6 +139,7 @@ export const DailyRounds = (props: any) => { "bp", "pulse", "resp", + "investigations", "ventilator_spo2", "rhythm", "rhythm_detail", @@ -132,6 +148,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, { @@ -163,6 +180,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), ); @@ -174,7 +198,11 @@ export const DailyRounds = (props: any) => { ...initialData, action: getAction, }); - formData = { ...formData, ...{ action: getAction } }; + formData = { + ...formData, + action: getAction, + investigations: data.last_consultation?.investigation ?? [], + }; } } else { setPatientName(""); @@ -207,6 +235,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; } @@ -220,6 +275,25 @@ 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, @@ -282,14 +356,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`, ); @@ -300,10 +384,16 @@ export const DailyRounds = (props: any) => { }; const handleFormFieldChange = (event: FieldChangeEvent) => { - dispatch({ - type: "set_form", - form: { ...state.form, [event.name]: event.value }, - }); + const form = { + ...state.form, + [event.name]: event.value, + }; + + if (event.name === "investigations") { + form["investigations_dirty"] = true; + } + + dispatch({ type: "set_form", form }); }; const field = (name: string) => { @@ -390,6 +480,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" @@ -426,34 +517,40 @@ export const DailyRounds = (props: any) => {
- option.desc} - optionValue={(option) => option.text} - value={prevAction} - onChange={(event) => { - handleFormFieldChange(event); - setPreviousAction(event.value); - }} - /> + {state.form.rounds_type !== "DOCTORS_LOG" && ( + <> + option.desc} + optionValue={(option) => option.text} + value={prevAction} + onChange={(event) => { + handleFormFieldChange(event); + setPreviousAction(event.value); + }} + /> - option.text} - optionValue={(option) => option.id} - value={prevReviewInterval} - onChange={(event) => { - handleFormFieldChange(event); - setPreviousReviewInterval(Number(event.value)); - }} - /> + option.text} + optionValue={(option) => option.id} + value={prevReviewInterval} + onChange={(event) => { + handleFormFieldChange(event); + setPreviousReviewInterval(Number(event.value)); + }} + /> + + )} - {["NORMAL", "TELEMEDICINE"].includes(state.form.rounds_type) && ( + {["NORMAL", "TELEMEDICINE", "DOCTORS_LOG"].includes( + state.form.rounds_type, + ) && ( <>

Vitals

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

+ {t("investigations")} +

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

+ {t("prescription_medications")} +

+ +
+
+

+ {t("prn_prescriptions")} +

+ +
+
+

+ {t("diagnosis")} +

+ {/* */} + {diagnoses ? ( + + ) : ( +
+ Fetching existing diagnosis of patient... +
+ )} +
+
+ + )}
@@ -580,11 +724,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/models.tsx b/src/Components/Patient/models.tsx index 9ca01fcc63f..f64d71146c4 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -276,6 +276,7 @@ export interface DailyRoundsOutput { export const DailyRoundTypes = [ "NORMAL", + "DOCTORS_LOG", "VENTILATOR", "AUTOMATED", "TELEMEDICINE", diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 58147f68943..f4a9732a40c 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -97,7 +97,10 @@ import { import { PaginatedResponse } from "../Utils/request/types"; import { ICD11DiagnosisModel } from "../Components/Diagnosis/types"; -import { EventGeneric } from "../Components/Facility/ConsultationDetails/Events/types"; +import { + EventGeneric, + type Type, +} from "../Components/Facility/ConsultationDetails/Events/types"; import { InvestigationGroup, InvestigationType, @@ -628,6 +631,14 @@ const routes = { TRes: Type(), }, + // Event Types + + listEventTypes: { + path: "/api/v1/event_types/", + method: "GET", + TRes: Type>(), + }, + // Hospital Beds createCapacity: { path: "/api/v1/facility/{facilityId}/capacity/", From 6b75578dc750ef779c754edd842d35a5b19012df Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 28 May 2024 19:12:48 +0530 Subject: [PATCH 22/24] Adds cypress tests (#7930) --- .../patient_spec/patient_consultation.cy.ts | 30 ++++++++----------- .../e2e/patient_spec/patient_logupdate.cy.ts | 22 +++++++------- .../pageobject/Patient/PatientConsultation.ts | 12 ++++---- .../pageobject/Patient/PatientLogupdate.ts | 10 +++++++ src/Components/Patient/DailyRounds.tsx | 3 +- src/Components/Symptoms/SymptomsBuilder.tsx | 2 ++ src/Components/Symptoms/SymptomsCard.tsx | 2 +- 7 files changed, 44 insertions(+), 37 deletions(-) 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/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/src/Components/Patient/DailyRounds.tsx b/src/Components/Patient/DailyRounds.tsx index 5675b56da6e..d0b2e321898 100644 --- a/src/Components/Patient/DailyRounds.tsx +++ b/src/Components/Patient/DailyRounds.tsx @@ -45,6 +45,7 @@ import { } from "../Diagnosis/types"; import { EncounterSymptomsBuilder } from "../Symptoms/SymptomsBuilder"; import { FieldLabel } from "../Form/FormFields/FormField"; + const Loading = lazy(() => import("../Common/Loading")); const initForm: any = { @@ -336,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( diff --git a/src/Components/Symptoms/SymptomsBuilder.tsx b/src/Components/Symptoms/SymptomsBuilder.tsx index 4d142a67841..de6901d8384 100644 --- a/src/Components/Symptoms/SymptomsBuilder.tsx +++ b/src/Components/Symptoms/SymptomsBuilder.tsx @@ -283,6 +283,7 @@ const AddSymptom = (props: { { const records = groupAndSortSymptoms(data.results); return ( -
+

Symptoms

From cf5f1c3edfeefe7f40f000030f73f2ad286d0ef4 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 29 May 2024 11:43:58 +0530 Subject: [PATCH 23/24] fixes up/down arrow of numeric inputs not clickable in facility create and other places (#7931) * fixes up/down arrow of numeric inputs not clickable in facility create and other places * fix wight and height --- src/Components/Facility/ConsultationForm.tsx | 4 ++-- src/Components/Facility/FacilityCreate.tsx | 2 +- src/Components/Patient/PatientRegister.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index 594769ec1c4..d362abe1ecc 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -1103,7 +1103,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { placeholder="Weight" trailingPadding=" " trailing={ -

+

Weight (kg)

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

+

Height (cm)

} diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index 1fd51940f8f..383a0646958 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -1001,5 +1001,5 @@ export const FacilityCreate = (props: FacilityProps) => { }; const FieldUnit = ({ unit }: { unit: string }) => { - return

{unit}

; + return

{unit}

; }; diff --git a/src/Components/Patient/PatientRegister.tsx b/src/Components/Patient/PatientRegister.tsx index 34d2ac66387..7d219514267 100644 --- a/src/Components/Patient/PatientRegister.tsx +++ b/src/Components/Patient/PatientRegister.tsx @@ -1345,8 +1345,9 @@ export const PatientRegister = (props: PatientRegisterProps) => { +

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

} placeholder="Enter the age" - className="col-span-6 sm:col-span-3" type="number" min={0} /> From 8ae85d6089920b815dc81fc787925d879ffe1755 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 29 May 2024 11:45:27 +0530 Subject: [PATCH 24/24] fixes overflow issues in consultation dashboard (#7932) * fixes overflow issues in consultation dashboard * Update src/Components/Common/components/Menu.tsx --- .../AdministrationTable.tsx | 4 ++-- src/Components/Patient/PatientInfoCard.tsx | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx index 98fa1021f9f..49a071b744b 100644 --- a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx @@ -24,7 +24,7 @@ export default function MedicineAdministrationTable({ return (
- +
@@ -51,7 +51,7 @@ export default function MedicineAdministrationTable({ disabled={!pagination.hasPrevious} onClick={pagination.previous} tooltip="Previous 24 hours" - tooltipClassName="tooltip-bottom -translate-x-1/2 text-xs" + tooltipClassName="tooltip-bottom text-xs" > diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index ba9280e903d..247d9312d6e 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -185,9 +185,9 @@ export default function PatientInfoCard(props: { )} -
+
{/* Can support for patient picture in the future */} @@ -269,7 +269,7 @@ export default function PatientInfoCard(props: {
-
+
{medicoLegalCase && ( - + MLC )} @@ -301,7 +301,7 @@ export default function PatientInfoCard(props: {
{consultation?.patient_no && ( @@ -520,12 +520,12 @@ export default function PatientInfoCard(props: {
{consultation?.suggestion === "A" && (
-
+
)} {consultation?.last_daily_round && ( -
+
)} @@ -633,7 +633,7 @@ export default function PatientInfoCard(props: { } className="xl:justify-center" containerClassName="w-full lg:w-auto mt-2 2xl:mt-0 flex justify-center z-20"