"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 { Plus, Pencil } from "lucide-react"; import { accountingApi, type FiscalPeriod, type FiscalYear } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { PageHeader, Button, Dialog, ConfirmDialog, Input, Label, FieldError, DataTable, EmptyState, LoadingState, ErrorState, Badge, DatePicker, JalaliDateText, Select, Checkbox, } from "@/components/ds"; const yearSchema = z.object({ name: z.string().min(1, "نام الزامی است"), start_date: z.string().min(1, "تاریخ شروع الزامی است"), end_date: z.string().min(1, "تاریخ پایان الزامی است"), is_current: z.boolean().optional(), }); const periodSchema = z.object({ fiscal_year_id: z.string().uuid("سال مالی را انتخاب کنید"), name: z.string().min(1, "نام الزامی است"), period_number: z.number().int().min(1), start_date: z.string().min(1), end_date: z.string().min(1), is_current: z.boolean().optional(), }); const editNameSchema = z.object({ name: z.string().min(1, "نام الزامی است") }); export default function FiscalPage() { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [yearOpen, setYearOpen] = useState(false); const [periodOpen, setPeriodOpen] = useState(false); const [editYear, setEditYear] = useState(null); const [editPeriod, setEditPeriod] = useState(null); const [confirm, setConfirm] = useState< | { type: "year-close"; id: string } | { type: "period"; id: string; action: "lock" | "close" | "reopen" } | null >(null); const yearsQ = useQuery({ queryKey: ["accounting", tenantId, "fiscal-years"], queryFn: () => accountingApi.fiscal.listYears(tenantId!), enabled: !!tenantId, }); const periodsQ = useQuery({ queryKey: ["accounting", tenantId, "fiscal-periods"], queryFn: () => accountingApi.fiscal.listPeriods(tenantId!), enabled: !!tenantId, }); const yearForm = useForm>({ resolver: zodResolver(yearSchema), defaultValues: { name: "", start_date: "", end_date: "", is_current: true }, }); const periodForm = useForm>({ resolver: zodResolver(periodSchema), defaultValues: { fiscal_year_id: "", name: "", period_number: 1, start_date: "", end_date: "", is_current: false, }, }); const editYearForm = useForm>({ resolver: zodResolver(editNameSchema), }); const editPeriodForm = useForm>({ resolver: zodResolver(editNameSchema), }); const invalidate = async () => { await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "fiscal-years"] }); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "fiscal-periods"] }); }; const createYear = useMutation({ mutationFn: (d: z.infer) => accountingApi.fiscal.createYear(tenantId!, d), onSuccess: async () => { toast.success("سال مالی ایجاد شد"); setYearOpen(false); yearForm.reset(); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const updateYear = useMutation({ mutationFn: (d: z.infer) => accountingApi.fiscal.updateYear(tenantId!, editYear!.id, { name: d.name }), onSuccess: async () => { toast.success("سال مالی به‌روز شد"); setEditYear(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const setCurrentYear = useMutation({ mutationFn: (id: string) => accountingApi.fiscal.setCurrentYear(tenantId!, id), onSuccess: async () => { toast.success("سال جاری تنظیم شد"); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const closeYear = useMutation({ mutationFn: (id: string) => accountingApi.fiscal.closeYear(tenantId!, id), onSuccess: async () => { toast.success("سال مالی بسته شد"); setConfirm(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const generatePeriods = useMutation({ mutationFn: (id: string) => accountingApi.fiscal.generatePeriods(tenantId!, id), onSuccess: async (periods) => { toast.success(`${periods.length} دوره ایجاد شد`); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const createPeriod = useMutation({ mutationFn: (d: z.infer) => accountingApi.fiscal.createPeriod(tenantId!, d), onSuccess: async () => { toast.success("دوره مالی ایجاد شد"); setPeriodOpen(false); periodForm.reset(); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const updatePeriod = useMutation({ mutationFn: (d: z.infer) => accountingApi.fiscal.updatePeriod(tenantId!, editPeriod!.id, { name: d.name }), onSuccess: async () => { toast.success("دوره به‌روز شد"); setEditPeriod(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const setCurrentPeriod = useMutation({ mutationFn: (id: string) => accountingApi.fiscal.setCurrentPeriod(tenantId!, id), onSuccess: async () => { toast.success("دوره جاری تنظیم شد"); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const periodAction = useMutation({ mutationFn: async () => { if (!confirm || confirm.type !== "period") return; if (confirm.action === "lock") return accountingApi.fiscal.lockPeriod(tenantId!, confirm.id); if (confirm.action === "close") return accountingApi.fiscal.closePeriod(tenantId!, confirm.id); return accountingApi.fiscal.reopenPeriod(tenantId!, confirm.id); }, onSuccess: async () => { toast.success("وضعیت دوره به‌روز شد"); setConfirm(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId) return ; if (yearsQ.isLoading || periodsQ.isLoading) return ; if (yearsQ.error || periodsQ.error) { return ; } const yearName = (id: string) => yearsQ.data?.find((y) => y.id === id)?.name ?? id; const statusTone = (s: string) => s === "open" ? "success" : s === "locked" ? "warning" : s === "closed" ? "danger" : "default"; return (
} />

