feat: PC 端直接完结入口与当前工序口径(前端)
【直接完结入口】
- 共享 TransferModal 新增「🏁 直接完结」开关,与「入库」互斥;选中后下游字段
禁用(保留输入便于反悔)、标题/预览/按钮切换为独立视觉
- 入口仅超管/主管可见(useAuth + isAdminRole),并加防御性 useEffect 清掉
非管理员的残留选中态,避免提交出 finish_directly
- 表单重置改为「打开时重置」:TransferModal 被 memo 后常驻挂载会跨次残留状态,
且原实现于提交时清空会让等待期主题从「直接完结」闪回普通转交
【角色判断收口】
- 新增 ADMIN_ROLES / isAdminRole(与后端 task_service.ADMIN_ROLES 对齐)
此前该判断散落在 TaskFlowView / AdminProductsPage 各处,移动端那份还只判了
SUPER_ADMIN 漏了 SUPERVISOR,导致主管被前端误挡
【当前工序口径】
- 「当前工序」列改用后端派生的 current_step,不再用 overall_status ——
后者会被"最新主干任务名"覆盖,让已 COMPLETED 的历史工序(扫码出库/测试…)
冒充当前工序。显示与筛选值一并切换以保持一致
- ProductResponse 类型补齐 current_step
This commit is contained in:
@ -19,7 +19,8 @@ import {
|
||||
} from "../../services/taskApi";
|
||||
import { useToast } from "../ui/Toast";
|
||||
import type { ProductScanResponse, TaskResponse } from "../../types/api";
|
||||
import { getStatusConfig } from "../../constants/task";
|
||||
import { getStatusConfig, isAdminRole } from "../../constants/task";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import ImageUploader from "../ui/ImageUploader";
|
||||
import { extractErrorMessage } from "../../utils/errorMessage";
|
||||
|
||||
@ -266,6 +267,16 @@ export const TransferModal = memo(function TransferModal({
|
||||
setNote("");
|
||||
}, [open]);
|
||||
|
||||
// 🔒 直接完结入口仅超管/主管【可见】:普通人看不到这个选项,而不是点了才被后端 403。
|
||||
// 后端那道硬拦截依然保留 —— 这里是体验层,角色来自客户端存储、可被伪造。
|
||||
const { user } = useAuth();
|
||||
const canFinishDirectly = isAdminRole(user?.role);
|
||||
|
||||
// 防御:非管理员若因任何原因残留了选中态,强制清掉,避免提交出 finish_directly
|
||||
useEffect(() => {
|
||||
if (!canFinishDirectly) setFinishDirectly(false);
|
||||
}, [canFinishDirectly]);
|
||||
|
||||
if (!task) return null;
|
||||
|
||||
function handleAddAssignee() {
|
||||
@ -467,7 +478,8 @@ export const TransferModal = memo(function TransferModal({
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* 🏁 直接完结 — 与转交/入库互斥 */}
|
||||
{/* 🏁 直接完结 — 与转交/入库互斥。仅超管/主管可见 */}
|
||||
{canFinishDirectly && (
|
||||
<div>
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
@ -482,12 +494,15 @@ export const TransferModal = memo(function TransferModal({
|
||||
</label>
|
||||
{finishDirectly && (
|
||||
<p className="mt-1.5 rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
||||
任务将直接完结,不创建任何下游任务;产品宏观状态保持不变,
|
||||
<span className="font-semibold">不会进入「待仓库收货」</span>
|
||||
。适用于售后返厂直接发走、半成品被直接提走等无需入库的收官场景。
|
||||
任务将直接完结,不创建任何下游任务。
|
||||
<span className="font-semibold">它只是一个任务闭环动作,不改动产品状态</span>
|
||||
:已出库的设备完结后依然是<span className="font-semibold">「已出库」</span>
|
||||
,回归 WIP 矩阵的已出库列;不会变成「待仓库收货」,也无需仓库扫码实收。
|
||||
适用于售后返厂直接发走、半成品被直接提走等无需入库的收官场景。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 备注 */}
|
||||
<div>
|
||||
|
||||
@ -200,8 +200,17 @@ export const AFTER_SALES_OVERALL_OPTIONS = [
|
||||
export function taskOptionsFor(
|
||||
phase: string | null | undefined,
|
||||
hasHistory = true,
|
||||
overallStatus?: string | null,
|
||||
): string[] {
|
||||
if (phase === LIFECYCLE_PHASE.AFTER_SALES) return AFTER_SALES_TASK_OPTIONS;
|
||||
// 🔧 出库设备与售后生命周期同等待遇(与 track-uniapp/src/utils/lifecycle.js 同步):
|
||||
// 后端为消除不可回退的售后烙印,已不再把「正常已出库做质检」的设备翻成
|
||||
// AFTER_SALES。若这里仍只认 phase,工人永远选不到「发货测试 / 售后维修」。
|
||||
if (
|
||||
phase === LIFECYCLE_PHASE.AFTER_SALES ||
|
||||
String(overallStatus || "").trim() === "已出库"
|
||||
) {
|
||||
return AFTER_SALES_TASK_OPTIONS;
|
||||
}
|
||||
if (!hasHistory) return [...PRODUCTION_TASK_OPTIONS, ...AFTER_SALES_ONLY_STEPS];
|
||||
return PRODUCTION_TASK_OPTIONS;
|
||||
}
|
||||
@ -217,6 +226,22 @@ export function overallOptionsFor(
|
||||
}
|
||||
|
||||
/** 列表筛选枚举 — 两阶段并集(用于筛选,不是录入项) */
|
||||
/**
|
||||
* 管理角色 — 必须与后端 task_service.ADMIN_ROLES 保持一致。
|
||||
*
|
||||
* ⚠️ 收敛到这里的理由:此前这段判断散落在多处(TaskFlowView / AdminProductsPage),
|
||||
* 而移动端那份只判了 SUPER_ADMIN、漏了 SUPERVISOR,导致主管被前端误挡。
|
||||
* 新增判断一律用 isAdminRole(),不要再手写 === 比较。
|
||||
*
|
||||
* 注意:这是【体验层】判断,角色来自客户端存储、可被伪造。
|
||||
* 真正的拦截在后端(transfer_task 对 finish_directly 有 403 硬校验)。
|
||||
*/
|
||||
export const ADMIN_ROLES = ["SUPER_ADMIN", "SUPERVISOR"] as const;
|
||||
|
||||
export function isAdminRole(role?: string | null): boolean {
|
||||
return !!role && (ADMIN_ROLES as readonly string[]).includes(role);
|
||||
}
|
||||
|
||||
export const ALL_OVERALL_OPTIONS = [
|
||||
...PRODUCTION_OVERALL_OPTIONS, ...AFTER_SALES_ONLY_STEPS,
|
||||
];
|
||||
|
||||
@ -114,15 +114,19 @@ export default function AdminTasksPage() {
|
||||
render: (p) => <div className="text-xs text-gray-500 truncate">{p.spec_model || p.material_name || p.material_id || "—"}</div>,
|
||||
},
|
||||
{
|
||||
key: "overall_status", label: "当前工序", colSpan: 1,
|
||||
filterType: "enum", getFilterValue: (p) => p.overall_status || "—",
|
||||
key: "current_step", label: "当前工序", colSpan: 1,
|
||||
// 🔧 改用后端派生的 current_step,而不是 overall_status ——
|
||||
// 后者的取值被"最新主干任务名"覆盖(含已 COMPLETED 的历史工序),
|
||||
// 会让「扫码出库 / 测试 / 发货测试」等已完结工序冒充"当前工序"。
|
||||
// current_step 只认活跃主干任务;无活跃任务时返回终态或空。
|
||||
filterType: "enum", getFilterValue: (p) => p.current_step || "—",
|
||||
// 🔧 筛选用两阶段并集:售后设备的「发货测试 / 售后维修」也要能被筛出来
|
||||
enumOptions: ALL_OVERALL_OPTIONS.map((v) => ({ value: v, label: v })),
|
||||
render: (p) => {
|
||||
const lb = lifecycleBadge(p.overall_status, p.lifecycle_phase);
|
||||
const lb = lifecycleBadge(p.current_step, p.lifecycle_phase);
|
||||
return (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-medium text-gray-700">{p.overall_status || "—"}</span>
|
||||
<span className="text-xs font-medium text-gray-700">{p.current_step || "—"}</span>
|
||||
{lb && (
|
||||
<span className={`shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-bold leading-none ${lb.className}`}>
|
||||
{lb.label}
|
||||
|
||||
@ -17,6 +17,14 @@ export interface ProductResponse {
|
||||
current_location_name: string | null;
|
||||
macro_status: string | null;
|
||||
overall_status: string | null;
|
||||
/**
|
||||
* 【当前工序】— 只反映"此刻在做什么",绝不拿已完结的历史工序冒充。
|
||||
* · 有活跃主干任务(WIP/PENDING) → 该工序名
|
||||
* · 否则处于宏观终态(待仓库收货/已入库/在库/已出库) → 该终态
|
||||
* · 否则(活已干完)→ ""(列表显示「—」)
|
||||
* ⚠️ 不要用 overall_status 渲染该列:它会被"最新主干任务名"覆盖,含已 COMPLETED 的。
|
||||
*/
|
||||
current_step?: string;
|
||||
status: string;
|
||||
/** 🔧 生命周期阶段:PRODUCTION(生产制造/发货测试) | AFTER_SALES(出库后返厂售后维修) */
|
||||
lifecycle_phase?: LifecyclePhase;
|
||||
|
||||
Reference in New Issue
Block a user