"use client"; import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Box, Typography, FormControl, InputLabel, Select, MenuItem, Card, CardContent, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, CircularProgress, Chip } from '@mui/material'; import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import { fetchTruckScheduleDashboardClient, type TruckScheduleDashboardItem } from '@/app/api/do/client'; import { formatDepartureTime, arrayToDayjs } from '@/app/utils/formatUtil'; // Track completed items for hiding after 2 refresh cycles interface CompletedTracker { key: string; 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(""); const [selectedDate, setSelectedDate] = useState("today"); // Store data for all three dates for instant switching const [allData, setAllData] = useState({ today: [], tomorrow: [], dayAfterTomorrow: [] }); const [loading, setLoading] = useState(true); // Initialize as null to avoid SSR/client hydration mismatch const [currentTime, setCurrentTime] = useState(null); const [isClient, setIsClient] = useState(false); // Track completed items per date const completedTrackerRef = useRef>>(new Map([ ['today', new Map()], ['tomorrow', new Map()], ['dayAfterTomorrow', new Map()] ])); const refreshCountRef = useRef>(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 => { const offset = getDateOffset(dateOption); return dayjs().add(offset, 'day').format('YYYY-MM-DD'); }; // Set client flag and time on mount useEffect(() => { setIsClient(true); setCurrentTime(dayjs()); }, []); // Format time from array or string to HH:mm const formatTime = (timeData: string | number[] | null): string => { if (!timeData) return '-'; if (Array.isArray(timeData)) { if (timeData.length >= 2) { const hour = timeData[0] || 0; const minute = timeData[1] || 0; return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; } return '-'; } if (typeof timeData === 'string') { const parts = timeData.split(':'); if (parts.length >= 2) { const hour = parseInt(parts[0], 10); const minute = parseInt(parts[1], 10); return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; } } return '-'; }; // Format datetime from array or string const formatDateTime = (dateTimeData: string | number[] | null): string => { if (!dateTimeData) return '-'; if (Array.isArray(dateTimeData)) { return arrayToDayjs(dateTimeData, true).format('HH:mm'); } const parsed = dayjs(dateTimeData); if (parsed.isValid()) { return parsed.format('HH:mm'); } return '-'; }; // Calculate time remaining for truck departure const calculateTimeRemaining = useCallback((departureTime: string | number[] | null, dateOption: string): string => { if (!departureTime || !currentTime) return '-'; const now = currentTime; let departureHour: number; let departureMinute: number; if (Array.isArray(departureTime)) { if (departureTime.length < 2) return '-'; departureHour = departureTime[0] || 0; departureMinute = departureTime[1] || 0; } else if (typeof departureTime === 'string') { const parts = departureTime.split(':'); if (parts.length < 2) return '-'; departureHour = parseInt(parts[0], 10); departureMinute = parseInt(parts[1], 10); } else { return '-'; } // 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) { // Past departure time 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 { const hours = Math.floor(diffMinutes / 60); const minutes = diffMinutes % 60; return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; } }, [currentTime]); // Generate unique key for tracking completed items const getItemKey = (item: TruckScheduleDashboardItem): string => { return `${item.storeId}-${item.truckLanceCode}-${item.truckDepartureTime}`; }; // 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 dateOptions = ['today', 'tomorrow', 'dayAfterTomorrow'] as const; const dateParams = dateOptions.map(opt => getDateParam(opt)); // Fetch all three dates in parallel const [todayResult, tomorrowResult, dayAfterResult] = await Promise.all([ fetchTruckScheduleDashboardClient(dateParams[0]), fetchTruckScheduleDashboardClient(dateParams[1]), fetchTruckScheduleDashboardClient(dateParams[2]) ]); // Process each date's data with completed tracker logic setAllData({ today: processDataForDate(todayResult, 'today'), tomorrow: processDataForDate(tomorrowResult, 'tomorrow'), dayAfterTomorrow: processDataForDate(dayAfterResult, 'dayAfterTomorrow') }); } catch (error) { console.error('Error fetching truck schedule dashboard:', error); } finally { if (isInitialLoad) { setLoading(false); } } }, []); // Initial load and auto-refresh every 5 minutes useEffect(() => { loadData(true); // Initial load - show spinner const refreshInterval = setInterval(() => { loadData(false); // Refresh - don't show spinner, keep existing data visible }, 5 * 60 * 1000); // 5 minutes return () => clearInterval(refreshInterval); }, [loadData]); // Update current time every 1 minute for time remaining calculation useEffect(() => { if (!isClient) return; const timeInterval = setInterval(() => { setCurrentTime(dayjs()); }, 60 * 1000); // 1 minute return () => clearInterval(timeInterval); }, [isClient]); // Get data for selected date, then filter by store - both filters are instant const filteredData = useMemo(() => { // 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, dateOption: string): "success" | "warning" | "error" | "default" => { if (!departureTime || !currentTime) return "default"; const now = currentTime; let departureHour: number; let departureMinute: number; if (Array.isArray(departureTime)) { if (departureTime.length < 2) return "default"; departureHour = departureTime[0] || 0; departureMinute = departureTime[1] || 0; } else if (typeof departureTime === 'string') { const parts = departureTime.split(':'); if (parts.length < 2) return "default"; departureHour = parseInt(parts[0], 10); departureMinute = parseInt(parts[1], 10); } else { return "default"; } // 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 if (diffMinutes <= 30) return "warning"; // Within 30 minutes return "success"; // More than 30 minutes }; return ( {/* Filter */} {t("Store ID")} {t("Select Date")} {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} {/* Table */} {loading ? ( ) : ( {t("Store ID")} {t("Truck Schedule")} {t("Time Remaining")} {t("No. of Shops")} {t("Total Items")} {t("Tickets Released")} {t("First Ticket Start")} {t("Tickets Completed")} {t("Last Ticket End")} {t("Pick Time (min)")} {filteredData.length === 0 ? ( {t("No truck schedules available")} ({getDateParam(selectedDate)}) ) : ( filteredData.map((row, index) => { const timeRemaining = calculateTimeRemaining(row.truckDepartureTime, selectedDate); const chipColor = getTimeChipColor(row.truckDepartureTime, selectedDate); return ( 0 && row.numberOfTicketsCompleted >= row.numberOfPickTickets ? 'success.light' : 'inherit' }} > {row.truckLanceCode || '-'} ETD: {formatTime(row.truckDepartureTime)} {row.numberOfShopsToServe} [{row.numberOfPickTickets}] {row.totalItemsToPick} 0 ? 'info' : 'default'} /> {formatDateTime(row.firstTicketStartTime)} 0 ? 'success' : 'default'} /> {formatDateTime(row.lastTicketEndTime)} {row.pickTimeTakenMinutes !== null ? row.pickTimeTakenMinutes : '-'} ); }) )}
)}
); }; export default TruckScheduleDashboard;