TorbatYar/frontend/app/accounting/vouchers/page.tsx
Mortezakoohjani 067b499193 Connect Accounting frontend to domain APIs for phases 5.1-5.11.
Wire sales/purchase posting, settlements, compliance, payroll employees, and ops edit flows; document the integration report.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 22:17:41 +03:30

129 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { Suspense, useMemo, useState } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { Plus } from "lucide-react";
import { accountingApi } from "@/lib/accounting-api";
import { useTenantId } from "@/hooks/useTenantId";
import { formatMoney } from "@/lib/utils";
import {
PageHeader,
Button,
DataTable,
EmptyState,
LoadingState,
ErrorState,
Badge,
JalaliDateText,
Pagination,
} from "@/components/ds";
const STATUS_TITLES: Record<string, string> = {
draft: "اسناد پیش‌نویس",
reversed: "اسناد برگشتی",
posted: "تأیید و ثبت اسناد",
};
function VouchersList() {
const { tenantId } = useTenantId();
const sp = useSearchParams();
const statusFilter = sp.get("status") || "";
const [page, setPage] = useState(1);
const pageSize = 50;
const listQ = useQuery({
queryKey: ["accounting", tenantId, "vouchers", page, pageSize],
queryFn: () => accountingApi.vouchers.list(tenantId!, page, pageSize),
enabled: !!tenantId,
});
const rows = useMemo(() => {
const all = listQ.data ?? [];
if (!statusFilter) return all;
return all.filter((v) => String(v.status) === statusFilter);
}, [listQ.data, statusFilter]);
if (!tenantId || listQ.isLoading) return <LoadingState />;
if (listQ.error) return <ErrorState message={listQ.error.message} onRetry={() => listQ.refetch()} />;
const title = STATUS_TITLES[statusFilter] || "لیست اسناد حسابداری";
return (
<div>
<PageHeader
title={title}
description="چرخه عمر سند از طریق Posting Engine — بدون ایجاد مستقیم Journal Entry."
actions={
<Link href="/accounting/vouchers/new">
<Button>
<Plus className="h-4 w-4" />
سند جدید
</Button>
</Link>
}
/>
<DataTable
columns={[
{
key: "voucher_number",
header: "شماره",
render: (r) => (
<Link className="font-medium text-primary hover:underline" href={`/accounting/vouchers/${r.id}`}>
{String(r.voucher_number)}
</Link>
),
},
{
key: "voucher_date",
header: "تاریخ",
render: (r) => <JalaliDateText value={String(r.voucher_date ?? "")} />,
},
{
key: "status",
header: "وضعیت",
render: (r) => (
<Badge tone={r.status === "posted" ? "success" : r.status === "draft" ? "default" : "warning"}>
{String(r.status)}
</Badge>
),
},
{ key: "total_debit", header: "بدهکار", render: (r) => formatMoney(String(r.total_debit)) },
{ key: "total_credit", header: "بستانکار", render: (r) => formatMoney(String(r.total_credit)) },
{ key: "description", header: "شرح" },
]}
rows={rows as unknown as Record<string, unknown>[]}
empty={
<EmptyState
title="سندی وجود ندارد"
action={
<Link href="/accounting/vouchers/new">
<Button>ایجاد سند</Button>
</Link>
}
/>
}
/>
<Pagination
page={page}
pageSize={pageSize}
total={
(listQ.data?.length ?? 0) === pageSize
? page * pageSize + 1
: (page - 1) * pageSize + Math.max(listQ.data?.length ?? 0, 1)
}
onPageChange={setPage}
/>
</div>
);
}
export default function VouchersPage() {
return (
<Suspense fallback={<LoadingState />}>
<VouchersList />
</Suspense>
);
}