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

ProductionProcessStepExecution.tsx 20 KiB

1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
1ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  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;