from __future__ import annotations from .shared import * class ContextCacheUserSnapshotMixin: async def _build_user_snapshot( self, *, db: AsyncSession, tenant_id: int, user_id: int, ) -> dict[str, Any]: owned_rows = ( await db.execute( select(Project.id).where( and_( Project.tenant_id == tenant_id, Project.owner_id == user_id, ) ) ) ).scalars().all() member_rows = ( await db.execute( select(UserProject.project_id).where( and_( UserProject.tenant_id == tenant_id, UserProject.user_id == user_id, ) ) ) ).scalars().all() project_ids = sorted({_safe_int(pid, 0) for pid in [*owned_rows, *member_rows] if _safe_int(pid, 0) > 0}) project_rows: list[Any] = [] if project_ids: project_rows = ( await db.execute( select(Project.id, Project.name, Project.title, Project.status).where( and_( Project.tenant_id == tenant_id, Project.id.in_(project_ids), ) ) ) ).all() open_tasks = ( await db.execute( select(Task.id, Task.project_id, Task.title, Task.status, Task.due_date).where( and_( Task.tenant_id == tenant_id, Task.assignee_id == user_id, func.lower(Task.status).notin_(DONE_TASK_STATUSES), ) ) ) ).all() pending_signals: list[dict[str, Any]] = [] if project_ids: pending_rows = ( await db.execute( select(Signal.id, Signal.title, Signal.priority, Signal.status, Signal.matched_project_id).where( and_( Signal.tenant_id == tenant_id, Signal.matched_project_id.in_(project_ids), or_( Signal.processed.is_(False), func.lower(Signal.status).in_(PENDING_SIGNAL_STATUSES), ), ) ).limit(60) ) ).all() pending_signals = [ { "id": int(row.id), "title": str(row.title or ""), "priority": str(row.priority or ""), "status": str(row.status or ""), "project_id": _safe_int(row.matched_project_id, 0) or None, } for row in pending_rows ] workload_row = ( await db.execute( select(UserWorkload.workload_ratio).where( and_( UserWorkload.tenant_id == tenant_id, UserWorkload.user_id == user_id, ) ) ) ).scalar_one_or_none() if workload_row is None: fallback = ( await db.execute( select(User.workload).where( and_( User.tenant_id == tenant_id, User.id == user_id, ) ) ) ).scalar_one_or_none() workload_ratio = round(float(fallback or 0) / 100.0, 2) else: workload_ratio = float(workload_row or 0.0) return { "user_id": user_id, "projects": [ { "id": int(row.id), "name": str(row.title or row.name or f"Project#{int(row.id)}"), "status": str(row.status or "unknown"), } for row in project_rows ], "assigned_tasks": [ { "id": int(row.id), "project_id": int(row.project_id), "title": str(row.title or ""), "status": str(row.status or ""), "due_date": row.due_date.isoformat() if row.due_date else None, } for row in open_tasks ], "pending_signals": pending_signals, "workload_ratio": workload_ratio, "snapshot_at": now_cn_aware().isoformat(), }