Wire DocumentNumberSequence allocator, ledger-based subsidiary/detail reports, and posted-voucher reopen flow without mock ops forms. Co-authored-by: Cursor <cursoragent@cursor.com>
664 lines
23 KiB
TypeScript
664 lines
23 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState, Suspense } from "react";
|
||
import { useSearchParams } from "next/navigation";
|
||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||
import { toast } from "sonner";
|
||
import { accountingApi } from "@/lib/accounting-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { formatMoney } from "@/lib/utils";
|
||
import {
|
||
PageHeader,
|
||
Button,
|
||
LoadingState,
|
||
ErrorState,
|
||
Card,
|
||
CardContent,
|
||
CardHeader,
|
||
CardTitle,
|
||
Badge,
|
||
Select,
|
||
FormField,
|
||
DataTable,
|
||
EmptyState,
|
||
StatCard,
|
||
} from "@/components/ds";
|
||
|
||
type EngineKind = "trial-balance" | "balance-sheet" | "income-statement";
|
||
type LedgerReportKind =
|
||
| "subsidiary"
|
||
| "detail"
|
||
| "analytical"
|
||
| "cash-flow"
|
||
| "equity";
|
||
|
||
const ENGINE_LABELS: Record<EngineKind, string> = {
|
||
"trial-balance": "تراز آزمایشی",
|
||
"balance-sheet": "ترازنامه",
|
||
"income-statement": "سود و زیان",
|
||
};
|
||
|
||
const LEDGER_REPORT_META: Record<
|
||
LedgerReportKind,
|
||
{ title: string; description: string; needsAccount?: boolean }
|
||
> = {
|
||
subsidiary: {
|
||
title: "گزارش معین",
|
||
description: "مانده و گردش حسابهای سطح معین بر اساس دفتر و اسناد ثبتشده.",
|
||
needsAccount: true,
|
||
},
|
||
detail: {
|
||
title: "گزارش تفصیلی",
|
||
description: "مانده و گردش حسابهای سطح تفصیلی بر اساس دفتر و اسناد ثبتشده.",
|
||
needsAccount: true,
|
||
},
|
||
analytical: {
|
||
title: "گزارش تحلیلی",
|
||
description: "تجمیع ماندهها بر اساس نوع و دسته حساب از داده واقعی دفتر.",
|
||
},
|
||
"cash-flow": {
|
||
title: "جریان وجوه نقد",
|
||
description: "گردش حسابهای نقدی/بانکی از مانده و اسناد ثبتشده.",
|
||
},
|
||
equity: {
|
||
title: "تغییرات حقوق صاحبان سهام",
|
||
description: "مانده و گردش حسابهای حقوق صاحبان سهام در دوره انتخابی.",
|
||
},
|
||
};
|
||
|
||
const FIELD_LABELS: Record<string, string> = {
|
||
total_debit: "جمع بدهکار",
|
||
total_credit: "جمع بستانکار",
|
||
difference: "اختلاف",
|
||
is_balanced: "متوازن",
|
||
total_assets: "جمع داراییها",
|
||
account_count: "تعداد حساب",
|
||
total_revenue: "جمع درآمد",
|
||
total_expense: "جمع هزینه",
|
||
net: "خالص",
|
||
net_cash_change: "تغییر خالص نقد",
|
||
inflows: "ورودیها",
|
||
outflows: "خروجیها",
|
||
};
|
||
|
||
type BalanceRow = {
|
||
account_id: string;
|
||
account_code: string;
|
||
account_name: string;
|
||
account_type?: string;
|
||
account_category?: string;
|
||
level?: number;
|
||
opening_balance: string;
|
||
debit_total: string;
|
||
credit_total: string;
|
||
closing_balance: string;
|
||
};
|
||
|
||
type StatementLine = {
|
||
id: string;
|
||
entry_date: string;
|
||
description?: string | null;
|
||
debit: string;
|
||
credit: string;
|
||
running_balance: string;
|
||
entry_number?: string;
|
||
};
|
||
|
||
type LedgerReportData = {
|
||
mode?: string;
|
||
level_mode?: string;
|
||
account?: { id: string; code: string; name: string; level: number };
|
||
rows?: BalanceRow[];
|
||
lines?: StatementLine[];
|
||
totals?: Record<string, string | number>;
|
||
by_type?: Array<Record<string, string | number>>;
|
||
by_category?: Array<Record<string, string | number>>;
|
||
net_cash_change?: string;
|
||
inflows?: string;
|
||
outflows?: string;
|
||
};
|
||
|
||
function parseReportData(data: unknown): Record<string, unknown> | null {
|
||
if (!data) return null;
|
||
if (typeof data === "string") {
|
||
try {
|
||
return JSON.parse(data) as Record<string, unknown>;
|
||
} catch {
|
||
return { raw: data };
|
||
}
|
||
}
|
||
if (typeof data === "object" && !Array.isArray(data)) {
|
||
return data as Record<string, unknown>;
|
||
}
|
||
return { value: data };
|
||
}
|
||
|
||
function formatCellValue(key: string, value: unknown): React.ReactNode {
|
||
if (value === null || value === undefined) return "—";
|
||
if (typeof value === "boolean") return value ? "بله" : "خیر";
|
||
if (
|
||
key.includes("total") ||
|
||
key.includes("difference") ||
|
||
key.includes("balance") ||
|
||
key === "net" ||
|
||
key.includes("amount") ||
|
||
key.includes("inflow") ||
|
||
key.includes("outflow") ||
|
||
key.includes("change")
|
||
) {
|
||
return formatMoney(String(value));
|
||
}
|
||
return String(value);
|
||
}
|
||
|
||
const MONEY_COLS = [
|
||
{
|
||
key: "opening_balance",
|
||
header: "مانده اول دوره",
|
||
render: (r: Record<string, unknown>) => formatMoney(String(r.opening_balance ?? "0")),
|
||
},
|
||
{
|
||
key: "debit_total",
|
||
header: "بدهکار",
|
||
render: (r: Record<string, unknown>) => formatMoney(String(r.debit_total ?? "0")),
|
||
},
|
||
{
|
||
key: "credit_total",
|
||
header: "بستانکار",
|
||
render: (r: Record<string, unknown>) => formatMoney(String(r.credit_total ?? "0")),
|
||
},
|
||
{
|
||
key: "closing_balance",
|
||
header: "مانده پایان",
|
||
render: (r: Record<string, unknown>) => formatMoney(String(r.closing_balance ?? "0")),
|
||
},
|
||
];
|
||
|
||
function EngineReports({ initialKind }: { initialKind?: string }) {
|
||
const { tenantId } = useTenantId();
|
||
const [periodId, setPeriodId] = useState("");
|
||
const [result, setResult] = useState<{
|
||
kind: EngineKind;
|
||
payload: { id: string; data: unknown };
|
||
} | null>(null);
|
||
|
||
const periodsQ = useQuery({
|
||
queryKey: ["accounting", tenantId, "fiscal-periods"],
|
||
queryFn: () => accountingApi.fiscal.listPeriods(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
const runM = useMutation({
|
||
mutationFn: async (kind: EngineKind) => {
|
||
if (!periodId) throw new Error("دوره مالی را انتخاب کنید");
|
||
if (kind === "trial-balance") {
|
||
const payload = await accountingApi.reporting.trialBalance(tenantId!, periodId);
|
||
return { kind, payload };
|
||
}
|
||
if (kind === "balance-sheet") {
|
||
const payload = await accountingApi.reporting.balanceSheet(tenantId!, periodId);
|
||
return { kind, payload };
|
||
}
|
||
const payload = await accountingApi.reporting.incomeStatement(tenantId!, periodId);
|
||
return { kind, payload };
|
||
},
|
||
onSuccess: (data) => {
|
||
setResult(data);
|
||
toast.success("گزارش تولید شد");
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const exportM = useMutation({
|
||
mutationFn: (format: "json" | "csv") =>
|
||
accountingApi.reporting.export(tenantId!, result!.payload.id, format),
|
||
onSuccess: (data) => {
|
||
toast.success(`خروجی ${data.format} ثبت شد`);
|
||
if (data.file_path) toast.message(data.file_path);
|
||
if (result) {
|
||
const blob = new Blob([JSON.stringify(result.payload, null, 2)], {
|
||
type: "application/json",
|
||
});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = `report-${result.payload.id}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const parsed = useMemo(
|
||
() => (result ? parseReportData(result.payload.data) : null),
|
||
[result]
|
||
);
|
||
|
||
const tableRows = useMemo(() => {
|
||
if (!parsed) return [];
|
||
return Object.entries(parsed).map(([key, value]) => ({
|
||
id: key,
|
||
field: FIELD_LABELS[key] ?? key,
|
||
value,
|
||
rawKey: key,
|
||
}));
|
||
}, [parsed]);
|
||
|
||
if (!tenantId || periodsQ.isLoading) return <LoadingState />;
|
||
if (periodsQ.error) {
|
||
return <ErrorState message={periodsQ.error.message} onRetry={() => periodsQ.refetch()} />;
|
||
}
|
||
|
||
const highlight =
|
||
initialKind === "trial-balance" ||
|
||
initialKind === "balance-sheet" ||
|
||
initialKind === "income-statement"
|
||
? (initialKind as EngineKind)
|
||
: null;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
title="گزارشهای مالی"
|
||
description="تراز آزمایشی، ترازنامه و سود و زیان — از Report Engine."
|
||
/>
|
||
|
||
<Card className="mb-6">
|
||
<CardContent className="grid gap-4 pt-6 sm:grid-cols-[1fr_auto] sm:items-end">
|
||
<FormField label="دوره مالی">
|
||
<Select value={periodId} onChange={(e) => setPeriodId(e.target.value)}>
|
||
<option value="">انتخاب دوره</option>
|
||
{(periodsQ.data ?? []).map((p) => (
|
||
<option key={p.id} value={p.id}>
|
||
{p.name}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</FormField>
|
||
<div className="flex flex-wrap gap-2">
|
||
{(Object.keys(ENGINE_LABELS) as EngineKind[]).map((kind) => (
|
||
<Button
|
||
key={kind}
|
||
type="button"
|
||
variant={highlight === kind ? "default" : "outline"}
|
||
disabled={runM.isPending || !periodId}
|
||
onClick={() => runM.mutate(kind)}
|
||
>
|
||
{ENGINE_LABELS[kind]}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{result ? (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex flex-wrap items-center gap-2">
|
||
{ENGINE_LABELS[result.kind]}
|
||
<Badge tone="primary">زنده</Badge>
|
||
<span className="text-xs font-normal text-[var(--muted)]" dir="ltr">
|
||
ID: {result.payload.id}
|
||
</span>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
className="ms-auto"
|
||
disabled={exportM.isPending}
|
||
onClick={() => exportM.mutate("json")}
|
||
>
|
||
خروجی / Export
|
||
</Button>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{parsed && tableRows.length > 0 ? (
|
||
<DataTable
|
||
columns={[
|
||
{ key: "field", header: "عنوان" },
|
||
{
|
||
key: "value",
|
||
header: "مقدار",
|
||
render: (r) => formatCellValue(String(r.rawKey), r.value),
|
||
},
|
||
]}
|
||
rows={tableRows as unknown as Record<string, unknown>[]}
|
||
/>
|
||
) : (
|
||
<EmptyState title="دادهای برای نمایش نیست" />
|
||
)}
|
||
<details className="rounded-xl border border-[var(--border)] bg-[var(--surface-muted)] p-3">
|
||
<summary className="cursor-pointer text-sm font-medium text-secondary">JSON خام</summary>
|
||
<pre
|
||
className="mt-3 max-h-[320px] overflow-auto text-xs leading-relaxed text-secondary"
|
||
dir="ltr"
|
||
>
|
||
{JSON.stringify(result.payload, null, 2)}
|
||
</pre>
|
||
</details>
|
||
</CardContent>
|
||
</Card>
|
||
) : (
|
||
<p className="text-sm text-[var(--muted)]">یک دوره انتخاب کنید و گزارش را اجرا کنید.</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function LedgerReports({ kind }: { kind: LedgerReportKind }) {
|
||
const meta = LEDGER_REPORT_META[kind];
|
||
const { tenantId } = useTenantId();
|
||
const [periodId, setPeriodId] = useState("");
|
||
const [accountId, setAccountId] = useState("");
|
||
const [result, setResult] = useState<{ id: string; data: LedgerReportData } | null>(null);
|
||
|
||
const periodsQ = useQuery({
|
||
queryKey: ["accounting", tenantId, "fiscal-periods"],
|
||
queryFn: () => accountingApi.fiscal.listPeriods(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
const accountsQ = useQuery({
|
||
queryKey: ["accounting", tenantId, "accounts", "report-picker", kind],
|
||
queryFn: () => accountingApi.accounts.list(tenantId!, 1, 500),
|
||
enabled: !!tenantId && !!meta.needsAccount,
|
||
});
|
||
|
||
const accountOptions = useMemo(() => {
|
||
const accounts = accountsQ.data ?? [];
|
||
if (kind === "subsidiary") {
|
||
const level3 = accounts.filter((a) => a.level === 3);
|
||
if (level3.length) return level3;
|
||
return accounts.filter((a) => a.is_postable && !a.is_group && a.level <= 3);
|
||
}
|
||
if (kind === "detail") {
|
||
const detail = accounts.filter((a) => a.level >= 4);
|
||
if (detail.length) return detail;
|
||
return accounts.filter((a) => a.is_postable && !a.is_group);
|
||
}
|
||
return accounts.filter((a) => a.is_postable && !a.is_group);
|
||
}, [accountsQ.data, kind]);
|
||
|
||
const runM = useMutation({
|
||
mutationFn: async () => {
|
||
if (!periodId) throw new Error("دوره مالی را انتخاب کنید");
|
||
const account = accountId || undefined;
|
||
if (kind === "subsidiary") {
|
||
return accountingApi.reporting.subsidiaryLedger(tenantId!, periodId, account);
|
||
}
|
||
if (kind === "detail") {
|
||
return accountingApi.reporting.detailLedger(tenantId!, periodId, account);
|
||
}
|
||
if (kind === "analytical") {
|
||
return accountingApi.reporting.analytical(tenantId!, periodId);
|
||
}
|
||
if (kind === "cash-flow") {
|
||
return accountingApi.reporting.cashFlow(tenantId!, periodId);
|
||
}
|
||
return accountingApi.reporting.equityStatement(tenantId!, periodId);
|
||
},
|
||
onSuccess: (payload) => {
|
||
const data = (parseReportData(payload.data) ?? {}) as LedgerReportData;
|
||
setResult({ id: payload.id, data });
|
||
toast.success("گزارش از داده دفتر تولید شد");
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
if (!tenantId || periodsQ.isLoading) return <LoadingState />;
|
||
if (periodsQ.error) {
|
||
return <ErrorState message={periodsQ.error.message} onRetry={() => periodsQ.refetch()} />;
|
||
}
|
||
|
||
const rows = result?.data.rows ?? [];
|
||
const lines = result?.data.lines ?? [];
|
||
const totals = result?.data.totals ?? {};
|
||
const byType = result?.data.by_type ?? [];
|
||
const byCategory = result?.data.by_category ?? [];
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader title={meta.title} description={meta.description} />
|
||
|
||
<Card className="mb-6">
|
||
<CardContent className="grid gap-4 pt-6 sm:grid-cols-2 lg:grid-cols-[1fr_1fr_auto] lg:items-end">
|
||
<FormField label="دوره مالی">
|
||
<Select value={periodId} onChange={(e) => setPeriodId(e.target.value)}>
|
||
<option value="">انتخاب دوره</option>
|
||
{(periodsQ.data ?? []).map((p) => (
|
||
<option key={p.id} value={p.id}>
|
||
{p.name} {p.is_current ? "(جاری)" : ""}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</FormField>
|
||
|
||
{meta.needsAccount ? (
|
||
<FormField label="حساب (اختیاری — برای گردش جزئیات)">
|
||
<Select value={accountId} onChange={(e) => setAccountId(e.target.value)}>
|
||
<option value="">همه حسابهای این سطح</option>
|
||
{accountOptions.map((a) => (
|
||
<option key={a.id} value={a.id}>
|
||
{a.code} — {a.name}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</FormField>
|
||
) : (
|
||
<div />
|
||
)}
|
||
|
||
<Button
|
||
type="button"
|
||
disabled={runM.isPending || !periodId}
|
||
onClick={() => runM.mutate()}
|
||
>
|
||
{runM.isPending ? "در حال تولید…" : "اجرای گزارش"}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{result ? (
|
||
<div className="space-y-6">
|
||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||
<StatCard
|
||
label="مانده اول دوره"
|
||
value={formatMoney(String(totals.opening_balance ?? "0"))}
|
||
/>
|
||
<StatCard label="جمع بدهکار" value={formatMoney(String(totals.debit_total ?? "0"))} />
|
||
<StatCard label="جمع بستانکار" value={formatMoney(String(totals.credit_total ?? "0"))} />
|
||
<StatCard
|
||
label="مانده پایان"
|
||
value={formatMoney(String(totals.closing_balance ?? "0"))}
|
||
/>
|
||
</div>
|
||
|
||
{kind === "cash-flow" ? (
|
||
<div className="grid gap-4 sm:grid-cols-3">
|
||
<StatCard
|
||
label="ورودیها"
|
||
value={formatMoney(String(result.data.inflows ?? totals.debit_total ?? "0"))}
|
||
/>
|
||
<StatCard
|
||
label="خروجیها"
|
||
value={formatMoney(String(result.data.outflows ?? totals.credit_total ?? "0"))}
|
||
/>
|
||
<StatCard
|
||
label="تغییر خالص نقد"
|
||
value={formatMoney(String(result.data.net_cash_change ?? "0"))}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
|
||
{result.data.account ? (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex flex-wrap items-center gap-2 text-base">
|
||
گردش حساب
|
||
<Badge tone="primary">
|
||
{result.data.account.code} — {result.data.account.name}
|
||
</Badge>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{lines.length ? (
|
||
<DataTable
|
||
columns={[
|
||
{ key: "entry_date", header: "تاریخ" },
|
||
{
|
||
key: "entry_number",
|
||
header: "شماره",
|
||
render: (r) => String(r.entry_number ?? "—"),
|
||
},
|
||
{
|
||
key: "description",
|
||
header: "شرح",
|
||
render: (r) => String(r.description ?? "—"),
|
||
},
|
||
{
|
||
key: "debit",
|
||
header: "بدهکار",
|
||
render: (r) => formatMoney(String(r.debit ?? "0")),
|
||
},
|
||
{
|
||
key: "credit",
|
||
header: "بستانکار",
|
||
render: (r) => formatMoney(String(r.credit ?? "0")),
|
||
},
|
||
{
|
||
key: "running_balance",
|
||
header: "مانده جاری",
|
||
render: (r) => formatMoney(String(r.running_balance ?? "0")),
|
||
},
|
||
]}
|
||
rows={lines as unknown as Record<string, unknown>[]}
|
||
/>
|
||
) : (
|
||
<EmptyState
|
||
title="گردشی برای این حساب در دوره نیست"
|
||
description="اسناد ثبتشده (Posted) روی این حساب در این دوره یافت نشد."
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
) : null}
|
||
|
||
{kind === "analytical" ? (
|
||
<>
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>بر اساس نوع حساب</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{byType.length ? (
|
||
<DataTable
|
||
columns={[
|
||
{ key: "key", header: "نوع" },
|
||
{
|
||
key: "account_count",
|
||
header: "تعداد",
|
||
render: (r) => String(r.account_count ?? 0),
|
||
},
|
||
...MONEY_COLS,
|
||
]}
|
||
rows={byType as unknown as Record<string, unknown>[]}
|
||
/>
|
||
) : (
|
||
<EmptyState title="دادهای نیست" />
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>بر اساس دسته حساب</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{byCategory.length ? (
|
||
<DataTable
|
||
columns={[
|
||
{ key: "key", header: "دسته" },
|
||
{
|
||
key: "account_count",
|
||
header: "تعداد",
|
||
render: (r) => String(r.account_count ?? 0),
|
||
},
|
||
...MONEY_COLS,
|
||
]}
|
||
rows={byCategory as unknown as Record<string, unknown>[]}
|
||
/>
|
||
) : (
|
||
<EmptyState title="دادهای نیست" />
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</>
|
||
) : null}
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex flex-wrap items-center gap-2">
|
||
{result.data.mode === "statement" ? "مانده حساب انتخابشده" : "خلاصه مانده حسابها"}
|
||
<Badge tone="primary">زنده از دفتر</Badge>
|
||
<span className="text-xs font-normal text-[var(--muted)]" dir="ltr">
|
||
ID: {result.id}
|
||
</span>
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{rows.length ? (
|
||
<DataTable
|
||
columns={[
|
||
{
|
||
key: "account_code",
|
||
header: "کد",
|
||
render: (r) => (
|
||
<span dir="ltr">{String(r.account_code ?? "")}</span>
|
||
),
|
||
},
|
||
{ key: "account_name", header: "نام حساب" },
|
||
{
|
||
key: "level",
|
||
header: "سطح",
|
||
render: (r) => String(r.level ?? "—"),
|
||
},
|
||
...MONEY_COLS,
|
||
]}
|
||
rows={rows as unknown as Record<string, unknown>[]}
|
||
/>
|
||
) : (
|
||
<EmptyState
|
||
title="حسابی برای نمایش نیست"
|
||
description="حسابهای این سطح را در درخت حسابها تعریف کنید و اسناد را ثبت (Post) کنید."
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-[var(--muted)]">
|
||
دوره مالی را انتخاب کنید و گزارش را اجرا کنید. داده از مانده دفتر و اسناد ثبتشده خوانده میشود — نه فرم ساختگی.
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ReportsRouter() {
|
||
const sp = useSearchParams();
|
||
const type = sp.get("type") || "";
|
||
if (type in LEDGER_REPORT_META) {
|
||
return <LedgerReports kind={type as LedgerReportKind} />;
|
||
}
|
||
return <EngineReports initialKind={type || undefined} />;
|
||
}
|
||
|
||
export default function ReportsPage() {
|
||
return (
|
||
<Suspense fallback={<LoadingState />}>
|
||
<ReportsRouter />
|
||
</Suspense>
|
||
);
|
||
}
|