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 [activeStation, setActiveStation] = useState<string>("");
const [activeTimePeriod, setActiveTimePeriod] = useState<string>("current"); const [activeTimePeriod, setActiveTimePeriod] = useState<string>("current");
const scrollViewportRef = useRef<HTMLDivElement>(null); 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(() => { useEffect(() => {
if (!socket || !opened) return; if (!socket || !opened) return;
@ -89,15 +90,32 @@ function ModalHistory({
setHistoryData([]); setHistoryData([]);
setActiveStation(""); setActiveStation("");
setActiveTimePeriod("current"); setActiveTimePeriod("current");
stationRefs.current.clear();
} }
return () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [opened]); }, [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(() => { useEffect(() => {
if (scrollViewportRef?.current) { if (activeStation && isManualScrollRef.current) {
scrollViewportRef.current.scrollTop = 0; 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) // Utility function to format timestamp (can be used later if needed)
// const formatTimestamp = (timestamp: number) => { // const formatTimestamp = (timestamp: number) => {
@ -163,17 +181,35 @@ function ModalHistory({
return grouped; return grouped;
}; };
const currentStationData = historyData.find( // Apply time filter to ALL stations data (không filter bỏ stations không có data)
(station) => station.stationId.toString() === activeStation const allFilteredStations = historyData.map((station) => ({
); ...station,
filteredHistory: filterHistoryByTime(station.history),
}));
// Apply time filter to current station data // Group each station's history by line number
const filteredHistory = currentStationData const allGroupedStations = allFilteredStations.map((station) => ({
? filterHistoryByTime(currentStationData.history) ...station,
: []; groupedHistory: station.filteredHistory.length > 0
? groupHistoryByLine(station.filteredHistory)
: new Map<number, HistoryItem[]>(),
}));
// Group filtered history by line number // Function to scroll to a specific station
const groupedHistory = groupHistoryByLine(filteredHistory); 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; if (!opened) return null;
@ -262,9 +298,11 @@ function ModalHistory({
? "filled" ? "filled"
: "outline" : "outline"
} }
onClick={() => onClick={() => {
setActiveStation(station.stationId.toString()) isManualScrollRef.current = true;
} setActiveStation(station.stationId.toString());
scrollToStation(station.stationId);
}}
> >
{station.stationName} {station.stationName}
</Button> </Button>
@ -304,71 +342,101 @@ function ModalHistory({
<ScrollArea <ScrollArea
h="calc(75vh - 80px)" h="calc(75vh - 80px)"
viewportRef={scrollViewportRef} viewportRef={scrollViewportRef}
onScrollPositionChange={(position) => { onScrollPositionChange={() => {
if ( if (isManualScrollRef.current || !scrollViewportRef.current) return;
!scrollViewportRef.current ||
isAutoSwitchingRef.current
)
return;
const scrollTop = position.y; // Debounce để scroll mượt hơn
const target = scrollViewportRef.current; if (scrollTimeoutRef.current) {
const scrollHeight = target.scrollHeight; clearTimeout(scrollTimeoutRef.current);
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;
}
} }
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)" }}> <Box style={{ minHeight: "calc(75vh - 60px)" }}>
{groupedHistory.size > 0 ? ( {allGroupedStations.length > 0 ? (
<> <>
{Array.from(groupedHistory.entries()) {allGroupedStations.map((station) => (
.sort(([lineA], [lineB]) => lineA - lineB) <Box key={`station-${station.stationId}`} style={{ marginBottom: "24px" }}>
.map(([lineNumber, items]) => ( {/* Station Title */}
<Box <Box
key={`line-group-${lineNumber}`} ref={(el) => {
if (el) {
stationRefs.current.set(station.stationId, el);
}
}}
style={{ style={{
padding: "12px 16px",
backgroundColor: "#495057",
color: "white",
fontWeight: 700,
marginBottom: "8px", marginBottom: "8px",
border: "1px solid #dee2e6",
borderRadius: "4px", 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 */} {/* Header của nhóm - hiển thị line number */}
<Box <Box
style={{ style={{
@ -449,9 +517,26 @@ function ModalHistory({
</span> </span>
</Text> </Text>
</Box> </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 */} {/* Spacer để đảm bảo có thể scroll ngay cả khi content ít */}
<Box style={{ height: "700px" }} /> <Box style={{ height: "700px" }} />
@ -463,7 +548,7 @@ function ModalHistory({
style={{ height: "calc(75vh - 80px)" }} style={{ height: "calc(75vh - 80px)" }}
> >
<Text c="dimmed" size="lg"> <Text c="dimmed" size="lg">
{currentStationData {historyData.length > 0
? `No history data available for ${ ? `No history data available for ${
TIME_PERIODS.find( TIME_PERIODS.find(
(p) => p.value === activeTimePeriod (p) => p.value === activeTimePeriod