first commit

This commit is contained in:
2025-06-10 15:28:23 +05:30
commit e22a2dc275
699 changed files with 100382 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
import React, { useEffect, useState, useRef } from "react";
import { usePlayButtonStore } from "../../store/usePlayButtonStore";
import Panel from "./widgets/panel/Panel";
import AddButtons from "./widgets/panel/AddButtons";
import { useSelectedZoneStore } from "../../store/visualization/useZoneStore";
import DisplayZone from "./zone/DisplayZone";
import useModuleStore from "../../store/useModuleStore";
import { getZone2dData } from "../../services/visulization/zone/getZoneData";
import SocketRealTimeViz from "./socket/realTimeVizSocket.dev";
import RenderOverlay from "../../components/templates/Overlay";
import ConfirmationPopup from "../../components/layout/confirmationPopup/ConfirmationPopup";
import DroppedObjects from "./widgets/floating/DroppedFloatingWidgets";
import EditWidgetOption from "../../components/ui/menu/EditWidgetOption";
import { useEditWidgetOptionsStore, useRightClickSelected, useRightSelected, } from "../../store/visualization/useZone3DWidgetStore";
import OuterClick from "../../utils/outerClick";
import { useWidgetStore } from "../../store/useWidgetStore";
import { useParams } from "react-router-dom";
type Side = "top" | "bottom" | "left" | "right";
type FormattedZoneData = Record<
string,
{
activeSides: Side[];
panelOrder: Side[];
points: [];
lockedPanels: Side[];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: Widget[];
}
>;
type Widget = {
id: string;
type: string;
title: string;
panel: Side;
data: any;
};
// Define the type for HiddenPanels, where keys are zone IDs and values are arrays of hidden sides
interface HiddenPanels {
[zoneUuid: string]: Side[];
}
const RealTimeVisulization: React.FC = () => {
const [hiddenPanels, setHiddenPanels] = React.useState<HiddenPanels>({});
const containerRef = useRef<HTMLDivElement>(null);
const { isPlaying } = usePlayButtonStore();
const { activeModule } = useModuleStore();
const [zonesData, setZonesData] = useState<FormattedZoneData>({});
const { selectedZone, setSelectedZone } = useSelectedZoneStore();
const { setRightSelect } = useRightSelected();
const { editWidgetOptions, setEditWidgetOptions } =
useEditWidgetOptionsStore();
const { rightClickSelected, setRightClickSelected } = useRightClickSelected();
const [openConfirmationPopup, setOpenConfirmationPopup] = useState(false);
const { setSelectedChartId } = useWidgetStore();
const [waitingPanels, setWaitingPanels] = useState(null);
const { projectId } = useParams();
OuterClick({
contextClassName: [
"chart-container",
"floating",
"sidebar-right-wrapper",
"card",
"dropdown-menu",
"dropdown-options",
],
setMenuVisible: () => setSelectedChartId(null),
});
useEffect(() => {
async function GetZoneData() {
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0];
try {
const response = await getZone2dData(organization, projectId);
// console.log('responseRt: ', response);
if (!Array.isArray(response)) {
return;
}
const formattedData = response.reduce<FormattedZoneData>(
(acc, zone) => {
acc[zone.zoneName] = {
activeSides: [],
panelOrder: [],
lockedPanels: [],
points: zone.points,
zoneUuid: zone.zoneUuid,
zoneViewPortTarget: zone.viewPortCenter,
zoneViewPortPosition: zone.viewPortposition,
widgets: [],
};
return acc;
},
{}
);
setZonesData(formattedData);
} catch (error) {
echo.error("Failed to fetch zone data");
console.log(error);
}
}
GetZoneData();
}, [activeModule]); // Removed `zones` from dependencies
useEffect(() => {
setZonesData((prev) => {
if (!selectedZone) return prev;
return {
...prev,
[selectedZone.zoneName]: {
...prev[selectedZone.zoneName], // Keep existing properties
activeSides: selectedZone.activeSides || [],
panelOrder: selectedZone.panelOrder || [],
lockedPanels: selectedZone.lockedPanels || [],
points: selectedZone.points || [],
zoneUuid: selectedZone.zoneUuid || "",
zoneViewPortTarget: selectedZone.zoneViewPortTarget || [],
zoneViewPortPosition: selectedZone.zoneViewPortPosition || [],
widgets: selectedZone.widgets || [],
},
};
});
}, [selectedZone]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const editWidgetOptions = document.querySelector(
".editWidgetOptions-wrapper"
);
if (
editWidgetOptions &&
!editWidgetOptions.contains(event.target as Node)
) {
setRightClickSelected(null);
setRightSelect(null);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [setRightClickSelected, setRightSelect]);
const [canvasDimensions, setCanvasDimensions] = useState({
width: 0,
height: 0,
});
useEffect(() => {
const canvas = document.getElementById("work-space-three-d-canvas");
if (!canvas) return;
const updateCanvasDimensions = () => {
const rect = canvas.getBoundingClientRect();
setCanvasDimensions({
width: rect.width,
height: rect.height,
});
};
updateCanvasDimensions();
const resizeObserver = new ResizeObserver(updateCanvasDimensions);
resizeObserver.observe(canvas);
return () => resizeObserver.unobserve(canvas);
}, []);
return (
<>
<style>
{`
:root {
--realTimeViz-container-width: ${canvasDimensions.width};
--realTimeViz-container-height: ${canvasDimensions.height};
}
`}
</style>
<div
ref={containerRef}
className="realTime-viz"
id="real-time-vis-canvas"
style={{
height: isPlaying || activeModule !== "visualization" ? "100vh" : "",
width: isPlaying || activeModule !== "visualization" ? "100vw" : "",
left: isPlaying || activeModule !== "visualization" ? "0%" : "",
borderRadius:
isPlaying || activeModule !== "visualization" ? "" : "6px",
}}
>
<div className="realTime-viz-wrapper">
{openConfirmationPopup && (
<RenderOverlay>
<ConfirmationPopup
message={"Are you sure want to delete?"}
onConfirm={() => console.log("Confirmed")}
onCancel={() => setOpenConfirmationPopup(false)}
/>
</RenderOverlay>
)}
{activeModule === "visualization" && selectedZone.zoneName !== "" && (
<DroppedObjects />
)}
{activeModule === "visualization" && <SocketRealTimeViz />}
{activeModule === "visualization" &&
editWidgetOptions &&
rightClickSelected && (
<EditWidgetOption
options={[
"Duplicate",
"Vertical Move",
"Horizontal Move",
"RotateX",
"RotateY",
"Delete",
]}
onClick={(e) => {
setRightSelect(e);
setEditWidgetOptions(false);
}}
/>
)}
{activeModule === "visualization" && (
<>
<DisplayZone
zonesData={zonesData}
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
hiddenPanels={hiddenPanels}
setHiddenPanels={setHiddenPanels}
/>
{!isPlaying && selectedZone?.zoneName !== "" && (
<AddButtons
hiddenPanels={hiddenPanels}
setHiddenPanels={setHiddenPanels}
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
waitingPanels={waitingPanels}
setWaitingPanels={setWaitingPanels}
/>
)}
<Panel
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
hiddenPanels={hiddenPanels}
setZonesData={setZonesData}
waitingPanels={waitingPanels}
/>
</>
)}
</div>
</div>
</>
);
};
export default RealTimeVisulization;

View File

@@ -0,0 +1,34 @@
import html2canvas from "html2canvas";
export const captureVisualization = async (): Promise<string | null> => {
const container = document.getElementById("work-space-three-d-canvas");
if (!container) {
console.error("Container element not found");
return null;
}
try {
// Hide any elements you don't want in the screenshot
const originalVisibility = container.style.visibility;
container.style.visibility = 'visible';
const canvas = await html2canvas(container, {
scale: 2, // Higher scale for better quality
logging: false, // Disable console logging
useCORS: true, // Handle cross-origin images
allowTaint: true, // Allow tainted canvas
backgroundColor: '#ffffff', // Set white background
removeContainer: true // Clean up temporary containers
});
// Restore original visibility
container.style.visibility = originalVisibility;
// Convert to PNG with highest quality
return canvas.toDataURL('image/png', 1.0);
} catch (error) {
echo.error("Failed to capturing visualization");
console.error("Error capturing visualization:", error);
return null;
}
};

View File

@@ -0,0 +1,77 @@
export function determinePosition(
canvasRect: DOMRect,
relativeX: number,
relativeY: number
): {
top: number | "auto";
left: number | "auto";
right: number | "auto";
bottom: number | "auto";
} {
const centerX = canvasRect.width / 2;
const centerY = canvasRect.height / 2;
// Define a threshold for considering a point as "centered"
const centerThreshold = 10; // Adjust this value as needed
// Check if the point is within the center threshold
const isCenterX = Math.abs(relativeX - centerX) <= centerThreshold;
const isCenterY = Math.abs(relativeY - centerY) <= centerThreshold;
// If the point is centered, return a special "centered" position
if (isCenterX && isCenterY) {
return {
top: "auto",
left: "auto",
right: "auto",
bottom: "auto",
};
}
let position: {
top: number | "auto";
left: number | "auto";
right: number | "auto";
bottom: number | "auto";
};
if (relativeY < centerY) {
if (relativeX < centerX) {
// Top-left quadrant
position = {
top: relativeY - 41.5,
left: relativeX - 125,
right: "auto",
bottom: "auto",
};
} else {
// Top-right quadrant
position = {
top: relativeY - 41.5,
right: canvasRect.width - relativeX - 125,
left: "auto",
bottom: "auto",
};
}
} else {
if (relativeX < centerX) {
// Bottom-left quadrant
position = {
bottom: canvasRect.height - relativeY - 41.5,
left: relativeX - 125,
right: "auto",
top: "auto",
};
} else {
// Bottom-right quadrant
position = {
bottom: canvasRect.height - relativeY - 41.5,
right: canvasRect.width - relativeX - 125,
left: "auto",
top: "auto",
};
}
}
return position;
}

View File

@@ -0,0 +1,11 @@
export function getActiveProperties(position: any): [string, string] {
if (position.top !== "auto" && position.left !== "auto") {
return ["top", "left"]; // Top-left
} else if (position.top !== "auto" && position.right !== "auto") {
return ["top", "right"]; // Top-right
} else if (position.bottom !== "auto" && position.left !== "auto") {
return ["bottom", "left"]; // Bottom-left
} else {
return ["bottom", "right"]; // Bottom-right
}
}

View File

@@ -0,0 +1,99 @@
import { Template } from "../../../store/useTemplateStore";
import { captureVisualization } from "./captureVisualization";
type HandleSaveTemplateProps = {
addTemplate: (template: Template) => void;
floatingWidget: []; // Updated type from `[]` to `any[]` for clarity
widgets3D: []; // Updated type from `[]` to `any[]` for clarity
selectedZone: {
panelOrder: string[];
widgets: any[];
};
templates?: Template[];
visualizationSocket: any;
projectId?:string
};
// Generate a unique ID
const generateUniqueId = (): string => {
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
};
export const handleSaveTemplate = async ({
addTemplate,
floatingWidget,
widgets3D,
selectedZone,
templates = [],
visualizationSocket,
projectId
}: HandleSaveTemplateProps): Promise<void> => {
try {
// Check if the selected zone has any widgets
if (!selectedZone.panelOrder || selectedZone.panelOrder.length === 0) {
return;
}
// Check if the template already exists
const isDuplicate = templates.some(
(template) =>
JSON.stringify(template.panelOrder) ===
JSON.stringify(selectedZone.panelOrder) &&
JSON.stringify(template.widgets) ===
JSON.stringify(selectedZone.widgets)
);
if (isDuplicate) {
return;
}
// Capture visualization snapshot
const snapshot = await captureVisualization();
if (!snapshot) {
return;
}
// Create a new template
const newTemplate: Template = {
id: generateUniqueId(),
name: `Template ${new Date().toISOString()}`, // Better name formatting
panelOrder: selectedZone.panelOrder,
widgets: selectedZone.widgets,
snapshot,
floatingWidget,
widgets3D,
};
// Extract organization from email
const email = localStorage.getItem("email") || "";
const organization = email.includes("@")
? email.split("@")[1]?.split(".")[0]
: "";
const userId = localStorage.getItem("userId");
if (!organization) {
return;
}
let saveTemplate = {
organization: organization,
template: newTemplate,
userId,projectId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-template:add", saveTemplate);
}
// Save the template
try {
addTemplate(newTemplate);
// const response = await saveTemplateApi(organization, newTemplate);
//
// Add template only if API call succeeds
} catch (apiError) {
//
}
} catch (error) {
echo.error("Failed to save template");
}
};

View File

@@ -0,0 +1,130 @@
import { generateUniqueId } from "../../../functions/generateUniqueId";
import { useDroppedObjectsStore } from "../../../store/visualization/useDroppedObjectsStore";
import { determinePosition } from "./determinePosition";
import { getActiveProperties } from "./getActiveProperties";
interface HandleDropProps {
widgetSubOption: any;
visualizationSocket: any;
selectedZone: any;
setFloatingWidget: (value: any) => void;
event: React.DragEvent<HTMLDivElement>;
projectId?:string
}
export const createHandleDrop = ({
widgetSubOption,
visualizationSocket,
selectedZone,
setFloatingWidget,
event,projectId
}: HandleDropProps) => {
event.preventDefault();
try {
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
const data = event.dataTransfer.getData("text/plain");
if (widgetSubOption === "3D") return;
if (!data || selectedZone.zoneName === "") return;
const droppedData = JSON.parse(data);
const canvasElement = document.getElementById("work-space-three-d-canvas");
if (!canvasElement) throw new Error("Canvas element not found");
const rect = canvasElement.getBoundingClientRect();
const relativeX = event.clientX - rect.left;
const relativeY = event.clientY - rect.top;
// Widget dimensions
const widgetWidth = droppedData.width ?? 125;
const widgetHeight = droppedData.height ?? 100;
// Center the widget at cursor
const centerOffsetX = widgetWidth / 2;
const centerOffsetY = widgetHeight / 2;
const adjustedX = relativeX - centerOffsetX;
const adjustedY = relativeY - centerOffsetY;
const finalPosition = determinePosition(rect, adjustedX, adjustedY);
const [activeProp1, activeProp2] = getActiveProperties(finalPosition);
let finalY = 0;
let finalX = 0;
if (activeProp1 === "top") {
finalY = adjustedY;
} else {
finalY = rect.height - (adjustedY + widgetHeight);
}
if (activeProp2 === "left") {
finalX = adjustedX;
} else {
finalX = rect.width - (adjustedX + widgetWidth);
}
// Clamp to boundaries
finalX = Math.max(0, Math.min(rect.width - widgetWidth, finalX));
finalY = Math.max(0, Math.min(rect.height - widgetHeight, finalY));
const boundedPosition = {
...finalPosition,
[activeProp1]: finalY,
[activeProp2]: finalX,
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
[activeProp2 === "left" ? "right" : "left"]: "auto",
};
const newObject = {
...droppedData,
id: generateUniqueId(),
position: boundedPosition,
};
const existingZone =
useDroppedObjectsStore.getState().zones[selectedZone.zoneName];
if (!existingZone) {
useDroppedObjectsStore
.getState()
.setZone(selectedZone.zoneName, selectedZone.zoneUuid);
}
const addFloatingWidget = {
organization,
widget: newObject,
zoneUuid: selectedZone.zoneUuid,
projectId,userId
};
console.log('addFloatingWidget: ', addFloatingWidget);
console.log('visualizationSocket: ', visualizationSocket);
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-float:add", addFloatingWidget);
}
useDroppedObjectsStore
.getState()
.addObject(selectedZone.zoneName, newObject);
const droppedObjectsStore = useDroppedObjectsStore.getState();
const currentZone = droppedObjectsStore.zones[selectedZone.zoneName];
if (currentZone && currentZone.zoneUuid === selectedZone.zoneUuid) {
// console.log(
// `Objects for Zone ${selectedZone.zoneUuid}:`,
// currentZone.objects
// );
setFloatingWidget(currentZone.objects);
} else {
console.warn("Zone not found or zoneUuid mismatch");
}
} catch (error) {
echo.error("Failed to drop widget");
console.error("Error in handleDrop:", error);
}
};

View File

@@ -0,0 +1,20 @@
import { useEffect } from "react";
export const useClickOutside = (
ref: React.RefObject<HTMLElement>,
callback: () => void
) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !event.composedPath().includes(ref.current)) {
callback();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref, callback]);
};

View File

@@ -0,0 +1,109 @@
import { Html } from "@react-three/drei";
import * as THREE from "three";
import * as Types from "../../../types/world/worldTypes";
import { useDrieTemp, useDrieUIValue } from "../../../store/builder/store"
import UI from "./ui";
import { useEffect } from "react";
import { useThree } from "@react-three/fiber";
export default function DrieHtmlTemp() {
const { drieTemp, setDrieTemp } = useDrieTemp();
const { drieUIValue, setDrieUIValue } = useDrieUIValue();
const state = useThree();
const { camera, raycaster, scene } = state;
useEffect(() => {
const canvasElement = state.gl.domElement;
let drag = false;
let isLeftMouseDown = false;
const onMouseDown = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown = true;
drag = false;
}
};
const onMouseMove = () => {
if (isLeftMouseDown) {
drag = true;
}
};
const onMouseUp = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown = false;
if (drag) return;
if (!scene) return
let intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
let currentObject = intersects[0].object;
while (currentObject) {
if (currentObject.name === "Scene") {
break;
}
currentObject = currentObject.parent as THREE.Object3D;
}
if (currentObject && (currentObject.userData.name === "SV2 Controll pannel" || currentObject.userData.name === "forklift")) {
const worldPos = new THREE.Vector3();
currentObject.getWorldPosition(worldPos);
const rightOffset = new THREE.Vector3(1, 0, 0);
const upOffset = new THREE.Vector3(0, 1, 0);
currentObject.localToWorld(rightOffset);
currentObject.localToWorld(upOffset);
const finalPosition = worldPos.clone().addScaledVector(rightOffset.sub(currentObject.position).normalize(), 2.5).addScaledVector(upOffset.sub(currentObject.position).normalize(), 2.3);
setDrieTemp(finalPosition);
} else {
setDrieTemp(undefined);
}
}
else {
setDrieTemp(undefined);
}
}
};
canvasElement.addEventListener("mousedown", onMouseDown);
canvasElement.addEventListener("mouseup", onMouseUp);
canvasElement.addEventListener("mousemove", onMouseMove);
return () => {
canvasElement.removeEventListener("mousedown", onMouseDown);
canvasElement.removeEventListener("mouseup", onMouseUp);
canvasElement.removeEventListener("mousemove", onMouseMove);
};
}, [])
return (
<>
{drieTemp &&
<mesh position={[drieTemp.x, drieTemp.y, drieTemp.z]}>
<Html
as="div"
center
zIndexRange={[1, 0]}
transform
sprite
style={{
padding: "10px",
color: "white",
borderRadius: "8px",
textAlign: "center",
fontFamily: "Arial, sans-serif",
}}
scale={[0.3, 0.3, 0.3]}
// occlude
>
<UI temperature={drieUIValue.temperature} humidity={drieUIValue.humidity} touch={drieUIValue.touch} header={""} />
</Html>
</mesh>
}
</>
)
}

