瀏覽代碼

update

master
CANCERYS\kw093 3 週之前
父節點
當前提交
32f7897ca8
共有 8 個檔案被更改,包括 1009 行新增342 行删除
  1. +3
    -2
      src/app/(main)/production/page.tsx
  2. +111
    -45
      src/app/api/jo/actions.ts
  3. +2
    -5
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  4. +61
    -19
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  5. +411
    -0
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  6. +40
    -5
      src/components/ProductionProcess/ProductionProcessList.tsx
  7. +22
    -38
      src/components/ProductionProcess/ProductionProcessPage.tsx
  8. +359
    -228
      src/components/ProductionProcess/ProductionProcessStepExecution.tsx

+ 3
- 2
src/app/(main)/production/page.tsx 查看文件

@@ -8,6 +8,7 @@ import Typography from "@mui/material/Typography";
import { Metadata } from "next"; import { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { Suspense } from "react"; import { Suspense } from "react";
import { fetchPrinterCombo } from "@/app/api/settings/printer";


export const metadata: Metadata = { export const metadata: Metadata = {
title: "Claims", title: "Claims",
@@ -15,7 +16,7 @@ export const metadata: Metadata = {


const production: React.FC = async () => { const production: React.FC = async () => {
const { t } = await getServerI18n("claims"); const { t } = await getServerI18n("claims");
const printerCombo = await fetchPrinterCombo();
return ( return (
<> <>
<Stack <Stack
@@ -37,7 +38,7 @@ const production: React.FC = async () => {
{t("Create Process")} {t("Create Process")}
</Button> */} </Button> */}
</Stack> </Stack>
<ProductionProcessPage /> {/* Use new component */}
<ProductionProcessPage printerCombo={printerCombo} /> {/* Use new component */}
</> </>
); );
}; };


+ 111
- 45
src/app/api/jo/actions.ts 查看文件

@@ -171,20 +171,31 @@ export interface ProductProcessResponse {
} }


export interface ProductProcessLineResponse { 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 { export interface ProductProcessWithLinesResponse {
@@ -196,7 +207,19 @@ export interface ProductProcessWithLinesResponse {
date: string; date: string;
bomId?: number; bomId?: number;
jobOrderId?: 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[]; productProcessLines: ProductProcessLineResponse[];
} }
export interface UpdateProductProcessLineQtyRequest { export interface UpdateProductProcessLineQtyRequest {
productProcessLineId: number; productProcessLineId: number;
@@ -241,6 +264,7 @@ export interface AllJoborderProductProcessInfoResponse {
bomId?: number; bomId?: number;
itemName: string; itemName: string;
jobOrderId: number; jobOrderId: number;
stockInLineId: number;
jobOrderCode: string; jobOrderCode: string;
productProcessLineCount: number; productProcessLineCount: number;
FinishedProductProcessLineCount: number; FinishedProductProcessLineCount: number;
@@ -261,6 +285,7 @@ export interface ProductProcessLineQrscanUpadteRequest {
operatorId?: number; operatorId?: number;
equipmentId?: number; equipmentId?: number;
} }

export interface ProductProcessLineDetailResponse { export interface ProductProcessLineDetailResponse {
id: number, id: number,
productProcessId: number, productProcessId: number,
@@ -270,9 +295,17 @@ 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,
equipmentId: number,
equipment: string,
startTime: string, startTime: string,
endTime: string, endTime: string,
defectQty: number, defectQty: number,
@@ -283,50 +316,83 @@ export interface ProductProcessLineDetailResponse {
byproductName: string, byproductName: string,
byproductQty: number, byproductQty: number,
byproductUom: string | undefined, 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, 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, operatorId: number,
equipmentType: string,
operatorName: string, operatorName: string,
equipmentId: number,
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,
startTime: string,
endTime: string,
defectQty: number,
defectUom: string,
scrapQty: number,
scrapUom: string,
equipment_name: string,
status: string,
byproductId: number, byproductId: number,
byproductName: string, byproductName: string,
byproductQty: number, 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) => { export const fetchProductProcessLineDetail = cache(async (lineId: number) => {
return serverFetchJson<ProductProcessLineDetailResponse>(
return serverFetchJson<JobOrderProcessLineDetailResponse>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`, `${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`,
{ {
method: "GET", method: "GET",


+ 2
- 5
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx 查看文件

@@ -189,12 +189,9 @@ const validateForm = (): boolean => {
newErrors.actualPickQty = t('Qty cannot be negative'); newErrors.actualPickQty = t('Qty cannot be negative');
} }
// 2. 检查 actualPickQty 不能超过可用数量或需求数量
<<<<<<< Updated upstream
if (ap > Math.min(req)) {
=======


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'); newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty');
} }


+ 61
- 19
src/components/ProductionProcess/ProductionProcessDetail.tsx 查看文件

@@ -34,11 +34,12 @@ import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { import {
fetchProductProcessById, fetchProductProcessById,
fetchProductProcessLines,
updateProductProcessLineQrscan, updateProductProcessLineQrscan,
fetchProductProcessLineDetail, fetchProductProcessLineDetail,
ProductProcessLineDetailResponse, ProductProcessLineDetailResponse,
JobOrderProcessLineDetailResponse,
updateLineOutput, updateLineOutput,
ProductProcessLineInfoResponse,
ProductProcessResponse, ProductProcessResponse,
ProductProcessLineResponse, ProductProcessLineResponse,
completeProductProcessLine, completeProductProcessLine,
@@ -47,14 +48,6 @@ import {
} from "@/app/api/jo/actions"; } from "@/app/api/jo/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions";
import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; 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 { interface ProductProcessDetailProps {
@@ -73,7 +66,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
// 基本信息 // 基本信息
const [processData, setProcessData] = useState<any>(null); const [processData, setProcessData] = useState<any>(null);
const [lines, setLines] = useState<ProductProcessLineDetailResponse[]>([]);
const [lines, setLines] = useState<ProductProcessLineInfoResponse[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// 选中的 line 和执行状态 // 选中的 line 和执行状态
@@ -86,7 +79,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null); const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null);
const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null); const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null);
const [scanningLineId, setScanningLineId] = 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 [showScanDialog, setShowScanDialog] = useState(false);
const autoSubmitTimerRef = useRef<NodeJS.Timeout | null>(null); const autoSubmitTimerRef = useRef<NodeJS.Timeout | null>(null);


@@ -105,7 +98,11 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({


// 处理 QR 码扫描 // 处理 QR 码扫描
// 处理 QR 码扫描 // 处理 QR 码扫描
const handleBackFromStep = async () => {
await fetchProcessDetail(); // 重新拉取最新的 process/lines
setIsExecutingLine(false);
setSelectedLineId(null);
};


// 获取 process 和 lines 数据 // 获取 process 和 lines 数据
const fetchProcessDetail = useCallback(async () => { const fetchProcessDetail = useCallback(async () => {
@@ -230,6 +227,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({


if (!scannedOperatorId) { if (!scannedOperatorId) {
console.log("No operatorId, cannot submit"); console.log("No operatorId, cannot submit");
setIsAutoSubmitting(false);
return false; // 没有 operatorId,不能提交 return false; // 没有 operatorId,不能提交
} }


@@ -255,8 +253,15 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
// 检查响应中的 message 字段来判断是否成功 // 检查响应中的 message 字段来判断是否成功
// 如果后端返回 message 不为 null,说明验证失败 // 如果后端返回 message 不为 null,说明验证失败
if (response && response.message) { 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.")); //alert(response.message || t("Validation failed. Please check operator and equipment."));
return false; return false;
} }
// 验证通过,继续执行后续步骤 // 验证通过,继续执行后续步骤
console.log("Validation passed, starting line..."); console.log("Validation passed, starting line...");
@@ -297,11 +302,17 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({


// 开始扫描 // 开始扫描
const handleStartScan = useCallback((lineId: number) => { const handleStartScan = useCallback((lineId: number) => {
if (autoSubmitTimerRef.current) {
clearTimeout(autoSubmitTimerRef.current);
autoSubmitTimerRef.current = null;
}
setScanningLineId(lineId); setScanningLineId(lineId);
setIsManualScanning(true); setIsManualScanning(true);
setProcessedQrCodes(new Set()); setProcessedQrCodes(new Set());
setScannedOperatorId(null); setScannedOperatorId(null);
setScannedEquipmentId(null); setScannedEquipmentId(null);
setIsAutoSubmitting(false); // 添加:重置自动提交状态
setLineDetailForScan(null);
// 获取 line detail 以获取 bomProcessEquipmentId // 获取 line detail 以获取 bomProcessEquipmentId
fetchProductProcessLineDetail(lineId) fetchProductProcessLineDetail(lineId)
.then(setLineDetailForScan) .then(setLineDetailForScan)
@@ -311,7 +322,16 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({


// 停止扫描 // 停止扫描
const handleStopScan = useCallback(() => { const handleStopScan = useCallback(() => {
console.log("🛑 Stopping scan");
// 清除定时器
if (autoSubmitTimerRef.current) {
clearTimeout(autoSubmitTimerRef.current);
autoSubmitTimerRef.current = null;
}
setIsManualScanning(false); setIsManualScanning(false);
setIsAutoSubmitting(false); // 添加:重置自动提交状态
stopScan(); stopScan();
resetScan(); resetScan();
}, [stopScan, resetScan]); }, [stopScan, resetScan]);
@@ -374,11 +394,24 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
}, []); }, []);


const handleStartLineWithScan = async (lineId: number) => { 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); setScanningLineId(lineId);
setShowScanDialog(true); setShowScanDialog(true);
handleStartScan(lineId); handleStartScan(lineId);
}; };

const selectedLine = lines.find(l => l.id === selectedLineId); const selectedLine = lines.find(l => l.id === selectedLineId);


if (loading) { if (loading) {
@@ -391,14 +424,14 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({


return ( return (
<Box> <Box>
{/* 返回按钮 */}
<Box sx={{ mb: 2 }}>
{/*
<Box sx={{ mb: 2 }}>
<Button variant="outlined" onClick={onBack}> <Button variant="outlined" onClick={onBack}>
{t("Back to List")} {t("Back to List")}
</Button> </Button>
</Box> </Box>


{/* ========== 第一部分:基本信息 ========== */}
<Paper sx={{ p: 3, mb: 3 }}> <Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom fontWeight="bold"> <Typography variant="h6" gutterBottom fontWeight="bold">
{t("Production Process Information")} {t("Production Process Information")}
@@ -437,7 +470,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
</Typography> </Typography>
</Stack> </Stack>
</Paper> </Paper>
*/}
{/* ========== 第二部分:Process Lines ========== */} {/* ========== 第二部分:Process Lines ========== */}
<Paper sx={{ p: 3 }}> <Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom fontWeight="bold"> <Typography variant="h6" gutterBottom fontWeight="bold">
@@ -452,8 +485,12 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
<TableRow> <TableRow>
<TableCell>{t("Seq")}</TableCell> <TableCell>{t("Seq")}</TableCell>
<TableCell>{t("Step Name")}</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("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("Status")}</TableCell>
<TableCell align="center">{t("Action")}</TableCell> <TableCell align="center">{t("Action")}</TableCell>
</TableRow> </TableRow>
@@ -462,7 +499,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
{lines.map((line) => { {lines.map((line) => {
const status = (line as any).status || ''; const status = (line as any).status || '';
const statusLower = status.toLowerCase(); const statusLower = status.toLowerCase();
const equipmentName = (line as any).equipment_name || line.equipmentType || "-";
const equipmentName = line.equipment_name || "-";
const isCompleted = statusLower === 'completed'; const isCompleted = statusLower === 'completed';
const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress';
@@ -475,7 +512,11 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
<Typography fontWeight={500}>{line.name}</Typography> <Typography fontWeight={500}>{line.name}</Typography>
</TableCell> </TableCell>
<TableCell><Typography fontWeight={500}>{line.description || "-"}</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}>{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"> <TableCell align="center">
{isCompleted ? ( {isCompleted ? (
<Chip label={t("Completed")} color="success" size="small" /> <Chip label={t("Completed")} color="success" size="small" />
@@ -534,6 +575,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
/* ========== 步骤执行视图 ========== */ /* ========== 步骤执行视图 ========== */
<ProductionProcessStepExecution <ProductionProcessStepExecution
lineId={selectedLineId} lineId={selectedLineId}
onBack={handleBackFromStep}
//onClose={() => { //onClose={() => {
// setIsExecutingLine(false) // setIsExecutingLine(false)
// setSelectedLineId(null) // setSelectedLineId(null)


+ 411
- 0
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx 查看文件

@@ -0,0 +1,411 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import {
Box,
Button,
Paper,
Stack,
Typography,
TextField,
Grid,
Card,
CardContent,
CircularProgress,
Tabs,
Tab,
TabsProps,
} from "@mui/material";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useTranslation } from "react-i18next";
import { fetchProductProcessesByJobOrderId } from "@/app/api/jo/actions";
import ProductionProcessDetail from "./ProductionProcessDetail";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
import StyledDataGrid from "../StyledDataGrid/StyledDataGrid";
import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { decimalFormatter } from "@/app/utils/formatUtil";
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded';
import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
interface JobOrderLine {
id: number;
jobOrderId: number;
jobOrderCode: string;
itemId: number;
itemCode: string;
itemName: string;
reqQty: number;
stockQty: number;
uom: string;
shortUom: string;
availableStatus: string;
}

interface ProductProcessJobOrderDetailProps {
jobOrderId: number;
onBack: () => void;
}

const ProductionProcessJobOrderDetail: React.FC<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;

+ 40
- 5
src/components/ProductionProcess/ProductionProcessList.tsx 查看文件

@@ -14,6 +14,7 @@ import {
Grid, Grid,
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import QcStockInModal from "../Qc/QcStockInModal";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig"; import { SessionWithTokens } from "@/config/authConfig";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -22,21 +23,38 @@ import {
fetchAllJoborderProductProcessInfo, fetchAllJoborderProductProcessInfo,
AllJoborderProductProcessInfoResponse, AllJoborderProductProcessInfoResponse,
} from "@/app/api/jo/actions"; } from "@/app/api/jo/actions";

import { StockInLineInput } from "@/app/api/stockIn";
import { PrinterCombo } from "@/app/api/settings/printer";
interface ProductProcessListProps { interface ProductProcessListProps {
onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void; onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void;
printerCombo: PrinterCombo[];
} }



const PER_PAGE = 6; const PER_PAGE = 6;


const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess }) => {
const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess, printerCombo }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: session } = useSession() as { data: SessionWithTokens | null }; const { data: session } = useSession() as { data: SessionWithTokens | null };
const sessionToken = session as SessionWithTokens | null;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]); const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]);
const [page, setPage] = useState(0); 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 () => { const fetchProcesses = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -54,7 +72,12 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
useEffect(() => { useEffect(() => {
fetchProcesses(); fetchProcesses();
}, [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 startIdx = page * PER_PAGE;
const paged = processes.slice(startIdx, startIdx + 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)}> <Button variant="contained" size="small" onClick={() => onSelectProcess(process.jobOrderId, process.id)}>
{t("View Details")} {t("View Details")}
</Button> </Button>
{statusLower === "completed" && (
<Button onClick={() => handleViewStockIn(process)}>
{t("view stockin")}
</Button>
)}
<Box sx={{ flex: 1 }} /> <Box sx={{ flex: 1 }} />
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
{t("Lines")}: {totalCount} {t("Lines")}: {totalCount}
@@ -165,7 +193,13 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
); );
})} })}
</Grid> </Grid>

<QcStockInModal
session={sessionToken}
open={openModal}
onClose={closeNewModal}
inputDetail={modalInfo}
printerCombo={printerCombo}
/>
{processes.length > 0 && ( {processes.length > 0 && (
<TablePagination <TablePagination
component="div" component="div"
@@ -179,6 +213,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
</Box> </Box>
)} )}
</Box> </Box>
); );
}; };



+ 22
- 38
src/components/ProductionProcess/ProductionProcessPage.tsx 查看文件

@@ -4,53 +4,38 @@ import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig"; import { SessionWithTokens } from "@/config/authConfig";
import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList";
import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail";
import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail";

import { import {
fetchProductProcesses, fetchProductProcesses,
fetchProductProcessLines,
fetchProductProcessesByJobOrderId,
ProductProcessLineResponse ProductProcessLineResponse
} from "@/app/api/jo/actions"; } 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 [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
const { data: session } = useSession() as { data: SessionWithTokens | null }; const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined; 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) { if (selectedProcessId !== null) {
return ( return (
<ProductionProcessDetail
<ProductionProcessJobOrderDetail
jobOrderId={selectedProcessId} jobOrderId={selectedProcessId}
onBack={() => setSelectedProcessId(null)} onBack={() => setSelectedProcessId(null)}
/> />
@@ -59,12 +44,11 @@ const ProductionProcessPage = () => {


return ( return (
<ProductionProcessList <ProductionProcessList
onSelectProcess={(jobOrderId, productProcessId) => {
printerCombo={printerCombo}
onSelectProcess={(jobOrderId) => {
const id = jobOrderId ?? null; const id = jobOrderId ?? null;
if (id !== null) { if (id !== null) {
setSelectedProcessId(id); setSelectedProcessId(id);
} else {
} }
}} }}
/> />


+ 359
- 228
src/components/ProductionProcess/ProductionProcessStepExecution.tsx 查看文件

@@ -21,21 +21,24 @@ import StopIcon from "@mui/icons-material/Stop";
import PauseIcon from "@mui/icons-material/Pause"; import PauseIcon from "@mui/icons-material/Pause";
import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { useTranslation } from "react-i18next"; 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 { Operator, Machine } from "@/app/api/jo";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { fetchNameList, NameList } from "@/app/api/user/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions";
interface ProductionProcessStepExecutionProps { interface ProductionProcessStepExecutionProps {
lineId: number | null lineId: number | null
onBack: () => void
//onClose: () => void //onClose: () => void
// onOutputSubmitted: () => Promise<void> // onOutputSubmitted: () => Promise<void>
} }
const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({
lineId, lineId,
onBack,
}) => { }) => {
const { t } = useTranslation(); 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 & { const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & {
byproductName: string; byproductName: string;
byproductQty: number; byproductQty: number;
@@ -62,10 +65,39 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; 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 () => { const handleSubmitOutput = async () => {
if (!lineDetail?.id) return; if (!lineDetail?.id) return;


@@ -88,27 +120,15 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro


console.log(" Output data submitted successfully"); 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) => { .then((detail) => {
setLineDetail(detail);
setLineDetail(detail as any);
// 初始化 outputData 从 lineDetail // 初始化 outputData 从 lineDetail
setOutputData(prev => ({ setOutputData(prev => ({
...prev, ...prev,
productProcessLineId: detail.id, 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, defectQty: detail.defectQty || 0,
defectUom: detail.defectUom || "", defectUom: detail.defectUom || "",
scrapQty: detail.scrapQty || 0, scrapQty: detail.scrapQty || 0,
@@ -122,8 +142,12 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
console.error("Failed to load line detail", err); console.error("Failed to load line detail", err);
setLineDetail(null); setLineDetail(null);
}); });
}, [lineId]);

} catch (error) {
console.error("Error submitting output:", error);
alert("Failed to submit output data. Please try again.");
}
};
// 处理 QR 码扫描效果 // 处理 QR 码扫描效果
useEffect(() => { useEffect(() => {
if (isManualScanning && qrValues.length > 0 && lineDetail?.id) { if (isManualScanning && qrValues.length > 0 && lineDetail?.id) {
@@ -159,245 +183,352 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro


return ( return (
<Box> <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> </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> <TableRow>
<TableCell> <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>
<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>
<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> </TableCell>
</TableRow> </TableRow>
)}


{/* defect */}
{/* Defect */}
{lineDetail?.defectQty && lineDetail.defectQty > 0 && (
<TableRow sx={{ bgcolor: 'warning.50' }}> <TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell> <TableCell>
<Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography> <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
</TableCell> </TableCell>
<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>
<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> </TableCell>
</TableRow> </TableRow>
)}


{/* scrap */}
{/* Scrap */}
{lineDetail?.scrapQty && lineDetail.scrapQty > 0 && (
<TableRow sx={{ bgcolor: 'error.50' }}> <TableRow sx={{ bgcolor: 'error.50' }}>
<TableCell> <TableCell>
<Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
</TableCell> </TableCell>
<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>
<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> </TableCell>
</TableRow> </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> </Box>
); );


Loading…
取消
儲存