diff --git a/FRONTEND/src/components/ModalHistory.tsx b/FRONTEND/src/components/ModalHistory.tsx index 867159d..aef06c9 100644 --- a/FRONTEND/src/components/ModalHistory.tsx +++ b/FRONTEND/src/components/ModalHistory.tsx @@ -48,8 +48,6 @@ function ModalHistory({ opened, onClose, socket, stationIds = [] }: ModalHistory const [activeTimePeriod, setActiveTimePeriod] = useState("current"); const scrollViewportRef = useRef(null); const isAutoSwitchingRef = useRef(false); - const lastScrollTopRef = useRef(0); - const scrollTimeoutRef = useRef(null); useEffect(() => { if (!socket || !opened) return; @@ -88,20 +86,6 @@ function ModalHistory({ opened, onClose, socket, stationIds = [] }: ModalHistory } }, [opened]); - // Initialize scroll position when content loads - useEffect(() => { - if (scrollViewportRef.current && historyData.length > 0) { - setTimeout(() => { - if (scrollViewportRef.current) { - const scrollHeight = scrollViewportRef.current.scrollHeight; - const clientHeight = scrollViewportRef.current.clientHeight; - // Start at middle position - scrollViewportRef.current.scrollTop = (scrollHeight - clientHeight) / 2; - lastScrollTopRef.current = scrollViewportRef.current.scrollTop; - } - }, 200); - } - }, [historyData]); // Utility function to format timestamp (can be used later if needed) // const formatTimestamp = (timestamp: number) => { @@ -109,27 +93,27 @@ function ModalHistory({ opened, onClose, socket, stationIds = [] }: ModalHistory // return date.toLocaleString(); // }; - // Get time range based on selected period (returns [startTime, endTime]) + // 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: từ 0-4h gần nhất + // Current: chỉ hiện tại (4h gần nhất) return [now - 4 * HOUR, now]; case "last_4h": - // Last 4h: từ 4h-8h trước - return [now - 8 * HOUR, now - 4 * HOUR]; + // Last 4h: bao gồm current + last 4h (0-8h) + return [now - 8 * HOUR, now]; case "last_8h": - // Last 8h: từ 8h-24h trước - return [now - 24 * HOUR, now - 8 * HOUR]; + // Last 8h: bao gồm current + last 4h + last 8h (0-24h) + return [now - 24 * HOUR, now]; case "last_24h": - // Last 24h: từ 24h-48h trước - return [now - 48 * HOUR, now - 24 * HOUR]; + // Last 24h: bao gồm current + last 4h + last 8h + last 24h (0-48h) + return [now - 48 * HOUR, now]; case "last_48h": - // Last 48h: từ 48h trở về trước (tất cả data cũ hơn 48h) - return [0, now - 48 * HOUR]; + // Last 48h: tất cả data (từ đầu đến giờ) + return [0, now]; default: return [0, now]; } @@ -143,6 +127,25 @@ function ModalHistory({ opened, onClose, socket, stationIds = [] }: ModalHistory ); }; + // Group history items by line number + const groupHistoryByLine = (history: HistoryItem[]): Map => { + const grouped = new Map(); + + 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 ); @@ -151,6 +154,9 @@ function ModalHistory({ opened, onClose, socket, stationIds = [] }: ModalHistory const filteredHistory = currentStationData ? filterHistoryByTime(currentStationData.history) : []; + + // Group filtered history by line number + const groupedHistory = groupHistoryByLine(filteredHistory); if (!opened) return null; @@ -282,134 +288,96 @@ function ModalHistory({ opened, onClose, socket, stationIds = [] }: ModalHistory h="calc(75vh - 80px)" viewportRef={scrollViewportRef} onScrollPositionChange={(position) => { - if (!scrollViewportRef.current) return; + if (!scrollViewportRef.current || isAutoSwitchingRef.current) return; const scrollTop = position.y; const target = scrollViewportRef.current; const scrollHeight = target.scrollHeight; const clientHeight = target.clientHeight; - // Clear existing timeout - if (scrollTimeoutRef.current) { - clearTimeout(scrollTimeoutRef.current); - } + // Check if scrolled to bottom + const isAtBottom = Math.abs(scrollTop + clientHeight - scrollHeight) < 5; - // Debounce scroll handling - scrollTimeoutRef.current = setTimeout(() => { - if (isAutoSwitchingRef.current) { - return; - } + if (isAtBottom && historyData.length > 0) { + isAutoSwitchingRef.current = true; - // Determine scroll direction - const isScrollingUp = scrollTop < lastScrollTopRef.current; - const isScrollingDown = scrollTop > lastScrollTopRef.current; - - // Update last scroll position AFTER checking direction - lastScrollTopRef.current = scrollTop; + const currentStationIndex = historyData.findIndex( + (station) => station.stationId.toString() === activeStation + ); - // Check if at absolute top or bottom (no spacer needed) - const isAtTop = scrollTop === 0; - const isAtBottom = Math.abs(scrollTop + clientHeight - scrollHeight) < 5; - - // Can only trigger one direction at a time - const canScrollUp = isAtTop && isScrollingUp && historyData.length > 0; - const canScrollDown = isAtBottom && isScrollingDown && historyData.length > 0; - - // Handle scroll up - khi scroll lên đến đầu - if (canScrollUp) { - isAutoSwitchingRef.current = true; - - const currentPeriodIndex = TIME_PERIODS.findIndex( - (p) => p.value === activeTimePeriod - ); - - if (currentPeriodIndex > 0) { - setActiveTimePeriod(TIME_PERIODS[currentPeriodIndex - 1].value); - } else { - const currentStationIndex = historyData.findIndex( - (station) => station.stationId.toString() === activeStation - ); - - if (currentStationIndex > 0) { - setActiveStation(historyData[currentStationIndex - 1].stationId.toString()); - setActiveTimePeriod(TIME_PERIODS[TIME_PERIODS.length - 1].value); - } else { - isAutoSwitchingRef.current = false; - return; - } - } + if (currentStationIndex !== -1 && currentStationIndex < historyData.length - 1) { + // Chuyển sang station tiếp theo + setActiveStation(historyData[currentStationIndex + 1].stationId.toString()); - // Scroll to bottom of new content - setTimeout(() => { - if (scrollViewportRef.current) { - const newScrollHeight = scrollViewportRef.current.scrollHeight; - const newClientHeight = scrollViewportRef.current.clientHeight; - scrollViewportRef.current.scrollTop = newScrollHeight - newClientHeight; - lastScrollTopRef.current = scrollViewportRef.current.scrollTop; - } - setTimeout(() => { - isAutoSwitchingRef.current = false; - }, 100); - }, 150); - } - // Handle scroll down - khi scroll xuống đến cuối - else if (canScrollDown) { - isAutoSwitchingRef.current = true; - - const currentPeriodIndex = TIME_PERIODS.findIndex( - (p) => p.value === activeTimePeriod - ); - - if (currentPeriodIndex < TIME_PERIODS.length - 1) { - setActiveTimePeriod(TIME_PERIODS[currentPeriodIndex + 1].value); - } else { - const currentStationIndex = historyData.findIndex( - (station) => station.stationId.toString() === activeStation - ); - - if (currentStationIndex !== -1 && currentStationIndex < historyData.length - 1) { - setActiveStation(historyData[currentStationIndex + 1].stationId.toString()); - setActiveTimePeriod(TIME_PERIODS[0].value); - } else { - isAutoSwitchingRef.current = false; - return; - } - } - - // Scroll to top of new content + // Reset scroll to top setTimeout(() => { if (scrollViewportRef.current) { scrollViewportRef.current.scrollTop = 0; - lastScrollTopRef.current = 0; } setTimeout(() => { isAutoSwitchingRef.current = false; }, 100); }, 150); + } else { + isAutoSwitchingRef.current = false; } - }, 100); // Debounce 100ms + } }} > - - {filteredHistory.length > 0 ? ( + + {groupedHistory.size > 0 ? ( <> - {filteredHistory.map((item, index) => ( + {Array.from(groupedHistory.entries()) + .sort(([lineA], [lineB]) => lineA - lineB) + .map(([lineNumber, items]) => ( - - Line {item.number}: {item.pid} {item.vid} SN: {item.sn} - - | {item.scenario} - - + {/* Header của nhóm - hiển thị line number */} + + + Line {lineNumber} ({items.length} {items.length > 1 ? 'records' : 'record'}) + + + + {/* Các items trong nhóm */} + {items.map((item, itemIndex) => ( + 0 ? "1px solid #f1f3f5" : "none", + backgroundColor: itemIndex % 2 === 0 ? "white" : "#f8f9fa", + }} + > + + {item.pid} {item.vid} SN: {item.sn} + + | {item.scenario} + + + {new Date(item.timestamp).toLocaleString()} + + + + ))} ))} + + {/* Spacer để đảm bảo có thể scroll ngay cả khi content ít */} + ) : (