سال‌های مالی

, }, { key: "end_date", header: "پایان", render: (r) => , }, { key: "is_current", header: "جاری", render: (r) => (r.is_current ? بله : "—"), }, { key: "is_closed", header: "بسته", render: (r) => r.is_closed ? بسته : باز, }, { key: "actions", header: "عملیات", render: (r) => { const id = String(r.id); const closed = Boolean(r.is_closed); const current = Boolean(r.is_current); return (
{!current && !closed ? ( ) : null} {!closed ? ( ) : null}
); }, }, ]} rows={(yearsQ.data ?? []) as unknown as Record[]} empty={ setYearOpen(true)}>ایجاد سال} /> } />

دوره‌های مالی

yearName(String(r.fiscal_year_id)), }, { key: "start_date", header: "شروع", render: (r) => , }, { key: "is_current", header: "جاری", render: (r) => (r.is_current ? بله : "—"), }, { key: "status", header: "وضعیت", render: (r) => {String(r.status)}, }, { key: "actions", header: "عملیات", render: (r) => { const id = String(r.id); const status = String(r.status); const current = Boolean(r.is_current); return (
{!current && status !== "closed" ? ( ) : null} {status === "open" ? ( ) : null} {status === "open" || status === "locked" ? ( ) : null} {status === "closed" || status === "locked" ? ( ) : null}
); }, }, ]} rows={(periodsQ.data ?? []) as unknown as Record[]} empty={} /> setYearOpen(false)} title="ایجاد سال مالی">
createYear.mutate(d))}>
{yearForm.formState.errors.name?.message}
( )} /> {yearForm.formState.errors.start_date?.message}
( )} /> {yearForm.formState.errors.end_date?.message}
setPeriodOpen(false)} title="ایجاد دوره مالی">
createPeriod.mutate(d))}>
{periodForm.formState.errors.fiscal_year_id?.message}
{periodForm.formState.errors.name?.message}
( )} />
( )} />
setEditYear(null)} title="ویرایش سال مالی">
updateYear.mutate(d))}>
{editYearForm.formState.errors.name?.message}
setEditPeriod(null)} title="ویرایش دوره مالی">
updatePeriod.mutate(d))}>
{editPeriodForm.formState.errors.name?.message}
setConfirm(null)} onConfirm={() => confirm?.type === "year-close" && closeYear.mutate(confirm.id)} title="بستن سال مالی؟" description="پس از بستن، سال دیگر قابل ثبت سند جدید نیست." confirmLabel="بستن سال" loading={closeYear.isPending} /> setConfirm(null)} onConfirm={() => periodAction.mutate()} title="تغییر وضعیت دوره" description="این عملیات روی دوره مالی واقعی اعمال می‌شود." confirmLabel="اعمال" loading={periodAction.isPending} />
); }