diff --git a/backend/app/api/v1/endpoints/dashboard.py b/backend/app/api/v1/endpoints/dashboard.py index e6a5b64..9fe7a1e 100644 --- a/backend/app/api/v1/endpoints/dashboard.py +++ b/backend/app/api/v1/endpoints/dashboard.py @@ -12,6 +12,7 @@ from app.services.dashboard_service import ( get_rejected_tasks, RejectedTask, get_user_operations, UserOperation, get_user_operation_detail, OperationDetail, + get_wip_matrix, WipMatrixRow, get_people_workload, PersonWorkload, get_people_history, PersonHistoryRecord, search_product_messages, ProductMessageList, @@ -100,6 +101,15 @@ async def user_operations_detail( ) +@router.get("/wip-matrix", response_model=list[WipMatrixRow]) +async def wip_matrix( + dimension: str = Query("assignee", description="聚合维度: assignee(人员) / task_name(工序)"), + db: AsyncSession = Depends(get_db), +): + """在制品分布透视表 — 规格型号 × 人员/工序 的设备数量交叉聚合""" + return await get_wip_matrix(db, dimension=dimension) + + @router.get("/people-workload", response_model=list[PersonWorkload]) async def people_workload( db: AsyncSession = Depends(get_db), diff --git a/backend/app/services/dashboard_service.py b/backend/app/services/dashboard_service.py index d1ddee9..74b105e 100644 --- a/backend/app/services/dashboard_service.py +++ b/backend/app/services/dashboard_service.py @@ -80,6 +80,13 @@ class OperationDetail(BaseModel): time: str # 操作时间 ISO(BEIJING_TZ) +class WipMatrixRow(BaseModel): + spec_model: str # 规格型号(Y 轴) + dimension_key: str # 人员姓名 或 工序名称(X 轴) + count: int = 0 # 该交叉点的设备数量 + assignees: list[str] = [] # 该交叉点涉及的主负责人(中文名,去重) + + class PersonDevice(BaseModel): product_id: str serial_number: str # 16位HEX身份证 @@ -593,6 +600,64 @@ async def get_user_operations( return items +# ============================================================ +# 在制品分布透视表(WIP Matrix:规格型号 × 人员/工序) +# ============================================================ + +async def get_wip_matrix( + db: AsyncSession, + dimension: str = "assignee", +) -> list[WipMatrixRow]: + """在制品交叉聚合:Y=规格型号,X=人员 或 工序,单元格=设备数量。 + + dimension: + - assignee: 按任务负责人聚合(dimension_key 为中文姓名) + - task_name: 按工序名聚合(dimension_key 为工序名,附主负责人) + """ + from app.models.task import Task, TASK_STATUS_WIP, TASK_STATUS_PENDING + from app.models.product import Product + + dim_expr = Task.task_name if dimension == "task_name" else Task.assignee_id + + stmt = ( + select( + Product.spec_model, + dim_expr, + func.count(Product.id), + func.array_agg(func.distinct(Task.assignee_id)), + ) + .join(Task, Task.product_id == Product.id) + .where(Task.status.in_([TASK_STATUS_WIP, TASK_STATUS_PENDING])) + .group_by(Product.spec_model, dim_expr) + .order_by(Product.spec_model, dim_expr) + ) + result = await db.execute(stmt) + rows = result.all() + + # 收集负责人 ID → 中文名 + raw_ids: set[str] = set() + for r in rows: + for aid in (r[3] or []): + if aid: + raw_ids.add(aid) + name_map: dict[str, str] = {} + if raw_ids: + from app.services.mom_cache import get_display_names + name_map = get_display_names(list(raw_ids)) + + items: list[WipMatrixRow] = [] + for spec, dim_key, cnt, assignee_ids in rows: + dim_display = name_map.get(dim_key or "", dim_key or "未分配") if dimension == "assignee" else (dim_key or "—") + assignees = [name_map.get(a, a) for a in (assignee_ids or []) if a] or [] + items.append(WipMatrixRow( + spec_model=spec or "未知型号", + dimension_key=dim_display, + count=cnt or 0, + assignees=assignees, + )) + return items + + # ============================================================ # 人员操作明细(点击数字下钻 — 接收/转交/上传备注) # ============================================================