瀏覽代碼

TruckScheduleDashboard & StockInTraceability report update

MergeProblem1
Tommy\2Fi-Staff 19 小時之前
父節點
當前提交
6479034e62
共有 2 個檔案被更改,包括 161 行新增15 行删除
  1. +147
    -9
      src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx
  2. +14
    -6
      src/config/reportConfig.ts

+ 147
- 9
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<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'
}}
>


+ 14
- 6
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 },
]
},
{


Loading…
取消
儲存