808 lines
30 KiB
TypeScript
808 lines
30 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Box,
|
|
Button,
|
|
CloseButton,
|
|
Flex,
|
|
Grid,
|
|
ScrollArea,
|
|
Tabs,
|
|
Text,
|
|
Card,
|
|
Badge,
|
|
} from "@mantine/core";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import classes from "./Component.module.css";
|
|
import type { IScenario, TLine, TStation, TUser } from "../untils/types";
|
|
import type { Socket } from "socket.io-client";
|
|
import { ButtonDPELP, ButtonSelect } from "./ButtonAction";
|
|
import DrawerLogs from "./Drawer/DrawerLogs";
|
|
import { DrawerAPCControl, DrawerSwitchControl } from "./Drawer/DrawerControl";
|
|
import DrawerScenario from "./Modal/ModalScenario";
|
|
import { isJsonString } from "../untils/helper";
|
|
import { motion } from "motion/react";
|
|
import {
|
|
IconCaretDown,
|
|
IconCaretUp,
|
|
IconPlayerPlay,
|
|
IconPlus,
|
|
} from "@tabler/icons-react";
|
|
import InputHistory from "./InputHistory";
|
|
|
|
interface TabsProps {
|
|
selectedLines: TLine[];
|
|
socket: Socket | null;
|
|
setSelectedLines: (value: React.SetStateAction<TLine[]>) => void;
|
|
isDisable: boolean;
|
|
station: TStation;
|
|
stationId: number;
|
|
setIsDisable: (value: React.SetStateAction<boolean>) => void;
|
|
testLogContent: string;
|
|
isLogModalOpen: boolean;
|
|
setIsLogModalOpen: (value: React.SetStateAction<boolean>) => void;
|
|
setTestLogContent: (value: React.SetStateAction<string>) => void;
|
|
scenarios: IScenario[];
|
|
setScenarios: (value: React.SetStateAction<IScenario[]>) => void;
|
|
setExpanded: (value: React.SetStateAction<boolean>) => void;
|
|
activeTabBottom: string;
|
|
setActiveTabBottom: (value: React.SetStateAction<string>) => void;
|
|
isExpand: boolean;
|
|
}
|
|
|
|
// Component cho từng Scenario Card
|
|
const ScenarioCard = ({
|
|
scenario,
|
|
index,
|
|
isDisable,
|
|
selectedLines,
|
|
user,
|
|
socket,
|
|
setOpenScenarioModal,
|
|
setIsDisable,
|
|
}: {
|
|
scenario: IScenario;
|
|
index: number;
|
|
isDisable: boolean;
|
|
selectedLines: TLine[];
|
|
user: TUser;
|
|
socket: Socket | null;
|
|
setOpenScenarioModal: (value: boolean) => void;
|
|
setIsDisable: (value: boolean) => void;
|
|
}) => {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const [overlayPosition, setOverlayPosition] = useState({ top: 0, left: 0 });
|
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
const steps = JSON.parse(scenario.body || "[]");
|
|
|
|
const handleMouseEnter = () => {
|
|
if (cardRef.current) {
|
|
const rect = cardRef.current.getBoundingClientRect();
|
|
const overlayWidth = 400;
|
|
const viewportWidth = window.innerWidth;
|
|
const gap = 10; // Khoảng cách giữa card và overlay
|
|
|
|
// Đặt overlay bên phải card
|
|
let left = rect.right + gap;
|
|
|
|
// Nếu overlay tràn ra ngoài màn hình bên phải → đặt bên trái card
|
|
if (left + overlayWidth > viewportWidth) {
|
|
left = rect.left - overlayWidth - gap;
|
|
|
|
// Nếu vẫn tràn ra ngoài bên trái → đặt ở giữa màn hình
|
|
if (left < 10) {
|
|
left = (viewportWidth - overlayWidth) / 2;
|
|
}
|
|
}
|
|
|
|
setOverlayPosition({
|
|
top: rect.top,
|
|
left: left,
|
|
});
|
|
}
|
|
setIsHovered(true);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isHovered && cardRef.current) {
|
|
const updatePosition = () => {
|
|
if (cardRef.current) {
|
|
const rect = cardRef.current.getBoundingClientRect();
|
|
const overlayWidth = 400;
|
|
const viewportWidth = window.innerWidth;
|
|
const gap = 10; // Khoảng cách giữa card và overlay
|
|
|
|
// Đặt overlay bên phải card
|
|
let left = rect.right + gap;
|
|
|
|
// Nếu overlay tràn ra ngoài màn hình bên phải → đặt bên trái card
|
|
if (left + overlayWidth > viewportWidth) {
|
|
left = rect.left - overlayWidth - gap;
|
|
|
|
// Nếu vẫn tràn ra ngoài bên trái → đặt ở giữa màn hình
|
|
if (left < 10) {
|
|
left = (viewportWidth - overlayWidth) / 2;
|
|
}
|
|
}
|
|
|
|
setOverlayPosition({
|
|
top: rect.top,
|
|
left: left,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Update position immediately
|
|
updatePosition();
|
|
|
|
// Listen to scroll events (including inside modal)
|
|
const scrollContainers = document.querySelectorAll('[style*="overflow"]');
|
|
scrollContainers.forEach((container) => {
|
|
container.addEventListener("scroll", updatePosition, true);
|
|
});
|
|
|
|
window.addEventListener("scroll", updatePosition, true);
|
|
window.addEventListener("resize", updatePosition);
|
|
|
|
return () => {
|
|
scrollContainers.forEach((container) => {
|
|
container.removeEventListener("scroll", updatePosition, true);
|
|
});
|
|
window.removeEventListener("scroll", updatePosition, true);
|
|
window.removeEventListener("resize", updatePosition);
|
|
};
|
|
}
|
|
}, [isHovered]);
|
|
|
|
return (
|
|
<Grid.Col key={scenario.id} span={3}>
|
|
<div ref={cardRef} style={{ position: "relative" }}>
|
|
<Card
|
|
shadow="sm"
|
|
padding="md"
|
|
radius="md"
|
|
withBorder
|
|
style={{
|
|
transition: "all 0.2s ease",
|
|
height: "auto",
|
|
minHeight: "80px",
|
|
}}
|
|
className={classes.scenarioCard}
|
|
>
|
|
<Flex direction="column" gap="sm" align="center" justify="center">
|
|
<Text
|
|
fw={600}
|
|
size="lg"
|
|
ta="center"
|
|
style={{
|
|
cursor: "pointer",
|
|
}}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
{scenario.title}
|
|
</Text>
|
|
|
|
<Button
|
|
fullWidth
|
|
variant="light"
|
|
color="green"
|
|
size="sm"
|
|
leftSection={<IconPlayerPlay size={16} />}
|
|
disabled={
|
|
isDisable ||
|
|
selectedLines.filter(
|
|
(el) =>
|
|
!el?.userEmailOpenCLI ||
|
|
el?.userEmailOpenCLI === user?.email
|
|
).length === 0
|
|
}
|
|
onClick={() => {
|
|
setOpenScenarioModal(false);
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 5000);
|
|
|
|
selectedLines
|
|
.filter(
|
|
(el) =>
|
|
!el?.userEmailOpenCLI ||
|
|
el?.userEmailOpenCLI === user?.email
|
|
)
|
|
.forEach((el) => {
|
|
socket?.emit(
|
|
"run_scenario",
|
|
Object.assign(el, {
|
|
scenario: scenario,
|
|
})
|
|
);
|
|
});
|
|
}}
|
|
>
|
|
Run
|
|
</Button>
|
|
</Flex>
|
|
</Card>
|
|
|
|
{/* Hover overlay - Fixed position để không bị cắt bởi modal */}
|
|
{isHovered && (
|
|
<div
|
|
style={{
|
|
position: "fixed",
|
|
top: `${overlayPosition.top}px`,
|
|
left: `${overlayPosition.left}px`,
|
|
width: "400px",
|
|
maxWidth: "90vw",
|
|
background: "white",
|
|
border: "2px solid #4dabf7",
|
|
borderRadius: "8px",
|
|
padding: "16px",
|
|
boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
|
|
zIndex: 99999,
|
|
minHeight: "200px",
|
|
maxHeight: "400px",
|
|
overflowY: "auto",
|
|
overflowX: "hidden",
|
|
}}
|
|
className={classes.hideScrollBar}
|
|
onClick={(e) => e.stopPropagation()}
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
<Flex direction="column" gap="xs">
|
|
<Flex justify="space-between" align="center">
|
|
<Text fw={700} size="md">
|
|
{scenario.title}
|
|
</Text>
|
|
<Badge color="blue" variant="light">
|
|
#{index + 1}
|
|
</Badge>
|
|
</Flex>
|
|
|
|
<Flex gap="xs" wrap="wrap" mt="xs">
|
|
<Badge size="sm" color="gray" variant="dot">
|
|
Timeout: {scenario.timeout}ms
|
|
</Badge>
|
|
{scenario.isReboot && (
|
|
<Badge size="sm" color="orange" variant="dot">
|
|
Reboot
|
|
</Badge>
|
|
)}
|
|
<Badge size="sm" color="green" variant="dot">
|
|
{steps.length} steps
|
|
</Badge>
|
|
</Flex>
|
|
|
|
<Text size="xs" fw={600} mt="xs" c="dimmed">
|
|
Commands Preview:
|
|
</Text>
|
|
<Box
|
|
style={{
|
|
background: "#f8f9fa",
|
|
padding: "8px",
|
|
borderRadius: "4px",
|
|
maxHeight: "100px",
|
|
overflow: "auto",
|
|
}}
|
|
>
|
|
{steps.slice(0, 5).map((step: { send: string }, i: number) => (
|
|
<Text
|
|
key={i}
|
|
size="xs"
|
|
c="dimmed"
|
|
style={{
|
|
fontFamily: "monospace",
|
|
whiteSpace: "nowrap",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
}}
|
|
>
|
|
{i + 1}. {step.send || "(empty)"}
|
|
</Text>
|
|
))}
|
|
{steps.length > 5 && (
|
|
<Text size="xs" c="dimmed" ta="center" mt="xs">
|
|
... and {steps.length - 5} more
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
</Flex>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Grid.Col>
|
|
);
|
|
};
|
|
|
|
const BottomToolBar = ({
|
|
selectedLines,
|
|
socket,
|
|
setSelectedLines,
|
|
isDisable,
|
|
station,
|
|
setIsDisable,
|
|
testLogContent,
|
|
isLogModalOpen,
|
|
setIsLogModalOpen,
|
|
setTestLogContent,
|
|
scenarios,
|
|
setScenarios,
|
|
setExpanded,
|
|
setActiveTabBottom,
|
|
activeTabBottom,
|
|
isExpand,
|
|
stationId,
|
|
}: TabsProps) => {
|
|
const user = useMemo(() => {
|
|
return localStorage.getItem("user") &&
|
|
isJsonString(localStorage.getItem("user"))
|
|
? JSON.parse(localStorage.getItem("user") || "")
|
|
: null;
|
|
}, []);
|
|
const [openScenarioModal, setOpenScenarioModal] = useState<boolean>(false);
|
|
const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false);
|
|
// const [activeTabBottom, setActiveTabBottom] = useState<string>("command");
|
|
// const [isExpand, setIsExpand] = useState<boolean>(true);
|
|
|
|
return (
|
|
<>
|
|
{/* Modal chọn Scenario - Custom Simple Modal */}
|
|
|
|
|
|
<motion.div
|
|
initial={false}
|
|
animate={{
|
|
height: isExpand ? "auto" : "auto",
|
|
y: 0,
|
|
}}
|
|
transition={{ type: "spring", stiffness: 180, damping: 20 }}
|
|
style={{
|
|
width: "100%",
|
|
position: "fixed",
|
|
bottom: 0,
|
|
left: 0,
|
|
zIndex: 1,
|
|
}}
|
|
>
|
|
<Box style={{ position: "relative" }}>
|
|
<ActionIcon
|
|
style={{
|
|
position: "absolute",
|
|
top: isExpand ? -4 : -24,
|
|
left: "50%",
|
|
translate: "-19px 0",
|
|
backgroundColor: "#e3e0e0",
|
|
width: "55px",
|
|
zIndex: 10,
|
|
}}
|
|
variant="light"
|
|
onClick={() => {
|
|
setExpanded((prev) => !prev);
|
|
}}
|
|
>
|
|
{isExpand ? (
|
|
<IconCaretDown color="green" />
|
|
) : (
|
|
<IconCaretUp color="green" />
|
|
)}
|
|
</ActionIcon>
|
|
<Grid>
|
|
<Grid.Col span={isExpand ? 1 : 3.5}></Grid.Col>
|
|
<Grid.Col span={isExpand ? 10 : 5}>
|
|
{isExpand ? (
|
|
<Tabs
|
|
defaultValue="command"
|
|
orientation="vertical"
|
|
value={activeTabBottom}
|
|
onChange={(val) => {
|
|
setActiveTabBottom(val || "command");
|
|
}}
|
|
className={classes.containerBottom}
|
|
>
|
|
<Tabs.List>
|
|
<Tabs.Tab
|
|
style={{
|
|
backgroundColor:
|
|
activeTabBottom === "command" ? "#c8d9fd" : "",
|
|
fontSize: "13px",
|
|
paddingTop: "8px",
|
|
paddingBottom: "8px",
|
|
}}
|
|
value="command"
|
|
>
|
|
Command Line
|
|
</Tabs.Tab>
|
|
<Tabs.Tab
|
|
style={{
|
|
backgroundColor:
|
|
activeTabBottom === "apc" ? "#c8d9fd" : "",
|
|
fontSize: "13px",
|
|
paddingTop: "8px",
|
|
paddingBottom: "8px",
|
|
}}
|
|
value="apc"
|
|
>
|
|
APC
|
|
</Tabs.Tab>
|
|
<Tabs.Tab
|
|
style={{
|
|
backgroundColor:
|
|
activeTabBottom === "switch" ? "#c8d9fd" : "",
|
|
fontSize: "13px",
|
|
paddingTop: "8px",
|
|
paddingBottom: "8px",
|
|
}}
|
|
value="switch"
|
|
>
|
|
Switch
|
|
</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Panel value="command" p={4}>
|
|
<Flex justify={"space-between"} align="flex-start">
|
|
<Box>
|
|
<ScrollArea h={"8vh"}>
|
|
<Flex wrap={"wrap"} gap={"8px"} w={"420px"}>
|
|
{selectedLines.map((el) => (
|
|
<Box
|
|
key={el.id}
|
|
style={{
|
|
position: "relative",
|
|
padding: "4px 6px",
|
|
height: "26px",
|
|
width: "60px",
|
|
backgroundColor: "#d4e3ff",
|
|
borderRadius: "8px",
|
|
}}
|
|
>
|
|
{/* Close button góc trên phải */}
|
|
<CloseButton
|
|
size="xs"
|
|
style={{
|
|
position: "absolute",
|
|
top: "-4px",
|
|
right: "-6px",
|
|
minWidth: "18px",
|
|
width: "18px",
|
|
height: "18px",
|
|
zIndex: 10,
|
|
}}
|
|
onClick={() => {
|
|
setSelectedLines(
|
|
selectedLines.filter(
|
|
(line) => line.id !== el.id
|
|
)
|
|
);
|
|
socket?.emit("close_cli", {
|
|
lineId: el?.id,
|
|
stationId: el.stationId || el.station_id,
|
|
});
|
|
}}
|
|
/>
|
|
|
|
<Flex
|
|
align={"center"}
|
|
justify={"center"}
|
|
h="100%"
|
|
>
|
|
<Text fz={"11px"}>Line {el.lineNumber}</Text>
|
|
</Flex>
|
|
</Box>
|
|
))}
|
|
|
|
{selectedLines.length > 0 && (
|
|
<Box
|
|
style={{
|
|
padding: "4px 10px",
|
|
height: "26px",
|
|
backgroundColor: "#ffe3e3",
|
|
borderRadius: "999px",
|
|
cursor: "pointer",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
}}
|
|
onClick={() => {
|
|
selectedLines.forEach((line) => {
|
|
socket?.emit("close_cli", {
|
|
lineId: line?.id,
|
|
stationId:
|
|
line.stationId || line.station_id,
|
|
});
|
|
});
|
|
setSelectedLines([]);
|
|
}}
|
|
>
|
|
<Text fz={"11px"} c="red">
|
|
Clear
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Flex>
|
|
</ScrollArea>
|
|
<Flex justify={"space-between"} align={"center"} mt={4}>
|
|
<Text fz={"11px"} c="dimmed">
|
|
Selected: {selectedLines.length} /{" "}
|
|
{station.lines.length}
|
|
</Text>
|
|
<ButtonSelect
|
|
selectedLines={selectedLines}
|
|
setSelectedLines={setSelectedLines}
|
|
station={station}
|
|
userName={user?.userName}
|
|
onClick={() => {
|
|
const lines = station.lines.filter(
|
|
(line) =>
|
|
!line?.userOpenCLI ||
|
|
line?.userOpenCLI === user?.userName
|
|
);
|
|
if (selectedLines.length !== lines.length) {
|
|
setSelectedLines(lines);
|
|
lines.forEach((line) => {
|
|
socket?.emit("open_cli", {
|
|
lineId: line.id,
|
|
stationId: line.stationId || line.station_id,
|
|
userEmail: user?.email,
|
|
userName: user?.userName,
|
|
});
|
|
});
|
|
} else {
|
|
selectedLines.forEach((line) => {
|
|
socket?.emit("close_cli", {
|
|
lineId: line?.id,
|
|
stationId: line.stationId || line.station_id,
|
|
});
|
|
});
|
|
setSelectedLines([]);
|
|
}
|
|
}}
|
|
/>
|
|
</Flex>
|
|
</Box>
|
|
<Box pl={"md"} pr={"md"}>
|
|
<Flex justify={"space-between"} mb={"xs"}>
|
|
<Flex></Flex>
|
|
<Button
|
|
fw={400}
|
|
disabled={isDisable || selectedLines.length === 0}
|
|
variant="filled"
|
|
color="orange"
|
|
size="xs"
|
|
radius="md"
|
|
onClick={() => {
|
|
const listLine = selectedLines.length
|
|
? selectedLines
|
|
: station?.lines;
|
|
if (listLine.length) {
|
|
socket?.emit("write_command_line_from_web", {
|
|
lineIds: listLine.map((line) => line.id),
|
|
stationId: station.id,
|
|
command: "spam_break",
|
|
});
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 5000);
|
|
}
|
|
}}
|
|
>
|
|
Send Break
|
|
</Button>
|
|
</Flex>
|
|
<Box>
|
|
<InputHistory
|
|
selectedLines={selectedLines}
|
|
socket={socket}
|
|
station={station}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
<Box style={{ width: "260px" }}>
|
|
<Flex
|
|
align={"center"}
|
|
justify={"flex-end"}
|
|
gap={"xs"}
|
|
wrap={"wrap"}
|
|
>
|
|
<ButtonDPELP
|
|
socket={socket}
|
|
selectedLines={selectedLines}
|
|
isDisable={isDisable || selectedLines.length === 0}
|
|
onClick={() => {
|
|
if (
|
|
selectedLines.length > 0
|
|
// &&
|
|
// selectedLines.length === station?.lines?.length
|
|
) {
|
|
socket?.emit("run_all_dpelp", {
|
|
lineIds: selectedLines.map((line) => line.id),
|
|
stationName: station.name,
|
|
stationId: station.id,
|
|
});
|
|
}
|
|
setIsDisable(true);
|
|
setTimeout(() => {
|
|
setIsDisable(false);
|
|
}, 5000);
|
|
}}
|
|
/>
|
|
<Button
|
|
fw={400}
|
|
disabled={isDisable || selectedLines.length === 0}
|
|
variant="filled"
|
|
color="yellow"
|
|
style={{ height: "30px", width: "100px" }}
|
|
onClick={() => setOpenScenarioModal(true)}
|
|
>
|
|
Scenario
|
|
</Button>
|
|
<DrawerLogs
|
|
socket={socket}
|
|
isLogModalOpen={isLogModalOpen}
|
|
setIsLogModalOpen={setIsLogModalOpen}
|
|
testLogContent={testLogContent}
|
|
setTestLogContent={setTestLogContent}
|
|
/>
|
|
</Flex>
|
|
</Box>
|
|
</Flex>
|
|
</Tabs.Panel>
|
|
<Tabs.Panel value="apc" ps={"xs"}>
|
|
<DrawerAPCControl
|
|
socket={socket}
|
|
stationAPI={station}
|
|
stationId={stationId}
|
|
/>
|
|
</Tabs.Panel>
|
|
<Tabs.Panel value="switch" ps={"xs"}>
|
|
<DrawerSwitchControl
|
|
socket={socket}
|
|
stationAPI={station}
|
|
stationId={stationId}
|
|
/>
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
) : (
|
|
<Box p={3}>
|
|
<Flex direction="column" gap="xs">
|
|
<Flex justify={"space-between"} align={"center"} wrap="wrap" gap="xs">
|
|
<Flex wrap={"wrap"} gap={"6px"} style={{ flex: 1, minWidth: 0 }}>
|
|
{selectedLines.map((el) => (
|
|
<Box
|
|
key={el.id}
|
|
style={{
|
|
position: "relative",
|
|
padding: "3px 5px",
|
|
height: "22px",
|
|
width: "55px",
|
|
backgroundColor: "#d4e3ff",
|
|
borderRadius: "6px",
|
|
}}
|
|
>
|
|
<CloseButton
|
|
size="xs"
|
|
style={{
|
|
position: "absolute",
|
|
top: "-4px",
|
|
right: "-6px",
|
|
minWidth: "16px",
|
|
width: "16px",
|
|
height: "16px",
|
|
zIndex: 10,
|
|
}}
|
|
onClick={() => {
|
|
setSelectedLines(
|
|
selectedLines.filter(
|
|
(line) => line.id !== el.id
|
|
)
|
|
);
|
|
socket?.emit("close_cli", {
|
|
lineId: el?.id,
|
|
stationId: el.stationId || el.station_id,
|
|
});
|
|
}}
|
|
/>
|
|
<Flex
|
|
align={"center"}
|
|
justify={"center"}
|
|
h="100%"
|
|
>
|
|
<Text fz={"10px"}>Line {el.lineNumber}</Text>
|
|
</Flex>
|
|
</Box>
|
|
))}
|
|
{selectedLines.length > 0 && (
|
|
<Box
|
|
style={{
|
|
padding: "3px 8px",
|
|
height: "22px",
|
|
backgroundColor: "#ffe3e3",
|
|
borderRadius: "999px",
|
|
cursor: "pointer",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
}}
|
|
onClick={() => {
|
|
selectedLines.forEach((line) => {
|
|
socket?.emit("close_cli", {
|
|
lineId: line?.id,
|
|
stationId:
|
|
line.stationId || line.station_id,
|
|
});
|
|
});
|
|
setSelectedLines([]);
|
|
}}
|
|
>
|
|
<Text fz={"10px"} c="red">
|
|
Clear
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Flex>
|
|
<Flex align={"center"} gap="xs">
|
|
<Text fz={"10px"} c="dark" fw={600}>
|
|
Selected: {selectedLines.length} /{" "}
|
|
{station.lines.length}
|
|
</Text>
|
|
<ButtonSelect
|
|
selectedLines={selectedLines}
|
|
setSelectedLines={setSelectedLines}
|
|
station={station}
|
|
userName={user?.userName}
|
|
onClick={() => {
|
|
const lines = station.lines.filter(
|
|
(line) =>
|
|
!line?.userOpenCLI ||
|
|
line?.userOpenCLI === user?.userName
|
|
);
|
|
if (selectedLines.length !== lines.length) {
|
|
setSelectedLines(lines);
|
|
lines.forEach((line) => {
|
|
socket?.emit("open_cli", {
|
|
lineId: line.id,
|
|
stationId: line.stationId || line.station_id,
|
|
userEmail: user?.email,
|
|
userName: user?.userName,
|
|
});
|
|
});
|
|
} else {
|
|
selectedLines.forEach((line) => {
|
|
socket?.emit("close_cli", {
|
|
lineId: line?.id,
|
|
stationId: line.stationId || line.station_id,
|
|
});
|
|
});
|
|
setSelectedLines([]);
|
|
}
|
|
}}
|
|
/>
|
|
</Flex>
|
|
</Flex>
|
|
<Box style={{ width: "100%" }}>
|
|
<InputHistory
|
|
selectedLines={selectedLines}
|
|
socket={socket}
|
|
station={station}
|
|
/>
|
|
</Box>
|
|
</Flex>
|
|
</Box>
|
|
)}
|
|
</Grid.Col>
|
|
<Grid.Col span={isExpand ? 1 : 3.5}></Grid.Col>
|
|
</Grid>
|
|
</Box>
|
|
</motion.div>
|
|
|
|
{/* Drawer Scenario để Add/Edit */}
|
|
<DrawerScenario
|
|
scenarios={scenarios}
|
|
setScenarios={setScenarios}
|
|
externalOpened={openDrawerScenario}
|
|
onExternalClose={() => setOpenDrawerScenario(false)}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default BottomToolBar;
|