FPSMS-frontend
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 

335 righe
13 KiB

  1. "use client";
  2. import React, { useState, useEffect, useCallback, useRef } from 'react';
  3. import {
  4. Box,
  5. Typography,
  6. Card,
  7. CardContent,
  8. Table,
  9. TableBody,
  10. TableCell,
  11. TableContainer,
  12. TableHead,
  13. TableRow,
  14. Paper,
  15. CircularProgress,
  16. Stack
  17. } from '@mui/material';
  18. import { useTranslation } from 'react-i18next';
  19. import dayjs from 'dayjs';
  20. import { fetchJobProcessStatus, JobProcessStatusResponse } from '@/app/api/jo/actions';
  21. import { arrayToDayjs } from '@/app/utils/formatUtil';
  22. import { FormControl, Select, MenuItem } from "@mui/material";
  23. const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes
  24. const JobProcessStatus: React.FC = () => {
  25. const { t } = useTranslation(["common", "jo"]);
  26. const [data, setData] = useState<JobProcessStatusResponse[]>([]);
  27. const [loading, setLoading] = useState<boolean>(true);
  28. const refreshCountRef = useRef<number>(0);
  29. const [currentTime, setCurrentTime] = useState(dayjs());
  30. const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD"));
  31. // Update current time every second for countdown
  32. useEffect(() => {
  33. const timer = setInterval(() => {
  34. setCurrentTime(dayjs());
  35. }, 1000);
  36. return () => clearInterval(timer);
  37. }, []);
  38. const loadData = useCallback(async () => {
  39. setLoading(true);
  40. try {
  41. const result = await fetchJobProcessStatus(selectedDate);
  42. setData(result);
  43. refreshCountRef.current += 1;
  44. } catch (error) {
  45. console.error('Error fetching job process status:', error);
  46. setData([]);
  47. } finally {
  48. setLoading(false);
  49. }
  50. }, [selectedDate]);
  51. useEffect(() => {
  52. loadData();
  53. const interval = setInterval(() => {
  54. loadData();
  55. }, REFRESH_INTERVAL);
  56. return () => clearInterval(interval);
  57. }, [loadData]);
  58. const formatTime = (timeData: any): string => {
  59. if (!timeData) return '-'; // 改为返回 '-' 而不是 'N/A'
  60. // Handle array format [year, month, day, hour, minute, second]
  61. if (Array.isArray(timeData)) {
  62. try {
  63. const parsed = arrayToDayjs(timeData, true);
  64. if (parsed.isValid()) {
  65. return parsed.format('HH:mm');
  66. }
  67. } catch (error) {
  68. console.error('Error parsing array time:', error);
  69. }
  70. }
  71. // Handle LocalDateTime ISO string format (e.g., "2026-01-09T18:01:54")
  72. if (typeof timeData === 'string') {
  73. const parsed = dayjs(timeData);
  74. if (parsed.isValid()) {
  75. return parsed.format('HH:mm');
  76. }
  77. }
  78. return '-';
  79. };
  80. const calculateRemainingTime = (planEndTime: any, processingTime: number | null, setupTime: number | null, changeoverTime: number | null): string => {
  81. if (!planEndTime) return '-';
  82. let endTime: dayjs.Dayjs;
  83. // Handle array format [year, month, day, hour, minute, second]
  84. // Use arrayToDayjs for consistency with other parts of the codebase
  85. if (Array.isArray(planEndTime)) {
  86. try {
  87. endTime = arrayToDayjs(planEndTime, true);
  88. console.log('Parsed planEndTime array:', {
  89. array: planEndTime,
  90. parsed: endTime.format('YYYY-MM-DD HH:mm:ss'),
  91. isValid: endTime.isValid()
  92. });
  93. } catch (error) {
  94. console.error('Error parsing array planEndTime:', error);
  95. return '-';
  96. }
  97. } else if (typeof planEndTime === 'string') {
  98. endTime = dayjs(planEndTime);
  99. console.log('Parsed planEndTime string:', {
  100. string: planEndTime,
  101. parsed: endTime.format('YYYY-MM-DD HH:mm:ss'),
  102. isValid: endTime.isValid()
  103. });
  104. } else {
  105. return '-';
  106. }
  107. if (!endTime.isValid()) {
  108. console.error('Invalid endTime:', planEndTime);
  109. return '-';
  110. }
  111. const diff = endTime.diff(currentTime, 'minute');
  112. console.log('Remaining time calculation:', {
  113. endTime: endTime.format('YYYY-MM-DD HH:mm:ss'),
  114. currentTime: currentTime.format('YYYY-MM-DD HH:mm:ss'),
  115. diffMinutes: diff
  116. });
  117. // If the planned end time is in the past, show 0 (or you could show negative time)
  118. if (diff < 0) return '0';
  119. const hours = Math.floor(diff / 60);
  120. const minutes = diff % 60;
  121. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
  122. };
  123. const calculateWaitTime = (
  124. currentProcessEndTime: any,
  125. nextProcessStartTime: any,
  126. isLastProcess: boolean
  127. ): string => {
  128. if (isLastProcess) return '-';
  129. if (!currentProcessEndTime) return '-';
  130. if (nextProcessStartTime) return '0'; // Next process has started, stop counting
  131. let endTime: dayjs.Dayjs;
  132. // Handle array format
  133. if (Array.isArray(currentProcessEndTime)) {
  134. try {
  135. endTime = arrayToDayjs(currentProcessEndTime, true);
  136. } catch (error) {
  137. console.error('Error parsing array endTime:', error);
  138. return '-';
  139. }
  140. } else if (typeof currentProcessEndTime === 'string') {
  141. endTime = dayjs(currentProcessEndTime);
  142. } else {
  143. return '-';
  144. }
  145. if (!endTime.isValid()) return '-';
  146. const diff = currentTime.diff(endTime, 'minute');
  147. return diff > 0 ? diff.toString() : '0';
  148. };
  149. return (
  150. <Card sx={{ mb: 2 }}>
  151. <CardContent>
  152. {/* Title */}
  153. <Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
  154. {t("Job Process Status Dashboard")}
  155. </Typography>
  156. {/* Filters */}
  157. <Stack direction="row" spacing={2} sx={{ mb: 3 }}>
  158. <FormControl size="small" sx={{ minWidth: 160 }}>
  159. <Select
  160. value={selectedDate}
  161. onChange={(e) => setSelectedDate(e.target.value)}
  162. >
  163. <MenuItem value={dayjs().format("YYYY-MM-DD")}>今天</MenuItem>
  164. <MenuItem value={dayjs().subtract(1, "day").format("YYYY-MM-DD")}>昨天</MenuItem>
  165. <MenuItem value={dayjs().subtract(2, "day").format("YYYY-MM-DD")}>前天</MenuItem>
  166. </Select>
  167. </FormControl>
  168. </Stack>
  169. <Box sx={{ mt: 2 }}>
  170. {loading ? (
  171. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  172. <CircularProgress />
  173. </Box>
  174. ) : (
  175. <TableContainer
  176. component={Paper}
  177. sx={{
  178. border: '3px solid #135fed',
  179. overflowX: 'auto', // 关键:允许横向滚动
  180. }}
  181. >
  182. <Table size="small" sx={{ minWidth: 1800 }}>
  183. <TableHead>
  184. <TableRow>
  185. <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}>
  186. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  187. {t("Job Order No.")}
  188. </Typography>
  189. </TableCell>
  190. <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}>
  191. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  192. {t("FG / WIP Item")}
  193. </Typography>
  194. </TableCell>
  195. <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}>
  196. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  197. {t("Production Time Remaining")}
  198. </Typography>
  199. </TableCell>
  200. </TableRow>
  201. <TableRow>
  202. {Array.from({ length: 16 }, (_, i) => i + 1).map((num) => (
  203. <TableCell key={num} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}>
  204. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  205. {t("Process")} {num}
  206. </Typography>
  207. </TableCell>
  208. ))}
  209. </TableRow>
  210. <TableRow>
  211. {Array.from({ length: 16 }, (_, i) => i + 1).map((num) => (
  212. <TableCell key={num} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}>
  213. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
  214. <Typography variant="caption" sx={{ fontWeight: 600 }}>
  215. {t("Start")}
  216. </Typography>
  217. <Typography variant="caption" sx={{ fontWeight: 600 }}>
  218. {t("Finish")}
  219. </Typography>
  220. <Typography variant="caption" sx={{ fontWeight: 600 }}>
  221. {t("Wait Time [minutes]")}
  222. </Typography>
  223. </Box>
  224. </TableCell>
  225. ))}
  226. </TableRow>
  227. </TableHead>
  228. <TableBody>
  229. {data.length === 0 ? (
  230. <TableRow>
  231. <TableCell colSpan={9} align="center" sx={{ padding: '20px' }}>
  232. {t("No data available")}
  233. </TableCell>
  234. </TableRow>
  235. ) : (
  236. data.map((row) => (
  237. <TableRow key={row.jobOrderId}>
  238. <TableCell sx={{ padding: '16px 20px' }}>
  239. {row.jobOrderCode || '-'}
  240. </TableCell>
  241. <TableCell sx={{ padding: '16px 20px' }}>
  242. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>{row.itemCode || '-'}</Box>
  243. <Box>{row.itemName || '-'}</Box>
  244. </TableCell>
  245. <TableCell sx={{ padding: '16px 20px' }}>
  246. {row.status === 'pending' ? '-' : calculateRemainingTime(row.planEndTime, row.processingTime, row.setupTime, row.changeoverTime)}
  247. </TableCell>
  248. {row.processes.map((process, index) => {
  249. const isLastProcess = index === row.processes.length - 1 ||
  250. !row.processes.slice(index + 1).some(p => p.isRequired);
  251. const nextProcess = index < row.processes.length - 1 ? row.processes[index + 1] : null;
  252. const waitTime = calculateWaitTime(
  253. process.endTime,
  254. nextProcess?.startTime,
  255. isLastProcess
  256. );
  257. // 如果工序不是必需的,只显示一个 N/A
  258. if (!process.isRequired) {
  259. return (
  260. <TableCell key={index} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}>
  261. <Typography variant="body2">
  262. N/A
  263. </Typography>
  264. </TableCell>
  265. );
  266. }
  267. const label = [
  268. process.processName,
  269. process.equipmentName,
  270. process.equipmentDetailName ? `-${process.equipmentDetailName}` : "",
  271. ].filter(Boolean).join(" ");
  272. // 如果工序是必需的,显示三行(Start、Finish、Wait Time)
  273. return (
  274. <TableCell key={index} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}>
  275. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
  276. <Typography variant="body2" sx={{ mb: 0.5 }}>{label || "-"}</Typography>
  277. <Typography variant="body2" sx={{ py: 0.5 }}>
  278. {formatTime(process.startTime)}
  279. </Typography>
  280. <Typography variant="body2" sx={{ py: 0.5 }}>
  281. {formatTime(process.endTime)}
  282. </Typography>
  283. <Typography variant="body2" sx={{
  284. color: waitTime !== '-' && parseInt(waitTime) > 0 ? 'warning.main' : 'text.primary',
  285. py: 0.5
  286. }}>
  287. {waitTime}
  288. </Typography>
  289. </Box>
  290. </TableCell>
  291. );
  292. })}
  293. </TableRow>
  294. ))
  295. )}
  296. </TableBody>
  297. </Table>
  298. </TableContainer>
  299. )}
  300. </Box>
  301. </CardContent>
  302. </Card>
  303. );
  304. };
  305. export default JobProcessStatus;