View File

@@ -0,0 +1,141 @@
export default function UI({ temperature, humidity, touch, header }) {
return (
<div
className="temp-visualization-wrapper"
style={{
padding: "24px",
width: "fit-content",
background: "white",
borderRadius: "20px",
color: "#282829",
// transform: "translate(0, -100%)"
}}
>
<div
className="header"
style={{ paddingBottom: "22px", fontWeight: "600" }}
>
{header ? header : "Sensor Details"}
</div>
<div className="container-1" style={{ display: "flex", gap: "24px" }}>
<div
className="temperature-container"
style={{
padding: "12px",
borderRadius: "12px",
background: "white",
boxShadow: "7px 7px 14px #e3e3e3, -7px -7px 14px #f4f4f4",
display: "flex",
gap: "6px",
flexDirection: "column",
width: "92px",
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "12px" }}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.73109 11.6758L9 11.5357V11.2324V4C9 2.61929 10.1193 1.5 11.5 1.5C12.8807 1.5 14 2.61929 14 4V11.2324V11.5357L14.2689 11.6758C16.1901 12.6771 17.5 14.6861 17.5 17.0002C17.5 20.3139 14.8137 23.0002 11.5 23.0002C8.18629 23.0002 5.5 20.3139 5.5 17.0002C5.5 14.6861 6.80994 12.6771 8.73109 11.6758Z"
stroke="#FE4519"
/>
<path d="M11.5 7V16" stroke="#FE4519" strokeLinecap="round" />
<circle cx="11.5" cy="17" r="3" fill="#FE4519" />
</svg>
</div>
<div className="key" style={{ fontSize: "12px" }}>
Temperature
</div>
<div
className="value"
style={{ fontSize: "18px", fontWeight: "600" }}
>
{temperature}
</div>
</div>
<div
className="humidity-container"
style={{
padding: "12px",
borderRadius: "12px",
background: "white",
boxShadow: "7px 7px 14px #e3e3e3, -7px -7px 14px #f4f4f4",
display: "flex",
gap: "6px",
flexDirection: "column",
width: "92px",
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "12px" }}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.8041 19.1765C15.2714 17.4843 14.6826 15.891 12.6962 13.7257C12.3217 13.3175 11.6786 13.3192 11.305 13.7284C9.1738 16.0628 8.77326 17.5784 9.16555 19.0737C9.32805 19.6931 9.79837 20.1765 10.3593 20.4854C11.742 21.2468 12.2655 21.3361 13.7514 20.4639C14.2463 20.1734 14.6514 19.7296 14.8041 19.1765Z"
stroke="#0F96F5"
strokeWidth="1.5"
/>
<path
d="M20.8104 9.0293C21.2043 7.39129 20.5932 5.82808 18.6645 3.72574C18.2899 3.31747 17.6469 3.3192 17.2733 3.72838C15.1959 6.00386 14.7629 7.50129 15.1056 8.96027C15.2684 9.65314 15.8159 10.18 16.4679 10.4655C17.7279 11.0173 18.291 11.0385 19.5446 10.4598C20.1511 10.1799 20.6542 9.6787 20.8104 9.0293Z"
stroke="#0F96F5"
strokeWidth="1.5"
/>
<path
d="M8.81041 9.0293C9.20431 7.39129 8.59319 5.82808 6.66448 3.72574C6.28992 3.31747 5.64687 3.3192 5.27331 3.72838C3.19591 6.00386 2.76287 7.50129 3.1056 8.96027C3.26837 9.65314 3.81593 10.18 4.46789 10.4655C5.72785 11.0173 6.29105 11.0385 7.54464 10.4598C8.15106 10.1799 8.65424 9.6787 8.81041 9.0293Z"
stroke="#0F96F5"
strokeWidth="1.5"
/>
</svg>
</div>
<div className="key" style={{ fontSize: "12px" }}>
Humidity
</div>
<div
className="value"
style={{ fontSize: "18px", fontWeight: "600" }}
>
{humidity}
</div>
</div>
</div>
<div className="container-2">
<div
className="touch-container"
style={{
display: "flex",
borderRadius: "12px",
background: "white",
boxShadow: "7px 7px 14px #e3e3e3, -7px -7px 14px #f4f4f4",
padding: "16px",
marginTop: "16px",
gap: "18px",
alignItems: "center",
fontWeight: "600",
}}
>
<div className="key" style={{ fontSize: "14px" }}>
Touch Sensor
</div>
<div
className="value"
style={
touch === "True"
? { color: "#2AA553", fontWeight: 500 }
: { color: "#FE4519", fontWeight: 500 }
}
>
{touch === "True" ? "Active" : "In active"}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,297 @@
import { useEffect } from "react";
import { useSocketStore } from "../../../store/builder/store";
import { useSelectedZoneStore } from "../../../store/visualization/useZoneStore";
import { useDroppedObjectsStore } from "../../../store/visualization/useDroppedObjectsStore";
import { useZoneWidgetStore } from "../../../store/visualization/useZone3DWidgetStore";
import useTemplateStore from "../../../store/useTemplateStore";
type WidgetData = {
id: string;
type: string;
position: [number, number, number];
rotation?: [number, number, number];
tempPosition?: [number, number, number];
};
export default function SocketRealTimeViz() {
const { visualizationSocket } = useSocketStore();
const { setSelectedZone } = useSelectedZoneStore();
const deleteObject = useDroppedObjectsStore((state) => state.deleteObject);
const updateObjectPosition = useDroppedObjectsStore(
(state) => state.updateObjectPosition
);
const { addWidget } = useZoneWidgetStore();
const { removeTemplate } = useTemplateStore();
const { setTemplates } = useTemplateStore();
const {
zoneWidgetData,
setZoneWidgetData,
updateWidgetPosition,
updateWidgetRotation,
} = useZoneWidgetStore();
useEffect(() => {
if (visualizationSocket) {
//add panel response
visualizationSocket.on("v1:viz-panel:response:add", (addPanel: any) => {
if (addPanel.success) {
let addPanelData = addPanel.data.data;
setSelectedZone(addPanelData);
}
});
//delete panel response
visualizationSocket.on(
"v1:viz-panel:response:delete",
(deletePanel: any) => {
if (deletePanel.success) {
let deletePanelData = deletePanel.data.data;
setSelectedZone(deletePanelData);
}
}
);
//clear Panel response
visualizationSocket.on("v1:viz-panel:response:clear", (clearPanel: any) => {
if (
clearPanel.success &&
clearPanel.message === "PanelWidgets cleared successfully"
) {
let clearPanelData = clearPanel.data.data;
setSelectedZone(clearPanelData);
}
});
//lock Panel response
visualizationSocket.on("v1:viz-panel:response:locked", (lockPanel: any) => {
if (
lockPanel.success &&
lockPanel.message === "locked panel updated successfully"
) {
let lockPanelData = lockPanel.data.data;
setSelectedZone(lockPanelData);
}
});
// add 2dWidget response
visualizationSocket.on(
"v1:viz-widget:response:updates",
(add2dWidget: any) => {
if (add2dWidget.success && add2dWidget.data) {
setSelectedZone((prev) => {
const isWidgetAlreadyAdded = prev.widgets.some(
(widget) => widget.id === add2dWidget.data.widgetData.id
);
if (isWidgetAlreadyAdded) return prev; // Prevent duplicate addition
return {
...prev,
zoneUuid: add2dWidget.data.zoneUuid,
zoneName: add2dWidget.data.zoneName,
widgets: [...prev.widgets, add2dWidget.data.widgetData], // Append new widget
};
});
}
}
);
//delete 2D Widget response
visualizationSocket.on(
"v1:viz-widget:response:delete",
(deleteWidget: any) => {
if (deleteWidget?.success && deleteWidget.data) {
setSelectedZone((prevZone: any) => ({
...prevZone,
zoneUuid: deleteWidget.data.zoneUuid,
zoneName: deleteWidget.data.zoneName,
widgets: deleteWidget.data.widgetDeleteDatas, // Replace with new widget list
}));
}
}
);
//add Floating Widget response
visualizationSocket.on(
"v1:viz-float:response:updates",
(addFloatingWidget: any) => {
console.log('addFloatingWidget: ', addFloatingWidget);
if (addFloatingWidget.success) {
if (
addFloatingWidget.success &&
addFloatingWidget.message === "FloatWidget created successfully"
) {
const state = useDroppedObjectsStore.getState();
const zone = state.zones[addFloatingWidget.data.zoneName];
if (!zone) {
state.setZone(
addFloatingWidget.data.zoneName,
addFloatingWidget.data.zoneUuid
);
}
const existingObjects = zone ? zone.objects : [];
const newWidget = addFloatingWidget.data.widget;
// ✅ Check if the widget ID already exists before adding
const isAlreadyAdded = existingObjects.some(
(obj) => obj.id === newWidget.id
);
if (isAlreadyAdded) {
return; // Don't add the widget if it already exists
}
// Add widget only if it doesn't exist
state.addObject(addFloatingWidget.data.zoneName, newWidget);
}
if (addFloatingWidget.message === "Widget updated successfully") {
updateObjectPosition(
addFloatingWidget.data.zoneName,
addFloatingWidget.data.index,
addFloatingWidget.data.position
);
}
}
}
);
//duplicate Floating Widget response
visualizationSocket.on(
"v1:viz-float:response:addDuplicate",
(duplicateFloatingWidget: any) => {
if (
duplicateFloatingWidget.success &&
duplicateFloatingWidget.message ===
"duplicate FloatWidget created successfully"
) {
useDroppedObjectsStore.setState((state) => {
const zone = state.zones[duplicateFloatingWidget.data.zoneName];
if (!zone) return state; // Zone doesn't exist, return state as is
const existingObjects = zone.objects;
const newWidget = duplicateFloatingWidget.data.widget;
// ✅ Check if the object with the same ID already exists
const isAlreadyAdded = existingObjects.some(
(obj) => obj.id === newWidget.id
);
if (isAlreadyAdded) {
return state; // Don't update state if it's already there
}
return {
zones: {
...state.zones,
[duplicateFloatingWidget.data.zoneName]: {
...zone,
objects: [...existingObjects, newWidget], // Append only if it's not a duplicate
},
},
};
});
}
}
);
//delete Floating Widget response
visualizationSocket.on(
"v1:viz-float:response:delete",
(deleteFloatingWidget: any) => {
if (deleteFloatingWidget.success) {
deleteObject(
deleteFloatingWidget.data.zoneName,
deleteFloatingWidget.data.floatWidgetID
);
}
}
);
//add 3D Widget response
visualizationSocket.on(
"v1:viz-widget3D:response:add",
(add3DWidget: any) => {
if (add3DWidget.success) {
if (add3DWidget.message === "Widget created successfully") {
addWidget(add3DWidget.data.zoneUuid, add3DWidget.data.widget);
}
}
}
);
//delete 3D Widget response
visualizationSocket.on(
"v1:viz-widget3D:response:delete",
(delete3DWidget: any) => {
// "3DWidget delete unsuccessfull"
if (
delete3DWidget.success &&
delete3DWidget.message === "3DWidget delete successfull"
) {
const activeZoneWidgets =
zoneWidgetData[delete3DWidget.data.zoneUuid] || [];
setZoneWidgetData(
delete3DWidget.data.zoneUuid,
activeZoneWidgets.filter(
(w: WidgetData) => w.id !== delete3DWidget.data.id
)
);
}
}
);
//update3D widget response
visualizationSocket.on(
"v1:viz-widget3D:response:modifyPositionRotation",
(update3DWidget: any) => {
if (
update3DWidget.success &&
update3DWidget.message === "widget update successfully"
) {
updateWidgetPosition(
update3DWidget.data.zoneUuid,
update3DWidget.data.widget.id,
update3DWidget.data.widget.position
);
updateWidgetRotation(
update3DWidget.data.zoneUuid,
update3DWidget.data.widget.id,
update3DWidget.data.widget.rotation
);
}
}
);
// add Template response
visualizationSocket.on(
"v1:viz-template:response:add",
(addingTemplate: any) => {
if (addingTemplate.success) {
if (addingTemplate.message === "Template saved successfully") {
setTemplates(addingTemplate.data);
}
}
}
);
//load Template response
visualizationSocket.on(
"v1:viz-template:response:addTemplateZone",
(loadTemplate: any) => {
if (loadTemplate.success) {
if (loadTemplate.message === "Template placed in Zone") {
let template = loadTemplate.data.template;
setSelectedZone({
panelOrder: template.panelOrder,
activeSides: Array.from(new Set(template.panelOrder)), // No merging with previous `activeSides`
widgets: template.widgets,
});
useDroppedObjectsStore
.getState()
.setZone(template.zoneName, template.zoneUuid);
if (Array.isArray(template.floatingWidget)) {
template.floatingWidget.forEach((val: any) => {
useDroppedObjectsStore
.getState()
.addObject(template.zoneName, val);
});
}
}
}
}
);
//delete Template response
visualizationSocket.on(
"v1:viz-template:response:delete",
(deleteTemplate: any) => {
if (deleteTemplate.success) {
if (deleteTemplate.message === "Template deleted successfully") {
removeTemplate(deleteTemplate.data);
}
}
}
);
}
}, [visualizationSocket]);
return <></>;
}

View File

@@ -0,0 +1,142 @@
import { useEffect } from "react";
import useTemplateStore from "../../../store/useTemplateStore";
import { useSelectedZoneStore } from "../../../store/visualization/useZoneStore";
import { useSocketStore } from "../../../store/builder/store";
import { getTemplateData } from "../../../services/visulization/zone/getTemplate";
import { useDroppedObjectsStore } from "../../../store/visualization/useDroppedObjectsStore";
import RenameInput from "../../../components/ui/inputs/RenameInput";
import { useParams } from "react-router-dom";
const Templates = () => {
const { templates, removeTemplate, setTemplates } = useTemplateStore();
const { setSelectedZone, selectedZone } = useSelectedZoneStore();
const { visualizationSocket } = useSocketStore();
const { projectId } = useParams();
useEffect(() => {
async function templateData() {
try {
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0];
let response = await getTemplateData(organization,projectId);
setTemplates(response);
} catch (error) {
echo.error("Failed to fetching template data");
console.error("Error fetching template data:", error);
}
}
templateData();
}, []);
const handleDeleteTemplate = async (
e: React.MouseEvent<HTMLButtonElement>,
id: string
) => {
try {
e.stopPropagation();
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
let deleteTemplate = {
organization: organization,
templateID: id,
userId,projectId
};
if (visualizationSocket) {
visualizationSocket.emit(
"v1:viz-template:deleteTemplate",
deleteTemplate
);
}
removeTemplate(id);
} catch (error) {
echo.error("Failed to delete template");
console.error("Error deleting template:", error);
}
};
const handleLoadTemplate = async (template: any) => {
try {
if (selectedZone.zoneName === "") return;
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
let loadingTemplate = {
organization: organization,
zoneUuid: selectedZone.zoneUuid,
templateID: template.id,
projectId,userId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-template:addToZone", loadingTemplate);
}
setSelectedZone({
panelOrder: template.panelOrder,
activeSides: Array.from(new Set(template.panelOrder)), // No merging with previous `activeSides`
widgets: template.widgets,
});
useDroppedObjectsStore
.getState()
.setZone(selectedZone.zoneName, selectedZone.zoneUuid);
if (Array.isArray(template.floatingWidget)) {
template.floatingWidget.forEach((val: any) => {
useDroppedObjectsStore
.getState()
.addObject(selectedZone.zoneName, val);
});
}
} catch (error) {
echo.error("Failed to load template");
console.error("Error loading template:", error);
}
};
return (
<div className="template-list">
{templates.map((template, index) => (
<div key={template.id} className="template-item">
{template?.snapshot && (
<div
className="template-image-container"
onClick={() => handleLoadTemplate(template)}
>
<img
src={template.snapshot}
alt={`${template.name} preview`}
className="template-image"
/>
</div>
)}
<div className="template-details">
<div className="template-name">
<RenameInput value={`Template ${index + 1}`} />
</div>
<button
id="template-delete-button"
onClick={(e) => handleDeleteTemplate(e, template.id)}
className="delete-button"
aria-label="Delete template"
>
Delete
</button>
</div>
</div>
))}
{templates.length === 0 && (
<div className="no-templates">
<h2>No saved templates yet.</h2>
<div className="content">Create one in the visualization view!</div>
</div>
)}
</div>
);
};
export default Templates;

View File

@@ -0,0 +1,25 @@
import React from 'react'
import Dropped3dWidgets from './widgets/3d/Dropped3dWidget'
import ZoneCentreTarget from './zone/zoneCameraTarget'
import ZoneAssets from './zone/zoneAssets'
import MqttEvents from '../../services/factoryBuilder/mqtt/mqttEvents'
const Visualization:React.FC = () => {
return (
<>
<Dropped3dWidgets />
<ZoneCentreTarget />
<ZoneAssets />
<MqttEvents />
{/* <DrieHtmlTemp /> */}
</>
)
}
export default Visualization;

View File

@@ -0,0 +1,411 @@
import { useWidgetStore } from "../../../../store/useWidgetStore";
import ProgressCard from "../2d/charts/ProgressCard";
import PieGraphComponent from "../2d/charts/PieGraphComponent";
import BarGraphComponent from "../2d/charts/BarGraphComponent";
import LineGraphComponent from "../2d/charts/LineGraphComponent";
import DoughnutGraphComponent from "../2d/charts/DoughnutGraphComponent";
import PolarAreaGraphComponent from "../2d/charts/PolarAreaGraphComponent";
import ProgressCard1 from "../2d/charts/ProgressCard1";
import ProgressCard2 from "../2d/charts/ProgressCard2";
import {
DeleteIcon,
DublicateIcon,
KebabIcon,
} from "../../../../components/icons/ExportCommonIcons";
import { useEffect, useRef, useState } from "react";
import { useClickOutside } from "../../functions/handleWidgetsOuterClick";
import { useSocketStore } from "../../../../store/builder/store";
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
import OuterClick from "../../../../utils/outerClick";
import useChartStore from "../../../../store/visualization/useChartStore";
import { useParams } from "react-router-dom";
type Side = "top" | "bottom" | "left" | "right";
interface Widget {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}
export const DraggableWidget = ({
widget,
hiddenPanels,
index,
onReorder,
openKebabId,
setOpenKebabId,
selectedZone,
setSelectedZone,
}: {
selectedZone: {
zoneName: string;
zoneUuid: string;
activeSides: Side[];
points: [];
panelOrder: Side[];
lockedPanels: Side[];
widgets: Widget[];
};
setSelectedZone: React.Dispatch<
React.SetStateAction<{
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
points: [];
lockedPanels: Side[];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}[];
}>
>;
widget: any;
hiddenPanels: string[];
index: number;
onReorder: (fromIndex: number, toIndex: number) => void;
openKebabId: string | null;
setOpenKebabId: (id: string | null) => void;
}) => {
const { visualizationSocket } = useSocketStore();
const { selectedChartId, setSelectedChartId } = useWidgetStore();
const [panelDimensions, setPanelDimensions] = useState<{
[side in Side]?: { width: number; height: number };
}>({});
const { measurements, duration, name } = useChartStore();
const { isPlaying } = usePlayButtonStore();
const [canvasDimensions, setCanvasDimensions] = useState({
width: 0,
height: 0,
});
const { projectId } = useParams();
useEffect(() => {}, [measurements, duration, name]);
const handlePointerDown = () => {
if (selectedChartId?.id !== widget.id) {
setSelectedChartId(widget);
}
};
const chartWidget = useRef<HTMLDivElement>(null);
const deleteSelectedChart = async () => {
try {
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
let deleteWidget = {
zoneUuid: selectedZone.zoneUuid,
widgetID: widget.id,
organization: organization,
projectId,userId
};
if (visualizationSocket) {
setSelectedChartId(null);
visualizationSocket.emit("v1:viz-widget:delete", deleteWidget);
}
const updatedWidgets = selectedZone.widgets.filter(
(w: Widget) => w.id !== widget.id
);
setSelectedZone((prevZone: any) => ({
...prevZone,
widgets: updatedWidgets,
}));
setOpenKebabId(null);
// const response = await deleteWidgetApi(widget.id, organization);
// if (response?.message === "Widget deleted successfully") {
// const updatedWidgets = selectedZone.widgets.filter(
// (w: Widget) => w.id !== widget.id
// );
// setSelectedZone((prevZone: any) => ({
// ...prevZone,
// widgets: updatedWidgets,
// }));
// }
} catch (error) {
echo.error("Failued to dublicate widgeet");
} finally {
setOpenKebabId(null);
}
};
// Calculate panel size
const panelSize = Math.max(
Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25),
170 // Min 170px
);
const getCurrentWidgetCount = (panel: Side) =>
selectedZone.widgets.filter((w) => w.panel === panel).length;
// Calculate panel capacity
const calculatePanelCapacity = (panel: Side) => {
const CHART_WIDTH = panelSize;
const CHART_HEIGHT = panelSize;
const dimensions = panelDimensions[panel];
if (!dimensions) {
return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities
}
return panel === "top" || panel === "bottom"
? Math.max(1, Math.floor(dimensions.width / CHART_WIDTH))
: Math.max(1, Math.floor(dimensions.height / CHART_HEIGHT));
};
const isPanelFull = (panel: Side) => {
const currentWidgetCount = getCurrentWidgetCount(panel);
const panelCapacity = calculatePanelCapacity(panel);
return currentWidgetCount > panelCapacity;
};
const duplicateWidget = async () => {
try {
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
const duplicatedWidget: Widget = {
...widget,
title: name === "" ? widget.title : name,
Data: {
duration: duration,
measurements: { ...measurements },
},
id: `${widget.id}-copy-${Date.now()}`,
};
let duplicateWidget = {
organization: organization,
zoneUuid: selectedZone.zoneUuid,
widget: duplicatedWidget,
projectId,userId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-widget:add", duplicateWidget);
}
setSelectedZone((prevZone: any) => ({
...prevZone,
widgets: [...prevZone.widgets, duplicatedWidget],
}));
// const response = await duplicateWidgetApi(selectedZone.zoneUuid, organization, duplicatedWidget);
// if (response?.message === "Widget created successfully") {
// setSelectedZone((prevZone: any) => ({
// ...prevZone,
// widgets: [...prevZone.widgets, duplicatedWidget],
// }));
// }
} catch (error) {
echo.error("Failued to dublicate widgeet");
} finally {
setOpenKebabId(null);
}
};
const handleKebabClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
if (openKebabId === widget.id) {
setOpenKebabId(null);
} else {
setOpenKebabId(widget.id);
}
};
const widgetRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
widgetRef.current &&
!widgetRef.current.contains(event.target as Node)
) {
setOpenKebabId(null);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [setOpenKebabId]);
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
event.dataTransfer.setData("text/plain", index.toString()); // Store the index of the dragged widget
};
const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Allow drop
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Allow drop
};
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
const fromIndex = parseInt(event.dataTransfer.getData("text/plain"), 10); // Get the dragged widget's index
const toIndex = index; // The index of the widget where the drop occurred
if (fromIndex !== toIndex) {
onReorder(fromIndex, toIndex); // Call the reorder function passed as a prop
}
};
// useClickOutside(chartWidget, () => {
// setSelectedChartId(null);
// });
// Track canvas dimensions
// Current: Two identical useEffect hooks for canvas dimensions
// Remove the duplicate and keep only one
useEffect(() => {
const canvas = document.getElementById("real-time-vis-canvas");
if (!canvas) return;
const updateCanvasDimensions = () => {
const rect = canvas.getBoundingClientRect();
setCanvasDimensions({
width: rect.width,
height: rect.height,
});
};
updateCanvasDimensions();
const resizeObserver = new ResizeObserver(updateCanvasDimensions);
resizeObserver.observe(canvas);
return () => resizeObserver.unobserve(canvas);
}, []);
return (
<>
<div
draggable
key={widget.id}
className={`chart-container ${
selectedChartId?.id === widget.id && !isPlaying && "activeChart"
}`}
onPointerDown={handlePointerDown}
onDragStart={handleDragStart}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{
width: ["top", "bottom"].includes(widget.panel)
? `calc(${canvasDimensions.width}px / 6)`
: undefined,
height: ["left", "right"].includes(widget.panel)
? `calc(${canvasDimensions.height - 15}px / 4)`
: undefined,
}}
ref={chartWidget}
onClick={() => {
setSelectedChartId(widget);
}}
>
{/* Kebab Icon */}
<div className="icon kebab" onClick={handleKebabClick}>
<KebabIcon />
</div>
{/* Kebab Options */}
{openKebabId === widget.id && (
<div className="kebab-options" ref={widgetRef}>
<div
className={`edit btn ${
isPanelFull(widget.panel) ? "btn-blur" : ""
}`}
onClick={duplicateWidget}
>
<div className="icon">
<DublicateIcon />
</div>
<div className="label">Duplicate</div>
</div>
<div
className="edit btn"
onClick={(e) => {
e.stopPropagation();
deleteSelectedChart();
}}
>
<div className="icon">
<DeleteIcon />
</div>
<div className="label">Delete</div>
</div>
</div>
)}
{/* Render charts based on widget type */}
{widget.type === "progress 1" && (
<ProgressCard1 title={widget.title} id={widget.id} />
)}
{widget.type === "progress 2" && (
<ProgressCard2 title={widget.title} id={widget.id} />
)}
{widget.type === "line" && (
<LineGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "bar" && (
<BarGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "pie" && (
<PieGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "doughnut" && (
<DoughnutGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "polarArea" && (
<PolarAreaGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
</div>
</>
);
};

View File

@@ -0,0 +1,228 @@
import React, { useEffect, useMemo, useState } from "react";
import { Bar } from "react-chartjs-2";
import io from "socket.io-client";
import axios from "axios";
import { useThemeStore } from "../../../../../store/useThemeStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
import { useParams } from "react-router-dom";
interface ChartComponentProps {
id: string;
type: any;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
}
const BarGraphComponent = ({
id,
type,
title,
fontFamily,
fontSize,
fontWeight = "Regular",
}: ChartComponentProps) => {
const { themeColor } = useThemeStore();
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState("Widget");
const [chartData, setChartData] = useState<{
labels: string[];
datasets: any[];
}>({
labels: [],
datasets: [],
});
const { selectedChartId } = useWidgetStore();
const { selectedZone } = useSelectedZoneStore();
const { projectId } = useParams();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const defaultData = {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [
{
label: "Dataset",
data: [12, 19, 3, 5, 2, 3],
backgroundColor: ["#6f42c1"],
borderColor: "#b392f0",
borderWidth: 1,
},
],
};
useEffect(() => { }, []);
// Memoize Theme Colors
const buttonActionColor = useMemo(
() => themeColor[0] || "#5c87df",
[themeColor]
);
const buttonAbortColor = useMemo(
() => themeColor[1] || "#ffffff",
[themeColor]
);
// Memoize Font Styling
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
const fontSizeValue = useMemo(
() => (fontSize ? parseInt(fontSize) : 12),
[fontSize]
);
const fontWeightValue = useMemo(
() => chartFontWeightMap[fontWeight],
[fontWeight, chartFontWeightMap]
);
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
// Memoize Chart Options
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: name,
font: chartFontStyle,
},
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
display: true, // This hides the x-axis labels
},
},
},
}),
[title, chartFontStyle, name]
);
// useEffect(() => {console.log(measurements);
// },[measurements])
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
backgroundColor: "#6f42c1",
borderColor: "#b392f0",
borderWidth: 1,
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
{
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
echo.error("Failed to fetch saved inputs");
console.log("Unexpected response:", response);
}
} catch (error) {
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
return (
<Bar
data={Object.keys(measurements).length > 0 ? chartData : defaultData}
options={options}
/>
);
};
export default BarGraphComponent;

