162 lines
4.8 KiB
Python
162 lines
4.8 KiB
Python
"""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.auth.models import PM_ROLES
|
|
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 PM_ROLES:
|
|
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
|