Update view history log, tested ports

This commit is contained in:
nguyentrungthat 2026-05-18 16:20:09 +07:00
parent 507f228888
commit d4ea801bef
4 changed files with 348 additions and 107 deletions

View File

@ -30,7 +30,7 @@ import axios from 'axios'
import redis from '@adonisjs/redis/services/main' import redis from '@adonisjs/redis/services/main'
import Line from '#models/line' import Line from '#models/line'
import PromptAi from '#models/prompt_ai' 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 momentTZ from 'moment-timezone'
import { PhysicalPortTest } from './physical_test_service.js' import { PhysicalPortTest } from './physical_test_service.js'
import Station from '#models/station' import Station from '#models/station'
@ -948,41 +948,67 @@ export default class LineConnection {
/** /**
* Add cache to list history devices on this line * Add cache to list history devices on this line
*/ */
async addHistory(stationId: number, lineId: number, item: HistoryItem) { async addHistory(
if (!item.pid || !item.sn) return 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 key = `station:${stationId}:line:${lineId}:history`
const now = Date.now() const now = Date.now()
const newItem = JSON.stringify({ // Tạo object chứa các field mở rộng nếu được truyền vào
...item, const extendedFields: any = {}
timestamp: now, 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) const lastItems = await redis.zrevrange(key, 0, 0)
if (lastItems.length > 0) { if (lastItems.length > 0) {
const last = JSON.parse(lastItems[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) { 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) const line = await Line.find(lineId)
if (line) { if (line) {
const listHistory = line.history ? JSON.parse(line.history) : [] const listHistory = line.history ? JSON.parse(line.history) : []
listHistory.unshift(newItem) listHistory.unshift({ ...item, timestamp: now })
line.history = JSON.stringify(listHistory) line.history = JSON.stringify(listHistory)
await line.save() await line.save()
} }
// Add vào ZSET
await redis.zadd(key, now, newItem)
// Tự động xóa item > 96h // Tự động xóa item > 96h
const expireTime = now - 96 * 60 * 60 * 1000 // const expireTime = now - 96 * 60 * 60 * 1000
await redis.zremrangebyscore(key, 0, expireTime) // 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 return true
} }
@ -1184,7 +1210,7 @@ Ports Missing/Down: ${missing.length}\n\n`
console.log('Running physical test') console.log('Running physical test')
return return
} }
this.setTimeoutSendSummaryReport(600000) this.setTimeoutSendSummaryReport(1200000)
this.config.runningPhysical = true this.config.runningPhysical = true
this.config.runningScenario = 'Physical Test' this.config.runningScenario = 'Physical Test'
this.config.isSkipPhysical = false 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) 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) this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP)
await sendMessageToMail( await sendMessageToMail(
`[ATC] - [${config.stationName} - L${config.lineNumber}] - ${this.config.inventory?.pid} - ${this.config.inventory?.sn} - Test Summary`, `[ATC] - [${config.stationName} - L${config.lineNumber}] - ${this.config.inventory?.pid} - ${this.config.inventory?.sn} - Test Summary`,

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
ActionIcon,
Box, Box,
CloseButton, CloseButton,
Flex, Flex,
@ -11,6 +12,9 @@ import {
import moment from "moment"; import moment from "moment";
import type { Socket } from "socket.io-client"; import type { Socket } from "socket.io-client";
import classes from "../Component.module.css"; import classes from "../Component.module.css";
import { IconFileText, IconListCheck } from "@tabler/icons-react";
import ModalPortPhysicalTest from "./ModalPortPhysicalTest";
import ModalLog from "./ModalLog";
interface LineHistoryItem { interface LineHistoryItem {
id: number; id: number;
@ -20,6 +24,13 @@ interface LineHistoryItem {
vid: string; vid: string;
sn: string; sn: string;
scenario: string; scenario: string;
output?: string;
portPhysical?: [
{
name: string;
tested: boolean;
},
];
timestamp: number; timestamp: number;
} }
@ -44,6 +55,10 @@ const ModalLineHistory = ({
}: ModalLineHistoryProps) => { }: ModalLineHistoryProps) => {
const [history, setHistory] = useState<LineHistoryItem[]>([]); const [history, setHistory] = useState<LineHistoryItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [openPortPhysical, setOpenPortPhysical] = useState(false);
const [selectedHistory, setSelectedHistory] =
useState<LineHistoryItem | null>(null);
const [openLog, setOpenLog] = useState(false);
useEffect(() => { useEffect(() => {
if (!socket || !opened) return; if (!socket || !opened) return;
@ -77,104 +92,157 @@ const ModalLineHistory = ({
const sorted = [...history].sort((a, b) => b.timestamp - a.timestamp); const sorted = [...history].sort((a, b) => b.timestamp - a.timestamp);
return ( return (
<div <>
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.6)",
zIndex: 100000,
display: "flex",
alignItems: "center",
justifyContent: "center",
backdropFilter: "blur(3px)",
}}
onClick={(e) => {
e.stopPropagation();
onClose();
}}
>
<div <div
style={{ style={{
background: "white", position: "fixed",
borderRadius: "12px", top: 0,
width: "70%", left: 0,
maxWidth: "1000px", right: 0,
maxHeight: "80vh", bottom: 0,
backgroundColor: "rgba(0,0,0,0.6)",
zIndex: 100000,
display: "flex", display: "flex",
flexDirection: "column", alignItems: "center",
boxShadow: "0 20px 60px rgba(0,0,0,0.3)", justifyContent: "center",
overflow: "hidden", backdropFilter: "blur(3px)",
}}
onClick={(e) => {
e.stopPropagation();
onClose();
}} }}
onClick={(e) => e.stopPropagation()}
> >
<Flex <div
justify="space-between" style={{
align="center" background: "white",
p="lg" borderRadius: "12px",
style={{ borderBottom: "1px solid #e9ecef", flexShrink: 0 }} width: "70%",
maxWidth: "1000px",
maxHeight: "80vh",
display: "flex",
flexDirection: "column",
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
overflow: "hidden",
}}
onClick={(e) => e.stopPropagation()}
> >
<Text fw={700} size="lg"> <Flex
🕘 Line history justify="space-between"
{lineNumber ? ` — Line ${lineNumber}` : ""} align="center"
{stationName ? ` (${stationName})` : ""} p="lg"
</Text> style={{ borderBottom: "1px solid #e9ecef", flexShrink: 0 }}
<CloseButton size="lg" onClick={onClose} /> >
</Flex> <Text fw={700} size="lg">
🕘 Line history
{lineNumber ? ` — Line ${lineNumber}` : ""}
{stationName ? ` (${stationName})` : ""}
</Text>
<CloseButton size="lg" onClick={onClose} />
</Flex>
<Box p="md" style={{ flex: 1, overflow: "hidden" }}> <Box p="md" style={{ flex: 1, overflow: "hidden" }}>
{loading ? ( {loading ? (
<Flex justify="center" align="center" h="40vh"> <Flex justify="center" align="center" h="40vh">
<Loader /> <Loader />
</Flex> </Flex>
) : sorted.length === 0 ? ( ) : sorted.length === 0 ? (
<Flex justify="center" align="center" h="40vh"> <Flex justify="center" align="center" h="40vh">
<Text c="dimmed">No history data available</Text> <Text c="dimmed">No history data available</Text>
</Flex> </Flex>
) : ( ) : (
<ScrollArea h="60vh" className={classes.hideScrollBar}> <ScrollArea h="60vh" className={classes.hideScrollBar}>
<Table striped highlightOnHover withTableBorder> <Table striped highlightOnHover withTableBorder>
<Table.Thead <Table.Thead
style={{ style={{
position: "sticky", position: "sticky",
top: 0, top: 0,
background: "#f1f3f5", background: "#f1f3f5",
zIndex: 1, zIndex: 1,
}} }}
> >
<Table.Tr> <Table.Tr>
<Table.Th>PID</Table.Th> <Table.Th>PID</Table.Th>
<Table.Th>VID</Table.Th> <Table.Th>VID</Table.Th>
<Table.Th>SN</Table.Th> <Table.Th>SN</Table.Th>
<Table.Th>Scenario</Table.Th> <Table.Th>Scenario</Table.Th>
<Table.Th>Time</Table.Th> <Table.Th>Log</Table.Th>
</Table.Tr> <Table.Th>Time</Table.Th>
</Table.Thead>
<Table.Tbody>
{sorted.map((item, i) => (
<Table.Tr key={`${item.timestamp}-${item.sn || ""}-${i}`}>
<Table.Td style={{ fontWeight: 600 }}>
{item.pid || "-"}
</Table.Td>
<Table.Td>{item.vid || "-"}</Table.Td>
<Table.Td>{item.sn || "-"}</Table.Td>
<Table.Td>{item.scenario || "-"}</Table.Td>
<Table.Td c="dimmed" style={{ fontSize: "12px" }}>
{item.timestamp
? moment(item.timestamp).format("DD/MM/YYYY HH:mm:ss")
: "-"}
</Table.Td>
</Table.Tr> </Table.Tr>
))} </Table.Thead>
</Table.Tbody> <Table.Tbody>
</Table> {sorted.map((item, i) => (
</ScrollArea> <Table.Tr key={`${item.timestamp}-${item.sn || ""}-${i}`}>
)} <Table.Td style={{ fontWeight: 600 }}>
</Box> {item.pid || "-"}
</Table.Td>
<Table.Td>{item.vid || "-"}</Table.Td>
<Table.Td>{item.sn || "-"}</Table.Td>
<Table.Td>{item.scenario || "-"}</Table.Td>
<Table.Td>
<Flex align="center" gap="xs">
{item.output ? (
<ActionIcon
variant="outline"
onClick={() => {
setSelectedHistory(item);
setOpenLog(true);
}}
>
<IconFileText size={16} />
</ActionIcon>
) : (
""
)}
{item.portPhysical ? (
<ActionIcon
variant="outline"
onClick={() => {
setSelectedHistory(item);
setOpenPortPhysical(true);
}}
>
<IconListCheck size={16} />
</ActionIcon>
) : (
""
)}
</Flex>
</Table.Td>
<Table.Td c="dimmed" style={{ fontSize: "12px" }}>
{item.timestamp
? moment(item.timestamp).format(
"DD/MM/YYYY HH:mm:ss",
)
: "-"}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea>
)}
</Box>
</div>
</div> </div>
</div> <ModalPortPhysicalTest
opened={openPortPhysical}
onClose={() => {
setSelectedHistory(null);
setOpenPortPhysical(false);
}}
selectedHistory={selectedHistory}
lineNumber={lineNumber}
stationName={stationName}
/>
<ModalLog
opened={openLog}
onClose={() => {
setSelectedHistory(null);
setOpenLog(false);
}}
testLogContent={selectedHistory?.output || ""}
/>
</>
); );
}; };

View File

@ -35,7 +35,7 @@ const ModalLog = ({
return `<span style="background-color: ${ return `<span style="background-color: ${
prefix.includes("start") ? colorStart : colorEnd prefix.includes("start") ? colorStart : colorEnd
}" title="${date}">${prefix}${timestamp}${suffix}</span>`; }" title="${date}">${prefix}${timestamp}${suffix}</span>`;
} },
) )
// Highlight full ---User--- // Highlight full ---User---
.replace(/^-------([^-\n]+)-------$/gm, (match) => { .replace(/^-------([^-\n]+)-------$/gm, (match) => {
@ -51,6 +51,7 @@ const ModalLog = ({
onClose={() => { onClose={() => {
onClose(); onClose();
}} }}
zIndex={100001}
title={ title={
<Text fz={"lg"} fw={"bolder"}> <Text fz={"lg"} fw={"bolder"}>
Log Content Log Content

View File

@ -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 (
<Modal
opened={opened}
onClose={onClose}
title={
<Flex align="center" gap="sm">
<Text fw={700} size="lg">
🔌 Port Physical Test Status
{lineNumber ? ` — Line ${lineNumber}` : ""}
{stationName ? ` (${stationName})` : ""}
</Text>
</Flex>
}
size="lg"
style={{ position: "absolute", left: 0 }}
centered
zIndex={100001}
>
<Flex mb="md" gap={"sm"}>
<Text fw={"bold"}>PID: {selectedHistory?.pid}</Text>
<Text fw={"bold"}>{selectedHistory?.vid}</Text>
<Text fw={"bold"}>SN: {selectedHistory?.sn}</Text>
</Flex>
<Box>
{selectedHistory?.portPhysical &&
selectedHistory.portPhysical.length > 0 ? (
<ScrollArea h="75vh">
<Table striped highlightOnHover withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Port Name</Table.Th>
<Table.Th>Status</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{selectedHistory?.portPhysical?.map((port, index) => (
<Table.Tr key={index}>
<Table.Td style={{ fontWeight: 600 }}>{port.name}</Table.Td>
<Table.Td>
<Flex align="center" gap="xs">
{port.tested ? (
<>
<ActionIcon
variant="filled"
color="green"
size="sm"
disabled
>
<IconCheck size={16} />
</ActionIcon>
<Text size="sm" c="green" fw={500}>
Tested
</Text>
</>
) : (
<>
<ActionIcon
variant="filled"
color="red"
size="sm"
disabled
>
<IconX size={16} />
</ActionIcon>
<Text size="sm" c="red" fw={500}>
Not Tested
</Text>
</>
)}
</Flex>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea>
) : (
<Flex justify="center" align="center" h="20vh">
<Text c="dimmed">No port data available</Text>
</Flex>
)}
</Box>
</Modal>
);
};
export default ModalPortPhysicalTest;