View File

@@ -0,0 +1,206 @@
import React, { useEffect, useMemo, useState } from "react";
import { Doughnut } from "react-chartjs-2";
import io from "socket.io-client";
import axios from "axios";
import { useThemeStore } from "../../../../../store/useThemeStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useParams } from "react-router-dom";
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
interface ChartComponentProps {
id: string;
type: any;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
}
const DoughnutGraphComponent = ({
id,
type,
title,
fontFamily,
fontSize,
fontWeight = "Regular",
}: ChartComponentProps) => {
const { themeColor } = useThemeStore();
const { measurements: chartMeasurements, duration: chartDuration, name: widgetName } = useChartStore();
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h")
const [name, setName] = useState("Widget")
const [chartData, setChartData] = useState<{ labels: string[]; datasets: any[] }>({
labels: [],
datasets: [],
});
const { selectedChartId } = useWidgetStore();
const { selectedZone } = useSelectedZoneStore();
const { projectId } = useParams();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0]
const defaultData = {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [
{
label: "Dataset",
data: [12, 19, 3, 5, 2, 3],
backgroundColor: ["#6f42c1"],
borderColor: "#b392f0",
borderWidth: 1,
},
],
};
useEffect(() => {
},[])
// Memoize Theme Colors
const buttonActionColor = useMemo(() => themeColor[0] || "#5c87df", [themeColor]);
const buttonAbortColor = useMemo(() => themeColor[1] || "#ffffff", [themeColor]);
// Memoize Font Styling
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
const fontSizeValue = useMemo(() => (fontSize ? parseInt(fontSize) : 12), [fontSize]);
const fontWeightValue = useMemo(() => chartFontWeightMap[fontWeight], [fontWeight, chartFontWeightMap]);
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
// Memoize Chart Options
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: name,
font: chartFontStyle,
},
legend: {
display: false,
},
},
scales: {
// x: {
// ticks: {
// display: true, // This hides the x-axis labels
// },
// },
},
}),
[title, chartFontStyle, name]
);
// useEffect(() => {console.log(measurements);
// },[measurements])
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0) return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
backgroundColor: "#6f42c1",
borderColor: "#b392f0",
borderWidth: 1,
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async() => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
{
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements)
setDuration(response.data.Data.duration)
setName(response.data.widgetName)
} else {
console.log("Unexpected response:", response);
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
console.error("There was an error!", error);
}
}
}
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}
,[chartMeasurements, chartDuration, widgetName])
return <Doughnut data={Object.keys(measurements).length > 0 ? chartData : defaultData} options={options} />;
};
export default DoughnutGraphComponent;

View File

@@ -0,0 +1,226 @@
import React, { useEffect, useMemo, useState } from "react";
import { Line } from "react-chartjs-2";
import io from "socket.io-client";
import axios from "axios";
import { useThemeStore } from "../../../../../store/useThemeStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import { useParams } from "react-router-dom";
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
interface ChartComponentProps {
id: string;
type: any;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
}
const LineGraphComponent = ({
id,
type,
title,
fontFamily,
fontSize,
fontWeight = "Regular",
}: ChartComponentProps) => {
const { themeColor } = useThemeStore();
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState("Widget");
const [chartData, setChartData] = useState<{
labels: string[];
datasets: any[];
}>({
labels: [],
datasets: [],
});
const { selectedChartId } = useWidgetStore();
const { selectedZone } = useSelectedZoneStore();
const { projectId } = useParams();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const defaultData = {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [
{
label: "Dataset",
data: [12, 19, 3, 5, 2, 3],
backgroundColor: ["#6f42c1"],
borderColor: "#b392f0",
borderWidth: 1,
},
],
};
useEffect(() => {}, []);
// Memoize Theme Colors
const buttonActionColor = useMemo(
() => themeColor[0] || "#5c87df",
[themeColor]
);
const buttonAbortColor = useMemo(
() => themeColor[1] || "#ffffff",
[themeColor]
);
// Memoize Font Styling
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
const fontSizeValue = useMemo(
() => (fontSize ? parseInt(fontSize) : 12),
[fontSize]
);
const fontWeightValue = useMemo(
() => chartFontWeightMap[fontWeight],
[fontWeight, chartFontWeightMap]
);
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
// Memoize Chart Options
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: name,
font: chartFontStyle,
},
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
display: true, // This hides the x-axis labels
},
},
},
}),
[title, chartFontStyle, name]
);
// useEffect(() => {console.log(measurements);
// },[measurements])
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
backgroundColor: "#6f42c1",
borderColor: "#b392f0",
borderWidth: 1,
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
{
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
echo.error("Failed to fetch saved inputs");
console.log("Unexpected response:", response);
}
} catch (error) {
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
return (
<Line
data={Object.keys(measurements).length > 0 ? chartData : defaultData}
options={options}
/>
);
};
export default LineGraphComponent;

View File

@@ -0,0 +1,225 @@
import React, { useEffect, useMemo, useState } from "react";
import { Pie } from "react-chartjs-2";
import io from "socket.io-client";
import axios from "axios";
import { useThemeStore } from "../../../../../store/useThemeStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
import { useParams } from "react-router-dom";
interface ChartComponentProps {
id: string;
type: any;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
}
const PieChartComponent = ({
id,
type,
title,
fontFamily,
fontSize,
fontWeight = "Regular",
}: ChartComponentProps) => {
const { themeColor } = useThemeStore();
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState("Widget");
const [chartData, setChartData] = useState<{
labels: string[];
datasets: any[];
}>({
labels: [],
datasets: [],
});
const { selectedChartId } = useWidgetStore();
const { selectedZone } = useSelectedZoneStore();
const { projectId } = useParams();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const defaultData = {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [
{
label: "Dataset",
data: [12, 19, 3, 5, 2, 3],
backgroundColor: ["#6f42c1"],
borderColor: "#b392f0",
borderWidth: 1,
},
],
};
useEffect(() => {}, []);
// Memoize Theme Colors
const buttonActionColor = useMemo(
() => themeColor[0] || "#5c87df",
[themeColor]
);
const buttonAbortColor = useMemo(
() => themeColor[1] || "#ffffff",
[themeColor]
);
// Memoize Font Styling
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
const fontSizeValue = useMemo(
() => (fontSize ? parseInt(fontSize) : 12),
[fontSize]
);
const fontWeightValue = useMemo(
() => chartFontWeightMap[fontWeight],
[fontWeight, chartFontWeightMap]
);
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
// Memoize Chart Options
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: name,
font: chartFontStyle,
},
legend: {
display: false,
},
},
scales: {
// x: {
// ticks: {
// display: true, // This hides the x-axis labels
// },
// },
},
}),
[title, chartFontStyle, name]
);
// useEffect(() => {console.log(measurements);
// },[measurements])
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
backgroundColor: "#6f42c1",
borderColor: "#b392f0",
borderWidth: 1,
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
{
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
echo.error("Failed to fetch saved inputs");
console.log("Unexpected response:", response);
}
} catch (error) {
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
return (
<Pie
data={Object.keys(measurements).length > 0 ? chartData : defaultData}
options={options}
/>
);
};
export default PieChartComponent;

View File

@@ -0,0 +1,225 @@
import React, { useEffect, useMemo, useState } from "react";
import { PolarArea } from "react-chartjs-2";
import io from "socket.io-client";
import axios from "axios";
import { useThemeStore } from "../../../../../store/useThemeStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
import { useParams } from "react-router-dom";
interface ChartComponentProps {
id: string;
type: any;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
}
const PolarAreaGraphComponent = ({
id,
type,
title,
fontFamily,
fontSize,
fontWeight = "Regular",
}: ChartComponentProps) => {
const { themeColor } = useThemeStore();
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState("Widget");
const [chartData, setChartData] = useState<{
labels: string[];
datasets: any[];
}>({
labels: [],
datasets: [],
});
const { selectedChartId } = useWidgetStore();
const { selectedZone } = useSelectedZoneStore();
const { projectId } = useParams();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const defaultData = {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [
{
label: "Dataset",
data: [12, 19, 3, 5, 2, 3],
backgroundColor: ["#6f42c1"],
borderColor: "#b392f0",
borderWidth: 1,
},
],
};
useEffect(() => {}, []);
// Memoize Theme Colors
const buttonActionColor = useMemo(
() => themeColor[0] || "#5c87df",
[themeColor]
);
const buttonAbortColor = useMemo(
() => themeColor[1] || "#ffffff",
[themeColor]
);
// Memoize Font Styling
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
const fontSizeValue = useMemo(
() => (fontSize ? parseInt(fontSize) : 12),
[fontSize]
);
const fontWeightValue = useMemo(
() => chartFontWeightMap[fontWeight],
[fontWeight, chartFontWeightMap]
);
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
// Memoize Chart Options
const options = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: name,
font: chartFontStyle,
},
legend: {
display: false,
},
},
scales: {
// x: {
// ticks: {
// display: true, // This hides the x-axis labels
// },
// },
},
}),
[title, chartFontStyle, name]
);
// useEffect(() => {console.log(measurements);
// },[measurements])
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
backgroundColor: "#6f42c1",
borderColor: "#b392f0",
borderWidth: 1,
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
{
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
echo.error("Failed to fetch saved inputs");
console.log("Unexpected response:", response);
}
} catch (error) {
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
return (
<PolarArea
data={Object.keys(measurements).length > 0 ? chartData : defaultData}
options={options}
/>
);
};
export default PolarAreaGraphComponent;

