From 6479034e629aed6573b1688f0e4fe92dc853651c Mon Sep 17 00:00:00 2001 From: "Tommy\\2Fi-Staff" Date: Mon, 9 Feb 2026 16:18:57 +0800 Subject: [PATCH] TruckScheduleDashboard & StockInTraceability report update --- .../truckSchedule/TruckScheduleDashboard.tsx | 156 +++++++++++++++++- src/config/reportConfig.ts | 20 ++- 2 files changed, 161 insertions(+), 15 deletions(-) diff --git a/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx b/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx index 2c0ba24..123795d 100644 --- a/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx +++ b/src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx @@ -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(""); @@ -49,6 +61,8 @@ const TruckScheduleDashboard: React.FC = () => { // Initialize as null to avoid SSR/client hydration mismatch const [currentTime, setCurrentTime] = useState(null); const [isClient, setIsClient] = useState(false); + // Track when data was last refreshed (not current time) + const [lastDataRefreshTime, setLastDataRefreshTime] = useState(null); // Track completed items per date const completedTrackerRef = useRef>>(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>(); + const refreshCountMap = new Map(); + + 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 = () => { - {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') : '--:--:--'} @@ -380,7 +518,7 @@ const TruckScheduleDashboard: React.FC = () => { ) : ( 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' }} > diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index a533559..7b7e5f0 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -72,12 +72,20 @@ export const REPORTS: ReportDefinition[] = [ title: "入倉記錄報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-in-traceability`, fields: [ - { label: "倉存類別 Stock Category", name: "stockCategory", type: "text", required: false, placeholder: "e.g. Meat" }, - { label: "倉存細分類 Stock Sub Category", name: "stockSubCategory", type: "text", required: false, placeholder: "e.g. Chicken" }, - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false, placeholder: "e.g. MT-001" }, - { label: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, - { label: "入倉日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, - { label: "入倉日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, + { label: "倉存類別 Stock Category", name: "stockCategory", type: "select", required: true, + multiple: true, + options: [ + { label: "All", value: "MAT,FG,WIP,NM,CMB"}, + { label: "MAT", value: "MAT" }, + { label: "FG", value: "FG" }, + { label: "WIP", value: "WIP" }, + { label: "NM", value: "NM" }, + { label: "CMB", value: "CMB" } + ] + }, + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "入倉日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: true }, + { label: "入倉日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: true }, ] }, {