diff --git a/backend/app/services/dashboard_service.py b/backend/app/services/dashboard_service.py index 7905a24..f9f0289 100644 --- a/backend/app/services/dashboard_service.py +++ b/backend/app/services/dashboard_service.py @@ -672,15 +672,17 @@ async def get_wip_matrix( Product.material_name, Task.task_name, Task.assignee_id, + Task.status.label("task_status"), Task.created_at, Task.completed_at, Product.current_location_id, Product.overall_status, Product.status, ) - .join(Task, Task.product_id == Product.id) + .outerjoin(Task, Task.product_id == Product.id) .where( or_( + Task.id.is_(None), # 🔧 允许该产品完全没有任务记录(新建档/待接收) Task.parent_task_id.is_(None), Task.task_type.in_(["TRANSFER", "RECOVERY", "WAREHOUSE"]), ) @@ -692,7 +694,7 @@ async def get_wip_matrix( # 每台设备 → (spec, 当前工序/负责人, 当前负责人ID) device_cur: dict[str, tuple] = {} seen: set[str] = set() - for pid, spec, product_name, task_name, assignee, created, completed, loc, overall_status, product_status in rows: + for pid, spec, product_name, task_name, assignee, tstatus, created, completed, loc, overall_status, product_status in rows: if pid in seen: continue seen.add(pid) @@ -730,41 +732,65 @@ async def get_wip_matrix( elif overall_status == "待仓库收货" or loc == "virtual_warehouse": key = "已完成" else: - key = task_name or "—" + # 🔧 对齐全景/产品管理:PENDING(含 task_name=待确认) 与 无任务新品 统一归「待接收」 + if tstatus == "PENDING" or (tstatus is None and task_name is None): + key = "待接收" + else: + key = task_name if (task_name and task_name != "—") else "待接收" else: key = assignee or "未分配" device_cur[pid] = (spec or "未知型号", key, assignee or "", product_name or "") - # 聚合:规格 × 当前工序 → 设备数;同时收集负责人 与 产品名称 + from collections import Counter + + # 聚合:规格 × 当前工序 → 设备数;负责人按人头计数,空值显式计为「未分配」 agg: dict[tuple, int] = {} - assignee_map: dict[tuple, set] = {} + assignee_counter: dict[tuple, Counter] = {} product_name_map: dict[tuple, str] = {} for spec, key, assignee, product_name in device_cur.values(): k = (spec, key) agg[k] = agg.get(k, 0) + 1 product_name_map.setdefault(k, product_name) - if assignee: - assignee_map.setdefault(k, set()).add(assignee) + raw = assignee if assignee and str(assignee).strip() not in ("", "—", "-", "null", "None") else "" + assignee_counter.setdefault(k, Counter())[raw] += 1 # 负责人 ID → 中文名 raw_ids: set[str] = set() - for s in assignee_map.values(): - raw_ids |= s + for counter in assignee_counter.values(): + raw_ids |= set(counter.keys()) + raw_ids.discard("") 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)) + def _disp(raw: str) -> str: + """负责人显示名:空值统一为 未分配""" + return "未分配" if not raw else name_map.get(raw, raw) + + def _fmt_label(raw: str, n: int) -> str: + """负责人标签:>1 台必须带数量后缀;1 台不带""" + disp = _disp(raw) + return f"{disp}({n})" if n > 1 else disp + items: list[WipMatrixRow] = [] for (spec, key), cnt in agg.items(): - dim_display = name_map.get(key, key) if dimension == "assignee" else key - assignees = [name_map.get(a, a) for a in assignee_map.get((spec, key), set())] or [] + dim_display = _disp(key) if dimension == "assignee" else key + counter = assignee_counter.get((spec, key), Counter()) + # 负责人名单:未分配固定排最前,其余按台数降序、名称升序;带数量后缀(>1 台) + labels = [ + _fmt_label(raw, n) + for raw, n in sorted( + counter.items(), + key=lambda kv: (0 if kv[0] == "" else 1, -kv[1], _disp(kv[0])), + ) + ] items.append(WipMatrixRow( spec_model=spec, product_name=product_name_map.get((spec, key), ""), dimension_key=dim_display, count=cnt, - assignees=assignees, + assignees=labels, )) items.sort(key=lambda x: (x.spec_model, x.dimension_key)) return items @@ -812,9 +838,10 @@ async def get_wip_matrix_detail( Task.completed_at, Task.received_at, ) - .join(Task, Task.product_id == Product.id) + .outerjoin(Task, Task.product_id == Product.id) .where( or_( + Task.id.is_(None), # 🔧 允许该产品完全没有任务记录(新建档/待接收) Task.parent_task_id.is_(None), Task.task_type.in_(["TRANSFER", "RECOVERY", "WAREHOUSE"]), ) @@ -863,7 +890,11 @@ async def get_wip_matrix_detail( elif overall == "待仓库收货" or loc == "virtual_warehouse": key = "已完成" else: - key = task_name or "—" + # 🔧 对齐全景/产品管理:PENDING(含 task_name=待确认) 与 无任务新品 统一归「待接收」 + if tstatus == "PENDING" or (tstatus is None and task_name is None): + key = "待接收" + else: + key = task_name if (task_name and task_name != "—") else "待接收" if process and key != process: continue diff --git a/frontend/src/pages/MatrixBoard.tsx b/frontend/src/pages/MatrixBoard.tsx index 5c7e53c..6e6549a 100644 --- a/frontend/src/pages/MatrixBoard.tsx +++ b/frontend/src/pages/MatrixBoard.tsx @@ -12,7 +12,7 @@ import { fetchDeviceRecords, type DeviceRecord } from "../services/analyticsApi" const { RangePicker } = DatePicker; // 🔧 固定标准主轴:核心工序 + 状态列始终显示(即使计数为 0),表头不随操作内容增减 -const FIXED_STEP_COLUMNS = ["备货", "生产", "测试", "维修", "待确认", "已完成", "已入库", "已出库"]; +const FIXED_STEP_COLUMNS = ["待接收", "备货", "生产", "测试", "维修", "已完成", "已入库", "已出库"]; // ⏰ 时间筛选(与全局概览一致) type DateRangeKey = "today" | "7d" | "30d" | "custom"; @@ -42,6 +42,21 @@ function imageUrl(u: string) { return base + path; } +// ─── 单元格负责人聚合预览:数量解析 + TOP 折叠 ──────────────────── +function labelCount(label: string): number { + const m = label.match(/\((\d+)\)\s*$/); + return m ? Number(m[1]) : 1; +} +function summarizeAssignees(list: string[]): string { + if (!list.length) return ""; + // 数量降序(“未分配”同样参与按台数排序),数量最多者排最前 + const sorted = [...list].sort((a, b) => labelCount(b) - labelCount(a)); + if (sorted.length <= 2) return sorted.join("、"); + // ≥3 人:只显示台数最多的第 1 人(去掉数量后缀),折叠为 “等 N 人” + const first = sorted[0].replace(/\(\d+\)\s*$/, ""); + return `${first}等 ${sorted.length} 人`; +} + export default function MatrixBoard() { const [dateKey, setDateKey] = useState("today"); const [customRange, setCustomRange] = useState<[Dayjs, Dayjs] | null>(null); @@ -140,11 +155,8 @@ export default function MatrixBoard() { >
0 ? "text-blue-600" : "text-gray-800"}`}>{v}
{assignees.length > 0 && ( -
- {assignees.join("、")} +
+ {summarizeAssignees(assignees)}
)}