View File

@@ -0,0 +1,29 @@
import { StockIncreseIcon } from "../../../../../components/icons/RealTimeVisulationIcons";
const ProgressCard = ({
title,
data,
}: {
title: string;
data: { stocks: Array<{ key: string; value: number; description: string }> };
}) => (
<div className="chart progressBar">
<div className="header">{title}</div>
{data?.stocks?.map((stock, index) => (
<div key={index} className="stock">
<span className="stock-item">
<span className="stockValues">
<div className="key">{stock.key}</div>
<div className="value">{stock.value}</div>
</span>
<div className="stock-description">{stock.description}</div>
</span>
<div className="icon">
<StockIncreseIcon />
</div>
</div>
))}
</div>
);
export default ProgressCard;

View File

@@ -0,0 +1,119 @@
import React, { useEffect, useMemo, useState } from "react";
import { Line } from "react-chartjs-2";
import io from "socket.io-client";
import axios from "axios";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import { StockIncreseIcon } from "../../../../../components/icons/RealTimeVisulationIcons";
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
import { useParams } from "react-router-dom";
const ProgressCard1 = ({ id, title }: { id: string; title: string }) => {
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState(title);
const [value, setValue] = useState<any>("");
const { selectedChartId } = useWidgetStore();
const { selectedZone } = useSelectedZoneStore();
const { projectId } = useParams();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
useEffect(() => {
const socket = io(`http://${iotApiUrl}`);
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lastInput", inputData);
};
socket.on("connect", startStream);
socket.on("lastOutput", (response) => {
const responseData = response.input1;
setValue(responseData);
});
return () => {
socket.off("lastOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
{
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
echo.error("Failed to fetch saved inputs");
console.log("Unexpected response:", response);
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
return (
<div className="chart progressBar">
<div className="header">{name}</div>
<div className="stock">
<span className="stock-item">
<span className="stockValues">
<div className="value">{value}</div>
<div className="key">Units</div>
</span>
<div className="stock-description">
{measurements ? `${measurements?.input1?.fields}` : "description"}
</div>
</span>
<div className="icon">
<StockIncreseIcon />
</div>
</div>
</div>
);
};
export default ProgressCard1;

View File

@@ -0,0 +1,138 @@
import React, { useEffect, useMemo, useState } from "react";
import { Line } from "react-chartjs-2";
import io from "socket.io-client";
import axios from "axios";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import { StockIncreseIcon } from "../../../../../components/icons/RealTimeVisulationIcons";
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
import { useParams } from "react-router-dom";
const ProgressCard2 = ({ id, title }: { id: string; title: string }) => {
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState(title);
const [value1, setValue1] = useState<any>("");
const [value2, setValue2] = useState<any>("");
const { selectedChartId } = useWidgetStore();
const { selectedZone } = useSelectedZoneStore();
const { projectId } = useParams();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lastInput", inputData);
};
socket.on("connect", startStream);
socket.on("lastOutput", (response) => {
const responseData1 = response.input1;
const responseData2 = response.input2;
setValue1(responseData1);
setValue2(responseData2);
});
return () => {
socket.off("lastOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
{
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
console.log("Unexpected response:", response);
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
return (
<div className="chart progressBar">
<div className="header">{name}</div>
<div className="stock">
<span className="stock-item">
<span className="stockValues">
<div className="value">{value1}</div>
<div className="key">Units</div>
</span>
<div className="stock-description">
{measurements ? `${measurements?.input1?.fields}` : "description"}
</div>
</span>
<div className="icon">
<StockIncreseIcon />
</div>
</div>
<div className="stock">
<span className="stock-item">
<span className="stockValues">
<div className="value">{value2}</div>
<div className="key">Units</div>
</span>
<div className="stock-description">
{measurements ? `${measurements?.input2?.fields}` : "description"}
</div>
</span>
<div className="icon">
<StockIncreseIcon />
</div>
</div>
</div>
);
};
export default ProgressCard2;

View File

@@ -0,0 +1,102 @@
import React, { useMemo } from "react";
import { Radar } from "react-chartjs-2";
import { ChartOptions, ChartData, RadialLinearScaleOptions } from "chart.js";
interface ChartComponentProps {
type: string;
title: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: "Light" | "Regular" | "Bold";
data: number[]; // Expecting an array of numbers for radar chart data
}
const RadarGraphComponent = ({
title,
fontFamily,
fontSize,
fontWeight = "Regular",
data, // Now guaranteed to be number[]
}: ChartComponentProps) => {
// Memoize Font Weight Mapping
const chartFontWeightMap = useMemo(
() => ({
Light: "lighter" as const,
Regular: "normal" as const,
Bold: "bold" as const,
}),
[]
);
const fontSizeValue = useMemo(
() => (fontSize ? parseInt(fontSize) : 12),
[fontSize]
);
const fontWeightValue = useMemo(
() => chartFontWeightMap[fontWeight],
[fontWeight, chartFontWeightMap]
);
const chartFontStyle = useMemo(
() => ({
family: fontFamily || "Arial",
size: fontSizeValue,
weight: fontWeightValue,
}),
[fontFamily, fontSizeValue, fontWeightValue]
);
const options: ChartOptions<"radar"> = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: title,
font: chartFontStyle,
},
legend: {
display: false,
position: "top",
},
},
scales: {
r: {
min: 0,
max: 100,
angleLines: {
display: true,
},
ticks: {
display: true,
stepSize: 20,
},
} as RadialLinearScaleOptions,
},
}),
[title, chartFontStyle]
);
const chartData: ChartData<"radar"> = useMemo(
() => ({
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "Dataset 1",
data, // Use the data passed as a prop
backgroundColor: "rgba(111, 66, 193, 0.2)",
borderColor: "#6f42c1",
borderWidth: 2,
fill: true,
},
],
}),
[data]
);
return <Radar data={chartData} options={options} />;
};
export default RadarGraphComponent;

View File

@@ -0,0 +1,828 @@
import { useThree } from "@react-three/fiber";
import React, { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import {
useAsset3dWidget,
useSocketStore,
useWidgetSubOption,
} from "../../../../store/builder/store";
import useModuleStore from "../../../../store/useModuleStore";
import { ThreeState } from "../../../../types/world/worldTypes";
import { useSelectedZoneStore } from "../../../../store/visualization/useZoneStore";
import {
useEditWidgetOptionsStore,
useLeftData,
useRightClickSelected,
useRightSelected,
useTopData,
useZoneWidgetStore,
} from "../../../../store/visualization/useZone3DWidgetStore";
import { use3DWidget } from "../../../../store/visualization/useDroppedObjectsStore";
import { get3dWidgetZoneData } from "../../../../services/visulization/zone/get3dWidgetData";
import { generateUniqueId } from "../../../../functions/generateUniqueId";
import ProductionCapacity from "./cards/ProductionCapacity";
import ReturnOfInvestment from "./cards/ReturnOfInvestment";
import StateWorking from "./cards/StateWorking";
import Throughput from "./cards/Throughput";
import { useWidgetStore } from "../../../../store/useWidgetStore";
import useChartStore from "../../../../store/visualization/useChartStore";
import { useParams } from "react-router-dom";
type WidgetData = {
id: string;
type: string;
position: [number, number, number];
rotation?: [number, number, number];
tempPosition?: [number, number, number];
};
export default function Dropped3dWidgets() {
const { widgetSelect } = useAsset3dWidget();
const { activeModule } = useModuleStore();
const { raycaster, gl, scene, mouse, camera }: ThreeState = useThree();
const { widgetSubOption } = useWidgetSubOption();
const { selectedZone } = useSelectedZoneStore();
let lastClientY = useRef<number | null>(null);
const { top, setTop } = useTopData();
const { left, setLeft } = useLeftData();
const { rightSelect, setRightSelect } = useRightSelected();
const { editWidgetOptions, setEditWidgetOptions } =
useEditWidgetOptionsStore();
const {
zoneWidgetData,
setZoneWidgetData,
addWidget,
updateWidgetPosition,
updateWidgetRotation,
tempWidget,
tempWidgetPosition,
} = useZoneWidgetStore();
const { setWidgets3D } = use3DWidget();
const { visualizationSocket } = useSocketStore();
const { rightClickSelected, setRightClickSelected } = useRightClickSelected();
const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)); // Floor plane for horizontal move
const verticalPlane = useRef(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0)); // Vertical plane for vertical move
const planeIntersect = useRef(new THREE.Vector3());
const rotationStartRef = useRef<[number, number, number]>([0, 0, 0]);
const mouseStartRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const { setSelectedChartId } = useWidgetStore();
const { measurements, duration } = useChartStore();
let [floorPlanesVertical, setFloorPlanesVertical] = useState(
new THREE.Plane(new THREE.Vector3(0, 1, 0))
);
const [intersectcontextmenu, setintersectcontextmenu] = useState<
number | undefined
>();
const [horizontalX, setHorizontalX] = useState<number | undefined>();
const [horizontalZ, setHorizontalZ] = useState<number | undefined>();
const activeZoneWidgets = zoneWidgetData[selectedZone.zoneUuid] || [];
const { projectId } = useParams();
useEffect(() => {
if (activeModule !== "visualization") return;
if (!selectedZone.zoneUuid) return;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
async function get3dWidgetData() {
const result = await get3dWidgetZoneData(
selectedZone.zoneUuid,
organization,
projectId
);
setWidgets3D(result);
if (result.length < 0) return;
const formattedWidgets = result?.map((widget: WidgetData) => ({
id: widget.id,
type: widget.type,
position: widget.position,
rotation: widget.rotation || [0, 0, 0],
}));
setZoneWidgetData(selectedZone.zoneUuid, formattedWidgets);
}
get3dWidgetData();
}, [selectedZone.zoneUuid, activeModule]);
const createdWidgetRef = useRef<WidgetData | null>(null);
useEffect(() => {
if (activeModule !== "visualization") return;
if (widgetSubOption === "Floating" || widgetSubOption === "2D") return;
if (selectedZone.zoneName === "") return;
const canvasElement = document.getElementById("work-space-three-d-canvas");
if (!canvasElement) return;
const hasEntered = { current: false };
const handleDragEnter = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
if (hasEntered.current || !widgetSelect.startsWith("ui")) return;
hasEntered.current = true;
const group1 = scene.getObjectByName("itemsGroup");
if (!group1) return;
const rect = canvasElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.name.includes("agv-collider") &&
!intersect.object.name.includes("zonePlane") &&
!intersect.object.name.includes("SelectionGroup") &&
!intersect.object.name.includes("selectionAssetGroup") &&
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
intersect.object.type !== "GridHelper"
);
if (intersects.length > 0) {
const { x, y, z } = intersects[0].point;
const newWidget: WidgetData = {
id: generateUniqueId(),
type: widgetSelect,
position: [x, y, z],
rotation: [0, 0, 0],
};
createdWidgetRef.current = newWidget;
tempWidget(selectedZone.zoneUuid, newWidget); // temp add in UI
}
};
const handleDragOver = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
event.dataTransfer!.dropEffect = "move"; // ✅ Add this line
const widget = createdWidgetRef.current;
if (!widget) return;
const rect = canvasElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.name.includes("agv-collider") &&
!intersect.object.name.includes("zonePlane") &&
!intersect.object.name.includes("SelectionGroup") &&
!intersect.object.name.includes("selectionAssetGroup") &&
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
intersect.object.type !== "GridHelper"
);
// Update widget's position in memory
if (intersects.length > 0) {
const { x, y, z } = intersects[0].point;
tempWidgetPosition(selectedZone.zoneUuid, widget.id, [x, y, z]);
widget.position = [x, y, z];
}
};
const onDrop = (event: any) => {
event.preventDefault();
event.stopPropagation();
hasEntered.current = false;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
const newWidget = createdWidgetRef.current;
if (!newWidget || !widgetSelect.startsWith("ui")) return;
// ✅ Extract 2D drop position
let [x, y, z] = newWidget.position;
// ✅ Clamp Y to at least 0
y = Math.max(y, 0);
newWidget.position = [x, y, z];
// ✅ Prepare polygon from selectedZone.points
const points3D = selectedZone.points as Array<[number, number, number]>;
const zonePolygonXZ = points3D.map(
([x, , z]) => [x, z] as [number, number]
);
const isInside = isPointInPolygon([x, z], zonePolygonXZ);
// ✅ Remove temp widget
const prevWidgets =
useZoneWidgetStore.getState().zoneWidgetData[selectedZone.zoneUuid] || [];
const cleanedWidgets = prevWidgets.filter((w) => w.id !== newWidget.id);
useZoneWidgetStore.setState((state) => ({
zoneWidgetData: {
...state.zoneWidgetData,
[selectedZone.zoneUuid]: cleanedWidgets,
},
}));
// (Optional) Prevent adding if dropped outside zone
// if (!isInside) {
// createdWidgetRef.current = null;
// return;
// }
// ✅ Add widget
addWidget(selectedZone.zoneUuid, newWidget);
const add3dWidget = {
organization,
widget: newWidget,
zoneUuid: selectedZone.zoneUuid,
projectId, userId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-3D-widget:add", add3dWidget);
}
createdWidgetRef.current = null;
};
canvasElement.addEventListener("dragenter", handleDragEnter);
canvasElement.addEventListener("dragover", handleDragOver);
canvasElement.addEventListener("drop", onDrop);
return () => {
canvasElement.removeEventListener("dragenter", handleDragEnter);
canvasElement.removeEventListener("dragover", handleDragOver);
canvasElement.removeEventListener("drop", onDrop);
};
}, [
widgetSelect,
activeModule,
selectedZone.zoneUuid,
widgetSubOption,
camera,
]);
useEffect(() => {
if (!rightClickSelected) return;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
if (rightSelect === "Duplicate") {
async function duplicateWidget() {
const widgetToDuplicate = activeZoneWidgets.find(
(w: WidgetData) => w.id === rightClickSelected
);
console.log("3d widget to duplecate", widgetToDuplicate);
if (!widgetToDuplicate) return;
const newWidget: any = {
id: generateUniqueId(),
type: widgetToDuplicate.type,
position: [
widgetToDuplicate.position[0] + 0.5,
widgetToDuplicate.position[1],
widgetToDuplicate.position[2] + 0.5,
],
rotation: widgetToDuplicate.rotation || [0, 0, 0],
Data: {
measurements: measurements,
duration: duration,
},
};
const adding3dWidget = {
organization: organization,
widget: newWidget,
zoneUuid: selectedZone.zoneUuid,
projectId, userId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-3D-widget:add", adding3dWidget);
}
// let response = await adding3dWidgets(selectedZone.zoneUuid, organization, newWidget)
//
addWidget(selectedZone.zoneUuid, newWidget);
setRightSelect(null);
setRightClickSelected(null);
}
duplicateWidget();
}
if (rightSelect === "Delete") {
const deleteWidgetApi = async () => {
try {
const deleteWidget = {
organization,
id: rightClickSelected,
zoneUuid: selectedZone.zoneUuid,
projectId, userId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-3D-widget:delete", deleteWidget);
}
// Call the API to delete the widget
// const response = await delete3dWidgetApi(selectedZone.zoneUuid, organization, rightClickSelected);
setZoneWidgetData(
selectedZone.zoneUuid,
activeZoneWidgets.filter(
(w: WidgetData) => w.id !== rightClickSelected
)
);
} catch (error) {
echo.error("Failed to delete widget");
} finally {
setRightClickSelected(null);
setRightSelect(null);
}
};
deleteWidgetApi();
}
}, [rightSelect, rightClickSelected]);
function isPointInPolygon(point: [number, number], polygon: Array<[number, number]>): boolean {
const [x, z] = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const [xi, zi] = polygon[i];
const [xj, zj] = polygon[j];
const intersect =
zi > z !== zj > z && x < ((xj - xi) * (z - zi)) / (zj - zi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
const [prevX, setPrevX] = useState(0);
useEffect(() => {
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
const handleMouseDown = (event: MouseEvent) => {
if (!rightClickSelected || !rightSelect) return;
const selectedzoneUuid = Object.keys(zoneWidgetData).find(
(zoneUuid: string) =>
zoneWidgetData[zoneUuid].some(
(widget: WidgetData) => widget.id === rightClickSelected
)
);
if (!selectedzoneUuid) return;
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
(widget: WidgetData) => widget.id === rightClickSelected
);
if (!selectedWidget) return;
// let points = [];
// points.push(new THREE.Vector3(0, 0, 0));
// points.push(new THREE.Vector3(0, selectedWidget.position[1], 0));
// const newgeometry = new THREE.BufferGeometry().setFromPoints(points);
// let vector = new THREE.Vector3();
// camera.getWorldDirection(vector);
// let cameraDirection = vector;
// let newPlane = new THREE.Plane(cameraDirection);
// floorPlanesVertical = newPlane;
// setFloorPlanesVertical(newPlane);
// const intersect1 = raycaster?.ray?.intersectPlane(
// floorPlanesVertical,
// planeIntersect.current
// );
// setintersectcontextmenu(intersect1.y);
if (rightSelect === "RotateX" || rightSelect === "RotateY") {
mouseStartRef.current = { x: event.clientX, y: event.clientY };
const selectedzoneUuid = Object.keys(zoneWidgetData).find(
(zoneUuid: string) =>
zoneWidgetData[zoneUuid].some(
(widget: WidgetData) => widget.id === rightClickSelected
)
);
if (!selectedzoneUuid) return;
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
(widget: WidgetData) => widget.id === rightClickSelected
);
if (selectedWidget) {
rotationStartRef.current = selectedWidget.rotation || [0, 0, 0];
}
}
};
const handleMouseMove = (event: MouseEvent) => {
if (!rightClickSelected || !rightSelect) return;
const selectedzoneUuid = Object.keys(zoneWidgetData).find(
(zoneUuid: string) =>
zoneWidgetData[zoneUuid].some(
(widget: WidgetData) => widget.id === rightClickSelected
)
);
if (!selectedzoneUuid) return;
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
(widget: WidgetData) => widget.id === rightClickSelected
);
if (!selectedWidget) return;
const rect = gl.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
if (rightSelect === "Horizontal Move") {
const intersect = raycaster.ray.intersectPlane(
plane.current,
planeIntersect.current
);
if (
intersect &&
typeof horizontalX === "number" &&
typeof horizontalZ === "number"
) {
const selectedzoneUuid = Object.keys(zoneWidgetData).find((zoneUuid) =>
zoneWidgetData[zoneUuid].some(
(widget) => widget.id === rightClickSelected
)
);
if (!selectedzoneUuid) return;
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
(widget) => widget.id === rightClickSelected
);
if (!selectedWidget) return;
const newPosition: [number, number, number] = [
intersect.x + horizontalX,
selectedWidget.position[1],
intersect.z + horizontalZ,
];
updateWidgetPosition(selectedzoneUuid, rightClickSelected, newPosition);
}
}
// if (rightSelect === "Vertical Move") {
// // console.log('rightSelect: ', rightSelect);
// // console.log('floorPlanesVertical: ', floorPlanesVertical);
// // console.log('planeIntersect.current: ', planeIntersect.current);
// // const intersect = raycaster.ray.intersectPlane(
// // floorPlanesVertical,
// // planeIntersect.current
// // );
// // console.log('intersect: ', intersect);
// let intersect = event.clientY
// if (intersect && typeof intersectcontextmenu === "number") {
// console.log('intersect: ', intersect);
// const diff = intersect - intersectcontextmenu;
// const unclampedY = selectedWidget.position[1] + diff;
// const newY = Math.max(0, unclampedY); // Prevent going below floor (y=0)
// setintersectcontextmenu(intersect);
// const newPosition: [number, number, number] = [
// selectedWidget.position[0],
// newY,
// selectedWidget.position[2],
// ];
// console.log('newPosition: ', newPosition);
// updateWidgetPosition(selectedzoneUuid, rightClickSelected, newPosition);
// }
// }
if (rightSelect === "Vertical Move") {
if (lastClientY.current === null) {
lastClientY.current = event.clientY;
return;
}
const diff = lastClientY.current - event.clientY; // dragging up = increase Y
const scaleFactor = 0.05; // tune this based on your scene scale
const unclampedY = selectedWidget.position[1] + diff * scaleFactor;
const newY = Math.max(0, unclampedY);
lastClientY.current = event.clientY;
const newPosition: [number, number, number] = [
selectedWidget.position[0],
newY,
selectedWidget.position[2],
];
updateWidgetPosition(selectedzoneUuid, rightClickSelected, newPosition);
}
if (rightSelect?.startsWith("Rotate")) {
const axis = rightSelect.slice(-1).toLowerCase(); // "x", "y", or "z"
const currentX = event.pageX;
const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
setPrevX(currentX);
if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
const index = axis === "x" ? 0 : axis === "y" ? 1 : 2;
const currentRotation = selectedWidget.rotation as [
number,
number,
number
]; // assert type
const newRotation: [number, number, number] = [...currentRotation];
newRotation[index] += 0.05 * sign;
updateWidgetRotation(selectedzoneUuid, rightClickSelected, newRotation);
}
}
// if (rightSelect === "RotateX") {
//
// const currentX = event.pageX;
// const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
//
// setPrevX(currentX);
// if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
//
// const newRotation: [number, number, number] = [
// selectedWidget.rotation[0] + 0.05 * sign,
// selectedWidget.rotation[1],
// selectedWidget.rotation[2],
// ];
// updateWidgetRotation(selectedzoneUuid, rightClickSelected, newRotation);
// }
// }
// if (rightSelect === "RotateY") {
// const currentX = event.pageX;
// const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
// setPrevX(currentX);
// if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
// const newRotation: [number, number, number] = [
// selectedWidget.rotation[0],
// selectedWidget.rotation[1] + 0.05 * sign,
// selectedWidget.rotation[2],
// ];
// updateWidgetRotation(selectedzoneUuid, rightClickSelected, newRotation);
// }
// }
// if (rightSelect === "RotateZ") {
// const currentX = event.pageX;
// const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
// setPrevX(currentX);
// if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
// const newRotation: [number, number, number] = [
// selectedWidget.rotation[0],
// selectedWidget.rotation[1],
// selectedWidget.rotation[2] + 0.05 * sign,
// ];
// updateWidgetRotation(selectedzoneUuid, rightClickSelected, newRotation);
// }
// }
};
const handleMouseUp = () => {
if (!rightClickSelected || !rightSelect) return;
const selectedzoneUuid = Object.keys(zoneWidgetData).find((zoneUuid) =>
zoneWidgetData[zoneUuid].some(
(widget) => widget.id === rightClickSelected
)
);
if (!selectedzoneUuid) return;
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
(widget) => widget.id === rightClickSelected
);
if (!selectedWidget) return;
// Format values to 2 decimal places
const formatValues = (vals: number[]) =>
vals.map((val) => parseFloat(val.toFixed(2)));
if (
rightSelect === "Horizontal Move" ||
rightSelect === "Vertical Move"
) {
let lastPosition = formatValues(selectedWidget.position) as [
number,
number,
number
];
// (async () => {
// let response = await update3dWidget(selectedzoneUuid, organization, rightClickSelected, lastPosition);
//
// if (response) {
//
// }
// })();
let updatingPosition = {
organization: organization,
zoneUuid: selectedzoneUuid,
id: rightClickSelected,
position: lastPosition,
projectId, userId
};
if (visualizationSocket) {
visualizationSocket.emit(
"v1:viz-3D-widget:modifyPositionRotation",
updatingPosition
);
}
} else if (rightSelect.includes("Rotate")) {
const rotation = selectedWidget.rotation || [0, 0, 0];
let lastRotation = formatValues(rotation) as [number, number, number];
// (async () => {
// let response = await update3dWidgetRotation(selectedzoneUuid, organization, rightClickSelected, lastRotation);
//
// if (response) {
//
// }
// })();
let updatingRotation = {
organization: organization,
zoneUuid: selectedzoneUuid,
id: rightClickSelected,
rotation: lastRotation,
projectId, userId
};
if (visualizationSocket) {
visualizationSocket.emit(
"v1:viz-3D-widget:modifyPositionRotation",
updatingRotation
);
}
}
// Reset selection
setTimeout(() => {
setRightClickSelected(null);
setRightSelect(null);
}, 50);
};
window.addEventListener("mousedown", handleMouseDown);
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousedown", handleMouseDown);
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [rightClickSelected, rightSelect, zoneWidgetData, gl]);
const handleRightClick3d = (event: React.MouseEvent, id: string) => {
event.preventDefault();
const canvasElement = document.getElementById("work-space-three-d-canvas");
if (!canvasElement) throw new Error("Canvas element not found");
const canvasRect = canvasElement.getBoundingClientRect();
const relativeX = event.clientX - canvasRect.left;
const relativeY = event.clientY - canvasRect.top;
setEditWidgetOptions(true);
setRightClickSelected(id);
setTop(relativeY);
setLeft(relativeX);
const selectedzoneUuid = Object.keys(zoneWidgetData).find((zoneUuid) =>
zoneWidgetData[zoneUuid].some((widget) => widget.id === id)
);
if (!selectedzoneUuid) return;
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
(widget) => widget.id === id
);
if (!selectedWidget) return;
const { top, left, width, height } = canvasElement.getBoundingClientRect();
mouse.x = ((event.clientX - left) / width) * 2 - 1;
mouse.y = -((event.clientY - top) / height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const up = new THREE.Vector3(0, 1, 0);
const cameraDirection = new THREE.Vector3();
camera.getWorldDirection(cameraDirection);
const right = new THREE.Vector3().crossVectors(up, cameraDirection).normalize();
const verticalPlane = new THREE.Plane(right, 0);
// const verticalPlane = new THREE.Plane(cameraDirection);
setFloorPlanesVertical(verticalPlane);
const intersectPoint = raycaster.ray.intersectPlane(
verticalPlane,
planeIntersect.current
);
if (intersectPoint) {
setintersectcontextmenu(intersectPoint.y);
}
const intersect2 = raycaster.ray.intersectPlane(
plane.current,
planeIntersect.current
);
if (intersect2) {
const xDiff = -intersect2.x + selectedWidget.position[0];
const zDiff = -intersect2.z + selectedWidget.position[2];
setHorizontalX(xDiff);
setHorizontalZ(zDiff);
}
};
return (
<>
{activeZoneWidgets.map(
({ id, type, position, Data, rotation = [0, 0, 0] }: any) => {
const handleRightClick = (event: React.MouseEvent, id: string) => {
setSelectedChartId({ id: id, type: type });
event.preventDefault();
const canvasElement = document.getElementById(
"work-space-three-d-canvas"
);
if (!canvasElement) throw new Error("Canvas element not found");
const canvasRect = canvasElement.getBoundingClientRect();
const relativeX = event.clientX - canvasRect.left;
const relativeY = event.clientY - canvasRect.top;
setEditWidgetOptions(true);
setRightClickSelected(id);
setTop(relativeY);
setLeft(relativeX);
handleRightClick3d(event, id);
};
switch (type) {
case "ui-Widget 1":
return (
<ProductionCapacity
key={id}
id={id}
type={type}
position={position}
rotation={rotation}
Data={Data}
onContextMenu={(e) => handleRightClick(e, id)}
/>
);
case "ui-Widget 2":
return (
<ReturnOfInvestment
key={id}
id={id}
type={type}
position={position}
rotation={rotation}
Data={Data}
onContextMenu={(e) => handleRightClick(e, id)}
/>
);
case "ui-Widget 3":
return (
<StateWorking
key={id}
id={id}
type={type}
position={position}
rotation={rotation}
Data={Data}
onContextMenu={(e) => handleRightClick(e, id)}
/>
);
case "ui-Widget 4":
return (
<Throughput
key={id}
id={id}
type={type}
position={position}
rotation={rotation}
Data={Data}
onContextMenu={(e) => handleRightClick(e, id)}
/>
);
default:
return null;
}
}
)}
</>
);
}

