|
|
@@ -1,6 +1,6 @@ |
|
|
"use client"; |
|
|
"use client"; |
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback, useRef } from 'react'; |
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from "react"; |
|
|
|
|
|
|
|
|
import { |
|
|
import { |
|
|
Box, |
|
|
Box, |
|
|
@@ -15,174 +15,393 @@ import { |
|
|
TableRow, |
|
|
TableRow, |
|
|
Paper, |
|
|
Paper, |
|
|
CircularProgress, |
|
|
CircularProgress, |
|
|
Stack |
|
|
|
|
|
} from '@mui/material'; |
|
|
|
|
|
import { useTranslation } from 'react-i18next'; |
|
|
|
|
|
import dayjs from 'dayjs'; |
|
|
|
|
|
import type { Dayjs } from 'dayjs'; |
|
|
|
|
|
import { fetchJobProcessStatus, JobProcessStatusResponse } from '@/app/api/jo/actions'; |
|
|
|
|
|
import { arrayToDayjs } from '@/app/utils/formatUtil'; |
|
|
|
|
|
|
|
|
Stack, |
|
|
|
|
|
Button, |
|
|
|
|
|
ToggleButton, |
|
|
|
|
|
ToggleButtonGroup, |
|
|
|
|
|
FormControl, |
|
|
|
|
|
InputLabel, |
|
|
|
|
|
Select, |
|
|
|
|
|
MenuItem, |
|
|
|
|
|
} from "@mui/material"; |
|
|
|
|
|
|
|
|
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
|
|
|
|
|
|
|
|
import dayjs from "dayjs"; |
|
|
|
|
|
|
|
|
|
|
|
import type { Dayjs } from "dayjs"; |
|
|
|
|
|
|
|
|
|
|
|
import { |
|
|
|
|
|
fetchJobProcessStatus, |
|
|
|
|
|
JobProcessStatusResponse, |
|
|
|
|
|
} from "@/app/api/jo/actions"; |
|
|
|
|
|
|
|
|
|
|
|
import { arrayToDayjs } from "@/app/utils/formatUtil"; |
|
|
|
|
|
|
|
|
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; |
|
|
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; |
|
|
|
|
|
|
|
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; |
|
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; |
|
|
|
|
|
|
|
|
const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes |
|
|
const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes |
|
|
|
|
|
|
|
|
|
|
|
/** 每頁顯示工序數(4~6);固定 5 以平衡可讀性與寬度 */ |
|
|
|
|
|
|
|
|
|
|
|
const PROCESSES_PER_PAGE = 5; |
|
|
|
|
|
|
|
|
|
|
|
/** 與後端 ProductProcessStatus.value 一致(用於篩選查詢) */ |
|
|
|
|
|
const PRODUCT_PROCESS_STATUS_FILTER_VALUES = [ |
|
|
|
|
|
"pending", |
|
|
|
|
|
"in_progress", |
|
|
|
|
|
"stopped", |
|
|
|
|
|
"completed", |
|
|
|
|
|
"cancelled", |
|
|
|
|
|
] as const; |
|
|
|
|
|
|
|
|
|
|
|
/** 工單狀態儀表板專用文案(與全域 pending/in_progress 等區隔) */ |
|
|
|
|
|
const JOB_DASHBOARD_PP_STATUS_I18N: Record< |
|
|
|
|
|
(typeof PRODUCT_PROCESS_STATUS_FILTER_VALUES)[number], |
|
|
|
|
|
string |
|
|
|
|
|
> = { |
|
|
|
|
|
pending: "Job dashboard PP status: pending", |
|
|
|
|
|
in_progress: "Job dashboard PP status: in_progress", |
|
|
|
|
|
stopped: "Job dashboard PP status: stopped", |
|
|
|
|
|
completed: "Job dashboard PP status: completed", |
|
|
|
|
|
cancelled: "Job dashboard PP status: cancelled", |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** 工序格僅顯示一類資訊(單行) */ |
|
|
|
|
|
type ProcessCellDetailMode = "time" | "processName" | "equipment" | "handler"; |
|
|
|
|
|
|
|
|
|
|
|
function parseTimeToDayjs(timeData: unknown): Dayjs | null { |
|
|
|
|
|
if (timeData == null) return null; |
|
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(timeData)) { |
|
|
|
|
|
try { |
|
|
|
|
|
const parsed = arrayToDayjs(timeData, true); |
|
|
|
|
|
|
|
|
|
|
|
return parsed.isValid() ? parsed : null; |
|
|
|
|
|
} catch { |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (typeof timeData === "string") { |
|
|
|
|
|
const parsed = dayjs(timeData); |
|
|
|
|
|
|
|
|
|
|
|
return parsed.isValid() ? parsed : null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const JobProcessStatus: React.FC = () => { |
|
|
const JobProcessStatus: React.FC = () => { |
|
|
const { t } = useTranslation(["common", "jo"]); |
|
|
const { t } = useTranslation(["common", "jo"]); |
|
|
|
|
|
|
|
|
const [data, setData] = useState<JobProcessStatusResponse[]>([]); |
|
|
const [data, setData] = useState<JobProcessStatusResponse[]>([]); |
|
|
|
|
|
|
|
|
const [loading, setLoading] = useState<boolean>(true); |
|
|
const [loading, setLoading] = useState<boolean>(true); |
|
|
const refreshCountRef = useRef<number>(0); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 每秒更新一次:剩餘時間、進行中耗時(與現有時鐘共用,成本可忽略) */ |
|
|
|
|
|
|
|
|
const [currentTime, setCurrentTime] = useState(dayjs()); |
|
|
const [currentTime, setCurrentTime] = useState(dayjs()); |
|
|
|
|
|
|
|
|
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs()); |
|
|
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs()); |
|
|
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<Dayjs | null>(null); |
|
|
|
|
|
|
|
|
|
|
|
// Update current time every second for countdown |
|
|
|
|
|
|
|
|
/** 後端 ProductProcessStatus,空字串 = 不篩選 */ |
|
|
|
|
|
const [productProcessStatusFilter, setProductProcessStatusFilter] = |
|
|
|
|
|
useState<string>(""); |
|
|
|
|
|
|
|
|
|
|
|
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<Dayjs | null>( |
|
|
|
|
|
null, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const [processPage, setProcessPage] = useState(0); |
|
|
|
|
|
|
|
|
|
|
|
const [cellDetailMode, setCellDetailMode] = |
|
|
|
|
|
useState<ProcessCellDetailMode>("time"); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
const timer = setInterval(() => { |
|
|
const timer = setInterval(() => { |
|
|
setCurrentTime(dayjs()); |
|
|
setCurrentTime(dayjs()); |
|
|
}, 1000); |
|
|
}, 1000); |
|
|
|
|
|
|
|
|
return () => clearInterval(timer); |
|
|
return () => clearInterval(timer); |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const loadData = useCallback(async () => { |
|
|
const loadData = useCallback(async () => { |
|
|
setLoading(true); |
|
|
setLoading(true); |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
const result = await fetchJobProcessStatus(selectedDate.format("YYYY-MM-DD")); |
|
|
|
|
|
|
|
|
const result = await fetchJobProcessStatus( |
|
|
|
|
|
selectedDate.format("YYYY-MM-DD"), |
|
|
|
|
|
productProcessStatusFilter || undefined, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
setData(result); |
|
|
setData(result); |
|
|
refreshCountRef.current += 1; |
|
|
|
|
|
|
|
|
|
|
|
setLastDataRefreshTime(dayjs()); |
|
|
setLastDataRefreshTime(dayjs()); |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
console.error('Error fetching job process status:', error); |
|
|
|
|
|
|
|
|
console.error("Error fetching job process status:", error); |
|
|
|
|
|
|
|
|
setData([]); |
|
|
setData([]); |
|
|
} finally { |
|
|
} finally { |
|
|
setLoading(false); |
|
|
setLoading(false); |
|
|
} |
|
|
} |
|
|
}, [selectedDate]); |
|
|
|
|
|
|
|
|
}, [selectedDate, productProcessStatusFilter]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
loadData(); |
|
|
loadData(); |
|
|
|
|
|
|
|
|
const interval = setInterval(() => { |
|
|
const interval = setInterval(() => { |
|
|
loadData(); |
|
|
loadData(); |
|
|
}, REFRESH_INTERVAL); |
|
|
}, REFRESH_INTERVAL); |
|
|
|
|
|
|
|
|
return () => clearInterval(interval); |
|
|
return () => clearInterval(interval); |
|
|
}, [loadData]); |
|
|
}, [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); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
setProcessPage(0); |
|
|
|
|
|
}, [selectedDate, productProcessStatusFilter]); |
|
|
|
|
|
|
|
|
|
|
|
const formatTime = (timeData: unknown): string => { |
|
|
|
|
|
const parsed = parseTimeToDayjs(timeData); |
|
|
|
|
|
|
|
|
|
|
|
return parsed ? parsed.format("HH:mm") : "-"; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const formatDurationMs = useCallback( |
|
|
|
|
|
(ms: number): string => { |
|
|
|
|
|
if (ms < 0) ms = 0; |
|
|
|
|
|
|
|
|
|
|
|
const totalSeconds = Math.floor(ms / 1000); |
|
|
|
|
|
|
|
|
|
|
|
const hours = Math.floor(totalSeconds / 3600); |
|
|
|
|
|
|
|
|
|
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60); |
|
|
|
|
|
|
|
|
|
|
|
const seconds = totalSeconds % 60; |
|
|
|
|
|
|
|
|
|
|
|
const parts: string[] = []; |
|
|
|
|
|
|
|
|
|
|
|
if (hours > 0) { |
|
|
|
|
|
parts.push(t("Duration hours", { count: hours })); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (minutes > 0) { |
|
|
|
|
|
parts.push(t("Duration minutes", { count: minutes })); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 (seconds > 0 || parts.length === 0) { |
|
|
|
|
|
parts.push(t("Duration seconds", { count: seconds })); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return parts.join(" "); |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
[t], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const calculateRemainingTime = (planEndTime: unknown): string => { |
|
|
|
|
|
if (!planEndTime) return "-"; |
|
|
|
|
|
|
|
|
|
|
|
let endTime: Dayjs; |
|
|
|
|
|
|
|
|
if (Array.isArray(planEndTime)) { |
|
|
if (Array.isArray(planEndTime)) { |
|
|
try { |
|
|
try { |
|
|
endTime = arrayToDayjs(planEndTime, true); |
|
|
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 '-'; |
|
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
return "-"; |
|
|
} |
|
|
} |
|
|
} else if (typeof planEndTime === 'string') { |
|
|
|
|
|
|
|
|
} else if (typeof planEndTime === "string") { |
|
|
endTime = dayjs(planEndTime); |
|
|
endTime = dayjs(planEndTime); |
|
|
console.log('Parsed planEndTime string:', { |
|
|
|
|
|
string: planEndTime, |
|
|
|
|
|
parsed: endTime.format('YYYY-MM-DD HH:mm:ss'), |
|
|
|
|
|
isValid: endTime.isValid() |
|
|
|
|
|
}); |
|
|
|
|
|
} else { |
|
|
} else { |
|
|
return '-'; |
|
|
|
|
|
|
|
|
return "-"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!endTime.isValid()) { |
|
|
if (!endTime.isValid()) { |
|
|
console.error('Invalid endTime:', planEndTime); |
|
|
|
|
|
return '-'; |
|
|
|
|
|
|
|
|
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 diff = endTime.diff(currentTime, "minute"); |
|
|
|
|
|
|
|
|
|
|
|
if (diff < 0) return "0"; |
|
|
|
|
|
|
|
|
const hours = Math.floor(diff / 60); |
|
|
const hours = Math.floor(diff / 60); |
|
|
|
|
|
|
|
|
const minutes = diff % 60; |
|
|
const minutes = diff % 60; |
|
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 '-'; |
|
|
|
|
|
|
|
|
const maxProcessSlots = useMemo(() => { |
|
|
|
|
|
if (data.length === 0) return 0; |
|
|
|
|
|
|
|
|
|
|
|
return Math.max( |
|
|
|
|
|
0, |
|
|
|
|
|
...data.map((d) => d.processes?.length ?? 0), |
|
|
|
|
|
); |
|
|
|
|
|
}, [data]); |
|
|
|
|
|
|
|
|
|
|
|
const totalPages = |
|
|
|
|
|
maxProcessSlots > 0 ? Math.ceil(maxProcessSlots / PROCESSES_PER_PAGE) : 0; |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (totalPages === 0) { |
|
|
|
|
|
setProcessPage(0); |
|
|
|
|
|
|
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setProcessPage((p) => Math.min(p, totalPages - 1)); |
|
|
|
|
|
}, [totalPages]); |
|
|
|
|
|
|
|
|
|
|
|
const startSlotIndex = processPage * PROCESSES_PER_PAGE; |
|
|
|
|
|
|
|
|
|
|
|
const visibleCount = |
|
|
|
|
|
maxProcessSlots > 0 |
|
|
|
|
|
? Math.min(PROCESSES_PER_PAGE, maxProcessSlots - startSlotIndex) |
|
|
|
|
|
: 0; |
|
|
|
|
|
|
|
|
|
|
|
const rangeFrom = maxProcessSlots > 0 ? startSlotIndex + 1 : 0; |
|
|
|
|
|
|
|
|
|
|
|
const rangeTo = maxProcessSlots > 0 ? startSlotIndex + visibleCount : 0; |
|
|
|
|
|
|
|
|
|
|
|
const renderProcessCell = (row: JobProcessStatusResponse, index: number) => { |
|
|
|
|
|
const process = row.processes[index]; |
|
|
|
|
|
|
|
|
|
|
|
if (!process) { |
|
|
|
|
|
return ( |
|
|
|
|
|
<TableCell |
|
|
|
|
|
key={`empty-${row.jobOrderId}-${index}`} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
padding: "10px 8px", |
|
|
|
|
|
verticalAlign: "top", |
|
|
|
|
|
wordBreak: "break-word", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
|
|
— |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!process.isRequired) { |
|
|
|
|
|
return ( |
|
|
|
|
|
<TableCell |
|
|
|
|
|
key={index} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
padding: "10px 8px", |
|
|
|
|
|
verticalAlign: "top", |
|
|
|
|
|
wordBreak: "break-word", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<Typography variant="body2">N/A</Typography> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const startD = parseTimeToDayjs(process.startTime); |
|
|
|
|
|
const endD = parseTimeToDayjs(process.endTime); |
|
|
|
|
|
let durationLabel = "-"; |
|
|
|
|
|
if (startD && endD) { |
|
|
|
|
|
durationLabel = formatDurationMs(endD.diff(startD)); |
|
|
|
|
|
} else if (startD && !endD) { |
|
|
|
|
|
durationLabel = formatDurationMs(currentTime.diff(startD)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const equipmentLine = [ |
|
|
|
|
|
process.equipmentName, |
|
|
|
|
|
process.equipmentDetailName ? `-${process.equipmentDetailName}` : "", |
|
|
|
|
|
] |
|
|
|
|
|
.filter((s) => s != null && String(s).length > 0) |
|
|
|
|
|
.join(""); |
|
|
|
|
|
|
|
|
|
|
|
let lineText = "—"; |
|
|
|
|
|
let lineColor: "text.primary" | "info.main" = "text.primary"; |
|
|
|
|
|
|
|
|
|
|
|
switch (cellDetailMode) { |
|
|
|
|
|
case "time": { |
|
|
|
|
|
const startStr = formatTime(process.startTime); |
|
|
|
|
|
const endStr = formatTime(process.endTime); |
|
|
|
|
|
if (startD && endD) { |
|
|
|
|
|
lineText = `${startStr} – ${endStr} · ${durationLabel}`; |
|
|
|
|
|
} else if (startD && !endD) { |
|
|
|
|
|
lineText = `${startStr} – … · ${durationLabel} · ${t("In progress")}`; |
|
|
|
|
|
lineColor = "info.main"; |
|
|
|
|
|
} else { |
|
|
|
|
|
lineText = "—"; |
|
|
|
|
|
} |
|
|
|
|
|
break; |
|
|
} |
|
|
} |
|
|
} else if (typeof currentProcessEndTime === 'string') { |
|
|
|
|
|
endTime = dayjs(currentProcessEndTime); |
|
|
|
|
|
} else { |
|
|
|
|
|
return '-'; |
|
|
|
|
|
|
|
|
case "processName": |
|
|
|
|
|
lineText = process.processName?.trim() || "—"; |
|
|
|
|
|
break; |
|
|
|
|
|
case "equipment": |
|
|
|
|
|
lineText = equipmentLine.trim().length ? equipmentLine : "—"; |
|
|
|
|
|
break; |
|
|
|
|
|
case "handler": { |
|
|
|
|
|
const h = process.handlerName?.trim(); |
|
|
|
|
|
lineText = h?.length ? h : "—"; |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
default: |
|
|
|
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!endTime.isValid()) return '-'; |
|
|
|
|
|
|
|
|
|
|
|
const diff = currentTime.diff(endTime, 'minute'); |
|
|
|
|
|
return diff > 0 ? diff.toString() : '0'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isTimeMode = cellDetailMode === "time"; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<TableCell |
|
|
|
|
|
key={index} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
padding: "10px 8px", |
|
|
|
|
|
verticalAlign: "top", |
|
|
|
|
|
// 時間欄內容較長,允許換行並顯示完整;其他模式維持單行省略 |
|
|
|
|
|
...(isTimeMode ? { minWidth: 0 } : { maxWidth: 0 }), |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="body2" |
|
|
|
|
|
sx={{ |
|
|
|
|
|
color: lineColor, |
|
|
|
|
|
...(isTimeMode |
|
|
|
|
|
? { |
|
|
|
|
|
whiteSpace: "normal", |
|
|
|
|
|
wordBreak: "break-word", |
|
|
|
|
|
overflow: "visible", |
|
|
|
|
|
lineHeight: 1.35, |
|
|
|
|
|
} |
|
|
|
|
|
: { |
|
|
|
|
|
overflow: "hidden", |
|
|
|
|
|
textOverflow: "ellipsis", |
|
|
|
|
|
whiteSpace: "nowrap", |
|
|
|
|
|
}), |
|
|
|
|
|
}} |
|
|
|
|
|
title={!isTimeMode && lineText !== "—" ? lineText : undefined} |
|
|
|
|
|
> |
|
|
|
|
|
{lineText} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<Card sx={{ mb: 2 }}> |
|
|
<Card sx={{ mb: 2 }}> |
|
|
<CardContent> |
|
|
<CardContent> |
|
|
{/* Title */} |
|
|
|
|
|
<Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}> |
|
|
<Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}> |
|
|
{t("Job Process Status Dashboard")} |
|
|
{t("Job Process Status Dashboard")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
|
|
|
|
|
|
{/* Filters */} |
|
|
|
|
|
<Stack direction="row" spacing={2} sx={{ mb: 3 }}> |
|
|
|
|
|
|
|
|
<Stack |
|
|
|
|
|
direction="row" |
|
|
|
|
|
spacing={2} |
|
|
|
|
|
sx={{ mb: 3, flexWrap: "wrap", alignItems: "center", gap: 1 }} |
|
|
|
|
|
> |
|
|
<LocalizationProvider dateAdapter={AdapterDayjs}> |
|
|
<LocalizationProvider dateAdapter={AdapterDayjs}> |
|
|
<DatePicker |
|
|
<DatePicker |
|
|
label={t("Date")} |
|
|
label={t("Date")} |
|
|
@@ -197,145 +416,306 @@ const JobProcessStatus: React.FC = () => { |
|
|
/> |
|
|
/> |
|
|
</LocalizationProvider> |
|
|
</LocalizationProvider> |
|
|
|
|
|
|
|
|
|
|
|
<FormControl size="small" sx={{ minWidth: 200 }}> |
|
|
|
|
|
<InputLabel id="jp-pp-status-filter-label"> |
|
|
|
|
|
{t("Product process status")} |
|
|
|
|
|
</InputLabel> |
|
|
|
|
|
<Select |
|
|
|
|
|
labelId="jp-pp-status-filter-label" |
|
|
|
|
|
id="jp-pp-status-filter" |
|
|
|
|
|
label={t("Product process status")} |
|
|
|
|
|
value={productProcessStatusFilter} |
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
setProductProcessStatusFilter( |
|
|
|
|
|
typeof e.target.value === "string" ? e.target.value : "", |
|
|
|
|
|
); |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<MenuItem value=""> |
|
|
|
|
|
<em>{t("All")}</em> |
|
|
|
|
|
</MenuItem> |
|
|
|
|
|
{PRODUCT_PROCESS_STATUS_FILTER_VALUES.map((v) => ( |
|
|
|
|
|
<MenuItem key={v} value={v}> |
|
|
|
|
|
{t(JOB_DASHBOARD_PP_STATUS_I18N[v])} |
|
|
|
|
|
</MenuItem> |
|
|
|
|
|
))} |
|
|
|
|
|
</Select> |
|
|
|
|
|
</FormControl> |
|
|
|
|
|
|
|
|
<Box sx={{ flexGrow: 1 }} /> |
|
|
<Box sx={{ flexGrow: 1 }} /> |
|
|
<Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}> |
|
|
|
|
|
<Typography variant="body2" sx={{ color: 'text.secondary' }} suppressHydrationWarning> |
|
|
|
|
|
{t("Now")}: {currentTime.format('HH:mm')} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<Stack direction="row" spacing={2} sx={{ alignSelf: "center" }}> |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="body2" |
|
|
|
|
|
sx={{ color: "text.secondary" }} |
|
|
|
|
|
suppressHydrationWarning |
|
|
|
|
|
> |
|
|
|
|
|
{t("Now")}: {currentTime.format("HH:mm")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}> |
|
|
|
|
|
{t("Auto-refresh every 10 minutes")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<Typography variant="body2" sx={{ color: "text.secondary" }}> |
|
|
|
|
|
{t("Auto-refresh every 10 minutes")} | {t("Last updated")}:{" "} |
|
|
|
|
|
{lastDataRefreshTime |
|
|
|
|
|
? lastDataRefreshTime.format("HH:mm:ss") |
|
|
|
|
|
: "--:--:--"} |
|
|
</Typography> |
|
|
</Typography> |
|
|
</Stack> |
|
|
</Stack> |
|
|
</Stack> |
|
|
</Stack> |
|
|
|
|
|
|
|
|
|
|
|
<Stack |
|
|
|
|
|
direction="row" |
|
|
|
|
|
alignItems="center" |
|
|
|
|
|
spacing={1} |
|
|
|
|
|
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }} |
|
|
|
|
|
> |
|
|
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
|
|
{t("Job process detail mode label")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
<ToggleButtonGroup |
|
|
|
|
|
value={cellDetailMode} |
|
|
|
|
|
exclusive |
|
|
|
|
|
size="small" |
|
|
|
|
|
color="primary" |
|
|
|
|
|
onChange={(_, value: ProcessCellDetailMode | null) => { |
|
|
|
|
|
if (value != null) setCellDetailMode(value); |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<ToggleButton value="time"> |
|
|
|
|
|
{t("Job process detail: time")} |
|
|
|
|
|
</ToggleButton> |
|
|
|
|
|
<ToggleButton value="processName"> |
|
|
|
|
|
{t("Job process detail: process name")} |
|
|
|
|
|
</ToggleButton> |
|
|
|
|
|
<ToggleButton value="equipment"> |
|
|
|
|
|
{t("Job process detail: equipment")} |
|
|
|
|
|
</ToggleButton> |
|
|
|
|
|
<ToggleButton value="handler"> |
|
|
|
|
|
{t("Job process detail: handler")} |
|
|
|
|
|
</ToggleButton> |
|
|
|
|
|
</ToggleButtonGroup> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
|
|
|
|
|
|
{maxProcessSlots > 0 && ( |
|
|
|
|
|
<Stack |
|
|
|
|
|
direction="row" |
|
|
|
|
|
spacing={2} |
|
|
|
|
|
alignItems="center" |
|
|
|
|
|
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }} |
|
|
|
|
|
> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
size="small" |
|
|
|
|
|
disabled={processPage <= 0} |
|
|
|
|
|
onClick={() => setProcessPage((p) => Math.max(0, p - 1))} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Previous page")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
|
|
|
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
|
|
{t("Process page summary", { |
|
|
|
|
|
from: rangeFrom, |
|
|
|
|
|
|
|
|
|
|
|
to: rangeTo, |
|
|
|
|
|
|
|
|
|
|
|
total: maxProcessSlots, |
|
|
|
|
|
})} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
size="small" |
|
|
|
|
|
disabled={processPage >= totalPages - 1} |
|
|
|
|
|
onClick={() => |
|
|
|
|
|
setProcessPage((p) => Math.min(totalPages - 1, p + 1)) |
|
|
|
|
|
} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Next page")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
<Box sx={{ mt: 2 }}> |
|
|
<Box sx={{ mt: 2 }}> |
|
|
{loading ? ( |
|
|
{loading ? ( |
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> |
|
|
|
|
|
|
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> |
|
|
<CircularProgress /> |
|
|
<CircularProgress /> |
|
|
</Box> |
|
|
</Box> |
|
|
) : ( |
|
|
) : ( |
|
|
<TableContainer |
|
|
<TableContainer |
|
|
component={Paper} |
|
|
|
|
|
|
|
|
component={Paper} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
border: "3px solid #135fed", |
|
|
|
|
|
|
|
|
|
|
|
maxHeight: 540, |
|
|
|
|
|
|
|
|
|
|
|
overflow: "auto", |
|
|
|
|
|
|
|
|
|
|
|
overflowX: "hidden", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<Table |
|
|
|
|
|
size="small" |
|
|
sx={{ |
|
|
sx={{ |
|
|
border: '3px solid #135fed', |
|
|
|
|
|
overflowX: 'auto', |
|
|
|
|
|
maxHeight: 540, |
|
|
|
|
|
overflow: 'auto' |
|
|
|
|
|
|
|
|
width: "100%", |
|
|
|
|
|
|
|
|
|
|
|
tableLayout: "fixed", |
|
|
}} |
|
|
}} |
|
|
> |
|
|
> |
|
|
<Table size="small" sx={{ minWidth: 1800 }}> |
|
|
|
|
|
<TableHead sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'grey.100' }}> |
|
|
|
|
|
<TableRow sx={{ backgroundColor: 'grey.100' }}> |
|
|
|
|
|
<TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> |
|
|
|
|
|
|
|
|
<TableHead |
|
|
|
|
|
sx={{ |
|
|
|
|
|
position: "sticky", |
|
|
|
|
|
|
|
|
|
|
|
top: 0, |
|
|
|
|
|
|
|
|
|
|
|
zIndex: 1, |
|
|
|
|
|
|
|
|
|
|
|
backgroundColor: "grey.100", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<TableRow sx={{ backgroundColor: "grey.100" }}> |
|
|
|
|
|
<TableCell |
|
|
|
|
|
rowSpan={2} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
padding: "12px 10px", |
|
|
|
|
|
|
|
|
|
|
|
width: "12%", |
|
|
|
|
|
|
|
|
|
|
|
verticalAlign: "middle", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}> |
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}> |
|
|
{t("Job Order No.")} |
|
|
{t("Job Order No.")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<TableCell |
|
|
|
|
|
rowSpan={2} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
padding: "12px 10px", |
|
|
|
|
|
|
|
|
|
|
|
width: "18%", |
|
|
|
|
|
|
|
|
|
|
|
verticalAlign: "middle", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}> |
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}> |
|
|
{t("FG / WIP Item")} |
|
|
{t("FG / WIP Item")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<TableCell |
|
|
|
|
|
rowSpan={2} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
padding: "12px 10px", |
|
|
|
|
|
|
|
|
|
|
|
width: "10%", |
|
|
|
|
|
|
|
|
|
|
|
verticalAlign: "middle", |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}> |
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}> |
|
|
{t("Production Time Remaining")} |
|
|
{t("Production Time Remaining")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
|
|
|
|
|
|
</TableRow> |
|
|
|
|
|
<TableRow sx={{ backgroundColor: 'grey.100' }}> |
|
|
|
|
|
{Array.from({ length: 16 }, (_, i) => i + 1).map((num) => ( |
|
|
|
|
|
<TableCell key={num} sx={{ padding: '16px 20px', minWidth: 150 }}> |
|
|
|
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}> |
|
|
|
|
|
{t("Process")} {num} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
))} |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
<TableRow sx={{ backgroundColor: 'grey.100' }}> |
|
|
|
|
|
{Array.from({ length: 16 }, (_, i) => i + 1).map((num) => ( |
|
|
|
|
|
<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")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
<Typography variant="caption" sx={{ fontWeight: 600 }}> |
|
|
|
|
|
{t("Finish")} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{maxProcessSlots > 0 && |
|
|
|
|
|
Array.from({ length: visibleCount }, (_, i) => ( |
|
|
|
|
|
<TableCell |
|
|
|
|
|
key={`h1-${startSlotIndex + i}`} |
|
|
|
|
|
sx={{ padding: "10px 8px" }} |
|
|
|
|
|
align="center" |
|
|
|
|
|
> |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="subtitle2" |
|
|
|
|
|
sx={{ fontWeight: 600 }} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Process")} {startSlotIndex + i + 1} |
|
|
</Typography> |
|
|
</Typography> |
|
|
<Typography variant="caption" sx={{ fontWeight: 600 }}> |
|
|
|
|
|
{t("Wait Time [minutes]")} |
|
|
|
|
|
|
|
|
</TableCell> |
|
|
|
|
|
))} |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
|
|
|
|
|
|
<TableRow sx={{ backgroundColor: "grey.100" }}> |
|
|
|
|
|
{maxProcessSlots > 0 && |
|
|
|
|
|
Array.from({ length: visibleCount }, (_, i) => ( |
|
|
|
|
|
<TableCell |
|
|
|
|
|
key={`h3-${startSlotIndex + i}`} |
|
|
|
|
|
sx={{ padding: "10px 8px" }} |
|
|
|
|
|
align="center" |
|
|
|
|
|
> |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="caption" |
|
|
|
|
|
sx={{ fontWeight: 600 }} |
|
|
|
|
|
> |
|
|
|
|
|
{cellDetailMode === "time" |
|
|
|
|
|
? t("Job process detail: time") |
|
|
|
|
|
: cellDetailMode === "processName" |
|
|
|
|
|
? t("Job process detail: process name") |
|
|
|
|
|
: cellDetailMode === "equipment" |
|
|
|
|
|
? t("Job process detail: equipment") |
|
|
|
|
|
: t("Job process detail: handler")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
</Box> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
))} |
|
|
|
|
|
|
|
|
</TableCell> |
|
|
|
|
|
))} |
|
|
</TableRow> |
|
|
</TableRow> |
|
|
</TableHead> |
|
|
</TableHead> |
|
|
|
|
|
|
|
|
<TableBody> |
|
|
<TableBody> |
|
|
{data.length === 0 ? ( |
|
|
{data.length === 0 ? ( |
|
|
<TableRow> |
|
|
<TableRow> |
|
|
<TableCell colSpan={9} align="center" sx={{ padding: '20px' }}> |
|
|
|
|
|
|
|
|
<TableCell |
|
|
|
|
|
colSpan={Math.max(3, 3 + visibleCount)} |
|
|
|
|
|
align="center" |
|
|
|
|
|
sx={{ padding: "20px" }} |
|
|
|
|
|
> |
|
|
|
|
|
{t("No data available")} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
) : maxProcessSlots === 0 ? ( |
|
|
|
|
|
<TableRow> |
|
|
|
|
|
<TableCell |
|
|
|
|
|
colSpan={3} |
|
|
|
|
|
align="center" |
|
|
|
|
|
sx={{ padding: "20px" }} |
|
|
|
|
|
> |
|
|
{t("No data available")} |
|
|
{t("No data available")} |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
</TableRow> |
|
|
</TableRow> |
|
|
) : ( |
|
|
) : ( |
|
|
data.map((row) => ( |
|
|
data.map((row) => ( |
|
|
<TableRow key={row.jobOrderId}> |
|
|
<TableRow key={row.jobOrderId}> |
|
|
<TableCell sx={{ padding: '16px 20px' }}> |
|
|
|
|
|
{row.jobOrderCode || '-'} |
|
|
|
|
|
|
|
|
<TableCell |
|
|
|
|
|
sx={{ padding: "10px", verticalAlign: "top" }} |
|
|
|
|
|
> |
|
|
|
|
|
{row.jobOrderCode || "-"} |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell sx={{ padding: '16px 20px' }}> |
|
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>{row.itemCode || '-'}</Box> |
|
|
|
|
|
<Box>{row.itemName || '-'}</Box> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<TableCell |
|
|
|
|
|
sx={{ padding: "10px", verticalAlign: "top" }} |
|
|
|
|
|
> |
|
|
|
|
|
<Box |
|
|
|
|
|
sx={{ |
|
|
|
|
|
display: "flex", |
|
|
|
|
|
flexDirection: "column", |
|
|
|
|
|
gap: 0.5, |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{row.itemCode || "-"} |
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
|
|
|
|
<Box>{row.itemName || "-"}</Box> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell sx={{ padding: '16px 20px' }}> |
|
|
|
|
|
|
|
|
|
|
|
{row.status === 'pending' ? '-' : calculateRemainingTime(row.planEndTime, row.processingTime, row.setupTime, row.changeoverTime)} |
|
|
|
|
|
|
|
|
<TableCell |
|
|
|
|
|
sx={{ padding: "10px", verticalAlign: "top" }} |
|
|
|
|
|
> |
|
|
|
|
|
{row.status === "pending" |
|
|
|
|
|
? "-" |
|
|
|
|
|
: calculateRemainingTime(row.planEndTime)} |
|
|
</TableCell> |
|
|
</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} 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} 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> |
|
|
|
|
|
); |
|
|
|
|
|
})} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{Array.from({ length: visibleCount }, (_, i) => |
|
|
|
|
|
renderProcessCell(row, startSlotIndex + i), |
|
|
|
|
|
)} |
|
|
</TableRow> |
|
|
</TableRow> |
|
|
)) |
|
|
)) |
|
|
)} |
|
|
)} |
|
|
@@ -344,11 +724,9 @@ const JobProcessStatus: React.FC = () => { |
|
|
</TableContainer> |
|
|
</TableContainer> |
|
|
)} |
|
|
)} |
|
|
</Box> |
|
|
</Box> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</CardContent> |
|
|
</CardContent> |
|
|
</Card> |
|
|
</Card> |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
export default JobProcessStatus; |
|
|
|
|
|
|
|
|
export default JobProcessStatus; |