FPSMS-frontend
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

920 lignes
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. // processData.date 可能是字符串或 Date 对象
  177. setPlanStartDate(dayjs(processData.date));
  178. } else {
  179. setPlanStartDate(dayjs());
  180. }
  181. setOpenPlanStartDialog(true);
  182. }, [processData?.date]);
  183. const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  184. setOpenPlanStartDialog(false);
  185. setPlanStartDate(null);
  186. }, []);
  187. const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => {
  188. const response = await updateJoPlanStart({ id: jobOrderId, planStart });
  189. if (response) {
  190. await fetchData();
  191. }
  192. }, [fetchData]);
  193. const handleConfirmPlanStart = useCallback(async () => {
  194. if (!jobOrderId || !planStartDate) return;
  195. // 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss)
  196. const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`;
  197. await handleUpdatePlanStart(jobOrderId, dateString);
  198. setOpenPlanStartDialog(false);
  199. setPlanStartDate(null);
  200. }, [jobOrderId, planStartDate, handleUpdatePlanStart]);
  201. const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
  202. const response = await updateProductProcessPriority(productProcessId, productionPriority)
  203. if (response) {
  204. await fetchData();
  205. }
  206. }, [jobOrderId]);
  207. const handleOpenPriorityDialog = () => {
  208. setOperationPriority(processData?.productionPriority ?? 50);
  209. setOpenOperationPriorityDialog(true);
  210. };
  211. const handleClosePriorityDialog = (_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  212. setOpenOperationPriorityDialog(false);
  213. };
  214. const handleConfirmPriority = async () => {
  215. if (!processData?.id) return;
  216. await handleUpdateOperationPriority(processData.id, Number(operationPriority));
  217. setOpenOperationPriorityDialog(false);
  218. };
  219. const isStockSufficient = (line: JobOrderLineInfo) => {
  220. if (line.type?.toLowerCase() === "consumables") {
  221. return false;
  222. }
  223. const stockAvailable = getStockAvailable(line);
  224. if (stockAvailable === null) {
  225. return false;
  226. }
  227. return stockAvailable >= line.reqQty;
  228. };
  229. const stockCounts = useMemo(() => {
  230. // 过滤掉 consumables 类型的 lines
  231. const nonConsumablesLines = jobOrderLines.filter(
  232. line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" && line.type?.toLowerCase() !== "nm"
  233. );
  234. const total = nonConsumablesLines.length;
  235. const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
  236. return {
  237. total,
  238. sufficient,
  239. insufficient: total - sufficient,
  240. };
  241. }, [jobOrderLines, inventoryData]);
  242. const status = processData?.status?.toLowerCase?.() ?? "";
  243. const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
  244. const response = await deleteJobOrder(jobOrderId)
  245. if (response) {
  246. //setProcessData(response.entity);
  247. //await fetchData();
  248. onBack();
  249. }
  250. }, [jobOrderId]);
  251. const handleRelease = useCallback(async ( jobOrderId: number) => {
  252. // TODO: 替换为实际的 release 调用
  253. console.log("Release clicked for jobOrderId:", jobOrderId);
  254. const response = await releaseJo({ id: jobOrderId })
  255. if (response) {
  256. //setProcessData(response.entity);
  257. await fetchData();
  258. }
  259. }, [jobOrderId]);
  260. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  261. (_e, newValue) => {
  262. setTabIndex(newValue);
  263. },
  264. [],
  265. );
  266. // 如果选择了 process detail,显示 detail 页面
  267. if (selectedProcessId !== null) {
  268. return (
  269. <ProductionProcessDetail
  270. jobOrderId={selectedProcessId}
  271. onBack={() => {
  272. setSelectedProcessId(null);
  273. fetchData(); // 刷新数据
  274. }}
  275. />
  276. );
  277. }
  278. if (loading) {
  279. return (
  280. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  281. <CircularProgress/>
  282. </Box>
  283. );
  284. }
  285. if (!processData) {
  286. return (
  287. <Box>
  288. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  289. {t("Back")}
  290. </Button>
  291. <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography>
  292. </Box>
  293. );
  294. }
  295. // InfoCard 组件内容
  296. const InfoCardContent = () => (
  297. <Card sx={{ display: "block", mt: 2 }}>
  298. <CardContent component={Stack} spacing={4}>
  299. <Box>
  300. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  301. <Grid item xs={6}>
  302. <TextField
  303. label={t("Job Order Code")}
  304. fullWidth
  305. disabled={true}
  306. value={processData?.jobOrderCode || ""}
  307. />
  308. </Grid>
  309. <Grid item xs={6}>
  310. <TextField
  311. label={t("Item Code")}
  312. fullWidth
  313. disabled={true}
  314. value={processData?.itemCode+"-"+processData?.itemName || ""}
  315. />
  316. </Grid>
  317. <Grid item xs={6}>
  318. <TextField
  319. label={t("Job Type")}
  320. fullWidth
  321. disabled={true}
  322. value={t(processData?.jobType) || t("N/A")}
  323. //value={t("N/A")}
  324. />
  325. </Grid>
  326. <Grid item xs={6}>
  327. <TextField
  328. label={t("Req. Qty")}
  329. fullWidth
  330. disabled={true}
  331. value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""}
  332. InputProps={{
  333. endAdornment: (processData?.jobOrderStatus === "planning" ? (
  334. <InputAdornment position="end">
  335. <IconButton size="small" onClick={handleOpenReqQtyDialog}>
  336. <EditIcon fontSize="small" />
  337. </IconButton>
  338. </InputAdornment>
  339. ) : null),
  340. }}
  341. />
  342. </Grid>
  343. <Grid item xs={6}>
  344. <TextField
  345. value={processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}
  346. label={t("Target Production Date")}
  347. fullWidth
  348. disabled={true}
  349. InputProps={{
  350. endAdornment: (processData?.jobOrderStatus === "planning" ? (
  351. <InputAdornment position="end">
  352. <IconButton size="small" onClick={handleOpenPlanStartDialog}>
  353. <EditIcon fontSize="small" />
  354. </IconButton>
  355. </InputAdornment>
  356. ) : null),
  357. }}
  358. />
  359. </Grid>
  360. <Grid item xs={6}>
  361. <TextField
  362. label={t("Production Priority")}
  363. fullWidth
  364. disabled={true}
  365. value={processData?.productionPriority ?? "50"}
  366. InputProps={{
  367. endAdornment: (
  368. <InputAdornment position="end">
  369. <IconButton size="small" onClick={handleOpenPriorityDialog}>
  370. <EditIcon fontSize="small" />
  371. </IconButton>
  372. </InputAdornment>
  373. ),
  374. }}
  375. />
  376. </Grid>
  377. <Grid item xs={6}>
  378. <TextField
  379. label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity")}
  380. fullWidth
  381. disabled={true}
  382. 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}`}
  383. />
  384. </Grid>
  385. </Grid>
  386. </Box>
  387. </CardContent>
  388. </Card>
  389. );
  390. const productionProcessesLineRemarkTableColumns: GridColDef[] = [
  391. {
  392. field: "seqNo",
  393. headerName: t("Seq"),
  394. flex: 0.2,
  395. align: "left",
  396. headerAlign: "left",
  397. type: "number",
  398. renderCell: (params) => {
  399. return <Typography sx={{ fontWeight: 500 }}>{params.value}</Typography>;
  400. },
  401. },
  402. {
  403. field: "description",
  404. headerName: t("Remark"),
  405. flex: 1,
  406. align: "left",
  407. headerAlign: "left",
  408. renderCell: (params) => {
  409. return(
  410. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  411. <Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
  412. <Typography sx={{ fontWeight: 500 }}>{params.value || ""}</Typography>
  413. <Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
  414. </Box>
  415. )
  416. },
  417. },
  418. ];
  419. const productionProcessesLineRemarkTableRows =
  420. processData?.productProcessLines?.map((line: any) => ({
  421. id: line.seqNo,
  422. seqNo: line.seqNo,
  423. description: line.description ?? "",
  424. })) ?? [];
  425. const pickTableColumns: GridColDef[] = [
  426. {
  427. field: "id",
  428. headerName: t("id"),
  429. flex: 0.2,
  430. align: "left",
  431. headerAlign: "left",
  432. type: "number",
  433. sortable: false, // ✅ 禁用排序
  434. },
  435. {
  436. field: "itemCode",
  437. headerName: t("Material Code"),
  438. flex: 0.6,
  439. sortable: false, // ✅ 禁用排序
  440. },
  441. {
  442. field: "itemName",
  443. headerName: t("Item Name"),
  444. flex: 1,
  445. sortable: false, // ✅ 禁用排序
  446. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  447. return `${params.value} (${params.row.reqUom})`;
  448. },
  449. },
  450. {
  451. field: "reqQty",
  452. headerName: t("Bom Req. Qty"),
  453. flex: 0.7,
  454. align: "right",
  455. headerAlign: "right",
  456. sortable: false, // ✅ 禁用排序
  457. // ✅ 将切换功能移到 header
  458. renderHeader: () => {
  459. const qty = showBaseQty ? t("Base") : t("Req");
  460. const uom = showBaseQty ? t("Base UOM") : t(" ");
  461. return (
  462. <Box
  463. onClick={toggleBaseQty}
  464. sx={{
  465. cursor: "pointer",
  466. userSelect: "none",
  467. width: "100%",
  468. textAlign: "right",
  469. "&:hover": {
  470. textDecoration: "underline",
  471. },
  472. }}
  473. >
  474. {t("Bom Req. Qty")} ({uom})
  475. </Box>
  476. );
  477. },
  478. // ✅ 移除 cell 中的 onClick,只显示值
  479. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  480. const qty = showBaseQty ? params.row.baseReqQty : params.value;
  481. const uom = showBaseQty ? params.row.reqBaseUom : params.row.reqUom;
  482. return (
  483. <Box sx={{ textAlign: "right" }}>
  484. {decimalFormatter.format(qty || 0)} ({uom || ""})
  485. </Box>
  486. );
  487. },
  488. },
  489. {
  490. field: "stockReqQty",
  491. headerName: t("Stock Req. Qty"),
  492. flex: 0.7,
  493. align: "right",
  494. headerAlign: "right",
  495. sortable: false, // ✅ 禁用排序
  496. // ✅ 将切换功能移到 header
  497. renderHeader: () => {
  498. const uom = showBaseQty ? t("Base UOM") : t("Stock UOM");
  499. return (
  500. <Box
  501. onClick={toggleBaseQty}
  502. sx={{
  503. cursor: "pointer",
  504. userSelect: "none",
  505. width: "100%",
  506. textAlign: "right",
  507. "&:hover": {
  508. textDecoration: "underline",
  509. },
  510. }}
  511. >
  512. {t("Stock Req. Qty")} ({uom})
  513. </Box>
  514. );
  515. },
  516. // ✅ 移除 cell 中的 onClick
  517. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  518. const qty = showBaseQty ? params.row.baseReqQty : params.value;
  519. const uom = showBaseQty ? params.row.reqBaseUom : params.row.stockUom;
  520. return (
  521. <Box sx={{ textAlign: "right" }}>
  522. {decimalFormatter.format(qty || 0)} ({uom || ""})
  523. </Box>
  524. );
  525. },
  526. },
  527. {
  528. field: "stockAvailable",
  529. headerName: t("Stock Available"),
  530. flex: 0.7,
  531. align: "right",
  532. headerAlign: "right",
  533. type: "number",
  534. sortable: false, // ✅ 禁用排序
  535. // ✅ 将切换功能移到 header
  536. renderHeader: () => {
  537. const uom = showBaseQty ? t("Base UOM") : t("Stock UOM");
  538. return (
  539. <Box
  540. onClick={toggleBaseQty}
  541. sx={{
  542. cursor: "pointer",
  543. userSelect: "none",
  544. width: "100%",
  545. textAlign: "right",
  546. "&:hover": {
  547. textDecoration: "underline",
  548. },
  549. }}
  550. >
  551. {t("Stock Available")} ({uom})
  552. </Box>
  553. );
  554. },
  555. // ✅ 移除 cell 中的 onClick
  556. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  557. const stockAvailable = getStockAvailable(params.row);
  558. const qty = showBaseQty ? params.row.baseStockQty : (stockAvailable || 0);
  559. const uom = showBaseQty ? params.row.stockBaseUom : params.row.stockUom;
  560. return (
  561. <Box sx={{ textAlign: "right" }}>
  562. {decimalFormatter.format(qty || 0)} ({uom || ""})
  563. </Box>
  564. );
  565. },
  566. },
  567. {
  568. field: "bomProcessSeqNo",
  569. headerName: t("Seq No"),
  570. flex: 0.5,
  571. align: "right",
  572. headerAlign: "right",
  573. type: "number",
  574. sortable: false, // ✅ 禁用排序
  575. },
  576. {
  577. field: "stockStatus",
  578. headerName: t("Stock Status"),
  579. flex: 0.5,
  580. align: "center",
  581. headerAlign: "center",
  582. type: "boolean",
  583. sortable: false, // ✅ 禁用排序
  584. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  585. return isStockSufficient(params.row)
  586. ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" />
  587. : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />;
  588. },
  589. },
  590. ];
  591. const pickTableRows = jobOrderLines.map((line, index) => ({
  592. ...line,
  593. //id: line.id || index,
  594. id: index + 1,
  595. }));
  596. const PickTableContent = () => (
  597. <Box sx={{ mt: 2 }}>
  598. <ProcessSummaryHeader processData={processData} />
  599. <Card sx={{ mb: 2 }}>
  600. <CardContent>
  601. <Stack
  602. direction="row"
  603. alignItems="center"
  604. justifyContent="space-between"
  605. spacing={2}
  606. >
  607. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  608. {t("Total lines: ")}<strong>{stockCounts.total}</strong>
  609. </Typography>
  610. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  611. {t("Lines with sufficient stock: ")}<strong style={{ color: "green" }}>{stockCounts.sufficient}</strong>
  612. </Typography>
  613. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  614. {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
  615. </Typography>
  616. {fromJosave && (
  617. <Button
  618. variant="contained"
  619. color="error"
  620. onClick={() => handleDeleteJobOrder(jobOrderId)}
  621. disabled={processData?.jobOrderStatus !== "planning"}
  622. >
  623. {t("Delete Job Order")}
  624. </Button>
  625. )}
  626. {fromJosave && (
  627. <Button
  628. variant="contained"
  629. color="primary"
  630. onClick={() => handleRelease(jobOrderId)}
  631. //disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"}
  632. disabled={processData?.jobOrderStatus !== "planning"}
  633. >
  634. {t("Release")}
  635. </Button>
  636. )}
  637. </Stack>
  638. </CardContent>
  639. </Card>
  640. <StyledDataGrid
  641. sx={{ "--DataGrid-overlayHeight": "100px" }}
  642. disableColumnMenu
  643. rows={pickTableRows}
  644. columns={pickTableColumns}
  645. getRowHeight={() => "auto"}
  646. />
  647. </Box>
  648. );
  649. const ProductionProcessesLineRemarkTableContent = () => (
  650. <Box sx={{ mt: 2 }}>
  651. <ProcessSummaryHeader processData={processData} />
  652. <StyledDataGrid
  653. sx={{
  654. "--DataGrid-overlayHeight": "100px",
  655. // ✅ Match ProductionProcessDetail font size (default body2 = 0.875rem)
  656. "& .MuiDataGrid-cell": {
  657. fontSize: "0.875rem", // ✅ Match default body2 size
  658. fontWeight: 500,
  659. },
  660. "& .MuiDataGrid-columnHeader": {
  661. fontSize: "0.875rem", // ✅ Match header size
  662. fontWeight: 600,
  663. },
  664. // ✅ Ensure empty columns are visible
  665. "& .MuiDataGrid-columnHeaders": {
  666. display: "flex",
  667. },
  668. "& .MuiDataGrid-row": {
  669. display: "flex",
  670. },
  671. }}
  672. disableColumnMenu
  673. rows={productionProcessesLineRemarkTableRows ?? []}
  674. columns={productionProcessesLineRemarkTableColumns}
  675. getRowHeight={() => 'auto'}
  676. hideFooter={false} // ✅ Ensure footer is visible
  677. />
  678. </Box>
  679. );
  680. return (
  681. <Box>
  682. {/* 返回按钮 */}
  683. <Box sx={{ mb: 2 }}>
  684. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  685. {t("Back to List")}
  686. </Button>
  687. </Box>
  688. {/* 标签页 */}
  689. <Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
  690. <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
  691. <Tab label={t("Job Order Info")} />
  692. <Tab label={t("BoM Material")} />
  693. <Tab label={t("Production Process")} />
  694. <Tab label={t("Production Process Line Remark")} />
  695. {/* {!fromJosave && (
  696. <Tab label={t("Matching Stock")} />
  697. )} */}
  698. </Tabs>
  699. </Box>
  700. {/* 标签页内容 */}
  701. <Box sx={{ p: 2 }}>
  702. {tabIndex === 0 && <InfoCardContent />}
  703. {tabIndex === 1 && <PickTableContent />}
  704. {tabIndex === 2 && (
  705. <ProductionProcessDetail
  706. jobOrderId={jobOrderId}
  707. onBack={() => {
  708. // 切换回第一个标签页,或者什么都不做
  709. setTabIndex(0);
  710. }}
  711. fromJosave={fromJosave}
  712. />
  713. )}
  714. {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}
  715. {/* {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} */}
  716. <Dialog
  717. open={openOperationPriorityDialog}
  718. onClose={handleClosePriorityDialog}
  719. fullWidth
  720. maxWidth="xs"
  721. >
  722. <DialogTitle>{t("Update Production Priority")}</DialogTitle>
  723. <DialogContent>
  724. <TextField
  725. autoFocus
  726. margin="dense"
  727. label={t("Production Priority")}
  728. type="number"
  729. fullWidth
  730. value={operationPriority}
  731. onChange={(e) => setOperationPriority(Number(e.target.value))}
  732. />
  733. </DialogContent>
  734. <DialogActions>
  735. <Button onClick={handleClosePriorityDialog}>{t("Cancel")}</Button>
  736. <Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button>
  737. </DialogActions>
  738. </Dialog>
  739. <Dialog
  740. open={openPlanStartDialog}
  741. onClose={handleClosePlanStartDialog}
  742. fullWidth
  743. maxWidth="xs"
  744. >
  745. <DialogTitle>{t("Update Target Production Date")}</DialogTitle>
  746. <DialogContent>
  747. <LocalizationProvider dateAdapter={AdapterDayjs}>
  748. <DatePicker
  749. label={t("Target Production Date")}
  750. value={planStartDate}
  751. onChange={(newValue) => setPlanStartDate(newValue)}
  752. slotProps={{
  753. textField: {
  754. fullWidth: true,
  755. margin: "dense",
  756. autoFocus: true,
  757. }
  758. }}
  759. />
  760. </LocalizationProvider>
  761. </DialogContent>
  762. <DialogActions>
  763. <Button onClick={handleClosePlanStartDialog}>{t("Cancel")}</Button>
  764. <Button
  765. variant="contained"
  766. onClick={handleConfirmPlanStart}
  767. disabled={!planStartDate}
  768. >
  769. {t("Save")}
  770. </Button>
  771. </DialogActions>
  772. </Dialog>
  773. <Dialog
  774. open={openReqQtyDialog}
  775. onClose={handleCloseReqQtyDialog}
  776. fullWidth
  777. maxWidth="sm"
  778. >
  779. <DialogTitle>{t("Update Required Quantity")}</DialogTitle>
  780. <DialogContent>
  781. <Stack spacing={2} sx={{ mt: 1 }}>
  782. <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
  783. <TextField
  784. label={t("Base Qty")}
  785. fullWidth
  786. type="number"
  787. variant="outlined"
  788. value={selectedBomForReqQty?.outputQty || 0}
  789. disabled
  790. InputProps={{
  791. endAdornment: selectedBomForReqQty?.outputQtyUom ? (
  792. <InputAdornment position="end">
  793. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  794. {selectedBomForReqQty.outputQtyUom}
  795. </Typography>
  796. </InputAdornment>
  797. ) : null
  798. }}
  799. sx={{ flex: 1 }}
  800. />
  801. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  802. ×
  803. </Typography>
  804. <TextField
  805. label={t("Batch Count")}
  806. fullWidth
  807. type="number"
  808. variant="outlined"
  809. value={reqQtyMultiplier}
  810. onChange={(e) => {
  811. const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
  812. setReqQtyMultiplier(val);
  813. }}
  814. inputProps={{
  815. min: 1,
  816. step: 1
  817. }}
  818. sx={{ flex: 1 }}
  819. />
  820. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  821. =
  822. </Typography>
  823. <TextField
  824. label={t("Req. Qty")}
  825. fullWidth
  826. variant="outlined"
  827. type="number"
  828. value={selectedBomForReqQty ? (reqQtyMultiplier * selectedBomForReqQty.outputQty) : ""}
  829. disabled
  830. InputProps={{
  831. endAdornment: selectedBomForReqQty?.outputQtyUom ? (
  832. <InputAdornment position="end">
  833. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  834. {selectedBomForReqQty.outputQtyUom}
  835. </Typography>
  836. </InputAdornment>
  837. ) : null
  838. }}
  839. sx={{ flex: 1 }}
  840. />
  841. </Box>
  842. </Stack>
  843. </DialogContent>
  844. <DialogActions>
  845. <Button onClick={handleCloseReqQtyDialog}>{t("Cancel")}</Button>
  846. <Button
  847. variant="contained"
  848. onClick={handleConfirmReqQty}
  849. disabled={!selectedBomForReqQty || reqQtyMultiplier < 1}
  850. >
  851. {t("Save")}
  852. </Button>
  853. </DialogActions>
  854. </Dialog>
  855. </Box>
  856. </Box>
  857. );
  858. };
  859. export default ProductionProcessJobOrderDetail;