From 32f7897ca892d4d1aea80bae27fd03d13936ac2a Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 18 Nov 2025 19:33:28 +0800 Subject: [PATCH] update --- src/app/(main)/production/page.tsx | 5 +- src/app/api/jo/actions.ts | 156 +++-- .../GoodPickExecutionForm.tsx | 7 +- .../ProductionProcessDetail.tsx | 80 ++- .../ProductionProcessJobOrderDetail.tsx | 411 ++++++++++++ .../ProductionProcessList.tsx | 45 +- .../ProductionProcessPage.tsx | 60 +- .../ProductionProcessStepExecution.tsx | 587 +++++++++++------- 8 files changed, 1009 insertions(+), 342 deletions(-) create mode 100644 src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx diff --git a/src/app/(main)/production/page.tsx b/src/app/(main)/production/page.tsx index 24e9e30..9cdec1a 100644 --- a/src/app/(main)/production/page.tsx +++ b/src/app/(main)/production/page.tsx @@ -8,6 +8,7 @@ import Typography from "@mui/material/Typography"; import { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; +import { fetchPrinterCombo } from "@/app/api/settings/printer"; export const metadata: Metadata = { title: "Claims", @@ -15,7 +16,7 @@ export const metadata: Metadata = { const production: React.FC = async () => { const { t } = await getServerI18n("claims"); - + const printerCombo = await fetchPrinterCombo(); return ( <> { {t("Create Process")} */} - {/* Use new component */} + {/* Use new component */} ); }; diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 55ddffa..1070597 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -171,20 +171,31 @@ export interface ProductProcessResponse { } export interface ProductProcessLineResponse { - id: number; - seqNo: number; - name: string; - description?: string; - equipmentType?: string; - startTime?: string; - endTime?: string; - outputFromProcessQty?: number; - outputFromProcessUom?: string; - defectQty?: number; - scrapQty?: number; - byproductName?: string; - byproductQty?: number; - handlerId?: number; + id: number, + bomprocessId: number, + operatorId: number, + operatorName: string, + equipmentId: number, + handlerId: number, + seqNo: number, + name: string, + description: string, + equipment_name: string, + status: string, + byproductId: number, + byproductName: string, + byproductQty: number, + byproductUom: string, + scrapQty: number, + defectQty: number, + defectUom: string, + outputFromProcessQty: number, + outputFromProcessUom: string, + durationInMinutes: number, + prepTimeInMinutes: number, + postProdTimeInMinutes: number, + startTime: string, + endTime: string, } export interface ProductProcessWithLinesResponse { @@ -196,7 +207,19 @@ export interface ProductProcessWithLinesResponse { date: string; bomId?: number; jobOrderId?: number; + jobOrderCode: string; + isDark: string; + isDense: number; + isFloat: string; + itemId: number; + itemCode: string; + itemName: string; + outputQty: number; + outputQtyUom: string; + productionPriority: number; + jobOrderLines: JobOrderLineInfo[]; productProcessLines: ProductProcessLineResponse[]; + } export interface UpdateProductProcessLineQtyRequest { productProcessLineId: number; @@ -241,6 +264,7 @@ export interface AllJoborderProductProcessInfoResponse { bomId?: number; itemName: string; jobOrderId: number; + stockInLineId: number; jobOrderCode: string; productProcessLineCount: number; FinishedProductProcessLineCount: number; @@ -261,6 +285,7 @@ export interface ProductProcessLineQrscanUpadteRequest { operatorId?: number; equipmentId?: number; } + export interface ProductProcessLineDetailResponse { id: number, productProcessId: number, @@ -270,9 +295,17 @@ export interface ProductProcessLineDetailResponse { operatorName: string, handlerId: number, seqNo: number, + isDark: string, + isDense: number, + isFloat: string, + outputQtyUom: string, + outputQty: number, + pickOrderId: number, + jobOrderCode: string, + jobOrderId: number, name: string, description: string, - equipmentId: number, + equipment: string, startTime: string, endTime: string, defectQty: number, @@ -283,50 +316,83 @@ export interface ProductProcessLineDetailResponse { byproductName: string, byproductQty: number, byproductUom: string | undefined, + totalStockQty: number, + insufficientStockQty: number, + sufficientStockQty: number, + productionPriority: number, + productProcessLines: ProductProcessLineInfoResponse[], + jobOrderLineInfo: JobOrderLineInfo[], } -export interface ProductProcessLineDetailResponse { +export interface JobOrderProcessLineDetailResponse { + id: number; + productProcessId: number; + bomProcessId: number; + operatorId: number; + equipmentType: string | null; + operatorName: string; + handlerId: number; + seqNo: number; + name: string; + description: string; + equipmentId: number; + startTime: string | number[]; // API 返回的是数组格式 + endTime: string | number[]; // API 返回的是数组格式 + status: string; + outputFromProcessQty: number; + outputFromProcessUom: string; + defectQty: number; + defectUom: string; + scrapQty: number; + scrapUom: string; + byproductId: number; + byproductName: string; + byproductQty: number; + byproductUom: string; +} +export interface JobOrderLineInfo { id: number, - productProcessId: number, - bomProcessId: number, + jobOrderId: number, + jobOrderCode: string, + itemId: number, + itemCode: string, + itemName: string, + reqQty: number, + stockQty: number, + uom: string, + shortUom: string, + availableStatus: string +} +export interface ProductProcessLineInfoResponse { + id: number, + bomprocessId: number, operatorId: number, - equipmentType: string, operatorName: string, + equipmentId: number, handlerId: number, seqNo: number, - isDark: string, - isDense: number, - isFloat: string, - outputQtyUom: string, - outputQty: number, - pickOrderId: number, - jobOrderCode: string, - jobOrderId: number, name: string, description: string, - equipment: string, - startTime: string, - endTime: string, - defectQty: number, - defectUom: string, - scrapQty: number, - scrapUom: string, + equipment_name: string, + status: string, byproductId: number, byproductName: string, byproductQty: number, - byproductUom: string | undefined, + byproductUom: string, + scrapQty: number, + defectQty: number, + defectUom: string, + durationInMinutes: number, + prepTimeInMinutes: number, + postProdTimeInMinutes: number, + outputFromProcessQty: number, + outputFromProcessUom: string, + startTime: string, + endTime: string } -export const fetchProductProcessLinesByJoid = cache(async (joid: number) => { - return serverFetchJson( - `${BASE_API_URL}/product-process/demo/joid/${joid}`, - { - method: "GET", - } - ); -}); -// /product-process/Demo/ProcessLine/detail/{lineId} + export const fetchProductProcessLineDetail = cache(async (lineId: number) => { - return serverFetchJson( + return serverFetchJson( `${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`, { method: "GET", diff --git a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx index 485e137..e6f2499 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx @@ -189,12 +189,9 @@ const validateForm = (): boolean => { newErrors.actualPickQty = t('Qty cannot be negative'); } - // 2. 检查 actualPickQty 不能超过可用数量或需求数量 -<<<<<<< Updated upstream - if (ap > Math.min(req)) { -======= + + if (ap > Math.min( req)) { ->>>>>>> Stashed changes newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty'); } diff --git a/src/components/ProductionProcess/ProductionProcessDetail.tsx b/src/components/ProductionProcess/ProductionProcessDetail.tsx index 531f8f0..6e75c71 100644 --- a/src/components/ProductionProcess/ProductionProcessDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessDetail.tsx @@ -34,11 +34,12 @@ import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { fetchProductProcessById, - fetchProductProcessLines, updateProductProcessLineQrscan, fetchProductProcessLineDetail, ProductProcessLineDetailResponse, + JobOrderProcessLineDetailResponse, updateLineOutput, + ProductProcessLineInfoResponse, ProductProcessResponse, ProductProcessLineResponse, completeProductProcessLine, @@ -47,14 +48,6 @@ import { } from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; -// 添加设备数据库(从 MachineScanner.tsx) -const machineDatabase: Machine[] = [ - { id: 1, name: "CNC Mill #1", code: "CNC001", qrCode: "QR-CNC001" }, - { id: 2, name: "Lathe #2", code: "LAT002", qrCode: "QR-LAT002" }, - { id: 3, name: "Press #3", code: "PRS003", qrCode: "QR-PRS003" }, - { id: 4, name: "Welder #4", code: "WLD004", qrCode: "QR-WLD004" }, - { id: 5, name: "Drill Press #5", code: "DRL005", qrCode: "QR-DRL005" }, -]; interface ProductProcessDetailProps { @@ -73,7 +66,7 @@ const ProductionProcessDetail: React.FC = ({ // 基本信息 const [processData, setProcessData] = useState(null); - const [lines, setLines] = useState([]); + const [lines, setLines] = useState([]); const [loading, setLoading] = useState(false); // 选中的 line 和执行状态 @@ -86,7 +79,7 @@ const ProductionProcessDetail: React.FC = ({ const [scannedOperatorId, setScannedOperatorId] = useState(null); const [scannedEquipmentId, setScannedEquipmentId] = useState(null); const [scanningLineId, setScanningLineId] = useState(null); - const [lineDetailForScan, setLineDetailForScan] = useState(null); + const [lineDetailForScan, setLineDetailForScan] = useState(null); const [showScanDialog, setShowScanDialog] = useState(false); const autoSubmitTimerRef = useRef(null); @@ -105,7 +98,11 @@ const ProductionProcessDetail: React.FC = ({ // 处理 QR 码扫描 // 处理 QR 码扫描 - + const handleBackFromStep = async () => { + await fetchProcessDetail(); // 重新拉取最新的 process/lines + setIsExecutingLine(false); + setSelectedLineId(null); + }; // 获取 process 和 lines 数据 const fetchProcessDetail = useCallback(async () => { @@ -230,6 +227,7 @@ const ProductionProcessDetail: React.FC = ({ if (!scannedOperatorId) { console.log("No operatorId, cannot submit"); + setIsAutoSubmitting(false); return false; // 没有 operatorId,不能提交 } @@ -255,8 +253,15 @@ const ProductionProcessDetail: React.FC = ({ // 检查响应中的 message 字段来判断是否成功 // 如果后端返回 message 不为 null,说明验证失败 if (response && response.message) { + setIsAutoSubmitting(false); + // 清除定时器 + if (autoSubmitTimerRef.current) { + clearTimeout(autoSubmitTimerRef.current); + autoSubmitTimerRef.current = null; + } //alert(response.message || t("Validation failed. Please check operator and equipment.")); return false; + } // 验证通过,继续执行后续步骤 console.log("Validation passed, starting line..."); @@ -297,11 +302,17 @@ const ProductionProcessDetail: React.FC = ({ // 开始扫描 const handleStartScan = useCallback((lineId: number) => { + if (autoSubmitTimerRef.current) { + clearTimeout(autoSubmitTimerRef.current); + autoSubmitTimerRef.current = null; + } setScanningLineId(lineId); setIsManualScanning(true); setProcessedQrCodes(new Set()); setScannedOperatorId(null); setScannedEquipmentId(null); + setIsAutoSubmitting(false); // 添加:重置自动提交状态 + setLineDetailForScan(null); // 获取 line detail 以获取 bomProcessEquipmentId fetchProductProcessLineDetail(lineId) .then(setLineDetailForScan) @@ -311,7 +322,16 @@ const ProductionProcessDetail: React.FC = ({ // 停止扫描 const handleStopScan = useCallback(() => { + console.log("🛑 Stopping scan"); + + // 清除定时器 + if (autoSubmitTimerRef.current) { + clearTimeout(autoSubmitTimerRef.current); + autoSubmitTimerRef.current = null; + } + setIsManualScanning(false); + setIsAutoSubmitting(false); // 添加:重置自动提交状态 stopScan(); resetScan(); }, [stopScan, resetScan]); @@ -374,11 +394,24 @@ const ProductionProcessDetail: React.FC = ({ }, []); const handleStartLineWithScan = async (lineId: number) => { + console.log("🚀 Starting line with scan for lineId:", lineId); + + // 确保状态完全重置 + setIsAutoSubmitting(false); + setScannedOperatorId(null); + setScannedEquipmentId(null); + setProcessedQrCodes(new Set()); + + // 清除之前的定时器 + if (autoSubmitTimerRef.current) { + clearTimeout(autoSubmitTimerRef.current); + autoSubmitTimerRef.current = null; + } + setScanningLineId(lineId); setShowScanDialog(true); handleStartScan(lineId); }; - const selectedLine = lines.find(l => l.id === selectedLineId); if (loading) { @@ -391,14 +424,14 @@ const ProductionProcessDetail: React.FC = ({ return ( - {/* 返回按钮 */} - + {/* + - {/* ========== 第一部分:基本信息 ========== */} + {t("Production Process Information")} @@ -437,7 +470,7 @@ const ProductionProcessDetail: React.FC = ({ - +*/} {/* ========== 第二部分:Process Lines ========== */} @@ -452,8 +485,12 @@ const ProductionProcessDetail: React.FC = ({ {t("Seq")} {t("Step Name")} - {t("Description")} + {t("Description")} + {t("Operator")} {t("Equipment Type")} + {t("Duration")} + {t("Prep Time")} + {t("Post Prod Time")} {t("Status")} {t("Action")} @@ -462,7 +499,7 @@ const ProductionProcessDetail: React.FC = ({ {lines.map((line) => { const status = (line as any).status || ''; const statusLower = status.toLowerCase(); - const equipmentName = (line as any).equipment_name || line.equipmentType || "-"; + const equipmentName = line.equipment_name || "-"; const isCompleted = statusLower === 'completed'; const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; @@ -475,7 +512,11 @@ const ProductionProcessDetail: React.FC = ({ {line.name} {line.description || "-"} + {line.operatorName} {equipmentName} + {line.durationInMinutes} {t("Minutes")} + {line.prepTimeInMinutes} {t("Minutes")} + {line.postProdTimeInMinutes} {t("Minutes")} {isCompleted ? ( @@ -534,6 +575,7 @@ const ProductionProcessDetail: React.FC = ({ /* ========== 步骤执行视图 ========== */ { // setIsExecutingLine(false) // setSelectedLineId(null) diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx new file mode 100644 index 0000000..b4f2f48 --- /dev/null +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -0,0 +1,411 @@ +"use client"; +import React, { useCallback, useEffect, useState } from "react"; +import { + Box, + Button, + Paper, + Stack, + Typography, + TextField, + Grid, + Card, + CardContent, + CircularProgress, + Tabs, + Tab, + TabsProps, +} from "@mui/material"; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { useTranslation } from "react-i18next"; +import { fetchProductProcessesByJobOrderId } from "@/app/api/jo/actions"; +import ProductionProcessDetail from "./ProductionProcessDetail"; +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 JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; +interface JobOrderLine { + id: number; + jobOrderId: number; + jobOrderCode: string; + itemId: number; + itemCode: string; + itemName: string; + reqQty: number; + stockQty: number; + uom: string; + shortUom: string; + availableStatus: string; +} + +interface ProductProcessJobOrderDetailProps { + jobOrderId: number; + onBack: () => void; +} + +const ProductionProcessJobOrderDetail: React.FC = ({ + jobOrderId, + onBack, +}) => { + 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 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]); + + // 获取库存数据 + 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]); + + 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 = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + + // PickTable 组件内容 + const getStockAvailable = (line: JobOrderLine) => { + 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 isStockSufficient = (line: JobOrderLine) => { + const stockAvailable = getStockAvailable(line); + return stockAvailable >= line.reqQty; + }; + const productionProcessesLineRemarkTableColumns: GridColDef[] = [ + { + field: "seqNo", + headerName: t("Seq"), + flex: 0.2, + align: "left", + headerAlign: "center", + type: "number", + }, + { + field: "description", + headerName: t("Remark"), + flex: 1, + align: "left", + headerAlign: "center", + }, + ]; + 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", + + }, + { + field: "itemCode", + headerName: t("Item Code"), + flex: 0.6, + }, + { + field: "itemName", + headerName: t("Item Name"), + flex: 1, + renderCell: (params: GridRenderCellParams) => { + return `${params.value} (${params.row.uom})`; + }, + }, + { + field: "reqQty", + headerName: t("Req. Qty"), + flex: 0.7, + align: "right", + headerAlign: "right", + renderCell: (params: GridRenderCellParams) => { + return `${decimalFormatter.format(params.value)} (${params.row.shortUom})`; + }, + }, + { + field: "stockAvailable", + headerName: t("Stock Available"), + flex: 0.7, + align: "right", + headerAlign: "right", + type: "number", + + renderCell: (params: GridRenderCellParams) => { + const stockAvailable = getStockAvailable(params.row); + return `${decimalFormatter.format(stockAvailable)} (${params.row.shortUom})`; + }, + }, + { + field: "stockStatus", + headerName: t("Stock Status"), + flex: 0.5, + align: "right", + headerAlign: "right", + type: "boolean", + renderCell: (params: GridRenderCellParams) => { + return isStockSufficient(params.row) + ? + : ; + }, + }, + ]; + + const pickTableRows = jobOrderLines.map((line, index) => ({ + ...line, + id: line.id || index, + })); + + const PickTableContent = () => ( + + 'auto'} + /> + + ); + const ProductionProcessesLineRemarkTableContent = () => ( + + 'auto'} + /> + + ); + + + return ( + + {/* 返回按钮 */} + + + + + {/* 标签页 */} + + + + + + + + + + + {/* 标签页内容 */} + + {tabIndex === 0 && } + {tabIndex === 1 && } + {tabIndex === 2 && ( + { + // 切换回第一个标签页,或者什么都不做 + setTabIndex(0); + }} + /> + )} + {tabIndex === 3 && } + {tabIndex === 4 && } + + + ); +}; + +export default ProductionProcessJobOrderDetail; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index dd5f134..6c48394 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -14,6 +14,7 @@ import { Grid, } from "@mui/material"; import { useTranslation } from "react-i18next"; +import QcStockInModal from "../Qc/QcStockInModal"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; @@ -22,21 +23,38 @@ import { fetchAllJoborderProductProcessInfo, AllJoborderProductProcessInfoResponse, } from "@/app/api/jo/actions"; - +import { StockInLineInput } from "@/app/api/stockIn"; +import { PrinterCombo } from "@/app/api/settings/printer"; interface ProductProcessListProps { onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void; + printerCombo: PrinterCombo[]; } + const PER_PAGE = 6; -const ProductProcessList: React.FC = ({ onSelectProcess }) => { +const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo }) => { const { t } = useTranslation(); const { data: session } = useSession() as { data: SessionWithTokens | null }; - + const sessionToken = session as SessionWithTokens | null; const [loading, setLoading] = useState(false); const [processes, setProcesses] = useState([]); const [page, setPage] = useState(0); + const [openModal, setOpenModal] = useState(false); + const [modalInfo, setModalInfo] = useState(); + const handleViewStockIn = useCallback((process: AllJoborderProductProcessInfoResponse) => { + if (!process.stockInLineId) { + alert(t("Invalid Stock In Line Id")); + return; + } + setModalInfo({ + id: process.stockInLineId, + expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT), + // 视需要补 itemId、jobOrderId 等 + }); + setOpenModal(true); + }, [t]); const fetchProcesses = useCallback(async () => { setLoading(true); try { @@ -54,7 +72,12 @@ const ProductProcessList: React.FC = ({ onSelectProcess useEffect(() => { fetchProcesses(); }, [fetchProcesses]); - + const closeNewModal = useCallback(() => { + // const response = updateJo({ id: 1, status: "storing" }); + setOpenModal(false); // Close the modal first + // setTimeout(() => { + // }, 300); // Add a delay to avoid immediate re-trigger of useEffect +}, []); const startIdx = page * PER_PAGE; const paged = processes.slice(startIdx, startIdx + PER_PAGE); @@ -155,6 +178,11 @@ const ProductProcessList: React.FC = ({ onSelectProcess + {statusLower === "completed" && ( + + )} {t("Lines")}: {totalCount} @@ -165,7 +193,13 @@ const ProductProcessList: React.FC = ({ onSelectProcess ); })} - + {processes.length > 0 && ( = ({ onSelectProcess )} + ); }; diff --git a/src/components/ProductionProcess/ProductionProcessPage.tsx b/src/components/ProductionProcess/ProductionProcessPage.tsx index 907b1ba..ce9828d 100644 --- a/src/components/ProductionProcess/ProductionProcessPage.tsx +++ b/src/components/ProductionProcess/ProductionProcessPage.tsx @@ -4,53 +4,38 @@ import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; +import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; + import { fetchProductProcesses, - fetchProductProcessLines, + fetchProductProcessesByJobOrderId, ProductProcessLineResponse } from "@/app/api/jo/actions"; +type PrinterCombo = { + id: number; + value: number; + label?: string; + code?: string; + name?: string; + description?: string; + ip?: string; + port?: number; +}; + +interface ProductionProcessPageProps { + printerCombo: PrinterCombo[]; +} -const ProductionProcessPage = () => { +const ProductionProcessPage: React.FC = ({ printerCombo }) => { const [selectedProcessId, setSelectedProcessId] = useState(null); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; - const checkAndRedirectToDetail = useCallback(async () => { - if (!currentUserId) return; - - try { - // 获取所有 processes - const processes = await fetchProductProcesses(); - - // 获取所有 lines 并检查是否有匹配的 - for (const process of processes.content || []) { - const lines = await fetchProductProcessLines(process.id); - const pendingLine = lines.find((line: ProductProcessLineResponse) => - line.handlerId === currentUserId && - !line.endTime && - line.startTime - ); - - if (pendingLine) { - setSelectedProcessId(process.id); - break; - } - } - } catch (error) { - console.error("Error checking pending lines:", error); - } - }, [currentUserId]); - - useEffect(() => { - if (currentUserId && !selectedProcessId) { - // 检查是否有当前用户的 pending line - checkAndRedirectToDetail(); - } - }, [currentUserId, selectedProcessId, checkAndRedirectToDetail]); + // …原有逻辑省略… if (selectedProcessId !== null) { return ( - setSelectedProcessId(null)} /> @@ -59,12 +44,11 @@ const ProductionProcessPage = () => { return ( { + printerCombo={printerCombo} + onSelectProcess={(jobOrderId) => { const id = jobOrderId ?? null; if (id !== null) { setSelectedProcessId(id); - } else { - } }} /> diff --git a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx index 46dbc5c..92b9a4b 100644 --- a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx +++ b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx @@ -21,21 +21,24 @@ import StopIcon from "@mui/icons-material/Stop"; import PauseIcon from "@mui/icons-material/Pause"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import { useTranslation } from "react-i18next"; -import { ProductProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest} from "@/app/api/jo/actions"; +import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest} from "@/app/api/jo/actions"; import { Operator, Machine } from "@/app/api/jo"; import React, { useCallback, useEffect, useState } from "react"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { fetchNameList, NameList } from "@/app/api/user/actions"; interface ProductionProcessStepExecutionProps { lineId: number | null + onBack: () => void //onClose: () => void // onOutputSubmitted: () => Promise } const ProductionProcessStepExecution: React.FC = ({ lineId, + onBack, }) => { const { t } = useTranslation(); - const [lineDetail, setLineDetail] = useState(null); + const [lineDetail, setLineDetail] = useState(null); + const isCompleted = lineDetail?.status === "Completed"; const [outputData, setOutputData] = useState { + if (!lineId) { + setLineDetail(null); + return; + } + fetchProductProcessLineDetail(lineId) + .then((detail) => { + setLineDetail(detail as any); + // 初始化 outputData 从 lineDetail + setOutputData(prev => ({ + ...prev, + productProcessLineId: detail.id, + outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言 + outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言 + defectQty: detail.defectQty || 0, + defectUom: detail.defectUom || "", + scrapQty: detail.scrapQty || 0, + scrapUom: detail.scrapUom || "", + byproductName: detail.byproductName || "", + byproductQty: detail.byproductQty || 0, + byproductUom: detail.byproductUom || "" + })); + }) + .catch(err => { + console.error("Failed to load line detail", err); + setLineDetail(null); + }); + }, [lineId]); const handleSubmitOutput = async () => { if (!lineDetail?.id) return; @@ -88,27 +120,15 @@ const ProductionProcessStepExecution: React.FC { - if (!lineId) { - setLineDetail(null); - return; - } - fetchProductProcessLineDetail(lineId) + fetchProductProcessLineDetail(lineDetail.id) .then((detail) => { - setLineDetail(detail); + setLineDetail(detail as any); // 初始化 outputData 从 lineDetail setOutputData(prev => ({ ...prev, productProcessLineId: detail.id, - //outputFromProcessQty: detail.outputFromProcessQty || 0, - // outputFromProcessUom: detail.outputFromProcessUom || "", + outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言 + outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言 defectQty: detail.defectQty || 0, defectUom: detail.defectUom || "", scrapQty: detail.scrapQty || 0, @@ -122,8 +142,12 @@ const ProductionProcessStepExecution: React.FC { if (isManualScanning && qrValues.length > 0 && lineDetail?.id) { @@ -159,245 +183,352 @@ const ProductionProcessStepExecution: React.FC - {/* 当前步骤信息 */} - - - - - - {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) - - - {lineDetail?.description} - - - {t("Operator")}: {lineDetail?.operatorName || "-"} - - - {t("Equipment")}: {equipmentName} - - - - {!isPaused ? ( - - ) : ( - - )} - - - - - - - + + + - - + {/* 如果已完成,显示合并的视图 */} + {isCompleted ? ( + + + + {t("Completed Step")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) + + + {/**/} + + {/* 步骤信息部分 */} + + {t("Step Information")} + + + + + {t("Description")}: {lineDetail?.description || "-"} + + + + + {t("Operator")}: {lineDetail?.operatorName || "-"} + + + + + {t("Equipment")}: {equipmentName} + + + + + {t("Status")}: {lineDetail?.status || "-"} + + + + + {/**/} - {/* ========== 产出输入表单 ========== */} - {bothScanned && ( - - - - {t("Production Output Data Entry")} + {/* 产出数据部分 */} + + {t("Production Output Data")} - - - - {showOutputTable && ( - - - - - {t("Type")} - {t("Quantity")} - {t("Unit")} - - - - {/* start line output */} +
+ + + {t("Type")} + {t("Quantity")} + {t("Unit")} + + + + {/* Output from Process */} + + + {t("Output from Process")} + + + {lineDetail?.outputFromProcessQty || 0} + + + {lineDetail?.outputFromProcessUom || "-"} + + + + {/* By-product */} + {lineDetail?.byproductQty && lineDetail.byproductQty > 0 && ( - {t("Output from Process")} + {t("By-product")} + {lineDetail.byproductName && ( + + ({lineDetail.byproductName}) + + )} - setOutputData({ - ...outputData, - outputFromProcessQty: parseInt(e.target.value) || 0 - })} - /> + {lineDetail.byproductQty} - setOutputData({ - ...outputData, - outputFromProcessUom: e.target.value - })} - //placeholder="KG, L, PCS..." - /> - - - {/* byproduct */} - - - - {t("By-product")} - setOutputData({ - ...outputData, - byproductName: e.target.value - })} - placeholder={t("By-product name")} - sx={{ mt: 1 }} - /> - - - - setOutputData({ - ...outputData, - byproductQty: parseInt(e.target.value) || 0 - })} - /> - - - setOutputData({ - ...outputData, - byproductUom: e.target.value - })} - //placeholder="KG, L, PCS..." - /> + {lineDetail.byproductUom || "-"} + )} - {/* defect */} + {/* Defect */} + {lineDetail?.defectQty && lineDetail.defectQty > 0 && ( {t("Defect")} - setOutputData({ - ...outputData, - defectQty: parseInt(e.target.value) || 0 - })} - /> + {lineDetail.defectQty} - setOutputData({ - ...outputData, - defectUom: e.target.value - })} - //placeholder="KG, L, PCS..." - /> + {lineDetail.defectUom || "-"} + )} - {/* scrap */} + {/* Scrap */} + {lineDetail?.scrapQty && lineDetail.scrapQty > 0 && ( {t("Scrap")} - setOutputData({ - ...outputData, - scrapQty: parseInt(e.target.value) || 0 - })} - /> + {lineDetail.scrapQty} - setOutputData({ - ...outputData, - scrapUom: e.target.value - })} - //placeholder="KG, L, PCS..." - /> + {lineDetail.scrapUom || "-"} - -
+ )} + + +
+
+ ) : ( + <> + {/* 如果未完成,显示原来的两个部分 */} + {/* 当前步骤信息 */} + + + + + + {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) + + + {lineDetail?.description} + + + {t("Operator")}: {lineDetail?.operatorName || "-"} + + + {t("Equipment")}: {equipmentName} + + + + {!isPaused ? ( + + ) : ( + + )} + + + + + + + {/* ========== 产出输入表单 ========== */} + + + + {t("Production Output Data Entry")} + + + + + {showOutputTable && ( + + + + + {t("Type")} + {t("Quantity")} + {t("Unit")} + + + + {/* start line output */} + + + {t("Output from Process")} + + + setOutputData({ + ...outputData, + outputFromProcessQty: parseInt(e.target.value) || 0 + })} + /> + + + setOutputData({ + ...outputData, + outputFromProcessUom: e.target.value + })} + /> + + + {/* byproduct */} + + + + {t("By-product")} + + + + setOutputData({ + ...outputData, + byproductQty: parseInt(e.target.value) || 0 + })} + /> + + + setOutputData({ + ...outputData, + byproductUom: e.target.value + })} + /> + + - {/* submit button */} - - - - - - )} - + {/* defect */} + + + {t("Defect")} + + + setOutputData({ + ...outputData, + defectQty: parseInt(e.target.value) || 0 + })} + /> + + + setOutputData({ + ...outputData, + defectUom: e.target.value + })} + /> + + + + {/* scrap */} + + + {t("Scrap")} + + + setOutputData({ + ...outputData, + scrapQty: parseInt(e.target.value) || 0 + })} + /> + + + setOutputData({ + ...outputData, + scrapUom: e.target.value + })} + /> + + + +
+ + {/* submit button */} + + + + +
+ )} +
+ )}
);