diff --git a/src/app/(main)/production/page.tsx b/src/app/(main)/production/page.tsx index 9cdec1a..2386750 100644 --- a/src/app/(main)/production/page.tsx +++ b/src/app/(main)/production/page.tsx @@ -1,5 +1,5 @@ import ProductionProcessPage from "../../../components/ProductionProcess/ProductionProcessPage"; -import { getServerI18n } from "../../../i18n"; +import { I18nProvider, getServerI18n } from "../../../i18n"; import Add from "@mui/icons-material/Add"; import Button from "@mui/material/Button"; @@ -15,7 +15,7 @@ export const metadata: Metadata = { }; const production: React.FC = async () => { - const { t } = await getServerI18n("claims"); + const { t } = await getServerI18n("common"); const printerCombo = await fetchPrinterCombo(); return ( <> @@ -38,7 +38,9 @@ const production: React.FC = async () => { {t("Create Process")} */} - {/* Use new component */} + + {/* Use new component */} + ); }; diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 1070597..f3ba859 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -263,6 +263,7 @@ export interface AllJoborderProductProcessInfoResponse { date: string; bomId?: number; itemName: string; + requiredQty: number; jobOrderId: number; stockInLineId: number; jobOrderCode: string; @@ -332,6 +333,7 @@ export interface JobOrderProcessLineDetailResponse { operatorName: string; handlerId: number; seqNo: number; + durationInMinutes: number; name: string; description: string; equipmentId: number; @@ -360,7 +362,10 @@ export interface JobOrderLineInfo { stockQty: number, uom: string, shortUom: string, - availableStatus: string + availableStatus: string, + bomProcessId: number, + bomProcessSeqNo: number, + } export interface ProductProcessLineInfoResponse { id: number, diff --git a/src/components/ProductionProcess/ProductionOutputForm.tsx b/src/components/ProductionProcess/ProductionOutputForm.tsx new file mode 100644 index 0000000..1f1d536 --- /dev/null +++ b/src/components/ProductionProcess/ProductionOutputForm.tsx @@ -0,0 +1,210 @@ +"use client"; +import React from "react"; +import { + Box, + Button, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, + Typography, +} from "@mui/material"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import { useTranslation } from "react-i18next"; +import { UpdateProductProcessLineQtyRequest } from "@/app/api/jo/actions"; + +interface ProductionOutputFormProps { + outputData: (UpdateProductProcessLineQtyRequest & { + byproductName: string; + byproductQty: number; + byproductUom: string; + }); + setOutputData: React.Dispatch< + React.SetStateAction< + UpdateProductProcessLineQtyRequest & { + byproductName: string; + byproductQty: number; + byproductUom: string; + } + > + >; + onSubmit: () => Promise | void; + onCancel: () => void; +} + +const ProductionOutputForm: React.FC = ({ + outputData, + setOutputData, + onSubmit, + onCancel, +}) => { + const { t } = useTranslation(); + + return ( + + + + + {t("Type")} + {t("Quantity")} + {t("Unit")} + + + + + + {t("Output from Process")} + + + + setOutputData({ + ...outputData, + outputFromProcessQty: parseInt(e.target.value) || 0, + }) + } + /> + + + + setOutputData({ + ...outputData, + outputFromProcessUom: e.target.value, + }) + } + /> + + + + + + + {t("By-product")} + + + + + setOutputData({ + ...outputData, + byproductQty: parseInt(e.target.value) || 0, + }) + } + /> + + + + setOutputData({ + ...outputData, + byproductUom: e.target.value, + }) + } + /> + + + + + + + {t("Defect")} + + + + + setOutputData({ + ...outputData, + defectQty: parseInt(e.target.value) || 0, + }) + } + /> + + + + setOutputData({ + ...outputData, + defectUom: e.target.value, + }) + } + /> + + + + + + + {t("Scrap")} + + + + + setOutputData({ + ...outputData, + scrapQty: parseInt(e.target.value) || 0, + }) + } + /> + + + + setOutputData({ + ...outputData, + scrapUom: e.target.value, + }) + } + /> + + + +
+ + + + + +
+ ); +}; + +export default ProductionOutputForm; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionOutputFormPage.tsx b/src/components/ProductionProcess/ProductionOutputFormPage.tsx new file mode 100644 index 0000000..633942b --- /dev/null +++ b/src/components/ProductionProcess/ProductionOutputFormPage.tsx @@ -0,0 +1,146 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { + Box, + Button, + Typography, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { + fetchProductProcessLineDetail, + updateProductProcessLineQty, + UpdateProductProcessLineQtyRequest, + JobOrderProcessLineDetailResponse, +} from "@/app/api/jo/actions"; +import ProductionOutputForm from "./ProductionOutputForm"; + +interface ProductionOutputFormPageProps { + lineId: number | null; + onBack: () => void; +} + +const ProductionOutputFormPage: React.FC = ({ + lineId, + onBack, +}) => { + const { t } = useTranslation(); + const [lineDetail, setLineDetail] = useState(null); + const [outputData, setOutputData] = useState({ + productProcessLineId: lineId ?? 0, + outputFromProcessQty: 0, + outputFromProcessUom: "", + defectQty: 0, + defectUom: "", + scrapQty: 0, + scrapUom: "", + byproductName: "", + byproductQty: 0, + byproductUom: "", + }); + + useEffect(() => { + if (!lineId) { + setLineDetail(null); + return; + } + fetchProductProcessLineDetail(lineId) + .then((detail) => { + setLineDetail(detail as any); + setOutputData((prev) => ({ + ...prev, + productProcessLineId: detail.id, + outputFromProcessQty: (detail as any).outputFromProcessQty || 0, + outputFromProcessUom: (detail as any).outputFromProcessUom || "", + defectQty: detail.defectQty || 0, + defectUom: detail.defectUom || "", + scrapQty: detail.scrapQty || 0, + scrapUom: detail.scrapUom || "", + byproductName: detail.byproductName || "", + byproductQty: detail.byproductQty || 0, + byproductUom: detail.byproductUom || "", + })); + }) + .catch((err) => { + console.error("Failed to load line detail", err); + setLineDetail(null); + }); + }, [lineId]); + + const handleSubmitOutput = async () => { + if (!lineDetail?.id) return; + + try { + await updateProductProcessLineQty({ + productProcessLineId: lineDetail.id || 0, + byproductName: outputData.byproductName, + byproductQty: outputData.byproductQty, + byproductUom: outputData.byproductUom, + outputFromProcessQty: outputData.outputFromProcessQty, + outputFromProcessUom: outputData.outputFromProcessUom, + defectQty: outputData.defectQty, + defectUom: outputData.defectUom, + scrapQty: outputData.scrapQty, + scrapUom: outputData.scrapUom, + }); + + console.log("Output data submitted successfully"); + + // 重新加载数据 + const detail = await fetchProductProcessLineDetail(lineDetail.id); + setLineDetail(detail as any); + 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 || "", + })); + + // 提交成功后返回 + onBack(); + } catch (error) { + console.error("Error submitting output:", error); + alert("Failed to submit output data. Please try again."); + } + }; + + return ( + + + + + + + + {t("Production Output Data Entry")} + + {lineDetail && ( + + {t("Step")}: {lineDetail.name} (Seq: {lineDetail.seqNo}) + + )} + + + + + ); +}; + +export default ProductionOutputFormPage; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionProcessDetail.tsx b/src/components/ProductionProcess/ProductionProcessDetail.tsx index 6e75c71..1cc4138 100644 --- a/src/components/ProductionProcess/ProductionProcessDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessDetail.tsx @@ -48,7 +48,7 @@ import { } from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; - +import ProductionOutputFormPage from "./ProductionOutputFormPage"; interface ProductProcessDetailProps { jobOrderId: number; @@ -63,7 +63,7 @@ const ProductionProcessDetail: React.FC = ({ const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); - + const [showOutputPage, setShowOutputPage] = useState(false); // 基本信息 const [processData, setProcessData] = useState(null); const [lines, setLines] = useState([]); @@ -102,6 +102,7 @@ const ProductionProcessDetail: React.FC = ({ await fetchProcessDetail(); // 重新拉取最新的 process/lines setIsExecutingLine(false); setSelectedLineId(null); + setShowOutputPage(false); }; // 获取 process 和 lines 数据 @@ -486,11 +487,12 @@ const ProductionProcessDetail: React.FC = ({ {t("Seq")} {t("Step Name")} {t("Description")} + {t("Equipment Type/Code")} {t("Operator")} - {t("Equipment Type")} - {t("Duration")} - {t("Prep Time")} - {t("Post Prod Time")} + + {t("Processing Time (mins)")} + {t("Setup Time (mins)")} + {t("Changeover Time (mins)")} {t("Status")} {t("Action")} @@ -512,16 +514,30 @@ const ProductionProcessDetail: React.FC = ({ {line.name} {line.description || "-"} - {line.operatorName} {equipmentName} - {line.durationInMinutes} {t("Minutes")} - {line.prepTimeInMinutes} {t("Minutes")} - {line.postProdTimeInMinutes} {t("Minutes")} + {line.operatorName} + + {line.durationInMinutes} + {line.prepTimeInMinutes} + {line.postProdTimeInMinutes} {isCompleted ? ( - + { + setSelectedLineId(line.id); + setShowOutputPage(false); // 不显示输出页面 + setIsExecutingLine(true); + await fetchProcessDetail(); + }} + /> ) : isInProgress ? ( - + { + setSelectedLineId(line.id); + setShowOutputPage(false); // 不显示输出页面 + setIsExecutingLine(true); + await fetchProcessDetail(); + }} /> ) : isPending ? ( ) : ( diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index b4f2f48..23ad8d5 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useMemo } from "react"; import { Box, Button, @@ -99,7 +99,36 @@ const ProductionProcessJobOrderDetail: React.FC { fetchData(); }, [fetchData]); +// 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 stockCounts = useMemo(() => { + const total = jobOrderLines.length; + const sufficient = jobOrderLines.filter(isStockSufficient).length; + return { + total, + sufficient, + insufficient: total - sufficient, + }; +}, [jobOrderLines, inventoryData]); +const status = processData?.status?.toLowerCase?.() ?? ""; +const handleRelease = useCallback(() => { + // TODO: 替换为实际的 release 调用 + console.log("Release clicked for jobOrderId:", jobOrderId); +}, [jobOrderId]); const handleTabChange = useCallback>( (_e, newValue) => { setTabIndex(newValue); @@ -139,6 +168,7 @@ const ProductionProcessJobOrderDetail: React.FC ( @@ -153,21 +183,13 @@ const ProductionProcessJobOrderDetail: React.FC - + - - - @@ -175,17 +197,10 @@ const ProductionProcessJobOrderDetail: React.FC - - - + - - - - - - + ); - // 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", @@ -321,11 +306,28 @@ const ProductionProcessJobOrderDetail: React.FC) => { return isStockSufficient(params.row) @@ -342,14 +344,44 @@ const ProductionProcessJobOrderDetail: React.FC ( + + + + + {t("Total lines: ")}{stockCounts.total} + + + + {t("Lines with sufficient stock: ")}{stockCounts.sufficient} + + + + {t("Lines with insufficient stock: ")}{stockCounts.insufficient} + + + + + + + 'auto'} + getRowHeight={() => "auto"} /> ); @@ -381,7 +413,7 @@ const ProductionProcessJobOrderDetail: React.FC - + diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index 6c48394..a659e29 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -34,7 +34,7 @@ interface ProductProcessListProps { const PER_PAGE = 6; const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo }) => { - const { t } = useTranslation(); + const { t } = useTranslation( ["common", "production","purchaseOrder"]); const { data: session } = useSession() as { data: SessionWithTokens | null }; const sessionToken = session as SessionWithTokens | null; const [loading, setLoading] = useState(false); @@ -144,16 +144,22 @@ const ProductProcessList: React.FC = ({ onSelectProcess - {process.productProcessCode} + {t("Job Order")}: {jobOrderCode} - - {t("Job Order")}: {jobOrderCode} - - + + + {t("Item Name")}: {process.itemName} + + + {t("Required Qty")}: {process.requiredQty} + + + {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"} + {statusLower !== "pending" && linesWithStatus.length > 0 && ( @@ -184,9 +190,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess )} - - {t("Lines")}: {totalCount} - + diff --git a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx index 92b9a4b..a20958e 100644 --- a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx +++ b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx @@ -63,7 +63,7 @@ const ProductionProcessStepExecution: React.FC(null); // 检查是否两个都已扫描 //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; @@ -98,6 +98,27 @@ const ProductionProcessStepExecution: React.FC { + if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) { + setRemainingTime(null); + return; + } + const start = new Date(lineDetail.startTime as any); + const end = new Date(start.getTime() + lineDetail.durationInMinutes * 60_000); + const update = () => { + const diff = end.getTime() - Date.now(); + if (diff <= 0) { + setRemainingTime("00:00"); + return; + } + const minutes = Math.floor(diff / 60000).toString().padStart(2, "0"); + const seconds = Math.floor((diff % 60000) / 1000).toString().padStart(2, "0"); + setRemainingTime(`${minutes}:${seconds}`); + }; + update(); + const timer = setInterval(update, 1000); + return () => clearInterval(timer); + }, [lineDetail?.durationInMinutes, lineDetail?.startTime]); const handleSubmitOutput = async () => { if (!lineDetail?.id) return; @@ -255,7 +276,7 @@ const ProductionProcessStepExecution: React.FC {/* By-product */} - {lineDetail?.byproductQty && lineDetail.byproductQty > 0 && ( + {t("By-product")} @@ -272,10 +293,9 @@ const ProductionProcessStepExecution: React.FC{lineDetail.byproductUom || "-"} - )} {/* Defect */} - {lineDetail?.defectQty && lineDetail.defectQty > 0 && ( + {t("Defect")} @@ -287,10 +307,8 @@ const ProductionProcessStepExecution: React.FC{lineDetail.defectUom || "-"} - )} {/* Scrap */} - {lineDetail?.scrapQty && lineDetail.scrapQty > 0 && ( {t("Scrap")} @@ -302,7 +320,6 @@ const ProductionProcessStepExecution: React.FC{lineDetail.scrapUom || "-"} - )} @@ -311,8 +328,9 @@ const ProductionProcessStepExecution: React.FC {/* 如果未完成,显示原来的两个部分 */} {/* 当前步骤信息 */} + {!showOutputTable && ( - + @@ -327,6 +345,7 @@ const ProductionProcessStepExecution: React.FC {t("Equipment")}: {equipmentName} + - + )} {/* ========== 产出输入表单 ========== */} + {showOutputTable && ( - - - {t("Production Output Data Entry")} - - - + - {showOutputTable && ( @@ -526,8 +542,8 @@ const ProductionProcessStepExecution: React.FC - )} - + + )} )} diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index f38e4c2..aa72170 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -16,7 +16,6 @@ "Add Record": "新增", "Clean Record": "重置", "Dashboard": "資訊展示面板", - "Store Management": "原材料", "Stock Take Management": "盤點管理", "Store Management": "倉庫管理", "Delivery": "送貨訂單", @@ -27,7 +26,7 @@ "User Group": "用戶群組", "Items": "物料", "Demand Forecast Setting": "需求預測設定", - "Equipment Type": "設備類型", + "Equipment Type/Code": "使用設備-編號", "Equipment": "設備", "Warehouse": "倉庫", "Supplier": "供應商", @@ -107,6 +106,93 @@ "Row per page": "每頁行數", "No data available": "沒有資料", "jodetail": "工單細節", - "Sign out": "登出" + "Sign out": "登出", + + + "By-product": "副產品", + "Complete Step": "完成步驟", + "Defect": "缺陷", + "Output from Process": "流程輸出", + "Quantity": "數量", + "Scrap": "廢料", + "Unit": "單位", + "Back to List": "返回列表", + "Production Output Data Entry": "生產輸出數據輸入", + "Step": "步驟", + "Quality Check": "品質檢查", + "Action": "操作", + "Changeover Time (mins)": "生產後轉換時間(分鐘)", + "Completed": "完成", + "completed": "完成", + "Date": "日期", + "Failed to submit scan data. Please try again.": "掃碼數據提交失敗. 請重試.", + "In Progress": "進行中", + "Is Dark": "是否黑暗", + "Is Dense": "是否密集", + "Is Float": "是否浮動", + "Job Order Code": "工單編號", + "Operator": "操作員", + "Output Qty": "輸出數量", + "Pending": "待處理", + "pending": "待處理", + + "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", + "Please scan operator code": "請掃描操作員編號", + "Please scan operator code first": "請先掃描操作員編號", + "Processing Time (mins)": "步驟時間(分鐘)", + "Production Process Information": "生產流程信息", + "Production Process Steps": "生產流程步驟", + "Scan Operator & Equipment": "掃描操作員和設備", + "Seq": "序號", + "Setup Time (mins)": "生產前預備時間(分鐘)", + "Start": "開始", + "Start QR Scan": "開始掃碼", + "Status": "狀態", + "in_progress": "進行中", + "In_Progress": "進行中", + "inProgress": "進行中", + + "Step Name": "步驟名稱", + "Stop QR Scan": "停止掃碼", + "Submit & Start": "提交並開始", + "Total Steps": "總步驟數", + "Unknown": "", + "Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.", + "View": "查看", + "Back": "返回", + "BoM Material": "物料清單", + "Is Dark | Dense | Float": "是否黑暗 | 密集 | 浮動", + "Item Code": "物料編號", + "Item Name": "物料名稱", + "Job Order Info": "工單信息", + "Matching Stock": "匹配庫存", + "No data found": "沒有找到資料", + "Production Priority": "生產優先級", + "Production Process": "工藝流程", + "Production Process Line Remark": "工藝明細", + "Remark": "明細", + "Req. Qty": "需求數量", + "Seq No": "序號", + "Seq No Remark": "序號明細", + "Stock Available": "庫存可用", + "Stock Status": "庫存狀態", + "Target Production Date": "目標生產日期", + "id": "ID", + "Finished lines": "完成行", + "Invalid Stock In Line Id": "無效庫存行ID", + "Production date": "生產日期", + "Required Qty": "需求數量", + "Total processes": "總流程數", + "View Details": "查看詳情", + "view stockin": "查看入庫", + "Completed Step": "完成步驟", + "Continue": "繼續", + "Executing": "執行中", + "Order Complete": "訂單完成", + "Pause": "暫停", + "Production Output Data": "生產輸出數據", + "Step Information": "步驟信息", + "Stop": "停止", + "Putaway Detail": "上架詳情" }