Wire sales/purchase posting, settlements, compliance, payroll employees, and ops edit flows; document the integration report. Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
3.9 KiB
TypeScript
129 lines
3.9 KiB
TypeScript
"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>
|
||
);
|
||
}
|