FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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