From 6b3e0e7f3aa84d6c2aa31304d0525ab970a985fc Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Tue, 17 Dec 2024 11:30:44 +0530 Subject: [PATCH 1/3] Patient Files v1 --- package-lock.json | 166 +++++++ package.json | 1 + public/locale/en.json | 9 + .../Patient/PatientDetailsTab/Files.tsx | 444 ++++++++++++++++++ .../Patient/PatientDetailsTab/index.tsx | 5 + src/components/ui/tabs.tsx | 58 +++ 6 files changed, 683 insertions(+) create mode 100644 src/components/Patient/PatientDetailsTab/Files.tsx create mode 100644 src/components/ui/tabs.tsx diff --git a/package-lock.json b/package-lock.json index 94a84d3d179..adf41ceb731 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.4", "@sentry/browser": "^8.42.0", @@ -4200,6 +4201,171 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz", + "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz", diff --git a/package.json b/package.json index e5eaf3342c9..db0731d4fb2 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.4", "@sentry/browser": "^8.42.0", diff --git a/public/locale/en.json b/public/locale/en.json index 2ceaf310fac..7f724c569f1 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -279,6 +279,7 @@ "action_irreversible": "This action is irreversible", "actions": "Actions", "active": "Active", + "active_files": "Active Files", "active_prescriptions": "Active Prescriptions", "add": "Add", "add_as": "Add as", @@ -289,6 +290,7 @@ "add_consultation_update": "Add Consultation Update", "add_details_of_patient": "Add Details of Patient", "add_facility": "Add Facility", + "add_files": "Add Files", "add_insurance_details": "Add Insurance Details", "add_location": "Add Location", "add_new_beds": "Add New Bed(s)", @@ -333,6 +335,7 @@ "approving_facility": "Name of Approving Facility", "archive": "Archive", "archived": "Archived", + "archived_files": "Archived Files", "are_non_editable_fields": "are non-editable fields", "are_you_still_watching": "Are you still watching?", "are_you_sure_want_to_delete": "Are you sure you want to delete {{name}}?", @@ -471,6 +474,7 @@ "claims": "Claims", "clear": "Clear", "clear_all_filters": "Clear All Filters", + "clear_filter": "Clear Filter", "clear_home_facility": "Clear Home Facility", "clear_home_facility_confirm": "Are you sure you want to clear the home facility", "clear_home_facility_error": "Error while clearing home facility. Try again later.", @@ -579,6 +583,7 @@ "cylinders": "Cylinders", "cylinders_per_day": "Cylinders/day", "daily_rounds": "Daily Rounds", + "date": "Date", "date_and_time": "Date and Time", "date_declared_positive": "Date of declaring positive", "date_of_admission": "Date of Admission", @@ -758,8 +763,10 @@ "file_list_headings__patient": "Patient Files", "file_list_headings__sample_report": "Sample Report", "file_list_headings__supporting_info": "Supporting Info", + "file_name": "File Name", "file_preview": "File Preview", "file_preview_not_supported": "Can't preview this file. Try downloading it.", + "file_type": "File Type", "file_uploaded": "File Uploaded Successfully", "filter": "Filter", "filter_by": "Filter By", @@ -1089,6 +1096,7 @@ "password_uppercase_validation": "Password must contain at least one uppercase letter (A-Z)", "password_validation": "Password must contain at least: 8 characters, 1 uppercase letter (A-Z), 1 lowercase letter (a-z), and 1 number (0-9)", "patient": "Patient", + "patient-files": "Files", "patient-notes": "Notes", "patient__general-info": "General Info", "patient__insurance-details": "Insurance Details", @@ -1342,6 +1350,7 @@ "settings_and_filters": "Settings and Filters", "severity_of_breathlessness": "Severity of Breathlessness", "sex": "Sex", + "shared_by": "Shared By", "shift": "Shift Patient", "shift_request_updated_successfully": "Shift request updated successfully", "shifting": "Shifting", diff --git a/src/components/Patient/PatientDetailsTab/Files.tsx b/src/components/Patient/PatientDetailsTab/Files.tsx new file mode 100644 index 00000000000..15847d9132e --- /dev/null +++ b/src/components/Patient/PatientDetailsTab/Files.tsx @@ -0,0 +1,444 @@ +import { useQuery } from "@tanstack/react-query"; +import dayjs from "dayjs"; +import { useTranslation } from "react-i18next"; + +import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +import Loading from "@/components/Common/Loading"; + +import useFileManager from "@/hooks/useFileManager"; +import useFileUpload from "@/hooks/useFileUpload"; +import useFilters from "@/hooks/useFilters"; + +import { FILE_EXTENSIONS } from "@/common/constants"; + +import routes from "@/Utils/request/api"; +import request from "@/Utils/request/request"; +import { classNames } from "@/Utils/utils"; + +import { PatientProps } from "."; +import { FileUploadModel } from "../models"; + +const PatientFiles = (props: PatientProps) => { + const { patientData, id } = props; + const { qParams, updateQuery } = useFilters({}); + const { t } = useTranslation(); + + const fileCategories = [ + { value: "all", label: "All" }, + { value: "imaging", label: "Imaging" }, + { value: "lab_reports", label: "Lab Reports" }, + { value: "documents", label: "Documents" }, + { value: "audio", label: "Audio" }, + ]; + + const handleTabChange = (value: string) => { + updateQuery({ file_category: value === "all" ? undefined : value }); + }; + + const { + data: files, + isLoading: filesLoading, + refetch, + } = useQuery({ + queryKey: ["patient-files", patientData.id, qParams.is_archived], + queryFn: async () => { + const response = await request(routes.viewUpload, { + query: { + file_type: "PATIENT", + associating_id: id, + limit: qParams.limit, + offset: qParams.offset, + ...(qParams.is_archived !== undefined && { + is_archived: qParams.is_archived, + }), + //file_category: qParams.file_category, + }, + }); + return response.data; + }, + }); + + const fileManager = useFileManager({ + type: "PATIENT", + onArchive: refetch, + onEdit: refetch, + uploadedFiles: + files?.results + .slice() + .reverse() + .map((file) => ({ + ...file, + associating_id: id, + })) || [], + }); + + const fileUpload = useFileUpload({ + type: "PATIENT", + allowedExtensions: [ + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "tiff", + "mp4", + "mov", + "avi", + "wmv", + "mp3", + "wav", + "ogg", + "txt", + "csv", + "rtf", + "doc", + "odt", + "pdf", + "xls", + "xlsx", + "ods", + "pdf", + ], + allowNameFallback: false, + onUpload: () => refetch(), + }); + + const getFileType = (file: FileUploadModel) => { + return fileManager.getFileType(file); + }; + + if (filesLoading) return ; + + const icons: Record = { + AUDIO: "l-volume", + IMAGE: "l-image", + PRESENTATION: "l-presentation-play", + VIDEO: "l-video", + UNKNOWN: "l-file-medical", + DOCUMENT: "l-file-medical", + }; + + const getTableHeaders = () => { + return ( + + + + + {t("file_name")} + + + {t("file_type")} + + + {t("date")} + + + {t("shared_by")} + + + + + + ); + }; + + const GetButtons = (file: FileUploadModel) => { + const filetype = getFileType(file); + const fileData = useQuery({ + queryKey: [routes.retrieveUpload, "PATIENT", file.id], + queryFn: async () => { + const response = await request(routes.retrieveUpload, { + query: { file_type: "PATIENT", associating_id: id }, + pathParams: { id: file.id || "" }, + }); + return response.data; + }, + enabled: filetype === "AUDIO" && !file.is_archived, + }); + return ( +
+ {filetype === "AUDIO" && ( +
+
+ )} + {fileManager.isPreviewable(file) && ( + + )} + + + + + + + + + + + + + + + + +
+ ); + }; + + const getArchivedMessage = () => { + return ( +
+ + {t("archived")} + +
+ ); + }; + + const getFilterButton = () => { + return ( + + + + + + { + updateQuery({ is_archived: "false" }); + }} + > + {t("active_files")} + + { + updateQuery({ is_archived: "true" }); + }} + > + {t("archived_files")} + + + + ); + }; + + const getFilterBadges = () => { + if (typeof qParams.is_archived === "undefined") + return
; + return ( +
+ updateQuery({ is_archived: undefined })} + > + {t( + qParams.is_archived === "false" ? "active_files" : "archived_files", + )} + + +
+ ); + }; + + const getFileUploadButtons = () => { + return ( + + + + + + + + {fileUpload.Input({ className: "hidden" })} + + + + + + + + + + ); + }; + + const getTableRow = (file: FileUploadModel) => { + const filetype = getFileType(file); + return ( + + + + + + + {file.name && file.name.length > 10 ? ( + + + + + {file.name?.slice(0, 8) + "..."} + + + + + {file.name} + {file.extension} + + + + + ) : ( + + {file.name} + {file.extension} + + )} + + + {filetype} + + {dayjs(file.created_date).format("DD MMM YYYY, hh:mm A")} + + {file.uploaded_by?.username} + + {file.is_archived ? getArchivedMessage() : GetButtons(file)} + + + ); + }; + + const RenderTable = () => { + return ( +
+ + {getTableHeaders()} + + {files?.results?.map((file) => { + return getTableRow(file); + })} + +
+
+ ); + }; + + return ( +
+
+ {fileUpload.Dialogues} + {fileManager.Dialogues} +
+ +
+
+ + {fileCategories.map((category) => ( + + {category.label} + + ))} + + {getFilterButton()} +
+ {getFileUploadButtons()} +
+ {getFilterBadges()} + {fileCategories.map((category) => ( + + + + ))} +
+
+ ); +}; + +export default PatientFiles; diff --git a/src/components/Patient/PatientDetailsTab/index.tsx b/src/components/Patient/PatientDetailsTab/index.tsx index 6f4b7ecc982..9c306926207 100644 --- a/src/components/Patient/PatientDetailsTab/index.tsx +++ b/src/components/Patient/PatientDetailsTab/index.tsx @@ -1,6 +1,7 @@ import { PatientModel } from "../models"; import { Demography } from "./Demography"; import EncounterHistory from "./EncounterHistory"; +import PatientFiles from "./Files"; import { HealthProfileSummary } from "./HealthProfileSummary"; import { ImmunisationRecords } from "./ImmunisationRecords"; import PatientNotes from "./Notes"; @@ -33,6 +34,10 @@ export const patientTabs = [ route: "shift", component: ShiftingHistory, }, + { + route: "patient-files", + component: PatientFiles, + }, { route: "patient-notes", component: PatientNotes, diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 00000000000..1b42c9a7620 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,58 @@ +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; From 54b439a2409a58f2c135788f1b7933b9d41869fa Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 18 Dec 2024 11:02:31 +0530 Subject: [PATCH 2/3] audio player, file upload dialogues nand more --- package-lock.json | 207 ++++++++++++++ package.json | 2 + public/locale/en.json | 3 + src/components/Common/AudioPlayer.tsx | 148 ++++++++++ .../Patient/PatientDetailsTab/Files.tsx | 264 ++++++++++++++---- src/components/ui/progress.tsx | 26 ++ src/components/ui/slider.tsx | 26 ++ 7 files changed, 625 insertions(+), 51 deletions(-) create mode 100644 src/components/Common/AudioPlayer.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/slider.tsx diff --git a/package-lock.json b/package-lock.json index adf41ceb731..19a270ee285 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,9 @@ "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.2", @@ -4107,6 +4109,82 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz", + "integrity": "sha512-6diOawA84f/eMxFHcWut0aE1C2kyE9dOyCTQOMRR2C/qPiXz/X0SaiA/RLbapQaXUCmy0/hLMf9meSccD1N0pA==", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", @@ -4183,6 +4261,121 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz", + "integrity": "sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -4500,6 +4693,20 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", diff --git a/package.json b/package.json index db0731d4fb2..0d88ca4dcb2 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,9 @@ "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.2", diff --git a/public/locale/en.json b/public/locale/en.json index 7f724c569f1..7b9314660ba 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1150,6 +1150,8 @@ "phone_number": "Phone Number", "phone_number_at_current_facility": "Phone Number of Contact person at current Facility", "pincode": "Pincode", + "play": "Play", + "play_audio": "Play Audio", "please_assign_bed_to_patient": "Please assign a bed to this patient", "please_confirm_password": "Please confirm your new password.", "please_enter_a_reason_for_the_shift": "Please enter a reason for the shift.", @@ -1492,6 +1494,7 @@ "updating": "Updating", "upload": "Upload", "upload_an_image": "Upload an image", + "upload_file": "Upload File", "upload_headings__consultation": "Upload New Consultation File", "upload_headings__patient": "Upload New Patient File", "upload_headings__sample_report": "Upload Sample Report", diff --git a/src/components/Common/AudioPlayer.tsx b/src/components/Common/AudioPlayer.tsx new file mode 100644 index 00000000000..9f4545d69ab --- /dev/null +++ b/src/components/Common/AudioPlayer.tsx @@ -0,0 +1,148 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import { cn } from "@/lib/utils"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; +import { Slider } from "@/components/ui/slider"; + +interface AudioPlayerProps { + src: string; + className?: string; +} + +function AudioPlayer({ src, className }: AudioPlayerProps) { + const [isPlaying, setIsPlaying] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [isMuted, setIsMuted] = useState(false); + const audioRef = useRef(null); + + const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds.toString().padStart(2, "0")}`; + }; + + const handleTimeUpdate = useCallback(() => { + if (audioRef.current) { + setCurrentTime(audioRef.current.currentTime); + } + }, []); + + const handleLoadedMetadata = useCallback(() => { + if (audioRef.current) { + audioRef.current.currentTime = 24 * 60 * 60; // Seek to 24 hours + } + }, []); + + const handleDurationChange = useCallback(() => { + if (audioRef.current && isFinite(audioRef.current.duration)) { + setDuration(audioRef.current.duration); + setIsLoading(false); + audioRef.current.currentTime = 0; + setCurrentTime(0); + } + }, []); + + useEffect(() => { + // Create the audio element + const audio = new Audio(src); + audio.preload = "metadata"; + + // Add event listeners + audio.addEventListener("loadedmetadata", handleLoadedMetadata); + audio.addEventListener("durationchange", handleDurationChange); + audio.addEventListener("timeupdate", handleTimeUpdate); + audio.addEventListener("ended", () => setIsPlaying(false)); + + // Set the ref + audioRef.current = audio; + + // Cleanup + return () => { + audio.removeEventListener("loadedmetadata", handleLoadedMetadata); + audio.removeEventListener("durationchange", handleDurationChange); + audio.removeEventListener("timeupdate", handleTimeUpdate); + audio.removeEventListener("ended", () => setIsPlaying(false)); + audio.remove(); + }; + }, [src, handleLoadedMetadata, handleDurationChange, handleTimeUpdate]); + + const togglePlay = useCallback(() => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }, [isPlaying]); + + const toggleMute = useCallback(() => { + if (audioRef.current) { + audioRef.current.muted = !audioRef.current.muted; + setIsMuted(!isMuted); + } + }, [isMuted]); + + const handleSliderChange = useCallback((value: number[]) => { + if (audioRef.current) { + audioRef.current.currentTime = value[0]; + setCurrentTime(value[0]); + } + }, []); + + const stopPlayback = () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.currentTime = 0; + setIsPlaying(false); + } + }; + + const Player = () => ( +
+ +
+ + {formatTime(currentTime)} / {formatTime(duration)} + + + +
+
+ ); + + return { Player, isPlaying, isLoading, stopPlayback }; +} + +export default AudioPlayer; diff --git a/src/components/Patient/PatientDetailsTab/Files.tsx b/src/components/Patient/PatientDetailsTab/Files.tsx index 15847d9132e..6b225d67d31 100644 --- a/src/components/Patient/PatientDetailsTab/Files.tsx +++ b/src/components/Patient/PatientDetailsTab/Files.tsx @@ -1,17 +1,25 @@ import { useQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Progress } from "@/components/ui/progress"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -20,10 +28,12 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; +import AudioPlayer from "@/components/Common/AudioPlayer"; import Loading from "@/components/Common/Loading"; +import TextFormField from "@/components/Form/FormFields/TextFormField"; import useFileManager from "@/hooks/useFileManager"; -import useFileUpload from "@/hooks/useFileUpload"; +import useFileUpload, { FileUploadReturn } from "@/hooks/useFileUpload"; import useFilters from "@/hooks/useFilters"; import { FILE_EXTENSIONS } from "@/common/constants"; @@ -39,6 +49,10 @@ const PatientFiles = (props: PatientProps) => { const { patientData, id } = props; const { qParams, updateQuery } = useFilters({}); const { t } = useTranslation(); + const [openUploadDialog, setOpenUploadDialog] = useState(false); + const [selectedAudioFile, setSelectedAudioFile] = + useState(null); + const [openAudioPlayerDialog, setOpenAudioPlayerDialog] = useState(false); const fileCategories = [ { value: "all", label: "All" }, @@ -120,6 +134,20 @@ const PatientFiles = (props: PatientProps) => { onUpload: () => refetch(), }); + useEffect(() => { + if (fileUpload.files.length > 0 && fileUpload.files[0] !== undefined) { + setOpenUploadDialog(true); + } else { + setOpenUploadDialog(false); + } + }, [fileUpload.files]); + + useEffect(() => { + if (!openUploadDialog) { + fileUpload.clearFiles(); + } + }, [openUploadDialog]); + const getFileType = (file: FileUploadModel) => { return fileManager.getFileType(file); }; @@ -161,29 +189,21 @@ const PatientFiles = (props: PatientProps) => { const GetButtons = (file: FileUploadModel) => { const filetype = getFileType(file); - const fileData = useQuery({ - queryKey: [routes.retrieveUpload, "PATIENT", file.id], - queryFn: async () => { - const response = await request(routes.retrieveUpload, { - query: { file_type: "PATIENT", associating_id: id }, - pathParams: { id: file.id || "" }, - }); - return response.data; - }, - enabled: filetype === "AUDIO" && !file.is_archived, - }); return (
- {filetype === "AUDIO" && ( -
-
+ {filetype === "AUDIO" && !file.is_archived && ( + )} {fileManager.isPreviewable(file) && ( )} - - - - - - - - - - - - - - - - + { + + + + + + + + + + + + + + + + + }
); }; @@ -303,10 +325,15 @@ const PatientFiles = (props: PatientProps) => { - + { + e.preventDefault(); + }} + >