TorbatYar/frontend/app/accounting/vouchers/page.tsx
Mortezakoohjani 7953e47f8b Ship accounting UX: Rial integers, auto doc numbers, reverse-and-edit, real reports, customers nav.
Wire DocumentNumberSequence allocator, ledger-based subsidiary/detail reports, and posted-voucher reopen flow without mock ops forms.

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

137 lines
4.2 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_FA: Record<string, string> = {
draft: "پیش‌نویس",
validated: "اعتبارسنجی‌شده",
posted: "ثبت قطعی",
reversed: "برگشتی",
cancelled: "لغو شده",
};
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"}>
{STATUS_FA[String(r.status)] ?? 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>
);
}