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.
 
 

997 lines
32 KiB

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