import { useState, useCallback } from "react";
import {
Search,
Loader2,
AlertCircle,
GitBranch,
RefreshCw,
X,
Plus,
Warehouse,
UserPlus,
} from "lucide-react";
import {
getTaskTree,
receiveTask,
rejectTask,
transferTask,
} from "../../services/taskApi";
import { useToast } from "../ui/Toast";
import type { ProductScanResponse, TaskResponse, TaskStatus } from "../../types/api";
import { TASK_STATUS } from "../../types/api";
// ============================================================
// 状态 → 颜色/标签映射
// ============================================================
const STATUS_CONFIG: Record<
string,
{ bg: string; text: string; ring: string; label: string }
> = {
[TASK_STATUS.PENDING]: {
bg: "bg-yellow-50",
text: "text-yellow-700",
ring: "ring-yellow-400",
label: "待接收",
},
[TASK_STATUS.WIP]: {
bg: "bg-blue-50",
text: "text-blue-700",
ring: "ring-blue-400",
label: "进行中",
},
[TASK_STATUS.COMPLETED]: {
bg: "bg-green-50",
text: "text-green-700",
ring: "ring-green-400",
label: "已完成",
},
[TASK_STATUS.REJECTED]: {
bg: "bg-red-50",
text: "text-red-700",
ring: "ring-red-400",
label: "已驳回",
},
[TASK_STATUS.ARCHIVED]: {
bg: "bg-gray-50",
text: "text-gray-600",
ring: "ring-gray-300",
label: "已入库",
},
};
function getStatusConfig(status: string) {
return (
STATUS_CONFIG[status] ?? {
bg: "bg-gray-50",
text: "text-gray-600",
ring: "ring-gray-300",
label: status,
}
);
}
// ============================================================
// 通用 Modal 容器
// ============================================================
function Modal({
open,
onClose,
title,
children,
}: {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}) {
if (!open) return null;
return (
);
}
// ============================================================
// 确认接收弹窗
// ============================================================
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)
确认接收后,该任务将标记为"进行中",系统将记录接收时间。
);
}
// ============================================================
// 品质驳回弹窗
// ============================================================
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}
⚠ 驳回后将自动生成返工任务,分配给上一道工序负责人
);
}
// ============================================================
// 完工裂变转交弹窗
// ============================================================
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}
完成任务并批量创建下一道工序任务,支持多路裂变分支
);
}
// ============================================================
// 单个任务节点卡片
// ============================================================
interface ModalTarget {
task: TaskResponse;
action: "receive" | "reject" | "transfer";
}
function TaskNodeCard({
task,
isLast,
onAction,
}: {
task: TaskResponse;
isLast: boolean;
onAction: (target: ModalTarget) => void;
}) {
const { bg, text, ring, label } = getStatusConfig(task.status);
return (
{/* 树形连接线 */}
{task.child_tasks.length > 0 && (
<>
{task.child_tasks.length > 1 && (
)}
>
)}
{/* 卡片本体 */}
{/* 左侧:任务名 + 标签 */}
{task.is_rework && (
⚠ 返工
)}
{task.child_tasks.length > 1 && (
裂变×{task.child_tasks.length}
)}
{task.task_name}
{task.assignee_id && 负责人: {task.assignee_id}}
{task.received_at && (
接收: {new Date(task.received_at).toLocaleDateString("zh-CN")}
)}
{task.completed_at && (
完成:{" "}
{new Date(task.completed_at).toLocaleDateString("zh-CN")}
)}
{task.reject_reason && (
驳回原因: {task.reject_reason}
)}
{/* 右侧:状态标签 + 操作按钮 */}
{label}
{/* 操作按钮 */}
{task.status === TASK_STATUS.PENDING && (
)}
{task.status === TASK_STATUS.WIP && (
)}
{/* 递归渲染子任务 */}
{task.child_tasks.length > 0 && (
{task.child_tasks.map((child, idx) => (
))}
)}
);
}
// ============================================================
// 主容器组件
// ============================================================
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);
}
}
// ---- 渲染 ----
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
? ` · ${countAllTasks(product.task_tree)} 总计`
: ""}
)}
{/* ---- 任务树 ---- */}
{product && !loading && (
<>
{product.task_tree && product.task_tree.length > 0 ? (
任务流转树状图
{product.task_tree.map((task, idx) => (
))}
) : (
)}
>
)}
{/* ---- 弹窗 ---- */}
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);
}