From e5ecbe4f9b6a6bb082bdf92ab4320f14733ef9ee Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Tue, 21 May 2024 12:25:11 +0530 Subject: [PATCH 01/13] . --- src/Components/Facility/ConsultationForm.tsx | 216 +--------------- src/Components/Facility/models.tsx | 47 ++-- .../Patient/PatientConsentRecords.tsx | 237 ++++++++++++++++++ src/Components/Patient/PatientInfoCard.tsx | 136 +++++----- src/Routers/routes/ConsultationRoutes.tsx | 4 + 5 files changed, 348 insertions(+), 292 deletions(-) create mode 100644 src/Components/Patient/PatientConsentRecords.tsx diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index ba248b8db6f..8ea9a2a7dc6 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -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"; @@ -71,13 +69,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; @@ -126,7 +117,6 @@ type FormDetails = { death_confirmed_doctor: string; InvestigationAdvice: InvestigationType[]; procedures: ProcedureType[]; - consent_records: ConsentRecord[]; }; const initForm: FormDetails = { @@ -177,7 +167,6 @@ const initForm: FormDetails = { death_confirmed_doctor: "", InvestigationAdvice: [], procedures: [], - consent_records: [], }; const initError = Object.assign( @@ -228,7 +217,6 @@ type ConsultationFormSection = | "Consultation Details" | "Diagnosis" | "Treatment Plan" - | "Consent Records" | "Bed Status"; type Props = { @@ -261,14 +249,9 @@ 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(); @@ -288,11 +271,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, @@ -305,7 +283,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; }); @@ -313,7 +290,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { consultationDetailsVisible, diagnosisVisible, treatmentPlanVisible, - consentRecordsVisible, bedStatusVisible, ]); @@ -408,7 +384,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { admitted_to: data.admitted_to ? data.admitted_to : "", category: data.category ? PATIENT_CATEGORIES.find((i) => i.text === data.category)?.id ?? - "" + "" : "", patient_no: data.patient_no ?? "", OPconsultation: data.consultation_notes, @@ -738,12 +714,12 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { : undefined, referred_from_facility: state.form.route_to_facility === 20 && - !state.form.referred_from_facility_external + !state.form.referred_from_facility_external ? state.form.referred_from_facility : undefined, referred_from_facility_external: state.form.route_to_facility === 20 && - !state.form.referred_from_facility + !state.form.referred_from_facility ? state.form.referred_from_facility_external : undefined, referred_by_external: @@ -771,7 +747,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( @@ -919,64 +894,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) + ["Bed Status"].includes(sectionTitle) ) { return null; } @@ -1006,9 +923,8 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => { const section = sections[sectionTitle as ConsultationFormSection]; return (
- {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 d61d5a9f129..63a448ae331 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; @@ -101,6 +102,14 @@ export type PatientCategory = | "Abnormal" | "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; @@ -432,15 +441,15 @@ export type VentilatorPlotRes = { export interface DailyRoundsBody { page?: number; fields: - | ABGPlotsFields[] - | DialysisPlotsFields[] - | NeurologicalTablesFields[] - | NursingPlotFields[] - | NutritionPlotsFields[] - | PainDiagramsFields[] - | PressureSoreDiagramsFields[] - | PrimaryParametersPlotFields[] - | VentilatorPlotFields[]; + | ABGPlotsFields[] + | DialysisPlotsFields[] + | NeurologicalTablesFields[] + | NursingPlotFields[] + | NutritionPlotsFields[] + | PainDiagramsFields[] + | PressureSoreDiagramsFields[] + | PrimaryParametersPlotFields[] + | VentilatorPlotFields[]; } export interface DailyRoundsRes { @@ -448,15 +457,15 @@ export interface DailyRoundsRes { page_size: number; results: { [date: string]: - | PressureSoreDiagramsRes - | ABGPlotsRes - | DialysisPlotsRes - | NeurologicalTablesRes - | NursingPlotRes - | NutritionPlotsRes - | PainDiagramsRes - | PrimaryParametersPlotRes - | VentilatorPlotRes; + | PressureSoreDiagramsRes + | ABGPlotsRes + | DialysisPlotsRes + | NeurologicalTablesRes + | NursingPlotRes + | NutritionPlotsRes + | PainDiagramsRes + | PrimaryParametersPlotRes + | VentilatorPlotRes; }; } diff --git a/src/Components/Patient/PatientConsentRecords.tsx b/src/Components/Patient/PatientConsentRecords.tsx new file mode 100644 index 00000000000..cb0c1fbbbbf --- /dev/null +++ b/src/Components/Patient/PatientConsentRecords.tsx @@ -0,0 +1,237 @@ +import { 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 { FieldChangeEventHandler } from "../Form/FormFields/Utils"; +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 { FileUpload } from "./FileUpload"; +import { formatDateTime } from "../../Utils/utils"; + +export default function PatientConsentRecords(props: { + facilityId: string, + patientId: string, + consultationId: string, +}) { + + const { facilityId, patientId, consultationId } = props + + const { data: patient, loading: patientLoading, refetch: patientRefresh } = useQuery(routes.getPatient, { + pathParams: { + id: patientId, + }, + }); + const { data: consultation, loading: consultationLoading, refetch } = useQuery( + routes.getConsultation, + { + pathParams: { id: consultationId! }, + onResponse: (data) => { + if (data.data && data.data.consent_records) { + setConsentRecords(data.data.consent_records); + } + } + }, + ); + + const [collapsedConsentRecords, setCollapsedConsentRecords] = useState< + number[] + >([]); + const [showDeleteConsent, setShowDeleteConsent] = useState( + null, + ); + + const [consentRecords, setConsentRecords] = useState(null); + + + const handleConsentTypeChange: FieldChangeEventHandler = async ( + event, + ) => { + if (!consultationId || !consentRecords) return; + 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: consultationId }, + body: { consent_records: newRecords }, + }); + setConsentRecords(newRecords); + } + }; + + const handleConsentPCSChange: FieldChangeEventHandler = (event) => { + if (!consentRecords) return; + setConsentRecords(consentRecords.map((cr) => + cr.type === 2 ? { ...cr, patient_code_status: event.value } : cr, + )); + }; + + 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, + ); + await request(routes.partialUpdateConsultation, { + pathParams: { id: consultationId }, + body: { consent_records: newRecords }, + }); + 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, + }; + }; + + return ( + + 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" + /> +
+
+

+ Add New Record +

+ + !consentRecords + ?.filter((r) => r.deleted !== true) + .map((record) => record.type) + .includes(c.id), + )} + /> +
+
+ +
+ {consentRecords + ?.filter((record) => record.deleted !== true) + .map((record, index) => ( +
+
+ + +
+
+
+ {record.type === 2 && ( + + )} +
+ +
+
+ ))} +
+ +
+ ) +} \ No newline at end of file diff --git a/src/Components/Patient/PatientInfoCard.tsx b/src/Components/Patient/PatientInfoCard.tsx index cd99d169544..0549ed61b8e 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -171,7 +171,7 @@ export default function PatientInfoCard(props: { className={`w-24 min-w-20 bg-gray-200 ${categoryClass}-profile h-full`} > {consultation?.current_bed && - consultation?.discharge_date === null ? ( + consultation?.discharge_date === null ? (

{ @@ -279,9 +279,8 @@ export default function PatientInfoCard(props: { {consultation?.patient_no && ( - {`${consultation?.suggestion === "A" ? "IP" : "OP"}: ${ - consultation?.patient_no - }`} + {`${consultation?.suggestion === "A" ? "IP" : "OP"}: ${consultation?.patient_no + }`} )} @@ -424,45 +423,45 @@ export default function PatientInfoCard(props: {

{consultation?.diagnoses?.length ? (() => { - const principal_diagnosis = consultation.diagnoses.find( - (diagnosis) => diagnosis.is_principal, - ); - return principal_diagnosis ? ( -
-
- Principal Diagnosis: -
-
- {principal_diagnosis.diagnosis_object?.label ?? "-"}{" "} - - -

- {principal_diagnosis.verification_status} -

-
-
+ const principal_diagnosis = consultation.diagnoses.find( + (diagnosis) => diagnosis.is_principal, + ); + return principal_diagnosis ? ( +
+
+ Principal Diagnosis: +
+
+ {principal_diagnosis.diagnosis_object?.label ?? "-"}{" "} + + +

+ {principal_diagnosis.verification_status} +

+
- ) : null; - })() +
+ ) : null; + })() : null} {(consultation?.treating_physician_object || consultation?.deprecated_verified_by) && ( -
- - {t("treating_doctor")}:{" "} - - {consultation?.treating_physician_object - ? `${consultation?.treating_physician_object.first_name} ${consultation?.treating_physician_object.last_name}` - : consultation?.deprecated_verified_by} - -
- )} +
+ + {t("treating_doctor")}:{" "} + + {consultation?.treating_physician_object + ? `${consultation?.treating_physician_object.first_name} ${consultation?.treating_physician_object.last_name}` + : consultation?.deprecated_verified_by} + +
+ )}
@@ -532,10 +531,10 @@ export default function PatientInfoCard(props: { { close(); navigate( - `/shifting/${ - activeShiftingData[ - activeShiftingData.length - 1 - ].id + `/shifting/${activeShiftingData[ + activeShiftingData.length - 1 + ].id }`, ); }} @@ -832,10 +836,9 @@ export default function PatientInfoCard(props: { {({ close }) => (
{ if (!consultation?.discharge_date) { close(); @@ -846,11 +849,10 @@ export default function PatientInfoCard(props: {

{t("discharge_from_care")}

diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx index 6dc5fa9c05d..dc76882a397 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,9 @@ export default { }: any) => ( ), + "/facility/:facilityId/patient/:patientId/consultation/:id/consent-records": ({ facilityId, patientId, id }: any) => ( + + ), "/facility/:facilityId/patient/:patientId/consultation/:id/files/": ({ facilityId, patientId, From 0d82bcde2b75261d98aa8014f1b72ccdb6ec62e4 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Wed, 22 May 2024 18:44:48 +0530 Subject: [PATCH 02/13] completed patient consent records --- src/Common/constants.tsx | 2 +- src/Components/Patient/FileUpload.tsx | 5 +- .../Patient/PatientConsentRecordBlock.tsx | 87 +++ .../Patient/PatientConsentRecords.tsx | 524 +++++++++++------- src/Components/Patient/PatientHome.tsx | 11 +- src/Components/Patient/PatientInfoCard.tsx | 144 ++--- src/Components/Patient/models.tsx | 2 +- src/Utils/useFileUpload.tsx | 446 +++++++++++++++ 8 files changed, 941 insertions(+), 280 deletions(-) create mode 100644 src/Components/Patient/PatientConsentRecordBlock.tsx create mode 100644 src/Utils/useFileUpload.tsx diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 3d6352b3260..1d539749406 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -1315,7 +1315,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/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..3a322267e0e --- /dev/null +++ b/src/Components/Patient/PatientConsentRecordBlock.tsx @@ -0,0 +1,87 @@ +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"; + +export default function PatientConsentRecordBlockGroup(props: { + consentRecord: ConsentRecord; + previewFile: (file_id: string, file_associating_id: string) => void; + onDelete: (consentRecord: ConsentRecord) => void; + refreshTrigger: any; +}) { + const { consentRecord, previewFile } = props; + + const filesQuery = useQuery(routes.viewUpload, { + query: { + file_type: "CONSENT_RECORD", + associating_id: consentRecord.id, + is_archived: false, + limit: 30, + 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, + ); + + useEffect(() => { + filesQuery.refetch(); + }, [props.refreshTrigger]); + + return ( +
+
+

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

+ +
+ + {filesQuery?.data?.results.map((file: FileUploadModel, i: number) => ( +
+
+
+ +
+
+
+ {file.name} + {file.extension} {file.is_archived && "(Archived)"} +
+
+ {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} +
+
+
+
+ previewFile(file.id || "", consentRecord.id)} + className="" + > + + View + +
+
+ ))} +
+ ); +} diff --git a/src/Components/Patient/PatientConsentRecords.tsx b/src/Components/Patient/PatientConsentRecords.tsx index cb0c1fbbbbf..6523fc62992 100644 --- a/src/Components/Patient/PatientConsentRecords.tsx +++ b/src/Components/Patient/PatientConsentRecords.tsx @@ -1,237 +1,339 @@ import { useState } from "react"; -import { CONSENT_PATIENT_CODE_STATUS_CHOICES, CONSENT_TYPE_CHOICES } from "../../Common/constants"; +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 { FieldChangeEventHandler } from "../Form/FormFields/Utils"; +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 { FileUpload } from "./FileUpload"; +import { ExtImage, StateInterface } from "./FileUpload"; 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 FilePreviewDialog from "../Common/FilePreviewDialog"; export default function PatientConsentRecords(props: { - facilityId: string, - patientId: string, - consultationId: string, + facilityId: string; + patientId: string; + consultationId: string; }) { + const { facilityId, patientId, consultationId } = props; + const [downloadURL, setDownloadURL] = useState(); + const [showPCSChangeModal, setShowPCSChangeModal] = useState( + null, + ); + const [newConsent, setNewConsent] = useState({ + type: 0, + patient_code_status: 4, + }); - const { facilityId, patientId, consultationId } = props + const fileUpload = useFileUpload({ + type: "CONSENT_RECORD", + }); + const [file_state, setFileState] = useState({ + open: false, + isImage: false, + name: "", + extension: "", + zoom: 4, + isZoomInDisabled: false, + isZoomOutDisabled: false, + rotation: 0, + }); + const [fileUrl, setFileUrl] = useState(""); - const { data: patient, loading: patientLoading, refetch: patientRefresh } = useQuery(routes.getPatient, { - pathParams: { - id: patientId, - }, - }); - const { data: consultation, loading: consultationLoading, refetch } = useQuery( - routes.getConsultation, - { - pathParams: { id: consultationId! }, - onResponse: (data) => { - if (data.data && data.data.consent_records) { - setConsentRecords(data.data.consent_records); - } - } - }, + 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 handleConsentPCSChange = (type: number) => { + if (!consentRecords) return; + setConsentRecords( + consentRecords.map((cr) => + cr.type === 2 ? { ...cr, patient_code_status: type } : cr, + ), ); + }; - const [collapsedConsentRecords, setCollapsedConsentRecords] = useState< - number[] - >([]); - const [showDeleteConsent, setShowDeleteConsent] = 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, ); + await request(routes.partialUpdateConsultation, { + pathParams: { id: consultationId }, + body: { consent_records: newRecords }, + }); + setConsentRecords(newRecords); + setShowDeleteConsent(null); + }; - const [consentRecords, setConsentRecords] = useState(null); - - - const handleConsentTypeChange: FieldChangeEventHandler = async ( - event, - ) => { - if (!consultationId || !consentRecords) return; - 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: consultationId }, - body: { consent_records: newRecords }, - }); - setConsentRecords(newRecords); - } + const selectField = (name: string) => { + return { + name, + optionValue: (option: any) => option.id, + optionLabel: (option: any) => option.text, + optionDescription: (option: any) => option.desc, }; + }; - const handleConsentPCSChange: FieldChangeEventHandler = (event) => { - if (!consentRecords) return; - setConsentRecords(consentRecords.map((cr) => - cr.type === 2 ? { ...cr, patient_code_status: event.value } : cr, - )); - }; + const getExtension = (url: string) => { + const div1 = url.split("?")[0].split("."); + const ext: string = div1[div1.length - 1].toLowerCase(); + return ext; + }; - 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, - ); - await request(routes.partialUpdateConsultation, { - pathParams: { id: consultationId }, - body: { consent_records: newRecords }, - }); - setConsentRecords(newRecords); - setShowDeleteConsent(null); - }; + const downloadFileUrl = (url: string) => { + fetch(url) + .then((res) => res.blob()) + .then((blob) => { + setDownloadURL(URL.createObjectURL(blob)); + }); + }; - const selectField = (name: string) => { - return { - name, - optionValue: (option: any) => option.id, - optionLabel: (option: any) => option.text, - optionDescription: (option: any) => option.desc, - }; - }; + const previewFile = async (id: string, consent_id: string) => { + setFileUrl(""); + setFileState({ ...file_state, open: true }); + const { data } = await request(routes.retrieveUpload, { + query: { + file_type: "CONSENT_RECORD", + associating_id: consent_id, + }, + pathParams: { id }, + }); - return ( - { + setDownloadURL(""); + setFileState({ + ...file_state, + open: false, + zoom: 4, + isZoomInDisabled: false, + isZoomOutDisabled: false, + }); + }; + + const handleUpload = async () => { + if (newConsent.type === 0) return; + const consentTypeExists = consentRecords?.find( + (record) => record.type === newConsent.type && record.deleted !== true, + ); + if (consentTypeExists) { + await fileUpload.handleFileUpload(consentTypeExists.id); + } else { + const randomId = "consent-" + new Date().getTime().toString(); + const newRecords = [ + ...consentRecords!, + { + 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 }, + }); + setConsentRecords(newRecords); + await fileUpload.handleFileUpload(randomId); + } + + refetch(); + }; + + return ( + + + + 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" + /> + setShowPCSChangeModal(null)} + onConfirm={() => { + if (showPCSChangeModal !== null) { + setNewConsent({ + ...newConsent, + patient_code_status: showPCSChangeModal, + }); + handleConsentPCSChange(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. Changing this will change the existing records. Are you sure you want to proceed?`} + title="Change Previous Records" + className="w-auto" + /> +
+
+

Add New Record

+ { + setNewConsent({ ...newConsent, type: e.value }); }} - backUrl={`/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}/`} - > - setShowDeleteConsent(null)} - onConfirm={handleDeleteConsent} - action="Delete" - variant="danger" - description={ - "Are you sure you want to delete this consent record?" + value={newConsent.type} + label="Consent Type" + options={CONSENT_TYPE_CHOICES} + required + /> + {newConsent.type === 2 && ( + { + if ( + consentRecords?.find( + (record) => + record.type === newConsent.type && + record.patient_code_status !== e.value, + ) + ) { + setShowPCSChangeModal(e.value); + } else { + setNewConsent({ + ...newConsent, + patient_code_status: e.value, + }); } - title="Delete Consent" - className="w-auto" + }} + label="Patient Code Status" + value={newConsent.patient_code_status} + options={CONSENT_PATIENT_CODE_STATUS_CHOICES} + required /> -
-
-

- Add New Record -

- - !consentRecords - ?.filter((r) => r.deleted !== true) - .map((record) => record.type) - .includes(c.id), - )} - /> -
-
- -
- {consentRecords - ?.filter((record) => record.deleted !== true) - .map((record, index) => ( -
-
- - -
-
-
- {record.type === 2 && ( - - )} -
- -
-
- ))} -
- - - ) -} \ No newline at end of file + )} + fileUpload.setFileName(e.value)} + /> +
+ {fileUpload.file ? ( + <> + + + Upload + + + + + + ) : ( + <> + + + + )} +
+
+
+
+ {consentRecords + ?.filter((record) => record.deleted !== true) + .map((record, index) => ( + setShowDeleteConsent(record.id)} + refreshTrigger={consultation} + /> + ))} +
+
+
+
+ ); +} diff --git a/src/Components/Patient/PatientHome.tsx b/src/Components/Patient/PatientHome.tsx index 93e109a2eae..a988f0ee91b 100644 --- a/src/Components/Patient/PatientHome.tsx +++ b/src/Components/Patient/PatientHome.tsx @@ -572,7 +572,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 0549ed61b8e..dd63585a0aa 100644 --- a/src/Components/Patient/PatientInfoCard.tsx +++ b/src/Components/Patient/PatientInfoCard.tsx @@ -171,7 +171,7 @@ export default function PatientInfoCard(props: { className={`w-24 min-w-20 bg-gray-200 ${categoryClass}-profile h-full`} > {consultation?.current_bed && - consultation?.discharge_date === null ? ( + consultation?.discharge_date === null ? (

{ @@ -279,8 +279,9 @@ export default function PatientInfoCard(props: { {consultation?.patient_no && ( - {`${consultation?.suggestion === "A" ? "IP" : "OP"}: ${consultation?.patient_no - }`} + {`${consultation?.suggestion === "A" ? "IP" : "OP"}: ${ + consultation?.patient_no + }`} )} @@ -325,6 +326,18 @@ export default function PatientInfoCard(props: {

)} + {( + consultation?.consent_records?.filter((c) => !c.deleted) || + [] + ).length < 1 && ( +
+
+ + Consent Records Missing + +
+
+ )} {consultation?.suggestion === "DC" && (
@@ -423,45 +436,45 @@ export default function PatientInfoCard(props: {
{consultation?.diagnoses?.length ? (() => { - const principal_diagnosis = consultation.diagnoses.find( - (diagnosis) => diagnosis.is_principal, - ); - return principal_diagnosis ? ( -
-
- Principal Diagnosis: -
-
- {principal_diagnosis.diagnosis_object?.label ?? "-"}{" "} - - -

- {principal_diagnosis.verification_status} -

-
+ const principal_diagnosis = consultation.diagnoses.find( + (diagnosis) => diagnosis.is_principal, + ); + return principal_diagnosis ? ( +
+
+ Principal Diagnosis: +
+
+ {principal_diagnosis.diagnosis_object?.label ?? "-"}{" "} + + +

+ {principal_diagnosis.verification_status} +

+
+
-
- ) : null; - })() + ) : null; + })() : null} {(consultation?.treating_physician_object || consultation?.deprecated_verified_by) && ( -
- - {t("treating_doctor")}:{" "} - - {consultation?.treating_physician_object - ? `${consultation?.treating_physician_object.first_name} ${consultation?.treating_physician_object.last_name}` - : consultation?.deprecated_verified_by} - -
- )} +
+ + {t("treating_doctor")}:{" "} + + {consultation?.treating_physician_object + ? `${consultation?.treating_physician_object.first_name} ${consultation?.treating_physician_object.last_name}` + : consultation?.deprecated_verified_by} + +
+ )}
@@ -531,10 +544,10 @@ export default function PatientInfoCard(props: { { close(); navigate( - `/shifting/${activeShiftingData[ - activeShiftingData.length - 1 - ].id + `/shifting/${ + activeShiftingData[ + activeShiftingData.length - 1 + ].id }`, ); }} @@ -836,9 +850,10 @@ export default function PatientInfoCard(props: { {({ close }) => (
{ if (!consultation?.discharge_date) { close(); @@ -849,10 +864,11 @@ export default function PatientInfoCard(props: {

{t("discharge_from_care")}

diff --git a/src/Components/Patient/models.tsx b/src/Components/Patient/models.tsx index 81a06ff659d..71745240471 100644 --- a/src/Components/Patient/models.tsx +++ b/src/Components/Patient/models.tsx @@ -342,7 +342,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/Utils/useFileUpload.tsx b/src/Utils/useFileUpload.tsx new file mode 100644 index 00000000000..808fc3212b6 --- /dev/null +++ b/src/Utils/useFileUpload.tsx @@ -0,0 +1,446 @@ +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("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( + 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 */} +