From cbc4a8c9b088ee33138a041936b89946a399e8e2 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:25:40 +0700 Subject: [PATCH] Add ticket management API and update baud rate handling Introduces a new TicketsController and Ticket model to support ticket CRUD operations via new /api/ticket routes. Updates line_connection service to improve baud rate command sequence. Adjusts CardLine component to include stationId when emitting set_baud events. Minor improvements to socket_io_provider for safer disconnect handling. --- BACKEND/app/controllers/tickets_controller.ts | 188 ++++++++++++++++++ BACKEND/app/models/ticket.ts | 47 +++++ BACKEND/app/services/line_connection.ts | 18 +- BACKEND/providers/socket_io_provider.ts | 4 +- BACKEND/start/routes.ts | 11 + FRONTEND/src/components/CardLine.tsx | 2 + 6 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 BACKEND/app/controllers/tickets_controller.ts create mode 100644 BACKEND/app/models/ticket.ts diff --git a/BACKEND/app/controllers/tickets_controller.ts b/BACKEND/app/controllers/tickets_controller.ts new file mode 100644 index 0000000..8adb28e --- /dev/null +++ b/BACKEND/app/controllers/tickets_controller.ts @@ -0,0 +1,188 @@ +import Ticket from '#models/ticket' +import type { HttpContext } from '@adonisjs/core/http' +import db from '@adonisjs/lucid/services/db' + +export default class TicketsController { + /** + * List all tickets + */ + async get({ request, response, auth }: HttpContext) { + try { + const perPage = request.input('per_page', 1) + const page = request.input('page', 1) + + const queryLatest = Ticket.query() + .where('station_id', request.input('station_id')) + .where('sn', request.input('sn')) + + const query = Ticket.query() + .where('station_id', request.input('station_id')) + .where('sn', request.input('sn')) + .whereNot('status', 'closed') + + const tickets = await query.orderBy('tickets.created_at', 'desc').paginate(page, perPage) + return response.ok({ + status: true, + data: tickets, + latest: + (await queryLatest.orderBy('tickets.created_at', 'desc').paginate(page, perPage)) || null, + }) + } catch (error) { + return response.internalServerError({ + status: false, + message: 'Failed to fetch tickets', + error, + }) + } + } + + async getAll({ request, response, auth }: HttpContext) { + try { + const perPage = request.input('per_page', 20) + const page = request.input('page', 1) + + const query = Ticket.query() + .where('station_id', request.input('station_id')) + .where('sn', request.input('sn')) + // .whereNot('status', 'closed') + + const tickets = await query.orderBy('tickets.created_at', 'desc').paginate(page, perPage) + return response.ok({ + status: true, + data: tickets, + }) + } catch (error) { + return response.internalServerError({ + status: false, + message: 'Failed to fetch tickets', + error, + }) + } + } + + /** + * Create a new ticket + */ + async create({ request, response, auth }: HttpContext) { + try { + const payload = await request.all() + + const history = [ + { + userId: payload.userId, + userName: payload.userName, + status: payload.status, + description: payload.description.trim(), + time: Date.now(), + }, + ] + + const trx = await db.transaction() + try { + const ticket = await Ticket.create( + { + description: payload.description.trim(), + model: payload.model.trim(), + sn: payload.sn.trim(), + stationId: payload.station_id, + status: 'open', + history: JSON.stringify(history), + }, + { client: trx } + ) + + await trx.commit() + + return response.ok({ + status: true, + message: 'Ticket created successfully', + data: ticket, + }) + } catch (error) { + await trx.rollback() + + return response.internalServerError({ + status: false, + message: 'Failed to create ticket, please try again!', + error, + }) + } + } catch (error) { + return response.internalServerError({ + status: false, + message: 'Failed to create ticket', + error, + }) + } + } + + /** + * Get a single ticket by ID + */ + async show({ params, response }: HttpContext) { + try { + const ticket = await Ticket.findOrFail(params.id) + return response.ok({ status: true, data: ticket }) + } catch (error) { + return response.notFound({ status: false, message: 'Ticket not found' }) + } + } + + /** + * Update a ticket + */ + async update({ request, response, auth }: HttpContext) { + try { + const ticketId = request.param('id') + const payload = await request.all() + const ticket = await Ticket.findOrFail(ticketId) + const history = { + userId: payload.userId, + userName: payload.userName, + status: payload.status, + description: payload.description.trim(), + time: Date.now(), + } + if (!ticket) { + return response.notFound({ message: 'Ticket not found' }) + } + const listHistory = ticket.history ? JSON.parse(ticket.history) : [] + listHistory.unshift(history) + payload.history = JSON.stringify(listHistory) + ticket.merge(payload) + await ticket.save() + + return response.ok({ status: true, message: 'Ticket updated successfully', data: ticket }) + } catch (error) { + return response.internalServerError({ + status: false, + message: 'Failed to update ticket', + error, + }) + } + } + + /** + * Delete a ticket + */ + async delete({ request, response }: HttpContext) { + try { + const ticketId = request.param('id') + const ticket = await Ticket.findOrFail(ticketId) + + if (!ticket) { + return response.notFound({ message: 'Ticket not found' }) + } + + await ticket.delete() + + return response.ok({ status: true, message: 'Ticket deleted successfully' }) + } catch (error) { + return response.internalServerError({ + status: false, + message: 'Failed to delete ticket', + error, + }) + } + } +} diff --git a/BACKEND/app/models/ticket.ts b/BACKEND/app/models/ticket.ts new file mode 100644 index 0000000..26e9af7 --- /dev/null +++ b/BACKEND/app/models/ticket.ts @@ -0,0 +1,47 @@ +import Station from '#models/station' +import { BaseModel, belongsTo, column } from '@adonisjs/lucid/orm' +import type { BelongsTo } from '@adonisjs/lucid/types/relations' +import { DateTime } from 'luxon' +import Line from './line.js' + +export default class Ticket extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare description: string + + @column() + declare model: string + + @column() + declare sn: string + + @column() + declare stationId: string + + @column() + declare lineId: string + + @column() + declare status: string + + @column() + declare history: string + + @belongsTo(() => Station, { + foreignKey: 'station_id', + }) + declare station: BelongsTo + + @belongsTo(() => Line, { + foreignKey: 'line_id', + }) + declare line: BelongsTo + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + declare updatedAt: DateTime +} diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index c3f3725..2f247ac 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -192,13 +192,13 @@ export default class LineConnection { async writeCommand(cmd: string | Buffer, userName = '') { if (this.client.destroyed) { console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`) - if (this.retryConnect <= 3) { - await sleep(2000) - console.log('Retry connect times', this.retryConnect) - this.retryConnect += 1 - await this.connect() - await this.writeCommand(cmd) - } + // if (this.retryConnect <= 3) { + // await sleep(2000) + // console.log('Retry connect times', this.retryConnect) + // this.retryConnect += 1 + // await this.connect() + // await this.writeCommand(cmd) + // } return } @@ -553,9 +553,11 @@ export default class LineConnection { async setBaud(baud: number) { this.writeCommand('enable\r\n') await sleep(500) + this.writeCommand('configure terminal\r\n') + await sleep(500) this.writeCommand('line console 0\r\n') await sleep(500) - this.writeCommand(`speed ${baud}\r\n`) + this.writeCommand(`speed ${baud.toString()}\r\n`) await sleep(500) this.writeCommand('end\r\n') await sleep(500) diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index f48038c..f1bf195 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -520,7 +520,7 @@ export class WebSocketIo { delete this.intervalMap[`${lineId}`] } const interval = setInterval(() => { - lineConn.disconnect() + if (lineConn.disconnect) lineConn.disconnect() // this.lineMap.delete(lineId) if (this.intervalMap[`${lineId}`]) { clearInterval(this.intervalMap[`${lineId}`]) @@ -544,7 +544,7 @@ export class WebSocketIo { for (const lineId of lineIds) { try { const line = this.lineMap.get(lineId) - + // console.log(line?.config) if (line && line.config.status === 'connected') { this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) this.setTimeoutConnect(lineId, line, options.timeout) diff --git a/BACKEND/start/routes.ts b/BACKEND/start/routes.ts index 165f514..6981a3a 100644 --- a/BACKEND/start/routes.ts +++ b/BACKEND/start/routes.ts @@ -70,3 +70,14 @@ router router.post('/register', '#controllers/auth_controller.register') }) .prefix('api/auth') + +router + .group(() => { + router.post('/', '#controllers/tickets_controller.get') + router.post('/all', '#controllers/tickets_controller.getAll') + router.post('create', '#controllers/tickets_controller.create') + + router.put('update/:id', '#controllers/tickets_controller.update') + router.delete('delete/:id', '#controllers/tickets_controller.delete') + }) + .prefix('api/ticket') diff --git a/FRONTEND/src/components/CardLine.tsx b/FRONTEND/src/components/CardLine.tsx index b901c1a..aaba7dc 100644 --- a/FRONTEND/src/components/CardLine.tsx +++ b/FRONTEND/src/components/CardLine.tsx @@ -408,6 +408,7 @@ const CardLine = ({ socket?.emit("set_baud", { lineId: line.id, baud: el, + stationId: Number(stationItem.id), }); setIsDisabled(true); setTimeout(() => { @@ -427,6 +428,7 @@ const CardLine = ({ socket?.emit("set_baud", { lineId: line.id, baud: Number(valueBaud), + stationId: Number(stationItem.id), }); setValueBaud(""); setIsDisabled(true);