| @@ -203,87 +203,91 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| return '-'; | 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 { | } 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 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 (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') { | } else if (typeof departureTime === 'string') { | ||||
| const parts = departureTime.split(':'); | 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 { | } 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) { | if (diffMinutes < 0) { | ||||
| // Past departure time | // Past departure time | ||||
| @@ -296,7 +300,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| const minutes = diffMinutes % 60; | const minutes = diffMinutes % 60; | ||||
| return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; | return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; | ||||
| } | } | ||||
| }, [currentTime]); | |||||
| }, [getDiffMinutesForRemaining]); | |||||
| // Generate unique key for tracking completed items | // Generate unique key for tracking completed items | ||||
| const getItemKey = (item: TruckScheduleDashboardItem): string => { | const getItemKey = (item: TruckScheduleDashboardItem): string => { | ||||
| @@ -414,34 +418,12 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| }, [allData, selectedDate, selectedStore]); | }, [allData, selectedDate, selectedStore]); | ||||
| // Get chip color based on time remaining | // 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 ( | return ( | ||||
| @@ -535,7 +517,9 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| ) : ( | ) : ( | ||||
| filteredData.map((row, index) => { | filteredData.map((row, index) => { | ||||
| const timeRemaining = calculateTimeRemaining(row, selectedDate); | 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 ( | return ( | ||||
| <TableRow | <TableRow | ||||
| @@ -597,10 +581,20 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <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> | ||||
| <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> | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography | <Typography | ||||