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:
parent
cbc4a8c9b0
commit
d908cf204c
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue