fix: 位置回溯父任务优先 + Tab筛选改为本地calcProductStatus过滤
This commit is contained in:
@ -34,19 +34,42 @@ VIRTUAL_WAREHOUSE = "virtual_warehouse"
|
|||||||
ADMIN_ROLES = {"SUPER_ADMIN", "SUPERVISOR"}
|
ADMIN_ROLES = {"SUPER_ADMIN", "SUPERVISOR"}
|
||||||
|
|
||||||
|
|
||||||
async def _recalc_product_location(db: AsyncSession, product_id: uuid.UUID) -> None:
|
async def _recalc_product_location(db: AsyncSession, product_id: uuid.UUID, completed_task_id: uuid.UUID | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
任务完工/结束时触发:沿任务树向上回溯,
|
任务完工/结束时触发:位置回溯 父任务优先 策略。
|
||||||
将产品 current_location 更新为最近一个 WIP 任务的负责人。
|
① 如果完工任务有父任务且父任务未完成 → 强制回溯到父任务负责人
|
||||||
若无进行中任务,位置置空。
|
② 否则查找所有 WIP 任务 → 最新 WIP 负责人
|
||||||
|
③ 无 WIP → 兜底最后完工者
|
||||||
|
④ 全结束 → 置空
|
||||||
"""
|
"""
|
||||||
from sqlalchemy import select as sa_select
|
from sqlalchemy import select as sa_select
|
||||||
|
|
||||||
|
# ① 父任务优先:如果有父任务且未完成 → 位置给父任务负责人
|
||||||
|
if completed_task_id:
|
||||||
|
task_result = await db.execute(
|
||||||
|
sa_select(Task).where(Task.id == completed_task_id)
|
||||||
|
)
|
||||||
|
current_task = task_result.scalar_one_or_none()
|
||||||
|
if current_task and current_task.parent_task_id:
|
||||||
|
parent_result = await db.execute(
|
||||||
|
sa_select(Task).where(Task.id == current_task.parent_task_id)
|
||||||
|
)
|
||||||
|
parent = parent_result.scalar_one_or_none()
|
||||||
|
if parent and parent.status not in (TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED, TASK_STATUS_CANCELED, TASK_STATUS_ARCHIVED):
|
||||||
|
product_result = await db.execute(
|
||||||
|
sa_select(Product).where(Product.id == product_id)
|
||||||
|
)
|
||||||
|
product = product_result.scalar_one_or_none()
|
||||||
|
if product and product.current_location_id != parent.assignee_id:
|
||||||
|
product.current_location_id = parent.assignee_id
|
||||||
|
return
|
||||||
|
|
||||||
|
# ② 无父任务/父任务已完工 → 查找其他 WIP 任务
|
||||||
product_result = await db.execute(sa_select(Product).where(Product.id == product_id))
|
product_result = await db.execute(sa_select(Product).where(Product.id == product_id))
|
||||||
product = product_result.scalar_one_or_none()
|
product = product_result.scalar_one_or_none()
|
||||||
if not product:
|
if not product:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 查找所有 WIP 状态的任务
|
|
||||||
task_result = await db.execute(
|
task_result = await db.execute(
|
||||||
sa_select(Task).where(
|
sa_select(Task).where(
|
||||||
Task.product_id == product_id,
|
Task.product_id == product_id,
|
||||||
@ -56,7 +79,6 @@ async def _recalc_product_location(db: AsyncSession, product_id: uuid.UUID) -> N
|
|||||||
wip_tasks = task_result.scalars().all()
|
wip_tasks = task_result.scalars().all()
|
||||||
|
|
||||||
if wip_tasks:
|
if wip_tasks:
|
||||||
# 有进行中的任务 → 位置更新为最新WIP任务的负责人
|
|
||||||
latest_wip = wip_tasks[0]
|
latest_wip = wip_tasks[0]
|
||||||
new_location = latest_wip.assignee_id or product.current_location_id
|
new_location = latest_wip.assignee_id or product.current_location_id
|
||||||
else:
|
else:
|
||||||
@ -364,8 +386,8 @@ async def end_task(
|
|||||||
await _create_task_log(db, task_id, action_type="end",
|
await _create_task_log(db, task_id, action_type="end",
|
||||||
operator_id=operator_id, remark=f"分支「{task.task_name}」已终止(无下游)")
|
operator_id=operator_id, remark=f"分支「{task.task_name}」已终止(无下游)")
|
||||||
|
|
||||||
# 🔧 位置回溯:分支结束后重新计算产品当前位置
|
# 🔧 位置回溯:分支结束后优先回溯到父任务负责人
|
||||||
await _recalc_product_location(db, task.product_id)
|
await _recalc_product_location(db, task.product_id, task.id)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(task)
|
await db.refresh(task)
|
||||||
@ -806,9 +828,9 @@ async def transfer_task(
|
|||||||
product.current_location_id = real_branches[0][1]
|
product.current_location_id = real_branches[0][1]
|
||||||
product.overall_status = real_branches[0][0]
|
product.overall_status = real_branches[0][0]
|
||||||
|
|
||||||
# 🔧 位置回溯:如果有新任务创建,优先新任务负责人;否则回溯到上级WIP任务
|
# 🔧 位置回溯:如果有新任务创建,优先新任务负责人;否则回溯到父任务
|
||||||
if not real_branches and not has_warehouse:
|
if not real_branches and not has_warehouse:
|
||||||
await _recalc_product_location(db, task.product_id)
|
await _recalc_product_location(db, task.product_id, task_id)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -908,8 +930,8 @@ async def complete_task(
|
|||||||
remark=f"由任务「{task.task_name}」完成后转交创建",
|
remark=f"由任务「{task.task_name}」完成后转交创建",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 🔧 位置回溯:老接口也触发
|
# 🔧 位置回溯:老接口也触发(父任务优先)
|
||||||
await _recalc_product_location(db, task.product_id)
|
await _recalc_product_location(db, task.product_id, task_id)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
@ -55,14 +55,13 @@ export default function AdminTasksPage() {
|
|||||||
const [modalTarget, setModalTarget] = useState<ModalTarget | null>(null);
|
const [modalTarget, setModalTarget] = useState<ModalTarget | null>(null);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// ---- 加载产品列表 ----
|
// ---- 加载产品列表(始终拉全量,不做服务端状态过滤) ----
|
||||||
async function loadProducts(kw: string, st: string) {
|
async function loadProducts(kw: string) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const params: Record<string, string | number> = { limit: 1000 };
|
const params: Record<string, string | number> = { limit: 1000 };
|
||||||
if (kw.trim()) params.keyword = kw.trim();
|
if (kw.trim()) params.keyword = kw.trim();
|
||||||
if (st) params.status = st;
|
|
||||||
const { data } = await api.get<ProductResponse[]>("/products/", { params });
|
const { data } = await api.get<ProductResponse[]>("/products/", { params });
|
||||||
setProducts(data);
|
setProducts(data);
|
||||||
} catch {
|
} catch {
|
||||||
@ -72,28 +71,36 @@ export default function AdminTasksPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首次加载
|
useEffect(() => { loadProducts(keyword); }, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
useEffect(() => {
|
|
||||||
loadProducts(keyword, statusFilter);
|
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
// 🚀 状态筛选 Tab 变化时自动重新查询
|
|
||||||
useEffect(() => {
|
|
||||||
loadProducts(keyword, statusFilter);
|
|
||||||
}, [statusFilter]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
function handleSearch(e?: React.FormEvent) {
|
function handleSearch(e?: React.FormEvent) {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
setExpandedOrders(new Set());
|
setExpandedOrders(new Set());
|
||||||
setExpandedProducts(new Set());
|
setExpandedProducts(new Set());
|
||||||
setTaskTrees({});
|
setTaskTrees({});
|
||||||
loadProducts(keyword, statusFilter);
|
loadProducts(keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 按订单分组 ----
|
/** 🔧 本地计算产品综合状态(与表格列渲染逻辑100%统一) */
|
||||||
|
function calcProductStatus(p: ProductResponse): string {
|
||||||
|
const tree = taskTrees[p.serial_number];
|
||||||
|
if (tree?.task_tree?.length) {
|
||||||
|
if (tree.task_tree.some(t => t.status === "WIP")) return "WIP";
|
||||||
|
if (tree.task_tree.every(t => t.status === "COMPLETED" || t.status === "ARCHIVED")) return "COMPLETED";
|
||||||
|
return tree.task_tree[0].status;
|
||||||
|
}
|
||||||
|
return p.status; // 未展开流转树时兜底产品状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 按订单分组 + 本地状态过滤 ----
|
||||||
const orderGroups = useMemo<OrderGroup[]>(() => {
|
const orderGroups = useMemo<OrderGroup[]>(() => {
|
||||||
const map = new Map<string, ProductResponse[]>();
|
const map = new Map<string, ProductResponse[]>();
|
||||||
for (const p of products) {
|
for (const p of products) {
|
||||||
|
// 🔧 本地过滤:Tab切换时不再请求后端
|
||||||
|
if (statusFilter) {
|
||||||
|
const s = calcProductStatus(p).toUpperCase();
|
||||||
|
if (s !== statusFilter) continue;
|
||||||
|
}
|
||||||
const key = p.order_no || "未绑定订单";
|
const key = p.order_no || "未绑定订单";
|
||||||
if (!map.has(key)) map.set(key, []);
|
if (!map.has(key)) map.set(key, []);
|
||||||
map.get(key)!.push(p);
|
map.get(key)!.push(p);
|
||||||
@ -107,6 +114,7 @@ export default function AdminTasksPage() {
|
|||||||
(p) => p.current_location_id === "virtual_warehouse"
|
(p) => p.current_location_id === "virtual_warehouse"
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
}, [products, statusFilter, taskTrees]);
|
||||||
}, [products]);
|
}, [products]);
|
||||||
|
|
||||||
// ---- 手风琴切换 ----
|
// ---- 手风琴切换 ----
|
||||||
@ -170,7 +178,7 @@ export default function AdminTasksPage() {
|
|||||||
const sn = modalTarget.task.product_sn || "";
|
const sn = modalTarget.task.product_sn || "";
|
||||||
setModalTarget(null);
|
setModalTarget(null);
|
||||||
if (sn) await refreshProductTree(sn);
|
if (sn) await refreshProductTree(sn);
|
||||||
await loadProducts(keyword, statusFilter);
|
await loadProducts(keyword);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast(err?.response?.data?.detail ?? err?.message ?? "接收失败", "error");
|
toast(err?.response?.data?.detail ?? err?.message ?? "接收失败", "error");
|
||||||
} finally {
|
} finally {
|
||||||
@ -187,7 +195,7 @@ export default function AdminTasksPage() {
|
|||||||
const sn = modalTarget.task.product_sn || "";
|
const sn = modalTarget.task.product_sn || "";
|
||||||
setModalTarget(null);
|
setModalTarget(null);
|
||||||
if (sn) await refreshProductTree(sn);
|
if (sn) await refreshProductTree(sn);
|
||||||
await loadProducts(keyword, statusFilter);
|
await loadProducts(keyword);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast(err?.response?.data?.detail ?? err?.message ?? "驳回失败", "error");
|
toast(err?.response?.data?.detail ?? err?.message ?? "驳回失败", "error");
|
||||||
} finally {
|
} finally {
|
||||||
@ -210,7 +218,7 @@ export default function AdminTasksPage() {
|
|||||||
const sn = modalTarget.task.product_sn || "";
|
const sn = modalTarget.task.product_sn || "";
|
||||||
setModalTarget(null);
|
setModalTarget(null);
|
||||||
if (sn) await refreshProductTree(sn);
|
if (sn) await refreshProductTree(sn);
|
||||||
await loadProducts(keyword, statusFilter);
|
await loadProducts(keyword);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast(err?.response?.data?.detail ?? err?.message ?? "转交失败", "error");
|
toast(err?.response?.data?.detail ?? err?.message ?? "转交失败", "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user