"""Dashboard widget configuration service. Provides async functions for loading and persisting per-user and tenant-default dashboard widget layouts. """ import logging import uuid from datetime import datetime from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.admin.models import DashboardConfig logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Widget type literals # --------------------------------------------------------------------------- WIDGET_TYPES = ( "ProductionStats", "QueueStatus", "RecentRenders", "CostOverview", "WorkerStatus", "KPISummary", "OrderThroughput", "RevenueChart", "ItemStatus", "ProcessingTimes", "RenderTimeByOutputType", "OutputTypeUsage", "TopProducts", "OrdersByUser", "RenderBackendStats", ) # Default layouts per role _DEFAULT_ADMIN_WIDGETS: list[dict] = [ {"widget_type": "KPISummary", "position": {"col": 0, "row": 0, "w": 1, "h": 1}}, {"widget_type": "QueueStatus", "position": {"col": 1, "row": 0, "w": 1, "h": 1}}, {"widget_type": "WorkerStatus", "position": {"col": 2, "row": 0, "w": 1, "h": 1}}, {"widget_type": "OrderThroughput","position": {"col": 0, "row": 1, "w": 2, "h": 1}}, {"widget_type": "ItemStatus", "position": {"col": 2, "row": 1, "w": 1, "h": 1}}, {"widget_type": "RevenueChart", "position": {"col": 0, "row": 2, "w": 2, "h": 1}}, {"widget_type": "ProcessingTimes","position": {"col": 2, "row": 2, "w": 1, "h": 1}}, ] _DEFAULT_CLIENT_WIDGETS: list[dict] = [ {"widget_type": "RecentRenders", "position": {"col": 0, "row": 0, "w": 2, "h": 1}}, {"widget_type": "ProductionStats", "position": {"col": 2, "row": 0, "w": 1, "h": 1}}, ] def get_default_widgets_for_role(role: str) -> list[dict]: """Return systemwide default widget layout for a given role. admin / project_manager: KPI + analytics defaults. client: RecentRenders + ProductionStats only. """ if role in ("admin", "project_manager"): return [w.copy() for w in _DEFAULT_ADMIN_WIDGETS] return [w.copy() for w in _DEFAULT_CLIENT_WIDGETS] async def get_user_dashboard_config( db: AsyncSession, user_id: uuid.UUID, tenant_id: uuid.UUID | None, role: str, ) -> list[dict]: """Load widget config with fallback cascade. 1. User-specific config (user_id match). 2. Tenant-default config (is_tenant_default=True for the tenant). 3. System default based on role. """ # 1. User-specific result = await db.execute( select(DashboardConfig).where(DashboardConfig.user_id == user_id) ) config = result.scalar_one_or_none() if config is not None: return list(config.widgets) if config.widgets else [] # 2. Tenant default if tenant_id is not None: result = await db.execute( select(DashboardConfig).where( DashboardConfig.tenant_id == tenant_id, DashboardConfig.is_tenant_default.is_(True), ) ) tenant_config = result.scalar_one_or_none() if tenant_config is not None: return list(tenant_config.widgets) if tenant_config.widgets else [] # 3. System default return get_default_widgets_for_role(role) async def upsert_user_dashboard_config( db: AsyncSession, user_id: uuid.UUID, tenant_id: uuid.UUID | None, widgets: list[dict], ) -> DashboardConfig: """Save or update the user-specific widget config.""" result = await db.execute( select(DashboardConfig).where(DashboardConfig.user_id == user_id) ) config = result.scalar_one_or_none() if config is None: config = DashboardConfig( user_id=user_id, tenant_id=tenant_id, widgets=widgets, is_tenant_default=False, ) db.add(config) else: config.widgets = widgets config.updated_at = datetime.utcnow() await db.commit() await db.refresh(config) return config async def upsert_tenant_default( db: AsyncSession, tenant_id: uuid.UUID, widgets: list[dict], ) -> DashboardConfig: """Set the tenant-default widget config (admin only).""" result = await db.execute( select(DashboardConfig).where( DashboardConfig.tenant_id == tenant_id, DashboardConfig.is_tenant_default.is_(True), ) ) config = result.scalar_one_or_none() if config is None: config = DashboardConfig( tenant_id=tenant_id, user_id=None, widgets=widgets, is_tenant_default=True, ) db.add(config) else: config.widgets = widgets config.updated_at = datetime.utcnow() await db.commit() await db.refresh(config) return config