Merge remote-tracking branch 'origin/main-demo' into ui
This commit is contained in:
@@ -75,13 +75,13 @@ function MainScene() {
|
||||
clearComparisonProduct();
|
||||
setIsVersionSaved(false);
|
||||
}
|
||||
}, [activeModule])
|
||||
}, [activeModule, clearComparisonProduct, setIsVersionSaved])
|
||||
|
||||
useEffect(() => {
|
||||
if (versionHistory.length > 0) {
|
||||
setSelectedVersion(versionHistory[0])
|
||||
}
|
||||
}, [versionHistory])
|
||||
}, [setSelectedVersion, versionHistory])
|
||||
|
||||
const handleSelectVersion = (option: string) => {
|
||||
const version = versionHistory.find((version) => version.versionName === option);
|
||||
@@ -140,7 +140,7 @@ function MainScene() {
|
||||
{!selectedUser && (
|
||||
<>
|
||||
<KeyPressListener />
|
||||
{/* {loadingProgress > 0 && <LoadingPage progress={loadingProgress} />} */}
|
||||
{loadingProgress > 0 && <LoadingPage progress={loadingProgress} />}
|
||||
{!isPlaying && (
|
||||
<>
|
||||
{toggleThreeD && !isVersionSaved && <ModuleToggle />}
|
||||
@@ -155,7 +155,7 @@ function MainScene() {
|
||||
)}
|
||||
{(isPlaying) &&
|
||||
activeModule === "simulation" &&
|
||||
loadingProgress == 0 && <SimulationPlayer />}
|
||||
loadingProgress === 0 && <SimulationPlayer />}
|
||||
{(isPlaying) &&
|
||||
activeModule !== "simulation" && <ControlsPlayer />}
|
||||
|
||||
@@ -188,7 +188,7 @@ function MainScene() {
|
||||
}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
>
|
||||
{/* <Scene layout="Main Layout" /> */}
|
||||
<Scene layout="Main Layout" />
|
||||
</div>
|
||||
|
||||
{selectedProduct && selectedVersion && isVersionSaved && !isPlaying && activeModule === "simulation" && (
|
||||
|
||||
@@ -4,54 +4,92 @@ import DropDownList from "../../ui/list/DropDownList";
|
||||
import { useSceneContext } from "../../../modules/scene/sceneContext";
|
||||
import { isPointInsidePolygon } from "../../../functions/isPointInsidePolygon";
|
||||
|
||||
interface AssetData {
|
||||
id: string;
|
||||
name: string;
|
||||
position?: [];
|
||||
rotation?: {};
|
||||
}
|
||||
|
||||
interface ZoneData {
|
||||
id: string;
|
||||
name: string;
|
||||
assets: { id: string; name: string; position?: []; rotation?: {} }[];
|
||||
assets: AssetData[];
|
||||
}
|
||||
|
||||
const Outline: React.FC = () => {
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
const [zoneDataList, setZoneDataList] = useState<ZoneData[]>([]);
|
||||
const [buildingsList, setBuildingsList] = useState<{ id: string; name: string }[]>([]);
|
||||
const [isLayersOpen, setIsLayersOpen] = useState(true);
|
||||
const [isBuildingsOpen, setIsBuildingsOpen] = useState(false);
|
||||
const [isZonesOpen, setIsZonesOpen] = useState(false);
|
||||
const [sceneAssetsDataList, setSceneAssetsDataList] = useState<
|
||||
ZoneData[] | AssetData[]
|
||||
>([]);
|
||||
// const [buildingsList, setBuildingsList] = useState<{ id: string; name: string }[]>([]);
|
||||
// const [isLayersOpen, setIsLayersOpen] = useState(true);
|
||||
// const [isBuildingsOpen, setIsBuildingsOpen] = useState(false);
|
||||
const [isZonesOpen, setIsZonesOpen] = useState(true);
|
||||
const { assetStore, zoneStore } = useSceneContext();
|
||||
const { assets } = assetStore();
|
||||
const { zones } = zoneStore();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const updatedZoneList: ZoneData[] = zones?.map((zone: any) => {
|
||||
const polygon2D = zone.points.map((p: any) => [p.position[0], p.position[2]]);
|
||||
const assetsInZone = assets.filter((item: any) => {
|
||||
const [x, , z] = item.position;
|
||||
return isPointInsidePolygon([x, z], polygon2D as [number, number][]);
|
||||
})
|
||||
.map((item: any) => ({
|
||||
id: item.modelUuid,
|
||||
name: item.modelName,
|
||||
position: item.position,
|
||||
rotation: item.rotation,
|
||||
}));
|
||||
const assignedAssets = new Set<string>();
|
||||
|
||||
|
||||
return {
|
||||
id: zone.zoneUuid,
|
||||
name: zone.zoneName,
|
||||
assets: assetsInZone,
|
||||
};
|
||||
});
|
||||
const updatedZoneList: ZoneData[] =
|
||||
zones?.map((zone: any) => {
|
||||
const polygon2D = zone.points.map((p: any) => [
|
||||
p.position[0],
|
||||
p.position[2],
|
||||
]);
|
||||
const assetsInZone = assets
|
||||
.filter((item: any) => {
|
||||
const [x, , z] = item.position;
|
||||
const inside = isPointInsidePolygon(
|
||||
[x, z],
|
||||
polygon2D as [number, number][]
|
||||
);
|
||||
if (inside) assignedAssets.add(item.modelUuid);
|
||||
return inside;
|
||||
})
|
||||
.map((item: any) => ({
|
||||
id: item.modelUuid,
|
||||
name: item.modelName,
|
||||
position: item.position,
|
||||
rotation: item.rotation,
|
||||
}));
|
||||
|
||||
setZoneDataList(updatedZoneList);
|
||||
return {
|
||||
id: zone.zoneUuid,
|
||||
name: zone.zoneName,
|
||||
assets: assetsInZone,
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
// Collect unassigned assets
|
||||
const unassignedAssets = assets
|
||||
.filter((item: any) => !assignedAssets.has(item.modelUuid))
|
||||
.map((item: any) => ({
|
||||
id: item.modelUuid,
|
||||
name: item.modelName,
|
||||
position: item.position,
|
||||
rotation: item.rotation,
|
||||
}));
|
||||
|
||||
// Add as a separate "zone"
|
||||
if (unassignedAssets.length > 0) {
|
||||
updatedZoneList.push({
|
||||
id: "unassigned-zone",
|
||||
name: "Unassigned",
|
||||
assets: unassignedAssets,
|
||||
});
|
||||
}
|
||||
|
||||
setSceneAssetsDataList(updatedZoneList);
|
||||
}, [zones, assets]);
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchValue(value);
|
||||
};
|
||||
|
||||
const dropdownItems = [{ id: "1", name: "Ground Floor" }];
|
||||
// const dropdownItems = [{ id: "1", name: "Ground Floor" }];
|
||||
|
||||
return (
|
||||
<div className="outline-container">
|
||||
@@ -63,7 +101,7 @@ const Outline: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="outline-content-container">
|
||||
<section className="outline-section">
|
||||
{/* <section className="outline-section">
|
||||
<DropDownList
|
||||
value="Layers"
|
||||
items={dropdownItems}
|
||||
@@ -73,19 +111,19 @@ const Outline: React.FC = () => {
|
||||
showFocusIcon={true}
|
||||
remove
|
||||
/>
|
||||
</section>
|
||||
</section> */}
|
||||
<section className="outline-section overflow">
|
||||
<DropDownList
|
||||
{/* <DropDownList
|
||||
value="Buildings"
|
||||
items={buildingsList}
|
||||
isOpen={isBuildingsOpen}
|
||||
onToggle={() => setIsBuildingsOpen((prev) => !prev)}
|
||||
showKebabMenu={false}
|
||||
showAddIcon={false}
|
||||
/>
|
||||
/> */}
|
||||
<DropDownList
|
||||
value="Zones"
|
||||
items={zoneDataList}
|
||||
items={sceneAssetsDataList}
|
||||
isOpen={isZonesOpen}
|
||||
onToggle={() => setIsZonesOpen((prev) => !prev)}
|
||||
showKebabMenu={false}
|
||||
|
||||
@@ -4,130 +4,134 @@ import InputWithDropDown from "../../../ui/inputs/InputWithDropDown";
|
||||
import { RemoveIcon } from "../../../icons/ExportCommonIcons";
|
||||
import PositionInput from "../customInput/PositionInputs";
|
||||
import RotationInput from "../customInput/RotationInput";
|
||||
import { useSelectedFloorItem, useObjectPosition, useObjectRotation } from "../../../../store/builder/store";
|
||||
import {
|
||||
useSelectedFloorItem,
|
||||
useObjectPosition,
|
||||
useObjectRotation,
|
||||
} from "../../../../store/builder/store";
|
||||
import { useSceneContext } from "../../../../modules/scene/sceneContext";
|
||||
import { useBuilderStore } from "../../../../store/builder/useBuilderStore";
|
||||
|
||||
interface UserData {
|
||||
id: number;
|
||||
label: string;
|
||||
value: string;
|
||||
id: number;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const AssetProperties: React.FC = () => {
|
||||
const [userData, setUserData] = useState<UserData[]>([]);
|
||||
const { selectedFloorItem } = useSelectedFloorItem();
|
||||
const { objectPosition } = useObjectPosition();
|
||||
const { objectRotation } = useObjectRotation();
|
||||
const { assetStore } = useSceneContext();
|
||||
const { assets, setCurrentAnimation } = assetStore();
|
||||
const { loopAnimation } = useBuilderStore();
|
||||
const [hoveredIndex, setHoveredIndex] = useState<any>(null);
|
||||
const [userData, setUserData] = useState<UserData[]>([]);
|
||||
const { selectedFloorItem } = useSelectedFloorItem();
|
||||
const { objectPosition } = useObjectPosition();
|
||||
const { objectRotation } = useObjectRotation();
|
||||
const { assetStore } = useSceneContext();
|
||||
const { assets, setCurrentAnimation } = assetStore();
|
||||
const { loopAnimation } = useBuilderStore();
|
||||
const [hoveredIndex, setHoveredIndex] = useState<any>(null);
|
||||
|
||||
const handleAddUserData = () => {
|
||||
};
|
||||
const handleAddUserData = () => {
|
||||
setUserData([]);
|
||||
};
|
||||
|
||||
const handleUserDataChange = (id: number, newValue: string) => {
|
||||
};
|
||||
const handleUserDataChange = (id: number, newValue: string) => {};
|
||||
|
||||
const handleRemoveUserData = (id: number) => {
|
||||
};
|
||||
const handleRemoveUserData = (id: number) => {};
|
||||
|
||||
const handleAnimationClick = (animation: string) => {
|
||||
if (selectedFloorItem) {
|
||||
setCurrentAnimation(selectedFloorItem.uuid, animation, true, loopAnimation, true);
|
||||
}
|
||||
const handleAnimationClick = (animation: string) => {
|
||||
if (selectedFloorItem) {
|
||||
setCurrentAnimation(
|
||||
selectedFloorItem.uuid,
|
||||
animation,
|
||||
true,
|
||||
loopAnimation,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedFloorItem) return null;
|
||||
if (!selectedFloorItem) return null;
|
||||
|
||||
return (
|
||||
<div className="asset-properties-container">
|
||||
{/* Name */}
|
||||
<div className="header">{selectedFloorItem.userData.modelName}</div>
|
||||
<section>
|
||||
{objectPosition &&
|
||||
<PositionInput
|
||||
onChange={() => { }}
|
||||
value1={parseFloat(objectPosition.x.toFixed(5))}
|
||||
value2={parseFloat(objectPosition.z.toFixed(5))}
|
||||
/>
|
||||
}
|
||||
{objectRotation &&
|
||||
<RotationInput
|
||||
onChange={() => { }}
|
||||
value={parseFloat(objectRotation.y.toFixed(5))}
|
||||
/>
|
||||
}
|
||||
</section>
|
||||
return (
|
||||
<div className="asset-properties-container">
|
||||
{/* Name */}
|
||||
<div className="header">{selectedFloorItem.userData.modelName}</div>
|
||||
<section>
|
||||
{objectPosition && (
|
||||
<PositionInput
|
||||
onChange={() => {}}
|
||||
value1={parseFloat(objectPosition.x.toFixed(5))}
|
||||
value2={parseFloat(objectPosition.z.toFixed(5))}
|
||||
/>
|
||||
)}
|
||||
{objectRotation && (
|
||||
<RotationInput
|
||||
onChange={() => {}}
|
||||
value={parseFloat(objectRotation.y.toFixed(5))}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="header">Render settings</div>
|
||||
<InputToggle inputKey="visible" label="Visible" />
|
||||
<InputToggle inputKey="frustumCull" label="Frustum cull" />
|
||||
</section>
|
||||
<div className="header">Render settings</div>
|
||||
<section>
|
||||
<InputToggle inputKey="visible" label="Visible" />
|
||||
<InputToggle inputKey="frustumCull" label="Frustum cull" />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="header">User Data</div>
|
||||
{userData.map((data) => (
|
||||
<div className="input-container">
|
||||
<InputWithDropDown
|
||||
key={data.id}
|
||||
label={data.label}
|
||||
value={data.value}
|
||||
editableLabel
|
||||
onChange={(newValue) => handleUserDataChange(data.id, newValue)}
|
||||
/>
|
||||
<div
|
||||
className="remove-button"
|
||||
onClick={() => handleRemoveUserData(data.id)}
|
||||
>
|
||||
<RemoveIcon />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add new user data */}
|
||||
<div className="optimize-button" onClick={handleAddUserData}>
|
||||
+ Add
|
||||
</div>
|
||||
</section>
|
||||
<div style={{ display: "flex", flexDirection: "column", outline: "1px solid var(--border-color)" }}>
|
||||
{selectedFloorItem.uuid && <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>Animations</div>}
|
||||
{assets.map((asset) => (
|
||||
<div key={asset.modelUuid} className="asset-item">
|
||||
{asset.modelUuid === selectedFloorItem.uuid &&
|
||||
asset.animations &&
|
||||
asset.animations.length > 0 &&
|
||||
asset.animations.map((animation, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{ gap: "15px", cursor: "pointer", padding: "5px" }}
|
||||
>
|
||||
<div
|
||||
onClick={() => handleAnimationClick(animation)}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
style={{
|
||||
height: "20px",
|
||||
width: "100%",
|
||||
borderRadius: "5px",
|
||||
background:
|
||||
hoveredIndex === index
|
||||
? "#7b4cd3"
|
||||
: "transparent",
|
||||
}}
|
||||
>
|
||||
{animation.charAt(0).toUpperCase() +
|
||||
animation.slice(1).toLowerCase()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<section>
|
||||
<div className="header">User Data</div>
|
||||
{userData.map((data) => (
|
||||
<div className="input-container">
|
||||
<InputWithDropDown
|
||||
key={data.id}
|
||||
label={data.label}
|
||||
value={data.value}
|
||||
editableLabel
|
||||
onChange={(newValue) => handleUserDataChange(data.id, newValue)}
|
||||
/>
|
||||
<div
|
||||
className="remove-button"
|
||||
onClick={() => handleRemoveUserData(data.id)}
|
||||
>
|
||||
<RemoveIcon />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add new user data */}
|
||||
<div className="optimize-button" onClick={handleAddUserData}>
|
||||
+ Add
|
||||
</div>
|
||||
);
|
||||
</section>
|
||||
<div className="header">Animations</div>
|
||||
<section className="animations-lists">
|
||||
{assets.map((asset) => (
|
||||
<>
|
||||
{asset.modelUuid === selectedFloorItem.uuid &&
|
||||
asset.animations &&
|
||||
asset.animations.length > 0 &&
|
||||
asset.animations.map((animation, index) => (
|
||||
<div key={index} className="animations-list-wrapper">
|
||||
<div
|
||||
onClick={() => handleAnimationClick(animation)}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
className="animations-list"
|
||||
style={{
|
||||
background:
|
||||
hoveredIndex === index
|
||||
? "#7b4cd3"
|
||||
: "var(--background-color)",
|
||||
}}
|
||||
>
|
||||
{animation.charAt(0).toUpperCase() +
|
||||
animation.slice(1).toLowerCase()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetProperties;
|
||||
|
||||
@@ -3,53 +3,51 @@ import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
|
||||
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
|
||||
|
||||
interface StorageActionProps {
|
||||
type: "store" | "spawn" | "default";
|
||||
value: string;
|
||||
maxCapacity: string;
|
||||
spawnedCount: string;
|
||||
min: number;
|
||||
max?: number;
|
||||
defaultValue: string;
|
||||
maxCapacityDefault: string;
|
||||
spawnedCountCefault: string;
|
||||
currentMaterialType: string;
|
||||
handleCapacityChange: (value: string) => void;
|
||||
handleSpawnCountChange: (value: string) => void;
|
||||
handleMaterialTypeChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const StorageAction: React.FC<StorageActionProps> = ({ type, value, min, max, defaultValue, currentMaterialType, handleCapacityChange, handleMaterialTypeChange }) => {
|
||||
const StorageAction: React.FC<StorageActionProps> = ({ maxCapacity, spawnedCount, min, max, maxCapacityDefault, spawnedCountCefault, currentMaterialType, handleCapacityChange, handleSpawnCountChange, handleMaterialTypeChange }) => {
|
||||
return (
|
||||
<>
|
||||
{type === 'store' &&
|
||||
<InputWithDropDown
|
||||
label="Storage Capacity"
|
||||
value={value}
|
||||
min={min}
|
||||
step={1}
|
||||
max={max}
|
||||
defaultValue={defaultValue}
|
||||
activeOption="unit"
|
||||
onClick={() => { }}
|
||||
onChange={handleCapacityChange}
|
||||
/>
|
||||
}
|
||||
{type === 'spawn' &&
|
||||
<>
|
||||
<InputWithDropDown
|
||||
label="Spawn Capacity"
|
||||
value={value}
|
||||
min={min}
|
||||
step={1}
|
||||
max={max}
|
||||
defaultValue={defaultValue}
|
||||
activeOption="unit"
|
||||
onClick={() => { }}
|
||||
onChange={handleCapacityChange}
|
||||
/>
|
||||
<LabledDropdown
|
||||
label={"Material Type"}
|
||||
defaultOption={currentMaterialType}
|
||||
options={["Default material", "Material 1", "Material 2", "Material 3"]}
|
||||
onSelect={handleMaterialTypeChange}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
<InputWithDropDown
|
||||
key={'Storage Capacity'}
|
||||
label="Storage Capacity"
|
||||
value={maxCapacity}
|
||||
min={min}
|
||||
step={1}
|
||||
max={max}
|
||||
defaultValue={maxCapacityDefault}
|
||||
activeOption="unit"
|
||||
onClick={() => { }}
|
||||
onChange={handleCapacityChange}
|
||||
/>
|
||||
<InputWithDropDown
|
||||
key={"Spawn Count"}
|
||||
label="Spawn Count"
|
||||
value={spawnedCount}
|
||||
min={min}
|
||||
step={1}
|
||||
max={max}
|
||||
defaultValue={spawnedCountCefault}
|
||||
activeOption="unit"
|
||||
onClick={() => { }}
|
||||
onChange={handleSpawnCountChange}
|
||||
/>
|
||||
<LabledDropdown
|
||||
label={"Material Type"}
|
||||
defaultOption={currentMaterialType}
|
||||
options={["Default material", "Material 1", "Material 2", "Material 3"]}
|
||||
onSelect={handleMaterialTypeChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -125,26 +125,6 @@ function ConveyorMechanics() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameAction = (newName: string) => {
|
||||
if (!selectedPointData) return;
|
||||
|
||||
setActionName(newName);
|
||||
const event = updateAction(
|
||||
selectedProduct.productUuid,
|
||||
selectedPointData.action.actionUuid,
|
||||
{ actionName: newName }
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpawnCountChange = (value: string) => {
|
||||
if (!selectedPointData) return;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ function CraneMechanics() {
|
||||
|
||||
const { selectedEventData } = useSelectedEventData();
|
||||
const { productStore } = useSceneContext();
|
||||
const { getPointByUuid, updateAction, addAction, removeAction } = productStore();
|
||||
const { getPointByUuid, addAction, removeAction } = productStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { selectedAction, setSelectedAction, clearSelectedAction } = useSelectedAction();
|
||||
@@ -62,36 +62,6 @@ function CraneMechanics() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleRenameAction = (newName: string) => {
|
||||
if (!selectedAction.actionId || !selectedPointData) return;
|
||||
|
||||
const event = updateAction(
|
||||
selectedProduct.productUuid,
|
||||
selectedAction.actionId,
|
||||
{ actionName: newName }
|
||||
);
|
||||
|
||||
const updatedActions = selectedPointData.actions.map(action =>
|
||||
action.actionUuid === selectedAction.actionId
|
||||
? { ...action, actionName: newName }
|
||||
: action
|
||||
);
|
||||
|
||||
setSelectedPointData({
|
||||
...selectedPointData,
|
||||
actions: updatedActions,
|
||||
});
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAction = () => {
|
||||
if (!selectedEventData || !selectedPointData) return;
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ function HumanMechanics() {
|
||||
if (point?.actions?.length) {
|
||||
setSelectedPointData(point);
|
||||
const firstAction = point.actions[0];
|
||||
setSelectedAction(firstAction.actionUuid, firstAction.actionName);
|
||||
setCurrentAction(firstAction);
|
||||
setSpeed((
|
||||
getEventByModelUuid(
|
||||
@@ -142,7 +143,6 @@ function HumanMechanics() {
|
||||
if (isNaN(numericValue)) return;
|
||||
|
||||
const updatedEvent = {
|
||||
...selectedEventData.data,
|
||||
speed: numericValue
|
||||
} as HumanEventSchema;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function RoboticArmMechanics() {
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEventData) {
|
||||
if (selectedEventData && selectedEventData.data.type === 'roboticArm') {
|
||||
const point = getPointByUuid(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid,
|
||||
@@ -71,36 +71,6 @@ function RoboticArmMechanics() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleRenameAction = (newName: string) => {
|
||||
if (!selectedAction.actionId || !selectedPointData) return;
|
||||
|
||||
const event = updateAction(
|
||||
selectedProduct.productUuid,
|
||||
selectedAction.actionId,
|
||||
{ actionName: newName }
|
||||
);
|
||||
|
||||
const updatedActions = selectedPointData.actions.map(action =>
|
||||
action.actionUuid === selectedAction.actionId
|
||||
? { ...action, actionName: newName }
|
||||
: action
|
||||
);
|
||||
|
||||
setSelectedPointData({
|
||||
...selectedPointData,
|
||||
actions: updatedActions,
|
||||
});
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpeedChange = (value: string) => {
|
||||
if (!selectedEventData) return;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MathUtils } from "three";
|
||||
import RenameInput from "../../../../../ui/inputs/RenameInput";
|
||||
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
|
||||
import Trigger from "../trigger/Trigger";
|
||||
@@ -6,58 +7,78 @@ import StorageAction from "../actions/StorageAction";
|
||||
import ActionsList from "../components/ActionsList";
|
||||
import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi";
|
||||
import { useSelectedAction, useSelectedEventData } from "../../../../../../store/simulation/useSimulationStore";
|
||||
import * as THREE from 'three';
|
||||
import { useProductContext } from "../../../../../../modules/simulation/products/productContext";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useVersionContext } from "../../../../../../modules/builder/version/versionContext";
|
||||
import { useSceneContext } from "../../../../../../modules/scene/sceneContext";
|
||||
|
||||
function StorageMechanics() {
|
||||
const [activeOption, setActiveOption] = useState<"default" | "store" | "spawn">("default");
|
||||
const [activeOption, setActiveOption] = useState<"store" | "spawn">("store");
|
||||
const [currentCapacity, setCurrentCapacity] = useState("1");
|
||||
const [spawnedCount, setSpawnedCount] = useState("0");
|
||||
const [spawnedMaterial, setSpawnedMaterial] = useState("Default material");
|
||||
const [selectedPointData, setSelectedPointData] = useState<StoragePointSchema | undefined>();
|
||||
const { selectedEventData } = useSelectedEventData();
|
||||
const { productStore } = useSceneContext();
|
||||
const { getPointByUuid, updateAction } = productStore();
|
||||
const { getPointByUuid, updateAction, updateEvent, getEventByModelUuid, getActionByUuid, addAction, removeAction } = productStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { setSelectedAction, clearSelectedAction } = useSelectedAction();
|
||||
const { selectedAction, setSelectedAction, clearSelectedAction } = useSelectedAction();
|
||||
const { selectedVersionStore } = useVersionContext();
|
||||
const { selectedVersion } = selectedVersionStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const updateSelectedPointData = () => {
|
||||
if (selectedEventData && selectedProduct) {
|
||||
const point = getPointByUuid(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData?.data.modelUuid,
|
||||
selectedEventData?.selectedPoint
|
||||
) as StoragePointSchema | undefined;
|
||||
if (point && "action" in point) {
|
||||
setSelectedPointData(point);
|
||||
const uiOption = point.action.actionType === "retrieve" ? "spawn" : point.action.actionType;
|
||||
setActiveOption(uiOption as "store" | "spawn");
|
||||
setSelectedAction(point.action.actionUuid, point.action.actionName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEventData) {
|
||||
if (selectedEventData && selectedEventData.data.type === "storageUnit") {
|
||||
const point = getPointByUuid(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData?.data.modelUuid,
|
||||
selectedEventData?.selectedPoint
|
||||
selectedEventData.data.modelUuid,
|
||||
selectedEventData.selectedPoint
|
||||
) as StoragePointSchema | undefined;
|
||||
if (point && "action" in point) {
|
||||
|
||||
if (point?.actions?.length) {
|
||||
setSelectedPointData(point);
|
||||
const uiOption = point.action.actionType === "retrieve" ? "spawn" : point.action.actionType;
|
||||
setActiveOption(uiOption as "store" | "spawn");
|
||||
setSelectedAction(point.action.actionUuid, point.action.actionName);
|
||||
const firstAction = point.actions[0];
|
||||
|
||||
const eventData = getEventByModelUuid(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid
|
||||
) as StorageEventSchema | undefined;
|
||||
|
||||
setCurrentCapacity(eventData?.storageCapacity?.toString() || "1");
|
||||
setSpawnedCount(eventData?.storageCount?.toString() || "0");
|
||||
setSpawnedMaterial(eventData?.materialType?.toString() || "Default material");
|
||||
setSelectedAction(firstAction.actionUuid, firstAction.actionName);
|
||||
setActiveOption(firstAction.actionType === "retrieve" ? "spawn" : "store");
|
||||
}
|
||||
} else {
|
||||
clearSelectedAction();
|
||||
}
|
||||
}, [selectedProduct, selectedEventData]);
|
||||
}, [selectedEventData, selectedProduct]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEventData && selectedEventData.data.type === "storageUnit" && selectedAction.actionId) {
|
||||
const point = getPointByUuid(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid,
|
||||
selectedEventData.selectedPoint
|
||||
) as StoragePointSchema | undefined;
|
||||
|
||||
const actionUuid = selectedAction.actionId || point?.actions[0].actionUuid || '';
|
||||
|
||||
const newCurrentAction = getActionByUuid(selectedProduct.productUuid, actionUuid);
|
||||
|
||||
if (newCurrentAction && (newCurrentAction.actionType === 'store' || newCurrentAction.actionType === 'retrieve')) {
|
||||
if (!selectedAction.actionId) {
|
||||
setSelectedAction(newCurrentAction.actionUuid, newCurrentAction.actionName);
|
||||
}
|
||||
const uiOption = newCurrentAction.actionType === "retrieve" ? "spawn" : "store";
|
||||
setActiveOption(uiOption);
|
||||
} else {
|
||||
clearSelectedAction();
|
||||
}
|
||||
}
|
||||
}, [selectedAction, selectedProduct, selectedEventData]);
|
||||
|
||||
const updateBackend = (
|
||||
productName: string,
|
||||
@@ -75,48 +96,63 @@ function StorageMechanics() {
|
||||
}
|
||||
|
||||
const handleActionTypeChange = (option: string) => {
|
||||
if (!selectedEventData || !selectedPointData) return;
|
||||
const internalOption = actionTypeMap[option as keyof typeof actionTypeMap] as "store" | "retrieve";
|
||||
if (!selectedAction.actionId || !selectedPointData) return;
|
||||
|
||||
const internalOption = option === "spawn" ? "retrieve" : "store";
|
||||
|
||||
const updatedAction = {
|
||||
actionType: internalOption as "store" | "retrieve"
|
||||
};
|
||||
|
||||
const updatedActions = selectedPointData.actions.map(action =>
|
||||
action.actionUuid === selectedAction.actionId ? {
|
||||
...action,
|
||||
actionType: updatedAction.actionType
|
||||
} : action
|
||||
);
|
||||
|
||||
const updatedPoint = { ...selectedPointData, actions: updatedActions };
|
||||
|
||||
const event = updateAction(
|
||||
selectedProduct.productUuid,
|
||||
selectedAction.actionId,
|
||||
updatedAction
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedPointData(updatedPoint);
|
||||
setActiveOption(option as "store" | "spawn");
|
||||
|
||||
const event = updateAction(selectedProduct.productUuid, selectedPointData.action.actionUuid, {
|
||||
actionType: internalOption,
|
||||
});
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
updateSelectedPointData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameAction = (newName: string) => {
|
||||
if (!selectedPointData) return;
|
||||
const event = updateAction(selectedProduct.productUuid, selectedPointData.action.actionUuid, { actionName: newName });
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
updateSelectedPointData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCapacityChange = (value: string) => {
|
||||
if (!selectedEventData || !selectedPointData) return;
|
||||
const newCapacity = parseInt(value);
|
||||
if (!selectedEventData) return;
|
||||
|
||||
const event = updateAction(selectedProduct.productUuid, selectedPointData.action.actionUuid, {
|
||||
storageCapacity: newCapacity,
|
||||
});
|
||||
const numericValue = parseInt(value);
|
||||
if (isNaN(numericValue)) return;
|
||||
|
||||
const updatedEvent = {
|
||||
storageCapacity: numericValue
|
||||
} as StorageEventSchema;
|
||||
|
||||
const currentCount = parseInt(spawnedCount);
|
||||
if (currentCount > numericValue) {
|
||||
updatedEvent.storageCount = numericValue;
|
||||
setSpawnedCount(numericValue.toString());
|
||||
}
|
||||
|
||||
const event = updateEvent(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid,
|
||||
updatedEvent
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
@@ -125,25 +161,54 @@ function StorageMechanics() {
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
updateSelectedPointData();
|
||||
}
|
||||
|
||||
setCurrentCapacity(value);
|
||||
};
|
||||
|
||||
const createNewMaterial = (materialType: string): { materialType: string; materialId: string } | null => {
|
||||
if (!selectedEventData || !selectedPointData) return null;
|
||||
const materialId = THREE.MathUtils.generateUUID();
|
||||
return {
|
||||
materialType,
|
||||
materialId
|
||||
};
|
||||
const handleSpawnCountChange = (value: string) => {
|
||||
if (!selectedEventData) return;
|
||||
|
||||
const numericValue = parseInt(value);
|
||||
if (isNaN(numericValue)) return;
|
||||
|
||||
const maxCapacity = parseInt(currentCapacity);
|
||||
if (numericValue > maxCapacity) return;
|
||||
|
||||
const updatedEvent = {
|
||||
storageCount: numericValue
|
||||
} as StorageEventSchema;
|
||||
|
||||
const event = updateEvent(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid,
|
||||
updatedEvent
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
setSpawnedCount(value);
|
||||
};
|
||||
|
||||
const handleMaterialTypeChange = (value: string) => {
|
||||
if (!selectedEventData || !selectedPointData) return;
|
||||
if (!selectedEventData) return;
|
||||
|
||||
const event = updateAction(selectedProduct.productUuid, selectedPointData.action.actionUuid, {
|
||||
materialType: value,
|
||||
});
|
||||
const updatedEvent = {
|
||||
materialType: value
|
||||
} as StorageEventSchema;
|
||||
|
||||
const event = updateEvent(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid,
|
||||
updatedEvent
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
@@ -152,70 +217,114 @@ function StorageMechanics() {
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
updateSelectedPointData();
|
||||
}
|
||||
|
||||
setSpawnedMaterial(value);
|
||||
};
|
||||
|
||||
const currentActionName = useMemo(() =>
|
||||
selectedPointData ? selectedPointData.action.actionName : "Action Name",
|
||||
[selectedPointData]
|
||||
);
|
||||
const handleAddAction = () => {
|
||||
if (!selectedEventData || !selectedPointData) return;
|
||||
|
||||
const currentCapacity = useMemo(() =>
|
||||
selectedPointData ? selectedPointData.action.storageCapacity.toString() : "0",
|
||||
[selectedPointData]
|
||||
);
|
||||
const newAction: StorageAction = {
|
||||
actionUuid: MathUtils.generateUUID(),
|
||||
actionName: `Action ${selectedPointData.actions.length + 1}`,
|
||||
actionType: "store",
|
||||
triggers: [],
|
||||
};
|
||||
|
||||
const currentMaterialType = useMemo(() =>
|
||||
selectedPointData?.action.materialType || "Default material",
|
||||
[selectedPointData]
|
||||
);
|
||||
const updatedActions = [...(selectedPointData.actions || []), newAction];
|
||||
const updatedPoint = { ...selectedPointData, actions: updatedActions };
|
||||
|
||||
const availableActions = {
|
||||
defaultOption: "store",
|
||||
options: ["store", "spawn"],
|
||||
const event = addAction(
|
||||
selectedProduct.productUuid,
|
||||
selectedEventData.data.modelUuid,
|
||||
selectedEventData.selectedPoint,
|
||||
newAction
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
|
||||
}
|
||||
|
||||
setSelectedPointData(updatedPoint);
|
||||
setSelectedAction(newAction.actionUuid, newAction.actionName);
|
||||
};
|
||||
|
||||
const actionTypeMap = {
|
||||
spawn: "retrieve",
|
||||
store: "store"
|
||||
const handleDeleteAction = (actionUuid: string) => {
|
||||
if (!selectedPointData || !actionUuid) return;
|
||||
|
||||
const updatedActions = selectedPointData.actions.filter(action => action.actionUuid !== actionUuid);
|
||||
const updatedPoint = { ...selectedPointData, actions: updatedActions };
|
||||
|
||||
const event = removeAction(
|
||||
selectedProduct.productUuid,
|
||||
actionUuid
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event);
|
||||
}
|
||||
|
||||
setSelectedPointData(updatedPoint);
|
||||
|
||||
const index = selectedPointData.actions.findIndex((a) => a.actionUuid === selectedAction.actionId);
|
||||
const nextAction = updatedPoint.actions[index] || updatedPoint.actions[index - 1];
|
||||
if (nextAction) {
|
||||
setSelectedAction(nextAction.actionUuid, nextAction.actionName);
|
||||
} else {
|
||||
clearSelectedAction();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedEventData && (
|
||||
<section>
|
||||
<ActionsList
|
||||
selectedPointData={selectedPointData}
|
||||
/>
|
||||
<div className="selected-actions-details">
|
||||
<div className="selected-actions-header">
|
||||
<RenameInput
|
||||
value={currentActionName}
|
||||
canEdit={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="selected-actions-list">
|
||||
<LabledDropdown
|
||||
defaultOption={activeOption}
|
||||
options={availableActions.options}
|
||||
onSelect={handleActionTypeChange}
|
||||
/>
|
||||
<StorageAction
|
||||
type={activeOption}
|
||||
value={currentCapacity}
|
||||
defaultValue="0"
|
||||
min={0}
|
||||
currentMaterialType={currentMaterialType}
|
||||
handleCapacityChange={handleCapacityChange}
|
||||
handleMaterialTypeChange={handleMaterialTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tirgger">
|
||||
<Trigger selectedPointData={selectedPointData as any} type={'StorageUnit'} />
|
||||
</div>
|
||||
</section>
|
||||
{selectedEventData && selectedEventData.data.type === "storageUnit" && (
|
||||
<>
|
||||
<section>
|
||||
<StorageAction
|
||||
maxCapacity={currentCapacity}
|
||||
spawnedCount={spawnedCount}
|
||||
maxCapacityDefault="0"
|
||||
spawnedCountCefault="0"
|
||||
min={0}
|
||||
currentMaterialType={spawnedMaterial}
|
||||
handleCapacityChange={handleCapacityChange}
|
||||
handleSpawnCountChange={handleSpawnCountChange}
|
||||
handleMaterialTypeChange={handleMaterialTypeChange}
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<ActionsList
|
||||
selectedPointData={selectedPointData}
|
||||
multipleAction={true}
|
||||
handleAddAction={handleAddAction}
|
||||
handleDeleteAction={handleDeleteAction}
|
||||
/>
|
||||
|
||||
{selectedAction.actionId && (
|
||||
<div className="selected-actions-details">
|
||||
<div className="selected-actions-header">
|
||||
<RenameInput
|
||||
value={selectedAction.actionName || ""}
|
||||
canEdit={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="selected-actions-list">
|
||||
<LabledDropdown
|
||||
label="Action Type"
|
||||
defaultOption={activeOption}
|
||||
options={["store", "spawn"]}
|
||||
onSelect={handleActionTypeChange}
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="tirgger">
|
||||
<Trigger selectedPointData={selectedPointData as any} type={'StorageUnit'} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -119,26 +119,6 @@ function VehicleMechanics() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameAction = (newName: string) => {
|
||||
if (!selectedPointData) return;
|
||||
|
||||
setActionName(newName);
|
||||
const event = updateAction(
|
||||
selectedProduct.productUuid,
|
||||
selectedPointData.action.actionUuid,
|
||||
{ actionName: newName }
|
||||
);
|
||||
|
||||
if (event) {
|
||||
updateBackend(
|
||||
selectedProduct.productName,
|
||||
selectedProduct.productUuid,
|
||||
projectId || '',
|
||||
event
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadCapacityChange = (value: string) => {
|
||||
if (!selectedPointData) return;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useSceneContext } from "../../../../../../modules/scene/sceneContext";
|
||||
|
||||
type TriggerProps = {
|
||||
selectedPointData?: PointsScheme | undefined;
|
||||
type?: "Conveyor" | "Vehicle" | "RoboticArm" | "Machine" | "StorageUnit" | "Human";
|
||||
type?: "Conveyor" | "Vehicle" | "RoboticArm" | "Machine" | "StorageUnit" | "Human" | "Crane";
|
||||
};
|
||||
|
||||
const Trigger = ({ selectedPointData, type }: TriggerProps) => {
|
||||
@@ -36,9 +36,9 @@ const Trigger = ({ selectedPointData, type }: TriggerProps) => {
|
||||
|
||||
let actionUuid: string | undefined;
|
||||
|
||||
if (type === "Conveyor" || type === "Vehicle" || type === "Machine" || type === "StorageUnit") {
|
||||
actionUuid = (selectedPointData as | ConveyorPointSchema | VehiclePointSchema | MachinePointSchema | StoragePointSchema).action?.actionUuid;
|
||||
} else if ((type === "RoboticArm" || type === "Human") && selectedAction.actionId) {
|
||||
if (type === "Conveyor" || type === "Vehicle" || type === "Machine") {
|
||||
actionUuid = (selectedPointData as | ConveyorPointSchema | VehiclePointSchema | MachinePointSchema).action?.actionUuid;
|
||||
} else if ((type === "RoboticArm" || type === "Human" || type === "StorageUnit" || type === 'Crane') && selectedAction.actionId) {
|
||||
actionUuid = selectedAction.actionId;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { ClockThreeIcon, LocationPinIcon, TargetIcon } from '../../../../icons/ExportCommonIcons'
|
||||
import NavigateCatagory from '../NavigateCatagory'
|
||||
// import NavigateCatagory from '../NavigateCatagory'
|
||||
|
||||
const Hrm = () => {
|
||||
const [selectedCard, setSelectedCard] = useState(0);
|
||||
@@ -105,6 +105,7 @@ const Hrm = () => {
|
||||
<div
|
||||
className={`analysis-wrapper ${selectedCard === index ? "active" : ""}`}
|
||||
onClick={() => setSelectedCard(index)}
|
||||
key={index}
|
||||
>
|
||||
<header>
|
||||
<div className="user-details">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react'
|
||||
import NavigateCatagory from '../../NavigateCatagory'
|
||||
import { useState } from 'react'
|
||||
// import NavigateCatagory from '../../NavigateCatagory'
|
||||
import { EyeIcon, ForkLiftIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons';
|
||||
import assetImage from "../../../../../../assets/image/asset-image.png"
|
||||
const AssetManagement = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState("All Assets");
|
||||
// const [selectedCategory, setSelectedCategory] = useState("All Assets");
|
||||
const [expandedAssetId, setExpandedAssetId] = useState<string | null>(null);
|
||||
|
||||
const dummyAssets = [
|
||||
@@ -56,7 +56,7 @@ const AssetManagement = () => {
|
||||
|
||||
<div className='assetManagement-container assetManagement-wrapper'>
|
||||
{dummyAssets.map((asset, index) => (
|
||||
<div className={`assetManagement-card-wrapper ${expandedAssetId === asset.id ? "openViewMore" : ""}`}>
|
||||
<div className={`assetManagement-card-wrapper ${expandedAssetId === asset.id ? "openViewMore" : ""}`} key={index}>
|
||||
<header>
|
||||
<div className="header-wrapper">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user