from __future__ import annotations from .shared import * async def get_team_workload_board( db: AsyncSession, tenant_id: int = 1, project_id: Optional[int] = None, ) -> list[dict]: """团队负载看板数据。""" query = ( select(UserWorkload, User) .join(User, UserWorkload.user_id == User.id) .where( and_( UserWorkload.tenant_id == tenant_id, User.tenant_id == tenant_id, ) ) ) if project_id is not None: query = query.join( UserProject, and_( UserProject.tenant_id == tenant_id, UserProject.user_id == User.id, ), ).where(UserProject.project_id == project_id) query = query.order_by(UserWorkload.workload_ratio.desc(), User.id.asc()) rows = (await db.execute(query)).all() return [ { "user_id": str(w.user_id), "user_name": u.name, "username": u.username, "role": u.role, "is_active": u.is_active, "current_assigned_hours": float(w.current_assigned_hours or 0), "max_weekly_hours": float(w.max_available_hours_per_week or 40), "workload_ratio": float(w.workload_ratio or 0), "remaining_hours": round( max( 0.0, float(w.max_available_hours_per_week or 40) - float(w.current_assigned_hours or 0), ), 2, ), "updated_at": w.updated_at.isoformat() if w.updated_at else None, } for w, u in rows ] async def get_user_personal_workload( db: AsyncSession, tenant_id: int, user_id: int, ) -> Optional[dict]: """个人负载详情。""" row = ( await db.execute( select(UserWorkload, User) .join(User, UserWorkload.user_id == User.id) .where( and_( UserWorkload.tenant_id == tenant_id, UserWorkload.user_id == user_id, User.tenant_id == tenant_id, ) ) ) ).first() if row: w, u = row return { "user_id": str(u.id), "user_name": u.name, "username": u.username, "is_active": u.is_active, "current_assigned_hours": float(w.current_assigned_hours or 0), "max_weekly_hours": float(w.max_available_hours_per_week or 40), "workload_ratio": float(w.workload_ratio or 0), "remaining_hours": round( max( 0.0, float(w.max_available_hours_per_week or 40) - float(w.current_assigned_hours or 0), ), 2, ), "updated_at": w.updated_at.isoformat() if w.updated_at else None, } user = ( await db.execute( select(User).where(and_(User.tenant_id == tenant_id, User.id == user_id)) ) ).scalar_one_or_none() if not user: return None workload = await ensure_user_workload(db, tenant_id, user_id) await db.commit() return { "user_id": str(user.id), "user_name": user.name, "username": user.username, "is_active": user.is_active, "current_assigned_hours": float(workload.current_assigned_hours or 0), "max_weekly_hours": float(workload.max_available_hours_per_week or 40), "workload_ratio": float(workload.workload_ratio or 0), "remaining_hours": round( max( 0.0, float(workload.max_available_hours_per_week or 40) - float(workload.current_assigned_hours or 0), ), 2, ), "updated_at": workload.updated_at.isoformat() if workload.updated_at else None, } async def update_user_max_weekly_hours( db: AsyncSession, tenant_id: int, user_id: int, max_weekly_hours: float, ) -> Optional[dict]: """更新每周可用工时并重算负载率。""" user = ( await db.execute( select(User).where(and_(User.tenant_id == tenant_id, User.id == user_id)) ) ).scalar_one_or_none() if not user: return None workload = await ensure_user_workload(db, tenant_id, user_id) workload.max_available_hours_per_week = max(1.0, float(max_weekly_hours)) await recalculate_user_workload_ratio(db, tenant_id, user_id, commit=False) await db.commit() await db.refresh(workload) return { "user_id": str(user.id), "max_weekly_hours": float(workload.max_available_hours_per_week), "workload_ratio": float(workload.workload_ratio or 0), } async def list_user_assigned_tasks( db: AsyncSession, tenant_id: int, user_id: int, *, status: Optional[str] = None, priority: Optional[str] = None, ) -> list[dict]: """查询用户任务列表。""" query = select(Task).where( and_( Task.tenant_id == tenant_id, Task.assignee_id == user_id, ) ) if status: query = query.where(Task.status == status) if priority: query = query.where(Task.priority == priority) rows = (await db.execute(query.order_by(Task.updated_at.desc()))).scalars().all() return [ { "id": str(t.id), "title": t.title, "project_id": str(t.project_id), "priority": t.priority, "status": t.status, "estimated_hours": float(t.estimated_hours or 0), "due_date": t.due_date.isoformat() if t.due_date else None, } for t in rows ]