|
- "use client";
-
- import React, { useState, useEffect, useCallback, useRef } from 'react';
-
- import {
- Box,
- Typography,
- Card,
- CardContent,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- CircularProgress,
- Stack
- } from '@mui/material';
- import { useTranslation } from 'react-i18next';
- import dayjs from 'dayjs';
- import { fetchJobProcessStatus, JobProcessStatusResponse } from '@/app/api/jo/actions';
- import { arrayToDayjs } from '@/app/utils/formatUtil';
- import { FormControl, Select, MenuItem } from "@mui/material";
- const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes
-
- const JobProcessStatus: React.FC = () => {
- const { t } = useTranslation(["common", "jo"]);
- const [data, setData] = useState<JobProcessStatusResponse[]>([]);
- const [loading, setLoading] = useState<boolean>(true);
- const refreshCountRef = useRef<number>(0);
- const [currentTime, setCurrentTime] = useState(dayjs());
- const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD"));
-
- // Update current time every second for countdown
- useEffect(() => {
- const timer = setInterval(() => {
- setCurrentTime(dayjs());
- }, 1000);
- return () => clearInterval(timer);
- }, []);
-
- const loadData = useCallback(async () => {
- setLoading(true);
- try {
- const result = await fetchJobProcessStatus(selectedDate);
- setData(result);
- refreshCountRef.current += 1;
- } catch (error) {
- console.error('Error fetching job process status:', error);
- setData([]);
- } finally {
- setLoading(false);
- }
- }, [selectedDate]);
-
- useEffect(() => {
- loadData();
- const interval = setInterval(() => {
- loadData();
- }, REFRESH_INTERVAL);
- return () => clearInterval(interval);
- }, [loadData]);
-
- const formatTime = (timeData: any): string => {
- if (!timeData) return '-'; // 改为返回 '-' 而不是 'N/A'
-
- // Handle array format [year, month, day, hour, minute, second]
- if (Array.isArray(timeData)) {
- try {
- const parsed = arrayToDayjs(timeData, true);
- if (parsed.isValid()) {
- return parsed.format('HH:mm');
- }
- } catch (error) {
- console.error('Error parsing array time:', error);
- }
- }
-
- // Handle LocalDateTime ISO string format (e.g., "2026-01-09T18:01:54")
- if (typeof timeData === 'string') {
- const parsed = dayjs(timeData);
- if (parsed.isValid()) {
- return parsed.format('HH:mm');
- }
- }
-
- return '-';
- };
-
- const calculateRemainingTime = (planEndTime: any, processingTime: number | null, setupTime: number | null, changeoverTime: number | null): string => {
- if (!planEndTime) return '-';
-
- let endTime: dayjs.Dayjs;
-
- // Handle array format [year, month, day, hour, minute, second]
- // Use arrayToDayjs for consistency with other parts of the codebase
- if (Array.isArray(planEndTime)) {
- try {
- endTime = arrayToDayjs(planEndTime, true);
- console.log('Parsed planEndTime array:', {
- array: planEndTime,
- parsed: endTime.format('YYYY-MM-DD HH:mm:ss'),
- isValid: endTime.isValid()
- });
- } catch (error) {
- console.error('Error parsing array planEndTime:', error);
- return '-';
- }
- } else if (typeof planEndTime === 'string') {
- endTime = dayjs(planEndTime);
- console.log('Parsed planEndTime string:', {
- string: planEndTime,
- parsed: endTime.format('YYYY-MM-DD HH:mm:ss'),
- isValid: endTime.isValid()
- });
- } else {
- return '-';
- }
-
- if (!endTime.isValid()) {
- console.error('Invalid endTime:', planEndTime);
- return '-';
- }
-
- const diff = endTime.diff(currentTime, 'minute');
- console.log('Remaining time calculation:', {
- endTime: endTime.format('YYYY-MM-DD HH:mm:ss'),
- currentTime: currentTime.format('YYYY-MM-DD HH:mm:ss'),
- diffMinutes: diff
- });
-
- // If the planned end time is in the past, show 0 (or you could show negative time)
- if (diff < 0) return '0';
-
- const hours = Math.floor(diff / 60);
- const minutes = diff % 60;
- return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
- };
-
- const calculateWaitTime = (
- currentProcessEndTime: any,
- nextProcessStartTime: any,
- isLastProcess: boolean
- ): string => {
- if (isLastProcess) return '-';
- if (!currentProcessEndTime) return '-';
- if (nextProcessStartTime) return '0'; // Next process has started, stop counting
-
- let endTime: dayjs.Dayjs;
-
- // Handle array format
- if (Array.isArray(currentProcessEndTime)) {
- try {
- endTime = arrayToDayjs(currentProcessEndTime, true);
- } catch (error) {
- console.error('Error parsing array endTime:', error);
- return '-';
- }
- } else if (typeof currentProcessEndTime === 'string') {
- endTime = dayjs(currentProcessEndTime);
- } else {
- return '-';
- }
-
- if (!endTime.isValid()) return '-';
-
- const diff = currentTime.diff(endTime, 'minute');
- return diff > 0 ? diff.toString() : '0';
- };
-
- return (
- <Card sx={{ mb: 2 }}>
- <CardContent>
- {/* Title */}
- <Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
- {t("Job Process Status 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")}>今天</MenuItem>
- <MenuItem value={dayjs().subtract(1, "day").format("YYYY-MM-DD")}>昨天</MenuItem>
- <MenuItem value={dayjs().subtract(2, "day").format("YYYY-MM-DD")}>前天</MenuItem>
- </Select>
- </FormControl>
- </Stack>
- <Box sx={{ mt: 2 }}>
- {loading ? (
- <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
- <CircularProgress />
- </Box>
- ) : (
- <TableContainer
- component={Paper}
- sx={{
- border: '3px solid #135fed',
- overflowX: 'auto', // 关键:允许横向滚动
- }}
- >
- <Table size="small" sx={{ minWidth: 1800 }}>
- <TableHead>
- <TableRow>
- <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Job Order No.")}
- </Typography>
- </TableCell>
- <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("FG / WIP Item")}
- </Typography>
- </TableCell>
- <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Production Time Remaining")}
- </Typography>
- </TableCell>
-
- </TableRow>
- <TableRow>
- {Array.from({ length: 16 }, (_, i) => i + 1).map((num) => (
- <TableCell key={num} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Process")} {num}
- </Typography>
- </TableCell>
- ))}
- </TableRow>
- <TableRow>
- {Array.from({ length: 16 }, (_, i) => i + 1).map((num) => (
- <TableCell key={num} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}>
- <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
- <Typography variant="caption" sx={{ fontWeight: 600 }}>
- {t("Start")}
- </Typography>
- <Typography variant="caption" sx={{ fontWeight: 600 }}>
- {t("Finish")}
- </Typography>
- <Typography variant="caption" sx={{ fontWeight: 600 }}>
- {t("Wait Time [minutes]")}
- </Typography>
- </Box>
- </TableCell>
- ))}
- </TableRow>
- </TableHead>
- <TableBody>
- {data.length === 0 ? (
- <TableRow>
- <TableCell colSpan={9} align="center" sx={{ padding: '20px' }}>
- {t("No data available")}
- </TableCell>
- </TableRow>
- ) : (
- data.map((row) => (
- <TableRow key={row.jobOrderId}>
- <TableCell sx={{ padding: '16px 20px' }}>
- {row.jobOrderCode || '-'}
- </TableCell>
- <TableCell sx={{ padding: '16px 20px' }}>
- <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>{row.itemCode || '-'}</Box>
- <Box>{row.itemName || '-'}</Box>
- </TableCell>
- <TableCell sx={{ padding: '16px 20px' }}>
-
- {row.status === 'pending' ? '-' : calculateRemainingTime(row.planEndTime, row.processingTime, row.setupTime, row.changeoverTime)}
- </TableCell>
- {row.processes.map((process, index) => {
- const isLastProcess = index === row.processes.length - 1 ||
- !row.processes.slice(index + 1).some(p => p.isRequired);
- const nextProcess = index < row.processes.length - 1 ? row.processes[index + 1] : null;
- const waitTime = calculateWaitTime(
- process.endTime,
- nextProcess?.startTime,
- isLastProcess
- );
-
- // 如果工序不是必需的,只显示一个 N/A
- if (!process.isRequired) {
- return (
- <TableCell key={index} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}>
- <Typography variant="body2">
- N/A
- </Typography>
- </TableCell>
- );
- }
- const label = [
- process.processName,
- process.equipmentName,
- process.equipmentDetailName ? `-${process.equipmentDetailName}` : "",
- ].filter(Boolean).join(" ");
- // 如果工序是必需的,显示三行(Start、Finish、Wait Time)
- return (
- <TableCell key={index} align="center" 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 }}>
- {formatTime(process.startTime)}
- </Typography>
- <Typography variant="body2" sx={{ py: 0.5 }}>
- {formatTime(process.endTime)}
- </Typography>
- <Typography variant="body2" sx={{
- color: waitTime !== '-' && parseInt(waitTime) > 0 ? 'warning.main' : 'text.primary',
- py: 0.5
- }}>
- {waitTime}
- </Typography>
- </Box>
- </TableCell>
- );
- })}
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </TableContainer>
- )}
- </Box>
-
-
- </CardContent>
- </Card>
- );
- };
-
- export default JobProcessStatus;
|