View File

@@ -0,0 +1,275 @@
import { Html } from "@react-three/drei";
import React, { useEffect, useMemo, useState } from "react";
import { Bar } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
TooltipItem, // Import TooltipItem for typing
} from "chart.js";
import axios from "axios";
import io from "socket.io-client";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
// Register ChartJS components
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
interface ProductionCapacityProps {
id: string;
type: string;
position: [number, number, number];
rotation: [number, number, number];
scale?: [number, number, number];
Data?: any;
onContextMenu?: (event: React.MouseEvent) => void;
// onPointerDown:any
}
const ProductionCapacity: React.FC<ProductionCapacityProps> = ({
id,
type,
Data,
position,
rotation = [0, 0, 0],
scale = [0.5, 0.5, 0.5],
onContextMenu,
}) => {
const { selectedChartId, setSelectedChartId } = useWidgetStore();
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>(
Data?.measurements ? Data.measurements : {}
);
const [duration, setDuration] = useState(
Data?.duration ? Data.duration : "1h"
);
const [name, setName] = useState("Widget");
const [chartData, setChartData] = useState<{
labels: string[];
datasets: any[];
}>({
labels: [],
datasets: [],
});
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
// Chart data for a week
const defaultChartData = {
labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], // Days of the week
datasets: [
{
label: "Production Capacity (units/day)",
data: [1500, 1600, 1400, 1700, 1800, 1900, 2000], // Example daily production data
backgroundColor: "#6f42c1", // Theme color
borderColor: "#6f42c1",
borderWidth: 1,
borderRadius: 8, // Rounded corners for the bars
borderSkipped: false, // Ensure all corners are rounded
},
],
};
// Chart options
const chartOptions = {
responsive: true,
plugins: {
legend: {
display: false, // Hide legend
},
title: {
display: true,
text: "Weekly Production Capacity",
font: {
size: 16,
},
},
tooltip: {
callbacks: {
// Explicitly type the context parameter
label: (context: TooltipItem<"bar">) => {
const value = context.parsed.y; // Extract the y-axis value
return `${value} units`; // Customize tooltip to display "units"
},
},
},
},
scales: {
x: {
grid: {
display: false, // Hide x-axis grid lines
},
},
y: {
display: false, // Remove the y-axis completely
},
},
};
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
backgroundColor: "#6f42c1", // Theme color
borderColor: "#6f42c1",
borderWidth: 1,
borderRadius: 8, // Rounded corners for the bars
borderSkipped: false, // Ensure all corners are rounded
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
useEffect(() => {}, [rotation]);
return (
<>
{position && scale && rotation && <Html
// data
position={position}
scale={scale}
rotation={rotation}
// class
wrapperClass="pointer-none"
// other
transform
zIndexRange={[1, 0]}
prepend
// sprite
distanceFactor={20}
// events
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onDrop={(e) => {
e.preventDefault();
}}
>
<div
className={`productionCapacity-wrapper card ${selectedChartId?.id === id ? "activeChart" : ""
}`}
onClick={() => setSelectedChartId({ id: id, type: type })}
onContextMenu={onContextMenu}
style={{
width: "300px", // Original width
height: "300px", // Original height
// transform: transformStyle.transform,
transformStyle: "preserve-3d",
position: "absolute",
transform: "translate(-50%, -50%)",
}}
>
<div className="headeproductionCapacityr-wrapper">
<div className="header">Production Capacity</div>
<div className="production-capacity">
<div className="value">1,200</div>{" "}
<div className="value">units/hour</div>
</div>
<div className="production-capacity">
<div className="current">
<div className="key">Current</div>
<div className="value">1500</div>
</div>
<div className="target">
<div className="key">Target</div>
<div className="value">2.345</div>
</div>
{/* <div className="value">units/hour</div> */}
</div>
</div>{" "}
<div className="bar-chart charts">
{/* Bar Chart */}
<Bar
data={
Object.keys(measurements).length > 0
? chartData
: defaultChartData
}
options={chartOptions}
/>
</div>
</div>
</Html>}
</>
);
};
export default ProductionCapacity;

View File

@@ -0,0 +1,282 @@
import { Html } from "@react-three/drei";
import React, { useEffect, useMemo, useState } from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Tooltip,
ChartData,
ChartOptions,
} from "chart.js";
import axios from "axios";
import io from "socket.io-client";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { WavyIcon } from "../../../../../components/icons/3dChartIcons";
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Tooltip
);
// Define Props for SmoothLineGraphComponent
interface SmoothLineGraphProps {
data: ChartData<"line">; // Type for chart data
options?: ChartOptions<"line">; // Type for chart options (optional)
}
// SmoothLineGraphComponent using react-chartjs-2
const SmoothLineGraphComponent: React.FC<SmoothLineGraphProps> = ({
data,
options,
}) => {
return <Line data={data} options={options} />;
};
interface ReturnOfInvestmentProps {
id: string;
type: string;
position: [number, number, number];
rotation: [number, number, number];
Data?: any;
onContextMenu?: (event: React.MouseEvent) => void;
}
const ReturnOfInvestment: React.FC<ReturnOfInvestmentProps> = ({
id,
type,
Data,
position,
rotation,
onContextMenu,
}) => {
const { selectedChartId, setSelectedChartId } = useWidgetStore();
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>(
Data?.measurements ? Data.measurements : {}
);
const [duration, setDuration] = useState(
Data?.duration ? Data.duration : "1h"
);
const [name, setName] = useState("Widget");
const [chartData, setChartData] = useState<{
labels: string[];
datasets: any[];
}>({
labels: [],
datasets: [],
});
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
// Improved sample data for the smooth curve graph (single day)
const graphData: ChartData<"line"> = {
labels: [
"12 AM",
"3 AM",
"6 AM",
"9 AM",
"12 PM",
"3 PM",
"6 PM",
"9 PM",
"12 AM",
],
datasets: [
{
label: "Investment",
data: [100, 250, 400, 400, 500, 600, 700, 800, 900], // Example investment growth
borderColor: "rgba(75, 192, 192, 1)", // Light blue color
backgroundColor: "rgba(75, 192, 192, 0.2)",
fill: true,
tension: 0.4, // Smooth curve effect
pointRadius: 0, // Hide dots
pointHoverRadius: 0, // Hide hover dots
},
{
label: "Return",
data: [100, 200, 500, 250, 300, 350, 400, 450, 500], // Example return values
borderColor: "rgba(255, 99, 132, 1)", // Pink color
backgroundColor: "rgba(255, 99, 132, 0.2)",
fill: true,
tension: 0.4, // Smooth curve effect
pointRadius: 0, // Hide dots
pointHoverRadius: 0, // Hide hover dots
},
],
};
// Options for the smooth curve graph
const graphOptions: ChartOptions<"line"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
enabled: true, // Enable tooltips on hover
mode: "index", // Show both datasets' values at the same index
intersect: false, // Allow hovering anywhere on the graph
},
},
scales: {
x: {
grid: {
display: false, // Hide x-axis grid lines
},
ticks: {
display: false, // Hide x-axis labels
},
},
y: {
grid: {
display: false, // Hide y-axis grid lines
},
ticks: {
display: false, // Hide y-axis labels
},
},
},
};
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key, index) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
borderColor:
index === 0 ? "rgba(75, 192, 192, 1)" : "rgba(255, 99, 132, 1)", // Light blue color
backgroundColor:
index === 0 ? "rgba(75, 192, 192, 0.2)" : "rgba(255, 99, 132, 0.2)",
fill: true,
tension: 0.4, // Smooth curve effect
pointRadius: 0, // Hide dots
pointHoverRadius: 0, // Hide hover dots
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
console.log("Unexpected response:", response);
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
const rotationDegrees = {
x: (rotation[0] * 180) / Math.PI,
y: (rotation[1] * 180) / Math.PI,
z: (rotation[2] * 180) / Math.PI,
};
const transformStyle = {
transform: `rotateX(${rotationDegrees.x}deg) rotateY(${rotationDegrees.y}deg) rotateZ(${rotationDegrees.z}deg)`,
};
return (
<Html
position={[position[0], position[1], position[2]]}
rotation={rotation}
scale={[0.5, 0.5, 0.5]}
transform
zIndexRange={[1, 0]}
prepend
distanceFactor={20}
>
<div
className={`returnOfInvestment card ${
selectedChartId?.id === id ? "activeChart" : ""
}`}
onClick={() => setSelectedChartId({ id: id, type: type })}
onContextMenu={onContextMenu}
>
<div className="header">Return of Investment</div>
<div className="lineGraph charts">
{/* Smooth curve graph with two datasets */}
<SmoothLineGraphComponent
data={Object.keys(measurements).length > 0 ? chartData : graphData}
options={graphOptions}
/>
</div>
<div className="returns-wrapper">
<div className="icon">
<WavyIcon />
</div>
<div className="value">5.78</div>
<div className="key">Years</div>
</div>
<div className="footer">
in <span>5y</span> with avg <span>7%</span> yearly return
</div>
</div>
</Html>
);
};
export default ReturnOfInvestment;

