| @@ -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 */} | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -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", | ||||
| @@ -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'); | ||||
| } | } | ||||
| @@ -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) | ||||
| @@ -0,0 +1,411 @@ | |||||
| "use client"; | |||||
| import React, { useCallback, useEffect, useState } from "react"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Paper, | |||||
| Stack, | |||||
| Typography, | |||||
| TextField, | |||||
| Grid, | |||||
| Card, | |||||
| CardContent, | |||||
| CircularProgress, | |||||
| Tabs, | |||||
| Tab, | |||||
| TabsProps, | |||||
| } from "@mui/material"; | |||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { fetchProductProcessesByJobOrderId } from "@/app/api/jo/actions"; | |||||
| import ProductionProcessDetail from "./ProductionProcessDetail"; | |||||
| import dayjs from "dayjs"; | |||||
| import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; | |||||
| import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; | |||||
| import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; | |||||
| import { decimalFormatter } from "@/app/utils/formatUtil"; | |||||
| import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; | |||||
| import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; | |||||
| import { fetchInventories } from "@/app/api/inventory/actions"; | |||||
| import { InventoryResult } from "@/app/api/inventory"; | |||||
| import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | |||||
| interface JobOrderLine { | |||||
| id: number; | |||||
| jobOrderId: number; | |||||
| jobOrderCode: string; | |||||
| itemId: number; | |||||
| itemCode: string; | |||||
| itemName: string; | |||||
| reqQty: number; | |||||
| stockQty: number; | |||||
| uom: string; | |||||
| shortUom: string; | |||||
| availableStatus: string; | |||||
| } | |||||
| interface ProductProcessJobOrderDetailProps { | |||||
| jobOrderId: number; | |||||
| onBack: () => void; | |||||
| } | |||||
| const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({ | |||||
| jobOrderId, | |||||
| onBack, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const [loading, setLoading] = useState(false); | |||||
| const [processData, setProcessData] = useState<any>(null); | |||||
| const [jobOrderLines, setJobOrderLines] = useState<JobOrderLine[]>([]); | |||||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | |||||
| // 获取数据 | |||||
| const fetchData = useCallback(async () => { | |||||
| setLoading(true); | |||||
| try { | |||||
| const data = await fetchProductProcessesByJobOrderId(jobOrderId); | |||||
| if (data && data.length > 0) { | |||||
| const firstProcess = data[0]; | |||||
| setProcessData(firstProcess); | |||||
| setJobOrderLines((firstProcess as any).jobOrderLines || []); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error loading data:", error); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }, [jobOrderId]); | |||||
| // 获取库存数据 | |||||
| useEffect(() => { | |||||
| const fetchInventoryData = async () => { | |||||
| try { | |||||
| const inventoryResponse = await fetchInventories({ | |||||
| code: "", | |||||
| name: "", | |||||
| type: "", | |||||
| pageNum: 0, | |||||
| pageSize: 1000 | |||||
| }); | |||||
| setInventoryData(inventoryResponse.records); | |||||
| } catch (error) { | |||||
| console.error("Error fetching inventory data:", error); | |||||
| } | |||||
| }; | |||||
| fetchInventoryData(); | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| fetchData(); | |||||
| }, [fetchData]); | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| // 如果选择了 process detail,显示 detail 页面 | |||||
| if (selectedProcessId !== null) { | |||||
| return ( | |||||
| <ProductionProcessDetail | |||||
| jobOrderId={selectedProcessId} | |||||
| onBack={() => { | |||||
| setSelectedProcessId(null); | |||||
| fetchData(); // 刷新数据 | |||||
| }} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| if (loading) { | |||||
| return ( | |||||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||||
| <CircularProgress/> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| if (!processData) { | |||||
| return ( | |||||
| <Box> | |||||
| <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}> | |||||
| {t("Back")} | |||||
| </Button> | |||||
| <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| // InfoCard 组件内容 | |||||
| const InfoCardContent = () => ( | |||||
| <Card sx={{ display: "block", mt: 2 }}> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Job Order Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.jobOrderCode || ""} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}/> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Item Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.itemCode || ""} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Item Name")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.itemName || ""} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Req. Qty")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.outputQty ? integerFormatter.format(processData.outputQty) : ""} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("UoM")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.outputQtyUom || ""} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| value={processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""} | |||||
| label={t("Target Production Date")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Production Priority")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.productionPriority || "0"} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Is Dark")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.isDark || ""} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Is Dense")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.isDense || ""} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Is Float")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={processData?.isFloat || ""} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| // PickTable 组件内容 | |||||
| const getStockAvailable = (line: JobOrderLine) => { | |||||
| const inventory = inventoryData.find(inv => | |||||
| inv.itemCode === line.itemCode || inv.itemName === line.itemName | |||||
| ); | |||||
| if (inventory) { | |||||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||||
| } | |||||
| return line.stockQty || 0; | |||||
| }; | |||||
| const isStockSufficient = (line: JobOrderLine) => { | |||||
| const stockAvailable = getStockAvailable(line); | |||||
| return stockAvailable >= line.reqQty; | |||||
| }; | |||||
| const productionProcessesLineRemarkTableColumns: GridColDef[] = [ | |||||
| { | |||||
| field: "seqNo", | |||||
| headerName: t("Seq"), | |||||
| flex: 0.2, | |||||
| align: "left", | |||||
| headerAlign: "center", | |||||
| type: "number", | |||||
| }, | |||||
| { | |||||
| field: "description", | |||||
| headerName: t("Remark"), | |||||
| flex: 1, | |||||
| align: "left", | |||||
| headerAlign: "center", | |||||
| }, | |||||
| ]; | |||||
| const productionProcessesLineRemarkTableRows = | |||||
| processData?.productProcessLines?.map((line: any) => ({ | |||||
| id: line.seqNo, | |||||
| seqNo: line.seqNo, | |||||
| description: line.description ?? "", | |||||
| })) ?? []; | |||||
| const pickTableColumns: GridColDef[] = [ | |||||
| { | |||||
| field: "id", | |||||
| headerName: t("id"), | |||||
| flex: 0.2, | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| type: "number", | |||||
| }, | |||||
| { | |||||
| field: "itemCode", | |||||
| headerName: t("Item Code"), | |||||
| flex: 0.6, | |||||
| }, | |||||
| { | |||||
| field: "itemName", | |||||
| headerName: t("Item Name"), | |||||
| flex: 1, | |||||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||||
| return `${params.value} (${params.row.uom})`; | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "reqQty", | |||||
| headerName: t("Req. Qty"), | |||||
| flex: 0.7, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||||
| return `${decimalFormatter.format(params.value)} (${params.row.shortUom})`; | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "stockAvailable", | |||||
| headerName: t("Stock Available"), | |||||
| flex: 0.7, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| type: "number", | |||||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||||
| const stockAvailable = getStockAvailable(params.row); | |||||
| return `${decimalFormatter.format(stockAvailable)} (${params.row.shortUom})`; | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "stockStatus", | |||||
| headerName: t("Stock Status"), | |||||
| flex: 0.5, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| type: "boolean", | |||||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||||
| return isStockSufficient(params.row) | |||||
| ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" /> | |||||
| : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />; | |||||
| }, | |||||
| }, | |||||
| ]; | |||||
| const pickTableRows = jobOrderLines.map((line, index) => ({ | |||||
| ...line, | |||||
| id: line.id || index, | |||||
| })); | |||||
| const PickTableContent = () => ( | |||||
| <Box sx={{ mt: 2 }}> | |||||
| <StyledDataGrid | |||||
| sx={{ | |||||
| "--DataGrid-overlayHeight": "100px", | |||||
| }} | |||||
| disableColumnMenu | |||||
| rows={pickTableRows} | |||||
| columns={pickTableColumns} | |||||
| getRowHeight={() => 'auto'} | |||||
| /> | |||||
| </Box> | |||||
| ); | |||||
| const ProductionProcessesLineRemarkTableContent = () => ( | |||||
| <Box sx={{ mt: 2 }}> | |||||
| <StyledDataGrid | |||||
| sx={{ | |||||
| "--DataGrid-overlayHeight": "100px", | |||||
| }} | |||||
| disableColumnMenu | |||||
| rows={productionProcessesLineRemarkTableRows ?? []} | |||||
| columns={productionProcessesLineRemarkTableColumns} | |||||
| getRowHeight={() => 'auto'} | |||||
| /> | |||||
| </Box> | |||||
| ); | |||||
| return ( | |||||
| <Box> | |||||
| {/* 返回按钮 */} | |||||
| <Box sx={{ mb: 2 }}> | |||||
| <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}> | |||||
| {t("Back to List")} | |||||
| </Button> | |||||
| </Box> | |||||
| {/* 标签页 */} | |||||
| <Box sx={{ borderBottom: '1px solid #e0e0e0' }}> | |||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||||
| <Tab label={t("Job Order Info")} /> | |||||
| <Tab label={t("Job Order Lines")} /> | |||||
| <Tab label={t("Production Process")} /> | |||||
| <Tab label={t("Production Process Line Remark")} /> | |||||
| <Tab label={t("Matching Stock")} /> | |||||
| </Tabs> | |||||
| </Box> | |||||
| {/* 标签页内容 */} | |||||
| <Box sx={{ p: 2 }}> | |||||
| {tabIndex === 0 && <InfoCardContent />} | |||||
| {tabIndex === 1 && <PickTableContent />} | |||||
| {tabIndex === 2 && ( | |||||
| <ProductionProcessDetail | |||||
| jobOrderId={jobOrderId} | |||||
| onBack={() => { | |||||
| // 切换回第一个标签页,或者什么都不做 | |||||
| setTabIndex(0); | |||||
| }} | |||||
| /> | |||||
| )} | |||||
| {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />} | |||||
| {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} | |||||
| </Box> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default ProductionProcessJobOrderDetail; | |||||
| @@ -14,6 +14,7 @@ import { | |||||
| Grid, | 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> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -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 { | |||||
| } | } | ||||
| }} | }} | ||||
| /> | /> | ||||
| @@ -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> | ||||
| ); | ); | ||||