import { Button, Input, Modal } from "@heroui/react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { AppModalShell } from "../../components/ui/AppModalShell"; import { FieldSelect } from "../../components/ui/FieldSelect"; import { useDebouncedValue } from "../../hooks/useDebouncedValue"; import { useToast } from "../../hooks/useToast"; import { useI18n } from "../../i18n"; import { bizCompareListMappings, bizCompareCreateMapping, bizCompareUpdateMapping, bizMigrationCollectNow, bizMigrationCreateBatch, bizMigrationCreateProject, bizMigrationEnsureHighfreq, bizMigrationEvaluate, bizMigrationFinishBatch, bizMigrationGetBoard, bizMigrationListBaselineExpect, bizMigrationListBatches, bizMigrationListDiffs, bizMigrationListProjects, bizMigrationListRedTickets, bizMigrationListRuns, bizMigrationPatchBatch, bizMigrationPatchProject, bizMigrationResolveRedTicket, bizMonitorListTemplates, bizStateListBatches, bizStateListTasks, ApiRequestError, formatErr, } from "../../services/api"; import { formatSystemTime } from "../../utils/time"; import { jobChipColor, NmStatusChip } from "./nmChips"; type TaskOpt = { id: string; ne_name: string; ne_ip: string; note?: string; interval_sec?: number }; type PortMapping = { id: string; name: string; rows: Array<{ before_if: string; after_if: string }>; }; type MonitorTplOpt = { id: string; name: string; compare_template_id?: string; compare_template_name?: string; collect_metric_ids?: string[]; }; type Project = { id: string; name: string; old_task_id: string; new_task_id: string; old_baseline_batch_id: string; new_baseline_batch_id: string; mapping_id: string; monitor_template_id?: string; monitor_template?: MonitorTplOpt; status: string; note?: string; created_at?: string | null; old_task?: TaskOpt; new_task?: TaskOpt; }; type MigBatch = { id: string; batch_label: string; status: string; expect_set?: { ports?: string[]; items?: Array<{ metric_id: string; sheet_id?: string; key?: string; keys?: string[] }>; }; accept_status?: string; accept_run_id?: string; accept_summary?: { passed?: boolean; progress_ok?: number; progress_total?: number; anomaly?: number; verdict_counts?: Record; }; }; type RedTicket = { id: string; batch_id: string; key_str: string; new_key_str?: string; verdict: string; old_status?: string; new_status?: string; status: string; }; type SheetCard = { metric_id: string; sheet_id?: string; title?: string; progress_ok: number; progress_total: number; anomaly: number; new_baseline_missing?: boolean; new_baseline_mode?: string; }; type DiffRow = { id: string; metric_id: string; sheet_id?: string; verdict: string; color: string; old_kind: string; new_kind: string; in_expect: boolean; key_str?: string; new_key_str?: string; old_status?: string; new_status?: string; rule_hit?: string; }; type EvalRun = { id: string; created_at?: string | null; purpose?: string; old_batch_id?: string; new_batch_id?: string; summary?: { anomaly?: number; progress?: { ok?: number; total?: number } }; }; type ExpectSheetItem = { key: string; keys: string[]; mapped_to?: string; label?: string; row?: Record; }; type ExpectSheet = { metric_id: string; sheet_id?: string; title?: string; key_fields: string[]; iface_fields: string[]; items: ExpectSheetItem[]; }; function sheetIdentity(s: { sheet_id?: string; metric_id?: string } | null | undefined): string { return String(s?.sheet_id || s?.metric_id || "").trim(); } function sheetLabel(s: { title?: string; sheet_id?: string; metric_id?: string } | null | undefined): string { return String(s?.title || s?.sheet_id || s?.metric_id || "").trim() || "—"; } type DetailTab = "setup" | "batches" | "board"; const COLOR: Record = { green: "#16a34a", yellow: "#ca8a04", red: "#dc2626", gray: "#6b7280", }; const VERDICT_I18N: Record = { migrated: "bizMigration.verdictMigrated", migrating: "bizMigration.verdictMigrating", lost: "bizMigration.verdictLost", anomaly: "bizMigration.verdictAnomaly", not_involved: "bizMigration.verdictNotInvolved", unexpected_new: "bizMigration.verdictUnexpected", unfinished: "bizMigration.verdictUnfinished", ok: "bizMigration.verdictOk", }; function fmtTime(v?: string | null) { if (!v) return "—"; return formatSystemTime(v) || v; } function taskLabel(x: TaskOpt) { const base = `${x.ne_name || x.id} (${x.ne_ip || "-"})`; const note = x.note ? ` · ${x.note}` : ""; const iv = x.interval_sec ? ` · ${x.interval_sec}s` : ""; return `${base}${note}${iv}`; } export function BizMigrationPage() { const { t } = useI18n(); const { showOk, showError } = useToast(); const [projects, setProjects] = useState([]); const [tasks, setTasks] = useState([]); const [mappings, setMappings] = useState([]); const [busy, setBusy] = useState(false); const [listKeyword, setListKeyword] = useState(""); const debouncedListKw = useDebouncedValue(listKeyword, 250); const [createOpen, setCreateOpen] = useState(false); const [createName, setCreateName] = useState(""); const [createOldTaskId, setCreateOldTaskId] = useState(""); const [createNewTaskId, setCreateNewTaskId] = useState(""); const [createMonitorTplId, setCreateMonitorTplId] = useState(""); const [createOldBaselineId, setCreateOldBaselineId] = useState(""); const [createNewBaselineId, setCreateNewBaselineId] = useState(""); const [createOldBatches, setCreateOldBatches] = useState<{ id: string; started_at?: string | null }[]>([]); const [createNewBatches, setCreateNewBatches] = useState<{ id: string; started_at?: string | null }[]>([]); const [createShowMore, setCreateShowMore] = useState(false); const [monitorTpls, setMonitorTpls] = useState([]); const [boardSubTab, setBoardSubTab] = useState<"diffs" | "red">("diffs"); const [projectId, setProjectId] = useState(""); const [detailTab, setDetailTab] = useState("setup"); const [mappingId, setMappingId] = useState(""); const [mapName, setMapName] = useState(""); const [mapText, setMapText] = useState(""); const [monitorTplId, setMonitorTplId] = useState(""); const [oldBaselineId, setOldBaselineId] = useState(""); const [newBaselineId, setNewBaselineId] = useState(""); const [oldBatches, setOldBatches] = useState<{ id: string; started_at?: string | null }[]>([]); const [newBatches, setNewBatches] = useState<{ id: string; started_at?: string | null }[]>([]); const [batches, setBatches] = useState([]); const [batchId, setBatchId] = useState(""); const [board, setBoard] = useState<{ run?: { id: string; summary?: { sheet_cards?: SheetCard[]; progress?: { ok: number; total: number }; anomaly?: number; expect_ports?: string[]; }; } | null; } | null>(null); const [diffs, setDiffs] = useState([]); const [onlyExpect, setOnlyExpect] = useState(false); const [boardMetricId, setBoardMetricId] = useState(""); const [batchLabel, setBatchLabel] = useState(""); const [expectSheets, setExpectSheets] = useState([]); const [expectMetricId, setExpectMetricId] = useState(""); const [selectedExpectKeys, setSelectedExpectKeys] = useState>(new Set()); const [portFilter, setPortFilter] = useState(""); const [redTickets, setRedTickets] = useState([]); const [openRedCount, setOpenRedCount] = useState(0); const [acceptInfo, setAcceptInfo] = useState(null); const [pinOldBatchId, setPinOldBatchId] = useState(""); const [pinNewBatchId, setPinNewBatchId] = useState(""); const [evalRuns, setEvalRuns] = useState([]); const [selectedRunId, setSelectedRunId] = useState(""); const project = useMemo( () => projects.find((p) => p.id === projectId) || null, [projects, projectId], ); const batch = useMemo(() => batches.find((b) => b.id === batchId) || null, [batches, batchId]); const sheetCards = board?.run?.summary?.sheet_cards || []; const visibleDiffs = useMemo(() => { let rows = diffs; if (boardMetricId) { rows = rows.filter( (d) => sheetIdentity(d) === boardMetricId || d.metric_id === boardMetricId, ); } if (onlyExpect) rows = rows.filter((d) => d.in_expect); return rows; }, [diffs, onlyExpect, boardMetricId]); const boardMetricOptions = useMemo(() => { const ids = new Set(); for (const c of sheetCards) { const id = sheetIdentity(c); if (id) ids.add(id); } for (const d of diffs) { const id = sheetIdentity(d); if (id) ids.add(id); } return [...ids]; }, [sheetCards, diffs]); const filteredProjects = useMemo(() => { const kw = debouncedListKw.trim().toLowerCase(); if (!kw) return projects; return projects.filter((p) => { const blob = [ p.name, p.status, p.note || "", p.old_task?.ne_name || "", p.old_task?.ne_ip || "", p.new_task?.ne_name || "", p.new_task?.ne_ip || "", p.old_task_id, p.new_task_id, ] .join(" ") .toLowerCase(); return blob.includes(kw); }); }, [projects, debouncedListKw]); const activeCount = useMemo( () => projects.filter((p) => String(p.status || "").toLowerCase() === "active").length, [projects], ); useEffect(() => { if (batch?.accept_summary && Object.keys(batch.accept_summary).length) { setAcceptInfo(batch.accept_summary); } else if (batch && batch.accept_status === "none") { setAcceptInfo(null); } }, [batch]); const filteredExpectItems = useMemo(() => { const sheet = expectSheets.find((s) => sheetIdentity(s) === expectMetricId) || expectSheets[0]; const items = sheet?.items || []; const kw = portFilter.trim().toLowerCase(); if (!kw) return items; return items.filter((it) => { const blob = `${it.key} ${it.label || ""} ${JSON.stringify(it.row || {})}`.toLowerCase(); return blob.includes(kw); }); }, [expectSheets, expectMetricId, portFilter]); const reloadProjects = useCallback(async () => { const res = await bizMigrationListProjects(); setProjects((res.items || []) as Project[]); }, []); const reloadMappings = useCallback(async () => { const mp = await bizCompareListMappings(); setMappings( ((mp.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), name: String(x.name || x.id || ""), rows: Array.isArray(x.rows) ? (x.rows as Array<{ before_if?: string; after_if?: string }>).map((r) => ({ before_if: String(r.before_if || ""), after_if: String(r.after_if || ""), })) : [], })), ); }, []); const parseMapRows = () => { const rows: { before_if: string; after_if: string }[] = []; for (const line of mapText.split(/\r?\n/)) { const s = line.trim(); if (!s || s.startsWith("#") || s.toLowerCase().startsWith("old") || s.toLowerCase().startsWith("before")) { continue; } const parts = s.split(/[,|\t]+/).map((x) => x.trim()); if (parts.length >= 2 && parts[0] && parts[1]) { rows.push({ before_if: parts[0], after_if: parts[1] }); } } return rows; }; const loadMappingText = (id: string, list?: PortMapping[]) => { const m = (list || mappings).find((x) => x.id === id); if (!m) { setMappingId(id); return; } setMappingId(id); setMapName(m.name); setMapText(m.rows.map((r) => `${r.before_if},${r.after_if}`).join("\n")); }; const resetMappingEditor = () => { setMappingId(""); setMapName(t("bizCompare.mapping")); setMapText(""); }; const saveMapping = async (): Promise => { const rows = parseMapRows(); if (mappingId) { await bizCompareUpdateMapping(mappingId, { name: mapName.trim() || t("bizCompare.mapping"), rows }); await reloadMappings(); return mappingId; } const m = await bizCompareCreateMapping({ name: mapName.trim() || t("bizCompare.mapping"), rows, }); const id = String(m.id || ""); setMappingId(id); await reloadMappings(); return id; }; const loadBaselineExpect = useCallback(async (pid: string) => { if (!pid) { setExpectSheets([]); return; } try { const res = await bizMigrationListBaselineExpect(pid); const sheets = (res.sheets || []) as ExpectSheet[]; setExpectSheets(sheets); setExpectMetricId((prev) => { if (prev && sheets.some((s) => sheetIdentity(s) === prev)) return prev; return sheets[0] ? sheetIdentity(sheets[0]) : ""; }); } catch { setExpectSheets([]); } }, []); const loadRedTickets = useCallback(async (pid: string) => { if (!pid) { setRedTickets([]); setOpenRedCount(0); return; } try { const res = await bizMigrationListRedTickets(pid); setRedTickets((res.items || []) as RedTicket[]); setOpenRedCount(Number(res.open_count || 0)); } catch { setRedTickets([]); setOpenRedCount(0); } }, []); useEffect(() => { void (async () => { try { const [pr, tk, mp, mt] = await Promise.all([ bizMigrationListProjects(), bizStateListTasks(), bizCompareListMappings(), bizMonitorListTemplates(), ]); setProjects((pr.items || []) as Project[]); setTasks( ((tk.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), ne_name: String(x.ne_name || ""), ne_ip: String(x.ne_ip || ""), note: String(x.note || ""), interval_sec: Number(x.interval_sec || 0) || undefined, })), ); setMappings( ((mp.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), name: String(x.name || x.id || ""), rows: Array.isArray(x.rows) ? (x.rows as Array<{ before_if?: string; after_if?: string }>).map((r) => ({ before_if: String(r.before_if || ""), after_if: String(r.after_if || ""), })) : [], })), ); const mts = ((mt.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), name: String(x.name || x.id || ""), compare_template_id: String(x.compare_template_id || ""), compare_template_name: String(x.compare_template_name || ""), collect_metric_ids: Array.isArray(x.collect_metric_ids) ? (x.collect_metric_ids as string[]) : [], })); setMonitorTpls(mts); if (!createMonitorTplId && mts.length) { const port = mts.find((x) => x.name.includes("端口") || x.name.toLowerCase().includes("port")); setCreateMonitorTplId(port?.id || mts[0].id); } } catch (e) { showError(formatErr(e)); } })(); }, [showError]); useEffect(() => { if (!projectId) { setBatches([]); setBatchId(""); setExpectSheets([]); setExpectMetricId(""); setSelectedExpectKeys(new Set()); setRedTickets([]); setOpenRedCount(0); setBoard(null); setDiffs([]); setAcceptInfo(null); return; } void (async () => { try { const res = await bizMigrationListBatches(projectId); const items = (res.items || []) as MigBatch[]; setBatches(items); setBatchId((prev) => (items.find((b) => b.id === prev) ? prev : items[0]?.id || "")); await loadBaselineExpect(projectId); await loadRedTickets(projectId); } catch (e) { showError(formatErr(e)); } })(); }, [projectId, showError, loadBaselineExpect, loadRedTickets]); useEffect(() => { if (!batchId) { setBoard(null); setDiffs([]); setEvalRuns([]); setSelectedRunId(""); return; } void loadRuns(batchId); void loadBoard(batchId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [batchId]); useEffect(() => { if (!createOldTaskId) { setCreateOldBatches([]); return; } void bizStateListBatches(createOldTaskId, 30).then((r) => { setCreateOldBatches( ((r.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), started_at: (x.started_at as string) || null, })), ); }); }, [createOldTaskId]); useEffect(() => { if (!createNewTaskId) { setCreateNewBatches([]); return; } void bizStateListBatches(createNewTaskId, 30).then((r) => { setCreateNewBatches( ((r.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), started_at: (x.started_at as string) || null, })), ); }); }, [createNewTaskId]); useEffect(() => { if (!project?.old_task_id) { setOldBatches([]); return; } void bizStateListBatches(project.old_task_id, 30).then((r) => { setOldBatches( ((r.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), started_at: (x.started_at as string) || null, })), ); }); }, [project?.old_task_id]); useEffect(() => { if (!project?.new_task_id) { setNewBatches([]); return; } void bizStateListBatches(project.new_task_id, 30).then((r) => { setNewBatches( ((r.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), started_at: (x.started_at as string) || null, })), ); }); }, [project?.new_task_id]); useEffect(() => { if (!project) return; setMonitorTplId(project.monitor_template_id || project.monitor_template?.id || ""); setOldBaselineId(project.old_baseline_batch_id || ""); setNewBaselineId(project.new_baseline_batch_id || ""); if (project.mapping_id) loadMappingText(project.mapping_id); else resetMappingEditor(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [project?.id, project?.mapping_id, mappings.length]); async function loadRuns(bid: string) { try { const res = await bizMigrationListRuns(bid, 20); const items = (res.items || []) as EvalRun[]; setEvalRuns(items); setSelectedRunId((prev) => { if (prev && items.some((r) => r.id === prev)) return prev; return items[0]?.id || ""; }); } catch { setEvalRuns([]); } } async function loadBoard(bid: string, runId = "") { try { const b = await bizMigrationGetBoard(bid, runId); setBoard(b as typeof board); const rid = String((b as { run?: { id?: string } })?.run?.id || runId || ""); if (rid) setSelectedRunId(rid); if (rid) { const d = await bizMigrationListDiffs({ runId: rid, limit: 500 }); setDiffs((d.items || []) as DiffRow[]); } else { setDiffs([]); } } catch (e) { showError(formatErr(e)); } } function resetCreateForm() { setCreateName(""); setCreateOldTaskId(""); setCreateNewTaskId(""); setCreateOldBaselineId(""); setCreateNewBaselineId(""); setCreateShowMore(false); resetMappingEditor(); const port = monitorTpls.find((x) => x.name.includes("端口") || x.name.toLowerCase().includes("port")); setCreateMonitorTplId(port?.id || monitorTpls[0]?.id || ""); } const openCreate = () => { closeProject(); resetCreateForm(); setCreateOpen(true); }; const closeCreate = () => { setCreateOpen(false); resetCreateForm(); }; const openProject = (id: string, tab: DetailTab = "setup") => { setProjectId(id); setDetailTab(tab); setSelectedExpectKeys(new Set()); setPortFilter(""); setBatchLabel(""); setOnlyExpect(true); setBoardSubTab("diffs"); setBoardMetricId(""); }; const closeProject = () => { setProjectId(""); setDetailTab("setup"); }; async function onCreateProject() { if (!createName.trim() || !createOldTaskId || !createNewTaskId) { showError(t("bizMigration.needProjectFields")); return; } setBusy(true); try { let mid = mappingId; if (parseMapRows().length) { mid = await saveMapping(); } const p = (await bizMigrationCreateProject({ name: createName.trim(), old_task_id: createOldTaskId, new_task_id: createNewTaskId, mapping_id: mid || "", monitor_template_id: createMonitorTplId || "", old_baseline_batch_id: createOldBaselineId || "", new_baseline_batch_id: createNewBaselineId || "", status: "active", })) as Project; await reloadProjects(); closeCreate(); showOk(t("bizMigration.projectCreated")); openProject(p.id, "setup"); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } } async function onSaveMappingOnly() { if (!projectId) return; setBusy(true); try { const mid = await saveMapping(); if (mid) { await bizMigrationPatchProject(projectId, { mapping_id: mid }); await reloadProjects(); } showOk(t("bizMigration.mappingBound")); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } } async function onSaveBaseline() { if (!projectId) return; setBusy(true); try { let mid = mappingId; if (mappingId || parseMapRows().length) { mid = await saveMapping(); } else { mid = ""; } await bizMigrationPatchProject(projectId, { old_baseline_batch_id: oldBaselineId || project?.old_baseline_batch_id || "", new_baseline_batch_id: newBaselineId || project?.new_baseline_batch_id || "", mapping_id: mid || "", monitor_template_id: monitorTplId || project?.monitor_template_id || "", }); await reloadProjects(); await loadBaselineExpect(projectId); showOk(t("bizMigration.baselineSaved")); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } } async function onCreateBatch() { if (!projectId) return; // One item per selected key. A flat `keys: [a, b]` is parsed as one composite key. const items: Array<{ metric_id: string; sheet_id?: string; key: string }> = []; const ports: string[] = []; for (const sheet of expectSheets) { const keys = sheet.items.map((it) => it.key).filter((k) => selectedExpectKeys.has(k)); const sid = sheetIdentity(sheet); for (const key of keys) { items.push({ metric_id: sheet.metric_id, sheet_id: sid, key }); } if (sheet.metric_id === "interface_brief") ports.push(...keys); } if (!items.length && !ports.length) { showError(t("bizMigration.needExpectPorts")); return; } setBusy(true); try { const b = (await bizMigrationCreateBatch(projectId, { batch_label: batchLabel.trim() || t("bizMigration.defaultBatchLabel"), expect_set: { ports, items }, status: "pending", })) as MigBatch; const res = await bizMigrationListBatches(projectId); setBatches((res.items || []) as MigBatch[]); setBatchId(b.id); setBatchLabel(""); setSelectedExpectKeys(new Set()); if (Number((b as { open_red_count?: number }).open_red_count || 0) > 0) { showOk( t("bizMigration.batchCreatedWithRed", { n: String((b as { open_red_count?: number }).open_red_count), }), ); } else { showOk(t("bizMigration.batchCreated")); } await loadRedTickets(projectId); setDetailTab("board"); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } } async function setBatchStatus(status: string) { if (!batchId) return; setBusy(true); try { if (status === "review" || status === "done") { const res = (await bizMigrationFinishBatch(batchId, status === "done")) as { batch?: MigBatch; accept_summary?: MigBatch["accept_summary"]; open_red_count?: number; }; const list = await bizMigrationListBatches(projectId); setBatches((list.items || []) as MigBatch[]); setAcceptInfo(res.accept_summary || res.batch?.accept_summary || null); await loadRedTickets(projectId); if (res.accept_summary?.passed) { showOk(t("bizMigration.acceptPassed")); } else { showOk(t("bizMigration.acceptFailed", { n: String(res.open_red_count ?? openRedCount) })); } if (res.batch?.accept_run_id) { await loadBoard(batchId, res.batch.accept_run_id); } setDetailTab("board"); } else { await bizMigrationPatchBatch(batchId, { status }); const res = await bizMigrationListBatches(projectId); setBatches((res.items || []) as MigBatch[]); showOk(t("bizMigration.statusUpdated")); if (status === "active") await loadRedTickets(projectId); } } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } } async function onResolveTicket(id: string) { try { await bizMigrationResolveRedTicket(id); await loadRedTickets(projectId); showOk(t("bizMigration.redResolved")); } catch (e) { showError(formatErr(e)); } } async function onEvaluate() { if (!batchId) return; if (!project?.old_baseline_batch_id || !project?.new_baseline_batch_id) { showError(t("bizMigration.needBothBaselines")); return; } setBusy(true); try { const body: Record = {}; if (pinOldBatchId) body.old_batch_id = pinOldBatchId; if (pinNewBatchId) body.new_batch_id = pinNewBatchId; const run = await bizMigrationEvaluate(batchId, body); const rid = String((run as { id?: string }).id || ""); await loadRuns(batchId); await loadBoard(batchId, rid); showOk(t("bizMigration.evaluated")); setDetailTab("board"); } catch (e) { const detail = e instanceof ApiRequestError ? String(e.detail || "") : ""; if (detail === "old_baseline_required") showError(t("bizMigration.errOldBaselineRequired")); else if (detail === "new_baseline_required") showError(t("bizMigration.errNewBaselineRequired")); else showError(formatErr(e)); } finally { setBusy(false); } } async function onEnsureHighfreq() { if (!projectId) return; setBusy(true); try { await bizMigrationEnsureHighfreq(projectId, { interval_sec: 60, collect_now: true, }); await reloadProjects(); showOk(t("bizMigration.highfreqReady")); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } } async function onCollectNow() { if (!projectId) return; setBusy(true); try { await bizMigrationCollectNow(projectId); showOk(t("bizMigration.collectTriggered")); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } } function toggleExpectKey(key: string) { setSelectedExpectKeys((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); } const selectedMonitorTpl = useMemo(() => { const id = monitorTplId || project?.monitor_template_id || ""; return monitorTpls.find((x) => x.id === id) || project?.monitor_template || null; }, [monitorTplId, monitorTpls, project]); function verdictLabel(v: string) { const key = VERDICT_I18N[v]; return key ? t(key) : v; } function statusLabel(status: string) { const s = String(status || "").toLowerCase(); if (s === "active") return t("bizMigration.statusActive"); if (s === "done" || s === "completed") return t("bizMigration.statusDone"); if (s === "paused") return t("bizMigration.statusPaused"); if (s === "pending") return t("bizMigration.statusPending"); if (s === "review") return t("bizMigration.statusReview"); return status || "—"; } const taskOptions = ( <> {tasks.map((x) => ( ))} ); const batchOptions = (items: { id: string; started_at?: string | null }[]) => ( <> {items.map((x) => ( ))} ); return (

