"use client"; import { 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 { Calendar, Plus } from "lucide-react"; import { accountingApi } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { formatMoney } from "@/lib/utils"; import { PageHeader, Button, Dialog, Drawer, Input, DataTable, EmptyState, LoadingState, ErrorState, Badge, Tabs, FormField, Select, MoneyInput, } from "@/components/ds"; const categorySchema = z.object({ code: z.string().min(1, "کد الزامی است"), name: z.string().min(1, "نام الزامی است"), default_useful_life_months: z.number().int().min(1).optional(), }); const assetSchema = z.object({ code: z.string().min(1, "کد الزامی است"), name: z.string().min(1, "نام الزامی است"), category_id: z.string().optional(), acquisition_cost: z.string().optional(), residual_value: z.string().optional(), }); const depreciateSchema = z.object({ expense_account_id: z.string().min(1, "حساب هزینه را انتخاب کنید"), accumulated_account_id: z.string().min(1, "حساب استهلاک انباشته را انتخاب کنید"), }); type AssetRow = { id: string; code: string; name: string; status: string }; type TabId = "categories" | "assets"; export default function AssetsPage() { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [tab, setTab] = useState("assets"); const [categoryOpen, setCategoryOpen] = useState(false); const [assetOpen, setAssetOpen] = useState(false); const [depreciateAsset, setDepreciateAsset] = useState(null); const [scheduleAsset, setScheduleAsset] = useState(null); const categoriesQ = useQuery({ queryKey: ["accounting", tenantId, "asset-categories"], queryFn: () => accountingApi.assets.listCategories(tenantId!), enabled: !!tenantId, }); const assetsQ = useQuery({ queryKey: ["accounting", tenantId, "assets"], queryFn: () => accountingApi.assets.list(tenantId!), enabled: !!tenantId, }); const accountsQ = useQuery({ queryKey: ["accounting", tenantId, "accounts"], queryFn: () => accountingApi.accounts.list(tenantId!), enabled: !!tenantId, }); const scheduleQ = useQuery({ queryKey: ["accounting", tenantId, "asset-schedule", scheduleAsset?.id], queryFn: () => accountingApi.assets.schedule(tenantId!, scheduleAsset!.id), enabled: !!tenantId && !!scheduleAsset, }); const categoryForm = useForm>({ resolver: zodResolver(categorySchema), defaultValues: { code: "", name: "", default_useful_life_months: 60 }, }); const assetForm = useForm>({ resolver: zodResolver(assetSchema), defaultValues: { code: "", name: "", category_id: "", acquisition_cost: "0", residual_value: "0" }, }); const depreciateForm = useForm>({ resolver: zodResolver(depreciateSchema), defaultValues: { expense_account_id: "", accumulated_account_id: "" }, }); const postableAccounts = (accountsQ.data ?? []).filter((a) => a.is_postable && a.status === "active"); const invalidateCategories = () => qc.invalidateQueries({ queryKey: ["accounting", tenantId, "asset-categories"] }); const invalidateAssets = () => qc.invalidateQueries({ queryKey: ["accounting", tenantId, "assets"] }); const createCategoryM = useMutation({ mutationFn: (d: z.infer) => accountingApi.assets.createCategory(tenantId!, { code: d.code, name: d.name, default_useful_life_months: d.default_useful_life_months, }), onSuccess: async () => { toast.success("دسته‌بندی ایجاد شد"); setCategoryOpen(false); categoryForm.reset(); await invalidateCategories(); }, onError: (e: Error) => toast.error(e.message), }); const createAssetM = useMutation({ mutationFn: (d: z.infer) => accountingApi.assets.create(tenantId!, { code: d.code, name: d.name, category_id: d.category_id || undefined, acquisition_cost: d.acquisition_cost, residual_value: d.residual_value, }), onSuccess: async () => { toast.success("دارایی ایجاد شد"); setAssetOpen(false); assetForm.reset(); await invalidateAssets(); }, onError: (e: Error) => toast.error(e.message), }); const activateM = useMutation({ mutationFn: (id: string) => accountingApi.assets.activate(tenantId!, id), onSuccess: async () => { toast.success("دارایی فعال شد"); await invalidateAssets(); }, onError: (e: Error) => toast.error(e.message), }); const depreciateM = useMutation({ mutationFn: (d: z.infer) => accountingApi.assets.depreciate(tenantId!, depreciateAsset!.id, d), onSuccess: async (res) => { toast.success(`استهلاک ثبت شد — مبلغ ${formatMoney(res.amount)}`); setDepreciateAsset(null); depreciateForm.reset(); await invalidateAssets(); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId) return ; const loading = categoriesQ.isLoading || assetsQ.isLoading || accountsQ.isLoading; if (loading) return ; const err = categoriesQ.error || assetsQ.error || accountsQ.error; if (err) { return ( { void categoriesQ.refetch(); void assetsQ.refetch(); void accountsQ.refetch(); }} /> ); } const statusTone = (s: string) => s === "active" ? "success" : s === "draft" ? "default" : "warning"; const tabAction = tab === "categories" ? ( ) : ( ); const scheduleRows = (scheduleQ.data?.schedule ?? []) as Record[]; const scheduleColumns = scheduleRows.length > 0 ? Object.keys(scheduleRows[0]).map((k) => ({ key: k, header: k })) : [{ key: "period", header: "دوره" }]; return (
setTab(id as TabId)} items={[ { id: "assets", label: "دارایی‌ها" }, { id: "categories", label: "دسته‌بندی‌ها" }, ]} /> {tab === "categories" && ( String(r.default_useful_life_months ?? "—"), }, ]} rows={(categoriesQ.data ?? []) as unknown as Record[]} empty={ setCategoryOpen(true)}>ایجاد دسته} /> } /> )} {tab === "assets" && ( {String(r.status)}, }, { key: "actions", header: "عملیات", render: (r) => { const row = r as unknown as AssetRow; return (
{row.status !== "active" && ( )} {row.status === "active" && ( )}
); }, }, ]} rows={(assetsQ.data ?? []) as unknown as Record[]} empty={ setAssetOpen(true)}>ایجاد دارایی} />} /> )} setCategoryOpen(false)} title="دسته‌بندی جدید">
createCategoryM.mutate(d))}>
setAssetOpen(false)} title="دارایی جدید">
createAssetM.mutate(d))}>
setDepreciateAsset(null)} title="ثبت استهلاک" description={depreciateAsset ? `${depreciateAsset.code} — ${depreciateAsset.name}` : undefined} >
depreciateM.mutate(d))}>
setScheduleAsset(null)} title="برنامه استهلاک" description={scheduleAsset ? `${scheduleAsset.code} — ${scheduleAsset.name}` : undefined} width="lg" > {scheduleQ.isLoading ? ( ) : scheduleQ.error ? ( scheduleQ.refetch()} /> ) : ( ({ ...c, render: (r) => { const v = r[c.key]; if (typeof v === "number" || (typeof v === "string" && /^\d/.test(v))) { return formatMoney(String(v)); } return String(v ?? "—"); }, }))} rows={scheduleRows} empty={} /> )}
); }