import { useEffect, useRef, useState } from "react"; import { useDroppedObjectsStore } from "../../../../store/visualization/useDroppedObjectsStore"; import useModuleStore from "../../../../store/useModuleStore"; import { determinePosition } from "../../functions/determinePosition"; import { getActiveProperties } from "../../functions/getActiveProperties"; import { addingFloatingWidgets } from "../../../../services/visulization/zone/addFloatingWidgets"; import { DublicateIcon, KebabIcon, DeleteIcon, } from "../../../../components/icons/ExportCommonIcons"; import DistanceLines from "./DistanceLines"; // Import the DistanceLines component import TotalCardComponent from "./cards/TotalCardComponent"; import WarehouseThroughputComponent from "./cards/WarehouseThroughputComponent"; import FleetEfficiencyComponent from "./cards/FleetEfficiencyComponent"; import { useWidgetStore } from "../../../../store/useWidgetStore"; import { useSocketStore } from "../../../../store/store"; import { usePlayButtonStore } from "../../../../store/usePlayButtonStore"; import { useSelectedZoneStore } from "../../../../store/visualization/useZoneStore"; interface DraggingState { zone: string; index: number; initialPosition: { top: number | "auto"; left: number | "auto"; right: number | "auto"; bottom: number | "auto"; }; } interface DraggingState { zone: string; index: number; initialPosition: { top: number | "auto"; left: number | "auto"; right: number | "auto"; bottom: number | "auto"; }; } const DroppedObjects: React.FC = () => { const { visualizationSocket } = useSocketStore(); const { isPlaying } = usePlayButtonStore(); const zones = useDroppedObjectsStore((state) => state.zones); const [openKebabId, setOpenKebabId] = useState(null); const updateObjectPosition = useDroppedObjectsStore( (state) => state.updateObjectPosition ); const { selectedZone } = useSelectedZoneStore(); const deleteObject = useDroppedObjectsStore((state) => state.deleteObject); const duplicateObject = useDroppedObjectsStore( (state) => state.duplicateObject ); const [draggingIndex, setDraggingIndex] = useState( null ); const [offset, setOffset] = useState<[number, number] | null>(null); const { selectedChartId, setSelectedChartId } = useWidgetStore(); const [activeEdges, setActiveEdges] = useState<{ vertical: "top" | "bottom"; horizontal: "left" | "right"; } | null>(null); // State to track active edges for distance lines const [currentPosition, setCurrentPosition] = useState<{ top: number | "auto"; left: number | "auto"; right: number | "auto"; bottom: number | "auto"; } | null>(null); // State to track the current position during drag const animationRef = useRef(null); const chartWidget = useRef(null); const kebabRef = useRef(null); // Clean up animation frame on unmount useEffect(() => { return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, []); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( kebabRef.current && !kebabRef.current.contains(event.target as Node) ) { setOpenKebabId(null); } }; // Add event listener when component mounts document.addEventListener("mousedown", handleClickOutside); // Clean up event listener when component unmounts return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); const zoneEntries = Object.entries(zones); if (zoneEntries.length === 0) return null; const [zoneName, zone] = zoneEntries[0]; function handleDuplicate(zoneName: string, index: number) { setOpenKebabId(null); duplicateObject(zoneName, index); // Call the duplicateObject method from the store setSelectedChartId(null); } async function handleDelete(zoneName: string, id: string) { try { setSelectedChartId(null); const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; let deleteFloatingWidget = { floatWidgetID: id, organization: organization, zoneId: zone.zoneId, }; if (visualizationSocket) { visualizationSocket.emit("v2:viz-float:delete", deleteFloatingWidget); } deleteObject(zoneName, id); } catch (error) { echo.error("Failed to delete widget"); } } const handlePointerDown = (event: React.PointerEvent, index: number) => { if ( (event.target as HTMLElement).closest(".kebab-options") || (event.target as HTMLElement).closest(".kebab") ) { return; // Prevent dragging when clicking on the kebab menu or its options } const obj = zone.objects[index]; const element = event.currentTarget as HTMLElement; element.setPointerCapture(event.pointerId); const container = document.getElementById("work-space-three-d-canvas"); if (!container) return; const rect = container.getBoundingClientRect(); const relativeX = event.clientX - rect.left; const relativeY = event.clientY - rect.top; // Determine active properties for the initial position const [activeProp1, activeProp2] = getActiveProperties(obj.position); // Set active edges for distance lines const vertical = activeProp1 === "top" ? "top" : "bottom"; const horizontal = activeProp2 === "left" ? "left" : "right"; setActiveEdges({ vertical, horizontal }); // Store the initial position strategy and active edges setDraggingIndex({ zone: zoneName, index, initialPosition: { ...obj.position }, }); // Calculate offset from mouse to object edges let offsetX = 0; let offsetY = 0; if (activeProp1 === "top") { offsetY = relativeY - (obj.position.top as number); } else { offsetY = rect.height - relativeY - (obj.position.bottom as number); } if (activeProp2 === "left") { offsetX = relativeX - (obj.position.left as number); } else { offsetX = rect.width - relativeX - (obj.position.right as number); } setOffset([offsetY, offsetX]); // Add native event listeners for smoother tracking const handlePointerMoveNative = (e: PointerEvent) => { if (!draggingIndex || !offset) return; if (isPlaying === true) return; const rect = container.getBoundingClientRect(); const relativeX = e.clientX - rect.left; const relativeY = e.clientY - rect.top; // Use requestAnimationFrame for smooth updates animationRef.current = requestAnimationFrame(() => { // Dynamically determine the current position strategy const newPositionStrategy = determinePosition( rect, relativeX, relativeY ); const [activeProp1, activeProp2] = getActiveProperties(newPositionStrategy); // Update active edges for distance lines const vertical = activeProp1 === "top" ? "top" : "bottom"; const horizontal = activeProp2 === "left" ? "left" : "right"; setActiveEdges({ vertical, horizontal }); // Calculate new position based on the active properties let newY = 0; let newX = 0; if (activeProp1 === "top") { newY = relativeY - offset[0]; } else { newY = rect.height - (relativeY + offset[0]); } if (activeProp2 === "left") { newX = relativeX - offset[1]; } else { newX = rect.width - (relativeX + offset[1]); } // Apply boundaries newX = Math.max(0, Math.min(rect.width - 50, newX)); newY = Math.max(0, Math.min(rect.height - 50, newY)); // Create new position object const newPosition = { ...newPositionStrategy, [activeProp1]: newY, [activeProp2]: newX, // Clear opposite properties [activeProp1 === "top" ? "bottom" : "top"]: "auto", [activeProp2 === "left" ? "right" : "left"]: "auto", }; // Update the current position state for DistanceLines setCurrentPosition(newPosition); updateObjectPosition(zoneName, draggingIndex.index, newPosition); }); }; const handlePointerUpNative = async (e: PointerEvent) => { // Clean up native event listeners element.removeEventListener("pointermove", handlePointerMoveNative); element.removeEventListener("pointerup", handlePointerUpNative); element.releasePointerCapture(e.pointerId); if (!draggingIndex || !offset) return; const rect = container.getBoundingClientRect(); const relativeX = e.clientX - rect.left; const relativeY = e.clientY - rect.top; // Determine final position strategy const finalPosition = determinePosition(rect, relativeX, relativeY); const [activeProp1, activeProp2] = getActiveProperties(finalPosition); // Calculate final position let finalY = 0; let finalX = 0; if (activeProp1 === "top") { finalY = relativeY - offset[0]; } else { finalY = rect.height - (relativeY + offset[0]); } if (activeProp2 === "left") { finalX = relativeX - offset[1]; } else { finalX = rect.width - (relativeX + offset[1]); } // Apply boundaries finalX = Math.max(0, Math.min(rect.width - 50, finalX)); finalY = Math.max(0, Math.min(rect.height - 50, finalY)); const boundedPosition = { ...finalPosition, [activeProp1]: finalY, [activeProp2]: finalX, [activeProp1 === "top" ? "bottom" : "top"]: "auto", [activeProp2 === "left" ? "right" : "left"]: "auto", }; try { // Save to backend const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; const response = await addingFloatingWidgets( zone.zoneId, organization, { ...zone.objects[draggingIndex.index], position: boundedPosition, } ); if (response.message === "Widget updated successfully") { updateObjectPosition(zoneName, draggingIndex.index, boundedPosition); } } catch (error) { echo.error("Failed to add widget"); console.log(error); } finally { setDraggingIndex(null); setOffset(null); setActiveEdges(null); setCurrentPosition(null); if (animationRef.current) { cancelAnimationFrame(animationRef.current); animationRef.current = null; } } }; // Add native event listeners element.addEventListener("pointermove", handlePointerMoveNative); element.addEventListener("pointerup", handlePointerUpNative); }; const handlePointerMove = (event: React.PointerEvent) => { if (!draggingIndex || !offset) return; if (isPlaying === true) return; const container = document.getElementById("work-space-three-d-canvas"); if (!container) return; const rect = container.getBoundingClientRect(); const relativeX = event.clientX - rect.left; const relativeY = event.clientY - rect.top; // Dynamically determine the current position strategy const newPositionStrategy = determinePosition(rect, relativeX, relativeY); const [activeProp1, activeProp2] = getActiveProperties(newPositionStrategy); // Update active edges for distance lines const vertical = activeProp1 === "top" ? "top" : "bottom"; const horizontal = activeProp2 === "left" ? "left" : "right"; setActiveEdges({ vertical, horizontal }); // Calculate new position based on the active properties let newY = 0; let newX = 0; if (activeProp1 === "top") { newY = relativeY - offset[0]; } else { newY = rect.height - (relativeY + offset[0]); } if (activeProp2 === "left") { newX = relativeX - offset[1]; } else { newX = rect.width - (relativeX + offset[1]); } // Apply boundaries newX = Math.max(0, Math.min(rect.width - 50, newX)); newY = Math.max(0, Math.min(rect.height - 50, newY)); // Create new position object const newPosition = { ...newPositionStrategy, [activeProp1]: newY, [activeProp2]: newX, // Clear opposite properties [activeProp1 === "top" ? "bottom" : "top"]: "auto", [activeProp2 === "left" ? "right" : "left"]: "auto", }; // Update the current position state for DistanceLines setCurrentPosition(newPosition); // Update position immediately without animation frame updateObjectPosition(zoneName, draggingIndex.index, newPosition); }; const handlePointerUp = async (event: React.PointerEvent) => { try { if (!draggingIndex || !offset) return; if (isPlaying === true) return; const container = document.getElementById("work-space-three-d-canvas"); if (!container) return; const rect = container.getBoundingClientRect(); const relativeX = event.clientX - rect.left; const relativeY = event.clientY - rect.top; // Determine final position strategy const finalPosition = determinePosition(rect, relativeX, relativeY); const [activeProp1, activeProp2] = getActiveProperties(finalPosition); // Calculate final position let finalY = 0; let finalX = 0; if (activeProp1 === "top") { finalY = relativeY - offset[0]; } else { finalY = rect.height - (relativeY + offset[0]); } if (activeProp2 === "left") { finalX = relativeX - offset[1]; } else { finalX = rect.width - (relativeX + offset[1]); } // Apply boundaries finalX = Math.max(0, Math.min(rect.width - 50, finalX)); finalY = Math.max(0, Math.min(rect.height - 50, finalY)); const boundedPosition = { ...finalPosition, [activeProp1]: finalY, [activeProp2]: finalX, // Clear opposite properties [activeProp1 === "top" ? "bottom" : "top"]: "auto", [activeProp2 === "left" ? "right" : "left"]: "auto", }; // Save to backend const email = localStorage.getItem("email") || ""; const organization = email?.split("@")[1]?.split(".")[0]; let updateFloatingWidget = { organization: organization, widget: { ...zone.objects[draggingIndex.index], position: boundedPosition, }, index: draggingIndex.index, zoneId: zone.zoneId, }; if (visualizationSocket) { visualizationSocket.emit("v2:viz-float:add", updateFloatingWidget); } updateObjectPosition(zoneName, draggingIndex.index, boundedPosition); } catch (error) { echo.error("Failed to add widget"); console.log(error); } finally { // Clean up regardless of success or failure setDraggingIndex(null); setOffset(null); setActiveEdges(null); setCurrentPosition(null); // Cancel any pending animation frame if (animationRef.current) { cancelAnimationFrame(animationRef.current); animationRef.current = null; } } }; const handleKebabClick = (id: string, event: React.MouseEvent) => { event.stopPropagation(); setOpenKebabId((prevId) => (prevId === id ? null : id)); }; const containerHeight = getComputedStyle( document.documentElement ).getPropertyValue("--realTimeViz-container-height"); const containerWidth = getComputedStyle( document.documentElement ).getPropertyValue("--realTimeViz-container-width"); const heightMultiplier = parseFloat(containerHeight) * 0.14; const widthMultiplier = parseFloat(containerWidth) * 0.13; console.log('zone?.objects: ', zone?.objects); return (
{zone?.objects?.map((obj, index) => { const topPosition = typeof obj?.position?.top === "number" ? `calc(${obj?.position?.top}px + ${ isPlaying && selectedZone?.activeSides?.includes("top") ? `${heightMultiplier - 55}px` : "0px" })` : "auto"; const leftPosition = typeof obj?.position?.left === "number" ? `calc(${obj?.position?.left}px + ${ isPlaying && selectedZone?.activeSides?.includes("left") ? `${widthMultiplier - 150}px` : "0px" })` : "auto"; const rightPosition = typeof obj?.position?.right === "number" ? `calc(${obj?.position?.right}px + ${ isPlaying && selectedZone?.activeSides?.includes("right") ? `${widthMultiplier - 150}px` : "0px" })` : "auto"; const bottomPosition = typeof obj?.position?.bottom === "number" ? `calc(${obj?.position?.bottom}px + ${ isPlaying && selectedZone?.activeSides?.includes("bottom") ? `${heightMultiplier - 55}px` : "0px" })` : "auto"; return (
{ setSelectedChartId(obj); handlePointerDown(event, index); }} > {obj.className === "floating total-card" ? ( ) : obj.className === "warehouseThroughput floating" ? ( ) : obj.className === "fleetEfficiency floating" ? ( ) : null}
{ event.stopPropagation(); handleKebabClick(obj.id, event); }} >
{openKebabId === obj.id && (
{ event.stopPropagation(); handleDuplicate(zoneName, index); // Call the duplicate handler }} >
Duplicate
{ event.stopPropagation(); handleDelete(zoneName, obj.id); // Call the delete handler }} >
Delete
)}
); })} {/* Render DistanceLines component during drag */} {isPlaying === false && draggingIndex !== null && activeEdges !== null && currentPosition !== null && ( )}
); }; export default DroppedObjects;