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.
 
 

537 line
20 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Paper,
  6. Stack,
  7. Typography,
  8. TextField,
  9. Table,
  10. TableBody,
  11. TableCell,
  12. TableHead,
  13. TableRow,
  14. Card,
  15. CardContent,
  16. Grid,
  17. } from "@mui/material";
  18. import QrCodeIcon from '@mui/icons-material/QrCode';
  19. import CheckCircleIcon from "@mui/icons-material/CheckCircle";
  20. import StopIcon from "@mui/icons-material/Stop";
  21. import PauseIcon from "@mui/icons-material/Pause";
  22. import PlayArrowIcon from "@mui/icons-material/PlayArrow";
  23. import { useTranslation } from "react-i18next";
  24. import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest} from "@/app/api/jo/actions";
  25. import { Operator, Machine } from "@/app/api/jo";
  26. import React, { useCallback, useEffect, useState } from "react";
  27. import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
  28. import { fetchNameList, NameList } from "@/app/api/user/actions";
  29. interface ProductionProcessStepExecutionProps {
  30. lineId: number | null
  31. onBack: () => void
  32. //onClose: () => void
  33. // onOutputSubmitted: () => Promise<void>
  34. }
  35. const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({
  36. lineId,
  37. onBack,
  38. }) => {
  39. const { t } = useTranslation();
  40. const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null);
  41. const isCompleted = lineDetail?.status === "Completed";
  42. const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & {
  43. byproductName: string;
  44. byproductQty: number;
  45. byproductUom: string;
  46. }>({
  47. productProcessLineId: lineId ?? 0,
  48. outputFromProcessQty: 0,
  49. outputFromProcessUom: "",
  50. defectQty: 0,
  51. defectUom: "",
  52. scrapQty: 0,
  53. scrapUom: "",
  54. byproductName: "",
  55. byproductQty: 0,
  56. byproductUom: ""
  57. });
  58. const [isManualScanning, setIsManualScanning] = useState(false);
  59. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  60. const [scannedOperators, setScannedOperators] = useState<Operator[]>([]);
  61. const [scannedMachines, setScannedMachines] = useState<Machine[]>([]);
  62. const [isPaused, setIsPaused] = useState(false);
  63. const [showOutputTable, setShowOutputTable] = useState(false);
  64. const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  65. const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-";
  66. // 检查是否两个都已扫描
  67. //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId;
  68. useEffect(() => {
  69. if (!lineId) {
  70. setLineDetail(null);
  71. return;
  72. }
  73. fetchProductProcessLineDetail(lineId)
  74. .then((detail) => {
  75. setLineDetail(detail as any);
  76. // 初始化 outputData 从 lineDetail
  77. setOutputData(prev => ({
  78. ...prev,
  79. productProcessLineId: detail.id,
  80. outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言
  81. outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言
  82. defectQty: detail.defectQty || 0,
  83. defectUom: detail.defectUom || "",
  84. scrapQty: detail.scrapQty || 0,
  85. scrapUom: detail.scrapUom || "",
  86. byproductName: detail.byproductName || "",
  87. byproductQty: detail.byproductQty || 0,
  88. byproductUom: detail.byproductUom || ""
  89. }));
  90. })
  91. .catch(err => {
  92. console.error("Failed to load line detail", err);
  93. setLineDetail(null);
  94. });
  95. }, [lineId]);
  96. const handleSubmitOutput = async () => {
  97. if (!lineDetail?.id) return;
  98. try {
  99. // 直接使用 actions.ts 中定义的函数
  100. await updateProductProcessLineQty({
  101. productProcessLineId: lineDetail?.id || 0 as number,
  102. byproductName: outputData.byproductName,
  103. byproductQty: outputData.byproductQty,
  104. byproductUom: outputData.byproductUom,
  105. outputFromProcessQty: outputData.outputFromProcessQty,
  106. outputFromProcessUom: outputData.outputFromProcessUom,
  107. // outputFromProcessUom: outputData.outputFromProcessUom,
  108. defectQty: outputData.defectQty,
  109. defectUom: outputData.defectUom,
  110. scrapQty: outputData.scrapQty,
  111. scrapUom: outputData.scrapUom,
  112. });
  113. console.log(" Output data submitted successfully");
  114. fetchProductProcessLineDetail(lineDetail.id)
  115. .then((detail) => {
  116. setLineDetail(detail as any);
  117. // 初始化 outputData 从 lineDetail
  118. setOutputData(prev => ({
  119. ...prev,
  120. productProcessLineId: detail.id,
  121. outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言
  122. outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言
  123. defectQty: detail.defectQty || 0,
  124. defectUom: detail.defectUom || "",
  125. scrapQty: detail.scrapQty || 0,
  126. scrapUom: detail.scrapUom || "",
  127. byproductName: detail.byproductName || "",
  128. byproductQty: detail.byproductQty || 0,
  129. byproductUom: detail.byproductUom || ""
  130. }));
  131. })
  132. .catch(err => {
  133. console.error("Failed to load line detail", err);
  134. setLineDetail(null);
  135. });
  136. } catch (error) {
  137. console.error("Error submitting output:", error);
  138. alert("Failed to submit output data. Please try again.");
  139. }
  140. };
  141. // 处理 QR 码扫描效果
  142. useEffect(() => {
  143. if (isManualScanning && qrValues.length > 0 && lineDetail?.id) {
  144. const latestQr = qrValues[qrValues.length - 1];
  145. if (processedQrCodes.has(latestQr)) {
  146. return;
  147. }
  148. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  149. //processQrCode(latestQr);
  150. }
  151. }, [qrValues, isManualScanning, lineDetail?.id, processedQrCodes]);
  152. // 开始扫描
  153. const handlePause = () => {
  154. setIsPaused(true);
  155. };
  156. const handleContinue = () => {
  157. setIsPaused(false);
  158. };
  159. const handleStop = () => {
  160. setIsPaused(false);
  161. // TODO: 调用停止流程的 API
  162. };
  163. return (
  164. <Box>
  165. <Box sx={{ mb: 2 }}>
  166. <Button variant="outlined" onClick={onBack}>
  167. {t("Back to List")}
  168. </Button>
  169. </Box>
  170. {/* 如果已完成,显示合并的视图 */}
  171. {isCompleted ? (
  172. <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}>
  173. <CardContent>
  174. <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold">
  175. {t("Completed Step")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo})
  176. </Typography>
  177. {/*<Divider sx={{ my: 2 }} />*/}
  178. {/* 步骤信息部分 */}
  179. <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
  180. {t("Step Information")}
  181. </Typography>
  182. <Grid container spacing={2} sx={{ mb: 3 }}>
  183. <Grid item xs={12} md={6}>
  184. <Typography variant="body2" color="text.secondary">
  185. <strong>{t("Description")}:</strong> {lineDetail?.description || "-"}
  186. </Typography>
  187. </Grid>
  188. <Grid item xs={12} md={6}>
  189. <Typography variant="body2" color="text.secondary">
  190. <strong>{t("Operator")}:</strong> {lineDetail?.operatorName || "-"}
  191. </Typography>
  192. </Grid>
  193. <Grid item xs={12} md={6}>
  194. <Typography variant="body2" color="text.secondary">
  195. <strong>{t("Equipment")}:</strong> {equipmentName}
  196. </Typography>
  197. </Grid>
  198. <Grid item xs={12} md={6}>
  199. <Typography variant="body2" color="text.secondary">
  200. <strong>{t("Status")}:</strong> {lineDetail?.status || "-"}
  201. </Typography>
  202. </Grid>
  203. </Grid>
  204. {/*<Divider sx={{ my: 2 }} />*/}
  205. {/* 产出数据部分 */}
  206. <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
  207. {t("Production Output Data")}
  208. </Typography>
  209. <Table size="small" sx={{ mt: 2 }}>
  210. <TableHead>
  211. <TableRow>
  212. <TableCell width="30%"><strong>{t("Type")}</strong></TableCell>
  213. <TableCell width="35%"><strong>{t("Quantity")}</strong></TableCell>
  214. <TableCell width="35%"><strong>{t("Unit")}</strong></TableCell>
  215. </TableRow>
  216. </TableHead>
  217. <TableBody>
  218. {/* Output from Process */}
  219. <TableRow>
  220. <TableCell>
  221. <Typography fontWeight={500}>{t("Output from Process")}</Typography>
  222. </TableCell>
  223. <TableCell>
  224. <Typography>{lineDetail?.outputFromProcessQty || 0}</Typography>
  225. </TableCell>
  226. <TableCell>
  227. <Typography>{lineDetail?.outputFromProcessUom || "-"}</Typography>
  228. </TableCell>
  229. </TableRow>
  230. {/* By-product */}
  231. {lineDetail?.byproductQty && lineDetail.byproductQty > 0 && (
  232. <TableRow>
  233. <TableCell>
  234. <Typography fontWeight={500}>{t("By-product")}</Typography>
  235. {lineDetail.byproductName && (
  236. <Typography variant="caption" color="text.secondary">
  237. ({lineDetail.byproductName})
  238. </Typography>
  239. )}
  240. </TableCell>
  241. <TableCell>
  242. <Typography>{lineDetail.byproductQty}</Typography>
  243. </TableCell>
  244. <TableCell>
  245. <Typography>{lineDetail.byproductUom || "-"}</Typography>
  246. </TableCell>
  247. </TableRow>
  248. )}
  249. {/* Defect */}
  250. {lineDetail?.defectQty && lineDetail.defectQty > 0 && (
  251. <TableRow sx={{ bgcolor: 'warning.50' }}>
  252. <TableCell>
  253. <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
  254. </TableCell>
  255. <TableCell>
  256. <Typography>{lineDetail.defectQty}</Typography>
  257. </TableCell>
  258. <TableCell>
  259. <Typography>{lineDetail.defectUom || "-"}</Typography>
  260. </TableCell>
  261. </TableRow>
  262. )}
  263. {/* Scrap */}
  264. {lineDetail?.scrapQty && lineDetail.scrapQty > 0 && (
  265. <TableRow sx={{ bgcolor: 'error.50' }}>
  266. <TableCell>
  267. <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
  268. </TableCell>
  269. <TableCell>
  270. <Typography>{lineDetail.scrapQty}</Typography>
  271. </TableCell>
  272. <TableCell>
  273. <Typography>{lineDetail.scrapUom || "-"}</Typography>
  274. </TableCell>
  275. </TableRow>
  276. )}
  277. </TableBody>
  278. </Table>
  279. </CardContent>
  280. </Card>
  281. ) : (
  282. <>
  283. {/* 如果未完成,显示原来的两个部分 */}
  284. {/* 当前步骤信息 */}
  285. <Grid container spacing={2} sx={{ mb: 3 }}>
  286. <Grid item xs={12} md={6}>
  287. <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}>
  288. <CardContent>
  289. <Typography variant="h6" color="primary.main" gutterBottom>
  290. {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo})
  291. </Typography>
  292. <Typography variant="body2" color="text.secondary">
  293. {lineDetail?.description}
  294. </Typography>
  295. <Typography variant="body2" color="text.secondary">
  296. {t("Operator")}: {lineDetail?.operatorName || "-"}
  297. </Typography>
  298. <Typography variant="body2" color="text.secondary">
  299. {t("Equipment")}: {equipmentName}
  300. </Typography>
  301. <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}>
  302. <Button
  303. variant="contained"
  304. color="error"
  305. startIcon={<StopIcon />}
  306. onClick={handleStop}
  307. >
  308. {t("Stop")}
  309. </Button>
  310. {!isPaused ? (
  311. <Button
  312. variant="contained"
  313. color="warning"
  314. startIcon={<PauseIcon />}
  315. onClick={handlePause}
  316. >
  317. {t("Pause")}
  318. </Button>
  319. ) : (
  320. <Button
  321. variant="contained"
  322. color="success"
  323. startIcon={<PlayArrowIcon />}
  324. onClick={handleContinue}
  325. >
  326. {t("Continue")}
  327. </Button>
  328. )}
  329. </Stack>
  330. </CardContent>
  331. </Card>
  332. </Grid>
  333. </Grid>
  334. {/* ========== 产出输入表单 ========== */}
  335. <Box>
  336. <Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
  337. <Typography variant="h6" fontWeight={600}>
  338. {t("Production Output Data Entry")}
  339. </Typography>
  340. <Button
  341. variant="outlined"
  342. onClick={() => setShowOutputTable(!showOutputTable)}
  343. >
  344. {showOutputTable ? t("Hide Table") : t("Show Table")}
  345. </Button>
  346. </Box>
  347. {showOutputTable && (
  348. <Paper sx={{ p: 3, bgcolor: 'grey.50' }}>
  349. <Table size="small">
  350. <TableHead>
  351. <TableRow>
  352. <TableCell width="30%">{t("Type")}</TableCell>
  353. <TableCell width="35%">{t("Quantity")}</TableCell>
  354. <TableCell width="35%">{t("Unit")}</TableCell>
  355. </TableRow>
  356. </TableHead>
  357. <TableBody>
  358. {/* start line output */}
  359. <TableRow>
  360. <TableCell>
  361. <Typography fontWeight={500}>{t("Output from Process")}</Typography>
  362. </TableCell>
  363. <TableCell>
  364. <TextField
  365. type="number"
  366. fullWidth
  367. size="small"
  368. value={outputData.outputFromProcessQty}
  369. onChange={(e) => setOutputData({
  370. ...outputData,
  371. outputFromProcessQty: parseInt(e.target.value) || 0
  372. })}
  373. />
  374. </TableCell>
  375. <TableCell>
  376. <TextField
  377. fullWidth
  378. size="small"
  379. value={outputData.outputFromProcessUom}
  380. onChange={(e) => setOutputData({
  381. ...outputData,
  382. outputFromProcessUom: e.target.value
  383. })}
  384. />
  385. </TableCell>
  386. </TableRow>
  387. {/* byproduct */}
  388. <TableRow>
  389. <TableCell>
  390. <Stack>
  391. <Typography fontWeight={500}>{t("By-product")}</Typography>
  392. </Stack>
  393. </TableCell>
  394. <TableCell>
  395. <TextField
  396. type="number"
  397. fullWidth
  398. size="small"
  399. value={outputData.byproductQty}
  400. onChange={(e) => setOutputData({
  401. ...outputData,
  402. byproductQty: parseInt(e.target.value) || 0
  403. })}
  404. />
  405. </TableCell>
  406. <TableCell>
  407. <TextField
  408. fullWidth
  409. size="small"
  410. value={outputData.byproductUom}
  411. onChange={(e) => setOutputData({
  412. ...outputData,
  413. byproductUom: e.target.value
  414. })}
  415. />
  416. </TableCell>
  417. </TableRow>
  418. {/* defect */}
  419. <TableRow sx={{ bgcolor: 'warning.50' }}>
  420. <TableCell>
  421. <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
  422. </TableCell>
  423. <TableCell>
  424. <TextField
  425. type="number"
  426. fullWidth
  427. size="small"
  428. value={outputData.defectQty}
  429. onChange={(e) => setOutputData({
  430. ...outputData,
  431. defectQty: parseInt(e.target.value) || 0
  432. })}
  433. />
  434. </TableCell>
  435. <TableCell>
  436. <TextField
  437. fullWidth
  438. size="small"
  439. value={outputData.defectUom}
  440. onChange={(e) => setOutputData({
  441. ...outputData,
  442. defectUom: e.target.value
  443. })}
  444. />
  445. </TableCell>
  446. </TableRow>
  447. {/* scrap */}
  448. <TableRow sx={{ bgcolor: 'error.50' }}>
  449. <TableCell>
  450. <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
  451. </TableCell>
  452. <TableCell>
  453. <TextField
  454. type="number"
  455. fullWidth
  456. size="small"
  457. value={outputData.scrapQty}
  458. onChange={(e) => setOutputData({
  459. ...outputData,
  460. scrapQty: parseInt(e.target.value) || 0
  461. })}
  462. />
  463. </TableCell>
  464. <TableCell>
  465. <TextField
  466. fullWidth
  467. size="small"
  468. value={outputData.scrapUom}
  469. onChange={(e) => setOutputData({
  470. ...outputData,
  471. scrapUom: e.target.value
  472. })}
  473. />
  474. </TableCell>
  475. </TableRow>
  476. </TableBody>
  477. </Table>
  478. {/* submit button */}
  479. <Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
  480. <Button
  481. variant="outlined"
  482. onClick={() => setShowOutputTable(false)}
  483. >
  484. {t("Cancel")}
  485. </Button>
  486. <Button
  487. variant="contained"
  488. startIcon={<CheckCircleIcon />}
  489. onClick={handleSubmitOutput}
  490. >
  491. {t("Complete Step")}
  492. </Button>
  493. </Box>
  494. </Paper>
  495. )}
  496. </Box>
  497. </>
  498. )}
  499. </Box>
  500. );
  501. };
  502. export default ProductionProcessStepExecution;