diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx index b03c232ff37..0c516f9e218 100644 --- a/src/Routers/routes/ConsultationRoutes.tsx +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -91,11 +91,11 @@ const consultationRoutes: AppRoutes = { ), "/facility/:facilityId/patient/:patientId/encounter/:encounterId/report/template/:templateSlug": ({ encounterId, templateSlug }) => ( - + ), "/facility/:facilityId/patient/:patientId/encounter/:encounterId/report/:reportId": ({ encounterId, reportId }) => ( - + ), "/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire": ({ facilityId, encounterId, patientId }) => ( diff --git a/src/Routers/routes/FacilityRoutes.tsx b/src/Routers/routes/FacilityRoutes.tsx index 2389dbfc538..73f2ca52ff8 100644 --- a/src/Routers/routes/FacilityRoutes.tsx +++ b/src/Routers/routes/FacilityRoutes.tsx @@ -7,6 +7,7 @@ import MedicationDispenseRedirect from "@/pages/Facility/billing/account/compone import BedAvailabilityDashboard from "@/pages/Facility/BedAvailabilityDashboard"; import { AppRoutes } from "@/Routers/AppRouter"; +import ReportViewer from "@/pages/Encounters/ReportViewer"; import TemplateBuilder from "@/pages/Encounters/TemplateBuilder/TemplateBuilder"; import TemplatePage from "@/pages/Encounters/TemplateBuilder/TemplatePage"; import AccountList from "@/pages/Facility/billing/account/AccountList"; @@ -28,6 +29,7 @@ import DiagnosticReportPrint from "@/pages/Facility/services/diagnosticReports/D import DiagnosticReportView from "@/pages/Facility/services/diagnosticReports/DiagnosticReportView"; import ServiceRequestShow from "@/pages/Facility/services/serviceRequests/ServiceRequestShow"; import { SettingsLayout } from "@/pages/Facility/settings/layout"; +import { ReportType } from "@/types/emr/report/report"; const FacilityRoutes: AppRoutes = { "/facility": () => , @@ -104,6 +106,24 @@ const FacilityRoutes: AppRoutes = { facilityId, accountId, }) => , + "/facility/:facilityId/billing/account/:accountId/report/template/:templateSlug": + ({ accountId, templateSlug }) => ( + + ), + "/facility/:facilityId/billing/account/:accountId/report/:reportId": ({ + accountId, + reportId, + }) => ( + + ), "/facility/:facilityId/billing/account/:accountId/:tab": ({ facilityId, accountId, diff --git a/src/Routers/routes/PatientRoutes.tsx b/src/Routers/routes/PatientRoutes.tsx index fe57e9388c4..c901b13b790 100644 --- a/src/Routers/routes/PatientRoutes.tsx +++ b/src/Routers/routes/PatientRoutes.tsx @@ -10,9 +10,11 @@ import { AppRoutes } from "@/Routers/AppRouter"; import { PatientRegistration } from "@/components/Patient/PatientRegistration"; import { ConsentDetailPage } from "@/pages/Encounters/ConsentDetail"; import EncountersOverview from "@/pages/Encounters/EncountersOverview"; +import ReportViewer from "@/pages/Encounters/ReportViewer"; import { EncounterProvider } from "@/pages/Encounters/utils/EncounterProvider"; import ClinicalHistoryPage from "@/pages/Patient/History"; import PatientHome from "@/pages/Patient/PatientHome"; +import { ReportType } from "@/types/emr/report/report"; import careConfig from "@careConfig"; const ExcalidrawEditor = lazy( @@ -164,6 +166,26 @@ const PatientRoutes: AppRoutes = { fallBackUrl={`/patient/${patientId}`} /> ), + "/facility/:facilityId/patient/:patientId/report/template/:templateSlug": ({ + patientId, + templateSlug, + }) => ( + + ), + "/facility/:facilityId/patient/:patientId/report/:reportId": ({ + patientId, + reportId, + }) => ( + + ), }; export default PatientRoutes; diff --git a/src/components/Common/FilePreviewDialog.tsx b/src/components/Common/FilePreviewDialog.tsx index 6293107ea30..29eefca4fb8 100644 --- a/src/components/Common/FilePreviewDialog.tsx +++ b/src/components/Common/FilePreviewDialog.tsx @@ -28,8 +28,6 @@ import { FileReadMinimal, getVideoMimeType, } from "@/types/files/file"; -import { ShortcutBadge } from "@/Utils/keyboardShortcutComponents"; -import { Printer } from "lucide-react"; const PDFViewer = lazy(() => import("@/components/Common/PDFViewer")); export interface StateInterface { @@ -247,41 +245,6 @@ export default function FilePreviewDialog(props: FilePreviewProps) { )}
- {file_state.extension === "pdf" && ( - - )} {file_state.extension === "pdf" && fileUrl && (
{facility && ( - - - {t("generate_report")} - - } - onSuccess={() => { - refetch(); - }} /> )} @@ -426,3 +446,102 @@ export function ReportSubTab({ ); } + +function GenerateReportDropdown({ + facilityId, + patientId, + associatingId, + reportType, +}: { + facilityId: string; + patientId?: string; + associatingId: string; + reportType?: ReportType; +}) { + const { t } = useTranslation(); + const { facility } = useCurrentFacilitySilently(); + const { hasPermission } = usePermissions(); + + const canListTemplate = hasPermission( + PERMISSION_LIST_TEMPLATE, + facility?.permissions, + ); + + const { data: templatesData, isLoading } = useQuery({ + queryKey: ["templates", facilityId, reportType], + queryFn: query(templateApi.listTemplates, { + queryParams: { + facility: facilityId, + template_type: reportType, + status: "active", + }, + }), + enabled: canListTemplate && !!reportType, + }); + + const getTemplateUrl = (slug: string) => { + const basePath = getReportBasePath( + reportType, + associatingId, + facilityId, + patientId, + ); + return basePath ? `${basePath}/report/template/${slug}` : null; + }; + + const templates = (templatesData?.results ?? []).flatMap((template) => { + const url = getTemplateUrl(template.slug); + return url ? [{ template, url }] : []; + }); + + if (!isLoading && templates.length === 0) return null; + + if (isLoading || templates.length === 1) { + return ( + + ); + } + + return ( + + + + + +
+
+ {templates.map(({ template, url }) => { + return ( + navigate(url)} + > + + {template.name} + + ); + })} +
+ +
+
+
+ ); +} diff --git a/src/components/ui/multi-filter/utils/navigation-helper.tsx b/src/components/ui/multi-filter/utils/navigation-helper.tsx index 66d2117acd5..92a4ade4b25 100644 --- a/src/components/ui/multi-filter/utils/navigation-helper.tsx +++ b/src/components/ui/multi-filter/utils/navigation-helper.tsx @@ -7,8 +7,10 @@ import useBreakpoints from "@/hooks/useBreakpoints"; export default function NavigationHelper({ isActiveFilter, + hideRightArrow, }: { isActiveFilter?: boolean; + hideRightArrow?: boolean; }) { const { t } = useTranslation(); const isMobile = useBreakpoints({ sm: false, default: true }); @@ -33,7 +35,7 @@ export default function NavigationHelper({
- {!isActiveFilter && ( + {!isActiveFilter && !hideRightArrow && (
diff --git a/src/pages/Encounters/ReportViewer.tsx b/src/pages/Encounters/ReportViewer.tsx index 284399b57dc..c11ff623e06 100644 --- a/src/pages/Encounters/ReportViewer.tsx +++ b/src/pages/Encounters/ReportViewer.tsx @@ -39,7 +39,7 @@ import { EmptyState } from "@/components/ui/empty-state"; import { usePermissions } from "@/context/PermissionContext"; import { cn } from "@/lib/utils"; import { useCurrentFacilitySilently } from "@/pages/Facility/utils/useCurrentFacility"; -import { ReportReadList } from "@/types/emr/report/report"; +import { ReportReadList, ReportType } from "@/types/emr/report/report"; import reportApi from "@/types/emr/report/reportApi"; import { TemplateBaseRead } from "@/types/emr/template/template"; import templateApi from "@/types/emr/template/templateApi"; @@ -52,15 +52,17 @@ const POLL_INTERVAL_MS = 2000; const POLL_TIMEOUT_MS = 30000; interface ReportViewerProps { - encounterId: string; + associatingId: string; templateSlug?: string; reportId?: string; + reportType?: ReportType; } export default function ReportViewer({ - encounterId, + associatingId, templateSlug, reportId, + reportType = ReportType.DISCHARGE_SUMMARY, }: ReportViewerProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); @@ -89,7 +91,7 @@ export default function ReportViewer({ facility?.permissions, ); - const { data: initialReport } = useQuery({ + const { data: initialReport, isLoading: isLoadingInitialReport } = useQuery({ queryKey: ["report", reportId], queryFn: query(reportApi.retrieveReport, { pathParams: { id: reportId! }, @@ -114,18 +116,24 @@ export default function ReportViewer({ isLoading: isLoadingReports, refetch: refetchReports, } = useQuery({ - queryKey: ["reports", encounterId, "template", effectiveTSlug], + queryKey: [ + "reports", + associatingId, + "template", + effectiveTSlug, + reportType, + ], queryFn: query(reportApi.listReports, { queryParams: { - associating_id: encounterId, + associating_id: associatingId, upload_completed: "true", - report_type: "discharge_summary", + report_type: reportType, is_archived: "false", template: effectiveTSlug, limit: 50, }, }), - enabled: !!encounterId && !!effectiveTSlug, + enabled: !!associatingId && !!effectiveTSlug, }); const reports = useMemo( @@ -184,7 +192,7 @@ export default function ReportViewer({ const response = await callApi(reportApi.createReport, { body: { template_id: tmpl.id, - associating_id: encounterId, + associating_id: associatingId, output_format: tmpl.default_format, options: JSON.stringify({}), force: false, @@ -200,16 +208,17 @@ export default function ReportViewer({ const freshData = await queryClient.fetchQuery({ queryKey: [ "reports", - encounterId, + associatingId, "template", effectiveTSlug, + reportType, "fresh", ], queryFn: query(reportApi.listReports, { queryParams: { - associating_id: encounterId, + associating_id: associatingId, upload_completed: "true", - report_type: "discharge_summary", + report_type: reportType, is_archived: "false", template: effectiveTSlug, limit: 1, @@ -236,7 +245,15 @@ export default function ReportViewer({ // Continue polling on transient errors } }, - [encounterId, effectiveTSlug, stopPolling, queryClient, refetchReports, t], + [ + associatingId, + reportType, + effectiveTSlug, + stopPolling, + queryClient, + refetchReports, + t, + ], ); const startPolling = useCallback( @@ -279,7 +296,7 @@ export default function ReportViewer({ triggerGeneration( { template_id: tmpl.id, - associating_id: encounterId, + associating_id: associatingId, output_format: tmpl.default_format, options: JSON.stringify({}), force: false, @@ -292,7 +309,7 @@ export default function ReportViewer({ }, ); }, - [isGenerating, encounterId, triggerGeneration, startPolling, t], + [isGenerating, associatingId, triggerGeneration, startPolling, t], ); // Auto-generate report on first load if none exist @@ -370,7 +387,7 @@ export default function ReportViewer({ } }, [pdfUrl, t]); - if (isLoadingTemplate || isLoadingReports) { + if (isLoadingInitialReport || isLoadingTemplate || isLoadingReports) { return ; } diff --git a/src/pages/Encounters/TemplateBuilder/TemplateReportSheet.tsx b/src/pages/Encounters/TemplateBuilder/TemplateReportSheet.tsx deleted file mode 100644 index b086c5d6feb..00000000000 --- a/src/pages/Encounters/TemplateBuilder/TemplateReportSheet.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button } from "@/components/ui/button"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; - -import { getPermissions } from "@/common/Permissions"; - -import { usePermissions } from "@/context/PermissionContext"; - -import { TemplateType } from "@/types/emr/template/template"; -import { navigate } from "raviger"; -import TemplateList from "./TemplateList"; - -interface TemplateReportSheetProps { - facilityId: string; - encounterId?: string; - patientId?: string; - associatingId: string; - trigger: React.ReactNode; - onSuccess?: () => void; - permissions: string[]; - reportType?: TemplateType; -} - -export default function TemplateReportSheet({ - facilityId, - associatingId, - trigger, - permissions, - reportType, - onSuccess, -}: TemplateReportSheetProps) { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const { hasPermission } = usePermissions(); - const { canWriteTemplate } = getPermissions(hasPermission, permissions); - - const handleSuccess = () => { - setOpen(false); - onSuccess?.(); - }; - - return ( - - {trigger} - - - - {t("available_templates")} - {canWriteTemplate && ( - - )} - - -
- -
-
-
- ); -} diff --git a/src/pages/Facility/billing/account/AccountShow.tsx b/src/pages/Facility/billing/account/AccountShow.tsx index 8e09b86799c..29c865ec699 100644 --- a/src/pages/Facility/billing/account/AccountShow.tsx +++ b/src/pages/Facility/billing/account/AccountShow.tsx @@ -1,7 +1,7 @@ import { DialogDescription } from "@radix-ui/react-dialog"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { formatDistanceToNow } from "date-fns"; -import { Hash, MoreVertical } from "lucide-react"; +import { FileText, Hash, Loader, MoreVertical } from "lucide-react"; import { Link, navigate } from "raviger"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -23,6 +23,8 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { MonetaryDisplay } from "@/components/ui/monetary-display"; @@ -40,7 +42,7 @@ import TagAssignmentSheet from "@/components/Tags/TagAssignmentSheet"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; -import { getPermissions } from "@/common/Permissions"; +import { PERMISSION_LIST_TEMPLATE, getPermissions } from "@/common/Permissions"; import { usePermissions } from "@/context/PermissionContext"; import { useShortcutSubContext } from "@/context/ShortcutContext"; import { PaymentReconciliationSheet } from "@/pages/Facility/billing/PaymentReconciliationSheet"; @@ -69,6 +71,7 @@ import { } from "@/pages/Facility/billing/account/utils"; import useCurrentFacility from "@/pages/Facility/utils/useCurrentFacility"; import { ReportType } from "@/types/emr/report/report"; +import templateApi from "@/types/emr/template/templateApi"; import AccountSheet from "./AccountSheet"; import TransferPaymentSheet from "./TransferPaymentSheet"; import BedChargeItemsTable from "./components/BedChargeItemsTable"; @@ -128,6 +131,7 @@ export function AccountShow({ }); }; const [transferPaymentOpen, setTransferPaymentOpen] = useState(false); + const [dropdownOpen, setDropdownOpen] = useState(false); const queryClient = useQueryClient(); const [closeAccountStatus, setCloseAccountStatus] = useState<{ sheetOpen: boolean; @@ -142,6 +146,11 @@ export function AccountShow({ facility?.permissions || [], ); + const canListTemplates = hasPermission( + PERMISSION_LIST_TEMPLATE, + facility?.permissions || [], + ); + useShortcutSubContext("facility:account:show"); const { data: account, isLoading } = useQuery({ @@ -166,6 +175,20 @@ export function AccountShow({ const hasBillableItems = (billableChargeItems?.count ?? 0) > 0; + const { data: templatesData, isLoading: isLoadingTemplates } = useQuery({ + queryKey: ["templates", facilityId, "account_report"], + queryFn: query(templateApi.listTemplates, { + queryParams: { + facility: facilityId, + template_type: ReportType.ACCOUNT_REPORT, + status: "active", + }, + }), + enabled: dropdownOpen && canListTemplates, + }); + + const accountTemplates = templatesData?.results ?? []; + const showMoreAfterIndex = useBreakpoints({ default: 1, xs: 2, @@ -331,6 +354,7 @@ export function AccountShow({ ), shortcutId: "switch-to-reports-tab", @@ -420,7 +444,7 @@ export function AccountShow({ )} {account.status == AccountStatus.active && ( - + - + {!isAccountBillingClosed(account) && ( <> {t("transfer_payment")} + {canListTemplates && isLoadingTemplates && ( + + + {t("loading")} + + )} + {canListTemplates && accountTemplates.length > 0 && ( + <> + + {t("reports")} +
+ {accountTemplates.map((template) => ( + + + + {template.name} + + + ))} +
+ + )}
)}