|
- "use client";
-
- import React, { useState, useEffect, useCallback } from "react";
- import {
- Box,
- Card,
- CardContent,
- CircularProgress,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- Typography,
- Tabs,
- Tab,
- Chip,
- Stack
- } from "@mui/material";
- import { useTranslation } from "react-i18next";
- import dayjs from "dayjs";
- import {
- fetchEquipmentStatus,
- EquipmentStatusByTypeResponse,
- EquipmentStatusPerDetail,
- } from "@/app/api/jo/actions";
- import { arrayToDayjs } from "@/app/utils/formatUtil";
-
- const REFRESH_INTERVAL = 60 * 1000; // 1 分鐘
-
- const STATUS_COLORS: Record<string, "success" | "default" | "warning" | "error"> = {
- Processing: "success",
- Idle: "default",
- Repair: "warning",
- };
-
- const formatDateTime = (value: any): string => {
- if (!value) return "-";
-
- if (Array.isArray(value)) {
- try {
- const parsed = arrayToDayjs(value, true);
- if (parsed.isValid()) {
- return parsed.format("YYYY-MM-DD HH:mm");
- }
- } catch (e) {
- console.error("Error parsing datetime array:", e);
- }
- }
-
- if (typeof value === "string") {
- const parsed = dayjs(value);
- if (parsed.isValid()) {
- return parsed.format("YYYY-MM-DD HH:mm");
- }
- }
-
- return "-";
- };
-
- const formatTime = (value: any): string => {
- if (!value) return "-";
-
- if (Array.isArray(value)) {
- try {
- const parsed = arrayToDayjs(value, true);
- if (parsed.isValid()) {
- return parsed.format("HH:mm");
- }
- } catch (e) {
- console.error("Error parsing time array:", e);
- }
- }
-
- if (typeof value === "string") {
- const parsed = dayjs(value);
- if (parsed.isValid()) {
- return parsed.format("HH:mm");
- }
- }
-
- return "-";
- };
-
- // 计算预计完成时间
- const calculateEstimatedCompletionTime = (
- startTime: any,
- processingTime: number | null | undefined
- ): string => {
- if (!startTime || !processingTime || processingTime <= 0) return "-";
-
- try {
- const start = arrayToDayjs(startTime, true);
- if (!start.isValid()) return "-";
-
- const estimated = start.add(processingTime, "minute");
- return estimated.format("YYYY-MM-DD HH:mm");
- } catch (e) {
- console.error("Error calculating estimated completion time:", e);
- return "-";
- }
- };
-
- // 计算剩余时间(分钟)
- const calculateRemainingTime = (
- startTime: any,
- processingTime: number | null | undefined
- ): string => {
- if (!startTime || !processingTime || processingTime <= 0) return "-";
-
- try {
- const start = arrayToDayjs(startTime, true);
- if (!start.isValid()) return "-";
-
- const now = dayjs();
- const estimated = start.add(processingTime, "minute");
- const remainingMinutes = estimated.diff(now, "minute");
-
- if (remainingMinutes < 0) {
- return `-${Math.abs(remainingMinutes)}`;
- }
- return remainingMinutes.toString();
- } catch (e) {
- console.error("Error calculating remaining time:", e);
- return "-";
- }
- };
-
- const EquipmentStatusDashboard: React.FC = () => {
- const { t } = useTranslation(["common", "jo"]);
- const [data, setData] = useState<EquipmentStatusByTypeResponse[]>([]);
- const [loading, setLoading] = useState<boolean>(true);
- const [tabIndex, setTabIndex] = useState<number>(0);
- const [now, setNow] = useState(dayjs());
- const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null);
-
- 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([]);
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- loadData();
- const interval = setInterval(() => {
- loadData();
- }, REFRESH_INTERVAL);
- return () => clearInterval(interval);
- }, [loadData]);
-
- // 添加定时更新剩余时间
- useEffect(() => {
- const timer = setInterval(() => {
- // 触发重新渲染以更新剩余时间
- setData((prev) => [...prev]);
- }, 60000); // 每分钟更新一次
- return () => clearInterval(timer);
- }, []);
-
- useEffect(() => {
- const timer = setInterval(() => setNow(dayjs()), 60 * 1000);
- return () => clearInterval(timer);
- }, []);
-
- const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
- setTabIndex(newValue);
- };
-
- const displayTypes =
- tabIndex === 0
- ? data
- : data.filter((_, index) => index === tabIndex - 1);
-
- return (
- <Box>
- <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
- <Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
- {t("Production Equipment Status Dashboard")}
- </Typography>
- </Box>
-
- <Box sx={{ display: 'flex', alignItems: 'center', width: '100%', mb: 2, gap: 2 }}>
- <Box sx={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
- <Tabs
- value={tabIndex}
- onChange={handleTabChange}
- 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>
- <Stack direction="row" spacing={2} sx={{ flexShrink: 0, 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 }}>
- <CircularProgress />
- </Box>
- ) : displayTypes.length === 0 ? (
- <Box sx={{ textAlign: "center", p: 3 }}>
- {t("No data available")}
- </Box>
- ) : (
- <Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
- {displayTypes.map((type) => {
- const details = type.details || [];
- if (details.length === 0) return null;
-
- return (
- <Card
- key={type.equipmentTypeId}
- sx={{
- border: "3px solid #135fed",
- overflowX: "auto",
- }}
- >
- <CardContent>
- <Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
- {type.equipmentTypeName || "-"}
- </Typography>
-
- <TableContainer component={Paper} sx={{ maxHeight: 440, overflow: 'auto' }}>
- <Table size="small" sx={{ tableLayout: 'fixed', width: '100%' }}>
- <TableHead>
- <TableRow sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'background.paper' }}>
- <TableCell sx={{ width: '15%', minWidth: 150 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Equipment Name and Code")}
- </Typography>
- </TableCell>
- {details.map((d) => (
- <TableCell
- key={d.equipmentDetailId}
- sx={{
- width: `${85 / details.length}%`,
- textAlign: 'left'
- }}
- >
- <Box sx={{ display: "flex", flexDirection: "column" }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {d.equipmentDetailName || "-"}
- </Typography>
- <Typography variant="caption" color="text.secondary">
- {d.equipmentDetailCode || "-"}
- </Typography>
- </Box>
- </TableCell>
- ))}
- </TableRow>
- </TableHead>
- <TableBody>
- {/* 工序 Row */}
- <TableRow>
- <TableCell sx={{ width: '15%', minWidth: 150 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Process")}
- </Typography>
- </TableCell>
- {details.map((d) => (
- <TableCell
- key={d.equipmentDetailId}
- sx={{
- width: `${85 / details.length}%`,
- textAlign: 'left'
- }}
- >
- {d.status === "Processing" ? d.currentProcess?.processName || "-" : "-"}
- </TableCell>
- ))}
- </TableRow>
-
- {/* 狀態 Row - 修改:Processing 时只显示 job order code */}
- <TableRow>
- <TableCell sx={{ width: '15%', minWidth: 150 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Status")}
- </Typography>
- </TableCell>
- {details.map((d) => {
- const chipColor = STATUS_COLORS[d.status] || "default";
- const cp = d.currentProcess;
-
- // Processing 时只显示 job order code,不显示 Chip
- if (d.status === "Processing" && cp?.jobOrderCode) {
- return (
- <TableCell
- key={d.equipmentDetailId}
- sx={{
- width: `${85 / details.length}%`,
- textAlign: 'left'
- }}
- >
- <Typography variant="body2" sx={{ fontWeight: 500 }}>
- {cp.jobOrderCode}
- </Typography>
- </TableCell>
- );
- }
-
- // 其他状态显示 Chip
- return (
- <TableCell
- key={d.equipmentDetailId}
- sx={{
- width: `${85 / details.length}%`,
- textAlign: 'left'
- }}
- >
- <Chip label={t(`${d.status}`)} color={chipColor} size="small" />
- </TableCell>
- );
- })}
- </TableRow>
-
-
-
- {/* 開始時間 Row */}
- <TableRow>
- <TableCell sx={{ width: '15%', minWidth: 150 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Start Time")}
- </Typography>
- </TableCell>
- {details.map((d) => (
- <TableCell
- key={d.equipmentDetailId}
- sx={{
- width: `${85 / details.length}%`,
- textAlign: 'left'
- }}
- >
- {d.status === "Processing"
- ? formatDateTime(d.currentProcess?.startTime)
- : "-"}
- </TableCell>
- ))}
- </TableRow>
-
- {/* 預計完成時間 Row */}
- <TableRow>
- <TableCell sx={{ width: '15%', minWidth: 150 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("預計完成時間")}
- </Typography>
- </TableCell>
- {details.map((d) => (
- <TableCell
- key={d.equipmentDetailId}
- sx={{
- width: `${85 / details.length}%`,
- textAlign: 'left'
- }}
- >
- {d.status === "Processing"
- ? calculateEstimatedCompletionTime(
- d.currentProcess?.startTime,
- d.currentProcess?.processingTime
- )
- : "-"}
- </TableCell>
- ))}
- </TableRow>
-
- {/* 剩餘時間 Row */}
- <TableRow>
- <TableCell sx={{ width: '15%', minWidth: 150 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Remaining Time (min)")}
- </Typography>
- </TableCell>
- {details.map((d) => (
- <TableCell
- key={d.equipmentDetailId}
- sx={{
- width: `${85 / details.length}%`,
- textAlign: 'left'
- }}
- >
- {d.status === "Processing"
- ? calculateRemainingTime(
- d.currentProcess?.startTime,
- d.currentProcess?.processingTime
- )
- : "-"}
- </TableCell>
- ))}
- </TableRow>
- </TableBody>
- </Table>
- </TableContainer>
- </CardContent>
- </Card>
- );
- })}
- </Box>
- )}
- </Box>
- );
- };
-
- export default EquipmentStatusDashboard;
|