import { useEffect, useMemo, useRef, useState, type PointerEvent, type ReactNode, type RefObject } from "react"; import { ChevronDown, FolderPlus, LocateFixed, Move, Plus, RefreshCw, Save, Search, Settings2, X, ZoomIn, ZoomOut, } from "lucide-react"; import satelliteImage from "../../../assets/sattelite169.png"; import { useSynopticVariables } from "../hooks/useSynopticVariables"; import type { SynopticVariable } from "../hooks/useSynopticVariables"; type SynopticPageProps = { theme: "dark" | "light"; }; type CardVariant = "compact" | "large"; type MapItem = { id: string; variableId: string; x: number; y: number; variant: CardVariant; showLabel: boolean; }; const initialMapItems: MapItem[] = []; export function SynopticPage({ theme }: SynopticPageProps) { const isDark = theme === "dark"; const canvasRef = useRef(null); const synoptic = useSynopticVariables(); const [items, setItems] = useState(initialMapItems); const [selectedItemId, setSelectedItemId] = useState(null); const [activeGroup, setActiveGroup] = useState("Todas"); const [search, setSearch] = useState(""); const [pendingVariableDrag, setPendingVariableDrag] = useState<{ id: string; pointerId: number; } | null>(null); const [dragState, setDragState] = useState<{ id: string; pointerId: number; offsetX: number; offsetY: number; } | null>(null); const variablesById = useMemo( () => new Map( synoptic.variables.map((variable) => [ variable.id, variable, ]), ), [synoptic.variables], ); const selectedItem = items.find((item) => item.id === selectedItemId) ?? null; const selectedVariable = selectedItem ? variablesById.get(selectedItem.variableId) ?? null : null; const placedVariableIds = useMemo( () => new Set(items.map((item) => item.variableId)), [items], ); const visiblePlacedVariableIds = useMemo(() => { const ids = new Set(placedVariableIds); if (pendingVariableDrag) { const pendingItem = items.find((item) => item.id === pendingVariableDrag.id); if (pendingItem) { ids.delete(pendingItem.variableId); } } return ids; }, [items, placedVariableIds, pendingVariableDrag]); const availableGroups = useMemo( () => Array.from( new Set( synoptic.variables .filter((variable) => !visiblePlacedVariableIds.has(variable.id)) .map((variable) => variable.group), ), ), [synoptic.variables, visiblePlacedVariableIds], ); useEffect(() => { if (activeGroup !== "Todas" && !availableGroups.includes(activeGroup)) { setActiveGroup("Todas"); } }, [activeGroup, availableGroups]); function getPointInCanvas(event: PointerEvent) { const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) { return null; } const x = ((event.clientX - rect.left) / rect.width) * 100; const y = ((event.clientY - rect.top) / rect.height) * 100; if (x < 0 || x > 100 || y < 0 || y > 100) { return null; } return { x, y }; } function handleCardPointerDown(event: PointerEvent, item: MapItem) { const point = getPointInCanvas(event); if (!point) { return; } event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); setSelectedItemId(item.id); setDragState({ id: item.id, pointerId: event.pointerId, offsetX: point.x - item.x, offsetY: point.y - item.y, }); } function handleCardPointerMove(event: PointerEvent) { if (!dragState || event.pointerId !== dragState.pointerId) { return; } const point = getPointInCanvas(event); if (!point) { return; } setItems((currentItems) => currentItems.map((item) => item.id === dragState.id ? { ...item, x: clampPercent(point.x - dragState.offsetX), y: clampPercent(point.y - dragState.offsetY), } : item, ), ); } function handleCardPointerUp(event: PointerEvent) { if (dragState && event.pointerId === dragState.pointerId) { event.currentTarget.releasePointerCapture(event.pointerId); setDragState(null); } } function createMapItemFromVariable(variable: SynopticVariable, point: { x: number; y: number }) { const newItem: MapItem = { id: `map-item-${crypto.randomUUID()}`, variableId: variable.id, x: clampPercent(point.x), y: clampPercent(point.y), variant: "compact", showLabel: true, }; setItems((currentItems) => [...currentItems, newItem]); setSelectedItemId(newItem.id); return newItem; } function handleVariablePointerDown( event: PointerEvent, variable: SynopticVariable, ) { event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); const point = getPointInCanvas(event) ?? { x: 50, y: 50, }; const newItem = createMapItemFromVariable(variable, point); setPendingVariableDrag({ id: newItem.id, pointerId: event.pointerId, }); setDragState({ id: newItem.id, pointerId: event.pointerId, offsetX: 0, offsetY: 0, }); } function handleVariablePointerMove(event: PointerEvent) { if (!pendingVariableDrag || event.pointerId !== pendingVariableDrag.pointerId) { return; } const point = getPointInCanvas(event); if (!point) { return; } setItems((currentItems) => currentItems.map((item) => item.id === pendingVariableDrag.id ? { ...item, x: clampPercent(point.x), y: clampPercent(point.y), } : item, ), ); } function handleVariablePointerUp(event: PointerEvent) { if (!pendingVariableDrag || event.pointerId !== pendingVariableDrag.pointerId) { return; } const point = getPointInCanvas(event); if (!point) { setItems((currentItems) => currentItems.filter((item) => item.id !== pendingVariableDrag.id), ); if (selectedItemId === pendingVariableDrag.id) { setSelectedItemId(null); } } if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } setPendingVariableDrag(null); setDragState(null); } function handleRemoveSelectedItem() { if (!selectedItemId) { return; } setItems((currentItems) => currentItems.filter((item) => item.id !== selectedItemId), ); setSelectedItemId(null); } function handleSelectedVariantChange(variant: CardVariant) { if (!selectedItemId) { return; } setItems((currentItems) => currentItems.map((item) => item.id === selectedItemId ? { ...item, variant, } : item, ), ); } function handleSelectedShowLabelChange(showLabel: boolean) { if (!selectedItemId) { return; } setItems((currentItems) => currentItems.map((item) => item.id === selectedItemId ? { ...item, showLabel, } : item, ), ); } useEffect(() => { function handleKeyDown(event: KeyboardEvent) { if ( event.key !== "Delete" && event.key !== "Backspace" ) { return; } if (!selectedItemId) { return; } const activeElement = document.activeElement; const typing = activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement || activeElement instanceof HTMLSelectElement; if (typing) { return; } setItems((currentItems) => currentItems.filter( (item) => item.id !== selectedItemId, ), ); setSelectedItemId(null); } window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [selectedItemId]); return (
); } function WorkspaceBar({ isDark }: { isDark: boolean }) { return (

Workspace sinótico

} label="Novo" /> } label="Guardar" primary /> } label="Atualizar" />
Alterações guardadas
); } function WorkspaceButton({ isDark, icon, label, primary, }: { isDark: boolean; icon: ReactNode; label: string; primary?: boolean; }) { if (primary) { return ( ); } return ( ); } function VariablesPanel({ isDark, variables, placedVariableIds, availableGroups, activeGroup, search, onActiveGroupChange, onSearchChange, onVariablePointerDown, onVariablePointerMove, onVariablePointerUp, }: { isDark: boolean; variables: SynopticVariable[]; placedVariableIds: Set; availableGroups: string[]; activeGroup: string; search: string; onActiveGroupChange: (group: string) => void; onSearchChange: (value: string) => void; onVariablePointerDown: ( event: PointerEvent, variable: SynopticVariable, ) => void; onVariablePointerMove: (event: PointerEvent) => void; onVariablePointerUp: (event: PointerEvent) => void; }) { const groups = availableGroups; const tabs = ["Todas", ...availableGroups]; const filteredVariables = variables.filter((variable) => { if (placedVariableIds.has(variable.id)) { return false; } const matchesGroup = activeGroup === "Todas" || variable.group === activeGroup; const query = search.trim().toLowerCase(); const matchesSearch = query.length === 0 || `${variable.label} ${variable.key} ${variable.unit}` .toLowerCase() .includes(query); return matchesGroup && matchesSearch; }); return ( ); } function Toolbar({ isDark }: { isDark: boolean }) { const tools = [ { label: "Selecionar", icon: , active: true }, { label: "Mover", icon: }, { label: "Texto", icon: T }, ]; return (
{tools.map((tool) => ( ))}
); } function CanvasMock({ isDark, canvasRef, items, variablesById, selectedItemId, draggingItemId, onCardPointerDown, onCardPointerMove, onCardPointerUp, }: { isDark: boolean; canvasRef: RefObject; items: MapItem[]; variablesById: Map; selectedItemId: string | null; draggingItemId?: string; onCardPointerDown: (event: PointerEvent, item: MapItem) => void; onCardPointerMove: (event: PointerEvent) => void; onCardPointerUp: (event: PointerEvent) => void; }) { return (
Vista aérea da instalação {items.length === 0 && (

Arraste variáveis para a imagem

Os cartões ficam presos ao ponto exato da imagem.

)} {items.map((item) => { const variable = variablesById.get(item.variableId); if (!variable) { return null; } return ( ); })}
100%
); } function MapCard({ item, variable, selected, dragging, onPointerDown, onPointerMove, onPointerUp, }: { item: MapItem; variable: SynopticVariable; selected: boolean; dragging: boolean; onPointerDown: (event: PointerEvent, item: MapItem) => void; onPointerMove: (event: PointerEvent) => void; onPointerUp: (event: PointerEvent) => void; }) { const Icon = variable.icon; const isLarge = item.variant === "large"; return (
onPointerDown(event, item)} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerUp} className="absolute z-10 cursor-grab touch-none select-none active:cursor-grabbing" style={{ left: `${item.x}%`, top: `${item.y}%`, transform: "translate(-50%, -100%)", boxShadow: dragging ? "0 24px 80px rgba(24,184,166,0.26)" : undefined, }} >
{selected && ( <> {[ "-top-1 -left-1", "-top-1 -right-1", "-bottom-1 -left-1", "-bottom-1 -right-1", ].map((position) => ( ))} )}
{item.showLabel && (

{variable.label}

)}

