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>
179 lines
6.5 KiB
TypeScript
179 lines
6.5 KiB
TypeScript
"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>
|
||
);
|
||
}
|