"use client"; import { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { Controller, useFieldArray, 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, Trash2 } from "lucide-react"; import { accountingApi } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { formatMoney } from "@/lib/utils"; import { PageHeader, Button, Badge, DataTable, LoadingState, ErrorState, ConfirmDialog, Card, CardContent, JalaliDateText, DatePicker, Input, Label, FieldError, Select, MoneyInput, } from "@/components/ds"; const lineSchema = z.object({ account_id: z.string().uuid("حساب معتبر انتخاب کنید"), debit: z.string().min(1), credit: z.string().min(1), description: z.string().optional(), cost_center_id: z.string().optional(), project_id: z.string().optional(), }); const editSchema = z.object({ voucher_date: z.string().min(1), description: z.string().optional(), lines: z.array(lineSchema).min(2, "حداقل دو ردیف لازم است"), }); type EditForm = z.infer; export default function VoucherDetailPage() { const params = useParams<{ id: string }>(); const id = params.id; const { tenantId } = useTenantId(); const qc = useQueryClient(); const router = useRouter(); const [confirm, setConfirm] = useState<"post" | "reverse" | "cancel" | null>(null); const [editing, setEditing] = useState(false); const voucherQ = useQuery({ queryKey: ["accounting", tenantId, "voucher", id], queryFn: () => accountingApi.vouchers.get(tenantId!, id), enabled: !!tenantId && !!id, }); const accountsQ = useQuery({ queryKey: ["accounting", tenantId, "accounts"], queryFn: () => accountingApi.accounts.list(tenantId!), enabled: !!tenantId, }); const costCentersQ = useQuery({ queryKey: ["accounting", tenantId, "cost-centers"], queryFn: () => accountingApi.costCenters.list(tenantId!), enabled: !!tenantId, }); const projectsQ = useQuery({ queryKey: ["accounting", tenantId, "projects"], queryFn: () => accountingApi.projects.list(tenantId!), enabled: !!tenantId, }); const editForm = useForm({ resolver: zodResolver(editSchema), defaultValues: { voucher_date: "", description: "", lines: [] }, }); const { fields, append, remove } = useFieldArray({ control: editForm.control, name: "lines" }); useEffect(() => { const v = voucherQ.data; if (!v || v.status !== "draft") return; editForm.reset({ voucher_date: v.voucher_date, description: v.description ?? "", lines: v.lines.map((l) => ({ account_id: l.account_id, debit: l.debit, credit: l.credit, description: l.description ?? "", cost_center_id: l.cost_center_id ?? "", project_id: l.project_id ?? "", })), }); }, [voucherQ.data, editForm]); const invalidate = async () => { await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "voucher", id] }); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "vouchers"] }); }; const updateM = useMutation({ mutationFn: (data: EditForm) => accountingApi.vouchers.update(tenantId!, id, { voucher_date: data.voucher_date, description: data.description || undefined, lines: data.lines.map((l) => ({ account_id: l.account_id, debit: l.debit || "0", credit: l.credit || "0", description: l.description || undefined, cost_center_id: l.cost_center_id || null, project_id: l.project_id || null, })), }), onSuccess: async () => { toast.success("سند به‌روز شد"); setEditing(false); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const validateM = useMutation({ mutationFn: () => accountingApi.vouchers.validate(tenantId!, id), onSuccess: async () => { toast.success("سند اعتبارسنجی شد"); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const postM = useMutation({ mutationFn: () => accountingApi.vouchers.post(tenantId!, id, "manual"), onSuccess: async () => { toast.success("سند ثبت قطعی شد"); setConfirm(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const reverseM = useMutation({ mutationFn: () => accountingApi.vouchers.reverse(tenantId!, id), onSuccess: async () => { toast.success("سند برگشت خورد"); setConfirm(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const cancelM = useMutation({ mutationFn: () => accountingApi.vouchers.cancel(tenantId!, id), onSuccess: async () => { toast.success("سند لغو شد"); setConfirm(null); await invalidate(); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId || voucherQ.isLoading) return ; if (voucherQ.error) return voucherQ.refetch()} />; const v = voucherQ.data!; const accountMap = new Map((accountsQ.data ?? []).map((a) => [a.id, `${a.code} — ${a.name}`])); const postable = (accountsQ.data ?? []).filter((a) => a.is_postable); const activeCostCenters = (costCentersQ.data ?? []).filter((c) => c.is_active); const activeProjects = (projectsQ.data ?? []).filter((p) => p.is_active); const isDraft = v.status === "draft"; return (
{isDraft ? ( ) : null} {v.status === "draft" || v.status === "validated" ? ( ) : null} {v.status === "draft" || v.status === "validated" ? ( ) : null} {v.status === "posted" ? ( ) : null} {v.status === "draft" || v.status === "validated" ? ( ) : null}
} />

وضعیت

{v.status}

تاریخ

جمع بدهکار

{formatMoney(v.total_debit)}

جمع بستانکار

{formatMoney(v.total_credit)}

{isDraft && editing ? (
updateM.mutate(d))}>
( )} /> {editForm.formState.errors.voucher_date?.message}

ردیف‌های سند

{fields.map((field, index) => (
))} {editForm.formState.errors.lines?.message || editForm.formState.errors.lines?.root?.message}
) : ( accountMap.get(String(r.account_id)) || String(r.account_id), }, { key: "debit", header: "بدهکار", render: (r) => formatMoney(String(r.debit)) }, { key: "credit", header: "بستانکار", render: (r) => formatMoney(String(r.credit)) }, { key: "description", header: "شرح" }, ]} rows={(v.lines ?? []) as unknown as Record[]} /> )} setConfirm(null)} title="ثبت قطعی سند؟" description="پس از ثبت، Journal Entry فقط از طریق Posting Engine ایجاد می‌شود و قابل ویرایش مستقیم نیست." confirmLabel="ثبت قطعی" loading={postM.isPending} onConfirm={() => postM.mutate()} /> setConfirm(null)} title="برگشت سند؟" description="یک سند معکوس از طریق Posting Engine ایجاد می‌شود." confirmLabel="برگشت" loading={reverseM.isPending} onConfirm={() => reverseM.mutate()} /> setConfirm(null)} title="لغو سند؟" description="سند پیش‌نویس لغو می‌شود و دیگر قابل ثبت نیست." confirmLabel="لغو سند" danger loading={cancelM.isPending} onConfirm={() => cancelM.mutate()} /> ); }