import { useState, useCallback, useMemo, memo } from "react";
import {
Search,
Loader2,
AlertCircle,
GitBranch,
RefreshCw,
X,
Plus,
Warehouse,
UserPlus,
} from "lucide-react";
import TaskFlowView from "./TaskFlowView";
import {
getTaskTree,
receiveTask,
rejectTask,
transferTask,
} from "../../services/taskApi";
import { useToast } from "../ui/Toast";
import type { ProductScanResponse, TaskResponse } from "../../types/api";
import { getStatusConfig } from "../../constants/task";
// ============================================================
// 通用 Modal 容器
// ============================================================
const Modal = memo(function Modal({
open,
onClose,
title,
children,
}: {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}) {
if (!open) return null;
return (
);
});
// ============================================================
// 确认接收弹窗
// ============================================================
export const ReceiveConfirmModal = memo(function ReceiveConfirmModal({
open,
task,
submitting,
onClose,
onConfirm,
}: {
open: boolean;
task: TaskResponse | null;
submitting: boolean;
onClose: () => void;
onConfirm: () => void;
}) {
if (!task) return null;
return (
{task.task_name}
当前状态: {getStatusConfig(task.status).label}
{" → "}
进行中 (WIP)
确认接收后,该任务将标记为"进行中",系统将记录接收时间。
);
});
// ============================================================
// 品质驳回弹窗
// ============================================================
export const RejectModal = memo(function RejectModal({
open,
task,
submitting,
onClose,
onSubmit,
}: {
open: boolean;
task: TaskResponse | null;
submitting: boolean;
onClose: () => void;
onSubmit: (reason: string) => void;
}) {
const [reason, setReason] = useState("");
if (!task) return null;
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const trimmed = reason.trim();
if (!trimmed) return;
onSubmit(trimmed);
setReason("");
}
function handleClose() {
setReason("");
onClose();
}
return (
{task.task_name}
⚠ 驳回后将自动生成返工任务,分配给上一道工序负责人
);
});
// ============================================================
// 完工裂变转交弹窗
// ============================================================
export const TransferModal = memo(function TransferModal({
open,
task,
submitting,
onClose,
onSubmit,
}: {
open: boolean;
task: TaskResponse | null;
submitting: boolean;
onClose: () => void;
onSubmit: (nextTaskName: string, assignees: string[], note: string) => void;
}) {
const [nextTaskName, setNextTaskName] = useState("");
const [assigneeInput, setAssigneeInput] = useState("");
const [assignees, setAssignees] = useState([]);
const [useWarehouse, setUseWarehouse] = useState(false);
const [note, setNote] = useState("");
if (!task) return null;
function handleAddAssignee() {
const trimmed = assigneeInput.trim();
if (!trimmed) return;
if (assignees.includes(trimmed)) {
setAssigneeInput("");
return;
}
setAssignees((prev) => [...prev, trimmed]);
setAssigneeInput("");
}
function handleRemoveAssignee(id: string) {
setAssignees((prev) => prev.filter((a) => a !== id));
}
function handleAssigneeKeyDown(e: React.KeyboardEvent) {
if (e.key === "Enter") {
e.preventDefault();
handleAddAssignee();
}
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const name = nextTaskName.trim();
if (!name) return;
const finalAssignees = [...assignees];
if (useWarehouse) {
finalAssignees.push("virtual_warehouse");
}
if (finalAssignees.length === 0) return;
onSubmit(name, finalAssignees, note.trim());
// 重置表单
setNextTaskName("");
setAssignees([]);
setAssigneeInput("");
setUseWarehouse(false);
setNote("");
}
function handleClose() {
setNextTaskName("");
setAssignees([]);
setAssigneeInput("");
setUseWarehouse(false);
setNote("");
onClose();
}
const allAssignees = [
...assignees,
...(useWarehouse ? ["virtual_warehouse"] : []),
];
return (
{task.task_name}
完成任务并批量创建下一道工序任务,支持多路裂变分支
);
});
// ============================================================
// 操作弹窗目标类型
// ============================================================
export interface ModalTarget {
task: TaskResponse;
action: "receive" | "reject" | "transfer";
}
// ============================================================
// 主容器组件
// ============================================================
export default function TaskTreeViewer() {
const { toast } = useToast();
// 搜索状态
const [serial, setSerial] = useState("");
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 弹窗状态
const [modalTarget, setModalTarget] = useState(null);
const [submitting, setSubmitting] = useState(false);
// ---- 数据刷新 ----
const refresh = useCallback(async () => {
const trimmed = serial.trim();
if (trimmed.length !== 16) return;
try {
const result = await getTaskTree(trimmed);
setProduct(result);
} catch {
// 静默失败 — 不影响当前展示
}
}, [serial]);
async function handleSearch(e?: React.FormEvent) {
e?.preventDefault();
const trimmed = serial.trim();
if (trimmed.length !== 16) {
setError("请输入 16 位产品身份证");
return;
}
setLoading(true);
setError(null);
setProduct(null);
try {
const result = await getTaskTree(trimmed);
setProduct(result);
} catch (err: any) {
const msg =
err?.response?.status === 404
? `未找到产品身份证 ${trimmed} 对应的产品`
: err?.response?.data?.detail ??
err?.message ??
"查询失败,请检查后端服务";
setError(msg);
} finally {
setLoading(false);
}
}
// ---- 操作处理 ----
async function handleReceive() {
if (!modalTarget) return;
setSubmitting(true);
try {
await receiveTask(modalTarget.task.id);
toast("任务已接收 (PENDING → WIP)", "success");
setModalTarget(null);
await refresh();
} catch (err: any) {
toast(
err?.response?.data?.detail ?? err?.message ?? "接收失败",
"error"
);
} finally {
setSubmitting(false);
}
}
async function handleReject(reason: string) {
if (!modalTarget) return;
setSubmitting(true);
try {
await rejectTask(modalTarget.task.id, reason);
toast("任务已驳回,已自动创建返工任务", "success");
setModalTarget(null);
await refresh();
} catch (err: any) {
toast(
err?.response?.data?.detail ?? err?.message ?? "驳回失败",
"error"
);
} finally {
setSubmitting(false);
}
}
async function handleTransfer(
nextTaskName: string,
assignees: string[],
note: string
) {
if (!modalTarget) return;
setSubmitting(true);
try {
const result = await transferTask(
modalTarget.task.id,
assignees,
nextTaskName,
note || undefined
);
toast(result.message, "success");
setModalTarget(null);
await refresh();
} catch (err: any) {
toast(
err?.response?.data?.detail ?? err?.message ?? "转交失败",
"error"
);
} finally {
setSubmitting(false);
}
}
// ---- 渲染 ----
// 🚀 缓存递归计算 — 仅在 task_tree 变化时重新计算
const totalTaskCount = useMemo(
() => (product?.task_tree ? countAllTasks(product.task_tree) : 0),
[product?.task_tree],
);
const modalTask = modalTarget?.task ?? null;
return (
{/* ---- 搜索栏 ---- */}
{/* ---- 错误 ---- */}
{error && (
)}
{/* ---- 加载中 ---- */}
{loading && (
)}
{/* ---- 产品信息摘要 ---- */}
{product && !loading && (
产品身份证
{product.serial_number}
订单编号
{product.order_no || "—"}
当前位置
{product.current_location_id === "virtual_warehouse" ? (
🏭 仓库
) : (
product.current_location_id ?? "—"
)}
任务总数
{product.top_level_tasks?.length ?? 0} 顶层
{product.task_tree?.length
? ` · ${totalTaskCount} 总计`
: ""}
)}
{/* ---- 任务流转分支/卡片视图 ---- */}
{product && !loading && (
<>
{product.task_tree && product.task_tree.length > 0 ? (
) : (
)}
>
)}
{/* ---- 弹窗 ---- */}
setModalTarget(null)}
onConfirm={handleReceive}
/>
setModalTarget(null)}
onSubmit={handleReject}
/>
setModalTarget(null)}
onSubmit={handleTransfer}
/>
);
}
// ============================================================
// 工具函数:递归统计任务总数
// ============================================================
function countAllTasks(tasks: TaskResponse[]): number {
return tasks.reduce((sum, t) => sum + 1 + countAllTasks(t.child_tasks), 0);
}