| @@ -32,33 +32,52 @@ interface CompletedTracker { | |||
| refreshCount: number; | |||
| } | |||
| // Data stored per date for instant switching | |||
| interface DateData { | |||
| today: TruckScheduleDashboardItem[]; | |||
| tomorrow: TruckScheduleDashboardItem[]; | |||
| dayAfterTomorrow: TruckScheduleDashboardItem[]; | |||
| } | |||
| const TruckScheduleDashboard: React.FC = () => { | |||
| const { t } = useTranslation("dashboard"); | |||
| const [selectedStore, setSelectedStore] = useState<string>(""); | |||
| 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); | |||
| // Initialize as null to avoid SSR/client hydration mismatch | |||
| const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null); | |||
| 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") | |||
| const getDateLabel = (offset: number): string => { | |||
| 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 | |||
| 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 | |||
| @@ -109,7 +128,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| }; | |||
| // 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 '-'; | |||
| const now = currentTime; | |||
| @@ -129,8 +148,9 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| 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'); | |||
| if (diffMinutes < 0) { | |||
| @@ -151,58 +171,81 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| 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 { | |||
| 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) { | |||
| console.error('Error fetching truck schedule dashboard:', error); | |||
| } finally { | |||
| setLoading(false); | |||
| if (isInitialLoad) { | |||
| setLoading(false); | |||
| } | |||
| } | |||
| }, [selectedDate]); | |||
| }, []); | |||
| // Initial load and auto-refresh every 5 minutes | |||
| useEffect(() => { | |||
| loadData(); | |||
| loadData(true); // Initial load - show spinner | |||
| 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); | |||
| }, [loadData]); | |||
| @@ -218,14 +261,17 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| return () => clearInterval(timeInterval); | |||
| }, [isClient]); | |||
| // Filter data by selected store | |||
| // Get data for selected date, then filter by store - both filters are instant | |||
| 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 | |||
| 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"; | |||
| const now = currentTime; | |||
| @@ -245,7 +291,9 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| 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'); | |||
| if (diffMinutes < 0) return "error"; // Past due | |||
| @@ -332,8 +380,8 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| </TableRow> | |||
| ) : ( | |||
| 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 ( | |||
| <TableRow | |||