From 323092dcdbaebc3b93d2a0a60a81f2b9f9994343 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Thu, 10 Sep 2026 14:05:36 +0000 Subject: [PATCH] feat: reformulacao completa do mapa interativo com esteira industrial e drawer didatico --- src/pages/MapaInterativo.old.tsx | 645 ++++++++++++++ src/pages/MapaInterativo.tsx | 1381 +++++++++++++++++------------- 2 files changed, 1428 insertions(+), 598 deletions(-) create mode 100644 src/pages/MapaInterativo.old.tsx diff --git a/src/pages/MapaInterativo.old.tsx b/src/pages/MapaInterativo.old.tsx new file mode 100644 index 0000000..9b6606b --- /dev/null +++ b/src/pages/MapaInterativo.old.tsx @@ -0,0 +1,645 @@ +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { + X, + Filter, + Eye, + Workflow, + Shield, + RotateCcw, + Edit, + Save +} from 'lucide-react'; +import { useSystemMapAutoDiscovery, SystemMapNode, SystemMapConnection } from '@/hooks/useSystemMapAutoDiscovery'; +import { useUserRole } from '@/hooks/useUserRole'; +import { UserResourcePermissions } from '@/components/mapa-interativo/UserResourcePermissions'; +import { cn } from '@/lib/utils'; + +const createCurvedPath = (fromX: number, fromY: number, toX: number, toY: number) => { + const midX = (fromX + toX) / 2; + const midY = (fromY + toY) / 2; + + const dx = toX - fromX; + const dy = toY - fromY; + const distance = Math.sqrt(dx * dx + dy * dy); + + const curvature = Math.min(distance * 0.3, 100); + const offsetX = -dy / distance * curvature; + const offsetY = dx / distance * curvature; + + const controlX = midX + offsetX; + const controlY = midY + offsetY; + + return `M ${fromX} ${fromY} Q ${controlX} ${controlY} ${toX} ${toY}`; +}; + +const getArrowPositions = (fromX: number, fromY: number, toX: number, toY: number) => { + const arrows = []; + const steps = 3; // Número de setas ao longo da linha + + for (let i = 1; i <= steps; i++) { + const t = i / (steps + 1); + + const midX = (fromX + toX) / 2; + const midY = (fromY + toY) / 2; + const dx = toX - fromX; + const dy = toY - fromY; + const distance = Math.sqrt(dx * dx + dy * dy); + const curvature = Math.min(distance * 0.3, 100); + const offsetX = -dy / distance * curvature; + const offsetY = dx / distance * curvature; + const controlX = midX + offsetX; + const controlY = midY + offsetY; + + const x = (1 - t) * (1 - t) * fromX + 2 * (1 - t) * t * controlX + t * t * toX; + const y = (1 - t) * (1 - t) * fromY + 2 * (1 - t) * t * controlY + t * t * toY; + + const tangentX = 2 * (1 - t) * (controlX - fromX) + 2 * t * (toX - controlX); + const tangentY = 2 * (1 - t) * (controlY - fromY) + 2 * t * (toY - controlY); + const angle = Math.atan2(tangentY, tangentX) * 180 / Math.PI; + + arrows.push({ x, y, angle }); + } + + return arrows; +}; + +export default function MapaInterativo() { + const { nodes, connections } = useSystemMapAutoDiscovery(); + const { isAdmin } = useUserRole(); + const [activeFilter, setActiveFilter] = useState('all'); + const [selectedNode, setSelectedNode] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [draggedNode, setDraggedNode] = useState(null); + const [highlightedPath, setHighlightedPath] = useState([]); + const [isEditing, setIsEditing] = useState(false); + const [editData, setEditData] = useState<{ title: string; description: string; group: string }>({ title: '', description: '', group: '' }); + const mapContainerRef = useRef(null); + + const filters = [ + { id: 'all', label: 'Ver Tudo', icon: Eye }, + { id: 'main-flow', label: 'Fluxo Principal', icon: Workflow }, + { id: 'permissions', label: 'Meus Acessos', icon: Shield } + ]; + + const filteredNodes = useMemo(() => { + return nodes.filter(node => { + switch (activeFilter) { + case 'main-flow': return node.isMainFlow; + case 'permissions': return node.hasAccess; + default: return true; + } + }); + }, [nodes, activeFilter]); + + const filteredConnections = useMemo(() => { + return connections.filter(conn => { + const sourceExists = filteredNodes.some(n => n.id === conn.from); + const targetExists = filteredNodes.some(n => n.id === conn.to); + return sourceExists && targetExists; + }); + }, [connections, filteredNodes]); + + const getMapDimensions = useCallback(() => { + if (filteredNodes.length === 0) return { width: 1200, height: 800 }; + + const cardWidth = 240; + const cardHeight = 140; + const padding = 100; // Padding extra para garantir espaço + + let maxX = 0; + let maxY = 0; + + filteredNodes.forEach(node => { + const nodeEl = mapContainerRef.current?.querySelector(`[data-node="${node.id}"]`) as HTMLElement; + if (nodeEl) { + const rect = nodeEl.getBoundingClientRect(); + const containerRect = mapContainerRef.current!.getBoundingClientRect(); + maxX = Math.max(maxX, rect.left - containerRect.left + cardWidth); + maxY = Math.max(maxY, rect.top - containerRect.top + cardHeight); + } else { + // Usar posições iniciais se o elemento ainda não foi renderizado + maxX = Math.max(maxX, node.left + cardWidth); + maxY = Math.max(maxY, node.top + cardHeight); + } + }); + + return { + width: Math.max(1200, maxX + padding), + height: Math.max(800, maxY + padding) + }; + }, [filteredNodes]); + + const [mapDimensions, setMapDimensions] = useState({ width: 1200, height: 800 }); + + useEffect(() => { + const updateDimensions = () => { + setMapDimensions(prev => { + const next = getMapDimensions(); + if (prev.width === next.width && prev.height === next.height) { + return prev; + } + return next; + }); + }; + + updateDimensions(); + const interval = setInterval(updateDimensions, 1000); // Atualizar periodicamente + + return () => clearInterval(interval); + }, [getMapDimensions]); + + + + const updateConnections = useCallback(() => { + if (!mapContainerRef.current) return; + + const container = mapContainerRef.current; + const containerRect = container.getBoundingClientRect(); + + filteredConnections.forEach(conn => { + const fromEl = container.querySelector(`[data-node="${conn.from}"]`) as HTMLElement; + const toEl = container.querySelector(`[data-node="${conn.to}"]`) as HTMLElement; + + if (!fromEl || !toEl) return; + + const fromRect = fromEl.getBoundingClientRect(); + const toRect = toEl.getBoundingClientRect(); + + const fromX = fromRect.left + fromRect.width / 2 - containerRect.left; + const fromY = fromRect.top + fromRect.height / 2 - containerRect.top; + const toX = toRect.left + toRect.width / 2 - containerRect.left; + const toY = toRect.top + toRect.height / 2 - containerRect.top; + + const svgEl = container.querySelector(`[data-connection-svg="${conn.from}-${conn.to}"]`) as HTMLElement; + if (svgEl) { + const pathEl = svgEl.querySelector('path'); + const arrowsGroup = svgEl.querySelector('.arrows-group'); + + if (pathEl) { + pathEl.setAttribute('d', createCurvedPath(fromX, fromY, toX, toY)); + } + + if (arrowsGroup) { + const arrows = getArrowPositions(fromX, fromY, toX, toY); + arrowsGroup.innerHTML = arrows.map(arrow => + `` + ).join(''); + } + } + }); + }, [filteredConnections]); + + useEffect(() => { + updateConnections(); + const handleResize = () => updateConnections(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [updateConnections]); + + const handleNodeMouseOver = (nodeId: string) => { + const relatedNodes = new Set(); + const relatedConnections: string[] = []; + + connections.forEach(conn => { + if (conn.from === nodeId || conn.to === nodeId) { + relatedNodes.add(conn.from); + relatedNodes.add(conn.to); + relatedConnections.push(`${conn.from}-${conn.to}`); + } + }); + + setHighlightedPath([...relatedNodes]); + }; + + const handleNodeMouseOut = () => { + setHighlightedPath([]); + }; + + const handleNodeDoubleClick = (node: SystemMapNode) => { + if (isDragging) return; + setSelectedNode(node); + setEditData({ title: node.title, description: node.description, group: node.group }); + setIsEditing(false); + }; + + const handleMapClick = (e: React.MouseEvent) => { + if (e.target === mapContainerRef.current) { + setSelectedNode(null); + setIsEditing(false); + } + }; + + const handleNodeClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedNode(null); + setIsEditing(false); + }; + + const checkCollision = (nodeEl: HTMLElement, newX: number, newY: number, excludeId: string) => { + const nodeWidth = 240; + const nodeHeight = 140; + const margin = 20; + + return filteredNodes.some(node => { + if (node.id === excludeId) return false; + + const otherEl = mapContainerRef.current?.querySelector(`[data-node="${node.id}"]`) as HTMLElement; + if (!otherEl) return false; + + const otherRect = otherEl.getBoundingClientRect(); + const containerRect = mapContainerRef.current!.getBoundingClientRect(); + const otherX = otherRect.left - containerRect.left; + const otherY = otherRect.top - containerRect.top; + + return !(newX + nodeWidth + margin < otherX || + newX > otherX + nodeWidth + margin || + newY + nodeHeight + margin < otherY || + newY > otherY + nodeHeight + margin); + }); + }; + + const findNonCollidingPosition = (originalX: number, originalY: number, nodeId: string) => { + const step = 30; + const maxAttempts = 50; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const angle = (attempt * 45) % 360; + const distance = Math.floor(attempt / 8) * step; + const newX = originalX + Math.cos(angle * Math.PI / 180) * distance; + const newY = originalY + Math.sin(angle * Math.PI / 180) * distance; + + if (newX < 0 || newY < 0 || newX > mapDimensions.width - 240 || newY > mapDimensions.height - 140) { + continue; + } + + if (!checkCollision(document.createElement('div'), newX, newY, nodeId)) { + return { x: newX, y: newY }; + } + } + + return { x: originalX, y: originalY }; + }; + + const handleNodeDragStart = (e: React.MouseEvent, nodeId: string) => { + e.preventDefault(); + setIsDragging(true); + setDraggedNode(nodeId); + + const startX = e.clientX; + const startY = e.clientY; + const nodeEl = mapContainerRef.current?.querySelector(`[data-node="${nodeId}"]`) as HTMLElement; + if (!nodeEl) return; + + const initialRect = nodeEl.getBoundingClientRect(); + const containerRect = mapContainerRef.current!.getBoundingClientRect(); + const initialLeft = initialRect.left - containerRect.left; + const initialTop = initialRect.top - containerRect.top; + + const handleMouseMove = (moveEvent: MouseEvent) => { + if (!mapContainerRef.current) return; + + const deltaX = moveEvent.clientX - startX; + const deltaY = moveEvent.clientY - startY; + + let newLeft = initialLeft + deltaX; + let newTop = initialTop + deltaY; + + newLeft = Math.max(0, Math.min(newLeft, mapDimensions.width - 240)); + newTop = Math.max(0, Math.min(newTop, mapDimensions.height - 140)); + + if (!checkCollision(nodeEl, newLeft, newTop, nodeId)) { + nodeEl.style.left = `${newLeft}px`; + nodeEl.style.top = `${newTop}px`; + } else { + const { x, y } = findNonCollidingPosition(newLeft, newTop, nodeId); + nodeEl.style.left = `${x}px`; + nodeEl.style.top = `${y}px`; + } + + updateConnections(); + setMapDimensions(getMapDimensions()); + }; + + const handleMouseUp = () => { + setIsDragging(false); + setDraggedNode(null); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + setTimeout(() => setMapDimensions(getMapDimensions()), 100); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + + const handleEditToggle = () => { + setIsEditing(!isEditing); + if (!isEditing && selectedNode) { + setEditData({ title: selectedNode.title, description: selectedNode.description, group: selectedNode.group }); + } + }; + + const handleSaveEdit = () => { + if (selectedNode) { + console.log('Saving changes:', editData); + setIsEditing(false); + } + }; + + const resetLayout = () => { + if (!mapContainerRef.current) return; + + filteredNodes.forEach(node => { + const nodeEl = mapContainerRef.current!.querySelector(`[data-node="${node.id}"]`) as HTMLElement; + if (nodeEl) { + nodeEl.style.left = `${node.left}px`; + nodeEl.style.top = `${node.top}px`; + } + }); + updateConnections(); + setMapDimensions(getMapDimensions()); + }; + + return ( +
+
+
+
+

+ Mapa Interativo - TrackSteel +

+

+ Arraste os módulos, passe o mouse para ver fluxos e duplo-clique para detalhes +

+
+ +
+
+ {filters.map(filter => { + const Icon = filter.icon; + return ( + + ); + })} +
+ + +
+
+
+ +
+
+ + {filteredConnections.map(conn => ( + + + + + ))} + + + {filteredNodes.map(node => ( +
handleNodeDragStart(e, node.id)} + onMouseOver={() => handleNodeMouseOver(node.id)} + onMouseOut={handleNodeMouseOut} + onDoubleClick={() => handleNodeDoubleClick(node)} + onClick={handleNodeClick} + > +
+
+ {node.icon} +
+
+

+ {node.title} +

+

+ {node.group} +

+
+
+ +
+ {node.profiles.slice(0, 2).map((profile, index) => ( + + {profile} + + ))} + {node.profiles.length > 2 && ( + + +{node.profiles.length - 2} + + )} +
+
+ ))} +
+
+ + {selectedNode && ( +
+
+
+
+ {selectedNode.icon} + {isEditing ? ( + setEditData({ ...editData, title: e.target.value })} + className="text-xl font-bold" + /> + ) : ( +

+ {selectedNode.title} +

+ )} +
+
+ {isAdmin && ( + <> + {isEditing ? ( + + ) : ( + + )} + + )} + +
+
+ +
+
+

Descrição

+ {isEditing ? ( +