FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

ProductionProcessDetail.tsx 30 KiB

1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
4週間前
4週間前
1ヶ月前
4週間前
4週間前
4週間前
4週間前
4週間前
4週間前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
1週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
4週間前
4週間前
4週間前
4週間前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
2週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1週間前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
  1. "use client";
  2. import React, { useCallback, useEffect, useState, useRef } from "react";
  3. import {
  4. Box,
  5. Button,
  6. Paper,
  7. Stack,
  8. Typography,
  9. TextField,
  10. Table,
  11. TableBody,
  12. TableCell,
  13. TableContainer,
  14. TableHead,
  15. TableRow,
  16. Chip,
  17. Card,
  18. CardContent,
  19. CircularProgress,
  20. Dialog,
  21. DialogTitle,
  22. DialogContent,
  23. DialogActions,
  24. } from "@mui/material";
  25. import QrCodeIcon from '@mui/icons-material/QrCode';
  26. import { useTranslation } from "react-i18next";
  27. import { Operator, Machine } from "@/app/api/jo";
  28. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  29. import { useSession } from "next-auth/react";
  30. import { SessionWithTokens } from "@/config/authConfig";
  31. import PlayArrowIcon from "@mui/icons-material/PlayArrow";
  32. import CheckCircleIcon from "@mui/icons-material/CheckCircle";
  33. import dayjs from "dayjs";
  34. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  35. import {
  36. // updateProductProcessLineQrscan,
  37. newUpdateProductProcessLineQrscan,
  38. fetchProductProcessLineDetail,
  39. JobOrderProcessLineDetailResponse,
  40. ProductProcessLineInfoResponse,
  41. startProductProcessLine,
  42. fetchProductProcessesByJobOrderId
  43. } from "@/app/api/jo/actions";
  44. import { fetchNameList, NameList } from "@/app/api/user/actions";
  45. import ProductionProcessStepExecution from "./ProductionProcessStepExecution";
  46. import ProductionOutputFormPage from "./ProductionOutputFormPage";
  47. import ProcessSummaryHeader from "./ProcessSummaryHeader";
  48. interface ProductProcessDetailProps {
  49. jobOrderId: number;
  50. onBack: () => void;
  51. fromJosave?: boolean;
  52. }
  53. const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
  54. jobOrderId,
  55. onBack,
  56. fromJosave,
  57. }) => {
  58. const { t } = useTranslation("common");
  59. const { data: session } = useSession() as { data: SessionWithTokens | null };
  60. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  61. const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  62. const [showOutputPage, setShowOutputPage] = useState(false);
  63. // 基本信息
  64. const [processData, setProcessData] = useState<any>(null);
  65. const [lines, setLines] = useState<ProductProcessLineInfoResponse[]>([]);
  66. const [loading, setLoading] = useState(false);
  67. // 选中的 line 和执行状态
  68. const [selectedLineId, setSelectedLineId] = useState<number | null>(null);
  69. const [isExecutingLine, setIsExecutingLine] = useState(false);
  70. const [isAutoSubmitting, setIsAutoSubmitting] = useState(false);
  71. // 扫描器状态
  72. const [isManualScanning, setIsManualScanning] = useState(false);
  73. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  74. const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null);
  75. const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null);
  76. // const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null);
  77. const [scannedStaffNo, setScannedStaffNo] = useState<string | null>(null);
  78. // const [scannedEquipmentDetailId, setScannedEquipmentDetailId] = useState<number | null>(null);
  79. const [scannedEquipmentCode, setScannedEquipmentCode] = useState<string | null>(null);
  80. const [scanningLineId, setScanningLineId] = useState<number | null>(null);
  81. const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null);
  82. const [showScanDialog, setShowScanDialog] = useState(false);
  83. const autoSubmitTimerRef = useRef<NodeJS.Timeout | null>(null);
  84. // 产出表单
  85. const [outputData, setOutputData] = useState({
  86. byproductName: "",
  87. byproductQty: "",
  88. byproductUom: "",
  89. scrapQty: "",
  90. scrapUom: "",
  91. defectQty: "",
  92. defectUom: "",
  93. outputFromProcessQty: "",
  94. outputFromProcessUom: "",
  95. });
  96. // 处理 QR 码扫描
  97. // 处理 QR 码扫描
  98. const handleBackFromStep = async () => {
  99. await fetchProcessDetail(); // 重新拉取最新的 process/lines
  100. setIsExecutingLine(false);
  101. setSelectedLineId(null);
  102. setShowOutputPage(false);
  103. };
  104. // 获取 process 和 lines 数据
  105. const fetchProcessDetail = useCallback(async () => {
  106. setLoading(true);
  107. try {
  108. console.log(`🔍 Loading process detail for JobOrderId: ${jobOrderId}`);
  109. // 使用 fetchProductProcessesByJobOrderId 获取基础数据
  110. const processesWithLines = await fetchProductProcessesByJobOrderId(jobOrderId);
  111. if (!processesWithLines || processesWithLines.length === 0) {
  112. throw new Error("No processes found for this job order");
  113. }
  114. // 如果有多个 process,取第一个(或者可以根据需要选择)
  115. const currentProcess = processesWithLines[0];
  116. setProcessData(currentProcess);
  117. // 使用 productProcessLines 字段(API 返回的字段名)
  118. const lines = (currentProcess as any).productProcessLines || [];
  119. setLines(lines);
  120. console.log(" Process data loaded:", currentProcess);
  121. console.log(" Lines loaded:", lines);
  122. } catch (error) {
  123. console.error("❌ Error loading process detail:", error);
  124. //alert(`无法加载 Job Order ID ${jobOrderId} 的生产流程。该记录可能不存在。`);
  125. onBack();
  126. } finally {
  127. setLoading(false);
  128. }
  129. }, [jobOrderId, onBack]);
  130. useEffect(() => {
  131. fetchProcessDetail();
  132. }, [fetchProcessDetail]);
  133. // 开始执行某个 line
  134. // 提交产出数据
  135. /*
  136. const processQrCode = useCallback((qrValue: string, lineId: number) => {
  137. // 操作员格式:{2fitestu1} - 键盘模拟输入(测试用)
  138. if (qrValue.match(/\{2fitestu(\d+)\}/)) {
  139. const match = qrValue.match(/\{2fitestu(\d+)\}/);
  140. const userId = parseInt(match![1]);
  141. fetchNameList().then((users: NameList[]) => {
  142. const user = users.find((u: NameList) => u.id === userId);
  143. if (user) {
  144. setScannedOperatorId(user.id);
  145. }
  146. });
  147. return;
  148. }
  149. // 设备格式:{2fiteste1} - 键盘模拟输入(测试用)
  150. if (qrValue.match(/\{2fiteste(\d+)\}/)) {
  151. const match = qrValue.match(/\{2fiteste(\d+)\}/);
  152. const equipmentId = parseInt(match![1]);
  153. setScannedEquipmentId(equipmentId);
  154. return;
  155. }
  156. // 正常 QR 扫描器扫描:格式为 "operatorId: 1" 或 "equipmentId: 1"
  157. const trimmedValue = qrValue.trim();
  158. // 检查 operatorId 格式
  159. const operatorMatch = trimmedValue.match(/^operatorId:\s*(\d+)$/i);
  160. if (operatorMatch) {
  161. const operatorId = parseInt(operatorMatch[1]);
  162. fetchNameList().then((users: NameList[]) => {
  163. const user = users.find((u: NameList) => u.id === operatorId);
  164. if (user) {
  165. setScannedOperatorId(user.id);
  166. } else {
  167. console.warn(`User with ID ${operatorId} not found`);
  168. }
  169. });
  170. return;
  171. }
  172. // 检查 equipmentId 格式
  173. const equipmentMatch = trimmedValue.match(/^equipmentId:\s*(\d+)$/i);
  174. if (equipmentMatch) {
  175. const equipmentId = parseInt(equipmentMatch[1]);
  176. setScannedEquipmentId(equipmentId);
  177. return;
  178. }
  179. // 其他格式处理(JSON、普通文本等)
  180. try {
  181. const qrData = JSON.parse(qrValue);
  182. // TODO: 处理 JSON 格式的 QR 码
  183. } catch {
  184. // 普通文本格式
  185. // TODO: 处理普通文本格式
  186. }
  187. }, []);
  188. */
  189. // 提交产出数据
  190. const processQrCode = useCallback((qrValue: string, lineId: number) => {
  191. // 设备快捷格式:{2fiteste数字} - 自动生成 equipmentTypeSubTypeEquipmentNo
  192. // 格式:{2fiteste数字} = line.equipment_name + "-数字號"
  193. // 例如:{2fiteste1} = "包裝機類-真空八爪魚機-1號"
  194. if (qrValue.match(/\{2fiteste(\d+)\}/)) {
  195. const match = qrValue.match(/\{2fiteste(\d+)\}/);
  196. const equipmentNo = parseInt(match![1]);
  197. // 根据 lineId 找到对应的 line
  198. const currentLine = lines.find(l => l.id === lineId);
  199. if (currentLine && currentLine.equipment_name) {
  200. const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`;
  201. setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo);
  202. console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`);
  203. } else {
  204. // 如果找不到 line,尝试从 API 获取 line detail
  205. console.warn(`Line with ID ${lineId} not found in current lines, fetching from API...`);
  206. fetchProductProcessLineDetail(lineId)
  207. .then((lineDetail) => {
  208. // 从 lineDetail 中获取 equipment_name
  209. const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || "";
  210. if (equipmentName) {
  211. const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`;
  212. setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo);
  213. console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`);
  214. } else {
  215. console.warn(`Equipment name not found in line detail for lineId: ${lineId}`);
  216. }
  217. })
  218. .catch((err) => {
  219. console.error(`Failed to fetch line detail for lineId ${lineId}:`, err);
  220. });
  221. }
  222. return;
  223. }
  224. // 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo
  225. // 例如:{2fitestu123} = staffNo: "123"
  226. // 例如:{2fitestustaff001} = staffNo: "staff001"
  227. if (qrValue.match(/\{2fitestu(.+)\}/)) {
  228. const match = qrValue.match(/\{2fitestu(.+)\}/);
  229. const staffNo = match![1];
  230. setScannedStaffNo(staffNo);
  231. return;
  232. }
  233. // 正常 QR 扫描器扫描格式
  234. const trimmedValue = qrValue.trim();
  235. // 检查 staffNo 格式:"staffNo: STAFF001" 或 "staffNo:STAFF001"
  236. const staffNoMatch = trimmedValue.match(/^staffNo:\s*(.+)$/i);
  237. if (staffNoMatch) {
  238. const staffNo = staffNoMatch[1].trim();
  239. setScannedStaffNo(staffNo);
  240. return;
  241. }
  242. // 检查 equipmentCode 格式
  243. const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo|equipmentCode):\s*(.+)$/i);
  244. if (equipmentCodeMatch) {
  245. const equipmentCode = equipmentCodeMatch[1].trim();
  246. setScannedEquipmentCode(equipmentCode);
  247. return;
  248. }
  249. // 其他格式处理(JSON、普通文本等)
  250. try {
  251. const qrData = JSON.parse(qrValue);
  252. if (qrData.staffNo) {
  253. setScannedStaffNo(String(qrData.staffNo));
  254. }
  255. if (qrData.equipmentTypeSubTypeEquipmentNo || qrData.equipmentCode) {
  256. setScannedEquipmentCode(
  257. String(qrData.equipmentTypeSubTypeEquipmentNo ?? qrData.equipmentCode)
  258. );
  259. }
  260. } catch {
  261. // 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode
  262. if (trimmedValue.length > 0) {
  263. if (trimmedValue.toUpperCase().startsWith("STAFF") || /^\d+$/.test(trimmedValue)) {
  264. // 可能是员工编号
  265. setScannedStaffNo(trimmedValue);
  266. } else if (trimmedValue.includes("-")) {
  267. // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號")
  268. setScannedEquipmentCode(trimmedValue);
  269. }
  270. }
  271. }
  272. }, [lines]);
  273. // 处理 QR 码扫描效果
  274. useEffect(() => {
  275. if (isManualScanning && qrValues.length > 0 && scanningLineId) {
  276. const latestQr = qrValues[qrValues.length - 1];
  277. if (processedQrCodes.has(latestQr)) {
  278. return;
  279. }
  280. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  281. processQrCode(latestQr, scanningLineId);
  282. }
  283. }, [qrValues, isManualScanning, scanningLineId, processedQrCodes, processQrCode]);
  284. const submitScanAndStart = useCallback(async (lineId: number) => {
  285. console.log("submitScanAndStart called with:", {
  286. lineId,
  287. scannedStaffNo,
  288. // scannedEquipmentTypeSubTypeEquipmentNo,
  289. scannedEquipmentCode,
  290. });
  291. if (!scannedStaffNo) {
  292. console.log("No staffNo, cannot submit");
  293. setIsAutoSubmitting(false);
  294. return false;
  295. }
  296. try {
  297. const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId);
  298. // ✅ 统一使用一个最终的 equipmentCode(优先用 scannedEquipmentCode,其次用 scannedEquipmentTypeSubTypeEquipmentNo)
  299. const effectiveEquipmentCode =
  300. scannedEquipmentCode ?? null;
  301. console.log("Submitting scan data with equipmentCode:", {
  302. productProcessLineId: lineId,
  303. staffNo: scannedStaffNo,
  304. equipmentCode: effectiveEquipmentCode,
  305. });
  306. const response = await newUpdateProductProcessLineQrscan({
  307. productProcessLineId: lineId,
  308. equipmentCode: effectiveEquipmentCode ?? "",
  309. staffNo: scannedStaffNo,
  310. });
  311. console.log("Scan submit response:", response);
  312. if (response && response.type === "error") {
  313. console.error("Scan validation failed:", response.message);
  314. alert(t(response.message) || t("Validation failed. Please check your input."));
  315. setIsAutoSubmitting(false);
  316. if (autoSubmitTimerRef.current) {
  317. clearTimeout(autoSubmitTimerRef.current);
  318. autoSubmitTimerRef.current = null;
  319. }
  320. return false;
  321. }
  322. console.log("Validation passed, starting line...");
  323. handleStopScan();
  324. setShowScanDialog(false);
  325. setIsAutoSubmitting(false);
  326. await handleStartLine(lineId);
  327. setSelectedLineId(lineId);
  328. setIsExecutingLine(true);
  329. await fetchProcessDetail();
  330. return true;
  331. } catch (error) {
  332. console.error("Error submitting scan:", error);
  333. alert("Failed to submit scan data. Please try again.");
  334. setIsAutoSubmitting(false);
  335. return false;
  336. }
  337. }, [
  338. scannedStaffNo,
  339. scannedEquipmentCode,
  340. lineDetailForScan,
  341. t,
  342. fetchProcessDetail,
  343. ]);
  344. const handleSubmitScanAndStart = useCallback(async (lineId: number) => {
  345. console.log("handleSubmitScanAndStart called with lineId:", lineId);
  346. if (!scannedStaffNo) {
  347. //alert(t("Please scan operator code first"));
  348. return;
  349. }
  350. // 如果正在自动提交,等待一下
  351. if (isAutoSubmitting) {
  352. console.log("Already auto-submitting, skipping manual submit");
  353. return;
  354. }
  355. await submitScanAndStart(lineId);
  356. }, [scannedOperatorId, isAutoSubmitting, submitScanAndStart, t]);
  357. // 开始扫描
  358. const handleStartScan = useCallback((lineId: number) => {
  359. if (autoSubmitTimerRef.current) {
  360. clearTimeout(autoSubmitTimerRef.current);
  361. autoSubmitTimerRef.current = null;
  362. }
  363. setScanningLineId(lineId);
  364. setIsManualScanning(true);
  365. setProcessedQrCodes(new Set());
  366. setScannedOperatorId(null);
  367. setScannedEquipmentId(null);
  368. setScannedStaffNo(null); // ✅ Add this
  369. setScannedEquipmentCode(null);
  370. setIsAutoSubmitting(false); // 添加:重置自动提交状态
  371. setLineDetailForScan(null);
  372. // 获取 line detail 以获取 bomProcessEquipmentId
  373. fetchProductProcessLineDetail(lineId)
  374. .then(setLineDetailForScan)
  375. .catch(err => {
  376. console.error("Failed to load line detail", err);
  377. // 不阻止扫描继续,line detail 不是必需的
  378. });
  379. startScan();
  380. }, [startScan]);
  381. // 停止扫描
  382. const handleStopScan = useCallback(() => {
  383. console.log("🛑 Stopping scan");
  384. // 清除定时器
  385. if (autoSubmitTimerRef.current) {
  386. clearTimeout(autoSubmitTimerRef.current);
  387. autoSubmitTimerRef.current = null;
  388. }
  389. setIsManualScanning(false);
  390. setIsAutoSubmitting(false);
  391. setScannedStaffNo(null); // ✅ Add this
  392. setScannedEquipmentCode(null);
  393. stopScan();
  394. resetScan();
  395. }, [stopScan, resetScan]);
  396. // 开始执行某个 line(原有逻辑,现在在验证通过后调用)
  397. const handleStartLine = async (lineId: number) => {
  398. try {
  399. await startProductProcessLine(lineId);
  400. } catch (error) {
  401. console.error("Error starting line:", error);
  402. //alert("Failed to start line. Please try again.");
  403. }
  404. };
  405. // 提交扫描结果并验证
  406. /*
  407. useEffect(() => {
  408. console.log("Auto-submit check:", {
  409. scanningLineId,
  410. scannedStaffNo,
  411. scannedEquipmentCode,
  412. isAutoSubmitting,
  413. isManualScanning,
  414. });
  415. // ✅ Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId
  416. if (
  417. scanningLineId &&
  418. scannedStaffNo !== null &&
  419. (scannedEquipmentCode !== null) &&
  420. !isAutoSubmitting &&
  421. isManualScanning
  422. ) {
  423. console.log("Auto-submitting triggered!");
  424. setIsAutoSubmitting(true);
  425. // 清除之前的定时器(如果有)
  426. if (autoSubmitTimerRef.current) {
  427. clearTimeout(autoSubmitTimerRef.current);
  428. }
  429. // 延迟一点时间,让用户看到两个都扫描完成了
  430. autoSubmitTimerRef.current = setTimeout(() => {
  431. console.log("Executing auto-submit...");
  432. submitScanAndStart(scanningLineId);
  433. autoSubmitTimerRef.current = null;
  434. }, 500);
  435. }
  436. // 清理函数:只在组件卸载或条件不再满足时清除定时器
  437. return () => {
  438. // 注意:这里不立即清除定时器,因为我们需要它执行
  439. // 只在组件卸载时清除
  440. };
  441. }, [scanningLineId, scannedStaffNo, scannedEquipmentCode, isAutoSubmitting, isManualScanning, submitScanAndStart]);
  442. */
  443. useEffect(() => {
  444. return () => {
  445. if (autoSubmitTimerRef.current) {
  446. clearTimeout(autoSubmitTimerRef.current);
  447. }
  448. };
  449. }, []);
  450. const handleStartLineWithScan = async (lineId: number) => {
  451. console.log("🚀 Starting line with scan for lineId:", lineId);
  452. // 确保状态完全重置
  453. setIsAutoSubmitting(false);
  454. setScannedOperatorId(null);
  455. setScannedEquipmentId(null);
  456. setProcessedQrCodes(new Set());
  457. setScannedStaffNo(null);
  458. setScannedEquipmentCode(null);
  459. setProcessedQrCodes(new Set());
  460. // 清除之前的定时器
  461. if (autoSubmitTimerRef.current) {
  462. clearTimeout(autoSubmitTimerRef.current);
  463. autoSubmitTimerRef.current = null;
  464. }
  465. setScanningLineId(lineId);
  466. setShowScanDialog(true);
  467. handleStartScan(lineId);
  468. };
  469. const selectedLine = lines.find(l => l.id === selectedLineId);
  470. if (loading) {
  471. return (
  472. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  473. <CircularProgress/>
  474. </Box>
  475. );
  476. }
  477. return (
  478. <Box>
  479. {/*
  480. <Box sx={{ mb: 2 }}>
  481. <Button variant="outlined" onClick={onBack}>
  482. {t("Back to List")}
  483. </Button>
  484. </Box>
  485. <Paper sx={{ p: 3, mb: 3 }}>
  486. <Typography variant="h6" gutterBottom fontWeight="bold">
  487. {t("Production Process Information")}
  488. </Typography>
  489. <Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
  490. <Typography variant="subtitle1">
  491. <strong>{t("Job Order Code")}:</strong> {processData?.jobOrderCode}
  492. </Typography>
  493. <Typography variant="subtitle1">
  494. <strong>{t("Is Dark")}:</strong> {processData?.isDark}
  495. </Typography>
  496. <Typography variant="subtitle1">
  497. <strong>{t("Is Dense")}:</strong> {processData?.isDense}
  498. </Typography>
  499. <Typography variant="subtitle1">
  500. <strong>{t("Is Float")}:</strong> {processData?.isFloat}
  501. </Typography>
  502. <Typography variant="subtitle1">
  503. <strong>{t("Output Qty")}:</strong> {processData?.outputQty+" "+"("+processData?.outputQtyUom +")"}
  504. </Typography>
  505. <Box>
  506. <strong>{t("Status")}:</strong>{" "}
  507. <Chip
  508. label={
  509. processData?.status === 'completed' ? t("Completed") : processData?.status === 'IN_PROGRESS' ? t("In Progress") : processData?.status === 'pending' ? t("Pending") : t("Unknown")
  510. }
  511. color={processData?.status === 'completed' ? 'success' : processData?.status === 'IN_PROGRESS' ? 'success' : processData?.status === 'pending' ? 'primary' : 'error'}
  512. size="small"
  513. />
  514. </Box>
  515. <Typography variant="subtitle1">
  516. <strong>{t("Date")}:</strong> {dayjs(processData?.date).format(OUTPUT_DATE_FORMAT)}
  517. </Typography>
  518. <Typography variant="subtitle1">
  519. <strong>{t("Total Steps")}:</strong> {lines.length}
  520. </Typography>
  521. </Stack>
  522. </Paper>
  523. */}
  524. {/* ========== 第二部分:Process Lines ========== */}
  525. <Paper sx={{ p: 3 }}>
  526. <Typography variant="h6" gutterBottom fontWeight="bold">
  527. {t("Production Process Steps")}
  528. </Typography>
  529. <ProcessSummaryHeader processData={processData} />
  530. {!isExecutingLine ? (
  531. /* ========== 步骤列表视图 ========== */
  532. <TableContainer>
  533. <Table>
  534. <TableHead>
  535. <TableRow>
  536. <TableCell>{t("Seq")}</TableCell>
  537. <TableCell>{t("Step Name")}</TableCell>
  538. <TableCell>{t("Description")}</TableCell>
  539. <TableCell>{t("EquipmentType-EquipmentName-Code")}</TableCell>
  540. <TableCell>{t("Operator")}</TableCell>
  541. {/*}
  542. <TableCell>{t("Processing Time (mins)")}</TableCell>
  543. <TableCell>{t("Setup Time (mins)")}</TableCell>
  544. <TableCell>{t("Changeover Time (mins)")}</TableCell>
  545. */}
  546. <TableCell>
  547. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  548. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  549. {t("Time Information(mins)")}
  550. </Typography>
  551. {/*
  552. <Typography variant="caption" sx={{ color: 'text.secondary' }}>
  553. {t("Processing Time")}-
  554. </Typography>
  555. <Typography variant="caption" sx={{ color: 'text.secondary' }}>
  556. {t("Setup Time")} - {t("Changeover Time")}
  557. </Typography>
  558. */}
  559. </Box>
  560. </TableCell>
  561. <TableCell align="center">{t("Status")}</TableCell>
  562. {!fromJosave&&(<TableCell align="center">{t("Action")}</TableCell>)}
  563. </TableRow>
  564. </TableHead>
  565. <TableBody>
  566. {lines.map((line) => {
  567. const status = (line as any).status || '';
  568. const statusLower = status.toLowerCase();
  569. const equipmentName = line.equipment_name || "-";
  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. return (
  575. <TableRow key={line.id}>
  576. <TableCell>{line.seqNo}</TableCell>
  577. <TableCell>
  578. <Typography fontWeight={500}>{line.name}</Typography>
  579. </TableCell>
  580. <TableCell><Typography fontWeight={500}>{line.description || "-"}</Typography></TableCell>
  581. <TableCell><Typography fontWeight={500}>{line.equipmentDetailCode||equipmentName}</Typography></TableCell>
  582. <TableCell><Typography fontWeight={500}>{line.operatorName}</Typography></TableCell>
  583. {/*
  584. <TableCell><Typography fontWeight={500}>{line.durationInMinutes} </Typography></TableCell>
  585. <TableCell><Typography fontWeight={500}>{line.prepTimeInMinutes} </Typography></TableCell>
  586. <TableCell><Typography fontWeight={500}>{line.postProdTimeInMinutes} </Typography></TableCell>
  587. */}
  588. <TableCell>
  589. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  590. <Typography variant="body2" >
  591. {t("Processing Time")}: {line.durationInMinutes}{t("mins")}
  592. </Typography>
  593. <Typography variant="body2" >
  594. {t("Setup Time")}: {line.prepTimeInMinutes} {t("mins")}
  595. </Typography>
  596. <Typography variant="body2" >
  597. {t("Changeover Time")}: {line.postProdTimeInMinutes} {t("mins")}
  598. </Typography>
  599. </Box>
  600. </TableCell>
  601. <TableCell align="center">
  602. {isCompleted ? (
  603. <Chip label={t("Completed")} color="success" size="small"
  604. onClick={async () => {
  605. setSelectedLineId(line.id);
  606. setShowOutputPage(false); // 不显示输出页面
  607. setIsExecutingLine(true);
  608. await fetchProcessDetail();
  609. }}
  610. />
  611. ) : isInProgress ? (
  612. <Chip label={t("In Progress")} color="primary" size="small"
  613. onClick={async () => {
  614. setSelectedLineId(line.id);
  615. setShowOutputPage(false); // 不显示输出页面
  616. setIsExecutingLine(true);
  617. await fetchProcessDetail();
  618. }} />
  619. ) : isPending ? (
  620. <Chip label={t("Pending")} color="default" size="small" />
  621. ) : isPaused ? (
  622. <Chip label={t("Paused")} color="warning" size="small" />
  623. ) : (
  624. <Chip label={t("Unknown")} color="error" size="small" />
  625. )}
  626. </TableCell>
  627. {!fromJosave&&(
  628. <TableCell align="center">
  629. {statusLower === 'pending' ? (
  630. <Button
  631. variant="contained"
  632. size="small"
  633. startIcon={<PlayArrowIcon />}
  634. onClick={() => handleStartLineWithScan(line.id)}
  635. >
  636. {t("Start")}
  637. </Button>
  638. ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? (
  639. <Button
  640. variant="contained"
  641. size="small"
  642. startIcon={<CheckCircleIcon />}
  643. onClick={async () => {
  644. setSelectedLineId(line.id);
  645. setIsExecutingLine(true);
  646. await fetchProcessDetail();
  647. }}
  648. >
  649. {t("View")}
  650. </Button>
  651. ) : (
  652. <Button
  653. variant="outlined"
  654. size="small"
  655. onClick={async() => {
  656. setSelectedLineId(line.id);
  657. setIsExecutingLine(true);
  658. await fetchProcessDetail();
  659. }}
  660. >
  661. {t("View")}
  662. </Button>
  663. )}
  664. </TableCell>
  665. )}
  666. </TableRow>
  667. );
  668. })}
  669. </TableBody>
  670. </Table>
  671. </TableContainer>
  672. ) : (
  673. /* ========== 步骤执行视图 ========== */
  674. <ProductionProcessStepExecution
  675. lineId={selectedLineId}
  676. onBack={handleBackFromStep}
  677. //onClose={() => {
  678. // setIsExecutingLine(false)
  679. // setSelectedLineId(null)
  680. //}}
  681. //onOutputSubmitted={async () => {
  682. // await fetchProcessDetail()
  683. //}}
  684. />
  685. )}
  686. </Paper>
  687. {/* QR 扫描对话框 */}
  688. <Dialog
  689. open={showScanDialog}
  690. onClose={() => {
  691. handleStopScan();
  692. setShowScanDialog(false);
  693. }}
  694. maxWidth="sm"
  695. fullWidth
  696. >
  697. <DialogTitle>{t("Scan Operator & Equipment")}</DialogTitle>
  698. <DialogContent>
  699. <Stack spacing={2} sx={{ mt: 2 }}>
  700. <Box>
  701. <Typography variant="body2" color="text.secondary">
  702. {scannedStaffNo
  703. ? `${t("Staff No")}: ${scannedStaffNo}`
  704. : t("Please scan staff no")
  705. }
  706. </Typography>
  707. </Box>
  708. <Box>
  709. <Typography variant="body2" color="text.secondary">
  710. {/* ✅ Show both options */}
  711. {scannedEquipmentCode
  712. ? `${t("Equipment Code")}: ${scannedEquipmentCode}`
  713. : t("Please scan equipment code")
  714. }
  715. </Typography>
  716. </Box>
  717. <Button
  718. variant={isManualScanning ? "outlined" : "contained"}
  719. startIcon={<QrCodeIcon />}
  720. onClick={isManualScanning ? handleStopScan : () => scanningLineId && handleStartScan(scanningLineId)}
  721. color={isManualScanning ? "secondary" : "primary"}
  722. fullWidth
  723. >
  724. {isManualScanning ? t("Stop QR Scan") : t("Start QR Scan")}
  725. </Button>
  726. </Stack>
  727. </DialogContent>
  728. <DialogActions>
  729. <Button onClick={() => {
  730. handleStopScan();
  731. setShowScanDialog(false);
  732. }}>
  733. {t("Cancel")}
  734. </Button>
  735. <Button
  736. variant="contained"
  737. onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)}
  738. disabled={!scannedStaffNo }
  739. >
  740. {t("Submit & Start")}
  741. </Button>
  742. </DialogActions>
  743. </Dialog>
  744. </Box>
  745. );
  746. };
  747. export default ProductionProcessDetail;