| @@ -26,7 +26,15 @@ export interface SearchJoResultRequest extends Pageable { | |||||
| planStartTo?: string; | planStartTo?: string; | ||||
| } | } | ||||
| export interface productProcessLineQtyRequest { | |||||
| productProcessLineId: number; | |||||
| outputFromProcessQty: number; | |||||
| outputFromProcessUom: string; | |||||
| defectQty: number; | |||||
| defectUom: string; | |||||
| scrapQty: number; | |||||
| scrapUom: string; | |||||
| } | |||||
| export interface SearchJoResultResponse { | export interface SearchJoResultResponse { | ||||
| records: JobOrder[]; | records: JobOrder[]; | ||||
| total: number; | total: number; | ||||
| @@ -194,6 +202,9 @@ export interface UpdateProductProcessLineQtyRequest { | |||||
| productProcessLineId: number; | productProcessLineId: number; | ||||
| outputFromProcessQty: number; | outputFromProcessQty: number; | ||||
| outputFromProcessUom: string; | outputFromProcessUom: string; | ||||
| byproductName: string; | |||||
| byproductQty: number; | |||||
| byproductUom: string; | |||||
| defectQty: number; | defectQty: number; | ||||
| defectUom: string; | defectUom: string; | ||||
| scrapQty: number; | scrapQty: number; | ||||
| @@ -246,7 +257,7 @@ export interface ProductProcessInfoResponse { | |||||
| status: string; | status: string; | ||||
| } | } | ||||
| export interface ProductProcessLineQrscanUpadteRequest { | export interface ProductProcessLineQrscanUpadteRequest { | ||||
| lineId: number; | |||||
| productProcessLineId: number; | |||||
| operatorId?: number; | operatorId?: number; | ||||
| equipmentId?: number; | equipmentId?: number; | ||||
| } | } | ||||
| @@ -261,7 +272,7 @@ export interface ProductProcessLineDetailResponse { | |||||
| seqNo: number, | seqNo: number, | ||||
| name: string, | name: string, | ||||
| description: string, | description: string, | ||||
| equipment: string, | |||||
| equipmentId: number, | |||||
| startTime: string, | startTime: string, | ||||
| endTime: string, | endTime: string, | ||||
| defectQty: number, | defectQty: number, | ||||
| @@ -282,6 +293,14 @@ export interface ProductProcessLineDetailResponse { | |||||
| operatorName: string, | operatorName: string, | ||||
| handlerId: number, | handlerId: number, | ||||
| seqNo: number, | seqNo: number, | ||||
| isDark: string, | |||||
| isDense: number, | |||||
| isFloat: string, | |||||
| outputQtyUom: string, | |||||
| outputQty: number, | |||||
| pickOrderId: number, | |||||
| jobOrderCode: string, | |||||
| jobOrderId: number, | |||||
| name: string, | name: string, | ||||
| description: string, | description: string, | ||||
| equipment: string, | equipment: string, | ||||
| @@ -314,6 +333,17 @@ export const fetchProductProcessLineDetail = cache(async (lineId: number) => { | |||||
| } | } | ||||
| ); | ); | ||||
| }); | }); | ||||
| export const updateProductProcessLineQty = cache(async (request: UpdateProductProcessLineQtyRequest) => { | |||||
| return serverFetchJson<UpdateProductProcessLineQtyResponse>( | |||||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/update/qty/${request.productProcessLineId}`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify(request), | |||||
| } | |||||
| ); | |||||
| }); | |||||
| export const updateProductProcessLineQrscan = cache(async (request: ProductProcessLineQrscanUpadteRequest) => { | export const updateProductProcessLineQrscan = cache(async (request: ProductProcessLineQrscanUpadteRequest) => { | ||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/product-process/Demo/update`, | `${BASE_API_URL}/product-process/Demo/update`, | ||||
| @@ -333,6 +363,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async () => { | |||||
| } | } | ||||
| ); | ); | ||||
| }); | }); | ||||
| /* | |||||
| export const updateProductProcessLineQty = async (request: UpdateProductProcessLineQtyRequest) => { | export const updateProductProcessLineQty = async (request: UpdateProductProcessLineQtyRequest) => { | ||||
| return serverFetchJson<UpdateProductProcessLineQtyResponse>( | return serverFetchJson<UpdateProductProcessLineQtyResponse>( | ||||
| `${BASE_API_URL}/product-process/lines/${request.productProcessLineId}/update/qty`, | `${BASE_API_URL}/product-process/lines/${request.productProcessLineId}/update/qty`, | ||||
| @@ -343,7 +374,7 @@ export const updateProductProcessLineQty = async (request: UpdateProductProcessL | |||||
| } | } | ||||
| ); | ); | ||||
| }; | }; | ||||
| */ | |||||
| export const startProductProcessLine = async (lineId: number) => { | export const startProductProcessLine = async (lineId: number) => { | ||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/start/${lineId}`, | `${BASE_API_URL}/product-process/Demo/ProcessLine/start/${lineId}`, | ||||
| @@ -568,20 +568,25 @@ if (showDetailView && selectedDoPickOrder) { | |||||
| </Paper> | </Paper> | ||||
| {/* 添加:多个 Pick Orders 信息(如果有) */} | {/* 添加:多个 Pick Orders 信息(如果有) */} | ||||
| {selectedDoPickOrder.pickOrderIds && selectedDoPickOrder.pickOrderIds.length > 1 && ( | |||||
| {selectedDoPickOrder.pickOrderIds && selectedDoPickOrder.pickOrderIds.length > 0 && ( | |||||
| <Paper sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5' }}> | <Paper sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5' }}> | ||||
| <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}> | <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}> | ||||
| {t("This ticket contains")} {selectedDoPickOrder.pickOrderIds.length} {t("pick orders")}: | {t("This ticket contains")} {selectedDoPickOrder.pickOrderIds.length} {t("pick orders")}: | ||||
| </Typography> | </Typography> | ||||
| <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}> | <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}> | ||||
| {selectedDoPickOrder.pickOrderCodes?.split(', ').map((code, idx) => ( | |||||
| <Chip | |||||
| key={idx} | |||||
| label={code} | |||||
| size="small" | |||||
| variant="outlined" | |||||
| /> | |||||
| ))} | |||||
| {(typeof selectedDoPickOrder.pickOrderCodes === 'string' | |||||
| ? selectedDoPickOrder.pickOrderCodes.split(',').map(code => code.trim()) | |||||
| : Array.isArray(selectedDoPickOrder.pickOrderCodes) | |||||
| ? selectedDoPickOrder.pickOrderCodes | |||||
| : [] | |||||
| ).filter(Boolean).map((code, idx) => ( | |||||
| <Chip | |||||
| key={idx} | |||||
| label={code} | |||||
| size="small" | |||||
| variant="outlined" | |||||
| /> | |||||
| ))} | |||||
| </Box> | </Box> | ||||
| </Paper> | </Paper> | ||||
| )} | )} | ||||
| @@ -17,6 +17,10 @@ import { | |||||
| Card, | Card, | ||||
| CardContent, | CardContent, | ||||
| CircularProgress, | CircularProgress, | ||||
| Dialog, | |||||
| DialogTitle, | |||||
| DialogContent, | |||||
| DialogActions, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | import QrCodeIcon from '@mui/icons-material/QrCode'; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| @@ -75,13 +79,17 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| // 选中的 line 和执行状态 | // 选中的 line 和执行状态 | ||||
| const [selectedLineId, setSelectedLineId] = useState<number | null>(null); | const [selectedLineId, setSelectedLineId] = useState<number | null>(null); | ||||
| const [isExecutingLine, setIsExecutingLine] = useState(false); | const [isExecutingLine, setIsExecutingLine] = useState(false); | ||||
| const [isAutoSubmitting, setIsAutoSubmitting] = useState(false); | |||||
| // 扫描器状态 | // 扫描器状态 | ||||
| const [isManualScanning, setIsManualScanning] = useState(false); | const [isManualScanning, setIsManualScanning] = useState(false); | ||||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | ||||
| const [scannedOperators, setScannedOperators] = useState<Operator[]>([]); | |||||
| const [scannedMachines, setScannedMachines] = useState<Machine[]>([]); | |||||
| 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 [showScanDialog, setShowScanDialog] = useState(false); | |||||
| const autoSubmitTimerRef = useRef<NodeJS.Timeout | null>(null); | |||||
| // 产出表单 | // 产出表单 | ||||
| const [outputData, setOutputData] = useState({ | const [outputData, setOutputData] = useState({ | ||||
| byproductName: "", | byproductName: "", | ||||
| @@ -97,133 +105,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| // 处理 QR 码扫描 | // 处理 QR 码扫描 | ||||
| // 处理 QR 码扫描 | // 处理 QR 码扫描 | ||||
| const processQrCode = useCallback((qrValue: string) => { | |||||
| // 操作员格式:{2fitestu1} - 键盘模拟输入(测试用) | |||||
| if (qrValue.match(/\{2fitestu(\d+)\}/)) { | |||||
| const match = qrValue.match(/\{2fitestu(\d+)\}/); | |||||
| const userId = parseInt(match![1]); | |||||
| // 调用 API 获取用户信息 | |||||
| fetchNameList().then((users: NameList[]) => { | |||||
| const user = users.find((u: NameList) => u.id === userId); | |||||
| if (user) { | |||||
| setScannedOperators([{ | |||||
| id: user.id, | |||||
| name: user.name, | |||||
| username: user.name | |||||
| }]); | |||||
| updateProductProcessLineQrscan({ | |||||
| lineId: selectedLineId || 0 as number, | |||||
| operatorId: user.id, | |||||
| }); | |||||
| } | |||||
| }); | |||||
| return; | |||||
| } | |||||
| // 设备格式:{2fiteste1} - 键盘模拟输入(测试用) | |||||
| if (qrValue.match(/\{2fiteste(\d+)\}/)) { | |||||
| const match = qrValue.match(/\{2fiteste(\d+)\}/); | |||||
| const equipmentId = parseInt(match![1]); | |||||
| // 使用本地设备数据库 | |||||
| const machine = machineDatabase.find((m: Machine) => m.id === equipmentId); | |||||
| if (machine) { | |||||
| setScannedMachines([machine]); | |||||
| } | |||||
| updateProductProcessLineQrscan({ | |||||
| lineId: selectedLineId || 0 as number, | |||||
| equipmentId: equipmentId, | |||||
| }).then((res) => { | |||||
| console.log(res); | |||||
| }); | |||||
| return; | |||||
| } | |||||
| // 正常 QR 扫描器扫描:格式为 "operatorId: 1" 或 "equipmentId: 1" | |||||
| const trimmedValue = qrValue.trim(); | |||||
| // 检查 operatorId 格式 | |||||
| const operatorMatch = trimmedValue.match(/^operatorId:\s*(\d+)$/i); | |||||
| if (operatorMatch) { | |||||
| const operatorId = parseInt(operatorMatch[1]); | |||||
| fetchNameList().then((users: NameList[]) => { | |||||
| const user = users.find((u: NameList) => u.id === operatorId); | |||||
| if (user) { | |||||
| setScannedOperators([{ | |||||
| id: user.id, | |||||
| name: user.name, | |||||
| username: user.name | |||||
| }]); | |||||
| updateProductProcessLineQrscan({ | |||||
| lineId: selectedLineId || 0 as number, | |||||
| operatorId: user.id, | |||||
| }); | |||||
| } else { | |||||
| console.warn(`User with ID ${operatorId} not found`); | |||||
| } | |||||
| }); | |||||
| return; | |||||
| } | |||||
| // 检查 equipmentId 格式 | |||||
| const equipmentMatch = trimmedValue.match(/^equipmentId:\s*(\d+)$/i); | |||||
| if (equipmentMatch) { | |||||
| const equipmentId = parseInt(equipmentMatch[1]); | |||||
| const machine = machineDatabase.find((m: Machine) => m.id === equipmentId); | |||||
| if (machine) { | |||||
| setScannedMachines([machine]); | |||||
| } | |||||
| updateProductProcessLineQrscan({ | |||||
| lineId: selectedLineId || 0 as number, | |||||
| equipmentId: equipmentId, | |||||
| }).then((res) => { | |||||
| console.log(res); | |||||
| }); | |||||
| return; | |||||
| } | |||||
| // 其他格式处理(JSON、普通文本等) | |||||
| try { | |||||
| const qrData = JSON.parse(qrValue); | |||||
| // TODO: 处理 JSON 格式的 QR 码 | |||||
| } catch { | |||||
| // 普通文本格式 | |||||
| // TODO: 处理普通文本格式 | |||||
| } | |||||
| }, [selectedLineId]); | |||||
| // 处理 QR 码扫描效果 | |||||
| useEffect(() => { | |||||
| if (isManualScanning && qrValues.length > 0 && isExecutingLine) { | |||||
| const latestQr = qrValues[qrValues.length - 1]; | |||||
| if (processedQrCodes.has(latestQr)) { | |||||
| return; | |||||
| } | |||||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||||
| processQrCode(latestQr); | |||||
| } | |||||
| }, [qrValues, isManualScanning, isExecutingLine, processedQrCodes, processQrCode]); | |||||
| // 开始扫描 | |||||
| const handleStartScan = useCallback(() => { | |||||
| setIsManualScanning(true); | |||||
| setProcessedQrCodes(new Set()); | |||||
| startScan(); | |||||
| }, [startScan]); | |||||
| // 停止扫描 | |||||
| const handleStopScan = useCallback(() => { | |||||
| setIsManualScanning(false); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| }, [stopScan, resetScan]); | |||||
| // 获取 process 和 lines 数据 | // 获取 process 和 lines 数据 | ||||
| const fetchProcessDetail = useCallback(async () => { | const fetchProcessDetail = useCallback(async () => { | ||||
| @@ -263,53 +145,239 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| }, [fetchProcessDetail]); | }, [fetchProcessDetail]); | ||||
| // 开始执行某个 line | // 开始执行某个 line | ||||
| const handleStartLine = async (lineId: number) => { | |||||
| try { | |||||
| // 使用 Server Action 而不是直接 fetch | |||||
| await startProductProcessLine(lineId); | |||||
| // 提交产出数据 | |||||
| const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| // 操作员格式:{2fitestu1} - 键盘模拟输入(测试用) | |||||
| if (qrValue.match(/\{2fitestu(\d+)\}/)) { | |||||
| const match = qrValue.match(/\{2fitestu(\d+)\}/); | |||||
| const userId = parseInt(match![1]); | |||||
| // 刷新数据 | |||||
| //await fetchProcessDetail(); | |||||
| } catch (error) { | |||||
| console.error("Error starting line:", error); | |||||
| alert("Failed to start line. Please try again."); | |||||
| fetchNameList().then((users: NameList[]) => { | |||||
| const user = users.find((u: NameList) => u.id === userId); | |||||
| if (user) { | |||||
| setScannedOperatorId(user.id); | |||||
| } | |||||
| }); | |||||
| return; | |||||
| } | |||||
| // 设备格式:{2fiteste1} - 键盘模拟输入(测试用) | |||||
| if (qrValue.match(/\{2fiteste(\d+)\}/)) { | |||||
| const match = qrValue.match(/\{2fiteste(\d+)\}/); | |||||
| const equipmentId = parseInt(match![1]); | |||||
| setScannedEquipmentId(equipmentId); | |||||
| return; | |||||
| } | } | ||||
| }; | |||||
| // 提交产出数据 | |||||
| const handleSubmitOutput = async () => { | |||||
| if (!selectedLineId) return; | |||||
| if (scannedOperators.length === 0 || scannedMachines.length === 0) { | |||||
| alert("Please scan operator and machine first!"); | |||||
| // 正常 QR 扫描器扫描:格式为 "operatorId: 1" 或 "equipmentId: 1" | |||||
| const trimmedValue = qrValue.trim(); | |||||
| // 检查 operatorId 格式 | |||||
| const operatorMatch = trimmedValue.match(/^operatorId:\s*(\d+)$/i); | |||||
| if (operatorMatch) { | |||||
| const operatorId = parseInt(operatorMatch[1]); | |||||
| fetchNameList().then((users: NameList[]) => { | |||||
| const user = users.find((u: NameList) => u.id === operatorId); | |||||
| if (user) { | |||||
| setScannedOperatorId(user.id); | |||||
| } else { | |||||
| console.warn(`User with ID ${operatorId} not found`); | |||||
| } | |||||
| }); | |||||
| return; | |||||
| } | |||||
| // 检查 equipmentId 格式 | |||||
| const equipmentMatch = trimmedValue.match(/^equipmentId:\s*(\d+)$/i); | |||||
| if (equipmentMatch) { | |||||
| const equipmentId = parseInt(equipmentMatch[1]); | |||||
| setScannedEquipmentId(equipmentId); | |||||
| return; | return; | ||||
| } | } | ||||
| // 其他格式处理(JSON、普通文本等) | |||||
| try { | |||||
| const qrData = JSON.parse(qrValue); | |||||
| // TODO: 处理 JSON 格式的 QR 码 | |||||
| } catch { | |||||
| // 普通文本格式 | |||||
| // TODO: 处理普通文本格式 | |||||
| } | |||||
| }, []); | |||||
| // 处理 QR 码扫描效果 | |||||
| useEffect(() => { | |||||
| if (isManualScanning && qrValues.length > 0 && scanningLineId) { | |||||
| const latestQr = qrValues[qrValues.length - 1]; | |||||
| if (processedQrCodes.has(latestQr)) { | |||||
| return; | |||||
| } | |||||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||||
| processQrCode(latestQr, scanningLineId); | |||||
| } | |||||
| }, [qrValues, isManualScanning, scanningLineId, processedQrCodes, processQrCode]); | |||||
| const submitScanAndStart = useCallback(async (lineId: number) => { | |||||
| console.log("submitScanAndStart called with:", { | |||||
| lineId, | |||||
| scannedOperatorId, | |||||
| scannedEquipmentId, | |||||
| }); | |||||
| if (!scannedOperatorId) { | |||||
| console.log("No operatorId, cannot submit"); | |||||
| return false; // 没有 operatorId,不能提交 | |||||
| } | |||||
| try { | try { | ||||
| // 直接使用 actions.ts 中定义的函数 | |||||
| await updateLineOutput(selectedLineId, { | |||||
| outputQty: parseFloat(outputData.outputFromProcessQty) || 0, | |||||
| outputUom: outputData.outputFromProcessUom, | |||||
| defectQty: parseFloat(outputData.defectQty) || 0, | |||||
| defectUom: outputData.defectUom, | |||||
| scrapQty: parseFloat(outputData.scrapQty) || 0, | |||||
| scrapUom: outputData.scrapUom, | |||||
| byproductName: outputData.byproductName, | |||||
| byproductQty: parseFloat(outputData.byproductQty) || 0, | |||||
| byproductUom: outputData.byproductUom, | |||||
| // 获取 line detail 以检查 bomProcessEquipmentId | |||||
| const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); | |||||
| // 提交 operatorId 和 equipmentId | |||||
| console.log("Submitting scan data:", { | |||||
| productProcessLineId: lineId, | |||||
| operatorId: scannedOperatorId, | |||||
| equipmentId: scannedEquipmentId || undefined, | |||||
| }); | |||||
| const response = await updateProductProcessLineQrscan({ | |||||
| productProcessLineId: lineId, | |||||
| operatorId: scannedOperatorId, | |||||
| equipmentId: scannedEquipmentId || undefined, | |||||
| }); | }); | ||||
| console.log(" Output data submitted successfully"); | |||||
| setIsExecutingLine(false); | |||||
| setSelectedLineId(null); | |||||
| handleStopScan(); | |||||
| await fetchProcessDetail(); | |||||
| console.log("Scan submit response:", response); | |||||
| // 检查响应中的 message 字段来判断是否成功 | |||||
| // 如果后端返回 message 不为 null,说明验证失败 | |||||
| if (response && response.message) { | |||||
| //alert(response.message || t("Validation failed. Please check operator and equipment.")); | |||||
| return false; | |||||
| } | |||||
| // 验证通过,继续执行后续步骤 | |||||
| console.log("Validation passed, starting line..."); | |||||
| handleStopScan(); | |||||
| setShowScanDialog(false); | |||||
| setIsAutoSubmitting(false); | |||||
| await handleStartLine(lineId); | |||||
| setSelectedLineId(lineId); | |||||
| setIsExecutingLine(true); | |||||
| await fetchProcessDetail(); | |||||
| return true; | |||||
| } catch (error) { | |||||
| console.error("Error submitting scan:", error); | |||||
| //alert(t("Failed to submit scan data. Please try again.")); | |||||
| setIsAutoSubmitting(false); | |||||
| return false; | |||||
| } | |||||
| }, [scannedOperatorId, scannedEquipmentId, lineDetailForScan, t, fetchProcessDetail]); | |||||
| const handleSubmitScanAndStart = useCallback(async (lineId: number) => { | |||||
| console.log("handleSubmitScanAndStart called with lineId:", lineId); | |||||
| if (!scannedOperatorId) { | |||||
| //alert(t("Please scan operator code first")); | |||||
| return; | |||||
| } | |||||
| // 如果正在自动提交,等待一下 | |||||
| if (isAutoSubmitting) { | |||||
| console.log("Already auto-submitting, skipping manual submit"); | |||||
| return; | |||||
| } | |||||
| await submitScanAndStart(lineId); | |||||
| }, [scannedOperatorId, isAutoSubmitting, submitScanAndStart, t]); | |||||
| // 开始扫描 | |||||
| const handleStartScan = useCallback((lineId: number) => { | |||||
| setScanningLineId(lineId); | |||||
| setIsManualScanning(true); | |||||
| setProcessedQrCodes(new Set()); | |||||
| setScannedOperatorId(null); | |||||
| setScannedEquipmentId(null); | |||||
| // 获取 line detail 以获取 bomProcessEquipmentId | |||||
| fetchProductProcessLineDetail(lineId) | |||||
| .then(setLineDetailForScan) | |||||
| .catch(err => console.error("Failed to load line detail", err)); | |||||
| startScan(); | |||||
| }, [startScan]); | |||||
| // 停止扫描 | |||||
| const handleStopScan = useCallback(() => { | |||||
| setIsManualScanning(false); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| }, [stopScan, resetScan]); | |||||
| // 开始执行某个 line(原有逻辑,现在在验证通过后调用) | |||||
| const handleStartLine = async (lineId: number) => { | |||||
| try { | |||||
| await startProductProcessLine(lineId); | |||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error submitting output:", error); | |||||
| alert("Failed to submit output data. Please try again."); | |||||
| console.error("Error starting line:", error); | |||||
| //alert("Failed to start line. Please try again."); | |||||
| } | } | ||||
| }; | }; | ||||
| // 提交扫描结果并验证 | |||||
| useEffect(() => { | |||||
| console.log("Auto-submit check:", { | |||||
| scanningLineId, | |||||
| scannedOperatorId, | |||||
| scannedEquipmentId, | |||||
| isAutoSubmitting, | |||||
| isManualScanning, | |||||
| }); | |||||
| if ( | |||||
| scanningLineId && | |||||
| scannedOperatorId !== null && | |||||
| scannedEquipmentId !== null && | |||||
| !isAutoSubmitting && | |||||
| isManualScanning | |||||
| ) { | |||||
| console.log("Auto-submitting triggered!"); | |||||
| setIsAutoSubmitting(true); | |||||
| // 清除之前的定时器(如果有) | |||||
| if (autoSubmitTimerRef.current) { | |||||
| clearTimeout(autoSubmitTimerRef.current); | |||||
| } | |||||
| // 延迟一点时间,让用户看到两个都扫描完成了 | |||||
| autoSubmitTimerRef.current = setTimeout(() => { | |||||
| console.log("Executing auto-submit..."); | |||||
| submitScanAndStart(scanningLineId); | |||||
| autoSubmitTimerRef.current = null; | |||||
| }, 500); | |||||
| } | |||||
| // 清理函数:只在组件卸载或条件不再满足时清除定时器 | |||||
| return () => { | |||||
| // 注意:这里不立即清除定时器,因为我们需要它执行 | |||||
| // 只在组件卸载时清除 | |||||
| }; | |||||
| }, [scanningLineId, scannedOperatorId, scannedEquipmentId, isAutoSubmitting, isManualScanning, submitScanAndStart]); | |||||
| useEffect(() => { | |||||
| return () => { | |||||
| if (autoSubmitTimerRef.current) { | |||||
| clearTimeout(autoSubmitTimerRef.current); | |||||
| } | |||||
| }; | |||||
| }, []); | |||||
| const handleStartLineWithScan = async (lineId: number) => { | |||||
| setScanningLineId(lineId); | |||||
| setShowScanDialog(true); | |||||
| handleStartScan(lineId); | |||||
| }; | |||||
| const selectedLine = lines.find(l => l.id === selectedLineId); | const selectedLine = lines.find(l => l.id === selectedLineId); | ||||
| @@ -337,16 +405,30 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| </Typography> | </Typography> | ||||
| <Stack spacing={2} direction="row" useFlexGap flexWrap="wrap"> | <Stack spacing={2} direction="row" useFlexGap flexWrap="wrap"> | ||||
| <Typography variant="subtitle1"> | <Typography variant="subtitle1"> | ||||
| <strong>{t("Process Code")}:</strong> {processData?.productProcessCode} | |||||
| <strong>{t("Job Order Code")}:</strong> {processData?.jobOrderCode} | |||||
| </Typography> | |||||
| <Typography variant="subtitle1"> | |||||
| <strong>{t("Is Dark")}:</strong> {processData?.isDark} | |||||
| </Typography> | |||||
| <Typography variant="subtitle1"> | |||||
| <strong>{t("Is Dense")}:</strong> {processData?.isDense} | |||||
| </Typography> | |||||
| <Typography variant="subtitle1"> | |||||
| <strong>{t("Is Float")}:</strong> {processData?.isFloat} | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="subtitle1"> | <Typography variant="subtitle1"> | ||||
| <strong>{t("Output Qty")}:</strong> {processData?.outputQty+" "+"("+processData?.outputQtyUom +")"} | |||||
| </Typography> | |||||
| <Box> | |||||
| <strong>{t("Status")}:</strong>{" "} | <strong>{t("Status")}:</strong>{" "} | ||||
| <Chip | <Chip | ||||
| label={t(processData?.status || 'pending')} | |||||
| color={processData?.status === 'completed' ? 'success' : 'primary'} | |||||
| label={ | |||||
| processData?.status === 'completed' ? t("Completed") : processData?.status === 'IN_PROGRESS' ? t("In Progress") : processData?.status === 'pending' ? t("Pending") : t("Unknown") | |||||
| } | |||||
| color={processData?.status === 'completed' ? 'success' : processData?.status === 'IN_PROGRESS' ? 'success' : processData?.status === 'pending' ? 'primary' : 'error'} | |||||
| size="small" | size="small" | ||||
| /> | /> | ||||
| </Typography> | |||||
| </Box> | |||||
| <Typography variant="subtitle1"> | <Typography variant="subtitle1"> | ||||
| <strong>{t("Date")}:</strong> {dayjs(processData?.date).format(OUTPUT_DATE_FORMAT)} | <strong>{t("Date")}:</strong> {dayjs(processData?.date).format(OUTPUT_DATE_FORMAT)} | ||||
| </Typography> | </Typography> | ||||
| @@ -376,103 +458,151 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| <TableCell align="center">{t("Action")}</TableCell> | <TableCell align="center">{t("Action")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | |||||
| {lines.map((line) => { | |||||
| const status = (line as any).status || ''; | |||||
| const statusLower = status.toLowerCase(); | |||||
| const equipmentName = (line as any).equipment_name || line.equipmentType || "-"; | |||||
| // 使用 status 字段判断状态 | |||||
| const isCompleted = statusLower === 'completed'; | |||||
| const isInProgress = statusLower === 'in_progress' || statusLower === 'in progress'; | |||||
| const isPending = statusLower === 'pending' || status === ''; | |||||
| return ( | |||||
| <TableRow key={line.id}> | |||||
| <TableCell>{line.seqNo}</TableCell> | |||||
| <TableCell> | |||||
| <Typography fontWeight={500}>{line.name}</Typography> | |||||
| </TableCell> | |||||
| <TableCell>{line.description || "-"}</TableCell> | |||||
| <TableCell>{equipmentName}</TableCell> | |||||
| <TableCell align="center"> | |||||
| {isCompleted ? ( | |||||
| <Chip label={t("Completed")} color="success" size="small" /> | |||||
| ) : isInProgress ? ( | |||||
| <Chip label={t("In Progress")} color="primary" size="small" /> | |||||
| ) : ( | |||||
| <Chip label={t("Pending")} color="default" size="small" /> | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| {statusLower === 'pending' ? ( | |||||
| <Button | |||||
| variant="contained" | |||||
| size="small" | |||||
| startIcon={<PlayArrowIcon />} | |||||
| onClick={async () => { | |||||
| await handleStartLine(line.id); | |||||
| setSelectedLineId(line.id); | |||||
| setIsExecutingLine(true); | |||||
| await fetchProcessDetail(); | |||||
| }} | |||||
| > | |||||
| {t("Start")} | |||||
| </Button> | |||||
| ): | |||||
| statusLower === 'in_progress' || statusLower === 'in progress' ? ( | |||||
| <Button | |||||
| variant="contained" | |||||
| size="small" | |||||
| startIcon={<CheckCircleIcon />} | |||||
| onClick={async () => { | |||||
| setSelectedLineId(line.id); | |||||
| setIsExecutingLine(true); | |||||
| await fetchProcessDetail(); | |||||
| }} | |||||
| > | |||||
| {t("View")} | |||||
| </Button> | |||||
| ):( | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={async() => { | |||||
| setSelectedLineId(line.id); | |||||
| setIsExecutingLine(true); | |||||
| await fetchProcessDetail(); | |||||
| }} | |||||
| > | |||||
| {t("View")} | |||||
| </Button> | |||||
| )} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| );})} | |||||
| <TableBody> | |||||
| {lines.map((line) => { | |||||
| const status = (line as any).status || ''; | |||||
| const statusLower = status.toLowerCase(); | |||||
| const equipmentName = (line as any).equipment_name || line.equipmentType || "-"; | |||||
| const isCompleted = statusLower === 'completed'; | |||||
| const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; | |||||
| const isPending = statusLower === 'pending' || status === ''; | |||||
| return ( | |||||
| <TableRow key={line.id}> | |||||
| <TableCell>{line.seqNo}</TableCell> | |||||
| <TableCell> | |||||
| <Typography fontWeight={500}>{line.name}</Typography> | |||||
| </TableCell> | |||||
| <TableCell><Typography fontWeight={500}>{line.description || "-"}</Typography></TableCell> | |||||
| <TableCell><Typography fontWeight={500}>{equipmentName}</Typography></TableCell> | |||||
| <TableCell align="center"> | |||||
| {isCompleted ? ( | |||||
| <Chip label={t("Completed")} color="success" size="small" /> | |||||
| ) : isInProgress ? ( | |||||
| <Chip label={t("In Progress")} color="primary" size="small" /> | |||||
| ) : isPending ? ( | |||||
| <Chip label={t("Pending")} color="default" size="small" /> | |||||
| ) : ( | |||||
| <Chip label={t("Unknown")} color="error" size="small" /> | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| {statusLower === 'pending' ? ( | |||||
| <Button | |||||
| variant="contained" | |||||
| size="small" | |||||
| startIcon={<PlayArrowIcon />} | |||||
| onClick={() => handleStartLineWithScan(line.id)} | |||||
| > | |||||
| {t("Start")} | |||||
| </Button> | |||||
| ) : statusLower === 'in_progress' || statusLower === 'in progress' ? ( | |||||
| <Button | |||||
| variant="contained" | |||||
| size="small" | |||||
| startIcon={<CheckCircleIcon />} | |||||
| onClick={async () => { | |||||
| setSelectedLineId(line.id); | |||||
| setIsExecutingLine(true); | |||||
| await fetchProcessDetail(); | |||||
| }} | |||||
| > | |||||
| {t("View")} | |||||
| </Button> | |||||
| ) : ( | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={async() => { | |||||
| setSelectedLineId(line.id); | |||||
| setIsExecutingLine(true); | |||||
| await fetchProcessDetail(); | |||||
| }} | |||||
| > | |||||
| {t("View")} | |||||
| </Button> | |||||
| )} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ); | |||||
| })} | |||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| </TableContainer> | </TableContainer> | ||||
| ) : ( | ) : ( | ||||
| /* ========== 步骤执行视图 ========== */ | /* ========== 步骤执行视图 ========== */ | ||||
| <ProductionProcessStepExecution | <ProductionProcessStepExecution | ||||
| selectedLine={selectedLine} | |||||
| scannedOperators={scannedOperators} | |||||
| scannedMachines={scannedMachines} | |||||
| isManualScanning={isManualScanning} | |||||
| outputData={outputData} | |||||
| onStartScan={handleStartScan} | |||||
| onStopScan={handleStopScan} | |||||
| onCancel={() => { | |||||
| setIsExecutingLine(false); | |||||
| setSelectedLineId(null); | |||||
| handleStopScan(); | |||||
| fetchProcessDetail(); | |||||
| }} | |||||
| onSubmitOutput={handleSubmitOutput} | |||||
| onOutputDataChange={(data) => setOutputData({...outputData, ...data})} | |||||
| /> | |||||
| lineId={selectedLineId} | |||||
| //onClose={() => { | |||||
| // setIsExecutingLine(false) | |||||
| // setSelectedLineId(null) | |||||
| //}} | |||||
| //onOutputSubmitted={async () => { | |||||
| // await fetchProcessDetail() | |||||
| //}} | |||||
| /> | |||||
| )} | )} | ||||
| </Paper> | </Paper> | ||||
| {/* QR 扫描对话框 */} | |||||
| <Dialog | |||||
| open={showScanDialog} | |||||
| onClose={() => { | |||||
| handleStopScan(); | |||||
| setShowScanDialog(false); | |||||
| }} | |||||
| maxWidth="sm" | |||||
| fullWidth | |||||
| > | |||||
| <DialogTitle>{t("Scan Operator & Equipment")}</DialogTitle> | |||||
| <DialogContent> | |||||
| <Stack spacing={2} sx={{ mt: 2 }}> | |||||
| <Box> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {scannedOperatorId | |||||
| ? `${t("Operator")}: ${scannedOperatorId}` | |||||
| : t("Please scan operator code") | |||||
| } | |||||
| </Typography> | |||||
| </Box> | |||||
| <Box> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {scannedEquipmentId | |||||
| ? `${t("Equipment")}: ${scannedEquipmentId}` | |||||
| : t("Please scan equipment code (optional if not required)") | |||||
| } | |||||
| </Typography> | |||||
| </Box> | |||||
| <Button | |||||
| variant={isManualScanning ? "outlined" : "contained"} | |||||
| startIcon={<QrCodeIcon />} | |||||
| onClick={isManualScanning ? handleStopScan : () => scanningLineId && handleStartScan(scanningLineId)} | |||||
| color={isManualScanning ? "secondary" : "primary"} | |||||
| fullWidth | |||||
| > | |||||
| {isManualScanning ? t("Stop QR Scan") : t("Start QR Scan")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </DialogContent> | |||||
| <DialogActions> | |||||
| <Button onClick={() => { | |||||
| handleStopScan(); | |||||
| setShowScanDialog(false); | |||||
| }}> | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)} | |||||
| disabled={!scannedOperatorId} | |||||
| > | |||||
| {t("Submit & Start")} | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -1,5 +1,4 @@ | |||||
| "use client"; | "use client"; | ||||
| import React from "react"; | |||||
| import { | import { | ||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| @@ -14,255 +13,392 @@ import { | |||||
| TableRow, | TableRow, | ||||
| Card, | Card, | ||||
| CardContent, | CardContent, | ||||
| Grid, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | import QrCodeIcon from '@mui/icons-material/QrCode'; | ||||
| import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | ||||
| 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 { useTranslation } from "react-i18next"; | ||||
| import { ProductProcessLineResponse } from "@/app/api/jo/actions"; | |||||
| import { ProductProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest} from "@/app/api/jo/actions"; | |||||
| import { Operator, Machine } from "@/app/api/jo"; | 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 { | interface ProductionProcessStepExecutionProps { | ||||
| selectedLine: ProductProcessLineResponse | undefined; | |||||
| scannedOperators: Operator[]; | |||||
| scannedMachines: Machine[]; | |||||
| isManualScanning: boolean; | |||||
| outputData: { | |||||
| byproductName: string; | |||||
| byproductQty: string; | |||||
| byproductUom: string; | |||||
| scrapQty: string; | |||||
| scrapUom: string; | |||||
| defectQty: string; | |||||
| defectUom: string; | |||||
| outputFromProcessQty: string; | |||||
| outputFromProcessUom: string; | |||||
| }; | |||||
| onStartScan: () => void; | |||||
| onStopScan: () => void; | |||||
| onCancel: () => void; | |||||
| onSubmitOutput: () => void; | |||||
| onOutputDataChange: (data: Partial<ProductionProcessStepExecutionProps['outputData']>) => void; | |||||
| lineId: number | null | |||||
| //onClose: () => void | |||||
| // onOutputSubmitted: () => Promise<void> | |||||
| } | } | ||||
| const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | ||||
| selectedLine, | |||||
| scannedOperators, | |||||
| scannedMachines, | |||||
| isManualScanning, | |||||
| outputData, | |||||
| onStartScan, | |||||
| onStopScan, | |||||
| onCancel, | |||||
| onSubmitOutput, | |||||
| onOutputDataChange, | |||||
| lineId, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const equipmentName = (selectedLine as any)?.equipment_name || selectedLine?.equipmentType || "-"; | |||||
| const [lineDetail, setLineDetail] = useState<ProductProcessLineDetailResponse | null>(null); | |||||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||||
| byproductName: string; | |||||
| byproductQty: number; | |||||
| byproductUom: string; | |||||
| }>({ | |||||
| productProcessLineId: lineId ?? 0, | |||||
| outputFromProcessQty: 0, | |||||
| outputFromProcessUom: "", | |||||
| defectQty: 0, | |||||
| defectUom: "", | |||||
| scrapQty: 0, | |||||
| scrapUom: "", | |||||
| byproductName: "", | |||||
| byproductQty: 0, | |||||
| byproductUom: "" | |||||
| }); | |||||
| const [isManualScanning, setIsManualScanning] = useState(false); | |||||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||||
| const [scannedOperators, setScannedOperators] = useState<Operator[]>([]); | |||||
| const [scannedMachines, setScannedMachines] = useState<Machine[]>([]); | |||||
| const [isPaused, setIsPaused] = useState(false); | |||||
| const [showOutputTable, setShowOutputTable] = useState(false); | |||||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||||
| const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | |||||
| // 检查是否两个都已扫描 | |||||
| const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; | |||||
| const handleSubmitOutput = async () => { | |||||
| if (!lineDetail?.id) return; | |||||
| try { | |||||
| // 直接使用 actions.ts 中定义的函数 | |||||
| await updateProductProcessLineQty({ | |||||
| productProcessLineId: lineDetail?.id || 0 as number, | |||||
| byproductName: outputData.byproductName, | |||||
| byproductQty: outputData.byproductQty, | |||||
| byproductUom: outputData.byproductUom, | |||||
| outputFromProcessQty: outputData.outputFromProcessQty, | |||||
| outputFromProcessUom: outputData.outputFromProcessUom, | |||||
| // outputFromProcessUom: outputData.outputFromProcessUom, | |||||
| defectQty: outputData.defectQty, | |||||
| defectUom: outputData.defectUom, | |||||
| scrapQty: outputData.scrapQty, | |||||
| scrapUom: outputData.scrapUom, | |||||
| }); | |||||
| 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) | |||||
| .then((detail) => { | |||||
| setLineDetail(detail); | |||||
| // 初始化 outputData 从 lineDetail | |||||
| setOutputData(prev => ({ | |||||
| ...prev, | |||||
| productProcessLineId: detail.id, | |||||
| //outputFromProcessQty: detail.outputFromProcessQty || 0, | |||||
| // outputFromProcessUom: detail.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]); | |||||
| // 处理 QR 码扫描效果 | |||||
| useEffect(() => { | |||||
| if (isManualScanning && qrValues.length > 0 && lineDetail?.id) { | |||||
| const latestQr = qrValues[qrValues.length - 1]; | |||||
| if (processedQrCodes.has(latestQr)) { | |||||
| return; | |||||
| } | |||||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||||
| //processQrCode(latestQr); | |||||
| } | |||||
| }, [qrValues, isManualScanning, lineDetail?.id, processedQrCodes]); | |||||
| // 开始扫描 | |||||
| const handlePause = () => { | |||||
| setIsPaused(true); | |||||
| }; | |||||
| const handleContinue = () => { | |||||
| setIsPaused(false); | |||||
| }; | |||||
| const handleStop = () => { | |||||
| setIsPaused(false); | |||||
| // TODO: 调用停止流程的 API | |||||
| }; | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| {/* 当前步骤信息 */} | {/* 当前步骤信息 */} | ||||
| <Card sx={{ mb: 3, bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main' }}> | |||||
| <CardContent> | |||||
| <Typography variant="h6" color="primary.main" gutterBottom> | |||||
| {t("Executing")}: {selectedLine?.name} (Seq: {selectedLine?.seqNo}) | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {selectedLine?.description} | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("Equipment")}: {equipmentName} | |||||
| </Typography> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Stack spacing={3}> | |||||
| {/* 合并的扫描器 */} | |||||
| <Paper sx={{ p: 3, mb: 3 }}> | |||||
| <Typography variant="h6" gutterBottom> | |||||
| {t("Scan Operator & Equipment")} | |||||
| </Typography> | |||||
| <Stack spacing={2}> | |||||
| {/* 操作员扫描 */} | |||||
| <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"> | <Typography variant="body2" color="text.secondary"> | ||||
| {scannedOperators.length > 0 | |||||
| ? `${t("Operator")}: ${scannedOperators[0].name || scannedOperators[0].username}` | |||||
| : t("Please scan operator code") | |||||
| } | |||||
| {lineDetail?.description} | |||||
| </Typography> | </Typography> | ||||
| </Box> | |||||
| {/* 设备扫描 */} | |||||
| <Box> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {scannedMachines.length > 0 | |||||
| ? `${t("Equipment")}: ${scannedMachines[0].name || scannedMachines[0].code}` | |||||
| : t("Please scan equipment code") | |||||
| } | |||||
| {t("Operator")}: {lineDetail?.operatorName || "-"} | |||||
| </Typography> | </Typography> | ||||
| </Box> | |||||
| {/* 单个扫描按钮 */} | |||||
| <Button | |||||
| variant={isManualScanning ? "outlined" : "contained"} | |||||
| startIcon={<QrCodeIcon />} | |||||
| onClick={isManualScanning ? onStopScan : onStartScan} | |||||
| color={isManualScanning ? "secondary" : "primary"} | |||||
| > | |||||
| {isManualScanning ? t("Stop QR Scan") : t("Start QR Scan")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Paper> | |||||
| <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> | |||||
| {/* ========== 产出输入表单 ========== */} | |||||
| {scannedOperators.length > 0 && scannedMachines.length > 0 && ( | |||||
| <Paper sx={{ p: 3, bgcolor: 'grey.50' }}> | |||||
| <Typography variant="h6" gutterBottom fontWeight={600}> | |||||
| {/* ========== 产出输入表单 ========== */} | |||||
| {bothScanned && ( | |||||
| <Box> | |||||
| <Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |||||
| <Typography variant="h6" fontWeight={600}> | |||||
| {t("Production Output Data Entry")} | {t("Production Output Data Entry")} | ||||
| </Typography> | </Typography> | ||||
| <Table size="small"> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell width="30%">{t("Type")}</TableCell> | |||||
| <TableCell width="35%">{t("Quantity")}</TableCell> | |||||
| <TableCell width="35%">{t("Unit of Measure")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {/* 步骤收成 */} | |||||
| <TableRow> | |||||
| <TableCell> | |||||
| <Typography fontWeight={500}>{t("Output from Process")}</Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| type="number" | |||||
| fullWidth | |||||
| size="small" | |||||
| value={outputData.outputFromProcessQty} | |||||
| onChange={(e) => onOutputDataChange({ outputFromProcessQty: e.target.value })} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| fullWidth | |||||
| size="small" | |||||
| value={outputData.outputFromProcessUom} | |||||
| onChange={(e) => onOutputDataChange({ outputFromProcessUom: e.target.value })} | |||||
| placeholder="KG, L, PCS..." | |||||
| /> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| {/* 副产品 */} | |||||
| <TableRow> | |||||
| <TableCell> | |||||
| <Stack> | |||||
| <Typography fontWeight={500}>{t("By-product")}</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 | |||||
| })} | |||||
| //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 | <TextField | ||||
| fullWidth | fullWidth | ||||
| size="small" | size="small" | ||||
| value={outputData.byproductName} | |||||
| onChange={(e) => onOutputDataChange({ byproductName: e.target.value })} | |||||
| placeholder={t("By-product name")} | |||||
| sx={{ mt: 1 }} | |||||
| value={outputData.byproductUom} | |||||
| onChange={(e) => setOutputData({ | |||||
| ...outputData, | |||||
| byproductUom: e.target.value | |||||
| })} | |||||
| //placeholder="KG, L, PCS..." | |||||
| /> | /> | ||||
| </Stack> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| type="number" | |||||
| fullWidth | |||||
| size="small" | |||||
| value={outputData.byproductQty} | |||||
| onChange={(e) => onOutputDataChange({ byproductQty: e.target.value })} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| fullWidth | |||||
| size="small" | |||||
| value={outputData.byproductUom} | |||||
| onChange={(e) => onOutputDataChange({ byproductUom: e.target.value })} | |||||
| placeholder="KG, L, PCS..." | |||||
| /> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| {/* 次品 */} | |||||
| <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) => onOutputDataChange({ defectQty: e.target.value })} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| fullWidth | |||||
| size="small" | |||||
| value={outputData.defectUom} | |||||
| onChange={(e) => onOutputDataChange({ defectUom: e.target.value })} | |||||
| placeholder="KG, L, PCS..." | |||||
| /> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| {/* 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 | |||||
| })} | |||||
| //placeholder="KG, L, PCS..." | |||||
| /> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| {/* 废品 */} | |||||
| <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) => onOutputDataChange({ scrapQty: e.target.value })} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| fullWidth | |||||
| size="small" | |||||
| value={outputData.scrapUom} | |||||
| onChange={(e) => onOutputDataChange({ scrapUom: e.target.value })} | |||||
| placeholder="KG, L, PCS..." | |||||
| /> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableBody> | |||||
| </Table> | |||||
| {/* 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 | |||||
| })} | |||||
| //placeholder="KG, L, PCS..." | |||||
| /> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableBody> | |||||
| </Table> | |||||
| {/* 提交按钮 */} | |||||
| <Box sx={{ mt: 3, display: 'flex', gap: 2 }}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| onClick={onCancel} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<CheckCircleIcon />} | |||||
| onClick={onSubmitOutput} | |||||
| > | |||||
| {t("Complete Step")} | |||||
| </Button> | |||||
| </Box> | |||||
| </Paper> | |||||
| )} | |||||
| </Stack> | |||||
| {/* 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> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||