"use client"; import { ChevronRight, Search } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { AdminLoadingInline } from "@/components/admin/admin-loading-state"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { formatAdminCreditMajorDecimal } from "@/lib/money"; import type { AgentNodeRow } from "@/types/api/admin-agent"; function formatCredit(amount: number, currencyCode = "NPR"): string { return formatAdminCreditMajorDecimal(amount, currencyCode); } function nodeMatchesKeyword( node: AgentNodeRow, normalized: string, parentNameMap: Map, ): boolean { if (normalized === "") { return true; } const parentName = node.parent_id !== null ? (parentNameMap.get(node.parent_id) ?? "") : ""; return [node.name, node.code, node.username ?? "", parentName] .join(" ") .toLowerCase() .includes(normalized); } function pruneTreeForSearch( nodes: AgentNodeRow[], normalized: string, parentNameMap: Map, ): AgentNodeRow[] { if (normalized === "") { return nodes; } const out: AgentNodeRow[] = []; for (const node of nodes) { const children = pruneTreeForSearch(node.children ?? [], normalized, parentNameMap); const selfMatch = nodeMatchesKeyword(node, normalized, parentNameMap); if (selfMatch || children.length > 0) { out.push({ ...node, children }); } } return out; } export type AgentLineSidebarProps = { siteLabel: string | null; /** API 返回的嵌套树(含 children) */ tree: AgentNodeRow[]; parentNameMap: Map; selectedId: number | null; keyword: string; agentCount: number; loading?: boolean; onKeywordChange: (value: string) => void; onSelect: (node: AgentNodeRow) => void; }; type TreeRowProps = { node: AgentNodeRow; depth: number; selectedId: number | null; expandedIds: Set; onToggleExpand: (id: number) => void; onSelect: (node: AgentNodeRow) => void; }; function TreeRow({ node, depth, selectedId, expandedIds, onToggleExpand, onSelect, }: TreeRowProps): React.ReactElement { const { t } = useTranslation(["agents", "common"]); const children = node.children ?? []; const hasChildren = children.length > 0; const expanded = expandedIds.has(node.id); const active = selectedId === node.id; const indent = depth * 14; return (
  • {hasChildren ? ( ) : ( )}
    {hasChildren && expanded ? (
      {children.map((child) => ( ))}
    ) : null}
  • ); } export function AgentLineSidebar({ siteLabel, tree, parentNameMap, selectedId, keyword, agentCount, loading = false, onKeywordChange, onSelect, }: AgentLineSidebarProps): React.ReactElement { const { t } = useTranslation(["agents", "common"]); const [expandedIds, setExpandedIds] = useState>(() => new Set()); const normalizedKeyword = keyword.trim().toLowerCase(); const displayForest = useMemo(() => { return pruneTreeForSearch(tree, normalizedKeyword, parentNameMap); }, [normalizedKeyword, parentNameMap, tree]); useEffect(() => { if (tree.length === 0) { setExpandedIds(new Set()); return; } setExpandedIds((prev) => { const next = new Set(prev); for (const node of tree) { if ((node.children?.length ?? 0) > 0) { next.add(node.id); } } return next; }); }, [tree]); useEffect(() => { if (selectedId === null) { return; } setExpandedIds((prev) => { const next = new Set(prev); const walk = (nodes: AgentNodeRow[], ancestors: number[]): boolean => { for (const node of nodes) { const chain = [...ancestors, node.id]; if (node.id === selectedId) { for (const id of ancestors) { next.add(id); } return true; } if (walk(node.children ?? [], chain)) { return true; } } return false; }; walk(tree, []); return next; }); }, [selectedId, tree]); const toggleExpand = useCallback((id: number) => { setExpandedIds((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, []); const hasAnyAgent = displayForest.length > 0; return ( ); } export { formatCredit };