|
- "use client";
-
- import React, { useState, useEffect, useCallback, useRef } from "react";
- import {
- Box,
- Card,
- CardContent,
- CircularProgress,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- Typography,
- FormControl,
- Select,
- MenuItem,
- Stack
- } from "@mui/material";
- import { useTranslation } from "react-i18next";
- import dayjs from "dayjs";
- import { fetchOperatorKpi, OperatorKpiResponse, OperatorKpiProcessInfo } from "@/app/api/jo/actions";
- import { arrayToDayjs } from "@/app/utils/formatUtil";
-
- const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 分鐘
-
- const OperatorKpiDashboard: React.FC = () => {
- const { t } = useTranslation(["common", "jo"]);
- const [data, setData] = useState<OperatorKpiResponse[]>([]);
- 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 "-";
-
- if (Array.isArray(timeData)) {
- try {
- const parsed = arrayToDayjs(timeData, true);
- if (parsed.isValid()) {
- return parsed.format("HH:mm");
- }
- } catch (e) {
- console.error("Error parsing time array:", e);
- }
- }
-
- if (typeof timeData === "string") {
- const parsed = dayjs(timeData);
- if (parsed.isValid()) {
- return parsed.format("HH:mm");
- }
- }
-
- return "-";
- };
-
- const formatMinutesToHHmm = (minutes: number): string => {
- if (!minutes || minutes <= 0) return "00:00";
- const hours = Math.floor(minutes / 60);
- const mins = minutes % 60;
- return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
- };
-
- const loadData = useCallback(async () => {
- setLoading(true);
- try {
- const result = await fetchOperatorKpi(selectedDate);
- setData(result);
- setLastDataRefreshTime(dayjs());
- refreshCountRef.current += 1;
- } catch (error) {
- console.error("Error fetching operator KPI:", error);
- setData([]);
- } finally {
- setLoading(false);
- }
- }, [selectedDate]);
-
- useEffect(() => {
- loadData();
- const interval = setInterval(() => {
- loadData();
- }, REFRESH_INTERVAL);
- 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 (
- <Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
- -
- </Typography>
- );
- }
-
- // 只顯示目前一個處理中的工序(樣式比照 Excel:欄位名稱縱向排列)
- const p = processes[0];
- const jobOrder = p.jobOrderCode ? `[${p.jobOrderCode}]` : "-";
- const itemInfo = p.itemCode && p.itemName
- ? `${p.itemCode} - ${p.itemName}`
- : p.itemCode || p.itemName || "-";
-
- // 格式化所需時間(分鐘轉換為 HH:mm)
- const formatRequiredTime = (minutes: number | null | undefined): string => {
- if (!minutes || minutes <= 0) return "-";
- const hours = Math.floor(minutes / 60);
- const mins = minutes % 60;
- return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
- };
-
- // 計算預計完成時間
- const calculateEstimatedCompletionTime = (): string => {
- if (!p.startTime || !p.processingTime || p.processingTime <= 0) return "-";
-
- try {
- const start = arrayToDayjs(p.startTime, true);
- if (!start.isValid()) return "-";
-
- const estimated = start.add(p.processingTime, "minute");
- return estimated.format("HH:mm");
- } catch (e) {
- console.error("Error calculating estimated completion time:", e);
- return "-";
- }
- };
-
- return (
- <>
- <Typography variant="body2" sx={{ lineHeight: 1.6 }}>
- {t("Job Order and Product")}: {jobOrder} {itemInfo}
- </Typography>
- <Typography variant="body2" sx={{ lineHeight: 1.6 }}>
- {t("Process")}: {p.processName || "-"}
- </Typography>
- <Typography variant="body2" sx={{ lineHeight: 1.6 }}>
- {t("Start Time")}: {formatTime(p.startTime)}
- </Typography>
- <Typography variant="body2" sx={{ lineHeight: 1.6 }}>
- {t("Required Time")}: {formatRequiredTime(p.processingTime)}
- </Typography>
- <Typography variant="body2" sx={{ lineHeight: 1.6 }}>
- {t("Estimated Completion Time")}: {calculateEstimatedCompletionTime()}
- </Typography>
- </>
- );
- };
-
- return (
- <Card sx={{ mb: 2 }}>
- <CardContent>
- {/* Title */}
- <Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
- {t("Operator KPI Dashboard")}
- </Typography>
-
- {/* Filters */}
- <Stack direction="row" spacing={2} sx={{ mb: 3 }}>
- <FormControl size="small" sx={{ minWidth: 160 }}>
- <Select
- value={selectedDate}
- onChange={(e) => setSelectedDate(e.target.value)}
- >
- <MenuItem value={dayjs().format("YYYY-MM-DD")}>{t("Today")}</MenuItem>
- <MenuItem value={dayjs().subtract(1, "day").format("YYYY-MM-DD")}>{t("Yesterday")}</MenuItem>
- <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 ? (
- <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
- <CircularProgress />
- </Box>
- ) : (
- <TableContainer sx={{ border: "3px solid #135fed", overflowX: "auto", maxHeight: 440, overflow: 'auto' }}
- component={Paper}
- >
- <Table size="small" sx={{ minWidth: 800 }}>
- <TableHead>
- <TableRow
- sx={{
- bgcolor: "#424242",
- "& th": {
- borderBottom: "none",
- py: 1.5,
- position: 'sticky', top: 0, zIndex: 1
- },
- }}
- >
- <TableCell align="right" sx={{ width: 80 }}>
- <Typography
- variant="subtitle2"
- sx={{
- fontWeight: 600,
- //color: "#ffffff",
- }}
- >
- {t("No.")}
- </Typography>
- </TableCell>
- <TableCell sx={{ minWidth: 280 }}>
- <Typography
- variant="subtitle2"
- sx={{
- fontWeight: 600,
- //color: "#ffffff",
- }}
- >
- {t("Operator")}
- </Typography>
- </TableCell>
- <TableCell sx={{ minWidth: 300 }}>
- <Typography
- variant="subtitle2"
- sx={{
- fontWeight: 600,
- //color: "#ffffff",
- }}
- >
- {t("Job Details")}
- </Typography>
- </TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {data.length === 0 ? (
- <TableRow>
- <TableCell colSpan={4} align="center">
- {t("No data available")}
- </TableCell>
- </TableRow>
- ) : (
- data.map((row, index) => {
- const jobOrderCount = row.totalJobOrderCount || 0;
-
- return (
- <TableRow
- key={row.operatorId}
- sx={{
- "&:hover": {
- bgcolor: "#f9f9f9",
- },
- "& td": {
- borderBottom: "1px solid #e0e0e0",
- py: 2,
- verticalAlign: "top",
- },
- }}
- >
- <TableCell align="right"
- sx={{
- width: 80,
- fontWeight: 500,
- verticalAlign: "top",
- }}
- >
- {index + 1}
- </TableCell>
- <TableCell
- sx={{
- minWidth: 280,
- padding: 0,
- verticalAlign: "top",
- height: "100%",
- }}
- >
- <Box
- sx={{
- p: 1.5,
- display: "flex",
- flexDirection: "column",
- gap: 0.75,
- bgcolor: "#f5f5f5",
- border: "1px solid #e0e0e0",
- borderRadius: 1.5,
- boxSizing: "border-box",
- height: "180px",
-
- }}
- >
- <Typography variant="body2" sx={{ lineHeight: 1.6 }}>
- {t("Operator Name & No.")}:{" "}
- <Box component="span" sx={{ fontWeight: 500 }}>
- {row.operatorName || "-"}{" "}
- {row.staffNo ? `(${row.staffNo})` : ""}
- </Box>
- </Typography>
- <Typography variant="body2" sx={{ lineHeight: 1.6 }}>
- {t("Count of Job Orders")}:{" "}
- <Box component="span" sx={{ fontWeight: 500 }}>
- {jobOrderCount}
- </Box>
- </Typography>
- <Typography variant="body2" sx={{ lineHeight: 1.6 }}>
- {t("Total Processing Time")}:{" "}
- <Box component="span" sx={{ fontWeight: 500 }}>
- {formatMinutesToHHmm(row.totalProcessingMinutes || 0)}
- </Box>
- </Typography>
- </Box>
- </TableCell>
- <TableCell
- sx={{
- minWidth: 300,
- padding: 0,
- verticalAlign: "top",
- height: "100%",
- }}
- >
- <Box
- sx={{
- p: 1.5,
- display: "flex",
- flexDirection: "column",
- gap: 0.75,
- bgcolor: "#f5f5f5",
- border: "1px solid #e0e0e0",
- borderRadius: 1.5,
- boxSizing: "border-box",
- height: "180px",
- }}
- >
- {renderCurrentProcesses(row.currentProcesses)}
- </Box>
- </TableCell>
- </TableRow>
- );
- })
- )}
- </TableBody>
- </Table>
- </TableContainer>
- )}
- </CardContent>
- </Card>
- );
- };
-
- export default OperatorKpiDashboard;
|