TorbatYar/frontend/app/accounting/vouchers/page.tsx
Mortezakoohjani 7d66932da8 Complete Accounting sidebar modules with ops CRUD, BFF proxy, and treasury APIs.
Wire nested nav pages for phases 5.1-5.11 to real list/create endpoints, fix COA import connectivity via same-origin proxy, and add operational migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 16:10:06 +03:30

116 lines
3.5 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 } 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,
} 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 listQ = useQuery({
queryKey: ["accounting", tenantId, "vouchers"],
queryFn: () => accountingApi.vouchers.list(tenantId!),
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>
}
/>
}
/>
</div>
);
}
export default function VouchersPage() {
return (
<Suspense fallback={<LoadingState />}>
<VouchersList />
</Suspense>
);
}