| @@ -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 ( | |||
| <> | |||
| <Stack | |||
| @@ -37,7 +38,7 @@ const production: React.FC = async () => { | |||
| {t("Create Process")} | |||
| </Button> */} | |||
| </Stack> | |||
| <ProductionProcessPage /> {/* Use new component */} | |||
| <ProductionProcessPage printerCombo={printerCombo} /> {/* Use new component */} | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -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<ProductProcessLineDetailResponse>( | |||
| `${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<ProductProcessLineDetailResponse>( | |||
| return serverFetchJson<JobOrderProcessLineDetailResponse>( | |||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`, | |||
| { | |||
| method: "GET", | |||
| @@ -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'); | |||
| } | |||
| @@ -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<ProductProcessDetailProps> = ({ | |||
| // 基本信息 | |||
| const [processData, setProcessData] = useState<any>(null); | |||
| const [lines, setLines] = useState<ProductProcessLineDetailResponse[]>([]); | |||
| const [lines, setLines] = useState<ProductProcessLineInfoResponse[]>([]); | |||
| const [loading, setLoading] = useState(false); | |||
| // 选中的 line 和执行状态 | |||
| @@ -86,7 +79,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null); | |||
| const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null); | |||
| const [scanningLineId, setScanningLineId] = useState<number | null>(null); | |||
| const [lineDetailForScan, setLineDetailForScan] = useState<ProductProcessLineDetailResponse | null>(null); | |||
| const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null); | |||
| const [showScanDialog, setShowScanDialog] = useState(false); | |||
| const autoSubmitTimerRef = useRef<NodeJS.Timeout | null>(null); | |||
| @@ -105,7 +98,11 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| // 处理 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<ProductProcessDetailProps> = ({ | |||
| if (!scannedOperatorId) { | |||
| console.log("No operatorId, cannot submit"); | |||
| setIsAutoSubmitting(false); | |||
| return false; // 没有 operatorId,不能提交 | |||
| } | |||
| @@ -255,8 +253,15 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| // 检查响应中的 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<ProductProcessDetailProps> = ({ | |||
| // 开始扫描 | |||
| 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<ProductProcessDetailProps> = ({ | |||
| // 停止扫描 | |||
| 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<ProductProcessDetailProps> = ({ | |||
| }, []); | |||
| 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<ProductProcessDetailProps> = ({ | |||
| return ( | |||
| <Box> | |||
| {/* 返回按钮 */} | |||
| <Box sx={{ mb: 2 }}> | |||
| {/* | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| {/* ========== 第一部分:基本信息 ========== */} | |||
| <Paper sx={{ p: 3, mb: 3 }}> | |||
| <Typography variant="h6" gutterBottom fontWeight="bold"> | |||
| {t("Production Process Information")} | |||
| @@ -437,7 +470,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| </Typography> | |||
| </Stack> | |||
| </Paper> | |||
| */} | |||
| {/* ========== 第二部分:Process Lines ========== */} | |||
| <Paper sx={{ p: 3 }}> | |||
| <Typography variant="h6" gutterBottom fontWeight="bold"> | |||
| @@ -452,8 +485,12 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| <TableRow> | |||
| <TableCell>{t("Seq")}</TableCell> | |||
| <TableCell>{t("Step Name")}</TableCell> | |||
| <TableCell>{t("Description")}</TableCell> | |||
| <TableCell>{t("Description")}</TableCell> | |||
| <TableCell>{t("Operator")}</TableCell> | |||
| <TableCell>{t("Equipment Type")}</TableCell> | |||
| <TableCell>{t("Duration")}</TableCell> | |||
| <TableCell>{t("Prep Time")}</TableCell> | |||
| <TableCell>{t("Post Prod Time")}</TableCell> | |||
| <TableCell align="center">{t("Status")}</TableCell> | |||
| <TableCell align="center">{t("Action")}</TableCell> | |||
| </TableRow> | |||
| @@ -462,7 +499,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| {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<ProductProcessDetailProps> = ({ | |||
| <Typography fontWeight={500}>{line.name}</Typography> | |||
| </TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.description || "-"}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.operatorName}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{equipmentName}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.durationInMinutes} {t("Minutes")}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.prepTimeInMinutes} {t("Minutes")}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.postProdTimeInMinutes} {t("Minutes")}</Typography></TableCell> | |||
| <TableCell align="center"> | |||
| {isCompleted ? ( | |||
| <Chip label={t("Completed")} color="success" size="small" /> | |||
| @@ -534,6 +575,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| /* ========== 步骤执行视图 ========== */ | |||
| <ProductionProcessStepExecution | |||
| lineId={selectedLineId} | |||
| onBack={handleBackFromStep} | |||
| //onClose={() => { | |||
| // setIsExecutingLine(false) | |||
| // setSelectedLineId(null) | |||
| @@ -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<ProductProcessJobOrderDetailProps> = ({ | |||
| jobOrderId, | |||
| onBack, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const [loading, setLoading] = useState(false); | |||
| const [processData, setProcessData] = useState<any>(null); | |||
| const [jobOrderLines, setJobOrderLines] = useState<JobOrderLine[]>([]); | |||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(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<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| // 如果选择了 process detail,显示 detail 页面 | |||
| if (selectedProcessId !== null) { | |||
| return ( | |||
| <ProductionProcessDetail | |||
| jobOrderId={selectedProcessId} | |||
| onBack={() => { | |||
| setSelectedProcessId(null); | |||
| fetchData(); // 刷新数据 | |||
| }} | |||
| /> | |||
| ); | |||
| } | |||
| if (loading) { | |||
| return ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||
| <CircularProgress/> | |||
| </Box> | |||
| ); | |||
| } | |||
| if (!processData) { | |||
| return ( | |||
| <Box> | |||
| <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}> | |||
| {t("Back")} | |||
| </Button> | |||
| <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography> | |||
| </Box> | |||
| ); | |||
| } | |||
| // InfoCard 组件内容 | |||
| const InfoCardContent = () => ( | |||
| <Card sx={{ display: "block", mt: 2 }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Job Order Code")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.jobOrderCode || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}/> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Item Code")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.itemCode || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Item Name")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.itemName || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Req. Qty")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.outputQty ? integerFormatter.format(processData.outputQty) : ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("UoM")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.outputQtyUom || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| value={processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""} | |||
| label={t("Target Production Date")} | |||
| fullWidth | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Production Priority")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.productionPriority || "0"} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Is Dark")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.isDark || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Is Dense")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.isDense || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Is Float")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.isFloat || ""} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| // 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<JobOrderLine>) => { | |||
| return `${params.value} (${params.row.uom})`; | |||
| }, | |||
| }, | |||
| { | |||
| field: "reqQty", | |||
| headerName: t("Req. Qty"), | |||
| flex: 0.7, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||
| 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<JobOrderLine>) => { | |||
| 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<JobOrderLine>) => { | |||
| return isStockSufficient(params.row) | |||
| ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" /> | |||
| : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />; | |||
| }, | |||
| }, | |||
| ]; | |||
| const pickTableRows = jobOrderLines.map((line, index) => ({ | |||
| ...line, | |||
| id: line.id || index, | |||
| })); | |||
| const PickTableContent = () => ( | |||
| <Box sx={{ mt: 2 }}> | |||
| <StyledDataGrid | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| }} | |||
| disableColumnMenu | |||
| rows={pickTableRows} | |||
| columns={pickTableColumns} | |||
| getRowHeight={() => 'auto'} | |||
| /> | |||
| </Box> | |||
| ); | |||
| const ProductionProcessesLineRemarkTableContent = () => ( | |||
| <Box sx={{ mt: 2 }}> | |||
| <StyledDataGrid | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| }} | |||
| disableColumnMenu | |||
| rows={productionProcessesLineRemarkTableRows ?? []} | |||
| columns={productionProcessesLineRemarkTableColumns} | |||
| getRowHeight={() => 'auto'} | |||
| /> | |||
| </Box> | |||
| ); | |||
| return ( | |||
| <Box> | |||
| {/* 返回按钮 */} | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| {/* 标签页 */} | |||
| <Box sx={{ borderBottom: '1px solid #e0e0e0' }}> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("Job Order Info")} /> | |||
| <Tab label={t("Job Order Lines")} /> | |||
| <Tab label={t("Production Process")} /> | |||
| <Tab label={t("Production Process Line Remark")} /> | |||
| <Tab label={t("Matching Stock")} /> | |||
| </Tabs> | |||
| </Box> | |||
| {/* 标签页内容 */} | |||
| <Box sx={{ p: 2 }}> | |||
| {tabIndex === 0 && <InfoCardContent />} | |||
| {tabIndex === 1 && <PickTableContent />} | |||
| {tabIndex === 2 && ( | |||
| <ProductionProcessDetail | |||
| jobOrderId={jobOrderId} | |||
| onBack={() => { | |||
| // 切换回第一个标签页,或者什么都不做 | |||
| setTabIndex(0); | |||
| }} | |||
| /> | |||
| )} | |||
| {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />} | |||
| {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default ProductionProcessJobOrderDetail; | |||
| @@ -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<ProductProcessListProps> = ({ onSelectProcess }) => { | |||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ 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<AllJoborderProductProcessInfoResponse[]>([]); | |||
| const [page, setPage] = useState(0); | |||
| const [openModal, setOpenModal] = useState<boolean>(false); | |||
| const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | |||
| 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<ProductProcessListProps> = ({ 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<ProductProcessListProps> = ({ onSelectProcess | |||
| <Button variant="contained" size="small" onClick={() => onSelectProcess(process.jobOrderId, process.id)}> | |||
| {t("View Details")} | |||
| </Button> | |||
| {statusLower === "completed" && ( | |||
| <Button onClick={() => handleViewStockIn(process)}> | |||
| {t("view stockin")} | |||
| </Button> | |||
| )} | |||
| <Box sx={{ flex: 1 }} /> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Lines")}: {totalCount} | |||
| @@ -165,7 +193,13 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| ); | |||
| })} | |||
| </Grid> | |||
| <QcStockInModal | |||
| session={sessionToken} | |||
| open={openModal} | |||
| onClose={closeNewModal} | |||
| inputDetail={modalInfo} | |||
| printerCombo={printerCombo} | |||
| /> | |||
| {processes.length > 0 && ( | |||
| <TablePagination | |||
| component="div" | |||
| @@ -179,6 +213,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| ); | |||
| }; | |||
| @@ -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<ProductionProcessPageProps> = ({ printerCombo }) => { | |||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(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 ( | |||
| <ProductionProcessDetail | |||
| <ProductionProcessJobOrderDetail | |||
| jobOrderId={selectedProcessId} | |||
| onBack={() => setSelectedProcessId(null)} | |||
| /> | |||
| @@ -59,12 +44,11 @@ const ProductionProcessPage = () => { | |||
| return ( | |||
| <ProductionProcessList | |||
| onSelectProcess={(jobOrderId, productProcessId) => { | |||
| printerCombo={printerCombo} | |||
| onSelectProcess={(jobOrderId) => { | |||
| const id = jobOrderId ?? null; | |||
| if (id !== null) { | |||
| setSelectedProcessId(id); | |||
| } else { | |||
| } | |||
| }} | |||
| /> | |||
| @@ -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<void> | |||
| } | |||
| const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | |||
| lineId, | |||
| onBack, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const [lineDetail, setLineDetail] = useState<ProductProcessLineDetailResponse | null>(null); | |||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | |||
| const isCompleted = lineDetail?.status === "Completed"; | |||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| @@ -62,10 +65,39 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | |||
| // 检查是否两个都已扫描 | |||
| const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; | |||
| //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; | |||
| useEffect(() => { | |||
| 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<ProductionProcessStepExecutionPro | |||
| console.log(" Output data submitted successfully"); | |||
| } catch (error) { | |||
| console.error("Error submitting output:", error); | |||
| alert("Failed to submit output data. Please try again."); | |||
| } | |||
| }; | |||
| useEffect(() => { | |||
| 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<ProductionProcessStepExecutionPro | |||
| console.error("Failed to load line detail", err); | |||
| setLineDetail(null); | |||
| }); | |||
| }, [lineId]); | |||
| } catch (error) { | |||
| console.error("Error submitting output:", error); | |||
| alert("Failed to submit output data. Please try again."); | |||
| } | |||
| }; | |||
| // 处理 QR 码扫描效果 | |||
| useEffect(() => { | |||
| if (isManualScanning && qrValues.length > 0 && lineDetail?.id) { | |||
| @@ -159,245 +183,352 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| return ( | |||
| <Box> | |||
| {/* 当前步骤信息 */} | |||
| <Grid container spacing={2} sx={{ mb: 3 }}> | |||
| <Grid item xs={12} md={6}> | |||
| <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}> | |||
| <CardContent> | |||
| <Typography variant="h6" color="primary.main" gutterBottom> | |||
| {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {lineDetail?.description} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Operator")}: {lineDetail?.operatorName || "-"} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Equipment")}: {equipmentName} | |||
| </Typography> | |||
| <Stack direction="row" spacing={2} justifyContent="center"> | |||
| <Button | |||
| variant="contained" | |||
| color="error" | |||
| startIcon={<StopIcon />} | |||
| onClick={handleStop} | |||
| > | |||
| {t("Stop")} | |||
| </Button> | |||
| {!isPaused ? ( | |||
| <Button | |||
| variant="contained" | |||
| color="warning" | |||
| startIcon={<PauseIcon />} | |||
| onClick={handlePause} | |||
| > | |||
| {t("Pause")} | |||
| </Button> | |||
| ) : ( | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| startIcon={<PlayArrowIcon />} | |||
| onClick={handleContinue} | |||
| > | |||
| {t("Continue")} | |||
| </Button> | |||
| )} | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| </Grid> | |||
| </Grid> | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| {/* 如果已完成,显示合并的视图 */} | |||
| {isCompleted ? ( | |||
| <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | |||
| <CardContent> | |||
| <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | |||
| {t("Completed Step")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) | |||
| </Typography> | |||
| {/*<Divider sx={{ my: 2 }} />*/} | |||
| {/* 步骤信息部分 */} | |||
| <Typography variant="h6" gutterBottom sx={{ mt: 2 }}> | |||
| {t("Step Information")} | |||
| </Typography> | |||
| <Grid container spacing={2} sx={{ mb: 3 }}> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Description")}:</strong> {lineDetail?.description || "-"} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Operator")}:</strong> {lineDetail?.operatorName || "-"} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Equipment")}:</strong> {equipmentName} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Status")}:</strong> {lineDetail?.status || "-"} | |||
| </Typography> | |||
| </Grid> | |||
| </Grid> | |||
| {/*<Divider sx={{ my: 2 }} />*/} | |||
| {/* ========== 产出输入表单 ========== */} | |||
| {bothScanned && ( | |||
| <Box> | |||
| <Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |||
| <Typography variant="h6" fontWeight={600}> | |||
| {t("Production Output Data Entry")} | |||
| {/* 产出数据部分 */} | |||
| <Typography variant="h6" gutterBottom sx={{ mt: 2 }}> | |||
| {t("Production Output Data")} | |||
| </Typography> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => setShowOutputTable(!showOutputTable)} | |||
| > | |||
| {showOutputTable ? t("Hide Table") : t("Show Table")} | |||
| </Button> | |||
| </Box> | |||
| {showOutputTable && ( | |||
| <Paper sx={{ p: 3, bgcolor: 'grey.50' }}> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell width="30%">{t("Type")}</TableCell> | |||
| <TableCell width="35%">{t("Quantity")}</TableCell> | |||
| <TableCell width="35%">{t("Unit")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {/* start line output */} | |||
| <Table size="small" sx={{ mt: 2 }}> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell width="30%"><strong>{t("Type")}</strong></TableCell> | |||
| <TableCell width="35%"><strong>{t("Quantity")}</strong></TableCell> | |||
| <TableCell width="35%"><strong>{t("Unit")}</strong></TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {/* Output from Process */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography fontWeight={500}>{t("Output from Process")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Typography>{lineDetail?.outputFromProcessQty || 0}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Typography>{lineDetail?.outputFromProcessUom || "-"}</Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* By-product */} | |||
| {lineDetail?.byproductQty && lineDetail.byproductQty > 0 && ( | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography fontWeight={500}>{t("Output from Process")}</Typography> | |||
| <Typography fontWeight={500}>{t("By-product")}</Typography> | |||
| {lineDetail.byproductName && ( | |||
| <Typography variant="caption" color="text.secondary"> | |||
| ({lineDetail.byproductName}) | |||
| </Typography> | |||
| )} | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.outputFromProcessQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| outputFromProcessQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| <Typography>{lineDetail.byproductQty}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.outputFromProcessUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| outputFromProcessUom: e.target.value | |||
| })} | |||
| //placeholder="KG, L, PCS..." | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* byproduct */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Stack> | |||
| <Typography fontWeight={500}>{t("By-product")}</Typography> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductName} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductName: e.target.value | |||
| })} | |||
| placeholder={t("By-product name")} | |||
| sx={{ mt: 1 }} | |||
| /> | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductUom: e.target.value | |||
| })} | |||
| //placeholder="KG, L, PCS..." | |||
| /> | |||
| <Typography>{lineDetail.byproductUom || "-"}</Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| )} | |||
| {/* defect */} | |||
| {/* Defect */} | |||
| {lineDetail?.defectQty && lineDetail.defectQty > 0 && ( | |||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.defectQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| defectQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| <Typography>{lineDetail.defectQty}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.defectUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| defectUom: e.target.value | |||
| })} | |||
| //placeholder="KG, L, PCS..." | |||
| /> | |||
| <Typography>{lineDetail.defectUom || "-"}</Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| )} | |||
| {/* scrap */} | |||
| {/* Scrap */} | |||
| {lineDetail?.scrapQty && lineDetail.scrapQty > 0 && ( | |||
| <TableRow sx={{ bgcolor: 'error.50' }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.scrapQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| scrapQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| <Typography>{lineDetail.scrapQty}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.scrapUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| scrapUom: e.target.value | |||
| })} | |||
| //placeholder="KG, L, PCS..." | |||
| /> | |||
| <Typography>{lineDetail.scrapUom || "-"}</Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableBody> | |||
| </Table> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </CardContent> | |||
| </Card> | |||
| ) : ( | |||
| <> | |||
| {/* 如果未完成,显示原来的两个部分 */} | |||
| {/* 当前步骤信息 */} | |||
| <Grid container spacing={2} sx={{ mb: 3 }}> | |||
| <Grid item xs={12} md={6}> | |||
| <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}> | |||
| <CardContent> | |||
| <Typography variant="h6" color="primary.main" gutterBottom> | |||
| {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {lineDetail?.description} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Operator")}: {lineDetail?.operatorName || "-"} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Equipment")}: {equipmentName} | |||
| </Typography> | |||
| <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}> | |||
| <Button | |||
| variant="contained" | |||
| color="error" | |||
| startIcon={<StopIcon />} | |||
| onClick={handleStop} | |||
| > | |||
| {t("Stop")} | |||
| </Button> | |||
| {!isPaused ? ( | |||
| <Button | |||
| variant="contained" | |||
| color="warning" | |||
| startIcon={<PauseIcon />} | |||
| onClick={handlePause} | |||
| > | |||
| {t("Pause")} | |||
| </Button> | |||
| ) : ( | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| startIcon={<PlayArrowIcon />} | |||
| onClick={handleContinue} | |||
| > | |||
| {t("Continue")} | |||
| </Button> | |||
| )} | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| </Grid> | |||
| </Grid> | |||
| {/* ========== 产出输入表单 ========== */} | |||
| <Box> | |||
| <Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |||
| <Typography variant="h6" fontWeight={600}> | |||
| {t("Production Output Data Entry")} | |||
| </Typography> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => setShowOutputTable(!showOutputTable)} | |||
| > | |||
| {showOutputTable ? t("Hide Table") : t("Show Table")} | |||
| </Button> | |||
| </Box> | |||
| {showOutputTable && ( | |||
| <Paper sx={{ p: 3, bgcolor: 'grey.50' }}> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell width="30%">{t("Type")}</TableCell> | |||
| <TableCell width="35%">{t("Quantity")}</TableCell> | |||
| <TableCell width="35%">{t("Unit")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {/* start line output */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography fontWeight={500}>{t("Output from Process")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.outputFromProcessQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| outputFromProcessQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.outputFromProcessUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| outputFromProcessUom: e.target.value | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* byproduct */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Stack> | |||
| <Typography fontWeight={500}>{t("By-product")}</Typography> | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductUom: e.target.value | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* submit button */} | |||
| <Box sx={{ mt: 3, display: 'flex', gap: 2 }}> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => setShowOutputTable(false)} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<CheckCircleIcon />} | |||
| onClick={handleSubmitOutput} | |||
| > | |||
| {t("Complete Step")} | |||
| </Button> | |||
| </Box> | |||
| </Paper> | |||
| )} | |||
| </Box> | |||
| {/* defect */} | |||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.defectQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| defectQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.defectUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| defectUom: e.target.value | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* scrap */} | |||
| <TableRow sx={{ bgcolor: 'error.50' }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.scrapQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| scrapQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.scrapUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| scrapUom: e.target.value | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableBody> | |||
| </Table> | |||
| {/* submit button */} | |||
| <Box sx={{ mt: 3, display: 'flex', gap: 2 }}> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => setShowOutputTable(false)} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<CheckCircleIcon />} | |||
| onClick={handleSubmitOutput} | |||
| > | |||
| {t("Complete Step")} | |||
| </Button> | |||
| </Box> | |||
| </Paper> | |||
| )} | |||
| </Box> | |||
| </> | |||
| )} | |||
| </Box> | |||
| ); | |||