- {NAV.map((item) => {
- const active = isActive(pathname, item.href);
- return (
-
- {item.label}
-
- );
- })}
+
+
+ {ACCOUNTING_NAV.flatMap((g) =>
+ g.href
+ ? [{ href: g.href, label: g.label }]
+ : (g.items ?? []).slice(0, 1).map((i) => ({ href: i.href, label: g.label }))
+ ).map((item) => (
+
+ {item.label}
+
+ ))}
{children}
-
);
diff --git a/frontend/components/accounting/BusinessDocsPage.tsx b/frontend/components/accounting/BusinessDocsPage.tsx
new file mode 100644
index 0000000..c4653ab
--- /dev/null
+++ b/frontend/components/accounting/BusinessDocsPage.tsx
@@ -0,0 +1,1047 @@
+"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 } from "lucide-react";
+import { accountingApi } from "@/lib/accounting-api";
+import { useTenantId } from "@/hooks/useTenantId";
+import { formatMoney } from "@/lib/utils";
+import {
+ PageHeader,
+ Button,
+ Dialog,
+ Input,
+ DataTable,
+ EmptyState,
+ LoadingState,
+ ErrorState,
+ Badge,
+ FormField,
+ MoneyInput,
+ DatePicker,
+ JalaliDateText,
+ ConfirmDialog,
+ Tabs,
+ Select,
+} from "@/components/ds";
+
+const schema = z.object({
+ number: z.string().min(1, "شماره الزامی است"),
+ doc_date: z.string().min(1, "تاریخ الزامی است"),
+ party_name: z.string().optional(),
+ amount: z.string().min(1, "مبلغ الزامی است"),
+ description: z.string().optional(),
+});
+
+type FormValues = z.infer
;
+
+export function BusinessDocsPage({
+ title,
+ description,
+ module,
+ docType,
+}: {
+ title: string;
+ description?: string;
+ module: string;
+ docType: string;
+}) {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const [confirmId, setConfirmId] = useState(null);
+
+ const listQ = useQuery({
+ queryKey: ["accounting", tenantId, "ops-docs", module, docType],
+ queryFn: () => accountingApi.ops.listDocuments(tenantId!, module, docType),
+ enabled: !!tenantId,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ number: "",
+ doc_date: new Date().toISOString().slice(0, 10),
+ party_name: "",
+ amount: "0",
+ description: "",
+ },
+ });
+
+ const createM = useMutation({
+ mutationFn: (d: FormValues) =>
+ accountingApi.ops.createDocument(tenantId!, {
+ module,
+ doc_type: docType,
+ number: d.number,
+ doc_date: d.doc_date,
+ party_name: d.party_name || undefined,
+ amount: d.amount,
+ description: d.description || undefined,
+ status: "draft",
+ }),
+ onSuccess: async () => {
+ toast.success("ثبت شد");
+ setOpen(false);
+ form.reset({
+ number: "",
+ doc_date: new Date().toISOString().slice(0, 10),
+ party_name: "",
+ amount: "0",
+ description: "",
+ });
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", module, docType] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ const confirmM = useMutation({
+ mutationFn: (id: string) => accountingApi.ops.confirmDocument(tenantId!, id),
+ onSuccess: async () => {
+ toast.success("تأیید شد");
+ setConfirmId(null);
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", module, docType] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || listQ.isLoading) return ;
+ if (listQ.error) {
+ return listQ.refetch()} />;
+ }
+
+ return (
+
+
setOpen(true)}>
+
+ ثبت جدید
+
+ }
+ />
+
+ ,
+ },
+ { key: "party_name", header: "طرف حساب" },
+ {
+ key: "amount",
+ header: "مبلغ",
+ render: (r) => formatMoney(String(r.amount ?? "0")),
+ },
+ {
+ key: "status",
+ header: "وضعیت",
+ render: (r) => (
+ {String(r.status)}
+ ),
+ },
+ {
+ key: "id",
+ header: "عملیات",
+ render: (r) =>
+ r.status === "draft" ? (
+
+ ) : (
+ "—"
+ ),
+ },
+ ]}
+ rows={(listQ.data ?? []) as unknown as Record[]}
+ empty={}
+ />
+
+
+
+ setConfirmId(null)}
+ onConfirm={() => confirmId && confirmM.mutate(confirmId)}
+ title="تأیید سند؟"
+ description="پس از تأیید، وضعیت به confirmed تغییر میکند."
+ confirmLabel="تأیید"
+ loading={confirmM.isPending}
+ />
+
+ );
+}
+
+export function BlockedModulePage({ title }: { title: string }) {
+ return (
+
+ );
+}
+
+export function InventoryItemsPage() {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const form = useForm({
+ defaultValues: { code: "", name: "", unit: "عدد", quantity_on_hand: "0", unit_cost: "0" },
+ });
+
+ const listQ = useQuery({
+ queryKey: ["accounting", tenantId, "ops-items"],
+ queryFn: () => accountingApi.ops.listItems(tenantId!),
+ enabled: !!tenantId,
+ });
+
+ const createM = useMutation({
+ mutationFn: (d: { code: string; name: string; unit: string; quantity_on_hand: string; unit_cost: string }) =>
+ accountingApi.ops.createItem(tenantId!, d),
+ onSuccess: async () => {
+ toast.success("کالا ثبت شد");
+ setOpen(false);
+ form.reset();
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-items"] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || listQ.isLoading) return ;
+ if (listQ.error) return listQ.refetch()} />;
+
+ return (
+
+
setOpen(true)}>
+
+ کالای جدید
+
+ }
+ />
+ String(r.quantity_on_hand ?? "0"),
+ },
+ {
+ key: "unit_cost",
+ header: "بهای واحد",
+ render: (r) => formatMoney(String(r.unit_cost ?? "0")),
+ },
+ {
+ key: "is_active",
+ header: "وضعیت",
+ render: (r) => {r.is_active ? "فعال" : "غیرفعال"},
+ },
+ ]}
+ rows={(listQ.data ?? []) as unknown as Record[]}
+ empty={}
+ />
+
+
+ );
+}
+
+export function WarehousesPage() {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const form = useForm({ defaultValues: { code: "", name: "" } });
+ const listQ = useQuery({
+ queryKey: ["accounting", tenantId, "ops-warehouses"],
+ queryFn: () => accountingApi.ops.listWarehouses(tenantId!),
+ enabled: !!tenantId,
+ });
+ const createM = useMutation({
+ mutationFn: (d: { code: string; name: string }) => accountingApi.ops.createWarehouse(tenantId!, d),
+ onSuccess: async () => {
+ toast.success("انبار ثبت شد");
+ setOpen(false);
+ form.reset();
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-warehouses"] });
+ },
+ onError: (e: Error) => toast.error(e.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={}
+ />
+
+
+ );
+}
+
+export function ChequesPage({
+ defaultIncoming,
+ initialOpen = false,
+}: {
+ defaultIncoming?: boolean;
+ initialOpen?: boolean;
+}) {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+ const [tab, setTab] = useState<"all" | "in" | "out" | "bounced">("all");
+ const [open, setOpen] = useState(initialOpen);
+ const form = useForm({
+ defaultValues: {
+ cheque_number: "",
+ amount: "0",
+ issue_date: new Date().toISOString().slice(0, 10),
+ due_date: "",
+ is_incoming: defaultIncoming !== false,
+ payee: "",
+ },
+ });
+
+ const listQ = useQuery({
+ queryKey: ["accounting", tenantId, "cheques"],
+ queryFn: () => accountingApi.treasury.listCheques(tenantId!),
+ enabled: !!tenantId,
+ });
+
+ const createM = useMutation({
+ mutationFn: (d: {
+ cheque_number: string;
+ amount: string;
+ issue_date: string;
+ due_date?: string;
+ is_incoming: boolean;
+ payee?: string;
+ }) =>
+ accountingApi.treasury.createCheque(tenantId!, {
+ ...d,
+ due_date: d.due_date || undefined,
+ status: d.is_incoming ? "received" : "issued",
+ }),
+ onSuccess: async () => {
+ toast.success("چک ثبت شد");
+ setOpen(false);
+ form.reset({
+ cheque_number: "",
+ amount: "0",
+ issue_date: new Date().toISOString().slice(0, 10),
+ due_date: "",
+ is_incoming: true,
+ payee: "",
+ });
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "cheques"] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || listQ.isLoading) return ;
+ if (listQ.error) return listQ.refetch()} />;
+
+ const rows = (listQ.data ?? []).filter((c) => {
+ if (tab === "in") return c.is_incoming;
+ if (tab === "out") return !c.is_incoming;
+ if (tab === "bounced") return c.status === "bounced";
+ return true;
+ });
+
+ return (
+
+
setOpen(true)}>
+
+ ثبت چک
+
+ }
+ />
+ setTab(v as typeof tab)}
+ items={[
+ { id: "all", label: "همه" },
+ { id: "in", label: "دریافتی" },
+ { id: "out", label: "پرداختی" },
+ { id: "bounced", label: "برگشتی" },
+ ]}
+ />
+
+ (r.due_date ? : "—"),
+ },
+ {
+ key: "issue_date",
+ header: "تاریخ",
+ render: (r) => ,
+ },
+ { key: "payee", header: "ذینفع" },
+ {
+ key: "status",
+ header: "وضعیت",
+ render: (r) => {String(r.status)},
+ },
+ {
+ key: "amount",
+ header: "مبلغ",
+ render: (r) => formatMoney(String(r.amount)),
+ },
+ ]}
+ rows={rows as unknown as Record[]}
+ empty={}
+ />
+
+
+
+ );
+}
+
+export function ReceiptsPaymentsPage() {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+ const [mode, setMode] = useState<"receipt" | "payment">("receipt");
+ const [open, setOpen] = useState(false);
+ const form = useForm({
+ defaultValues: {
+ number: "",
+ date: new Date().toISOString().slice(0, 10),
+ amount: "0",
+ payment_method: "cash",
+ description: "",
+ },
+ });
+
+ const receiptsQ = useQuery({
+ queryKey: ["accounting", tenantId, "receipts"],
+ queryFn: () => accountingApi.treasury.listReceipts(tenantId!),
+ enabled: !!tenantId,
+ });
+ const paymentsQ = useQuery({
+ queryKey: ["accounting", tenantId, "payments"],
+ queryFn: () => accountingApi.treasury.listPayments(tenantId!),
+ enabled: !!tenantId,
+ });
+
+ const createM = useMutation({
+ mutationFn: async (d: {
+ number: string;
+ date: string;
+ amount: string;
+ payment_method: string;
+ description?: string;
+ }) => {
+ if (mode === "receipt") {
+ return accountingApi.treasury.createReceipt(tenantId!, {
+ receipt_number: d.number,
+ receipt_date: d.date,
+ amount: d.amount,
+ payment_method: d.payment_method,
+ description: d.description,
+ });
+ }
+ return accountingApi.treasury.createPayment(tenantId!, {
+ payment_number: d.number,
+ payment_date: d.date,
+ amount: d.amount,
+ payment_method: d.payment_method,
+ description: d.description,
+ });
+ },
+ onSuccess: async () => {
+ toast.success("ثبت شد");
+ setOpen(false);
+ form.reset();
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "receipts"] });
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "payments"] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || receiptsQ.isLoading || paymentsQ.isLoading) return ;
+ if (receiptsQ.error || paymentsQ.error) {
+ return (
+ {
+ receiptsQ.refetch();
+ paymentsQ.refetch();
+ }}
+ />
+ );
+ }
+
+ const rows =
+ mode === "receipt"
+ ? (receiptsQ.data ?? []).map((r) => ({
+ id: r.id,
+ number: r.receipt_number,
+ date: r.receipt_date,
+ amount: r.amount,
+ payment_method: r.payment_method,
+ description: r.description,
+ }))
+ : (paymentsQ.data ?? []).map((p) => ({
+ id: p.id,
+ number: p.payment_number,
+ date: p.payment_date,
+ amount: p.amount,
+ payment_method: p.payment_method,
+ description: p.description,
+ }));
+
+ return (
+
+
setOpen(true)}>
+
+ ثبت جدید
+
+ }
+ />
+ setMode(v as typeof mode)}
+ items={[
+ { id: "receipt", label: "دریافتها" },
+ { id: "payment", label: "پرداختها" },
+ ]}
+ />
+
+ ,
+ },
+ { key: "payment_method", header: "روش" },
+ {
+ key: "amount",
+ header: "مبلغ",
+ render: (r) => formatMoney(String(r.amount)),
+ },
+ { key: "description", header: "توضیح" },
+ ]}
+ rows={rows as unknown as Record[]}
+ empty={}
+ />
+
+
+
+ );
+}
+
+export function TransfersPage() {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const cashBoxesQ = useQuery({
+ queryKey: ["accounting", tenantId, "cash-boxes"],
+ queryFn: () => accountingApi.treasury.listCashBoxes(tenantId!),
+ enabled: !!tenantId,
+ });
+ const bankAccountsQ = useQuery({
+ queryKey: ["accounting", tenantId, "bank-accounts"],
+ queryFn: () => accountingApi.treasury.listBankAccounts(tenantId!),
+ enabled: !!tenantId,
+ });
+ const listQ = useQuery({
+ queryKey: ["accounting", tenantId, "transfers"],
+ queryFn: () => accountingApi.treasury.listTransfers(tenantId!),
+ enabled: !!tenantId,
+ });
+ const form = useForm({
+ defaultValues: {
+ transfer_date: new Date().toISOString().slice(0, 10),
+ amount: "0",
+ from_key: "",
+ to_key: "",
+ description: "",
+ },
+ });
+
+ const createM = useMutation({
+ mutationFn: (d: {
+ transfer_date: string;
+ amount: string;
+ from_key: string;
+ to_key: string;
+ description?: string;
+ }) => {
+ const [from_type, from_id] = d.from_key.split(":");
+ const [to_type, to_id] = d.to_key.split(":");
+ return accountingApi.treasury.createTransfer(tenantId!, {
+ transfer_date: d.transfer_date,
+ amount: d.amount,
+ from_type,
+ from_id,
+ to_type,
+ to_id,
+ description: d.description,
+ });
+ },
+ onSuccess: async () => {
+ toast.success("انتقال ثبت شد");
+ setOpen(false);
+ form.reset();
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "transfers"] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || listQ.isLoading) return ;
+ if (listQ.error) return listQ.refetch()} />;
+
+ const accountOptions = [
+ ...(cashBoxesQ.data ?? []).map((c) => ({ value: `cash:${c.id}`, label: `صندوق: ${c.name}` })),
+ ...(bankAccountsQ.data ?? []).map((b) => ({
+ value: `bank:${b.id}`,
+ label: `بانک: ${b.account_number}`,
+ })),
+ ];
+
+ return (
+
+
setOpen(true)}>
+
+ انتقال جدید
+
+ }
+ />
+ ,
+ },
+ {
+ key: "amount",
+ header: "مبلغ",
+ render: (r) => formatMoney(String(r.amount)),
+ },
+ { key: "from_type", header: "از" },
+ { key: "to_type", header: "به" },
+ { key: "description", header: "توضیح" },
+ ]}
+ rows={(listQ.data ?? []) as unknown as Record[]}
+ empty={}
+ />
+
+
+ );
+}
+
+export function ReconciliationPage() {
+ const { tenantId } = useTenantId();
+ const qc = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const banksQ = useQuery({
+ queryKey: ["accounting", tenantId, "bank-accounts"],
+ queryFn: () => accountingApi.treasury.listBankAccounts(tenantId!),
+ enabled: !!tenantId,
+ });
+ const listQ = useQuery({
+ queryKey: ["accounting", tenantId, "reconciliations"],
+ queryFn: () => accountingApi.treasury.listReconciliations(tenantId!),
+ enabled: !!tenantId,
+ });
+ const form = useForm({
+ defaultValues: {
+ bank_account_id: "",
+ statement_date: new Date().toISOString().slice(0, 10),
+ statement_balance: "0",
+ book_balance: "0",
+ },
+ });
+ const createM = useMutation({
+ mutationFn: (d: {
+ bank_account_id: string;
+ statement_date: string;
+ statement_balance: string;
+ book_balance: string;
+ }) => accountingApi.treasury.createReconciliation(tenantId!, d),
+ onSuccess: async () => {
+ toast.success("تطبیق ثبت شد");
+ setOpen(false);
+ form.reset();
+ await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "reconciliations"] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ if (!tenantId || listQ.isLoading) return ;
+ if (listQ.error) return listQ.refetch()} />;
+
+ return (
+
+
setOpen(true)}>
+
+ تطبیق جدید
+
+ }
+ />
+ ,
+ },
+ {
+ key: "statement_balance",
+ header: "مانده صورتحساب",
+ render: (r) => formatMoney(String(r.statement_balance)),
+ },
+ {
+ key: "book_balance",
+ header: "مانده دفتر",
+ render: (r) => formatMoney(String(r.book_balance)),
+ },
+ {
+ key: "difference",
+ header: "اختلاف",
+ render: (r) => formatMoney(String(r.difference)),
+ },
+ {
+ key: "status",
+ header: "وضعیت",
+ render: (r) => {String(r.status)},
+ },
+ ]}
+ rows={(listQ.data ?? []) as unknown as Record[]}
+ empty={}
+ />
+
+
+ );
+}
diff --git a/frontend/lib/accounting-api.ts b/frontend/lib/accounting-api.ts
index 42eba2e..242d155 100644
--- a/frontend/lib/accounting-api.ts
+++ b/frontend/lib/accounting-api.ts
@@ -7,7 +7,11 @@
import { getStoredToken, getValidAccessToken, refreshSession } from "@/lib/auth";
const ACCOUNTING_BASE =
- process.env.NEXT_PUBLIC_ACCOUNTING_API_URL || "http://localhost:8002";
+ typeof window !== "undefined"
+ ? "/api/accounting"
+ : process.env.ACCOUNTING_SERVICE_URL ||
+ process.env.NEXT_PUBLIC_ACCOUNTING_API_URL ||
+ "http://localhost:8002";
export class AccountingApiError extends Error {
constructor(
@@ -32,18 +36,25 @@ async function request(path: string, options: RequestOptions): Promise {
if (token) headers.set("Authorization", `Bearer ${token}`);
}
+ const url = path.startsWith("http")
+ ? path
+ : `${ACCOUNTING_BASE.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
+
let res: Response;
try {
- res = await fetch(`${ACCOUNTING_BASE}${path}`, { ...init, headers });
- } catch {
- throw new Error(`اتصال به سرویس حسابداری برقرار نشد (${ACCOUNTING_BASE})`);
+ res = await fetch(url, { ...init, headers });
+ } catch (err) {
+ const detail = err instanceof Error ? err.message : "network_error";
+ throw new Error(
+ `اتصال به سرویس حسابداری برقرار نشد (${url}) — ${detail}`
+ );
}
if (res.status === 401 && auth) {
const refreshed = await refreshSession();
if (refreshed) {
headers.set("Authorization", `Bearer ${refreshed}`);
- res = await fetch(`${ACCOUNTING_BASE}${path}`, { ...init, headers });
+ res = await fetch(url, { ...init, headers });
}
}
@@ -281,9 +292,9 @@ export const accountingApi = {
},
accounts: {
- list: (tenantId: string, page = 1, pageSize = 200, chartId?: string) =>
+ list: (tenantId: string, page = 1, pageSize = 100, chartId?: string) =>
request(
- `/api/v1/accounts${qs({ page, page_size: pageSize, chart_id: chartId })}`,
+ `/api/v1/accounts${qs({ page, page_size: Math.min(pageSize, 500), chart_id: chartId })}`,
{ tenantId }
),
get: (tenantId: string, id: string) =>
@@ -686,6 +697,238 @@ export const accountingApi = {
method: "POST",
body: JSON.stringify(body),
}),
+ listCheques: (tenantId: string) =>
+ request<
+ {
+ id: string;
+ cheque_number: string;
+ amount: string;
+ issue_date: string;
+ due_date: string | null;
+ status: string;
+ is_incoming: boolean;
+ payee: string | null;
+ }[]
+ >("/api/v1/treasury/cheques", { tenantId }),
+ createCheque: (
+ tenantId: string,
+ body: {
+ cheque_number: string;
+ amount: string;
+ issue_date: string;
+ due_date?: string;
+ is_incoming?: boolean;
+ payee?: string;
+ bank_account_id?: string;
+ status?: string;
+ }
+ ) =>
+ request<{ id: string; cheque_number: string }>("/api/v1/treasury/cheques", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ listTransfers: (tenantId: string) =>
+ request<
+ {
+ id: string;
+ transfer_date: string;
+ amount: string;
+ from_type: string;
+ from_id: string;
+ to_type: string;
+ to_id: string;
+ description: string | null;
+ }[]
+ >("/api/v1/treasury/transfers", { tenantId }),
+ createTransfer: (
+ tenantId: string,
+ body: {
+ transfer_date: string;
+ amount: string;
+ from_type: string;
+ from_id: string;
+ to_type: string;
+ to_id: string;
+ description?: string;
+ }
+ ) =>
+ request<{ id: string }>("/api/v1/treasury/transfers", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ listReceipts: (tenantId: string) =>
+ request<
+ {
+ id: string;
+ receipt_number: string;
+ receipt_date: string;
+ amount: string;
+ payment_method: string;
+ description: string | null;
+ }[]
+ >("/api/v1/treasury/receipts", { tenantId }),
+ createReceipt: (
+ tenantId: string,
+ body: {
+ receipt_number: string;
+ receipt_date: string;
+ amount: string;
+ payment_method?: string;
+ cash_box_id?: string;
+ bank_account_id?: string;
+ description?: string;
+ }
+ ) =>
+ request<{ id: string }>("/api/v1/treasury/receipts", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ listPayments: (tenantId: string) =>
+ request<
+ {
+ id: string;
+ payment_number: string;
+ payment_date: string;
+ amount: string;
+ payment_method: string;
+ description: string | null;
+ }[]
+ >("/api/v1/treasury/payments", { tenantId }),
+ createPayment: (
+ tenantId: string,
+ body: {
+ payment_number: string;
+ payment_date: string;
+ amount: string;
+ payment_method?: string;
+ cash_box_id?: string;
+ bank_account_id?: string;
+ description?: string;
+ }
+ ) =>
+ request<{ id: string }>("/api/v1/treasury/payments", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ listReconciliations: (tenantId: string) =>
+ request<
+ {
+ id: string;
+ bank_account_id: string;
+ statement_date: string;
+ statement_balance: string;
+ book_balance: string;
+ difference: string;
+ status: string;
+ }[]
+ >("/api/v1/treasury/reconciliations", { tenantId }),
+ createReconciliation: (
+ tenantId: string,
+ body: {
+ bank_account_id: string;
+ statement_date: string;
+ statement_balance: string;
+ book_balance: string;
+ }
+ ) =>
+ request<{ id: string; difference: string }>("/api/v1/treasury/reconciliations", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ },
+
+ ops: {
+ listDocuments: (tenantId: string, module?: string, docType?: string) => {
+ const q = new URLSearchParams();
+ if (module) q.set("module", module);
+ if (docType) q.set("doc_type", docType);
+ const qs = q.toString();
+ return request<
+ {
+ id: string;
+ tenant_id: string;
+ module: string;
+ doc_type: string;
+ number: string;
+ doc_date: string;
+ party_name: string | null;
+ amount: string;
+ status: string;
+ description: string | null;
+ }[]
+ >(`/api/v1/ops/documents${qs ? `?${qs}` : ""}`, { tenantId });
+ },
+ createDocument: (
+ tenantId: string,
+ body: {
+ module: string;
+ doc_type: string;
+ number: string;
+ doc_date: string;
+ party_name?: string;
+ party_id?: string;
+ amount?: string;
+ status?: string;
+ description?: string;
+ meta_json?: string;
+ }
+ ) =>
+ request<{ id: string }>("/api/v1/ops/documents", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ confirmDocument: (tenantId: string, id: string) =>
+ request<{ id: string; status: string }>(`/api/v1/ops/documents/${id}/confirm`, {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify({}),
+ }),
+ listItems: (tenantId: string) =>
+ request<
+ {
+ id: string;
+ code: string;
+ name: string;
+ item_type: string;
+ unit: string;
+ quantity_on_hand: string;
+ unit_cost: string;
+ is_active: boolean;
+ }[]
+ >("/api/v1/ops/items", { tenantId }),
+ createItem: (
+ tenantId: string,
+ body: {
+ code: string;
+ name: string;
+ item_type?: string;
+ unit?: string;
+ quantity_on_hand?: string;
+ unit_cost?: string;
+ }
+ ) =>
+ request<{ id: string }>("/api/v1/ops/items", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+ listWarehouses: (tenantId: string) =>
+ request<{ id: string; code: string; name: string; is_active: boolean }[]>(
+ "/api/v1/ops/warehouses",
+ { tenantId }
+ ),
+ createWarehouse: (tenantId: string, body: { code: string; name: string }) =>
+ request<{ id: string }>("/api/v1/ops/warehouses", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
},
customers: {
@@ -884,5 +1127,19 @@ export const accountingApi = {
request<
{ id: string; action: string; resource_type: string; actor_user_id: string; created_at: string }[]
>("/api/v1/compliance/audit-records", { tenantId }),
+ listPolicies: (tenantId: string) =>
+ request<{ id: string; code: string; name: string; policy_type: string; is_active: boolean }[]>(
+ "/api/v1/compliance/policies",
+ { tenantId }
+ ),
+ createPolicy: (
+ tenantId: string,
+ body: { code: string; name: string; policy_type: string; rules_config: string; country_code?: string }
+ ) =>
+ request<{ id: string }>("/api/v1/compliance/policies", {
+ tenantId,
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
},
};
diff --git a/frontend/lib/accounting-nav.ts b/frontend/lib/accounting-nav.ts
new file mode 100644
index 0000000..e1e02b1
--- /dev/null
+++ b/frontend/lib/accounting-nav.ts
@@ -0,0 +1,311 @@
+/**
+ * Accounting nested navigation — mirrors enterprise sidebar design (phases 5.1–5.12).
+ * AI module stays blocked until provider is Active.
+ */
+import type { LucideIcon } from "lucide-react";
+import {
+ LayoutDashboard,
+ Wallet,
+ ShoppingCart,
+ ShoppingBag,
+ FileText,
+ Briefcase,
+ Building2,
+ Package,
+ BookOpen,
+ FileBarChart2,
+ Target,
+ ShieldCheck,
+ Sparkles,
+ Settings,
+ Cable,
+ FolderOpen,
+ Activity,
+ Scale,
+ Wrench,
+} from "lucide-react";
+
+export type AccountingNavItem = {
+ href: string;
+ label: string;
+ blocked?: boolean;
+};
+
+export type AccountingNavGroup = {
+ id: string;
+ label: string;
+ icon: LucideIcon;
+ href?: string;
+ items?: AccountingNavItem[];
+ blocked?: boolean;
+};
+
+export const ACCOUNTING_NAV: AccountingNavGroup[] = [
+ {
+ id: "dashboard",
+ label: "داشبورد",
+ icon: LayoutDashboard,
+ href: "/accounting",
+ },
+ {
+ id: "treasury",
+ label: "عملیات خزانه",
+ icon: Wallet,
+ items: [
+ { href: "/accounting/treasury/receipts-payments", label: "دریافت و پرداخت" },
+ { href: "/accounting/treasury/cash-boxes", label: "صندوقها" },
+ { href: "/accounting/treasury/banks", label: "بانکها" },
+ { href: "/accounting/treasury/transfers", label: "انتقال وجه" },
+ { href: "/accounting/treasury/reconciliation", label: "تطبیق بانکی" },
+ { href: "/accounting/treasury/cheques", label: "چکها" },
+ { href: "/accounting/treasury/cheques/new", label: "ثبت چک" },
+ { href: "/accounting/treasury/guarantees", label: "ضمانتها" },
+ { href: "/accounting/treasury/facilities", label: "تسهیلات و اعتبارات" },
+ ],
+ },
+ {
+ id: "purchase",
+ label: "عملیات خرید",
+ icon: ShoppingCart,
+ items: [
+ { href: "/accounting/purchase/requests", label: "درخواست خرید" },
+ { href: "/accounting/purchase/orders", label: "سفارش خرید" },
+ { href: "/accounting/purchase/goods-receipts", label: "رسید کالا" },
+ { href: "/accounting/purchase/invoices", label: "فاکتور خرید" },
+ { href: "/accounting/purchase/returns", label: "مرجوعی خرید" },
+ { href: "/accounting/purchase/prepayments", label: "پیشپرداختها" },
+ { href: "/accounting/purchase/settlements", label: "تسویه با تأمینکننده" },
+ { href: "/accounting/suppliers", label: "فاکتورهای تأمینکننده" },
+ ],
+ },
+ {
+ id: "sales",
+ label: "عملیات فروش",
+ icon: ShoppingBag,
+ items: [
+ { href: "/accounting/sales/opportunities", label: "فرصتهای فروش" },
+ { href: "/accounting/sales/proformas", label: "پیشفاکتور" },
+ { href: "/accounting/sales/orders", label: "سفارش فروش" },
+ { href: "/accounting/sales/invoices", label: "فاکتور فروش" },
+ { href: "/accounting/sales/returns", label: "مرجوعی فروش" },
+ { href: "/accounting/sales/pre-receipts", label: "پیشدریافتها" },
+ { href: "/accounting/sales/settlements", label: "تسویه حساب مشتریان" },
+ { href: "/accounting/customers", label: "فاکتورهای مشتریان" },
+ ],
+ },
+ {
+ id: "vouchers",
+ label: "اسناد حسابداری",
+ icon: FileText,
+ items: [
+ { href: "/accounting/vouchers/new", label: "صدور سند" },
+ { href: "/accounting/vouchers", label: "لیست اسناد" },
+ { href: "/accounting/vouchers?status=draft", label: "اسناد پیشنویس" },
+ { href: "/accounting/vouchers/recurring", label: "اسناد تکراری" },
+ { href: "/accounting/vouchers/adjustments", label: "اسناد اصلاحی" },
+ { href: "/accounting/vouchers?status=reversed", label: "اسناد برگشتی" },
+ { href: "/accounting/vouchers?status=posted", label: "تأیید و ثبت اسناد" },
+ { href: "/accounting/vouchers/templates", label: "الگوی سند" },
+ ],
+ },
+ {
+ id: "payroll",
+ label: "حقوق و دستمزد",
+ icon: Briefcase,
+ items: [
+ { href: "/accounting/payroll/employees", label: "کارمندان" },
+ { href: "/accounting/payroll/contracts", label: "تعریف قرارداد" },
+ { href: "/accounting/payroll/structures", label: "ساختار حقوق" },
+ { href: "/accounting/payroll/items", label: "اقلام حقوق و مزایا" },
+ { href: "/accounting/payroll/calculate", label: "محاسبه حقوق" },
+ { href: "/accounting/payroll/payments", label: "پرداخت حقوق" },
+ { href: "/accounting/payroll/allocation", label: "تسهیم هزینه حقوق" },
+ { href: "/accounting/payroll/reports", label: "گزارشهای حقوق" },
+ ],
+ },
+ {
+ id: "assets",
+ label: "داراییهای ثابت",
+ icon: Building2,
+ items: [
+ { href: "/accounting/assets", label: "ثبت دارایی" },
+ { href: "/accounting/assets/dashboard", label: "داشبورد دارایی" },
+ { href: "/accounting/assets/depreciate", label: "محاسبه استهلاک" },
+ { href: "/accounting/assets/schedule", label: "برنامه استهلاک" },
+ { href: "/accounting/assets/transfers", label: "انتقال دارایی" },
+ { href: "/accounting/assets/accumulated", label: "استهلاک انباشته" },
+ { href: "/accounting/assets/valuation", label: "ارزشگذاری دارایی" },
+ { href: "/accounting/assets/dispose", label: "معرفی و فروش دارایی" },
+ ],
+ },
+ {
+ id: "inventory",
+ label: "حسابداری انبار",
+ icon: Package,
+ items: [
+ { href: "/accounting/inventory/items", label: "کارت کالا / خدمت" },
+ { href: "/accounting/inventory/movements", label: "گردش کالا" },
+ { href: "/accounting/inventory/valuation", label: "ارزشگذاری موجودی" },
+ { href: "/accounting/inventory/settings", label: "تنظیمات انبار" },
+ { href: "/accounting/inventory/transfers", label: "انتقال انبار" },
+ { href: "/accounting/inventory/receipts-issues", label: "رسید ورود و خروج" },
+ { href: "/accounting/inventory/adjustments", label: "تعدیل موجودی" },
+ { href: "/accounting/inventory/reports", label: "گزارشهای انبار" },
+ ],
+ },
+ {
+ id: "coa",
+ label: "حسابها و سرفصلها",
+ icon: BookOpen,
+ items: [
+ { href: "/accounting/chart-of-accounts", label: "دستهبندی حسابها" },
+ { href: "/accounting/chart-of-accounts?level=ledger", label: "حسابهای کل" },
+ { href: "/accounting/chart-of-accounts?level=subsidiary", label: "حسابهای معین" },
+ { href: "/accounting/chart-of-accounts?level=detail", label: "حسابهای تفصیلی" },
+ { href: "/accounting/cost-centers", label: "مراکز هزینه" },
+ { href: "/accounting/projects", label: "پروژهها" },
+ { href: "/accounting/chart-of-accounts", label: "تنظیمات ساختار" },
+ { href: "/accounting/ledger", label: "دفتر کل" },
+ ],
+ },
+ {
+ id: "reports",
+ label: "گزارشات مالی",
+ icon: FileBarChart2,
+ items: [
+ { href: "/accounting/reports?type=balance-sheet", label: "ترازنامه" },
+ { href: "/accounting/reports?type=income-statement", label: "سود و زیان" },
+ { href: "/accounting/reports?type=cash-flow", label: "جریان وجوه نقد" },
+ { href: "/accounting/reports?type=equity", label: "تغییرات حقوق صاحبان سهام" },
+ { href: "/accounting/reports?type=trial-balance", label: "تراز آزمایشی" },
+ { href: "/accounting/reports?type=subsidiary", label: "گزارش معین" },
+ { href: "/accounting/reports?type=detail", label: "گزارش تفصیلی" },
+ { href: "/accounting/reports?type=analytical", label: "گزارش تحلیلی" },
+ ],
+ },
+ {
+ id: "budget",
+ label: "بودجه و کنترل مالی",
+ icon: Target,
+ items: [
+ { href: "/accounting/budget/definitions", label: "تعریف بودجه" },
+ { href: "/accounting/budget/operational", label: "بودجهریزی عملیاتی" },
+ { href: "/accounting/budget/control", label: "کنترل بودجه" },
+ { href: "/accounting/budget/variance", label: "تحلیل انحراف بودجه" },
+ { href: "/accounting/budget/performance", label: "مقایسه عملکرد" },
+ { href: "/accounting/budget/forecast", label: "پیشبینی مالی" },
+ { href: "/accounting/budget/scenarios", label: "سناریوهای مالی" },
+ { href: "/accounting/budget/reports", label: "گزارش بودجه" },
+ ],
+ },
+ {
+ id: "compliance",
+ label: "کنترل و انطباق",
+ icon: ShieldCheck,
+ items: [
+ { href: "/accounting/compliance/policies", label: "سیاستها و قوانین" },
+ { href: "/accounting/compliance/approvals", label: "گردش تأیید" },
+ { href: "/accounting/compliance/sod", label: "تفکیک وظایف (SoD)" },
+ { href: "/accounting/compliance/controls", label: "کنترلهای داخلی" },
+ { href: "/accounting/compliance/risks", label: "مدیریت ریسک" },
+ { href: "/accounting/audit", label: "انطباق و حسابرسی" },
+ { href: "/accounting/compliance/records", label: "اسناد و سوابق" },
+ { href: "/accounting/compliance/violations", label: "گزارش تخلفات" },
+ ],
+ },
+ {
+ id: "ai",
+ label: "هوش مصنوعی",
+ icon: Sparkles,
+ blocked: true,
+ items: [
+ { href: "/accounting/ai", label: "دستیار حسابداری", blocked: true },
+ { href: "/accounting/ai/qa", label: "پرسش و پاسخ", blocked: true },
+ { href: "/accounting/ai/analytics", label: "تحلیل هوشمند", blocked: true },
+ { href: "/accounting/ai/forecasts", label: "پیشبینیها", blocked: true },
+ { href: "/accounting/ai/anomalies", label: "کشف ناهنجاری", blocked: true },
+ { href: "/accounting/ai/reconciliation", label: "تطبیق هوشمند", blocked: true },
+ { href: "/accounting/ai/documents", label: "تحلیل اسناد", blocked: true },
+ { href: "/accounting/ai/recommendations", label: "پیشنهادهای هوشمند", blocked: true },
+ ],
+ },
+ {
+ id: "settings",
+ label: "تنظیمات سیستم",
+ icon: Settings,
+ items: [
+ { href: "/accounting/settings/company", label: "اطلاعات شرکت" },
+ { href: "/accounting/fiscal", label: "دورههای مالی" },
+ { href: "/accounting/currencies", label: "ارزها و نرخها" },
+ { href: "/accounting/settings", label: "تنظیمات حسابداری" },
+ { href: "/accounting/settings/tax", label: "تنظیمات مالیاتی" },
+ { href: "/accounting/settings/general", label: "تنظیمات عمومی" },
+ { href: "/dashboard/settings", label: "کاربران و دسترسی" },
+ { href: "/accounting/settings/backup", label: "پشتیبانگیری" },
+ ],
+ },
+ {
+ id: "integration",
+ label: "یکپارچهسازی",
+ icon: Cable,
+ items: [
+ { href: "/accounting/integration/bank", label: "اتصال بانک" },
+ { href: "/accounting/integration/gateway", label: "درگاه پرداخت" },
+ { href: "/accounting/integration/crm", label: "اتصال CRM" },
+ { href: "/accounting/integration/inventory", label: "اتصال انبار" },
+ { href: "/accounting/integration/payroll", label: "اتصال حقوق" },
+ { href: "/accounting/integration/webhooks", label: "وبسرویسها" },
+ { href: "/accounting/integration/import-export", label: "ورود / خروج داده" },
+ { href: "/accounting/integration/reports", label: "گزارش یکپارچگی" },
+ ],
+ },
+ {
+ id: "documents",
+ label: "مدیریت اسناد",
+ icon: FolderOpen,
+ items: [
+ { href: "/accounting/documents/financial", label: "اسناد مالی" },
+ { href: "/accounting/documents/purchase", label: "اسناد خرید" },
+ { href: "/accounting/documents/sales", label: "اسناد فروش" },
+ { href: "/accounting/documents/hr", label: "اسناد پرسنلی" },
+ { href: "/accounting/documents/categories", label: "دستهبندی" },
+ { href: "/accounting/documents/search", label: "جستجوی اسناد" },
+ { href: "/accounting/documents/versions", label: "نسخههای سند" },
+ { href: "/accounting/documents/retention", label: "نگهداری" },
+ ],
+ },
+ {
+ id: "monitoring",
+ label: "مانیتورینگ و نظارت",
+ icon: Activity,
+ items: [
+ { href: "/accounting/monitoring", label: "داشبورد نظارتی" },
+ { href: "/accounting/monitoring/recent", label: "عملیات اخیر" },
+ { href: "/accounting/monitoring/alerts", label: "هشدارها" },
+ { href: "/accounting/monitoring/activity", label: "فعالیت کاربران" },
+ { href: "/accounting/audit", label: "لاگ سیستم" },
+ { href: "/accounting/monitoring/health", label: "وضعیت سرویس" },
+ { href: "/accounting/monitoring/kpis", label: "شاخصهای کلیدی" },
+ { href: "/accounting/monitoring/performance", label: "عملکرد سیستم" },
+ ],
+ },
+ {
+ id: "setup",
+ label: "راهاندازی",
+ icon: Wrench,
+ href: "/accounting/setup",
+ },
+];
+
+export const ACCOUNTING_QUICK_BAR: AccountingNavItem[] = [
+ { href: "/accounting/vouchers/new", label: "صدور سند" },
+ { href: "/accounting/treasury/receipts-payments", label: "دریافت/پرداخت" },
+ { href: "/accounting/sales/invoices", label: "فاکتور فروش" },
+ { href: "/accounting/treasury/cheques/new", label: "ثبت چک" },
+ { href: "/accounting/reports", label: "گزارشات" },
+ { href: "/accounting", label: "داشبورد" },
+];
+
+export const LEGACY_SHORTCUTS = [
+ { href: "/accounting/ledger", label: "دفتر کل", icon: Scale },
+];
diff --git a/scripts/generate_accounting_pages.py b/scripts/generate_accounting_pages.py
new file mode 100644
index 0000000..80859f7
--- /dev/null
+++ b/scripts/generate_accounting_pages.py
@@ -0,0 +1,237 @@
+"""Generate Accounting submenu page.tsx files from nav map."""
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1] / "frontend" / "app" / "accounting"
+
+# (relative_path_without_page, kind, kwargs)
+# kinds: biz, blocked, cheques, cheques_new, receipts, transfers, recon,
+# items, warehouses, redirect, reexport
+
+PAGES = [
+ # treasury
+ ("treasury/receipts-payments", "receipts", {}),
+ ("treasury/cash-boxes", "redirect", {"to": "/accounting/treasury"}),
+ ("treasury/banks", "redirect", {"to": "/accounting/treasury"}),
+ ("treasury/transfers", "transfers", {}),
+ ("treasury/reconciliation", "recon", {}),
+ ("treasury/cheques", "cheques", {}),
+ ("treasury/cheques/new", "cheques_new", {}),
+ ("treasury/guarantees", "biz", {"title": "ضمانتها", "module": "treasury", "docType": "guarantee"}),
+ ("treasury/facilities", "biz", {"title": "تسهیلات و اعتبارات", "module": "treasury", "docType": "facility"}),
+ # purchase
+ ("purchase/requests", "biz", {"title": "درخواست خرید", "module": "purchase", "docType": "request"}),
+ ("purchase/orders", "biz", {"title": "سفارش خرید", "module": "purchase", "docType": "order"}),
+ ("purchase/goods-receipts", "biz", {"title": "رسید کالا", "module": "purchase", "docType": "goods_receipt"}),
+ ("purchase/invoices", "biz", {"title": "فاکتور خرید", "module": "purchase", "docType": "invoice"}),
+ ("purchase/returns", "biz", {"title": "مرجوعی خرید", "module": "purchase", "docType": "return"}),
+ ("purchase/prepayments", "biz", {"title": "پیشپرداختها", "module": "purchase", "docType": "prepayment"}),
+ ("purchase/settlements", "biz", {"title": "تسویه با تأمینکننده", "module": "purchase", "docType": "settlement"}),
+ # sales
+ ("sales/opportunities", "biz", {"title": "فرصتهای فروش", "module": "sales", "docType": "opportunity"}),
+ ("sales/proformas", "biz", {"title": "پیشفاکتور", "module": "sales", "docType": "proforma"}),
+ ("sales/orders", "biz", {"title": "سفارش فروش", "module": "sales", "docType": "order"}),
+ ("sales/invoices", "biz", {"title": "فاکتور فروش", "module": "sales", "docType": "invoice"}),
+ ("sales/returns", "biz", {"title": "مرجوعی فروش", "module": "sales", "docType": "return"}),
+ ("sales/pre-receipts", "biz", {"title": "پیشدریافتها", "module": "sales", "docType": "pre_receipt"}),
+ ("sales/settlements", "biz", {"title": "تسویه حساب مشتریان", "module": "sales", "docType": "settlement"}),
+ # vouchers extras
+ ("vouchers/recurring", "biz", {"title": "اسناد تکراری", "module": "vouchers", "docType": "recurring"}),
+ ("vouchers/adjustments", "biz", {"title": "اسناد اصلاحی", "module": "vouchers", "docType": "adjustment"}),
+ ("vouchers/templates", "biz", {"title": "الگوی سند", "module": "vouchers", "docType": "template"}),
+ # payroll
+ ("payroll/employees", "biz", {"title": "کارمندان", "module": "payroll", "docType": "employee"}),
+ ("payroll/contracts", "biz", {"title": "تعریف قرارداد", "module": "payroll", "docType": "contract"}),
+ ("payroll/structures", "biz", {"title": "ساختار حقوق", "module": "payroll", "docType": "structure"}),
+ ("payroll/items", "biz", {"title": "اقلام حقوق و مزایا", "module": "payroll", "docType": "pay_item"}),
+ ("payroll/calculate", "redirect", {"to": "/accounting/payroll"}),
+ ("payroll/payments", "biz", {"title": "پرداخت حقوق", "module": "payroll", "docType": "payment"}),
+ ("payroll/allocation", "biz", {"title": "تسهیم هزینه حقوق", "module": "payroll", "docType": "allocation"}),
+ ("payroll/reports", "biz", {"title": "گزارشهای حقوق", "module": "payroll", "docType": "report"}),
+ # assets
+ ("assets/dashboard", "redirect", {"to": "/accounting/assets"}),
+ ("assets/depreciate", "redirect", {"to": "/accounting/assets"}),
+ ("assets/schedule", "biz", {"title": "برنامه استهلاک", "module": "assets", "docType": "schedule"}),
+ ("assets/transfers", "biz", {"title": "انتقال دارایی", "module": "assets", "docType": "transfer"}),
+ ("assets/accumulated", "biz", {"title": "استهلاک انباشته", "module": "assets", "docType": "accumulated"}),
+ ("assets/valuation", "biz", {"title": "ارزشگذاری دارایی", "module": "assets", "docType": "valuation"}),
+ ("assets/dispose", "biz", {"title": "معرفی و فروش دارایی", "module": "assets", "docType": "dispose"}),
+ # inventory
+ ("inventory/items", "items", {}),
+ ("inventory/movements", "biz", {"title": "گردش کالا", "module": "inventory", "docType": "movement"}),
+ ("inventory/valuation", "biz", {"title": "ارزشگذاری موجودی", "module": "inventory", "docType": "valuation"}),
+ ("inventory/settings", "warehouses", {}),
+ ("inventory/transfers", "biz", {"title": "انتقال انبار", "module": "inventory", "docType": "transfer"}),
+ ("inventory/receipts-issues", "biz", {"title": "رسید ورود و خروج", "module": "inventory", "docType": "receipt_issue"}),
+ ("inventory/adjustments", "biz", {"title": "تعدیل موجودی", "module": "inventory", "docType": "adjustment"}),
+ ("inventory/reports", "biz", {"title": "گزارشهای انبار", "module": "inventory", "docType": "report"}),
+ # budget
+ ("budget/definitions", "biz", {"title": "تعریف بودجه", "module": "budget", "docType": "definition"}),
+ ("budget/operational", "biz", {"title": "بودجهریزی عملیاتی", "module": "budget", "docType": "operational"}),
+ ("budget/control", "biz", {"title": "کنترل بودجه", "module": "budget", "docType": "control"}),
+ ("budget/variance", "biz", {"title": "تحلیل انحراف بودجه", "module": "budget", "docType": "variance"}),
+ ("budget/performance", "biz", {"title": "مقایسه عملکرد", "module": "budget", "docType": "performance"}),
+ ("budget/forecast", "biz", {"title": "پیشبینی مالی", "module": "budget", "docType": "forecast"}),
+ ("budget/scenarios", "biz", {"title": "سناریوهای مالی", "module": "budget", "docType": "scenario"}),
+ ("budget/reports", "biz", {"title": "گزارش بودجه", "module": "budget", "docType": "report"}),
+ # compliance
+ ("compliance/policies", "biz", {"title": "سیاستها و قوانین", "module": "compliance", "docType": "policy"}),
+ ("compliance/approvals", "biz", {"title": "گردش تأیید", "module": "compliance", "docType": "approval"}),
+ ("compliance/sod", "biz", {"title": "تفکیک وظایف (SoD)", "module": "compliance", "docType": "sod"}),
+ ("compliance/controls", "biz", {"title": "کنترلهای داخلی", "module": "compliance", "docType": "control"}),
+ ("compliance/risks", "biz", {"title": "مدیریت ریسک", "module": "compliance", "docType": "risk"}),
+ ("compliance/records", "biz", {"title": "اسناد و سوابق", "module": "compliance", "docType": "record"}),
+ ("compliance/violations", "biz", {"title": "گزارش تخلفات", "module": "compliance", "docType": "violation"}),
+ # AI blocked
+ ("ai", "blocked", {"title": "دستیار حسابداری"}),
+ ("ai/qa", "blocked", {"title": "پرسش و پاسخ"}),
+ ("ai/analytics", "blocked", {"title": "تحلیل هوشمند"}),
+ ("ai/forecasts", "blocked", {"title": "پیشبینیها"}),
+ ("ai/anomalies", "blocked", {"title": "کشف ناهنجاری"}),
+ ("ai/reconciliation", "blocked", {"title": "تطبیق هوشمند"}),
+ ("ai/documents", "blocked", {"title": "تحلیل اسناد"}),
+ ("ai/recommendations", "blocked", {"title": "پیشنهادهای هوشمند"}),
+ # settings
+ ("settings/company", "biz", {"title": "اطلاعات شرکت", "module": "settings", "docType": "company"}),
+ ("settings/tax", "biz", {"title": "تنظیمات مالیاتی", "module": "settings", "docType": "tax"}),
+ ("settings/general", "biz", {"title": "تنظیمات عمومی", "module": "settings", "docType": "general"}),
+ ("settings/backup", "biz", {"title": "پشتیبانگیری", "module": "settings", "docType": "backup"}),
+ # integration
+ ("integration/bank", "biz", {"title": "اتصال بانک", "module": "integration", "docType": "bank"}),
+ ("integration/gateway", "biz", {"title": "درگاه پرداخت", "module": "integration", "docType": "gateway"}),
+ ("integration/crm", "biz", {"title": "اتصال CRM", "module": "integration", "docType": "crm"}),
+ ("integration/inventory", "biz", {"title": "اتصال انبار", "module": "integration", "docType": "inventory"}),
+ ("integration/payroll", "biz", {"title": "اتصال حقوق", "module": "integration", "docType": "payroll"}),
+ ("integration/webhooks", "biz", {"title": "وبسرویسها", "module": "integration", "docType": "webhook"}),
+ ("integration/import-export", "biz", {"title": "ورود / خروج داده", "module": "integration", "docType": "import_export"}),
+ ("integration/reports", "biz", {"title": "گزارش یکپارچگی", "module": "integration", "docType": "report"}),
+ # documents
+ ("documents/financial", "biz", {"title": "اسناد مالی", "module": "documents", "docType": "financial"}),
+ ("documents/purchase", "biz", {"title": "اسناد خرید", "module": "documents", "docType": "purchase"}),
+ ("documents/sales", "biz", {"title": "اسناد فروش", "module": "documents", "docType": "sales"}),
+ ("documents/hr", "biz", {"title": "اسناد پرسنلی", "module": "documents", "docType": "hr"}),
+ ("documents/categories", "biz", {"title": "دستهبندی اسناد", "module": "documents", "docType": "category"}),
+ ("documents/search", "biz", {"title": "جستجوی اسناد", "module": "documents", "docType": "search"}),
+ ("documents/versions", "biz", {"title": "نسخههای سند", "module": "documents", "docType": "version"}),
+ ("documents/retention", "biz", {"title": "نگهداری اسناد", "module": "documents", "docType": "retention"}),
+ # monitoring
+ ("monitoring", "biz", {"title": "داشبورد نظارتی", "module": "monitoring", "docType": "dashboard"}),
+ ("monitoring/recent", "biz", {"title": "عملیات اخیر", "module": "monitoring", "docType": "recent"}),
+ ("monitoring/alerts", "biz", {"title": "هشدارها", "module": "monitoring", "docType": "alert"}),
+ ("monitoring/activity", "biz", {"title": "فعالیت کاربران", "module": "monitoring", "docType": "activity"}),
+ ("monitoring/health", "biz", {"title": "وضعیت سرویس", "module": "monitoring", "docType": "health"}),
+ ("monitoring/kpis", "biz", {"title": "شاخصهای کلیدی", "module": "monitoring", "docType": "kpi"}),
+ ("monitoring/performance", "biz", {"title": "عملکرد سیستم", "module": "monitoring", "docType": "performance"}),
+]
+
+
+def render(kind: str, kw: dict) -> str:
+ if kind == "biz":
+ return f'''"use client";
+
+import {{ BusinessDocsPage }} from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {{
+ return (
+
+ );
+}}
+'''
+ if kind == "blocked":
+ return f'''"use client";
+
+import {{ BlockedModulePage }} from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {{
+ return ;
+}}
+'''
+ if kind == "cheques":
+ return '''"use client";
+
+import { ChequesPage } from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {
+ return ;
+}
+'''
+ if kind == "cheques_new":
+ return '''"use client";
+
+import { ChequesPage } from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {
+ return ;
+}
+'''
+ if kind == "receipts":
+ return '''"use client";
+
+import { ReceiptsPaymentsPage } from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {
+ return ;
+}
+'''
+ if kind == "transfers":
+ return '''"use client";
+
+import { TransfersPage } from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {
+ return ;
+}
+'''
+ if kind == "recon":
+ return '''"use client";
+
+import { ReconciliationPage } from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {
+ return ;
+}
+'''
+ if kind == "items":
+ return '''"use client";
+
+import { InventoryItemsPage } from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {
+ return ;
+}
+'''
+ if kind == "warehouses":
+ return '''"use client";
+
+import { WarehousesPage } from "@/components/accounting/BusinessDocsPage";
+
+export default function Page() {
+ return ;
+}
+'''
+ if kind == "redirect":
+ return f'''import {{ redirect }} from "next/navigation";
+
+export default function Page() {{
+ redirect("{kw["to"]}");
+}}
+'''
+ raise ValueError(kind)
+
+
+def main() -> None:
+ created = 0
+ for rel, kind, kw in PAGES:
+ path = ROOT / rel / "page.tsx"
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(render(kind, kw), encoding="utf-8")
+ created += 1
+ print(path.relative_to(ROOT.parent.parent))
+ print(f"Created/updated {created} pages")
+
+
+if __name__ == "__main__":
+ main()