from __future__ import annotations from dataclasses import dataclass from datetime import date, datetime from typing import Any from sqlalchemy import func, select from sqlalchemy.orm import Session from app.modules.core.iam.models import User from app.modules.core.rbac.models import Role, UserRole from app.modules.core.tenancy.models import Branch, FinancialYear, Tenant from app.modules.clients.models import Client from app.modules.services.models import ( FirmServiceSelection, FirmServiceTaskTemplate, ServiceCatalogue, ) try: from app.modules.billing.models import BillingSettings except Exception: # pragma: no cover - optional module guard BillingSettings = None try: from app.modules.email_integration.models import PlatformEmailSettings except Exception: # pragma: no cover - optional W2 guard PlatformEmailSettings = None try: from app.modules.core.audit.models import AuditLog except Exception: # pragma: no cover - older schema guard AuditLog = None FIRM_ADMIN_ROLES = {"Firm Admin", "System Admin"} def _count(db: Session, stmt) -> int: value = db.execute(stmt).scalar() return int(value or 0) def _active_tenant_id(request, current_user, roles: set[str]) -> int | None: tenant_id = request.session.get("active_tenant_id") or getattr(current_user, "tenant_id", None) if "System Admin" not in roles: tenant_id = getattr(current_user, "tenant_id", None) return int(tenant_id) if tenant_id else None def _active_branch_id(request, current_user, roles: set[str]) -> int | None: branch_id = request.session.get("active_branch_id") or getattr(current_user, "branch_id", None) if "System Admin" in roles or "Firm Admin" in roles: if branch_id in (None, "", 0, "0"): return None return int(branch_id) return int(getattr(current_user, "branch_id", 0) or 0) or None def _tenant(db: Session, tenant_id: int | None) -> Tenant | None: return db.get(Tenant, int(tenant_id)) if tenant_id else None def _role_ids(db: Session, role_names: set[str]) -> list[int]: return list(db.execute(select(Role.id).where(Role.name.in_(role_names))).scalars().all()) def get_user_role_names(db: Session, user_id: int) -> list[str]: return list( db.execute( select(Role.name) .join(UserRole, UserRole.role_id == Role.id) .where(UserRole.user_id == int(user_id), Role.is_active.is_(True)) .order_by(Role.name.asc()) ).scalars().all() ) def can_access_firm_admin_dashboard(db: Session, current_user) -> bool: roles = set(get_user_role_names(db, current_user.id)) return bool(roles.intersection(FIRM_ADMIN_ROLES)) def _branch_rows(db: Session, tenant_id: int | None) -> list[dict[str, Any]]: if not tenant_id: return [] rows = db.execute( select(Branch) .where(Branch.tenant_id == tenant_id) .order_by(Branch.is_head_office.desc(), Branch.name.asc()) ).scalars().all() out: list[dict[str, Any]] = [] for branch in rows: partner_count = _count( db, select(func.count(func.distinct(User.id))) .join(UserRole, UserRole.user_id == User.id) .join(Role, Role.id == UserRole.role_id) .where( User.tenant_id == tenant_id, User.branch_id == branch.id, User.is_active.is_(True), Role.name == "Partner", ), ) user_count = _count( db, select(func.count(User.id)).where( User.tenant_id == tenant_id, User.branch_id == branch.id, User.is_active.is_(True), ), ) client_count = _count( db, select(func.count(Client.id)).where( Client.tenant_id == tenant_id, Client.branch_id == branch.id, Client.is_active.is_(True), ), ) out.append( { "id": branch.id, "code": branch.code, "name": branch.name, "is_active": branch.is_active, "is_head_office": branch.is_head_office, "allow_login": branch.allow_login, "allow_new_assignments": branch.allow_new_assignments, "timezone": branch.timezone, "partner_count": partner_count, "user_count": user_count, "client_count": client_count, "local_storage_path": branch.local_storage_path, "smtp_configured": bool(branch.smtp_host and branch.smtp_port and branch.smtp_username), } ) return out def _user_rows(db: Session, tenant_id: int | None) -> list[dict[str, Any]]: if not tenant_id: return [] users = db.execute( select(User) .where(User.tenant_id == tenant_id) .order_by(User.is_active.desc(), User.full_name.asc(), User.email.asc()) .limit(100) ).scalars().all() rows: list[dict[str, Any]] = [] for user in users: rows.append( { "id": user.id, "name": user.full_name or user.email, "email": user.email, "designation": user.designation, "mobile": user.mobile, "branch_id": user.branch_id, "roles": get_user_role_names(db, user.id), "is_active": user.is_active, "allow_login": user.allow_login, "is_locked": user.is_locked, "must_change_password": user.must_change_password, } ) return rows def _service_rows(db: Session, tenant_id: int | None) -> list[dict[str, Any]]: if not tenant_id: return [] selections = db.execute( select(FirmServiceSelection, ServiceCatalogue) .join(ServiceCatalogue, ServiceCatalogue.id == FirmServiceSelection.service_catalogue_id) .where(FirmServiceSelection.tenant_id == tenant_id) .order_by(ServiceCatalogue.category.asc(), ServiceCatalogue.service_name.asc()) ).all() out: list[dict[str, Any]] = [] for selection, catalogue in selections: template_count = _count( db, select(func.count(FirmServiceTaskTemplate.id)).where( FirmServiceTaskTemplate.tenant_id == tenant_id, FirmServiceTaskTemplate.service_catalogue_id == catalogue.id, FirmServiceTaskTemplate.is_active.is_(True), ), ) out.append( { "id": selection.id, "service_code": catalogue.service_code, "service_name": catalogue.service_name, "category": catalogue.category or getattr(getattr(catalogue, "service_category", None), "name", None) or "-", "recurrence_type": catalogue.recurrence_type or "-", "is_enabled": selection.is_enabled, "default_branch_id": selection.default_branch_id, "template_count": template_count, "ready": bool(selection.is_enabled and template_count > 0), } ) return out def _financial_year_rows(db: Session, tenant_id: int | None) -> list[dict[str, Any]]: if not tenant_id: return [] years = db.execute( select(FinancialYear) .where(FinancialYear.tenant_id == tenant_id) .order_by(FinancialYear.start_date.desc(), FinancialYear.year_code.desc()) ).scalars().all() return [ { "id": fy.id, "year_code": fy.year_code, "assessment_year": fy.assessment_year, "start_date": fy.start_date, "end_date": fy.end_date, "is_current": fy.is_current, "is_locked": fy.is_locked, "locked_at_utc": fy.locked_at_utc, } for fy in years ] def _billing_settings_count(db: Session, tenant_id: int | None) -> int: if not tenant_id or BillingSettings is None: return 0 return _count(db, select(func.count(BillingSettings.id)).where(BillingSettings.tenant_id == tenant_id)) def _smtp_status(db: Session, tenant_id: int | None) -> dict[str, Any]: if PlatformEmailSettings is not None: try: row = db.execute(select(PlatformEmailSettings).order_by(PlatformEmailSettings.id.desc())).scalars().first() return { "available": True, "configured": bool(row and row.smtp_host and row.smtp_port and row.from_email), "label": "Configured" if row and row.smtp_host and row.smtp_port and row.from_email else "Pending", "message": getattr(row, "from_email", None) or "Platform SMTP not configured", } except Exception: pass return {"available": False, "configured": False, "label": "Check settings", "message": "Open email settings to verify SMTP."} def build_dashboard_payload(db: Session, request, current_user) -> dict[str, Any]: roles = set(get_user_role_names(db, current_user.id)) tenant_id = _active_tenant_id(request, current_user, roles) branch_id = _active_branch_id(request, current_user, roles) tenant = _tenant(db, tenant_id) branches = _branch_rows(db, tenant_id) users = _user_rows(db, tenant_id) services = _service_rows(db, tenant_id) financial_years = _financial_year_rows(db, tenant_id) billing_settings_count = _billing_settings_count(db, tenant_id) firm_admin_roles = _role_ids(db, {"Firm Admin"}) firm_admin_count = 0 if tenant_id and firm_admin_roles: firm_admin_count = _count( db, select(func.count(func.distinct(User.id))) .join(UserRole, UserRole.user_id == User.id) .where(User.tenant_id == tenant_id, UserRole.role_id.in_(firm_admin_roles), User.is_active.is_(True)), ) partner_count = _count( db, select(func.count(func.distinct(User.id))) .join(UserRole, UserRole.user_id == User.id) .join(Role, Role.id == UserRole.role_id) .where(User.tenant_id == tenant_id, Role.name == "Partner", User.is_active.is_(True)), ) if tenant_id else 0 client_count = _count(db, select(func.count(Client.id)).where(Client.tenant_id == tenant_id, Client.is_active.is_(True))) if tenant_id else 0 user_count = _count(db, select(func.count(User.id)).where(User.tenant_id == tenant_id, User.is_active.is_(True))) if tenant_id else 0 service_count = len([s for s in services if s["is_enabled"]]) services_without_templates = len([s for s in services if s["is_enabled"] and s["template_count"] <= 0]) current_fy = next((fy for fy in financial_years if fy["is_current"]), None) branding_ready = bool(tenant and (tenant.display_name or tenant.logo_path or tenant.primary_color or tenant.contact_email)) setup_checks = [ {"key": "branches", "label": "Branches created", "ok": len(branches) > 0, "detail": f"{len(branches)} branch(es)"}, {"key": "firm_admin", "label": "Firm Admin user", "ok": firm_admin_count > 0, "detail": f"{firm_admin_count} active"}, {"key": "partners", "label": "Partners assigned", "ok": partner_count > 0, "detail": f"{partner_count} active"}, {"key": "services", "label": "Services selected", "ok": service_count > 0, "detail": f"{service_count} enabled"}, {"key": "templates", "label": "Task templates ready", "ok": services_without_templates == 0 and service_count > 0, "detail": f"{services_without_templates} service(s) pending"}, {"key": "fy", "label": "Current financial year", "ok": bool(current_fy), "detail": current_fy["year_code"] if current_fy else "Not set"}, {"key": "branding", "label": "Branding/contact", "ok": branding_ready, "detail": "Started" if branding_ready else "Pending"}, {"key": "billing", "label": "Billing settings", "ok": billing_settings_count > 0, "detail": f"{billing_settings_count} setup row(s)"}, ] setup_score = sum(1 for item in setup_checks if item["ok"]) overview = { "tenant": tenant, "tenant_id": tenant_id, "branch_id": branch_id, "branch_count": len(branches), "active_branch_count": len([b for b in branches if b["is_active"]]), "user_count": user_count, "firm_admin_count": firm_admin_count, "partner_count": partner_count, "client_count": client_count, "enabled_service_count": service_count, "services_without_templates": services_without_templates, "fy_count": len(financial_years), "current_fy": current_fy, "billing_settings_count": billing_settings_count, "branding_ready": branding_ready, "setup_score": setup_score, "setup_total": len(setup_checks), "setup_percent": round((setup_score / len(setup_checks)) * 100) if setup_checks else 0, "smtp": _smtp_status(db, tenant_id), "today": date.today(), } return { "roles": sorted(roles), "tenant": tenant, "overview": overview, "setup_checks": setup_checks, "branches": branches, "users": users, "services": services, "financial_years": financial_years, "reports": _report_cards(), "wizards": _wizard_cards(), "audit_logs": _audit_rows(db, tenant_id), } def _report_cards() -> list[dict[str, str]]: return [ {"group": "Setup Reports", "title": "Firm Setup Completeness", "desc": "Branches, users, roles, services, FY, branding and billing readiness.", "href": "/firm-admin/dashboard?tab=overview"}, {"group": "Branch Reports", "title": "Branch Readiness", "desc": "Active branches, partner assignment, users and local storage readiness.", "href": "/firm-admin/dashboard?tab=branches"}, {"group": "User Reports", "title": "Users & Roles", "desc": "Firm Admin, Partner, Manager, Staff and login readiness.", "href": "/firm-admin/dashboard?tab=users"}, {"group": "Service Reports", "title": "Service Setup Readiness", "desc": "Enabled services and missing task templates.", "href": "/firm-admin/dashboard?tab=services"}, {"group": "FY Reports", "title": "Financial Year Status", "desc": "Current, locked and historical financial years.", "href": "/firm-admin/dashboard?tab=financial-years"}, ] def _wizard_cards() -> list[dict[str, str]]: return [ {"title": "Add / Manage Branches", "desc": "Create branch, office timing, login control and local storage path.", "href": "/system-settings/branches"}, {"title": "Invite Users", "desc": "Create Firm Admin, Partner, Manager, Staff and role mapping.", "href": "/system-settings/users"}, {"title": "Firm Profile & Branding", "desc": "Display name, logo, colours and contact details.", "href": "/system-settings/branding"}, {"title": "Select Firm Services", "desc": "Enable services from system catalogue for this firm.", "href": "/services"}, {"title": "Firm Task Templates", "desc": "Customize task list and document requirements for enabled services.", "href": "/services/templates"}, {"title": "Financial Years", "desc": "Create, switch, lock and manage financial years.", "href": "/system-settings/financial-years"}, {"title": "Billing Settings", "desc": "Invoice prefix, GST settings, payment gateway and bank details.", "href": "/billing/settings"}, ] def _audit_rows(db: Session, tenant_id: int | None) -> list[Any]: if AuditLog is None or not tenant_id: return [] try: return list( db.execute( select(AuditLog) .where(getattr(AuditLog, "target_tenant_id", tenant_id) == tenant_id) .order_by(AuditLog.created_at_utc.desc()) .limit(25) ).scalars().all() ) except Exception: try: return list(db.execute(select(AuditLog).order_by(AuditLog.created_at_utc.desc()).limit(25)).scalars().all()) except Exception: return []