From 1a613039073cbe14845ca0cc4e1e5934f78ab04e Mon Sep 17 00:00:00 2001 From: tommy Date: Mon, 2 Nov 2026 15:51:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=92=E7=94=A2=20Excel=20=E5=9C=A8=E3=80=8C?= =?UTF-8?q?=E7=89=A9=E6=96=99=E7=94=A8=E9=87=8F=E7=B8=BD=E7=B5=90=E3=80=8D?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=E3=80=8C=E9=9C=80=E6=8E=A1=E8=B3=BC/?= =?UTF-8?q?=E7=94=9F=E7=94=A2=E6=95=B8=E9=87=8F=E3=80=8D=EF=BC=9D=E4=B8=8B?= =?UTF-8?q?=E9=80=B1=E7=94=A8=E9=87=8F=20=E2=88=92=20=E5=BA=AB=E5=AD=98?= =?UTF-8?q?=EF=BC=88=E4=B8=8D=E6=89=A3=20PO=EF=BC=8C=E5=8F=AF=E7=82=BA?= =?UTF-8?q?=E8=B2=A0=EF=BC=89=20=E6=AC=84=E5=AF=AC=E5=8A=A0=E5=AF=AC?= =?UTF-8?q?=EF=BC=9B=E4=B8=8D=E6=94=BE=E5=9C=A8=E6=AF=8F=E6=97=A5=E5=88=86?= =?UTF-8?q?=E9=A0=81=20=E5=B7=A5=E5=96=AE=20=E5=88=97=E8=A1=A8=E9=A1=AF?= =?UTF-8?q?=E7=A4=BA=20BOM=20=E5=B7=A5=E5=BA=8F=E6=A8=99=E6=BA=96=E5=B7=A5?= =?UTF-8?q?=E6=99=82=EF=BC=88=E9=A0=90=E4=BC=B0=E5=88=86=E9=90=98=EF=BC=89?= =?UTF-8?q?=20=E3=80=8CBOM=E6=9D=90=E6=96=99=E3=80=8D=E2=86=92=E3=80=8C?= =?UTF-8?q?=E6=9D=90=E6=96=99=E3=80=8D=20=E8=80=97=E6=9D=90=E3=80=8C?= =?UTF-8?q?=E4=B8=8D=E9=81=A9=E7=94=A8=E3=80=8D=E2=86=92=E3=80=8C=E4=B8=8D?= =?UTF-8?q?=E7=94=A8=E6=8F=90=E6=96=99=E3=80=8D=20BOM=20=E5=B7=A5=E8=97=9D?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=20bom.details=EF=BC=88=E5=90=AB=20Liquiba?= =?UTF-8?q?se=EF=BC=89=20=E3=80=8C=E5=B7=A5=E8=97=9D=E6=98=8E=E7=B4=B0?= =?UTF-8?q?=E3=80=8D=E6=94=B9=E7=82=BA=E5=8F=AF=E5=84=B2=E5=AD=98=E7=9A=84?= =?UTF-8?q?=E8=87=AA=E7=94=B1=E6=96=87=E5=AD=97=EF=BC=9B=E3=80=8C=E5=B7=A5?= =?UTF-8?q?=E8=97=9D=E6=B5=81=E7=A8=8B=E3=80=8D=E4=BB=8D=E7=94=A8=E5=90=8D?= =?UTF-8?q?=E7=A8=B1=20=E7=9C=8B=E6=9D=BF=20/=20=E5=88=86=E9=A0=81=20?= =?UTF-8?q?=E5=B7=A5=E5=96=AE=E7=8B=80=E6=85=8B=E7=9C=8B=E6=9D=BF=E7=B6=93?= =?UTF-8?q?=E6=89=8B=E4=BA=BA=EF=BC=9A=E7=84=A1=20handler=20=E6=99=82?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E9=A1=AF=E7=A4=BA=20operator=20=E9=9A=B1?= =?UTF-8?q?=E8=97=8F=E9=A3=B2=E6=96=99=E7=94=9F=E7=94=A2=E6=95=B8=E9=87=8F?= =?UTF-8?q?=E5=88=86=E9=A0=81=20=E6=8E=92=E7=94=A2=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E6=96=87=E6=A1=88=20=E3=80=8C=E4=B8=80=E8=88=AC=E6=97=A5?= =?UTF-8?q?=E3=80=8D=E2=86=92=E3=80=8C=E4=B8=80=E8=88=AC=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E3=80=8D=20=E3=80=8C=E5=A4=9A=E5=82=99=E6=97=A5=E3=80=8D?= =?UTF-8?q?=E7=AD=89=E3=80=8C=E5=A4=9A=E5=82=99=E3=80=8D=E7=9B=B8=E9=97=9C?= =?UTF-8?q?=20=E2=86=92=E3=80=8C=E7=89=B9=E5=88=A5=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E3=80=8D=20=E3=80=8C=E5=A4=9A=E5=82=99=E6=98=9F=E6=9C=9F?= =?UTF-8?q?=E3=80=8D=E2=86=92=E3=80=8C=E7=89=B9=E5=88=A5=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E9=81=A9=E7=94=A8=E6=98=9F=E6=9C=9F=E3=80=8D=EF=BC=88=E5=89=8D?= =?UTF-8?q?=E5=BE=8C=E7=AB=AF=E5=90=8C=E6=AD=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/bom/client.ts | 12 ++ src/app/api/jo/index.ts | 2 + src/components/JoSearch/JoSearch.tsx | 16 ++ .../JoWorkbench/JoWorkbenchSearch.tsx | 16 ++ .../ProductionProcessJobOrderDetail.tsx | 142 +++++++++--------- .../ProductionProcessPage.tsx | 8 +- src/i18n/en/jo.json | 8 +- src/i18n/en/productionProcess.json | 4 + src/i18n/en/scheduling.json | 12 +- src/i18n/zh/jo.json | 4 + src/i18n/zh/productionProcess.json | 4 + src/i18n/zh/scheduling.json | 12 +- 12 files changed, 144 insertions(+), 96 deletions(-) diff --git a/src/app/api/bom/client.ts b/src/app/api/bom/client.ts index 8d1db363..05c7075c 100644 --- a/src/app/api/bom/client.ts +++ b/src/app/api/bom/client.ts @@ -107,6 +107,18 @@ export async function fetchBomComboClient(options?: { ); return response.data; } + + /** Update free-text craft details (工藝明細) on BOM. */ + export async function updateBomDetailsClient( + id: number, + details: string | null, + ): Promise<{ id: number; details: string | null }> { + const response = await axiosInstance.put<{ id: number; details: string | null }>( + `${NEXT_PUBLIC_API_URL}/bom/${id}/details`, + { details }, + ); + return response.data; + } export type BomExcelCheckProgress = { batchId: string; totalFiles: number; diff --git a/src/app/api/jo/index.ts b/src/app/api/jo/index.ts index 67bd46e7..73e845d3 100644 --- a/src/app/api/jo/index.ts +++ b/src/app/api/jo/index.ts @@ -39,6 +39,8 @@ export interface JobOrder { stockInLineStatus?: string; silHandlerId?: number; lotNo?: string; + /** Standard BOM process minutes (prep + duration + post). */ + estimatedMinutes?: number | null; } export interface Machine { diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index c7ed6376..e8d271ac 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -459,6 +459,22 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT return row.item?.uom ? t(row.item.uom.udfudesc) : '-' } }, + { + name: "estimatedMinutes" as keyof JobOrder, + label: t("Estimated Process Time"), + align: "left", + headerAlign: "left", + renderCell: (row) => { + const mins = row.estimatedMinutes; + if (mins == null || mins <= 0) return "—"; + const hours = Math.floor(mins / 60); + const minutes = mins % 60; + const parts: string[] = []; + if (hours > 0) parts.push(t("Duration hours", { count: hours })); + if (minutes > 0 || hours === 0) parts.push(t("Duration minutes", { count: minutes })); + return parts.join(" "); + } + }, { name: "stockStatus" as keyof JobOrder, label: t("BOM Status"), diff --git a/src/components/JoWorkbench/JoWorkbenchSearch.tsx b/src/components/JoWorkbench/JoWorkbenchSearch.tsx index def91204..ca13ab8e 100644 --- a/src/components/JoWorkbench/JoWorkbenchSearch.tsx +++ b/src/components/JoWorkbench/JoWorkbenchSearch.tsx @@ -460,6 +460,22 @@ const JoWorkbenchSearch: React.FC = ({ defaultInputs, bomCombo, printerCo return row.item?.uom ? t(row.item.uom.udfudesc) : '-' } }, + { + name: "estimatedMinutes" as keyof JobOrder, + label: t("Estimated Process Time"), + align: "left", + headerAlign: "left", + renderCell: (row) => { + const mins = row.estimatedMinutes; + if (mins == null || mins <= 0) return "—"; + const hours = Math.floor(mins / 60); + const minutes = mins % 60; + const parts: string[] = []; + if (hours > 0) parts.push(t("Duration hours", { count: hours })); + if (minutes > 0 || hours === 0) parts.push(t("Duration minutes", { count: minutes })); + return parts.join(" "); + } + }, { name: "stockStatus" as keyof JobOrder, label: t("BOM Status"), diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index 78818a53..bd773ae6 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -27,6 +27,7 @@ import { fetchProductProcessesByJobOrderId , setJobOrderHidden, updateProductPro import ProductionProcessDetail from "./ProductionProcessDetail"; import { BomCombo } from "@/app/api/bom"; import { fetchBomCombo } from "@/app/api/bom/index"; +import { updateBomDetailsClient } from "@/app/api/bom/client"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; @@ -44,6 +45,7 @@ import EditIcon from "@mui/icons-material/Edit"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { dayjsToDateString } from "@/app/utils/formatUtil"; +import { msg } from "../Swal/CustomAlerts"; @@ -77,6 +79,8 @@ const [reqQtyMultiplier, setReqQtyMultiplier] = useState(1); const [selectedBomForReqQty, setSelectedBomForReqQty] = useState(null); const [bomCombo, setBomCombo] = useState([]); const [showBaseQty, setShowBaseQty] = useState(false); +const [bomDetailsText, setBomDetailsText] = useState(""); +const [savingBomDetails, setSavingBomDetails] = useState(false); const fetchData = useCallback(async () => { setLoading(true); @@ -86,6 +90,7 @@ const [showBaseQty, setShowBaseQty] = useState(false); const firstProcess = data[0]; setProcessData(firstProcess); setJobOrderLines((firstProcess as any).jobOrderLines || []); + setBomDetailsText((firstProcess as any).bomDetails ?? ""); } } catch (error) { console.error("Error loading data:", error); @@ -93,6 +98,27 @@ const [showBaseQty, setShowBaseQty] = useState(false); setLoading(false); } }, [jobOrderId]); + + const handleSaveBomDetails = useCallback(async () => { + const bomId = processData?.bomId as number | undefined; + if (!bomId) { + msg(t("update failed")); + return; + } + setSavingBomDetails(true); + try { + const result = await updateBomDetailsClient(bomId, bomDetailsText); + setProcessData((prev: any) => + prev ? { ...prev, bomDetails: result.details ?? "" } : prev, + ); + msg(t("update success")); + } catch (e) { + console.error(e); + msg(t("update failed")); + } finally { + setSavingBomDetails(false); + } + }, [bomDetailsText, processData?.bomId, t]); const toggleBaseQty = useCallback(() => { setShowBaseQty(prev => !prev); }, []); @@ -456,52 +482,6 @@ const handleRelease = useCallback(async (jobOrderId: number) => { ); - const productionProcessesLineRemarkTableColumns: GridColDef[] = [ - { - field: "seqNo", - headerName: t("SEQ"), - flex: 0.2, - align: "left", - headerAlign: "left", - type: "number", - renderCell: (params) => { - return {params.value}; - }, - }, - { - field: "description", - headerName: t("Remark"), - flex: 1, - align: "left", - headerAlign: "left", - renderCell: (params) => { - return( - -   - {params.value || ""} - -   - - ) - - }, - }, - - ]; - const productionProcessesLineRemarkTableRows = - processData?.productProcessLines?.map((line: any) => ({ - id: line.seqNo, - seqNo: line.seqNo, - - description: line.description ?? "", - - - - })) ?? []; - - - - const pickTableColumns: GridColDef[] = [ { field: "id", @@ -649,7 +629,7 @@ const handleRelease = useCallback(async (jobOrderId: number) => { return ( - {t("N/A")} + {t("No pick required")} ); @@ -687,7 +667,7 @@ const handleRelease = useCallback(async (jobOrderId: number) => { if (isExcludedFromPickStock(params.row.type)) { return ( - {t("N/A")} + {t("No pick required")} ); } @@ -764,32 +744,44 @@ const handleRelease = useCallback(async (jobOrderId: number) => { const ProductionProcessesLineRemarkTableContent = () => ( - 'auto'} - hideFooter={false} // ✅ Ensure footer is visible - /> + + + + setBomDetailsText(e.target.value)} + placeholder={t("Production Process Line Remark")} + InputLabelProps={{ shrink: false }} + sx={{ + "& .MuiInputBase-input::placeholder": { + color: "text.secondary", + opacity: 1, + }, + "& .MuiInputBase-input": { + color: "text.primary", + }, + }} + /> + + + + + + ); @@ -807,7 +799,7 @@ const handleRelease = useCallback(async (jobOrderId: number) => { - + diff --git a/src/components/ProductionProcess/ProductionProcessPage.tsx b/src/components/ProductionProcess/ProductionProcessPage.tsx index 19e6d2fe..5b0b0a0b 100644 --- a/src/components/ProductionProcess/ProductionProcessPage.tsx +++ b/src/components/ProductionProcess/ProductionProcessPage.tsx @@ -13,7 +13,6 @@ import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionse import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus"; import OperatorKpiDashboard from "@/components/ProductionProcess/OperatorKpiDashboard"; import EquipmentStatusDashboard from "@/components/ProductionProcess/EquipmentStatusDashboard"; -import DrinkProductionQtyDashboard from "@/components/ProductionProcess/DrinkProductionQtyDashboard"; import JobOrderOpsTable from "@/components/ProductionProcess/JobOrderOpsTable"; import type { PrinterCombo } from "@/app/api/settings/printer"; import { useTranslation } from "react-i18next"; @@ -30,8 +29,7 @@ type PageTab = | "ops" | "jobProcessStatus" | "operatorKpi" - | "equipmentStatus" - | "drinkQty"; + | "equipmentStatus"; /** * FP-MTMS Version Checklist | Functions Ref. No. 26 | v1.0.0 | 2026-07-20 @@ -169,7 +167,6 @@ const ProductionProcessPage: React.FC = ({ printerCo - {pageTab === "list" && ( @@ -211,9 +208,6 @@ const ProductionProcessPage: React.FC = ({ printerCo {pageTab === "equipmentStatus" && ( )} - {pageTab === "drinkQty" && ( - - )}