From 1682a28029856e71d1757850e67527127bba2f4c Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:37:37 +0700 Subject: [PATCH] Update --- BACKEND/app/controllers/auth_controller.ts | 18 +- .../app/controllers/stations_controller.ts | 7 - BACKEND/app/controllers/users_controller.ts | 14 +- BACKEND/app/models/user.ts | 2 +- BACKEND/app/services/line_connection.ts | 20 +- ...e_full_name_to_user_name_in_users_table.ts | 23 ++ BACKEND/providers/socket_io_provider.ts | 10 +- FRONTEND/.env.example | 4 + FRONTEND/src/App.tsx | 204 ++++++++---------- .../src/components/Authentication/Login.tsx | 22 +- .../components/Authentication/Register.tsx | 15 +- FRONTEND/src/components/ButtonAction.tsx | 127 ++++++++++- FRONTEND/src/components/CardLine.tsx | 30 ++- FRONTEND/src/components/Component.module.css | 6 +- FRONTEND/src/components/DragTabs.tsx | 114 ++++++++-- FRONTEND/src/components/DrawerScenario.tsx | 28 ++- FRONTEND/src/components/FormAddEdit.tsx | 2 +- FRONTEND/src/components/ModalLog.tsx | 11 +- FRONTEND/src/components/ModalTerminal.tsx | 4 +- FRONTEND/src/components/TerminalXTerm.tsx | 3 +- FRONTEND/src/context/SocketContext.tsx | 2 +- FRONTEND/src/untils/helper.ts | 6 + FRONTEND/src/untils/types.ts | 4 + 23 files changed, 456 insertions(+), 220 deletions(-) create mode 100644 BACKEND/database/migrations/1761895556353_rename_full_name_to_user_name_in_users_table.ts create mode 100644 FRONTEND/.env.example diff --git a/BACKEND/app/controllers/auth_controller.ts b/BACKEND/app/controllers/auth_controller.ts index 1b115c9..0bb7ddb 100644 --- a/BACKEND/app/controllers/auth_controller.ts +++ b/BACKEND/app/controllers/auth_controller.ts @@ -5,39 +5,39 @@ export default class AuthController { // Đăng ký async register({ request, response }: HttpContext) { try { - const data = request.only(['email', 'password', 'full_name']) + const data = request.only(['email', 'password', 'user_name']) - const user = await User.query().where('email', data.email).first() + const user = await User.query().where('user_name', data.user_name).first() if (user) { - return response.status(401).json({ status: false, message: 'Email is exist' }) + return response.status(401).json({ status: false, message: 'Username is exist' }) } const newUser = await User.create(data) return response.json({ status: true, message: 'User created', user: newUser }) } catch (error) { - return response.status(401).json({ status: false, message: 'Invalid credentials' }) + return response.status(401).json({ error, status: false, message: 'Invalid credentials' }) } } // Đăng nhập async login({ request, auth, response }: HttpContext) { - const { email, password } = request.only(['email', 'password']) - const user = await User.query().where('email', email).first() + const { user_name: userName, password } = request.only(['user_name', 'password']) + const user = await User.query().where('user_name', userName).first() if (!user) { - return response.status(401).json({ message: 'Invalid email or password' }) + return response.status(401).json({ message: 'Invalid Username or password' }) } try { // So sánh password if (user.password !== password) { - return response.status(401).json({ message: 'Invalid email or password' }) + return response.status(401).json({ message: 'Invalid username or password' }) } return response.json({ message: 'Login successful', - user: { id: user.id, email: user.email, fullName: user.fullName }, + user: { id: user.id, email: user.email, userName: user.userName }, }) } catch { return response.status(401).json({ message: 'Invalid credentials' }) diff --git a/BACKEND/app/controllers/stations_controller.ts b/BACKEND/app/controllers/stations_controller.ts index 530ac83..903758c 100644 --- a/BACKEND/app/controllers/stations_controller.ts +++ b/BACKEND/app/controllers/stations_controller.ts @@ -71,13 +71,6 @@ export default class StationsController { let lines: Line[] = request.body().lines || [] try { - // Kiểm tra trùng name hoặc ip - if (await Station.findBy('name', payload.name)) - return response.status(400).json({ message: 'Station name exist!' }) - - if (await Station.findBy('ip', payload.ip)) - return response.status(400).json({ message: 'Ip of station is exist!' }) - const station = await Station.find(request.body().id) // If the station does not exist, return a 404 response diff --git a/BACKEND/app/controllers/users_controller.ts b/BACKEND/app/controllers/users_controller.ts index 88730d9..f2594bc 100644 --- a/BACKEND/app/controllers/users_controller.ts +++ b/BACKEND/app/controllers/users_controller.ts @@ -25,19 +25,19 @@ export default class UsersController { async store({ request, response }: HttpContext) { try { - const data = request.only(['full_name', 'email', 'password']) + const data = request.only(['user_name', 'email', 'password']) // Check if email already exists - const existingUser = await User.findBy('email', data.email) + const existingUser = await User.findBy('user_name', data.user_name) if (existingUser) { return response.conflict({ status: false, - message: 'Email already exists', + message: 'Username already exists', }) } const user = await User.create({ - fullName: data.full_name, + userName: data.user_name, email: data.email, password: data.password, }) @@ -55,16 +55,16 @@ export default class UsersController { async update({ params, request, response }: HttpContext) { try { const user = await User.findOrFail(params.id) - const data = request.only(['full_name', 'email', 'password']) // Include password + const data = request.only(['user_name', 'email', 'password']) // Include password // Check if email already exists for another user if (data.email) if (data.email !== user.email) { - const existingUser = await User.findBy('email', data.email) + const existingUser = await User.findBy('user_name', data.user_name) if (existingUser) { return response.conflict({ status: false, - message: 'Email already exists', + message: 'Username already exists', }) } } diff --git a/BACKEND/app/models/user.ts b/BACKEND/app/models/user.ts index 7fff672..e5068e2 100644 --- a/BACKEND/app/models/user.ts +++ b/BACKEND/app/models/user.ts @@ -6,7 +6,7 @@ export default class User extends BaseModel { declare id: number @column() - declare fullName: string | null + declare userName: string @column() declare email: string diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 7e2c508..3f6cb41 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -23,6 +23,11 @@ interface LineConfig { openCLI: boolean userEmailOpenCLI: string userOpenCLI: string + inventory?: string + latestScenario?: { + name: string + time: number + } data: { command: string output: string @@ -184,6 +189,10 @@ export default class LineConnection { this.config.lineNumber, this.config.port ) + this.config.latestScenario = { + name: script?.title, + time: now, + } const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : [] let stepIndex = 0 @@ -228,10 +237,17 @@ export default class LineConnection { if (err) return const logScenarios = getLogWithTimeScenario(content, now) || '' - const data = await textfsmResults(logScenarios, '') + const data = textfsmResults(logScenarios, '') try { data.forEach((item) => { if (item?.textfsm && isValidJson(item?.textfsm)) { + if ( + ['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes( + item.command + ) + ) { + this.config.inventory = JSON.parse(item.textfsm)[0] + } item.textfsm = JSON.parse(item.textfsm) } }) @@ -240,6 +256,8 @@ export default class LineConnection { stationId: this.config.stationId, lineId: this.config.id, data, + inventory: this.config.inventory || null, + latestScenario: this.config.latestScenario || null, }) } catch (error) { console.log(error) diff --git a/BACKEND/database/migrations/1761895556353_rename_full_name_to_user_name_in_users_table.ts b/BACKEND/database/migrations/1761895556353_rename_full_name_to_user_name_in_users_table.ts new file mode 100644 index 0000000..fa9611f --- /dev/null +++ b/BACKEND/database/migrations/1761895556353_rename_full_name_to_user_name_in_users_table.ts @@ -0,0 +1,23 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'users' + + public async up() { + this.schema.alterTable(this.tableName, (table) => { + table.renameColumn('full_name', 'user_name') + }) + this.schema.alterTable(this.tableName, (table) => { + table.string('user_name').notNullable().unique().alter() + table.dropUnique(['email']) + }) + } + + public async down() { + this.schema.alterTable(this.tableName, (table) => { + table.unique(['email']) + table.string('user_name').nullable().alter() + table.renameColumn('user_name', 'full_name') + }) + } +} diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 1515e05..adc5185 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -78,15 +78,19 @@ export class WebSocketIo { this.userConnecting.set(userId, { userId, userName }) setTimeout(() => { - io.emit('user_connecting', Array.from(this.userConnecting.values())) - }, 200) + const listUser = Array.from(this.userConnecting.values()) + if (!listUser.find((el) => el.userId === userId)) { + listUser.push({ userId, userName }) + } + io.emit('user_connecting', listUser) + }, 500) setTimeout(() => { io.to(socket.id).emit( 'init', Array.from(this.lineMap.values()).map((el) => el.config) ) - }, 200) + }, 500) socket.on('disconnect', () => { console.log(`FE disconnected: ${socket.id}`) diff --git a/FRONTEND/.env.example b/FRONTEND/.env.example new file mode 100644 index 0000000..a86ffce --- /dev/null +++ b/FRONTEND/.env.example @@ -0,0 +1,4 @@ +VITE_BACKEND_URL=http://localhost:3333/ +VITE_SOCKET_SERVER=http://localhost:8989/ +VITE_LOCALSTORAGE_VARIABLE=au_ma_te_da +VITE_DOMAIN=http://localhost:5173/ \ No newline at end of file diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index dee847f..3732443 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -13,7 +13,6 @@ import { MantineProvider, Grid, ScrollArea, - Button, LoadingOverlay, } from "@mantine/core"; import type { @@ -22,7 +21,6 @@ import type { LineConfig, ReceivedFile, ResponseData, - TextFSM, TLine, TStation, TUser, @@ -30,7 +28,13 @@ import type { import axios from "axios"; import CardLine from "./components/CardLine"; import { SocketProvider, useSocket } from "./context/SocketContext"; -import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction"; +import { + ButtonConnect, + ButtonCopy, + ButtonDPELP, + ButtonScenario, + ButtonSelect, +} from "./components/ButtonAction"; import StationSetting from "./components/FormAddEdit"; import DrawerScenario from "./components/DrawerScenario"; import { Notifications } from "@mantine/notifications"; @@ -230,6 +234,8 @@ function App() { setTimeout(() => { updateValueLineStation(data.lineId, { data: data.data, + inventory: data.inventory, + latestScenario: data.latestScenario, }); }, 100); }); @@ -319,19 +325,19 @@ function App() { if (!line.userEmailOpenCLI) { data.cliOpened = true; data.userEmailOpenCLI = user?.email; - data.userOpenCLI = user?.fullName; + data.userOpenCLI = user?.userName; socket?.emit("open_cli", { lineId: line.id, stationId: line.station_id, userEmail: user?.email, - userName: user?.fullName, + userName: user?.userName, }); } setSelectedLine(data); }; return ( - + - - - -
- + + + +
+ +
- {scenarios.map((el, i) => ( - - typeof el?.userEmailOpenCLI === "undefined" || - el?.userEmailOpenCLI === user?.email - )} - isDisable={isDisable || selectedLines.length === 0} - onClick={() => { - setSelectedLines([]); - setIsDisable(true); - setTimeout(() => { - setIsDisable(false); - }, 10000); - }} - scenario={el} - /> - ))} + + + {scenarios.map((el, i) => ( + + typeof el?.userEmailOpenCLI === "undefined" || + el?.userEmailOpenCLI === user?.email + )} + isDisable={isDisable || selectedLines.length === 0} + onClick={() => { + setSelectedLines([]); + setIsDisable(true); + setTimeout(() => { + setIsDisable(false); + }, 10000); + }} + scenario={el} + /> + ))} + + { + const listLine = selectedLines.length + ? selectedLines + : stations.find((el) => el.id === Number(activeTab))?.lines; + if (listLine?.length) { + socket?.emit("write_command_line_from_web", { + lineIds: listLine.map((line) => line.id), + stationId: Number(activeTab), + command: value + "\n", + }); + setTimeout(() => { + socket?.emit("write_command_line_from_web", { + lineIds: listLine.map((line) => line.id), + stationId: Number(activeTab), + command: " \n", + }); + }, 1000); + } + }} /> { const formLogin = useForm({ initialValues: { - email: "", + user_name: "", password: "", }, validate: (values) => ({ - email: values.email === "" ? "Email is required" : null, + user_name: values.user_name === "" ? "Email is required" : null, password: values.password === "" ? "Password is required" : null, }), @@ -25,10 +25,10 @@ const Login = () => { const handleLogin = async () => { try { - if (!formLogin.values.email) { + if (!formLogin.values.user_name) { notifications.show({ title: "Error", - message: "Email is required", + message: "Username is required", color: "red", }); return; @@ -42,7 +42,7 @@ const Login = () => { return; } const payload = { - email: formLogin.values.email, + user_name: formLogin.values.user_name, password: formLogin.values.password, }; const response = await axios.post(apiUrl + "api/auth/login", payload); @@ -69,12 +69,12 @@ const Login = () => { onSubmit={formLogin.onSubmit(handleLogin)} > { - formLogin.setFieldValue("email", e.target.value!); + formLogin.setFieldValue("user_name", e.target.value!); }} required size="md" diff --git a/FRONTEND/src/components/Authentication/Register.tsx b/FRONTEND/src/components/Authentication/Register.tsx index 0ad3f48..f8c059f 100644 --- a/FRONTEND/src/components/Authentication/Register.tsx +++ b/FRONTEND/src/components/Authentication/Register.tsx @@ -10,13 +10,13 @@ type TRegister = { email: string; password: string; confirm_password: string; - full_name: string; + user_name: string; }; function Register() { const [formRegister, setFormRegister] = useState({ email: "", - full_name: "", + user_name: "", password: "", confirm_password: "", }); @@ -42,12 +42,12 @@ function Register() { const payload = { email: formRegister.email, password: formRegister.password, - full_name: formRegister.full_name, + user_name: formRegister.user_name, }; const response = await axios.post(apiUrl + "api/auth/register", payload); if (response.data.user) { const user = response.data.user; - user.fullName = user.full_name; + user.userName = user.user_name; localStorage.setItem("user", JSON.stringify(user)); window.location.href = "/"; } else { @@ -86,18 +86,17 @@ function Register() { onChange={(e) => { setFormRegister({ ...formRegister, email: e.target.value }); }} - required size="md" mb="md" /> { - setFormRegister({ ...formRegister, full_name: e.target.value }); + setFormRegister({ ...formRegister, user_name: e.target.value }); }} required size="md" diff --git a/FRONTEND/src/components/ButtonAction.tsx b/FRONTEND/src/components/ButtonAction.tsx index fe5778a..684a5a7 100644 --- a/FRONTEND/src/components/ButtonAction.tsx +++ b/FRONTEND/src/components/ButtonAction.tsx @@ -1,5 +1,5 @@ import type { Socket } from "socket.io-client"; -import type { IScenario, TLine } from "../untils/types"; +import type { IScenario, TextFSM, TLine, TStation } from "../untils/types"; import { Button } from "@mantine/core"; import classes from "./Component.module.css"; @@ -27,6 +27,13 @@ export const ButtonDPELP = ({ onClick(); selectedLines?.forEach((el) => { const body = [ + { + expect: "", + send: " show inventory", + delay: "1000", + repeat: "1", + note: "", + }, { expect: "", send: " show diag", @@ -149,7 +156,7 @@ export const ButtonScenario = ({ + ); +}; + +export const ButtonSelect = ({ + selectedLines, + setSelectedLines, + station, +}: { + setSelectedLines: (value: React.SetStateAction) => void; + selectedLines: TLine[]; + station: TStation; +}) => { + return ( + + ); +}; + +export const ButtonConnect = ({ + selectedLines, + setSelectedLines, + station, + socket, +}: { + setSelectedLines: (value: React.SetStateAction) => void; + selectedLines: TLine[]; + station: TStation; + socket: Socket | null; +}) => { + return ( + + ); +}; diff --git a/FRONTEND/src/components/CardLine.tsx b/FRONTEND/src/components/CardLine.tsx index 7af0fa3..0301d31 100644 --- a/FRONTEND/src/components/CardLine.tsx +++ b/FRONTEND/src/components/CardLine.tsx @@ -5,6 +5,7 @@ import TerminalCLI from "./TerminalXTerm"; import type { Socket } from "socket.io-client"; import { IconCircleCheckFilled } from "@tabler/icons-react"; import { memo, useMemo } from "react"; +import { convertTimestampToDate } from "../untils/helper"; const CardLine = ({ line, @@ -62,7 +63,10 @@ const CardLine = ({ // align={"center"} > - + Line: {line.lineNumber || line.line_number} - {line.port}{" "} {line.status === "connected" && ( @@ -80,9 +84,17 @@ const CardLine = ({ {line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""} - {/* PID: WS-C3560CG-8PC-S -
SN: FGL2240307M
-
VID: V01
*/} + +
+ PID: {line?.inventory?.pid || ""} +
+
+ SN: {line?.inventory?.sn || ""} +
+
+ VID: {line?.inventory?.vid || ""} +
+
{ e.preventDefault(); @@ -118,6 +130,16 @@ const CardLine = ({ }} /> + +
+ Latest: {line?.latestScenario?.name || ""} + + {line?.latestScenario?.time + ? "(" + convertTimestampToDate(line?.latestScenario?.time) + ")" + : ""} + +
+
); diff --git a/FRONTEND/src/components/Component.module.css b/FRONTEND/src/components/Component.module.css index 2616948..1c8be38 100644 --- a/FRONTEND/src/components/Component.module.css +++ b/FRONTEND/src/components/Component.module.css @@ -1,6 +1,6 @@ .card_line { width: 320px; - height: 220px; + height: 250px; padding: 8px; gap: 8px; cursor: pointer; @@ -8,10 +8,10 @@ .info_line { color: dimgrey; - font-size: 12px; + font-size: 11px; display: flex; gap: 4px; - margin-top: 4px; + /* margin-top: 4px; */ height: 20px; } diff --git a/FRONTEND/src/components/DragTabs.tsx b/FRONTEND/src/components/DragTabs.tsx index 35c4f8a..ba83b3d 100644 --- a/FRONTEND/src/components/DragTabs.tsx +++ b/FRONTEND/src/components/DragTabs.tsx @@ -2,11 +2,15 @@ import { ActionIcon, Avatar, Box, - Button, + CloseButton, Flex, + Group, + Input, + Menu, Tabs, Text, Tooltip, + UnstyledButton, } from "@mantine/core"; import { DndContext, @@ -24,7 +28,13 @@ import { } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useEffect, useMemo, useState, type JSX } from "react"; -import { IconEdit, IconSettingsPlus } from "@tabler/icons-react"; +import { + IconChevronRight, + IconEdit, + IconLogout, + IconSettingsPlus, + IconUsersGroup, +} from "@tabler/icons-react"; import classes from "./Component.module.css"; import type { TStation, TUser } from "../untils/types"; import type { Socket } from "socket.io-client"; @@ -43,6 +53,7 @@ interface DraggableTabsProps { setStationEdit: (value: React.SetStateAction) => void; active: string; setActive: (value: React.SetStateAction) => void; + onSendCommand: (value: string) => void; } function SortableTab({ @@ -103,6 +114,7 @@ export default function DraggableTabs({ setStationEdit, active, setActive, + onSendCommand, }: DraggableTabsProps) { const user = useMemo(() => { return localStorage.getItem("user") && @@ -113,6 +125,7 @@ export default function DraggableTabs({ const [tabs, setTabs] = useState(tabsData); const [isChangeTab, setIsChangeTab] = useState(false); const [isSetActive, setIsSetActive] = useState(false); + const [valueInput, setValueInput] = useState(""); // const [active, setActive] = useState( // tabsData?.length > 0 ? tabsData[0]?.id.toString() : null // ); @@ -219,14 +232,33 @@ export default function DraggableTabs({ w={w} > - - {usersConnecting.map((el) => ( - - - {el.userName.slice(0, 2)} - - - ))} + + { + const newValue = event.currentTarget.value; + setValueInput(newValue); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + onSendCommand(valueInput); + setValueInput(""); + } + }} + rightSectionPointerEvents="all" + rightSection={ + setValueInput("")} + style={{ display: valueInput ? undefined : "none" }} + /> + } + /> - - {user?.fullName} - + + + + + + + + +
+ + {user?.userName || user?.user_name || ""} + + + + {user?.email} + +
+ + +
+
+
+ + { + localStorage.removeItem("user"); + window.location.href = "/"; + socket?.disconnect(); + }} + color="red" + leftSection={} + > + Logout + + +
diff --git a/FRONTEND/src/components/DrawerScenario.tsx b/FRONTEND/src/components/DrawerScenario.tsx index be03ed1..c074928 100644 --- a/FRONTEND/src/components/DrawerScenario.tsx +++ b/FRONTEND/src/components/DrawerScenario.tsx @@ -1,7 +1,6 @@ import { useDisclosure } from "@mantine/hooks"; import { Drawer, - ActionIcon, Box, ScrollArea, Table, @@ -9,6 +8,7 @@ import { TextInput, Button, Checkbox, + Text, } from "@mantine/core"; import { IconSettingsPlus } from "@tabler/icons-react"; import TableRows from "./Scenario/TableRows"; @@ -361,17 +361,25 @@ function DrawerScenario({ - - { - open(); + - - + Scenarios + { + open(); + }} + /> + } - size={"60%"} + size={"80%"} style={{ position: "absolute", left: 0 }} centered opened={isOpen} diff --git a/FRONTEND/src/components/ModalLog.tsx b/FRONTEND/src/components/ModalLog.tsx index e6b776f..99c6158 100644 --- a/FRONTEND/src/components/ModalLog.tsx +++ b/FRONTEND/src/components/ModalLog.tsx @@ -1,5 +1,6 @@ import { Modal, Text } from "@mantine/core"; import classes from "./Component.module.css"; +import { convertTimestampToDate } from "../untils/helper"; const ModalLog = ({ opened, @@ -25,8 +26,8 @@ const ModalLog = ({ const colorPhysicalStart = "#7fffd4"; const colorPhysicalEnd = "#ffa589"; return logText - .replace(/^---split-point-scenario---.*$/gm, "") // Remove split-point lines - .replace(/^---split-point---.*$/gm, "") // Remove split-point lines + .replace(/^---scenario---.*$/gm, "") // Remove split-point lines + .replace(/^---send-command---.*$/gm, "") // Remove split-point lines .replace( /^(---start-testing---|---end-testing---|---start-scenarios---|---end-scenarios---)(\d+)(---.*)?$/gm, (_, prefix, timestamp, suffix = "") => { @@ -48,12 +49,6 @@ const ModalLog = ({ ); }; - // Function to convert timestamp to date - const convertTimestampToDate = (timestamp: number) => { - const date = new Date(Number(timestamp)); - return date.toLocaleString(); - }; - return ( = ({ // Gửi input từ người dùng lên server terminal.current.onData((data) => { - socket?.emit(SOCKET_EVENTS.CLI.WRITE_COMMAND_FROM_WEB, { + socket?.emit("write_command_line_from_web", { lineIds: [line_id], stationId: station_id, command: data, diff --git a/FRONTEND/src/context/SocketContext.tsx b/FRONTEND/src/context/SocketContext.tsx index 8ceb005..2cb84a4 100644 --- a/FRONTEND/src/context/SocketContext.tsx +++ b/FRONTEND/src/context/SocketContext.tsx @@ -34,7 +34,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ const newSocket = io(SOCKET_URL, { auth: { userId: user?.id, - userName: user?.fullName, + userName: user?.userName, }, }); diff --git a/FRONTEND/src/untils/helper.ts b/FRONTEND/src/untils/helper.ts index 2813696..e39746b 100644 --- a/FRONTEND/src/untils/helper.ts +++ b/FRONTEND/src/untils/helper.ts @@ -26,3 +26,9 @@ export function mergeArray(array: any[], key: string) { .flat() .filter((el) => Object.keys(el).length > 0); } + +// Function to convert timestamp to date +export const convertTimestampToDate = (timestamp: number) => { + const date = new Date(Number(timestamp)); + return date.toLocaleString(); +}; diff --git a/FRONTEND/src/untils/types.ts b/FRONTEND/src/untils/types.ts index 0f62d76..db957a0 100644 --- a/FRONTEND/src/untils/types.ts +++ b/FRONTEND/src/untils/types.ts @@ -83,6 +83,10 @@ export type TLine = { userOpenCLI?: string; userEmailOpenCLI?: string; statusTicket?: string; + latestScenario?: { + name: string; + time: number; + }; }; export type TUser = {