| @@ -32,33 +32,52 @@ interface CompletedTracker { | |||||
| refreshCount: number; | refreshCount: number; | ||||
| } | } | ||||
| // Data stored per date for instant switching | |||||
| interface DateData { | |||||
| today: TruckScheduleDashboardItem[]; | |||||
| tomorrow: TruckScheduleDashboardItem[]; | |||||
| dayAfterTomorrow: TruckScheduleDashboardItem[]; | |||||
| } | |||||
| const TruckScheduleDashboard: React.FC = () => { | const TruckScheduleDashboard: React.FC = () => { | ||||
| const { t } = useTranslation("dashboard"); | const { t } = useTranslation("dashboard"); | ||||
| const [selectedStore, setSelectedStore] = useState<string>(""); | const [selectedStore, setSelectedStore] = useState<string>(""); | ||||
| const [selectedDate, setSelectedDate] = useState<string>("today"); | const [selectedDate, setSelectedDate] = useState<string>("today"); | ||||
| const [data, setData] = useState<TruckScheduleDashboardItem[]>([]); | |||||
| // Store data for all three dates for instant switching | |||||
| const [allData, setAllData] = useState<DateData>({ today: [], tomorrow: [], dayAfterTomorrow: [] }); | |||||
| const [loading, setLoading] = useState<boolean>(true); | const [loading, setLoading] = useState<boolean>(true); | ||||
| // Initialize as null to avoid SSR/client hydration mismatch | // Initialize as null to avoid SSR/client hydration mismatch | ||||
| const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null); | const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null); | ||||
| const [isClient, setIsClient] = useState<boolean>(false); | const [isClient, setIsClient] = useState<boolean>(false); | ||||
| const completedTrackerRef = useRef<Map<string, CompletedTracker>>(new Map()); | |||||
| const refreshCountRef = useRef<number>(0); | |||||
| // Track completed items per date | |||||
| const completedTrackerRef = useRef<Map<string, Map<string, CompletedTracker>>>(new Map([ | |||||
| ['today', new Map()], | |||||
| ['tomorrow', new Map()], | |||||
| ['dayAfterTomorrow', new Map()] | |||||
| ])); | |||||
| const refreshCountRef = useRef<Map<string, number>>(new Map([ | |||||
| ['today', 0], | |||||
| ['tomorrow', 0], | |||||
| ['dayAfterTomorrow', 0] | |||||
| ])); | |||||
| // Get date label for display (e.g., "2026-01-17") | // Get date label for display (e.g., "2026-01-17") | ||||
| const getDateLabel = (offset: number): string => { | const getDateLabel = (offset: number): string => { | ||||
| return dayjs().add(offset, 'day').format('YYYY-MM-DD'); | return dayjs().add(offset, 'day').format('YYYY-MM-DD'); | ||||
| }; | }; | ||||
| // Get day offset based on date option | |||||
| const getDateOffset = (dateOption: string): number => { | |||||
| if (dateOption === "today") return 0; | |||||
| if (dateOption === "tomorrow") return 1; | |||||
| if (dateOption === "dayAfterTomorrow") return 2; | |||||
| return 0; | |||||
| }; | |||||
| // Convert date option to YYYY-MM-DD format for API | // Convert date option to YYYY-MM-DD format for API | ||||
| const getDateParam = (dateOption: string): string => { | const getDateParam = (dateOption: string): string => { | ||||
| if (dateOption === "today") { | |||||
| return dayjs().format('YYYY-MM-DD'); | |||||
| } else if (dateOption === "tomorrow") { | |||||
| return dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||||
| } else if (dateOption === "dayAfterTomorrow") { | |||||
| return dayjs().add(2, 'day').format('YYYY-MM-DD'); | |||||
| } | |||||
| return dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||||
| const offset = getDateOffset(dateOption); | |||||
| return dayjs().add(offset, 'day').format('YYYY-MM-DD'); | |||||
| }; | }; | ||||
| // Set client flag and time on mount | // Set client flag and time on mount | ||||
| @@ -109,7 +128,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| }; | }; | ||||
| // Calculate time remaining for truck departure | // Calculate time remaining for truck departure | ||||
| const calculateTimeRemaining = useCallback((departureTime: string | number[] | null): string => { | |||||
| const calculateTimeRemaining = useCallback((departureTime: string | number[] | null, dateOption: string): string => { | |||||
| if (!departureTime || !currentTime) return '-'; | if (!departureTime || !currentTime) return '-'; | ||||
| const now = currentTime; | const now = currentTime; | ||||
| @@ -129,8 +148,9 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| return '-'; | return '-'; | ||||
| } | } | ||||
| // Create departure datetime for today | |||||
| const departure = now.clone().hour(departureHour).minute(departureMinute).second(0); | |||||
| // Create departure datetime for the selected date (today, tomorrow, or day after tomorrow) | |||||
| const dateOffset = getDateOffset(dateOption); | |||||
| const departure = now.clone().add(dateOffset, 'day').hour(departureHour).minute(departureMinute).second(0); | |||||
| const diffMinutes = departure.diff(now, 'minute'); | const diffMinutes = departure.diff(now, 'minute'); | ||||
| if (diffMinutes < 0) { | if (diffMinutes < 0) { | ||||
| @@ -151,58 +171,81 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| return `${item.storeId}-${item.truckLanceCode}-${item.truckDepartureTime}`; | return `${item.storeId}-${item.truckLanceCode}-${item.truckDepartureTime}`; | ||||
| }; | }; | ||||
| // Load data from API | |||||
| const loadData = useCallback(async () => { | |||||
| // Process data for a specific date option with completed tracker logic | |||||
| const processDataForDate = (result: TruckScheduleDashboardItem[], dateOption: string): TruckScheduleDashboardItem[] => { | |||||
| const tracker = completedTrackerRef.current.get(dateOption) || new Map(); | |||||
| const currentRefresh = (refreshCountRef.current.get(dateOption) || 0) + 1; | |||||
| refreshCountRef.current.set(dateOption, currentRefresh); | |||||
| result.forEach(item => { | |||||
| const key = getItemKey(item); | |||||
| // If all tickets are completed, track it | |||||
| if (item.numberOfPickTickets > 0 && item.numberOfTicketsCompleted >= item.numberOfPickTickets) { | |||||
| const existing = tracker.get(key); | |||||
| if (!existing) { | |||||
| tracker.set(key, { key, refreshCount: currentRefresh }); | |||||
| } | |||||
| } else { | |||||
| // Remove from tracker if no longer completed | |||||
| tracker.delete(key); | |||||
| } | |||||
| }); | |||||
| completedTrackerRef.current.set(dateOption, tracker); | |||||
| // Filter out items that have been completed for 2+ refresh cycles | |||||
| return result.filter(item => { | |||||
| const key = getItemKey(item); | |||||
| const itemTracker = tracker.get(key); | |||||
| if (itemTracker) { | |||||
| // Hide if completed for 2 or more refresh cycles | |||||
| if (currentRefresh - itemTracker.refreshCount >= 2) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| return true; | |||||
| }); | |||||
| }; | |||||
| // Load data for all three dates in parallel for instant switching | |||||
| const loadData = useCallback(async (isInitialLoad: boolean = false) => { | |||||
| // Only show loading spinner on initial load, not during refresh | |||||
| if (isInitialLoad) { | |||||
| setLoading(true); | |||||
| } | |||||
| try { | try { | ||||
| const dateParam = getDateParam(selectedDate); | |||||
| const result = await fetchTruckScheduleDashboardClient(dateParam); | |||||
| const dateOptions = ['today', 'tomorrow', 'dayAfterTomorrow'] as const; | |||||
| const dateParams = dateOptions.map(opt => getDateParam(opt)); | |||||
| // Update completed tracker | |||||
| refreshCountRef.current += 1; | |||||
| const currentRefresh = refreshCountRef.current; | |||||
| // Fetch all three dates in parallel | |||||
| const [todayResult, tomorrowResult, dayAfterResult] = await Promise.all([ | |||||
| fetchTruckScheduleDashboardClient(dateParams[0]), | |||||
| fetchTruckScheduleDashboardClient(dateParams[1]), | |||||
| fetchTruckScheduleDashboardClient(dateParams[2]) | |||||
| ]); | |||||
| result.forEach(item => { | |||||
| const key = getItemKey(item); | |||||
| // If all tickets are completed, track it | |||||
| if (item.numberOfPickTickets > 0 && item.numberOfTicketsCompleted >= item.numberOfPickTickets) { | |||||
| const existing = completedTrackerRef.current.get(key); | |||||
| if (!existing) { | |||||
| completedTrackerRef.current.set(key, { key, refreshCount: currentRefresh }); | |||||
| } | |||||
| } else { | |||||
| // Remove from tracker if no longer completed | |||||
| completedTrackerRef.current.delete(key); | |||||
| } | |||||
| // Process each date's data with completed tracker logic | |||||
| setAllData({ | |||||
| today: processDataForDate(todayResult, 'today'), | |||||
| tomorrow: processDataForDate(tomorrowResult, 'tomorrow'), | |||||
| dayAfterTomorrow: processDataForDate(dayAfterResult, 'dayAfterTomorrow') | |||||
| }); | }); | ||||
| // Filter out items that have been completed for 2+ refresh cycles | |||||
| const filteredResult = result.filter(item => { | |||||
| const key = getItemKey(item); | |||||
| const tracker = completedTrackerRef.current.get(key); | |||||
| if (tracker) { | |||||
| // Hide if completed for 2 or more refresh cycles | |||||
| if (currentRefresh - tracker.refreshCount >= 2) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| return true; | |||||
| }); | |||||
| setData(filteredResult); | |||||
| } catch (error) { | } catch (error) { | ||||
| console.error('Error fetching truck schedule dashboard:', error); | console.error('Error fetching truck schedule dashboard:', error); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | |||||
| if (isInitialLoad) { | |||||
| setLoading(false); | |||||
| } | |||||
| } | } | ||||
| }, [selectedDate]); | |||||
| }, []); | |||||
| // Initial load and auto-refresh every 5 minutes | // Initial load and auto-refresh every 5 minutes | ||||
| useEffect(() => { | useEffect(() => { | ||||
| loadData(); | |||||
| loadData(true); // Initial load - show spinner | |||||
| const refreshInterval = setInterval(() => { | const refreshInterval = setInterval(() => { | ||||
| loadData(); | |||||
| }, 0.1 * 60 * 1000); // 5 minutes | |||||
| loadData(false); // Refresh - don't show spinner, keep existing data visible | |||||
| }, 5 * 60 * 1000); // 5 minutes | |||||
| return () => clearInterval(refreshInterval); | return () => clearInterval(refreshInterval); | ||||
| }, [loadData]); | }, [loadData]); | ||||
| @@ -218,14 +261,17 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| return () => clearInterval(timeInterval); | return () => clearInterval(timeInterval); | ||||
| }, [isClient]); | }, [isClient]); | ||||
| // Filter data by selected store | |||||
| // Get data for selected date, then filter by store - both filters are instant | |||||
| const filteredData = useMemo(() => { | const filteredData = useMemo(() => { | ||||
| if (!selectedStore) return data; | |||||
| return data.filter(item => item.storeId === selectedStore); | |||||
| }, [data, selectedStore]); | |||||
| // First get the data for the selected date | |||||
| const dateData = allData[selectedDate as keyof DateData] || []; | |||||
| // Then filter by store if selected | |||||
| if (!selectedStore) return dateData; | |||||
| return dateData.filter(item => item.storeId === selectedStore); | |||||
| }, [allData, selectedDate, selectedStore]); | |||||
| // Get chip color based on time remaining | // Get chip color based on time remaining | ||||
| const getTimeChipColor = (departureTime: string | number[] | null): "success" | "warning" | "error" | "default" => { | |||||
| const getTimeChipColor = (departureTime: string | number[] | null, dateOption: string): "success" | "warning" | "error" | "default" => { | |||||
| if (!departureTime || !currentTime) return "default"; | if (!departureTime || !currentTime) return "default"; | ||||
| const now = currentTime; | const now = currentTime; | ||||
| @@ -245,7 +291,9 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| return "default"; | return "default"; | ||||
| } | } | ||||
| const departure = now.clone().hour(departureHour).minute(departureMinute).second(0); | |||||
| // Create departure datetime for the selected date (today, tomorrow, or day after tomorrow) | |||||
| const dateOffset = getDateOffset(dateOption); | |||||
| const departure = now.clone().add(dateOffset, 'day').hour(departureHour).minute(departureMinute).second(0); | |||||
| const diffMinutes = departure.diff(now, 'minute'); | const diffMinutes = departure.diff(now, 'minute'); | ||||
| if (diffMinutes < 0) return "error"; // Past due | if (diffMinutes < 0) return "error"; // Past due | ||||
| @@ -332,8 +380,8 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| </TableRow> | </TableRow> | ||||
| ) : ( | ) : ( | ||||
| filteredData.map((row, index) => { | filteredData.map((row, index) => { | ||||
| const timeRemaining = calculateTimeRemaining(row.truckDepartureTime); | |||||
| const chipColor = getTimeChipColor(row.truckDepartureTime); | |||||
| const timeRemaining = calculateTimeRemaining(row.truckDepartureTime, selectedDate); | |||||
| const chipColor = getTimeChipColor(row.truckDepartureTime, selectedDate); | |||||
| return ( | return ( | ||||
| <TableRow | <TableRow | ||||