View File

@@ -0,0 +1,211 @@
import { Html } from "@react-three/drei";
import React, { useEffect, useMemo, useState } from "react";
import axios from "axios";
import io from "socket.io-client";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
// import image from "../../../../assets/image/temp/image.png";
interface StateWorkingProps {
id: string;
type: string;
position: [number, number, number];
rotation: [number, number, number];
Data?: any;
onContextMenu?: (event: React.MouseEvent) => void;
}
const StateWorking: React.FC<StateWorkingProps> = ({
id,
type,
Data,
position,
rotation,
onContextMenu,
}) => {
const { selectedChartId, setSelectedChartId } = useWidgetStore();
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>(
Data?.measurements ? Data.measurements : {}
);
const [duration, setDuration] = useState(
Data?.duration ? Data.duration : "1h"
);
const [name, setName] = useState("Widget");
const [datas, setDatas] = useState<any>({});
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
// const datas = [
// { key: "Oil Tank:", value: "24/341" },
// { key: "Oil Refin:", value: 36.023 },
// { key: "Transmission:", value: 36.023 },
// { key: "Fuel:", value: 36732 },
// { key: "Power:", value: 1300 },
// { key: "Time:", value: 13 - 9 - 2023 },
// ];
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lastInput", inputData);
};
socket.on("connect", startStream);
socket.on("lastOutput", (response) => {
const responseData = response;
setDatas(responseData);
});
return () => {
socket.off("lastOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
console.log("Unexpected response:", response);
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
const rotationDegrees = {
x: (rotation[0] * 180) / Math.PI,
y: (rotation[1] * 180) / Math.PI,
z: (rotation[2] * 180) / Math.PI,
};
const transformStyle = {
transform: `rotateX(${rotationDegrees.x}deg) rotateY(${rotationDegrees.y}deg) rotateZ(${rotationDegrees.z}deg)`,
};
return (
<Html
position={[position[0], position[1], position[2]]}
rotation={rotation}
scale={[0.5, 0.5, 0.5]}
transform
zIndexRange={[1, 0]}
// sprite={true}
prepend
distanceFactor={20}
// style={{
// transform: transformStyle.transform,
// transformStyle: "preserve-3d",
// transition: "transform 0.1s ease-out",
// }}
>
<div
className={`stateWorking-wrapper card ${
selectedChartId?.id === id ? "activeChart" : ""
}`}
onClick={() => setSelectedChartId({ id: id, type: type })}
onContextMenu={onContextMenu}
>
<div className="header-wrapper">
<div className="header">
<span>State</span>
<span>
{datas?.input1 ? datas.input1 : "input1"} <span>.</span>
</span>
</div>
<div className="img">{/* <img src={image} alt="" /> */}</div>
</div>
{/* Data */}
<div className="data-wrapper">
{/* {datas.map((data, index) => (
<div className="data-table" key={index}>
<div className="data">{data.key}</div>
<div className="key">{data.value}</div>
</div>
))} */}
<div className="data-table">
<div className="data">
{measurements?.input2?.fields
? measurements.input2.fields
: "input2"}
</div>
<div className="key">{datas?.input2 ? datas.input2 : "data"}</div>
</div>
<div className="data-table">
<div className="data">
{measurements?.input3?.fields
? measurements.input3.fields
: "input3"}
</div>
<div className="key">{datas?.input3 ? datas.input3 : "data"}</div>
</div>
<div className="data-table">
<div className="data">
{measurements?.input4?.fields
? measurements.input4.fields
: "input4"}
</div>
<div className="key">{datas?.input4 ? datas.input4 : "data"}</div>
</div>
<div className="data-table">
<div className="data">
{measurements?.input5?.fields
? measurements.input5.fields
: "input5"}
</div>
<div className="key">{datas?.input5 ? datas.input5 : "data"}</div>
</div>
<div className="data-table">
<div className="data">
{measurements?.input6?.fields
? measurements.input6.fields
: "input6"}
</div>
<div className="key">{datas?.input6 ? datas.input6 : "data"}</div>
</div>
<div className="data-table">
<div className="data">
{measurements?.input7?.fields
? measurements.input7.fields
: "input7"}
</div>
<div className="key">{datas?.input7 ? datas.input7 : "data"}</div>
</div>
</div>
</div>
</Html>
);
};
export default StateWorking;

View File

@@ -0,0 +1,272 @@
import { Html } from "@react-three/drei";
import React, { useEffect, useMemo, useState } from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
ChartData,
ChartOptions,
} from "chart.js";
import axios from "axios";
import io from "socket.io-client";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { ThroughputIcon } from "../../../../../components/icons/3dChartIcons";
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
// Define Props for LineGraphComponent
interface LineGraphProps {
data: ChartData<"line">; // Type for chart data
options?: ChartOptions<"line">; // Type for chart options (optional)
}
// LineGraphComponent using react-chartjs-2
const LineGraphComponent: React.FC<LineGraphProps> = ({ data, options }) => {
return <Line data={data} options={options} />;
};
interface ThroughputProps {
id: string;
type: string;
position: [number, number, number];
rotation: [number, number, number];
Data?: any;
onContextMenu?: (event: React.MouseEvent) => void;
}
const Throughput: React.FC<ThroughputProps> = ({
id,
type,
Data,
position,
rotation,
onContextMenu,
}) => {
const { selectedChartId, setSelectedChartId } = useWidgetStore();
const {
measurements: chartMeasurements,
duration: chartDuration,
name: widgetName,
} = useChartStore();
const [measurements, setmeasurements] = useState<any>(
Data?.measurements ? Data.measurements : {}
);
const [duration, setDuration] = useState(
Data?.duration ? Data.duration : "1h"
);
const [name, setName] = useState("Widget");
const [chartData, setChartData] = useState<{
labels: string[];
datasets: any[];
}>({
labels: [],
datasets: [],
});
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
// Sample data for the line graph
const graphData: ChartData<"line"> = {
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
datasets: [
{
label: "Throughput",
data: [1000, 1200, 1100, 1300, 1250, 1400], // Example throughput values
borderColor: "rgba(75, 192, 192, 1)",
backgroundColor: "rgba(75, 192, 192, 0.2)",
fill: true,
},
],
};
// Options for the line graph
const graphOptions: ChartOptions<"line"> = {
responsive: true,
plugins: {
legend: {
position: "top",
display: false,
},
title: {
display: true,
text: "Throughput Over Time",
},
},
scales: {
x: {
grid: {
display: true, // Show vertical grid lines
},
ticks: {
display: false, // Hide x-axis labels
},
},
y: {
grid: {
display: false, // Hide horizontal grid lines
},
ticks: {
display: false, // Hide y-axis labels
},
},
},
};
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
borderColor: "rgba(75, 192, 192, 1)",
backgroundColor: "rgba(75, 192, 192, 0.2)",
fill: true,
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.widgetName);
} else {
console.log("Unexpected response:", response);
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === id) {
fetchSavedInputes();
}
}, [chartMeasurements, chartDuration, widgetName]);
const rotationDegrees = {
x: (rotation[0] * 180) / Math.PI,
y: (rotation[1] * 180) / Math.PI,
z: (rotation[2] * 180) / Math.PI,
};
const transformStyle = {
transform: `rotateX(${rotationDegrees.x}deg) rotateY(${rotationDegrees.y}deg) rotateZ(${rotationDegrees.z}deg)`,
};
return (
<Html
position={[position[0], position[1], position[2]]}
rotation={rotation}
scale={[0.5, 0.5, 0.5]}
transform
zIndexRange={[1, 0]}
prepend
distanceFactor={20}
// sprite={true}
>
<div
className={`throughput-wrapper card ${selectedChartId?.id === id ? "activeChart" : ""
}`}
onClick={() => setSelectedChartId({ id: id, type: type })}
onContextMenu={onContextMenu}
>
<div className="header">{name}</div>
<div className="display-value">
<div className="left">
<div className="icon">
<ThroughputIcon />
</div>
<div className="value-container">
<div className="value-wrapper">
<div className="value">1,200</div>
<div className="key"> Units/hr</div>
</div>
<div className="total-sales">
<div className="value">316</div>
<div className="key">sales</div>
</div>
</div>
</div>
<div className="right">
<div className="percent-increase">5.77%</div>
</div>
</div>
<div className="line-graph">
{/* Line graph using react-chartjs-2 */}
<LineGraphComponent
data={Object.keys(measurements).length > 0 ? chartData : graphData}
options={graphOptions}
/>
</div>
<div className="footer">
You made an extra <span className="value">$1256.13</span> this month
</div>
</div>
</Html>
);
};
export default Throughput;

View File

@@ -0,0 +1,130 @@
import React from "react";
interface DistanceLinesProps {
obj: {
position: {
top?: number | "auto";
left?: number | "auto";
right?: number | "auto";
bottom?: number | "auto";
};
};
activeEdges: {
vertical: "top" | "bottom";
horizontal: "left" | "right";
} | null;
}
const DistanceLines: React.FC<DistanceLinesProps> = ({ obj, activeEdges }) => {
if (!activeEdges) return null;
return (
<>
{activeEdges.vertical === "top" &&
typeof obj.position.top === "number" && (
<div
className="distance-line top"
style={{
top: 0,
left:
activeEdges.horizontal === "left"
? `${(obj.position.left as number) + 125}px`
: `calc(100% - ${(obj.position.right as number) + 125}px)`,
height: `${obj.position.top}px`,
}}
>
<span
className="distance-label"
style={{
position: "absolute",
top: "50%",
transform: "translate(-50%,0%)",
}}
>
{obj.position.top.toFixed(1)}px
</span>
</div>
)}
{activeEdges.vertical === "bottom" &&
typeof obj.position.bottom === "number" && (
<div
className="distance-line bottom"
style={{
bottom: 0,
left:
activeEdges.horizontal === "left"
? `${(obj.position.left as number) + 125}px`
: `calc(100% - ${(obj.position.right as number) + 125}px)`,
height: `${obj.position.bottom}px`,
}}
>
<span
className="distance-label"
style={{
position: "absolute",
bottom: "50%",
transform: "translate(-50%,0%)",
}}
>
{obj.position.bottom.toFixed(1)}px
</span>
</div>
)}
{activeEdges.horizontal === "left" &&
typeof obj.position.left === "number" && (
<div
className="distance-line left"
style={{
left: 0,
top:
activeEdges.vertical === "top"
? `${(obj.position.top as number) + 41.5}px`
: `calc(100% - ${(obj.position.bottom as number) + 41.5}px)`,
width: `${obj.position.left}px`,
}}
>
<span
className="distance-label"
style={{
position: "absolute",
left: "50%",
transform: "translate(0,-50%)",
}}
>
{obj.position.left.toFixed(1)}px
</span>
</div>
)}
{activeEdges.horizontal === "right" &&
typeof obj.position.right === "number" && (
<div
className="distance-line right"
style={{
right: 0,
top:
activeEdges.vertical === "top"
? `${(obj.position.top as number) + 41.5}px`
: `calc(100% - ${(obj.position.bottom as number) + 41.5}px)`,
width: `${obj.position.right}px`,
}}
>
<span
className="distance-label"
style={{
position: "absolute",
right: "50%",
transform: "translate(0,-50%)",
}}
>
{obj.position.right.toFixed(1)}px
</span>
</div>
)}
</>
);
};
export default DistanceLines;

View File