{formatVariableValue(variable)}

{isLarge && (
{variable.group} {variable.connected ? "Online" : "Offline"}
)}
); } function PropertiesPanel({ isDark, selectedItem, selectedVariable, onRemoveSelectedItem, onVariantChange, onShowLabelChange, }: { isDark: boolean; selectedItem: MapItem | null; selectedVariable: SynopticVariable | null; onRemoveSelectedItem: () => void; onVariantChange: (variant: CardVariant) => void; onShowLabelChange: (showLabel: boolean) => void; }) { if (!selectedItem || !selectedVariable) { return ( ); } const Icon = selectedVariable.icon; return ( ); } function PanelSection({ title, children, }: { title: string; children: ReactNode; }) { return (

{title}

{children}
); } function InputMock({ label, value, isDark, }: { label: string; value: string; isDark: boolean; }) { return (
{label} {value}
); } function clampPercent(value: number) { return Math.min(96, Math.max(4, value)); } function SegmentedControl({ isDark, value, options, onChange, }: { isDark: boolean; value: string; options: { label: string; value: string }[]; onChange: (value: string) => void; }) { return (
{options.map((option) => ( ))}
); } function smallToolClass(isDark: boolean) { return isDark ? "grid h-9 w-9 place-items-center rounded-[6px] text-[#A8B3C7] transition hover:bg-white/10 hover:text-white" : "grid h-9 w-9 place-items-center rounded-[6px] text-slate-600 transition hover:bg-[#F1F5F9] hover:text-[#0F172A]"; } function formatVariableValue(variable: SynopticVariable) { if (variable.value === null || variable.value === undefined) { return "—"; } if (typeof variable.value === "number") { const value = Number.isInteger(variable.value) ? variable.value.toString() : variable.value.toFixed(1); return `${value} ${variable.unit}`.trim(); } return `${variable.value} ${variable.unit}`.trim(); } export default SynopticPage;