| @@ -203,87 +203,91 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| return '-'; | |||
| }; | |||
| // Calculate time remaining for truck departure | |||
| 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 '-'; | |||
| // Compare a timestamp against the selected date (today/tomorrow/...) and return a short label. | |||
| // Used to avoid confusion when picking crosses midnight. | |||
| const relativeDayLabel = useCallback( | |||
| (dateTimeData: string | number[] | null, dateOption: string): string | null => { | |||
| if (!dateTimeData) return null; | |||
| const dateOffset = getDateOffset(dateOption); | |||
| const baseDate = dayjs().startOf('day').add(dateOffset, 'day'); | |||
| let dt: dayjs.Dayjs; | |||
| if (Array.isArray(dateTimeData)) { | |||
| dt = arrayToDayjs(dateTimeData, true); | |||
| } else { | |||
| return '-'; | |||
| dt = dayjs(dateTimeData); | |||
| } | |||
| // Parse departure time | |||
| if (!dt.isValid()) return null; | |||
| const diffDays = dt.startOf('day').diff(baseDate, 'day'); | |||
| if (diffDays === 0) return null; | |||
| if (diffDays === -1) return '昨日'; | |||
| if (diffDays === 1) return '明日'; | |||
| if (diffDays <= -2) return `${Math.abs(diffDays)}日前`; | |||
| return `${diffDays}日後`; | |||
| }, | |||
| [], | |||
| ); | |||
| const getDepartureEtdDayjs = useCallback( | |||
| (departureTime: string | number[] | null, dateOption: string): dayjs.Dayjs | null => { | |||
| if (!departureTime) return null; | |||
| const dateOffset = getDateOffset(dateOption); | |||
| const baseDate = dayjs().add(dateOffset, 'day'); | |||
| let departureDayjs: dayjs.Dayjs; | |||
| const baseDate = dayjs().startOf('day').add(dateOffset, 'day'); | |||
| let hour: number; | |||
| let minute: number; | |||
| 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); | |||
| if (departureTime.length < 2) return null; | |||
| hour = departureTime[0] || 0; | |||
| minute = departureTime[1] || 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); | |||
| if (parts.length < 2) return null; | |||
| hour = parseInt(parts[0], 10); | |||
| minute = parseInt(parts[1], 10); | |||
| } else { | |||
| return '-'; | |||
| return null; | |||
| } | |||
| // 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; | |||
| 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 '-'; | |||
| return baseDate.hour(hour).minute(minute).second(0); | |||
| }, | |||
| [], | |||
| ); | |||
| const getLastEndDayjsOrNull = useCallback((lastTicketEndTime: string | number[] | null): dayjs.Dayjs | null => { | |||
| if (!lastTicketEndTime) return null; | |||
| if (Array.isArray(lastTicketEndTime)) { | |||
| return arrayToDayjs(lastTicketEndTime, true); | |||
| } | |||
| // 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'); | |||
| const d = dayjs(lastTicketEndTime); | |||
| return d.isValid() ? d : null; | |||
| }, []); | |||
| const getDiffMinutesForRemaining = useCallback( | |||
| (item: TruckScheduleDashboardItem, dateOption: string): number | null => { | |||
| if (!currentTime) return null; | |||
| const etd = getDepartureEtdDayjs(item.truckDepartureTime, dateOption); | |||
| if (!etd) return null; | |||
| const allDone = | |||
| item.numberOfTicketsReleased > 0 && item.numberOfTicketsCompleted >= item.numberOfTicketsReleased; | |||
| if (allDone) { | |||
| const lastEnd = getLastEndDayjsOrNull(item.lastTicketEndTime); | |||
| if (!lastEnd) return null; | |||
| return etd.diff(lastEnd, 'minute'); | |||
| } | |||
| return etd.diff(currentTime, 'minute'); | |||
| }, | |||
| [currentTime, getDepartureEtdDayjs, getLastEndDayjsOrNull], | |||
| ); | |||
| // Calculate time remaining for truck departure | |||
| const calculateTimeRemaining = useCallback((item: TruckScheduleDashboardItem, dateOption: string): string => { | |||
| const diffMinutes = getDiffMinutesForRemaining(item, dateOption); | |||
| if (diffMinutes === null) return '-'; | |||
| if (diffMinutes < 0) { | |||
| // Past departure time | |||
| @@ -296,7 +300,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| const minutes = diffMinutes % 60; | |||
| return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; | |||
| } | |||
| }, [currentTime]); | |||
| }, [getDiffMinutesForRemaining]); | |||
| // Generate unique key for tracking completed items | |||
| const getItemKey = (item: TruckScheduleDashboardItem): string => { | |||
| @@ -414,34 +418,12 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| }, [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 | |||
| const getTimeChipColor = (item: TruckScheduleDashboardItem, dateOption: string): "success" | "warning" | "error" | "default" => { | |||
| const diffMinutes = getDiffMinutesForRemaining(item, dateOption); | |||
| if (diffMinutes === null) return "default"; | |||
| if (diffMinutes < 0) return "error"; | |||
| if (diffMinutes < 30) return "warning"; | |||
| return "success"; | |||
| }; | |||
| return ( | |||
| @@ -535,7 +517,9 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| ) : ( | |||
| filteredData.map((row, index) => { | |||
| const timeRemaining = calculateTimeRemaining(row, selectedDate); | |||
| const chipColor = getTimeChipColor(row.truckDepartureTime, selectedDate); | |||
| const chipColor = getTimeChipColor(row, selectedDate); | |||
| const startRel = relativeDayLabel(row.firstTicketStartTime, selectedDate); | |||
| const endRel = relativeDayLabel(row.lastTicketEndTime, selectedDate); | |||
| return ( | |||
| <TableRow | |||
| @@ -597,10 +581,20 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| {formatDateTime(row.firstTicketStartTime)} | |||
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}> | |||
| <Typography variant="body2">{formatDateTime(row.firstTicketStartTime)}</Typography> | |||
| {startRel && ( | |||
| <Chip size="small" variant="outlined" color="warning" label={startRel} /> | |||
| )} | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell> | |||
| {formatDateTime(row.lastTicketEndTime)} | |||
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}> | |||
| <Typography variant="body2">{formatDateTime(row.lastTicketEndTime)}</Typography> | |||
| {endRel && ( | |||
| <Chip size="small" variant="outlined" color="warning" label={endRel} /> | |||
| )} | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell align="right"> | |||
| <Typography | |||