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

ProductionProcessDetail.tsx 34 KiB

3ヶ月前
1ヶ月前
3週間前
1ヶ月前
3ヶ月前
2ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
3週間前
3ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
1ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
3週間前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
1ヶ月前
3週間前
1ヶ月前
3週間前
1ヶ月前
3週間前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
3ヶ月前
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  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;