Dwinzo_dev/app/src/modules/visualization/widgets/floating/DroppedFloatingWidgets.tsx

667 lines
22 KiB
TypeScript

import { WalletIcon } from "../../../../components/icons/3dChartIcons";
import { useEffect, useRef, useState } from "react";
import {
useDroppedObjectsStore,
Zones,
} from "../../../../store/useDroppedObjectsStore";
import useModuleStore from "../../../../store/useModuleStore";
import { determinePosition } from "../../functions/determinePosition";
import { getActiveProperties } from "../../functions/getActiveProperties";
import { addingFloatingWidgets } from "../../../../services/realTimeVisulization/zoneData/addFloatingWidgets";
import {
DublicateIcon,
KebabIcon,
DeleteIcon,
} from "../../../../components/icons/ExportCommonIcons";
import DistanceLines from "./DistanceLines"; // Import the DistanceLines component
import { deleteFloatingWidgetApi } from "../../../../services/realTimeVisulization/zoneData/deleteFloatingWidget";
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 { useClickOutside } from "../../functions/handleWidgetsOuterClick";
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
import { useSelectedZoneStore } from "../../../../store/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<string | null>(null);
const updateObjectPosition = useDroppedObjectsStore(
(state) => state.updateObjectPosition
);
const { setSelectedZone, selectedZone } = useSelectedZoneStore();
const deleteObject = useDroppedObjectsStore((state) => state.deleteObject);
const duplicateObject = useDroppedObjectsStore(
(state) => state.duplicateObject
);
const [draggingIndex, setDraggingIndex] = useState<DraggingState | null>(
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<number | null>(null);
const { activeModule } = useModuleStore();
const chartWidget = useRef<HTMLDivElement>(null);
// useClickOutside(chartWidget, () => {
// setSelectedChartId(null);
// });
const kebabRef = useRef<HTMLDivElement>(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
}
async function handleDelete(zoneName: string, id: string) {
try {
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);
// let res = await deleteFloatingWidgetApi(id, organization);
// s
// if (res.message === "FloatingWidget deleted successfully") {
// deleteObject(zoneName, id, index); // Call the deleteObject method from the store
// }
} catch (error) {}
}
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("real-time-vis-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) {
} 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("real-time-vis-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);
// if (!animationRef.current) {
// animationRef.current = requestAnimationFrame(() => {
// updateObjectPosition(zoneName, draggingIndex.index, newPosition);
// animationRef.current = null;
// });
// }
};
const handlePointerUp = async (event: React.PointerEvent<HTMLDivElement>) => {
try {
if (!draggingIndex || !offset) return;
if (isPlaying === true) return;
const container = document.getElementById("real-time-vis-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];
// const response = await addingFloatingWidgets(zone.zoneId, organization, {
// ...zone.objects[draggingIndex.index],
// position: boundedPosition,
// });
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);
}
// if (response.message === "Widget updated successfully") {
updateObjectPosition(zoneName, draggingIndex.index, boundedPosition);
// }
// // Clean up
// setDraggingIndex(null);
// setOffset(null);
// setActiveEdges(null); // Clear active edges
// setCurrentPosition(null); // Reset current position
// if (animationRef.current) {
// cancelAnimationFrame(animationRef.current);
// animationRef.current = null;
// }
} catch (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;
return (
<div
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
className="floating-wrapper"
>
{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 - 100}px`
: "0px"
})`
: "auto";
const rightPosition =
typeof obj.position.right === "number"
? `calc(${obj.position.right}px + ${
isPlaying && selectedZone.activeSides.includes("right")
? `${widthMultiplier - 100}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 (
<div
key={`${zoneName}-${index}`}
className={`${obj.className} ${
selectedChartId?.id === obj.id && "activeChart"
} `}
ref={chartWidget}
style={{
position: "absolute",
top: topPosition,
left: leftPosition,
right: rightPosition,
bottom: bottomPosition,
pointerEvents: isPlaying ? "none" : "auto",
minHeight: `${
obj.className === "warehouseThroughput" && "150px !important"
} `,
}}
onPointerDown={(event) => {
setSelectedChartId(obj);
handlePointerDown(event, index);
}}
>
{obj.className === "floating total-card" ? (
<TotalCardComponent object={obj} />
) : obj.className === "warehouseThroughput floating" ? (
<WarehouseThroughputComponent object={obj} />
) : obj.className === "fleetEfficiency floating" ? (
<FleetEfficiencyComponent object={obj} />
) : null}
<div
className="icon kebab"
ref={kebabRef}
onClick={(event) => {
event.stopPropagation();
handleKebabClick(obj.id, event);
}}
>
<KebabIcon />
</div>
{openKebabId === obj.id && (
<div className="kebab-options" ref={kebabRef}>
<div
className="dublicate btn"
onClick={(event) => {
event.stopPropagation();
handleDuplicate(zoneName, index); // Call the duplicate handler
}}
>
<div className="icon">
<DublicateIcon />
</div>
<div className="label">Duplicate</div>
</div>
<div
className="edit btn"
onClick={(event) => {
event.stopPropagation();
handleDelete(zoneName, obj.id); // Call the delete handler
}}
>
<div className="icon">
<DeleteIcon />
</div>
<div className="label">Delete</div>
</div>
</div>
)}
</div>
);
})}
{/* Render DistanceLines component during drag */}
{isPlaying === false &&
draggingIndex !== null &&
activeEdges !== null &&
currentPosition !== null && (
<DistanceLines
obj={{
position: {
top:
currentPosition.top !== "auto"
? currentPosition.top
: undefined,
bottom:
currentPosition.bottom !== "auto"
? currentPosition.bottom
: undefined,
left:
currentPosition.left !== "auto"
? currentPosition.left
: undefined,
right:
currentPosition.right !== "auto"
? currentPosition.right
: undefined,
},
}}
activeEdges={activeEdges}
/>
)}
</div>
);
};
export default DroppedObjects;