FPSMS-frontend
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 

615 рядки
24 KiB

  1. "use client";
  2. import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
  3. import {
  4. Box,
  5. Typography,
  6. FormControl,
  7. InputLabel,
  8. Select,
  9. MenuItem,
  10. Card,
  11. CardContent,
  12. Stack,
  13. Table,
  14. TableBody,
  15. TableCell,
  16. TableContainer,
  17. TableHead,
  18. TableRow,
  19. Paper,
  20. CircularProgress,
  21. Chip
  22. } from '@mui/material';
  23. import { useTranslation } from 'react-i18next';
  24. import dayjs from 'dayjs';
  25. import { fetchTruckScheduleDashboardClient, type TruckScheduleDashboardItem } from '@/app/api/do/client';
  26. import { formatDepartureTime, arrayToDayjs } from '@/app/utils/formatUtil';
  27. // Track completed items for hiding after 2 refresh cycles
  28. interface CompletedTracker {
  29. key: string;
  30. refreshCount: number;
  31. }
  32. // Data stored per date for instant switching
  33. interface DateData {
  34. today: TruckScheduleDashboardItem[];
  35. tomorrow: TruckScheduleDashboardItem[];
  36. dayAfterTomorrow: TruckScheduleDashboardItem[];
  37. }
  38. // Storage structure for persisting completed tracker state
  39. interface PersistedTrackerState {
  40. completedTracker: {
  41. [dateOption: string]: { [key: string]: CompletedTracker };
  42. };
  43. refreshCount: {
  44. [dateOption: string]: number;
  45. };
  46. }
  47. const STORAGE_KEY = 'truckScheduleCompletedTracker';
  48. const TruckScheduleDashboard: React.FC = () => {
  49. const { t } = useTranslation("dashboard");
  50. const [selectedStore, setSelectedStore] = useState<string>("");
  51. const [selectedDate, setSelectedDate] = useState<string>("today");
  52. // Store data for all three dates for instant switching
  53. const [allData, setAllData] = useState<DateData>({ today: [], tomorrow: [], dayAfterTomorrow: [] });
  54. const [loading, setLoading] = useState<boolean>(true);
  55. // Initialize as null to avoid SSR/client hydration mismatch
  56. const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null);
  57. const [isClient, setIsClient] = useState<boolean>(false);
  58. // Track when data was last refreshed (not current time)
  59. const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null);
  60. // Track completed items per date
  61. const completedTrackerRef = useRef<Map<string, Map<string, CompletedTracker>>>(new Map([
  62. ['today', new Map()],
  63. ['tomorrow', new Map()],
  64. ['dayAfterTomorrow', new Map()]
  65. ]));
  66. const refreshCountRef = useRef<Map<string, number>>(new Map([
  67. ['today', 0],
  68. ['tomorrow', 0],
  69. ['dayAfterTomorrow', 0]
  70. ]));
  71. // Save completed tracker state to sessionStorage
  72. const saveCompletedTrackerToStorage = useCallback(() => {
  73. if (typeof window === 'undefined') return;
  74. try {
  75. const completedTrackerObj: { [dateOption: string]: { [key: string]: CompletedTracker } } = {};
  76. const refreshCountObj: { [dateOption: string]: number } = {};
  77. // Convert Maps to plain objects
  78. completedTrackerRef.current.forEach((tracker, dateOption) => {
  79. completedTrackerObj[dateOption] = Object.fromEntries(tracker);
  80. });
  81. refreshCountRef.current.forEach((count, dateOption) => {
  82. refreshCountObj[dateOption] = count;
  83. });
  84. const state: PersistedTrackerState = {
  85. completedTracker: completedTrackerObj,
  86. refreshCount: refreshCountObj
  87. };
  88. sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
  89. } catch (error) {
  90. console.warn('Failed to save completed tracker to sessionStorage:', error);
  91. }
  92. }, []);
  93. // Load completed tracker state from sessionStorage
  94. const loadCompletedTrackerFromStorage = useCallback((): boolean => {
  95. if (typeof window === 'undefined') return false;
  96. try {
  97. const saved = sessionStorage.getItem(STORAGE_KEY);
  98. if (!saved) return false;
  99. const state: PersistedTrackerState = JSON.parse(saved);
  100. // Reconstruct Maps from plain objects
  101. const completedTrackerMap = new Map<string, Map<string, CompletedTracker>>();
  102. const refreshCountMap = new Map<string, number>();
  103. Object.entries(state.completedTracker).forEach(([dateOption, trackerObj]) => {
  104. completedTrackerMap.set(dateOption, new Map(Object.entries(trackerObj)));
  105. });
  106. Object.entries(state.refreshCount).forEach(([dateOption, count]) => {
  107. refreshCountMap.set(dateOption, count);
  108. });
  109. completedTrackerRef.current = completedTrackerMap;
  110. refreshCountRef.current = refreshCountMap;
  111. return true;
  112. } catch (error) {
  113. console.warn('Failed to load completed tracker from sessionStorage:', error);
  114. return false;
  115. }
  116. }, []);
  117. // Get date label for display (e.g., "2026-01-17")
  118. const getDateLabel = (offset: number): string => {
  119. return dayjs().add(offset, 'day').format('YYYY-MM-DD');
  120. };
  121. // Get day offset based on date option
  122. const getDateOffset = (dateOption: string): number => {
  123. if (dateOption === "today") return 0;
  124. if (dateOption === "tomorrow") return 1;
  125. if (dateOption === "dayAfterTomorrow") return 2;
  126. return 0;
  127. };
  128. // Convert date option to YYYY-MM-DD format for API
  129. const getDateParam = (dateOption: string): string => {
  130. const offset = getDateOffset(dateOption);
  131. return dayjs().add(offset, 'day').format('YYYY-MM-DD');
  132. };
  133. // Set client flag and time on mount, load persisted state
  134. useEffect(() => {
  135. setIsClient(true);
  136. setCurrentTime(dayjs());
  137. // Load persisted completed tracker state from sessionStorage
  138. loadCompletedTrackerFromStorage();
  139. }, [loadCompletedTrackerFromStorage]);
  140. // Format time from array or string to HH:mm
  141. const formatTime = (timeData: string | number[] | null): string => {
  142. if (!timeData) return '-';
  143. if (Array.isArray(timeData)) {
  144. if (timeData.length >= 2) {
  145. const hour = timeData[0] || 0;
  146. const minute = timeData[1] || 0;
  147. return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
  148. }
  149. return '-';
  150. }
  151. if (typeof timeData === 'string') {
  152. const parts = timeData.split(':');
  153. if (parts.length >= 2) {
  154. const hour = parseInt(parts[0], 10);
  155. const minute = parseInt(parts[1], 10);
  156. return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
  157. }
  158. }
  159. return '-';
  160. };
  161. // Format datetime from array or string
  162. const formatDateTime = (dateTimeData: string | number[] | null): string => {
  163. if (!dateTimeData) return '-';
  164. if (Array.isArray(dateTimeData)) {
  165. return arrayToDayjs(dateTimeData, true).format('HH:mm');
  166. }
  167. const parsed = dayjs(dateTimeData);
  168. if (parsed.isValid()) {
  169. return parsed.format('HH:mm');
  170. }
  171. return '-';
  172. };
  173. // Calculate time remaining for truck departure
  174. const calculateTimeRemaining = useCallback((item: TruckScheduleDashboardItem, dateOption: string): string => {
  175. // If all tickets are completed, return the difference between ETD and last ticket end time
  176. if (item.numberOfPickTickets > 0 && item.numberOfTicketsCompleted >= item.numberOfPickTickets) {
  177. const lastTicketEndTime = item.lastTicketEndTime;
  178. const departureTime = item.truckDepartureTime;
  179. if (!lastTicketEndTime || !departureTime) return '-';
  180. // Parse last ticket end time
  181. let lastEndDayjs: dayjs.Dayjs;
  182. if (Array.isArray(lastTicketEndTime)) {
  183. lastEndDayjs = arrayToDayjs(lastTicketEndTime, true);
  184. } else if (typeof lastTicketEndTime === 'string') {
  185. lastEndDayjs = dayjs(lastTicketEndTime);
  186. if (!lastEndDayjs.isValid()) return '-';
  187. } else {
  188. return '-';
  189. }
  190. // Parse departure time
  191. const dateOffset = getDateOffset(dateOption);
  192. const baseDate = dayjs().add(dateOffset, 'day');
  193. let departureDayjs: dayjs.Dayjs;
  194. if (Array.isArray(departureTime)) {
  195. if (departureTime.length < 2) return '-';
  196. const hour = departureTime[0] || 0;
  197. const minute = departureTime[1] || 0;
  198. departureDayjs = baseDate.hour(hour).minute(minute).second(0);
  199. } else if (typeof departureTime === 'string') {
  200. const parts = departureTime.split(':');
  201. if (parts.length < 2) return '-';
  202. const hour = parseInt(parts[0], 10);
  203. const minute = parseInt(parts[1], 10);
  204. departureDayjs = baseDate.hour(hour).minute(minute).second(0);
  205. } else {
  206. return '-';
  207. }
  208. // Calculate difference: ETD - lastTicketEndTime
  209. const diffMinutes = departureDayjs.diff(lastEndDayjs, 'minute');
  210. if (diffMinutes < 0) {
  211. // ETD is before last ticket end (negative difference)
  212. const absDiff = Math.abs(diffMinutes);
  213. const hours = Math.floor(absDiff / 60);
  214. const minutes = absDiff % 60;
  215. return `-${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
  216. } else {
  217. // ETD is after last ticket end (positive difference)
  218. const hours = Math.floor(diffMinutes / 60);
  219. const minutes = diffMinutes % 60;
  220. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
  221. }
  222. }
  223. const departureTime = item.truckDepartureTime;
  224. if (!departureTime || !currentTime) return '-';
  225. const now = currentTime;
  226. let departureHour: number;
  227. let departureMinute: number;
  228. if (Array.isArray(departureTime)) {
  229. if (departureTime.length < 2) return '-';
  230. departureHour = departureTime[0] || 0;
  231. departureMinute = departureTime[1] || 0;
  232. } else if (typeof departureTime === 'string') {
  233. const parts = departureTime.split(':');
  234. if (parts.length < 2) return '-';
  235. departureHour = parseInt(parts[0], 10);
  236. departureMinute = parseInt(parts[1], 10);
  237. } else {
  238. return '-';
  239. }
  240. // Create departure datetime for the selected date (today, tomorrow, or day after tomorrow)
  241. const dateOffset = getDateOffset(dateOption);
  242. const departure = now.clone().add(dateOffset, 'day').hour(departureHour).minute(departureMinute).second(0);
  243. const diffMinutes = departure.diff(now, 'minute');
  244. if (diffMinutes < 0) {
  245. // Past departure time
  246. const absDiff = Math.abs(diffMinutes);
  247. const hours = Math.floor(absDiff / 60);
  248. const minutes = absDiff % 60;
  249. return `-${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
  250. } else {
  251. const hours = Math.floor(diffMinutes / 60);
  252. const minutes = diffMinutes % 60;
  253. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
  254. }
  255. }, [currentTime]);
  256. // Generate unique key for tracking completed items
  257. const getItemKey = (item: TruckScheduleDashboardItem): string => {
  258. return `${item.storeId}-${item.truckLanceCode}-${item.truckDepartureTime}`;
  259. };
  260. // Process data for a specific date option with completed tracker logic
  261. const processDataForDate = useCallback((result: TruckScheduleDashboardItem[], dateOption: string): TruckScheduleDashboardItem[] => {
  262. const tracker = completedTrackerRef.current.get(dateOption) || new Map();
  263. const currentRefresh = (refreshCountRef.current.get(dateOption) || 0) + 1;
  264. refreshCountRef.current.set(dateOption, currentRefresh);
  265. result.forEach(item => {
  266. const key = getItemKey(item);
  267. // If all tickets are completed, track it
  268. if (item.numberOfPickTickets > 0 && item.numberOfTicketsCompleted >= item.numberOfPickTickets) {
  269. const existing = tracker.get(key);
  270. if (!existing) {
  271. tracker.set(key, { key, refreshCount: currentRefresh });
  272. }
  273. } else {
  274. // Remove from tracker if no longer completed
  275. tracker.delete(key);
  276. }
  277. });
  278. completedTrackerRef.current.set(dateOption, tracker);
  279. // Save to sessionStorage after updating tracker
  280. saveCompletedTrackerToStorage();
  281. // Filter out items that have been completed for 2+ refresh cycles
  282. return result.filter(item => {
  283. const key = getItemKey(item);
  284. const itemTracker = tracker.get(key);
  285. if (itemTracker) {
  286. // Hide if completed for 2 or more refresh cycles
  287. if (currentRefresh - itemTracker.refreshCount >= 2) {
  288. return false;
  289. }
  290. }
  291. return true;
  292. });
  293. }, [saveCompletedTrackerToStorage]);
  294. // Load data for all three dates in parallel for instant switching
  295. const loadData = useCallback(async (isInitialLoad: boolean = false) => {
  296. // Only show loading spinner on initial load, not during refresh
  297. if (isInitialLoad) {
  298. setLoading(true);
  299. }
  300. try {
  301. const dateOptions = ['today', 'tomorrow', 'dayAfterTomorrow'] as const;
  302. const dateParams = dateOptions.map(opt => getDateParam(opt));
  303. // Fetch all three dates in parallel
  304. const [todayResult, tomorrowResult, dayAfterResult] = await Promise.all([
  305. fetchTruckScheduleDashboardClient(dateParams[0]),
  306. fetchTruckScheduleDashboardClient(dateParams[1]),
  307. fetchTruckScheduleDashboardClient(dateParams[2])
  308. ]);
  309. // Process each date's data with completed tracker logic
  310. setAllData({
  311. today: processDataForDate(todayResult, 'today'),
  312. tomorrow: processDataForDate(tomorrowResult, 'tomorrow'),
  313. dayAfterTomorrow: processDataForDate(dayAfterResult, 'dayAfterTomorrow')
  314. });
  315. // Update last data refresh time only when data is successfully loaded
  316. setLastDataRefreshTime(dayjs());
  317. } catch (error) {
  318. console.error('Error fetching truck schedule dashboard:', error);
  319. } finally {
  320. if (isInitialLoad) {
  321. setLoading(false);
  322. }
  323. }
  324. }, [processDataForDate]);
  325. // Initial load and auto-refresh every 5 minutes
  326. useEffect(() => {
  327. loadData(true); // Initial load - show spinner
  328. const refreshInterval = setInterval(() => {
  329. loadData(false); // Refresh - don't show spinner, keep existing data visible
  330. }, 5 * 60 * 1000); // 5 minutes
  331. return () => clearInterval(refreshInterval);
  332. }, [loadData]);
  333. // Update current time every 1 minute for time remaining calculation
  334. useEffect(() => {
  335. if (!isClient) return;
  336. const timeInterval = setInterval(() => {
  337. setCurrentTime(dayjs());
  338. }, 60 * 1000); // 1 minute
  339. return () => clearInterval(timeInterval);
  340. }, [isClient]);
  341. // Get data for selected date, then filter by store - both filters are instant
  342. const filteredData = useMemo(() => {
  343. // First get the data for the selected date
  344. const dateData = allData[selectedDate as keyof DateData] || [];
  345. // Then filter by store if selected
  346. if (!selectedStore) return dateData;
  347. return dateData.filter(item => item.storeId === selectedStore);
  348. }, [allData, selectedDate, selectedStore]);
  349. // Get chip color based on time remaining
  350. const getTimeChipColor = (departureTime: string | number[] | null, dateOption: string): "success" | "warning" | "error" | "default" => {
  351. if (!departureTime || !currentTime) return "default";
  352. const now = currentTime;
  353. let departureHour: number;
  354. let departureMinute: number;
  355. if (Array.isArray(departureTime)) {
  356. if (departureTime.length < 2) return "default";
  357. departureHour = departureTime[0] || 0;
  358. departureMinute = departureTime[1] || 0;
  359. } else if (typeof departureTime === 'string') {
  360. const parts = departureTime.split(':');
  361. if (parts.length < 2) return "default";
  362. departureHour = parseInt(parts[0], 10);
  363. departureMinute = parseInt(parts[1], 10);
  364. } else {
  365. return "default";
  366. }
  367. // Create departure datetime for the selected date (today, tomorrow, or day after tomorrow)
  368. const dateOffset = getDateOffset(dateOption);
  369. const departure = now.clone().add(dateOffset, 'day').hour(departureHour).minute(departureMinute).second(0);
  370. const diffMinutes = departure.diff(now, 'minute');
  371. if (diffMinutes < 0) return "error"; // Past due
  372. if (diffMinutes <= 30) return "warning"; // Within 30 minutes
  373. return "success"; // More than 30 minutes
  374. };
  375. return (
  376. <Card sx={{ mb: 2 }}>
  377. <CardContent>
  378. {/* Filter */}
  379. <Stack direction="row" spacing={2} sx={{ mb: 3 }}>
  380. <FormControl sx={{ minWidth: 150 }} size="small">
  381. <InputLabel id="store-select-label" shrink={true}>
  382. {t("Store ID")}
  383. </InputLabel>
  384. <Select
  385. labelId="store-select-label"
  386. id="store-select"
  387. value={selectedStore}
  388. label={t("Store ID")}
  389. onChange={(e) => setSelectedStore(e.target.value)}
  390. displayEmpty
  391. >
  392. <MenuItem value="">{t("All Stores")}</MenuItem>
  393. <MenuItem value="2/F">2/F</MenuItem>
  394. <MenuItem value="4/F">4/F</MenuItem>
  395. </Select>
  396. </FormControl>
  397. <FormControl sx={{ minWidth: 200 }} size="small">
  398. <InputLabel id="date-select-label" shrink={true}>
  399. {t("Select Date")}
  400. </InputLabel>
  401. <Select
  402. labelId="date-select-label"
  403. id="date-select"
  404. value={selectedDate}
  405. label={t("Select Date")}
  406. onChange={(e) => setSelectedDate(e.target.value)}
  407. >
  408. <MenuItem value="today">{t("Today")} ({getDateLabel(0)})</MenuItem>
  409. <MenuItem value="tomorrow">{t("Tomorrow")} ({getDateLabel(1)})</MenuItem>
  410. <MenuItem value="dayAfterTomorrow">{t("Day After Tomorrow")} ({getDateLabel(2)})</MenuItem>
  411. </Select>
  412. </FormControl>
  413. <Typography variant="body2" sx={{ alignSelf: 'center', color: 'text.secondary' }}>
  414. {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'}
  415. </Typography>
  416. </Stack>
  417. {/* Table */}
  418. <Box sx={{ mt: 2 }}>
  419. {loading ? (
  420. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  421. <CircularProgress />
  422. </Box>
  423. ) : (
  424. <TableContainer component={Paper}>
  425. <Table size="small" sx={{ minWidth: 1200 }}>
  426. <TableHead>
  427. <TableRow sx={{ backgroundColor: 'grey.100' }}>
  428. <TableCell sx={{ fontWeight: 600 }}>{t("Store ID")}</TableCell>
  429. <TableCell sx={{ fontWeight: 600 }}>{t("Truck Schedule")}</TableCell>
  430. <TableCell sx={{ fontWeight: 600 }}>{t("Time Remaining")}</TableCell>
  431. <TableCell sx={{ fontWeight: 600 }} align="center">{t("No. of Shops")}</TableCell>
  432. <TableCell sx={{ fontWeight: 600 }} align="center">{t("Tickets Released")}</TableCell>
  433. <TableCell sx={{ fontWeight: 600 }} align="center">{t("Tickets Completed")}</TableCell>
  434. <TableCell sx={{ fontWeight: 600 }} align="center">{t("Total Items")}</TableCell>
  435. <TableCell sx={{ fontWeight: 600 }}>{t("First Ticket Start")}</TableCell>
  436. <TableCell sx={{ fontWeight: 600 }}>{t("Last Ticket End")}</TableCell>
  437. <TableCell sx={{ fontWeight: 600 }} align="center">{t("Pick Time (min)")}</TableCell>
  438. </TableRow>
  439. </TableHead>
  440. <TableBody>
  441. {filteredData.length === 0 ? (
  442. <TableRow>
  443. <TableCell colSpan={10} align="center">
  444. <Typography variant="body2" color="text.secondary">
  445. {t("No truck schedules available")} ({getDateParam(selectedDate)})
  446. </Typography>
  447. </TableCell>
  448. </TableRow>
  449. ) : (
  450. filteredData.map((row, index) => {
  451. const timeRemaining = calculateTimeRemaining(row, selectedDate);
  452. const chipColor = getTimeChipColor(row.truckDepartureTime, selectedDate);
  453. return (
  454. <TableRow
  455. key={`${row.storeId}-${row.truckLanceCode}-${index}`}
  456. sx={{
  457. '&:hover': { backgroundColor: 'grey.50' },
  458. backgroundColor: row.numberOfPickTickets > 0 && row.numberOfTicketsCompleted >= row.numberOfPickTickets
  459. ? 'rgba(76, 175, 80, 0.15)'
  460. : 'inherit'
  461. }}
  462. >
  463. <TableCell>
  464. <Chip
  465. label={row.storeId || '-'}
  466. size="small"
  467. color={row.storeId === '2/F' ? 'primary' : 'secondary'}
  468. />
  469. </TableCell>
  470. <TableCell>
  471. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  472. <Typography variant="body2" sx={{ fontWeight: 500 }}>
  473. {row.truckLanceCode || '-'}
  474. </Typography>
  475. <Typography variant="caption" color="text.secondary">
  476. ETD: {formatTime(row.truckDepartureTime)}
  477. </Typography>
  478. </Box>
  479. </TableCell>
  480. <TableCell>
  481. <Chip
  482. label={timeRemaining}
  483. size="small"
  484. color={chipColor}
  485. sx={{ fontWeight: 600 }}
  486. />
  487. </TableCell>
  488. <TableCell align="center">
  489. <Typography variant="body2">
  490. {row.numberOfShopsToServe} [{row.numberOfPickTickets}]
  491. </Typography>
  492. </TableCell>
  493. <TableCell align="center">
  494. <Chip
  495. label={row.numberOfTicketsReleased}
  496. size="small"
  497. color={row.numberOfTicketsReleased > 0 ? 'info' : 'default'}
  498. />
  499. </TableCell>
  500. <TableCell align="center">
  501. <Chip
  502. label={row.numberOfTicketsCompleted}
  503. size="small"
  504. color={row.numberOfTicketsCompleted > 0 ? 'success' : 'default'}
  505. />
  506. </TableCell>
  507. <TableCell align="center">
  508. <Typography variant="body2" sx={{ fontWeight: 500 }}>
  509. {row.totalItemsToPick}
  510. </Typography>
  511. </TableCell>
  512. <TableCell>
  513. {formatDateTime(row.firstTicketStartTime)}
  514. </TableCell>
  515. <TableCell>
  516. {formatDateTime(row.lastTicketEndTime)}
  517. </TableCell>
  518. <TableCell align="center">
  519. <Typography
  520. variant="body2"
  521. sx={{
  522. fontWeight: 500,
  523. color: row.pickTimeTakenMinutes !== null ? 'text.primary' : 'text.secondary'
  524. }}
  525. >
  526. {row.pickTimeTakenMinutes !== null ? row.pickTimeTakenMinutes : '-'}
  527. </Typography>
  528. </TableCell>
  529. </TableRow>
  530. );
  531. })
  532. )}
  533. </TableBody>
  534. </Table>
  535. </TableContainer>
  536. )}
  537. </Box>
  538. </CardContent>
  539. </Card>
  540. );
  541. };
  542. export default TruckScheduleDashboard;