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.
 
 

651 line
25 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. const [remainingTime, setRemainingTime] = useState<string | null>(null);
  67. // 检查是否两个都已扫描
  68. //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId;
  69. useEffect(() => {
  70. if (!lineId) {
  71. setLineDetail(null);
  72. return;
  73. }
  74. fetchProductProcessLineDetail(lineId)
  75. .then((detail) => {
  76. setLineDetail(detail as any);
  77. // 初始化 outputData 从 lineDetail
  78. setOutputData(prev => ({
  79. ...prev,
  80. productProcessLineId: detail.id,
  81. outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言
  82. outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言
  83. defectQty: detail.defectQty || 0,
  84. defectUom: detail.defectUom || "",
  85. scrapQty: detail.scrapQty || 0,
  86. scrapUom: detail.scrapUom || "",
  87. byproductName: detail.byproductName || "",
  88. byproductQty: detail.byproductQty || 0,
  89. byproductUom: detail.byproductUom || ""
  90. }));
  91. })
  92. .catch(err => {
  93. console.error("Failed to load line detail", err);
  94. setLineDetail(null);
  95. });
  96. }, [lineId]);
  97. useEffect(() => {
  98. if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) {
  99. setRemainingTime(null);
  100. return;
  101. }
  102. const start = new Date(lineDetail.startTime as any);
  103. const end = new Date(start.getTime() + lineDetail.durationInMinutes * 60_000);
  104. const update = () => {
  105. const diff = end.getTime() - Date.now();
  106. if (diff <= 0) {
  107. setRemainingTime("00:00");
  108. return;
  109. }
  110. const minutes = Math.floor(diff / 60000).toString().padStart(2, "0");
  111. const seconds = Math.floor((diff % 60000) / 1000).toString().padStart(2, "0");
  112. setRemainingTime(`${minutes}:${seconds}`);
  113. };
  114. update();
  115. const timer = setInterval(update, 1000);
  116. return () => clearInterval(timer);
  117. }, [lineDetail?.durationInMinutes, lineDetail?.startTime]);
  118. const handleSubmitOutput = async () => {
  119. if (!lineDetail?.id) return;
  120. try {
  121. // 直接使用 actions.ts 中定义的函数
  122. await updateProductProcessLineQty({
  123. productProcessLineId: lineDetail?.id || 0 as number,
  124. byproductName: outputData.byproductName,
  125. byproductQty: outputData.byproductQty,
  126. byproductUom: outputData.byproductUom,
  127. outputFromProcessQty: outputData.outputFromProcessQty,
  128. outputFromProcessUom: outputData.outputFromProcessUom,
  129. // outputFromProcessUom: outputData.outputFromProcessUom,
  130. defectQty: outputData.defectQty,
  131. defectUom: outputData.defectUom,
  132. scrapQty: outputData.scrapQty,
  133. scrapUom: outputData.scrapUom,
  134. });
  135. console.log(" Output data submitted successfully");
  136. fetchProductProcessLineDetail(lineDetail.id)
  137. .then((detail) => {
  138. setLineDetail(detail as any);
  139. // 初始化 outputData 从 lineDetail
  140. setOutputData(prev => ({
  141. ...prev,
  142. productProcessLineId: detail.id,
  143. outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言
  144. outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言
  145. defectQty: detail.defectQty || 0,
  146. defectUom: detail.defectUom || "",
  147. scrapQty: detail.scrapQty || 0,
  148. scrapUom: detail.scrapUom || "",
  149. byproductName: detail.byproductName || "",
  150. byproductQty: detail.byproductQty || 0,
  151. byproductUom: detail.byproductUom || ""
  152. }));
  153. })
  154. .catch(err => {
  155. console.error("Failed to load line detail", err);
  156. setLineDetail(null);
  157. });
  158. } catch (error) {
  159. console.error("Error submitting output:", error);
  160. alert("Failed to submit output data. Please try again.");
  161. }
  162. };
  163. // 处理 QR 码扫描效果
  164. useEffect(() => {
  165. if (isManualScanning && qrValues.length > 0 && lineDetail?.id) {
  166. const latestQr = qrValues[qrValues.length - 1];
  167. if (processedQrCodes.has(latestQr)) {
  168. return;
  169. }
  170. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  171. //processQrCode(latestQr);
  172. }
  173. }, [qrValues, isManualScanning, lineDetail?.id, processedQrCodes]);
  174. // 开始扫描
  175. const handlePause = () => {
  176. setIsPaused(true);
  177. };
  178. const handleContinue = () => {
  179. setIsPaused(false);
  180. };
  181. const handleStop = () => {
  182. setIsPaused(false);
  183. // TODO: 调用停止流程的 API
  184. };
  185. return (
  186. <Box>
  187. <Box sx={{ mb: 2 }}>
  188. <Button variant="outlined" onClick={onBack}>
  189. {t("Back to List")}
  190. </Button>
  191. </Box>
  192. {/* 如果已完成,显示合并的视图 */}
  193. {isCompleted ? (
  194. <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}>
  195. <CardContent>
  196. <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold">
  197. {t("Completed Step")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo})
  198. </Typography>
  199. {/*<Divider sx={{ my: 2 }} />*/}
  200. {/* 步骤信息部分 */}
  201. <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
  202. {t("Step Information")}
  203. </Typography>
  204. <Grid container spacing={2} sx={{ mb: 3 }}>
  205. <Grid item xs={12} md={6}>
  206. <Typography variant="body2" color="text.secondary">
  207. <strong>{t("Description")}:</strong> {lineDetail?.description || "-"}
  208. </Typography>
  209. </Grid>
  210. <Grid item xs={12} md={6}>
  211. <Typography variant="body2" color="text.secondary">
  212. <strong>{t("Operator")}:</strong> {lineDetail?.operatorName || "-"}
  213. </Typography>
  214. </Grid>
  215. <Grid item xs={12} md={6}>
  216. <Typography variant="body2" color="text.secondary">
  217. <strong>{t("Equipment")}:</strong> {equipmentName}
  218. </Typography>
  219. </Grid>
  220. <Grid item xs={12} md={6}>
  221. <Typography variant="body2" color="text.secondary">
  222. <strong>{t("Status")}:</strong> {lineDetail?.status || "-"}
  223. </Typography>
  224. </Grid>
  225. </Grid>
  226. {/*<Divider sx={{ my: 2 }} />*/}
  227. {/* 产出数据部分 */}
  228. <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
  229. {t("Production Output Data")}
  230. </Typography>
  231. <Table size="small" sx={{ mt: 2 }}>
  232. <TableHead>
  233. <TableRow>
  234. <TableCell width="30%"><strong>{t("Type")}</strong></TableCell>
  235. <TableCell width="35%"><strong>{t("Quantity")}</strong></TableCell>
  236. <TableCell width="35%"><strong>{t("Unit")}</strong></TableCell>
  237. </TableRow>
  238. </TableHead>
  239. <TableBody>
  240. {/* Output from Process */}
  241. <TableRow>
  242. <TableCell>
  243. <Typography fontWeight={500}>{t("Output from Process")}</Typography>
  244. </TableCell>
  245. <TableCell>
  246. <Typography>{lineDetail?.outputFromProcessQty || 0}</Typography>
  247. </TableCell>
  248. <TableCell>
  249. <Typography>{lineDetail?.outputFromProcessUom || "-"}</Typography>
  250. </TableCell>
  251. </TableRow>
  252. {/* By-product */}
  253. {/*
  254. <TableRow>
  255. <TableCell>
  256. <Typography fontWeight={500}>{t("By-product")}</Typography>
  257. {lineDetail.byproductName && (
  258. <Typography variant="caption" color="text.secondary">
  259. ({lineDetail.byproductName})
  260. </Typography>
  261. )}
  262. </TableCell>
  263. <TableCell>
  264. <Typography>{lineDetail.byproductQty}</Typography>
  265. </TableCell>
  266. <TableCell>
  267. <Typography>{lineDetail.byproductUom || "-"}</Typography>
  268. </TableCell>
  269. </TableRow>
  270. */}
  271. {/* Defect */}
  272. <TableRow sx={{ bgcolor: 'warning.50' }}>
  273. <TableCell>
  274. <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
  275. </TableCell>
  276. <TableCell>
  277. <Typography>{lineDetail.defectQty}</Typography>
  278. </TableCell>
  279. <TableCell>
  280. <Typography>{lineDetail.defectUom || "-"}</Typography>
  281. </TableCell>
  282. </TableRow>
  283. {/* Scrap */}
  284. <TableRow sx={{ bgcolor: 'error.50' }}>
  285. <TableCell>
  286. <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
  287. </TableCell>
  288. <TableCell>
  289. <Typography>{lineDetail.scrapQty}</Typography>
  290. </TableCell>
  291. <TableCell>
  292. <Typography>{lineDetail.scrapUom || "-"}</Typography>
  293. </TableCell>
  294. </TableRow>
  295. </TableBody>
  296. </Table>
  297. </CardContent>
  298. </Card>
  299. ) : (
  300. <>
  301. {/* 如果未完成,显示原来的两个部分 */}
  302. {/* 当前步骤信息 */}
  303. {!showOutputTable && (
  304. <Grid container spacing={2} sx={{ mb: 3 }}>
  305. <Grid item xs={12} >
  306. <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}>
  307. <CardContent>
  308. <Typography variant="h6" color="primary.main" gutterBottom>
  309. {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo})
  310. </Typography>
  311. <Typography variant="body2" color="text.secondary">
  312. {lineDetail?.description}
  313. </Typography>
  314. <Typography variant="body2" color="text.secondary">
  315. {t("Operator")}: {lineDetail?.operatorName || "-"}
  316. </Typography>
  317. <Typography variant="body2" color="text.secondary">
  318. {t("Equipment")}: {equipmentName}
  319. </Typography>
  320. <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}>
  321. {/*
  322. <Button
  323. variant="contained"
  324. color="error"
  325. startIcon={<StopIcon />}
  326. onClick={handleStop}
  327. >
  328. {t("Stop")}
  329. </Button>
  330. {!isPaused ? (
  331. <Button
  332. variant="contained"
  333. color="warning"
  334. startIcon={<PauseIcon />}
  335. onClick={handlePause}
  336. >
  337. {t("Pause")}
  338. </Button>
  339. ) : (
  340. <Button
  341. variant="contained"
  342. color="success"
  343. startIcon={<PlayArrowIcon />}
  344. onClick={handleContinue}
  345. >
  346. {t("Continue")}
  347. </Button>
  348. )}
  349. */}
  350. <Button
  351. sx={{ mt: 2, alignSelf: "flex-end" }}
  352. variant="outlined"
  353. onClick={() => setShowOutputTable(true)}
  354. >
  355. {t("Order Complete")}
  356. </Button>
  357. </Stack>
  358. </CardContent>
  359. </Card>
  360. </Grid>
  361. </Grid>
  362. )}
  363. {/* ========== 产出输入表单 ========== */}
  364. {showOutputTable && (
  365. <Box>
  366. <Paper sx={{ p: 3, bgcolor: 'grey.50' }}>
  367. <Table size="small">
  368. <TableHead>
  369. <TableRow>
  370. <TableCell width="25%" align="center">{t("Type")}</TableCell>
  371. <TableCell width="25%" align="center">{t("Quantity")}</TableCell>
  372. <TableCell width="25%" align="center">{t("Unit")}</TableCell>
  373. <TableCell width="25%" align="center">{t(" ")}</TableCell>
  374. </TableRow>
  375. </TableHead>
  376. <TableBody>
  377. {/* start line output */}
  378. <TableRow>
  379. <TableCell>
  380. <Typography fontWeight={500}>{t("Output from Process")}</Typography>
  381. </TableCell>
  382. <TableCell>
  383. <TextField
  384. type="number"
  385. fullWidth
  386. size="small"
  387. value={outputData.outputFromProcessQty}
  388. onChange={(e) => setOutputData({
  389. ...outputData,
  390. outputFromProcessQty: parseInt(e.target.value) || 0
  391. })}
  392. />
  393. </TableCell>
  394. <TableCell>
  395. <TextField
  396. fullWidth
  397. size="small"
  398. value={outputData.outputFromProcessUom}
  399. onChange={(e) => setOutputData({
  400. ...outputData,
  401. outputFromProcessUom: e.target.value
  402. })}
  403. />
  404. </TableCell>
  405. <TableCell>
  406. <Typography fontSize={15} align="center"> <strong>{t("Description")}</strong></Typography>
  407. </TableCell>
  408. </TableRow>
  409. {/* byproduct */}
  410. {/*
  411. <TableRow>
  412. <TableCell>
  413. <Stack>
  414. <Typography fontWeight={500}>{t("By-product")}</Typography>
  415. </Stack>
  416. </TableCell>
  417. <TableCell>
  418. <TextField
  419. type="number"
  420. fullWidth
  421. size="small"
  422. value={outputData.byproductQty}
  423. onChange={(e) => setOutputData({
  424. ...outputData,
  425. byproductQty: parseInt(e.target.value) || 0
  426. })}
  427. />
  428. </TableCell>
  429. <TableCell>
  430. <TextField
  431. fullWidth
  432. size="small"
  433. value={outputData.byproductUom}
  434. onChange={(e) => setOutputData({
  435. ...outputData,
  436. byproductUom: e.target.value
  437. })}
  438. />
  439. </TableCell>
  440. </TableRow>
  441. */}
  442. {/* defect 1 */}
  443. <TableRow sx={{ bgcolor: 'warning.50' }}>
  444. <TableCell>
  445. <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(1)")}</Typography>
  446. </TableCell>
  447. <TableCell>
  448. <TextField
  449. type="number"
  450. fullWidth
  451. size="small"
  452. value={outputData.defectQty}
  453. onChange={(e) => setOutputData({
  454. ...outputData,
  455. defectQty: parseInt(e.target.value) || 0
  456. })}
  457. />
  458. </TableCell>
  459. <TableCell>
  460. <TextField
  461. fullWidth
  462. size="small"
  463. value={outputData.defectUom}
  464. onChange={(e) => setOutputData({
  465. ...outputData,
  466. defectUom: e.target.value
  467. })}
  468. />
  469. </TableCell>
  470. <TableCell>
  471. <TextField
  472. fullWidth
  473. size="small"
  474. //value={outputData.defectUom}
  475. onChange={(e) => setOutputData({
  476. ...outputData,
  477. defectUom: e.target.value
  478. })}
  479. />
  480. </TableCell>
  481. </TableRow>
  482. {/* defect 2 */}
  483. <TableRow sx={{ bgcolor: 'warning.50' }}>
  484. <TableCell>
  485. <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(2)")}</Typography>
  486. </TableCell>
  487. <TableCell>
  488. <TextField
  489. type="number"
  490. fullWidth
  491. size="small"
  492. value={outputData.defectQty}
  493. onChange={(e) => setOutputData({
  494. ...outputData,
  495. defectQty: parseInt(e.target.value) || 0
  496. })}
  497. />
  498. </TableCell>
  499. <TableCell>
  500. <TextField
  501. fullWidth
  502. size="small"
  503. value={outputData.defectUom}
  504. onChange={(e) => setOutputData({
  505. ...outputData,
  506. defectUom: e.target.value
  507. })}
  508. />
  509. </TableCell>
  510. <TableCell>
  511. <TextField
  512. fullWidth
  513. size="small"
  514. //value={outputData.defectUom}
  515. onChange={(e) => setOutputData({
  516. ...outputData,
  517. defectUom: e.target.value
  518. })}
  519. />
  520. </TableCell>
  521. </TableRow>
  522. {/* defect 3 */}
  523. <TableRow sx={{ bgcolor: 'warning.50' }}>
  524. <TableCell>
  525. <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(3)")}</Typography>
  526. </TableCell>
  527. <TableCell>
  528. <TextField
  529. type="number"
  530. fullWidth
  531. size="small"
  532. value={outputData.defectQty}
  533. onChange={(e) => setOutputData({
  534. ...outputData,
  535. defectQty: parseInt(e.target.value) || 0
  536. })}
  537. />
  538. </TableCell>
  539. <TableCell>
  540. <TextField
  541. fullWidth
  542. size="small"
  543. value={outputData.defectUom}
  544. onChange={(e) => setOutputData({
  545. ...outputData,
  546. defectUom: e.target.value
  547. })}
  548. />
  549. </TableCell>
  550. <TableCell>
  551. <TextField
  552. fullWidth
  553. size="small"
  554. //value={outputData.defectUom}
  555. onChange={(e) => setOutputData({
  556. ...outputData,
  557. defectUom: e.target.value
  558. })}
  559. />
  560. </TableCell>
  561. </TableRow>
  562. {/* scrap */}
  563. <TableRow sx={{ bgcolor: 'error.50' }}>
  564. <TableCell>
  565. <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
  566. </TableCell>
  567. <TableCell>
  568. <TextField
  569. type="number"
  570. fullWidth
  571. size="small"
  572. value={outputData.scrapQty}
  573. onChange={(e) => setOutputData({
  574. ...outputData,
  575. scrapQty: parseInt(e.target.value) || 0
  576. })}
  577. />
  578. </TableCell>
  579. <TableCell>
  580. <TextField
  581. fullWidth
  582. size="small"
  583. value={outputData.scrapUom}
  584. onChange={(e) => setOutputData({
  585. ...outputData,
  586. scrapUom: e.target.value
  587. })}
  588. />
  589. </TableCell>
  590. </TableRow>
  591. </TableBody>
  592. </Table>
  593. {/* submit button */}
  594. <Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
  595. <Button
  596. variant="outlined"
  597. onClick={() => setShowOutputTable(false)}
  598. >
  599. {t("Cancel")}
  600. </Button>
  601. <Button
  602. variant="contained"
  603. startIcon={<CheckCircleIcon />}
  604. onClick={handleSubmitOutput}
  605. >
  606. {t("Complete Step")}
  607. </Button>
  608. </Box>
  609. </Paper>
  610. </Box>
  611. )}
  612. </>
  613. )}
  614. </Box>
  615. );
  616. };
  617. export default ProductionProcessStepExecution;