From 72c8f5bafd541915cea306abef68bacddab69c5a Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Thu, 6 Aug 2026 18:11:30 +0800 Subject: [PATCH 1/4] =?UTF-8?q?html=20=E7=94=B1=E5=9B=BA=E5=AE=9A=20height?= =?UTF-8?q?:=20100%=20=E6=94=B9=E7=82=BA=20min-height:=20100%=EF=BC=8F100d?= =?UTF-8?q?vh=EF=BC=8C=E9=81=BF=E5=85=8D=E9=95=B7=E9=A0=81=E5=BA=95?= =?UTF-8?q?=E9=83=A8=E9=9C=B2=E5=87=BA=E7=99=BD=E9=82=8A=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/global.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/global.css b/src/app/global.css index 61f2c223..8ab7c6a9 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -2,6 +2,8 @@ @tailwind components; @tailwind utilities; +/* FP-MTMS Version Checklist | Functions Ref. No. 57 | v1.0.0 | 2026-08-06 */ + /* UI standard: light default, primary #3b82f6, accent #10b981 */ @layer base { :root { @@ -41,11 +43,14 @@ html:has([data-po-workbench-layout]) body { /* Tablet/mobile: stable layout when virtual keyboard opens */ html { - /* Prefer dynamic viewport height so layout can adapt to keyboard (if browser resizes) */ - height: 100%; + /* min-height (not height) so the document can grow with content; + fixed height: 100% locked the box to the viewport and exposed white below long pages. */ + min-height: 100%; + min-height: 100dvh; /* Base font size: slightly larger for readability */ font-size: 16px; } + @media (min-width: 640px) { html { font-size: 17px; From 6d0531a412dc62d7cedd9d1b9418789e7e1c057f Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Thu, 6 Aug 2026 22:56:15 +0800 Subject: [PATCH 2/4] =?UTF-8?q?List=20=E5=8D=A1=E7=89=87=E7=9A=84=E3=80=8C?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=B7=A5=E5=96=AE=E3=80=8D=E7=8F=BE=E5=9C=A8?= =?UTF-8?q?=E5=8F=AA=E6=9C=89=20ADMIN=20=E6=89=8D=E6=9C=83=E9=A1=AF?= =?UTF-8?q?=E7=A4=BA=EF=BC=8C=E9=9D=9E=20ADMIN=20=E5=AE=8C=E5=85=A8?= =?UTF-8?q?=E7=9C=8B=E4=B8=8D=E5=88=B0=E6=8C=89=E9=88=95=E3=80=82=20?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=B7=A5=E5=96=AE=EF=BC=8F=E5=AE=8C=E6=88=90?= =?UTF-8?q?=20API=20=E6=94=B9=E7=82=BA=E5=83=85=20ADMIN=EF=BC=9B=E3=80=8C?= =?UTF-8?q?=E5=B7=A5=E5=96=AE=E7=94=9F=E7=94=A2=E6=B5=81=E7=A8=8B=E3=80=8D?= =?UTF-8?q?tab=20=E5=83=85=20ADMIN=20=E5=8F=AF=E8=A6=8B=EF=BC=9B=E3=80=8C?= =?UTF-8?q?=E5=B7=B2=E6=8F=90=E6=96=99=E3=80=8D=E6=94=B9=E7=82=BA=E5=85=A8?= =?UTF-8?q?=E9=83=A8=20POL=20completed=EF=BC=9B=E6=96=87=E6=A1=88=E3=80=8C?= =?UTF-8?q?=E6=9C=AA=E6=8F=90=E6=96=99=E3=80=8D=E2=86=92=E3=80=8C=E6=9C=AA?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=8F=90=E6=96=99=E3=80=8D=EF=BC=8FPick=20in?= =?UTF-8?q?complete=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/jo/actions.ts | 2 +- src/authorities.ts | 4 +- .../ProductionProcess/JobOrderOpsTable.tsx | 7 +-- .../ProductionProcessJobOrderDetail.tsx | 2 +- .../ProductionProcessList.tsx | 23 ++++---- .../ProductionProcessPage.tsx | 57 ++++++++++++------- src/i18n/en/productionProcess.json | 10 ++-- src/i18n/zh/productionProcess.json | 10 ++-- 8 files changed, 65 insertions(+), 50 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 477bf4cc..5f380266 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -905,7 +905,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async (type?: string | n ); }); -/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.5 | 2026-08-06 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ export const fetchJoborderProductProcessesPage = cache(async (params: { /** Job order / process date(YYYY-MM-DD) */ date?: string | null; diff --git a/src/authorities.ts b/src/authorities.ts index 08e222ed..0ce7f019 100644 --- a/src/authorities.ts +++ b/src/authorities.ts @@ -20,7 +20,7 @@ export const AUTH = { JOB_MAT: "JOB_MAT", JOB_PROD: "JOB_PROD", /** - * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.0 | 2026-08-05 + * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06 * 工單 生產流程 完成工單 */ PRODUCT_PROCESS: "PRODUCT_PROCESS", @@ -28,7 +28,7 @@ export const AUTH = { } as const; /** - * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.0 | 2026-08-05 + * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06 * Match session ability codes (exact, trimmed). */ export function hasAbility( diff --git a/src/components/ProductionProcess/JobOrderOpsTable.tsx b/src/components/ProductionProcess/JobOrderOpsTable.tsx index 2dc17177..90a555a9 100644 --- a/src/components/ProductionProcess/JobOrderOpsTable.tsx +++ b/src/components/ProductionProcess/JobOrderOpsTable.tsx @@ -223,7 +223,7 @@ interface JobOrderOpsTableProps { printerCombo?: PrinterCombo[]; } -/** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.4 | 2026-08-06 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.5 | 2026-08-06 */ const JobOrderOpsTable: React.FC = ({ onSelectProcess, printerCombo = [], @@ -232,10 +232,9 @@ const JobOrderOpsTable: React.FC = ({ const { data: session } = useSession() as { data: SessionWithTokens | null }; const sessionToken = session as SessionWithTokens | null; const abilities = session?.abilities ?? session?.user?.abilities ?? []; - /** 取消工單:僅 ADMIN */ + /** 取消工單 / 完成工單:僅 ADMIN */ const canCancel = hasAbility(abilities, AUTH.ADMIN); - /** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單) */ - const canComplete = hasAbility(abilities, AUTH.PRODUCT_PROCESS); + const canComplete = hasAbility(abilities, AUTH.ADMIN); const labelPrinterCombo = useMemo( () => (printerCombo || []).filter((p) => p.type === "Label"), diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index c735421b..dbb73b76 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -53,7 +53,7 @@ interface ProductProcessJobOrderDetailProps { initialTabIndex?: number; } -/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.5 | 2026-08-06 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ const ProductionProcessJobOrderDetail: React.FC = ({ jobOrderId, onBack, diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index eeb5ff79..81593055 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -178,7 +178,7 @@ function isWaitingQcPutAway( return s !== "completed" && s !== "rejected"; } -/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.5 | 2026-08-06 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo, @@ -209,8 +209,8 @@ const ProductProcessList: React.FC = ({ const [modalInfo, setModalInfo] = useState(); const currentUserId = session?.id ? parseInt(session.id) : undefined; const abilities = session?.abilities ?? session?.user?.abilities ?? []; - /** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單);不含 ADMIN 以免群組 ADMIN 繞過勾選 */ - const canManageUpdateJo = hasAbility(abilities, AUTH.PRODUCT_PROCESS); + /** 完成工單:僅 ADMIN */ + const canManageUpdateJo = hasAbility(abilities, AUTH.ADMIN); type ProcessFilter = "all" | "drink" | "Powder_Mixture" | "other"; const listTab = normalizeListTab(listPersistedState.pickBucket); @@ -1230,20 +1230,17 @@ const ProductProcessList: React.FC = ({ {t("Matching Stock")} - {statusLower !== "completed" && ( + {canManageUpdateJo && statusLower !== "completed" && ( + { + + + {t("Drink detail mode label")} + + { + if (value != null) setViewMode(value); + }} + > + + {t("Drink detail mode: actual")} + + + {t("Drink detail mode: planned")} + + + + {loading ? ( @@ -181,14 +345,18 @@ const DrinkProductionQtyDashboard: React.FC = () => { {t("Unit")} - + - {t("Stock Req. Qty")} + {isPlanned + ? t("Planned Output Qty") + : t("Stock Req. Qty")} - {t("Production Qty")} + {isPlanned + ? t("Actual Output Qty") + : t("Production Qty")} @@ -208,15 +376,18 @@ const DrinkProductionQtyDashboard: React.FC = () => { ) : ( data.map((row, idx) => { const rowKey = getRowKey(row, idx); - const jobOrders = row.jobOrders ?? []; + const allJobOrders = row.jobOrders ?? []; + const jobOrders = filterJobOrders(allJobOrders); const isExpanded = expandedRowKeys.has(rowKey); - const hasJobOrders = jobOrders.length > 0; + const hasExpandable = + allJobOrders.length > 0 && + (joStatusFilter === "" || jobOrders.length > 0); return ( - + - {hasJobOrders ? ( + {hasExpandable ? ( { - {hasJobOrders && ( + {hasExpandable && ( { unmountOnExit > - - - - - {t("Job Order Code")} - - - {t("Production Date")} - - - {t("Stock Req. Qty")} - - - {t("Production Qty")} - - - - - {jobOrders.map((jo) => ( - - - {jo.jobOrderId > 0 ? ( - - {jo.jobOrderCode || - `JO-${jo.jobOrderId}`} - - ) : ( - jo.jobOrderCode || "-" - )} + {jobOrders.length === 0 ? ( + + {t("No matching job orders for status")} + + ) : ( +
+ + + + {t("Job Order Code")} - - {formatProductionDate( - jo.productionDate, - )} - - - {formatQty(jo.reqQty)} + + {t("Job Order Status")} - - {formatQty(jo.productionQty)} + + {t("Production Date")} + {isPlanned ? ( + <> + + {t("Assume Time Need")} + + + {t("Latest Start By")} + + + {t("Start Time")} + + + {t("Assume End Time")} + + + {t("Actual End Time")} + + + {t("Planned Output Qty")} + + + {t("Actual Output Qty")} + + + ) : ( + <> + + {t("Start Time")} + + + {t("Actual End Time")} + + + {t("Stock Req. Qty")} + + + {t("Production Qty")} + + + )} - ))} - -
+ + + {jobOrders.map((jo) => ( + + + {jo.jobOrderId > 0 ? ( + + {jo.jobOrderCode || + `JO-${jo.jobOrderId}`} + + ) : ( + jo.jobOrderCode || "-" + )} + + + {renderJoStatusChip( + jo.jobOrderStatus, + )} + + + {formatProductionDate( + jo.productionDate, + )} + + {isPlanned ? ( + <> + + + {jo.assumeTimeNeedMins ?? 0}{" "} + {t("minutes")} + + + + + + + + + + + + + {renderActualEnd(jo)} + + + {formatQty(jo.reqQty)} + + + {formatQty(jo.productionQty)} + + + ) : ( + <> + + + + + {renderActualEnd(jo)} + + + {formatQty(jo.reqQty)} + + + {formatQty(jo.productionQty)} + + + )} + + ))} + + + )}
diff --git a/src/components/ProductionProcess/exportDrinkProductionQtyXlsx.ts b/src/components/ProductionProcess/exportDrinkProductionQtyXlsx.ts new file mode 100644 index 00000000..8584a884 --- /dev/null +++ b/src/components/ProductionProcess/exportDrinkProductionQtyXlsx.ts @@ -0,0 +1,141 @@ +import dayjs from "dayjs"; +import type { TFunction } from "i18next"; +import { exportMultiSheetToXlsx } from "@/app/(main)/chart/_components/exportChartToXlsx"; +import type { + DrinkProductionQtyJobOrderDetail, + DrinkProductionQtyResponse, +} from "@/app/api/jo/actions"; + +type DrinkViewMode = "actual" | "planned"; + +const formatDateTime = (value: unknown): string => { + if (value == null || value === "") return ""; + if (Array.isArray(value)) { + const [y, m, d, h = 0, min = 0] = value as number[]; + if (y == null || m == null || d == null) return ""; + return dayjs(new Date(y, m - 1, d, h, min)).format("YYYY-MM-DD HH:mm"); + } + const parsed = dayjs(value as string); + return parsed.isValid() ? parsed.format("YYYY-MM-DD HH:mm") : String(value); +}; + +const formatDate = (value: string | null | undefined): string => { + if (!value) return ""; + const parsed = dayjs(value); + return parsed.isValid() ? parsed.format("YYYY-MM-DD") : value; +}; + +export type ExportDrinkProductionQtyParams = { + data: DrinkProductionQtyResponse[]; + viewMode: DrinkViewMode; + selectedDate: string; + statusFilter: string; + t: TFunction; +}; + +export function exportDrinkProductionQtyXlsx({ + data, + viewMode, + selectedDate, + statusFilter, + t, +}: ExportDrinkProductionQtyParams): void { + const viewLabel = + viewMode === "planned" + ? t("Drink detail mode: planned") + : t("Drink detail mode: actual"); + const statusFilterLabel = statusFilter + ? t(statusFilter, { ns: "jo", defaultValue: statusFilter }) + : t("All"); + const exportedAt = dayjs().format("YYYY-MM-DD HH:mm:ss"); + + const filterJos = ( + jobOrders: DrinkProductionQtyJobOrderDetail[], + ): DrinkProductionQtyJobOrderDetail[] => { + if (!statusFilter) return jobOrders; + return jobOrders.filter((jo) => jo.jobOrderStatus === statusFilter); + }; + + const joRows: Record[] = []; + const stepRows: Record[] = []; + + for (const item of data) { + for (const jo of filterJos(item.jobOrders ?? [])) { + joRows.push({ + [t("Export meta: exported at")]: exportedAt, + [t("Drink detail mode label")]: viewLabel, + [t("Date")]: selectedDate, + [t("Job Order Status") + ` (${t("Filter")})`]: statusFilterLabel, + [t("Item Code")]: item.itemCode ?? "", + [t("Goods Name")]: item.itemName ?? "", + [t("Unit")]: item.uom ?? "", + [t("Job Order Code")]: jo.jobOrderCode ?? "", + [t("Job Order Status")]: jo.jobOrderStatus + ? t(jo.jobOrderStatus, { ns: "jo", defaultValue: jo.jobOrderStatus }) + : "", + [t("Production Date")]: formatDate(jo.productionDate), + [viewMode === "planned" + ? t("Planned Output Qty") + : t("Stock Req. Qty")]: jo.reqQty ?? 0, + [viewMode === "planned" + ? t("Actual Output Qty") + : t("Production Qty")]: jo.productionQty ?? 0, + [t("Assume Time Need")]: jo.assumeTimeNeedMins ?? 0, + [t("Latest Start By")]: formatDateTime(jo.latestStartBy), + [t("Start Time")]: formatDateTime(jo.startTime), + [t("Assume End Time")]: formatDateTime(jo.assumeEndTime), + [t("Actual End Time")]: formatDateTime(jo.actualEndTime), + [t("Process operators")]: jo.processOperators ?? "", + [t("Process handlers")]: jo.processHandlers ?? "", + [t("QC users")]: jo.qcUsers ?? "", + [t("Put away users")]: jo.putAwayUsers ?? "", + }); + + for (const step of jo.processSteps ?? []) { + stepRows.push({ + [t("Export meta: exported at")]: exportedAt, + [t("Drink detail mode label")]: viewLabel, + [t("Date")]: selectedDate, + [t("Item Code")]: step.itemCode ?? item.itemCode ?? "", + [t("Goods Name")]: step.itemName ?? item.itemName ?? "", + [t("Job Order Code")]: step.jobOrderCode ?? jo.jobOrderCode ?? "", + [t("Process seq")]: step.seqNo ?? "", + [t("Process Name")]: step.processName ?? "", + [t("Operator Name & No.")]: step.operatorName ?? "", + [t("Handler")]: step.handlerName ?? "", + [t("Start Time")]: formatDateTime(step.startTime), + [t("End Time")]: formatDateTime(step.endTime), + [t("Status")]: step.status ?? "", + }); + } + } + } + + const itemRows = data.map((item) => ({ + [t("Export meta: exported at")]: exportedAt, + [t("Drink detail mode label")]: viewLabel, + [t("Date")]: selectedDate, + [t("Item Code")]: item.itemCode ?? "", + [t("Goods Name")]: item.itemName ?? "", + [t("Unit")]: item.uom ?? "", + [viewMode === "planned" + ? t("Planned Output Qty") + : t("Stock Req. Qty")]: item.totalReqQty ?? 0, + [viewMode === "planned" + ? t("Actual Output Qty") + : t("Production Qty")]: item.totalQty ?? 0, + [t("JO count")]: filterJos(item.jobOrders ?? []).length, + })); + + const modeSlug = viewMode === "planned" ? "planned" : "actual"; + const filename = `DrinkProductionQty_${modeSlug}_${selectedDate}_${dayjs().format("HHmm")}`; + + exportMultiSheetToXlsx( + [ + { name: t("Excel sheet: JO detail"), rows: joRows }, + { name: t("Excel sheet: process people"), rows: stepRows }, + { name: t("Excel sheet: item summary"), rows: itemRows }, + ], + filename, + ); +} diff --git a/src/i18n/en/itemTracing.json b/src/i18n/en/itemTracing.json index fdb698b4..281b85ac 100644 --- a/src/i18n/en/itemTracing.json +++ b/src/i18n/en/itemTracing.json @@ -286,6 +286,12 @@ "code.joStatus.storing": "Storing", "code.joStatus.PARTIAL": "Partial", "code.joStatus.partial": "Partial", + "code.productionStatus.Pass": "Pass", + "code.productionStatus.Completed": "Completed", + "code.productionStatus.Pending": "Pending", + "code.productionStatus.Paused": "Paused", + "code.productionStatus.InProgress": "In progress", + "code.productionStatus.Skip": "Skip", "continuousScanBlocked": "Finish current scan first", "nodeJoOut": "Job order material issue", "nodePoOut": "Purchase pick", @@ -317,5 +323,13 @@ "direction": "Direction", "origins": "Origins", "refCode": "Ref.", - "lastMove": "Last move" + "lastMove": "Last move", + "N/A": "N/A", + "pendingPick": "Pending pick", + "detailBomReqQty": "BOM req. qty", + "detailStockReqQty": "Stock req. qty", + "detailStockAvailable": "Stock available", + "detailStockStatus": "Stock status", + "stockStatusSufficient": "Sufficient", + "stockStatusInsufficient": "Insufficient" } diff --git a/src/i18n/en/productionProcess.json b/src/i18n/en/productionProcess.json index 4668f623..af69d1f0 100644 --- a/src/i18n/en/productionProcess.json +++ b/src/i18n/en/productionProcess.json @@ -95,6 +95,27 @@ "Drink Production Qty Dashboard": "Drink Production Qty Dashboard", "Expand job order details": "Expand job order details", "Collapse job order details": "Collapse job order details", + "Drink detail mode label": "Detail display", + "Drink detail mode: actual": "Actual production", + "Drink detail mode: planned": "Planned production", + "Planned Output Qty": "Planned output", + "Actual Output Qty": "Actual output", + "Latest Start By": "Latest start by", + "Actual End Time": "Actual End Time", + "Job Order Status": "Job Order Status", + "No matching job orders for status": "No job orders match the selected status", + "Export Excel": "Export Excel", + "Export meta: exported at": "Exported at", + "Process operators": "Process operators", + "Process handlers": "Process handlers", + "QC users": "QC users", + "Put away users": "Put-away users", + "Excel sheet: JO detail": "JO detail", + "Excel sheet: process people": "Process people", + "Excel sheet: item summary": "Item summary", + "JO count": "JO count", + "Process seq": "Process seq", + "Handler": "Handler", "Goods Name": "Goods Name", "Job Type": "Job Type", "Job process detail mode label": "Job process detail mode label", diff --git a/src/i18n/zh/itemTracing.json b/src/i18n/zh/itemTracing.json index 5c3d1ba8..031dee0a 100644 --- a/src/i18n/zh/itemTracing.json +++ b/src/i18n/zh/itemTracing.json @@ -283,9 +283,15 @@ "code.joStatus.packaging": "提料中", "code.joStatus.processing": "生產中", "code.joStatus.pendingQC": "待品檢", - "code.joStatus.storing": "待品檢入倉", + "code.joStatus.storing": "待QC上架", "code.joStatus.PARTIAL": "部分完成", "code.joStatus.partial": "部分完成", + "code.productionStatus.Pass": "通過", + "code.productionStatus.Completed": "完成", + "code.productionStatus.Pending": "待處理", + "code.productionStatus.Paused": "已暫停", + "code.productionStatus.InProgress": "進行中", + "code.productionStatus.Skip": "跳過", "continuousScanBlocked": "請先完成目前掃描", "nodeJoOut": "工單提料", "nodePoOut": "採購提料", @@ -317,5 +323,13 @@ "direction": "方向", "origins": "來源", "refCode": "單號", - "lastMove": "最近異動" + "lastMove": "最近異動", + "N/A": "不適用", + "pendingPick": "待提料", + "detailBomReqQty": "BOM 需求數", + "detailStockReqQty": "庫存需求數", + "detailStockAvailable": "庫存數", + "detailStockStatus": "庫存狀態", + "stockStatusSufficient": "足夠", + "stockStatusInsufficient": "不足" } diff --git a/src/i18n/zh/productionProcess.json b/src/i18n/zh/productionProcess.json index f78826f1..2c339f30 100644 --- a/src/i18n/zh/productionProcess.json +++ b/src/i18n/zh/productionProcess.json @@ -95,6 +95,27 @@ "Drink Production Qty Dashboard": "儀表板 - 飲料生產數量", "Expand job order details": "展開工單明細", "Collapse job order details": "收合工單明細", + "Drink detail mode label": "明細顯示", + "Drink detail mode: actual": "實際生產", + "Drink detail mode: planned": "預計生產", + "Planned Output Qty": "預計生產數量", + "Actual Output Qty": "實際生產數量", + "Latest Start By": "最晚開工時間", + "Actual End Time": "實際完成時間", + "Job Order Status": "工單狀態", + "No matching job orders for status": "無符合狀態的工單", + "Export Excel": "匯出 Excel", + "Export meta: exported at": "匯出時間", + "Process operators": "工序操作員", + "Process handlers": "工序處理人", + "QC users": "QC人員", + "Put away users": "上架人員", + "Excel sheet: JO detail": "JO明細", + "Excel sheet: process people": "工序人員", + "Excel sheet: item summary": "貨品彙總", + "JO count": "工單筆數", + "Process seq": "工序序號", + "Handler": "處理人", "Goods Name": "貨品名稱", "Job Type": "工單類型", "Job process detail mode label": "工序格顯示", From 5bda3eae63dcd7f56ea08d2218d52e4408b88984 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sun, 9 Aug 2026 22:11:19 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E5=8C=85=E8=A3=9D=20Complete/Pass=20?= =?UTF-8?q?=E2=86=92=20=E5=90=8C=20JO=20=E5=85=B6=E4=BB=96=E5=8C=85?= =?UTF-8?q?=E8=A3=9D=20auto=20Pass=20QC=20ready=20=E6=94=B9=E6=88=90?= =?UTF-8?q?=E3=80=8C=E5=85=A8=E9=83=A8=20line=20done=E3=80=8D=EF=BC=88?= =?UTF-8?q?=E6=8B=BF=E6=8E=89=E3=80=8C=E4=BB=BB=E4=B8=80=E5=8C=85=E8=A3=9D?= =?UTF-8?q?=E5=8D=B3=E5=8F=AF=E3=80=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProductionProcessList.tsx | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index 81593055..e8a85f9a 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -178,7 +178,7 @@ function isWaitingQcPutAway( return s !== "completed" && s !== "rejected"; } -/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.8 | 2026-08-09 */ const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo, @@ -447,7 +447,7 @@ const ProductProcessList: React.FC = ({ return productionCache; }, [listTab, productionCache, pendingQcCache, putawayedCache, offPlanRows]); - // QC 的业务判定:同一个 jobOrder 下,所有 productProcess 的所有 lines 都必须是 Completed/Pass + // QC ready: same JO — all lines Completed/Pass (sibling 包裝 auto-Pass on backend) + has SIL const jobOrderQcReadyById = useMemo(() => { const byJobOrder = new Map(); for (const p of tabProcesses) { @@ -465,31 +465,11 @@ const ProductProcessList: React.FC = ({ byJobOrder.forEach((jobOrderProcesses, jobOrderId) => { const hasStockInLine = jobOrderProcesses.some((p) => p.stockInLineId != null); + const allLines = jobOrderProcesses.flatMap((p) => p.lines ?? []); + const allLinesDone = + allLines.length > 0 && allLines.every((l) => isDone(l.status)); - const packingProcesses = jobOrderProcesses.filter( - (p) => String((p as any).code ?? "").trim() === "包裝", - ); - const nonPackingProcesses = jobOrderProcesses.filter( - (p) => String((p as any).code ?? "").trim() !== "包裝", - ); - - const allNonPackingDone = - nonPackingProcesses.length === 0 || - nonPackingProcesses.every((p) => { - const lines = p.lines ?? []; - return lines.length > 0 && lines.every((l) => isDone(l.status)); - }); - - const hasOnePackingDone = - packingProcesses.length > 0 && - packingProcesses.some((p) => { - const lines = p.lines ?? []; - return lines.some((l) => isDone(l.status)); - }); - - const packingOk = packingProcesses.length === 0 ? true : hasOnePackingDone; - - result.set(jobOrderId, hasStockInLine && allNonPackingDone && packingOk); + result.set(jobOrderId, hasStockInLine && allLinesDone); }); return result;