@@ -0,0 +1,634 @@
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/builder/store";
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
import { useSelectedZoneStore } from "../../../../store/visualization/useZoneStore";
import { useParams } from "react-router-dom";
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 { projectId } = useParams();
const [openKebabId, setOpenKebabId] = useState<string | null>(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<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 chartWidget = useRef<HTMLDivElement>(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,projectId); // 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];
const userId = localStorage.getItem("userId");
let deleteFloatingWidget = {
floatWidgetID: id,
organization: organization,
zoneUuid: zone.zoneUuid,
userId,projectId
};
if (visualizationSocket) {
visualizationSocket.emit("v1: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.zoneUuid,
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<HTMLDivElement>) => {
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,
zoneUuid: zone.zoneUuid,
};
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;
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 - 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 (
<div
key={`${obj.id}-${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;

View File

@@ -0,0 +1,49 @@
const FleetEfficiency = () => {
const progress = 50; // Example progress value (0-100)
// Calculate the rotation angle for the progress bar
const rotationAngle = 45 + progress * 1.8;
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect(); // Get position
const cardData = JSON.stringify({
className: event.currentTarget.className,
position: [rect.top, rect.left], // Store position
value: rotationAngle, // Example value (you can change if dynamic)
per: progress,
});
event.dataTransfer.setData("text/plain", cardData);
};
return (
<div className="fleetEfficiency floating" draggable onDragStart={handleDragStart}>
<h2 className="header">Fleet Efficiency</h2>
<div className="progressContainer">
<div className="progress">
<div className="barOverflow">
{/* Apply dynamic rotation to the bar */}
<div
className="bar"
style={{ transform: `rotate(${rotationAngle}deg)` }}
></div>
</div>
</div>
</div>
<div className="scaleLabels">
<span>0%</span>
<div className="centerText">
<div className="percentage">{progress}%</div>
<div className="status">Optimal</div>
</div>
<span>100%</span>
</div>
</div>
);
};
export default FleetEfficiency;

View File

@@ -0,0 +1,118 @@
import React, { useState, useEffect } from "react";
import { Line } from "react-chartjs-2";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import axios from "axios";
import io from "socket.io-client";
import { usePlayButtonStore } from "../../../../../store/usePlayButtonStore";
const FleetEfficiencyComponent = ({ object }: any) => {
const [progress, setProgress] = useState<any>(0);
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState(object.header ? object.header : "");
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const { header, flotingDuration, flotingMeasurements } = useChartStore();
const { selectedChartId } = useWidgetStore();
const { isPlaying } = usePlayButtonStore();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
// Calculate the rotation angle for the progress bar
const rotationAngle = 45 + progress * 1.8;
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lastInput", inputData);
};
socket.on("connect", startStream);
socket.on("lastOutput", (response) => {
const responseData = response.input1;
// console.log(responseData);
if (typeof responseData === "number") {
console.log("It's a number!");
setProgress(responseData);
}
});
return () => {
socket.off("lastOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (object?.id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/A_floatWidget/${object?.id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.header);
} else {
console.log("Unexpected response:", response);
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === object?.id) {
fetchSavedInputes();
}
}, [header, flotingDuration, flotingMeasurements]);
return (
<>
<h2 className="header">{name}</h2>
<div
className="progressContainer"
style={{ transform: isPlaying ? "skew(-14deg, 0deg)" : "none" }}
>
<div className="progress">
<div className="barOverflow">
<div
className="bar"
style={{ transform: `rotate(${rotationAngle}deg)` }}
></div>
</div>
</div>
</div>
<div className="scaleLabels">
<span>0%</span>
<div className="centerText">
<div className="percentage">{progress}%</div>
<div className="status">Optimal</div>
</div>
<span>100%</span>
</div>
</>
);
};
export default FleetEfficiencyComponent;

View File

@@ -0,0 +1,86 @@
import React from "react";
interface ProductivityData {
distancePerTask: number;
spaceUtilization: number;
taskCompletionTime: string;
}
const ProductivityDashboard: React.FC = () => {
const data: ProductivityData = {
distancePerTask: 45,
spaceUtilization: 72,
taskCompletionTime: "7:44",
};
// Function to calculate the stroke dash offset for the circular progress
const calculateDashOffset = (percentage: number, circumference: number) => {
return circumference - (percentage / 100) * circumference;
};
// Constants for the circular progress chart
const radius = 60; // Radius of the circle
const strokeWidth = 10; // Thickness of the stroke
const diameter = radius * 2; // Diameter of the circle
const circumference = Math.PI * (radius * 2); // Circumference of the circle
return (
<div className="productivity-dashboard">
<header>
<h2>Productivity</h2>
<div className="options">...</div>
</header>
<main>
<section className="metrics">
<div className="metric">
<p className="label">Distance per Task</p>
<p className="value">{data.distancePerTask} m</p>
</div>
<div className="metric">
<p className="label">Space Utilization</p>
<p className="value">{data.spaceUtilization}%</p>
</div>
</section>
<section className="chart-section">
<svg
className="progress-circle"
width={diameter}
height={diameter}
viewBox={`0 0 ${diameter} ${diameter}`}
>
{/* Background Circle */}
<circle
cx={radius}
cy={radius}
r={radius - strokeWidth / 2}
fill="transparent"
stroke="#6c757d"
strokeWidth={strokeWidth}
/>
{/* Progress Circle */}
<circle
cx={radius}
cy={radius}
r={radius - strokeWidth / 2}
fill="transparent"
stroke="#2ecc71"
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={calculateDashOffset(data.spaceUtilization, circumference)}
strokeLinecap="round"
transform={`rotate(-90 ${radius} ${radius})`}
/>
</svg>
<div className="chart-details">
<p className="title">Task Completion Time</p>
<p className="time">{data.taskCompletionTime}</p>
<p className="subtitle">Total Score</p>
</div>
</section>
</main>
</div>
);
};
export default ProductivityDashboard;

View File

@@ -0,0 +1,58 @@
import React from "react";
interface SimpleCardProps {
header: string;
icon: React.ElementType; // React component for SVG icon
iconName?: string;
value: string;
per: string; // Percentage change
position?: [number, number];
}
const SimpleCard: React.FC<SimpleCardProps> = ({
header,
icon: Icon,
iconName,
value,
per,
position = [0, 0],
}) => {
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect(); // Get position
const cardData = JSON.stringify({
header,
value,
per,
iconName: iconName || "UnknownIcon", // Use the custom iconName
className: event.currentTarget.className,
position: [rect.top, rect.left], // ✅ Store position
});
event.dataTransfer.setData("text/plain", cardData);
// console.log('cardData: ', cardData);
};
return (
<div
className="floating total-card"
draggable
onDragStart={handleDragStart}
style={{ top: position[0], left: position[1] }} // No need for ?? 0 if position is guaranteed
>
<div className="header-wrapper">
<div className="header">{header}</div>
<div className="data-values">
<div className="value">{value}</div>
<div className="per">{per}</div>
</div>
</div>
<div className="icon">
<Icon />
</div>
</div>
);
};
export default SimpleCard;

View File

@@ -0,0 +1,115 @@
import React, { useState, useEffect } from "react";
import { Line } from "react-chartjs-2";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import axios from "axios";
import io from "socket.io-client";
import {
CartIcon,
DocumentIcon,
GlobeIcon,
WalletIcon,
} from "../../../../../components/icons/3dChartIcons";
const TotalCardComponent = ({ object }: any) => {
const [progress, setProgress] = useState<any>(0);
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState(object.header ? object.header : "");
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const { header, flotingDuration, flotingMeasurements } = useChartStore();
const { selectedChartId } = useWidgetStore();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lastInput", inputData);
};
socket.on("connect", startStream);
socket.on("lastOutput", (response) => {
const responseData = response.input1;
if (typeof responseData === "number") {
setProgress(responseData);
}
});
return () => {
socket.off("lastOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (object?.id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/A_floatWidget/${object?.id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.header);
} else {
echo.error("Failed to fetch saved inputs");
}
} catch (error) {}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === object?.id) {
fetchSavedInputes();
}
}, [header, flotingDuration, flotingMeasurements]);
const mapIcon = (iconName: string) => {
switch (iconName) {
case "WalletIcon":
return <WalletIcon />;
case "GlobeIcon":
return <GlobeIcon />;
case "DocumentIcon":
return <DocumentIcon />;
case "CartIcon":
return <CartIcon />;
default:
return <WalletIcon />;
}
};
return (
<>
<div className="header-wrapper">
<div className="header">{name}</div>
<div className="data-values">
<div className="value">{progress}</div>
<div className="per">{object.per}</div>
</div>
</div>
<div className="icon">{mapIcon(object.iconName)}</div>
</>
);
};
export default TotalCardComponent;

View File

@@ -0,0 +1,149 @@
import React from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler, // Import Filler for area fill
} from "chart.js";
// Register ChartJS components
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
);
const WarehouseThroughput = () => {
// Line graph data for a year (monthly throughput)
const lineGraphData = {
labels: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
], // Months of the year
datasets: [
{
label: "Throughput (units/month)",
data: [500, 400, 300, 450, 350, 250, 200, 300, 250, 150, 100, 150], // Example monthly data
borderColor: "#6f42c1", // Use the desired color for the line (purple)
backgroundColor: "rgba(111, 66, 193, 0.2)", // Use a semi-transparent purple for the fill
borderWidth: 2, // Line thickness
fill: true, // Enable fill for this dataset
pointRadius: 0, // Remove dots at each data point
tension: 0.5, // Smooth interpolation for the line
},
],
};
// Line graph options
const lineGraphOptions = {
responsive: true,
maintainAspectRatio: false, // Allow custom height/width adjustments
plugins: {
legend: {
display: false, // Hide legend
},
title: {
display: false, // No chart title needed
},
tooltip: {
callbacks: {
label: (context: any) => {
const value = context.parsed.y;
return `${value} units`; // Customize tooltip to display "units"
},
},
},
},
scales: {
x: {
grid: {
display: false, // Hide x-axis grid lines
},
ticks: {
maxRotation: 0, // Prevent label rotation
autoSkip: false, // Display all months
font: {
size: 8, // Adjust font size for readability
color: "#ffffff", // Light text color for labels
},
},
},
y: {
display: true, // Show y-axis
grid: {
drawBorder: false, // Remove border line
color: "rgba(255, 255, 255, 0.2)", // Light gray color for grid lines
borderDash: [5, 5], // Dotted line style (array defines dash and gap lengths)
},
ticks: {
font: {
size: 8, // Adjust font size for readability
color: "#ffffff", // Light text color for ticks
},
},
},
},
elements: {
line: {
tension: 0.5, // Smooth interpolation for the line
},
},
};
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect(); // Get element position
const cardData = JSON.stringify({
header: "Warehouse Throughput", // Static header
value: "+5", // Example value (you can change if dynamic)
per: "2025", // Example percentage or date
icon: "📊", // Placeholder for an icon (if needed)
className: event.currentTarget.className,
position: [rect.top, rect.left], // ✅ Store initial position
lineGraphData, // ✅ Include chart data
lineGraphOptions, // ✅ Include chart options
});
event.dataTransfer.setData("text/plain", cardData);
// event.dataTransfer.effectAllowed = "move"; // Improve drag effect
};
return (
<div className="warehouseThroughput floating" style={{ minHeight: "160px !important" }} draggable onDragStart={handleDragStart}>
<div className="header">
<h2>Warehouse Throughput</h2>
<p>
<span>(+5) more</span> in 2025
</p>
</div>
<div className="lineGraph" style={{ height: "100%" }}>
{/* Line Graph */}
<Line data={lineGraphData} options={lineGraphOptions} />
</div>
</div>
);
};
export default WarehouseThroughput;

View File

@@ -0,0 +1,209 @@
import React, { useState, useEffect } from "react";
import { Line } from "react-chartjs-2";
import useChartStore from "../../../../../store/visualization/useChartStore";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import axios from "axios";
import io from "socket.io-client";
const WarehouseThroughputComponent = ({ object }: any) => {
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState(object.header ? object.header : "");
const [chartData, setChartData] = useState<{
labels: string[];
datasets: any[];
}>({
labels: [],
datasets: [],
});
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const { header, flotingDuration, flotingMeasurements } = useChartStore();
const { selectedChartId } = useWidgetStore();
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const lineGraphData = {
labels: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
], // Months of the year
datasets: [
{
label: "Throughput (units/month)",
data: [500, 400, 300, 450, 350, 250, 200, 300, 250, 150, 100, 150], // Example monthly data
borderColor: "#6f42c1", // Use the desired color for the line (purple)
backgroundColor: "rgba(111, 66, 193, 0.2)", // Use a semi-transparent purple for the fill
borderWidth: 2, // Line thickness
fill: true, // Enable fill for this dataset
pointRadius: 0, // Remove dots at each data point
tension: 0.5, // Smooth interpolation for the line
},
],
};
// Line graph options
const lineGraphOptions = {
responsive: true,
maintainAspectRatio: false, // Allow custom height/width adjustments
plugins: {
legend: {
display: false, // Hide legend
},
title: {
display: false, // No chart title needed
},
tooltip: {
callbacks: {
label: (context: any) => {
const value = context.parsed.y;
return `${value} units`; // Customize tooltip to display "units"
},
},
},
},
scales: {
x: {
grid: {
display: false, // Hide x-axis grid lines
},
ticks: {
maxRotation: 0, // Prevent label rotation
autoSkip: false, // Display all months
font: {
size: 8, // Adjust font size for readability
color: "#ffffff", // Light text color for labels
},
},
},
y: {
display: true, // Show y-axis
grid: {
drawBorder: false, // Remove border line
color: "rgba(255, 255, 255, 0.2)", // Light gray color for grid lines
borderDash: [5, 5], // Dotted line style (array defines dash and gap lengths)
},
ticks: {
font: {
size: 8, // Adjust font size for readability
color: "#ffffff", // Light text color for ticks
},
},
},
},
elements: {
line: {
tension: 0.5, // Smooth interpolation for the line
},
},
};
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lineInput", inputData);
};
socket.on("connect", startStream);
socket.on("lineOutput", (response) => {
const responseData = response.data;
// Extract timestamps and values
const labels = responseData.time;
const datasets = Object.keys(measurements).map((key) => {
const measurement = measurements[key];
const datasetKey = `${measurement.name}.${measurement.fields}`;
return {
label: datasetKey,
data: responseData[datasetKey]?.values ?? [],
borderColor: "#6f42c1", // Use the desired color for the line (purple)
backgroundColor: "rgba(111, 66, 193, 0.2)", // Use a semi-transparent purple for the fill
borderWidth: 2, // Line thickness
fill: true, // Enable fill for this dataset
pointRadius: 0, // Remove dots at each data point
tension: 0.5, // Smooth interpolation for the line
};
});
setChartData({ labels, datasets });
});
return () => {
socket.off("lineOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (object?.id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/A_floatWidget/${object?.id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.header);
} else {
console.log("Unexpected response:", response);
}
} catch (error) {
echo.error("Failed to fetch saved inputs");
console.error("There was an error!", error);
}
}
};
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === object?.id) {
fetchSavedInputes();
}
}, [header, flotingDuration, flotingMeasurements]);
return (
<>
<div className="header">
<h2>{name}</h2>
{/* <p>
<span>(+5) more</span> in 2025
</p> */}
</div>
<div className="lineGraph" style={{ height: "100%" }}>
<Line
data={
Object.keys(measurements).length > 0 ? chartData : lineGraphData
}
options={lineGraphOptions}
/>
</div>
</>
);
};
export default WarehouseThroughputComponent;

View File

@@ -0,0 +1,354 @@
import React from "react";
import {
CleanPannel,
EyeIcon,
LockIcon,
} from "../../../../components/icons/RealTimeVisulationIcons";
import { AddIcon } from "../../../../components/icons/ExportCommonIcons";
import { useSocketStore } from "../../../../store/builder/store";
import { useParams } from "react-router-dom";
// Define the type for `Side`
type Side = "top" | "bottom" | "left" | "right";
// Define the type for HiddenPanels, where keys are zone IDs and values are arrays of hidden sides
interface HiddenPanels {
[zoneUuid: string]: Side[];
}
// Define the type for the props passed to the Buttons component
interface ButtonsProps {
selectedZone: {
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}[];
};
setSelectedZone: React.Dispatch<
React.SetStateAction<{
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}[];
}>
>;
hiddenPanels: HiddenPanels; // Updated prop type
setHiddenPanels: React.Dispatch<React.SetStateAction<HiddenPanels>>; // Updated prop type
waitingPanels: any;
setWaitingPanels: any;
}
const AddButtons: React.FC<ButtonsProps> = ({
selectedZone,
setSelectedZone,
setHiddenPanels,
hiddenPanels,
waitingPanels,
setWaitingPanels,
}) => {
const { visualizationSocket } = useSocketStore();
const { projectId } = useParams();
// Function to toggle visibility of a panel
const toggleVisibility = (side: Side) => {
const isHidden = hiddenPanels[selectedZone.zoneUuid]?.includes(side) ?? false;
if (isHidden) {
// If the panel is already hidden, remove it from the hiddenPanels array for this zone
setHiddenPanels((prevHiddenPanels) => ({
...prevHiddenPanels,
[selectedZone.zoneUuid]: prevHiddenPanels[selectedZone.zoneUuid].filter(
(panel) => panel !== side
),
}));
} else {
// If the panel is visible, add it to the hiddenPanels array for this zone
setHiddenPanels((prevHiddenPanels) => ({
...prevHiddenPanels,
[selectedZone.zoneUuid]: [
...(prevHiddenPanels[selectedZone.zoneUuid] || []),
side,
],
}));
}
};
// Function to toggle lock/unlock a panel
const toggleLockPanel = async (side: Side) => {
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0]; // Fallback value
const userId = localStorage.getItem("userId");
//add api
const newLockedPanels = selectedZone.lockedPanels.includes(side)
? selectedZone.lockedPanels.filter((panel) => panel !== side)
: [...selectedZone.lockedPanels, side];
const updatedZone = {
...selectedZone,
lockedPanels: newLockedPanels,
};
let lockedPanel = {
organization: organization,
lockedPanel: newLockedPanels,
zoneUuid: selectedZone.zoneUuid,
userId, projectId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-panel:locked", lockedPanel);
}
setSelectedZone(updatedZone);
};
// Function to clean all widgets from a panel
const cleanPanel = async (side: Side) => {
//add api
// console.log('side: ', side);
if (
hiddenPanels[selectedZone.zoneUuid]?.includes(side) ||
selectedZone.lockedPanels.includes(side)
)
return;
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0]; // Fallback value
const userId = localStorage.getItem("userId");
let clearPanel = {
organization: organization,
panelName: side,
zoneUuid: selectedZone.zoneUuid,
userId, projectId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-panel:clear", clearPanel);
}
const cleanedWidgets = selectedZone.widgets.filter(
(widget) => widget.panel !== side
);
const updatedZone = {
...selectedZone,
widgets: cleanedWidgets,
};
// Update the selectedZone state
setSelectedZone(updatedZone);
};
// Function to handle "+" button click
const handlePlusButtonClick = async (side: Side) => {
const zoneUuid = selectedZone.zoneUuid;
if (selectedZone.activeSides.includes(side)) {
// Already active: Schedule removal
setWaitingPanels(side); // Mark as waiting
setTimeout(() => {
console.log("Removing after wait...");
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0] ?? "";
const userId = localStorage.getItem("userId");
// Remove widgets for that side
const cleanedWidgets = selectedZone.widgets.filter(
(widget) => widget.panel !== side
);
const newActiveSides = selectedZone.activeSides.filter(
(s) => s !== side
);
const updatedZone = {
...selectedZone,
widgets: cleanedWidgets,
activeSides: newActiveSides,
panelOrder: newActiveSides,
};
const deletePanel = {
organization,
panelName: side,
zoneUuid,
projectId,
userId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-panel:delete", deletePanel);
}
setSelectedZone(updatedZone);
if (hiddenPanels[zoneUuid]?.includes(side)) {
setHiddenPanels((prev) => ({
...prev,
[zoneUuid]: prev[zoneUuid].filter((s) => s !== side),
}));
}
// Remove from waiting state
setWaitingPanels(null);
}, 500); // Wait for 2 seconds
} else {
// Panel does not exist: Add it
try {
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0] ?? "";
const userId = localStorage.getItem("userId");
const newActiveSides = selectedZone.activeSides.includes(side)
? [...selectedZone.activeSides]
: [...selectedZone.activeSides, side];
const updatedZone = {
...selectedZone,
activeSides: newActiveSides,
panelOrder: newActiveSides,
};
const addPanel = {
organization,
zoneUuid,
projectId,
userId,
panelOrder: newActiveSides,
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-panel:add", addPanel);
}
setSelectedZone(updatedZone);
} catch (error) {
echo.error("Failed to adding panel");
console.error("Error adding panel:", error);
}
}
};
return (
<div>
{(["top", "right", "bottom", "left"] as Side[]).map((side) => (
<div key={side} className={`side-button-container ${side}`}>
{/* "+" Button */}
<button
id={`${side}-add-button`}
className={`side-button ${side}${selectedZone.activeSides.includes(side) ? " active" : ""
}`}
onClick={() => handlePlusButtonClick(side)}
title={
selectedZone.activeSides.includes(side)
? `Remove all items and close ${side} panel`
: `Activate ${side} panel`
}
>
<div className="add-icon">
<AddIcon />
</div>
</button>
{/* Extra Buttons */}
{selectedZone.activeSides.includes(side) && (
<div
className={`extra-Bs
${waitingPanels === side ? "extra-Bs-addclosing" : ""}
${!hiddenPanels[selectedZone.zoneUuid]?.includes(side) &&
waitingPanels !== side
? "extra-Bs-addopening"
: ""
}
`}
>
{/* Hide Panel */}
<button
className={`icon ${hiddenPanels[selectedZone.zoneUuid]?.includes(side)
? "active"
: ""
}`}
id={`${side}-hide-panel-visulization`}
title={
hiddenPanels[selectedZone.zoneUuid]?.includes(side)
? "Show Panel"
: "Hide Panel"
}
onClick={() => toggleVisibility(side)}
>
<EyeIcon
fill={
hiddenPanels[selectedZone.zoneUuid]?.includes(side)
? "var(--icon-default-color-active)"
: "var(--text-color)"
}
/>
</button>
{/* Clean Panel */}
<button
className="icon"
title="Clean Panel"
id={`${side}-clean-panel-visulization`}
onClick={() => cleanPanel(side)}
style={{
cursor:
hiddenPanels[selectedZone.zoneUuid]?.includes(side) ||
selectedZone.lockedPanels.includes(side)
? "not-allowed"
: "pointer",
}}
>
<CleanPannel />
</button>
{/* Lock/Unlock Panel */}
<button
className={`icon ${selectedZone.lockedPanels.includes(side) ? "active" : ""
}`}
id={`${side}-lock-panel-visulization`}
title={
selectedZone.lockedPanels.includes(side)
? "Unlock Panel"
: "Lock Panel"
}
onClick={() => toggleLockPanel(side)}
>
<LockIcon
fill={
selectedZone.lockedPanels.includes(side)
? "var(--icon-default-color-active)"
: "var(--text-color)"
}
/>
</button>
</div>
)}
</div>
))}
</div>
);
};
export default AddButtons;

View File

@@ -0,0 +1,343 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { arrayMove } from "@dnd-kit/sortable";
import { useSocketStore } from "../../../../store/builder/store";
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
import { useWidgetStore } from "../../../../store/useWidgetStore";
import { DraggableWidget } from "../2d/DraggableWidget";
import { useParams } from "react-router-dom";
type Side = "top" | "bottom" | "left" | "right";
interface Widget {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}
interface PanelProps {
selectedZone: {
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: Widget[];
};
setSelectedZone: React.Dispatch<
React.SetStateAction<{
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: Widget[];
}>
>;
hiddenPanels: any;
setZonesData: React.Dispatch<React.SetStateAction<any>>;
waitingPanels: any;
}
const generateUniqueId = () =>
`${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
const Panel: React.FC<PanelProps> = ({
selectedZone,
setSelectedZone,
hiddenPanels,
setZonesData,
waitingPanels,
}) => {
const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({});
const [panelDimensions, setPanelDimensions] = useState<{
[side in Side]?: { width: number; height: number };
}>({});
const [openKebabId, setOpenKebabId] = useState<string | null>(null);
const { isPlaying } = usePlayButtonStore();
const { visualizationSocket } = useSocketStore();
const [canvasDimensions, setCanvasDimensions] = useState({
width: 0,
height: 0,
});
const { projectId } = useParams();
// Track canvas dimensions
useEffect(() => {
const canvas = document.getElementById("real-time-vis-canvas");
if (!canvas) return;
const updateCanvasDimensions = () => {
const rect = canvas.getBoundingClientRect();
setCanvasDimensions({
width: rect.width,
height: rect.height,
});
};
updateCanvasDimensions();
const resizeObserver = new ResizeObserver(updateCanvasDimensions);
resizeObserver.observe(canvas);
return () => resizeObserver.unobserve(canvas);
}, []);
// Calculate panel size
const panelSize = Math.max(
Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25),
170 // Min 170px
);
// Define getPanelStyle
const getPanelStyle = useMemo(
() => (side: Side) => {
const currentIndex = selectedZone.panelOrder.indexOf(side);
const previousPanels = selectedZone.panelOrder.slice(0, currentIndex);
const leftActive = previousPanels.includes("left");
const rightActive = previousPanels.includes("right");
const topActive = previousPanels.includes("top");
const bottomActive = previousPanels.includes("bottom");
switch (side) {
case "top":
case "bottom":
return {
minWidth: "170px",
width: `calc(100% - ${(leftActive ? panelSize : 0) + (rightActive ? panelSize : 0)
}px)`,
minHeight: "170px",
height: `${panelSize}px`,
left: leftActive ? `${panelSize}px` : "0",
right: rightActive ? `${panelSize}px` : "0",
[side]: "0",
};
case "left":
case "right":
return {
minWidth: "170px",
width: `${panelSize}px`,
minHeight: "170px",
height: `calc(100% - ${(topActive ? panelSize : 0) + (bottomActive ? panelSize : 0)
}px)`,
top: topActive ? `${panelSize}px` : "0",
bottom: bottomActive ? `${panelSize}px` : "0",
[side]: "0",
};
default:
return {};
}
},
[selectedZone.panelOrder, panelSize]
);
// Handle drop event
const handleDrop = (e: React.DragEvent, panel: Side) => {
e.preventDefault();
const { draggedAsset } = useWidgetStore.getState();
if (
!draggedAsset ||
isPanelLocked(panel) ||
hiddenPanels[selectedZone.zoneUuid]?.includes(panel)
)
return;
const currentWidgetsCount = getCurrentWidgetCount(panel);
const maxCapacity = calculatePanelCapacity(panel);
if (currentWidgetsCount < maxCapacity) {
addWidgetToPanel(draggedAsset, panel);
}
};
// Check if panel is locked
const isPanelLocked = (panel: Side) =>
selectedZone.lockedPanels.includes(panel);
// Get current widget count in a panel
const getCurrentWidgetCount = (panel: Side) =>
selectedZone.widgets.filter((w) => w.panel === panel).length;
// Calculate panel capacity
const calculatePanelCapacity = (panel: Side) => {
const CHART_WIDTH = panelSize - 10;
const CHART_HEIGHT = panelSize - 10;
const dimensions = panelDimensions[panel];
if (!dimensions) {
return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities
}
return panel === "top" || panel === "bottom"
? Math.max(1, Math.floor(dimensions.width / CHART_WIDTH))
: Math.max(1, Math.floor(dimensions.height / CHART_HEIGHT));
};
// Add widget to panel
const addWidgetToPanel = async (asset: any, panel: Side) => {
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0];
const userId = localStorage.getItem("userId");
const newWidget = {
...asset,
id: generateUniqueId(),
panel,
};
let addWidget = {
organization: organization,
zoneUuid: selectedZone.zoneUuid,
widget: newWidget,
projectId, userId
};
if (visualizationSocket) {
visualizationSocket.emit("v1:viz-widget:add", addWidget);
}
setSelectedZone((prev) => ({
...prev,
widgets: [...prev.widgets, newWidget],
}));
};
// Observe panel dimensions
useEffect(() => {
const observers: ResizeObserver[] = [];
const currentPanelRefs = panelRefs.current;
selectedZone.activeSides.forEach((side) => {
const element = currentPanelRefs[side];
if (element) {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
setPanelDimensions((prev) => ({
...prev,
[side]: { width, height },
}));
}
});
observer.observe(element);
observers.push(observer);
}
});
return () => {
observers.forEach((observer) => observer.disconnect());
};
}, [selectedZone.activeSides]);
// Handle widget reordering
const handleReorder = (fromIndex: number, toIndex: number, panel: Side) => {
setSelectedZone((prev) => {
const widgetsInPanel = prev.widgets.filter((w) => w.panel === panel);
const reorderedWidgets = arrayMove(widgetsInPanel, fromIndex, toIndex);
const updatedWidgets = prev.widgets
.filter((widget) => widget.panel !== panel)
.concat(reorderedWidgets);
return {
...prev,
widgets: updatedWidgets,
};
});
};
// Calculate capacities and dimensions
const topWidth = getPanelStyle("top").width;
const bottomWidth = getPanelStyle("bottom").height;
const leftHeight = getPanelStyle("left").height;
const rightHeight = getPanelStyle("right").height;
const topCapacity = calculatePanelCapacity("top");
const bottomCapacity = calculatePanelCapacity("bottom");
const leftCapacity = calculatePanelCapacity("left");
const rightCapacity = calculatePanelCapacity("right");
return (
<>
<style>
{`
:root {
--topWidth: ${topWidth};
--bottomWidth: ${bottomWidth} ;
--leftHeight: ${leftHeight};
--rightHeight: ${rightHeight};
--topCapacity: ${topCapacity};
--bottomCapacity: ${bottomCapacity};
--leftCapacity: ${leftCapacity};
--rightCapacity: ${rightCapacity};
}
`}
</style>
{selectedZone.activeSides.map((side) => (
<div
key={side}
id={`panel-wrapper-${side}`}
className={`panel ${side}-panel absolute ${hiddenPanels[selectedZone.zoneUuid]?.includes(side) ? "hidePanel" : ""
}`}
style={getPanelStyle(side)}
onDrop={(e) => handleDrop(e, side)}
onDragOver={(e) => e.preventDefault()}
ref={(el) => {
if (el) {
panelRefs.current[side] = el;
} else {
delete panelRefs.current[side];
}
}}
>
<div
className={`panel-content ${waitingPanels === side ? `${side}-closing` : ""
}${!hiddenPanels[selectedZone.zoneUuid]?.includes(side) &&
waitingPanels !== side
? `${side}-opening`
: ""
} ${isPlaying ? "fullScreen" : ""}`}
style={{
pointerEvents:
selectedZone.lockedPanels.includes(side) ||
hiddenPanels[selectedZone.zoneUuid]?.includes(side)
? "none"
: "auto",
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
}}
>
{selectedZone.widgets
.filter((w) => w.panel === side)
.map((widget, index) => (
<DraggableWidget
hiddenPanels={hiddenPanels}
widget={widget}
key={widget.id}
index={index}
onReorder={(fromIndex, toIndex) =>
handleReorder(fromIndex, toIndex, side)
}
openKebabId={openKebabId}
setOpenKebabId={setOpenKebabId}
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
/>
))}
</div>
</div>
))}
</>
);
};
export default Panel;