{t("bizMigration.title")}

{t("bizMigration.hintShort")}{" "} {t("network.nav.monitorTemplates")} {" · "} {t("network.nav.compareTemplates")}

{t("bizMigration.kpiProjects")}
{projects.length}
{t("bizMigration.kpiActive")}
{activeCount}
setListKeyword(e.target.value)} />
{filteredProjects.map((row) => ( ))} {!filteredProjects.length ? ( ) : null}
{t("bizMigration.colName")} {t("bizMigration.colPair")} {t("bizMigration.colStatus")} {t("bizMigration.colBaseline")} {t("bizMigration.colCreated")} {t("bizMigration.colActions")}
{row.name}
{row.note ?
{row.note}
: null}
{row.old_task?.ne_name || row.old_task_id.slice(0, 8)}
→ {row.new_task?.ne_name || row.new_task_id.slice(0, 8)} {row.old_task?.ne_ip || row.new_task?.ne_ip ? ` · ${row.old_task?.ne_ip || "—"} / ${row.new_task?.ne_ip || "—"}` : ""}
{statusLabel(row.status)} {row.old_baseline_batch_id && row.new_baseline_batch_id ? ( {t("bizMigration.baselineSet")} ) : ( {t("bizMigration.baselineUnset")} )} {fmtTime(row.created_at)}
{t("bizMigration.empty")}
{/* Create task */} {t("bizMigration.create")}

{t("bizMigration.createHintShort")}

setCreateName(e.target.value)} placeholder={t("bizMigration.projectNamePh")} aria-label={t("bizMigration.projectName")} />
{ setCreateOldTaskId(e.target.value); setCreateOldBaselineId(""); }} fullWidth > {taskOptions} { setCreateNewTaskId(e.target.value); setCreateNewBaselineId(""); }} fullWidth > {taskOptions}
{!tasks.length ? (

{t("bizMigration.needBizStateTasks")}{" "} {t("bizMigration.openBizState")}

) : null} {createShowMore ? (
setCreateMonitorTplId(e.target.value)} fullWidth > {monitorTpls.map((m) => ( ))}
{t("bizMigration.portMapping")}

{t("bizCompare.mappingOptionalHint")}

{ const id = e.target.value; if (id) loadMappingText(id); else resetMappingEditor(); }} fullWidth > {mappings.map((m) => ( ))} setMapName(e.target.value)} aria-label={t("bizCompare.mapName")} />