Compare commits
2 Commits
d757c7985b
...
b088d25df9
| Author | SHA1 | Date | |
|---|---|---|---|
| b088d25df9 | |||
| 7b7dbbb0d8 |
@ -672,15 +672,17 @@ async def get_wip_matrix(
|
|||||||
Product.material_name,
|
Product.material_name,
|
||||||
Task.task_name,
|
Task.task_name,
|
||||||
Task.assignee_id,
|
Task.assignee_id,
|
||||||
|
Task.status.label("task_status"),
|
||||||
Task.created_at,
|
Task.created_at,
|
||||||
Task.completed_at,
|
Task.completed_at,
|
||||||
Product.current_location_id,
|
Product.current_location_id,
|
||||||
Product.overall_status,
|
Product.overall_status,
|
||||||
Product.status,
|
Product.status,
|
||||||
)
|
)
|
||||||
.join(Task, Task.product_id == Product.id)
|
.outerjoin(Task, Task.product_id == Product.id)
|
||||||
.where(
|
.where(
|
||||||
or_(
|
or_(
|
||||||
|
Task.id.is_(None), # 🔧 允许该产品完全没有任务记录(新建档/待接收)
|
||||||
Task.parent_task_id.is_(None),
|
Task.parent_task_id.is_(None),
|
||||||
Task.task_type.in_(["TRANSFER", "RECOVERY", "WAREHOUSE"]),
|
Task.task_type.in_(["TRANSFER", "RECOVERY", "WAREHOUSE"]),
|
||||||
)
|
)
|
||||||
@ -692,7 +694,7 @@ async def get_wip_matrix(
|
|||||||
# 每台设备 → (spec, 当前工序/负责人, 当前负责人ID)
|
# 每台设备 → (spec, 当前工序/负责人, 当前负责人ID)
|
||||||
device_cur: dict[str, tuple] = {}
|
device_cur: dict[str, tuple] = {}
|
||||||
seen: set[str] = set()
|
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:
|
if pid in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(pid)
|
seen.add(pid)
|
||||||
@ -730,41 +732,65 @@ async def get_wip_matrix(
|
|||||||
elif overall_status == "待仓库收货" or loc == "virtual_warehouse":
|
elif overall_status == "待仓库收货" or loc == "virtual_warehouse":
|
||||||
key = "已完成"
|
key = "已完成"
|
||||||
else:
|
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:
|
else:
|
||||||
key = assignee or "未分配"
|
key = assignee or "未分配"
|
||||||
device_cur[pid] = (spec or "未知型号", key, assignee or "", product_name or "")
|
device_cur[pid] = (spec or "未知型号", key, assignee or "", product_name or "")
|
||||||
|
|
||||||
# 聚合:规格 × 当前工序 → 设备数;同时收集负责人 与 产品名称
|
from collections import Counter
|
||||||
|
|
||||||
|
# 聚合:规格 × 当前工序 → 设备数;负责人按人头计数,空值显式计为「未分配」
|
||||||
agg: dict[tuple, int] = {}
|
agg: dict[tuple, int] = {}
|
||||||
assignee_map: dict[tuple, set] = {}
|
assignee_counter: dict[tuple, Counter] = {}
|
||||||
product_name_map: dict[tuple, str] = {}
|
product_name_map: dict[tuple, str] = {}
|
||||||
for spec, key, assignee, product_name in device_cur.values():
|
for spec, key, assignee, product_name in device_cur.values():
|
||||||
k = (spec, key)
|
k = (spec, key)
|
||||||
agg[k] = agg.get(k, 0) + 1
|
agg[k] = agg.get(k, 0) + 1
|
||||||
product_name_map.setdefault(k, product_name)
|
product_name_map.setdefault(k, product_name)
|
||||||
if assignee:
|
raw = assignee if assignee and str(assignee).strip() not in ("", "—", "-", "null", "None") else ""
|
||||||
assignee_map.setdefault(k, set()).add(assignee)
|
assignee_counter.setdefault(k, Counter())[raw] += 1
|
||||||
|
|
||||||
# 负责人 ID → 中文名
|
# 负责人 ID → 中文名
|
||||||
raw_ids: set[str] = set()
|
raw_ids: set[str] = set()
|
||||||
for s in assignee_map.values():
|
for counter in assignee_counter.values():
|
||||||
raw_ids |= s
|
raw_ids |= set(counter.keys())
|
||||||
|
raw_ids.discard("")
|
||||||
name_map: dict[str, str] = {}
|
name_map: dict[str, str] = {}
|
||||||
if raw_ids:
|
if raw_ids:
|
||||||
from app.services.mom_cache import get_display_names
|
from app.services.mom_cache import get_display_names
|
||||||
name_map = get_display_names(list(raw_ids))
|
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] = []
|
items: list[WipMatrixRow] = []
|
||||||
for (spec, key), cnt in agg.items():
|
for (spec, key), cnt in agg.items():
|
||||||
dim_display = name_map.get(key, key) if dimension == "assignee" else key
|
dim_display = _disp(key) if dimension == "assignee" else key
|
||||||
assignees = [name_map.get(a, a) for a in assignee_map.get((spec, key), set())] or []
|
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(
|
items.append(WipMatrixRow(
|
||||||
spec_model=spec,
|
spec_model=spec,
|
||||||
product_name=product_name_map.get((spec, key), ""),
|
product_name=product_name_map.get((spec, key), ""),
|
||||||
dimension_key=dim_display,
|
dimension_key=dim_display,
|
||||||
count=cnt,
|
count=cnt,
|
||||||
assignees=assignees,
|
assignees=labels,
|
||||||
))
|
))
|
||||||
items.sort(key=lambda x: (x.spec_model, x.dimension_key))
|
items.sort(key=lambda x: (x.spec_model, x.dimension_key))
|
||||||
return items
|
return items
|
||||||
@ -812,9 +838,10 @@ async def get_wip_matrix_detail(
|
|||||||
Task.completed_at,
|
Task.completed_at,
|
||||||
Task.received_at,
|
Task.received_at,
|
||||||
)
|
)
|
||||||
.join(Task, Task.product_id == Product.id)
|
.outerjoin(Task, Task.product_id == Product.id)
|
||||||
.where(
|
.where(
|
||||||
or_(
|
or_(
|
||||||
|
Task.id.is_(None), # 🔧 允许该产品完全没有任务记录(新建档/待接收)
|
||||||
Task.parent_task_id.is_(None),
|
Task.parent_task_id.is_(None),
|
||||||
Task.task_type.in_(["TRANSFER", "RECOVERY", "WAREHOUSE"]),
|
Task.task_type.in_(["TRANSFER", "RECOVERY", "WAREHOUSE"]),
|
||||||
)
|
)
|
||||||
@ -863,7 +890,11 @@ async def get_wip_matrix_detail(
|
|||||||
elif overall == "待仓库收货" or loc == "virtual_warehouse":
|
elif overall == "待仓库收货" or loc == "virtual_warehouse":
|
||||||
key = "已完成"
|
key = "已完成"
|
||||||
else:
|
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:
|
if process and key != process:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -146,6 +146,13 @@ const SlimCard = memo(function SlimCard({
|
|||||||
⏰ {fmtTime(task.created_at)}
|
⏰ {fmtTime(task.created_at)}
|
||||||
{task.completed_at ? ` → ${fmtTime(task.completed_at)}` : " → 至今"}
|
{task.completed_at ? ` → ${fmtTime(task.completed_at)}` : " → 至今"}
|
||||||
</p>
|
</p>
|
||||||
|
{/* 驳回/返工原因:原 REJECTED 取 reject_reason;返工节点取含“驳回/返工”的备注 */}
|
||||||
|
{(() => {
|
||||||
|
const rj = task.reject_reason;
|
||||||
|
const rm = task.remark;
|
||||||
|
const txt = rj ? `❌ 驳回: ${rj}` : (rm && /驳回|返工/.test(rm) ? rm : null);
|
||||||
|
return txt ? <p className={`${sz.sub} text-red-500 whitespace-pre-wrap`}>{txt}</p> : null;
|
||||||
|
})()}
|
||||||
{legacy && <p className={`${sz.sub} text-orange-500`}>源自: {assigneeName || (findParent(task)?.assignee_id) || "历史任务"}</p>}
|
{legacy && <p className={`${sz.sub} text-orange-500`}>源自: {assigneeName || (findParent(task)?.assignee_id) || "历史任务"}</p>}
|
||||||
{isNestedSpawn && <p className={`${sz.sub} text-purple-500`}>协助: {findParent(task)?.assignee_id || "—"}</p>}
|
{isNestedSpawn && <p className={`${sz.sub} text-purple-500`}>协助: {findParent(task)?.assignee_id || "—"}</p>}
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { fetchDeviceRecords, type DeviceRecord } from "../services/analyticsApi"
|
|||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
// 🔧 固定标准主轴:核心工序 + 状态列始终显示(即使计数为 0),表头不随操作内容增减
|
// 🔧 固定标准主轴:核心工序 + 状态列始终显示(即使计数为 0),表头不随操作内容增减
|
||||||
const FIXED_STEP_COLUMNS = ["备货", "生产", "测试", "维修", "待确认", "已完成", "已入库", "已出库"];
|
const FIXED_STEP_COLUMNS = ["待接收", "备货", "生产", "测试", "维修", "已完成", "已入库", "已出库"];
|
||||||
|
|
||||||
// ⏰ 时间筛选(与全局概览一致)
|
// ⏰ 时间筛选(与全局概览一致)
|
||||||
type DateRangeKey = "today" | "7d" | "30d" | "custom";
|
type DateRangeKey = "today" | "7d" | "30d" | "custom";
|
||||||
@ -42,6 +42,21 @@ function imageUrl(u: string) {
|
|||||||
return base + path;
|
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() {
|
export default function MatrixBoard() {
|
||||||
const [dateKey, setDateKey] = useState<DateRangeKey>("today");
|
const [dateKey, setDateKey] = useState<DateRangeKey>("today");
|
||||||
const [customRange, setCustomRange] = useState<[Dayjs, Dayjs] | null>(null);
|
const [customRange, setCustomRange] = useState<[Dayjs, Dayjs] | null>(null);
|
||||||
@ -140,11 +155,8 @@ export default function MatrixBoard() {
|
|||||||
>
|
>
|
||||||
<div className={`text-sm font-bold leading-none ${v > 0 ? "text-blue-600" : "text-gray-800"}`}>{v}</div>
|
<div className={`text-sm font-bold leading-none ${v > 0 ? "text-blue-600" : "text-gray-800"}`}>{v}</div>
|
||||||
{assignees.length > 0 && (
|
{assignees.length > 0 && (
|
||||||
<div
|
<div className="mx-auto max-w-[92px] text-xs leading-tight text-gray-500">
|
||||||
className="mx-auto max-w-[90px] truncate text-[10px] leading-tight text-gray-400"
|
{summarizeAssignees(assignees)}
|
||||||
title={assignees.join("、")}
|
|
||||||
>
|
|
||||||
{assignees.join("、")}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
</view>
|
</view>
|
||||||
<!-- 时间独占一行 -->
|
<!-- 时间独占一行 -->
|
||||||
<text class="fc-time">⏰ {{ fmtDate(node.created_at) }}{{ node.completed_at ? '→' + fmtDate(node.completed_at) : '→至今' }}</text>
|
<text class="fc-time">⏰ {{ fmtDate(node.created_at) }}{{ node.completed_at ? '→' + fmtDate(node.completed_at) : '→至今' }}</text>
|
||||||
|
<!-- 驳回/返工原因:原 REJECTED 取 reject_reason;返工节点取含“驳回/返工”的备注 -->
|
||||||
|
<text v-if="reasonText" style="display:block;margin:8rpx 0 0;font-size:22rpx;line-height:1.4;color:#dc2626;font-weight:600;">❌ {{ reasonText }}</text>
|
||||||
<!-- 卡片底部:全宽蓝色日志条(对齐 Web 端) -->
|
<!-- 卡片底部:全宽蓝色日志条(对齐 Web 端) -->
|
||||||
<view v-if="node.records && node.records.length" class="ft-log-bar" @tap.stop="$emit('viewRecords', node)">
|
<view v-if="node.records && node.records.length" class="ft-log-bar" @tap.stop="$emit('viewRecords', node)">
|
||||||
查看操作日志 ({{ node.records.length }}条) ›
|
查看操作日志 ({{ node.records.length }}条) ›
|
||||||
@ -34,6 +36,13 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ["viewRecords"],
|
emits: ["viewRecords"],
|
||||||
computed: {
|
computed: {
|
||||||
|
// 驳回/返工原因:原 REJECTED 有 reject_reason;返工节点原因在 remark(形如“返工任务(驳回自…原因…)”)
|
||||||
|
reasonText() {
|
||||||
|
const n = this.node || {};
|
||||||
|
if (n.reject_reason) return `驳回: ${n.reject_reason}`;
|
||||||
|
const rm = n.remark;
|
||||||
|
return rm && (rm.indexOf("驳回") >= 0 || rm.indexOf("返工") >= 0) ? rm : "";
|
||||||
|
},
|
||||||
isMainNode() {
|
isMainNode() {
|
||||||
return !this.node.parent_task_id
|
return !this.node.parent_task_id
|
||||||
|| this.node.task_type === "TRANSFER"
|
|| this.node.task_type === "TRANSFER"
|
||||||
|
|||||||
@ -41,6 +41,9 @@
|
|||||||
|
|
||||||
<text class="tc-name">{{ card.task_name }}</text>
|
<text class="tc-name">{{ card.task_name }}</text>
|
||||||
|
|
||||||
|
<!-- 被驳回原任务:显示驳回原因(返工节点原因由下方“备注”框承载) -->
|
||||||
|
<text v-if="card.reject_reason" style="display:block;margin-top:6rpx;font-size:22rpx;line-height:1.4;color:#dc2626;font-weight:600;">❌ 驳回: {{ card.reject_reason }}</text>
|
||||||
|
|
||||||
<view class="tc-meta">
|
<view class="tc-meta">
|
||||||
<view class="tc-meta-row">
|
<view class="tc-meta-row">
|
||||||
<!-- 仓储任务:assignee 为 null,直接显示系统名,不取用户头像 -->
|
<!-- 仓储任务:assignee 为 null,直接显示系统名,不取用户头像 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user