FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1003 line
34 KiB

  1. "use client";
  2. import React, { useCallback, useEffect, useState, useRef } from "react";
  3. import EditIcon from "@mui/icons-material/Edit";
  4. import AddIcon from '@mui/icons-material/Add';
  5. import DeleteIcon from '@mui/icons-material/Delete';
  6. import Fab from '@mui/material/Fab';
  7. import {
  8. Box,
  9. Button,
  10. Paper,
  11. Stack,
  12. Typography,
  13. TextField,
  14. Table,
  15. TableBody,
  16. TableCell,
  17. TableContainer,
  18. TableHead,
  19. TableRow,
  20. Chip,
  21. Card,
  22. CardContent,
  23. CircularProgress,
  24. Dialog,
  25. DialogTitle,
  26. DialogContent,
  27. DialogActions,
  28. IconButton
  29. } from "@mui/material";
  30. import QrCodeIcon from '@mui/icons-material/QrCode';
  31. import { useTranslation } from "react-i18next";
  32. import { Operator, Machine } from "@/app/api/jo";
  33. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  34. import { useSession } from "next-auth/react";
  35. import { SessionWithTokens } from "@/config/authConfig";
  36. import PlayArrowIcon from "@mui/icons-material/PlayArrow";
  37. import CheckCircleIcon from "@mui/icons-material/CheckCircle";
  38. import dayjs from "dayjs";
  39. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  40. import {
  41. // updateProductProcessLineQrscan,
  42. newUpdateProductProcessLineQrscan,
  43. fetchProductProcessLineDetail,
  44. JobOrderProcessLineDetailResponse,
  45. ProductProcessLineInfoResponse,
  46. startProductProcessLine,
  47. fetchProductProcessesByJobOrderId,
  48. ProductProcessWithLinesResponse, // 添加
  49. ProductProcessLineResponse,
  50. passProductProcessLine,
  51. newProductProcessLine,
  52. updateProductProcessLineProcessingTimeSetupTimeChangeoverTime,
  53. UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest,
  54. deleteProductProcessLine,
  55. } from "@/app/api/jo/actions";
  56. import { updateProductProcessLineStatus } from "@/app/api/jo/actions";
  57. import { fetchNameList, NameList } from "@/app/api/user/actions";
  58. import ProductionProcessStepExecution from "./ProductionProcessStepExecution";
  59. import ProductionOutputFormPage from "./ProductionOutputFormPage";
  60. import ProcessSummaryHeader from "./ProcessSummaryHeader";
  61. interface ProductProcessDetailProps {
  62. jobOrderId: number;
  63. onBack: () => void;
  64. fromJosave?: boolean;
  65. }
  66. const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
  67. jobOrderId,
  68. onBack,
  69. fromJosave,
  70. }) => {
  71. console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave });
  72. const { t } = useTranslation("common");
  73. const { data: session } = useSession() as { data: SessionWithTokens | null };
  74. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  75. const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  76. const [showOutputPage, setShowOutputPage] = useState(false);
  77. // 基本信息
  78. const [processData, setProcessData] = useState<ProductProcessWithLinesResponse | null>(null); // 修改类型
  79. const [lines, setLines] = useState<ProductProcessLineResponse[]>([]); // 修改类型
  80. const [loading, setLoading] = useState(false);
  81. const linesRef = useRef<ProductProcessLineResponse[]>([]);
  82. const onBackRef = useRef(onBack);
  83. const fetchProcessDetailRef = useRef<() => Promise<void>>();
  84. // 选中的 line 和执行状态
  85. const [selectedLineId, setSelectedLineId] = useState<number | null>(null);
  86. const [isExecutingLine, setIsExecutingLine] = useState(false);
  87. const [isAutoSubmitting, setIsAutoSubmitting] = useState(false);
  88. // 扫描器状态
  89. const [isManualScanning, setIsManualScanning] = useState(false);
  90. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  91. const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null);
  92. const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null);
  93. // const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null);
  94. const [scannedStaffNo, setScannedStaffNo] = useState<string | null>(null);
  95. // const [scannedEquipmentDetailId, setScannedEquipmentDetailId] = useState<number | null>(null);
  96. const [scannedEquipmentCode, setScannedEquipmentCode] = useState<string | null>(null);
  97. const [scanningLineId, setScanningLineId] = useState<number | null>(null);
  98. const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null);
  99. const [showScanDialog, setShowScanDialog] = useState(false);
  100. const autoSubmitTimerRef = useRef<NodeJS.Timeout | null>(null);
  101. const [openTimeDialog, setOpenTimeDialog] = useState(false);
  102. const [editingLineId, setEditingLineId] = useState<number | null>(null);
  103. const [timeValues, setTimeValues] = useState({
  104. durationInMinutes: 0,
  105. prepTimeInMinutes: 0,
  106. postProdTimeInMinutes: 0,
  107. });
  108. const [outputData, setOutputData] = useState({
  109. byproductName: "",
  110. byproductQty: "",
  111. byproductUom: "",
  112. scrapQty: "",
  113. scrapUom: "",
  114. defectQty: "",
  115. defectUom: "",
  116. outputFromProcessQty: "",
  117. outputFromProcessUom: "",
  118. });
  119. // 处理 QR 码扫描
  120. // 处理 QR 码扫描
  121. const handleBackFromStep = async () => {
  122. await fetchProcessDetail(); // 重新拉取最新的 process/lines
  123. setIsExecutingLine(false);
  124. setSelectedLineId(null);
  125. setShowOutputPage(false);
  126. };
  127. useEffect(() => {
  128. onBackRef.current = onBack;
  129. }, [onBack]);
  130. // 获取 process 和 lines 数据
  131. const fetchProcessDetail = useCallback(async () => {
  132. console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() });
  133. setLoading(true);
  134. try {
  135. console.log(` Loading process detail for JobOrderId: ${jobOrderId}`);
  136. const processesWithLines = await fetchProductProcessesByJobOrderId(jobOrderId);
  137. if (!processesWithLines || processesWithLines.length === 0) {
  138. throw new Error("No processes found for this job order");
  139. }
  140. const currentProcess = processesWithLines[0];
  141. setProcessData(currentProcess);
  142. const lines = currentProcess.productProcessLines || [];
  143. setLines(lines);
  144. linesRef.current = lines;
  145. console.log(" Process data loaded:", currentProcess);
  146. console.log(" Lines loaded:", lines);
  147. } catch (error) {
  148. console.error(" Error loading process detail:", error);
  149. onBackRef.current();
  150. } finally {
  151. setLoading(false);
  152. }
  153. }, [jobOrderId]);
  154. const handleOpenTimeDialog = useCallback((lineId: number) => {
  155. console.log("🔓 handleOpenTimeDialog CALLED", { lineId, timestamp: new Date().toISOString() });
  156. // 直接使用 linesRef.current,避免触发 setLines
  157. const line = linesRef.current.find(l => l.id === lineId);
  158. if (line) {
  159. console.log(" Found line:", line);
  160. setEditingLineId(lineId);
  161. setTimeValues({
  162. durationInMinutes: line.durationInMinutes || 0,
  163. prepTimeInMinutes: line.prepTimeInMinutes || 0,
  164. postProdTimeInMinutes: line.postProdTimeInMinutes || 0,
  165. });
  166. setOpenTimeDialog(true);
  167. console.log(" Dialog opened");
  168. } else {
  169. console.warn(" Line not found:", lineId);
  170. }
  171. }, []);
  172. useEffect(() => {
  173. fetchProcessDetailRef.current = fetchProcessDetail;
  174. }, [fetchProcessDetail]);
  175. const handleCloseTimeDialog = useCallback(() => {
  176. console.log("🔒 handleCloseTimeDialog CALLED", { timestamp: new Date().toISOString() });
  177. setOpenTimeDialog(false);
  178. setEditingLineId(null);
  179. setTimeValues({
  180. durationInMinutes: 0,
  181. prepTimeInMinutes: 0,
  182. postProdTimeInMinutes: 0,
  183. });
  184. console.log(" Dialog closed");
  185. }, []);
  186. const handleConfirmTimeUpdate = useCallback(async () => {
  187. console.log("💾 handleConfirmTimeUpdate CALLED", { editingLineId, timeValues, timestamp: new Date().toISOString() });
  188. if (!editingLineId) return;
  189. try {
  190. const request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest = {
  191. productProcessLineId: editingLineId,
  192. processingTime: timeValues.durationInMinutes,
  193. setupTime: timeValues.prepTimeInMinutes,
  194. changeoverTime: timeValues.postProdTimeInMinutes,
  195. };
  196. await updateProductProcessLineProcessingTimeSetupTimeChangeoverTime(editingLineId, request);
  197. await fetchProcessDetail();
  198. handleCloseTimeDialog();
  199. } catch (error) {
  200. console.error("Error updating time:", error);
  201. alert(t("update failed"));
  202. }
  203. }, [editingLineId, timeValues, fetchProcessDetail, handleCloseTimeDialog, t]);
  204. useEffect(() => {
  205. console.log("🔄 useEffect [jobOrderId] TRIGGERED", {
  206. jobOrderId,
  207. timestamp: new Date().toISOString()
  208. });
  209. if (fetchProcessDetailRef.current) {
  210. fetchProcessDetailRef.current();
  211. }
  212. }, [jobOrderId]);
  213. // 添加监听 openTimeDialog 变化的 useEffect
  214. useEffect(() => {
  215. console.log(" openTimeDialog changed:", { openTimeDialog, timestamp: new Date().toISOString() });
  216. }, [openTimeDialog]);
  217. // 添加监听 timeValues 变化的 useEffect
  218. useEffect(() => {
  219. console.log(" timeValues changed:", { timeValues, timestamp: new Date().toISOString() });
  220. }, [timeValues]);
  221. // 添加监听 lines 变化的 useEffect
  222. useEffect(() => {
  223. console.log(" lines changed:", { count: lines.length, lines, timestamp: new Date().toISOString() });
  224. }, [lines]);
  225. // 添加监听 editingLineId 变化的 useEffect
  226. useEffect(() => {
  227. console.log(" editingLineId changed:", { editingLineId, timestamp: new Date().toISOString() });
  228. }, [editingLineId]);
  229. const handlePassLine = useCallback(async (lineId: number) => {
  230. try {
  231. await passProductProcessLine(lineId);
  232. // 刷新数据
  233. await fetchProcessDetail();
  234. } catch (error) {
  235. console.error("Error passing line:", error);
  236. alert(t("Failed to pass line. Please try again."));
  237. }
  238. }, [fetchProcessDetail, t]);
  239. const handleCreateNewLine = useCallback(async (lineId: number) => {
  240. try {
  241. await newProductProcessLine(lineId);
  242. // 刷新数据
  243. await fetchProcessDetail();
  244. } catch (error) {
  245. console.error("Error creating new line:", error);
  246. alert(t("Failed to create new line. Please try again."));
  247. }
  248. }, [fetchProcessDetail, t]);
  249. const handleDeleteLine = useCallback(async (lineId: number) => {
  250. if (!confirm(t("Are you sure you want to delete this process?"))) {
  251. return;
  252. }
  253. try {
  254. await deleteProductProcessLine(lineId);
  255. // 刷新数据
  256. await fetchProcessDetail();
  257. } catch (error) {
  258. console.error("Error deleting line:", error);
  259. alert(t("Failed to delete line. Please try again."));
  260. }
  261. }, [fetchProcessDetail, t]);
  262. const processQrCode = useCallback((qrValue: string, lineId: number) => {
  263. // 设备快捷格式:{2fitesteXXX} - XXX 直接作为设备代码
  264. // 格式:{2fitesteXXX} = equipmentCode: "XXX"
  265. // 例如:{2fiteste包裝機類-真空八爪魚機-1號} = equipmentCode: "包裝機類-真空八爪魚機-1號"
  266. if (qrValue.match(/\{2fiteste(.+)\}/)) {
  267. const match = qrValue.match(/\{2fiteste(.+)\}/);
  268. const equipmentCode = match![1];
  269. setScannedEquipmentCode(equipmentCode);
  270. console.log(`Set equipmentCode from shortcut: ${equipmentCode}`);
  271. return;
  272. }
  273. // 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo
  274. // 例如:{2fitestu123} = staffNo: "123"
  275. // 例如:{2fitestustaff001} = staffNo: "staff001"
  276. if (qrValue.match(/\{2fitestu(.+)\}/)) {
  277. const match = qrValue.match(/\{2fitestu(.+)\}/);
  278. const staffNo = match![1];
  279. setScannedStaffNo(staffNo);
  280. return;
  281. }
  282. // 正常 QR 扫描器扫描格式
  283. const trimmedValue = qrValue.trim();
  284. // 检查 staffNo 格式:"staffNo: STAFF001" 或 "staffNo:STAFF001"
  285. const staffNoMatch = trimmedValue.match(/^staffNo:\s*(.+)$/i);
  286. if (staffNoMatch) {
  287. const staffNo = staffNoMatch[1].trim();
  288. setScannedStaffNo(staffNo);
  289. return;
  290. }
  291. // 检查 equipmentCode 格式
  292. const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo|equipmentCode):\s*(.+)$/i);
  293. if (equipmentCodeMatch) {
  294. const equipmentCode = equipmentCodeMatch[1].trim();
  295. setScannedEquipmentCode(equipmentCode);
  296. return;
  297. }
  298. // 其他格式处理(JSON、普通文本等)
  299. try {
  300. const qrData = JSON.parse(qrValue);
  301. if (qrData.staffNo) {
  302. setScannedStaffNo(String(qrData.staffNo));
  303. }
  304. if (qrData.equipmentTypeSubTypeEquipmentNo || qrData.equipmentCode) {
  305. setScannedEquipmentCode(
  306. String(qrData.equipmentTypeSubTypeEquipmentNo ?? qrData.equipmentCode)
  307. );
  308. }
  309. } catch {
  310. // 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode
  311. if (trimmedValue.length > 0) {
  312. if (trimmedValue.toUpperCase().startsWith("STAFF") || /^\d+$/.test(trimmedValue)) {
  313. // 可能是员工编号
  314. setScannedStaffNo(trimmedValue);
  315. } else if (trimmedValue.includes("-")) {
  316. // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號")
  317. setScannedEquipmentCode(trimmedValue);
  318. }
  319. }
  320. }
  321. }, [lines]);
  322. // 处理 QR 码扫描效果
  323. useEffect(() => {
  324. if (isManualScanning && qrValues.length > 0 && scanningLineId) {
  325. const latestQr = qrValues[qrValues.length - 1];
  326. if (processedQrCodes.has(latestQr)) {
  327. return;
  328. }
  329. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  330. processQrCode(latestQr, scanningLineId);
  331. }
  332. }, [qrValues, isManualScanning, scanningLineId, processedQrCodes, processQrCode]);
  333. const submitScanAndStart = useCallback(async (lineId: number) => {
  334. console.log("submitScanAndStart called with:", {
  335. lineId,
  336. scannedStaffNo,
  337. // scannedEquipmentTypeSubTypeEquipmentNo,
  338. scannedEquipmentCode,
  339. });
  340. if (!scannedStaffNo) {
  341. console.log("No staffNo, cannot submit");
  342. setIsAutoSubmitting(false);
  343. return false;
  344. }
  345. try {
  346. const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId);
  347. // 统一使用一个最终的 equipmentCode(优先用 scannedEquipmentCode,其次用 scannedEquipmentTypeSubTypeEquipmentNo)
  348. const effectiveEquipmentCode =
  349. scannedEquipmentCode ?? null;
  350. console.log("Submitting scan data with equipmentCode:", {
  351. productProcessLineId: lineId,
  352. staffNo: scannedStaffNo,
  353. equipmentCode: effectiveEquipmentCode,
  354. });
  355. const response = await newUpdateProductProcessLineQrscan({
  356. productProcessLineId: lineId,
  357. equipmentCode: effectiveEquipmentCode ?? "",
  358. staffNo: scannedStaffNo,
  359. });
  360. console.log("Scan submit response:", response);
  361. if (response && response.type === "error") {
  362. console.error("Scan validation failed:", response.message);
  363. alert(t(response.message) || t("Validation failed. Please check your input."));
  364. setIsAutoSubmitting(false);
  365. if (autoSubmitTimerRef.current) {
  366. clearTimeout(autoSubmitTimerRef.current);
  367. autoSubmitTimerRef.current = null;
  368. }
  369. return false;
  370. }
  371. console.log("Validation passed, starting line...");
  372. handleStopScan();
  373. setShowScanDialog(false);
  374. setIsAutoSubmitting(false);
  375. await handleStartLine(lineId);
  376. setSelectedLineId(lineId);
  377. setIsExecutingLine(true);
  378. await fetchProcessDetail();
  379. return true;
  380. } catch (error) {
  381. console.error("Error submitting scan:", error);
  382. alert("Failed to submit scan data. Please try again.");
  383. setIsAutoSubmitting(false);
  384. return false;
  385. }
  386. }, [
  387. scannedStaffNo,
  388. scannedEquipmentCode,
  389. lineDetailForScan,
  390. t,
  391. fetchProcessDetail,
  392. ]);
  393. const handleSubmitScanAndStart = useCallback(async (lineId: number) => {
  394. console.log("handleSubmitScanAndStart called with lineId:", lineId);
  395. if (!scannedStaffNo) {
  396. //alert(t("Please scan operator code first"));
  397. return;
  398. }
  399. // 如果正在自动提交,等待一下
  400. if (isAutoSubmitting) {
  401. console.log("Already auto-submitting, skipping manual submit");
  402. return;
  403. }
  404. await submitScanAndStart(lineId);
  405. }, [scannedOperatorId, isAutoSubmitting, submitScanAndStart, t]);
  406. // 开始扫描
  407. const handleStartScan = useCallback((lineId: number) => {
  408. if (autoSubmitTimerRef.current) {
  409. clearTimeout(autoSubmitTimerRef.current);
  410. autoSubmitTimerRef.current = null;
  411. }
  412. setScanningLineId(lineId);
  413. setIsManualScanning(true);
  414. setProcessedQrCodes(new Set());
  415. setScannedOperatorId(null);
  416. setScannedEquipmentId(null);
  417. setScannedStaffNo(null); // Add this
  418. setScannedEquipmentCode(null);
  419. setIsAutoSubmitting(false); // 添加:重置自动提交状态
  420. setLineDetailForScan(null);
  421. // 获取 line detail 以获取 bomProcessEquipmentId
  422. fetchProductProcessLineDetail(lineId)
  423. .then(setLineDetailForScan)
  424. .catch(err => {
  425. console.error("Failed to load line detail", err);
  426. // 不阻止扫描继续,line detail 不是必需的
  427. });
  428. startScan();
  429. }, [startScan]);
  430. // 停止扫描
  431. const handleStopScan = useCallback(() => {
  432. console.log("🛑 Stopping scan");
  433. // 清除定时器
  434. if (autoSubmitTimerRef.current) {
  435. clearTimeout(autoSubmitTimerRef.current);
  436. autoSubmitTimerRef.current = null;
  437. }
  438. setIsManualScanning(false);
  439. setIsAutoSubmitting(false);
  440. setScannedStaffNo(null); // Add this
  441. setScannedEquipmentCode(null);
  442. stopScan();
  443. resetScan();
  444. }, [stopScan, resetScan]);
  445. // 开始执行某个 line(原有逻辑,现在在验证通过后调用)
  446. const handleStartLine = async (lineId: number) => {
  447. try {
  448. await startProductProcessLine(lineId);
  449. } catch (error) {
  450. console.error("Error starting line:", error);
  451. //alert("Failed to start line. Please try again.");
  452. }
  453. };
  454. // 提交扫描结果并验证
  455. /*
  456. useEffect(() => {
  457. console.log("Auto-submit check:", {
  458. scanningLineId,
  459. scannedStaffNo,
  460. scannedEquipmentCode,
  461. isAutoSubmitting,
  462. isManualScanning,
  463. });
  464. // Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId
  465. if (
  466. scanningLineId &&
  467. scannedStaffNo !== null &&
  468. (scannedEquipmentCode !== null) &&
  469. !isAutoSubmitting &&
  470. isManualScanning
  471. ) {
  472. console.log("Auto-submitting triggered!");
  473. setIsAutoSubmitting(true);
  474. // 清除之前的定时器(如果有)
  475. if (autoSubmitTimerRef.current) {
  476. clearTimeout(autoSubmitTimerRef.current);
  477. }
  478. // 延迟一点时间,让用户看到两个都扫描完成了
  479. autoSubmitTimerRef.current = setTimeout(() => {
  480. console.log("Executing auto-submit...");
  481. submitScanAndStart(scanningLineId);
  482. autoSubmitTimerRef.current = null;
  483. }, 500);
  484. }
  485. // 清理函数:只在组件卸载或条件不再满足时清除定时器
  486. return () => {
  487. // 注意:这里不立即清除定时器,因为我们需要它执行
  488. // 只在组件卸载时清除
  489. };
  490. }, [scanningLineId, scannedStaffNo, scannedEquipmentCode, isAutoSubmitting, isManualScanning, submitScanAndStart]);
  491. */
  492. useEffect(() => {
  493. return () => {
  494. if (autoSubmitTimerRef.current) {
  495. clearTimeout(autoSubmitTimerRef.current);
  496. }
  497. };
  498. }, []);
  499. const handleStartLineWithScan = async (lineId: number) => {
  500. console.log("🚀 Starting line with scan for lineId:", lineId);
  501. // 确保状态完全重置
  502. setIsAutoSubmitting(false);
  503. setScannedOperatorId(null);
  504. setScannedEquipmentId(null);
  505. setProcessedQrCodes(new Set());
  506. setScannedStaffNo(null);
  507. setScannedEquipmentCode(null);
  508. setProcessedQrCodes(new Set());
  509. // 清除之前的定时器
  510. if (autoSubmitTimerRef.current) {
  511. clearTimeout(autoSubmitTimerRef.current);
  512. autoSubmitTimerRef.current = null;
  513. }
  514. setScanningLineId(lineId);
  515. setShowScanDialog(true);
  516. handleStartScan(lineId);
  517. };
  518. const selectedLine = lines.find(l => l.id === selectedLineId);
  519. // 添加组件卸载日志
  520. useEffect(() => {
  521. return () => {
  522. console.log("🗑️ ProductionProcessDetail UNMOUNTING");
  523. };
  524. }, []);
  525. if (loading) {
  526. return (
  527. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  528. <CircularProgress/>
  529. </Box>
  530. );
  531. }
  532. return (
  533. <Box>
  534. {/* ========== 第二部分:Process Lines ========== */}
  535. <Paper sx={{ p: 3 }}>
  536. <Typography variant="h6" gutterBottom fontWeight="bold">
  537. {t("Production Process Steps")}
  538. </Typography>
  539. <ProcessSummaryHeader processData={processData} />
  540. {!isExecutingLine ? (
  541. /* ========== 步骤列表视图 ========== */
  542. <TableContainer>
  543. <Table>
  544. <TableHead>
  545. <TableRow>
  546. <TableCell>{t(" ")}</TableCell>
  547. <TableCell>{t("Seq")}</TableCell>
  548. <TableCell>{t("Step Name")}</TableCell>
  549. <TableCell>{t("Description")}</TableCell>
  550. <TableCell>{t("EquipmentType-EquipmentName-Code")}</TableCell>
  551. <TableCell>{t("Operator")}</TableCell>
  552. <TableCell>{t("Assume End Time")}</TableCell>
  553. <TableCell>
  554. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  555. <Typography variant="body2" sx={{ fontWeight: 500 }}>
  556. {t("Time Information(mins)")}
  557. </Typography>
  558. </Box>
  559. </TableCell>
  560. <TableCell align="center">{t("Status")}</TableCell>
  561. {!fromJosave&&(<TableCell align="center">{t("Action")}</TableCell>)}
  562. </TableRow>
  563. </TableHead>
  564. <TableBody>
  565. {lines.map((line) => {
  566. const status = (line as any).status || '';
  567. const statusLower = status.toLowerCase();
  568. const equipmentName = line.equipment_name || "-";
  569. const isPlanning = processData?.jobOrderStatus === "planning";
  570. const isCompleted = statusLower === 'completed';
  571. const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress';
  572. const isPaused = statusLower === 'paused';
  573. const isPending = statusLower === 'pending' || status === '';
  574. const isPass = statusLower === 'pass';
  575. const isPassDisabled = isCompleted || isPass;
  576. return (
  577. <TableRow key={line.id}>
  578. <TableCell>
  579. {isPlanning && (
  580. <Fab
  581. size="small"
  582. color="primary"
  583. aria-label={t("Create New Line")}
  584. onClick={() => handleCreateNewLine(line.id)}
  585. sx={{
  586. width: 32,
  587. height: 32,
  588. minHeight: 32,
  589. boxShadow: 1,
  590. '&:hover': { boxShadow: 3 },
  591. }}
  592. >
  593. <AddIcon fontSize="small" />
  594. </Fab>
  595. )}
  596. {isPlanning && line.isOringinal !== true && (
  597. <IconButton
  598. size="small"
  599. color="error"
  600. onClick={() => handleDeleteLine(line.id)}
  601. sx={{ padding: 0.5 }}
  602. >
  603. <DeleteIcon fontSize="small" />
  604. </IconButton>
  605. )}
  606. </TableCell>
  607. <TableCell>
  608. <Stack direction="row" spacing={1} alignItems="center">
  609. <Typography variant="body2" textAlign="center">{line.seqNo}</Typography>
  610. </Stack>
  611. </TableCell>
  612. <TableCell>
  613. <Typography variant="body2" fontWeight={500}>{line.name}</Typography>
  614. </TableCell>
  615. <TableCell>
  616. <Typography variant="body2" fontWeight={500} maxWidth={200} sx={{ wordBreak: 'break-word', whiteSpace: 'normal', lineHeight: 1.5 }}>{line.description || "-"}</Typography>
  617. </TableCell>
  618. <TableCell>
  619. <Typography variant="body2" fontWeight={500}>{line.equipmentDetailCode||equipmentName}</Typography>
  620. </TableCell>
  621. <TableCell>
  622. <Typography variant="body2" fontWeight={500}>{line.operatorName}</Typography>
  623. </TableCell>
  624. <TableCell>
  625. <Typography variant="body2" fontWeight={500}>
  626. {line.startTime && line.durationInMinutes
  627. ? dayjs(line.startTime)
  628. .add(line.durationInMinutes, 'minute')
  629. .format('MM-DD HH:mm')
  630. : '-'}
  631. </Typography>
  632. </TableCell>
  633. <TableCell>
  634. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  635. <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
  636. <Typography variant="body2">
  637. {t("Processing Time")}: {line.durationInMinutes || 0}{t("mins")}
  638. </Typography>
  639. {processData?.jobOrderStatus === "planning" && (
  640. <IconButton
  641. size="small"
  642. onClick={() => {
  643. console.log("🖱️ Edit button clicked for line:", line.id);
  644. handleOpenTimeDialog(line.id);
  645. }}
  646. sx={{ padding: 0.5 }}
  647. >
  648. <EditIcon fontSize="small" />
  649. </IconButton>
  650. )}
  651. </Box>
  652. <Typography variant="body2">
  653. {t("Setup Time")}: {line.prepTimeInMinutes || 0} {t("mins")}
  654. </Typography>
  655. <Typography variant="body2">
  656. {t("Changeover Time")}: {line.postProdTimeInMinutes || 0} {t("mins")}
  657. </Typography>
  658. </Box>
  659. </TableCell>
  660. <TableCell align="center">
  661. {isCompleted ? (
  662. <Chip label={t("Completed")} color="success" size="small"
  663. onClick={async () => {
  664. setSelectedLineId(line.id);
  665. setShowOutputPage(false);
  666. setIsExecutingLine(true);
  667. await fetchProcessDetail();
  668. }}
  669. />
  670. ) : isInProgress ? (
  671. <Chip label={t("In Progress")} color="primary" size="small"
  672. onClick={async () => {
  673. setSelectedLineId(line.id);
  674. setShowOutputPage(false);
  675. setIsExecutingLine(true);
  676. await fetchProcessDetail();
  677. }} />
  678. ) : isPending ? (
  679. <Chip label={t("Pending")} color="default" size="small" />
  680. ) : isPaused ? (
  681. <Chip label={t("Paused")} color="warning" size="small" />
  682. ) : isPass ? (
  683. <Chip label={t("Pass")} color="success" size="small" />
  684. ) : (
  685. <Chip label={t("Unknown")} color="error" size="small" />
  686. )
  687. }
  688. </TableCell>
  689. {!fromJosave&&(
  690. <TableCell align="center">
  691. <Stack direction="row" spacing={1} justifyContent="center">
  692. {statusLower === 'pending' ? (
  693. <>
  694. <Button
  695. variant="contained"
  696. size="small"
  697. startIcon={<PlayArrowIcon />}
  698. onClick={() => handleStartLineWithScan(line.id)}
  699. >
  700. {t("Start")}
  701. </Button>
  702. <Button
  703. variant="outlined"
  704. size="small"
  705. color="success"
  706. onClick={() => handlePassLine(line.id)}
  707. disabled={isPassDisabled}
  708. >
  709. {t("Pass")}
  710. </Button>
  711. </>
  712. ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? (
  713. <>
  714. <Button
  715. variant="contained"
  716. size="small"
  717. startIcon={<CheckCircleIcon />}
  718. onClick={async () => {
  719. setSelectedLineId(line.id);
  720. setShowOutputPage(false);
  721. setIsExecutingLine(true);
  722. await fetchProcessDetail();
  723. }}
  724. >
  725. {t("View")}
  726. </Button>
  727. <Button
  728. variant="outlined"
  729. size="small"
  730. color="success"
  731. onClick={() => handlePassLine(line.id)}
  732. disabled={isPassDisabled}
  733. >
  734. {t("Pass")}
  735. </Button>
  736. </>
  737. ) : (
  738. <>
  739. <Button
  740. variant="outlined"
  741. size="small"
  742. onClick={async() => {
  743. setSelectedLineId(line.id);
  744. setIsExecutingLine(true);
  745. await fetchProcessDetail();
  746. }}
  747. >
  748. {t("View")}
  749. </Button>
  750. <Button
  751. variant="outlined"
  752. size="small"
  753. color="success"
  754. onClick={() => handlePassLine(line.id)}
  755. disabled={isPassDisabled}
  756. >
  757. {t("Pass")}
  758. </Button>
  759. </>
  760. )}
  761. </Stack>
  762. </TableCell>
  763. )}
  764. </TableRow>
  765. );
  766. })}
  767. </TableBody>
  768. </Table>
  769. </TableContainer>
  770. ) : (
  771. /* ========== 步骤执行视图 ========== */
  772. <ProductionProcessStepExecution
  773. lineId={selectedLineId}
  774. onBack={handleBackFromStep}
  775. processData={processData} // 添加
  776. allLines={lines} // 添加
  777. jobOrderId={jobOrderId} // 添加
  778. />
  779. )}
  780. </Paper>
  781. {/* QR 扫描对话框 */}
  782. <Dialog
  783. open={showScanDialog}
  784. onClose={() => {
  785. handleStopScan();
  786. setShowScanDialog(false);
  787. }}
  788. maxWidth="sm"
  789. fullWidth
  790. >
  791. <DialogTitle>{t("Scan Operator & Equipment")}</DialogTitle>
  792. <DialogContent>
  793. <Stack spacing={2} sx={{ mt: 2 }}>
  794. <Box>
  795. <Typography variant="body2" color="text.secondary">
  796. {scannedStaffNo
  797. ? `${t("Staff No")}: ${scannedStaffNo}`
  798. : t("Please scan staff no")
  799. }
  800. </Typography>
  801. </Box>
  802. <Box>
  803. <Typography variant="body2" color="text.secondary">
  804. {scannedEquipmentCode
  805. ? `${t("Equipment Code")}: ${scannedEquipmentCode}`
  806. : t("Please scan equipment code")
  807. }
  808. </Typography>
  809. </Box>
  810. <Button
  811. variant={isManualScanning ? "outlined" : "contained"}
  812. startIcon={<QrCodeIcon />}
  813. onClick={isManualScanning ? handleStopScan : () => scanningLineId && handleStartScan(scanningLineId)}
  814. color={isManualScanning ? "secondary" : "primary"}
  815. fullWidth
  816. >
  817. {isManualScanning ? t("Stop QR Scan") : t("Start QR Scan")}
  818. </Button>
  819. </Stack>
  820. </DialogContent>
  821. <DialogActions>
  822. <Button type="button" onClick={() => {
  823. handleStopScan();
  824. setShowScanDialog(false);
  825. }}>
  826. {t("Cancel")}
  827. </Button>
  828. <Button
  829. type="button"
  830. variant="contained"
  831. onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)}
  832. disabled={!scannedStaffNo }
  833. >
  834. {t("Submit & Start")}
  835. </Button>
  836. </DialogActions>
  837. </Dialog>
  838. <Dialog
  839. open={openTimeDialog}
  840. onClose={handleCloseTimeDialog} // 直接传递函数,不要包装
  841. fullWidth
  842. maxWidth="sm"
  843. >
  844. <DialogTitle>{t("Update Time Information")}</DialogTitle>
  845. <DialogContent>
  846. <Stack spacing={2} sx={{ mt: 1 }}>
  847. <TextField
  848. label={t("Processing Time (mins)")}
  849. type="number"
  850. fullWidth
  851. value={timeValues.durationInMinutes}
  852. onChange={(e) => {
  853. console.log("⌨️ Processing Time onChange:", {
  854. value: e.target.value,
  855. openTimeDialog,
  856. editingLineId,
  857. timestamp: new Date().toISOString()
  858. });
  859. const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0;
  860. setTimeValues(prev => ({
  861. ...prev,
  862. durationInMinutes: Math.max(0, value)
  863. }));
  864. }}
  865. inputProps={{
  866. min: 0,
  867. step: 1
  868. }}
  869. />
  870. <TextField
  871. label={t("Setup Time (mins)")}
  872. type="number"
  873. fullWidth
  874. value={timeValues.prepTimeInMinutes}
  875. onChange={(e) => {
  876. console.log("⌨️ Setup Time onChange:", {
  877. value: e.target.value,
  878. openTimeDialog,
  879. editingLineId,
  880. timestamp: new Date().toISOString()
  881. });
  882. const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0;
  883. setTimeValues(prev => ({
  884. ...prev,
  885. prepTimeInMinutes: Math.max(0, value)
  886. }));
  887. }}
  888. inputProps={{
  889. min: 0,
  890. step: 1
  891. }}
  892. />
  893. <TextField
  894. label={t("Changeover Time (mins)")}
  895. type="number"
  896. fullWidth
  897. value={timeValues.postProdTimeInMinutes}
  898. onChange={(e) => {
  899. console.log("⌨️ Changeover Time onChange:", {
  900. value: e.target.value,
  901. openTimeDialog,
  902. editingLineId,
  903. timestamp: new Date().toISOString()
  904. });
  905. const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0;
  906. setTimeValues(prev => ({
  907. ...prev,
  908. postProdTimeInMinutes: Math.max(0, value)
  909. }));
  910. }}
  911. inputProps={{
  912. min: 0,
  913. step: 1
  914. }}
  915. />
  916. </Stack>
  917. </DialogContent>
  918. <DialogActions>
  919. <Button
  920. type="button"
  921. onClick={handleCloseTimeDialog}
  922. >
  923. {t("Cancel")}
  924. </Button>
  925. <Button
  926. type="button"
  927. variant="contained"
  928. onClick={handleConfirmTimeUpdate}
  929. >
  930. {t("Save")}
  931. </Button>
  932. </DialogActions>
  933. </Dialog>
  934. </Box>
  935. );
  936. };
  937. export default ProductionProcessDetail;