Update view history log, tested ports
This commit is contained in:
parent
507f228888
commit
d4ea801bef
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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<LineHistoryItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openPortPhysical, setOpenPortPhysical] = useState(false);
|
||||
const [selectedHistory, setSelectedHistory] =
|
||||
useState<LineHistoryItem | null>(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 (
|
||||
<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
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "12px",
|
||||
width: "70%",
|
||||
maxWidth: "1000px",
|
||||
maxHeight: "80vh",
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
zIndex: 100000,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
||||
overflow: "hidden",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backdropFilter: "blur(3px)",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p="lg"
|
||||
style={{ borderBottom: "1px solid #e9ecef", flexShrink: 0 }}
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
borderRadius: "12px",
|
||||
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">
|
||||
🕘 Line history
|
||||
{lineNumber ? ` — Line ${lineNumber}` : ""}
|
||||
{stationName ? ` (${stationName})` : ""}
|
||||
</Text>
|
||||
<CloseButton size="lg" onClick={onClose} />
|
||||
</Flex>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p="lg"
|
||||
style={{ borderBottom: "1px solid #e9ecef", flexShrink: 0 }}
|
||||
>
|
||||
<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" }}>
|
||||
{loading ? (
|
||||
<Flex justify="center" align="center" h="40vh">
|
||||
<Loader />
|
||||
</Flex>
|
||||
) : sorted.length === 0 ? (
|
||||
<Flex justify="center" align="center" h="40vh">
|
||||
<Text c="dimmed">No history data available</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<ScrollArea h="60vh" className={classes.hideScrollBar}>
|
||||
<Table striped highlightOnHover withTableBorder>
|
||||
<Table.Thead
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
background: "#f1f3f5",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Table.Tr>
|
||||
<Table.Th>PID</Table.Th>
|
||||
<Table.Th>VID</Table.Th>
|
||||
<Table.Th>SN</Table.Th>
|
||||
<Table.Th>Scenario</Table.Th>
|
||||
<Table.Th>Time</Table.Th>
|
||||
</Table.Tr>
|
||||
</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>
|
||||
<Box p="md" style={{ flex: 1, overflow: "hidden" }}>
|
||||
{loading ? (
|
||||
<Flex justify="center" align="center" h="40vh">
|
||||
<Loader />
|
||||
</Flex>
|
||||
) : sorted.length === 0 ? (
|
||||
<Flex justify="center" align="center" h="40vh">
|
||||
<Text c="dimmed">No history data available</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<ScrollArea h="60vh" className={classes.hideScrollBar}>
|
||||
<Table striped highlightOnHover withTableBorder>
|
||||
<Table.Thead
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
background: "#f1f3f5",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Table.Tr>
|
||||
<Table.Th>PID</Table.Th>
|
||||
<Table.Th>VID</Table.Th>
|
||||
<Table.Th>SN</Table.Th>
|
||||
<Table.Th>Scenario</Table.Th>
|
||||
<Table.Th>Log</Table.Th>
|
||||
<Table.Th>Time</Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</Box>
|
||||
</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>
|
||||
<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>
|
||||
<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 || ""}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const ModalLog = ({
|
|||
return `<span style="background-color: ${
|
||||
prefix.includes("start") ? colorStart : colorEnd
|
||||
}" title="${date}">${prefix}${timestamp}${suffix}</span>`;
|
||||
}
|
||||
},
|
||||
)
|
||||
// Highlight full ---User---
|
||||
.replace(/^-------([^-\n]+)-------$/gm, (match) => {
|
||||
|
|
@ -51,6 +51,7 @@ const ModalLog = ({
|
|||
onClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
zIndex={100001}
|
||||
title={
|
||||
<Text fz={"lg"} fw={"bolder"}>
|
||||
Log Content
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue