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 */} +