diff --git a/BACKEND/app/controllers/scenarios_controller.ts b/BACKEND/app/controllers/scenarios_controller.ts index d09913f..624adb0 100644 --- a/BACKEND/app/controllers/scenarios_controller.ts +++ b/BACKEND/app/controllers/scenarios_controller.ts @@ -91,9 +91,10 @@ export default class ScenariosController { */ async update({ request, response, auth }: HttpContext) { try { - const scenarioId = request.param('id') - const payload = await request.all() - const scenario = await Scenario.findOrFail(scenarioId) + const scenarioId = request.body().id + const payload = request.body() + const scenario = await Scenario.find(scenarioId) + if (!scenario) return response.status(404).json({ message: 'Scenario not found' }) payload.body = JSON.stringify(payload.body) scenario.merge(payload) await scenario.save() @@ -113,7 +114,7 @@ export default class ScenariosController { */ async delete({ request, response }: HttpContext) { try { - const scenarioId = request.param('id') + const scenarioId = request.body().id const scenario = await Scenario.findOrFail(scenarioId) if (!scenario) { diff --git a/BACKEND/app/controllers/stations_controller.ts b/BACKEND/app/controllers/stations_controller.ts index 6e0ae07..e0a7ec2 100644 --- a/BACKEND/app/controllers/stations_controller.ts +++ b/BACKEND/app/controllers/stations_controller.ts @@ -1,5 +1,6 @@ import type { HttpContext } from '@adonisjs/core/http' import Station from '#models/station' +import Line from '#models/line' export default class StationsController { public async index({}: HttpContext) { @@ -8,6 +9,8 @@ export default class StationsController { public async store({ request, response }: HttpContext) { let payload = request.only(Array.from(Station.$columnsDefinitions.keys())) + let lines: Line[] = request.body().lines || [] + try { const stationName = await Station.findBy('name', payload.name) if (stationName) return response.status(400).json({ message: 'Station name exist!' }) @@ -16,10 +19,33 @@ export default class StationsController { if (stationIP) return response.status(400).json({ message: 'Ip of station is exist!' }) const station = await Station.create(payload) + + const newLines: Line[] = [] + if (lines && Array.isArray(lines)) { + lines.forEach(async (line) => { + if (line.id) { + const value = await Line.find(line.id) + if (value) { + Object.assign(value, line) + await value.save() + newLines.push(value) + } else { + const value1 = await Line.create({ ...line, stationId: station.id }) + newLines.push(value1) + } + } else { + const value2 = await Line.create({ ...line, stationId: station.id }) + newLines.push(value2) + } + }) + } + + const resStation = await Station.query().where('id', station.id).preload('lines') + return response.created({ status: true, message: 'Station created successfully', - data: station, + data: resStation.map((el) => ({ ...el.$original, lines: newLines })), }) } catch (error) { return response.badRequest({ error: error, message: 'Station create failed', status: false }) @@ -36,6 +62,8 @@ export default class StationsController { (f) => f !== 'created_at' && f !== 'updated_at' ) ) + let lines: Line[] = request.body().lines || [] + try { const station = await Station.find(request.body().id) @@ -48,7 +76,21 @@ export default class StationsController { await station.save() - return response.ok({ status: true, message: 'Station update successfully', data: station }) + if (lines && Array.isArray(lines)) { + lines.forEach(async (line) => { + if (line.id) { + const value = await Line.find(line.id) + if (value) { + Object.assign(value, line) + await value.save() + } else await Line.create({ ...line, stationId: station.id }) + } else await Line.create({ ...line, stationId: station.id }) + }) + } + + const resStation = await Station.query().where('id', station.id).preload('lines') + + return response.ok({ status: true, message: 'Station update successfully', data: resStation }) } catch (error) { return response.badRequest({ error: error, message: 'Station update failed', status: false }) } diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index aaba016..c910e7e 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -1,5 +1,5 @@ import net from 'node:net' -import { cleanData } from '../ultils/helper.js' +import { cleanData, sleep } from '../ultils/helper.js' import Scenario from '#models/scenario' interface LineConfig { @@ -59,7 +59,7 @@ export default class LineConnection { this.client.on('data', (data) => { if (this.connecting) return let message = data.toString() - this.outputBuffer += message + if (this.isRunningScript) this.outputBuffer += message // let output = cleanData(message) // console.log(`📨 [${this.config.port}] ${message}`) // Handle netOutput with backspace support @@ -109,7 +109,7 @@ export default class LineConnection { console.log(`⏳ Connection timeout line ${lineNumber}`) this.client.destroy() - reject(new Error('Connection timeout')) + // reject(new Error('Connection timeout')) }) }) } @@ -138,10 +138,14 @@ export default class LineConnection { async runScript(script: Scenario) { if (!this.client || this.client.destroyed) { - throw new Error('Not connected') + console.log('Not connected') + this.isRunningScript = false + this.outputBuffer = '' + return } if (this.isRunningScript) { - throw new Error('Script already running') + console.log('Script already running') + return } this.isRunningScript = true @@ -152,10 +156,10 @@ export default class LineConnection { const timeoutTimer = setTimeout(() => { this.isRunningScript = false this.outputBuffer = '' - reject(new Error('Script timeout')) + // reject(new Error('Script timeout')) }, script.timeout || 300000) - const runStep = (index: number) => { + const runStep = async (index: number) => { if (index >= steps.length) { clearTimeout(timeoutTimer) this.isRunningScript = false @@ -166,11 +170,9 @@ export default class LineConnection { const step = steps[index] let repeatCount = Number(step.repeat) || 1 - const sendCommand = () => { if (repeatCount <= 0) { // Done → next step - this.client.off('data', onOutput) stepIndex++ return runStep(stepIndex) } @@ -183,24 +185,19 @@ export default class LineConnection { setTimeout(() => sendCommand(), Number(step?.delay) || 500) } - // Lắng nghe output cho expect - const onOutput = (data: string) => { - const output = data.toString() - this.outputBuffer += output - - if (output.includes(step.expect)) { - this.client.off('data', onOutput) - setTimeout(() => sendCommand(), Number(step?.delay) || 500) - } - } - // Nếu expect rỗng → gửi ngay if (!step?.expect || step?.expect.trim() === '') { setTimeout(() => sendCommand(), Number(step?.delay) || 500) return } - this.client.on('data', onOutput) + while (this.outputBuffer) { + await sleep(200) + if (this.outputBuffer.includes(step.expect)) { + this.outputBuffer = '' + setTimeout(() => sendCommand(), Number(step?.delay) || 500) + } + } } runStep(stepIndex) diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index c5ddc46..2cdb42d 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -12,3 +12,7 @@ export const cleanData = (data: string) => { .replace(/[^\x20-\x7E\r\n]/g, '') // Remove non-printable characters // .replace(/\r\n/g, '\n') } + +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/BACKEND/database/migrations/1761185635441_create_lines_table.ts b/BACKEND/database/migrations/1761185635441_create_lines_table.ts index 58998c4..481e161 100644 --- a/BACKEND/database/migrations/1761185635441_create_lines_table.ts +++ b/BACKEND/database/migrations/1761185635441_create_lines_table.ts @@ -11,7 +11,7 @@ export default class extends BaseSchema { table.integer('line_clear') table.integer('outlet') table.integer('station_id').unsigned().references('id').inTable('stations') - table.string('apc_name').notNullable() + table.string('apc_name') table.timestamps() }) } diff --git a/BACKEND/database/migrations/1761185646058_create_scenarios_table.ts b/BACKEND/database/migrations/1761185646058_create_scenarios_table.ts index 2c3e2af..c957bb5 100644 --- a/BACKEND/database/migrations/1761185646058_create_scenarios_table.ts +++ b/BACKEND/database/migrations/1761185646058_create_scenarios_table.ts @@ -7,7 +7,6 @@ export default class extends BaseSchema { this.schema.createTable(this.tableName, (table) => { table.increments('id') table.string('title').notNullable() - table.string('name').notNullable() table.text('body').notNullable() table.integer('timeout').notNullable() table.boolean('isReboot').defaultTo(false) diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 4061e23..7919d41 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -50,6 +50,7 @@ export class WebSocketIo { intervalMap: { [key: string]: NodeJS.Timeout } = {} stationMap: Map = new Map() lineMap: Map = new Map() // key = lineId + lineConnecting: number[] = [] // key = lineId constructor(protected app: ApplicationService) {} @@ -94,15 +95,19 @@ export class WebSocketIo { for (const lineId of lineIds) { const line = this.lineMap.get(lineId) if (line) { + this.lineConnecting.filter((el) => el !== lineId) this.setTimeoutConnect(lineId, line) line.writeCommand(command) } else { + if (this.lineConnecting.includes(lineId)) continue const linesData = await Line.findBy('id', lineId) const stationData = await Station.findBy('id', stationId) if (linesData && stationData) { + this.lineConnecting.push(lineId) await this.connectLine(io, [linesData], stationData) const lineReconnect = this.lineMap.get(lineId) if (lineReconnect) { + this.lineConnecting.filter((el) => el !== lineId) this.setTimeoutConnect(lineId, lineReconnect) lineReconnect.writeCommand(command) } @@ -131,7 +136,7 @@ export class WebSocketIo { line.runScript(scenario) } else { const linesData = await Line.findBy('id', lineId) - const stationData = await Station.findBy('id', data.stationId) + const stationData = await Station.findBy('id', data.station_id) if (linesData && stationData) { await this.connectLine(io, [linesData], stationData) const lineReconnect = this.lineMap.get(lineId) diff --git a/FRONTEND/index.html b/FRONTEND/index.html index a65cfb7..924cbf9 100644 --- a/FRONTEND/index.html +++ b/FRONTEND/index.html @@ -2,7 +2,7 @@ - + Automation Test diff --git a/FRONTEND/package-lock.json b/FRONTEND/package-lock.json index 92c0a2c..141d0c1 100644 --- a/FRONTEND/package-lock.json +++ b/FRONTEND/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@mantine/core": "^8.3.5", "@mantine/dates": "^8.3.5", + "@mantine/form": "^8.3.5", + "@mantine/hooks": "^8.3.5", "@mantine/notifications": "^8.3.5", "@tabler/icons-react": "^3.35.0", "@xterm/addon-fit": "^0.10.0", @@ -1114,12 +1116,24 @@ "react-dom": "^18.x || ^19.x" } }, + "node_modules/@mantine/form": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.3.5.tgz", + "integrity": "sha512-i9UFiHtO1dlrJXZkquyt+71YcNNxPPSkIcJCRp7k0Tif7bPqWK2xijPDEXzqvA53YvMvEMoqaQCEQLVmH7Esdg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "klona": "^2.0.6" + }, + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, "node_modules/@mantine/hooks": { "version": "8.3.5", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz", "integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -2631,7 +2645,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3115,6 +3128,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/FRONTEND/package.json b/FRONTEND/package.json index da42df5..fc6e012 100644 --- a/FRONTEND/package.json +++ b/FRONTEND/package.json @@ -12,6 +12,8 @@ "dependencies": { "@mantine/core": "^8.3.5", "@mantine/dates": "^8.3.5", + "@mantine/form": "^8.3.5", + "@mantine/hooks": "^8.3.5", "@mantine/notifications": "^8.3.5", "@tabler/icons-react": "^3.35.0", "@xterm/addon-fit": "^0.10.0", diff --git a/FRONTEND/public/icon-ATC-removebg.png b/FRONTEND/public/icon-ATC-removebg.png new file mode 100644 index 0000000..4407f36 Binary files /dev/null and b/FRONTEND/public/icon-ATC-removebg.png differ diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index c1b9f2a..f02baa0 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -4,7 +4,7 @@ import "@mantine/notifications/styles.css"; import "./App.css"; import classes from "./App.module.css"; -import { useEffect, useState } from "react"; +import { Suspense, useEffect, useState } from "react"; import { Tabs, Text, @@ -16,20 +16,24 @@ import { ScrollArea, Button, ActionIcon, + LoadingOverlay, } from "@mantine/core"; -import type { LineConfig, TLine, TStation } from "./untils/types"; +import type { IScenario, LineConfig, TLine, TStation } from "./untils/types"; import axios from "axios"; import CardLine from "./components/CardLine"; import { IconEdit, IconSettingsPlus } from "@tabler/icons-react"; import { SocketProvider, useSocket } from "./context/SocketContext"; -import { ButtonDPELP } from "./components/ButtonAction"; +import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction"; +import StationSetting from "./components/FormAddEdit"; +import DrawerScenario from "./components/DrawerScenario"; +import { Notifications } from "@mantine/notifications"; const apiUrl = import.meta.env.VITE_BACKEND_URL; /** * Main Component */ -export function App() { +function App() { document.title = "Automation Test"; const { socket } = useSocket(); const [stations, setStations] = useState([]); @@ -45,6 +49,10 @@ export function App() { }; const [showBottomShadow, setShowBottomShadow] = useState(false); const [isDisable, setIsDisable] = useState(false); + const [isOpenAddStation, setIsOpenAddStation] = useState(false); + const [isEditStation, setIsEditStation] = useState(false); + const [stationEdit, setStationEdit] = useState(); + const [scenarios, setScenarios] = useState([]); // function get list station const getStation = async () => { @@ -62,8 +70,23 @@ export function App() { } }; + // function get list station + const getScenarios = async () => { + try { + const response = await axios.get(apiUrl + "api/scenarios"); + if (response.data.status) { + if (Array.isArray(response.data.data.data)) { + setScenarios(response.data.data.data); + } + } + } catch (error) { + console.log("Error get station", error); + } + }; + useEffect(() => { getStation(); + getScenarios(); }, []); useEffect(() => { @@ -121,7 +144,6 @@ export function App() { return ( - {/* Tabs (Top Bar) */} setActiveTab(id?.toString() || "0")} @@ -146,14 +168,35 @@ export function App() { className={classes.indicator} /> - - - - {Number(activeTab) && ( - + {Number(activeTab) ? ( + { + setStationEdit( + stations.find((el) => el.id === Number(activeTab)) + ); + setIsOpenAddStation(true); + setIsEditStation(true); + }} + > + ) : ( + "" )} + { + setIsOpenAddStation(true); + setIsEditStation(false); + setStationEdit(undefined); + }} + > + + @@ -249,6 +292,11 @@ export function App() { > Connect +
+ + {scenarios.map((el) => ( + { + setSelectedLines([]); + setIsDisable(true); + setTimeout(() => { + setIsDisable(false); + }, 10000); + }} + scenario={el} + /> + ))} ))}
+ + { + setIsOpenAddStation(false); + setIsEditStation(false); + setStationEdit(undefined); + }} + isEdit={isEditStation} + setStations={setStations} + setActiveTab={() => + setActiveTab(stations.length ? stations[0]?.id.toString() : "0") + } + />
); } @@ -275,7 +353,18 @@ export default function Main() { return ( - + + } + > + + + ); diff --git a/FRONTEND/src/components/ButtonAction.tsx b/FRONTEND/src/components/ButtonAction.tsx index 880372c..fe5778a 100644 --- a/FRONTEND/src/components/ButtonAction.tsx +++ b/FRONTEND/src/components/ButtonAction.tsx @@ -1,6 +1,7 @@ import type { Socket } from "socket.io-client"; -import type { TLine } from "../untils/types"; +import type { IScenario, TLine } from "../untils/types"; import { Button } from "@mantine/core"; +import classes from "./Component.module.css"; export const ButtonDPELP = ({ socket, @@ -18,7 +19,7 @@ export const ButtonDPELP = ({ disabled={isDisable} miw={"100px"} // radius="lg" - h={"24px"} + h={"28px"} mr={"5px"} variant="filled" color="#00a164" @@ -130,3 +131,42 @@ export const ButtonDPELP = ({ ); }; + +export const ButtonScenario = ({ + socket, + isDisable, + onClick, + selectedLines, + scenario, +}: { + socket: Socket | null; + isDisable: boolean; + onClick: () => void; + selectedLines: TLine[]; + scenario: IScenario; +}) => { + return ( + + ); +}; diff --git a/FRONTEND/src/components/CardLine.tsx b/FRONTEND/src/components/CardLine.tsx index 140cc0e..36171bd 100644 --- a/FRONTEND/src/components/CardLine.tsx +++ b/FRONTEND/src/components/CardLine.tsx @@ -49,7 +49,7 @@ const CardLine = ({ >
- Line {line.lineNumber} - {line.port}{" "} + Line: {line.lineNumber || line.line_number} - {line.port}{" "} {line.status === "connected" && ( )} diff --git a/FRONTEND/src/components/Component.module.css b/FRONTEND/src/components/Component.module.css index 7a0bb91..697553b 100644 --- a/FRONTEND/src/components/Component.module.css +++ b/FRONTEND/src/components/Component.module.css @@ -13,4 +13,8 @@ gap: 4px; margin-top: 4px; height: 20px; +} + +.buttonScenario :global(.mantine-Button-label) { + white-space: normal !important; } \ No newline at end of file diff --git a/FRONTEND/src/components/DialogConfirm.tsx b/FRONTEND/src/components/DialogConfirm.tsx new file mode 100644 index 0000000..3565434 --- /dev/null +++ b/FRONTEND/src/components/DialogConfirm.tsx @@ -0,0 +1,71 @@ +import { Box, Button, Group, Modal, Text } from "@mantine/core"; +import { useState } from "react"; +const DialogConfirm = ({ + opened, + close, + message, + handle, +}: { + opened: boolean; + close: () => void; + message: string; + handle: () => void; + centered?: boolean; + bottom?: boolean; +}) => { + const [disableBtn, setDisableBtn] = useState(false); + return ( + + + + {message} + + + + + + + + + + ); +}; + +export default DialogConfirm; diff --git a/FRONTEND/src/components/DrawerScenario.tsx b/FRONTEND/src/components/DrawerScenario.tsx new file mode 100644 index 0000000..1b98930 --- /dev/null +++ b/FRONTEND/src/components/DrawerScenario.tsx @@ -0,0 +1,344 @@ +import { useDisclosure } from "@mantine/hooks"; +import { + Drawer, + ActionIcon, + Box, + ScrollArea, + Table, + Grid, + TextInput, + Button, +} from "@mantine/core"; +import { IconSettingsPlus } from "@tabler/icons-react"; +import TableRows from "./Scenario/TableRows"; +import { useEffect, useState } from "react"; +import { useForm } from "@mantine/form"; +import DialogConfirm from "./DialogConfirm"; +import type { IBodyScenario, IScenario } from "../untils/types"; +import classes from "./Component.module.css"; +import axios from "axios"; +import { notifications } from "@mantine/notifications"; +const apiUrl = import.meta.env.VITE_BACKEND_URL; + +function DrawerScenario({ + scenarios, + setScenarios, +}: { + scenarios: IScenario[]; + setScenarios: (value: React.SetStateAction) => void; +}) { + const [opened, { open, close }] = useDisclosure(false); + const [isEdit, setIsEdit] = useState(false); + const [openConfirm, setOpenConfirm] = useState(false); + const [isSubmit, setIsSubmit] = useState(false); + const [dataScenario, setDataScenario] = useState(); + + const form = useForm({ + initialValues: { + title: "", + body: [ + { + expect: "", + send: "", + delay: "0", + repeat: "1", + note: "", + }, + ] as IBodyScenario[], + timeout: "30000", + is_reboot: false, + }, + validate: { + title: (value) => { + if (!value) return "Title is required"; + if (value.length > 100) return "The title cannot exceed 100 characters"; + return null; + }, + body: (value) => { + if (value.length === 0) return "The body is required"; + return null; + }, + timeout: (value) => { + if (!value) return "Title is required"; + return null; + }, + }, + }); + + const addRowUnder = (index: number) => { + const newBody = [...form.values.body]; + newBody.splice(index + 1, 0, { + expect: "", + send: "", + delay: "0", + repeat: "1", + note: "", + }); + form.setFieldValue("body", newBody); + }; + + const deleteRow = (index: number) => { + const newBody = [...form.values.body]; + newBody.splice(index, 1); + form.setFieldValue("body", newBody); + }; + + const handleSave = async () => { + setIsSubmit(true); + try { + const body = form.values.body.map((el: IBodyScenario) => ({ + ...el, + delay: Number(el.delay), + repeat: Number(el.repeat), + })); + + const payload = { + ...form.values, + body: body, + timeout: Number(form.values.timeout), + }; + const url = isEdit ? "api/scenarios/update" : "api/scenarios/create"; + const res = await axios.post( + apiUrl + url, + isEdit ? { ...payload, id: dataScenario?.id } : payload + ); + if (res.data.status === true) { + const scenario = res.data.data; + setScenarios((pre) => + isEdit + ? pre.map((el) => + el.id === scenario.id ? { ...el, ...scenario } : el + ) + : [...pre, scenario] + ); + notifications.show({ + title: "Success", + message: res.data.message, + color: "green", + }); + return; + } + } catch (error) { + console.log(error); + } finally { + setIsSubmit(false); + } + }; + + const handleDelete = async () => { + try { + const response = await axios.post(apiUrl + "api/scenarios/delete", { + id: dataScenario?.id, + }); + if (response.data.status) { + setScenarios((pre) => pre.filter((el) => el.id !== dataScenario?.id)); + notifications.show({ + title: "Success", + message: response.data.message, + color: "green", + }); + } + } catch (error) { + console.log(error); + notifications.show({ + title: "Error", + message: "Error delete scenario", + color: "red", + }); + } + }; + + useEffect(() => { + if (!opened) { + setIsEdit(false); + setIsSubmit(false); + setDataScenario(undefined); + form.reset(); + } + }, [opened]); + + return ( + <> + + + + {scenarios.map((scenario) => ( + + ))} + + + + + + + form.setFieldValue("title", e.target.value) + } + required + /> + + + + form.setFieldValue("timeout", e.target.value) + } + required + /> + + +
+ {isEdit && ( + + )} + +
+
+
+
+
+ + + + + + # + + {/* Expect */} + Expect + + Send + Delay(ms) + Repeat + + + + + {form.values.body.map( + (element: IBodyScenario, i: number) => ( + + ) + )} + +
+
+
+
+
+
+ + { + open(); + }} + > + + + + { + setOpenConfirm(false); + }} + message={"Are you sure delete this station?"} + handle={() => { + setOpenConfirm(false); + handleDelete(); + close(); + }} + centered={true} + /> + + ); +} + +export default DrawerScenario; diff --git a/FRONTEND/src/components/FormAddEdit.tsx b/FRONTEND/src/components/FormAddEdit.tsx new file mode 100644 index 0000000..808db9b --- /dev/null +++ b/FRONTEND/src/components/FormAddEdit.tsx @@ -0,0 +1,663 @@ +import { + ActionIcon, + Box, + Button, + Flex, + Group, + Modal, + NumberInput, + PasswordInput, + Select, + Table, + TextInput, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { useEffect, useState } from "react"; +import type { TLine, TStation } from "../untils/types"; +import DialogConfirm from "./DialogConfirm"; +import axios from "axios"; +import { notifications } from "@mantine/notifications"; +import { IconInputX } from "@tabler/icons-react"; + +const apiUrl = import.meta.env.VITE_BACKEND_URL; + +const lineInit = { + port: 0, + lineNumber: 0, + lineClear: 0, + station_id: 0, + apc_name: "", +}; + +const StationSetting = ({ + isOpen, + isEdit, + onClose, + dataStation, + setStations, + setActiveTab, +}: { + isOpen: boolean; + isEdit: boolean; + onClose: () => void; + dataStation?: TStation; + setStations: (value: React.SetStateAction) => void; + setActiveTab: () => void; +}) => { + const [lines, setLines] = useState([lineInit]); + const [openConfirm, setOpenConfirm] = useState(false); + + const ipRegex = + /(\b25[0-5]|\b2[0-4][0-9]|\b1[0-9]{2}|\b[1-9]?[0-9])\.(\b25[0-5]|\b2[0-4][0-9]|\b1[0-9]{2}|\b[1-9]?[0-9])\.(\b25[0-5]|\b2[0-4][0-9]|\b1[0-9]{2}|\b[1-9]?[0-9])\.(\b25[0-5]|\b2[0-4][0-9]|\b1[0-9]{2}|\b[1-9]?[0-9])\b/g; + const form = useForm({ + initialValues: dataStation, + validate: (values: TStation) => ({ + ip: !ipRegex.test(values.ip) ? "IP address invalid" : null, + }), + }); + + useEffect(() => { + if (dataStation) { + form.setFieldValue("name", dataStation.name); + form.setFieldValue("ip", dataStation.ip); + form.setFieldValue("port", dataStation.port); + form.setFieldValue("netmask", dataStation.netmask); + form.setFieldValue("network", dataStation.network); + form.setFieldValue("gateway", dataStation.gateway); + form.setFieldValue("tftp_ip", dataStation.tftp_ip); + form.setFieldValue("apc_1_ip", dataStation.apc_1_ip); + form.setFieldValue("apc_1_port", dataStation.apc_1_port); + form.setFieldValue("apc_1_username", dataStation.apc_1_username); + form.setFieldValue("apc_1_password", dataStation.apc_1_password); + form.setFieldValue("apc_2_ip", dataStation.apc_2_ip); + form.setFieldValue("apc_2_port", dataStation.apc_2_port); + form.setFieldValue("apc_2_username", dataStation.apc_2_username); + form.setFieldValue("apc_2_password", dataStation.apc_2_password); + form.setFieldValue("switch_control_ip", dataStation.switch_control_ip); + form.setFieldValue( + "switch_control_port", + dataStation.switch_control_port + ); + form.setFieldValue( + "switch_control_username", + dataStation.switch_control_username + ); + form.setFieldValue( + "switch_control_password", + dataStation.switch_control_password + ); + + const dataLine = dataStation.lines.map((value) => ({ + id: value.id, + lineNumber: value.line_number || 0, + port: value.port, + lineClear: value.line_clear || 0, + apc_name: value.apc_name, + outlet: value.outlet, + station_id: value.station_id, + })); + setLines(dataLine); + } + }, [dataStation]); + + useEffect(() => { + if (lines.length > 0) { + const lastLine = lines[lines.length - 1]; + if (lastLine?.lineNumber || lastLine?.port) + setLines((pre) => [...pre, lineInit]); + } + }, [lines]); + + useEffect(() => { + if (!isOpen) { + setLines([lineInit]); + setOpenConfirm(false); + form.reset(); + } + }, [isOpen]); + + const renderLinesTable = () => { + const rows = lines?.map((row: TLine, index: number) => { + return ( + + + + setLines((pre) => + pre.map((value, i) => + i === index ? { ...value, lineNumber: Number(e!) } : value + ) + ) + } + /> + + + + setLines((pre) => + pre.map((value, i) => + i === index ? { ...value, port: Number(e!) } : value + ) + ) + } + /> + + + + setLines((pre) => + pre.map((value, i) => + i === index ? { ...value, lineClear: Number(e!) } : value + ) + ) + } + /> + + +