"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 = { "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 = { 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; by_type?: Array>; by_category?: Array>; net_cash_change?: string; inflows?: string; outflows?: string; }; function parseReportData(data: unknown): Record | null { if (!data) return null; if (typeof data === "string") { try { return JSON.parse(data) as Record; } catch { return { raw: data }; } } if (typeof data === "object" && !Array.isArray(data)) { return data as Record; } 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) => formatMoney(String(r.opening_balance ?? "0")), }, { key: "debit_total", header: "بدهکار", render: (r: Record) => formatMoney(String(r.debit_total ?? "0")), }, { key: "credit_total", header: "بستانکار", render: (r: Record) => formatMoney(String(r.credit_total ?? "0")), }, { key: "closing_balance", header: "مانده پایان", render: (r: Record) => 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 ; if (periodsQ.error) { return periodsQ.refetch()} />; } const highlight = initialKind === "trial-balance" || initialKind === "balance-sheet" || initialKind === "income-statement" ? (initialKind as EngineKind) : null; return (
{(Object.keys(ENGINE_LABELS) as EngineKind[]).map((kind) => ( ))}
{result ? ( {ENGINE_LABELS[result.kind]} زنده ID: {result.payload.id} {parsed && tableRows.length > 0 ? ( formatCellValue(String(r.rawKey), r.value), }, ]} rows={tableRows as unknown as Record[]} /> ) : ( )}
JSON خام
                {JSON.stringify(result.payload, null, 2)}
              
) : (

یک دوره انتخاب کنید و گزارش را اجرا کنید.

)}
); } 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 ; if (periodsQ.error) { return 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 (
{meta.needsAccount ? ( ) : (
)} {result ? (
{kind === "cash-flow" ? (
) : null} {result.data.account ? ( گردش حساب {result.data.account.code} — {result.data.account.name} {lines.length ? ( 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[]} /> ) : ( )} ) : null} {kind === "analytical" ? ( <> بر اساس نوع حساب {byType.length ? ( String(r.account_count ?? 0), }, ...MONEY_COLS, ]} rows={byType as unknown as Record[]} /> ) : ( )} بر اساس دسته حساب {byCategory.length ? ( String(r.account_count ?? 0), }, ...MONEY_COLS, ]} rows={byCategory as unknown as Record[]} /> ) : ( )} ) : null} {result.data.mode === "statement" ? "مانده حساب انتخاب‌شده" : "خلاصه مانده حساب‌ها"} زنده از دفتر ID: {result.id} {rows.length ? ( ( {String(r.account_code ?? "")} ), }, { key: "account_name", header: "نام حساب" }, { key: "level", header: "سطح", render: (r) => String(r.level ?? "—"), }, ...MONEY_COLS, ]} rows={rows as unknown as Record[]} /> ) : ( )}
) : (

دوره مالی را انتخاب کنید و گزارش را اجرا کنید. داده از مانده دفتر و اسناد ثبت‌شده خوانده می‌شود — نه فرم ساختگی.

)}
); } function ReportsRouter() { const sp = useSearchParams(); const type = sp.get("type") || ""; if (type in LEDGER_REPORT_META) { return ; } return ; } export default function ReportsPage() { return ( }> ); }