"use client"; import { useMemo, useState } from "react"; import { 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 { Plus, Archive, Pencil } from "lucide-react"; import { accountingApi, type Account, type ChartOfAccounts } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { PageHeader, Button, Dialog, Input, Label, FieldError, Textarea, DataTable, EmptyState, LoadingState, ErrorState, Badge, Select, Drawer, TableToolbar, Tabs, Checkbox, } from "@/components/ds"; const chartSchema = z.object({ code: z.string().min(1, "کد الزامی است"), name: z.string().min(1, "نام الزامی است"), description: z.string().optional(), is_default: z.boolean().optional(), }); const accountSchema = z.object({ chart_id: z.string().uuid("دفتر حساب را انتخاب کنید"), code: z.string().min(1, "کد الزامی است"), name: z.string().min(1, "نام الزامی است"), account_type: z.string().min(1), account_category: z.string().min(1), parent_id: z.string().optional(), is_group: z.boolean().optional(), is_postable: z.boolean().optional(), description: z.string().optional(), }); type ChartForm = z.infer; type AccountForm = z.infer; const ACCOUNT_TYPES = [ { value: "asset", label: "دارایی" }, { value: "liability", label: "بدهی" }, { value: "equity", label: "حقوق صاحبان سهام" }, { value: "revenue", label: "درآمد" }, { value: "expense", label: "هزینه" }, ]; const ACCOUNT_CATEGORIES = [ { value: "current_asset", label: "دارایی جاری" }, { value: "non_current_asset", label: "دارایی غیرجاری" }, { value: "current_liability", label: "بدهی جاری" }, { value: "non_current_liability", label: "بدهی غیرجاری" }, { value: "equity", label: "حقوق صاحبان سهام" }, { value: "operating_revenue", label: "درآمد عملیاتی" }, { value: "other_revenue", label: "سایر درآمدها" }, { value: "operating_expense", label: "هزینه عملیاتی" }, { value: "other_expense", label: "سایر هزینه‌ها" }, ]; export default function ChartOfAccountsPage() { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [tab, setTab] = useState("accounts"); const [chartOpen, setChartOpen] = useState(false); const [accountOpen, setAccountOpen] = useState(false); const [editChart, setEditChart] = useState(null); const [selected, setSelected] = useState(null); const [filterChart, setFilterChart] = useState(""); const [search, setSearch] = useState(""); const [showInactive, setShowInactive] = useState(false); const chartsQ = useQuery({ queryKey: ["accounting", tenantId, "charts"], queryFn: () => accountingApi.charts.list(tenantId!), enabled: !!tenantId, }); const accountsQ = useQuery({ queryKey: ["accounting", tenantId, "accounts", filterChart], queryFn: () => accountingApi.accounts.list(tenantId!, 1, 500, filterChart || undefined), enabled: !!tenantId, }); const chartForm = useForm({ resolver: zodResolver(chartSchema), defaultValues: { code: "", name: "", description: "", is_default: false }, }); const accountForm = useForm({ resolver: zodResolver(accountSchema), defaultValues: { chart_id: "", code: "", name: "", account_type: "asset", account_category: "current_asset", parent_id: "", is_group: false, is_postable: true, description: "", }, }); const editChartForm = useForm({ resolver: zodResolver(chartSchema.omit({ code: true }).extend({ code: z.string().optional() })), }); const editAccountForm = useForm({ defaultValues: { name: "", status: "active", description: "", is_postable: true }, }); const invalidate = async () => { await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "charts"] }); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "accounts"] }); }; const createChart = useMutation({ mutationFn: (data: ChartForm) => accountingApi.charts.create(tenantId!, data), onSuccess: async () => { toast.success("دفتر حساب ایجاد شد"); setChartOpen(false); chartForm.reset(); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const updateChart = useMutation({ mutationFn: (data: ChartForm) => accountingApi.charts.update(tenantId!, editChart!.id, { name: data.name, description: data.description, is_default: data.is_default, }), onSuccess: async () => { toast.success("دفتر به‌روز شد"); setEditChart(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const archiveChart = useMutation({ mutationFn: (id: string) => accountingApi.charts.archive(tenantId!, id), onSuccess: async () => { toast.success("دفتر آرشیو شد"); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const createAccount = useMutation({ mutationFn: (data: AccountForm) => accountingApi.accounts.create(tenantId!, { ...data, parent_id: data.parent_id || null, is_postable: data.is_postable ?? true, is_group: data.is_group ?? false, }), onSuccess: async () => { toast.success("حساب ایجاد شد"); setAccountOpen(false); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const updateAccount = useMutation({ mutationFn: (body: { name: string; status: string; description?: string; is_postable: boolean }) => accountingApi.accounts.update(tenantId!, selected!.id, body), onSuccess: async () => { toast.success("حساب به‌روز شد"); setSelected(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const archiveAccount = useMutation({ mutationFn: (id: string) => accountingApi.accounts.archive(tenantId!, id), onSuccess: async () => { toast.success("حساب آرشیو شد"); setSelected(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const rows = useMemo(() => { let list = accountsQ.data ?? []; if (!showInactive) list = list.filter((a) => a.status === "active"); if (search.trim()) { const q = search.trim().toLowerCase(); list = list.filter( (a) => a.code.toLowerCase().includes(q) || a.name.toLowerCase().includes(q) ); } return [...list].sort((a, b) => a.code.localeCompare(b.code, "fa")); }, [accountsQ.data, search, showInactive]); if (!tenantId) return ; if (chartsQ.isLoading || accountsQ.isLoading) return ; if (chartsQ.error || accountsQ.error) { return ( { void chartsQ.refetch(); void accountsQ.refetch(); }} /> ); } const chartName = (id: string) => chartsQ.data?.find((c) => c.id === id)?.name ?? id; return (
} /> {tab === "charts" ? (
{chartsQ.data?.map((c) => (

{c.name}

{c.code}

{c.is_default ? پیش‌فرض : null} {c.is_active ? "فعال" : "آرشیو"}
{c.is_active ? ( ) : null}
))} {!chartsQ.data?.length ? ( setChartOpen(true)}> ایجاد دفتر } /> ) : null}
) : ( <> setShowInactive(e.target.checked)} /> } /> {/* Mobile cards */}
{rows.map((r) => ( ))} {!rows.length ? : null}
( {String(r.code)} ), }, { key: "name", header: "نام" }, { key: "chart_id", header: "دفتر", render: (r) => chartName(String(r.chart_id)), }, { key: "account_type", header: "نوع", render: (r) => ACCOUNT_TYPES.find((t) => t.value === r.account_type)?.label ?? String(r.account_type), }, { key: "status", header: "وضعیت", render: (r) => ( {String(r.status)} ), }, { key: "id", header: "", render: (r) => ( ), }, ]} rows={rows as unknown as Record[]} empty={ setChartOpen(true)}> ایجاد دفتر حساب } /> } />
)} setChartOpen(false)} title="ایجاد دفتر حساب">
createChart.mutate(d))}>
{chartForm.formState.errors.code?.message}
{chartForm.formState.errors.name?.message}