diff --git a/backend/app/api/v1/endpoints/dashboard.py b/backend/app/api/v1/endpoints/dashboard.py index 1a6e73a..e6a5b64 100644 --- a/backend/app/api/v1/endpoints/dashboard.py +++ b/backend/app/api/v1/endpoints/dashboard.py @@ -11,6 +11,7 @@ from app.services.dashboard_service import ( get_completed_tasks, CompletedTask, get_rejected_tasks, RejectedTask, get_user_operations, UserOperation, + get_user_operation_detail, OperationDetail, get_people_workload, PersonWorkload, get_people_history, PersonHistoryRecord, search_product_messages, ProductMessageList, @@ -83,6 +84,22 @@ async def user_operations( return await get_user_operations(db, since=since_dt, until=until_dt) +@router.get("/user-operations/detail", response_model=list[OperationDetail]) +async def user_operations_detail( + user_id: str = Query(..., description="人员ID(username)"), + action_type: str = Query(..., description="操作类型: receive/transfer/record"), + since: str | None = Query(None, description="起始日期 ISO"), + until: str | None = Query(None, description="截止日期 ISO"), + 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_user_operation_detail( + db, user_id, action_type, since=since_dt, until=until_dt, + ) + + @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 2b6a8ea..31c6252 100644 --- a/backend/app/services/dashboard_service.py +++ b/backend/app/services/dashboard_service.py @@ -72,6 +72,14 @@ class UserOperation(BaseModel): total: int = 0 # 总操作次数 +class OperationDetail(BaseModel): + task_name: str # 任务/工序名 + product_sn: str # 产品身份证 + material_name: str # 设备名称 + remark: str | None # 备注/说明 + time: str # 操作时间 ISO(BEIJING_TZ) + + class PersonDevice(BaseModel): product_id: str serial_number: str # 16位HEX身份证 @@ -583,6 +591,90 @@ async def get_user_operations( return items +# ============================================================ +# 人员操作明细(点击数字下钻 — 接收/转交/上传备注) +# ============================================================ + +async def get_user_operation_detail( + db: AsyncSession, + user_id: str, + action_type: str, + since: datetime | None = None, + until: datetime | None = None, +) -> list[OperationDetail]: + """查询某人在指定时段内的某类操作明细。 + + action_type: + - receive: 接收(task_logs.action_type='receive') + - transfer: 转交(task_logs.action_type='complete') + - record: 上传备注(该人名下任务的手动备注,排除系统自动生成的) + """ + from app.models.task_log import TaskLog + from app.models.task import Task, TaskRecord + from app.models.product import Product + from app.core.time_utils import BEIJING_TZ + + def _to_iso(dt) -> str: + if not dt: + return "" + if dt.tzinfo is None: + from datetime import timezone as dt_timezone + dt = dt.replace(tzinfo=dt_timezone.utc).astimezone(BEIJING_TZ) + else: + dt = dt.astimezone(BEIJING_TZ) + return dt.isoformat() + + items: list[OperationDetail] = [] + + if action_type in ("receive", "transfer"): + act = "receive" if action_type == "receive" else "complete" + stmt = ( + select(TaskLog.created_at, Task.task_name, Product.serial_number, Product.material_name, TaskLog.remark) + .join(Task, TaskLog.task_id == Task.id) + .join(Product, Task.product_id == Product.id) + .where(TaskLog.operator_id == user_id, TaskLog.action_type == act) + .order_by(TaskLog.created_at.desc()) + ) + if since: + stmt = stmt.where(TaskLog.created_at >= since) + if until: + stmt = stmt.where(TaskLog.created_at <= until) + rows = (await db.execute(stmt)).all() + for r in rows: + items.append(OperationDetail( + task_name=r[1] or "", + product_sn=r[2] or "", + material_name=r[3] or "", + remark=r[4] or "", + time=_to_iso(r[0]), + )) + elif action_type == "record": + stmt = ( + select(TaskRecord.created_at, Task.task_name, Product.serial_number, Product.material_name, TaskRecord.remark) + .join(Task, TaskRecord.task_id == Task.id) + .join(Product, Task.product_id == Product.id) + .where( + Task.assignee_id == user_id, + or_(TaskRecord.remark.is_(None), ~TaskRecord.remark.like("[%")), + ) + .order_by(TaskRecord.created_at.desc()) + ) + if since: + stmt = stmt.where(TaskRecord.created_at >= since) + if until: + stmt = stmt.where(TaskRecord.created_at <= until) + rows = (await db.execute(stmt)).all() + for r in rows: + items.append(OperationDetail( + task_name=r[1] or "", + product_sn=r[2] or "", + material_name=r[3] or "", + remark=r[4] or "", + time=_to_iso(r[0]), + )) + return items + + # ============================================================ # 人员负载(按人聚合在制品设备 — 独立「人员看板」) # ============================================================