fix chức năng history

This commit is contained in:
Truong Vo 2025-12-02 08:49:54 +07:00
parent 77027d4f8a
commit f60be2f543
1 changed files with 159 additions and 74 deletions

View File

@ -53,7 +53,8 @@ function ModalHistory({
const [activeStation, setActiveStation] = useState<string>("");
const [activeTimePeriod, setActiveTimePeriod] = useState<string>("current");
const scrollViewportRef = useRef<HTMLDivElement>(null);
const isAutoSwitchingRef = useRef(false);
const stationRefs = useRef<Map<number, HTMLDivElement>>(new Map());
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (!socket || !opened) return;
@ -89,15 +90,32 @@ function ModalHistory({
setHistoryData([]);
setActiveStation("");
setActiveTimePeriod("current");
stationRefs.current.clear();
}
return () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [opened]);
// Change station or filter will reset scroll
// Scroll to station when activeStation changes (only when clicked, not when auto-detected)
const isManualScrollRef = useRef(false);
useEffect(() => {
if (scrollViewportRef?.current) {
scrollViewportRef.current.scrollTop = 0;
if (activeStation && isManualScrollRef.current) {
setTimeout(() => {
scrollToStation(Number(activeStation));
// Reset flag after scroll completes
setTimeout(() => {
isManualScrollRef.current = false;
}, 500);
}, 100);
}
}, [activeStation, activeTimePeriod]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeStation]);
// Utility function to format timestamp (can be used later if needed)
// const formatTimestamp = (timestamp: number) => {
@ -163,17 +181,35 @@ function ModalHistory({
return grouped;
};
const currentStationData = historyData.find(
(station) => station.stationId.toString() === activeStation
);
// Apply time filter to ALL stations data (không filter bỏ stations không có data)
const allFilteredStations = historyData.map((station) => ({
...station,
filteredHistory: filterHistoryByTime(station.history),
}));
// Apply time filter to current station data
const filteredHistory = currentStationData
? filterHistoryByTime(currentStationData.history)
: [];
// Group each station's history by line number
const allGroupedStations = allFilteredStations.map((station) => ({
...station,
groupedHistory: station.filteredHistory.length > 0
? groupHistoryByLine(station.filteredHistory)
: new Map<number, HistoryItem[]>(),
}));
// Group filtered history by line number
const groupedHistory = groupHistoryByLine(filteredHistory);
// Function to scroll to a specific station
const scrollToStation = (stationId: number) => {
const stationElement = stationRefs.current.get(stationId);
if (stationElement && scrollViewportRef.current) {
const scrollContainer = scrollViewportRef.current;
const containerTop = scrollContainer.getBoundingClientRect().top;
const elementTop = stationElement.getBoundingClientRect().top;
const scrollTop = scrollContainer.scrollTop;
scrollContainer.scrollTo({
top: scrollTop + elementTop - containerTop - 10, // 10px offset from top
behavior: "smooth",
});
}
};
if (!opened) return null;
@ -262,9 +298,11 @@ function ModalHistory({
? "filled"
: "outline"
}
onClick={() =>
setActiveStation(station.stationId.toString())
}
onClick={() => {
isManualScrollRef.current = true;
setActiveStation(station.stationId.toString());
scrollToStation(station.stationId);
}}
>
{station.stationName}
</Button>
@ -304,71 +342,101 @@ function ModalHistory({
<ScrollArea
h="calc(75vh - 80px)"
viewportRef={scrollViewportRef}
onScrollPositionChange={(position) => {
if (
!scrollViewportRef.current ||
isAutoSwitchingRef.current
)
return;
onScrollPositionChange={() => {
if (isManualScrollRef.current || !scrollViewportRef.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;
}
// Debounce để scroll mượt hơn
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
scrollTimeoutRef.current = setTimeout(() => {
if (!scrollViewportRef.current) return;
const scrollContainer = scrollViewportRef.current;
const containerRect = scrollContainer.getBoundingClientRect();
const topThreshold = containerRect.top + 10; // Sát top với threshold 10px
// Find which station header is closest to top
// Ưu tiên: header đã vượt qua top threshold (ở trên) > header chưa đến (ở dưới)
type StationInfo = { id: number; distance: number; isAbove: boolean };
let bestStation: StationInfo | null = null;
Array.from(stationRefs.current.entries()).forEach(([stationId, element]) => {
const elementRect = element.getBoundingClientRect();
const elementTop = elementRect.top;
const elementBottom = elementRect.bottom;
// Check if station header is visible (có phần nào đó trong viewport)
const isVisible = elementBottom >= containerRect.top && elementTop <= containerRect.bottom;
if (isVisible) {
const isAbove = elementTop <= topThreshold; // Header đã vượt qua top
const distance = Math.abs(elementTop - topThreshold);
const stationIdNum = Number(stationId);
// Ưu tiên header đã vượt qua top (isAbove = true)
if (!bestStation ||
(isAbove && !bestStation.isAbove) ||
(isAbove === bestStation.isAbove && distance < bestStation.distance)) {
bestStation = { id: stationIdNum, distance, isAbove };
}
}
});
// Update active station if found
if (bestStation) {
const stationIdStr = String((bestStation as StationInfo).id);
if (stationIdStr !== activeStation) {
setActiveStation(stationIdStr);
}
}
}, 150); // Debounce 150ms để mượt hơn
}}
>
<Box style={{ minHeight: "calc(75vh - 60px)" }}>
{groupedHistory.size > 0 ? (
{allGroupedStations.length > 0 ? (
<>
{Array.from(groupedHistory.entries())
.sort(([lineA], [lineB]) => lineA - lineB)
.map(([lineNumber, items]) => (
{allGroupedStations.map((station) => (
<Box key={`station-${station.stationId}`} style={{ marginBottom: "24px" }}>
{/* Station Title */}
<Box
key={`line-group-${lineNumber}`}
ref={(el) => {
if (el) {
stationRefs.current.set(station.stationId, el);
}
}}
style={{
padding: "12px 16px",
backgroundColor: "#495057",
color: "white",
fontWeight: 700,
marginBottom: "8px",
border: "1px solid #dee2e6",
borderRadius: "4px",
overflow: "hidden",
position: "sticky",
top: 0,
zIndex: 10,
}}
>
<Text size="md" fw={700} c="white">
📍 {station.stationName}
</Text>
</Box>
{/* Line groups for this station */}
{station.groupedHistory.size > 0 ? (
Array.from(station.groupedHistory.entries())
.sort(([lineA], [lineB]) => lineA - lineB)
.map(([lineNumber, items]) => (
<Box
key={`station-${station.stationId}-line-${lineNumber}`}
style={{
marginBottom: "8px",
border: "1px solid #dee2e6",
borderRadius: "4px",
overflow: "hidden",
}}
>
{/* Header của nhóm - hiển thị line number */}
<Box
style={{
@ -449,9 +517,26 @@ function ModalHistory({
</span>
</Text>
</Box>
))}
</Box>
))}
))}
</Box>
))
) : (
<Box
style={{
padding: "20px",
textAlign: "center",
color: "#868e96",
}}
>
<Text size="sm" c="dimmed">
No history data available for {TIME_PERIODS.find(
(p) => p.value === activeTimePeriod
)?.label}
</Text>
</Box>
)}
</Box>
))}
{/* Spacer để đảm bảo có thể scroll ngay cả khi content ít */}
<Box style={{ height: "700px" }} />
@ -463,7 +548,7 @@ function ModalHistory({
style={{ height: "calc(75vh - 80px)" }}
>
<Text c="dimmed" size="lg">
{currentStationData
{historyData.length > 0
? `No history data available for ${
TIME_PERIODS.find(
(p) => p.value === activeTimePeriod