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 && (
diff --git a/src/components/Files/ReportSubTab.tsx b/src/components/Files/ReportSubTab.tsx
index d86d6872337..19389c50a5e 100644
--- a/src/components/Files/ReportSubTab.tsx
+++ b/src/components/Files/ReportSubTab.tsx
@@ -1,7 +1,10 @@
+import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";
-import { SearchIcon } from "lucide-react";
+import { FileText, Plus, SearchIcon } from "lucide-react";
+import { navigate } from "raviger";
import { useState } from "react";
import { useTranslation } from "react-i18next";
+import { toast } from "sonner";
import { cn } from "@/lib/utils";
@@ -26,30 +29,55 @@ import {
} from "@/components/ui/table";
import { TooltipComponent } from "@/components/ui/tooltip";
+import { PERMISSION_LIST_TEMPLATE } from "@/common/Permissions";
import Loading from "@/components/Common/Loading";
import { FilterBadges, FilterButton } from "@/components/Files/FileFilters";
import { EmptyState } from "@/components/ui/empty-state";
+import { usePermissions } from "@/context/PermissionContext";
import useFilters from "@/hooks/useFilters";
import useReportManager from "@/hooks/useReportManager";
+import query from "@/Utils/request/query";
import queryClient from "@/Utils/request/queryClient";
-import TemplateReportSheet from "@/pages/Encounters/TemplateBuilder/TemplateReportSheet";
+import NavigationHelper from "@/components/ui/multi-filter/utils/navigation-helper";
import { useCurrentFacilitySilently } from "@/pages/Facility/utils/useCurrentFacility";
import {
ReportRead,
ReportReadList,
ReportType,
} from "@/types/emr/report/report";
-import { navigate } from "raviger";
-import { toast } from "sonner";
+import templateApi from "@/types/emr/template/templateApi";
+
+function getReportBasePath(
+ reportType: ReportType | undefined,
+ associatingId: string,
+ facilityId?: string,
+ patientId?: string,
+): string | null {
+ switch (reportType) {
+ case ReportType.DISCHARGE_SUMMARY:
+ return facilityId && patientId
+ ? `/facility/${facilityId}/patient/${patientId}/encounter/${associatingId}`
+ : null;
+ case ReportType.ACCOUNT_REPORT:
+ return facilityId
+ ? `/facility/${facilityId}/billing/account/${associatingId}`
+ : null;
+ case ReportType.PATIENT_SUMMARY:
+ return facilityId && patientId
+ ? `/facility/${facilityId}/patient/${patientId}`
+ : null;
+ default:
+ return null;
+ }
+}
interface ReportTabProps {
associatingId: string;
reportType?: ReportType;
facilityId?: string;
patientId?: string;
- encounterId?: string;
}
export function ReportSubTab({
@@ -57,10 +85,10 @@ export function ReportSubTab({
reportType,
facilityId,
patientId,
- encounterId,
}: ReportTabProps) {
const { t } = useTranslation();
const { facility } = useCurrentFacilitySilently();
+
const { qParams, updateQuery, Pagination } = useFilters({
limit: 15,
disableCache: true,
@@ -72,7 +100,6 @@ export function ReportSubTab({
viewFile,
downloadFile,
archiveReport,
- refetch,
Dialogs,
} = useReportManager({
associatingId,
@@ -98,13 +125,15 @@ export function ReportSubTab({
return iconMap[reportType] || "l-file-alt";
};
- const canNavigateToPreview = !!(facilityId && patientId && encounterId);
-
const handleView = (report: ReportReadList) => {
- if (canNavigateToPreview) {
- navigate(
- `/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}/report/${report.id}`,
- );
+ const basePath = getReportBasePath(
+ reportType,
+ associatingId,
+ facilityId ?? facility?.id,
+ patientId,
+ );
+ if (basePath) {
+ navigate(`${basePath}/report/${report.id}`);
} else {
viewFile(report);
}
@@ -374,20 +403,11 @@ export function ReportSubTab({
{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}
+
+
+ ))}
+
+ >
+ )}
)}