feat(dashboard): 看板增强——流转完成率下钻明细 + 人员看板(按人聚合在制品设备)

This commit is contained in:
2026-08-13 12:00:12 +08:00
parent b6ef0751e0
commit c2e6ed240b
7 changed files with 452 additions and 8 deletions

View File

@ -6,6 +6,8 @@ from app.core.database import get_db
from app.services.dashboard_service import (
get_dashboard_stats, DashboardStats,
get_wip_tasks, WipTask,
get_completed_tasks, CompletedTask,
get_people_workload, PersonWorkload,
search_product_messages, ProductMessageList,
)
@ -38,6 +40,27 @@ async def wip_tasks(
return await get_wip_tasks(db, limit)
@router.get("/completed-tasks", response_model=list[CompletedTask])
async def completed_tasks(
since: str | None = Query(None, description="起始日期 ISO 如 2026-08-01T00:00:00"),
until: str | None = Query(None, description="截止日期 ISO"),
limit: int = Query(200, ge=1, le=500),
db: AsyncSession = Depends(get_db),
):
"""流转完成率下钻 — 按时段查询已完成任务明细"""
since_dt = datetime.fromisoformat(since) if since else None
until_dt = datetime.fromisoformat(until) if until else None
return await get_completed_tasks(db, since=since_dt, until=until_dt, limit=limit)
@router.get("/people-workload", response_model=list[PersonWorkload])
async def people_workload(
db: AsyncSession = Depends(get_db),
):
"""人员负载 — 按负责人聚合当前在制品设备数(独立人员看板)"""
return await get_people_workload(db)
@router.get("/messages", response_model=ProductMessageList)
async def dashboard_messages(
keyword: str = Query("", description="搜索: SN码/物料名/留言人/内容"),

View File

@ -37,6 +37,33 @@ class WipTask(BaseModel):
duration_hours: float
class CompletedTask(BaseModel):
task_id: str
task_name: str # 完成的工序节点
assignee: str # 完成人中文姓名
product_sn: str # 16位HEX身份证
external_serial: str | None # 业务序列号
material_name: str # 产品名称(物料名称)
spec_model: str # 规格型号
completed_at: str # 完成时间 ISO
class PersonDevice(BaseModel):
product_id: str
serial_number: str # 16位HEX身份证
external_serial: str | None # 业务序列号
material_name: str # 产品名称
spec_model: str # 规格型号
task_status: str # 该设备名下的状态 WIP/PENDING
class PersonWorkload(BaseModel):
assignee_id: str
assignee_name: str # 中文姓名
device_count: int
devices: list[PersonDevice]
class ProductMessageItem(BaseModel):
id: str
content: str
@ -180,6 +207,129 @@ async def get_wip_tasks(db: AsyncSession, limit: int = 20) -> list[WipTask]:
return wip_list[:limit]
# ============================================================
# 已完成任务明细(流转完成率下钻 — 按时段过滤)
# ============================================================
async def get_completed_tasks(
db: AsyncSession,
since: datetime | None = None,
until: datetime | None = None,
limit: int = 200,
) -> list[CompletedTask]:
"""按时段查询已完成任务明细(上帝视角),用于「流转完成率」卡片下钻。"""
from app.models.task import Task, TASK_STATUS_COMPLETED
from app.models.product import Product
from app.core.time_utils import BEIJING_TZ
stmt = (
select(Task, Product.serial_number, Product.external_serial, Product.material_name, Product.spec_model)
.join(Product, Task.product_id == Product.id)
.where(Task.status == TASK_STATUS_COMPLETED)
)
if since:
stmt = stmt.where(Task.completed_at >= since)
if until:
stmt = stmt.where(Task.completed_at <= until)
stmt = stmt.order_by(Task.completed_at.desc()).limit(limit)
result = await db.execute(stmt)
rows = result.all()
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)
items: list[CompletedTask] = []
for task, sn, ext, mat, spec in rows:
t = task.completed_at
if t:
if t.tzinfo is None:
from datetime import timezone as dt_timezone
t = t.replace(tzinfo=dt_timezone.utc).astimezone(BEIJING_TZ)
else:
t = t.astimezone(BEIJING_TZ)
time_str = t.isoformat() if t else ""
items.append(CompletedTask(
task_id=str(task.id),
task_name=task.task_name,
assignee=name_map.get(task.assignee_id or "", task.assignee_id or "未分配"),
product_sn=sn or "",
external_serial=ext or None,
material_name=mat or "",
spec_model=spec or "",
completed_at=time_str,
))
return items
# ============================================================
# 人员负载(按人聚合在制品设备 — 独立「人员看板」)
# ============================================================
async def get_people_workload(db: AsyncSession) -> list[PersonWorkload]:
"""上帝视角 — 按负责人聚合当前在制品设备WIP/PENDING 任务product 去重)。"""
from app.models.task import Task, TASK_STATUS_PENDING, TASK_STATUS_WIP
from app.models.product import Product
stmt = (
select(
Task.assignee_id,
Product.id, Product.serial_number, Product.external_serial,
Product.material_name, Product.spec_model,
Task.status,
)
.join(Product, Task.product_id == Product.id)
.where(
Task.status.in_([TASK_STATUS_PENDING, TASK_STATUS_WIP]),
Task.assignee_id.isnot(None),
)
.order_by(Task.assignee_id, Product.created_at.desc())
)
result = await db.execute(stmt)
rows = result.all()
# 按 assignee 聚合product 去重;状态优先级 WIP > PENDING
by_assignee: dict[str, dict[str, PersonDevice]] = {}
for row in rows:
assignee = row[0]
product_id = str(row[1])
status = row[6] or ""
devices = by_assignee.setdefault(assignee, {})
if product_id in devices:
# 已有该设备:若新状态为 WIP 则提升(更活跃)
if status == "WIP":
devices[product_id].task_status = "WIP"
continue
devices[product_id] = PersonDevice(
product_id=product_id,
serial_number=row[2] or "",
external_serial=row[3] or None,
material_name=row[4] or "",
spec_model=row[5] or "",
task_status=status,
)
raw_ids = list(by_assignee.keys())
name_map: dict[str, str] = {}
if raw_ids:
from app.services.mom_cache import get_display_names
name_map = get_display_names(raw_ids)
workloads = [
PersonWorkload(
assignee_id=assignee,
assignee_name=name_map.get(assignee, assignee),
device_count=len(devices),
devices=list(devices.values()),
)
for assignee, devices in by_assignee.items()
]
workloads.sort(key=lambda w: w.device_count, reverse=True)
return workloads
# ============================================================
# 协同留言搜索(上帝视角 — 全厂)
# ============================================================