diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 07d588b..b5cc80a 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -2,7 +2,7 @@ "extends": "next/core-web-vitals", "overrides": [ { - "files": ["app/accounting/**/*", "components/accounting/**/*"], + "files": ["app/accounting/**/*", "src/modules/accounting/**/*"], "rules": { "no-restricted-imports": [ "error", @@ -13,7 +13,9 @@ "@/components/beauty/**", "@/components/healthcare/**", "@/modules/beauty/**", - "@/modules/healthcare/**" + "@/modules/healthcare/**", + "@/src/modules/beauty/**", + "@/src/modules/healthcare/**" ], "message": "Accounting must not import beauty or healthcare modules." } @@ -34,7 +36,9 @@ "@/components/accounting/**", "@/components/healthcare/**", "@/modules/accounting/**", - "@/modules/healthcare/**" + "@/modules/healthcare/**", + "@/src/modules/accounting/**", + "@/src/modules/healthcare/**" ], "message": "Beauty must not import accounting or healthcare modules." } @@ -56,7 +60,8 @@ "@/components/beauty/**", "@/modules/beauty/**", "@/modules/accounting/**", - "@/src/modules/beauty/**" + "@/src/modules/beauty/**", + "@/src/modules/accounting/**" ], "message": "Healthcare must not import accounting or beauty modules." } @@ -79,7 +84,9 @@ "@/components/healthcare/**", "@/modules/beauty/**", "@/modules/healthcare/**", - "@/modules/accounting/**" + "@/modules/accounting/**", + "@/src/modules/accounting/**", + "@/src/modules/healthcare/**" ], "message": "CRM must not import accounting, beauty, or healthcare modules." } @@ -93,6 +100,7 @@ "components/ds/**/*", "shared/**/*", "components/providers/**/*", + "src/shared/**/*", "components/AuthGuard.tsx", "components/AdminAuthGuard.tsx", "components/ThemeProvider.tsx", @@ -111,6 +119,9 @@ "@/components/healthcare/**", "@/modules/beauty/**", "@/modules/healthcare/**", + "@/modules/hospitality/**", + "@/src/modules/healthcare/**", + "@/src/modules/accounting/**", "@/modules/accounting/**", "@/modules/crm/**" ], diff --git a/frontend/app/accounting/ai/analytics/page.tsx b/frontend/app/accounting/ai/analytics/page.tsx index 3804f8f..3a738d9 100644 --- a/frontend/app/accounting/ai/analytics/page.tsx +++ b/frontend/app/accounting/ai/analytics/page.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; - -export default function Page() { - return ; -} +"use client"; + +export { AiAnalyticsPage as default } from "@/src/modules/accounting/pages/ai/analytics"; diff --git a/frontend/app/accounting/ai/anomalies/page.tsx b/frontend/app/accounting/ai/anomalies/page.tsx index c2d3433..481f14d 100644 --- a/frontend/app/accounting/ai/anomalies/page.tsx +++ b/frontend/app/accounting/ai/anomalies/page.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; - -export default function Page() { - return ; -} +"use client"; + +export { AiAnomaliesPage as default } from "@/src/modules/accounting/pages/ai/anomalies"; diff --git a/frontend/app/accounting/ai/documents/page.tsx b/frontend/app/accounting/ai/documents/page.tsx index 93c288f..63b2210 100644 --- a/frontend/app/accounting/ai/documents/page.tsx +++ b/frontend/app/accounting/ai/documents/page.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; - -export default function Page() { - return ; -} +"use client"; + +export { AiDocumentsPage as default } from "@/src/modules/accounting/pages/ai/documents"; diff --git a/frontend/app/accounting/ai/forecasts/page.tsx b/frontend/app/accounting/ai/forecasts/page.tsx index e5e07e8..273f822 100644 --- a/frontend/app/accounting/ai/forecasts/page.tsx +++ b/frontend/app/accounting/ai/forecasts/page.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; - -export default function Page() { - return ; -} +"use client"; + +export { AiForecastsPage as default } from "@/src/modules/accounting/pages/ai/forecasts"; diff --git a/frontend/app/accounting/ai/page.tsx b/frontend/app/accounting/ai/page.tsx index d654b68..72b8a98 100644 --- a/frontend/app/accounting/ai/page.tsx +++ b/frontend/app/accounting/ai/page.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; - -export default function Page() { - return ; -} +"use client"; + +export { AiPage as default } from "@/src/modules/accounting/pages/ai"; diff --git a/frontend/app/accounting/ai/qa/page.tsx b/frontend/app/accounting/ai/qa/page.tsx index 725eefb..964536a 100644 --- a/frontend/app/accounting/ai/qa/page.tsx +++ b/frontend/app/accounting/ai/qa/page.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; - -export default function Page() { - return ; -} +"use client"; + +export { AiQaPage as default } from "@/src/modules/accounting/pages/ai/qa"; diff --git a/frontend/app/accounting/ai/recommendations/page.tsx b/frontend/app/accounting/ai/recommendations/page.tsx index 9264c62..b929e18 100644 --- a/frontend/app/accounting/ai/recommendations/page.tsx +++ b/frontend/app/accounting/ai/recommendations/page.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; - -export default function Page() { - return ; -} +"use client"; + +export { AiRecommendationsPage as default } from "@/src/modules/accounting/pages/ai/recommendations"; diff --git a/frontend/app/accounting/ai/reconciliation/page.tsx b/frontend/app/accounting/ai/reconciliation/page.tsx index d6fd2d8..9d3f8d7 100644 --- a/frontend/app/accounting/ai/reconciliation/page.tsx +++ b/frontend/app/accounting/ai/reconciliation/page.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; - -export default function Page() { - return ; -} +"use client"; + +export { AiReconciliationPage as default } from "@/src/modules/accounting/pages/ai/reconciliation"; diff --git a/frontend/app/accounting/assets/accumulated/page.tsx b/frontend/app/accounting/assets/accumulated/page.tsx index 7a9ef1d..4cf58ec 100644 --- a/frontend/app/accounting/assets/accumulated/page.tsx +++ b/frontend/app/accounting/assets/accumulated/page.tsx @@ -1,3 +1,3 @@ -"use client"; -import { AssetAccumulatedPage } from "@/components/accounting/WorkflowScreens"; -export default function Page() { return ; } +"use client"; + +export { AssetsAccumulatedPage as default } from "@/src/modules/accounting/pages/assets/accumulated"; diff --git a/frontend/app/accounting/assets/dashboard/page.tsx b/frontend/app/accounting/assets/dashboard/page.tsx index 5a512a4..11eb47b 100644 --- a/frontend/app/accounting/assets/dashboard/page.tsx +++ b/frontend/app/accounting/assets/dashboard/page.tsx @@ -1,5 +1,3 @@ -import { redirect } from "next/navigation"; - -export default function Page() { - redirect("/accounting/assets"); -} +"use client"; + +export { AssetsDashboardPage as default } from "@/src/modules/accounting/pages/assets/dashboard"; diff --git a/frontend/app/accounting/assets/depreciate/page.tsx b/frontend/app/accounting/assets/depreciate/page.tsx index 5a512a4..5c7bbf1 100644 --- a/frontend/app/accounting/assets/depreciate/page.tsx +++ b/frontend/app/accounting/assets/depreciate/page.tsx @@ -1,5 +1,3 @@ -import { redirect } from "next/navigation"; - -export default function Page() { - redirect("/accounting/assets"); -} +"use client"; + +export { AssetsDepreciatePage as default } from "@/src/modules/accounting/pages/assets/depreciate"; diff --git a/frontend/app/accounting/assets/dispose/page.tsx b/frontend/app/accounting/assets/dispose/page.tsx index 657f19c..6714516 100644 --- a/frontend/app/accounting/assets/dispose/page.tsx +++ b/frontend/app/accounting/assets/dispose/page.tsx @@ -1,3 +1,3 @@ -"use client"; -import { AssetDisposalsPage } from "@/components/accounting/WorkflowScreens"; -export default function Page() { return ; } +"use client"; + +export { AssetsDisposePage as default } from "@/src/modules/accounting/pages/assets/dispose"; diff --git a/frontend/app/accounting/assets/page.tsx b/frontend/app/accounting/assets/page.tsx index 9c919a0..267a201 100644 --- a/frontend/app/accounting/assets/page.tsx +++ b/frontend/app/accounting/assets/page.tsx @@ -1,422 +1,3 @@ -"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={} - /> - )} - -
- ); -} +"use client"; + +export { AssetsPage as default } from "@/src/modules/accounting/pages/assets"; diff --git a/frontend/app/accounting/assets/schedule/page.tsx b/frontend/app/accounting/assets/schedule/page.tsx index a738647..243813d 100644 --- a/frontend/app/accounting/assets/schedule/page.tsx +++ b/frontend/app/accounting/assets/schedule/page.tsx @@ -1,5 +1,3 @@ -import { AssetSchedulePage } from "@/components/accounting/DomainScreens"; - -export default function Page() { - return ; -} +"use client"; + +export { AssetsSchedulePage as default } from "@/src/modules/accounting/pages/assets/schedule"; diff --git a/frontend/app/accounting/assets/transfers/page.tsx b/frontend/app/accounting/assets/transfers/page.tsx index c60a5f4..23db370 100644 --- a/frontend/app/accounting/assets/transfers/page.tsx +++ b/frontend/app/accounting/assets/transfers/page.tsx @@ -1,3 +1,3 @@ -"use client"; -import { AssetTransfersPage } from "@/components/accounting/WorkflowScreens"; -export default function Page() { return ; } +"use client"; + +export { AssetsTransfersPage as default } from "@/src/modules/accounting/pages/assets/transfers"; diff --git a/frontend/app/accounting/assets/valuation/page.tsx b/frontend/app/accounting/assets/valuation/page.tsx index efa6a10..a4d06f2 100644 --- a/frontend/app/accounting/assets/valuation/page.tsx +++ b/frontend/app/accounting/assets/valuation/page.tsx @@ -1,3 +1,3 @@ -"use client"; -import { AssetRevaluationsPage } from "@/components/accounting/WorkflowScreens"; -export default function Page() { return ; } +"use client"; + +export { AssetsValuationPage as default } from "@/src/modules/accounting/pages/assets/valuation"; diff --git a/frontend/app/accounting/audit/page.tsx b/frontend/app/accounting/audit/page.tsx index 0aa16a0..1895edf 100644 --- a/frontend/app/accounting/audit/page.tsx +++ b/frontend/app/accounting/audit/page.tsx @@ -1,136 +1,3 @@ -"use client"; - -import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { accountingApi } from "@/lib/accounting-api"; -import { useTenantId } from "@/hooks/useTenantId"; -import { - PageHeader, - DataTable, - EmptyState, - LoadingState, - ErrorState, - JalaliDateText, - Tabs, - Badge, -} from "@/components/ds"; - -type TabId = "compliance" | "posting"; - -export default function AuditPage() { - const { tenantId } = useTenantId(); - const [tab, setTab] = useState("compliance"); - - const complianceQ = useQuery({ - queryKey: ["accounting", tenantId, "compliance-audit"], - queryFn: () => accountingApi.compliance.listAudit(tenantId!), - enabled: !!tenantId && tab === "compliance", - }); - - const postingQ = useQuery({ - queryKey: ["accounting", tenantId, "posting-audit-logs"], - queryFn: () => accountingApi.vouchers.auditLogs(tenantId!), - enabled: !!tenantId && tab === "posting", - }); - - const activeQ = tab === "compliance" ? complianceQ : postingQ; - - if (!tenantId) return ; - if (activeQ.isLoading) return ; - if (activeQ.error) { - return activeQ.refetch()} />; - } - - return ( -
- - - setTab(id as TabId)} - items={[ - { id: "compliance", label: "رکوردهای انطباق" }, - { id: "posting", label: "لاگ ثبت اسناد" }, - ]} - /> - - {tab === "compliance" && ( - , - }, - { key: "action", header: "عملیات" }, - { key: "resource_type", header: "منبع" }, - { - key: "actor_user_id", - header: "عامل", - render: (r) => ( - - {String(r.actor_user_id ?? "—")} - - ), - }, - ]} - rows={(complianceQ.data ?? []) as unknown as Record[]} - empty={ - - } - /> - )} - - {tab === "posting" && ( - , - }, - { - key: "operation", - header: "عملیات", - render: (r) => {String(r.operation)}, - }, - { key: "voucher_number", header: "شماره سند", render: (r) => String(r.voucher_number ?? "—") }, - { key: "resource_type", header: "نوع منبع" }, - { - key: "resource_id", - header: "شناسه", - render: (r) => ( - - {String(r.resource_id ?? "—")} - - ), - }, - { - key: "actor_user_id", - header: "عامل", - render: (r) => ( - - {String(r.actor_user_id ?? "—")} - - ), - }, - ]} - rows={(postingQ.data ?? []) as unknown as Record[]} - empty={ - - } - /> - )} -
- ); -} +"use client"; + +export { AuditPage as default } from "@/src/modules/accounting/pages/audit"; diff --git a/frontend/app/accounting/budget/control/page.tsx b/frontend/app/accounting/budget/control/page.tsx index 8e4da4e..e52984a 100644 --- a/frontend/app/accounting/budget/control/page.tsx +++ b/frontend/app/accounting/budget/control/page.tsx @@ -1,23 +1,3 @@ -"use client"; -import { SpecializedOpsPage } from "@/components/accounting/SpecializedOpsPage"; -export default function Page() { - return ( - - ); -} +"use client"; + +export { BudgetControlPage as default } from "@/src/modules/accounting/pages/budget/control"; diff --git a/frontend/app/accounting/budget/definitions/page.tsx b/frontend/app/accounting/budget/definitions/page.tsx index 07353d2..659bfdf 100644 --- a/frontend/app/accounting/budget/definitions/page.tsx +++ b/frontend/app/accounting/budget/definitions/page.tsx @@ -1,23 +1,3 @@ -"use client"; -import { SpecializedOpsPage } from "@/components/accounting/SpecializedOpsPage"; -export default function Page() { - return ( - - ); -} +"use client"; + +export { BudgetDefinitionsPage as default } from "@/src/modules/accounting/pages/budget/definitions"; diff --git a/frontend/app/accounting/budget/forecast/page.tsx b/frontend/app/accounting/budget/forecast/page.tsx index e1d42c7..e7d7124 100644 --- a/frontend/app/accounting/budget/forecast/page.tsx +++ b/frontend/app/accounting/budget/forecast/page.tsx @@ -1,23 +1,3 @@ -"use client"; -import { SpecializedOpsPage } from "@/components/accounting/SpecializedOpsPage"; -export default function Page() { - return ( - - ); -} +"use client"; + +export { BudgetForecastPage as default } from "@/src/modules/accounting/pages/budget/forecast"; diff --git a/frontend/app/accounting/budget/operational/page.tsx b/frontend/app/accounting/budget/operational/page.tsx index ad7c01e..bef5648 100644 --- a/frontend/app/accounting/budget/operational/page.tsx +++ b/frontend/app/accounting/budget/operational/page.tsx @@ -1,23 +1,3 @@ -"use client"; -import { SpecializedOpsPage } from "@/components/accounting/SpecializedOpsPage"; -export default function Page() { - return ( - - ); -} +"use client"; + +export { BudgetOperationalPage as default } from "@/src/modules/accounting/pages/budget/operational"; diff --git a/frontend/app/accounting/budget/performance/page.tsx b/frontend/app/accounting/budget/performance/page.tsx index ab80f33..c758730 100644 --- a/frontend/app/accounting/budget/performance/page.tsx +++ b/frontend/app/accounting/budget/performance/page.tsx @@ -1,23 +1,3 @@ -"use client"; -import { SpecializedOpsPage } from "@/components/accounting/SpecializedOpsPage"; -export default function Page() { - return ( - - ); -} +"use client"; + +export { BudgetPerformancePage as default } from "@/src/modules/accounting/pages/budget/performance"; diff --git a/frontend/app/accounting/budget/reports/page.tsx b/frontend/app/accounting/budget/reports/page.tsx index 8bb844e..8c6f8fd 100644 --- a/frontend/app/accounting/budget/reports/page.tsx +++ b/frontend/app/accounting/budget/reports/page.tsx @@ -1,23 +1,3 @@ -"use client"; -import { SpecializedOpsPage } from "@/components/accounting/SpecializedOpsPage"; -export default function Page() { - return ( - - ); -} +"use client"; + +export { BudgetReportsPage as default } from "@/src/modules/accounting/pages/budget/reports"; diff --git a/frontend/app/accounting/budget/scenarios/page.tsx b/frontend/app/accounting/budget/scenarios/page.tsx index 1575df7..6a6351f 100644 --- a/frontend/app/accounting/budget/scenarios/page.tsx +++ b/frontend/app/accounting/budget/scenarios/page.tsx @@ -1,23 +1,3 @@ -"use client"; -import { SpecializedOpsPage } from "@/components/accounting/SpecializedOpsPage"; -export default function Page() { - return ( - - ); -} +"use client"; + +export { BudgetScenariosPage as default } from "@/src/modules/accounting/pages/budget/scenarios"; diff --git a/frontend/app/accounting/budget/variance/page.tsx b/frontend/app/accounting/budget/variance/page.tsx index fde46ca..1b33052 100644 --- a/frontend/app/accounting/budget/variance/page.tsx +++ b/frontend/app/accounting/budget/variance/page.tsx @@ -1,23 +1,3 @@ -"use client"; -import { SpecializedOpsPage } from "@/components/accounting/SpecializedOpsPage"; -export default function Page() { - return ( - - ); -} +"use client"; + +export { BudgetVariancePage as default } from "@/src/modules/accounting/pages/budget/variance"; diff --git a/frontend/app/accounting/chart-of-accounts/page.tsx b/frontend/app/accounting/chart-of-accounts/page.tsx index a7aa766..c153548 100644 --- a/frontend/app/accounting/chart-of-accounts/page.tsx +++ b/frontend/app/accounting/chart-of-accounts/page.tsx @@ -1,644 +1,3 @@ -"use client"; - -import { useMemo, 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 { Plus, Archive, Pencil } from "lucide-react"; -import { accountingApi, type Account, type ChartOfAccounts } from "@/lib/accounting-api"; -import { useTenantId } from "@/hooks/useTenantId"; -import { - PageHeader, - Button, - Dialog, - Input, - Label, - FieldError, - Textarea, - DataTable, - EmptyState, - LoadingState, - ErrorState, - Badge, - Select, - Drawer, - TableToolbar, - Tabs, - Checkbox, -} from "@/components/ds"; - -const chartSchema = z.object({ - code: z.string().min(1, "کد الزامی است"), - name: z.string().min(1, "نام الزامی است"), - description: z.string().optional(), - is_default: z.boolean().optional(), -}); - -const accountSchema = z.object({ - chart_id: z.string().uuid("دفتر حساب را انتخاب کنید"), - code: z.string().min(1, "کد الزامی است"), - name: z.string().min(1, "نام الزامی است"), - account_type: z.string().min(1), - account_category: z.string().min(1), - parent_id: z.string().optional(), - is_group: z.boolean().optional(), - is_postable: z.boolean().optional(), - description: z.string().optional(), -}); - -type ChartForm = z.infer; -const editChartSchema = chartSchema.omit({ code: true }).extend({ code: z.string().optional() }); -type EditChartForm = z.infer; -type AccountForm = z.infer; - -const ACCOUNT_TYPES = [ - { value: "asset", label: "دارایی" }, - { value: "liability", label: "بدهی" }, - { value: "equity", label: "حقوق صاحبان سهام" }, - { value: "revenue", label: "درآمد" }, - { value: "expense", label: "هزینه" }, -]; - -const ACCOUNT_CATEGORIES = [ - { value: "current_asset", label: "دارایی جاری" }, - { value: "non_current_asset", label: "دارایی غیرجاری" }, - { value: "current_liability", label: "بدهی جاری" }, - { value: "non_current_liability", label: "بدهی غیرجاری" }, - { value: "equity", label: "حقوق صاحبان سهام" }, - { value: "operating_revenue", label: "درآمد عملیاتی" }, - { value: "other_revenue", label: "سایر درآمدها" }, - { value: "operating_expense", label: "هزینه عملیاتی" }, - { value: "other_expense", label: "سایر هزینه‌ها" }, -]; - -export default function ChartOfAccountsPage() { - const { tenantId } = useTenantId(); - const qc = useQueryClient(); - const [tab, setTab] = useState("accounts"); - const [chartOpen, setChartOpen] = useState(false); - const [accountOpen, setAccountOpen] = useState(false); - const [editChart, setEditChart] = useState(null); - const [selected, setSelected] = useState(null); - const [filterChart, setFilterChart] = useState(""); - const [search, setSearch] = useState(""); - const [showInactive, setShowInactive] = useState(false); - - const chartsQ = useQuery({ - queryKey: ["accounting", tenantId, "charts"], - queryFn: () => accountingApi.charts.list(tenantId!), - enabled: !!tenantId, - }); - const accountsQ = useQuery({ - queryKey: ["accounting", tenantId, "accounts", filterChart], - queryFn: () => accountingApi.accounts.list(tenantId!, 1, 500, filterChart || undefined), - enabled: !!tenantId, - }); - - const chartForm = useForm({ - resolver: zodResolver(chartSchema), - defaultValues: { code: "", name: "", description: "", is_default: false }, - }); - const accountForm = useForm({ - resolver: zodResolver(accountSchema), - defaultValues: { - chart_id: "", - code: "", - name: "", - account_type: "asset", - account_category: "current_asset", - parent_id: "", - is_group: false, - is_postable: true, - description: "", - }, - }); - const editChartForm = useForm({ - resolver: zodResolver(editChartSchema), - }); - const editAccountForm = useForm({ - defaultValues: { name: "", status: "active", description: "", is_postable: true }, - }); - - const invalidate = async () => { - await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "charts"] }); - await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "accounts"] }); - }; - - const createChart = useMutation({ - mutationFn: (data: ChartForm) => accountingApi.charts.create(tenantId!, data), - onSuccess: async () => { - toast.success("دفتر حساب ایجاد شد"); - setChartOpen(false); - chartForm.reset(); - await invalidate(); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const updateChart = useMutation({ - mutationFn: (data: ChartForm) => - accountingApi.charts.update(tenantId!, editChart!.id, { - name: data.name, - description: data.description, - is_default: data.is_default, - }), - onSuccess: async () => { - toast.success("دفتر به‌روز شد"); - setEditChart(null); - await invalidate(); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const archiveChart = useMutation({ - mutationFn: (id: string) => accountingApi.charts.archive(tenantId!, id), - onSuccess: async () => { - toast.success("دفتر آرشیو شد"); - await invalidate(); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const createAccount = useMutation({ - mutationFn: (data: AccountForm) => - accountingApi.accounts.create(tenantId!, { - ...data, - parent_id: data.parent_id || null, - is_postable: data.is_postable ?? true, - is_group: data.is_group ?? false, - }), - onSuccess: async () => { - toast.success("حساب ایجاد شد"); - setAccountOpen(false); - await invalidate(); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const updateAccount = useMutation({ - mutationFn: (body: { name: string; status: string; description?: string; is_postable: boolean }) => - accountingApi.accounts.update(tenantId!, selected!.id, body), - onSuccess: async () => { - toast.success("حساب به‌روز شد"); - setSelected(null); - await invalidate(); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const archiveAccount = useMutation({ - mutationFn: (id: string) => accountingApi.accounts.archive(tenantId!, id), - onSuccess: async () => { - toast.success("حساب آرشیو شد"); - setSelected(null); - await invalidate(); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const rows = useMemo(() => { - let list = accountsQ.data ?? []; - if (!showInactive) list = list.filter((a) => a.status === "active"); - if (search.trim()) { - const q = search.trim().toLowerCase(); - list = list.filter( - (a) => a.code.toLowerCase().includes(q) || a.name.toLowerCase().includes(q) - ); - } - return [...list].sort((a, b) => a.code.localeCompare(b.code, "fa")); - }, [accountsQ.data, search, showInactive]); - - if (!tenantId) return ; - if (chartsQ.isLoading || accountsQ.isLoading) return ; - if (chartsQ.error || accountsQ.error) { - return ( - { - void chartsQ.refetch(); - void accountsQ.refetch(); - }} - /> - ); - } - - const chartName = (id: string) => chartsQ.data?.find((c) => c.id === id)?.name ?? id; - - return ( -
- - - - - } - /> - - - - {tab === "charts" ? ( -
- {chartsQ.data?.map((c) => ( -
-
-
-

{c.name}

-

- {c.code} -

-
-
- {c.is_default ? پیش‌فرض : null} - - {c.is_active ? "فعال" : "آرشیو"} - -
-
-
- - {c.is_active ? ( - - ) : null} - -
-
- ))} - {!chartsQ.data?.length ? ( - setChartOpen(true)}> - ایجاد دفتر - - } - /> - ) : null} -
- ) : ( - <> - - - setShowInactive(e.target.checked)} - /> - - } - /> - - {/* Mobile cards */} -
- {rows.map((r) => ( - - ))} - {!rows.length ? : null} -
- -
- ( - - {String(r.code)} - - ), - }, - { key: "name", header: "نام" }, - { - key: "chart_id", - header: "دفتر", - render: (r) => chartName(String(r.chart_id)), - }, - { - key: "account_type", - header: "نوع", - render: (r) => - ACCOUNT_TYPES.find((t) => t.value === r.account_type)?.label ?? String(r.account_type), - }, - { - key: "status", - header: "وضعیت", - render: (r) => ( - {String(r.status)} - ), - }, - { - key: "id", - header: "", - render: (r) => ( - - ), - }, - ]} - rows={rows as unknown as Record[]} - empty={ - setChartOpen(true)}> - ایجاد دفتر حساب - - } - /> - } - /> -
- - )} - - setChartOpen(false)} title="ایجاد دفتر حساب"> -
createChart.mutate(d))}> -
- - - {chartForm.formState.errors.code?.message} -
-
- - - {chartForm.formState.errors.name?.message} -
-
- -