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

ProductionProcessStepExecution.tsx 21 KiB

1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
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ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  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. <TableRow>
  254. <TableCell>
  255. <Typography fontWeight={500}>{t("By-product")}</Typography>
  256. {lineDetail.byproductName && (
  257. <Typography variant="caption" color="text.secondary">
  258. ({lineDetail.byproductName})
  259. </Typography>
  260. )}
  261. </TableCell>
  262. <TableCell>
  263. <Typography>{lineDetail.byproductQty}</Typography>
  264. </TableCell>
  265. <TableCell>
  266. <Typography>{lineDetail.byproductUom || "-"}</Typography>
  267. </TableCell>
  268. </TableRow>
  269. {/* Defect */}
  270. <TableRow sx={{ bgcolor: 'warning.50' }}>
  271. <TableCell>
  272. <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
  273. </TableCell>
  274. <TableCell>
  275. <Typography>{lineDetail.defectQty}</Typography>
  276. </TableCell>
  277. <TableCell>
  278. <Typography>{lineDetail.defectUom || "-"}</Typography>
  279. </TableCell>
  280. </TableRow>
  281. {/* Scrap */}
  282. <TableRow sx={{ bgcolor: 'error.50' }}>
  283. <TableCell>
  284. <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
  285. </TableCell>
  286. <TableCell>
  287. <Typography>{lineDetail.scrapQty}</Typography>
  288. </TableCell>
  289. <TableCell>
  290. <Typography>{lineDetail.scrapUom || "-"}</Typography>
  291. </TableCell>
  292. </TableRow>
  293. </TableBody>
  294. </Table>
  295. </CardContent>
  296. </Card>
  297. ) : (
  298. <>
  299. {/* 如果未完成,显示原来的两个部分 */}
  300. {/* 当前步骤信息 */}
  301. {!showOutputTable && (
  302. <Grid container spacing={2} sx={{ mb: 3 }}>
  303. <Grid item xs={12} >
  304. <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}>
  305. <CardContent>
  306. <Typography variant="h6" color="primary.main" gutterBottom>
  307. {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo})
  308. </Typography>
  309. <Typography variant="body2" color="text.secondary">
  310. {lineDetail?.description}
  311. </Typography>
  312. <Typography variant="body2" color="text.secondary">
  313. {t("Operator")}: {lineDetail?.operatorName || "-"}
  314. </Typography>
  315. <Typography variant="body2" color="text.secondary">
  316. {t("Equipment")}: {equipmentName}
  317. </Typography>
  318. <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}>
  319. {/*
  320. <Button
  321. variant="contained"
  322. color="error"
  323. startIcon={<StopIcon />}
  324. onClick={handleStop}
  325. >
  326. {t("Stop")}
  327. </Button>
  328. {!isPaused ? (
  329. <Button
  330. variant="contained"
  331. color="warning"
  332. startIcon={<PauseIcon />}
  333. onClick={handlePause}
  334. >
  335. {t("Pause")}
  336. </Button>
  337. ) : (
  338. <Button
  339. variant="contained"
  340. color="success"
  341. startIcon={<PlayArrowIcon />}
  342. onClick={handleContinue}
  343. >
  344. {t("Continue")}
  345. </Button>
  346. )}
  347. */}
  348. <Button
  349. sx={{ mt: 2, alignSelf: "flex-end" }}
  350. variant="outlined"
  351. onClick={() => setShowOutputTable(true)}
  352. >
  353. {t("Order Complete")}
  354. </Button>
  355. </Stack>
  356. </CardContent>
  357. </Card>
  358. </Grid>
  359. </Grid>
  360. )}
  361. {/* ========== 产出输入表单 ========== */}
  362. {showOutputTable && (
  363. <Box>
  364. <Paper sx={{ p: 3, bgcolor: 'grey.50' }}>
  365. <Table size="small">
  366. <TableHead>
  367. <TableRow>
  368. <TableCell width="30%">{t("Type")}</TableCell>
  369. <TableCell width="35%">{t("Quantity")}</TableCell>
  370. <TableCell width="35%">{t("Unit")}</TableCell>
  371. </TableRow>
  372. </TableHead>
  373. <TableBody>
  374. {/* start line output */}
  375. <TableRow>
  376. <TableCell>
  377. <Typography fontWeight={500}>{t("Output from Process")}</Typography>
  378. </TableCell>
  379. <TableCell>
  380. <TextField
  381. type="number"
  382. fullWidth
  383. size="small"
  384. value={outputData.outputFromProcessQty}
  385. onChange={(e) => setOutputData({
  386. ...outputData,
  387. outputFromProcessQty: parseInt(e.target.value) || 0
  388. })}
  389. />
  390. </TableCell>
  391. <TableCell>
  392. <TextField
  393. fullWidth
  394. size="small"
  395. value={outputData.outputFromProcessUom}
  396. onChange={(e) => setOutputData({
  397. ...outputData,
  398. outputFromProcessUom: e.target.value
  399. })}
  400. />
  401. </TableCell>
  402. </TableRow>
  403. {/* byproduct */}
  404. <TableRow>
  405. <TableCell>
  406. <Stack>
  407. <Typography fontWeight={500}>{t("By-product")}</Typography>
  408. </Stack>
  409. </TableCell>
  410. <TableCell>
  411. <TextField
  412. type="number"
  413. fullWidth
  414. size="small"
  415. value={outputData.byproductQty}
  416. onChange={(e) => setOutputData({
  417. ...outputData,
  418. byproductQty: parseInt(e.target.value) || 0
  419. })}
  420. />
  421. </TableCell>
  422. <TableCell>
  423. <TextField
  424. fullWidth
  425. size="small"
  426. value={outputData.byproductUom}
  427. onChange={(e) => setOutputData({
  428. ...outputData,
  429. byproductUom: e.target.value
  430. })}
  431. />
  432. </TableCell>
  433. </TableRow>
  434. {/* defect */}
  435. <TableRow sx={{ bgcolor: 'warning.50' }}>
  436. <TableCell>
  437. <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
  438. </TableCell>
  439. <TableCell>
  440. <TextField
  441. type="number"
  442. fullWidth
  443. size="small"
  444. value={outputData.defectQty}
  445. onChange={(e) => setOutputData({
  446. ...outputData,
  447. defectQty: parseInt(e.target.value) || 0
  448. })}
  449. />
  450. </TableCell>
  451. <TableCell>
  452. <TextField
  453. fullWidth
  454. size="small"
  455. value={outputData.defectUom}
  456. onChange={(e) => setOutputData({
  457. ...outputData,
  458. defectUom: e.target.value
  459. })}
  460. />
  461. </TableCell>
  462. </TableRow>
  463. {/* scrap */}
  464. <TableRow sx={{ bgcolor: 'error.50' }}>
  465. <TableCell>
  466. <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
  467. </TableCell>
  468. <TableCell>
  469. <TextField
  470. type="number"
  471. fullWidth
  472. size="small"
  473. value={outputData.scrapQty}
  474. onChange={(e) => setOutputData({
  475. ...outputData,
  476. scrapQty: parseInt(e.target.value) || 0
  477. })}
  478. />
  479. </TableCell>
  480. <TableCell>
  481. <TextField
  482. fullWidth
  483. size="small"
  484. value={outputData.scrapUom}
  485. onChange={(e) => setOutputData({
  486. ...outputData,
  487. scrapUom: e.target.value
  488. })}
  489. />
  490. </TableCell>
  491. </TableRow>
  492. </TableBody>
  493. </Table>
  494. {/* submit button */}
  495. <Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
  496. <Button
  497. variant="outlined"
  498. onClick={() => setShowOutputTable(false)}
  499. >
  500. {t("Cancel")}
  501. </Button>
  502. <Button
  503. variant="contained"
  504. startIcon={<CheckCircleIcon />}
  505. onClick={handleSubmitOutput}
  506. >
  507. {t("Complete Step")}
  508. </Button>
  509. </Box>
  510. </Paper>
  511. </Box>
  512. )}
  513. </>
  514. )}
  515. </Box>
  516. );
  517. };
  518. export default ProductionProcessStepExecution;