From 067b499193a13e7a53d661977e0c5e6d3400cd3f Mon Sep 17 00:00:00 2001 From: Mortezakoohjani Date: Sat, 25 Jul 2026 22:17:41 +0330 Subject: [PATCH] Connect Accounting frontend to domain APIs for phases 5.1-5.11. Wire sales/purchase posting, settlements, compliance, payroll employees, and ops edit flows; document the integration report. Co-authored-by: Cursor --- .../accounting/app/api/v1/compliance.py | 30 + .../app/api/v1/purchase_inventory.py | 10 + .../app/api/v1/receivable_payable.py | 30 + .../tests/test_frontend_integration_routes.py | 28 + .../accounting-frontend-integration-report.md | 116 ++++ docs/frontend/completion-scoreboard.md | 42 +- docs/progress.md | 136 +++++ .../app/accounting/assets/schedule/page.tsx | 12 +- .../accounting/compliance/policies/page.tsx | 12 +- .../app/accounting/compliance/risks/page.tsx | 12 +- .../accounting/inventory/valuation/page.tsx | 12 +- .../app/accounting/monitoring/health/page.tsx | 12 +- .../app/accounting/payroll/employees/page.tsx | 12 +- .../purchase/goods-receipts/page.tsx | 12 +- .../accounting/purchase/settlements/page.tsx | 12 +- .../app/accounting/sales/invoices/page.tsx | 12 +- .../app/accounting/sales/settlements/page.tsx | 12 +- frontend/app/accounting/vouchers/page.tsx | 19 +- .../accounting/BusinessDocsPage.tsx | 151 ++++- .../components/accounting/DomainScreens.tsx | 560 ++++++++++++++++++ frontend/lib/accounting-api.ts | 217 +++++++ frontend/lib/accounting-scoreboard.ts | 66 ++- 22 files changed, 1383 insertions(+), 142 deletions(-) create mode 100644 backend/services/accounting/app/tests/test_frontend_integration_routes.py create mode 100644 docs/accounting-frontend-integration-report.md create mode 100644 frontend/components/accounting/DomainScreens.tsx diff --git a/backend/services/accounting/app/api/v1/compliance.py b/backend/services/accounting/app/api/v1/compliance.py index 0c9cdce..9f7fe13 100644 --- a/backend/services/accounting/app/api/v1/compliance.py +++ b/backend/services/accounting/app/api/v1/compliance.py @@ -184,6 +184,36 @@ async def reject_request( return {"id": str(request.id), "status": request.status.value} +@router.get("/risks") +async def list_risks( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + from app.models.compliance import RiskRecord + from sqlalchemy import select + + stmt = ( + select(RiskRecord) + .where(RiskRecord.tenant_id == tenant_id) + .order_by(RiskRecord.created_at.desc()) + .limit(200) + ) + rows = list((await db.execute(stmt)).scalars().all()) + return [ + { + "id": str(r.id), + "code": r.code, + "title": r.title, + "risk_category": r.risk_category, + "risk_level": r.risk_level.value, + "risk_score": r.risk_score, + "is_resolved": r.is_resolved, + } + for r in rows + ] + + @router.post("/risks", status_code=status.HTTP_201_CREATED) async def create_risk( body: RiskCreate, diff --git a/backend/services/accounting/app/api/v1/purchase_inventory.py b/backend/services/accounting/app/api/v1/purchase_inventory.py index a654dc8..4b82e7b 100644 --- a/backend/services/accounting/app/api/v1/purchase_inventory.py +++ b/backend/services/accounting/app/api/v1/purchase_inventory.py @@ -59,6 +59,16 @@ async def create_purchase_profile( return {"id": str(entity.id), "name": entity.name} +@router.get("/posting-profiles") +async def list_purchase_profiles( + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + rows = await PurchaseProfileRepo(db).list_by_tenant(tenant_id, limit=100) + return [{"id": str(r.id), "name": r.name, "is_default": r.is_default, "is_active": r.is_active} for r in rows] + + @router.post("/preview/goods-receipt") async def preview_goods_receipt( body: GoodsReceiptRequest, diff --git a/backend/services/accounting/app/api/v1/receivable_payable.py b/backend/services/accounting/app/api/v1/receivable_payable.py index c2a40bf..f99ecd1 100644 --- a/backend/services/accounting/app/api/v1/receivable_payable.py +++ b/backend/services/accounting/app/api/v1/receivable_payable.py @@ -216,6 +216,36 @@ async def create_receivable_invoice( return {"id": str(entity.id)} +@router.get("/settlements") +async def list_settlements( + party_type: str | None = None, + tenant_id: UUID = Depends(require_tenant), + db: AsyncSession = Depends(get_db), + _user: CurrentUser = Depends(get_current_user), +): + from app.models.receivable_payable import Settlement + from sqlalchemy import select + + stmt = select(Settlement).where(Settlement.tenant_id == tenant_id) + if party_type: + stmt = stmt.where(Settlement.party_type == party_type) + stmt = stmt.order_by(Settlement.settlement_date.desc()).limit(200) + rows = list((await db.execute(stmt)).scalars().all()) + return [ + { + "id": str(s.id), + "settlement_number": s.settlement_number, + "settlement_date": s.settlement_date.isoformat(), + "settlement_type": s.settlement_type, + "party_type": s.party_type, + "party_id": str(s.party_id), + "total_amount": str(s.total_amount), + "status": s.status.value, + } + for s in rows + ] + + @router.post("/settlements") async def create_settlement( body: SettlementCreate, diff --git a/backend/services/accounting/app/tests/test_frontend_integration_routes.py b/backend/services/accounting/app/tests/test_frontend_integration_routes.py new file mode 100644 index 0000000..d2cffbd --- /dev/null +++ b/backend/services/accounting/app/tests/test_frontend_integration_routes.py @@ -0,0 +1,28 @@ +"""Smoke: new accounting integration list routes import + OpenAPI presence.""" +from __future__ import annotations + +from fastapi.routing import APIRoute + +from app.main import app + + +def test_settlements_list_route_registered(): + paths = {r.path for r in app.routes if isinstance(r, APIRoute)} + assert "/api/v1/ar-ap/settlements" in paths + + +def test_compliance_risks_list_route_registered(): + paths = {r.path for r in app.routes if isinstance(r, APIRoute)} + assert "/api/v1/compliance/risks" in paths + + +def test_purchase_profiles_list_route_registered(): + paths = {r.path for r in app.routes if isinstance(r, APIRoute)} + assert "/api/v1/purchase-inventory/posting-profiles" in paths + + +def test_ops_and_sales_accounting_present(): + paths = {r.path for r in app.routes if isinstance(r, APIRoute)} + assert "/api/v1/ops/documents" in paths + assert "/api/v1/sales-accounting/post" in paths + assert "/api/v1/purchase-inventory/post/goods-receipt" in paths diff --git a/docs/accounting-frontend-integration-report.md b/docs/accounting-frontend-integration-report.md new file mode 100644 index 0000000..fb3674f --- /dev/null +++ b/docs/accounting-frontend-integration-report.md @@ -0,0 +1,116 @@ +# Accounting Frontend Integration Report + +> Phase: Enterprise Accounting Frontend Integration (connect FE ↔ existing BE 5.1–5.11) +> Date: 2026-07-25 +> Scope: Accounting module only — no new product features beyond wiring existing APIs. + +## Summary + +Scanned ~118 Accounting FE routes against `/api/v1` routers. Connected previously disconnected domain screens to real backend APIs. Kept Phase 5.12 AI blocked. Ops document scaffolding remains for areas without domain routers (budget, DMS, integration connectors) with full list/create/edit/confirm against `/api/v1/ops`. + +## Connected Pages (domain APIs) + +| Area | Routes | Backend | +|------|--------|---------| +| Dashboard / Setup | `/accounting`, `/setup`, `/settings` | setup, health, charts, vouchers | +| COA / dimensions | chart-of-accounts, cost-centers, projects, currencies | `/accounts` | +| Fiscal | `/fiscal` | `/fiscal` | +| Vouchers | list (paginated), new, `[id]` | `/posting` | +| Ledger | `/ledger` | `/ledger` | +| Reports (TB/BS/IS) | `/reports` | `/reporting` | +| Treasury | hub, receipts-payments, transfers, reconciliation, cheques | `/treasury` | +| Customers / Suppliers | `/customers`, `/suppliers` | `/ar-ap` | +| Settlements | `/sales/settlements`, `/purchase/settlements` | `/ar-ap/settlements` | +| Sales invoice accounting | `/sales/invoices` | ops docs + `/sales-accounting` preview/post | +| Purchase GR | `/purchase/goods-receipts` | ops + `/purchase-inventory` | +| Inventory valuation | `/inventory/valuation` | `/purchase-inventory/valuation` + ops items | +| Assets hub + schedule | `/assets`, `/assets/schedule` | `/assets` | +| Payroll hub + employees | `/payroll`, `/payroll/employees` | `/payroll` | +| Compliance policies/risks | `/compliance/policies`, `/compliance/risks` | `/compliance` | +| Audit | `/audit` | compliance audit + posting audit | +| Monitoring health | `/monitoring/health` | `/health` + setup status | +| Inventory items/warehouses | `/inventory/items`, `/inventory/settings` | `/ops/items`, `/ops/warehouses` | + +## Fixed Pages + +| Page | Before | After | +|------|--------|-------| +| `/payroll/employees` | Ops stub documents | Domain payroll employees CRUD list/create | +| `/compliance/policies` | Ops stub | Domain policies list/create | +| `/compliance/risks` | Ops stub | Domain risks list/create | +| `/sales/settlements`, `/purchase/settlements` | Ops stub | Domain settlements list/create | +| `/sales/invoices` | Ops-only | Ops docs + sales-accounting preview/post | +| `/purchase/goods-receipts` | Ops-only | Ops docs + purchase-inventory preview/post | +| `/inventory/valuation` | Ops stub | Real valuation API | +| `/monitoring/health` | Ops stub | Live health + setup | +| `/assets/schedule` | Ops stub | Asset depreciation schedule from API | +| Ops BusinessDocsPage | Create/confirm only | + detail + edit (PATCH) | +| Vouchers list | First page only | Pagination (`page` / `page_size`) | +| Receipts/payments | Missing cash/bank ids | Cash box / bank account selects | + +## Connected APIs (FE client additions) + +- `settlements.list` / `settlements.create` +- `salesAccounting.listProfiles|createProfile|preview|post` +- `purchaseInventory.listProfiles|createProfile|previewGoodsReceipt|postGoodsReceipt|valuation` +- `compliance.requestApproval|approve|reject|listRisks|createRisk` +- `ops.updateDocument` (PATCH) + +### Backend list endpoints added (integration completeness only) + +- `GET /api/v1/ar-ap/settlements?party_type=` +- `GET /api/v1/compliance/risks` +- `GET /api/v1/purchase-inventory/posting-profiles` + +## Modified Components + +- `frontend/lib/accounting-api.ts` +- `frontend/components/accounting/DomainScreens.tsx` (new) +- `frontend/components/accounting/BusinessDocsPage.tsx` +- `frontend/app/accounting/vouchers/page.tsx` +- Route pages listed under Fixed Pages +- `backend/services/accounting/app/api/v1/{receivable_payable,compliance,purchase_inventory}.py` + +## Validation Results + +| Check | Result | +|-------|--------| +| BE module imports | Pass | +| Architecture / accounting pytest | See CI/local run in this session | +| Tenant header `X-Tenant-ID` on all authenticated FE calls | Pass (via `request()`) | +| JWT + refresh before accounting calls | Pass (`getValidAccessToken` + force refresh on 401) | +| ADR-010 (no direct journal writes from UI) | Pass — vouchers via Posting Engine only; sales/purchase post via accounting services | +| AI pages | Remain blocked | +| Fake/mock data | None introduced | + +## Automatic Posting / Journal / Approval + +| Workflow | How verified | +|----------|----------------| +| Automatic voucher generation | Sales `POST /sales-accounting/post`, Purchase `POST /purchase-inventory/post/goods-receipt`, Treasury cash-receipt, Payroll post → return `voucher_id` | +| Journal workflow | FE vouchers: create → validate → post → reverse/cancel on `/posting` | +| Approval workflow | FE client wired to `/compliance/approvals` (+ approve/reject); UI for request uses domain screens where applicable | +| Accounting events | Unchanged producers in BE services; UI consumes resulting vouchers/reports | + +## Known Issues + +1. **Ops scaffolding** remains for budget, documents DMS, integration connectors, monitoring KPIs/alerts, voucher recurring/templates, guarantees/facilities — no dedicated domain routers yet; CRUD is against `/ops/documents`. +2. **Suppliers** still list+create only (BE has no PATCH). +3. **Treasury** cheques/transfers/receipts mostly list+create (status transitions not exposed). +4. **Assets** dispose/transfer lack domain HTTP surfaces — ops stubs remain for those submenus. +5. **Reporting** cash-flow / equity / analytical still ops-backed; TB/BS/IS use Report Engine. +6. **Gitea push** may still be denied for this repo (local commits only unless permissions fixed). +7. Phase **5.12 AI** intentionally blocked. + +## Next Recommendations + +1. Promote high-traffic ops modules (sales orders, purchase orders) to first-class domain models when product prioritizes them. +2. Add supplier PATCH/archive + payable invoice GET/PATCH. +3. Cheque status transition APIs + UI. +4. Server-side voucher status filter + total count for pagination. +5. Wire compliance approval list UI once workflow list endpoint exists. +6. Unblock AI only after AI Provider Active (phase 5.12). + +## Scoreboard note + +Module scoreboard should treat ops-heavy areas as `partial` where domain engines are incomplete; domain hubs remain `complete`. diff --git a/docs/frontend/completion-scoreboard.md b/docs/frontend/completion-scoreboard.md index 840ec19..f7b4a08 100644 --- a/docs/frontend/completion-scoreboard.md +++ b/docs/frontend/completion-scoreboard.md @@ -3,30 +3,31 @@ > Source of truth for enterprise completion. Updated after every module gate. > States: **Complete** | **Partial** | **Missing** | **Blocked** -Last updated: 2026-07-24 +Last updated: 2026-07-25 (Frontend Integration) ## Overall | Module | % | Gate | |--------|---|------| -| Dashboard | 90% | Complete | +| Dashboard | 95% | Complete | | Chart of Accounts | 98% | Complete | | Fiscal | 98% | Complete | | Currencies | 98% | Complete | | Cost Centers | 95% | Complete | | Projects | 95% | Complete | -| Vouchers | 95% | Complete | +| Vouchers | 96% | Complete | | Ledger | 95% | Complete | -| Treasury | 90% | Complete | +| Treasury | 88% | Partial | | Customers / AR | 92% | Complete | -| Suppliers / AP | 85% | Complete | -| Assets | 90% | Complete | -| Payroll | 88% | Complete | -| Reports | 92% | Complete | -| Compliance / Audit | 88% | Complete | -| Settings | 85% | Complete (IAM deep-links) | +| Suppliers / AP | 80% | Partial | +| Sales accounting | 85% | Partial | +| Purchase / Inventory | 85% | Partial | +| Assets | 88% | Partial | +| Payroll | 90% | Complete | +| Reports | 90% | Partial | +| Compliance / Audit | 82–90% | Partial / Complete | +| Settings | 80% | Partial | | Onboarding / Setup | 95% | Complete | -| Mobile / Nav polish | 90% | Complete | | AI | 0% | Blocked (provider Planned) | ## Category legend @@ -36,15 +37,20 @@ API · CRUD · Routing · Nav · Dashboard · Forms · Dialogs · Drawers · Tab ## Module notes - **Foundation masters** (COA, fiscal, currencies, cost centers, projects): create/edit/archive via PATCH; DatePicker on date fields. -- **Vouchers**: draft edit via `PATCH /posting/vouchers/{id}`; lines support cost center + project. +- **Vouchers**: draft edit via `PATCH /posting/vouchers/{id}`; list pagination; lines support cost center + project. - **Ledger**: balances, trial balance, recalculate — real `/api/v1/ledger/*`. -- **Treasury**: cash boxes, banks, bank accounts, cash receipts. -- **Customers**: edit drawer, aging, receivable invoices; suppliers list/create. -- **Assets**: categories, activate, depreciate, schedule. -- **Payroll**: departments, periods, calculate, post. -- **Reports**: trial balance / BS / IS from reporting API; structured display of `data`. -- **Setup**: `/api/v1/setup/status`, skippable tour, COA template import. +- **Treasury**: cash boxes, banks, receipts/payments (cash/bank binding), transfers, recon, cheques; guarantees/facilities still ops. +- **Customers**: edit drawer, aging, receivable invoices; settlements via `/ar-ap/settlements`. +- **Sales**: invoice docs + `/sales-accounting` preview/post; other sales nav still ops. +- **Purchase/Inventory**: goods receipt preview/post + valuation; item/warehouse masters via `/ops`. +- **Assets**: categories, activate, depreciate, schedule page; transfer/dispose ops. +- **Payroll**: departments, employees page, periods, calculate, post. +- **Reports**: trial balance / BS / IS from reporting API; other report types ops. +- **Compliance**: policies + risks domain; audit lists; SoD/controls still ops. +- **Setup**: `/api/v1/setup/status`, COA template import via BFF proxy. - **AI**: Blocked until provider registry marks AI Model Provider Active. - **IAM / White-label / Custom domain**: Deep-link to SuperApp settings — not rebuilt inside Accounting. +Integration report: [../accounting-frontend-integration-report.md](../accounting-frontend-integration-report.md). + See also: [component-library.md](component-library.md), [frontend-architecture.md](frontend-architecture.md). diff --git a/docs/progress.md b/docs/progress.md index 9e2ea10..6713eed 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -103,6 +103,18 @@ Remaining: Phase 5.12 AI UI blocked on AI provider; optional richer PDF export f --- +## Accounting Frontend Integration (FE ↔ BE 5.1–5.11) ✅ + +- [x] Full route/menu scan vs `/api/v1` routers; gap matrix executed +- [x] Domain rewires: payroll employees, compliance policies/risks, settlements, sales-accounting post, purchase GR + valuation, asset schedule, monitoring health +- [x] FE client: settlements, salesAccounting, purchaseInventory, compliance approvals/risks, ops PATCH +- [x] Ops documents: detail + edit; treasury receipt/payment cash/bank binding; voucher list pagination +- [x] BE list endpoints for settlements, risks, purchase posting-profiles (integration completeness) +- [x] Report: [accounting-frontend-integration-report.md](accounting-frontend-integration-report.md) +- [x] Scoreboard honesty for partial/ops-backed modules; AI remains blocked + +--- + ## Phase 6.0 — CRM Service Foundation ✅ - [x] `backend/services/crm` service scaffold (API / services / repositories / models) @@ -170,6 +182,118 @@ Remaining: Phase 5.12 AI UI blocked on AI provider; optional richer PDF export f --- +## Phase 7.0 — Loyalty Service Foundation ✅ + +- [x] `backend/services/loyalty` service scaffold (API / services / repositories / models) +- [x] Foundation aggregates: LoyaltyProgram, MembershipTier, Member, PointAccount, Reward, Campaign, LoyaltyAuditLog, OutboxEvent +- [x] No mutable balance on PointAccount — direct balance mutation forbidden (`extra=forbid`) +- [x] Optimistic locking on Program / Member / PointAccount; campaign `rule_version` +- [x] Publish-only events via transactional outbox (`loyalty.program|tier|member|point_account|reward|campaign.*`) +- [x] Loyalty APIs only (`/api/v1/programs|tiers|members|point-accounts|rewards|campaigns|audit`) +- [x] Platform provider contracts only (Notification, Analytics, Customer360, AI, ModuleIntegration, CRM, Communication, FileStorage) +- [x] Permissions `loyalty.*` trees with manage/view inheritance; tenant isolation via `X-Tenant-ID` +- [x] Alembic `0001_initial` + `loyalty_db` wiring (compose port 8004) +- [x] Soft-delete unique reclaim; IntegrityError → 409; null required-field → 422; production config guards +- [x] Enterprise validation & self-heal (incl. removal of incomplete 7.1 residue) — **52 tests passed** +- [x] Docs: [loyalty-phase-7-0.md](loyalty-phase-7-0.md), [loyalty-phase-7-0-audit.md](loyalty-phase-7-0-audit.md), ADR-011, module registry, phase area README + +--- + +## Phase 8.0–8.10 — Enterprise Communication Platform ✅ + +- [x] Independent `communication-service` / `communication_db` (port 8005) — not owned by CRM/Loyalty/Restaurant +- [x] Provider framework + registry (mock, payamak; stubs for email/push/whatsapp/telegram/rubika/voice) +- [x] Communication router with priority, automatic failover, retry, circuit breaker +- [x] SMS platform: sender numbers, balance, rate limits, provider logs +- [x] Template engine: variables, locale, versioning, approval, preview, rendering +- [x] Dynamic contact sources (manual, CSV, CRM/Loyalty/Restaurant/Marketplace/Accounting/Website/External REST) — resolve only, no duplication +- [x] Queue engine: async queue, scheduling, priority, backoff retry, DLQ, batch +- [x] Delivery tracking: queued/sent/delivered/failed/expired/cancelled + timeline +- [x] OTP platform: generate/verify/expire, rate limit, brute-force lock, multi-channel path +- [x] Incoming provider webhooks with signature verification +- [x] Health `/health`, Capability `/capabilities`, providers status, monitoring stats +- [x] Permissions `communication.*`; events `communication.*`; audit on every send +- [x] Alembic `0001_initial`; version `0.8.10.0` +- [x] Tests: architecture, health/security, providers/SMS/failover/queue, templates/contacts/OTP/webhooks, validators +- [x] Docs: [communication-phase-8.md](communication-phase-8.md), ADR-012, module/provider registry, progress + +--- + +## Phase AF — AI Development Framework ✅ + +> Documentation-only. No business functionality. No modifications to existing services, schemas, or APIs. + +- [x] Permanent area [`docs/ai-framework/`](ai-framework/README.md) +- [x] Master prompt, development loop, phase handover, quality gates +- [x] Templates: phase, service, module, entity, repository, service-layer, API, event, testing, documentation +- [x] Manifests: [phase-manifest.yaml](ai-framework/phase-manifest.yaml), [service-manifest.yaml](ai-framework/service-manifest.yaml) +- [x] Prompt rules + Cursor guidelines +- [x] Docs index / module registry / progress integration +- [x] ADR-013 Accepted +- [x] Architecture, documentation, cross-reference, link, template, and manifest validation + +--- + +## Phase SC-Reg — Sports Center Platform Registration ✅ + +> Documentation-only registration. No business code, models, APIs, migrations, or repositories. + +- [x] Phase roadmap 9.0–9.10 registered in [phase-manifest.yaml](ai-framework/phase-manifest.yaml) +- [x] Service registered in [service-manifest.yaml](ai-framework/service-manifest.yaml) +- [x] Planned modules registered in [module-registry.md](module-registry.md#sports_center) +- [x] Sports terminology added to [glossary.md](glossary.md#sports-center) +- [x] Roadmap doc: [sports-center-roadmap.md](sports-center-roadmap.md) +- [x] ADR-014 Accepted — Independent Sports Center Platform +- [x] Module boundaries + phase area README updated +- [x] Manifest / cross-reference / documentation / module validation passed + +--- + +## Phase 9.0 — Sports Center Platform Foundation ✅ + +- [x] `backend/services/sports_center` service scaffold (API / services / repositories / models) +- [x] Foundation aggregates: SportsCenter, Branch, Sport, MembershipType, Membership, Coach, SportsRole, SportsPermission, Facility, Court, Room, LockerRoom, Locker, DeviceProvider, Device, AttendanceGateway, SportsConfiguration, SportsEvent, SportsSetting, SportsAuditLog +- [x] Adapter-based connector framework (QR/RFID/Barcode/Fingerprint/Face/Turnstile/Door/Payment/Attendance) — interfaces only +- [x] Platform provider contracts only (Accounting, CRM, Loyalty, Communication, Notification, Storage, AI, Identity, Customer360) +- [x] Publish-only events (`sports_center.member|membership|coach|facility|device|attendance|locker.*`) +- [x] Sports APIs under `/api/v1` + device connect/disconnect + locker assign/release +- [x] Permissions `sports_center.*` trees; tenant isolation via `X-Tenant-ID` +- [x] Alembic `0001_initial` + `sports_center_db` wiring (compose port 8006) +- [x] Architecture / API / permissions / migration / dependency / connectors / docs tests +- [x] Docs: [sports-center-phase-9-0.md](sports-center-phase-9-0.md), [phase-handover/phase-9-0.md](phase-handover/phase-9-0.md), ADR-014, module registry +- [x] No business workflow engines (booking/attendance lifecycle deferred) + +--- + +## Phase 9.1 — Sports Center Membership Catalog ✅ + +- [x] Catalog aggregates: SportCategory, AgeGroup, PricingModel, MembershipPackage, MembershipPlan, MembershipRule, RenewalPolicy, FreezingRule, ExpirationPolicy +- [x] MembershipType extended with catalog refs (package/plan/pricing/age/category/policies), transferability, sort order +- [x] Validators for age range, pricing/billing/rule kinds, money amounts, catalog ref integrity +- [x] Publish-only catalog events; permissions trees for catalog modules +- [x] APIs under `/api/v1/sport-categories|age-groups|pricing-models|membership-packages|membership-plans|membership-rules|renewal-policies|freezing-rules|expiration-policies` +- [x] `/capabilities` endpoint; version `0.9.1.0` +- [x] Alembic `0002_phase_91_membership_catalog` +- [x] Tests: catalog API, tenant isolation, age validation, architecture, docs +- [x] Docs: [sports-center-phase-9-1.md](sports-center-phase-9-1.md), [phase-handover/phase-9-1.md](phase-handover/phase-9-1.md) +- [x] No member enrollment / coach / attendance / booking engines + +--- + +## Phase 9.2 — Sports Center Member Management ✅ + +- [x] Members aggregate + family members, emergency contacts, medical information shells +- [x] Membership assignment (`member_id`) consuming Phase 9.1 catalog types +- [x] Status management: activate / suspend / cancel / freeze / unfreeze +- [x] Membership cards (QR/physical/digital), digital membership passes, waivers (sign), member documents (Storage refs) +- [x] Validators, permissions (`sports_center.members.*` trees), publish-only events +- [x] Alembic `0003_phase_92_member_management`; version `0.9.2.0` +- [x] Tests: members API, tenant isolation, transitions, architecture, migration, docs +- [x] Docs: [sports-center-phase-9-2.md](sports-center-phase-9-2.md), [phase-handover/phase-9-2.md](phase-handover/phase-9-2.md) +- [x] No coach / attendance / booking engines + +--- + ## Related Documents - [Next Steps](next-steps.md) @@ -180,3 +304,15 @@ Remaining: Phase 5.12 AI UI blocked on AI provider; optional richer PDF export f - [CRM Phase 6.1](crm-phase-6-1.md) - [CRM Phase 6.2](crm-phase-6-2.md) - [CRM Phase 6.3](crm-phase-6-3.md) +- [Loyalty Phase 7.0](loyalty-phase-7-0.md) +- [Communication Phase 8](communication-phase-8.md) +- [Sports Center Phase 9.0](sports-center-phase-9-0.md) +- [Sports Center Phase 9.1](sports-center-phase-9-1.md) +- [Sports Center Phase 9.2](sports-center-phase-9-2.md) +- [Phase Handover 9.0](phase-handover/phase-9-0.md) +- [Phase Handover 9.1](phase-handover/phase-9-1.md) +- [Phase Handover 9.2](phase-handover/phase-9-2.md) +- [Sports Center Roadmap](sports-center-roadmap.md) +- [AI Development Framework](ai-framework/README.md) +- [ADR-013](architecture/adr/ADR-013.md) +- [ADR-014](architecture/adr/ADR-014.md) diff --git a/frontend/app/accounting/assets/schedule/page.tsx b/frontend/app/accounting/assets/schedule/page.tsx index 63b3358..a738647 100644 --- a/frontend/app/accounting/assets/schedule/page.tsx +++ b/frontend/app/accounting/assets/schedule/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { AssetSchedulePage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/compliance/policies/page.tsx b/frontend/app/accounting/compliance/policies/page.tsx index 43f804e..fda622a 100644 --- a/frontend/app/accounting/compliance/policies/page.tsx +++ b/frontend/app/accounting/compliance/policies/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { CompliancePoliciesPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/compliance/risks/page.tsx b/frontend/app/accounting/compliance/risks/page.tsx index ea132f3..2ddc4d2 100644 --- a/frontend/app/accounting/compliance/risks/page.tsx +++ b/frontend/app/accounting/compliance/risks/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { ComplianceRisksPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/inventory/valuation/page.tsx b/frontend/app/accounting/inventory/valuation/page.tsx index 9d6df7b..10590b6 100644 --- a/frontend/app/accounting/inventory/valuation/page.tsx +++ b/frontend/app/accounting/inventory/valuation/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { InventoryValuationPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/monitoring/health/page.tsx b/frontend/app/accounting/monitoring/health/page.tsx index c0c049d..4843324 100644 --- a/frontend/app/accounting/monitoring/health/page.tsx +++ b/frontend/app/accounting/monitoring/health/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { MonitoringHealthPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/payroll/employees/page.tsx b/frontend/app/accounting/payroll/employees/page.tsx index 06192b0..be42af8 100644 --- a/frontend/app/accounting/payroll/employees/page.tsx +++ b/frontend/app/accounting/payroll/employees/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { PayrollEmployeesPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/purchase/goods-receipts/page.tsx b/frontend/app/accounting/purchase/goods-receipts/page.tsx index 8d98db3..31a3381 100644 --- a/frontend/app/accounting/purchase/goods-receipts/page.tsx +++ b/frontend/app/accounting/purchase/goods-receipts/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { PurchaseGoodsReceiptPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/purchase/settlements/page.tsx b/frontend/app/accounting/purchase/settlements/page.tsx index 67814d6..e4edf54 100644 --- a/frontend/app/accounting/purchase/settlements/page.tsx +++ b/frontend/app/accounting/purchase/settlements/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { SettlementsPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/sales/invoices/page.tsx b/frontend/app/accounting/sales/invoices/page.tsx index b5c9b6b..c964ae7 100644 --- a/frontend/app/accounting/sales/invoices/page.tsx +++ b/frontend/app/accounting/sales/invoices/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { SalesInvoiceAccountingPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/sales/settlements/page.tsx b/frontend/app/accounting/sales/settlements/page.tsx index 7b9e71f..9ebaada 100644 --- a/frontend/app/accounting/sales/settlements/page.tsx +++ b/frontend/app/accounting/sales/settlements/page.tsx @@ -1,13 +1,5 @@ -"use client"; - -import { BusinessDocsPage } from "@/components/accounting/BusinessDocsPage"; +import { SettlementsPage } from "@/components/accounting/DomainScreens"; export default function Page() { - return ( - - ); + return ; } diff --git a/frontend/app/accounting/vouchers/page.tsx b/frontend/app/accounting/vouchers/page.tsx index 103596a..a99ab1b 100644 --- a/frontend/app/accounting/vouchers/page.tsx +++ b/frontend/app/accounting/vouchers/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Suspense, useMemo } from "react"; +import { Suspense, useMemo, useState } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; @@ -17,6 +17,7 @@ import { ErrorState, Badge, JalaliDateText, + Pagination, } from "@/components/ds"; const STATUS_TITLES: Record = { @@ -29,10 +30,12 @@ function VouchersList() { const { tenantId } = useTenantId(); const sp = useSearchParams(); const statusFilter = sp.get("status") || ""; + const [page, setPage] = useState(1); + const pageSize = 50; const listQ = useQuery({ - queryKey: ["accounting", tenantId, "vouchers"], - queryFn: () => accountingApi.vouchers.list(tenantId!), + queryKey: ["accounting", tenantId, "vouchers", page, pageSize], + queryFn: () => accountingApi.vouchers.list(tenantId!, page, pageSize), enabled: !!tenantId, }); @@ -102,6 +105,16 @@ function VouchersList() { /> } /> + ); } diff --git a/frontend/components/accounting/BusinessDocsPage.tsx b/frontend/components/accounting/BusinessDocsPage.tsx index c4653ab..ca7d06f 100644 --- a/frontend/components/accounting/BusinessDocsPage.tsx +++ b/frontend/components/accounting/BusinessDocsPage.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; -import { Plus } from "lucide-react"; +import { Eye, Pencil, Plus } from "lucide-react"; import { accountingApi } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { formatMoney } from "@/lib/utils"; @@ -54,6 +54,8 @@ export function BusinessDocsPage({ const qc = useQueryClient(); const [open, setOpen] = useState(false); const [confirmId, setConfirmId] = useState(null); + const [detailRow, setDetailRow] = useState | null>(null); + const [editRow, setEditRow] = useState | null>(null); const listQ = useQuery({ queryKey: ["accounting", tenantId, "ops-docs", module, docType], @@ -71,6 +73,16 @@ export function BusinessDocsPage({ description: "", }, }); + const editForm = useForm({ + resolver: zodResolver(schema), + defaultValues: { + number: "", + doc_date: "", + party_name: "", + amount: "0", + description: "", + }, + }); const createM = useMutation({ mutationFn: (d: FormValues) => @@ -109,6 +121,33 @@ export function BusinessDocsPage({ onError: (e: Error) => toast.error(e.message), }); + const openEdit = (row: Record) => { + setEditRow(row); + editForm.reset({ + number: String(row.number ?? ""), + doc_date: String(row.doc_date ?? ""), + party_name: String(row.party_name ?? ""), + amount: String(row.amount ?? "0"), + description: String(row.description ?? ""), + }); + }; + + const updateM = useMutation({ + mutationFn: (d: FormValues) => + accountingApi.ops.updateDocument(tenantId!, String(editRow!.id), { + doc_date: d.doc_date, + party_name: d.party_name || undefined, + amount: d.amount, + description: d.description || undefined, + }), + onSuccess: async () => { + toast.success("ویرایش ذخیره شد"); + setEditRow(null); + await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", module, docType] }); + }, + onError: (e: Error) => toast.error(e.message), + }); + if (!tenantId || listQ.isLoading) return ; if (listQ.error) { return listQ.refetch()} />; @@ -151,14 +190,25 @@ export function BusinessDocsPage({ { key: "id", header: "عملیات", - render: (r) => - r.status === "draft" ? ( - - ) : ( - "—" - ), + {r.status === "draft" ? ( + <> + + + + ) : null} + + ), }, ]} rows={(listQ.data ?? []) as unknown as Record[]} @@ -201,6 +251,51 @@ export function BusinessDocsPage({ + setEditRow(null)} title={`ویرایش ${title}`} size="lg"> +
updateM.mutate(d))}> + + + + + ( + + )} + /> + + + + + + + +
+ + + +
+
+ + +
+
+
+ + setDetailRow(null)} title={`جزئیات ${title}`}> + {detailRow ? ( +
+
شماره
{String(detailRow.number ?? "—")}
+
تاریخ
+
طرف حساب
{String(detailRow.party_name ?? "—")}
+
مبلغ
{formatMoney(String(detailRow.amount ?? "0"))}
+
وضعیت
{String(detailRow.status ?? "—")}
+
توضیح
{String(detailRow.description ?? "—")}
+
+ ) : null} +
+ setConfirmId(null)} @@ -581,6 +676,8 @@ export function ReceiptsPaymentsPage() { date: new Date().toISOString().slice(0, 10), amount: "0", payment_method: "cash", + cash_box_id: "", + bank_account_id: "", description: "", }, }); @@ -595,6 +692,16 @@ export function ReceiptsPaymentsPage() { queryFn: () => accountingApi.treasury.listPayments(tenantId!), enabled: !!tenantId, }); + const cashBoxesQ = useQuery({ + queryKey: ["accounting", tenantId, "cash-boxes"], + queryFn: () => accountingApi.treasury.listCashBoxes(tenantId!), + enabled: !!tenantId, + }); + const bankAccountsQ = useQuery({ + queryKey: ["accounting", tenantId, "bank-accounts"], + queryFn: () => accountingApi.treasury.listBankAccounts(tenantId!), + enabled: !!tenantId, + }); const createM = useMutation({ mutationFn: async (d: { @@ -602,6 +709,8 @@ export function ReceiptsPaymentsPage() { date: string; amount: string; payment_method: string; + cash_box_id?: string; + bank_account_id?: string; description?: string; }) => { if (mode === "receipt") { @@ -610,6 +719,8 @@ export function ReceiptsPaymentsPage() { receipt_date: d.date, amount: d.amount, payment_method: d.payment_method, + cash_box_id: d.payment_method === "cash" ? d.cash_box_id || undefined : undefined, + bank_account_id: d.payment_method === "bank" ? d.bank_account_id || undefined : undefined, description: d.description, }); } @@ -618,6 +729,8 @@ export function ReceiptsPaymentsPage() { payment_date: d.date, amount: d.amount, payment_method: d.payment_method, + cash_box_id: d.payment_method === "cash" ? d.cash_box_id || undefined : undefined, + bank_account_id: d.payment_method === "bank" ? d.bank_account_id || undefined : undefined, description: d.description, }); }, @@ -631,11 +744,11 @@ export function ReceiptsPaymentsPage() { onError: (e: Error) => toast.error(e.message), }); - if (!tenantId || receiptsQ.isLoading || paymentsQ.isLoading) return ; - if (receiptsQ.error || paymentsQ.error) { + if (!tenantId || receiptsQ.isLoading || paymentsQ.isLoading || cashBoxesQ.isLoading || bankAccountsQ.isLoading) return ; + if (receiptsQ.error || paymentsQ.error || cashBoxesQ.error || bankAccountsQ.error) { return ( { receiptsQ.refetch(); paymentsQ.refetch(); @@ -742,6 +855,22 @@ export function ReceiptsPaymentsPage() { + + + + + +
diff --git a/frontend/components/accounting/DomainScreens.tsx b/frontend/components/accounting/DomainScreens.tsx new file mode 100644 index 0000000..14f174e --- /dev/null +++ b/frontend/components/accounting/DomainScreens.tsx @@ -0,0 +1,560 @@ +"use client"; + +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Plus } from "lucide-react"; +import { toast } from "sonner"; +import { z } from "zod"; +import { accountingApi } from "@/lib/accounting-api"; +import { useTenantId } from "@/hooks/useTenantId"; +import { formatMoney } from "@/lib/utils"; +import { + Badge, + Button, + Card, + CardContent, + CardHeader, + CardTitle, + DataTable, + DatePicker, + Dialog, + EmptyState, + ErrorState, + FormField, + Input, + JalaliDateText, + LoadingState, + MoneyInput, + PageHeader, + Select, + StatCard, + Textarea, +} from "@/components/ds"; + +const today = () => new Date().toISOString().slice(0, 10); +const requiredText = z.string().min(1, "این فیلد الزامی است"); + +function Actions({ + onCancel, + pending, + submitLabel = "ذخیره", +}: { + onCancel: () => void; + pending: boolean; + submitLabel?: string; +}) { + return ( +
+ + +
+ ); +} + +const employeeSchema = z.object({ + employee_code: requiredText, + first_name: requiredText, + last_name: requiredText, + base_salary: requiredText, + department_id: z.string(), +}); + +export function PayrollEmployeesPage() { + const { tenantId } = useTenantId(); + const qc = useQueryClient(); + const [open, setOpen] = useState(false); + const form = useForm>({ + resolver: zodResolver(employeeSchema), + defaultValues: { employee_code: "", first_name: "", last_name: "", base_salary: "0", department_id: "" }, + }); + const employeesQ = useQuery({ + queryKey: ["accounting", tenantId, "payroll-employees"], + queryFn: () => accountingApi.payroll.listEmployees(tenantId!), + enabled: !!tenantId, + }); + const departmentsQ = useQuery({ + queryKey: ["accounting", tenantId, "payroll-departments"], + queryFn: () => accountingApi.payroll.listDepartments(tenantId!), + enabled: !!tenantId, + }); + const createM = useMutation({ + mutationFn: (values: z.infer) => + accountingApi.payroll.createEmployee(tenantId!, { + ...values, + department_id: values.department_id || undefined, + }), + onSuccess: async () => { + toast.success("کارمند ثبت شد"); + setOpen(false); + form.reset(); + await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "payroll-employees"] }); + }, + onError: (error: Error) => toast.error(error.message), + }); + if (!tenantId || employeesQ.isLoading || departmentsQ.isLoading) return ; + if (employeesQ.error || departmentsQ.error) { + const error = employeesQ.error || departmentsQ.error; + return { employeesQ.refetch(); departmentsQ.refetch(); }} />; + } + return ( +
+ setOpen(true)}>کارمند جدید} + /> + []} + empty={} + /> + setOpen(false)} title="ثبت کارمند" size="lg"> +
createM.mutate(v))}> + + + + + + + + setOpen(false)} pending={createM.isPending} /> + +
+
+ ); +} + +const policySchema = z.object({ + code: requiredText, + name: requiredText, + policy_type: requiredText, + rules_config: requiredText.refine((value) => { + try { JSON.parse(value); return true; } catch { return false; } + }, "JSON معتبر وارد کنید"), +}); + +export function CompliancePoliciesPage() { + const { tenantId } = useTenantId(); + const qc = useQueryClient(); + const [open, setOpen] = useState(false); + const form = useForm>({ + resolver: zodResolver(policySchema), + defaultValues: { code: "", name: "", policy_type: "", rules_config: "{}" }, + }); + const listQ = useQuery({ + queryKey: ["accounting", tenantId, "compliance-policies"], + queryFn: () => accountingApi.compliance.listPolicies(tenantId!), + enabled: !!tenantId, + }); + const createM = useMutation({ + mutationFn: (values: z.infer) => accountingApi.compliance.createPolicy(tenantId!, values), + onSuccess: async () => { + toast.success("سیاست ثبت شد"); + setOpen(false); + form.reset(); + await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "compliance-policies"] }); + }, + onError: (error: Error) => toast.error(error.message), + }); + if (!tenantId || listQ.isLoading) return ; + if (listQ.error) return listQ.refetch()} />; + return ( +
+ setOpen(true)}>سیاست جدید} /> + {r.is_active ? "فعال" : "غیرفعال"} }, + ]} + rows={listQ.data as unknown as Record[]} + empty={} + /> + setOpen(false)} title="سیاست جدید" size="lg"> +
createM.mutate(v))}> + + + +