TorbatYar/frontend/app/accounting/page.tsx
Mortezakoohjani 12c8615615 Ship enterprise Accounting FE/API with CRUD parity and production wiring.
Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 15:26:43 +03:30

179 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { accountingApi } from "@/lib/accounting-api";
import { useTenantId } from "@/hooks/useTenantId";
import { CompletionScoreboard } from "@/components/accounting/CompletionScoreboard";
import {
PageHeader,
StatCard,
LoadingState,
ErrorState,
Badge,
Button,
Card,
CardContent,
} from "@/components/ds";
export default function AccountingDashboardPage() {
const { tenantId, loading: tenantLoading } = useTenantId();
const healthQ = useQuery({
queryKey: ["accounting", "health"],
queryFn: () => accountingApi.health(),
});
const setupQ = useQuery({
queryKey: ["accounting", tenantId, "setup-status"],
queryFn: () => accountingApi.setup.status(tenantId!),
enabled: !!tenantId,
});
const chartsQ = useQuery({
queryKey: ["accounting", tenantId, "charts"],
queryFn: () => accountingApi.charts.list(tenantId!),
enabled: !!tenantId,
});
const accountsQ = useQuery({
queryKey: ["accounting", tenantId, "accounts"],
queryFn: () => accountingApi.accounts.list(tenantId!),
enabled: !!tenantId,
});
const yearsQ = useQuery({
queryKey: ["accounting", tenantId, "fiscal-years"],
queryFn: () => accountingApi.fiscal.listYears(tenantId!),
enabled: !!tenantId,
});
const vouchersQ = useQuery({
queryKey: ["accounting", tenantId, "vouchers"],
queryFn: () => accountingApi.vouchers.list(tenantId!),
enabled: !!tenantId,
});
if (tenantLoading || !tenantId) return <LoadingState />;
const err =
chartsQ.error || accountsQ.error || yearsQ.error || vouchersQ.error || healthQ.error;
if (err) {
return (
<ErrorState
message={err instanceof Error ? err.message : "خطا در دریافت داده‌ها"}
onRetry={() => {
void chartsQ.refetch();
void accountsQ.refetch();
void yearsQ.refetch();
void vouchersQ.refetch();
}}
/>
);
}
const loading = chartsQ.isLoading || accountsQ.isLoading || yearsQ.isLoading || vouchersQ.isLoading;
if (loading) return <LoadingState />;
const setup = setupQ.data;
const setupIncomplete = setup && setup.percent < 100;
return (
<div>
<PageHeader
title="داشبورد حسابداری"
description="نمای کلی وضعیت مالی workspace — داده‌ها از سرویس حسابداری خوانده می‌شوند."
actions={
<div className="flex items-center gap-2">
{healthQ.data ? (
<Badge tone={healthQ.data.status === "ok" ? "success" : "danger"}>
سرویس {healthQ.data.version}
</Badge>
) : null}
<Link href="/accounting/vouchers/new">
<Button>سند جدید</Button>
</Link>
</div>
}
/>
{setupIncomplete ? (
<Card className="mb-6 border-primary/30">
<CardContent className="flex flex-col gap-3 pt-5 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold text-secondary">راهاندازی حسابداری</p>
<p className="mt-1 text-sm text-[var(--muted)]">
{setup!.completed_steps} از {setup!.total_steps} مرحله تکمیل شده ({setup!.percent}٪)
</p>
</div>
<Link href="/accounting/setup">
<Button variant="outline">ادامه راهاندازی</Button>
</Link>
</CardContent>
</Card>
) : null}
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<StatCard label="دفاتر حساب" value={chartsQ.data?.length ?? 0} />
<StatCard label="حساب‌ها" value={accountsQ.data?.length ?? 0} />
<StatCard label="سال‌های مالی" value={yearsQ.data?.length ?? 0} />
<StatCard label="اسناد" value={vouchersQ.data?.length ?? 0} />
</div>
<div className="mt-8 grid gap-4 lg:grid-cols-2">
<CompletionScoreboard compact />
<section className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-5">
<h2 className="mb-3 font-semibold text-secondary">آخرین اسناد</h2>
{(vouchersQ.data?.length ?? 0) === 0 ? (
<p className="text-sm text-[var(--muted)]">هنوز سندی ثبت نشده است.</p>
) : (
<ul className="space-y-2">
{vouchersQ.data!.slice(0, 5).map((v) => (
<li key={v.id}>
<Link
href={`/accounting/vouchers/${v.id}`}
className="flex items-center justify-between rounded-xl px-3 py-2 text-sm hover:bg-[var(--surface-muted)]"
>
<span className="font-medium text-secondary">{v.voucher_number}</span>
<Badge
tone={
v.status === "posted"
? "success"
: v.status === "draft"
? "default"
: "warning"
}
>
{v.status}
</Badge>
</Link>
</li>
))}
</ul>
)}
</section>
</div>
<section className="mt-8 rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-5">
<h2 className="mb-3 font-semibold text-secondary">دسترسی سریع</h2>
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
{[
["/accounting/chart-of-accounts", "دفتر حساب‌ها"],
["/accounting/fiscal", "سال و دوره مالی"],
["/accounting/ledger", "دفتر کل"],
["/accounting/vouchers", "اسناد حسابداری"],
["/accounting/setup", "راه‌اندازی"],
["/accounting/reports", "گزارش‌ها"],
].map(([href, label]) => (
<Link
key={href}
href={href}
className="rounded-xl border border-[var(--border)] px-4 py-3 text-sm hover:bg-[var(--surface-muted)]"
>
{label}
</Link>
))}
</div>
</section>
</div>
);
}