Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Develop To Staging v24.36.0 #8468

Merged
merged 29 commits into from
Sep 4, 2024
Merged
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e567f59
Fix pdf previews (#8405)
sainak Aug 23, 2024
e5ede19
Fix clipping issue login page (#7891)
sunny-thakurwar Aug 23, 2024
10a991e
Fixes facility cover image from not reloading after upload (#8412)
rithviknishad Aug 23, 2024
0daadf2
Refactored FileUpload component (#8329)
shivankacker Aug 23, 2024
07db2de
removed unwanted printing of flow rate value (#8408)
nihal467 Aug 23, 2024
8c0cdd6
Cleanup unused files (#8326)
sainak Aug 23, 2024
142fee1
Add Abdominal Drain and Change Rules to Ryles on Ouptput DropDown (#8…
aSriram199 Aug 25, 2024
5bf2d9a
Make Treating Doctor Field Optional for Domiciliary Care Patients (#8…
JOSHIK27 Aug 25, 2024
348edf4
Added patient category descriptions for ICU bed patients (#7970)
shivankacker Aug 25, 2024
58a8fcf
Add option to filter users by no home facility (#8247)
shivankacker Aug 25, 2024
4f3261b
fix build workflows after the org rename (#8431)
sainak Aug 27, 2024
322818f
Change CSN references to OHCN (#8433)
rithviknishad Aug 28, 2024
d31a6a6
Remove fireRequestV2 (#8446)
sainak Aug 31, 2024
5ba2b2d
Fixes PR template tags (#8453)
rithviknishad Sep 2, 2024
3c0d9ed
Removes Storybook (#8452)
rithviknishad Sep 2, 2024
6245493
Adds validation for max dosage in PRN prescription #8324 (#8378)
Nithin9585 Sep 2, 2024
788e752
Fix allowed extensions not working for file input (#8419)
shivankacker Sep 2, 2024
dcedaa9
Fixed icd11 diagnosis multiselect field making unnecessary requests (…
shivankacker Sep 2, 2024
93347f4
Switched to uploadFile from axios in scribe (#8379)
shivankacker Sep 2, 2024
43de4e7
enable interaction with other components when a dropdown is open (#8432)
khavinshankar Sep 2, 2024
d65c6ee
Fixes unknown option for heartbeat rhythm; adds i18n to vitals sectio…
rithviknishad Sep 2, 2024
87b0123
Improve Treament Summary Report (#8295)
khavinshankar Sep 3, 2024
c471332
Navigate to session expired page instead of showing notification (#8363)
rithviknishad Sep 3, 2024
c161490
Removes auto fill for uploaded file name (#8464)
shivankacker Sep 3, 2024
5656d65
Added translations for home page (#8461)
shivankacker Sep 3, 2024
c1e5f32
Fixed User page Spacing and added translations (#8462)
shivankacker Sep 3, 2024
5e95d60
Add suggestion chips instead of autofilling diagnosis (#8369)
shivankacker Sep 3, 2024
28b02d9
Remove proxy and clean up config (#8445)
bodhish Sep 3, 2024
4c61baa
Adds support for "Still watching" prompt to prevent users from idling…
rithviknishad Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactored FileUpload component (#8329)
shivankacker authored Aug 23, 2024
commit 0daadf2f51645fe44675d38b6132c42564dcb33a
2 changes: 1 addition & 1 deletion cypress/e2e/patient_spec/patient_fileupload.cy.ts
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ function runTests(testDescription, visitPatientFileUploadSection) {
cy.verifyNotification("File Uploaded Successfully");
patientFileUpload.verifyUploadFilePresence(cypressAudioName);
// Verify the download of the audio file
cy.get("button").contains("DOWNLOAD").click();
cy.get("button").contains("Download").click();
cy.verifyNotification("Downloading file...");
});

4 changes: 2 additions & 2 deletions cypress/e2e/patient_spec/patient_logupdate.cy.ts
Original file line number Diff line number Diff line change
@@ -89,7 +89,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => {
cy.verifyNotification("Brief Update log created successfully");
cy.closeNotification();
// edit the card and verify the data.
cy.contains("Daily Rounds").click();
cy.contains("button", "Daily Rounds").click();
patientLogupdate.clickLogupdateCard("#dailyround-entry", patientCategory);
cy.verifyContentPresence("#consultation-preview", [
patientCategory,
@@ -110,7 +110,7 @@ describe("Patient Log Update in Normal, Critical and TeleIcu", () => {
patientLogupdate.typeDiastolic(patientModifiedDiastolic);
cy.submitButton("Continue");
cy.verifyNotification("Brief Update log updated successfully");
cy.contains("Daily Rounds").click();
cy.contains("button", "Daily Rounds").click();
patientLogupdate.clickLogupdateCard("#dailyround-entry", patientCategory);
cy.verifyContentPresence("#consultation-preview", [
patientModifiedDiastolic,
12 changes: 6 additions & 6 deletions cypress/e2e/shifting_spec/shifting.cy.ts
Original file line number Diff line number Diff line change
@@ -32,13 +32,13 @@ describe("Shifting Page", () => {

it("switch between active/archived", () => {
cy.intercept(/\/api\/v1\/shift/).as("shifting");
cy.contains("Archived").click().wait("@shifting");
cy.contains("Active").should("have.class", "text-primary-500");
cy.contains("Archived").should("have.class", "text-white");
cy.contains("button", "Archived").click().wait("@shifting");
cy.contains("button", "Active").should("have.class", "text-primary-500");
cy.contains("button", "Archived").should("have.class", "text-white");
cy.intercept(/\/api\/v1\/shift/).as("shifting");
cy.contains("Active").click().wait("@shifting");
cy.contains("Active").should("have.class", "text-white");
cy.contains("Archived").should("have.class", "text-primary-500");
cy.contains("button", "Active").click().wait("@shifting");
cy.contains("button", "Active").should("have.class", "text-white");
cy.contains("button", "Archived").should("have.class", "text-primary-500");
});

afterEach(() => {
37 changes: 20 additions & 17 deletions cypress/pageobject/Patient/PatientFileupload.ts
Original file line number Diff line number Diff line change
@@ -7,28 +7,30 @@ export class PatientFileUpload {
}

typeAudioName(name: string) {
cy.get("#consultation_audio_file").clear();
cy.get("#consultation_audio_file").click().type(name);
cy.get("#upload-file-name").clear();
cy.get("#upload-file-name").click().type(name);
}

clickFileTab() {
cy.verifyAndClickElement("#consultation_tab_nav", "Files");
}

typeFileName(name: string) {
cy.get("#consultation_file").clear();
cy.get("#consultation_file").click().type(name);
cy.get("#upload-file-name").clear();
cy.get("#upload-file-name").click().type(name);
}

recordAudio() {
cy.get("#record-audio").click();
cy.wait(5000);
cy.get("#stop-recording").click();
cy.wait(1000);
cy.get("#save-recording").click();
}

clickUploadAudioFile() {
cy.intercept("POST", "**/api/v1/files/").as("uploadAudioFile");
cy.verifyAndClickElement("#upload_audio_file", "Save");
cy.verifyAndClickElement("#upload_file_button", "Upload");
cy.wait("@uploadAudioFile").its("response.statusCode").should("eq", 201);
}

@@ -52,8 +54,8 @@ export class PatientFileUpload {
}

archiveFile() {
cy.get("button").contains("ARCHIVE").click().scrollIntoView();
cy.get("#editFileName").clear().type("Cypress File Archive");
cy.get("#file-div button").contains("Archive").click().scrollIntoView();
cy.get("#archive-file-reason").clear().type("Cypress File Archive");
}

clickSaveArchiveFile() {
@@ -63,27 +65,28 @@ export class PatientFileUpload {
}

verifyArchiveFile(fileName: string) {
cy.get("#archived-files").click();
cy.get("button").contains("MORE DETAILS").click().scrollIntoView();
cy.get("#archive-file-name").should("contain.text", fileName);
cy.get("#archive-file-reason").then(($reason) => {
expect($reason.text().split(":")[1]).to.contain("Cypress File Archive");
});
cy.get("button").contains("Archived Files").click();
cy.get("button").contains("More Info").click().scrollIntoView();
cy.get('[data-archive-info="File Name"]').should("contain.text", fileName);
cy.get('[data-archive-info="Archive Reason"]').should(
"contain.text",
"Cypress File Archive",
);
}

verifyFileRenameOption(status: boolean) {
cy.get("#file-div").then(($fileDiv) => {
if (status) {
expect($fileDiv.text()).to.contain("RENAME");
expect($fileDiv.text()).to.contain("Rename");
} else {
expect($fileDiv.text()).to.not.contain("RENAME");
expect($fileDiv.text()).to.not.contain("Rename");
}
});
}

renameFile(newFileName: string) {
cy.get("button").contains("RENAME").click().scrollIntoView();
cy.get("#editFileName").clear().type(newFileName);
cy.get("button").contains("Rename").click().scrollIntoView();
cy.get("#edit-file-name").clear().type(newFileName);
}

clickSaveFileName() {
12 changes: 6 additions & 6 deletions cypress/pageobject/Resource/ResourcePage.ts
Original file line number Diff line number Diff line change
@@ -15,27 +15,27 @@ class ResourcePage {
}

clickCompletedResources() {
cy.contains("Completed").click();
cy.contains("button", "Completed").click();
}

verifyCompletedResources() {
cy.wait("@resource").then((interception) => {
expect(interception.response.statusCode).to.equal(200);
});
cy.contains("Active").should("have.class", "text-primary-500");
cy.contains("Completed").should("have.class", "text-white");
cy.contains("button", "Active").should("have.class", "text-primary-500");
cy.contains("button", "Completed").should("have.class", "text-white");
}

clickActiveResources() {
cy.contains("Active").click();
cy.contains("button", "Active").click();
}

verifyActiveResources() {
cy.wait("@resource").then((interception) => {
expect(interception.response.statusCode).to.equal(200);
});
cy.contains("Active").should("have.class", "text-white");
cy.contains("Completed").should("have.class", "text-primary-500");
cy.contains("button", "Active").should("have.class", "text-white");
cy.contains("button", "Completed").should("have.class", "text-primary-500");
}

clickListViewButton() {
57 changes: 57 additions & 0 deletions src/Common/constants.tsx
Original file line number Diff line number Diff line change
@@ -1660,3 +1660,60 @@ export const PressureSoreTissueTypeOptions = [
"Slough",
"Necrotic",
] as const;

export const FILE_EXTENSIONS = {
IMAGE: ["jpeg", "jpg", "png", "gif", "svg", "bmp", "webp", "jfif"],
AUDIO: ["mp3", "wav"],
VIDEO: [
"webm",
"mpg",
"mp2",
"mpeg",
"mpe",
"mpv",
"ogg",
"mp4",
"m4v",
"avi",
"wmv",
"mov",
"qt",
"flv",
"swf",
],
PRESENTATION: ["pptx"],
DOCUMENT: ["pdf", "docx"],
} as const;

export const PREVIEWABLE_FILE_EXTENSIONS = [
"html",
"htm",
"pdf",
"mp4",
"webm",
"jpg",
"jpeg",
"png",
"gif",
"webp",
] as const;

export const HEADER_CONTENT_TYPES = {
pdf: "application/pdf",
txt: "text/plain",
jpeg: "image/jpeg",
jpg: "image/jpeg",
doc: "application/msword",
xls: "application/vnd.ms-excel",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
epub: "application/epub+zip",
gif: "image/gif",
html: "text/html",
htm: "text/html",
mp4: "video/mp4",
png: "image/png",
ppt: "application/vnd.ms-powerpoint",
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
svg: "image/svg+xml",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
} as const;
2 changes: 1 addition & 1 deletion src/Components/Common/FilePreviewDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import CircularProgress from "./components/CircularProgress";
import { useTranslation } from "react-i18next";
import { StateInterface } from "../Patient/FileUpload";
import { StateInterface } from "../Files/FileUpload";
import { Dispatch, ReactNode, SetStateAction, useState } from "react";
import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon";
import ButtonV2, { Cancel } from "./components/ButtonV2";
46 changes: 0 additions & 46 deletions src/Components/Common/components/SwitchTabs.tsx

This file was deleted.

70 changes: 70 additions & 0 deletions src/Components/Common/components/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useEffect, useRef, type ReactNode } from "react";
import { classNames } from "../../../Utils/utils";
import useWindowDimensions from "../../../Common/hooks/useWindowDimensions";

export default function Tabs(props: {
className?: string;
currentTab: string | number;
onTabChange: (value: string | number) => void;
tabs: { text: ReactNode; value: string | number }[];
}) {
const { className, currentTab, onTabChange, tabs } = props;
const ref = useRef<HTMLDivElement>(null);
const tabSwitcherRef = useRef<HTMLDivElement>(null);

const dimensions = useWindowDimensions();

useEffect(() => {
const currentTabIndex = tabs.findIndex((t) => t.value === currentTab);
if (
typeof currentTabIndex != "number" ||
!ref.current ||
!tabSwitcherRef.current
)
return;
const tabButton = ref.current.querySelectorAll("button")[currentTabIndex];
if (!tabButton) return;
tabSwitcherRef.current.style.width = tabButton.clientWidth + "px";
tabSwitcherRef.current.style.left =
tabButton.getBoundingClientRect().left -
ref.current.getBoundingClientRect().left +
ref.current.scrollLeft +
"px";
}, [currentTab, tabSwitcherRef.current, ref.current, dimensions]);

return (
<div
className={classNames(
"relative inline-flex w-full items-center justify-between overflow-auto rounded-md bg-primary-500/10 p-2 md:w-auto",
className,
)}
ref={ref}
>
<div
className="absolute inset-y-2 z-10 rounded bg-primary-500 transition-all"
ref={tabSwitcherRef}
style={{ left: 0 }}
/>
{/* There has to be a better way of handling this... */}
{tabs.map((tab, i) => (
<div
key={i}
className={`flex-1 whitespace-nowrap px-6 py-2 text-sm font-semibold text-transparent transition-all`}
>
{tab.text}
</div>
))}
<div className="absolute inset-2 z-10 flex items-center justify-between">
{tabs.map((tab, i) => (
<button
key={i}
onClick={() => onTabChange(tab.value)}
className={`${currentTab === tab.value ? "text-white" : "text-primary-500 hover:text-primary-600"} flex-1 whitespace-nowrap px-6 py-2 text-sm font-semibold transition-all`}
>
{tab.text}
</button>
))}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { ConsultationTabProps } from "./index";
import { FileUpload } from "../../Patient/FileUpload";
import { FileUpload } from "../../Files/FileUpload";

export const ConsultationFilesTab = (props: ConsultationTabProps) => {
return (
<div>
<div className="p-4">
<FileUpload
facilityId={props.facilityId}
patientId={props.patientId}
consultationId={props.consultationId}
type="CONSULTATION"
hideBack={true}
audio={true}
unspecified={true}
allowAudio
/>
</div>
);
Original file line number Diff line number Diff line change
@@ -18,12 +18,12 @@ import {
import ReadMore from "../../Common/components/Readmore";
import DailyRoundsList from "../Consultations/DailyRoundsList";
import EventsList from "./Events/EventsList";
import SwitchTabs from "../../Common/components/SwitchTabs";
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";
import Tabs from "../../Common/components/Tabs";

const PageTitle = lazy(() => import("../../Common/PageTitle"));

@@ -651,20 +651,24 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
</div>
</div>
<div className="w-full pl-0 md:pl-4 xl:w-1/3">
<SwitchTabs
<Tabs
className="mt-3 w-full lg:w-full"
tab2={
<div className="flex items-center justify-center gap-1 text-sm">
Events
<span className="rounded-lg bg-warning-400 p-px px-1 text-xs text-white">
beta
</span>
</div>
}
tab1="Daily Rounds"
onClickTab1={() => setShowEvents(false)}
onClickTab2={() => setShowEvents(true)}
isTab2Active={showEvents}
tabs={[
{
text: (
<div className="flex items-center justify-center gap-1 text-sm">
Events
<span className="rounded-lg bg-warning-400 p-px px-1 text-xs text-white">
beta
</span>
</div>
),
value: 1,
},
{ text: "Daily Rounds", value: 0 },
]}
onTabChange={(v) => setShowEvents(!!v)}
currentTab={showEvents ? 1 : 0}
/>
{showEvents ? (
<EventsList />
14 changes: 8 additions & 6 deletions src/Components/Facility/DischargedPatientsList.tsx
Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@ import CareIcon from "../../CAREUI/icons/CareIcon";
import RecordMeta from "../../CAREUI/display/RecordMeta";
import { formatPatientAge, humanizeStrings } from "../../Utils/utils";
import { useTranslation } from "react-i18next";
import SwitchTabs from "../Common/components/SwitchTabs";
import SortDropdownMenu from "../Common/SortDropdown";
import useFilters from "../../Common/hooks/useFilters";
import PatientFilter from "../Patient/PatientFilter";
@@ -36,6 +35,7 @@ import {
import { getDiagnosesByIds } from "../Diagnosis/utils";
import { ICD11DiagnosisModel } from "./models";
import FilterBadge from "../../CAREUI/display/FilterBadge";
import Tabs from "../Common/components/Tabs";

const DischargedPatientsList = ({
facility_external_id,
@@ -254,12 +254,14 @@ const DischargedPatientsList = ({
options={
<>
<div className="flex flex-col gap-4 lg:flex-row">
<SwitchTabs
tab1="Live"
tab2="Discharged"
<Tabs
tabs={[
{ text: "Live", value: 0 },
{ text: "Discharged", value: 1 },
]}
className="mr-4"
onClickTab1={() => navigate("/patients")}
isTab2Active
onTabChange={() => navigate("/patients")}
currentTab={1}
/>
<AdvancedFilterButton
onClick={() => advancedFilter.setShow(true)}
203 changes: 203 additions & 0 deletions src/Components/Files/AudioCaptureDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { useEffect, useState } from "react";
import useRecorder from "../../Utils/useRecorder";
import { Link } from "raviger";
import CareIcon from "../../CAREUI/icons/CareIcon";
import { useTimer } from "../../Utils/useTimer";
import { t } from "i18next";

export interface AudioCaptureDialogProps {
show: boolean;
onHide: () => void;
onCapture: (file: File) => void;
autoRecord?: boolean;
}

export default function AudioCaptureDialog(props: AudioCaptureDialogProps) {
type Status =
| "RECORDING"
| "WAITING_TO_RECORD"
| "PERMISSION_DENIED"
| "RECORDED";

const { show, onHide, onCapture, autoRecord = false } = props;

const [status, setStatus] = useState<Status | null>(null);

const [audioURL, , startRecording, stopRecording, , resetRecording] =
useRecorder((permission: boolean) => {
if (!permission) {
handleStopRecording();
resetRecording();
setStatus("PERMISSION_DENIED");
}
});

const timer = useTimer();

const handleStartRecording = () => {
setStatus("RECORDING");
startRecording();
timer.start();
};

const handleStopRecording = () => {
if (status !== "RECORDING") return;
setStatus("RECORDED");
stopRecording();
timer.stop();
};

const handleRestartRecording = () => {
if (status !== "RECORDED") return;
resetRecording();
handleStartRecording();
};

const handleSubmit = async () => {
const response = await fetch(audioURL);
const blob = await response.blob();
const file = new File(
[blob],
`recording_${new Date().toISOString().replaceAll(".", "_").replaceAll(":", "_")}.mp3`,
{ type: "audio/mpeg" },
);
resetRecording();
onHide();
onCapture(file);
};

useEffect(() => {
const checkMicPermission = async () => {
try {
const permissions = await navigator.permissions.query({
name: "microphone" as PermissionName,
});
setStatus(
permissions.state === "denied"
? "PERMISSION_DENIED"
: "WAITING_TO_RECORD",
);
} catch (error) {
setStatus(null);
}
};

show && checkMicPermission();

return () => {
setStatus(null);
};
}, [show]);

useEffect(() => {
if (autoRecord && show && status === "WAITING_TO_RECORD") {
handleStartRecording();
}
}, [autoRecord, status, show]);

return (
<div
className={`inset-0 bg-black/70 backdrop-blur transition-all ${show ? "visible opacity-100" : "invisible opacity-0"} fixed z-50 flex flex-col items-center justify-center gap-8 p-6 text-center`}
>
{status === "PERMISSION_DENIED" && (
<div>
<h2 className="font-bold text-white">
{t("audio__allow_permission")}
</h2>
<div className="text-secondary-200">
{t("audio__allow_permission_helper")}{" "}
{/* TODO: find a better link that supports all browsers */}
<Link
href="https://support.google.com/chrome/answer/2693767?hl=en&co=GENIE.Platform%3DAndroid"
target="_blank"
className="text-blue-400 underline"
>
{t("audio__allow_permission_button")}
</Link>
</div>
</div>
)}
{status === "WAITING_TO_RECORD" && (
<div>
<h2 className="font-bold text-white">{t("audio__record")}</h2>
<div className="text-secondary-200">{t("audio__record_helper")}</div>
<div className="mt-4">
<button
onClick={handleStartRecording}
className="inline-flex aspect-square w-32 items-center justify-center rounded-full bg-white/10 text-6xl text-white hover:bg-white/20"
>
<CareIcon icon="l-microphone" />
</button>
</div>
</div>
)}
{status === "RECORDING" && (
<div>
<h2 className="inline-flex animate-pulse items-center gap-2 font-bold text-red-500">
<div className="aspect-square w-5 rounded-full bg-red-500" />
{t("audio__recording")}
</h2>
<div className="text-secondary-200">
{t("audio__recording_helper")}
<br />
{t("audio__recording_helper_2")}
</div>
<div className="mt-4">
<button
onClick={handleStopRecording}
id="stop-recording"
className="inline-flex aspect-square w-32 animate-pulse items-center justify-center rounded-full bg-red-500/20 text-2xl text-red-500 hover:bg-red-500/30"
>
{timer.time}
</button>
</div>
</div>
)}
{status === "RECORDED" && (
<div>
<h2 className="font-bold text-white">{t("audio__recorded")}</h2>
<div className="text-secondary-200">
{audioURL && (
<div className="my-4">
<audio
className="m-auto max-h-full max-w-full object-contain"
src={audioURL}
controls
autoPlay
/>
</div>
)}
</div>
<div className="mt-4 inline-flex items-center gap-2">
<button
onClick={handleSubmit}
className="rounded-md bg-primary-500 px-4 py-2 text-white transition-all hover:bg-primary-600"
id="save-recording"
>
<CareIcon icon="l-check" className="mr-2 text-lg" />
{t("done")}
</button>
<button
onClick={handleRestartRecording}
className="rounded-md bg-white/10 px-4 py-2 text-white transition-all hover:bg-white/20"
>
<CareIcon icon="l-history" className="mr-2 text-lg" />
{t("audio__start_again")}
</button>
</div>
</div>
)}
<button
onClick={() => {
handleStopRecording();
onHide();
resetRecording();
}}
className="rounded-md bg-white/10 px-4 py-2 text-white transition-all hover:bg-white/20"
>
<CareIcon icon="l-times" className="mr-2 text-lg" />
{t("cancel")}
</button>
</div>
);
}
215 changes: 215 additions & 0 deletions src/Components/Files/CameraCaptureDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import Webcam from "react-webcam";
import CareIcon from "../../CAREUI/icons/CareIcon";
import DialogModal from "../Common/Dialog";
import ButtonV2, { Submit } from "../Common/components/ButtonV2";
import { t } from "i18next";
import { useCallback, useRef, useState } from "react";
import useWindowDimensions from "../../Common/hooks/useWindowDimensions";

export interface CameraCaptureDialogProps {
show: boolean;
onHide: () => void;
onCapture: (file: File) => void;
}

export default function CameraCaptureDialog(props: CameraCaptureDialogProps) {
const { show, onHide, onCapture } = props;

const [cameraFacingFront, setCameraFacingFront] = useState(true);
const [previewImage, setPreviewImage] = useState(null);
const webRef = useRef<any>(null);

const videoConstraints = {
width: { ideal: 4096 },
height: { ideal: 2160 },
facingMode: "user",
};

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,
});
onCapture(myFile);
});
};

const cameraFacingMode = cameraFacingFront
? "user"
: { exact: "environment" };

return (
<DialogModal
show={show}
title={
<div className="flex flex-row">
<div className="rounded-full bg-primary-100 px-5 py-4">
<CareIcon
icon="l-camera-change"
className="text-lg text-primary-500"
/>
</div>
<div className="m-4">
<h1 className="text-xl text-black">{t("camera")}</h1>
</div>
</div>
}
className="max-w-2xl"
onClose={onHide}
>
<div>
{!previewImage ? (
<div className="m-3">
<Webcam
forceScreenshotSourceSize
screenshotQuality={1}
audio={false}
screenshotFormat="image/jpeg"
ref={webRef}
videoConstraints={{
...videoConstraints,
facingMode: cameraFacingMode,
}}
/>
</div>
) : (
<div className="m-3">
<img src={previewImage} />
</div>
)}
</div>

{/* buttons for mobile screens */}
<div className="m-4 flex justify-evenly sm:hidden">
<div>
{!previewImage ? (
<ButtonV2 onClick={handleSwitchCamera} className="m-2">
{t("switch")}
</ButtonV2>
) : (
<></>
)}
</div>
<div>
{!previewImage ? (
<>
<div>
<ButtonV2
onClick={() => {
captureImage();
}}
className="m-2"
>
{t("capture")}
</ButtonV2>
</div>
</>
) : (
<>
<div className="flex space-x-2">
<ButtonV2
onClick={() => {
setPreviewImage(null);
}}
className="m-2"
>
{t("retake")}
</ButtonV2>
<Submit
onClick={() => {
setPreviewImage(null);
onHide();
}}
className="m-2"
>
{t("submit")}
</Submit>
</div>
</>
)}
</div>
<div className="sm:flex-1">
<ButtonV2
variant="secondary"
onClick={() => {
setPreviewImage(null);
onHide();
}}
className="m-2"
>
{t("close")}
</ButtonV2>
</div>
</div>
{/* buttons for laptop screens */}
<div className={`${isLaptopScreen ? " " : "hidden"}`}>
<div className="m-4 flex lg:hidden">
<ButtonV2 onClick={handleSwitchCamera}>
<CareIcon icon="l-camera-change" className="text-lg" />
{`${t("switch")} ${t("camera")}`}
</ButtonV2>
</div>

<div className="flex justify-end gap-2 p-4">
<div>
{!previewImage ? (
<>
<div>
<ButtonV2
onClick={() => {
captureImage();
}}
>
<CareIcon icon="l-capture" className="text-lg" />
{t("capture")}
</ButtonV2>
</div>
</>
) : (
<>
<div className="flex space-x-2">
<ButtonV2
onClick={() => {
setPreviewImage(null);
}}
>
{t("retake")}
</ButtonV2>
<Submit
onClick={() => {
onHide();
setPreviewImage(null);
}}
>
{t("submit")}
</Submit>
</div>
</>
)}
</div>
<div className="sm:flex-1" />
<ButtonV2
variant="secondary"
onClick={() => {
setPreviewImage(null);
onHide();
}}
>
{`${t("close")} ${t("camera")}`}
</ButtonV2>
</div>
</div>
</DialogModal>
);
}
131 changes: 131 additions & 0 deletions src/Components/Files/FileBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import dayjs from "dayjs";
import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon";
import ButtonV2 from "../Common/components/ButtonV2";
import { FileUploadModel } from "../Patient/models";
import { FileManagerResult } from "../../Utils/useFileManager";
import useQuery from "../../Utils/request/useQuery";
import routes from "../../Redux/api";
import { FILE_EXTENSIONS } from "../../Common/constants";
import { t } from "i18next";

export interface FileBlockProps {
file: FileUploadModel;
fileManager: FileManagerResult;
associating_id: string;
editable: boolean;
archivable?: boolean;
}

export default function FileBlock(props: FileBlockProps) {
const {
file,
fileManager,
associating_id,
editable = false,
archivable = false,
} = props;

const filetype = fileManager.getFileType(file);

const fileData = useQuery(routes.retrieveUpload, {
query: { file_type: fileManager.type, associating_id },
pathParams: { id: file.id || "" },
prefetch: filetype === "AUDIO" && !file.is_archived,
});

const icons: Record<keyof typeof FILE_EXTENSIONS | "UNKNOWN", IconName> = {
AUDIO: "l-volume",
IMAGE: "l-image",
PRESENTATION: "l-presentation-play",
VIDEO: "l-video",
UNKNOWN: "l-file-medical",
DOCUMENT: "l-file-medical",
};

const archived = file.is_archived;

return (
<div
id="file-div"
className={`flex flex-col justify-between gap-2 rounded-lg border border-secondary-300 lg:flex-row lg:items-center ${archived ? "text-secondary-600" : "bg-white"} px-4 py-2 transition-all hover:bg-secondary-100`}
>
<div className="flex items-center gap-4">
<div
className={`${archived ? "bg-secondary-100 text-secondary-500" : "bg-primary-500/10 text-primary-700"} flex aspect-square w-14 items-center justify-center rounded-full`}
>
<CareIcon icon={icons[filetype]} className="text-4xl" />
</div>
<div className="min-w-[40%] break-all">
<div className="">
{file.name}
{file.extension} {file.is_archived && "(Archived)"}
</div>
<div className="text-xs text-secondary-700">
{dayjs(
file.is_archived ? file.archived_datetime : file.created_date,
).format("DD MMM YYYY, hh:mm A")}{" "}
by{" "}
{file.is_archived
? file.archived_by?.username
: file.uploaded_by?.username}
</div>
</div>
</div>
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
{filetype === "AUDIO" && !file.is_archived && (
<div className="w-full md:w-[300px]">
<audio
className="max-h-full w-full object-contain"
src={fileData.data?.read_signed_url}
controls
preload="auto"
controlsList="nodownload"
/>
</div>
)}
{!file.is_archived &&
(fileManager.isPreviewable(file) ? (
<ButtonV2
onClick={() => fileManager.viewFile(file, associating_id)}
className="w-full md:w-auto"
>
<CareIcon icon="l-eye" />
{t("view")}
</ButtonV2>
) : (
<ButtonV2
onClick={() => fileManager.downloadFile(file, associating_id)}
className="w-full md:w-auto"
>
<CareIcon icon="l-arrow-circle-down" />
{t("download")}
</ButtonV2>
))}
<div className="inline-flex w-full gap-2 md:w-auto">
{!file.is_archived && editable && (
<ButtonV2
variant={"secondary"}
onClick={() => fileManager.editFile(file, associating_id)}
className="flex-1 md:flex-auto"
>
<CareIcon icon={"l-pen"} />
{t("rename")}
</ButtonV2>
)}
{(file.is_archived || editable) && archivable && (
<ButtonV2
variant={file.is_archived ? "primary" : "secondary"}
onClick={() => fileManager.archiveFile(file, associating_id)}
className="flex-1 md:flex-auto"
>
<CareIcon
icon={file.is_archived ? "l-info-circle" : "l-archive"}
/>
{file.is_archived ? t("more_info") : t("archive")}
</ButtonV2>
)}
</div>
</div>
</div>
);
}
379 changes: 379 additions & 0 deletions src/Components/Files/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
import { useState, ReactNode } from "react";
import { FileUploadModel } from "../Patient/models.js";
import Pagination from "../Common/Pagination.js";
import { RESULTS_PER_PAGE_LIMIT } from "../../Common/constants.js";
import { useTranslation } from "react-i18next";
import ButtonV2 from "../Common/components/ButtonV2.js";
import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon.js";
import TextFormField from "../Form/FormFields/TextFormField.js";
import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor.js";
import AuthorizedChild from "../../CAREUI/misc/AuthorizedChild.js";
import useAuthUser from "../../Common/hooks/useAuthUser.js";
import useQuery from "../../Utils/request/useQuery.js";
import routes from "../../Redux/api.js";
import useFileUpload from "../../Utils/useFileUpload.js";
import useFileManager from "../../Utils/useFileManager.js";
import Tabs from "../Common/components/Tabs.js";
import FileBlock from "./FileBlock.js";

export const LinearProgressWithLabel = (props: { value: number }) => {
return (
<div className="flex align-middle">
<div className="my-auto mr-2 w-full">
<div className="mr-2 h-1.5 w-full rounded-full bg-primary-200">
<div
className="h-1.5 rounded-full bg-primary-500"
style={{ width: `${props.value}%` }}
/>
</div>
</div>
<div className="min-w-[35]">
<p className="text-slate-600">{`${Math.round(props.value)}%`}</p>
</div>
</div>
);
};

interface FileUploadProps {
type: string;
patientId?: string;
consultationId?: string;
consentId?: string;
allowAudio?: boolean;
sampleId?: string;
claimId?: string;
className?: string;
hideUpload?: boolean;
}

export interface ModalDetails {
name?: string;
id?: string;
reason?: string;
userArchived?: string;
archiveTime?: string;
associatedId?: string;
}

export interface StateInterface {
open: boolean;
isImage: boolean;
name: string;
extension: string;
zoom: number;
isZoomInDisabled: boolean;
isZoomOutDisabled: boolean;
rotation: number;
}

export const FileUpload = (props: FileUploadProps) => {
const { t } = useTranslation();
const {
consultationId,
patientId,
consentId,
type,
sampleId,
claimId,
allowAudio,
hideUpload,
} = props;
const [currentPage, setCurrentPage] = useState(1);
const [offset, setOffset] = useState(0);
const [tab, setTab] = useState("UNARCHIVED");
const authUser = useAuthUser();

const handlePagination = (page: number, limit: number) => {
const offset = (page - 1) * limit;
setCurrentPage(page);
setOffset(offset);
};

const UPLOAD_HEADING: { [index: string]: string } = {
PATIENT: t("upload_headings__patient"),
CONSULTATION: t("upload_headings__consultation"),
SAMPLE_MANAGEMENT: t("upload_headings__sample_report"),
CLAIM: t("upload_headings__supporting_info"),
};
const VIEW_HEADING: { [index: string]: string } = {
PATIENT: t("file_list_headings__patient"),
CONSULTATION: t("file_list_headings__consultation"),
SAMPLE_MANAGEMENT: t("file_list_headings__sample_report"),
CLAIM: t("file_list_headings__supporting_info"),
};

const associatedId =
{
PATIENT: patientId,
CONSENT_RECORD: consentId,
CONSULTATION: consultationId,
SAMPLE_MANAGEMENT: sampleId,
CLAIM: claimId,
}[type] || "";

const activeFilesQuery = useQuery(routes.viewUpload, {
query: {
file_type: type,
associating_id: associatedId,
is_archived: false,
limit: RESULTS_PER_PAGE_LIMIT,
offset: offset,
},
});

const archivedFilesQuery = useQuery(routes.viewUpload, {
query: {
file_type: type,
associating_id: associatedId,
is_archived: true,
limit: RESULTS_PER_PAGE_LIMIT,
offset: offset,
},
});

const dischargeSummaryQuery = useQuery(routes.viewUpload, {
query: {
file_type: "DISCHARGE_SUMMARY",
associating_id: associatedId,
is_archived: false,
limit: RESULTS_PER_PAGE_LIMIT,
offset: offset,
},
prefetch: type === "CONSULTATION",
silent: true,
});

const queries = {
UNARCHIVED: activeFilesQuery,
ARCHIVED: archivedFilesQuery,
DISCHARGE_SUMMARY: dischargeSummaryQuery,
};

const refetchAll = async () =>
Promise.all(Object.values(queries).map((q) => q.refetch()));
const loading = Object.values(queries).some((q) => q.loading);

const fileQuery = queries[tab as keyof typeof queries];

const tabs = [
{ text: "Active Files", value: "UNARCHIVED" },
{ text: "Archived Files", value: "ARCHIVED" },
...(dischargeSummaryQuery.data?.results?.length
? [
{
text: "Discharge Summary",
value: "DISCHARGE_SUMMARY",
},
]
: []),
];

const fileUpload = useFileUpload({
type,
allowedExtensions: [
"jpg",
"jpeg",
"png",
"gif",
"bmp",
"tiff",
"mp4",
"mov",
"avi",
"wmv",
"mp3",
"wav",
"ogg",
"txt",
"csv",
"rtf",
"doc",
"odt",
"pdf",
"xls",
"xlsx",
"ods",
"pdf",
],
onUpload: refetchAll,
});

const fileManager = useFileManager({
type,
onArchive: refetchAll,
onEdit: refetchAll,
});

const dischargeSummaryFileManager = useFileManager({
type: "DISCHARGE_SUMMARY",
onArchive: refetchAll,
onEdit: refetchAll,
});

const uploadButtons: {
name: string;
icon: IconName;
onClick?: () => void;
children?: ReactNode;
show?: boolean;
id: string;
}[] = [
{
name: t("choose_file"),
icon: "l-file-upload-alt",
children: <fileUpload.Input />,
id: "upload-file",
},
{
name: t("open_camera"),
icon: "l-camera",
onClick: fileUpload.handleCameraCapture,
id: "open-webcam",
},
{
name: t("record"),
icon: "l-microphone",
onClick: fileUpload.handleAudioCapture,
show: allowAudio,
id: "record-audio",
},
];

return (
<div className={`md:p-4 ${props.className}`}>
{fileUpload.Dialogues}
{fileManager.Dialogues}
{dischargeSummaryFileManager.Dialogues}
{!hideUpload && (
<AuthorizedChild authorizeFor={NonReadOnlyUsers}>
{({ isAuthorized }) =>
isAuthorized ? (
<>
<h4 className="mb-6 text-2xl">{UPLOAD_HEADING[type]}</h4>
{fileUpload.file ? (
<div className="mb-8 rounded-lg border border-secondary-300 bg-white p-4">
<div className="mb-4 flex items-center justify-between gap-2 rounded-md bg-secondary-300 px-4 py-2">
<span>
<CareIcon icon="l-paperclip" className="mr-2" />
{fileUpload.file.name}
</span>
<button
onClick={fileUpload.clearFile}
disabled={!!fileUpload.progress}
className="text-lg"
>
<CareIcon icon="l-times" />
</button>
</div>
<TextFormField
name="consultation_file"
type="text"
label={t("enter_file_name")}
id="upload-file-name"
required
value={fileUpload.fileName}
disabled={!!fileUpload.progress}
onChange={(e) => fileUpload.setFileName(e.value)}
error={fileUpload.error || undefined}
/>
<div className="flex items-center gap-2">
<ButtonV2
onClick={() =>
fileUpload.handleFileUpload(associatedId)
}
loading={!!fileUpload.progress}
className="w-full"
id="upload_file_button"
>
<CareIcon icon="l-check" className="" />
{t("upload")}
</ButtonV2>
<ButtonV2
variant="danger"
onClick={fileUpload.clearFile}
disabled={!!fileUpload.progress}
>
<CareIcon icon="l-trash-alt" className="" />
{t("discard")}
</ButtonV2>
</div>
{!!fileUpload.progress && (
<LinearProgressWithLabel value={fileUpload.progress} />
)}
</div>
) : (
<div className="mb-8 flex flex-col items-center gap-4 md:flex-row">
{uploadButtons
.filter((b) => b.show !== false)
.map((button, i) => (
<label
key={i}
className="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-dashed border-primary-500/20 bg-primary-500/10 p-3 text-primary-700 transition-all hover:bg-primary-500/20 md:p-6"
onClick={button.onClick}
id={button.id}
>
<CareIcon icon={button.icon} className="text-2xl" />
<div className="text-lg">{button.name}</div>
{button.children}
</label>
))}
</div>
)}
</>
) : (
<></>
)
}
</AuthorizedChild>
)}
<div className="mb-4 flex flex-col items-center justify-between gap-4 md:flex-row">
<h3>{VIEW_HEADING[type]}</h3>
<Tabs
tabs={tabs}
onTabChange={(v) => setTab(v.toString())}
currentTab={tab}
/>
</div>
<div className="flex flex-col gap-2">
{!(fileQuery?.data?.results || []).length && loading && (
<div className="skeleton-animate-alpha h-32 rounded-lg" />
)}
{fileQuery?.data?.results.map((item: FileUploadModel) => (
<FileBlock
file={item}
key={item.id}
fileManager={
tab !== "DISCHARGE_SUMMARY"
? fileManager
: dischargeSummaryFileManager
}
associating_id={associatedId}
editable={
item?.uploaded_by?.username === authUser.username ||
authUser.user_type === "DistrictAdmin" ||
authUser.user_type === "StateAdmin"
}
archivable={tab !== "DISCHARGE_SUMMARY"}
/>
))}
{!(fileQuery?.data?.results || []).length && (
<div className="mt-4">
<div className="text-md flex items-center justify-center font-semibold capitalize text-secondary-500">
{t("no_files_found", { type: tab.toLowerCase() })}
</div>
</div>
)}
</div>
{(fileQuery?.data?.results || []).length > RESULTS_PER_PAGE_LIMIT && (
<div className="mt-4 flex w-full justify-center">
<Pagination
cPage={currentPage}
defaultPerPage={RESULTS_PER_PAGE_LIMIT}
data={{ totalCount: (fileQuery?.data?.results || []).length }}
onChange={handlePagination}
/>
</div>
)}
</div>
);
};
4 changes: 2 additions & 2 deletions src/Components/HCX/ClaimCreatedModal.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { HCXActions } from "../../Redux/actions";
import * as Notification from "../../Utils/Notifications";
import { Submit } from "../Common/components/ButtonV2";
import DialogModal from "../Common/Dialog";
import { FileUpload } from "../Patient/FileUpload";
import { FileUpload } from "../Files/FileUpload";
import { HCXClaimModel } from "./models";

interface Props {
@@ -50,7 +50,7 @@ export default function ClaimCreatedModal({ claim, ...props }: Props) {
}
>
<div className="p-4 pt-8">
<FileUpload type="CLAIM" claimId={claim.id!} hideBack unspecified />
<FileUpload type="CLAIM" claimId={claim.id} />
</div>
</DialogModal>
);
1,763 changes: 0 additions & 1,763 deletions src/Components/Patient/FileUpload.tsx

This file was deleted.

41 changes: 41 additions & 0 deletions src/Components/Patient/FileUploadPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import routes from "../../Redux/api";
import useQuery from "../../Utils/request/useQuery";
import Page from "../Common/components/Page";
import { FileUpload } from "../Files/FileUpload";

export default function FileUploadPage(props: {
facilityId: string;
patientId: string;
consultationId?: string;
type: "CONSULTATION" | "PATIENT";
}) {
const { facilityId, patientId, consultationId, type } = props;

const { data: patient } = useQuery(routes.getPatient, {
pathParams: { id: patientId },
prefetch: !!patientId,
});

return (
<Page
hideBack={false}
title="Patient Files"
crumbsReplacements={{
[facilityId]: { name: patient?.facility_object?.name },
[patientId]: { name: patient?.name },
}}
backUrl={
type === "CONSULTATION"
? `/facility/${facilityId}/patient/${patientId}/consultation/${consultationId}`
: `/facility/${facilityId}/patient/${patientId}`
}
>
<FileUpload
patientId={patientId}
consultationId={consultationId}
type={type}
allowAudio={true}
/>
</Page>
);
}
52 changes: 28 additions & 24 deletions src/Components/Patient/ManagePatients.tsx
Original file line number Diff line number Diff line change
@@ -31,7 +31,6 @@ import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField";
import RecordMeta from "../../CAREUI/display/RecordMeta";
import SearchInput from "../Form/SearchInput";
import SortDropdownMenu from "../Common/SortDropdown";
import SwitchTabs from "../Common/components/SwitchTabs";
import {
formatPatientAge,
humanizeStrings,
@@ -53,6 +52,7 @@ import {
} from "./DiagnosesFilter.js";
import { ICD11DiagnosisModel } from "../Diagnosis/types.js";
import { getDiagnosesByIds } from "../Diagnosis/utils.js";
import Tabs from "../Common/components/Tabs.js";

const Loading = lazy(() => import("../Common/Loading"));

@@ -846,32 +846,36 @@ export const PatientManager = () => {
</ButtonV2>
</div>
<div className="flex w-full flex-col items-center justify-end gap-2 lg:ml-3 lg:w-fit lg:flex-row lg:gap-3">
<SwitchTabs
tab1="Live"
tab2="Discharged"
onClickTab1={() => updateQuery({ is_active: "True" })}
onClickTab2={() => {
// Navigate to dedicated discharged list page if filtered by a facility or user has access only to one facility.
const id = qParams.facility || onlyAccessibleFacility?.id;
if (id) {
navigate(`facility/${id}/discharged-patients`);
return;
}
<Tabs
tabs={[
{ text: t("live"), value: 0 },
{ text: t("discharged"), value: 1 },
]}
onTabChange={(tab) => {
if (tab === "LIVE") {
updateQuery({ is_active: "True" });
} else {
const id = qParams.facility || onlyAccessibleFacility?.id;
if (id) {
navigate(`facility/${id}/discharged-patients`);
return;
}

if (
authUser.user_type === "StateAdmin" ||
authUser.user_type === "StateReadOnlyAdmin"
) {
updateQuery({ is_active: "False" });
return;
}
if (
authUser.user_type === "StateAdmin" ||
authUser.user_type === "StateReadOnlyAdmin"
) {
updateQuery({ is_active: "False" });
return;
}

Notification.Warn({
msg: "Facility needs to be selected to view discharged patients.",
});
setShowDialog("list-discharged");
Notification.Warn({
msg: t("select_facility_for_discharged_patients_warning"),
});
setShowDialog("list-discharged");
}
}}
isTab2Active={!!tabValue}
currentTab={tabValue}
/>
{!!params.facility && (
<ButtonV2
86 changes: 10 additions & 76 deletions src/Components/Patient/PatientConsentRecordBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,25 @@
import dayjs from "dayjs";
import {
CONSENT_PATIENT_CODE_STATUS_CHOICES,
CONSENT_TYPE_CHOICES,
} from "../../Common/constants";
import { FileUploadModel } from "./models";
import CareIcon from "../../CAREUI/icons/CareIcon";
import ButtonV2 from "../Common/components/ButtonV2";
import useAuthUser from "../../Common/hooks/useAuthUser";
import { PatientConsentModel } from "../Facility/models";
import { SelectFormField } from "../Form/FormFields/SelectFormField";
import { useEffect, useState } from "react";
import request from "../../Utils/request/request";
import routes from "../../Redux/api";
import FileBlock from "../Files/FileBlock";
import { FileManagerResult } from "../../Utils/useFileManager";

export default function PatientConsentRecordBlockGroup(props: {
consentRecord: PatientConsentModel;
consultationId: string;
previewFile: (file: FileUploadModel, file_associating_id: string) => void;
archiveFile: (
file: FileUploadModel,
file_associating_id: string,
skipPrompt?: { reason: string },
) => void;
editFile: (file: FileUploadModel) => void;
showArchive: boolean;
fileManager: FileManagerResult;
files?: FileUploadModel[];
}) {
const {
consentRecord,
previewFile,
archiveFile,
editFile,
files,
showArchive,
consultationId,
} = props;
const { consentRecord, fileManager, files, consultationId } = props;

const authUser = useAuthUser();

@@ -112,64 +97,13 @@ export default function PatientConsentRecordBlockGroup(props: {
</div>
)}
{files?.map((file: FileUploadModel, i: number) => (
<div
<FileBlock
fileManager={fileManager}
key={i}
className={`flex flex-col justify-between gap-2 rounded-lg border border-secondary-300 xl:flex-row xl:items-center ${showArchive ? "text-secondary-600" : "bg-white"} px-4 py-2 transition-all hover:bg-secondary-100`}
>
<div className="flex items-center gap-4">
<div>
<CareIcon icon="l-file" className="text-5xl text-secondary-600" />
</div>
<div className="min-w-[40%] break-all">
<div className="">
{file.name}
{file.extension} {file.is_archived && "(Archived)"}
</div>
<div className="text-xs text-secondary-700">
{dayjs(
file.is_archived ? file.archived_datetime : file.created_date,
).format("DD MMM YYYY, hh:mm A")}{" "}
by{" "}
{file.is_archived
? file.archived_by?.username
: file.uploaded_by?.username}
</div>
</div>
</div>
<div className="flex shrink-0 flex-wrap justify-end gap-2">
{!file.is_archived && (
<ButtonV2
onClick={() => previewFile(file, consentRecord.id)}
className=""
>
<CareIcon icon="l-eye" />
View
</ButtonV2>
)}
{!file.is_archived && hasEditPermission(file) && (
<ButtonV2
variant={"secondary"}
onClick={() => editFile(file)}
className=""
>
<CareIcon icon={"l-pen"} />
Rename
</ButtonV2>
)}
{(file.is_archived || hasEditPermission(file)) && (
<ButtonV2
variant={file.is_archived ? "primary" : "secondary"}
onClick={() => archiveFile(file, consentRecord.id)}
className=""
>
<CareIcon
icon={file.is_archived ? "l-info-circle" : "l-archive"}
/>
{file.is_archived ? "More Info" : "Archive"}
</ButtonV2>
)}
</div>
</div>
file={file}
editable={hasEditPermission(file)}
associating_id={consentRecord.id}
/>
))}
</div>
);
31 changes: 19 additions & 12 deletions src/Components/Patient/PatientConsentRecords.tsx
Original file line number Diff line number Diff line change
@@ -15,9 +15,10 @@ 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";
import { PatientConsentModel } from "../Facility/models";
import Tabs from "../Common/components/Tabs";
import { t } from "i18next";

export default function PatientConsentRecords(props: {
facilityId: string;
@@ -140,13 +141,14 @@ export default function PatientConsentRecords(props: {
title="Archive Previous Records"
className="w-auto"
/>
<SwitchTabs
tab1="Active"
tab2="Archived"
<Tabs
tabs={[
{ text: t("active"), value: 0 },
{ text: t("archived"), value: 1 },
]}
className="my-4"
onClickTab1={() => setShowArchived(false)}
onClickTab2={() => setShowArchived(true)}
isTab2Active={showArchived}
onTabChange={(v) => setShowArchived(!!v)}
currentTab={showArchived ? 1 : 0}
/>
<div className="mt-8 flex flex-col gap-4 lg:flex-row-reverse">
<div className="shrink-0 lg:w-[350px]">
@@ -221,7 +223,15 @@ export default function PatientConsentRecords(props: {
</>
) : (
<>
<fileUpload.UploadButton />
<label
className={
"button-size-default button-shape-square button-primary-default inline-flex h-min w-full cursor-pointer items-center justify-center gap-2 whitespace-pre font-medium outline-offset-1 transition-all duration-200 ease-in-out"
}
>
<CareIcon icon={"l-file-upload-alt"} className="text-lg" />
{t("choose_file")}
<fileUpload.Input />
</label>
<button
type="button"
className="flex aspect-square h-9 shrink-0 items-center justify-center rounded text-xl transition-all hover:bg-black/10"
@@ -256,10 +266,7 @@ export default function PatientConsentRecords(props: {
key={index}
consultationId={consultationId}
consentRecord={record}
previewFile={fileManager.viewFile}
archiveFile={fileManager.archiveFile}
editFile={fileManager.editFile}
showArchive={showArchived}
fileManager={fileManager}
files={record.files?.filter(
(f) =>
f.associating_id === record.id &&
9 changes: 3 additions & 6 deletions src/Components/Patient/SampleDetails.tsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { lazy } from "react";

import ButtonV2 from "../Common/components/ButtonV2";
import Card from "../../CAREUI/display/Card";
import { FileUpload } from "./FileUpload";
import { FileUpload } from "../Files/FileUpload";
import Page from "../Common/components/Page";
import _ from "lodash-es";
import { formatDateTime, formatPatientAge } from "../../Utils/utils";
@@ -428,7 +428,7 @@ export const SampleDetails = ({ id }: DetailRoute) => {
{showPatientCard(sampleDetails?.patient_object)}
</div>

<div>
<div className="mb-4">
<h4 className="mt-8">Sample Test History</h4>
{sampleDetails?.flow &&
sampleDetails.flow.map((flow: FlowModel) => renderFlow(flow))}
@@ -437,12 +437,9 @@ export const SampleDetails = ({ id }: DetailRoute) => {
<FileUpload
sampleId={id}
patientId=""
facilityId=""
consultationId=""
type="SAMPLE_MANAGEMENT"
hideBack={true}
unspecified={true}
audio={true}
allowAudio={true}
/>
</Page>
);
7 changes: 5 additions & 2 deletions src/Components/Patient/UpdateStatusDialog.tsx
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@ import {
SAMPLE_TEST_STATUS,
SAMPLE_TEST_RESULT,
SAMPLE_FLOW_RULES,
HEADER_CONTENT_TYPES,
} from "../../Common/constants";
import { CreateFileResponse, SampleTestModel } from "./models";
import * as Notification from "../../Utils/Notifications.js";
import { header_content_type, LinearProgressWithLabel } from "./FileUpload";
import { LinearProgressWithLabel } from "../Files/FileUpload";
import { Submit } from "../Common/components/ButtonV2";
import CareIcon from "../../CAREUI/icons/CareIcon";
import ConfirmDialog from "../Common/ConfirmDialog";
@@ -143,7 +144,9 @@ const UpdateStatusDialog = (props: Props) => {
setfile(e.target.files[0]);
const fileName = e.target.files[0].name;
const ext: string = fileName.split(".")[1];
setcontentType(header_content_type[ext]);
setcontentType(
HEADER_CONTENT_TYPES[ext as keyof typeof HEADER_CONTENT_TYPES],
);
return e.target.files[0];
};
const handleUpload = async () => {
15 changes: 8 additions & 7 deletions src/Components/Resource/ResourceBoardView.tsx
Original file line number Diff line number Diff line change
@@ -9,12 +9,12 @@ import BadgesList from "./BadgesList";
import { formatFilter } from "./Commons";
import useFilters from "../../Common/hooks/useFilters";
import { ExportButton } from "../Common/Export";
import SwitchTabs from "../Common/components/SwitchTabs";
import ButtonV2 from "../Common/components/ButtonV2";
import { useTranslation } from "react-i18next";
import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover";
import CareIcon from "../../CAREUI/icons/CareIcon";
import SearchInput from "../Form/SearchInput";
import Tabs from "../Common/components/Tabs";

const Loading = lazy(() => import("../Common/Loading"));
const PageTitle = lazy(() => import("../Common/PageTitle"));
@@ -67,12 +67,13 @@ export default function BoardView() {
onChange={(e) => updateQuery({ [e.name]: e.value })}
placeholder={t("search_resource")}
/>
<SwitchTabs
tab1="Active"
tab2="Completed"
onClickTab1={() => setBoardFilter(ACTIVE)}
onClickTab2={() => setBoardFilter(COMPLETED)}
isTab2Active={boardFilter !== ACTIVE}
<Tabs
tabs={[
{ text: t("active"), value: 0 },
{ text: t("completed"), value: 1 },
]}
onTabChange={(tab) => setBoardFilter(tab ? COMPLETED : ACTIVE)}
currentTab={boardFilter !== ACTIVE ? 1 : 0}
/>
<div className="flex w-full flex-col gap-2 lg:mr-4 lg:w-fit lg:flex-row lg:gap-4">
<ButtonV2 className="py-[11px]" onClick={onListViewBtnClick}>
17 changes: 10 additions & 7 deletions src/Components/Shifting/BoardView.tsx
Original file line number Diff line number Diff line change
@@ -18,9 +18,9 @@ import { lazy, useLayoutEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import withScrolling from "react-dnd-scrolling";
import ButtonV2 from "../Common/components/ButtonV2";
import SwitchTabs from "../Common/components/SwitchTabs";
import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover";
import CareIcon from "../../CAREUI/icons/CareIcon";
import Tabs from "../Common/components/Tabs";

const Loading = lazy(() => import("../Common/Loading"));
const PageTitle = lazy(() => import("../Common/PageTitle"));
@@ -153,12 +153,15 @@ export default function BoardView() {
placeholder={t("search_patient")}
/>

<SwitchTabs
tab1={t("active")}
tab2={t("completed")}
onClickTab1={() => setBoardFilter(activeBoards)}
onClickTab2={() => setBoardFilter(completedBoards)}
isTab2Active={boardFilter[0].text !== activeBoards[0].text}
<Tabs
tabs={[
{ text: t("active"), value: 0 },
{ text: t("archived"), value: 1 },
]}
onTabChange={(tab) =>
setBoardFilter(tab ? completedBoards : activeBoards)
}
currentTab={boardFilter[0].text !== activeBoards[0].text ? 1 : 0}
/>

<div className="flex w-full flex-col gap-2 lg:mr-4 lg:w-fit lg:flex-row lg:gap-4">
18 changes: 14 additions & 4 deletions src/Locale/en/Common.json
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@
"care": "CARE",
"something_went_wrong": "Something went wrong..!",
"stop": "Stop",
"record": "Record",
"record": "Record Audio",
"recording": "Recording",
"yes": "Yes",
"no": "No",
@@ -105,7 +105,7 @@
"filters": "Filters",
"unknown": "Unknown",
"active": "Active",
"completed": "Archived",
"completed": "Completed",
"on": "On",
"open": "Open",
"features": "Features",
@@ -148,7 +148,8 @@
"add_as": "Add as",
"sort_by": "Sort By",
"none": "None",
"choose_file": "Choose File",
"choose_file": "Upload From Device",
"open_camera": "Open Camera",
"file_preview": "File Preview",
"file_preview_not_supported": "Can't preview this file. Try downloading it.",
"view_faciliy": "View Facility",
@@ -182,5 +183,14 @@
"action_irreversible": "This action is irreversible",
"GENDER__1": "Male",
"GENDER__2": "Female",
"GENDER__3": "Non-binary"
"GENDER__3": "Non-binary",
"done": "Done",
"view": "View",
"rename": "Rename",
"more_info": "More Info",
"archive": "Archive",
"discard": "Discard",
"live": "Live",
"discharged": "Discharged",
"archived": "Archived"
}
1 change: 1 addition & 0 deletions src/Locale/en/Facility.json
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@
"discharged_patients": "Discharged Patients",
"discharged_patients_empty": "No discharged patients present in this facility",
"update_facility_middleware_success": "Facility middleware updated successfully",
"select_facility_for_discharged_patients_warning": "Facility needs to be selected to view discharged patients.",
"duplicate_patient_record_confirmation": "Admit the patient record to your facility by adding the year of birth",
"duplicate_patient_record_rejection": "I confirm that the suspect / patient I want to create is not on the list.",
"duplicate_patient_record_birth_unknown": "Please contact your district care coordinator, the shifting facility or the patient themselves if you are not sure about the patient's year of birth.",
29 changes: 29 additions & 0 deletions src/Locale/en/FileUpload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"audio__allow_permission": "Please allow microphone permission in site settings",
"audio__allow_permission_helper": "You might have denied microphone access in the past.",
"audio__allow_permission_button": "Click here to know how to allow",
"audio__record": "Record Audio",
"audio__record_helper": "Click the button to start recording",
"audio__recording": "Recording",
"audio__recording_helper": "Please speak into your microphone.",
"audio__recording_helper_2": "Click on the button to stop recording.",
"audio__recorded": "Audio Recorded",
"audio__start_again": "Start Again",
"enter_file_name": "Enter File Name",
"no_files_found": "No {{type}} files found",
"upload_headings__patient": "Upload New Patient File",
"upload_headings__consultation": "Upload New Consultation File",
"upload_headings__sample_report": "Upload Sample Report",
"upload_headings__supporting_info": "Upload Supporting Info",
"file_list_headings__patient": "Patient Files",
"file_list_headings__consultation": "Consultation Files",
"file_list_headings__sample_report": "Sample Report",
"file_list_headings__supporting_info": "Supporting Info",
"file_error__choose_file": "Please choose a file to upload",
"file_error__file_name": "Please enter file name",
"file_error__file_size": "Maximum size of files is 100 MB",
"file_error__file_type": "Invalid file type \".{{extension}}\" Allowed types: {{allowedExtensions}}",
"file_uploaded": "File Uploaded Successfully",
"file_error__dynamic": "Error Uploading File: {{statusText}}",
"file_error__network": "Error Uploading File: Network Error"
}
2 changes: 2 additions & 0 deletions src/Locale/en/index.js
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import Resource from "./Resource.json";
import Shifting from "./Shifting.json";
import SortOptions from "./SortOptions.json";
import Users from "./Users.json";
import FileUpload from "./FileUpload.json";

export default {
...Auth,
@@ -37,5 +38,6 @@ export default {
...Bed,
...Users,
...LogUpdate,
...FileUpload,
SortOptions,
};
7 changes: 2 additions & 5 deletions src/Routers/routes/ConsultationRoutes.tsx
Original file line number Diff line number Diff line change
@@ -4,14 +4,14 @@ import ShowInvestigation from "../../Components/Facility/Investigations/ShowInve
import ManagePrescriptions from "../../Components/Medicine/ManagePrescriptions";
import { DailyRoundListDetails } from "../../Components/Patient/DailyRoundListDetails";
import { DailyRounds } from "../../Components/Patient/DailyRounds";
import { FileUpload } from "../../Components/Patient/FileUpload";
import { ConsultationDetails } from "../../Components/Facility/ConsultationDetails";
import TreatmentSummary from "../../Components/Facility/TreatmentSummary";
import ConsultationDoctorNotes from "../../Components/Facility/ConsultationDoctorNotes";
import PatientConsentRecords from "../../Components/Patient/PatientConsentRecords";
import CriticalCareEditor from "../../Components/LogUpdate/CriticalCareEditor";
import PrescriptionsPrintPreview from "../../Components/Medicine/PrintPreview";
import CriticalCarePreview from "../../Components/LogUpdate/CriticalCarePreview";
import FileUploadPage from "../../Components/Patient/FileUploadPage";

export default {
"/facility/:facilityId/patient/:patientId/consultation": ({
@@ -38,14 +38,11 @@ export default {
patientId,
id,
}: any) => (
<FileUpload
<FileUploadPage
facilityId={facilityId}
patientId={patientId}
consultationId={id}
type="CONSULTATION"
hideBack={false}
audio={true}
unspecified={true}
/>
),
"/facility/:facilityId/patient/:patientId/consultation/:consultationId/prescriptions":
10 changes: 3 additions & 7 deletions src/Routers/routes/PatientRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import InvestigationReports from "../../Components/Facility/Investigations/Reports";
import { FileUpload } from "../../Components/Patient/FileUpload";
import { PatientManager } from "../../Components/Patient/ManagePatients";
import { PatientHome } from "../../Components/Patient/PatientHome";
import PatientNotes from "../../Components/Patient/PatientNotes";
import { PatientRegister } from "../../Components/Patient/PatientRegister";
import { DetailRoute } from "../types";
import DeathReport from "../../Components/DeathReport/DeathReport";
import { InsuranceDetails } from "../../Components/Patient/InsuranceDetails";
import FileUploadPage from "../../Components/Patient/FileUploadPage";

export default {
"/patients": () => <PatientManager />,
@@ -36,14 +36,10 @@ export default {
facilityId,
patientId,
}: any) => (
<FileUpload
patientId={patientId}
<FileUploadPage
facilityId={facilityId}
consultationId=""
patientId={patientId}
type="PATIENT"
hideBack={false}
audio={true}
unspecified={true}
/>
),
"/death_report/:id": ({ id }: any) => <DeathReport id={id} />,
102 changes: 0 additions & 102 deletions src/Utils/VoiceRecorder.tsx

This file was deleted.

88 changes: 81 additions & 7 deletions src/Utils/useFileManager.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from "react";
import FilePreviewDialog from "../Components/Common/FilePreviewDialog";
import { FileUploadModel } from "../Components/Patient/models";
import { ExtImage, StateInterface } from "../Components/Patient/FileUpload";
import { StateInterface } from "../Components/Files/FileUpload";
import request from "./request/request";
import routes from "../Redux/api";
import DialogModal from "../Components/Common/Dialog";
@@ -11,22 +11,34 @@ import { Cancel, Submit } from "../Components/Common/components/ButtonV2";
import { formatDateTime } from "./utils";
import * as Notification from "./Notifications.js";
import TextFormField from "../Components/Form/FormFields/TextFormField";
import {
FILE_EXTENSIONS,
PREVIEWABLE_FILE_EXTENSIONS,
} from "../Common/constants";

export interface FileManagerOptions {
type: string;
onArchive?: () => void;
onEdit?: () => void;
}

export interface FileManagerResult {
viewFile: (file: FileUploadModel, associating_id: string) => void;
archiveFile: (
file: FileUploadModel,
associating_id: string,
skipPrompt?: { reason: string },
) => void;
editFile: (file: FileUploadModel) => void;
editFile: (file: FileUploadModel, associating_id: string) => void;
Dialogues: React.ReactNode;
isPreviewable: (file: FileUploadModel) => boolean;
getFileType: (
file: FileUploadModel,
) => keyof typeof FILE_EXTENSIONS | "UNKNOWN";
downloadFile: (
file: FileUploadModel,
associating_id: string,
) => Promise<void>;
type: string;
}

export default function useFileManager(
@@ -92,7 +104,9 @@ export default function useFileManager(
open: true,
name: data.name as string,
extension,
isImage: ExtImage.includes(extension),
isImage: FILE_EXTENSIONS.IMAGE.includes(
extension as (typeof FILE_EXTENSIONS.IMAGE)[number],
),
});
downloadFileUrl(signedUrl);
setFileUrl(signedUrl);
@@ -195,8 +209,8 @@ export default function useFileManager(
setEditing(false);
};

const editFile = (file: FileUploadModel) => {
setEditDialogueOpen(file);
const editFile = (file: FileUploadModel, associating_id: string) => {
setEditDialogueOpen({ ...file, associating_id });
};

const Dialogues = (
@@ -245,6 +259,7 @@ export default function useFileManager(
<div>
<TextAreaFormField
name="editFileName"
id="archive-file-reason"
label={
<span>
State the reason for archiving{" "}
@@ -327,7 +342,12 @@ export default function useFileManager(
<div className="text-xs uppercase text-secondary-700">
{item.label}
</div>
<div className="break-words text-base">{item.content}</div>
<div
className="break-words text-base"
data-archive-info={item.label}
>
{item.content}
</div>
</div>
</div>
))}
@@ -364,6 +384,7 @@ export default function useFileManager(
<div>
<TextFormField
name="editFileName"
id="edit-file-name"
label="Enter the file name"
value={editDialogueOpen?.name}
onChange={(e) => {
@@ -388,10 +409,63 @@ export default function useFileManager(
</>
);

const isPreviewable = (file: FileUploadModel) =>
!!file.extension &&
PREVIEWABLE_FILE_EXTENSIONS.includes(
file.extension.slice(1) as (typeof PREVIEWABLE_FILE_EXTENSIONS)[number],
);

const getFileType: (
f: FileUploadModel,
) => keyof typeof FILE_EXTENSIONS | "UNKNOWN" = (file: FileUploadModel) => {
if (!file.extension) return "UNKNOWN";
const ftype = (
Object.keys(FILE_EXTENSIONS) as (keyof typeof FILE_EXTENSIONS)[]
).find((type) =>
FILE_EXTENSIONS[type].includes((file.extension?.slice(1) || "") as never),
);
return ftype || "UNKNOWN";
};

const downloadFile = async (
file: FileUploadModel,
associating_id: string,
) => {
try {
if (!file.id) return;
Notification.Success({ msg: "Downloading file..." });
const { data: fileData } = await request(routes.retrieveUpload, {
query: { file_type: fileType, associating_id },
pathParams: { id: file.id },
});
const response = await fetch(fileData?.read_signed_url || "");
if (!response.ok) throw new Error("Network response was not ok.");

const data = await response.blob();
const blobUrl = window.URL.createObjectURL(data);

const a = document.createElement("a");
a.href = blobUrl;
a.download = file.name || "file";
document.body.appendChild(a);
a.click();

// Clean up
window.URL.revokeObjectURL(blobUrl);
document.body.removeChild(a);
} catch (err) {
Notification.Error({ msg: "Failed to download file" });
}
};

return {
viewFile,
archiveFile,
editFile,
Dialogues,
isPreviewable,
getFileType,
downloadFile,
type: fileType,
};
}
314 changes: 66 additions & 248 deletions src/Utils/useFileUpload.tsx

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/Utils/useRecorder.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// why is this file in js? can we convert to ts?

import { useEffect, useState } from "react";
import { Error } from "./Notifications";

52 changes: 52 additions & 0 deletions src/Utils/useTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect, useState } from "react";

/**
* Custom hook to manage a timer in MM:SS format. This can be useful for tracking time during recording sessions, user actions, or any other timed event.
*
* @returns {Object} A set of properties and methods to control and display the timer:
*
* @property {number} seconds - The total elapsed time in seconds.
* @property {JSX.Element} time - A JSX element displaying the current time in MM:SS format.
* @property {function} start - Function to start the timer.
* @property {function} stop - Function to stop the timer.
*
* @example
* const { time, start, stop } = useTimer();
*
* // To start the timer:
* start();
*
* // To stop the timer:
* stop();
*
* // To display the timer in your component:
* <div>{time}</div>
*/
export const useTimer = () => {
const [running, setRunning] = useState(false);
const [time, setTime] = useState(0);

useEffect(() => {
let interval: NodeJS.Timeout;
if (running) {
interval = setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 10);
} else {
setTime(0);
}
return () => clearInterval(interval);
}, [running]);

return {
seconds: time,
time: (
<span>
{("0" + Math.floor((time / 6000) % 60)).slice(-2)}:
{("0" + Math.floor((time / 100) % 60)).slice(-2)}
</span>
),
start: () => setRunning(true),
stop: () => setRunning(false),
};
};