Compare commits
	
		
			2 Commits
		
	
	
		
			dea4d2b804
			...
			cbc8397ea8
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						cbc8397ea8 | |
| 
							
							
								 | 
						0a0dd559f0 | 
| 
						 | 
					@ -23,3 +23,5 @@ yarn-error.log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Platform specific
 | 
					# Platform specific
 | 
				
			||||||
.DS_Store
 | 
					.DS_Store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					storage/system_logs
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,9 +4,20 @@ import User from '../models/user.js'
 | 
				
			||||||
export default class AuthController {
 | 
					export default class AuthController {
 | 
				
			||||||
  // Đăng ký
 | 
					  // Đăng ký
 | 
				
			||||||
  async register({ request, response }: HttpContext) {
 | 
					  async register({ request, response }: HttpContext) {
 | 
				
			||||||
    const data = request.only(['email', 'password', 'full_name'])
 | 
					    try {
 | 
				
			||||||
    const user = await User.create(data)
 | 
					      const data = request.only(['email', 'password', 'full_name'])
 | 
				
			||||||
    return response.json({ message: 'User created', user })
 | 
					
 | 
				
			||||||
 | 
					      const user = await User.query().where('email', data.email).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (user) {
 | 
				
			||||||
 | 
					        return response.status(401).json({ status: false, message: 'Email 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' })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Đăng nhập
 | 
					  // Đăng nhập
 | 
				
			||||||
| 
						 | 
					@ -24,11 +35,9 @@ export default class AuthController {
 | 
				
			||||||
        return response.status(401).json({ message: 'Invalid email or password' })
 | 
					        return response.status(401).json({ message: 'Invalid email or password' })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // ✅ Nếu dùng token thủ công:
 | 
					 | 
				
			||||||
      const token = Math.random().toString(36).substring(2) // hoặc JWT nếu bạn cài auth
 | 
					 | 
				
			||||||
      return response.json({
 | 
					      return response.json({
 | 
				
			||||||
        message: 'Login successful',
 | 
					        message: 'Login successful',
 | 
				
			||||||
        user: { id: user.id, email: user.email, token },
 | 
					        user: { id: user.id, email: user.email, fullName: user.fullName },
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    } catch {
 | 
					    } catch {
 | 
				
			||||||
      return response.status(401).json({ message: 'Invalid credentials' })
 | 
					      return response.status(401).json({ message: 'Invalid credentials' })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,6 @@
 | 
				
			||||||
import Scenario from '#models/scenario'
 | 
					import Scenario from '#models/scenario'
 | 
				
			||||||
import type { HttpContext } from '@adonisjs/core/http'
 | 
					import type { HttpContext } from '@adonisjs/core/http'
 | 
				
			||||||
import { searchRequest } from '../utils/hasPaginationRequest.js'
 | 
					 | 
				
			||||||
import db from '@adonisjs/lucid/services/db'
 | 
					import db from '@adonisjs/lucid/services/db'
 | 
				
			||||||
import UserScenarios from '#models/user_scenario'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ScenariosController {
 | 
					export default class ScenariosController {
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
| 
						 | 
					@ -36,7 +34,6 @@ export default class ScenariosController {
 | 
				
			||||||
  async create({ request, response, auth }: HttpContext) {
 | 
					  async create({ request, response, auth }: HttpContext) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const payload = await request.all()
 | 
					      const payload = await request.all()
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const trx = await db.transaction()
 | 
					      const trx = await db.transaction()
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const scenario = await Scenario.create(
 | 
					        const scenario = await Scenario.create(
 | 
				
			||||||
| 
						 | 
					@ -44,7 +41,7 @@ export default class ScenariosController {
 | 
				
			||||||
            title: payload.title.trim(),
 | 
					            title: payload.title.trim(),
 | 
				
			||||||
            body: JSON.stringify(payload.body),
 | 
					            body: JSON.stringify(payload.body),
 | 
				
			||||||
            timeout: payload.timeout,
 | 
					            timeout: payload.timeout,
 | 
				
			||||||
            isReboot: payload.is_reboot,
 | 
					            isReboot: payload.isReboot,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          { client: trx }
 | 
					          { client: trx }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import net from 'node:net'
 | 
					import net from 'node:net'
 | 
				
			||||||
import { cleanData, sleep } from '../ultils/helper.js'
 | 
					import { appendLog, cleanData, sleep } from '../ultils/helper.js'
 | 
				
			||||||
import Scenario from '#models/scenario'
 | 
					import Scenario from '#models/scenario'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface LineConfig {
 | 
					interface LineConfig {
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,14 @@ interface LineConfig {
 | 
				
			||||||
  apcName?: string
 | 
					  apcName?: string
 | 
				
			||||||
  output: string
 | 
					  output: string
 | 
				
			||||||
  status: string
 | 
					  status: string
 | 
				
			||||||
 | 
					  openCLI: boolean
 | 
				
			||||||
 | 
					  userEmailOpenCLI: string
 | 
				
			||||||
 | 
					  userOpenCLI: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface User {
 | 
				
			||||||
 | 
					  userEmail: string
 | 
				
			||||||
 | 
					  userName: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class LineConnection {
 | 
					export default class LineConnection {
 | 
				
			||||||
| 
						 | 
					@ -77,6 +85,7 @@ export default class LineConnection {
 | 
				
			||||||
          lineId: id,
 | 
					          lineId: id,
 | 
				
			||||||
          data: message,
 | 
					          data: message,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					        appendLog(cleanData(message), this.config.stationId, this.config.id)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.client.on('error', (err) => {
 | 
					      this.client.on('error', (err) => {
 | 
				
			||||||
| 
						 | 
					@ -149,6 +158,11 @@ export default class LineConnection {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.isRunningScript = true
 | 
					    this.isRunningScript = true
 | 
				
			||||||
 | 
					    appendLog(
 | 
				
			||||||
 | 
					      `\n\n---start-scenarios---${Date.now()}---\n---scenario---${script?.title}---${Date.now()}---\n`,
 | 
				
			||||||
 | 
					      this.config.stationId,
 | 
				
			||||||
 | 
					      this.config.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
 | 
					    const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
 | 
				
			||||||
    let stepIndex = 0
 | 
					    let stepIndex = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -156,6 +170,13 @@ export default class LineConnection {
 | 
				
			||||||
      const timeoutTimer = setTimeout(() => {
 | 
					      const timeoutTimer = setTimeout(() => {
 | 
				
			||||||
        this.isRunningScript = false
 | 
					        this.isRunningScript = false
 | 
				
			||||||
        this.outputBuffer = ''
 | 
					        this.outputBuffer = ''
 | 
				
			||||||
 | 
					        this.config.output += 'Timeout run scenario'
 | 
				
			||||||
 | 
					        this.socketIO.emit('line_output', {
 | 
				
			||||||
 | 
					          stationId: this.config.stationId,
 | 
				
			||||||
 | 
					          lineId: this.config.id,
 | 
				
			||||||
 | 
					          data: 'Timeout run scenario',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        appendLog(`\n---end-scenarios---${Date.now()}---\n`, this.config.stationId, this.config.id)
 | 
				
			||||||
        // reject(new Error('Script timeout'))
 | 
					        // reject(new Error('Script timeout'))
 | 
				
			||||||
      }, script.timeout || 300000)
 | 
					      }, script.timeout || 300000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -164,11 +185,21 @@ export default class LineConnection {
 | 
				
			||||||
          clearTimeout(timeoutTimer)
 | 
					          clearTimeout(timeoutTimer)
 | 
				
			||||||
          this.isRunningScript = false
 | 
					          this.isRunningScript = false
 | 
				
			||||||
          this.outputBuffer = ''
 | 
					          this.outputBuffer = ''
 | 
				
			||||||
 | 
					          appendLog(
 | 
				
			||||||
 | 
					            `\n---end-scenarios---${Date.now()}---\n`,
 | 
				
			||||||
 | 
					            this.config.stationId,
 | 
				
			||||||
 | 
					            this.config.id
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
          resolve(true)
 | 
					          resolve(true)
 | 
				
			||||||
          return
 | 
					          return
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const step = steps[index]
 | 
					        const step = steps[index]
 | 
				
			||||||
 | 
					        appendLog(
 | 
				
			||||||
 | 
					          `\n---send-command---"${step?.send ?? ''}"---${Date.now()}---\n`,
 | 
				
			||||||
 | 
					          this.config.stationId,
 | 
				
			||||||
 | 
					          this.config.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        let repeatCount = Number(step.repeat) || 1
 | 
					        let repeatCount = Number(step.repeat) || 1
 | 
				
			||||||
        const sendCommand = () => {
 | 
					        const sendCommand = () => {
 | 
				
			||||||
          if (repeatCount <= 0) {
 | 
					          if (repeatCount <= 0) {
 | 
				
			||||||
| 
						 | 
					@ -203,4 +234,27 @@ export default class LineConnection {
 | 
				
			||||||
      runStep(stepIndex)
 | 
					      runStep(stepIndex)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  userOpenCLI(user: User) {
 | 
				
			||||||
 | 
					    this.config.openCLI = true
 | 
				
			||||||
 | 
					    this.config.userEmailOpenCLI = user.userEmail
 | 
				
			||||||
 | 
					    this.config.userOpenCLI = user.userName
 | 
				
			||||||
 | 
					    this.socketIO.emit('user_open_cli', {
 | 
				
			||||||
 | 
					      stationId: this.config.stationId,
 | 
				
			||||||
 | 
					      lineId: this.config.id,
 | 
				
			||||||
 | 
					      userEmailOpenCLI: user.userEmail,
 | 
				
			||||||
 | 
					      userOpenCLI: user.userName,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  userCloseCLI() {
 | 
				
			||||||
 | 
					    this.config.openCLI = false
 | 
				
			||||||
 | 
					    this.config.userEmailOpenCLI = ''
 | 
				
			||||||
 | 
					    this.config.userOpenCLI = ''
 | 
				
			||||||
 | 
					    this.socketIO.emit('user_close_cli', {
 | 
				
			||||||
 | 
					      stationId: this.config.stationId,
 | 
				
			||||||
 | 
					      lineId: this.config.id,
 | 
				
			||||||
 | 
					      userEmailOpenCLI: '',
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,6 @@
 | 
				
			||||||
 | 
					import fs from 'node:fs'
 | 
				
			||||||
 | 
					import path from 'node:path'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Function to clean up unwanted characters from the output data.
 | 
					 * Function to clean up unwanted characters from the output data.
 | 
				
			||||||
 * @param {string} data - The raw data to be cleaned.
 | 
					 * @param {string} data - The raw data to be cleaned.
 | 
				
			||||||
| 
						 | 
					@ -16,3 +19,20 @@ export const cleanData = (data: string) => {
 | 
				
			||||||
export function sleep(ms: number) {
 | 
					export function sleep(ms: number) {
 | 
				
			||||||
  return new Promise((resolve) => setTimeout(resolve, ms))
 | 
					  return new Promise((resolve) => setTimeout(resolve, ms))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function appendLog(output: string, stationId: number, lineId: number) {
 | 
				
			||||||
 | 
					  const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
 | 
				
			||||||
 | 
					  const logDir = path.join('storage', 'system_logs')
 | 
				
			||||||
 | 
					  const logFile = path.join(logDir, `${date}-Station_${stationId}-Line_${lineId}.log`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Ensure folder exists
 | 
				
			||||||
 | 
					  if (!fs.existsSync(logDir)) {
 | 
				
			||||||
 | 
					    fs.mkdirSync(logDir, { recursive: true })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fs.appendFile(logFile, output, (err) => {
 | 
				
			||||||
 | 
					    if (err) {
 | 
				
			||||||
 | 
					      console.error('❌ Failed to write log:', err.message)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ export default class extends BaseSchema {
 | 
				
			||||||
      table.string('title').notNullable()
 | 
					      table.string('title').notNullable()
 | 
				
			||||||
      table.text('body').notNullable()
 | 
					      table.text('body').notNullable()
 | 
				
			||||||
      table.integer('timeout').notNullable()
 | 
					      table.integer('timeout').notNullable()
 | 
				
			||||||
      table.boolean('isReboot').defaultTo(false)
 | 
					      table.boolean('is_reboot').defaultTo(false)
 | 
				
			||||||
      table.timestamps()
 | 
					      table.timestamps()
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,6 +51,7 @@ export class WebSocketIo {
 | 
				
			||||||
  stationMap: Map<number, Station> = new Map()
 | 
					  stationMap: Map<number, Station> = new Map()
 | 
				
			||||||
  lineMap: Map<number, LineConnection> = new Map() // key = lineId
 | 
					  lineMap: Map<number, LineConnection> = new Map() // key = lineId
 | 
				
			||||||
  lineConnecting: number[] = [] // key = lineId
 | 
					  lineConnecting: number[] = [] // key = lineId
 | 
				
			||||||
 | 
					  userConnecting: Map<number, { userId: number; userName: string }> = new Map()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(protected app: ApplicationService) {}
 | 
					  constructor(protected app: ApplicationService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,8 +71,14 @@ export class WebSocketIo {
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    io.on('connection', (socket: CustomSocket) => {
 | 
					    io.on('connection', (socket: CustomSocket) => {
 | 
				
			||||||
 | 
					      const { userId, userName } = socket.handshake.auth
 | 
				
			||||||
      console.log('Socket connected:', socket.id)
 | 
					      console.log('Socket connected:', socket.id)
 | 
				
			||||||
      socket.connectionTime = new Date()
 | 
					      socket.connectionTime = new Date()
 | 
				
			||||||
 | 
					      this.userConnecting.set(userId, { userId, userName })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        io.emit('user_connecting', Array.from(this.userConnecting.values()))
 | 
				
			||||||
 | 
					      }, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      setTimeout(() => {
 | 
					      setTimeout(() => {
 | 
				
			||||||
        io.to(socket.id).emit(
 | 
					        io.to(socket.id).emit(
 | 
				
			||||||
| 
						 | 
					@ -82,6 +89,10 @@ export class WebSocketIo {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      socket.on('disconnect', () => {
 | 
					      socket.on('disconnect', () => {
 | 
				
			||||||
        console.log(`FE disconnected: ${socket.id}`)
 | 
					        console.log(`FE disconnected: ${socket.id}`)
 | 
				
			||||||
 | 
					        this.userConnecting.delete(userId)
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					          io.emit('user_connecting', Array.from(this.userConnecting.values()))
 | 
				
			||||||
 | 
					        }, 200)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // FE gửi yêu cầu connect lines
 | 
					      // FE gửi yêu cầu connect lines
 | 
				
			||||||
| 
						 | 
					@ -94,8 +105,8 @@ export class WebSocketIo {
 | 
				
			||||||
        const { lineIds, stationId, command } = data
 | 
					        const { lineIds, stationId, command } = data
 | 
				
			||||||
        for (const lineId of lineIds) {
 | 
					        for (const lineId of lineIds) {
 | 
				
			||||||
          const line = this.lineMap.get(lineId)
 | 
					          const line = this.lineMap.get(lineId)
 | 
				
			||||||
          if (line) {
 | 
					          if (line && line.config.status === 'connected') {
 | 
				
			||||||
            this.lineConnecting.filter((el) => el !== lineId)
 | 
					            this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
 | 
				
			||||||
            this.setTimeoutConnect(lineId, line)
 | 
					            this.setTimeoutConnect(lineId, line)
 | 
				
			||||||
            line.writeCommand(command)
 | 
					            line.writeCommand(command)
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
| 
						 | 
					@ -107,7 +118,7 @@ export class WebSocketIo {
 | 
				
			||||||
              await this.connectLine(io, [linesData], stationData)
 | 
					              await this.connectLine(io, [linesData], stationData)
 | 
				
			||||||
              const lineReconnect = this.lineMap.get(lineId)
 | 
					              const lineReconnect = this.lineMap.get(lineId)
 | 
				
			||||||
              if (lineReconnect) {
 | 
					              if (lineReconnect) {
 | 
				
			||||||
                this.lineConnecting.filter((el) => el !== lineId)
 | 
					                this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
 | 
				
			||||||
                this.setTimeoutConnect(lineId, lineReconnect)
 | 
					                this.setTimeoutConnect(lineId, lineReconnect)
 | 
				
			||||||
                lineReconnect.writeCommand(command)
 | 
					                lineReconnect.writeCommand(command)
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
| 
						 | 
					@ -127,7 +138,7 @@ export class WebSocketIo {
 | 
				
			||||||
        const lineId = data.id
 | 
					        const lineId = data.id
 | 
				
			||||||
        const scenario = data.scenario
 | 
					        const scenario = data.scenario
 | 
				
			||||||
        const line = this.lineMap.get(lineId)
 | 
					        const line = this.lineMap.get(lineId)
 | 
				
			||||||
        if (line) {
 | 
					        if (line && line.config.status === 'connected') {
 | 
				
			||||||
          this.setTimeoutConnect(
 | 
					          this.setTimeoutConnect(
 | 
				
			||||||
            lineId,
 | 
					            lineId,
 | 
				
			||||||
            line,
 | 
					            line,
 | 
				
			||||||
| 
						 | 
					@ -155,9 +166,44 @@ export class WebSocketIo {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // FE yêu cầu ngắt kết nối 1 station
 | 
					      socket.on('open_cli', async (data) => {
 | 
				
			||||||
      socket.on('disconnect_station', (stationId) => {
 | 
					        const { lineId, userEmail, userName: name, stationId } = data
 | 
				
			||||||
        this.disconnectStation(stationId)
 | 
					        const line = this.lineMap.get(lineId)
 | 
				
			||||||
 | 
					        if (line) {
 | 
				
			||||||
 | 
					          line.userOpenCLI({ userEmail, userName: name })
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          if (this.lineConnecting.includes(lineId)) return
 | 
				
			||||||
 | 
					          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) {
 | 
				
			||||||
 | 
					              lineReconnect.userOpenCLI({ userEmail, userName: name })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      socket.on('close_cli', async (data) => {
 | 
				
			||||||
 | 
					        const { lineId, stationId } = data
 | 
				
			||||||
 | 
					        const line = this.lineMap.get(lineId)
 | 
				
			||||||
 | 
					        if (line) {
 | 
				
			||||||
 | 
					          line.userCloseCLI()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          if (this.lineConnecting.includes(lineId)) return
 | 
				
			||||||
 | 
					          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) {
 | 
				
			||||||
 | 
					              lineReconnect.userCloseCLI()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -182,6 +228,9 @@ export class WebSocketIo {
 | 
				
			||||||
            apcName: line.apcName,
 | 
					            apcName: line.apcName,
 | 
				
			||||||
            output: '',
 | 
					            output: '',
 | 
				
			||||||
            status: '',
 | 
					            status: '',
 | 
				
			||||||
 | 
					            openCLI: false,
 | 
				
			||||||
 | 
					            userEmailOpenCLI: '',
 | 
				
			||||||
 | 
					            userOpenCLI: '',
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          socket
 | 
					          socket
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -195,22 +244,6 @@ export class WebSocketIo {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private disconnectStation(stationId: number) {
 | 
					 | 
				
			||||||
    const station = this.stationMap.get(stationId)
 | 
					 | 
				
			||||||
    if (!station) return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const line of station.lines) {
 | 
					 | 
				
			||||||
      const conn = this.lineMap.get(line.id)
 | 
					 | 
				
			||||||
      if (conn) {
 | 
					 | 
				
			||||||
        conn.disconnect()
 | 
					 | 
				
			||||||
        this.lineMap.delete(line.id)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.stationMap.delete(stationId)
 | 
					 | 
				
			||||||
    console.log(`🔻 Station ${station.name} disconnected`)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 120000) => {
 | 
					  private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 120000) => {
 | 
				
			||||||
    if (this.intervalMap[`${lineId}`]) {
 | 
					    if (this.intervalMap[`${lineId}`]) {
 | 
				
			||||||
      clearInterval(this.intervalMap[`${lineId}`])
 | 
					      clearInterval(this.intervalMap[`${lineId}`])
 | 
				
			||||||
| 
						 | 
					@ -218,7 +251,7 @@ export class WebSocketIo {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const interval = setInterval(() => {
 | 
					    const interval = setInterval(() => {
 | 
				
			||||||
      lineConn.disconnect()
 | 
					      lineConn.disconnect()
 | 
				
			||||||
      this.lineMap.delete(lineId)
 | 
					      // this.lineMap.delete(lineId)
 | 
				
			||||||
      if (this.intervalMap[`${lineId}`]) {
 | 
					      if (this.intervalMap[`${lineId}`]) {
 | 
				
			||||||
        clearInterval(this.intervalMap[`${lineId}`])
 | 
					        clearInterval(this.intervalMap[`${lineId}`])
 | 
				
			||||||
        delete this.intervalMap[`${lineId}`]
 | 
					        delete this.intervalMap[`${lineId}`]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,5 +67,6 @@ router
 | 
				
			||||||
router
 | 
					router
 | 
				
			||||||
  .group(() => {
 | 
					  .group(() => {
 | 
				
			||||||
    router.post('/login', '#controllers/auth_controller.login')
 | 
					    router.post('/login', '#controllers/auth_controller.login')
 | 
				
			||||||
 | 
					    router.post('/register', '#controllers/auth_controller.register')
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  .prefix('api/auth')
 | 
					  .prefix('api/auth')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@
 | 
				
			||||||
        "axios": "^1.12.2",
 | 
					        "axios": "^1.12.2",
 | 
				
			||||||
        "react": "^19.1.1",
 | 
					        "react": "^19.1.1",
 | 
				
			||||||
        "react-dom": "^19.1.1",
 | 
					        "react-dom": "^19.1.1",
 | 
				
			||||||
 | 
					        "react-router-dom": "^7.9.4",
 | 
				
			||||||
        "socket.io-client": "^4.8.1",
 | 
					        "socket.io-client": "^4.8.1",
 | 
				
			||||||
        "xterm": "^5.3.0"
 | 
					        "xterm": "^5.3.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -2216,6 +2217,15 @@
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/cookie": {
 | 
				
			||||||
 | 
					      "version": "1.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=18"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/cross-spawn": {
 | 
					    "node_modules/cross-spawn": {
 | 
				
			||||||
      "version": "7.0.6",
 | 
					      "version": "7.0.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 | 
				
			||||||
| 
						 | 
					@ -3595,6 +3605,44 @@
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-router": {
 | 
				
			||||||
 | 
					      "version": "7.9.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "cookie": "^1.0.1",
 | 
				
			||||||
 | 
					        "set-cookie-parser": "^2.6.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "react": ">=18",
 | 
				
			||||||
 | 
					        "react-dom": ">=18"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependenciesMeta": {
 | 
				
			||||||
 | 
					        "react-dom": {
 | 
				
			||||||
 | 
					          "optional": true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/react-router-dom": {
 | 
				
			||||||
 | 
					      "version": "7.9.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "react-router": "7.9.4"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "react": ">=18",
 | 
				
			||||||
 | 
					        "react-dom": ">=18"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/react-style-singleton": {
 | 
					    "node_modules/react-style-singleton": {
 | 
				
			||||||
      "version": "2.2.3",
 | 
					      "version": "2.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -3753,6 +3801,12 @@
 | 
				
			||||||
        "semver": "bin/semver.js"
 | 
					        "semver": "bin/semver.js"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/set-cookie-parser": {
 | 
				
			||||||
 | 
					      "version": "2.7.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/shebang-command": {
 | 
					    "node_modules/shebang-command": {
 | 
				
			||||||
      "version": "2.0.0",
 | 
					      "version": "2.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@
 | 
				
			||||||
    "axios": "^1.12.2",
 | 
					    "axios": "^1.12.2",
 | 
				
			||||||
    "react": "^19.1.1",
 | 
					    "react": "^19.1.1",
 | 
				
			||||||
    "react-dom": "^19.1.1",
 | 
					    "react-dom": "^19.1.1",
 | 
				
			||||||
 | 
					    "react-router-dom": "^7.9.4",
 | 
				
			||||||
    "socket.io-client": "^4.8.1",
 | 
					    "socket.io-client": "^4.8.1",
 | 
				
			||||||
    "xterm": "^5.3.0"
 | 
					    "xterm": "^5.3.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 104 KiB  | 
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
body {
 | 
					body {
 | 
				
			||||||
  font-family: 'Mulish', sans-serif;
 | 
					  font-family: "Mulish", sans-serif;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.list {
 | 
					.list {
 | 
				
			||||||
| 
						 | 
					@ -48,7 +48,25 @@ body {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.content{
 | 
					.content {
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  border-top: 1px #ccc solid;
 | 
					  border-top: 1px #ccc solid;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.userName {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  color: #222;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  margin-right: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.userName::after {
 | 
				
			||||||
 | 
					  content: "";
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  height: 1px;
 | 
				
			||||||
 | 
					  background-color: #007bff; /* blue accent */
 | 
				
			||||||
 | 
					  margin: 0.1rem auto 0;
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import "@mantine/notifications/styles.css";
 | 
				
			||||||
import "./App.css";
 | 
					import "./App.css";
 | 
				
			||||||
import classes from "./App.module.css";
 | 
					import classes from "./App.module.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Suspense, useEffect, useState } from "react";
 | 
					import { Suspense, useEffect, useMemo, useState } from "react";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Tabs,
 | 
					  Tabs,
 | 
				
			||||||
  Text,
 | 
					  Text,
 | 
				
			||||||
| 
						 | 
					@ -17,8 +17,16 @@ import {
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  ActionIcon,
 | 
					  ActionIcon,
 | 
				
			||||||
  LoadingOverlay,
 | 
					  LoadingOverlay,
 | 
				
			||||||
 | 
					  Avatar,
 | 
				
			||||||
 | 
					  Tooltip,
 | 
				
			||||||
} from "@mantine/core";
 | 
					} from "@mantine/core";
 | 
				
			||||||
import type { IScenario, LineConfig, TLine, TStation } from "./untils/types";
 | 
					import type {
 | 
				
			||||||
 | 
					  IScenario,
 | 
				
			||||||
 | 
					  LineConfig,
 | 
				
			||||||
 | 
					  TLine,
 | 
				
			||||||
 | 
					  TStation,
 | 
				
			||||||
 | 
					  TUser,
 | 
				
			||||||
 | 
					} from "./untils/types";
 | 
				
			||||||
import axios from "axios";
 | 
					import axios from "axios";
 | 
				
			||||||
import CardLine from "./components/CardLine";
 | 
					import CardLine from "./components/CardLine";
 | 
				
			||||||
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
 | 
					import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
 | 
				
			||||||
| 
						 | 
					@ -27,6 +35,8 @@ import { ButtonDPELP, ButtonScenario } from "./components/ButtonAction";
 | 
				
			||||||
import StationSetting from "./components/FormAddEdit";
 | 
					import StationSetting from "./components/FormAddEdit";
 | 
				
			||||||
import DrawerScenario from "./components/DrawerScenario";
 | 
					import DrawerScenario from "./components/DrawerScenario";
 | 
				
			||||||
import { Notifications } from "@mantine/notifications";
 | 
					import { Notifications } from "@mantine/notifications";
 | 
				
			||||||
 | 
					import ModalTerminal from "./components/ModalTerminal";
 | 
				
			||||||
 | 
					import PageLogin from "./components/Authentication/LoginPage";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
					const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,6 +44,14 @@ const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
				
			||||||
 * Main Component
 | 
					 * Main Component
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function App() {
 | 
					function App() {
 | 
				
			||||||
 | 
					  const user = useMemo(() => {
 | 
				
			||||||
 | 
					    return localStorage.getItem("user") &&
 | 
				
			||||||
 | 
					      typeof localStorage.getItem("user") === "string"
 | 
				
			||||||
 | 
					      ? JSON.parse(localStorage.getItem("user") || "")
 | 
				
			||||||
 | 
					      : null;
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  if (!user) window.location.href = "/";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  document.title = "Automation Test";
 | 
					  document.title = "Automation Test";
 | 
				
			||||||
  const { socket } = useSocket();
 | 
					  const { socket } = useSocket();
 | 
				
			||||||
  const [stations, setStations] = useState<TStation[]>([]);
 | 
					  const [stations, setStations] = useState<TStation[]>([]);
 | 
				
			||||||
| 
						 | 
					@ -53,6 +71,10 @@ function App() {
 | 
				
			||||||
  const [isEditStation, setIsEditStation] = useState(false);
 | 
					  const [isEditStation, setIsEditStation] = useState(false);
 | 
				
			||||||
  const [stationEdit, setStationEdit] = useState<TStation | undefined>();
 | 
					  const [stationEdit, setStationEdit] = useState<TStation | undefined>();
 | 
				
			||||||
  const [scenarios, setScenarios] = useState<IScenario[]>([]);
 | 
					  const [scenarios, setScenarios] = useState<IScenario[]>([]);
 | 
				
			||||||
 | 
					  const [openModalTerminal, setOpenModalTerminal] = useState(false);
 | 
				
			||||||
 | 
					  const [selectedLine, setSelectedLine] = useState<TLine | undefined>();
 | 
				
			||||||
 | 
					  const [loadingTerminal, setLoadingTerminal] = useState(true);
 | 
				
			||||||
 | 
					  const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // function get list station
 | 
					  // function get list station
 | 
				
			||||||
  const getStation = async () => {
 | 
					  const getStation = async () => {
 | 
				
			||||||
| 
						 | 
					@ -93,24 +115,74 @@ function App() {
 | 
				
			||||||
    if (!socket || !stations?.length) return;
 | 
					    if (!socket || !stations?.length) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket.on("line_connected", updateStatus);
 | 
					    socket.on("line_connected", updateStatus);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket.on("line_disconnected", updateStatus);
 | 
					    socket.on("line_disconnected", updateStatus);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket?.on("line_output", (data) => {
 | 
				
			||||||
 | 
					      updateValueLineStation(data?.lineId, "netOutput", data.data);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket?.on("line_error", (data) => {
 | 
				
			||||||
 | 
					      updateValueLineStation(data?.lineId, "netOutput", data.error);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket?.on("init", (data) => {
 | 
				
			||||||
 | 
					      if (Array.isArray(data)) {
 | 
				
			||||||
 | 
					        data.forEach((value) => {
 | 
				
			||||||
 | 
					          updateValueLineStation(value?.id, "netOutput", value.output);
 | 
				
			||||||
 | 
					          updateStatus({ ...value, lineId: value.id });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket?.on("user_connecting", (data) => {
 | 
				
			||||||
 | 
					      if (Array.isArray(data)) {
 | 
				
			||||||
 | 
					        setUsersConnecting(data);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket?.on("user_open_cli", (data) => {
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        updateValueLineStation(data?.lineId, "cliOpened", true);
 | 
				
			||||||
 | 
					        updateValueLineStation(
 | 
				
			||||||
 | 
					          data?.lineId,
 | 
				
			||||||
 | 
					          "userEmailOpenCLI",
 | 
				
			||||||
 | 
					          data?.userEmailOpenCLI
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        updateValueLineStation(data?.lineId, "userOpenCLI", data?.userOpenCLI);
 | 
				
			||||||
 | 
					      }, 100);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    socket?.on("user_close_cli", (data) => {
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        updateValueLineStation(data?.lineId, "cliOpened", false);
 | 
				
			||||||
 | 
					        updateValueLineStation(data?.lineId, "userEmailOpenCLI", "");
 | 
				
			||||||
 | 
					        updateValueLineStation(data?.lineId, "userOpenCLI", "");
 | 
				
			||||||
 | 
					      }, 100);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // ✅ cleanup on unmount or when socket changes
 | 
					    // ✅ cleanup on unmount or when socket changes
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
 | 
					      socket.off("init");
 | 
				
			||||||
 | 
					      socket.off("line_output");
 | 
				
			||||||
 | 
					      socket.off("line_error");
 | 
				
			||||||
      socket.off("line_connected");
 | 
					      socket.off("line_connected");
 | 
				
			||||||
      socket.off("line_disconnected");
 | 
					      socket.off("line_disconnected");
 | 
				
			||||||
 | 
					      socket.off("user_connecting");
 | 
				
			||||||
 | 
					      socket.off("user_open_cli");
 | 
				
			||||||
 | 
					      socket.off("user_close_cli");
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, [socket, stations]);
 | 
					  }, [socket, stations]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateStatus = (data: LineConfig) => {
 | 
					  const updateStatus = (data: LineConfig) => {
 | 
				
			||||||
    const line = getLine(data.lineId, data.stationId);
 | 
					    const line = getLine(data.lineId, data.stationId);
 | 
				
			||||||
    if (line) {
 | 
					    if (line?.id) {
 | 
				
			||||||
      updateValueLineStation(line, "status", data.status);
 | 
					      updateValueLineStation(line.id, "status", data.status);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateValueLineStation = <K extends keyof TLine>(
 | 
					  const updateValueLineStation = <K extends keyof TLine>(
 | 
				
			||||||
    currentLine: TLine,
 | 
					    lineId: number,
 | 
				
			||||||
    field: K,
 | 
					    field: K,
 | 
				
			||||||
    value: TLine[K]
 | 
					    value: TLine[K]
 | 
				
			||||||
  ) => {
 | 
					  ) => {
 | 
				
			||||||
| 
						 | 
					@ -120,10 +192,20 @@ function App() {
 | 
				
			||||||
          ? {
 | 
					          ? {
 | 
				
			||||||
              ...station,
 | 
					              ...station,
 | 
				
			||||||
              lines: (station?.lines || [])?.map((lineItem: TLine) => {
 | 
					              lines: (station?.lines || [])?.map((lineItem: TLine) => {
 | 
				
			||||||
                if (lineItem.id === currentLine.id) {
 | 
					                if (lineItem.id === lineId) {
 | 
				
			||||||
                  return {
 | 
					                  return {
 | 
				
			||||||
                    ...lineItem,
 | 
					                    ...lineItem,
 | 
				
			||||||
                    [field]: value,
 | 
					                    [field]:
 | 
				
			||||||
 | 
					                      field === "netOutput"
 | 
				
			||||||
 | 
					                        ? (lineItem.netOutput || "") + value
 | 
				
			||||||
 | 
					                        : value,
 | 
				
			||||||
 | 
					                    output: field === "netOutput" ? value : lineItem.output,
 | 
				
			||||||
 | 
					                    loadingOutput:
 | 
				
			||||||
 | 
					                      field === "netOutput"
 | 
				
			||||||
 | 
					                        ? lineItem.loadingOutput
 | 
				
			||||||
 | 
					                          ? false
 | 
				
			||||||
 | 
					                          : true
 | 
				
			||||||
 | 
					                        : false,
 | 
				
			||||||
                  };
 | 
					                  };
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                return lineItem;
 | 
					                return lineItem;
 | 
				
			||||||
| 
						 | 
					@ -132,6 +214,24 @@ function App() {
 | 
				
			||||||
          : station
 | 
					          : station
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (selectedLine) {
 | 
				
			||||||
 | 
					      const line = {
 | 
				
			||||||
 | 
					        ...selectedLine,
 | 
				
			||||||
 | 
					        [field]:
 | 
				
			||||||
 | 
					          field === "netOutput"
 | 
				
			||||||
 | 
					            ? (selectedLine.netOutput || "") + value
 | 
				
			||||||
 | 
					            : value,
 | 
				
			||||||
 | 
					        output: field === "netOutput" ? value : selectedLine.output,
 | 
				
			||||||
 | 
					        loadingOutput:
 | 
				
			||||||
 | 
					          field === "netOutput"
 | 
				
			||||||
 | 
					            ? selectedLine.loadingOutput
 | 
				
			||||||
 | 
					              ? false
 | 
				
			||||||
 | 
					              : true
 | 
				
			||||||
 | 
					            : false,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      setSelectedLine(line);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const getLine = (lineId: number, stationId: number) => {
 | 
					  const getLine = (lineId: number, stationId: number) => {
 | 
				
			||||||
| 
						 | 
					@ -142,63 +242,106 @@ function App() {
 | 
				
			||||||
    } else return null;
 | 
					    } else return null;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const openTerminal = (line: TLine) => {
 | 
				
			||||||
 | 
					    setOpenModalTerminal(true);
 | 
				
			||||||
 | 
					    setSelectedLine(line);
 | 
				
			||||||
 | 
					    socket?.emit("open_cli", {
 | 
				
			||||||
 | 
					      lineId: line.id,
 | 
				
			||||||
 | 
					      stationId: line.station_id,
 | 
				
			||||||
 | 
					      userEmail: user?.email,
 | 
				
			||||||
 | 
					      userName: user?.fullName,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Container py="xl" w={"100%"} style={{ maxWidth: "100%" }}>
 | 
					    <Container py="md" w={"100%"} style={{ maxWidth: "100%" }}>
 | 
				
			||||||
      <Tabs
 | 
					      <Tabs
 | 
				
			||||||
        value={activeTab}
 | 
					        value={activeTab}
 | 
				
			||||||
        onChange={(id) => setActiveTab(id?.toString() || "0")}
 | 
					        onChange={(id) => {
 | 
				
			||||||
 | 
					          setActiveTab(id?.toString() || "0");
 | 
				
			||||||
 | 
					          setLoadingTerminal(false);
 | 
				
			||||||
 | 
					          setTimeout(() => {
 | 
				
			||||||
 | 
					            setLoadingTerminal(true);
 | 
				
			||||||
 | 
					          }, 100);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
        variant="none"
 | 
					        variant="none"
 | 
				
			||||||
        keepMounted={false}
 | 
					        keepMounted={false}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <Tabs.List ref={setRootRef} className={classes.list}>
 | 
					        <Flex justify={"space-between"}>
 | 
				
			||||||
          {stations.map((station) => (
 | 
					          <Flex wrap={"wrap"} style={{ maxWidth: "250px" }} gap={"4px"}>
 | 
				
			||||||
            <Tabs.Tab
 | 
					            {usersConnecting.map((el) => (
 | 
				
			||||||
              ref={setControlRef(station.id.toString())}
 | 
					              <Tooltip label={el.userName} key={el.userId}>
 | 
				
			||||||
              className={classes.tab}
 | 
					                <Avatar color="cyan" radius="xl" size={"md"}>
 | 
				
			||||||
              key={station.id}
 | 
					                  {el.userName.slice(0, 2)}
 | 
				
			||||||
              value={station.id.toString()}
 | 
					                </Avatar>
 | 
				
			||||||
            >
 | 
					              </Tooltip>
 | 
				
			||||||
              {station.name}
 | 
					            ))}
 | 
				
			||||||
            </Tabs.Tab>
 | 
					          </Flex>
 | 
				
			||||||
          ))}
 | 
					          <Tabs.List ref={setRootRef} className={classes.list}>
 | 
				
			||||||
 | 
					            {stations.map((station) => (
 | 
				
			||||||
 | 
					              <Tabs.Tab
 | 
				
			||||||
 | 
					                ref={setControlRef(station.id.toString())}
 | 
				
			||||||
 | 
					                className={classes.tab}
 | 
				
			||||||
 | 
					                key={station.id}
 | 
				
			||||||
 | 
					                value={station.id.toString()}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {station.name}
 | 
				
			||||||
 | 
					              </Tabs.Tab>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <FloatingIndicator
 | 
					            <FloatingIndicator
 | 
				
			||||||
            target={activeTab ? controlsRefs[activeTab] : null}
 | 
					              target={activeTab ? controlsRefs[activeTab] : null}
 | 
				
			||||||
            parent={rootRef}
 | 
					              parent={rootRef}
 | 
				
			||||||
            className={classes.indicator}
 | 
					              className={classes.indicator}
 | 
				
			||||||
          />
 | 
					            />
 | 
				
			||||||
          <Flex gap={"sm"}>
 | 
					            <Flex gap={"sm"}>
 | 
				
			||||||
            {Number(activeTab) ? (
 | 
					              {Number(activeTab) ? (
 | 
				
			||||||
 | 
					                <ActionIcon
 | 
				
			||||||
 | 
					                  title="Edit Station"
 | 
				
			||||||
 | 
					                  variant="outline"
 | 
				
			||||||
 | 
					                  onClick={() => {
 | 
				
			||||||
 | 
					                    setStationEdit(
 | 
				
			||||||
 | 
					                      stations.find((el) => el.id === Number(activeTab))
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    setIsOpenAddStation(true);
 | 
				
			||||||
 | 
					                    setIsEditStation(true);
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <IconEdit />
 | 
				
			||||||
 | 
					                </ActionIcon>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                ""
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
              <ActionIcon
 | 
					              <ActionIcon
 | 
				
			||||||
                title="Edit Station"
 | 
					                title="Add Station"
 | 
				
			||||||
                variant="outline"
 | 
					                variant="outline"
 | 
				
			||||||
 | 
					                color="green"
 | 
				
			||||||
                onClick={() => {
 | 
					                onClick={() => {
 | 
				
			||||||
                  setStationEdit(
 | 
					 | 
				
			||||||
                    stations.find((el) => el.id === Number(activeTab))
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                  setIsOpenAddStation(true);
 | 
					                  setIsOpenAddStation(true);
 | 
				
			||||||
                  setIsEditStation(true);
 | 
					                  setIsEditStation(false);
 | 
				
			||||||
 | 
					                  setStationEdit(undefined);
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <IconEdit />
 | 
					                <IconSettingsPlus />
 | 
				
			||||||
              </ActionIcon>
 | 
					              </ActionIcon>
 | 
				
			||||||
            ) : (
 | 
					            </Flex>
 | 
				
			||||||
              ""
 | 
					          </Tabs.List>
 | 
				
			||||||
            )}
 | 
					          <Flex gap={"sm"} align={"baseline"}>
 | 
				
			||||||
            <ActionIcon
 | 
					            <Text className={classes.userName}>{user?.fullName}</Text>
 | 
				
			||||||
              title="Add Station"
 | 
					            <Button
 | 
				
			||||||
              variant="outline"
 | 
					              variant="outline"
 | 
				
			||||||
              color="green"
 | 
					              color="red"
 | 
				
			||||||
 | 
					              style={{ height: "30px", width: "100px" }}
 | 
				
			||||||
              onClick={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                setIsOpenAddStation(true);
 | 
					                localStorage.removeItem("user");
 | 
				
			||||||
                setIsEditStation(false);
 | 
					                window.location.href = "/";
 | 
				
			||||||
                setStationEdit(undefined);
 | 
					                socket?.disconnect();
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <IconSettingsPlus />
 | 
					              Logout
 | 
				
			||||||
            </ActionIcon>
 | 
					            </Button>
 | 
				
			||||||
          </Flex>
 | 
					          </Flex>
 | 
				
			||||||
        </Tabs.List>
 | 
					        </Flex>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {stations.map((station) => (
 | 
					        {stations.map((station) => (
 | 
				
			||||||
          <Tabs.Panel
 | 
					          <Tabs.Panel
 | 
				
			||||||
| 
						 | 
					@ -238,7 +381,11 @@ function App() {
 | 
				
			||||||
                          line={line}
 | 
					                          line={line}
 | 
				
			||||||
                          selectedLines={selectedLines}
 | 
					                          selectedLines={selectedLines}
 | 
				
			||||||
                          setSelectedLines={setSelectedLines}
 | 
					                          setSelectedLines={setSelectedLines}
 | 
				
			||||||
                          updateStatus={updateStatus}
 | 
					                          openTerminal={openTerminal}
 | 
				
			||||||
 | 
					                          loadTerminal={
 | 
				
			||||||
 | 
					                            loadingTerminal &&
 | 
				
			||||||
 | 
					                            Number(station.id) === Number(activeTab)
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
                        />
 | 
					                        />
 | 
				
			||||||
                      ))}
 | 
					                      ))}
 | 
				
			||||||
                    </Flex>
 | 
					                    </Flex>
 | 
				
			||||||
| 
						 | 
					@ -309,8 +456,9 @@ function App() {
 | 
				
			||||||
                      }, 10000);
 | 
					                      }, 10000);
 | 
				
			||||||
                    }}
 | 
					                    }}
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                  {scenarios.map((el) => (
 | 
					                  {scenarios.map((el, i) => (
 | 
				
			||||||
                    <ButtonScenario
 | 
					                    <ButtonScenario
 | 
				
			||||||
 | 
					                      key={i}
 | 
				
			||||||
                      socket={socket}
 | 
					                      socket={socket}
 | 
				
			||||||
                      selectedLines={selectedLines}
 | 
					                      selectedLines={selectedLines}
 | 
				
			||||||
                      isDisable={isDisable || selectedLines.length === 0}
 | 
					                      isDisable={isDisable || selectedLines.length === 0}
 | 
				
			||||||
| 
						 | 
					@ -345,11 +493,24 @@ function App() {
 | 
				
			||||||
          setActiveTab(stations.length ? stations[0]?.id.toString() : "0")
 | 
					          setActiveTab(stations.length ? stations[0]?.id.toString() : "0")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ModalTerminal
 | 
				
			||||||
 | 
					        opened={openModalTerminal}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          setOpenModalTerminal(false);
 | 
				
			||||||
 | 
					          setSelectedLine(undefined);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        line={selectedLine}
 | 
				
			||||||
 | 
					        socket={socket}
 | 
				
			||||||
 | 
					        stationItem={stations.find((el) => el.id === Number(activeTab))}
 | 
				
			||||||
 | 
					        scenarios={scenarios}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    </Container>
 | 
					    </Container>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Main() {
 | 
					export default function Main() {
 | 
				
			||||||
 | 
					  const user = localStorage.getItem("user");
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <MantineProvider>
 | 
					    <MantineProvider>
 | 
				
			||||||
      <SocketProvider>
 | 
					      <SocketProvider>
 | 
				
			||||||
| 
						 | 
					@ -363,7 +524,13 @@ export default function Main() {
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Notifications position="top-right" autoClose={5000} />
 | 
					          <Notifications position="top-right" autoClose={5000} />
 | 
				
			||||||
          <App />
 | 
					          {user ? (
 | 
				
			||||||
 | 
					            <App />
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            <Container w={"100%"} style={{ maxWidth: "100%", padding: 0 }}>
 | 
				
			||||||
 | 
					              <PageLogin />
 | 
				
			||||||
 | 
					            </Container>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
        </Suspense>
 | 
					        </Suspense>
 | 
				
			||||||
      </SocketProvider>
 | 
					      </SocketProvider>
 | 
				
			||||||
    </MantineProvider>
 | 
					    </MantineProvider>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,70 +1,22 @@
 | 
				
			||||||
.wrapper {
 | 
					.wrapper {
 | 
				
			||||||
    min-height: rem(100vh);
 | 
					  height: 100vh;
 | 
				
			||||||
    background-size: cover;
 | 
					  background-size: cover;
 | 
				
			||||||
    background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80);
 | 
					  background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80);
 | 
				
			||||||
  }
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .form {
 | 
					.form {
 | 
				
			||||||
    border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
 | 
					  border-right: rem(1px) solid
 | 
				
			||||||
    min-height: rem(100vh);
 | 
					    light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
 | 
				
			||||||
    max-width: rem(500px);
 | 
					  max-width: rem(450px);
 | 
				
			||||||
    padding-top: rem(80px);
 | 
					  padding-top: rem(80px);
 | 
				
			||||||
    @media (max-width: var(--mantine-breakpoint-sm)) {
 | 
					 | 
				
			||||||
      max-width: 100%;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    font-weight: 600;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .title {
 | 
					 | 
				
			||||||
    color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
 | 
					 | 
				
			||||||
    font-family:
 | 
					 | 
				
			||||||
      Greycliff CF,
 | 
					 | 
				
			||||||
      var(--mantine-font-family);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .google-btn {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    border: 1px solid #4285f4;
 | 
					 | 
				
			||||||
    border-radius: 4px;
 | 
					 | 
				
			||||||
    background-color: white;
 | 
					 | 
				
			||||||
    margin: 15px auto;
 | 
					 | 
				
			||||||
    font-size: 16px;
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
    transition: background-color 0.3s ease;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  .google-btn:hover {
 | 
					 | 
				
			||||||
    background-color: #f0f0f0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  .google-btn:active {
 | 
					 | 
				
			||||||
    background-color: #e0e0e0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  .google-icon {
 | 
					 | 
				
			||||||
    width: 20px;
 | 
					 | 
				
			||||||
    height: 20px;
 | 
					 | 
				
			||||||
    margin-right: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  .title {
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  font-size: 1.5rem;
 | 
					 | 
				
			||||||
  font-weight: 600;
 | 
					  font-weight: 600;
 | 
				
			||||||
  color: #222;
 | 
					 | 
				
			||||||
  text-align: center;
 | 
					 | 
				
			||||||
  margin-bottom: 1.5rem;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.title::after {
 | 
					.title {
 | 
				
			||||||
  content: "";
 | 
					  color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
 | 
				
			||||||
  display: block;
 | 
					  font-family: Greycliff CF, var(--mantine-font-family);
 | 
				
			||||||
  width: 200px;
 | 
					 | 
				
			||||||
  height: 2px;
 | 
					 | 
				
			||||||
  background-color: #007bff;  /* blue accent */
 | 
					 | 
				
			||||||
  margin: 0.1rem auto 0;
 | 
					 | 
				
			||||||
  border-radius: 3px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,105 +1,80 @@
 | 
				
			||||||
import { useGoogleLogin } from '@react-oauth/google'
 | 
					import { Button, PasswordInput, TextInput } from "@mantine/core";
 | 
				
			||||||
import { emailRegex } from '@/utils/formRegexs'
 | 
					import { useForm } from "@mantine/form";
 | 
				
			||||||
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useDispatch, useSelector } from 'react-redux'
 | 
					const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
				
			||||||
import { AppDispatch, RootState } from '@/rtk/store'
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  loginAsync,
 | 
					 | 
				
			||||||
  loginERPAsync,
 | 
					 | 
				
			||||||
  loginWithGoogleAsync,
 | 
					 | 
				
			||||||
} from '@/rtk/slices/authSlice'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Box, Button, PasswordInput, TextInput } from '@mantine/core'
 | 
					 | 
				
			||||||
import { useForm } from '@mantine/form'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import classes from './AuthenticationImage.module.css'
 | 
					 | 
				
			||||||
import { useNavigate } from 'react-router-dom'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import ImgERP from '../../lib/images/erp.jpg'
 | 
					 | 
				
			||||||
import { useState } from 'react'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TLogin = {
 | 
					type TLogin = {
 | 
				
			||||||
  email: string
 | 
					  email: string;
 | 
				
			||||||
  password: string
 | 
					  password: string;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Login = () => {
 | 
					const Login = () => {
 | 
				
			||||||
  const navigate = useNavigate()
 | 
					 | 
				
			||||||
  const dispatch = useDispatch<AppDispatch>()
 | 
					 | 
				
			||||||
  const { status } = useSelector((state: RootState) => state.auth)
 | 
					 | 
				
			||||||
  const [isLoginERP, setIsLoginERP] = useState(false)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const formLogin = useForm<TLogin>({
 | 
					  const formLogin = useForm<TLogin>({
 | 
				
			||||||
    initialValues: {
 | 
					    initialValues: {
 | 
				
			||||||
      email: '',
 | 
					      email: "",
 | 
				
			||||||
      password: '',
 | 
					      password: "",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    validate: (values) => ({
 | 
					    validate: (values) => ({
 | 
				
			||||||
      email:
 | 
					      email: values.email === "" ? "Email is required" : null,
 | 
				
			||||||
        values.email === ''
 | 
					 | 
				
			||||||
          ? 'Email is required'
 | 
					 | 
				
			||||||
          : isLoginERP
 | 
					 | 
				
			||||||
          ? null
 | 
					 | 
				
			||||||
          : emailRegex.test(values.email)
 | 
					 | 
				
			||||||
          ? null
 | 
					 | 
				
			||||||
          : 'Invalid email',
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      password: values.password === '' ? 'Password is required' : null,
 | 
					      password: values.password === "" ? "Password is required" : null,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  })
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleLogin = async (values: TLogin) => {
 | 
					  const handleLogin = async () => {
 | 
				
			||||||
    if (isLoginERP) {
 | 
					    try {
 | 
				
			||||||
 | 
					      if (!formLogin.values.email) {
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Error",
 | 
				
			||||||
 | 
					          message: "Email is required",
 | 
				
			||||||
 | 
					          color: "red",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!formLogin.values.password) {
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Error",
 | 
				
			||||||
 | 
					          message: "Password is required",
 | 
				
			||||||
 | 
					          color: "red",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      const payload = {
 | 
					      const payload = {
 | 
				
			||||||
        userEmail: values.email,
 | 
					        email: formLogin.values.email,
 | 
				
			||||||
        password: values.password,
 | 
					        password: formLogin.values.password,
 | 
				
			||||||
      }
 | 
					      };
 | 
				
			||||||
      const resultAction = await dispatch(loginERPAsync(payload))
 | 
					      const response = await axios.post(apiUrl + "api/auth/login", payload);
 | 
				
			||||||
 | 
					      if (response.data.user) {
 | 
				
			||||||
      if (loginERPAsync.fulfilled.match(resultAction)) {
 | 
					        const user = response.data.user;
 | 
				
			||||||
        // set interval to wait for localStorage to be set
 | 
					        localStorage.setItem("user", JSON.stringify(user));
 | 
				
			||||||
        window.location.href = '/dashboard'
 | 
					        window.location.href = "/";
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      const resultAction = await dispatch(loginAsync(values))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (loginAsync.fulfilled.match(resultAction)) {
 | 
					 | 
				
			||||||
        navigate('/dashboard')
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Error",
 | 
				
			||||||
 | 
					        message: "Login fail, please try again!",
 | 
				
			||||||
 | 
					        color: "red",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleLoginGG = useGoogleLogin({
 | 
					 | 
				
			||||||
    onSuccess: async (codeResponse) => {
 | 
					 | 
				
			||||||
      const accessToken = codeResponse.access_token
 | 
					 | 
				
			||||||
      const resultAction = await dispatch(loginWithGoogleAsync(accessToken))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (loginWithGoogleAsync.fulfilled.match(resultAction)) {
 | 
					 | 
				
			||||||
        navigate('/dashboard')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    onError: (error) => console.log('Login Failed:', error),
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <form
 | 
					    <form
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
        padding: '10px 20px',
 | 
					        padding: "10px 20px",
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
      onSubmit={formLogin.onSubmit(handleLogin)}
 | 
					      onSubmit={formLogin.onSubmit(handleLogin)}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div style={{ textAlign: 'center' }}>
 | 
					 | 
				
			||||||
        <h3 className={classes.title}>
 | 
					 | 
				
			||||||
          {isLoginERP ? 'Login with ERP account' : 'Login with ATC account'}
 | 
					 | 
				
			||||||
        </h3>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <TextInput
 | 
					      <TextInput
 | 
				
			||||||
        label={isLoginERP ? 'Username/email:' : 'Email address'}
 | 
					        label={"Email address"}
 | 
				
			||||||
        placeholder="hello@gmail.com"
 | 
					        placeholder="hello@gmail.com"
 | 
				
			||||||
        value={formLogin.values.email}
 | 
					        value={formLogin.values.email}
 | 
				
			||||||
        error={formLogin.errors.email}
 | 
					        error={formLogin.errors.email}
 | 
				
			||||||
        onChange={(e) => {
 | 
					        onChange={(e) => {
 | 
				
			||||||
          formLogin.setFieldValue('email', e.target.value!)
 | 
					          formLogin.setFieldValue("email", e.target.value!);
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        required
 | 
					        required
 | 
				
			||||||
        size="md"
 | 
					        size="md"
 | 
				
			||||||
| 
						 | 
					@ -110,7 +85,7 @@ const Login = () => {
 | 
				
			||||||
        value={formLogin.values.password}
 | 
					        value={formLogin.values.password}
 | 
				
			||||||
        error={formLogin.errors.password}
 | 
					        error={formLogin.errors.password}
 | 
				
			||||||
        onChange={(e) => {
 | 
					        onChange={(e) => {
 | 
				
			||||||
          formLogin.setFieldValue('password', e.target.value!)
 | 
					          formLogin.setFieldValue("password", e.target.value!);
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        required
 | 
					        required
 | 
				
			||||||
        mt="md"
 | 
					        mt="md"
 | 
				
			||||||
| 
						 | 
					@ -122,60 +97,12 @@ const Login = () => {
 | 
				
			||||||
        mt="xl"
 | 
					        mt="xl"
 | 
				
			||||||
        size="md"
 | 
					        size="md"
 | 
				
			||||||
        type="submit"
 | 
					        type="submit"
 | 
				
			||||||
        loading={status === 'loading'}
 | 
					        loading={status === "loading"}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        Sign in
 | 
					        Sign in
 | 
				
			||||||
      </Button>
 | 
					      </Button>
 | 
				
			||||||
 | 
					 | 
				
			||||||
      {!isLoginERP ? (
 | 
					 | 
				
			||||||
        <Box ta={'center'}>
 | 
					 | 
				
			||||||
          <Button
 | 
					 | 
				
			||||||
            variant="outline"
 | 
					 | 
				
			||||||
            color="#228be6"
 | 
					 | 
				
			||||||
            radius={'5px'}
 | 
					 | 
				
			||||||
            className={classes['google-btn']}
 | 
					 | 
				
			||||||
            onClick={() => setIsLoginERP(true)}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <img
 | 
					 | 
				
			||||||
              src={ImgERP}
 | 
					 | 
				
			||||||
              alt="ERP logo"
 | 
					 | 
				
			||||||
              className={classes['google-icon']}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            Sign in with ERP
 | 
					 | 
				
			||||||
          </Button>
 | 
					 | 
				
			||||||
        </Box>
 | 
					 | 
				
			||||||
      ) : (
 | 
					 | 
				
			||||||
        <Box ta={'center'}>
 | 
					 | 
				
			||||||
          <Button
 | 
					 | 
				
			||||||
            variant="outline"
 | 
					 | 
				
			||||||
            color="#228be6"
 | 
					 | 
				
			||||||
            radius={'5px'}
 | 
					 | 
				
			||||||
            className={classes['google-btn']}
 | 
					 | 
				
			||||||
            onClick={() => setIsLoginERP(false)}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            Sign in normally
 | 
					 | 
				
			||||||
          </Button>
 | 
					 | 
				
			||||||
        </Box>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <Box ta={'center'}>
 | 
					 | 
				
			||||||
        <Button
 | 
					 | 
				
			||||||
          variant="outline"
 | 
					 | 
				
			||||||
          color="#228be6"
 | 
					 | 
				
			||||||
          radius={'5px'}
 | 
					 | 
				
			||||||
          onClick={() => handleLoginGG()}
 | 
					 | 
				
			||||||
          className={classes['google-btn']}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <img
 | 
					 | 
				
			||||||
            src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/480px-Google_%22G%22_logo.svg.png"
 | 
					 | 
				
			||||||
            alt="Google logo"
 | 
					 | 
				
			||||||
            className={classes['google-icon']}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          Sign in with Google
 | 
					 | 
				
			||||||
        </Button>
 | 
					 | 
				
			||||||
      </Box>
 | 
					 | 
				
			||||||
    </form>
 | 
					    </form>
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Login
 | 
					export default Login;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { Anchor, Image, Paper, Text } from "@mantine/core";
 | 
				
			||||||
 | 
					import Login from "./Login";
 | 
				
			||||||
 | 
					import Register from "./Register";
 | 
				
			||||||
 | 
					import classes from "./AuthenticationImage.module.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PageLogin = () => {
 | 
				
			||||||
 | 
					  const [isRegister, setIsRegister] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        height: "100vh",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div className={classes.wrapper}>
 | 
				
			||||||
 | 
					        <Paper className={classes.form} radius={0} p={30}>
 | 
				
			||||||
 | 
					          <Image
 | 
				
			||||||
 | 
					            w={"45%"}
 | 
				
			||||||
 | 
					            mt={"sm"}
 | 
				
			||||||
 | 
					            mb={"xs"}
 | 
				
			||||||
 | 
					            m={"0 auto"}
 | 
				
			||||||
 | 
					            src={import.meta.env.VITE_DOMAIN + "logo-ATC-removebg-preview.png"}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {isRegister ? (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <Register />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <Text ta="center" mt="md">
 | 
				
			||||||
 | 
					                You have an account?{" "}
 | 
				
			||||||
 | 
					                <Anchor<"a">
 | 
				
			||||||
 | 
					                  href="#"
 | 
				
			||||||
 | 
					                  fw={700}
 | 
				
			||||||
 | 
					                  onClick={() => setIsRegister(false)}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  Sign in
 | 
				
			||||||
 | 
					                </Anchor>
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <Login />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <Text ta="center" mt="md">
 | 
				
			||||||
 | 
					                Don't have an account?{" "}
 | 
				
			||||||
 | 
					                <Anchor<"a">
 | 
				
			||||||
 | 
					                  href="#"
 | 
				
			||||||
 | 
					                  fw={700}
 | 
				
			||||||
 | 
					                  onClick={() => setIsRegister(true)}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  Register
 | 
				
			||||||
 | 
					                </Anchor>
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </Paper>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default PageLogin;
 | 
				
			||||||
| 
						 | 
					@ -1,48 +1,77 @@
 | 
				
			||||||
import { useState } from 'react'
 | 
					import { useState } from "react";
 | 
				
			||||||
import { useDispatch, useSelector } from 'react-redux'
 | 
					 | 
				
			||||||
import { AppDispatch, RootState } from '@/rtk/store'
 | 
					 | 
				
			||||||
import { registerAsync } from '@/rtk/slices/authSlice'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
 | 
					import { Box, Button, PasswordInput, TextInput } from "@mantine/core";
 | 
				
			||||||
import { emailRegex, passwordRegex } from '@/utils/formRegexs'
 | 
					import { emailRegex } from "../../untils/helper";
 | 
				
			||||||
import { requirementsPassword } from '@/rtk/helpers/variables'
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
import { Box, Button, PasswordInput, TextInput } from '@mantine/core'
 | 
					const apiUrl = import.meta.env.VITE_BACKEND_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TRegister = {
 | 
					type TRegister = {
 | 
				
			||||||
  email: string
 | 
					  email: string;
 | 
				
			||||||
  password: string
 | 
					  password: string;
 | 
				
			||||||
  confirm_password: string
 | 
					  confirm_password: string;
 | 
				
			||||||
  full_name: string
 | 
					  full_name: string;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function Register() {
 | 
					function Register() {
 | 
				
			||||||
  const dispatch = useDispatch<AppDispatch>()
 | 
					 | 
				
			||||||
  const { status } = useSelector((state: RootState) => state.auth)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const [formRegister, setFormRegister] = useState<TRegister>({
 | 
					  const [formRegister, setFormRegister] = useState<TRegister>({
 | 
				
			||||||
    email: '',
 | 
					    email: "",
 | 
				
			||||||
    full_name: '',
 | 
					    full_name: "",
 | 
				
			||||||
    password: '',
 | 
					    password: "",
 | 
				
			||||||
    confirm_password: '',
 | 
					    confirm_password: "",
 | 
				
			||||||
  })
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleRegister = async () => {
 | 
					  const handleRegister = async () => {
 | 
				
			||||||
    // Dispatch action registerAsync với dữ liệu form và đợi kết quả
 | 
					    try {
 | 
				
			||||||
    const resultAction = await dispatch(registerAsync(formRegister))
 | 
					      if (!formRegister.email) {
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
    // Kiểm tra nếu action thành công
 | 
					          title: "Error",
 | 
				
			||||||
    if (registerAsync.fulfilled.match(resultAction)) {
 | 
					          message: "Email is required",
 | 
				
			||||||
        // Tải lại trang web
 | 
					          color: "red",
 | 
				
			||||||
        // window.location.reload()
 | 
					        });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!formRegister.password) {
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Error",
 | 
				
			||||||
 | 
					          message: "Password is required",
 | 
				
			||||||
 | 
					          color: "red",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const payload = {
 | 
				
			||||||
 | 
					        email: formRegister.email,
 | 
				
			||||||
 | 
					        password: formRegister.password,
 | 
				
			||||||
 | 
					        full_name: formRegister.full_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;
 | 
				
			||||||
 | 
					        localStorage.setItem("user", JSON.stringify(user));
 | 
				
			||||||
 | 
					        window.location.href = "/";
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Error",
 | 
				
			||||||
 | 
					          message: response.data.message,
 | 
				
			||||||
 | 
					          color: "red",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Error",
 | 
				
			||||||
 | 
					        message: "Register fail, please try again!",
 | 
				
			||||||
 | 
					        color: "red",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <form
 | 
					    <form
 | 
				
			||||||
      onSubmit={(e) => {
 | 
					      onSubmit={(e) => {
 | 
				
			||||||
        e.preventDefault()
 | 
					        e.preventDefault();
 | 
				
			||||||
        handleRegister()
 | 
					        handleRegister();
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <TextInput
 | 
					      <TextInput
 | 
				
			||||||
| 
						 | 
					@ -50,12 +79,12 @@ function Register() {
 | 
				
			||||||
        placeholder="hello@gmail.com"
 | 
					        placeholder="hello@gmail.com"
 | 
				
			||||||
        value={formRegister.email}
 | 
					        value={formRegister.email}
 | 
				
			||||||
        error={
 | 
					        error={
 | 
				
			||||||
          emailRegex.test(formRegister.email) || formRegister.email === ''
 | 
					          emailRegex.test(formRegister.email) || formRegister.email === ""
 | 
				
			||||||
            ? null
 | 
					            ? null
 | 
				
			||||||
            : 'Invalid email'
 | 
					            : "Invalid email"
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        onChange={(e) => {
 | 
					        onChange={(e) => {
 | 
				
			||||||
          setFormRegister({ ...formRegister, email: e.target.value })
 | 
					          setFormRegister({ ...formRegister, email: e.target.value });
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        required
 | 
					        required
 | 
				
			||||||
        size="md"
 | 
					        size="md"
 | 
				
			||||||
| 
						 | 
					@ -68,19 +97,22 @@ function Register() {
 | 
				
			||||||
        placeholder="Bill Gates"
 | 
					        placeholder="Bill Gates"
 | 
				
			||||||
        value={formRegister.full_name}
 | 
					        value={formRegister.full_name}
 | 
				
			||||||
        onChange={(e) => {
 | 
					        onChange={(e) => {
 | 
				
			||||||
          setFormRegister({ ...formRegister, full_name: e.target.value })
 | 
					          setFormRegister({ ...formRegister, full_name: e.target.value });
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        required
 | 
					        required
 | 
				
			||||||
        size="md"
 | 
					        size="md"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <PasswordRequirementInput
 | 
					      <PasswordInput
 | 
				
			||||||
        requirements={requirementsPassword}
 | 
					        mt="md"
 | 
				
			||||||
        value={formRegister}
 | 
					 | 
				
			||||||
        setValue={setFormRegister}
 | 
					 | 
				
			||||||
        label="Password"
 | 
					        label="Password"
 | 
				
			||||||
        placeholder="Password"
 | 
					        placeholder="Your password"
 | 
				
			||||||
        name="password"
 | 
					        value={formRegister.password}
 | 
				
			||||||
 | 
					        onChange={(e) => {
 | 
				
			||||||
 | 
					          setFormRegister({ ...formRegister, password: e.target.value });
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        required
 | 
				
			||||||
 | 
					        size="md"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <PasswordInput
 | 
					      <PasswordInput
 | 
				
			||||||
| 
						 | 
					@ -90,27 +122,29 @@ function Register() {
 | 
				
			||||||
        value={formRegister.confirm_password}
 | 
					        value={formRegister.confirm_password}
 | 
				
			||||||
        error={
 | 
					        error={
 | 
				
			||||||
          formRegister.confirm_password === formRegister.password ||
 | 
					          formRegister.confirm_password === formRegister.password ||
 | 
				
			||||||
          formRegister.confirm_password === ''
 | 
					          formRegister.confirm_password === ""
 | 
				
			||||||
            ? null
 | 
					            ? null
 | 
				
			||||||
            : 'Password do not match'
 | 
					            : "Password do not match"
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        onChange={(e) => {
 | 
					        onChange={(e) => {
 | 
				
			||||||
          setFormRegister({ ...formRegister, confirm_password: e.target.value })
 | 
					          setFormRegister({
 | 
				
			||||||
 | 
					            ...formRegister,
 | 
				
			||||||
 | 
					            confirm_password: e.target.value,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        required
 | 
					        required
 | 
				
			||||||
        size="md"
 | 
					        size="md"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <Box ta={'center'}>
 | 
					      <Box ta={"center"}>
 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          type="submit"
 | 
					          type="submit"
 | 
				
			||||||
          m="15px auto"
 | 
					          m="15px auto"
 | 
				
			||||||
          fullWidth
 | 
					          fullWidth
 | 
				
			||||||
          size="md"
 | 
					          size="md"
 | 
				
			||||||
          loading={status === 'loading'}
 | 
					          loading={status === "loading"}
 | 
				
			||||||
          disabled={
 | 
					          disabled={
 | 
				
			||||||
            formRegister.password !== '' &&
 | 
					            formRegister.password !== "" &&
 | 
				
			||||||
            passwordRegex.test(formRegister.password) &&
 | 
					 | 
				
			||||||
            formRegister.password === formRegister.confirm_password
 | 
					            formRegister.password === formRegister.confirm_password
 | 
				
			||||||
              ? false
 | 
					              ? false
 | 
				
			||||||
              : true
 | 
					              : true
 | 
				
			||||||
| 
						 | 
					@ -120,7 +154,7 @@ function Register() {
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
    </form>
 | 
					    </form>
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Register
 | 
					export default Register;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { Card, Text, Box, Flex } from "@mantine/core";
 | 
					import { Card, Text, Box, Flex } from "@mantine/core";
 | 
				
			||||||
import type { LineConfig, TLine, TStation } from "../untils/types";
 | 
					import type { TLine, TStation } from "../untils/types";
 | 
				
			||||||
import classes from "./Component.module.css";
 | 
					import classes from "./Component.module.css";
 | 
				
			||||||
import TerminalCLI from "./TerminalXTerm";
 | 
					import TerminalCLI from "./TerminalXTerm";
 | 
				
			||||||
import type { Socket } from "socket.io-client";
 | 
					import type { Socket } from "socket.io-client";
 | 
				
			||||||
| 
						 | 
					@ -12,14 +12,16 @@ const CardLine = ({
 | 
				
			||||||
  setSelectedLines,
 | 
					  setSelectedLines,
 | 
				
			||||||
  socket,
 | 
					  socket,
 | 
				
			||||||
  stationItem,
 | 
					  stationItem,
 | 
				
			||||||
  updateStatus,
 | 
					  openTerminal,
 | 
				
			||||||
 | 
					  loadTerminal,
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  line: TLine;
 | 
					  line: TLine;
 | 
				
			||||||
  selectedLines: TLine[];
 | 
					  selectedLines: TLine[];
 | 
				
			||||||
  setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void;
 | 
					  setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void;
 | 
				
			||||||
  socket: Socket | null;
 | 
					  socket: Socket | null;
 | 
				
			||||||
  stationItem: TStation;
 | 
					  stationItem: TStation;
 | 
				
			||||||
  updateStatus: (value: LineConfig) => void;
 | 
					  openTerminal: (value: TLine) => void;
 | 
				
			||||||
 | 
					  loadTerminal: boolean;
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Card
 | 
					    <Card
 | 
				
			||||||
| 
						 | 
					@ -33,6 +35,11 @@ const CardLine = ({
 | 
				
			||||||
          ? { backgroundColor: "#8bf55940" }
 | 
					          ? { backgroundColor: "#8bf55940" }
 | 
				
			||||||
          : {}
 | 
					          : {}
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      onDoubleClick={(e) => {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					        openTerminal(line);
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
      onClick={(e) => {
 | 
					      onClick={(e) => {
 | 
				
			||||||
        e.preventDefault();
 | 
					        e.preventDefault();
 | 
				
			||||||
        e.stopPropagation();
 | 
					        e.stopPropagation();
 | 
				
			||||||
| 
						 | 
					@ -45,16 +52,27 @@ const CardLine = ({
 | 
				
			||||||
        justify={"space-between"}
 | 
					        justify={"space-between"}
 | 
				
			||||||
        direction={"column"}
 | 
					        direction={"column"}
 | 
				
			||||||
        // gap={"md"}
 | 
					        // gap={"md"}
 | 
				
			||||||
        align={"center"}
 | 
					        // align={"center"}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div>
 | 
					        <Flex justify={"space-between"}>
 | 
				
			||||||
          <Text fw={600} style={{ display: "flex", gap: "4px" }}>
 | 
					          <Text fw={600} style={{ display: "flex", gap: "4px" }}>
 | 
				
			||||||
            Line: {line.lineNumber || line.line_number} - {line.port}{" "}
 | 
					            Line: {line.lineNumber || line.line_number} - {line.port}{" "}
 | 
				
			||||||
            {line.status === "connected" && (
 | 
					            {line.status === "connected" && (
 | 
				
			||||||
              <IconCircleCheckFilled color="green" />
 | 
					              <IconCircleCheckFilled color="green" />
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
          </Text>
 | 
					          </Text>
 | 
				
			||||||
        </div>
 | 
					          <div
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              alignItems: "center",
 | 
				
			||||||
 | 
					              marginLeft: "16px",
 | 
				
			||||||
 | 
					              fontSize: "12px",
 | 
				
			||||||
 | 
					              color: "red",
 | 
				
			||||||
 | 
					              display: "flex",
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {line?.userOpenCLI ? line?.userOpenCLI + " is using" : ""}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </Flex>
 | 
				
			||||||
        {/* <Text className={classes.info_line}>PID: WS-C3560CG-8PC-S</Text>
 | 
					        {/* <Text className={classes.info_line}>PID: WS-C3560CG-8PC-S</Text>
 | 
				
			||||||
          <div className={classes.info_line}>SN: FGL2240307M</div>
 | 
					          <div className={classes.info_line}>SN: FGL2240307M</div>
 | 
				
			||||||
          <div className={classes.info_line}>VID: V01</div> */}
 | 
					          <div className={classes.info_line}>VID: V01</div> */}
 | 
				
			||||||
| 
						 | 
					@ -66,9 +84,11 @@ const CardLine = ({
 | 
				
			||||||
          style={{ height: "175px", width: "300px" }}
 | 
					          style={{ height: "175px", width: "300px" }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <TerminalCLI
 | 
					          <TerminalCLI
 | 
				
			||||||
            cliOpened={true}
 | 
					            cliOpened={loadTerminal}
 | 
				
			||||||
            socket={socket}
 | 
					            socket={socket}
 | 
				
			||||||
            content={line.netOutput ?? ""}
 | 
					            content={line?.output ?? ""}
 | 
				
			||||||
 | 
					            initContent={line?.netOutput ?? ""}
 | 
				
			||||||
 | 
					            loadingContent={line?.loadingOutput}
 | 
				
			||||||
            line_id={Number(line?.id)}
 | 
					            line_id={Number(line?.id)}
 | 
				
			||||||
            station_id={Number(stationItem.id)}
 | 
					            station_id={Number(stationItem.id)}
 | 
				
			||||||
            isDisabled={false}
 | 
					            isDisabled={false}
 | 
				
			||||||
| 
						 | 
					@ -82,8 +102,9 @@ const CardLine = ({
 | 
				
			||||||
              padding: "0px",
 | 
					              padding: "0px",
 | 
				
			||||||
              paddingBottom: "0px",
 | 
					              paddingBottom: "0px",
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
            onDoubleClick={() => {}}
 | 
					            onDoubleClick={() => {
 | 
				
			||||||
            updateStatus={updateStatus}
 | 
					              openTerminal(line);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
      </Flex>
 | 
					      </Flex>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ import {
 | 
				
			||||||
  Grid,
 | 
					  Grid,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
 | 
					  Checkbox,
 | 
				
			||||||
} from "@mantine/core";
 | 
					} from "@mantine/core";
 | 
				
			||||||
import { IconSettingsPlus } from "@tabler/icons-react";
 | 
					import { IconSettingsPlus } from "@tabler/icons-react";
 | 
				
			||||||
import TableRows from "./Scenario/TableRows";
 | 
					import TableRows from "./Scenario/TableRows";
 | 
				
			||||||
| 
						 | 
					@ -42,11 +43,10 @@ function DrawerScenario({
 | 
				
			||||||
          send: "",
 | 
					          send: "",
 | 
				
			||||||
          delay: "0",
 | 
					          delay: "0",
 | 
				
			||||||
          repeat: "1",
 | 
					          repeat: "1",
 | 
				
			||||||
          note: "",
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ] as IBodyScenario[],
 | 
					      ] as IBodyScenario[],
 | 
				
			||||||
      timeout: "30000",
 | 
					      timeout: "30000",
 | 
				
			||||||
      is_reboot: false,
 | 
					      isReboot: false,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    validate: {
 | 
					    validate: {
 | 
				
			||||||
      title: (value) => {
 | 
					      title: (value) => {
 | 
				
			||||||
| 
						 | 
					@ -72,7 +72,6 @@ function DrawerScenario({
 | 
				
			||||||
      send: "",
 | 
					      send: "",
 | 
				
			||||||
      delay: "0",
 | 
					      delay: "0",
 | 
				
			||||||
      repeat: "1",
 | 
					      repeat: "1",
 | 
				
			||||||
      note: "",
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    form.setFieldValue("body", newBody);
 | 
					    form.setFieldValue("body", newBody);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					@ -84,6 +83,22 @@ function DrawerScenario({
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSave = async () => {
 | 
					  const handleSave = async () => {
 | 
				
			||||||
 | 
					    if (!form.values.title) {
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Error",
 | 
				
			||||||
 | 
					        message: "Title is required",
 | 
				
			||||||
 | 
					        color: "red",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!form.values.timeout) {
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Error",
 | 
				
			||||||
 | 
					        message: "Timeout is required",
 | 
				
			||||||
 | 
					        color: "red",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    setIsSubmit(true);
 | 
					    setIsSubmit(true);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const body = form.values.body.map((el: IBodyScenario) => ({
 | 
					      const body = form.values.body.map((el: IBodyScenario) => ({
 | 
				
			||||||
| 
						 | 
					@ -93,7 +108,8 @@ function DrawerScenario({
 | 
				
			||||||
      }));
 | 
					      }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const payload = {
 | 
					      const payload = {
 | 
				
			||||||
        ...form.values,
 | 
					        title: form.values.title,
 | 
				
			||||||
 | 
					        isReboot: form.values.isReboot,
 | 
				
			||||||
        body: body,
 | 
					        body: body,
 | 
				
			||||||
        timeout: Number(form.values.timeout),
 | 
					        timeout: Number(form.values.timeout),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
| 
						 | 
					@ -111,15 +127,28 @@ function DrawerScenario({
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
            : [...pre, scenario]
 | 
					            : [...pre, scenario]
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					        setIsEdit(true);
 | 
				
			||||||
 | 
					        setDataScenario(scenario);
 | 
				
			||||||
        notifications.show({
 | 
					        notifications.show({
 | 
				
			||||||
          title: "Success",
 | 
					          title: "Success",
 | 
				
			||||||
          message: res.data.message,
 | 
					          message: res.data.message,
 | 
				
			||||||
          color: "green",
 | 
					          color: "green",
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: "Error",
 | 
				
			||||||
 | 
					          message: res.data.message,
 | 
				
			||||||
 | 
					          color: "red",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.log(error);
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: "Error",
 | 
				
			||||||
 | 
					        message: "Failed to create scenario, please try again!",
 | 
				
			||||||
 | 
					        color: "red",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setIsSubmit(false);
 | 
					      setIsSubmit(false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -196,7 +225,7 @@ function DrawerScenario({
 | 
				
			||||||
                    form.setFieldValue("title", scenario.title);
 | 
					                    form.setFieldValue("title", scenario.title);
 | 
				
			||||||
                    form.setFieldValue("timeout", scenario.timeout.toString());
 | 
					                    form.setFieldValue("timeout", scenario.timeout.toString());
 | 
				
			||||||
                    form.setFieldValue("body", JSON.parse(scenario.body));
 | 
					                    form.setFieldValue("body", JSON.parse(scenario.body));
 | 
				
			||||||
                    form.setFieldValue("is_reboot", scenario.is_reboot);
 | 
					                    form.setFieldValue("isReboot", scenario.isReboot);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
| 
						 | 
					@ -231,7 +260,27 @@ function DrawerScenario({
 | 
				
			||||||
                    required
 | 
					                    required
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </Grid.Col>
 | 
					                </Grid.Col>
 | 
				
			||||||
                <Grid.Col span={6}>
 | 
					                <Grid.Col
 | 
				
			||||||
 | 
					                  span={3}
 | 
				
			||||||
 | 
					                  style={{
 | 
				
			||||||
 | 
					                    display: "flex",
 | 
				
			||||||
 | 
					                    alignItems: "end",
 | 
				
			||||||
 | 
					                    marginBottom: "8px",
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <Checkbox
 | 
				
			||||||
 | 
					                    label="Reboot"
 | 
				
			||||||
 | 
					                    style={{ color: "red" }}
 | 
				
			||||||
 | 
					                    checked={form.values.isReboot}
 | 
				
			||||||
 | 
					                    onChange={(event) =>
 | 
				
			||||||
 | 
					                      form.setFieldValue(
 | 
				
			||||||
 | 
					                        "isReboot",
 | 
				
			||||||
 | 
					                        event.currentTarget.checked
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Grid.Col>
 | 
				
			||||||
 | 
					                <Grid.Col span={3}>
 | 
				
			||||||
                  <div
 | 
					                  <div
 | 
				
			||||||
                    style={{
 | 
					                    style={{
 | 
				
			||||||
                      display: "flex",
 | 
					                      display: "flex",
 | 
				
			||||||
| 
						 | 
					@ -329,7 +378,7 @@ function DrawerScenario({
 | 
				
			||||||
        close={() => {
 | 
					        close={() => {
 | 
				
			||||||
          setOpenConfirm(false);
 | 
					          setOpenConfirm(false);
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        message={"Are you sure delete this station?"}
 | 
					        message={"Are you sure delete this scenario?"}
 | 
				
			||||||
        handle={() => {
 | 
					        handle={() => {
 | 
				
			||||||
          setOpenConfirm(false);
 | 
					          setOpenConfirm(false);
 | 
				
			||||||
          handleDelete();
 | 
					          handleDelete();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,120 @@
 | 
				
			||||||
 | 
					import { Box, Button, Grid, Modal, Text } from "@mantine/core";
 | 
				
			||||||
 | 
					import type { IScenario, TLine, TStation } from "../untils/types";
 | 
				
			||||||
 | 
					import TerminalCLI from "./TerminalXTerm";
 | 
				
			||||||
 | 
					import type { Socket } from "socket.io-client";
 | 
				
			||||||
 | 
					import classes from "./Component.module.css";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { IconCircleCheckFilled } from "@tabler/icons-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ModalTerminal = ({
 | 
				
			||||||
 | 
					  opened,
 | 
				
			||||||
 | 
					  onClose,
 | 
				
			||||||
 | 
					  line,
 | 
				
			||||||
 | 
					  socket,
 | 
				
			||||||
 | 
					  stationItem,
 | 
				
			||||||
 | 
					  scenarios,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  opened: boolean;
 | 
				
			||||||
 | 
					  onClose: () => void;
 | 
				
			||||||
 | 
					  line: TLine | undefined;
 | 
				
			||||||
 | 
					  socket: Socket | null;
 | 
				
			||||||
 | 
					  stationItem: TStation | undefined;
 | 
				
			||||||
 | 
					  scenarios: IScenario[];
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  const [isDisable, setIsDisable] = useState<boolean>(false);
 | 
				
			||||||
 | 
					  // console.log(line);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box>
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        opened={opened}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          onClose();
 | 
				
			||||||
 | 
					          socket?.emit("close_cli", {
 | 
				
			||||||
 | 
					            lineId: line?.id,
 | 
				
			||||||
 | 
					            stationId: line?.station_id,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        size={"80%"}
 | 
				
			||||||
 | 
					        style={{ position: "absolute", left: 0 }}
 | 
				
			||||||
 | 
					        title={
 | 
				
			||||||
 | 
					          <Box
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              display: "flex",
 | 
				
			||||||
 | 
					              justifyContent: "center",
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Text size="md" mr={10}>
 | 
				
			||||||
 | 
					              Line number: <strong>{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",
 | 
				
			||||||
 | 
					                color: "red",
 | 
				
			||||||
 | 
					                display: "flex",
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {line?.userOpenCLI
 | 
				
			||||||
 | 
					                ? line?.userOpenCLI + " is using"
 | 
				
			||||||
 | 
					                : "Terminal is used"}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Grid>
 | 
				
			||||||
 | 
					          <Grid.Col span={10} style={{ borderRight: "1px solid #ccc" }}>
 | 
				
			||||||
 | 
					            <TerminalCLI
 | 
				
			||||||
 | 
					              cliOpened={opened}
 | 
				
			||||||
 | 
					              socket={socket}
 | 
				
			||||||
 | 
					              content={line?.output ?? ""}
 | 
				
			||||||
 | 
					              initContent={line?.netOutput ?? ""}
 | 
				
			||||||
 | 
					              loadingContent={line?.loadingOutput}
 | 
				
			||||||
 | 
					              line_id={Number(line?.id)}
 | 
				
			||||||
 | 
					              station_id={Number(stationItem?.id)}
 | 
				
			||||||
 | 
					              isDisabled={false}
 | 
				
			||||||
 | 
					              line_status={line?.status || ""}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Grid.Col>
 | 
				
			||||||
 | 
					          <Grid.Col span={2}>
 | 
				
			||||||
 | 
					            {scenarios.map((scenario) => (
 | 
				
			||||||
 | 
					              <Button
 | 
				
			||||||
 | 
					                disabled={isDisable}
 | 
				
			||||||
 | 
					                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,
 | 
				
			||||||
 | 
					                      })
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {scenario.title}
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </Grid.Col>
 | 
				
			||||||
 | 
					        </Grid>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ModalTerminal;
 | 
				
			||||||
| 
						 | 
					@ -4,11 +4,11 @@ import "xterm/css/xterm.css";
 | 
				
			||||||
import { FitAddon } from "@xterm/addon-fit";
 | 
					import { FitAddon } from "@xterm/addon-fit";
 | 
				
			||||||
import { SOCKET_EVENTS } from "../untils/constanst";
 | 
					import { SOCKET_EVENTS } from "../untils/constanst";
 | 
				
			||||||
import type { Socket } from "socket.io-client";
 | 
					import type { Socket } from "socket.io-client";
 | 
				
			||||||
import type { LineConfig } from "../untils/types";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface TerminalCLIProps {
 | 
					interface TerminalCLIProps {
 | 
				
			||||||
  socket: Socket | null;
 | 
					  socket: Socket | null;
 | 
				
			||||||
  content?: string;
 | 
					  content?: string;
 | 
				
			||||||
 | 
					  initContent?: string;
 | 
				
			||||||
  line_id: number;
 | 
					  line_id: number;
 | 
				
			||||||
  line_status: string;
 | 
					  line_status: string;
 | 
				
			||||||
  station_id: number;
 | 
					  station_id: number;
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ interface TerminalCLIProps {
 | 
				
			||||||
  onDoubleClick?: () => void;
 | 
					  onDoubleClick?: () => void;
 | 
				
			||||||
  fontSize?: number;
 | 
					  fontSize?: number;
 | 
				
			||||||
  miniSize?: boolean;
 | 
					  miniSize?: boolean;
 | 
				
			||||||
  updateStatus: (value: LineConfig) => void;
 | 
					  loadingContent?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TerminalCLI: React.FC<TerminalCLIProps> = ({
 | 
					const TerminalCLI: React.FC<TerminalCLIProps> = ({
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
 | 
				
			||||||
  onDoubleClick = () => {},
 | 
					  onDoubleClick = () => {},
 | 
				
			||||||
  fontSize = 14,
 | 
					  fontSize = 14,
 | 
				
			||||||
  miniSize = false,
 | 
					  miniSize = false,
 | 
				
			||||||
  updateStatus,
 | 
					  initContent = "",
 | 
				
			||||||
 | 
					  loadingContent = false,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const xtermRef = useRef<HTMLDivElement>(null);
 | 
					  const xtermRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
  const terminal = useRef<Terminal>(null);
 | 
					  const terminal = useRef<Terminal>(null);
 | 
				
			||||||
| 
						 | 
					@ -128,40 +129,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (fitRef.current) setTimeout(() => fitRef.current?.fit(), 500);
 | 
					      if (fitRef.current) setTimeout(() => fitRef.current?.fit(), 500);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [content]);
 | 
					  }, [content, loadingContent]);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    // Nhận output từ thiết bị và ghi vào terminal
 | 
					 | 
				
			||||||
    socket?.on("line_output", (data) => {
 | 
					 | 
				
			||||||
      if (data?.lineId === line_id && terminal.current) {
 | 
					 | 
				
			||||||
        terminal.current?.write(data.data);
 | 
					 | 
				
			||||||
        terminal.current?.focus();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    socket?.on("line_error", (data) => {
 | 
					 | 
				
			||||||
      if (data?.lineId === line_id && terminal.current) {
 | 
					 | 
				
			||||||
        terminal.current?.write(data.error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    socket?.on("init", (data) => {
 | 
					 | 
				
			||||||
      if (Array.isArray(data)) {
 | 
					 | 
				
			||||||
        data.forEach((value) => {
 | 
					 | 
				
			||||||
          if (value?.id === line_id && terminal.current) {
 | 
					 | 
				
			||||||
            terminal.current?.write(value.output);
 | 
					 | 
				
			||||||
            updateStatus({ ...value, lineId: value.id });
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return () => {
 | 
					 | 
				
			||||||
      socket?.off("init");
 | 
					 | 
				
			||||||
      socket?.off("line_error");
 | 
					 | 
				
			||||||
      socket?.off("line_output");
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (cliOpened) {
 | 
					    if (cliOpened) {
 | 
				
			||||||
| 
						 | 
					@ -182,7 +150,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (!loading) {
 | 
					    if (!loading) {
 | 
				
			||||||
      if (terminal.current) {
 | 
					      if (terminal.current) {
 | 
				
			||||||
        terminal.current?.write(content);
 | 
					        terminal.current?.write(initContent);
 | 
				
			||||||
        if (!miniSize && !isDisabled) terminal.current?.focus();
 | 
					        if (!miniSize && !isDisabled) terminal.current?.focus();
 | 
				
			||||||
        terminal.current.scrollToBottom();
 | 
					        terminal.current.scrollToBottom();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					@ -211,7 +179,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
 | 
				
			||||||
          height: "100%",
 | 
					          height: "100%",
 | 
				
			||||||
          backgroundColor: "black",
 | 
					          backgroundColor: "black",
 | 
				
			||||||
          paddingBottom: customStyle.paddingBottom ?? "10px",
 | 
					          paddingBottom: customStyle.paddingBottom ?? "10px",
 | 
				
			||||||
          minHeight: customStyle.maxHeight ?? "60vh",
 | 
					          minHeight: customStyle.maxHeight ?? "75vh",
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
| 
						 | 
					@ -221,8 +189,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
 | 
				
			||||||
            paddingLeft: customStyle.paddingLeft ?? "10px",
 | 
					            paddingLeft: customStyle.paddingLeft ?? "10px",
 | 
				
			||||||
            paddingBottom: customStyle.paddingBottom ?? "10px",
 | 
					            paddingBottom: customStyle.paddingBottom ?? "10px",
 | 
				
			||||||
            fontSize: customStyle.fontSize ?? "9px",
 | 
					            fontSize: customStyle.fontSize ?? "9px",
 | 
				
			||||||
            maxHeight: customStyle.maxHeight ?? "60vh",
 | 
					            maxHeight: customStyle.maxHeight ?? "75vh",
 | 
				
			||||||
            height: customStyle.height ?? "60vh",
 | 
					            height: customStyle.height ?? "75vh",
 | 
				
			||||||
            padding: customStyle.padding ?? "4px",
 | 
					            padding: customStyle.padding ?? "4px",
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          onDoubleClick={(event) => {
 | 
					          onDoubleClick={(event) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,10 @@
 | 
				
			||||||
import React, { createContext, useContext, useEffect, useState } from "react";
 | 
					import React, {
 | 
				
			||||||
 | 
					  createContext,
 | 
				
			||||||
 | 
					  useContext,
 | 
				
			||||||
 | 
					  useEffect,
 | 
				
			||||||
 | 
					  useMemo,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					} from "react";
 | 
				
			||||||
import { io, Socket } from "socket.io-client";
 | 
					import { io, Socket } from "socket.io-client";
 | 
				
			||||||
import { SOCKET_EVENTS } from "../untils/constanst";
 | 
					import { SOCKET_EVENTS } from "../untils/constanst";
 | 
				
			||||||
import { notifications } from "@mantine/notifications";
 | 
					import { notifications } from "@mantine/notifications";
 | 
				
			||||||
| 
						 | 
					@ -15,9 +21,21 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
 | 
				
			||||||
  children,
 | 
					  children,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const [socket, setSocket] = useState<Socket | null>(null);
 | 
					  const [socket, setSocket] = useState<Socket | null>(null);
 | 
				
			||||||
 | 
					  const user = useMemo(() => {
 | 
				
			||||||
 | 
					    return localStorage.getItem("user") &&
 | 
				
			||||||
 | 
					      typeof localStorage.getItem("user") === "string"
 | 
				
			||||||
 | 
					      ? JSON.parse(localStorage.getItem("user") || "")
 | 
				
			||||||
 | 
					      : null;
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const newSocket = io(SOCKET_URL);
 | 
					    if (!user) return;
 | 
				
			||||||
 | 
					    const newSocket = io(SOCKET_URL, {
 | 
				
			||||||
 | 
					      auth: {
 | 
				
			||||||
 | 
					        userId: user?.id,
 | 
				
			||||||
 | 
					        userName: user?.fullName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setSocket(newSocket);
 | 
					    setSocket(newSocket);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,7 +64,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      newSocket.disconnect();
 | 
					      newSocket.disconnect();
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, []);
 | 
					  }, [user]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <SocketContext.Provider value={{ socket }}>
 | 
					    <SocketContext.Provider value={{ socket }}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,3 +2,8 @@ export const numberOnly = (value: string): string => {
 | 
				
			||||||
  const matched = value.match(/[\d.]+/g);
 | 
					  const matched = value.match(/[\d.]+/g);
 | 
				
			||||||
  return matched ? matched.join("") : "";
 | 
					  return matched ? matched.join("") : "";
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const passwordRegex =
 | 
				
			||||||
 | 
					  /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,6 +67,8 @@ export type TLine = {
 | 
				
			||||||
  inventory?: any;
 | 
					  inventory?: any;
 | 
				
			||||||
  status?: string;
 | 
					  status?: string;
 | 
				
			||||||
  netOutput?: string;
 | 
					  netOutput?: string;
 | 
				
			||||||
 | 
					  output?: string;
 | 
				
			||||||
 | 
					  loadingOutput?: boolean;
 | 
				
			||||||
  outlet?: number;
 | 
					  outlet?: number;
 | 
				
			||||||
  cliOpened?: boolean;
 | 
					  cliOpened?: boolean;
 | 
				
			||||||
  systemLogUrl?: string;
 | 
					  systemLogUrl?: string;
 | 
				
			||||||
| 
						 | 
					@ -84,14 +86,8 @@ export type TLine = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type TUser = {
 | 
					export type TUser = {
 | 
				
			||||||
  id: number;
 | 
					  userId: number;
 | 
				
			||||||
  email: string;
 | 
					  userName: string;
 | 
				
			||||||
  email_cc: string;
 | 
					 | 
				
			||||||
  full_name: string;
 | 
					 | 
				
			||||||
  package_id: string;
 | 
					 | 
				
			||||||
  zulip: string;
 | 
					 | 
				
			||||||
  token?: string;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type APCProps = {
 | 
					export type APCProps = {
 | 
				
			||||||
| 
						 | 
					@ -144,7 +140,7 @@ export type IScenario = {
 | 
				
			||||||
  title: string;
 | 
					  title: string;
 | 
				
			||||||
  body: string;
 | 
					  body: string;
 | 
				
			||||||
  timeout: number;
 | 
					  timeout: number;
 | 
				
			||||||
  is_reboot: boolean;
 | 
					  isReboot: boolean;
 | 
				
			||||||
  updated_at: string;
 | 
					  updated_at: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -153,5 +149,4 @@ export type IBodyScenario = {
 | 
				
			||||||
  send: string;
 | 
					  send: string;
 | 
				
			||||||
  delay: string;
 | 
					  delay: string;
 | 
				
			||||||
  repeat: string;
 | 
					  repeat: string;
 | 
				
			||||||
  note: string;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue