"use client";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Plus } from "lucide-react";
import { toast } from "sonner";
import { z } from "zod";
import { accountingApi } from "@/lib/accounting-api";
import { useTenantId } from "@/hooks/useTenantId";
import { formatMoney, parseMoneyInput } from "@/lib/utils";
import {
Badge,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
DataTable,
DatePicker,
Dialog,
EmptyState,
ErrorState,
FormField,
Input,
JalaliDateText,
LoadingState,
MoneyInput,
PageHeader,
Select,
StatCard,
Textarea,
} from "@/components/ds";
import { PartyCombobox } from "@/components/accounting/EntityCombobox";
const today = () => new Date().toISOString().slice(0, 10);
const requiredText = z.string().min(1, "این فیلد الزامی است");
function Actions({
onCancel,
pending,
submitLabel = "ذخیره",
}: {
onCancel: () => void;
pending: boolean;
submitLabel?: string;
}) {
return (
);
}
const employeeSchema = z.object({
employee_code: requiredText,
first_name: requiredText,
last_name: requiredText,
base_salary: requiredText,
department_id: z.string(),
});
export function PayrollEmployeesPage() {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [open, setOpen] = useState(false);
const form = useForm>({
resolver: zodResolver(employeeSchema),
defaultValues: { employee_code: "", first_name: "", last_name: "", base_salary: "0", department_id: "" },
});
const employeesQ = useQuery({
queryKey: ["accounting", tenantId, "payroll-employees"],
queryFn: () => accountingApi.payroll.listEmployees(tenantId!),
enabled: !!tenantId,
});
const departmentsQ = useQuery({
queryKey: ["accounting", tenantId, "payroll-departments"],
queryFn: () => accountingApi.payroll.listDepartments(tenantId!),
enabled: !!tenantId,
});
const createM = useMutation({
mutationFn: (values: z.infer) =>
accountingApi.payroll.createEmployee(tenantId!, {
...values,
department_id: values.department_id || undefined,
}),
onSuccess: async () => {
toast.success("کارمند ثبت شد");
setOpen(false);
form.reset();
await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "payroll-employees"] });
},
onError: (error: Error) => toast.error(error.message),
});
if (!tenantId || employeesQ.isLoading || departmentsQ.isLoading) return ;
if (employeesQ.error || departmentsQ.error) {
const error = employeesQ.error || departmentsQ.error;
return { employeesQ.refetch(); departmentsQ.refetch(); }} />;
}
return (
setOpen(true)}>کارمند جدید}
/>
[]}
empty={}
/>
);
}
const policySchema = z.object({
code: requiredText,
name: requiredText,
policy_type: requiredText,
rules_config: requiredText.refine((value) => {
try { JSON.parse(value); return true; } catch { return false; }
}, "JSON معتبر وارد کنید"),
});
export function CompliancePoliciesPage() {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [open, setOpen] = useState(false);
const form = useForm>({
resolver: zodResolver(policySchema),
defaultValues: { code: "", name: "", policy_type: "", rules_config: "{}" },
});
const listQ = useQuery({
queryKey: ["accounting", tenantId, "compliance-policies"],
queryFn: () => accountingApi.compliance.listPolicies(tenantId!),
enabled: !!tenantId,
});
const createM = useMutation({
mutationFn: (values: z.infer) => accountingApi.compliance.createPolicy(tenantId!, values),
onSuccess: async () => {
toast.success("سیاست ثبت شد");
setOpen(false);
form.reset();
await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "compliance-policies"] });
},
onError: (error: Error) => toast.error(error.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={}
/>
);
}
const riskSchema = z.object({
code: requiredText,
title: requiredText,
risk_category: requiredText,
risk_level: z.enum(["low", "medium", "high", "critical"]),
risk_score: z.number().int().min(0),
});
export function ComplianceRisksPage() {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [open, setOpen] = useState(false);
const form = useForm>({
resolver: zodResolver(riskSchema),
defaultValues: { code: "", title: "", risk_category: "", risk_level: "low", risk_score: 0 },
});
const listQ = useQuery({ queryKey: ["accounting", tenantId, "compliance-risks"], queryFn: () => accountingApi.compliance.listRisks(tenantId!), enabled: !!tenantId });
const createM = useMutation({
mutationFn: (values: z.infer) => accountingApi.compliance.createRisk(tenantId!, values),
onSuccess: async () => {
toast.success("ریسک ثبت شد");
setOpen(false);
form.reset();
await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "compliance-risks"] });
},
onError: (error: Error) => toast.error(error.message),
});
if (!tenantId || listQ.isLoading) return ;
if (listQ.error) return listQ.refetch()} />;
return (
);
}
const settlementSchema = z.object({
settlement_number: requiredText,
settlement_date: requiredText,
settlement_type: z.enum(["receipt", "payment"]),
party_id: requiredText,
total_amount: requiredText,
allocations: requiredText.refine((value) => {
try { return Array.isArray(JSON.parse(value)); } catch { return false; }
}, "آرایه JSON معتبر وارد کنید"),
});
export function SettlementsPage({ partyType }: { partyType: "customer" | "supplier" }) {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [open, setOpen] = useState(false);
const form = useForm>({
resolver: zodResolver(settlementSchema),
defaultValues: { settlement_number: "", settlement_date: today(), settlement_type: partyType === "customer" ? "receipt" : "payment", party_id: "", total_amount: "0", allocations: "[]" },
});
const listQ = useQuery({ queryKey: ["accounting", tenantId, "settlements", partyType], queryFn: () => accountingApi.settlements.list(tenantId!, partyType), enabled: !!tenantId });
const partiesQ = useQuery({
queryKey: ["accounting", tenantId, partyType === "customer" ? "customers" : "suppliers"],
queryFn: () => partyType === "customer" ? accountingApi.customers.list(tenantId!) : accountingApi.suppliers.list(tenantId!),
enabled: !!tenantId,
});
const createM = useMutation({
mutationFn: (values: z.infer) => accountingApi.settlements.create(tenantId!, {
...values,
total_amount: parseMoneyInput(values.total_amount) || "0",
party_type: partyType,
allocations: JSON.parse(values.allocations) as { invoice_id?: string; amount: string }[],
}),
onSuccess: async () => {
toast.success("تسویه ثبت شد");
setOpen(false);
form.reset();
await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "settlements", partyType] });
},
onError: (error: Error) => toast.error(error.message),
});
if (!tenantId || listQ.isLoading || partiesQ.isLoading) return ;
if (listQ.error || partiesQ.error) return { listQ.refetch(); partiesQ.refetch(); }} />;
const partyNames = new Map((partiesQ.data ?? []).map((p) => [p.id, p.name]));
return (
setOpen(true)}>تسویه جدید} />
},
{ key: "party_id", header: "طرف حساب", render: (r) => partyNames.get(String(r.party_id)) || String(r.party_id) },
{ key: "settlement_type", header: "نوع" }, { key: "total_amount", header: "مبلغ", render: (r) => formatMoney(String(r.total_amount)) },
{ key: "status", header: "وضعیت", render: (r) => {String(r.status)} },
]}
rows={listQ.data as unknown as Record[]}
empty={
setOpen(true)}>
تسویه جدید
}
/>
}
/>
);
}
const documentSchema = z.object({
number: requiredText,
doc_date: requiredText,
party_name: z.string(),
party_id: z.string().optional(),
amount: requiredText,
description: z.string(),
});
const salesPostSchema = z.object({ source_document_id: requiredText, total_amount: requiredText, discount_amount: requiredText, tax_amount: requiredText, profile_id: z.string() });
export function SalesInvoiceAccountingPage() {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [createOpen, setCreateOpen] = useState(false);
const [profileOpen, setProfileOpen] = useState(false);
const [postOpen, setPostOpen] = useState(false);
const [preview, setPreview] = useState<{ is_balanced: boolean; total_debit: string; total_credit: string } | null>(null);
const docForm = useForm>({ resolver: zodResolver(documentSchema), defaultValues: { number: "", doc_date: today(), party_name: "", amount: "0", description: "" } });
const profileForm = useForm({ defaultValues: { name: "" } });
const postForm = useForm>({ resolver: zodResolver(salesPostSchema), defaultValues: { source_document_id: "", total_amount: "0", discount_amount: "0", tax_amount: "0", profile_id: "" } });
const docsQ = useQuery({ queryKey: ["accounting", tenantId, "ops-docs", "sales", "invoice"], queryFn: () => accountingApi.ops.listDocuments(tenantId!, "sales", "invoice"), enabled: !!tenantId });
const profilesQ = useQuery({ queryKey: ["accounting", tenantId, "sales-profiles"], queryFn: () => accountingApi.salesAccounting.listProfiles(tenantId!), enabled: !!tenantId });
const createDocM = useMutation({
mutationFn: (v: z.infer) => accountingApi.ops.createDocument(tenantId!, { module: "sales", doc_type: "invoice", status: "draft", ...v }),
onSuccess: async () => { toast.success("فاکتور ثبت شد"); setCreateOpen(false); docForm.reset(); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", "sales", "invoice"] }); },
onError: (e: Error) => toast.error(e.message),
});
const createProfileM = useMutation({
mutationFn: (v: { name: string }) => accountingApi.salesAccounting.createProfile(tenantId!, { name: v.name, document_type: "invoice" }),
onSuccess: async () => { toast.success("پروفایل ثبت شد"); setProfileOpen(false); profileForm.reset(); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "sales-profiles"] }); },
onError: (e: Error) => toast.error(e.message),
});
const previewM = useMutation({
mutationFn: (v: z.infer) => accountingApi.salesAccounting.preview(tenantId!, { document_type: "invoice", total_amount: v.total_amount, discount_amount: v.discount_amount, tax_amount: v.tax_amount, profile_id: v.profile_id || undefined }),
onSuccess: (data) => { setPreview(data); toast.success("پیشنمایش آماده شد"); },
onError: (e: Error) => toast.error(e.message),
});
const postM = useMutation({
mutationFn: (v: z.infer) => accountingApi.salesAccounting.post(tenantId!, { document_type: "invoice", ...v, profile_id: v.profile_id || undefined }),
onSuccess: (data) => { toast.success(`سند ${data.voucher_number} ثبت شد`); setPostOpen(false); setPreview(null); },
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || docsQ.isLoading || profilesQ.isLoading) return ;
if (docsQ.error || profilesQ.error) return { docsQ.refetch(); profilesQ.refetch(); }} />;
const openPosting = (row: Record) => {
postForm.reset({ source_document_id: String(row.id), total_amount: String(row.amount || "0"), discount_amount: "0", tax_amount: "0", profile_id: "" });
setPreview(null);
setPostOpen(true);
};
return (
>} />
},
{ key: "party_name", header: "مشتری" }, { key: "amount", header: "مبلغ", render: (r) => formatMoney(String(r.amount)) },
{ key: "status", header: "وضعیت", render: (r) => {String(r.status)} },
{ key: "actions", header: "عملیات", render: (r) => },
]}
rows={docsQ.data as unknown as Record[]}
empty={
setCreateOpen(true)}>
فاکتور جدید
}
/>
}
/>
setCreateOpen(false)} title="ثبت فاکتور فروش" form={docForm} mutation={createDocM} partyModule="sales" />
);
}
type DocumentForm = ReturnType>>;
type DocumentMutation = ReturnType>>;
function DocumentDialog({
open,
onClose,
title,
form,
mutation,
partyModule,
}: {
open: boolean;
onClose: () => void;
title: string;
form: DocumentForm;
mutation: DocumentMutation;
partyModule?: string;
}) {
return (
);
}
const purchasePostSchema = z.object({ source_document_id: requiredText, amount: requiredText, profile_id: z.string() });
export function PurchaseGoodsReceiptPage() {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [createOpen, setCreateOpen] = useState(false);
const [postOpen, setPostOpen] = useState(false);
const [preview, setPreview] = useState<{ is_balanced: boolean; total_debit: string } | null>(null);
const docForm = useForm>({ resolver: zodResolver(documentSchema), defaultValues: { number: "", doc_date: today(), party_name: "", amount: "0", description: "" } });
const postForm = useForm>({ resolver: zodResolver(purchasePostSchema), defaultValues: { source_document_id: "", amount: "0", profile_id: "" } });
const docsQ = useQuery({ queryKey: ["accounting", tenantId, "ops-docs", "purchase", "goods_receipt"], queryFn: () => accountingApi.ops.listDocuments(tenantId!, "purchase", "goods_receipt"), enabled: !!tenantId });
const profilesQ = useQuery({ queryKey: ["accounting", tenantId, "purchase-profiles"], queryFn: () => accountingApi.purchaseInventory.listProfiles(tenantId!), enabled: !!tenantId });
const createM = useMutation({
mutationFn: (v: z.infer) => accountingApi.ops.createDocument(tenantId!, { module: "purchase", doc_type: "goods_receipt", status: "draft", ...v }),
onSuccess: async () => { toast.success("رسید کالا ثبت شد"); setCreateOpen(false); docForm.reset(); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", "purchase", "goods_receipt"] }); },
onError: (e: Error) => toast.error(e.message),
});
const previewM = useMutation({
mutationFn: (v: z.infer) => accountingApi.purchaseInventory.previewGoodsReceipt(tenantId!, { ...v, profile_id: v.profile_id || undefined }),
onSuccess: (data) => { setPreview(data); toast.success("پیشنمایش آماده شد"); },
onError: (e: Error) => toast.error(e.message),
});
const postM = useMutation({
mutationFn: (v: z.infer) => accountingApi.purchaseInventory.postGoodsReceipt(tenantId!, { ...v, profile_id: v.profile_id || undefined }),
onSuccess: () => { toast.success("رسید به حسابداری ارسال شد"); setPostOpen(false); setPreview(null); },
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || docsQ.isLoading || profilesQ.isLoading) return ;
if (docsQ.error || profilesQ.error) return { docsQ.refetch(); profilesQ.refetch(); }} />;
const openPosting = (r: Record) => { postForm.reset({ source_document_id: String(r.id), amount: String(r.amount || "0"), profile_id: "" }); setPreview(null); setPostOpen(true); };
return (
setCreateOpen(true)}>رسید جدید} />
},
{ key: "party_name", header: "تأمینکننده" }, { key: "amount", header: "مبلغ", render: (r) => formatMoney(String(r.amount)) },
{ key: "status", header: "وضعیت", render: (r) => {String(r.status)} },
{ key: "actions", header: "عملیات", render: (r) => },
]}
rows={docsQ.data as unknown as Record[]}
empty={
setCreateOpen(true)}>
رسید جدید
}
/>
}
/>
setCreateOpen(false)} title="ثبت رسید کالا" form={docForm} mutation={createM} partyModule="purchase" />
);
}
const valuationSchema = z.object({ item_id: requiredText, quantity: requiredText, unit_cost: requiredText, warehouse_id: z.string() });
export function InventoryValuationPage() {
const { tenantId } = useTenantId();
const [result, setResult] = useState(null);
const form = useForm>({ resolver: zodResolver(valuationSchema), defaultValues: { item_id: "", quantity: "0", unit_cost: "0", warehouse_id: "" } });
const itemsQ = useQuery({ queryKey: ["accounting", tenantId, "ops-items"], queryFn: () => accountingApi.ops.listItems(tenantId!), enabled: !!tenantId });
const warehousesQ = useQuery({ queryKey: ["accounting", tenantId, "ops-warehouses"], queryFn: () => accountingApi.ops.listWarehouses(tenantId!), enabled: !!tenantId });
const valuationM = useMutation({
mutationFn: (v: z.infer) => accountingApi.purchaseInventory.valuation(tenantId!, { ...v, warehouse_id: v.warehouse_id || undefined }),
onSuccess: (data) => { setResult(data.total_value); toast.success("ارزشگذاری ثبت شد"); },
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || itemsQ.isLoading || warehousesQ.isLoading) return ;
if (itemsQ.error || warehousesQ.error) return { itemsQ.refetch(); warehousesQ.refetch(); }} />;
return (
);
}
export function MonitoringHealthPage() {
const { tenantId } = useTenantId();
const healthQ = useQuery({ queryKey: ["accounting", "health"], queryFn: () => accountingApi.health() });
const setupQ = useQuery({ queryKey: ["accounting", tenantId, "setup-status"], queryFn: () => accountingApi.setup.status(tenantId!), enabled: !!tenantId });
if (!tenantId || healthQ.isLoading || setupQ.isLoading) return ;
if (healthQ.error || setupQ.error) return { healthQ.refetch(); setupQ.refetch(); }} />;
return (
);
}
export function AssetSchedulePage() {
const { tenantId } = useTenantId();
const [selectedName, setSelectedName] = useState("");
const [schedule, setSchedule] = useState[] | null>(null);
const assetsQ = useQuery({ queryKey: ["accounting", tenantId, "assets"], queryFn: () => accountingApi.assets.list(tenantId!), enabled: !!tenantId });
const scheduleM = useMutation({
mutationFn: (id: string) => accountingApi.assets.schedule(tenantId!, id),
onSuccess: (data) => setSchedule(data.schedule as Record[]),
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || assetsQ.isLoading) return ;
if (assetsQ.error) return assetsQ.refetch()} />;
return (
{String(r.status)} },
{ key: "actions", header: "عملیات", render: (r) => },
]}
rows={assetsQ.data as unknown as Record[]}
empty={}
/>
);
}