diff --git a/BACKEND/app/controllers/auth_controller.ts b/BACKEND/app/controllers/auth_controller.ts index b423cc5..22cabea 100644 --- a/BACKEND/app/controllers/auth_controller.ts +++ b/BACKEND/app/controllers/auth_controller.ts @@ -6,7 +6,7 @@ export default class AuthController { // Đăng ký async register({ request, response }: HttpContext) { try { - const data = request.only(['email', 'password', 'user_name']) + const data = request.only(['email', 'password', 'user_name', 'first_name', 'last_name']) const user = await User.query().where('user_name', data.user_name).first() @@ -23,7 +23,12 @@ export default class AuthController { // Đăng nhập async login({ request, auth, response }: HttpContext) { - const { user_name: userName, password } = request.only(['user_name', 'password']) + const { user_name: userName, password } = request.only([ + 'user_name', + 'password', + 'first_name', + 'last_name', + ]) const user = await User.query().where('user_name', userName).first() if (!user) { @@ -47,10 +52,18 @@ export default class AuthController { email: remoteUser.userEmail, userName: userName, password: password, + firstName: remoteUser?.firstName || null, + lastName: remoteUser?.lastName || null, }) return response.json({ message: 'Login successful', - user: { id: newUser.id, email: newUser.email, userName: newUser.userName }, + user: { + id: newUser.id, + email: newUser.email, + userName: newUser.userName, + firstName: newUser.firstName, + lastName: newUser.lastName, + }, }) } @@ -62,7 +75,13 @@ export default class AuthController { return response.json({ message: 'Login successful', - user: { id: user.id, email: user.email, userName: user.userName }, + user: { + id: user.id, + email: user.email, + userName: user.userName, + firstName: user.firstName, + lastName: user.lastName, + }, }) } catch { return response.status(401).json({ message: 'Invalid credentials' }) diff --git a/BACKEND/app/models/user.ts b/BACKEND/app/models/user.ts index e5068e2..ce897bf 100644 --- a/BACKEND/app/models/user.ts +++ b/BACKEND/app/models/user.ts @@ -14,6 +14,12 @@ export default class User extends BaseModel { @column() declare password: string + @column() + declare firstName: string | null + + @column() + declare lastName: string | null + @column.dateTime({ autoCreate: true }) declare createdAt: DateTime diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 1aad6c9..ed5011a 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -545,10 +545,11 @@ export default class LineConnection { if ( ['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command) ) { - const dataInventory = JSON.parse(item.textfsm)[0] + const listInventory = JSON.parse(item.textfsm) + const dataInventory = listInventory[0] this.config.inventory = this.config.inventory - ? { ...this.config.inventory, ...dataInventory } - : dataInventory + ? { ...this.config.inventory, ...dataInventory, listInventory } + : { ...dataInventory, listInventory } pid = dataInventory?.pid || '' this.addHistory(this.config.stationId, this.config.id, { id: this.config.id, @@ -807,9 +808,10 @@ export default class LineConnection { data.forEach((item) => { if (item?.textfsm && isValidJson(item?.textfsm)) { if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) { - const dataInventory = JSON.parse(item.textfsm)[0] + const listInventory = JSON.parse(item.textfsm) + const dataInventory = listInventory[0] this.config.inventory = this.config.inventory - ? { ...this.config.inventory, ...dataInventory } + ? { ...this.config.inventory, ...dataInventory, listInventory } : dataInventory } item.textfsm = JSON.parse(item.textfsm) @@ -1164,7 +1166,7 @@ Ports Missing/Down: ${missing.length}\n\n` /** * Starting physical test (PoE ports testing) */ - async runPhysicalTest() { + async runPhysicalTest(userName?: string) { if (this.config.runningPhysical) { console.log('Running physical test') return @@ -1964,13 +1966,13 @@ Ports Missing/Down: ${missing.length}\n\n` : '—' // ---- Template-fallback values (use file's hardcoded content when no real data) ---- - const productName = 'Cisco Catalyst 9300-48P-A' - const productPN = escapeHtml(String(config?.inventory?.pid || 'C9300-48P-A')) - const productSN = escapeHtml(String(config?.inventory?.sn || 'FCW2425L0KP')) - const productVid = escapeHtml(String(config?.inventory?.vid || 'V02')) - const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || '17.09.04a')) - const memDisplay = escapeHtml(memText !== '—' ? memText : '8 GB') - const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '16 GB') + const productName = escapeHtml(String(config?.inventory?.name || '')) + const productPN = escapeHtml(String(config?.inventory?.pid || '')) + const productSN = escapeHtml(String(config?.inventory?.sn || '')) + const productVid = escapeHtml(String(config?.inventory?.vid || '')) + const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || '')) + const memDisplay = escapeHtml(memText !== '—' ? memText : '') + const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '') // AI issue rows (one per real AI issue, fall back to file's hardcoded row when none) const aiIssueRowsHtml = @@ -1988,6 +1990,7 @@ Ports Missing/Down: ${missing.length}\n\n` const licenseBoxesHtml = dataShowLic && dataShowLic.length > 0 ? dataShowLic + .filter((l) => l.LICENSE_TYPE && l.FEATURE) .map( (l: any) => `
${escapeHtml(String(l.FEATURE || ''))}
${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}
` @@ -1997,8 +2000,8 @@ Ports Missing/Down: ${missing.length}\n\n` // Port stat values (real numbers if any port data, else file's defaults) const hasPortData = portPhysical.length > 0 - const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '48/48' - const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '4/4' + const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '0/0' + const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '0/0' const poeColor = !hasPortData || (totalPoE > 0 && testedPoE.length === totalPoE) ? '#10b981' : '#f59e0b' const sfpColor = @@ -2104,7 +2107,7 @@ Ports Missing/Down: ${missing.length}\n\n` @@ -2310,11 +2351,15 @@ Ports Missing/Down: ${missing.length}\n\n` ` + ) + .join('') || '' + } +
- +
Product Info
@@ -2220,27 +2223,65 @@ Ports Missing/Down: ${missing.length}\n\n`
+ + + + + + + +
+ + - - - + -
-
-
Received
-
Trung Nguyen
-
06 May 10:30
-
-
-
Physical Check
-
Khanh Le
-
06 May 11:15
-
-
-
Software Test
-
Duy Pham (remote)
-
06 May 14:00
-
+
+   +
+
+ +
+
+ ✓ +
+
+ Received +
+
+ Trung Nguyen +
+
+ 06 May 10:30 +
+
+
+ ✓ +
+ +
+ Physical Check +
+
+ Khanh Le +
+
+ 06 May 11:15 +
+
+
+ ✓ +
+
+ Software Test +
+
+ Duy Pham (remote) +
+
+ 06 May 14:00 +
+
Hardware Inventory
- - - - -
${productPN}${productSN}
PWR-C1-715WAC-PLIT241525W1
C9300-NM-4GFDO2420H0X1
FAN-T2-GEN2 (x3)OK
+ ${ + this.config?.inventory?.listInventory + ?.map( + (item: any) => ` +
${item.pid}${item.sn}
System & License
@@ -2328,8 +2373,8 @@ Ports Missing/Down: ${missing.length}\n\n`
${escapeHtml(sfpText)}
SFP+ UP
-
${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}
PoE+ Test
-
${Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%
Throughput
+
${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}
PoE+ Test
+
${totalPoE + totalSFP === 0 ? 100 : Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%
Throughput
${missingDetailsHtml} @@ -2340,7 +2385,7 @@ Ports Missing/Down: ${missing.length}\n\n` -
${snapshot?.outputTestLog || 'No test log available'}
+
CONSOLE RAW OUTPUT (Boot Log snippet)
${snapshot?.outputTestLog || 'No test log available'}
diff --git a/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts b/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts new file mode 100644 index 0000000..800c368 --- /dev/null +++ b/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts @@ -0,0 +1,19 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'users' + + async up() { + this.schema.alterTable(this.tableName, (table) => { + table.string('first_name').nullable().after('user_name') + table.string('last_name').nullable().after('first_name') + }) + } + + async down() { + this.schema.alterTable(this.tableName, (table) => { + table.dropColumn('first_name') + table.dropColumn('last_name') + }) + } +} diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 89c936d..b540061 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -728,7 +728,7 @@ export class WebSocketIo { }) socket.on('run_physical_test', async (data) => { - const { stationId, lineId } = data + const { stationId, lineId, userName: name } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return @@ -737,7 +737,7 @@ export class WebSocketIo { io, stationId, [lineId], - async (lineCon) => lineCon.runPhysicalTest(), + async (lineCon) => lineCon.runPhysicalTest(name), {} ) }) diff --git a/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx b/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx index ffb7a69..6bb5f6d 100644 --- a/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx +++ b/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Modal, Box, @@ -33,6 +33,12 @@ export default function ModalConfirmRunPhysical({ socket, station, }: Props) { + const user = useMemo(() => { + return localStorage.getItem("user") && + typeof localStorage.getItem("user") === "string" + ? JSON.parse(localStorage.getItem("user") || "") + : null; + }, []); const [dataLines, setDataLines] = useState([]); const [isDisabled, setIsDisabled] = useState(false); @@ -48,7 +54,7 @@ export default function ModalConfirmRunPhysical({ sn: line?.inventory?.sn, vid: line?.inventory?.vid, checked: true, - })) + })), ); } }, [listLines]); @@ -100,8 +106,8 @@ export default function ModalConfirmRunPhysical({ ...el, checked: e.target.checked, } - : el - ) + : el, + ), ) } /> @@ -133,13 +139,16 @@ export default function ModalConfirmRunPhysical({ socket?.emit("run_physical_test", { lineId: line?.id, stationId: Number(station?.id), + userName: user?.firstName + ? `${user.firstName} ${user.lastName || ""}` + : user?.userName || "Unknown User", }); }); setDataLines(listNotChecked); setListLines( listLines.filter((el) => - listNotChecked.find((li) => li.id === el.id) - ) + listNotChecked.find((li) => li.id === el.id), + ), ); setIsDisabled(true); setTimeout(() => {