|
|
|
@@ -39,6 +39,18 @@ interface DateData { |
|
|
|
dayAfterTomorrow: TruckScheduleDashboardItem[]; |
|
|
|
} |
|
|
|
|
|
|
|
// Storage structure for persisting completed tracker state |
|
|
|
interface PersistedTrackerState { |
|
|
|
completedTracker: { |
|
|
|
[dateOption: string]: { [key: string]: CompletedTracker }; |
|
|
|
}; |
|
|
|
refreshCount: { |
|
|
|
[dateOption: string]: number; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
const STORAGE_KEY = 'truckScheduleCompletedTracker'; |
|
|
|
|
|
|
|
const TruckScheduleDashboard: React.FC = () => { |
|
|
|
const { t } = useTranslation("dashboard"); |
|
|
|
const [selectedStore, setSelectedStore] = useState<string>(""); |
|
|
|
@@ -49,6 +61,8 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
// Initialize as null to avoid SSR/client hydration mismatch |
|
|
|
const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null); |
|
|
|
const [isClient, setIsClient] = useState<boolean>(false); |
|
|
|
// Track when data was last refreshed (not current time) |
|
|
|
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null); |
|
|
|
// Track completed items per date |
|
|
|
const completedTrackerRef = useRef<Map<string, Map<string, CompletedTracker>>>(new Map([ |
|
|
|
['today', new Map()], |
|
|
|
@@ -61,6 +75,66 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
['dayAfterTomorrow', 0] |
|
|
|
])); |
|
|
|
|
|
|
|
// Save completed tracker state to sessionStorage |
|
|
|
const saveCompletedTrackerToStorage = useCallback(() => { |
|
|
|
if (typeof window === 'undefined') return; |
|
|
|
|
|
|
|
try { |
|
|
|
const completedTrackerObj: { [dateOption: string]: { [key: string]: CompletedTracker } } = {}; |
|
|
|
const refreshCountObj: { [dateOption: string]: number } = {}; |
|
|
|
|
|
|
|
// Convert Maps to plain objects |
|
|
|
completedTrackerRef.current.forEach((tracker, dateOption) => { |
|
|
|
completedTrackerObj[dateOption] = Object.fromEntries(tracker); |
|
|
|
}); |
|
|
|
|
|
|
|
refreshCountRef.current.forEach((count, dateOption) => { |
|
|
|
refreshCountObj[dateOption] = count; |
|
|
|
}); |
|
|
|
|
|
|
|
const state: PersistedTrackerState = { |
|
|
|
completedTracker: completedTrackerObj, |
|
|
|
refreshCount: refreshCountObj |
|
|
|
}; |
|
|
|
|
|
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); |
|
|
|
} catch (error) { |
|
|
|
console.warn('Failed to save completed tracker to sessionStorage:', error); |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
// Load completed tracker state from sessionStorage |
|
|
|
const loadCompletedTrackerFromStorage = useCallback((): boolean => { |
|
|
|
if (typeof window === 'undefined') return false; |
|
|
|
|
|
|
|
try { |
|
|
|
const saved = sessionStorage.getItem(STORAGE_KEY); |
|
|
|
if (!saved) return false; |
|
|
|
|
|
|
|
const state: PersistedTrackerState = JSON.parse(saved); |
|
|
|
|
|
|
|
// Reconstruct Maps from plain objects |
|
|
|
const completedTrackerMap = new Map<string, Map<string, CompletedTracker>>(); |
|
|
|
const refreshCountMap = new Map<string, number>(); |
|
|
|
|
|
|
|
Object.entries(state.completedTracker).forEach(([dateOption, trackerObj]) => { |
|
|
|
completedTrackerMap.set(dateOption, new Map(Object.entries(trackerObj))); |
|
|
|
}); |
|
|
|
|
|
|
|
Object.entries(state.refreshCount).forEach(([dateOption, count]) => { |
|
|
|
refreshCountMap.set(dateOption, count); |
|
|
|
}); |
|
|
|
|
|
|
|
completedTrackerRef.current = completedTrackerMap; |
|
|
|
refreshCountRef.current = refreshCountMap; |
|
|
|
|
|
|
|
return true; |
|
|
|
} catch (error) { |
|
|
|
console.warn('Failed to load completed tracker from sessionStorage:', error); |
|
|
|
return false; |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
// Get date label for display (e.g., "2026-01-17") |
|
|
|
const getDateLabel = (offset: number): string => { |
|
|
|
return dayjs().add(offset, 'day').format('YYYY-MM-DD'); |
|
|
|
@@ -80,11 +154,13 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
return dayjs().add(offset, 'day').format('YYYY-MM-DD'); |
|
|
|
}; |
|
|
|
|
|
|
|
// Set client flag and time on mount |
|
|
|
// Set client flag and time on mount, load persisted state |
|
|
|
useEffect(() => { |
|
|
|
setIsClient(true); |
|
|
|
setCurrentTime(dayjs()); |
|
|
|
}, []); |
|
|
|
// Load persisted completed tracker state from sessionStorage |
|
|
|
loadCompletedTrackerFromStorage(); |
|
|
|
}, [loadCompletedTrackerFromStorage]); |
|
|
|
|
|
|
|
// Format time from array or string to HH:mm |
|
|
|
const formatTime = (timeData: string | number[] | null): string => { |
|
|
|
@@ -128,7 +204,63 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
}; |
|
|
|
|
|
|
|
// Calculate time remaining for truck departure |
|
|
|
const calculateTimeRemaining = useCallback((departureTime: string | number[] | null, dateOption: string): string => { |
|
|
|
const calculateTimeRemaining = useCallback((item: TruckScheduleDashboardItem, dateOption: string): string => { |
|
|
|
// If all tickets are completed, return the difference between ETD and last ticket end time |
|
|
|
if (item.numberOfPickTickets > 0 && item.numberOfTicketsCompleted >= item.numberOfPickTickets) { |
|
|
|
const lastTicketEndTime = item.lastTicketEndTime; |
|
|
|
const departureTime = item.truckDepartureTime; |
|
|
|
|
|
|
|
if (!lastTicketEndTime || !departureTime) return '-'; |
|
|
|
|
|
|
|
// Parse last ticket end time |
|
|
|
let lastEndDayjs: dayjs.Dayjs; |
|
|
|
if (Array.isArray(lastTicketEndTime)) { |
|
|
|
lastEndDayjs = arrayToDayjs(lastTicketEndTime, true); |
|
|
|
} else if (typeof lastTicketEndTime === 'string') { |
|
|
|
lastEndDayjs = dayjs(lastTicketEndTime); |
|
|
|
if (!lastEndDayjs.isValid()) return '-'; |
|
|
|
} else { |
|
|
|
return '-'; |
|
|
|
} |
|
|
|
|
|
|
|
// Parse departure time |
|
|
|
const dateOffset = getDateOffset(dateOption); |
|
|
|
const baseDate = dayjs().add(dateOffset, 'day'); |
|
|
|
let departureDayjs: dayjs.Dayjs; |
|
|
|
|
|
|
|
if (Array.isArray(departureTime)) { |
|
|
|
if (departureTime.length < 2) return '-'; |
|
|
|
const hour = departureTime[0] || 0; |
|
|
|
const minute = departureTime[1] || 0; |
|
|
|
departureDayjs = baseDate.hour(hour).minute(minute).second(0); |
|
|
|
} else if (typeof departureTime === 'string') { |
|
|
|
const parts = departureTime.split(':'); |
|
|
|
if (parts.length < 2) return '-'; |
|
|
|
const hour = parseInt(parts[0], 10); |
|
|
|
const minute = parseInt(parts[1], 10); |
|
|
|
departureDayjs = baseDate.hour(hour).minute(minute).second(0); |
|
|
|
} else { |
|
|
|
return '-'; |
|
|
|
} |
|
|
|
|
|
|
|
// Calculate difference: ETD - lastTicketEndTime |
|
|
|
const diffMinutes = departureDayjs.diff(lastEndDayjs, 'minute'); |
|
|
|
|
|
|
|
if (diffMinutes < 0) { |
|
|
|
// ETD is before last ticket end (negative difference) |
|
|
|
const absDiff = Math.abs(diffMinutes); |
|
|
|
const hours = Math.floor(absDiff / 60); |
|
|
|
const minutes = absDiff % 60; |
|
|
|
return `-${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; |
|
|
|
} else { |
|
|
|
// ETD is after last ticket end (positive difference) |
|
|
|
const hours = Math.floor(diffMinutes / 60); |
|
|
|
const minutes = diffMinutes % 60; |
|
|
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const departureTime = item.truckDepartureTime; |
|
|
|
if (!departureTime || !currentTime) return '-'; |
|
|
|
|
|
|
|
const now = currentTime; |
|
|
|
@@ -172,7 +304,7 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
}; |
|
|
|
|
|
|
|
// Process data for a specific date option with completed tracker logic |
|
|
|
const processDataForDate = (result: TruckScheduleDashboardItem[], dateOption: string): TruckScheduleDashboardItem[] => { |
|
|
|
const processDataForDate = useCallback((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); |
|
|
|
@@ -193,6 +325,9 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
|
|
|
|
completedTrackerRef.current.set(dateOption, tracker); |
|
|
|
|
|
|
|
// Save to sessionStorage after updating tracker |
|
|
|
saveCompletedTrackerToStorage(); |
|
|
|
|
|
|
|
// Filter out items that have been completed for 2+ refresh cycles |
|
|
|
return result.filter(item => { |
|
|
|
const key = getItemKey(item); |
|
|
|
@@ -205,7 +340,7 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
} |
|
|
|
return true; |
|
|
|
}); |
|
|
|
}; |
|
|
|
}, [saveCompletedTrackerToStorage]); |
|
|
|
|
|
|
|
// Load data for all three dates in parallel for instant switching |
|
|
|
const loadData = useCallback(async (isInitialLoad: boolean = false) => { |
|
|
|
@@ -230,6 +365,9 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
tomorrow: processDataForDate(tomorrowResult, 'tomorrow'), |
|
|
|
dayAfterTomorrow: processDataForDate(dayAfterResult, 'dayAfterTomorrow') |
|
|
|
}); |
|
|
|
|
|
|
|
// Update last data refresh time only when data is successfully loaded |
|
|
|
setLastDataRefreshTime(dayjs()); |
|
|
|
} catch (error) { |
|
|
|
console.error('Error fetching truck schedule dashboard:', error); |
|
|
|
} finally { |
|
|
|
@@ -237,7 +375,7 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
setLoading(false); |
|
|
|
} |
|
|
|
} |
|
|
|
}, []); |
|
|
|
}, [processDataForDate]); |
|
|
|
|
|
|
|
// Initial load and auto-refresh every 5 minutes |
|
|
|
useEffect(() => { |
|
|
|
@@ -342,7 +480,7 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
</FormControl> |
|
|
|
|
|
|
|
<Typography variant="body2" sx={{ alignSelf: 'center', color: 'text.secondary' }}> |
|
|
|
{t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} |
|
|
|
{t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} |
|
|
|
</Typography> |
|
|
|
</Stack> |
|
|
|
|
|
|
|
@@ -380,7 +518,7 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
</TableRow> |
|
|
|
) : ( |
|
|
|
filteredData.map((row, index) => { |
|
|
|
const timeRemaining = calculateTimeRemaining(row.truckDepartureTime, selectedDate); |
|
|
|
const timeRemaining = calculateTimeRemaining(row, selectedDate); |
|
|
|
const chipColor = getTimeChipColor(row.truckDepartureTime, selectedDate); |
|
|
|
|
|
|
|
return ( |
|
|
|
@@ -389,7 +527,7 @@ const TruckScheduleDashboard: React.FC = () => { |
|
|
|
sx={{ |
|
|
|
'&:hover': { backgroundColor: 'grey.50' }, |
|
|
|
backgroundColor: row.numberOfPickTickets > 0 && row.numberOfTicketsCompleted >= row.numberOfPickTickets |
|
|
|
? 'success.light' |
|
|
|
? 'rgba(76, 175, 80, 0.15)' |
|
|
|
: 'inherit' |
|
|
|
}} |
|
|
|
> |
|
|
|
|