From d4ea801bef9ecd081a18d89f9f4875f0b55bb8b8 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Mon, 18 May 2026 16:20:09 +0700 Subject: [PATCH] Update view history log, tested ports --- BACKEND/app/services/line_connection.ts | 75 ++++-- .../src/components/Modal/ModalLineHistory.tsx | 246 +++++++++++------- FRONTEND/src/components/Modal/ModalLog.tsx | 3 +- .../Modal/ModalPortPhysicalTest.tsx | 131 ++++++++++ 4 files changed, 348 insertions(+), 107 deletions(-) create mode 100644 FRONTEND/src/components/Modal/ModalPortPhysicalTest.tsx diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 5282ff6..6b47ea5 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -30,7 +30,7 @@ import axios from 'axios' import redis from '@adonisjs/redis/services/main' import Line from '#models/line' import PromptAi from '#models/prompt_ai' -import { CustomSocket, ErrorRow, TestResult } from '../ultils/types.js' +import { CustomSocket, ErrorRow, PortState, TestResult } from '../ultils/types.js' import momentTZ from 'moment-timezone' import { PhysicalPortTest } from './physical_test_service.js' import Station from '#models/station' @@ -948,41 +948,67 @@ export default class LineConnection { /** * Add cache to list history devices on this line */ - async addHistory(stationId: number, lineId: number, item: HistoryItem) { - if (!item.pid || !item.sn) return + async addHistory( + stationId: number, + lineId: number, + item: HistoryItem, + outputLog?: string, + portPhysical?: PortState[] + ) { + if (!item.pid || !item.sn) return false const key = `station:${stationId}:line:${lineId}:history` const now = Date.now() - const newItem = JSON.stringify({ - ...item, - timestamp: now, - }) + // Tạo object chứa các field mở rộng nếu được truyền vào + const extendedFields: any = {} + if (outputLog !== undefined) extendedFields.output = outputLog + if (portPhysical !== undefined) extendedFields.portPhysical = portPhysical - // Lấy phần tử cuối + // Lấy phần tử cuối cùng trong ZSET mang tính timeline const lastItems = await redis.zrevrange(key, 0, 0) + if (lastItems.length > 0) { const last = JSON.parse(lastItems[0]) + // TRƯỜNG HỢP 1: Trùng pid và sn -> Cập nhật lại bản ghi cũ if (last.pid === item.pid && last.sn === item.sn) { - return false // không thay đổi + const updatedItemObj = { + ...last, + ...extendedFields, + } + const updatedItemStr = JSON.stringify(updatedItemObj) + + // Nếu dữ liệu mới không khác gì dữ liệu cũ thì không cần làm gì cả + if (lastItems[0] === updatedItemStr) { + return false + } + + await redis.multi().zrem(key, lastItems[0]).zadd(key, last.timestamp, updatedItemStr).exec() + + return true } } const line = await Line.find(lineId) if (line) { const listHistory = line.history ? JSON.parse(line.history) : [] - listHistory.unshift(newItem) + listHistory.unshift({ ...item, timestamp: now }) line.history = JSON.stringify(listHistory) await line.save() } - // Add vào ZSET - await redis.zadd(key, now, newItem) - // Tự động xóa item > 96h - const expireTime = now - 96 * 60 * 60 * 1000 - await redis.zremrangebyscore(key, 0, expireTime) + // const expireTime = now - 96 * 60 * 60 * 1000 + // await redis.zremrangebyscore(key, 0, expireTime) + // TRƯỜNG HỢP 2: Sản phẩm mới hoàn toàn -> Thêm mới vào ZSET + const newItem = JSON.stringify({ + ...item, + ...extendedFields, + timestamp: now, + }) + + await redis.zadd(key, now, newItem) return true } @@ -1184,7 +1210,7 @@ Ports Missing/Down: ${missing.length}\n\n` console.log('Running physical test') return } - this.setTimeoutSendSummaryReport(600000) + this.setTimeoutSendSummaryReport(1200000) this.config.runningPhysical = true this.config.runningScenario = 'Physical Test' this.config.isSkipPhysical = false @@ -2495,7 +2521,22 @@ Ports Missing/Down: ${missing.length}\n\n` console.error(`Failed to save report for SN ${reportSN}:`, err) } } - + this.addHistory( + this.config.stationId, + this.config.id, + { + id: this.config.id, + number: this.config.lineNumber, + stationId: this.config.stationId, + pid: productPN, + sn: productSN, + vid: productVid, + scenario: '', + timestamp: Date.now(), + }, + this.outputTestLog, + portPhysical + ) this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP) await sendMessageToMail( `[ATC] - [${config.stationName} - L${config.lineNumber}] - ${this.config.inventory?.pid} - ${this.config.inventory?.sn} - Test Summary`, diff --git a/FRONTEND/src/components/Modal/ModalLineHistory.tsx b/FRONTEND/src/components/Modal/ModalLineHistory.tsx index 89e78b8..252e699 100644 --- a/FRONTEND/src/components/Modal/ModalLineHistory.tsx +++ b/FRONTEND/src/components/Modal/ModalLineHistory.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { + ActionIcon, Box, CloseButton, Flex, @@ -11,6 +12,9 @@ import { import moment from "moment"; import type { Socket } from "socket.io-client"; import classes from "../Component.module.css"; +import { IconFileText, IconListCheck } from "@tabler/icons-react"; +import ModalPortPhysicalTest from "./ModalPortPhysicalTest"; +import ModalLog from "./ModalLog"; interface LineHistoryItem { id: number; @@ -20,6 +24,13 @@ interface LineHistoryItem { vid: string; sn: string; scenario: string; + output?: string; + portPhysical?: [ + { + name: string; + tested: boolean; + }, + ]; timestamp: number; } @@ -44,6 +55,10 @@ const ModalLineHistory = ({ }: ModalLineHistoryProps) => { const [history, setHistory] = useState([]); const [loading, setLoading] = useState(false); + const [openPortPhysical, setOpenPortPhysical] = useState(false); + const [selectedHistory, setSelectedHistory] = + useState(null); + const [openLog, setOpenLog] = useState(false); useEffect(() => { if (!socket || !opened) return; @@ -77,104 +92,157 @@ const ModalLineHistory = ({ const sorted = [...history].sort((a, b) => b.timestamp - a.timestamp); return ( -
{ - e.stopPropagation(); - onClose(); - }} - > + <>
{ + e.stopPropagation(); + onClose(); }} - onClick={(e) => e.stopPropagation()} > - e.stopPropagation()} > - - 🕘 Line history - {lineNumber ? ` — Line ${lineNumber}` : ""} - {stationName ? ` (${stationName})` : ""} - - - + + + 🕘 Line history + {lineNumber ? ` — Line ${lineNumber}` : ""} + {stationName ? ` (${stationName})` : ""} + + + - - {loading ? ( - - - - ) : sorted.length === 0 ? ( - - No history data available - - ) : ( - - - - - PID - VID - SN - Scenario - Time - - - - {sorted.map((item, i) => ( - - - {item.pid || "-"} - - {item.vid || "-"} - {item.sn || "-"} - {item.scenario || "-"} - - {item.timestamp - ? moment(item.timestamp).format("DD/MM/YYYY HH:mm:ss") - : "-"} - + + {loading ? ( + + + + ) : sorted.length === 0 ? ( + + No history data available + + ) : ( + +
+ + + PID + VID + SN + Scenario + Log + Time - ))} - -
-
- )} -
+ + + {sorted.map((item, i) => ( + + + {item.pid || "-"} + + {item.vid || "-"} + {item.sn || "-"} + {item.scenario || "-"} + + + {item.output ? ( + { + setSelectedHistory(item); + setOpenLog(true); + }} + > + + + ) : ( + "" + )} + {item.portPhysical ? ( + { + setSelectedHistory(item); + setOpenPortPhysical(true); + }} + > + + + ) : ( + "" + )} + + + + {item.timestamp + ? moment(item.timestamp).format( + "DD/MM/YYYY HH:mm:ss", + ) + : "-"} + + + ))} + + + + )} + +
- + { + setSelectedHistory(null); + setOpenPortPhysical(false); + }} + selectedHistory={selectedHistory} + lineNumber={lineNumber} + stationName={stationName} + /> + { + setSelectedHistory(null); + setOpenLog(false); + }} + testLogContent={selectedHistory?.output || ""} + /> + ); }; diff --git a/FRONTEND/src/components/Modal/ModalLog.tsx b/FRONTEND/src/components/Modal/ModalLog.tsx index 25d0448..7793afe 100644 --- a/FRONTEND/src/components/Modal/ModalLog.tsx +++ b/FRONTEND/src/components/Modal/ModalLog.tsx @@ -35,7 +35,7 @@ const ModalLog = ({ return `${prefix}${timestamp}${suffix}`; - } + }, ) // Highlight full ---User--- .replace(/^-------([^-\n]+)-------$/gm, (match) => { @@ -51,6 +51,7 @@ const ModalLog = ({ onClose={() => { onClose(); }} + zIndex={100001} title={ Log Content diff --git a/FRONTEND/src/components/Modal/ModalPortPhysicalTest.tsx b/FRONTEND/src/components/Modal/ModalPortPhysicalTest.tsx new file mode 100644 index 0000000..7dfc8a9 --- /dev/null +++ b/FRONTEND/src/components/Modal/ModalPortPhysicalTest.tsx @@ -0,0 +1,131 @@ +import { + ActionIcon, + Box, + Flex, + Modal, + ScrollArea, + Table, + Text, +} from "@mantine/core"; +import { IconCheck, IconX } from "@tabler/icons-react"; + +interface LineHistoryItem { + id: number; + number: number; + stationId: number; + pid: string; + vid: string; + sn: string; + scenario: string; + output?: string; + portPhysical?: [ + { + name: string; + tested: boolean; + }, + ]; + timestamp: number; +} + +interface ModalPortPhysicalTestProps { + opened: boolean; + onClose: () => void; + selectedHistory: LineHistoryItem | null; + lineNumber?: number; + stationName?: string; +} + +const ModalPortPhysicalTest = ({ + opened, + onClose, + selectedHistory, + lineNumber, + stationName, +}: ModalPortPhysicalTestProps) => { + return ( + + + 🔌 Port Physical Test Status + {lineNumber ? ` — Line ${lineNumber}` : ""} + {stationName ? ` (${stationName})` : ""} + + + } + size="lg" + style={{ position: "absolute", left: 0 }} + centered + zIndex={100001} + > + + PID: {selectedHistory?.pid} + {selectedHistory?.vid} + SN: {selectedHistory?.sn} + + + {selectedHistory?.portPhysical && + selectedHistory.portPhysical.length > 0 ? ( + + + + + Port Name + Status + + + + {selectedHistory?.portPhysical?.map((port, index) => ( + + {port.name} + + + {port.tested ? ( + <> + + + + + Tested + + + ) : ( + <> + + + + + Not Tested + + + )} + + + + ))} + +
+
+ ) : ( + + No port data available + + )} +
+
+ ); +}; + +export default ModalPortPhysicalTest;