View File

@@ -0,0 +1,7 @@
import React from "react";
const WidgetPlaceHolder = () => {
return <div className="widget-placeholder"></div>;
};
export default WidgetPlaceHolder;

View File

@@ -0,0 +1,277 @@
import React, { useEffect, useRef, useState, useCallback } from "react";
import { useWidgetStore, Widget } from "../../../store/useWidgetStore";
import {
useDroppedObjectsStore,
useFloatingWidget,
} from "../../../store/visualization/useDroppedObjectsStore";
import { getSelect2dZoneData } from "../../../services/visulization/zone/getSelect2dZoneData";
import { getFloatingZoneData } from "../../../services/visulization/zone/getFloatingData";
import { get3dWidgetZoneData } from "../../../services/visulization/zone/get3dWidgetData";
import {
MoveArrowLeft,
MoveArrowRight,
} from "../../../components/icons/SimulationIcons";
import { InfoIcon } from "../../../components/icons/ExportCommonIcons";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import { useParams } from "react-router-dom";
// Define the type for `Side`
type Side = "top" | "bottom" | "left" | "right";
interface HiddenPanels {
[zoneUuid: string]: Side[];
}
interface DisplayZoneProps {
zonesData: {
[key: string]: {
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
widgets: Widget[];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
};
};
selectedZone: {
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
zoneUuid: string;
points: [];
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}[];
};
setSelectedZone: React.Dispatch<
React.SetStateAction<{
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: {
id: string;
type: string;
title: string;
panel: Side;
data: any;
}[];
}>
>;
hiddenPanels: HiddenPanels; // Updated prop type
setHiddenPanels: React.Dispatch<React.SetStateAction<HiddenPanels>>; // Updated prop type
}
const DisplayZone: React.FC<DisplayZoneProps> = ({
zonesData,
selectedZone,
setSelectedZone,
hiddenPanels,
}) => {
// Ref for the container element
const containerRef = useRef<HTMLDivElement | null>(null);
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
// State to track overflow visibility
const [showLeftArrow, setShowLeftArrow] = useState(false);
const [showRightArrow, setShowRightArrow] = useState(false);
const { floatingWidget, setFloatingWidget } = useFloatingWidget();
const { isPlaying } = usePlayButtonStore();
const { setSelectedChartId } = useWidgetStore();
const { projectId } = useParams();
// Function to calculate overflow state
const updateOverflowState = useCallback(() => {
const container = scrollContainerRef.current;
if (container) {
const isOverflowing = container.scrollWidth > container.clientWidth;
const canScrollLeft = container.scrollLeft > 0;
const canScrollRight =
container.scrollLeft + container.clientWidth + 1 <
container.scrollWidth;
setShowLeftArrow(isOverflowing && canScrollLeft);
setShowRightArrow(isOverflowing && canScrollRight);
// console.log('canScrollRight: ', canScrollRight);
// console.log('isOverflowing: ', isOverflowing);
}
}, []);
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
// Initial calculation after the DOM has been rendered
const observer = new ResizeObserver(updateOverflowState);
observer.observe(container);
// Update on scroll
const handleScroll = () => updateOverflowState();
container.addEventListener("scroll", handleScroll);
// Add mouse wheel listener for horizontal scrolling
const handleWheel = (event: WheelEvent) => {
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
event.preventDefault();
container.scrollBy({
left: event.deltaY * 2,
behavior: "smooth",
});
}
};
container.addEventListener("wheel", handleWheel, { passive: false });
// Initial check
updateOverflowState();
return () => {
observer.disconnect();
container.removeEventListener("scroll", handleScroll);
container.removeEventListener("wheel", handleWheel);
};
}, [updateOverflowState]);
// Handle scrolling with navigation arrows
const handleScrollLeft = () => {
const container = scrollContainerRef.current;
if (container) {
container.scrollBy({
left: -200,
behavior: "smooth",
});
}
};
const handleScrollRight = () => {
const container = scrollContainerRef.current;
if (container) {
container.scrollBy({
left: 200,
behavior: "smooth",
});
}
};
async function handleSelect2dZoneData(zoneUuid: string, zoneName: string) {
try {
if (selectedZone?.zoneUuid === zoneUuid) {
return;
}
// setSelectedChartId(null);
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
let response = await getSelect2dZoneData(zoneUuid, organization, projectId);
// console.log('response2d: ', response);
let res = await getFloatingZoneData(zoneUuid, organization, projectId);
// console.log("resFloating: ", res);
setFloatingWidget(res);
// Set the selected zone in the store
useDroppedObjectsStore.getState().setZone(zoneName, zoneUuid);
if (Array.isArray(res)) {
res.forEach((val) => {
useDroppedObjectsStore.getState().addObject(zoneName, val);
});
}
setSelectedZone({
zoneName,
activeSides: response.activeSides || [],
panelOrder: response.panelOrder || [],
lockedPanels: response.lockedPanels || [],
widgets: response.widgets || [],
points: response.points || [],
zoneUuid: zoneUuid,
zoneViewPortTarget: response.viewPortCenter || {},
zoneViewPortPosition: response.viewPortposition || {},
});
} catch (error) {
echo.error("Failed to select zone");
}
}
return (
<div
ref={containerRef}
id="zone-container"
className={`zone-container ${selectedZone?.activeSides?.includes("bottom") &&
!hiddenPanels[selectedZone.zoneUuid]?.includes("bottom")
? "bottom"
: ""
} ${isPlaying ? "visualization-playing" : ""}`}
>
{/* Left Arrow */}
{showLeftArrow && (
<button
id="zone-preview-left-arrow"
className="arrow left-arrow"
onClick={handleScrollLeft}
>
<MoveArrowLeft />
</button>
)}
{/* Scrollable Zones Container */}
<div
ref={scrollContainerRef}
className="zones-wrapper"
style={{ overflowX: "auto", whiteSpace: "nowrap" }}
>
{Object.keys(zonesData).length !== 0 ? (
<>
{Object.keys(zonesData).map((zoneName, index) => (
<div
key={index}
className={`zone ${selectedZone.zoneName === zoneName ? "active" : ""
}`}
onClick={() => {
console.log('zonesData: ', zonesData);
handleSelect2dZoneData(zonesData[zoneName]?.zoneUuid, zoneName)
}
}
>
{zoneName}
</div>
))}
</>
) : (
<div className="no-zone">
<InfoIcon />
No zones? Create one!
</div>
)}
</div>
{/* Right Arrow */}
{showRightArrow && (
<button
id="zone-preview-right-arrow"
className="arrow right-arrow"
onClick={handleScrollRight}
>
<MoveArrowRight />
</button>
)}
</div>
);
};
export default DisplayZone;

View File

@@ -0,0 +1,78 @@
import React, { useEffect, useRef } from 'react'
import { useSelectedFloorItem, useZoneAssetId } from '../../../store/builder/store';
import * as THREE from "three";
import { useThree } from '@react-three/fiber';
import * as Types from "../../../types/world/worldTypes";
export default function ZoneAssets() {
const { zoneAssetId, setZoneAssetId } = useZoneAssetId();
const { setSelectedFloorItem } = useSelectedFloorItem();
const { raycaster, controls, scene }: any = useThree();
useEffect(() => {
// console.log('zoneAssetId: ', zoneAssetId);
if (!zoneAssetId) return
let AssetMesh = scene.getObjectByProperty("uuid", zoneAssetId.id);
if (AssetMesh) {
const bbox = new THREE.Box3().setFromObject(AssetMesh);
const size = bbox.getSize(new THREE.Vector3());
const center = bbox.getCenter(new THREE.Vector3());
const front = new THREE.Vector3(0, 0, 1);
AssetMesh.localToWorld(front);
front.sub(AssetMesh.position).normalize();
const distance = Math.max(size.x, size.y, size.z) * 2;
const newPosition = center.clone().addScaledVector(front, distance);
controls.setPosition(newPosition.x, newPosition.y, newPosition.z, true);
controls.setTarget(center.x, center.y, center.z, true);
controls.fitToBox(AssetMesh, true, { cover: true, paddingTop: 5, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, });
setSelectedFloorItem(AssetMesh);
} else {
if (Array.isArray(zoneAssetId.position) && zoneAssetId.position.length >= 3) {
let selectedAssetPosition = [
zoneAssetId.position[0],
10,
zoneAssetId.position[2]
];
let selectedAssetTarget = [
zoneAssetId.position[0],
zoneAssetId.position[1],
zoneAssetId.position[2]
];
const setCam = async () => {
await controls?.setLookAt(...selectedAssetPosition, ...selectedAssetTarget, true);
setTimeout(() => {
let AssetMesh = scene.getObjectByProperty("uuid", zoneAssetId.id);
if (AssetMesh) {
const bbox = new THREE.Box3().setFromObject(AssetMesh);
const size = bbox.getSize(new THREE.Vector3());
const center = bbox.getCenter(new THREE.Vector3());
const front = new THREE.Vector3(0, 0, 1);
AssetMesh.localToWorld(front);
front.sub(AssetMesh.position).normalize();
const distance = Math.max(size.x, size.y, size.z) * 2;
const newPosition = center.clone().addScaledVector(front, distance);
controls.setPosition(newPosition.x, newPosition.y, newPosition.z, true);
controls.setTarget(center.x, center.y, center.z, true);
controls.fitToBox(AssetMesh, true, { cover: true, paddingTop: 5, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, });
setSelectedFloorItem(AssetMesh);
}
}, 500)
};
setCam();
}
}
}, [zoneAssetId, scene, controls])
return (
<>
</>
)
}

View File

@@ -0,0 +1,102 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";
import { useSelectedZoneStore } from "../../../store/visualization/useZoneStore";
import {
useEditPosition,
usezonePosition,
usezoneTarget,
} from "../../../store/builder/store";
export default function ZoneCentreTarget() {
const { selectedZone } = useSelectedZoneStore();
const [previousZoneCentre, setPreviousZoneCentre] = useState<number[] | null>(
null
);
const sphereRef = useRef<THREE.Mesh>(null);
const { controls }: any = useThree();
const { setZonePosition } = usezonePosition();
const { setZoneTarget } = usezoneTarget();
const { Edit } = useEditPosition();
const TRANSITION_SPEED = 2000;
useEffect(() => {
if (
selectedZone.zoneViewPortTarget &&
JSON.stringify(previousZoneCentre) !==
JSON.stringify(selectedZone.zoneViewPortTarget)
) {
setPreviousZoneCentre(selectedZone.zoneViewPortTarget);
}
}, [selectedZone.zoneViewPortTarget, previousZoneCentre]);
const centrePoint = useMemo(() => {
if (!previousZoneCentre || !selectedZone.zoneViewPortTarget) return null;
return previousZoneCentre.map(
(value, index) => (value + selectedZone.zoneViewPortTarget[index]) / 2
);
}, [previousZoneCentre, selectedZone.zoneViewPortTarget]);
useEffect(() => {
if (selectedZone.zoneName !== "") {
if (sphereRef.current) {
sphereRef.current.position.set(
selectedZone.zoneViewPortTarget[0],
selectedZone.zoneViewPortTarget[1],
selectedZone.zoneViewPortTarget[2]
);
}
if (centrePoint) {
if (centrePoint.length > 0) {
const setCam = async () => {
controls.setLookAt(
centrePoint[0],
26,
centrePoint[2],
...centrePoint,
true,
TRANSITION_SPEED
);
setTimeout(() => {
controls?.setLookAt(
...selectedZone.zoneViewPortPosition,
...selectedZone.zoneViewPortTarget,
true,
TRANSITION_SPEED
);
}, 100);
};
setCam();
} else {
const setCam = async () => {
controls?.setLookAt(
...selectedZone.zoneViewPortPosition,
...selectedZone.zoneViewPortTarget,
true,
TRANSITION_SPEED
);
};
setCam();
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedZone.zoneViewPortTarget]);
useFrame(() => {
if (Edit) {
setZonePosition([
controls.getPosition().x,
controls.getPosition().y,
controls.getPosition().z,
]);
setZoneTarget([
controls.getTarget().x,
controls.getTarget().y,
controls.getTarget().z,
]);
}
});
return <></>;
}