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.
 
 

936 line
30 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. IconButton,
  18. Dialog,
  19. DialogTitle,
  20. DialogContent,
  21. DialogActions,
  22. InputAdornment
  23. } from "@mui/material";
  24. import ArrowBackIcon from '@mui/icons-material/ArrowBack';
  25. import { useTranslation } from "react-i18next";
  26. import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart,updateJoReqQty,newProductProcessLine,JobOrderLineInfo} from "@/app/api/jo/actions";
  27. import ProductionProcessDetail from "./ProductionProcessDetail";
  28. import { BomCombo } from "@/app/api/bom";
  29. import { fetchBomCombo } from "@/app/api/bom/index";
  30. import dayjs from "dayjs";
  31. import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
  32. import StyledDataGrid from "../StyledDataGrid/StyledDataGrid";
  33. import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
  34. import { decimalFormatter } from "@/app/utils/formatUtil";
  35. import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
  36. import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded';
  37. import { fetchInventories } from "@/app/api/inventory/actions";
  38. import { InventoryResult } from "@/app/api/inventory";
  39. import { releaseJo, startJo } from "@/app/api/jo/actions";
  40. import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
  41. import ProcessSummaryHeader from "./ProcessSummaryHeader";
  42. import EditIcon from "@mui/icons-material/Edit";
  43. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  44. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  45. import { dayjsToDateString } from "@/app/utils/formatUtil";
  46. interface ProductProcessJobOrderDetailProps {
  47. jobOrderId: number;
  48. onBack: () => void;
  49. fromJosave?: boolean;
  50. }
  51. const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({
  52. jobOrderId,
  53. onBack,
  54. fromJosave,
  55. }) => {
  56. const { t } = useTranslation();
  57. const [loading, setLoading] = useState(false);
  58. const [processData, setProcessData] = useState<any>(null);
  59. const [jobOrderLines, setJobOrderLines] = useState<JobOrderLineInfo[]>([]);
  60. const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
  61. const [tabIndex, setTabIndex] = useState(0);
  62. const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
  63. const [operationPriority, setOperationPriority] = useState<number>(50);
  64. const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false);
  65. const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false);
  66. const [planStartDate, setPlanStartDate] = useState<dayjs.Dayjs | null>(null);
  67. const [openReqQtyDialog, setOpenReqQtyDialog] = useState(false);
  68. const [reqQtyMultiplier, setReqQtyMultiplier] = useState<number>(1);
  69. const [selectedBomForReqQty, setSelectedBomForReqQty] = useState<BomCombo | null>(null);
  70. const [bomCombo, setBomCombo] = useState<BomCombo[]>([]);
  71. const [showBaseQty, setShowBaseQty] = useState<boolean>(false);
  72. const fetchData = useCallback(async () => {
  73. setLoading(true);
  74. try {
  75. const data = await fetchProductProcessesByJobOrderId(jobOrderId);
  76. if (data && data.length > 0) {
  77. const firstProcess = data[0];
  78. setProcessData(firstProcess);
  79. setJobOrderLines((firstProcess as any).jobOrderLines || []);
  80. }
  81. } catch (error) {
  82. console.error("Error loading data:", error);
  83. } finally {
  84. setLoading(false);
  85. }
  86. }, [jobOrderId]);
  87. const toggleBaseQty = useCallback(() => {
  88. setShowBaseQty(prev => !prev);
  89. }, []);
  90. // 4. 添加处理函数(约第 166 行后)
  91. const handleOpenReqQtyDialog = useCallback(async () => {
  92. if (!processData || !processData.outputQty || !processData.outputQtyUom) {
  93. alert(t("BOM data not available"));
  94. return;
  95. }
  96. const baseOutputQty = processData.bomBaseQty;
  97. const currentMultiplier = baseOutputQty > 0
  98. ? Math.round(processData.outputQty / baseOutputQty)
  99. : 1;
  100. const bomData = {
  101. id: processData.bomId || 0,
  102. value: processData.bomId || 0,
  103. label: processData.bomDescription || "",
  104. outputQty: baseOutputQty,
  105. outputQtyUom: processData.outputQtyUom,
  106. description: processData.bomDescription || ""
  107. };
  108. setSelectedBomForReqQty(bomData);
  109. setReqQtyMultiplier(currentMultiplier);
  110. setOpenReqQtyDialog(true);
  111. }, [processData, t]);
  112. const handleCloseReqQtyDialog = useCallback(() => {
  113. setOpenReqQtyDialog(false);
  114. setSelectedBomForReqQty(null);
  115. setReqQtyMultiplier(1);
  116. }, []);
  117. const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => {
  118. try {
  119. const response = await updateJoReqQty({
  120. id: jobOrderId,
  121. reqQty: Math.round(newReqQty)
  122. });
  123. if (response) {
  124. await fetchData();
  125. }
  126. } catch (error) {
  127. console.error("Error updating reqQty:", error);
  128. alert(t("update failed"));
  129. }
  130. }, [fetchData, t]);
  131. const handleConfirmReqQty = useCallback(async () => {
  132. if (!jobOrderId || !selectedBomForReqQty) return;
  133. const newReqQty = reqQtyMultiplier * selectedBomForReqQty.outputQty;
  134. await handleUpdateReqQty(jobOrderId, newReqQty);
  135. setOpenReqQtyDialog(false);
  136. setSelectedBomForReqQty(null);
  137. setReqQtyMultiplier(1);
  138. }, [jobOrderId, selectedBomForReqQty, reqQtyMultiplier, handleUpdateReqQty]);
  139. // 获取库存数据
  140. useEffect(() => {
  141. const fetchInventoryData = async () => {
  142. try {
  143. const inventoryResponse = await fetchInventories({
  144. code: "",
  145. name: "",
  146. type: "",
  147. pageNum: 0,
  148. pageSize: 1000
  149. });
  150. setInventoryData(inventoryResponse.records);
  151. } catch (error) {
  152. console.error("Error fetching inventory data:", error);
  153. }
  154. };
  155. fetchInventoryData();
  156. }, []);
  157. useEffect(() => {
  158. fetchData();
  159. }, [fetchData]);
  160. // PickTable 组件内容
  161. const getStockAvailable = (line: JobOrderLineInfo) => {
  162. if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") {
  163. return line.stockQty || 0;
  164. }
  165. const inventory = inventoryData.find(inv =>
  166. inv.itemCode === line.itemCode || inv.itemName === line.itemName
  167. );
  168. if (inventory) {
  169. return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
  170. }
  171. return line.stockQty || 0;
  172. };
  173. const handleOpenPlanStartDialog = useCallback(() => {
  174. // 将 processData.date 转换为 dayjs 对象
  175. if (processData?.date) {
  176. // 只取日期部分,避免时区换算导致前一天/后一天
  177. const dateOnly = String(processData.date).slice(0, 10);
  178. setPlanStartDate(dayjs(dateOnly));
  179. } else {
  180. setPlanStartDate(dayjs());
  181. }
  182. setOpenPlanStartDialog(true);
  183. }, [processData?.date]);
  184. const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  185. setOpenPlanStartDialog(false);
  186. setPlanStartDate(null);
  187. }, []);
  188. const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => {
  189. const response = await updateJoPlanStart({ id: jobOrderId, planStart });
  190. if (response) {
  191. await fetchData();
  192. }
  193. }, [fetchData]);
  194. const handleConfirmPlanStart = useCallback(async () => {
  195. if (!jobOrderId || !planStartDate) return;
  196. // 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss)
  197. const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`;
  198. await handleUpdatePlanStart(jobOrderId, dateString);
  199. setOpenPlanStartDialog(false);
  200. setPlanStartDate(null);
  201. }, [jobOrderId, planStartDate, handleUpdatePlanStart]);
  202. const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
  203. const response = await updateProductProcessPriority(productProcessId, productionPriority)
  204. if (response) {
  205. await fetchData();
  206. }
  207. }, [jobOrderId]);
  208. const handleOpenPriorityDialog = () => {
  209. setOperationPriority(processData?.productionPriority ?? 50);
  210. setOpenOperationPriorityDialog(true);
  211. };
  212. const handleClosePriorityDialog = (_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  213. setOpenOperationPriorityDialog(false);
  214. };
  215. const handleConfirmPriority = async () => {
  216. if (!processData?.id) return;
  217. await handleUpdateOperationPriority(processData.id, Number(operationPriority));
  218. setOpenOperationPriorityDialog(false);
  219. };
  220. const isStockSufficient = (line: JobOrderLineInfo) => {
  221. if (line.type?.toLowerCase() === "consumables") {
  222. return false;
  223. }
  224. const stockAvailable = getStockAvailable(line);
  225. if (stockAvailable === null) {
  226. return false;
  227. }
  228. return stockAvailable >= line.reqQty;
  229. };
  230. const stockCounts = useMemo(() => {
  231. // 过滤掉 consumables 类型的 lines
  232. const nonConsumablesLines = jobOrderLines.filter(
  233. line => {
  234. const type = line.type?.toLowerCase();
  235. return type !== "consumables" &&
  236. type !== "consumable" && // ✅ 添加单数形式
  237. type !== "cmb" &&
  238. type !== "nm"
  239. }
  240. );
  241. const total = nonConsumablesLines.length;
  242. const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
  243. return {
  244. total,
  245. sufficient,
  246. insufficient: total - sufficient,
  247. };
  248. }, [jobOrderLines, inventoryData]);
  249. const status = processData?.status?.toLowerCase?.() ?? "";
  250. const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
  251. const response = await deleteJobOrder(jobOrderId)
  252. if (response) {
  253. //setProcessData(response.entity);
  254. //await fetchData();
  255. onBack();
  256. }
  257. }, [jobOrderId]);
  258. const handleRelease = useCallback(async ( jobOrderId: number) => {
  259. // TODO: 替换为实际的 release 调用
  260. console.log("Release clicked for jobOrderId:", jobOrderId);
  261. const response = await releaseJo({ id: jobOrderId })
  262. if (response) {
  263. //setProcessData(response.entity);
  264. await fetchData();
  265. }
  266. }, [jobOrderId]);
  267. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  268. (_e, newValue) => {
  269. setTabIndex(newValue);
  270. },
  271. [],
  272. );
  273. // 如果选择了 process detail,显示 detail 页面
  274. if (selectedProcessId !== null) {
  275. return (
  276. <ProductionProcessDetail
  277. jobOrderId={selectedProcessId}
  278. onBack={() => {
  279. setSelectedProcessId(null);
  280. fetchData(); // 刷新数据
  281. }}
  282. />
  283. );
  284. }
  285. if (loading) {
  286. return (
  287. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  288. <CircularProgress/>
  289. </Box>
  290. );
  291. }
  292. if (!processData) {
  293. return (
  294. <Box>
  295. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  296. {t("Back")}
  297. </Button>
  298. <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography>
  299. </Box>
  300. );
  301. }
  302. // InfoCard 组件内容
  303. const InfoCardContent = () => (
  304. <Card sx={{ display: "block", mt: 2 }}>
  305. <CardContent component={Stack} spacing={4}>
  306. <Box>
  307. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  308. <Grid item xs={6}>
  309. <TextField
  310. label={t("Job Order Code")}
  311. fullWidth
  312. disabled={true}
  313. value={processData?.jobOrderCode || ""}
  314. />
  315. </Grid>
  316. <Grid item xs={6}>
  317. <TextField
  318. label={t("Item Code")}
  319. fullWidth
  320. disabled={true}
  321. value={processData?.itemCode+"-"+processData?.itemName || ""}
  322. />
  323. </Grid>
  324. <Grid item xs={6}>
  325. <TextField
  326. label={t("Job Type")}
  327. fullWidth
  328. disabled={true}
  329. value={t(processData?.jobType) || t("N/A")}
  330. //value={t("N/A")}
  331. />
  332. </Grid>
  333. <Grid item xs={6}>
  334. <TextField
  335. label={t("Req. Qty")}
  336. fullWidth
  337. disabled={true}
  338. value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""}
  339. InputProps={{
  340. endAdornment: (processData?.jobOrderStatus === "planning" ? (
  341. <InputAdornment position="end">
  342. <IconButton size="small" onClick={handleOpenReqQtyDialog}>
  343. <EditIcon fontSize="small" />
  344. </IconButton>
  345. </InputAdornment>
  346. ) : null),
  347. }}
  348. />
  349. </Grid>
  350. <Grid item xs={6}>
  351. <TextField
  352. value={processData?.date ? String(processData.date).slice(0, 10) : ""}
  353. label={t("Target Production Date")}
  354. fullWidth
  355. disabled={true}
  356. InputProps={{
  357. endAdornment: (processData?.jobOrderStatus === "planning" ? (
  358. <InputAdornment position="end">
  359. <IconButton size="small" onClick={handleOpenPlanStartDialog}>
  360. <EditIcon fontSize="small" />
  361. </IconButton>
  362. </InputAdornment>
  363. ) : null),
  364. }}
  365. />
  366. </Grid>
  367. <Grid item xs={6}>
  368. <TextField
  369. label={t("Production Priority")}
  370. fullWidth
  371. disabled={true}
  372. value={processData?.productionPriority ?? "50"}
  373. InputProps={{
  374. endAdornment: (
  375. <InputAdornment position="end">
  376. <IconButton size="small" onClick={handleOpenPriorityDialog}>
  377. <EditIcon fontSize="small" />
  378. </IconButton>
  379. </InputAdornment>
  380. ),
  381. }}
  382. />
  383. </Grid>
  384. <Grid item xs={6}>
  385. <TextField
  386. label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity")}
  387. fullWidth
  388. disabled={true}
  389. 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)} | ${processData?.timeSequence == null || processData?.timeSequence === "" ? t("N/A") : processData.timeSequence} | ${processData?.complexity == null || processData?.complexity === "" ? t("N/A") : processData.complexity}`}
  390. />
  391. </Grid>
  392. </Grid>
  393. </Box>
  394. </CardContent>
  395. </Card>
  396. );
  397. const productionProcessesLineRemarkTableColumns: GridColDef[] = [
  398. {
  399. field: "seqNo",
  400. headerName: t("SEQ"),
  401. flex: 0.2,
  402. align: "left",
  403. headerAlign: "left",
  404. type: "number",
  405. renderCell: (params) => {
  406. return <Typography sx={{ fontWeight: 500 }}>{params.value}</Typography>;
  407. },
  408. },
  409. {
  410. field: "description",
  411. headerName: t("Remark"),
  412. flex: 1,
  413. align: "left",
  414. headerAlign: "left",
  415. renderCell: (params) => {
  416. return(
  417. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  418. <Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
  419. <Typography sx={{ fontWeight: 500 }}>{params.value || ""}</Typography>
  420. <Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
  421. </Box>
  422. )
  423. },
  424. },
  425. ];
  426. const productionProcessesLineRemarkTableRows =
  427. processData?.productProcessLines?.map((line: any) => ({
  428. id: line.seqNo,
  429. seqNo: line.seqNo,
  430. description: line.description ?? "",
  431. })) ?? [];
  432. const pickTableColumns: GridColDef[] = [
  433. {
  434. field: "id",
  435. headerName: t("id"),
  436. flex: 0.2,
  437. align: "left",
  438. headerAlign: "left",
  439. type: "number",
  440. sortable: false, // ✅ 禁用排序
  441. },
  442. {
  443. field: "itemCode",
  444. headerName: t("Material Code"),
  445. flex: 0.6,
  446. sortable: false,
  447. },
  448. {
  449. field: "itemName",
  450. headerName: t("Item Name"),
  451. flex: 1,
  452. sortable: false, // ✅ 禁用排序
  453. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  454. return `${params.value} (${params.row.reqUom})`;
  455. },
  456. },
  457. {
  458. field: "reqQty",
  459. headerName: t("Bom Req. Qty"),
  460. flex: 0.7,
  461. align: "right",
  462. headerAlign: "right",
  463. sortable: false,
  464. renderHeader: () => {
  465. const uom = showBaseQty ? t("Base UOM") : t("Bom Uom");
  466. return (
  467. <Box
  468. onClick={toggleBaseQty}
  469. sx={{
  470. cursor: "pointer",
  471. userSelect: "none",
  472. width: "100%",
  473. textAlign: "right",
  474. "&:hover": {
  475. textDecoration: "underline",
  476. },
  477. }}
  478. >
  479. <Typography variant="body2">
  480. {t("Bom Req. Qty")}<br/>
  481. ({uom})
  482. </Typography>
  483. </Box>
  484. );
  485. },
  486. // ✅ 移除 cell 中的 onClick,只显示值
  487. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  488. const qty = showBaseQty ? params.row.baseReqQty : params.value;
  489. const uom = showBaseQty ? params.row.reqBaseUom : params.row.reqUom;
  490. return (
  491. <Box sx={{ textAlign: "right" }}>
  492. {decimalFormatter.format(qty || 0)} ({uom || ""})
  493. </Box>
  494. );
  495. },
  496. },
  497. {
  498. field: "stockReqQty",
  499. headerName: t("Stock Req. Qty"),
  500. flex: 0.7,
  501. align: "right",
  502. headerAlign: "right",
  503. sortable: false, // ✅ 禁用排序
  504. // ✅ 将切换功能移到 header
  505. renderHeader: () => {
  506. const uom = showBaseQty ? t("Base UOM") : t("Stock UOM");
  507. return (
  508. <Box
  509. onClick={toggleBaseQty}
  510. sx={{
  511. cursor: "pointer",
  512. userSelect: "none",
  513. width: "100%",
  514. textAlign: "right",
  515. "&:hover": {
  516. textDecoration: "underline",
  517. },
  518. }}
  519. >
  520. <Typography variant="body2">
  521. {t("Stock Req. Qty")} <br/>
  522. ({uom})
  523. </Typography>
  524. </Box>
  525. );
  526. },
  527. // ✅ 移除 cell 中的 onClick
  528. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  529. const qty = showBaseQty ? params.row.baseReqQty : params.value;
  530. const uom = showBaseQty ? params.row.reqBaseUom : params.row.stockUom;
  531. return (
  532. <Box sx={{ textAlign: "right" }}>
  533. {decimalFormatter.format(qty || 0)} ({uom || ""})
  534. </Box>
  535. );
  536. },
  537. },
  538. {
  539. field: "stockAvailable",
  540. headerName: t("Stock Available"),
  541. flex: 0.7,
  542. align: "right",
  543. headerAlign: "right",
  544. type: "number",
  545. sortable: false, // ✅ 禁用排序
  546. // ✅ 将切换功能移到 header
  547. renderHeader: () => {
  548. const uom = showBaseQty ? t("Base UOM") : t("Stock UOM");
  549. return (
  550. <Box
  551. onClick={toggleBaseQty}
  552. sx={{
  553. cursor: "pointer",
  554. userSelect: "none",
  555. width: "100%",
  556. textAlign: "right",
  557. "&:hover": {
  558. textDecoration: "underline",
  559. },
  560. }}
  561. >
  562. <Typography variant="body2">
  563. {t("Stock Available")} <br/>
  564. ({uom})
  565. </Typography>
  566. </Box>
  567. );
  568. },
  569. // ✅ 移除 cell 中的 onClick
  570. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  571. const stockAvailable = getStockAvailable(params.row);
  572. const qty = showBaseQty ? params.row.baseStockQty : (stockAvailable || 0);
  573. const uom = showBaseQty ? params.row.stockBaseUom : params.row.stockUom;
  574. return (
  575. <Box sx={{ textAlign: "right" }}>
  576. {decimalFormatter.format(qty || 0)} ({uom || ""})
  577. </Box>
  578. );
  579. },
  580. },
  581. {
  582. field: "bomProcessSeqNo",
  583. headerName: t("Seq No"),
  584. flex: 0.5,
  585. align: "right",
  586. headerAlign: "right",
  587. type: "number",
  588. sortable: false, // ✅ 禁用排序
  589. },
  590. {
  591. field: "stockStatus",
  592. headerName: t("Stock Status"),
  593. flex: 0.5,
  594. align: "center",
  595. headerAlign: "center",
  596. type: "boolean",
  597. sortable: false, // ✅ 禁用排序
  598. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  599. return isStockSufficient(params.row)
  600. ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" />
  601. : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />;
  602. },
  603. },
  604. ];
  605. const pickTableRows = jobOrderLines.map((line, index) => ({
  606. ...line,
  607. //id: line.id || index,
  608. id: index + 1,
  609. }));
  610. const PickTableContent = () => (
  611. <Box sx={{ mt: 2 }}>
  612. <ProcessSummaryHeader processData={processData} />
  613. <Card sx={{ mb: 2 }}>
  614. <CardContent>
  615. <Stack
  616. direction="row"
  617. alignItems="center"
  618. justifyContent="space-between"
  619. spacing={2}
  620. >
  621. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  622. {t("Total lines: ")}<strong>{stockCounts.total}</strong>
  623. </Typography>
  624. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  625. {t("Lines with sufficient stock: ")}<strong style={{ color: "green" }}>{stockCounts.sufficient}</strong>
  626. </Typography>
  627. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  628. {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
  629. </Typography>
  630. {fromJosave && (
  631. <Button
  632. variant="contained"
  633. color="error"
  634. onClick={() => handleDeleteJobOrder(jobOrderId)}
  635. disabled={processData?.jobOrderStatus !== "planning"}
  636. >
  637. {t("Delete Job Order")}
  638. </Button>
  639. )}
  640. {fromJosave && (
  641. <Button
  642. variant="contained"
  643. color="primary"
  644. onClick={() => handleRelease(jobOrderId)}
  645. //disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"}
  646. disabled={processData?.jobOrderStatus !== "planning"}
  647. >
  648. {t("Release")}
  649. </Button>
  650. )}
  651. </Stack>
  652. </CardContent>
  653. </Card>
  654. <StyledDataGrid
  655. sx={{ "--DataGrid-overlayHeight": "200px" }}
  656. disableColumnMenu
  657. rows={pickTableRows}
  658. columns={pickTableColumns}
  659. getRowHeight={() => "auto"}
  660. />
  661. </Box>
  662. );
  663. const ProductionProcessesLineRemarkTableContent = () => (
  664. <Box sx={{ mt: 2 }}>
  665. <ProcessSummaryHeader processData={processData} />
  666. <StyledDataGrid
  667. sx={{
  668. "--DataGrid-overlayHeight": "100px",
  669. // ✅ Match ProductionProcessDetail font size (default body2 = 0.875rem)
  670. "& .MuiDataGrid-cell": {
  671. fontSize: "0.875rem", // ✅ Match default body2 size
  672. fontWeight: 500,
  673. },
  674. "& .MuiDataGrid-columnHeader": {
  675. fontSize: "0.875rem", // ✅ Match header size
  676. fontWeight: 600,
  677. },
  678. // ✅ Ensure empty columns are visible
  679. "& .MuiDataGrid-columnHeaders": {
  680. display: "flex",
  681. },
  682. "& .MuiDataGrid-row": {
  683. display: "flex",
  684. },
  685. }}
  686. disableColumnMenu
  687. rows={productionProcessesLineRemarkTableRows ?? []}
  688. columns={productionProcessesLineRemarkTableColumns}
  689. getRowHeight={() => 'auto'}
  690. hideFooter={false} // ✅ Ensure footer is visible
  691. />
  692. </Box>
  693. );
  694. return (
  695. <Box>
  696. {/* 返回按钮 */}
  697. <Box sx={{ mb: 2 }}>
  698. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  699. {t("Back to List")}
  700. </Button>
  701. </Box>
  702. {/* 标签页 */}
  703. <Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
  704. <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
  705. <Tab label={t("Job Order Info")} />
  706. <Tab label={t("BoM Material")} />
  707. <Tab label={t("Production Process")} />
  708. <Tab label={t("Production Process Line Remark")} />
  709. {/* {!fromJosave && (
  710. <Tab label={t("Matching Stock")} />
  711. )} */}
  712. </Tabs>
  713. </Box>
  714. {/* 标签页内容 */}
  715. <Box sx={{ p: 2 }}>
  716. {tabIndex === 0 && <InfoCardContent />}
  717. {tabIndex === 1 && <PickTableContent />}
  718. {tabIndex === 2 && (
  719. <ProductionProcessDetail
  720. jobOrderId={jobOrderId}
  721. onBack={() => {
  722. // 切换回第一个标签页,或者什么都不做
  723. setTabIndex(0);
  724. }}
  725. fromJosave={fromJosave}
  726. />
  727. )}
  728. {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}
  729. {/* {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} */}
  730. <Dialog
  731. open={openOperationPriorityDialog}
  732. onClose={handleClosePriorityDialog}
  733. fullWidth
  734. maxWidth="xs"
  735. >
  736. <DialogTitle>{t("Update Production Priority")}</DialogTitle>
  737. <DialogContent>
  738. <TextField
  739. autoFocus
  740. margin="dense"
  741. label={t("Production Priority")}
  742. type="number"
  743. fullWidth
  744. value={operationPriority}
  745. onChange={(e) => setOperationPriority(Number(e.target.value))}
  746. />
  747. </DialogContent>
  748. <DialogActions>
  749. <Button onClick={handleClosePriorityDialog}>{t("Cancel")}</Button>
  750. <Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button>
  751. </DialogActions>
  752. </Dialog>
  753. <Dialog
  754. open={openPlanStartDialog}
  755. onClose={handleClosePlanStartDialog}
  756. fullWidth
  757. maxWidth="xs"
  758. >
  759. <DialogTitle>{t("Update Target Production Date")}</DialogTitle>
  760. <DialogContent>
  761. <LocalizationProvider dateAdapter={AdapterDayjs}>
  762. <DatePicker
  763. label={t("Target Production Date")}
  764. value={planStartDate}
  765. onChange={(newValue) => setPlanStartDate(newValue)}
  766. slotProps={{
  767. textField: {
  768. fullWidth: true,
  769. margin: "dense",
  770. autoFocus: true,
  771. }
  772. }}
  773. />
  774. </LocalizationProvider>
  775. </DialogContent>
  776. <DialogActions>
  777. <Button onClick={handleClosePlanStartDialog}>{t("Cancel")}</Button>
  778. <Button
  779. variant="contained"
  780. onClick={handleConfirmPlanStart}
  781. disabled={!planStartDate}
  782. >
  783. {t("Save")}
  784. </Button>
  785. </DialogActions>
  786. </Dialog>
  787. <Dialog
  788. open={openReqQtyDialog}
  789. onClose={handleCloseReqQtyDialog}
  790. fullWidth
  791. maxWidth="sm"
  792. >
  793. <DialogTitle>{t("Update Required Quantity")}</DialogTitle>
  794. <DialogContent>
  795. <Stack spacing={2} sx={{ mt: 1 }}>
  796. <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
  797. <TextField
  798. label={t("Base Qty")}
  799. fullWidth
  800. type="number"
  801. variant="outlined"
  802. value={selectedBomForReqQty?.outputQty || 0}
  803. disabled
  804. InputProps={{
  805. endAdornment: selectedBomForReqQty?.outputQtyUom ? (
  806. <InputAdornment position="end">
  807. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  808. {selectedBomForReqQty.outputQtyUom}
  809. </Typography>
  810. </InputAdornment>
  811. ) : null
  812. }}
  813. sx={{ flex: 1 }}
  814. />
  815. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  816. ×
  817. </Typography>
  818. <TextField
  819. label={t("Batch Count")}
  820. fullWidth
  821. type="number"
  822. variant="outlined"
  823. value={reqQtyMultiplier}
  824. onChange={(e) => {
  825. const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
  826. setReqQtyMultiplier(val);
  827. }}
  828. inputProps={{
  829. min: 1,
  830. step: 1
  831. }}
  832. sx={{ flex: 1 }}
  833. />
  834. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  835. =
  836. </Typography>
  837. <TextField
  838. label={t("Req. Qty")}
  839. fullWidth
  840. variant="outlined"
  841. type="number"
  842. value={selectedBomForReqQty ? (reqQtyMultiplier * selectedBomForReqQty.outputQty) : ""}
  843. disabled
  844. InputProps={{
  845. endAdornment: selectedBomForReqQty?.outputQtyUom ? (
  846. <InputAdornment position="end">
  847. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  848. {selectedBomForReqQty.outputQtyUom}
  849. </Typography>
  850. </InputAdornment>
  851. ) : null
  852. }}
  853. sx={{ flex: 1 }}
  854. />
  855. </Box>
  856. </Stack>
  857. </DialogContent>
  858. <DialogActions>
  859. <Button onClick={handleCloseReqQtyDialog}>{t("Cancel")}</Button>
  860. <Button
  861. variant="contained"
  862. onClick={handleConfirmReqQty}
  863. disabled={!selectedBomForReqQty || reqQtyMultiplier < 1}
  864. >
  865. {t("Save")}
  866. </Button>
  867. </DialogActions>
  868. </Dialog>
  869. </Box>
  870. </Box>
  871. );
  872. };
  873. export default ProductionProcessJobOrderDetail;