FPSMS-frontend
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

530 行
16 KiB

  1. "use client";
  2. import React, { useCallback, useEffect, useState, useMemo } from "react";
  3. import {
  4. Box,
  5. Button,
  6. Paper,
  7. Stack,
  8. Typography,
  9. TextField,
  10. Grid,
  11. Card,
  12. CardContent,
  13. CircularProgress,
  14. Tabs,
  15. Tab,
  16. TabsProps,
  17. } from "@mui/material";
  18. import ArrowBackIcon from '@mui/icons-material/ArrowBack';
  19. import { useTranslation } from "react-i18next";
  20. import { fetchProductProcessesByJobOrderId ,deleteJobOrder} from "@/app/api/jo/actions";
  21. import ProductionProcessDetail from "./ProductionProcessDetail";
  22. import dayjs from "dayjs";
  23. import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
  24. import StyledDataGrid from "../StyledDataGrid/StyledDataGrid";
  25. import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
  26. import { decimalFormatter } from "@/app/utils/formatUtil";
  27. import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
  28. import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded';
  29. import { fetchInventories } from "@/app/api/inventory/actions";
  30. import { InventoryResult } from "@/app/api/inventory";
  31. import { releaseJo, startJo } from "@/app/api/jo/actions";
  32. import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
  33. import ProcessSummaryHeader from "./ProcessSummaryHeader";
  34. interface JobOrderLine {
  35. id: number;
  36. jobOrderId: number;
  37. jobOrderCode: string;
  38. itemId: number;
  39. itemCode: string;
  40. itemName: string;
  41. reqQty: number;
  42. stockQty: number;
  43. uom: string;
  44. shortUom: string;
  45. availableStatus: string;
  46. type: string;
  47. }
  48. interface ProductProcessJobOrderDetailProps {
  49. jobOrderId: number;
  50. onBack: () => void;
  51. fromJosave?: boolean;
  52. }
  53. const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({
  54. jobOrderId,
  55. onBack,
  56. fromJosave,
  57. }) => {
  58. const { t } = useTranslation();
  59. const [loading, setLoading] = useState(false);
  60. const [processData, setProcessData] = useState<any>(null);
  61. const [jobOrderLines, setJobOrderLines] = useState<JobOrderLine[]>([]);
  62. const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
  63. const [tabIndex, setTabIndex] = useState(0);
  64. const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
  65. // 获取数据
  66. const fetchData = useCallback(async () => {
  67. setLoading(true);
  68. try {
  69. const data = await fetchProductProcessesByJobOrderId(jobOrderId);
  70. if (data && data.length > 0) {
  71. const firstProcess = data[0];
  72. setProcessData(firstProcess);
  73. setJobOrderLines((firstProcess as any).jobOrderLines || []);
  74. }
  75. } catch (error) {
  76. console.error("Error loading data:", error);
  77. } finally {
  78. setLoading(false);
  79. }
  80. }, [jobOrderId]);
  81. // 获取库存数据
  82. useEffect(() => {
  83. const fetchInventoryData = async () => {
  84. try {
  85. const inventoryResponse = await fetchInventories({
  86. code: "",
  87. name: "",
  88. type: "",
  89. pageNum: 0,
  90. pageSize: 1000
  91. });
  92. setInventoryData(inventoryResponse.records);
  93. } catch (error) {
  94. console.error("Error fetching inventory data:", error);
  95. }
  96. };
  97. fetchInventoryData();
  98. }, []);
  99. useEffect(() => {
  100. fetchData();
  101. }, [fetchData]);
  102. // PickTable 组件内容
  103. const getStockAvailable = (line: JobOrderLine) => {
  104. if (line.type?.toLowerCase() === "consumables") {
  105. return null;
  106. }
  107. const inventory = inventoryData.find(inv =>
  108. inv.itemCode === line.itemCode || inv.itemName === line.itemName
  109. );
  110. if (inventory) {
  111. return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
  112. }
  113. return line.stockQty || 0;
  114. };
  115. const isStockSufficient = (line: JobOrderLine) => {
  116. if (line.type?.toLowerCase() === "consumables") {
  117. return false;
  118. }
  119. const stockAvailable = getStockAvailable(line);
  120. if (stockAvailable === null) {
  121. return false;
  122. }
  123. return stockAvailable >= line.reqQty;
  124. };
  125. const stockCounts = useMemo(() => {
  126. // 过滤掉 consumables 类型的 lines
  127. const nonConsumablesLines = jobOrderLines.filter(
  128. line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb"
  129. );
  130. const total = nonConsumablesLines.length;
  131. const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
  132. return {
  133. total,
  134. sufficient,
  135. insufficient: total - sufficient,
  136. };
  137. }, [jobOrderLines, inventoryData]);
  138. const status = processData?.status?.toLowerCase?.() ?? "";
  139. const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
  140. const response = await deleteJobOrder(jobOrderId)
  141. if (response) {
  142. //setProcessData(response.entity);
  143. await fetchData();
  144. }
  145. }, [jobOrderId]);
  146. const handleRelease = useCallback(async ( jobOrderId: number) => {
  147. // TODO: 替换为实际的 release 调用
  148. console.log("Release clicked for jobOrderId:", jobOrderId);
  149. const response = await releaseJo({ id: jobOrderId })
  150. if (response) {
  151. //setProcessData(response.entity);
  152. await fetchData();
  153. }
  154. }, [jobOrderId]);
  155. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  156. (_e, newValue) => {
  157. setTabIndex(newValue);
  158. },
  159. [],
  160. );
  161. // 如果选择了 process detail,显示 detail 页面
  162. if (selectedProcessId !== null) {
  163. return (
  164. <ProductionProcessDetail
  165. jobOrderId={selectedProcessId}
  166. onBack={() => {
  167. setSelectedProcessId(null);
  168. fetchData(); // 刷新数据
  169. }}
  170. />
  171. );
  172. }
  173. if (loading) {
  174. return (
  175. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  176. <CircularProgress/>
  177. </Box>
  178. );
  179. }
  180. if (!processData) {
  181. return (
  182. <Box>
  183. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  184. {t("Back")}
  185. </Button>
  186. <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography>
  187. </Box>
  188. );
  189. }
  190. // InfoCard 组件内容
  191. const InfoCardContent = () => (
  192. <Card sx={{ display: "block", mt: 2 }}>
  193. <CardContent component={Stack} spacing={4}>
  194. <Box>
  195. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  196. <Grid item xs={6}>
  197. <TextField
  198. label={t("Job Order Code")}
  199. fullWidth
  200. disabled={true}
  201. value={processData?.jobOrderCode || ""}
  202. />
  203. </Grid>
  204. <Grid item xs={6}>
  205. <TextField
  206. label={t("Item Code")}
  207. fullWidth
  208. disabled={true}
  209. value={processData?.itemCode+"-"+processData?.itemName || ""}
  210. />
  211. </Grid>
  212. <Grid item xs={6}>
  213. <TextField
  214. label={t("Job Type")}
  215. fullWidth
  216. disabled={true}
  217. value={t(processData?.jobType) || t("N/A")}
  218. //value={t("N/A")}
  219. />
  220. </Grid>
  221. <Grid item xs={6}>
  222. <TextField
  223. label={t("Req. Qty")}
  224. fullWidth
  225. disabled={true}
  226. value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""}
  227. />
  228. </Grid>
  229. <Grid item xs={6}>
  230. <TextField
  231. value={processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}
  232. label={t("Target Production Date")}
  233. fullWidth
  234. disabled={true}
  235. />
  236. </Grid>
  237. <Grid item xs={6}>
  238. <TextField
  239. label={t("Production Priority")}
  240. fullWidth
  241. disabled={true}
  242. value={processData?.productionPriority ||processData?.isDense === 0 ? "50" : processData?.productionPriority || "0"}
  243. />
  244. </Grid>
  245. <Grid item xs={6}>
  246. <TextField
  247. label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance")}
  248. fullWidth
  249. disabled={true}
  250. value={`${processData?.isDark == null || processData?.isDark === "" ? t("N/A") : processData.isDark} | ${processData?.isDense == null || processData?.isDense === "" || processData?.isDense === 0 ? t("N/A") : processData.isDense} | ${processData?.isFloat == null || processData?.isFloat === "" ? t("N/A") : processData.isFloat} | ${processData?.scrapRate == -1 || processData?.scrapRate === "" ? t("N/A") : processData.scrapRate} | ${processData?.allergicSubstance == null || processData?.allergicSubstance === "" ? t("N/A") :t (processData.allergicSubstance)}`}
  251. />
  252. </Grid>
  253. </Grid>
  254. </Box>
  255. </CardContent>
  256. </Card>
  257. );
  258. const productionProcessesLineRemarkTableColumns: GridColDef[] = [
  259. {
  260. field: "seqNo",
  261. headerName: t("Seq"),
  262. flex: 0.2,
  263. align: "left",
  264. headerAlign: "center",
  265. type: "number",
  266. renderCell: (params) => {
  267. return <Typography sx={{ fontSize: "14px" }}>{params.value}</Typography>;
  268. },
  269. },
  270. {
  271. field: "description",
  272. headerName: t("Remark"),
  273. flex: 1,
  274. align: "left",
  275. headerAlign: "center",
  276. renderCell: (params) => {
  277. return <Typography sx={{ fontSize: "14px" }}>{params.value || ""}</Typography>;
  278. },
  279. },
  280. ];
  281. const productionProcessesLineRemarkTableRows =
  282. processData?.productProcessLines?.map((line: any) => ({
  283. id: line.seqNo,
  284. seqNo: line.seqNo,
  285. description: line.description ?? "",
  286. })) ?? [];
  287. const pickTableColumns: GridColDef[] = [
  288. {
  289. field: "id",
  290. headerName: t("id"),
  291. flex: 0.2,
  292. align: "left",
  293. headerAlign: "left",
  294. type: "number",
  295. },
  296. {
  297. field: "itemCode",
  298. headerName: t("Item Code"),
  299. flex: 0.6,
  300. },
  301. {
  302. field: "itemName",
  303. headerName: t("Item Name"),
  304. flex: 1,
  305. renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
  306. return `${params.value} (${params.row.uom})`;
  307. },
  308. },
  309. {
  310. field: "reqQty",
  311. headerName: t("Req. Qty"),
  312. flex: 0.7,
  313. align: "right",
  314. headerAlign: "right",
  315. renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
  316. if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
  317. return t("N/A");
  318. }
  319. return `${decimalFormatter.format(params.value)} (${params.row.shortUom})`;
  320. },
  321. },
  322. {
  323. field: "stockAvailable",
  324. headerName: t("Stock Available"),
  325. flex: 0.7,
  326. align: "right",
  327. headerAlign: "right",
  328. type: "number",
  329. renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
  330. // 如果是 consumables,显示 N/A
  331. if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
  332. return t("N/A");
  333. }
  334. const stockAvailable = getStockAvailable(params.row);
  335. if (stockAvailable === null) {
  336. return t("N/A");
  337. }
  338. return `${decimalFormatter.format(stockAvailable)} (${params.row.shortUom})`;
  339. },
  340. },
  341. {
  342. field: "bomProcessSeqNo",
  343. headerName: t("Seq No"),
  344. flex: 0.5,
  345. align: "right",
  346. headerAlign: "right",
  347. type: "number",
  348. },
  349. /*
  350. {
  351. field: "seqNoRemark",
  352. headerName: t("Seq No Remark"),
  353. flex: 1,
  354. align: "left",
  355. headerAlign: "left",
  356. type: "string",
  357. },
  358. */
  359. {
  360. field: "stockStatus",
  361. headerName: t("Stock Status"),
  362. flex: 0.5,
  363. align: "center",
  364. headerAlign: "center",
  365. type: "boolean",
  366. renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
  367. if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
  368. return <Typography>{t("N/A")}</Typography>;
  369. }
  370. return isStockSufficient(params.row)
  371. ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" />
  372. : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />;
  373. },
  374. },
  375. ];
  376. const pickTableRows = jobOrderLines.map((line, index) => ({
  377. ...line,
  378. //id: line.id || index,
  379. id: index + 1,
  380. }));
  381. const PickTableContent = () => (
  382. <Box sx={{ mt: 2 }}>
  383. <ProcessSummaryHeader processData={processData} />
  384. <Card sx={{ mb: 2 }}>
  385. <CardContent>
  386. <Stack
  387. direction="row"
  388. alignItems="center"
  389. justifyContent="space-between"
  390. spacing={2}
  391. >
  392. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  393. {t("Total lines: ")}<strong>{stockCounts.total}</strong>
  394. </Typography>
  395. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  396. {t("Lines with sufficient stock: ")}<strong style={{ color: "green" }}>{stockCounts.sufficient}</strong>
  397. </Typography>
  398. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  399. {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
  400. </Typography>
  401. {fromJosave && (
  402. <Button
  403. variant="contained"
  404. color="error"
  405. onClick={() => handleDeleteJobOrder(jobOrderId)}
  406. disabled={processData?.jobOrderStatus !== "planning"}
  407. >
  408. {t("Delete Job Order")}
  409. </Button>
  410. )}
  411. {fromJosave && (
  412. <Button
  413. variant="contained"
  414. color="primary"
  415. onClick={() => handleRelease(jobOrderId)}
  416. disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"}
  417. >
  418. {t("Release")}
  419. </Button>
  420. )}
  421. </Stack>
  422. </CardContent>
  423. </Card>
  424. <StyledDataGrid
  425. sx={{ "--DataGrid-overlayHeight": "100px" }}
  426. disableColumnMenu
  427. rows={pickTableRows}
  428. columns={pickTableColumns}
  429. getRowHeight={() => "auto"}
  430. />
  431. </Box>
  432. );
  433. const ProductionProcessesLineRemarkTableContent = () => (
  434. <Box sx={{ mt: 2 }}>
  435. <ProcessSummaryHeader processData={processData} />
  436. <StyledDataGrid
  437. sx={{
  438. "--DataGrid-overlayHeight": "100px",
  439. }}
  440. disableColumnMenu
  441. rows={productionProcessesLineRemarkTableRows ?? []}
  442. columns={productionProcessesLineRemarkTableColumns}
  443. getRowHeight={() => 'auto'}
  444. />
  445. </Box>
  446. );
  447. return (
  448. <Box>
  449. {/* 返回按钮 */}
  450. <Box sx={{ mb: 2 }}>
  451. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  452. {t("Back to List")}
  453. </Button>
  454. </Box>
  455. {/* 标签页 */}
  456. <Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
  457. <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
  458. <Tab label={t("Job Order Info")} />
  459. <Tab label={t("BoM Material")} />
  460. <Tab label={t("Production Process")} />
  461. <Tab label={t("Production Process Line Remark")} />
  462. {!fromJosave && (
  463. <Tab label={t("Matching Stock")} />
  464. )}
  465. </Tabs>
  466. </Box>
  467. {/* 标签页内容 */}
  468. <Box sx={{ p: 2 }}>
  469. {tabIndex === 0 && <InfoCardContent />}
  470. {tabIndex === 1 && <PickTableContent />}
  471. {tabIndex === 2 && (
  472. <ProductionProcessDetail
  473. jobOrderId={jobOrderId}
  474. onBack={() => {
  475. // 切换回第一个标签页,或者什么都不做
  476. setTabIndex(0);
  477. }}
  478. fromJosave={fromJosave}
  479. />
  480. )}
  481. {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}
  482. {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />}
  483. </Box>
  484. </Box>
  485. );
  486. };
  487. export default ProductionProcessJobOrderDetail;