diff --git a/backend/services/accounting/alembic/versions/0003_operational.py b/backend/services/accounting/alembic/versions/0003_operational.py new file mode 100644 index 0000000..e65d35f --- /dev/null +++ b/backend/services/accounting/alembic/versions/0003_operational.py @@ -0,0 +1,73 @@ +"""Alembic: operational docs, inventory items, warehouses.""" +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +from app.models.types import GUID + +revision = "0003_operational" +down_revision = "0002_phases_57_511" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "business_documents", + sa.Column("id", GUID(), primary_key=True), + sa.Column("tenant_id", GUID(), nullable=False), + sa.Column("module", sa.String(40), nullable=False), + sa.Column("doc_type", sa.String(50), nullable=False), + sa.Column("number", sa.String(50), nullable=False), + sa.Column("doc_date", sa.Date(), nullable=False), + sa.Column("party_name", sa.String(255), nullable=True), + sa.Column("party_id", GUID(), nullable=True), + sa.Column("amount", sa.Numeric(18, 4), nullable=False, server_default="0"), + sa.Column("status", sa.String(30), nullable=False, server_default="draft"), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("meta_json", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.UniqueConstraint("tenant_id", "module", "number", name="uq_biz_doc_tenant_module_number"), + ) + op.create_index( + "ix_biz_doc_tenant_module_type", + "business_documents", + ["tenant_id", "module", "doc_type"], + ) + + op.create_table( + "inventory_items", + sa.Column("id", GUID(), primary_key=True), + sa.Column("tenant_id", GUID(), nullable=False), + sa.Column("code", sa.String(50), nullable=False), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("item_type", sa.String(30), nullable=False, server_default="goods"), + sa.Column("unit", sa.String(30), nullable=False, server_default="عدد"), + sa.Column("quantity_on_hand", sa.Numeric(18, 4), nullable=False, server_default="0"), + sa.Column("unit_cost", sa.Numeric(18, 4), nullable=False, server_default="0"), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.UniqueConstraint("tenant_id", "code", name="uq_inventory_item_tenant_code"), + ) + + op.create_table( + "warehouses", + sa.Column("id", GUID(), primary_key=True), + sa.Column("tenant_id", GUID(), nullable=False), + sa.Column("code", sa.String(50), nullable=False), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.UniqueConstraint("tenant_id", "code", name="uq_warehouse_tenant_code"), + ) + + +def downgrade() -> None: + op.drop_table("warehouses") + op.drop_table("inventory_items") + op.drop_index("ix_biz_doc_tenant_module_type", table_name="business_documents") + op.drop_table("business_documents") diff --git a/backend/services/accounting/app/api/deps.py b/backend/services/accounting/app/api/deps.py index 73034fb..5cccef5 100644 --- a/backend/services/accounting/app/api/deps.py +++ b/backend/services/accounting/app/api/deps.py @@ -23,7 +23,7 @@ __all__ = [ def get_pagination( page: int = Query(default=1, ge=1), - page_size: int = Query(default=20, ge=1, le=100), + page_size: int = Query(default=20, ge=1, le=500), ) -> PaginationParams: return PaginationParams(page=page, page_size=page_size) diff --git a/backend/services/accounting/app/api/v1/__init__.py b/backend/services/accounting/app/api/v1/__init__.py index 65c10eb..07b78bf 100644 --- a/backend/services/accounting/app/api/v1/__init__.py +++ b/backend/services/accounting/app/api/v1/__init__.py @@ -7,6 +7,7 @@ from app.api.v1 import ( fixed_assets, health, ledger, + operational, payroll, posting, purchase_inventory, @@ -31,3 +32,4 @@ api_router.include_router(payroll.router, prefix="/payroll", tags=["payroll"]) api_router.include_router(reporting.router, prefix="/reporting", tags=["reporting"]) api_router.include_router(compliance.router, prefix="/compliance", tags=["compliance"]) api_router.include_router(setup.router, prefix="/setup", tags=["setup"]) +api_router.include_router(operational.router, prefix="/ops", tags=["ops"]) diff --git a/backend/services/accounting/app/api/v1/compliance.py b/backend/services/accounting/app/api/v1/compliance.py index db4bb2b..0c9cdce 100644 --- a/backend/services/accounting/app/api/v1/compliance.py +++ b/backend/services/accounting/app/api/v1/compliance.py @@ -105,6 +105,25 @@ async def create_audit_record( return {"id": str(record.id)} +@router.get("/policies") +async def list_compliance_policies( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + rows = await CompliancePolicyRepo(db).list_by_tenant(tenant_id, limit=200) + return [ + { + "id": str(p.id), + "code": p.code, + "name": p.name, + "policy_type": p.policy_type, + "is_active": getattr(p, "is_active", True), + } + for p in rows + ] + + @router.post("/policies", status_code=status.HTTP_201_CREATED) async def create_compliance_policy( body: CompliancePolicyCreate, diff --git a/backend/services/accounting/app/api/v1/operational.py b/backend/services/accounting/app/api/v1/operational.py new file mode 100644 index 0000000..578d5cd --- /dev/null +++ b/backend/services/accounting/app/api/v1/operational.py @@ -0,0 +1,218 @@ +"""Operational documents + inventory masters API.""" +from __future__ import annotations + +from datetime import date +from decimal import Decimal +from uuid import UUID + +from fastapi import APIRouter, Depends, Query, status +from pydantic import BaseModel, Field +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_db, require_tenant +from app.core.security import get_current_user +from app.models.operational import BusinessDocument, InventoryItem, Warehouse +from app.repositories.base import TenantBaseRepository +from shared.exceptions import NotFoundError +from shared.security import CurrentUser + +router = APIRouter() + + +class BizDocCreate(BaseModel): + module: str = Field(max_length=40) + doc_type: str = Field(max_length=50) + number: str = Field(max_length=50) + doc_date: date + party_name: str | None = None + party_id: UUID | None = None + amount: Decimal = Decimal("0") + status: str = "draft" + description: str | None = None + meta_json: str | None = None + + +class BizDocUpdate(BaseModel): + party_name: str | None = None + amount: Decimal | None = None + status: str | None = None + description: str | None = None + doc_date: date | None = None + meta_json: str | None = None + + +class BizDocRead(BaseModel): + id: UUID + tenant_id: UUID + module: str + doc_type: str + number: str + doc_date: date + party_name: str | None + amount: Decimal + status: str + description: str | None + + model_config = {"from_attributes": True} + + +class ItemCreate(BaseModel): + code: str + name: str + item_type: str = "goods" + unit: str = "عدد" + quantity_on_hand: Decimal = Decimal("0") + unit_cost: Decimal = Decimal("0") + + +class ItemRead(BaseModel): + id: UUID + code: str + name: str + item_type: str + unit: str + quantity_on_hand: Decimal + unit_cost: Decimal + is_active: bool + + model_config = {"from_attributes": True} + + +class WarehouseCreate(BaseModel): + code: str + name: str + + +class WarehouseRead(BaseModel): + id: UUID + code: str + name: str + is_active: bool + + model_config = {"from_attributes": True} + + +class BizDocRepo(TenantBaseRepository[BusinessDocument]): + model = BusinessDocument + + +class ItemRepo(TenantBaseRepository[InventoryItem]): + model = InventoryItem + + +class WarehouseRepo(TenantBaseRepository[Warehouse]): + model = Warehouse + + +@router.get("/documents", response_model=list[BizDocRead]) +async def list_documents( + module: str | None = Query(default=None), + doc_type: str | None = Query(default=None), + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + stmt = select(BusinessDocument).where(BusinessDocument.tenant_id == tenant_id) + if module: + stmt = stmt.where(BusinessDocument.module == module) + if doc_type: + stmt = stmt.where(BusinessDocument.doc_type == doc_type) + stmt = stmt.order_by(BusinessDocument.doc_date.desc()).limit(200) + result = await db.execute(stmt) + return list(result.scalars().all()) + + +@router.post("/documents", response_model=BizDocRead, status_code=status.HTTP_201_CREATED) +async def create_document( + body: BizDocCreate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + entity = BusinessDocument(tenant_id=tenant_id, **body.model_dump()) + await BizDocRepo(db).add(entity) + await db.commit() + await db.refresh(entity) + return entity + + +@router.patch("/documents/{doc_id}", response_model=BizDocRead) +async def update_document( + doc_id: UUID, + body: BizDocUpdate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + repo = BizDocRepo(db) + entity = await repo.get(tenant_id, doc_id) + if entity is None: + raise NotFoundError("سند یافت نشد", error_code="document_not_found") + for k, v in body.model_dump(exclude_unset=True).items(): + setattr(entity, k, v) + await db.commit() + await db.refresh(entity) + return entity + + +@router.post("/documents/{doc_id}/confirm", response_model=BizDocRead) +async def confirm_document( + doc_id: UUID, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + entity = await BizDocRepo(db).get(tenant_id, doc_id) + if entity is None: + raise NotFoundError("سند یافت نشد", error_code="document_not_found") + entity.status = "confirmed" + await db.commit() + await db.refresh(entity) + return entity + + +@router.get("/items", response_model=list[ItemRead]) +async def list_items( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + return await ItemRepo(db).list_by_tenant(tenant_id, limit=200) + + +@router.post("/items", response_model=ItemRead, status_code=status.HTTP_201_CREATED) +async def create_item( + body: ItemCreate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + entity = InventoryItem(tenant_id=tenant_id, **body.model_dump()) + await ItemRepo(db).add(entity) + await db.commit() + await db.refresh(entity) + return entity + + +@router.get("/warehouses", response_model=list[WarehouseRead]) +async def list_warehouses( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + return await WarehouseRepo(db).list_by_tenant(tenant_id, limit=100) + + +@router.post("/warehouses", response_model=WarehouseRead, status_code=status.HTTP_201_CREATED) +async def create_warehouse( + body: WarehouseCreate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + entity = Warehouse(tenant_id=tenant_id, **body.model_dump()) + await WarehouseRepo(db).add(entity) + await db.commit() + await db.refresh(entity) + return entity diff --git a/backend/services/accounting/app/api/v1/treasury.py b/backend/services/accounting/app/api/v1/treasury.py index b867c96..aedb772 100644 --- a/backend/services/accounting/app/api/v1/treasury.py +++ b/backend/services/accounting/app/api/v1/treasury.py @@ -204,3 +204,256 @@ async def update_cash_box( await db.commit() await db.refresh(entity) return CashBoxRead.model_validate(entity) + + +# ── Cheques / transfers / receipts / payments / reconciliation ─────────────── + +from app.models.treasury import Cheque, PaymentVoucher, ReceiptVoucher, Reconciliation, Transfer +from app.models.types import ChequeStatus, ReconciliationStatus + + +class ChequeRepo(TenantBaseRepository[Cheque]): + model = Cheque + + +class TransferRepo(TenantBaseRepository[Transfer]): + model = Transfer + + +class ReceiptRepo(TenantBaseRepository[ReceiptVoucher]): + model = ReceiptVoucher + + +class PaymentRepo(TenantBaseRepository[PaymentVoucher]): + model = PaymentVoucher + + +class ReconciliationRepo(TenantBaseRepository[Reconciliation]): + model = Reconciliation + + +class ChequeCreate(BaseModel): + cheque_number: str + amount: Decimal + issue_date: date + due_date: date | None = None + is_incoming: bool = True + payee: str | None = None + bank_account_id: UUID | None = None + status: ChequeStatus = ChequeStatus.RECEIVED + + +class TransferCreate(BaseModel): + transfer_date: date + amount: Decimal + from_type: str + from_id: UUID + to_type: str + to_id: UUID + description: str | None = None + + +class ReceiptCreate(BaseModel): + receipt_number: str + receipt_date: date + amount: Decimal + payment_method: str = "cash" + cash_box_id: UUID | None = None + bank_account_id: UUID | None = None + description: str | None = None + + +class PaymentCreate(BaseModel): + payment_number: str + payment_date: date + amount: Decimal + payment_method: str = "cash" + cash_box_id: UUID | None = None + bank_account_id: UUID | None = None + description: str | None = None + + +class ReconciliationCreate(BaseModel): + bank_account_id: UUID + statement_date: date + statement_balance: Decimal + book_balance: Decimal + + +@router.get("/cheques") +async def list_cheques( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + rows = await ChequeRepo(db).list_by_tenant(tenant_id, limit=200) + return [ + { + "id": str(c.id), + "cheque_number": c.cheque_number, + "amount": str(c.amount), + "issue_date": c.issue_date.isoformat(), + "due_date": c.due_date.isoformat() if c.due_date else None, + "status": c.status.value, + "is_incoming": c.is_incoming, + "payee": c.payee, + } + for c in rows + ] + + +@router.post("/cheques", status_code=status.HTTP_201_CREATED) +async def create_cheque( + body: ChequeCreate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + entity = Cheque(tenant_id=tenant_id, **body.model_dump()) + await ChequeRepo(db).add(entity) + await db.commit() + return {"id": str(entity.id), "cheque_number": entity.cheque_number} + + +@router.get("/transfers") +async def list_transfers( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + rows = await TransferRepo(db).list_by_tenant(tenant_id, limit=200) + return [ + { + "id": str(t.id), + "transfer_date": t.transfer_date.isoformat(), + "amount": str(t.amount), + "from_type": t.from_type, + "from_id": str(t.from_id), + "to_type": t.to_type, + "to_id": str(t.to_id), + "description": t.description, + } + for t in rows + ] + + +@router.post("/transfers", status_code=status.HTTP_201_CREATED) +async def create_transfer( + body: TransferCreate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + entity = Transfer(tenant_id=tenant_id, **body.model_dump()) + await TransferRepo(db).add(entity) + await db.commit() + return {"id": str(entity.id)} + + +@router.get("/receipts") +async def list_receipts( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + rows = await ReceiptRepo(db).list_by_tenant(tenant_id, limit=200) + return [ + { + "id": str(r.id), + "receipt_number": r.receipt_number, + "receipt_date": r.receipt_date.isoformat(), + "amount": str(r.amount), + "payment_method": r.payment_method, + "description": r.description, + } + for r in rows + ] + + +@router.post("/receipts", status_code=status.HTTP_201_CREATED) +async def create_receipt( + body: ReceiptCreate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + entity = ReceiptVoucher(tenant_id=tenant_id, **body.model_dump()) + await ReceiptRepo(db).add(entity) + await db.commit() + return {"id": str(entity.id)} + + +@router.get("/payments") +async def list_payments( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + rows = await PaymentRepo(db).list_by_tenant(tenant_id, limit=200) + return [ + { + "id": str(p.id), + "payment_number": p.payment_number, + "payment_date": p.payment_date.isoformat(), + "amount": str(p.amount), + "payment_method": p.payment_method, + "description": p.description, + } + for p in rows + ] + + +@router.post("/payments", status_code=status.HTTP_201_CREATED) +async def create_payment( + body: PaymentCreate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + entity = PaymentVoucher(tenant_id=tenant_id, **body.model_dump()) + await PaymentRepo(db).add(entity) + await db.commit() + return {"id": str(entity.id)} + + +@router.get("/reconciliations") +async def list_reconciliations( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + rows = await ReconciliationRepo(db).list_by_tenant(tenant_id, limit=100) + return [ + { + "id": str(r.id), + "bank_account_id": str(r.bank_account_id), + "statement_date": r.statement_date.isoformat(), + "statement_balance": str(r.statement_balance), + "book_balance": str(r.book_balance), + "difference": str(r.difference), + "status": r.status.value, + } + for r in rows + ] + + +@router.post("/reconciliations", status_code=status.HTTP_201_CREATED) +async def create_reconciliation( + body: ReconciliationCreate, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + diff = body.statement_balance - body.book_balance + entity = Reconciliation( + tenant_id=tenant_id, + bank_account_id=body.bank_account_id, + statement_date=body.statement_date, + statement_balance=body.statement_balance, + book_balance=body.book_balance, + difference=diff, + status=ReconciliationStatus.PENDING, + ) + await ReconciliationRepo(db).add(entity) + await db.commit() + return {"id": str(entity.id), "difference": str(diff)} diff --git a/backend/services/accounting/app/models/__init__.py b/backend/services/accounting/app/models/__init__.py index 9c9c0cc..63e4380 100644 --- a/backend/services/accounting/app/models/__init__.py +++ b/backend/services/accounting/app/models/__init__.py @@ -140,3 +140,8 @@ from app.models.treasury import ( # noqa: F401 Transfer, TreasurySettings, ) +from app.models.operational import ( # noqa: F401 + BusinessDocument, + InventoryItem, + Warehouse, +) diff --git a/backend/services/accounting/app/models/operational.py b/backend/services/accounting/app/models/operational.py new file mode 100644 index 0000000..89c0229 --- /dev/null +++ b/backend/services/accounting/app/models/operational.py @@ -0,0 +1,63 @@ +"""Phase ops — lightweight business documents for Sales/Purchase/Inventory UI.""" +from __future__ import annotations + +import uuid +from datetime import date +from decimal import Decimal + +from sqlalchemy import Boolean, Date, Index, Numeric, String, Text, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.database import Base +from app.models.base import TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin +from app.models.types import GUID + + +class BusinessDocument(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): + """Generic operational document used by Sales / Purchase / Inventory screens.""" + + __tablename__ = "business_documents" + + module: Mapped[str] = mapped_column(String(40), nullable=False) + doc_type: Mapped[str] = mapped_column(String(50), nullable=False) + number: Mapped[str] = mapped_column(String(50), nullable=False) + doc_date: Mapped[date] = mapped_column(Date, nullable=False) + party_name: Mapped[str | None] = mapped_column(String(255), nullable=True) + party_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) + amount: Mapped[Decimal] = mapped_column(Numeric(18, 4), default=Decimal("0"), nullable=False) + status: Mapped[str] = mapped_column(String(30), default="draft", nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + meta_json: Mapped[str | None] = mapped_column(Text, nullable=True) + + __table_args__ = ( + UniqueConstraint("tenant_id", "module", "number", name="uq_biz_doc_tenant_module_number"), + Index("ix_biz_doc_tenant_module_type", "tenant_id", "module", "doc_type"), + ) + + +class InventoryItem(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): + __tablename__ = "inventory_items" + + code: Mapped[str] = mapped_column(String(50), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + item_type: Mapped[str] = mapped_column(String(30), default="goods", nullable=False) + unit: Mapped[str] = mapped_column(String(30), default="عدد", nullable=False) + quantity_on_hand: Mapped[Decimal] = mapped_column(Numeric(18, 4), default=Decimal("0"), nullable=False) + unit_cost: Mapped[Decimal] = mapped_column(Numeric(18, 4), default=Decimal("0"), nullable=False) + is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + + __table_args__ = ( + UniqueConstraint("tenant_id", "code", name="uq_inventory_item_tenant_code"), + ) + + +class Warehouse(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): + __tablename__ = "warehouses" + + code: Mapped[str] = mapped_column(String(50), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + + __table_args__ = ( + UniqueConstraint("tenant_id", "code", name="uq_warehouse_tenant_code"), + ) diff --git a/backend/services/accounting/app/services/coa_templates.py b/backend/services/accounting/app/services/coa_templates.py index 1390441..e790a1a 100644 --- a/backend/services/accounting/app/services/coa_templates.py +++ b/backend/services/accounting/app/services/coa_templates.py @@ -283,6 +283,11 @@ async def import_coa_template( status_code=422, error_code="parent_missing", ) + existing_acc = await account_repo.get_by_code(tenant_id, item.code) + if existing_acc is not None: + # Reuse existing account code so re-import / overlapping COA does not crash + code_to_id[item.code] = existing_acc.id + continue acc = Account( tenant_id=tenant_id, chart_id=chart.id, diff --git a/backend/shared-lib/shared/pagination.py b/backend/shared-lib/shared/pagination.py index 2cbf094..ce340bb 100644 --- a/backend/shared-lib/shared/pagination.py +++ b/backend/shared-lib/shared/pagination.py @@ -11,7 +11,7 @@ from pydantic import BaseModel, Field T = TypeVar("T") DEFAULT_PAGE_SIZE = 20 -MAX_PAGE_SIZE = 100 +MAX_PAGE_SIZE = 500 class PaginationParams(BaseModel): diff --git a/docker-compose.yml b/docker-compose.yml index bc25772..fbef791 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -189,6 +189,7 @@ services: NEXT_PUBLIC_KEYCLOAK_CLIENT_ID: ${NEXT_PUBLIC_KEYCLOAK_CLIENT_ID:-superapp-frontend} NEXT_PUBLIC_PLATFORM_BASE_DOMAIN: ${NEXT_PUBLIC_PLATFORM_BASE_DOMAIN:-torbatyar.ir} NEXT_PUBLIC_ACCOUNTING_API_URL: ${NEXT_PUBLIC_ACCOUNTING_API_URL:-http://localhost:8002} + ACCOUNTING_SERVICE_URL: ${ACCOUNTING_SERVICE_URL:-http://accounting-service:8002} # polling برای hot-reload پایدار روی volume mount ویندوز WATCHPACK_POLLING: "true" CHOKIDAR_USEPOLLING: "true" diff --git a/frontend/app/accounting/ai/analytics/page.tsx b/frontend/app/accounting/ai/analytics/page.tsx new file mode 100644 index 0000000..3804f8f --- /dev/null +++ b/frontend/app/accounting/ai/analytics/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/ai/anomalies/page.tsx b/frontend/app/accounting/ai/anomalies/page.tsx new file mode 100644 index 0000000..c2d3433 --- /dev/null +++ b/frontend/app/accounting/ai/anomalies/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/ai/documents/page.tsx b/frontend/app/accounting/ai/documents/page.tsx new file mode 100644 index 0000000..93c288f --- /dev/null +++ b/frontend/app/accounting/ai/documents/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/ai/forecasts/page.tsx b/frontend/app/accounting/ai/forecasts/page.tsx new file mode 100644 index 0000000..e5e07e8 --- /dev/null +++ b/frontend/app/accounting/ai/forecasts/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/ai/page.tsx b/frontend/app/accounting/ai/page.tsx new file mode 100644 index 0000000..d654b68 --- /dev/null +++ b/frontend/app/accounting/ai/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/ai/qa/page.tsx b/frontend/app/accounting/ai/qa/page.tsx new file mode 100644 index 0000000..725eefb --- /dev/null +++ b/frontend/app/accounting/ai/qa/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/ai/recommendations/page.tsx b/frontend/app/accounting/ai/recommendations/page.tsx new file mode 100644 index 0000000..9264c62 --- /dev/null +++ b/frontend/app/accounting/ai/recommendations/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/ai/reconciliation/page.tsx b/frontend/app/accounting/ai/reconciliation/page.tsx new file mode 100644 index 0000000..d6fd2d8 --- /dev/null +++ b/frontend/app/accounting/ai/reconciliation/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { BlockedModulePage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/assets/accumulated/page.tsx b/frontend/app/accounting/assets/accumulated/page.tsx new file mode 100644 index 0000000..da09ff1 --- /dev/null +++ b/frontend/app/accounting/assets/accumulated/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/assets/dashboard/page.tsx b/frontend/app/accounting/assets/dashboard/page.tsx new file mode 100644 index 0000000..5a512a4 --- /dev/null +++ b/frontend/app/accounting/assets/dashboard/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/accounting/assets"); +} diff --git a/frontend/app/accounting/assets/depreciate/page.tsx b/frontend/app/accounting/assets/depreciate/page.tsx new file mode 100644 index 0000000..5a512a4 --- /dev/null +++ b/frontend/app/accounting/assets/depreciate/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/accounting/assets"); +} diff --git a/frontend/app/accounting/assets/dispose/page.tsx b/frontend/app/accounting/assets/dispose/page.tsx new file mode 100644 index 0000000..38f8ad1 --- /dev/null +++ b/frontend/app/accounting/assets/dispose/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/assets/schedule/page.tsx b/frontend/app/accounting/assets/schedule/page.tsx new file mode 100644 index 0000000..63b3358 --- /dev/null +++ b/frontend/app/accounting/assets/schedule/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/assets/transfers/page.tsx b/frontend/app/accounting/assets/transfers/page.tsx new file mode 100644 index 0000000..00b17fe --- /dev/null +++ b/frontend/app/accounting/assets/transfers/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/assets/valuation/page.tsx b/frontend/app/accounting/assets/valuation/page.tsx new file mode 100644 index 0000000..44dee16 --- /dev/null +++ b/frontend/app/accounting/assets/valuation/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/budget/control/page.tsx b/frontend/app/accounting/budget/control/page.tsx new file mode 100644 index 0000000..2fba5b5 --- /dev/null +++ b/frontend/app/accounting/budget/control/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/budget/definitions/page.tsx b/frontend/app/accounting/budget/definitions/page.tsx new file mode 100644 index 0000000..81f251c --- /dev/null +++ b/frontend/app/accounting/budget/definitions/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/budget/forecast/page.tsx b/frontend/app/accounting/budget/forecast/page.tsx new file mode 100644 index 0000000..0cc5571 --- /dev/null +++ b/frontend/app/accounting/budget/forecast/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/budget/operational/page.tsx b/frontend/app/accounting/budget/operational/page.tsx new file mode 100644 index 0000000..3cfead1 --- /dev/null +++ b/frontend/app/accounting/budget/operational/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/budget/performance/page.tsx b/frontend/app/accounting/budget/performance/page.tsx new file mode 100644 index 0000000..4b65cfb --- /dev/null +++ b/frontend/app/accounting/budget/performance/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/budget/reports/page.tsx b/frontend/app/accounting/budget/reports/page.tsx new file mode 100644 index 0000000..1730d2c --- /dev/null +++ b/frontend/app/accounting/budget/reports/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/budget/scenarios/page.tsx b/frontend/app/accounting/budget/scenarios/page.tsx new file mode 100644 index 0000000..d782ab4 --- /dev/null +++ b/frontend/app/accounting/budget/scenarios/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/budget/variance/page.tsx b/frontend/app/accounting/budget/variance/page.tsx new file mode 100644 index 0000000..f55476d --- /dev/null +++ b/frontend/app/accounting/budget/variance/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/compliance/approvals/page.tsx b/frontend/app/accounting/compliance/approvals/page.tsx new file mode 100644 index 0000000..33a382c --- /dev/null +++ b/frontend/app/accounting/compliance/approvals/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/compliance/controls/page.tsx b/frontend/app/accounting/compliance/controls/page.tsx new file mode 100644 index 0000000..4847185 --- /dev/null +++ b/frontend/app/accounting/compliance/controls/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/compliance/policies/page.tsx b/frontend/app/accounting/compliance/policies/page.tsx new file mode 100644 index 0000000..43f804e --- /dev/null +++ b/frontend/app/accounting/compliance/policies/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/compliance/records/page.tsx b/frontend/app/accounting/compliance/records/page.tsx new file mode 100644 index 0000000..b3da38d --- /dev/null +++ b/frontend/app/accounting/compliance/records/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/compliance/risks/page.tsx b/frontend/app/accounting/compliance/risks/page.tsx new file mode 100644 index 0000000..ea132f3 --- /dev/null +++ b/frontend/app/accounting/compliance/risks/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/compliance/sod/page.tsx b/frontend/app/accounting/compliance/sod/page.tsx new file mode 100644 index 0000000..ecd9769 --- /dev/null +++ b/frontend/app/accounting/compliance/sod/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/compliance/violations/page.tsx b/frontend/app/accounting/compliance/violations/page.tsx new file mode 100644 index 0000000..d500f80 --- /dev/null +++ b/frontend/app/accounting/compliance/violations/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/documents/categories/page.tsx b/frontend/app/accounting/documents/categories/page.tsx new file mode 100644 index 0000000..daac49c --- /dev/null +++ b/frontend/app/accounting/documents/categories/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/documents/financial/page.tsx b/frontend/app/accounting/documents/financial/page.tsx new file mode 100644 index 0000000..8ee3d80 --- /dev/null +++ b/frontend/app/accounting/documents/financial/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/documents/hr/page.tsx b/frontend/app/accounting/documents/hr/page.tsx new file mode 100644 index 0000000..10f29ff --- /dev/null +++ b/frontend/app/accounting/documents/hr/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/documents/purchase/page.tsx b/frontend/app/accounting/documents/purchase/page.tsx new file mode 100644 index 0000000..9826025 --- /dev/null +++ b/frontend/app/accounting/documents/purchase/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/documents/retention/page.tsx b/frontend/app/accounting/documents/retention/page.tsx new file mode 100644 index 0000000..529c6ba --- /dev/null +++ b/frontend/app/accounting/documents/retention/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/documents/sales/page.tsx b/frontend/app/accounting/documents/sales/page.tsx new file mode 100644 index 0000000..d21cbf9 --- /dev/null +++ b/frontend/app/accounting/documents/sales/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/documents/search/page.tsx b/frontend/app/accounting/documents/search/page.tsx new file mode 100644 index 0000000..cc1cbb1 --- /dev/null +++ b/frontend/app/accounting/documents/search/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/documents/versions/page.tsx b/frontend/app/accounting/documents/versions/page.tsx new file mode 100644 index 0000000..b375b35 --- /dev/null +++ b/frontend/app/accounting/documents/versions/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/integration/bank/page.tsx b/frontend/app/accounting/integration/bank/page.tsx new file mode 100644 index 0000000..874d862 --- /dev/null +++ b/frontend/app/accounting/integration/bank/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/integration/crm/page.tsx b/frontend/app/accounting/integration/crm/page.tsx new file mode 100644 index 0000000..a20eeb5 --- /dev/null +++ b/frontend/app/accounting/integration/crm/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/integration/gateway/page.tsx b/frontend/app/accounting/integration/gateway/page.tsx new file mode 100644 index 0000000..74977e0 --- /dev/null +++ b/frontend/app/accounting/integration/gateway/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/integration/import-export/page.tsx b/frontend/app/accounting/integration/import-export/page.tsx new file mode 100644 index 0000000..227ba98 --- /dev/null +++ b/frontend/app/accounting/integration/import-export/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/integration/inventory/page.tsx b/frontend/app/accounting/integration/inventory/page.tsx new file mode 100644 index 0000000..7306621 --- /dev/null +++ b/frontend/app/accounting/integration/inventory/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/integration/payroll/page.tsx b/frontend/app/accounting/integration/payroll/page.tsx new file mode 100644 index 0000000..1f8bed6 --- /dev/null +++ b/frontend/app/accounting/integration/payroll/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/integration/reports/page.tsx b/frontend/app/accounting/integration/reports/page.tsx new file mode 100644 index 0000000..646a051 --- /dev/null +++ b/frontend/app/accounting/integration/reports/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/integration/webhooks/page.tsx b/frontend/app/accounting/integration/webhooks/page.tsx new file mode 100644 index 0000000..5f4f325 --- /dev/null +++ b/frontend/app/accounting/integration/webhooks/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/inventory/adjustments/page.tsx b/frontend/app/accounting/inventory/adjustments/page.tsx new file mode 100644 index 0000000..cc4982c --- /dev/null +++ b/frontend/app/accounting/inventory/adjustments/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/inventory/items/page.tsx b/frontend/app/accounting/inventory/items/page.tsx new file mode 100644 index 0000000..d820f55 --- /dev/null +++ b/frontend/app/accounting/inventory/items/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { InventoryItemsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/inventory/movements/page.tsx b/frontend/app/accounting/inventory/movements/page.tsx new file mode 100644 index 0000000..2c474af --- /dev/null +++ b/frontend/app/accounting/inventory/movements/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/inventory/receipts-issues/page.tsx b/frontend/app/accounting/inventory/receipts-issues/page.tsx new file mode 100644 index 0000000..fdaad1b --- /dev/null +++ b/frontend/app/accounting/inventory/receipts-issues/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/inventory/reports/page.tsx b/frontend/app/accounting/inventory/reports/page.tsx new file mode 100644 index 0000000..52f769a --- /dev/null +++ b/frontend/app/accounting/inventory/reports/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/inventory/settings/page.tsx b/frontend/app/accounting/inventory/settings/page.tsx new file mode 100644 index 0000000..c4ca12b --- /dev/null +++ b/frontend/app/accounting/inventory/settings/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { WarehousesPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/inventory/transfers/page.tsx b/frontend/app/accounting/inventory/transfers/page.tsx new file mode 100644 index 0000000..7f32b09 --- /dev/null +++ b/frontend/app/accounting/inventory/transfers/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/inventory/valuation/page.tsx b/frontend/app/accounting/inventory/valuation/page.tsx new file mode 100644 index 0000000..9d6df7b --- /dev/null +++ b/frontend/app/accounting/inventory/valuation/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/monitoring/activity/page.tsx b/frontend/app/accounting/monitoring/activity/page.tsx new file mode 100644 index 0000000..52e2168 --- /dev/null +++ b/frontend/app/accounting/monitoring/activity/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/monitoring/alerts/page.tsx b/frontend/app/accounting/monitoring/alerts/page.tsx new file mode 100644 index 0000000..093c873 --- /dev/null +++ b/frontend/app/accounting/monitoring/alerts/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/monitoring/health/page.tsx b/frontend/app/accounting/monitoring/health/page.tsx new file mode 100644 index 0000000..c0c049d --- /dev/null +++ b/frontend/app/accounting/monitoring/health/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/monitoring/kpis/page.tsx b/frontend/app/accounting/monitoring/kpis/page.tsx new file mode 100644 index 0000000..f983bff --- /dev/null +++ b/frontend/app/accounting/monitoring/kpis/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/monitoring/page.tsx b/frontend/app/accounting/monitoring/page.tsx new file mode 100644 index 0000000..2da7c64 --- /dev/null +++ b/frontend/app/accounting/monitoring/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/monitoring/performance/page.tsx b/frontend/app/accounting/monitoring/performance/page.tsx new file mode 100644 index 0000000..c61c8ce --- /dev/null +++ b/frontend/app/accounting/monitoring/performance/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/monitoring/recent/page.tsx b/frontend/app/accounting/monitoring/recent/page.tsx new file mode 100644 index 0000000..c2533ac --- /dev/null +++ b/frontend/app/accounting/monitoring/recent/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/payroll/allocation/page.tsx b/frontend/app/accounting/payroll/allocation/page.tsx new file mode 100644 index 0000000..7a0a5d5 --- /dev/null +++ b/frontend/app/accounting/payroll/allocation/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/payroll/calculate/page.tsx b/frontend/app/accounting/payroll/calculate/page.tsx new file mode 100644 index 0000000..a7e81e2 --- /dev/null +++ b/frontend/app/accounting/payroll/calculate/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/accounting/payroll"); +} diff --git a/frontend/app/accounting/payroll/contracts/page.tsx b/frontend/app/accounting/payroll/contracts/page.tsx new file mode 100644 index 0000000..3401b22 --- /dev/null +++ b/frontend/app/accounting/payroll/contracts/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/payroll/employees/page.tsx b/frontend/app/accounting/payroll/employees/page.tsx new file mode 100644 index 0000000..06192b0 --- /dev/null +++ b/frontend/app/accounting/payroll/employees/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/payroll/items/page.tsx b/frontend/app/accounting/payroll/items/page.tsx new file mode 100644 index 0000000..acc813f --- /dev/null +++ b/frontend/app/accounting/payroll/items/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/payroll/payments/page.tsx b/frontend/app/accounting/payroll/payments/page.tsx new file mode 100644 index 0000000..56a895a --- /dev/null +++ b/frontend/app/accounting/payroll/payments/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/payroll/reports/page.tsx b/frontend/app/accounting/payroll/reports/page.tsx new file mode 100644 index 0000000..f4de3bd --- /dev/null +++ b/frontend/app/accounting/payroll/reports/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/payroll/structures/page.tsx b/frontend/app/accounting/payroll/structures/page.tsx new file mode 100644 index 0000000..01749ba --- /dev/null +++ b/frontend/app/accounting/payroll/structures/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/purchase/goods-receipts/page.tsx b/frontend/app/accounting/purchase/goods-receipts/page.tsx new file mode 100644 index 0000000..8d98db3 --- /dev/null +++ b/frontend/app/accounting/purchase/goods-receipts/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/purchase/invoices/page.tsx b/frontend/app/accounting/purchase/invoices/page.tsx new file mode 100644 index 0000000..56e837e --- /dev/null +++ b/frontend/app/accounting/purchase/invoices/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/purchase/orders/page.tsx b/frontend/app/accounting/purchase/orders/page.tsx new file mode 100644 index 0000000..bec8704 --- /dev/null +++ b/frontend/app/accounting/purchase/orders/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/purchase/prepayments/page.tsx b/frontend/app/accounting/purchase/prepayments/page.tsx new file mode 100644 index 0000000..410ec09 --- /dev/null +++ b/frontend/app/accounting/purchase/prepayments/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/purchase/requests/page.tsx b/frontend/app/accounting/purchase/requests/page.tsx new file mode 100644 index 0000000..10ee954 --- /dev/null +++ b/frontend/app/accounting/purchase/requests/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/purchase/returns/page.tsx b/frontend/app/accounting/purchase/returns/page.tsx new file mode 100644 index 0000000..eaf8b95 --- /dev/null +++ b/frontend/app/accounting/purchase/returns/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/purchase/settlements/page.tsx b/frontend/app/accounting/purchase/settlements/page.tsx new file mode 100644 index 0000000..67814d6 --- /dev/null +++ b/frontend/app/accounting/purchase/settlements/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/reports/page.tsx b/frontend/app/accounting/reports/page.tsx index 0baa952..de33c89 100644 --- a/frontend/app/accounting/reports/page.tsx +++ b/frontend/app/accounting/reports/page.tsx @@ -1,11 +1,13 @@ "use client"; -import { useMemo, useState } from "react"; +import { useMemo, useState, Suspense } from "react"; +import { useSearchParams } from "next/navigation"; import { useMutation, useQuery } from "@tanstack/react-query"; import { toast } from "sonner"; import { accountingApi } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { formatMoney } from "@/lib/utils"; +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; import { PageHeader, Button, @@ -30,6 +32,14 @@ const LABELS: Record = { "income-statement": "سود و زیان", }; +const OPS_REPORTS: Record = { + "cash-flow": { title: "جریان وجوه نقد", module: "reports", docType: "cash_flow" }, + equity: { title: "تغییرات حقوق صاحبان سهام", module: "reports", docType: "equity" }, + subsidiary: { title: "گزارش معین", module: "reports", docType: "subsidiary" }, + detail: { title: "گزارش تفصیلی", module: "reports", docType: "detail" }, + analytical: { title: "گزارش تحلیلی", module: "reports", docType: "analytical" }, +}; + const FIELD_LABELS: Record = { total_debit: "جمع بدهکار", total_credit: "جمع بستانکار", @@ -66,7 +76,7 @@ function formatCellValue(key: string, value: unknown): React.ReactNode { return String(value); } -export default function ReportsPage() { +function EngineReports({ initialKind }: { initialKind?: string }) { const { tenantId } = useTenantId(); const [periodId, setPeriodId] = useState(""); const [result, setResult] = useState<{ kind: ReportKind; payload: { id: string; data: unknown } } | null>( @@ -105,10 +115,7 @@ export default function ReportsPage() { accountingApi.reporting.export(tenantId!, result!.payload.id, format), onSuccess: (data) => { toast.success(`خروجی ${data.format} ثبت شد`); - if (data.file_path) { - toast.message(data.file_path); - } - // Download client-side JSON snapshot for immediate UX + if (data.file_path) toast.message(data.file_path); if (result) { const blob = new Blob([JSON.stringify(result.payload, null, 2)], { type: "application/json", @@ -144,6 +151,13 @@ export default function ReportsPage() { return periodsQ.refetch()} />; } + const highlight = + initialKind === "trial-balance" || + initialKind === "balance-sheet" || + initialKind === "income-statement" + ? (initialKind as ReportKind) + : null; + return (
runM.mutate(kind)} > @@ -233,3 +247,21 @@ export default function ReportsPage() {
); } + +function ReportsRouter() { + const sp = useSearchParams(); + const type = sp.get("type") || ""; + const ops = OPS_REPORTS[type]; + if (ops) { + return ; + } + return ; +} + +export default function ReportsPage() { + return ( + }> + + + ); +} diff --git a/frontend/app/accounting/sales/invoices/page.tsx b/frontend/app/accounting/sales/invoices/page.tsx new file mode 100644 index 0000000..b5c9b6b --- /dev/null +++ b/frontend/app/accounting/sales/invoices/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/sales/opportunities/page.tsx b/frontend/app/accounting/sales/opportunities/page.tsx new file mode 100644 index 0000000..6d49180 --- /dev/null +++ b/frontend/app/accounting/sales/opportunities/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/sales/orders/page.tsx b/frontend/app/accounting/sales/orders/page.tsx new file mode 100644 index 0000000..d2a977e --- /dev/null +++ b/frontend/app/accounting/sales/orders/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/sales/pre-receipts/page.tsx b/frontend/app/accounting/sales/pre-receipts/page.tsx new file mode 100644 index 0000000..8fe24e9 --- /dev/null +++ b/frontend/app/accounting/sales/pre-receipts/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/sales/proformas/page.tsx b/frontend/app/accounting/sales/proformas/page.tsx new file mode 100644 index 0000000..78e356d --- /dev/null +++ b/frontend/app/accounting/sales/proformas/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/sales/returns/page.tsx b/frontend/app/accounting/sales/returns/page.tsx new file mode 100644 index 0000000..5e672f8 --- /dev/null +++ b/frontend/app/accounting/sales/returns/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/sales/settlements/page.tsx b/frontend/app/accounting/sales/settlements/page.tsx new file mode 100644 index 0000000..7b9e71f --- /dev/null +++ b/frontend/app/accounting/sales/settlements/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/settings/backup/page.tsx b/frontend/app/accounting/settings/backup/page.tsx new file mode 100644 index 0000000..ad6b5d1 --- /dev/null +++ b/frontend/app/accounting/settings/backup/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/settings/company/page.tsx b/frontend/app/accounting/settings/company/page.tsx new file mode 100644 index 0000000..a6417ef --- /dev/null +++ b/frontend/app/accounting/settings/company/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/settings/general/page.tsx b/frontend/app/accounting/settings/general/page.tsx new file mode 100644 index 0000000..d2d319b --- /dev/null +++ b/frontend/app/accounting/settings/general/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/settings/tax/page.tsx b/frontend/app/accounting/settings/tax/page.tsx new file mode 100644 index 0000000..e4b9d12 --- /dev/null +++ b/frontend/app/accounting/settings/tax/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/setup/page.tsx b/frontend/app/accounting/setup/page.tsx index a46ad4a..2c1b505 100644 --- a/frontend/app/accounting/setup/page.tsx +++ b/frontend/app/accounting/setup/page.tsx @@ -49,7 +49,7 @@ const STEP_LINKS = { has_base_currency: { href: "/accounting/currencies", label: "ارز پایه" }, has_fiscal_year: { href: "/accounting/fiscal", label: "سال مالی" }, has_fiscal_period: { href: "/accounting/fiscal", label: "دوره مالی" }, - has_cash_box: { href: "/accounting/treasury", label: "صندوق" }, + has_cash_box: { href: "/accounting/treasury/cash-boxes", label: "صندوق" }, has_customer: { href: "/accounting/customers", label: "مشتری" }, has_voucher: { href: "/accounting/vouchers", label: "سند حسابداری" }, } as const; diff --git a/frontend/app/accounting/treasury/banks/page.tsx b/frontend/app/accounting/treasury/banks/page.tsx new file mode 100644 index 0000000..ba23cdf --- /dev/null +++ b/frontend/app/accounting/treasury/banks/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/accounting/treasury"); +} diff --git a/frontend/app/accounting/treasury/cash-boxes/page.tsx b/frontend/app/accounting/treasury/cash-boxes/page.tsx new file mode 100644 index 0000000..ba23cdf --- /dev/null +++ b/frontend/app/accounting/treasury/cash-boxes/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/accounting/treasury"); +} diff --git a/frontend/app/accounting/treasury/cheques/new/page.tsx b/frontend/app/accounting/treasury/cheques/new/page.tsx new file mode 100644 index 0000000..cb3cf2c --- /dev/null +++ b/frontend/app/accounting/treasury/cheques/new/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ChequesPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/treasury/cheques/page.tsx b/frontend/app/accounting/treasury/cheques/page.tsx new file mode 100644 index 0000000..ae6c68b --- /dev/null +++ b/frontend/app/accounting/treasury/cheques/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ChequesPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/treasury/facilities/page.tsx b/frontend/app/accounting/treasury/facilities/page.tsx new file mode 100644 index 0000000..f060821 --- /dev/null +++ b/frontend/app/accounting/treasury/facilities/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/treasury/guarantees/page.tsx b/frontend/app/accounting/treasury/guarantees/page.tsx new file mode 100644 index 0000000..03442bd --- /dev/null +++ b/frontend/app/accounting/treasury/guarantees/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/treasury/receipts-payments/page.tsx b/frontend/app/accounting/treasury/receipts-payments/page.tsx new file mode 100644 index 0000000..8449be3 --- /dev/null +++ b/frontend/app/accounting/treasury/receipts-payments/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ReceiptsPaymentsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/treasury/reconciliation/page.tsx b/frontend/app/accounting/treasury/reconciliation/page.tsx new file mode 100644 index 0000000..4483ad1 --- /dev/null +++ b/frontend/app/accounting/treasury/reconciliation/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ReconciliationPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/treasury/transfers/page.tsx b/frontend/app/accounting/treasury/transfers/page.tsx new file mode 100644 index 0000000..04285ae --- /dev/null +++ b/frontend/app/accounting/treasury/transfers/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { TransfersPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ; +} diff --git a/frontend/app/accounting/vouchers/adjustments/page.tsx b/frontend/app/accounting/vouchers/adjustments/page.tsx new file mode 100644 index 0000000..a359f07 --- /dev/null +++ b/frontend/app/accounting/vouchers/adjustments/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/vouchers/page.tsx b/frontend/app/accounting/vouchers/page.tsx index d9e8eef..103596a 100644 --- a/frontend/app/accounting/vouchers/page.tsx +++ b/frontend/app/accounting/vouchers/page.tsx @@ -1,32 +1,63 @@ "use client"; +import { Suspense, useMemo } from "react"; import Link from "next/link"; +import { useSearchParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { Plus } from "lucide-react"; import { accountingApi } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { formatMoney } from "@/lib/utils"; -import { PageHeader, Button, DataTable, EmptyState, LoadingState, ErrorState, Badge, JalaliDateText } from "@/components/ds"; +import { + PageHeader, + Button, + DataTable, + EmptyState, + LoadingState, + ErrorState, + Badge, + JalaliDateText, +} from "@/components/ds"; -export default function VouchersPage() { +const STATUS_TITLES: Record = { + draft: "اسناد پیش‌نویس", + reversed: "اسناد برگشتی", + posted: "تأیید و ثبت اسناد", +}; + +function VouchersList() { const { tenantId } = useTenantId(); + const sp = useSearchParams(); + const statusFilter = sp.get("status") || ""; + const listQ = useQuery({ queryKey: ["accounting", tenantId, "vouchers"], queryFn: () => accountingApi.vouchers.list(tenantId!), enabled: !!tenantId, }); + const rows = useMemo(() => { + const all = listQ.data ?? []; + if (!statusFilter) return all; + return all.filter((v) => String(v.status) === statusFilter); + }, [listQ.data, statusFilter]); + if (!tenantId || listQ.isLoading) return ; if (listQ.error) return listQ.refetch()} />; + const title = STATUS_TITLES[statusFilter] || "لیست اسناد حسابداری"; + return (
- + } /> @@ -41,7 +72,11 @@ export default function VouchersPage() { ), }, - { key: "voucher_date", header: "تاریخ", render: (r) => }, + { + key: "voucher_date", + header: "تاریخ", + render: (r) => , + }, { key: "status", header: "وضعیت", @@ -55,7 +90,7 @@ export default function VouchersPage() { { key: "total_credit", header: "بستانکار", render: (r) => formatMoney(String(r.total_credit)) }, { key: "description", header: "شرح" }, ]} - rows={(listQ.data ?? []) as unknown as Record[]} + rows={rows as unknown as Record[]} empty={ ); } + +export default function VouchersPage() { + return ( + }> + + + ); +} diff --git a/frontend/app/accounting/vouchers/recurring/page.tsx b/frontend/app/accounting/vouchers/recurring/page.tsx new file mode 100644 index 0000000..2501c91 --- /dev/null +++ b/frontend/app/accounting/vouchers/recurring/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/accounting/vouchers/templates/page.tsx b/frontend/app/accounting/vouchers/templates/page.tsx new file mode 100644 index 0000000..acf93b8 --- /dev/null +++ b/frontend/app/accounting/vouchers/templates/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; + +export default function Page() { + return ( + + ); +} diff --git a/frontend/app/api/accounting/[...path]/route.ts b/frontend/app/api/accounting/[...path]/route.ts new file mode 100644 index 0000000..d5c8f48 --- /dev/null +++ b/frontend/app/api/accounting/[...path]/route.ts @@ -0,0 +1,96 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * Same-origin BFF proxy → accounting service. + * Browser calls /api/accounting/* so TLS/CORS/DNS issues to accounting.torbatyar.ir + * do not block setup / CRUD from the SuperApp origin. + */ +const UPSTREAM = + process.env.ACCOUNTING_SERVICE_URL || + process.env.NEXT_PUBLIC_ACCOUNTING_API_URL || + "http://localhost:8002"; + +async function proxy(req: NextRequest, pathSegments: string[]) { + const path = pathSegments.join("/"); + const url = new URL(req.url); + const target = `${UPSTREAM.replace(/\/$/, "")}/${path}${url.search}`; + + const headers = new Headers(); + const pass = ["authorization", "content-type", "x-tenant-id", "accept"]; + for (const h of pass) { + const v = req.headers.get(h); + if (v) headers.set(h, v); + } + + let body: ArrayBuffer | undefined; + if (req.method !== "GET" && req.method !== "HEAD") { + body = await req.arrayBuffer(); + } + + try { + const res = await fetch(target, { + method: req.method, + headers, + body, + cache: "no-store", + }); + const outHeaders = new Headers(); + const ct = res.headers.get("content-type"); + if (ct) outHeaders.set("content-type", ct); + return new NextResponse(await res.arrayBuffer(), { + status: res.status, + headers: outHeaders, + }); + } catch (err) { + const detail = err instanceof Error ? err.message : "upstream_unreachable"; + return NextResponse.json( + { + error: { + code: "accounting_proxy_error", + message: `پروکسی حسابداری به ${UPSTREAM} وصل نشد — ${detail}`, + }, + }, + { status: 502 } + ); + } +} + +export async function GET( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} + +export async function POST( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} + +export async function PATCH( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} + +export async function PUT( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} + +export async function DELETE( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} diff --git a/frontend/components/accounting/AccountingShell.tsx b/frontend/components/accounting/AccountingShell.tsx index 8fcaf67..992d370 100644 --- a/frontend/components/accounting/AccountingShell.tsx +++ b/frontend/components/accounting/AccountingShell.tsx @@ -1,154 +1,176 @@ "use client"; +import { useMemo, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { - LayoutDashboard, - BookOpen, - CalendarRange, - Coins, - Landmark, - FolderKanban, - FileText, - Wallet, - Users, - Building2, - Briefcase, - FileBarChart2, - ShieldCheck, - Settings, - Moon, - Sun, - ChevronRight, - Scale, - Wrench, - Truck, - Plus, - MoreHorizontal, -} from "lucide-react"; +import { ChevronDown, Moon, Sun, ChevronRight, Plus, Lock } from "lucide-react"; import { cn } from "@/lib/utils"; import { useColorMode } from "@/components/providers/ColorModeProvider"; import { Button } from "@/components/ds"; - -const NAV = [ - { href: "/accounting", label: "داشبورد", icon: LayoutDashboard }, - { href: "/accounting/chart-of-accounts", label: "دفتر حساب‌ها", icon: BookOpen }, - { href: "/accounting/fiscal", label: "سال و دوره مالی", icon: CalendarRange }, - { href: "/accounting/currencies", label: "ارزها", icon: Coins }, - { href: "/accounting/cost-centers", label: "مراکز هزینه", icon: Landmark }, - { href: "/accounting/projects", label: "پروژه‌ها", icon: FolderKanban }, - { href: "/accounting/vouchers", label: "اسناد و ثبت", icon: FileText }, - { href: "/accounting/ledger", label: "دفتر کل", icon: Scale }, - { href: "/accounting/treasury", label: "خزانه", icon: Wallet }, - { href: "/accounting/customers", label: "مشتریان", icon: Users }, - { href: "/accounting/suppliers", label: "تأمین‌کنندگان", icon: Truck }, - { href: "/accounting/assets", label: "دارایی‌ها", icon: Building2 }, - { href: "/accounting/payroll", label: "حقوق و دستمزد", icon: Briefcase }, - { href: "/accounting/reports", label: "گزارش‌ها", icon: FileBarChart2 }, - { href: "/accounting/audit", label: "حسابرسی", icon: ShieldCheck }, - { href: "/accounting/setup", label: "راه‌اندازی", icon: Wrench }, - { href: "/accounting/settings", label: "تنظیمات", icon: Settings }, -]; - -const MOBILE_PRIMARY = [ - { href: "/accounting", label: "داشبورد", icon: LayoutDashboard }, - { href: "/accounting/vouchers", label: "اسناد", icon: FileText }, - { href: "/accounting/chart-of-accounts", label: "حساب‌ها", icon: BookOpen }, - { href: "/accounting/treasury", label: "خزانه", icon: Wallet }, - { href: "/accounting/setup", label: "بیشتر", icon: MoreHorizontal }, -]; +import { ACCOUNTING_NAV, ACCOUNTING_QUICK_BAR } from "@/lib/accounting-nav"; function isActive(pathname: string, href: string) { - if (href === "/accounting") return pathname === "/accounting"; - return pathname === href || pathname.startsWith(`${href}/`); + const base = href.split("?")[0]; + if (base === "/accounting") return pathname === "/accounting"; + return pathname === base || pathname.startsWith(`${base}/`); } export function AccountingShell({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const { mode, toggle } = useColorMode(); + const [openGroups, setOpenGroups] = useState>({}); + + const autoOpen = useMemo(() => { + const map: Record = {}; + for (const g of ACCOUNTING_NAV) { + if (g.href && isActive(pathname, g.href)) map[g.id] = true; + if (g.items?.some((i) => isActive(pathname, i.href))) map[g.id] = true; + } + return map; + }, [pathname]); + + const expanded = { ...autoOpen, ...openGroups }; + + const toggleGroup = (id: string) => + setOpenGroups((prev) => ({ ...prev, [id]: !(expanded[id] ?? false) })); return (
-