-
- Proyecto
-
- {stageList.map((stage) => (
-
- {stage}
-
- ))}
+
+ {/* Project header */}
+
+
+ {project.name}
+
+
+ {totalTasks} tareas
+
-
- {projects.map((project) => (
-
+ {stages.map(([stageName, tasks], i) => {
+ const pct = (tasks.length / totalTasks) * 100;
+ if (pct < 1) return null;
+ return (
+
+ );
+ })}
+
+
+ {/* Stage chips */}
+
+ {stages.map(([stageName, tasks], i) => (
+
-
- {project.name}
-
- {stageList.map((stage) => (
-
- {(project.stages[stage] || []).map((task) => (
-
- ))}
-
- ))}
-
+
+
{stageName}
+
{tasks.length}
+
))}
);
}
+
+export function KanbanBoard({ projects }: KanbanBoardProps) {
+ const activeProjects = projects.filter((p) => {
+ const total = Object.values(p.stages).reduce((s, t) => s + t.length, 0);
+ return total > 0;
+ });
+
+ const totalTasks = activeProjects.reduce(
+ (sum, p) => sum + Object.values(p.stages).reduce((s, t) => s + t.length, 0),
+ 0
+ );
+
+ return (
+
+ {/* Summary bar */}
+
+
+ {activeProjects.length} proyectos
+ activos
+
+ |
+
+ {totalTasks} tareas totales
+
+
+
+ {/* Projects grid */}
+
+
+ {activeProjects.map((project) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/src/components/Topology/NetworkGraph.tsx b/frontend/src/components/Topology/NetworkGraph.tsx
index c408b0f..9e1a08a 100644
--- a/frontend/src/components/Topology/NetworkGraph.tsx
+++ b/frontend/src/components/Topology/NetworkGraph.tsx
@@ -1,5 +1,3 @@
-import { useEffect, useRef, useState } from "react";
-import * as d3 from "d3";
import { NodeCard } from "./NodeCard";
import type { NetworkNode } from "../../types";
@@ -7,105 +5,86 @@ interface NetworkGraphProps {
nodes: NetworkNode[];
}
-interface SimNode extends d3.SimulationNodeDatum {
- id: string;
- data: NetworkNode;
-}
+const CATEGORY_ORDER: [string, string[]][] = [
+ ["Infraestructura", ["firewall", "router", "switch", "ap"]],
+ ["Servidores", ["server"]],
+ ["Almacenamiento", ["nas"]],
+ ["Equipos", ["pc"]],
+ ["Periféricos", ["printer", "phone", "camera"]],
+ ["Otros", ["device"]],
+];
-interface SimLink extends d3.SimulationLinkDatum
{
- source: SimNode;
- target: SimNode;
+function categorizeNodes(nodes: NetworkNode[]) {
+ const categorized: { label: string; nodes: NetworkNode[] }[] = [];
+ const assigned = new Set();
+
+ for (const [label, icons] of CATEGORY_ORDER) {
+ const matching = nodes.filter(
+ (n) => icons.includes(n.icon) && !assigned.has(n.ip)
+ );
+ if (matching.length > 0) {
+ categorized.push({ label, nodes: matching });
+ for (const n of matching) assigned.add(n.ip);
+ }
+ }
+
+ // Any remaining nodes
+ const remaining = nodes.filter((n) => !assigned.has(n.ip));
+ if (remaining.length > 0) {
+ categorized.push({ label: "Otros", nodes: remaining });
+ }
+
+ return categorized;
}
export function NetworkGraph({ nodes }: NetworkGraphProps) {
- const svgRef = useRef(null);
- const [positions, setPositions] = useState