Files
HartOMat/backend/app/domains/admin/dashboard_service.py
T
Hartmut f15b035b88 feat(L1): modular widget dashboard — 15 configurable widgets
Replaces monolithic AdminDashboard/ClientDashboard with a per-user
configurable widget grid. 15 widget types: ProductionStats, QueueStatus,
RecentRenders, CostOverview, WorkerStatus, KPISummary, OrderThroughput,
Revenue, ItemStatus, ProcessingTimes, RenderTimeByOutputType,
OutputTypeUsage, TopProducts, OrdersByUser, RenderBackendStats.

DashboardTimeframeContext provides shared timeframe state. Dashboard
config persisted in DB via GET/PUT /api/dashboard/config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:11:13 +01:00

161 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.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