"""Dashboard 统计服务""" from datetime import datetime from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel class DashboardStats(BaseModel): # 产品 products_total: int products_pending: int products_in_progress: int products_completed: int # 任务 tasks_total: int tasks_pending: int tasks_in_progress: int tasks_completed: int tasks_rejected: int tasks_rework: int # 通知 + 留言 unread_notifications: int = 0 unread_messages: int = 0 class WipTask(BaseModel): """在制品 — 当前卡在手里的任务""" task_id: str task_name: str assignee: str # 中文姓名 product_sn: str status: str # PENDING / WIP received_at: str # 接收时间 duration_hours: float # 已滞留小时数 async def get_dashboard_stats(db: AsyncSession) -> DashboardStats: from app.models.product import Product from app.models.task import ( Task, TASK_STATUS_PENDING, TASK_STATUS_WIP, TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED, ) from app.models.notification import Notification from app.models.message import ProductMessage p_total = await db.scalar(select(func.count(Product.id))) p_pending = await db.scalar(select(func.count(Product.id)).where(Product.status == "pending")) p_progress = await db.scalar(select(func.count(Product.id)).where(Product.status == "in_progress")) p_done = await db.scalar(select(func.count(Product.id)).where(Product.status == "completed")) t_total = await db.scalar(select(func.count(Task.id))) t_pending = await db.scalar(select(func.count(Task.id)).where(Task.status == TASK_STATUS_PENDING)) t_progress = await db.scalar(select(func.count(Task.id)).where(Task.status == TASK_STATUS_WIP)) t_done = await db.scalar(select(func.count(Task.id)).where(Task.status == TASK_STATUS_COMPLETED)) t_rejected = await db.scalar(select(func.count(Task.id)).where(Task.status == TASK_STATUS_REJECTED)) t_rework = await db.scalar(select(func.count(Task.id)).where(Task.is_rework.is_(True))) unread_notif = await db.scalar( select(func.count(Notification.id)).where(Notification.is_read.is_(False)) ) # 留言板未读数 — 全局(所有产品下的留言总数) unread_msg = await db.scalar(select(func.count(ProductMessage.id))) return DashboardStats( products_total=p_total or 0, products_pending=p_pending or 0, products_in_progress=p_progress or 0, products_completed=p_done or 0, tasks_total=t_total or 0, tasks_pending=t_pending or 0, tasks_in_progress=t_progress or 0, tasks_completed=t_done or 0, tasks_rejected=t_rejected or 0, tasks_rework=t_rework or 0, unread_notifications=unread_notif or 0, unread_messages=unread_msg or 0, ) async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]: """ 在制品看板 — 当前所有 PENDING/WIP 状态的任务, 按接收时间升序(最早接收的排最前 = 滞留最久)。 """ from app.models.task import ( Task, TASK_STATUS_PENDING, TASK_STATUS_WIP, ) from app.models.product import Product from app.core.time_utils import get_beijing_time, BEIJING_TZ stmt = ( select(Task, Product.serial_number) .join(Product, Task.product_id == Product.id) .where(Task.status.in_([TASK_STATUS_PENDING, TASK_STATUS_WIP])) .order_by(Task.received_at.asc().nulls_first(), Task.created_at.asc()) .limit(limit) ) result = await db.execute(stmt) rows = result.all() # 收集 assignee_id 批量翻译中文姓名 raw_ids = list({t.assignee_id for t, _ in rows if t.assignee_id}) name_map: dict[str, str] = {} if raw_ids: from app.services.mom_cache import get_display_names name_map = get_display_names(raw_ids) now = get_beijing_time() wip_list: list[WipTask] = [] for task, product_sn in rows: # 计算滞留时长 start = task.received_at or task.created_at if start: if start.tzinfo is None: start = start.replace(tzinfo=BEIJING_TZ) else: start = start.astimezone(BEIJING_TZ) hours = round((now - start).total_seconds() / 3600, 1) recv_str = start.strftime("%m-%d %H:%M") else: hours = 0 recv_str = "" wip_list.append(WipTask( task_id=str(task.id), task_name=task.task_name, assignee=name_map.get(task.assignee_id or "", task.assignee_id or "未分配"), product_sn=product_sn or "", status=task.status, received_at=recv_str, duration_hours=hours, )) return wip_list def _action_label(action_type: str) -> str: labels = { "create": "创建任务", "receive": "确认接收", "complete": "完成任务", "transfer": "完工转交", "reject": "品质驳回", "end": "结束分支", "recall": "撤回转交", } return labels.get(action_type, action_type)