ATC_SIMPLE/FRONTEND/src/components/ModalHistory.tsx

451 lines
15 KiB
TypeScript

import { useEffect, useState, useRef } from "react";
import {
Box,
ScrollArea,
Text,
Flex,
CloseButton,
Button,
} from "@mantine/core";
import classes from "./Component.module.css";
import type { Socket } from "socket.io-client";
interface HistoryItem {
id: number;
number: number;
stationId: number;
pid: string;
sn: string;
vid: string;
scenario: string;
timestamp: number;
}
interface StationHistory {
stationId: number;
stationName: string;
history: HistoryItem[];
}
interface ModalHistoryProps {
opened: boolean;
onClose: () => void;
socket: Socket | null;
stationIds?: number[];
}
const TIME_PERIODS = [
{ label: "Current", value: "current" },
{ label: "last 4h", value: "last_4h" },
{ label: "last 8h", value: "last_8h" },
{ label: "last 24h", value: "last_24h" },
{ label: "last 48h", value: "last_48h" },
];
function ModalHistory({
opened,
onClose,
socket,
stationIds = [],
}: ModalHistoryProps) {
const [historyData, setHistoryData] = useState<StationHistory[]>([]);
const [activeStation, setActiveStation] = useState<string>("");
const [activeTimePeriod, setActiveTimePeriod] = useState<string>("current");
const scrollViewportRef = useRef<HTMLDivElement>(null);
const isAutoSwitchingRef = useRef(false);
useEffect(() => {
if (!socket || !opened) return;
// Listen for history response
const handleHistoryResponse = (data: StationHistory[]) => {
setHistoryData(data);
// Set first station as active by default only if not already set
if (data.length > 0 && !activeStation) {
setActiveStation(data[0].stationId.toString());
}
};
socket.on("list_histories", handleHistoryResponse);
return () => {
socket.off("list_histories", handleHistoryResponse);
};
}, [socket, opened, activeStation]);
// Request history when modal opens
useEffect(() => {
if (!socket || !opened) return;
// Request history with station IDs
socket.emit("get_list_history", { stationIds });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [socket, opened]); // Only re-fetch when modal opens/closes, not when stationIds changes
// Reset state when modal closes
useEffect(() => {
if (!opened) {
setHistoryData([]);
setActiveStation("");
setActiveTimePeriod("current");
}
}, [opened]);
// Utility function to format timestamp (can be used later if needed)
// const formatTimestamp = (timestamp: number) => {
// const date = new Date(timestamp);
// return date.toLocaleString();
// };
// Get time range based on selected period (cumulative - tích lũy)
const getTimeRange = (period: string): [number, number] => {
const now = Date.now();
const HOUR = 60 * 60 * 1000;
switch (period) {
case "current":
// Current: chỉ hiện tại (4h gần nhất)
return [0, now];
case "last_4h":
// Last 4h: bao gồm current + last 4h (0-8h)
return [now - 4 * HOUR, now];
case "last_8h":
// Last 8h: bao gồm current + last 4h + last 8h (0-24h)
return [now - 8 * HOUR, now];
case "last_24h":
// Last 24h: bao gồm current + last 4h + last 8h + last 24h (0-48h)
return [now - 24 * HOUR, now];
case "last_48h":
// Last 48h: tất cả data (từ đầu đến giờ)
return [now - 48 * HOUR, now];
default:
return [0, now];
}
};
// Filter history based on time period
const filterHistoryByTime = (history: HistoryItem[]): HistoryItem[] => {
const [startTime, endTime] = getTimeRange(activeTimePeriod);
return history.filter(
(item) => item.timestamp >= startTime && item.timestamp < endTime
);
};
// Group history items by line number
const groupHistoryByLine = (
history: HistoryItem[]
): Map<number, HistoryItem[]> => {
const grouped = new Map<number, HistoryItem[]>();
history.forEach((item) => {
if (!grouped.has(item.number)) {
grouped.set(item.number, []);
}
grouped.get(item.number)!.push(item);
});
// Sort items within each group by timestamp (newest first)
grouped.forEach((items) => {
items.sort((a, b) => b.timestamp - a.timestamp);
});
return grouped;
};
const currentStationData = historyData.find(
(station) => station.stationId.toString() === activeStation
);
// Apply time filter to current station data
const filteredHistory = currentStationData
? filterHistoryByTime(currentStationData.history)
: [];
// Group filtered history by line number
const groupedHistory = groupHistoryByLine(filteredHistory);
if (!opened) return null;
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",
maxWidth: "90vw",
width: "90%",
maxHeight: "85vh",
display: "flex",
flexDirection: "column",
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
overflow: "hidden",
}}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<Flex
justify="space-between"
align="center"
p="lg"
style={{
borderBottom: "1px solid #e9ecef",
flexShrink: 0,
}}
>
<Text fw={700} size="xl">
📊 History
</Text>
<CloseButton size="lg" onClick={onClose} />
</Flex>
{/* Content */}
<div
style={{
padding: "20px",
// overflowY: "auto",
// overflowX: "hidden",
flex: 1,
}}
className={classes.hideScrollBar}
>
<Flex gap="md" style={{ height: "75vh" }}>
{/* Sidebar - Station List */}
<Box
style={{
width: "200px",
borderRight: "1px solid #e9ecef",
paddingRight: "10px",
flexShrink: 0,
}}
>
<Text fw={600} size="sm" mb="xs" c="dimmed">
List stations
</Text>
<ScrollArea h="calc(100% - 30px)">
<Flex direction="column" gap="xs">
{historyData.map((station) => (
<Button
key={station.stationId}
fullWidth
style={{ minHeight: "36px" }}
variant={
activeStation === station.stationId.toString()
? "filled"
: "outline"
}
onClick={() =>
setActiveStation(station.stationId.toString())
}
>
{station.stationName}
</Button>
))}
</Flex>
</ScrollArea>
</Box>
{/* Main Content */}
<Box style={{ flex: 1, overflow: "hidden" }}>
{/* Time Period Tabs */}
<Flex gap="xs" mb="md" wrap="wrap">
{TIME_PERIODS.map((period) => (
<Button
key={period.value}
size="sm"
variant={
activeTimePeriod === period.value ? "filled" : "outline"
}
onClick={() => setActiveTimePeriod(period.value)}
style={{ flex: 1, minWidth: "100px" }}
>
{period.label}
</Button>
))}
</Flex>
{/* History Table */}
<Box
style={{
border: "1px solid #dee2e6",
borderRadius: "4px",
overflow: "hidden",
backgroundColor: "white",
}}
>
<ScrollArea
h="calc(75vh - 80px)"
viewportRef={scrollViewportRef}
onScrollPositionChange={(position) => {
if (
!scrollViewportRef.current ||
isAutoSwitchingRef.current
)
return;
const scrollTop = position.y;
const target = scrollViewportRef.current;
const scrollHeight = target.scrollHeight;
const clientHeight = target.clientHeight;
// Check if scrolled to bottom
const isAtBottom =
Math.abs(scrollTop + clientHeight - scrollHeight) < 5;
if (isAtBottom && historyData.length > 0) {
isAutoSwitchingRef.current = true;
const currentStationIndex = historyData.findIndex(
(station) =>
station.stationId.toString() === activeStation
);
if (
currentStationIndex !== -1 &&
currentStationIndex < historyData.length - 1
) {
// Chuyển sang station tiếp theo
setActiveStation(
historyData[
currentStationIndex + 1
].stationId.toString()
);
// Reset scroll to top
setTimeout(() => {
if (scrollViewportRef.current) {
scrollViewportRef.current.scrollTop = 0;
}
setTimeout(() => {
isAutoSwitchingRef.current = false;
}, 100);
}, 150);
} else {
isAutoSwitchingRef.current = false;
}
}
}}
>
<Box style={{ minHeight: "calc(75vh - 60px)" }}>
{groupedHistory.size > 0 ? (
<>
{Array.from(groupedHistory.entries())
.sort(([lineA], [lineB]) => lineA - lineB)
.map(([lineNumber, items]) => (
<Box
key={`line-group-${lineNumber}`}
style={{
marginBottom: "8px",
border: "1px solid #dee2e6",
borderRadius: "4px",
overflow: "hidden",
}}
>
{/* Header của nhóm - hiển thị line number */}
<Box
style={{
padding: "8px 16px",
backgroundColor: "#e9ecef",
fontWeight: 600,
}}
>
<Text size="sm" fw={600}>
Line {lineNumber} ({items.length}{" "}
{items.length > 1 ? "records" : "record"})
</Text>
</Box>
{/* Các items trong nhóm */}
{items.map((item, itemIndex) => (
<Box
key={`${item.stationId}-${item.number}-${item.timestamp}-${itemIndex}`}
style={{
padding: "8px 16px 8px 32px", // Tăng padding-left lên 32px
borderTop:
itemIndex > 0
? "1px solid #f1f3f5"
: "none",
backgroundColor:
itemIndex % 2 === 0 ? "white" : "#f8f9fa",
}}
>
<Text
size="sm"
style={{ fontFamily: "monospace" }}
>
{item.pid} {item.vid} SN: {item.sn}
<span
style={{
marginLeft: "20px",
color: "#868e96",
}}
>
| {item.scenario}
</span>
<span
style={{
marginLeft: "10px",
color: "#adb5bd",
fontSize: "11px",
}}
>
{new Date(
item.timestamp
).toLocaleString()}
</span>
</Text>
</Box>
))}
</Box>
))}
{/* Spacer để đảm bảo có thể scroll ngay cả khi content ít */}
<Box style={{ height: "200px" }} />
</>
) : (
<Flex
align="center"
justify="center"
style={{ height: "calc(75vh - 80px)" }}
>
<Text c="dimmed" size="lg">
{currentStationData
? `No history data available for ${
TIME_PERIODS.find(
(p) => p.value === activeTimePeriod
)?.label
}`
: "No history data available"}
</Text>
</Flex>
)}
</Box>
</ScrollArea>
</Box>
</Box>
</Flex>
</div>
</div>
</div>
);
}
export default ModalHistory;