Browse Source

update

master
CANCERYS\kw093 1 month ago
parent
commit
333e67e03b
4 changed files with 795 additions and 493 deletions
  1. +35
    -4
      src/app/api/jo/actions.ts
  2. +14
    -9
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  3. +388
    -258
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  4. +358
    -222
      src/components/ProductionProcess/ProductionProcessStepExecution.tsx

+ 35
- 4
src/app/api/jo/actions.ts View File

@@ -26,7 +26,15 @@ export interface SearchJoResultRequest extends Pageable {
planStartTo?: string;
}


export interface productProcessLineQtyRequest {
productProcessLineId: number;
outputFromProcessQty: number;
outputFromProcessUom: string;
defectQty: number;
defectUom: string;
scrapQty: number;
scrapUom: string;
}
export interface SearchJoResultResponse {
records: JobOrder[];
total: number;
@@ -194,6 +202,9 @@ export interface UpdateProductProcessLineQtyRequest {
productProcessLineId: number;
outputFromProcessQty: number;
outputFromProcessUom: string;
byproductName: string;
byproductQty: number;
byproductUom: string;
defectQty: number;
defectUom: string;
scrapQty: number;
@@ -246,7 +257,7 @@ export interface ProductProcessInfoResponse {
status: string;
}
export interface ProductProcessLineQrscanUpadteRequest {
lineId: number;
productProcessLineId: number;
operatorId?: number;
equipmentId?: number;
}
@@ -261,7 +272,7 @@ export interface ProductProcessLineDetailResponse {
seqNo: number,
name: string,
description: string,
equipment: string,
equipmentId: number,
startTime: string,
endTime: string,
defectQty: number,
@@ -282,6 +293,14 @@ export interface ProductProcessLineDetailResponse {
operatorName: string,
handlerId: number,
seqNo: number,
isDark: string,
isDense: number,
isFloat: string,
outputQtyUom: string,
outputQty: number,
pickOrderId: number,
jobOrderCode: string,
jobOrderId: number,
name: string,
description: string,
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) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/update`,
@@ -333,6 +363,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async () => {
}
);
});
/*
export const updateProductProcessLineQty = async (request: UpdateProductProcessLineQtyRequest) => {
return serverFetchJson<UpdateProductProcessLineQtyResponse>(
`${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) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/start/${lineId}`,


+ 14
- 9
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx View File

@@ -568,20 +568,25 @@ if (showDetailView && selectedDoPickOrder) {
</Paper>

{/* 添加:多个 Pick Orders 信息(如果有) */}
{selectedDoPickOrder.pickOrderIds && selectedDoPickOrder.pickOrderIds.length > 1 && (
{selectedDoPickOrder.pickOrderIds && selectedDoPickOrder.pickOrderIds.length > 0 && (
<Paper sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5' }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
{t("This ticket contains")} {selectedDoPickOrder.pickOrderIds.length} {t("pick orders")}:
</Typography>
<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>
</Paper>
)}


+ 388
- 258
src/components/ProductionProcess/ProductionProcessDetail.tsx View File

@@ -17,6 +17,10 @@ import {
Card,
CardContent,
CircularProgress,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material";
import QrCodeIcon from '@mui/icons-material/QrCode';
import { useTranslation } from "react-i18next";
@@ -75,13 +79,17 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
// 选中的 line 和执行状态
const [selectedLineId, setSelectedLineId] = useState<number | null>(null);
const [isExecutingLine, setIsExecutingLine] = useState(false);
const [isAutoSubmitting, setIsAutoSubmitting] = useState(false);
// 扫描器状态
const [isManualScanning, setIsManualScanning] = useState(false);
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({
byproductName: "",
@@ -97,133 +105,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({

// 处理 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 数据
const fetchProcessDetail = useCallback(async () => {
@@ -263,53 +145,239 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
}, [fetchProcessDetail]);

// 开始执行某个 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;
}
// 其他格式处理(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 {
// 直接使用 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) {
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);

@@ -337,16 +405,30 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
</Typography>
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
<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 variant="subtitle1">
<strong>{t("Output Qty")}:</strong> {processData?.outputQty+" "+"("+processData?.outputQtyUom +")"}
</Typography>
<Box>
<strong>{t("Status")}:</strong>{" "}
<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"
/>
</Typography>
</Box>
<Typography variant="subtitle1">
<strong>{t("Date")}:</strong> {dayjs(processData?.date).format(OUTPUT_DATE_FORMAT)}
</Typography>
@@ -376,103 +458,151 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
<TableCell align="center">{t("Action")}</TableCell>
</TableRow>
</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>
</Table>
</TableContainer>
) : (
/* ========== 步骤执行视图 ========== */
<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>

{/* 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>
);
};


+ 358
- 222
src/components/ProductionProcess/ProductionProcessStepExecution.tsx View File

@@ -1,5 +1,4 @@
"use client";
import React from "react";
import {
Box,
Button,
@@ -14,255 +13,392 @@ import {
TableRow,
Card,
CardContent,
Grid,
} from "@mui/material";
import QrCodeIcon from '@mui/icons-material/QrCode';
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 { 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 React, { useCallback, useEffect, useState } from "react";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { fetchNameList, NameList } from "@/app/api/user/actions";
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> = ({
selectedLine,
scannedOperators,
scannedMachines,
isManualScanning,
outputData,
onStartScan,
onStopScan,
onCancel,
onSubmitOutput,
onOutputDataChange,
lineId,
}) => {
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 (
<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">
{scannedOperators.length > 0
? `${t("Operator")}: ${scannedOperators[0].name || scannedOperators[0].username}`
: t("Please scan operator code")
}
{lineDetail?.description}
</Typography>
</Box>
{/* 设备扫描 */}
<Box>
<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>
</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")}
</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
fullWidth
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>
);
};


Loading…
Cancel
Save