Add ticket management to lines and improve UI

Backend changes add a tickets relationship to lines and preload tickets (ordered by updated_at) when fetching stations. The frontend now displays ticket information in CardLine, adds ticket history and management (create, update, close, issue) in ModalTerminal, and introduces new types for tickets. Various UI improvements include consistent button font weights, port name normalization, and enhanced log/tooltips. Obsolete takeover logic was removed from App and ModalTerminal.
This commit is contained in:
nguyentrungthat 2025-11-18 16:59:27 +07:00
parent cbc4a8c9b0
commit d908cf204c
12 changed files with 817 additions and 290 deletions

View File

@ -4,7 +4,11 @@ import Line from '#models/line'
export default class StationsController {
public async index({}: HttpContext) {
return await Station.query().preload('lines')
return await Station.query().preload('lines', (query) => {
query.preload('tickets', (ticketQuery) => {
ticketQuery.orderBy('updated_at', 'desc')
})
})
}
public async store({ request, response }: HttpContext) {

View File

@ -3,6 +3,7 @@ import { BaseModel, belongsTo, column, hasMany } from '@adonisjs/lucid/orm'
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations'
import Station from './station.js'
import Log from './log.js'
import Ticket from './ticket.js'
export default class Line extends BaseModel {
@column({ isPrimary: true })
@ -40,6 +41,11 @@ export default class Line extends BaseModel {
})
declare logs: HasMany<typeof Log>
@hasMany(() => Ticket, {
foreignKey: 'lineId',
})
declare tickets: HasMany<typeof Ticket>
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

View File

@ -9,7 +9,7 @@ import { CustomServer, CustomSocket } from '../app/ultils/types.js'
import Line from '#models/line'
import Station from '#models/station'
import APCController from '#services/apc_connection'
import { sleep } from '../app/ultils/helper.js'
import { appendLog, sleep } from '../app/ultils/helper.js'
import SwitchController from '#services/switch_connection'
import redis from '@adonisjs/redis/services/main'
@ -172,20 +172,13 @@ export class WebSocketIo {
const lineId = data.lineId
const baud = data.baud
const line = await Line.find(lineId)
if (line) {
Object.assign(line, { baud })
line?.save()
if (!line) {
console.log(`Line [${lineId}] not found!!!`)
return
}
await this.handleLineOperation(
io,
data.stationId,
[lineId],
async (value) => value.setBaud(baud),
{
baud,
timeout: 120000,
}
)
Object.assign(line, { baud })
line?.save()
await this.setBaudByLineNumber(data.stationId, line?.lineNumber, baud)
})
socket.on('open_cli', async (data) => {
@ -751,4 +744,53 @@ export class WebSocketIo {
this.intervalKeepConnect[`${ip}`] = interval
}
private async setBaudByLineNumber(stationId: number, lineNumber: number, baud: number) {
const station = await Station.find(stationId)
if (!station) {
console.log('[ERROR connect station] Not found!')
return
}
// Kết nối tới station qua Telnet / Socket
const client = new net.Socket()
return new Promise<void>((resolve, reject) => {
client.setTimeout(5000)
client.connect(station.port, station.ip, async () => {
console.log(`Connected to station ${station.name} (${station.ip})`)
// Gửi lệnh clear line
client.write(`conf t\r\n`)
await sleep(500)
client.write(`line ${lineNumber}\r\n`)
await sleep(500)
client.write(`speed ${baud.toString()}\r\n`)
await sleep(500)
client.write(`end`)
await sleep(500)
client.write(`\r\n`)
await sleep(500)
client.destroy()
resolve()
})
client.on('data', (data) => {
appendLog(data.toString(), 0, 0, lineNumber)
})
client.on('error', (err) => {
console.error(`Error clearing line ${lineNumber}:`, err)
resolve()
})
client.on('close', () => {
console.log(`Station connection closed (line ${lineNumber})`)
resolve()
})
client.on('timeout', () => {
console.log(`Station connection timeout (line ${lineNumber})`)
client.destroy()
resolve()
})
})
}
}

View File

@ -16,7 +16,6 @@ import {
LoadingOverlay,
} from "@mantine/core";
import type {
IDataTakeOver,
IScenario,
ReceivedFile,
ResponseData,
@ -76,10 +75,6 @@ function App() {
const [selectedLine, setSelectedLine] = useState<TLine | undefined>();
const [loadingTerminal, setLoadingTerminal] = useState(true);
const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]);
const [disableRequestTakeOver, setDisableRequestTakeOver] = useState(false);
const [countDownRequest, setCountDownRequest] = useState(0);
const [dataRequestTakeOver, setDataRequestTakeOver] =
useState<IDataTakeOver>();
const [testLogContent, setTestLogContent] = useState("");
const [isLogModalOpen, setIsLogModalOpen] = useState(false);
@ -230,25 +225,6 @@ function App() {
}, 100);
});
socket?.on("confirm_take_over", (data) => {
setDataRequestTakeOver(data);
if (data?.userEmail !== user?.email) {
setCountDownRequest(20);
const intervalCount = setInterval(() => {
setCountDownRequest((prev) => {
if (prev <= 1) {
setDataRequestTakeOver(undefined);
clearInterval(intervalCount);
return 0;
}
return prev - 1;
});
}, 1000);
}
if (!data?.userEmail) setCountDownRequest(0);
});
const receivedFiles: Record<string, ReceivedFile> = {};
socket?.on("response_content_log", (data: ResponseData) => {
if (!data.chunk) {
@ -315,7 +291,6 @@ function App() {
socket.off("user_connecting");
socket.off("user_open_cli");
socket.off("user_close_cli");
socket.off("confirm_take_over");
socket.off("response_content_log");
socket.off("data_textfsm");
};
@ -336,6 +311,8 @@ function App() {
return {
...lineItem,
...updates,
lineNumber: lineItem.lineNumber,
line_number: lineItem.line_number,
...(isNetOutput && {
netOutput:
(lineItem.netOutput || "") + (updates.netOutput || ""),
@ -581,12 +558,6 @@ function App() {
socket={socket}
stationItem={stations.find((el) => el.id === Number(activeTab))}
scenarios={scenarios}
dataRequestTakeOver={dataRequestTakeOver}
countDownRequest={countDownRequest}
setDisableRequestTakeOver={setDisableRequestTakeOver}
disableRequestTakeOver={disableRequestTakeOver}
setCountDownRequest={setCountDownRequest}
setDataRequestTakeOver={setDataRequestTakeOver}
/>
</Container>
);

View File

@ -144,6 +144,7 @@ const BottomToolBar = ({
<Flex justify={"space-between"} mb={"xs"}>
<Flex></Flex>
<Button
fw={400}
disabled={isDisable || selectedLines.length === 0}
variant="filled"
color="orange"
@ -265,6 +266,7 @@ const BottomToolBar = ({
<Menu shadow="md" position="top">
<Menu.Target>
<Button
fw={400}
disabled={isDisable || selectedLines.length === 0}
variant="filled"
color="yellow"

View File

@ -31,6 +31,7 @@ export const ButtonDPELP = ({
h={"28px"}
mr={"5px"}
variant="filled"
fw={400}
color="#00a164"
onClick={async () => {
onClick();
@ -148,6 +149,7 @@ export const ButtonScenario = ({
miw={"100px"}
style={{ minHeight: "28px", height: "auto", fontSize: fontSize }}
mr={"5px"}
fw={400}
variant="outline"
color="#00a164"
className={classes.buttonScenario}
@ -241,6 +243,7 @@ export const ButtonSelect = ({
return (
<Button
variant="filled"
fw={400}
style={{ height: "30px", width: "100px" }}
onClick={() => {
onClick();
@ -273,6 +276,7 @@ export const ButtonConnect = ({
selectedLines.filter((el) => el.status !== "connected").length === 0
}
variant="outline"
fw={400}
style={{ height: "30px", width: "100px" }}
onClick={() => {
const lines = selectedLines.filter((el) => el.status !== "connected");
@ -310,6 +314,7 @@ export const ButtonControlApc = ({
>
<Menu.Target>
<Button
fw={400}
color="green"
disabled={selectedLines.length === 0}
variant="outline"

View File

@ -256,7 +256,15 @@ const CardLine = ({
</div>
</Flex>
<Flex justify={"space-between"} w={"100%"}>
<Box></Box>
<Box>
{line?.tickets && line?.tickets?.length > 0 ? (
<Text fz={"13px"}>
<i>{line?.tickets[0].description ?? ""}</i>
</Text>
) : (
""
)}
</Box>
<div
style={{
fontSize: "11px",
@ -333,6 +341,7 @@ const CardLine = ({
>
<Menu.Target>
<Button
fw={400}
disabled={isDisabled}
variant="filled"
color="yellow"
@ -363,7 +372,7 @@ const CardLine = ({
}, 5000);
}}
scenario={el}
fontSize="9px"
fontSize="11px"
/>
))}
</Flex>

View File

@ -130,7 +130,8 @@
}
.buttonMenuTool {
font-size: 9px !important;
font-size: 10px !important;
font-weight: 400;
width: 70px !important;
min-width: 70px !important;
height: 30px !important;

View File

@ -228,6 +228,12 @@ export const DrawerAPCControl: React.FC<DrawerProps> = ({
}
};
useEffect(() => {
setTimeout(() => {
setIsSubmit(false);
}, 15000);
}, []);
return (
<Grid>
<Grid.Col span={6}>
@ -857,6 +863,24 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
});
};
const normalizePortName = (port: string): string => {
if (!port) return "";
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2"
const match = port.match(/^([A-Za-z]+)([\d/]+)$/);
if (!match) return port;
const type = match[1]; // Fa, Gi, Te, etc.
const numbers = match[2]; // "0/1" / "0/0/1" / "0/0/2"
// Get the last part after slash
const parts = numbers.split("/");
const last = parts[parts.length - 1];
return `${type}${last}`;
};
return loading ? (
<Box
style={{
@ -1176,7 +1200,7 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
}}
>
<Text fw={500} fz={"11px"}>
{port.name}
{normalizePortName(port.name)}
</Text>
</Box>
</Card>
@ -1229,7 +1253,7 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
}}
>
<Text fw={500} fz={"11px"}>
{port.name}
{normalizePortName(port.name)}
</Text>
</Box>
</Card>
@ -1297,7 +1321,7 @@ export const DrawerSwitchControl: React.FC<DrawerProps> = ({
}
/> */}
<Text fw={500} fz={"11px"}>
{port.name}
{normalizePortName(port.name)}
</Text>
</Box>
</Card>

View File

@ -7,10 +7,11 @@ import {
Table,
Text,
ScrollArea,
Tooltip,
} from "@mantine/core";
import { useEffect, useState } from "react";
import type { ISystemLog } from "../untils/types";
import { IconDownload, IconEye } from "@tabler/icons-react";
import { IconDownload, IconEye, IconInfoCircle } from "@tabler/icons-react";
import classes from "./Component.module.css";
import moment from "moment";
import type { Socket } from "socket.io-client";
@ -90,7 +91,29 @@ function DrawerLogs({
radius="md"
opened={opened}
onClose={close}
title={"List Logs"}
title={
<div>
<Tooltip
label={
<div>
Format:
<i style={{ marginLeft: "4px" }}>
YYYYMMDD-Station_{`{id}`}-Line_{`{number}`}_{`{port}`}
.log
</i>
</div>
}
position="right"
>
<Text
fw={700}
style={{ display: "flex", alignItems: "center", gap: "6px" }}
>
List Logs <IconInfoCircle color="#3bb7e9" fontSize={"12px"} />
</Text>
</Tooltip>
</div>
}
>
<Grid>
<Grid.Col span={12}>
@ -177,6 +200,7 @@ function DrawerLogs({
</Drawer>
<Button
fw={400}
style={{ height: "30px", width: "100px" }}
title="Add Scenario"
variant="outline"
@ -185,7 +209,7 @@ function DrawerLogs({
open();
}}
>
List logs
List logs{" "}
</Button>
</>
);

View File

@ -1,26 +1,36 @@
import {
Box,
Button,
Dialog,
CloseButton,
Flex,
Grid,
Group,
Input,
Menu,
Modal,
ScrollArea,
Text,
Tooltip,
} from "@mantine/core";
import type {
IDataTakeOver,
IScenario,
TDataTicket,
THistoryTicket,
TLine,
TStation,
} from "../untils/types";
import TerminalCLI from "./TerminalXTerm";
import type { Socket } from "socket.io-client";
import classes from "./Component.module.css";
import { useEffect, useMemo, useRef, useState } from "react";
import { IconCircleCheckFilled } from "@tabler/icons-react";
import { ButtonDPELP } from "./ButtonAction";
import { useEffect, useMemo, useState } from "react";
import {
IconCircleCheckFilled,
IconCircleDot,
IconInfoCircle,
} from "@tabler/icons-react";
import { ButtonDPELP, ButtonScenario } from "./ButtonAction";
import moment from "moment";
import axios from "axios";
import { notifications } from "@mantine/notifications";
const apiUrl = import.meta.env.VITE_BACKEND_URL;
const ModalTerminal = ({
opened,
@ -29,12 +39,6 @@ const ModalTerminal = ({
socket,
stationItem,
scenarios,
dataRequestTakeOver,
countDownRequest,
disableRequestTakeOver,
setDisableRequestTakeOver,
setCountDownRequest,
setDataRequestTakeOver,
selectedLines,
}: {
opened: boolean;
@ -43,15 +47,7 @@ const ModalTerminal = ({
socket: Socket | null;
stationItem: TStation | undefined;
scenarios: IScenario[];
dataRequestTakeOver: IDataTakeOver | undefined;
countDownRequest: number;
disableRequestTakeOver: boolean;
selectedLines: TLine[];
setDisableRequestTakeOver: (value: React.SetStateAction<boolean>) => void;
setCountDownRequest: (value: React.SetStateAction<number>) => void;
setDataRequestTakeOver: (
value: React.SetStateAction<IDataTakeOver | undefined>
) => void;
}) => {
const user = useMemo(() => {
return localStorage.getItem("user") &&
@ -59,38 +55,212 @@ const ModalTerminal = ({
? JSON.parse(localStorage.getItem("user") || "")
: null;
}, []);
const [inputTicket, setInputTicket] = useState<string>("");
const [isDisable, setIsDisable] = useState<boolean>(false);
const intervalTakeOverRef = useRef<NodeJS.Timeout | null>(null);
const [isDisableTicket, setIsDisableTicket] = useState<boolean>(false);
const [latestTicket, setLatestTicket] = useState<TDataTicket>({
description: "",
sn: "",
model: "",
station_id: 0,
history: "",
status: "open",
});
const [dataTicket, setDataTicket] = useState<TDataTicket>({
description: "",
sn: "",
model: "",
station_id: 0,
history: "",
status: "open",
});
useEffect(() => {
if (
typeof dataRequestTakeOver?.userName !== "undefined" &&
line?.userOpenCLI === user?.userName &&
dataRequestTakeOver?.userName !== user?.userName
) {
if (dataRequestTakeOver?.userName) {
intervalTakeOverRef.current = setInterval(() => {
socket?.emit("open_cli", {
lineId: line?.id,
stationId: line?.station_id,
userEmail: user?.userName,
userName: user?.userName,
});
socket?.emit("request_take_over", {
station_id: line?.station_id,
});
setDisableRequestTakeOver(false);
setCountDownRequest(0);
if (intervalTakeOverRef.current)
clearInterval(intervalTakeOverRef.current);
}, 20000);
}
} else {
if (intervalTakeOverRef.current)
clearInterval(intervalTakeOverRef.current);
if (opened && line?.tickets && line?.tickets?.length > 0) {
const data = line?.tickets[0];
setLatestTicket(data);
setDataTicket({ ...data, description: "" });
}
}, [dataRequestTakeOver?.userName]);
}, [opened, line?.tickets]);
const renderHistory = (data: TDataTicket) => {
const latest = JSON.parse(latestTicket?.history || "[]");
const list =
data?.history && data?.id
? JSON.parse(data?.history)
: latest?.length > 0
? [latest[0], latest[latest?.length - 1]]
: [];
return (
<div>
{list.reverse().map((item: THistoryTicket, index: number) => (
<div key={index}>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "4px",
}}
>
<Flex
style={{
gap: "4px",
color:
index === 0
? "#30d100"
: item?.status === "closed"
? "red"
: item?.status === "issue"
? "#fab005"
: "inherit",
}}
>
<Text style={{ fontSize: "14px", fontWeight: "bold" }} fw={600}>
@{item?.userName}:
</Text>
<Text
style={{
fontSize: "14px",
}}
c={
index === 0
? "#30d100"
: item?.status === "closed"
? "red"
: item?.status === "issue"
? "#fab005"
: "dimmed"
}
fw={400}
>
{item?.description}
</Text>
</Flex>
<Text
style={{
fontSize: "14px",
}}
c="dimmed"
fw={500}
>
{moment(Number(item?.time)).format("HH:m DD/M")}
</Text>
</div>
</div>
))}
</div>
);
};
const handleCreate = async () => {
if (!dataTicket?.description.trim()) {
notifications.show({
title: "Error",
message: "Description is required",
color: "red",
});
return;
}
const payload = {
id: dataTicket.id || 0,
description: dataTicket.description.trim(),
model: dataTicket.model.trim(),
sn: dataTicket.sn.trim(),
station_id: Number(dataTicket.station_id),
status: "open",
};
try {
const res = await axios.post(apiUrl + "api/ticket/create", payload);
if (res.status) {
// setDataTicket(res.data)
// notifications.show({
// title: 'Success',
// message: res.message,
// color: 'green',
// })
// socket?.emit(
// "create_ticket",
// payload,
// )
return;
}
} catch (error) {
console.log("Error create ticket", error);
notifications.show({
title: "Error",
message: "Failed to create ticket, please try again!",
color: "red",
});
}
};
const handleUpdate = async (status: string) => {
if (!dataTicket?.description.trim()) {
notifications.show({
title: "Error",
message: "Description is required",
color: "red",
});
return;
}
const payload = {
id: dataTicket.id || 0,
description: dataTicket.description.trim()
? dataTicket.description.trim()
: status === "closed"
? "Closed"
: "",
model: dataTicket.model.trim(),
sn: dataTicket.sn.trim(),
station_id: Number(dataTicket.station_id),
status: status,
};
try {
const res = await axios.put(
`${apiUrl + "api/ticket/create" + "/" + dataTicket.id}`,
payload
);
if (res.status) {
if (res?.data?.status !== "closed")
setDataTicket({ ...res.data, description: "" });
else
setDataTicket({
id: 0,
description: "",
model: latestTicket.model.trim(),
sn: latestTicket.sn.trim(),
station_id: latestTicket.station_id,
history: "",
status: "open",
});
// notifications.show({
// title: 'Success',
// message: res.message,
// color: 'green',
// })
// socket?.emit(
// SOCKET_EVENTS.RELOAD_TICKET.RELOAD_TICKET_FROM_WEB,
// payload,
// )
return;
}
} catch (error) {
console.log("Error update ticket", error);
notifications.show({
title: "Error",
message: "Failed to update ticket, please try again!",
color: "red",
});
}
};
return (
<Box>
@ -107,10 +277,10 @@ const ModalTerminal = ({
stationId: line?.station_id,
});
}}
size={"80%"}
size={"100%"}
style={{ position: "absolute", left: 0 }}
title={
<Flex align={"center"} justify={"space-between"} w={"100%"}>
<Flex>
<Box
style={{
display: "flex",
@ -118,21 +288,10 @@ const ModalTerminal = ({
width: "400px",
}}
>
<Text size="md" mr={10}>
Line number:{" "}
<strong>{line?.lineNumber || line?.line_number || ""}</strong>
</Text>
<Text size="md" mr={10}>
- <strong>{line?.port || ""}</strong>
</Text>
{line?.status === "connected" && (
<IconCircleCheckFilled color="green" />
)}
<div
style={{
alignItems: "center",
marginLeft: "16px",
fontSize: "12px",
fontSize: "13px",
color: "red",
display: "flex",
}}
@ -144,30 +303,142 @@ const ModalTerminal = ({
: "Terminal is used"}
</div>
</Box>
<Flex
align={"center"}
justify={"space-between"}
gap={"md"}
style={{
width: "400px",
}}
>
<div className={classes.info_line} style={{ fontSize: "14px" }}>
PID: {line?.inventory?.pid || ""}
</div>
<div className={classes.info_line} style={{ fontSize: "14px" }}>
SN: {line?.inventory?.sn || ""}
</div>
<div className={classes.info_line} style={{ fontSize: "14px" }}>
VID: {line?.inventory?.vid || ""}
</div>
</Flex>
<Box></Box>
</Flex>
}
>
<Grid>
<Grid.Col span={10} style={{ borderRight: "1px solid #ccc" }}>
<Grid.Col span={2}>
<Flex justify={"space-between"} direction={"column"} h={"100%"}>
<Box>
<Flex gap={"sm"} justify={"center"} align={"center"}>
<Text size="xl">
<strong>
Line {line?.lineNumber || line?.line_number || ""}
</strong>
</Text>
<Text size="xl">
- <strong>{line?.port || ""}</strong>
</Text>
{line?.status === "connected" && (
<IconCircleCheckFilled color="green" fontSize={"18px"} />
)}
</Flex>
<Flex mt="4px">
<Text size="md" w={"50px"}>
BAUD:
</Text>
<Text size="md">
<strong>{line?.baud || ""}</strong>
</Text>
</Flex>
<Flex mt="4px">
<Text size="md" w={"50px"}>
PID:
</Text>
<Text size="md">{line?.inventory?.pid || ""}</Text>
{line?.inventory?.vid ? (
<Text size="md" ml={"sm"}>
{line?.inventory?.vid}
</Text>
) : (
""
)}
</Flex>
<Flex mt="4px">
<Text size="md" w={"50px"}>
SN:
</Text>
<Text size="md">{line?.inventory?.sn || ""}</Text>
</Flex>
<Flex mt="4px">
<Text size="md" mr={"sm"} fw={"bold"} w={"50px"}>
IOS:
</Text>
<Text size="md">{""}</Text>
</Flex>
</Box>
<Flex>
<Text size="md" mr={"sm"} fw={"bold"}>
License:
</Text>
<Text size="md">{""}</Text>
</Flex>
<Flex>
<Text size="md" mr={"sm"} fw={"bold"}>
Sh env/module:
</Text>
<Text size="md">{""}</Text>
</Flex>
<Flex>
<Text size="md" mr={"sm"} fw={"bold"}>
Mem/Flash:
</Text>
<Text size="md">{""}</Text>
</Flex>
<Flex>
<Text size="md" mr={"sm"} fw={"bold"}>
Warning from test report:
</Text>
<Text size="md">{""}</Text>
</Flex>
<Box>
<Flex justify={"center"}>
<IconCircleDot color="green" />
<Text size="md" ml={"sm"}>
Internet Connected
</Text>
</Flex>
<Flex justify={"space-around"} mt={"4px"}>
<Button
fw={400}
variant="outline"
color="green"
size="xs"
onClick={() => {}}
>
ON
</Button>
<Button
fw={400}
variant="outline"
color="red"
size="xs"
onClick={() => {}}
>
OFF
</Button>
<Button
fw={400}
variant="outline"
color="orange"
size="xs"
onClick={() => {}}
>
Restart
</Button>
</Flex>
</Box>
<Flex justify={"center"}>
<Button
fw={400}
w={"120px"}
variant="outline"
color="green"
size="xs"
onClick={() => {}}
>
Clear line
</Button>
</Flex>
</Flex>
</Grid.Col>
<Grid.Col
span={7}
style={{
borderRight: "1px solid #ccc",
borderLeft: "1px solid #ccc",
}}
>
<TerminalCLI
cliOpened={opened}
socket={socket}
@ -183,166 +454,314 @@ const ModalTerminal = ({
}
line_status={line?.status || ""}
/>
</Grid.Col>
<Grid.Col span={2}>
<ScrollArea h={"60vh"} style={{ paddingBottom: "12px" }}>
<Flex w={"100%"} direction={"column"} wrap={"wrap"} gap={"6px"}>
<ButtonDPELP
socket={socket}
selectedLines={line ? [line] : []}
isDisable={isDisable}
onClick={() => {
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 10000);
}}
/>
{scenarios.map((scenario) => (
<Flex justify={"space-between"} mt={"md"} pt={"md"} pb={"md"}>
<ButtonDPELP
socket={socket}
selectedLines={line ? [line] : []}
isDisable={isDisable}
onClick={() => {
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 10000);
}}
/>
<Menu shadow="md" position="top">
<Menu.Target>
<Button
disabled={
isDisable ||
(typeof line?.userOpenCLI !== "undefined" &&
line?.userOpenCLI !== user?.userName)
}
className={classes.buttonScenario}
key={scenario.id}
miw={"100px"}
mb={"6px"}
style={{ minHeight: "24px" }}
mr={"5px"}
variant={"outline"}
onClick={async () => {
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 10000);
if (line)
socket?.emit(
"run_scenario",
Object.assign(line, {
scenario: scenario,
})
);
fw={400}
disabled={isDisable || selectedLines.length === 0}
variant="filled"
color="yellow"
style={{ height: "30px", width: "100px" }}
onClick={() => {}}
>
Scenario
</Button>
</Menu.Target>
<Menu.Dropdown>
<Box
px="xs"
py="sm"
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
}}
>
{scenario.title}
</Button>
))}
{scenarios.map((el, i) => (
<ButtonScenario
key={i}
socket={socket}
selectedLines={selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
)}
isDisable={
isDisable ||
selectedLines.filter(
(el) =>
!el?.userEmailOpenCLI ||
el?.userEmailOpenCLI === user?.email
).length === 0
}
onClick={() => {
// setSelectedLines([]);
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}}
scenario={el}
/>
))}
</Box>
</Menu.Dropdown>
</Menu>
<Button
fw={400}
variant="filled"
color="green"
size="xs"
onClick={() => {}}
>
Select license
</Button>
<Button
fw={400}
variant="filled"
color="green"
size="xs"
onClick={() => {}}
>
Select IOS
</Button>
<Button
fw={400}
disabled={isDisable}
variant="filled"
color="orange"
size="xs"
radius="md"
onClick={() => {
socket?.emit("write_command_line_from_web", {
lineIds: [line?.id],
stationId: stationItem?.id,
command: "spam_break",
});
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}}
>
Send Break
</Button>
<Flex justify={"end"}>
<Button
mr={"8px"}
fw={400}
variant="outline"
color="green"
size="xs"
onClick={() => {}}
>
ON
</Button>
<Button
mr={"8px"}
fw={400}
variant="outline"
color="red"
size="xs"
onClick={() => {}}
>
OFF
</Button>
<Button
fw={400}
variant="outline"
color="orange"
size="xs"
onClick={() => {}}
>
Restart
</Button>
</Flex>
</Flex>
</Grid.Col>
<Grid.Col span={3}>
<Box>
<Tooltip
label={
<div>
<Flex>
<Text
style={{
fontSize: "14px",
}}
c={"#30d100"}
fw={500}
>
Text
</Text>
: is opened
</Flex>
<Flex>
<Text
style={{
fontSize: "14px",
}}
c={"#fab005"}
fw={500}
>
Text
</Text>
: is issue
</Flex>
<Flex>
<Text
style={{
fontSize: "14px",
}}
c={"red"}
fw={500}
>
Text
</Text>
: is closed
</Flex>
</div>
}
position="right"
>
<Flex align={"center"} gap={"6px"} w={"85px"}>
<Text size="md">
<strong>Ticket:</strong>{" "}
</Text>
<IconInfoCircle
color="#3bb7e9"
width={"18px"}
height={"18px"}
/>
</Flex>
</Tooltip>
</Box>
<ScrollArea h={600} style={{ border: "1px solid #ccc" }} p={"4px"}>
{renderHistory(latestTicket)}
</ScrollArea>
<Box mt={"8px"}>
<Input
style={{
width: "100%",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
}}
placeholder={"Input description ticket"}
value={inputTicket}
onChange={(event) => {
const newValue = event.currentTarget.value;
setInputTicket(newValue);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
setInputTicket("");
if (dataTicket?.status === "closed") {
handleCreate();
} else handleUpdate("open");
setIsDisableTicket(true);
setTimeout(() => {
setIsDisableTicket(false);
}, 2000);
}
}}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setInputTicket("")}
style={{
display: inputTicket ? undefined : "none",
}}
/>
}
/>
</Box>
<Box mt={"8px"}>
<Flex justify={"end"} mt={"4px"}>
{dataTicket?.status === "closed" ? (
<Button
disabled={isDisableTicket}
mr={"8px"}
fw={400}
variant="outline"
size="xs"
onClick={() => {
handleCreate();
setIsDisableTicket(true);
setTimeout(() => {
setIsDisableTicket(false);
}, 2000);
}}
>
Open
</Button>
) : (
<Button
disabled={isDisableTicket}
mr={"8px"}
fw={400}
variant="outline"
size="xs"
onClick={() => {
handleUpdate("open");
setIsDisableTicket(true);
setTimeout(() => {
setIsDisableTicket(false);
}, 2000);
}}
>
Send
</Button>
)}
<Button
disabled={isDisableTicket || dataTicket?.status === "closed"}
mr={"8px"}
fw={400}
variant="outline"
color="orange"
size="xs"
onClick={() => {
handleUpdate("issue");
setIsDisableTicket(true);
setTimeout(() => {
setIsDisableTicket(false);
}, 2000);
}}
>
Issue
</Button>
<Button
disabled={isDisableTicket || dataTicket?.status === "closed"}
fw={400}
variant="outline"
color="red"
size="xs"
onClick={() => {
handleUpdate("closed");
setIsDisableTicket(true);
setTimeout(() => {
setIsDisableTicket(false);
}, 2000);
}}
>
Close
</Button>
</Flex>
</Box>
</Grid.Col>
</Grid>
<Flex justify={"space-between"}>
<Box></Box>
<Button
disabled={
disableRequestTakeOver ||
!line?.userOpenCLI ||
line?.userOpenCLI === user?.userName
}
variant="filled"
size="xs"
radius="xs"
mt={"md"}
ml={"20px"}
onClick={() => {
socket?.emit("request_take_over", {
line_id: line?.id,
station_id: Number(line?.station_id),
userName: user?.userName?.trim() || "",
userEmail: user?.userName || "",
});
setDisableRequestTakeOver(true);
setTimeout(() => {
setDisableRequestTakeOver(false);
}, 20000);
setCountDownRequest(20);
const intervalCount = setInterval(() => {
setCountDownRequest((prev) => {
if (prev <= 1) {
clearInterval(intervalCount);
return 0;
}
return prev - 1;
});
}, 1000);
}}
>
Take over{" "}
{countDownRequest > 0 &&
(typeof dataRequestTakeOver?.userName === "undefined" ||
dataRequestTakeOver?.userEmail === user?.userName)
? `(${countDownRequest}s)`
: ""}
</Button>
</Flex>
</Modal>
<Dialog
opened={
typeof dataRequestTakeOver?.userName !== "undefined" &&
line?.userOpenCLI === user?.userName &&
dataRequestTakeOver?.userName !== user?.userName
}
position={{ bottom: 20, right: 20 }}
withCloseButton
style={{ border: "solid 2px #ff6c6b", left: 0 }}
shadow="md"
onClose={close}
size="lg"
radius="md"
>
<Text size="sm" mb="xs" fw={700} c={"#ff6c6b"}>
{`${`${dataRequestTakeOver?.userName} ${
dataRequestTakeOver?.userEmail
? `(${dataRequestTakeOver?.userEmail})`
: ""
}`} want to take over this line? ${
countDownRequest > 0 &&
typeof dataRequestTakeOver?.userName !== "undefined" &&
line?.userOpenCLI === user?.userName
? `(${countDownRequest}s)`
: ""
}`}
</Text>
<Group style={{ display: "flex", justifyContent: "center" }}>
<Button
variant="gradient"
gradient={{ from: "pink", to: "red", deg: 90 }}
size="xs"
onClick={async () => {
socket?.emit("open_cli", {
lineId: line?.id,
stationId: line?.station_id,
userEmail: dataRequestTakeOver?.userEmail,
userName: dataRequestTakeOver?.userName,
});
socket?.emit("request_take_over", {
station_id: Number(line?.station_id),
});
setDisableRequestTakeOver(false);
setDataRequestTakeOver(undefined);
setCountDownRequest(0);
}}
>
Yes
</Button>
<Button
variant="gradient"
size="xs"
onClick={async () => {
setDisableRequestTakeOver(false);
setDataRequestTakeOver(undefined);
setCountDownRequest(0);
}}
>
No
</Button>
</Group>
</Dialog>
</Box>
);
};

View File

@ -95,6 +95,7 @@ export type TLine = {
commands?: string[];
interface?: string;
baud?: number;
tickets?: TDataTicket[];
};
export type TUser = {
@ -199,3 +200,22 @@ export type TextFSM = {
output: string;
textfsm: any;
};
export type TDataTicket = {
id?: number;
description: string;
sn: string;
model: string;
station_id: number;
history: string;
status: string;
};
export type THistoryTicket = {
time: number;
description: string;
closed: string;
status: string;
userName: string;
userId: number;
};