feat(dashboard): 看板增强——流转完成率下钻明细 + 人员看板(按人聚合在制品设备)
This commit is contained in:
@ -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码/物料名/留言人/内容"),
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 协同留言搜索(上帝视角 — 全厂)
|
||||
# ============================================================
|
||||
|
||||
Reference in New Issue
Block a user