"use client"; import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { Eye, Pencil, Plus } from "lucide-react"; import { accountingApi } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { formatMoney, parseMoneyInput } from "@/lib/utils"; import { PageHeader, Button, Dialog, Input, DataTable, EmptyState, LoadingState, ErrorState, Badge, FormField, MoneyInput, DatePicker, JalaliDateText, ConfirmDialog, Tabs, Select, } from "@/components/ds"; import { PartyCombobox } from "@/components/accounting/EntityCombobox"; const schema = z.object({ number: z.string().min(1, "شماره الزامی است"), doc_date: z.string().min(1, "تاریخ الزامی است"), party_name: z.string().optional(), party_id: z.string().optional(), amount: z.string().min(1, "مبلغ الزامی است"), description: z.string().optional(), }); type FormValues = z.infer; export function BusinessDocsPage({ title, description, module, docType, }: { title: string; description?: string; module: string; docType: string; }) { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [open, setOpen] = useState(false); const [confirmId, setConfirmId] = useState(null); const [detailRow, setDetailRow] = useState | null>(null); const [editRow, setEditRow] = useState | null>(null); const listQ = useQuery({ queryKey: ["accounting", tenantId, "ops-docs", module, docType], queryFn: () => accountingApi.ops.listDocuments(tenantId!, module, docType), enabled: !!tenantId, }); const form = useForm({ resolver: zodResolver(schema), defaultValues: { number: "", doc_date: new Date().toISOString().slice(0, 10), party_name: "", party_id: "", amount: "0", description: "", }, }); const editForm = useForm({ resolver: zodResolver(schema), defaultValues: { number: "", doc_date: "", party_name: "", party_id: "", amount: "0", description: "", }, }); const createM = useMutation({ mutationFn: (d: FormValues) => accountingApi.ops.createDocument(tenantId!, { module, doc_type: docType, number: d.number, doc_date: d.doc_date, party_name: d.party_name || undefined, party_id: d.party_id || undefined, amount: parseMoneyInput(d.amount) || "0", description: d.description || undefined, status: "draft", }), onSuccess: async () => { toast.success("ثبت شد"); setOpen(false); form.reset({ number: "", doc_date: new Date().toISOString().slice(0, 10), party_name: "", party_id: "", amount: "0", description: "", }); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", module, docType] }); }, onError: (e: Error) => toast.error(e.message), }); const confirmM = useMutation({ mutationFn: (id: string) => accountingApi.ops.confirmDocument(tenantId!, id), onSuccess: async () => { toast.success("تأیید شد"); setConfirmId(null); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", module, docType] }); }, onError: (e: Error) => toast.error(e.message), }); const openEdit = (row: Record) => { setEditRow(row); editForm.reset({ number: String(row.number ?? ""), doc_date: String(row.doc_date ?? ""), party_name: String(row.party_name ?? ""), party_id: String(row.party_id ?? ""), amount: String(row.amount ?? "0"), description: String(row.description ?? ""), }); }; const updateM = useMutation({ mutationFn: (d: FormValues) => accountingApi.ops.updateDocument(tenantId!, String(editRow!.id), { doc_date: d.doc_date, party_name: d.party_name || undefined, party_id: d.party_id || undefined, amount: parseMoneyInput(d.amount) || "0", description: d.description || undefined, }), onSuccess: async () => { toast.success("ویرایش ذخیره شد"); setEditRow(null); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", module, docType] }); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId || listQ.isLoading) return ; if (listQ.error) { return listQ.refetch()} />; } return (
setOpen(true)}> {title.includes("فاکتور") || title.includes("سند") || title.includes("چک") ? `${title} جدید` : `ثبت ${title}`} } /> , }, { key: "party_name", header: "طرف حساب" }, { key: "amount", header: "مبلغ", render: (r) => formatMoney(String(r.amount ?? "0")), }, { key: "status", header: "وضعیت", render: (r) => ( {String(r.status)} ), }, { key: "id", header: "عملیات", render: (r) => (
{r.status === "draft" ? ( <> ) : null}
), }, ]} rows={(listQ.data ?? []) as unknown as Record[]} empty={ setOpen(true)}> ثبت {title} } /> } /> setOpen(false)} title={`ثبت ${title}`} size="lg">
createM.mutate(d))}> ( )} /> { form.setValue("party_name", party?.name || ""); form.setValue("party_id", party?.id || ""); }} />
setEditRow(null)} title={`ویرایش ${title}`} size="lg">
updateM.mutate(d))}> ( )} /> { editForm.setValue("party_name", party?.name || ""); editForm.setValue("party_id", party?.id || ""); }} />
setDetailRow(null)} title={`جزئیات ${title}`}> {detailRow ? (
شماره
{String(detailRow.number ?? "—")}
تاریخ
طرف حساب
{String(detailRow.party_name ?? "—")}
مبلغ
{formatMoney(String(detailRow.amount ?? "0"))}
وضعیت
{String(detailRow.status ?? "—")}
توضیح
{String(detailRow.description ?? "—")}
) : null}
setConfirmId(null)} onConfirm={() => confirmId && confirmM.mutate(confirmId)} title="تأیید سند؟" description="پس از تأیید، وضعیت به confirmed تغییر می‌کند." confirmLabel="تأیید" loading={confirmM.isPending} />
); } export function BlockedModulePage({ title }: { title: string }) { return (
); } export function InventoryItemsPage() { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [open, setOpen] = useState(false); const form = useForm({ defaultValues: { code: "", name: "", unit: "عدد", quantity_on_hand: "0", unit_cost: "0" }, }); const listQ = useQuery({ queryKey: ["accounting", tenantId, "ops-items"], queryFn: () => accountingApi.ops.listItems(tenantId!), enabled: !!tenantId, }); const createM = useMutation({ mutationFn: (d: { code: string; name: string; unit: string; quantity_on_hand: string; unit_cost: string }) => accountingApi.ops.createItem(tenantId!, d), onSuccess: async () => { toast.success("کالا ثبت شد"); setOpen(false); form.reset(); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-items"] }); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId || listQ.isLoading) return ; if (listQ.error) return listQ.refetch()} />; return (
setOpen(true)}> کالای جدید } /> String(r.quantity_on_hand ?? "0"), }, { key: "unit_cost", header: "بهای واحد", render: (r) => formatMoney(String(r.unit_cost ?? "0")), }, { key: "is_active", header: "وضعیت", render: (r) => {r.is_active ? "فعال" : "غیرفعال"}, }, ]} rows={(listQ.data ?? []) as unknown as Record[]} empty={} /> setOpen(false)} title="ثبت کالا">
createM.mutate(d))} >
); } export function WarehousesPage() { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [open, setOpen] = useState(false); const form = useForm({ defaultValues: { code: "", name: "" } }); const listQ = useQuery({ queryKey: ["accounting", tenantId, "ops-warehouses"], queryFn: () => accountingApi.ops.listWarehouses(tenantId!), enabled: !!tenantId, }); const createM = useMutation({ mutationFn: (d: { code: string; name: string }) => accountingApi.ops.createWarehouse(tenantId!, d), onSuccess: async () => { toast.success("انبار ثبت شد"); setOpen(false); form.reset(); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-warehouses"] }); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId || listQ.isLoading) return ; if (listQ.error) return listQ.refetch()} />; return (
setOpen(true)}> انبار جدید } /> {r.is_active ? "فعال" : "غیرفعال"}, }, ]} rows={(listQ.data ?? []) as unknown as Record[]} empty={} /> setOpen(false)} title="ثبت انبار">
createM.mutate(d))}>
); } export function ChequesPage({ defaultIncoming, initialOpen = false, }: { defaultIncoming?: boolean; initialOpen?: boolean; }) { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [tab, setTab] = useState<"all" | "in" | "out" | "bounced">("all"); const [open, setOpen] = useState(initialOpen); const form = useForm({ defaultValues: { cheque_number: "", amount: "0", issue_date: new Date().toISOString().slice(0, 10), due_date: "", is_incoming: defaultIncoming !== false, payee: "", }, }); const listQ = useQuery({ queryKey: ["accounting", tenantId, "cheques"], queryFn: () => accountingApi.treasury.listCheques(tenantId!), enabled: !!tenantId, }); const createM = useMutation({ mutationFn: (d: { cheque_number: string; amount: string; issue_date: string; due_date?: string; is_incoming: boolean; payee?: string; }) => accountingApi.treasury.createCheque(tenantId!, { ...d, due_date: d.due_date || undefined, status: d.is_incoming ? "received" : "issued", }), onSuccess: async () => { toast.success("چک ثبت شد"); setOpen(false); form.reset({ cheque_number: "", amount: "0", issue_date: new Date().toISOString().slice(0, 10), due_date: "", is_incoming: true, payee: "", }); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "cheques"] }); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId || listQ.isLoading) return ; if (listQ.error) return listQ.refetch()} />; const rows = (listQ.data ?? []).filter((c) => { if (tab === "in") return c.is_incoming; if (tab === "out") return !c.is_incoming; if (tab === "bounced") return c.status === "bounced"; return true; }); return (
setOpen(true)}> ثبت چک } /> setTab(v as typeof tab)} items={[ { id: "all", label: "همه" }, { id: "in", label: "دریافتی" }, { id: "out", label: "پرداختی" }, { id: "bounced", label: "برگشتی" }, ]} />
(r.due_date ? : "—"), }, { key: "issue_date", header: "تاریخ", render: (r) => , }, { key: "payee", header: "ذینفع" }, { key: "status", header: "وضعیت", render: (r) => {String(r.status)}, }, { key: "amount", header: "مبلغ", render: (r) => formatMoney(String(r.amount)), }, ]} rows={rows as unknown as Record[]} empty={} />
setOpen(false)} title="ثبت چک جدید" size="lg">
createM.mutate(d))}>
( )} /> ( )} />
); } export function ReceiptsPaymentsPage() { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [mode, setMode] = useState<"receipt" | "payment">("receipt"); const [open, setOpen] = useState(false); const form = useForm({ defaultValues: { number: "", date: new Date().toISOString().slice(0, 10), amount: "0", payment_method: "cash", cash_box_id: "", bank_account_id: "", counter_account_id: "", description: "", }, }); const receiptsQ = useQuery({ queryKey: ["accounting", tenantId, "receipts"], queryFn: () => accountingApi.treasury.listReceipts(tenantId!), enabled: !!tenantId, }); const paymentsQ = useQuery({ queryKey: ["accounting", tenantId, "payments"], queryFn: () => accountingApi.treasury.listPayments(tenantId!), enabled: !!tenantId, }); const cashBoxesQ = useQuery({ queryKey: ["accounting", tenantId, "cash-boxes"], queryFn: () => accountingApi.treasury.listCashBoxes(tenantId!), enabled: !!tenantId, }); const bankAccountsQ = useQuery({ queryKey: ["accounting", tenantId, "bank-accounts"], queryFn: () => accountingApi.treasury.listBankAccounts(tenantId!), enabled: !!tenantId, }); const accountsQ = useQuery({ queryKey: ["accounting", tenantId, "accounts"], queryFn: () => accountingApi.accounts.list(tenantId!), enabled: !!tenantId, }); const policyQ = useQuery({ queryKey: ["accounting", tenantId, "posting-policy"], queryFn: () => accountingApi.setup.getPostingPolicy(tenantId!), enabled: !!tenantId, }); const autoPost = mode === "receipt" ? Boolean(policyQ.data?.auto_post?.treasury_receipt ?? true) : Boolean(policyQ.data?.auto_post?.treasury_payment ?? true); const createM = useMutation({ mutationFn: async (d: { number: string; date: string; amount: string; payment_method: string; cash_box_id?: string; bank_account_id?: string; counter_account_id?: string; description?: string; }) => { if (autoPost && !d.counter_account_id) { throw new Error("حساب مقابل برای ثبت خودکار سند حسابداری الزامی است"); } const counter = d.counter_account_id || undefined; if (mode === "receipt") { return accountingApi.treasury.createReceipt(tenantId!, { receipt_number: d.number, receipt_date: d.date, amount: d.amount, payment_method: d.payment_method, cash_box_id: d.payment_method === "cash" ? d.cash_box_id || undefined : undefined, bank_account_id: d.payment_method === "bank" ? d.bank_account_id || undefined : undefined, counter_account_id: counter, description: d.description, }); } return accountingApi.treasury.createPayment(tenantId!, { payment_number: d.number, payment_date: d.date, amount: d.amount, payment_method: d.payment_method, cash_box_id: d.payment_method === "cash" ? d.cash_box_id || undefined : undefined, bank_account_id: d.payment_method === "bank" ? d.bank_account_id || undefined : undefined, counter_account_id: counter, description: d.description, }); }, onSuccess: async (res) => { const vn = res && typeof res === "object" && "voucher_number" in res ? String((res as { voucher_number?: string | null }).voucher_number || "") : ""; toast.success(vn ? `ثبت شد — سند حسابداری ${vn}` : "ثبت شد"); setOpen(false); form.reset(); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "receipts"] }); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "payments"] }); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "vouchers"] }); }, onError: (e: Error) => toast.error(e.message), }); if ( !tenantId || receiptsQ.isLoading || paymentsQ.isLoading || cashBoxesQ.isLoading || bankAccountsQ.isLoading ) { return ; } if (receiptsQ.error || paymentsQ.error || cashBoxesQ.error || bankAccountsQ.error) { return ( { receiptsQ.refetch(); paymentsQ.refetch(); }} /> ); } const postable = (accountsQ.data ?? []).filter((a) => a.is_postable); const rows = mode === "receipt" ? (receiptsQ.data ?? []).map((r) => ({ id: r.id, number: r.receipt_number, date: r.receipt_date, amount: r.amount, payment_method: r.payment_method, description: r.description, voucher_id: r.voucher_id, })) : (paymentsQ.data ?? []).map((p) => ({ id: p.id, number: p.payment_number, date: p.payment_date, amount: p.amount, payment_method: p.payment_method, description: p.description, voucher_id: p.voucher_id, })); return (
setOpen(true)}> ثبت جدید } /> setMode(v as typeof mode)} items={[ { id: "receipt", label: "دریافت‌ها" }, { id: "payment", label: "پرداخت‌ها" }, ]} />
, }, { key: "payment_method", header: "روش" }, { key: "amount", header: "مبلغ", render: (r) => formatMoney(String(r.amount)), }, { key: "voucher_id", header: "سند GL", render: (r) => (r.voucher_id ? ثبت‌شده : "—"), }, { key: "description", header: "توضیح" }, ]} rows={rows as unknown as Record[]} empty={} />
setOpen(false)} title={mode === "receipt" ? "ثبت دریافت" : "ثبت پرداخت"}>
createM.mutate(d))}>
( )} />
{autoPost ? (

ثبت خودکار سند دفتر کل فعال است — حساب صندوق/بانک از تنظیمات صندوق/حساب بانکی گرفته می‌شود.

) : null}
); } export function TransfersPage() { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [open, setOpen] = useState(false); const cashBoxesQ = useQuery({ queryKey: ["accounting", tenantId, "cash-boxes"], queryFn: () => accountingApi.treasury.listCashBoxes(tenantId!), enabled: !!tenantId, }); const bankAccountsQ = useQuery({ queryKey: ["accounting", tenantId, "bank-accounts"], queryFn: () => accountingApi.treasury.listBankAccounts(tenantId!), enabled: !!tenantId, }); const listQ = useQuery({ queryKey: ["accounting", tenantId, "transfers"], queryFn: () => accountingApi.treasury.listTransfers(tenantId!), enabled: !!tenantId, }); const form = useForm({ defaultValues: { transfer_date: new Date().toISOString().slice(0, 10), amount: "0", from_key: "", to_key: "", description: "", }, }); const createM = useMutation({ mutationFn: (d: { transfer_date: string; amount: string; from_key: string; to_key: string; description?: string; }) => { const [from_type, from_id] = d.from_key.split(":"); const [to_type, to_id] = d.to_key.split(":"); return accountingApi.treasury.createTransfer(tenantId!, { transfer_date: d.transfer_date, amount: d.amount, from_type, from_id, to_type, to_id, description: d.description, }); }, onSuccess: async () => { toast.success("انتقال ثبت شد"); setOpen(false); form.reset(); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "transfers"] }); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId || listQ.isLoading) return ; if (listQ.error) return listQ.refetch()} />; const accountOptions = [ ...(cashBoxesQ.data ?? []).map((c) => ({ value: `cash:${c.id}`, label: `صندوق: ${c.name}` })), ...(bankAccountsQ.data ?? []).map((b) => ({ value: `bank:${b.id}`, label: `بانک: ${b.account_number}`, })), ]; return (
setOpen(true)}> انتقال جدید } /> , }, { key: "amount", header: "مبلغ", render: (r) => formatMoney(String(r.amount)), }, { key: "from_type", header: "از" }, { key: "to_type", header: "به" }, { key: "description", header: "توضیح" }, ]} rows={(listQ.data ?? []) as unknown as Record[]} empty={} /> setOpen(false)} title="انتقال بین حساب‌ها">
createM.mutate(d))}> ( )} />
); } export function ReconciliationPage() { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [open, setOpen] = useState(false); const banksQ = useQuery({ queryKey: ["accounting", tenantId, "bank-accounts"], queryFn: () => accountingApi.treasury.listBankAccounts(tenantId!), enabled: !!tenantId, }); const listQ = useQuery({ queryKey: ["accounting", tenantId, "reconciliations"], queryFn: () => accountingApi.treasury.listReconciliations(tenantId!), enabled: !!tenantId, }); const form = useForm({ defaultValues: { bank_account_id: "", statement_date: new Date().toISOString().slice(0, 10), statement_balance: "0", book_balance: "0", }, }); const createM = useMutation({ mutationFn: (d: { bank_account_id: string; statement_date: string; statement_balance: string; book_balance: string; }) => accountingApi.treasury.createReconciliation(tenantId!, d), onSuccess: async () => { toast.success("تطبیق ثبت شد"); setOpen(false); form.reset(); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "reconciliations"] }); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId || listQ.isLoading) return ; if (listQ.error) return listQ.refetch()} />; return (
setOpen(true)}> تطبیق جدید } /> , }, { key: "statement_balance", header: "مانده صورت‌حساب", render: (r) => formatMoney(String(r.statement_balance)), }, { key: "book_balance", header: "مانده دفتر", render: (r) => formatMoney(String(r.book_balance)), }, { key: "difference", header: "اختلاف", render: (r) => formatMoney(String(r.difference)), }, { key: "status", header: "وضعیت", render: (r) => {String(r.status)}, }, ]} rows={(listQ.data ?? []) as unknown as Record[]} empty={} /> setOpen(false)} title="تطبیق بانکی">
createM.mutate(d))}> ( )} />
); }