"use client"; import React, { useCallback, useEffect, useState, useMemo } from "react"; import { Box, Button, Paper, Stack, Typography, TextField, Grid, Card, CardContent, CircularProgress, Tabs, Tab, TabsProps, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, InputAdornment } from "@mui/material"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { useTranslation } from "react-i18next"; import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart,updateJoReqQty,newProductProcessLine,JobOrderLineInfo} from "@/app/api/jo/actions"; import ProductionProcessDetail from "./ProductionProcessDetail"; import { BomCombo } from "@/app/api/bom"; import { fetchBomCombo } from "@/app/api/bom/index"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { decimalFormatter } from "@/app/utils/formatUtil"; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; import { releaseJo, startJo } from "@/app/api/jo/actions"; import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; import ProcessSummaryHeader from "./ProcessSummaryHeader"; import EditIcon from "@mui/icons-material/Edit"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { dayjsToDateString } from "@/app/utils/formatUtil"; interface ProductProcessJobOrderDetailProps { jobOrderId: number; onBack: () => void; fromJosave?: boolean; } const ProductionProcessJobOrderDetail: React.FC = ({ jobOrderId, onBack, fromJosave, }) => { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [processData, setProcessData] = useState(null); const [jobOrderLines, setJobOrderLines] = useState([]); const [inventoryData, setInventoryData] = useState([]); const [tabIndex, setTabIndex] = useState(0); const [selectedProcessId, setSelectedProcessId] = useState(null); const [operationPriority, setOperationPriority] = useState(50); const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false); const [planStartDate, setPlanStartDate] = useState(null); const [openReqQtyDialog, setOpenReqQtyDialog] = useState(false); const [reqQtyMultiplier, setReqQtyMultiplier] = useState(1); const [selectedBomForReqQty, setSelectedBomForReqQty] = useState(null); const [bomCombo, setBomCombo] = useState([]); const [showBaseQty, setShowBaseQty] = useState(false); const fetchData = useCallback(async () => { setLoading(true); try { const data = await fetchProductProcessesByJobOrderId(jobOrderId); if (data && data.length > 0) { const firstProcess = data[0]; setProcessData(firstProcess); setJobOrderLines((firstProcess as any).jobOrderLines || []); } } catch (error) { console.error("Error loading data:", error); } finally { setLoading(false); } }, [jobOrderId]); const toggleBaseQty = useCallback(() => { setShowBaseQty(prev => !prev); }, []); // 4. 添加处理函数(约第 166 行后) const handleOpenReqQtyDialog = useCallback(async () => { if (!processData || !processData.outputQty || !processData.outputQtyUom) { alert(t("BOM data not available")); return; } const baseOutputQty = processData.bomBaseQty; const currentMultiplier = baseOutputQty > 0 ? Math.round(processData.outputQty / baseOutputQty) : 1; const bomData = { id: processData.bomId || 0, value: processData.bomId || 0, label: processData.bomDescription || "", outputQty: baseOutputQty, outputQtyUom: processData.outputQtyUom, description: processData.bomDescription || "" }; setSelectedBomForReqQty(bomData); setReqQtyMultiplier(currentMultiplier); setOpenReqQtyDialog(true); }, [processData, t]); const handleCloseReqQtyDialog = useCallback(() => { setOpenReqQtyDialog(false); setSelectedBomForReqQty(null); setReqQtyMultiplier(1); }, []); const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => { try { const response = await updateJoReqQty({ id: jobOrderId, reqQty: Math.round(newReqQty) }); if (response) { await fetchData(); } } catch (error) { console.error("Error updating reqQty:", error); alert(t("update failed")); } }, [fetchData, t]); const handleConfirmReqQty = useCallback(async () => { if (!jobOrderId || !selectedBomForReqQty) return; const newReqQty = reqQtyMultiplier * selectedBomForReqQty.outputQty; await handleUpdateReqQty(jobOrderId, newReqQty); setOpenReqQtyDialog(false); setSelectedBomForReqQty(null); setReqQtyMultiplier(1); }, [jobOrderId, selectedBomForReqQty, reqQtyMultiplier, handleUpdateReqQty]); // 获取库存数据 useEffect(() => { const fetchInventoryData = async () => { try { const inventoryResponse = await fetchInventories({ code: "", name: "", type: "", pageNum: 0, pageSize: 1000 }); setInventoryData(inventoryResponse.records); } catch (error) { console.error("Error fetching inventory data:", error); } }; fetchInventoryData(); }, []); useEffect(() => { fetchData(); }, [fetchData]); // PickTable 组件内容 const getStockAvailable = (line: JobOrderLineInfo) => { if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { return line.stockQty || 0; } const inventory = inventoryData.find(inv => inv.itemCode === line.itemCode || inv.itemName === line.itemName ); if (inventory) { return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); } return line.stockQty || 0; }; const handleOpenPlanStartDialog = useCallback(() => { // 将 processData.date 转换为 dayjs 对象 if (processData?.date) { // processData.date 可能是字符串或 Date 对象 setPlanStartDate(dayjs(processData.date)); } else { setPlanStartDate(dayjs()); } setOpenPlanStartDialog(true); }, [processData?.date]); const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { setOpenPlanStartDialog(false); setPlanStartDate(null); }, []); const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => { const response = await updateJoPlanStart({ id: jobOrderId, planStart }); if (response) { await fetchData(); } }, [fetchData]); const handleConfirmPlanStart = useCallback(async () => { if (!jobOrderId || !planStartDate) return; // 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss) const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`; await handleUpdatePlanStart(jobOrderId, dateString); setOpenPlanStartDialog(false); setPlanStartDate(null); }, [jobOrderId, planStartDate, handleUpdatePlanStart]); const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => { const response = await updateProductProcessPriority(productProcessId, productionPriority) if (response) { await fetchData(); } }, [jobOrderId]); const handleOpenPriorityDialog = () => { setOperationPriority(processData?.productionPriority ?? 50); setOpenOperationPriorityDialog(true); }; const handleClosePriorityDialog = (_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { setOpenOperationPriorityDialog(false); }; const handleConfirmPriority = async () => { if (!processData?.id) return; await handleUpdateOperationPriority(processData.id, Number(operationPriority)); setOpenOperationPriorityDialog(false); }; const isStockSufficient = (line: JobOrderLineInfo) => { if (line.type?.toLowerCase() === "consumables") { return false; } const stockAvailable = getStockAvailable(line); if (stockAvailable === null) { return false; } return stockAvailable >= line.reqQty; }; const stockCounts = useMemo(() => { // 过滤掉 consumables 类型的 lines const nonConsumablesLines = jobOrderLines.filter( line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" && line.type?.toLowerCase() !== "nm" ); const total = nonConsumablesLines.length; const sufficient = nonConsumablesLines.filter(isStockSufficient).length; return { total, sufficient, insufficient: total - sufficient, }; }, [jobOrderLines, inventoryData]); const status = processData?.status?.toLowerCase?.() ?? ""; const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => { const response = await deleteJobOrder(jobOrderId) if (response) { //setProcessData(response.entity); //await fetchData(); onBack(); } }, [jobOrderId]); const handleRelease = useCallback(async ( jobOrderId: number) => { // TODO: 替换为实际的 release 调用 console.log("Release clicked for jobOrderId:", jobOrderId); const response = await releaseJo({ id: jobOrderId }) if (response) { //setProcessData(response.entity); await fetchData(); } }, [jobOrderId]); const handleTabChange = useCallback>( (_e, newValue) => { setTabIndex(newValue); }, [], ); // 如果选择了 process detail,显示 detail 页面 if (selectedProcessId !== null) { return ( { setSelectedProcessId(null); fetchData(); // 刷新数据 }} /> ); } if (loading) { return ( ); } if (!processData) { return ( {t("No data found")} ); } // InfoCard 组件内容 const InfoCardContent = () => ( ) : null), }} /> ) : null), }} /> ), }} /> ); const productionProcessesLineRemarkTableColumns: GridColDef[] = [ { field: "seqNo", headerName: t("Seq"), flex: 0.2, align: "left", headerAlign: "left", type: "number", renderCell: (params) => { return {params.value}; }, }, { field: "description", headerName: t("Remark"), flex: 1, align: "left", headerAlign: "left", renderCell: (params) => { return(   {params.value || ""}   ) }, }, ]; const productionProcessesLineRemarkTableRows = processData?.productProcessLines?.map((line: any) => ({ id: line.seqNo, seqNo: line.seqNo, description: line.description ?? "", })) ?? []; const pickTableColumns: GridColDef[] = [ { field: "id", headerName: t("id"), flex: 0.2, align: "left", headerAlign: "left", type: "number", sortable: false, // ✅ 禁用排序 }, { field: "itemCode", headerName: t("Material Code"), flex: 0.6, sortable: false, // ✅ 禁用排序 }, { field: "itemName", headerName: t("Item Name"), flex: 1, sortable: false, // ✅ 禁用排序 renderCell: (params: GridRenderCellParams) => { return `${params.value} (${params.row.reqUom})`; }, }, { field: "reqQty", headerName: t("Bom Req. Qty"), flex: 0.7, align: "right", headerAlign: "right", sortable: false, // ✅ 禁用排序 // ✅ 将切换功能移到 header renderHeader: () => { const qty = showBaseQty ? t("Base") : t("Req"); const uom = showBaseQty ? t("Base UOM") : t(" "); return ( {t("Bom Req. Qty")} ({uom}) ); }, // ✅ 移除 cell 中的 onClick,只显示值 renderCell: (params: GridRenderCellParams) => { const qty = showBaseQty ? params.row.baseReqQty : params.value; const uom = showBaseQty ? params.row.reqBaseUom : params.row.reqUom; return ( {decimalFormatter.format(qty || 0)} ({uom || ""}) ); }, }, { field: "stockReqQty", headerName: t("Stock Req. Qty"), flex: 0.7, align: "right", headerAlign: "right", sortable: false, // ✅ 禁用排序 // ✅ 将切换功能移到 header renderHeader: () => { const uom = showBaseQty ? t("Base UOM") : t("Stock UOM"); return ( {t("Stock Req. Qty")} ({uom}) ); }, // ✅ 移除 cell 中的 onClick renderCell: (params: GridRenderCellParams) => { const qty = showBaseQty ? params.row.baseReqQty : params.value; const uom = showBaseQty ? params.row.reqBaseUom : params.row.stockUom; return ( {decimalFormatter.format(qty || 0)} ({uom || ""}) ); }, }, { field: "stockAvailable", headerName: t("Stock Available"), flex: 0.7, align: "right", headerAlign: "right", type: "number", sortable: false, // ✅ 禁用排序 // ✅ 将切换功能移到 header renderHeader: () => { const uom = showBaseQty ? t("Base UOM") : t("Stock UOM"); return ( {t("Stock Available")} ({uom}) ); }, // ✅ 移除 cell 中的 onClick renderCell: (params: GridRenderCellParams) => { const stockAvailable = getStockAvailable(params.row); const qty = showBaseQty ? params.row.baseStockQty : (stockAvailable || 0); const uom = showBaseQty ? params.row.stockBaseUom : params.row.stockUom; return ( {decimalFormatter.format(qty || 0)} ({uom || ""}) ); }, }, { field: "bomProcessSeqNo", headerName: t("Seq No"), flex: 0.5, align: "right", headerAlign: "right", type: "number", sortable: false, // ✅ 禁用排序 }, { field: "stockStatus", headerName: t("Stock Status"), flex: 0.5, align: "center", headerAlign: "center", type: "boolean", sortable: false, // ✅ 禁用排序 renderCell: (params: GridRenderCellParams) => { return isStockSufficient(params.row) ? : ; }, }, ]; const pickTableRows = jobOrderLines.map((line, index) => ({ ...line, //id: line.id || index, id: index + 1, })); const PickTableContent = () => ( {t("Total lines: ")}{stockCounts.total} {t("Lines with sufficient stock: ")}{stockCounts.sufficient} {t("Lines with insufficient stock: ")}{stockCounts.insufficient} {fromJosave && ( )} {fromJosave && ( )} "auto"} /> ); const ProductionProcessesLineRemarkTableContent = () => ( 'auto'} hideFooter={false} // ✅ Ensure footer is visible /> ); return ( {/* 返回按钮 */} {/* 标签页 */} {/* {!fromJosave && ( )} */} {/* 标签页内容 */} {tabIndex === 0 && } {tabIndex === 1 && } {tabIndex === 2 && ( { // 切换回第一个标签页,或者什么都不做 setTabIndex(0); }} fromJosave={fromJosave} /> )} {tabIndex === 3 && } {/* {tabIndex === 4 && } */} {t("Update Production Priority")} setOperationPriority(Number(e.target.value))} /> {t("Update Target Production Date")} setPlanStartDate(newValue)} slotProps={{ textField: { fullWidth: true, margin: "dense", autoFocus: true, } }} /> {t("Update Required Quantity")} {selectedBomForReqQty.outputQtyUom} ) : null }} sx={{ flex: 1 }} /> × { const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value))); setReqQtyMultiplier(val); }} inputProps={{ min: 1, step: 1 }} sx={{ flex: 1 }} /> = {selectedBomForReqQty.outputQtyUom} ) : null }} sx={{ flex: 1 }} /> ); }; export default ProductionProcessJobOrderDetail;