| @@ -62,6 +62,12 @@ const GoodsReceiptStatusNew: React.FC = () => { | |||
| return () => clearInterval(refreshInterval); | |||
| }, [loadData, screenCleared]); | |||
| const [now, setNow] = useState(dayjs()); | |||
| useEffect(() => { | |||
| const timer = setInterval(() => setNow(dayjs()), 60 * 1000); | |||
| return () => clearInterval(timer); | |||
| }, []); | |||
| const selectedDateLabel = useMemo(() => { | |||
| return selectedDate.format('YYYY-MM-DD'); | |||
| @@ -187,10 +193,14 @@ const GoodsReceiptStatusNew: React.FC = () => { | |||
| </Stack> | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 15 minutes")} | {t("Last updated")}: {lastUpdated ? lastUpdated.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| <Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Now")}: {now.format('HH:mm')} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 15 minutes")} | {t("Last updated")}: {lastUpdated ? lastUpdated.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| </Stack> | |||
| <Button variant="outlined" color="inherit" onClick={() => setScreenCleared(true)}> | |||
| {t("Exit Screen")} | |||
| @@ -204,7 +214,7 @@ const GoodsReceiptStatusNew: React.FC = () => { | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <TableContainer component={Paper}> | |||
| <TableContainer component={Paper} sx={{ maxHeight: 440, overflow: 'auto' }}> | |||
| <Table | |||
| size="small" | |||
| sx={{ | |||
| @@ -212,7 +222,7 @@ const GoodsReceiptStatusNew: React.FC = () => { | |||
| tableLayout: 'fixed', | |||
| }} | |||
| > | |||
| <TableHead> | |||
| <TableHead sx={{ backgroundColor: 'grey.100', position: 'sticky', top: 0, zIndex: 1 }}> | |||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | |||
| <TableCell sx={{ fontWeight: 600, width: '32%', padding: '4px 6px' }}>{t("Supplier")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600, width: '30%', padding: '4px 6px' }}>{t("Purchase Order Code")}</TableCell> | |||
| @@ -74,7 +74,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| ['tomorrow', 0], | |||
| ['dayAfterTomorrow', 0] | |||
| ])); | |||
| const [now, setNow] = useState(dayjs()); | |||
| // Save completed tracker state to sessionStorage | |||
| const saveCompletedTrackerToStorage = useCallback(() => { | |||
| if (typeof window === 'undefined') return; | |||
| @@ -377,6 +377,11 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| } | |||
| }, [processDataForDate]); | |||
| useEffect(() => { | |||
| const timer = setInterval(() => setNow(dayjs()), 60 * 1000); | |||
| return () => clearInterval(timer); | |||
| }, []); | |||
| // Initial load and auto-refresh every 5 minutes | |||
| useEffect(() => { | |||
| loadData(true); // Initial load - show spinner | |||
| @@ -483,11 +488,17 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| <MenuItem value="dayAfterTomorrow">{t("Day After Tomorrow")} ({getDateLabel(2)})</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <Typography variant="body2" sx={{ alignSelf: 'center', color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Now")}: {now.format('HH:mm')} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| </Stack> | |||
| </Stack> | |||
| {/* Table */} | |||
| <Box sx={{ mt: 2 }}> | |||
| @@ -496,20 +507,20 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <TableContainer component={Paper}> | |||
| <TableContainer component={Paper} sx={{ maxHeight: 440, overflow: 'auto' }}> | |||
| <Table size="small" sx={{ minWidth: 1200 }}> | |||
| <TableHead> | |||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | |||
| <TableRow sx={{ backgroundColor: 'grey.100', position: 'sticky', top: 0, zIndex: 1 }}> | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("Store ID")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("Truck Schedule")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("Time Remaining")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("No. of Shops")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Tickets Released")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Tickets Completed")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Total Items")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="right">{t("No. of Shops")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="right">{t("Tickets Released")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="right">{t("Tickets Completed")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="right">{t("Total Items")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("First Ticket Start")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("Last Ticket End")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Pick Time (min)")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="right">{t("Pick Time (min)")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| @@ -561,26 +572,26 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| sx={{ fontWeight: 600 }} | |||
| /> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <TableCell align="right"> | |||
| <Typography variant="body2"> | |||
| {row.numberOfShopsToServe} [{row.numberOfPickTickets}] | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <TableCell align="right"> | |||
| <Chip | |||
| label={row.numberOfTicketsReleased} | |||
| size="small" | |||
| color={row.numberOfTicketsReleased > 0 ? 'info' : 'default'} | |||
| /> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <TableCell align="right"> | |||
| <Chip | |||
| label={row.numberOfTicketsCompleted} | |||
| size="small" | |||
| color={row.numberOfTicketsCompleted > 0 ? 'success' : 'default'} | |||
| /> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <TableCell align="right"> | |||
| <Typography variant="body2" sx={{ fontWeight: 500 }}> | |||
| {row.totalItemsToPick} | |||
| </Typography> | |||
| @@ -591,7 +602,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| <TableCell> | |||
| {formatDateTime(row.lastTicketEndTime)} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <TableCell align="right"> | |||
| <Typography | |||
| variant="body2" | |||
| sx={{ | |||
| @@ -40,6 +40,8 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| pageSize: 5, | |||
| }); | |||
| const [now, setNow] = useState(dayjs()); | |||
| const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null); | |||
| const formatTime = (timeData: any): string => { | |||
| if (!timeData) return ''; | |||
| @@ -74,21 +76,25 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| return { startDate: today, endDate: dayAfterTomorrow }; | |||
| }; | |||
| useEffect(() => { | |||
| const loadData = async () => { | |||
| setLoading(true); | |||
| try { | |||
| const { startDate, endDate } = getDateRange(); | |||
| const result = await fetchTicketReleaseTable(startDate, endDate); | |||
| setData(result); | |||
| } catch (error) { | |||
| console.error('Error fetching ticket release table:', error); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }; | |||
| loadData(); | |||
| }, []); | |||
| const loadData = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const { startDate, endDate } = getDateRange(); | |||
| const result = await fetchTicketReleaseTable(startDate, endDate); | |||
| setData(result); | |||
| setLastDataRefreshTime(dayjs()); | |||
| } catch (error) { | |||
| console.error('Error fetching ticket release table:', error); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, []); | |||
| useEffect(() => { | |||
| loadData(); | |||
| const id = setInterval(loadData, 5 * 60 * 1000); | |||
| return () => clearInterval(id); | |||
| }, [loadData]); | |||
| const filteredData = data.filter((item) => { | |||
| // Filter by floor if selected | |||
| @@ -214,6 +220,16 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| <MenuItem value="completed">{t("completed")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Now")}: {now.format('HH:mm')} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| </Stack> | |||
| </Stack> | |||
| <Box sx={{ mt: 2 }}> | |||
| @@ -223,10 +239,10 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| </Box> | |||
| ) : ( | |||
| <> | |||
| <TableContainer component={Paper}> | |||
| <TableContainer component={Paper} sx={{ maxHeight: 440, overflow: 'auto' }}> | |||
| <Table size="small" sx={{ minWidth: 650 }}> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableRow sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'grey.100' }}> | |||
| <TableCell>{t("Store ID")}</TableCell> | |||
| <TableCell>{t("Required Delivery Date")}</TableCell> | |||
| <TableCell> | |||
| @@ -242,7 +258,7 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| {/*<TableCell>{t("Truck Departure Time")}</TableCell> | |||
| <TableCell>{t("Truck Lane Code")}</TableCell>*/} | |||
| <TableCell sx={{ minWidth: 200, width: '20%' }}>{t("Shop Name")}</TableCell> | |||
| <TableCell>{t("Loading Sequence")}</TableCell> | |||
| <TableCell align="right">{t("Loading Sequence")}</TableCell> | |||
| {/*<TableCell>{t("Delivery Order Code(s)")}</TableCell> | |||
| <TableCell>{t("Pick Order Code(s)")}</TableCell> | |||
| <TableCell>{t("Ticket Number")}</TableCell> | |||
| @@ -263,7 +279,7 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell>{t("Handler Name")}</TableCell> | |||
| <TableCell sx={{ minWidth: 100, width: '8%', whiteSpace: 'nowrap' }}>{t("Number of FG Items (Order Item(s) Count)")}</TableCell> | |||
| <TableCell align="right" sx={{ minWidth: 100, width: '8%', whiteSpace: 'nowrap' }}>{t("Number of FG Items (Order Item(s) Count)")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| @@ -309,9 +325,9 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| </TableCell> | |||
| <TableCell sx={{ minWidth: 200, width: '20%' }}>{row.shopName || '-'}</TableCell> | |||
| <TableCell>{row.loadingSequence || '-'}</TableCell> | |||
| <TableCell align="right">{row.loadingSequence || '-'}</TableCell> | |||
| {/*<TableCell>{row.deliveryOrderCode || '-'}</TableCell> | |||
| <TableCell>{row.pickOrderCode || '-'}</TableCell> | |||
| <TableCell align="right">{row.pickOrderCode || '-'}</TableCell> | |||
| <TableCell>{row.ticketNo || '-'}</TableCell> | |||
| <TableCell> | |||
| {row.ticketReleaseTime | |||
| @@ -360,8 +376,7 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| </Typography> | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell>{row.handlerName || '-'}</TableCell> | |||
| <TableCell sx={{ minWidth: 100, width: '8%' }}>{row.numberOfFGItems ?? 0}</TableCell> | |||
| <TableCell align="right" sx={{ minWidth: 100, width: '8%' }}>{row.numberOfFGItems ?? 0}</TableCell> | |||
| </TableRow> | |||
| ); | |||
| }) | |||
| @@ -37,7 +37,8 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| }); | |||
| const [now, setNow] = useState(dayjs()); | |||
| const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null); | |||
| const loadData = useCallback(async () => { | |||
| setLoading(true); | |||
| @@ -49,6 +50,7 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| setData(result || []); | |||
| } | |||
| refreshCountRef.current += 1; | |||
| setLastDataRefreshTime(dayjs()); | |||
| } catch (error) { | |||
| console.error('Error fetching material pick status:', error); | |||
| setData([]); | |||
| @@ -65,6 +67,11 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| return () => clearInterval(interval); | |||
| }, [loadData]); | |||
| useEffect(() => { | |||
| const timer = setInterval(() => setNow(dayjs()), 60 * 1000); | |||
| return () => clearInterval(timer); | |||
| }, []); | |||
| const formatTime = (timeData: any): string => { | |||
| if (!timeData) return ''; | |||
| @@ -245,6 +252,16 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| <MenuItem value={dayjs().subtract(2, "day").format("YYYY-MM-DD")}>{t("Two Days Ago")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Now")}: {now.format('HH:mm')} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 10 minutes")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| </Stack> | |||
| </Stack> | |||
| <Box sx={{ mt: 2 }}> | |||
| @@ -254,9 +271,9 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| </Box> | |||
| ) : ( | |||
| <> | |||
| <TableContainer component={Paper}> | |||
| <TableContainer component={Paper} sx={{ maxHeight: 440, overflow: 'auto' }}> | |||
| <Table size="small" sx={{ minWidth: 650 }}> | |||
| <TableHead> | |||
| <TableHead sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'grey.100' }}> | |||
| <TableRow> | |||
| <TableCell> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| @@ -267,7 +284,7 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TableCell align="right"> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Job Order Qty")} | |||
| @@ -276,7 +293,7 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TableCell align="right"> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("No. of Items to be Picked")} | |||
| @@ -284,7 +301,7 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TableCell align="right"> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("No. of Items with Issue During Pick")} | |||
| @@ -308,7 +325,7 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell sx={{ | |||
| <TableCell align="right" sx={{ | |||
| }}> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| @@ -343,16 +360,16 @@ const MaterialPickStatusTable: React.FC = () => { | |||
| </TableCell> | |||
| <TableCell> | |||
| <TableCell align="right"> | |||
| {row.jobOrderQty !== null && row.jobOrderQty !== undefined | |||
| ? `${row.jobOrderQty} ${row.uom || ''}` | |||
| : '-'} | |||
| </TableCell> | |||
| <TableCell>{row.numberOfItemsToPick ?? 0}</TableCell> | |||
| <TableCell>{row.numberOfItemsWithIssue ?? 0}</TableCell> | |||
| <TableCell align="right">{row.numberOfItemsToPick ?? 0}</TableCell> | |||
| <TableCell align="right">{row.numberOfItemsWithIssue ?? 0}</TableCell> | |||
| <TableCell>{formatTime(row.pickStartTime) || '-'}</TableCell> | |||
| <TableCell>{formatTime(row.pickEndTime) || '-'}</TableCell> | |||
| <TableCell sx={{ | |||
| <TableCell align="right" sx={{ | |||
| backgroundColor: 'rgba(76, 175, 80, 0.1)', | |||
| fontWeight: 600 | |||
| }}> | |||
| @@ -17,6 +17,7 @@ import { | |||
| Tabs, | |||
| Tab, | |||
| Chip, | |||
| Stack | |||
| } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import dayjs from "dayjs"; | |||
| @@ -34,7 +35,7 @@ const STATUS_COLORS: Record<string, "success" | "default" | "warning" | "error"> | |||
| Idle: "default", | |||
| Repair: "warning", | |||
| }; | |||
| const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null); | |||
| const formatDateTime = (value: any): string => { | |||
| if (!value) return "-"; | |||
| @@ -132,12 +133,14 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| const [data, setData] = useState<EquipmentStatusByTypeResponse[]>([]); | |||
| const [loading, setLoading] = useState<boolean>(true); | |||
| const [tabIndex, setTabIndex] = useState<number>(0); | |||
| const [now, setNow] = useState(dayjs()); | |||
| const loadData = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const result = await fetchEquipmentStatus(); | |||
| setData(result || []); | |||
| setLastDataRefreshTime(dayjs()); | |||
| } catch (error) { | |||
| console.error("Error fetching equipment status:", error); | |||
| setData([]); | |||
| @@ -163,6 +166,11 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| return () => clearInterval(timer); | |||
| }, []); | |||
| useEffect(() => { | |||
| const timer = setInterval(() => setNow(dayjs()), 60 * 1000); | |||
| return () => clearInterval(timer); | |||
| }, []); | |||
| const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { | |||
| setTabIndex(newValue); | |||
| }; | |||
| @@ -180,21 +188,32 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| </Typography> | |||
| </Box> | |||
| <Tabs | |||
| value={tabIndex} | |||
| onChange={handleTabChange} | |||
| sx={{ mb: 2 }} | |||
| variant="scrollable" | |||
| scrollButtons="auto" | |||
| > | |||
| <Tab label={t("All")} /> | |||
| {data.map((type, index) => ( | |||
| <Tab | |||
| key={type.equipmentTypeId} | |||
| label={type.equipmentTypeName || `${t("Equipment Type")} ${index + 1}`} | |||
| /> | |||
| ))} | |||
| </Tabs> | |||
| <Box sx={{ display: 'flex', alignItems: 'center', width: '100%', mb: 2 }}> | |||
| <Tabs | |||
| value={tabIndex} | |||
| onChange={handleTabChange} | |||
| sx={{ flexShrink: 0 }} | |||
| variant="scrollable" | |||
| scrollButtons="auto" | |||
| > | |||
| <Tab label={t("All")} /> | |||
| {data.map((type, index) => ( | |||
| <Tab | |||
| key={type.equipmentTypeId} | |||
| label={type.equipmentTypeName || `${t("Equipment Type")} ${index + 1}`} | |||
| /> | |||
| ))} | |||
| </Tabs> | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Now")}: {now.format('HH:mm')} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 1 minute")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| </Stack> | |||
| </Box> | |||
| {loading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| @@ -223,17 +242,17 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| {type.equipmentTypeName || "-"} | |||
| </Typography> | |||
| <TableContainer component={Paper}> | |||
| <TableContainer component={Paper} sx={{ maxHeight: 440, overflow: 'auto' }}> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableRow sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'background.paper' }}> | |||
| <TableCell> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Equipment Name and Code")} | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <TableCell key={d.equipmentDetailId}> | |||
| <Box sx={{ display: "flex", flexDirection: "column" }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {d.equipmentDetailName || "-"} | |||
| @@ -255,7 +274,7 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <TableCell key={d.equipmentDetailId}> | |||
| {d.status === "Processing" ? d.currentProcess?.processName || "-" : "-"} | |||
| </TableCell> | |||
| ))} | |||
| @@ -275,7 +294,7 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| // Processing 时只显示 job order code,不显示 Chip | |||
| if (d.status === "Processing" && cp?.jobOrderCode) { | |||
| return ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <TableCell key={d.equipmentDetailId}> | |||
| <Typography variant="body2" sx={{ fontWeight: 500 }}> | |||
| {cp.jobOrderCode} | |||
| </Typography> | |||
| @@ -285,7 +304,7 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| // 其他状态显示 Chip | |||
| return ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <TableCell key={d.equipmentDetailId}> | |||
| <Chip label={t(`${d.status}`)} color={chipColor} size="small" /> | |||
| </TableCell> | |||
| ); | |||
| @@ -302,7 +321,7 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <TableCell key={d.equipmentDetailId}> | |||
| {d.status === "Processing" | |||
| ? formatDateTime(d.currentProcess?.startTime) | |||
| : "-"} | |||
| @@ -318,7 +337,7 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <TableCell key={d.equipmentDetailId}> | |||
| {d.status === "Processing" | |||
| ? calculateEstimatedCompletionTime( | |||
| d.currentProcess?.startTime, | |||
| @@ -337,7 +356,7 @@ const EquipmentStatusDashboard: React.FC = () => { | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <TableCell align="right" key={d.equipmentDetailId}> | |||
| {d.status === "Processing" | |||
| ? calculateRemainingTime( | |||
| d.currentProcess?.startTime, | |||
| @@ -31,6 +31,7 @@ const JobProcessStatus: React.FC = () => { | |||
| const refreshCountRef = useRef<number>(0); | |||
| const [currentTime, setCurrentTime] = useState(dayjs()); | |||
| const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD")); | |||
| const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null); | |||
| // Update current time every second for countdown | |||
| useEffect(() => { | |||
| @@ -46,6 +47,7 @@ const JobProcessStatus: React.FC = () => { | |||
| const result = await fetchJobProcessStatus(selectedDate); | |||
| setData(result); | |||
| refreshCountRef.current += 1; | |||
| setLastDataRefreshTime(dayjs()); | |||
| } catch (error) { | |||
| console.error('Error fetching job process status:', error); | |||
| setData([]); | |||
| @@ -189,6 +191,16 @@ const JobProcessStatus: React.FC = () => { | |||
| <MenuItem value={dayjs().subtract(2, "day").format("YYYY-MM-DD")}>前天</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Now")}: {currentTime.format('HH:mm')} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 10 minutes")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| </Stack> | |||
| </Stack> | |||
| <Box sx={{ mt: 2 }}> | |||
| {loading ? ( | |||
| @@ -200,12 +212,14 @@ const JobProcessStatus: React.FC = () => { | |||
| component={Paper} | |||
| sx={{ | |||
| border: '3px solid #135fed', | |||
| overflowX: 'auto', // 关键:允许横向滚动 | |||
| overflowX: 'auto', | |||
| maxHeight: 440, | |||
| overflow: 'auto' | |||
| }} | |||
| > | |||
| <Table size="small" sx={{ minWidth: 1800 }}> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableHead sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'grey.100' }}> | |||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | |||
| <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Job Order No.")} | |||
| @@ -223,18 +237,18 @@ const JobProcessStatus: React.FC = () => { | |||
| </TableCell> | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | |||
| {Array.from({ length: 16 }, (_, i) => i + 1).map((num) => ( | |||
| <TableCell key={num} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <TableCell key={num} sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Process")} {num} | |||
| </Typography> | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | |||
| {Array.from({ length: 16 }, (_, i) => i + 1).map((num) => ( | |||
| <TableCell key={num} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <TableCell key={num} sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> | |||
| <Typography variant="caption" sx={{ fontWeight: 600 }}> | |||
| {t("Start")} | |||
| @@ -284,7 +298,7 @@ const JobProcessStatus: React.FC = () => { | |||
| // 如果工序不是必需的,只显示一个 N/A | |||
| if (!process.isRequired) { | |||
| return ( | |||
| <TableCell key={index} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <TableCell key={index} sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <Typography variant="body2"> | |||
| N/A | |||
| </Typography> | |||
| @@ -298,7 +312,7 @@ const JobProcessStatus: React.FC = () => { | |||
| ].filter(Boolean).join(" "); | |||
| // 如果工序是必需的,显示三行(Start、Finish、Wait Time) | |||
| return ( | |||
| <TableCell key={index} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <TableCell key={index} sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> | |||
| <Typography variant="body2" sx={{ mb: 0.5 }}>{label || "-"}</Typography> | |||
| <Typography variant="body2" sx={{ py: 0.5 }}> | |||
| @@ -32,6 +32,8 @@ const OperatorKpiDashboard: React.FC = () => { | |||
| const [loading, setLoading] = useState<boolean>(true); | |||
| const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD")); | |||
| const refreshCountRef = useRef<number>(0); | |||
| const [now, setNow] = useState(dayjs()); | |||
| const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null); | |||
| const formatTime = (timeData: any): string => { | |||
| if (!timeData) return "-"; | |||
| @@ -69,6 +71,7 @@ const OperatorKpiDashboard: React.FC = () => { | |||
| try { | |||
| const result = await fetchOperatorKpi(selectedDate); | |||
| setData(result); | |||
| setLastDataRefreshTime(dayjs()); | |||
| refreshCountRef.current += 1; | |||
| } catch (error) { | |||
| console.error("Error fetching operator KPI:", error); | |||
| @@ -86,6 +89,11 @@ const OperatorKpiDashboard: React.FC = () => { | |||
| return () => clearInterval(interval); | |||
| }, [loadData]); | |||
| useEffect(() => { | |||
| const timer = setInterval(() => setNow(dayjs()), 60 * 1000); | |||
| return () => clearInterval(timer); | |||
| }, []); | |||
| const renderCurrentProcesses = (processes: OperatorKpiProcessInfo[]) => { | |||
| if (!processes || processes.length === 0) { | |||
| return ( | |||
| @@ -167,6 +175,16 @@ const OperatorKpiDashboard: React.FC = () => { | |||
| <MenuItem value={dayjs().subtract(2, "day").format("YYYY-MM-DD")}>{t("Two Days Ago")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Now")}: {now.format('HH:mm')} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 10 minutes")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| </Stack> | |||
| </Stack> | |||
| {loading ? ( | |||
| @@ -174,25 +192,22 @@ const OperatorKpiDashboard: React.FC = () => { | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <TableContainer | |||
| <TableContainer sx={{ border: "3px solid #135fed", overflowX: "auto", maxHeight: 440, overflow: 'auto' }} | |||
| component={Paper} | |||
| sx={{ | |||
| border: "3px solid #135fed", | |||
| overflowX: "auto", | |||
| }} | |||
| > | |||
| <Table size="small" sx={{ minWidth: 800 }}> | |||
| <TableHead> | |||
| <TableRow | |||
| <TableRow | |||
| sx={{ | |||
| bgcolor: "#424242", | |||
| "& th": { | |||
| borderBottom: "none", | |||
| py: 1.5, | |||
| position: 'sticky', top: 0, zIndex: 1 | |||
| }, | |||
| }} | |||
| > | |||
| <TableCell sx={{ width: 80 }}> | |||
| <TableCell align="right" sx={{ width: 80 }}> | |||
| <Typography | |||
| variant="subtitle2" | |||
| sx={{ | |||
| @@ -252,10 +267,9 @@ const OperatorKpiDashboard: React.FC = () => { | |||
| }, | |||
| }} | |||
| > | |||
| <TableCell | |||
| <TableCell align="right" | |||
| sx={{ | |||
| width: 80, | |||
| textAlign: "center", | |||
| fontWeight: 500, | |||
| verticalAlign: "top", | |||
| }} | |||
| @@ -497,5 +497,11 @@ | |||
| "Handled By": "處理者", | |||
| "submit": "提交", | |||
| "Received Qty": "接收數量", | |||
| "bomWeighting": "物料清單" | |||
| "bomWeighting": "物料清單", | |||
| "Now": "現時", | |||
| "Last updated": "最後更新", | |||
| "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | |||
| "Auto-refresh every 10 minutes": "每10分鐘自動刷新", | |||
| "Auto-refresh every 15 minutes": "每15分鐘自動刷新", | |||
| "Auto-refresh every 1 minute": "每1分鐘自動刷新" | |||
| } | |||
| @@ -112,5 +112,11 @@ | |||
| "Show Purchase Order Codes": "顯示採購訂單編號", | |||
| "x/y orders received": "x/y張單已處理", | |||
| "Goods Receipt Status New": "採購單接收狀態", | |||
| "Status": "狀態" | |||
| "Status": "狀態", | |||
| "Now": "現時", | |||
| "Last updated": "最後更新", | |||
| "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | |||
| "Auto-refresh every 10 minutes": "每10分鐘自動刷新", | |||
| "Auto-refresh every 15 minutes": "每15分鐘自動刷新", | |||
| "Auto-refresh every 1 minute": "每1分鐘自動刷新" | |||
| } | |||
| @@ -151,6 +151,12 @@ | |||
| "View Details": "查看詳情", | |||
| "Skip": "跳過", | |||
| "Handler": "提料員", | |||
| "Now": "現時", | |||
| "Last updated": "最後更新", | |||
| "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | |||
| "Auto-refresh every 10 minutes": "每10分鐘自動刷新", | |||
| "Auto-refresh every 15 minutes": "每15分鐘自動刷新", | |||
| "Auto-refresh every 1 minute": "每1分鐘自動刷新", | |||
| "completed Job Order pick orders with Matching": "工單已完成提料和對料", | |||
| "No completed Job Order pick orders with matching found": "沒有相關記錄", | |||
| @@ -4,6 +4,12 @@ | |||
| "Today": "是日", | |||
| "Tomorrow": "翌日", | |||
| "Day After Tomorrow": "後日", | |||
| "Now": "現時", | |||
| "Last updated": "最後更新", | |||
| "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | |||
| "Auto-refresh every 10 minutes": "每10分鐘自動刷新", | |||
| "Auto-refresh every 15 minutes": "每15分鐘自動刷新", | |||
| "Auto-refresh every 1 minute": "每1分鐘自動刷新", | |||
| "Floor": "樓層", | |||
| "All Floors": "所有樓層", | |||
| "Store ID": "樓層", | |||