From c5a1a984170e36451065ef045ee309471c2a75e8 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 5 Aug 2026 14:48:58 +0800 Subject: [PATCH 1/5] =?UTF-8?q?ssueTypes=20=E6=94=B9=E5=90=8D=EF=BC=9A?= =?UTF-8?q?=E8=B7=B3=E9=81=8E=E6=8F=90=E6=96=99=EF=BC=88=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=80=8C=E5=B7=B2=E5=AE=8C=E6=88=90=E3=80=8D=EF=BC=89=EF=BC=8F?= =?UTF-8?q?=E5=A4=9A=E6=8F=90=EF=BC=8F=E5=B0=91=E6=8F=90=EF=BC=8F=E6=93=85?= =?UTF-8?q?=E8=87=AA=E6=8F=9B=E6=89=B9/=E6=8F=80=E9=8C=AF=E6=89=B9?= =?UTF-8?q?=EF=BC=9BExcel=E3=80=8C=E5=95=8F=E9=A1=8C=E9=A1=9E=E5=9E=8B?= =?UTF-8?q?=E3=80=8D=E2=86=92=E3=80=8C=E5=81=8F=E9=9B=A2=E9=A1=9E=E5=9E=8B?= =?UTF-8?q?=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 釐清 inventoryLotLineId=實際批,對齊 actualPickLotLineId(無行為變更) tab 數量 0 不顯示 (0);Badge→文字 (n);欄位「工單編號/貨品編號」;中文 i18n(工單流程、預計生產日期、含未按規劃完成…) --- .../ProductionProcess/JobOrderOpsTable.tsx | 67 ++++++++++++++++--- .../ProductionProcessList.tsx | 42 +----------- src/i18n/zh/productionProcess.json | 10 +-- 3 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/components/ProductionProcess/JobOrderOpsTable.tsx b/src/components/ProductionProcess/JobOrderOpsTable.tsx index bb27757..f472352 100644 --- a/src/components/ProductionProcess/JobOrderOpsTable.tsx +++ b/src/components/ProductionProcess/JobOrderOpsTable.tsx @@ -167,6 +167,7 @@ interface JobOrderOpsTableProps { printerCombo?: PrinterCombo[]; } +/** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.1 | 2026-08-05 */ const JobOrderOpsTable: React.FC = ({ onSelectProcess, printerCombo = [], @@ -517,17 +518,49 @@ const JobOrderOpsTable: React.FC = ({ scrollButtons="auto" sx={{ mb: 1, borderBottom: 1, borderColor: "divider" }} > - - - + 0 ? `${t("All")} (${counts.all})` : t("All")} + /> + 0 + ? `${t("pending")} (${counts.pending})` + : t("pending") + } + /> + 0 + ? `${t("Processing")} (${counts.processing})` + : t("Processing") + } + /> 0 + ? `${t("Waiting QC Put Away")} (${counts.pendingQc})` + : t("Waiting QC Put Away") + } + /> + 0 + ? `${t("Put Awayed")} (${counts.putawayed})` + : t("Put Awayed") + } /> - 0 + ? `${t("Issue")} (${counts.stop + counts.cancel})` + : t("Issue") + } /> @@ -561,8 +594,22 @@ const JobOrderOpsTable: React.FC = ({ onChange={(_, v: IssueSubTab) => setIssueSub(v)} sx={{ mb: 2 }} > - - + 0 + ? `${t("Stop (paused)")} (${counts.stop})` + : t("Stop (paused)") + } + /> + 0 + ? `${t("Cancelled")} (${counts.cancel})` + : t("Cancelled") + } + /> )} @@ -576,8 +623,8 @@ const JobOrderOpsTable: React.FC = ({ - {t("Job Order")} - {t("Item")} + {t("Job Order Code")} + {t("Item Code")} {t("Required Qty")} {t("Production Date")} {t("Status")} diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index 7ec8e83..a5b6955 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -22,7 +22,6 @@ import { DialogActions, Tabs, Tab, - Badge, Tooltip, IconButton, Avatar, @@ -145,7 +144,7 @@ function normalizeListTab(raw: string | undefined | null): ProductionProcessList return "needs_action"; } -/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.0 | 2026-08-03 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.2 | 2026-08-05 */ const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo, @@ -578,38 +577,8 @@ const ProductProcessList: React.FC = ({ }, [loading, filteredProcesses, totalPages]); // eslint-disable-line react-hooks/exhaustive-deps const renderBucketTabLabel = useCallback( - (labelKey: string, count: number) => ( - 0 ? t(labelKey) + `: ${count}` : t(labelKey)}> - - 99 ? "99+" : count} - invisible={count === 0} - sx={{ - "& .MuiBadge-badge": { - fontWeight: 800, - fontSize: "0.7rem", - minWidth: 18, - height: 18, - lineHeight: "18px", - px: 0.5, - right: -8, - top: 2, - }, - }} - > - 0 ? 1 : 0 }} - > - {t(labelKey)} - - - - - ), + (labelKey: string, count: number) => + count > 0 ? `${t(labelKey)} (${count})` : t(labelKey), [t], ); const handleUpdateJo = useCallback(async (process: AllJoborderProductProcessInfoResponse) => { @@ -822,17 +791,14 @@ const ProductProcessList: React.FC = ({ 0 ? 4 : 2 }} /> 0 ? 4 : 2 }} /> 0 ? 4 : 2 }} /> = ({ "Waiting QC Put Away", pendingQcCount, )} - sx={{ pr: pendingQcCount > 0 ? 4 : 2 }} /> 0 ? 4 : 2 }} /> diff --git a/src/i18n/zh/productionProcess.json b/src/i18n/zh/productionProcess.json index 3f378f6..81f6749 100644 --- a/src/i18n/zh/productionProcess.json +++ b/src/i18n/zh/productionProcess.json @@ -77,7 +77,7 @@ "Job Order No.": "工單編號", "Job Order and Product": "工單及貨品", "Issue": "異常", - "Job Order Ops Table": "查看工單流程情況", + "Job Order Ops Table": "工單流程", "Pending (picked)": "已提料", "Pending (not picked)": "未提料", "Processing (picked)": "已提料", @@ -148,7 +148,7 @@ "Processing Time": "生產時間", "Processing Time (mins)": "步驟時間(分鐘)", "Product process status": "生產流程狀態", - "Production Date": "生產日期", + "Production Date": "預計生產日期", "Production Equipment Status Dashboard": "儀表板 - 生產設備最新狀態", "Production Output Data": "生產輸出數據", "Production Output Data Entry": "生產輸出數據輸入", @@ -158,7 +158,7 @@ "Production Process Line Remark": "工藝明細", "Production Process Steps": "生產流程步驟", "Production Time Remaining": "生產剩餘時間", - "Production date": "生產日期", + "Production date": "預計生產日期", "Put Awayed Job Orders": "已上架工單", "Qty": "數量", "Quality Check": "品質檢查", @@ -212,8 +212,8 @@ "Total Time": "總時間", "Total finished QC job orders": "總完成QC工單數量", "Total job orders": "總工單數量", - "Including carried over": "含過去轉來", - "Carried over from past day": "過去轉來的工單", + "Including carried over": "含未按規劃完成工單", + "Carried over from past day": "未按規劃完成工單的工單", "All unfinished": "需處理", "Needs action": "需處理", "Not picked · Not started": "未提料 · 未開工", From cecd7cbc329ac13a463205e515ff3fff2f413d08 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 5 Aug 2026 17:16:07 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=AC=8A=E9=99=90=20+=20=E6=9C=AA=E6=8C=89?= =?UTF-8?q?=E8=A6=8F=E5=8A=83=E5=AE=8C=E6=88=90=20tab=20+=20lookback=20?= =?UTF-8?q?=E5=B0=8D=E9=BD=8A=20+=20=E5=A0=B1=E8=A1=A8=E6=AA=94=E5=90=8D?= 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 | 19 +- .../JobOrderFgStockInNavAlerts.tsx | 11 +- .../ProductionProcess/JobOrderOpsTable.tsx | 409 ++++++++++++------ .../ProductionProcessDetail.tsx | 13 +- .../ProductionProcessJobOrderDetail.tsx | 2 +- .../ProductionProcessList.tsx | 246 +++++++++-- .../ProductionProcessPage.tsx | 5 +- src/hooks/useJobOrderFgStockInAlerts.ts | 96 ++-- src/i18n/en/productionProcess.json | 1 + src/i18n/zh/productionProcess.json | 3 +- 11 files changed, 583 insertions(+), 224 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 00b634a..d24f4be 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.0 | 2026-08-03 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.3 | 2026-08-05 */ 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 1107afd..08e222e 100644 --- a/src/authorities.ts +++ b/src/authorities.ts @@ -19,5 +19,22 @@ export const AUTH = { JOB_PICK: "JOB_PICK", JOB_MAT: "JOB_MAT", JOB_PROD: "JOB_PROD", + /** + * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.0 | 2026-08-05 + * 工單 生產流程 完成工單 + */ + PRODUCT_PROCESS: "PRODUCT_PROCESS", REPORT_MGMT: "REPORT_MGMT", -} as const; \ No newline at end of file +} as const; + +/** + * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.0 | 2026-08-05 + * Match session ability codes (exact, trimmed). + */ +export function hasAbility( + abilities: string[] | undefined | null, + required: string, +): boolean { + const code = required.trim(); + return (abilities ?? []).some((a) => String(a).trim() === code); +} \ No newline at end of file diff --git a/src/components/NavigationContent/JobOrderFgStockInNavAlerts.tsx b/src/components/NavigationContent/JobOrderFgStockInNavAlerts.tsx index b463e82..c3c4b28 100644 --- a/src/components/NavigationContent/JobOrderFgStockInNavAlerts.tsx +++ b/src/components/NavigationContent/JobOrderFgStockInNavAlerts.tsx @@ -1,6 +1,6 @@ "use client"; -import { useJobOrderFgStockInAlerts } from "@/hooks/useJobOrderFgStockInAlerts"; +import { useJobOrderFgStockInAlerts, PRODUCTION_LOOKBACK_DAYS } from "@/hooks/useJobOrderFgStockInAlerts"; import type { JobOrderFgAlertItem } from "@/hooks/useJobOrderFgStockInAlerts"; import CloseIcon from "@mui/icons-material/Close"; import FactCheckIcon from "@mui/icons-material/FactCheck"; @@ -139,6 +139,7 @@ type Props = { enabled: boolean; }; +/** FP-MTMS Version Checklist | Functions Ref. No. 52 | v1.0.0 | 2026-08-05 */ const JobOrderFgStockInNavAlerts: React.FC = ({ enabled }) => { const { qcItems, putAwayItems, count, loading, reload } = useJobOrderFgStockInAlerts(enabled); const [open, setOpen] = useState(false); @@ -152,8 +153,8 @@ const JobOrderFgStockInNavAlerts: React.FC = ({ enabled }) => { 0 - ? `點擊查看:待 QC ${qcItems.length}、待上架 ${putAwayItems.length}(今日/昨日產程、完成QC工單列表資格)` - : "今日/昨日無待 QC/待上架提醒" + ? `點擊查看:待 QC ${qcItems.length}、待上架 ${putAwayItems.length}(與「待 QC 上架」tab 相同:今日往前 ${PRODUCTION_LOOKBACK_DAYS} 天)` + : `今日往前 ${PRODUCTION_LOOKBACK_DAYS} 天內無待 QC/待上架提醒` } placement="right" > @@ -202,8 +203,8 @@ const JobOrderFgStockInNavAlerts: React.FC = ({ enabled }) => { )} - 僅含產程日期為今日或昨日的工單,且與「完成QC工單」相同條件(該工單所有工序行均為 - Completed/Pass、有成品入庫且未完成/未拒絕)。待 QC:尚未進入已收貨;待上架:已收貨或部分完成入庫。 + 與產程頁「待 QC 上架」相同資料來源:搜索日為今日、往前 + {PRODUCTION_LOOKBACK_DAYS} 天產程;工序全完成、入庫未完成/未拒絕。待 QC:尚未進入已收貨;待上架:已收貨或部分完成入庫。 0 ? `${text} (${count})` : text; +} + function isPausedProcess(p: AllJoborderProductProcessInfoResponse): boolean { return (p.lines ?? []).some( (l) => String(l.status ?? "").trim().toLowerCase() === "paused", @@ -162,12 +187,40 @@ function dedupeByJobOrder(rows: OpsRow[]): OpsRow[] { return Array.from(map.values()); } +/** Prefer pending_qc over production when same JO appears in off-plan merge. */ +function dedupeOffPlanRows(rows: OpsRow[]): OpsRow[] { + const map = new Map(); + for (const row of rows) { + const existing = map.get(row.jobOrderId); + if (!existing) { + map.set(row.jobOrderId, row); + continue; + } + if (existing.rowKind !== "pending_qc" && row.rowKind === "pending_qc") { + map.set(row.jobOrderId, row); + continue; + } + if (!existing.isPaused && row.isPaused) { + map.set(row.jobOrderId, row); + } + } + return Array.from(map.values()); +} + +function isRowPutawayCompleted(row: OpsRow): boolean { + return ( + String(row.sourceProcess?.stockInLineStatus ?? "") + .trim() + .toLowerCase() === "completed" + ); +} + interface JobOrderOpsTableProps { onSelectProcess?: (jobOrderId: number) => void; printerCombo?: PrinterCombo[]; } -/** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.1 | 2026-08-05 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.2 | 2026-08-05 */ const JobOrderOpsTable: React.FC = ({ onSelectProcess, printerCombo = [], @@ -176,14 +229,18 @@ 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 ?? []; - const canManage = abilities.some((a) => a.trim() === AUTH.ADMIN); + /** 取消工單:僅 ADMIN */ + const canCancel = hasAbility(abilities, AUTH.ADMIN); + /** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單) */ + const canComplete = hasAbility(abilities, AUTH.PRODUCT_PROCESS); const [queryDate, setQueryDate] = useState(() => dayjs()); - const [primaryTab, setPrimaryTab] = useState("all"); - const [pendingSub, setPendingSub] = useState("all"); - const [processingSub, setProcessingSub] = useState("all"); + const [primaryTab, setPrimaryTab] = useState("needs_action"); + const [productionSub, setProductionSub] = useState("all"); const [issueSub, setIssueSub] = useState("stop"); + const searchDay = useMemo(() => queryDate.startOf("day"), [queryDate]); + const [productionRows, setProductionRows] = useState([]); const [pendingQcRows, setPendingQcRows] = useState([]); const [putawayedRows, setPutawayedRows] = useState([]); @@ -203,10 +260,9 @@ const JobOrderOpsTable: React.FC = ({ const loadProduction = useCallback(async () => { const dayStr = queryDate.format("YYYY-MM-DD"); - // lookbackDays=0 → exact search date only (still enables pickProcessBucket on backend) const data = await fetchJoborderProductProcessesPage({ date: dayStr, - lookbackDays: 0, + lookbackDays: PRODUCTION_LOOKBACK_DAYS, bucket: "all", qcReady: false, page: 0, @@ -214,8 +270,7 @@ const JobOrderOpsTable: React.FC = ({ }); const rows = (data?.content ?? []) .map((p) => toProductionRow(p, "production")) - .filter((r): r is OpsRow => r != null) - .filter((r) => !r.productionDate || r.productionDate === dayStr); + .filter((r): r is OpsRow => r != null); setProductionRows(dedupeByJobOrder(rows)); }, [queryDate]); @@ -224,7 +279,7 @@ const JobOrderOpsTable: React.FC = ({ try { const data = await fetchJoborderProductProcessesPage({ date: dayStr, - lookbackDays: 0, + lookbackDays: PRODUCTION_LOOKBACK_DAYS, qcReady: true, includePutaway: true, putawayStatus: "notCompleted", @@ -233,8 +288,7 @@ const JobOrderOpsTable: React.FC = ({ }); const rows = (data?.content ?? []) .map((p) => toProductionRow(p, "pending_qc")) - .filter((r): r is OpsRow => r != null) - .filter((r) => !r.productionDate || r.productionDate === dayStr); + .filter((r): r is OpsRow => r != null); setPendingQcRows(dedupeByJobOrder(rows)); } catch (e) { console.error("loadPendingQc failed", e); @@ -255,8 +309,7 @@ const JobOrderOpsTable: React.FC = ({ }); const rows = (data?.content ?? []) .map((p) => toProductionRow(p, "putawayed")) - .filter((r): r is OpsRow => r != null) - .filter((r) => !r.productionDate || r.productionDate === dayStr); + .filter((r): r is OpsRow => r != null); setPutawayedRows(dedupeByJobOrder(rows)); } catch (e) { console.error("loadPutawayed failed", e); @@ -303,65 +356,108 @@ const JobOrderOpsTable: React.FC = ({ useEffect(() => { setPage(0); - }, [primaryTab, pendingSub, processingSub, issueSub, queryDate]); + }, [primaryTab, productionSub, issueSub, queryDate]); + + useEffect(() => { + setProductionSub("all"); + }, [primaryTab]); + + const offPlanRows = useMemo(() => { + const fromProd = productionRows.filter( + (r) => isCarriedOver(r, searchDay) && !isRowPutawayCompleted(r), + ); + const fromQc = pendingQcRows.filter( + (r) => isCarriedOver(r, searchDay) && !isRowPutawayCompleted(r), + ); + return dedupeOffPlanRows([...fromProd, ...fromQc]); + }, [productionRows, pendingQcRows, searchDay]); + + const applyProductionSub = useCallback((list: OpsRow[], sub: ProductionSubTab) => { + if (sub === "not_picked") { + return list.filter((r) => NOT_PICKED_BUCKETS.has(String(r.pickProcessBucket ?? ""))); + } + if (sub === "picked") { + return list.filter((r) => PICKED_BUCKETS.has(String(r.pickProcessBucket ?? ""))); + } + return list; + }, []); const filteredRows = useMemo(() => { - if (primaryTab === "pending_qc") return pendingQcRows; if (primaryTab === "putawayed") return putawayedRows; + if (primaryTab === "pending_qc") return pendingQcRows; + if (primaryTab === "carried_over") return offPlanRows; if (primaryTab === "issue" && issueSub === "cancel") return cancelledRows; let list = productionRows; if (primaryTab === "pending") { - list = list.filter((r) => { - const b = r.pickProcessBucket; - if (pendingSub === "picked_not_started") return b === "picked_not_started"; - if (pendingSub === "not_picked_not_started") return b === "not_picked_not_started"; - return b === "picked_not_started" || b === "not_picked_not_started"; - }); + list = list.filter((r) => PENDING_BUCKETS.has(String(r.pickProcessBucket ?? ""))); } else if (primaryTab === "processing") { - list = list.filter((r) => { - const b = r.pickProcessBucket; - if (processingSub === "picked_started") return b === "picked_started"; - if (processingSub === "not_picked_started") return b === "not_picked_started"; - return b === "picked_started" || b === "not_picked_started"; - }); + list = list.filter((r) => PROCESSING_BUCKETS.has(String(r.pickProcessBucket ?? ""))); } else if (primaryTab === "issue" && issueSub === "stop") { - list = list.filter((r) => r.isPaused); + return list.filter((r) => r.isPaused); + } + + if ( + primaryTab === "needs_action" || + primaryTab === "pending" || + primaryTab === "processing" + ) { + list = applyProductionSub(list, productionSub); } return list; }, [ primaryTab, - pendingSub, - processingSub, + productionSub, issueSub, productionRows, pendingQcRows, putawayedRows, cancelledRows, + offPlanRows, + applyProductionSub, ]); const counts = useMemo(() => { - const pending = productionRows.filter( - (r) => - r.pickProcessBucket === "picked_not_started" || - r.pickProcessBucket === "not_picked_not_started", - ).length; - const processing = productionRows.filter( - (r) => - r.pickProcessBucket === "picked_started" || - r.pickProcessBucket === "not_picked_started", - ).length; + const pendingRows = productionRows.filter((r) => + PENDING_BUCKETS.has(String(r.pickProcessBucket ?? "")), + ); + const processingRows = productionRows.filter((r) => + PROCESSING_BUCKETS.has(String(r.pickProcessBucket ?? "")), + ); const stop = productionRows.filter((r) => r.isPaused).length; + + const scopeForSub = + primaryTab === "pending" + ? pendingRows + : primaryTab === "processing" + ? processingRows + : productionRows; + return { - all: productionRows.length, - pending, - processing, + needsAction: productionRows.length, + pending: pendingRows.length, + processing: processingRows.length, + carriedOver: offPlanRows.length, pendingQc: pendingQcRows.length, putawayed: putawayedRows.length, stop, cancel: cancelledRows.length, + subAll: scopeForSub.length, + subNotPicked: scopeForSub.filter((r) => + NOT_PICKED_BUCKETS.has(String(r.pickProcessBucket ?? "")), + ).length, + subPicked: scopeForSub.filter((r) => + PICKED_BUCKETS.has(String(r.pickProcessBucket ?? "")), + ).length, }; - }, [productionRows, pendingQcRows, putawayedRows, cancelledRows]); + }, [ + productionRows, + pendingQcRows, + putawayedRows, + cancelledRows, + offPlanRows, + primaryTab, + ]); const paginatedRows = useMemo(() => { const start = page * pageSize; @@ -404,7 +500,7 @@ const JobOrderOpsTable: React.FC = ({ const handleComplete = useCallback( (row: OpsRow) => { - if (!canManage || row.isCancelled) return; + if (!canComplete || row.isCancelled) return; openConfirm(t("Confirm to update this Job Order?"), async () => { if (actionInFlightRef.current) return; actionInFlightRef.current = true; @@ -429,12 +525,12 @@ const JobOrderOpsTable: React.FC = ({ } }); }, - [canManage, openConfirm, t, loadData], + [canComplete, openConfirm, t, loadData], ); const handleCancel = useCallback( (row: OpsRow) => { - if (!canManage || row.isCancelled) return; + if (!canCancel || row.isCancelled) return; openConfirm(t("Cancel job order confirm message"), async () => { if (actionInFlightRef.current) return; actionInFlightRef.current = true; @@ -448,7 +544,7 @@ const JobOrderOpsTable: React.FC = ({ } }); }, - [canManage, openConfirm, t, loadData], + [canCancel, openConfirm, t, loadData], ); const onConfirm = useCallback(async () => { @@ -470,6 +566,18 @@ const JobOrderOpsTable: React.FC = ({ if (row.rowKind === "pending_qc") return t("Waiting QC Put Away"); if (row.rowKind === "putawayed") return t("Put Awayed"); if (row.isCancelled) return t("Cancelled"); + if (primaryTab === "carried_over") { + switch (row.pickProcessBucket) { + case "not_picked_not_started": + case "picked_not_started": + return t("pending"); + case "picked_started": + case "not_picked_started": + return t("processing"); + default: + break; + } + } switch (row.pickProcessBucket) { case "not_picked_not_started": case "not_picked_started": @@ -478,14 +586,27 @@ const JobOrderOpsTable: React.FC = ({ case "picked_started": return t("Picked"); default: + // Prefer explicit key over t("completed") → purchaseOrder「已上架」 + if (String(row.statusLabel).toLowerCase() === "completed") { + return t("Completed"); + } return row.statusLabel || "-"; } }; - const showManageActions = - primaryTab !== "pending_qc" && - primaryTab !== "putawayed" && - !(primaryTab === "issue" && issueSub === "cancel"); + const showManageActionsForRow = (row: OpsRow) => { + if (row.isCancelled || row.rowKind === "putawayed") return false; + if (row.rowKind === "pending_qc") return false; + if (primaryTab === "pending_qc" || primaryTab === "putawayed") return false; + if (primaryTab === "issue" && issueSub === "cancel") return false; + return true; + }; + + const showQcActionForRow = (row: OpsRow) => + (primaryTab === "pending_qc" || + primaryTab === "putawayed" || + (primaryTab === "carried_over" && row.rowKind === "pending_qc")) && + row.stockInLineId != null; return ( @@ -519,72 +640,52 @@ const JobOrderOpsTable: React.FC = ({ sx={{ mb: 1, borderBottom: 1, borderColor: "divider" }} > 0 ? `${t("All")} (${counts.all})` : t("All")} + value="needs_action" + label={tabLabel(t("Needs action"), counts.needsAction)} /> 0 - ? `${t("pending")} (${counts.pending})` - : t("pending") - } + label={tabLabel(t("pending"), counts.pending)} /> 0 - ? `${t("Processing")} (${counts.processing})` - : t("Processing") - } + label={tabLabel(t("Processing"), counts.processing)} + /> + 0 - ? `${t("Waiting QC Put Away")} (${counts.pendingQc})` - : t("Waiting QC Put Away") - } + label={tabLabel(t("Waiting QC Put Away"), counts.pendingQc)} /> 0 - ? `${t("Put Awayed")} (${counts.putawayed})` - : t("Put Awayed") - } + label={tabLabel(t("Put Awayed"), counts.putawayed)} /> 0 - ? `${t("Issue")} (${counts.stop + counts.cancel})` - : t("Issue") - } + label={tabLabel(t("Issue"), counts.stop + counts.cancel)} /> - {primaryTab === "pending" && ( - setPendingSub(v)} - sx={{ mb: 2 }} - > - - - - - )} - - {primaryTab === "processing" && ( + {( + ["needs_action", "pending", "processing"] as PrimaryTab[] + ).includes(primaryTab) && ( setProcessingSub(v)} + value={productionSub} + onChange={(_, v: ProductionSubTab) => setProductionSub(v)} sx={{ mb: 2 }} > - - - + + + )} @@ -596,19 +697,11 @@ const JobOrderOpsTable: React.FC = ({ > 0 - ? `${t("Stop (paused)")} (${counts.stop})` - : t("Stop (paused)") - } + label={tabLabel(t("Stop (paused)"), counts.stop)} /> 0 - ? `${t("Cancelled")} (${counts.cancel})` - : t("Cancelled") - } + label={tabLabel(t("Cancelled"), counts.cancel)} /> )} @@ -643,24 +736,78 @@ const JobOrderOpsTable: React.FC = ({ ) : ( paginatedRows.map((row) => { const busy = rowBusyIds.has(row.jobOrderId); + const carriedOver = isCarriedOver(row, searchDay); + const carriedCellSx = carriedOver + ? { + bgcolor: "warning.light", + borderBottomColor: "warning.main", + } + : undefined; return ( - - {row.jobOrderCode} - + .MuiTableCell-root": { + bgcolor: "warning.light", + }, + "&:hover > .MuiTableCell-root": { + bgcolor: "warning.light", + }, + } + : undefined + } + > + + + + {row.jobOrderCode} + + {carriedOver ? ( + + + ! + + + ) : null} + + + {[row.itemCode, row.itemName].filter(Boolean).join(" ")} - + {row.requiredQty} {row.uom ? ` ${row.uom}` : ""} - + {row.productionDate && dayjs(row.productionDate).isValid() ? dayjs(row.productionDate).format(OUTPUT_DATE_FORMAT) : "-"} - + {row.isPaused && ( @@ -672,7 +819,7 @@ const JobOrderOpsTable: React.FC = ({ /> - + = ({ > {t("View Details")} - {primaryTab === "pending_qc" && row.stockInLineId != null && ( - - )} - {primaryTab === "putawayed" && row.stockInLineId != null && ( + {showQcActionForRow(row) && ( )} - {showManageActions && !row.isCancelled && ( + {showManageActionsForRow(row) && ( )} - {showManageActions && !row.isCancelled && ( + {showManageActionsForRow(row) && ( - setPrimaryTab(v)} - variant="scrollable" - scrollButtons="auto" - sx={{ mb: 1, borderBottom: 1, borderColor: "divider" }} + - - - - - - - setPrimaryTab(v)} + variant="scrollable" + scrollButtons="auto" + sx={{ + flex: 1, + minWidth: 0, + borderBottom: 1, + borderColor: "divider", + "& .MuiTabs-flexContainer": { + columnGap: 0.5, + }, + "& .MuiTab-root": { + minWidth: "auto", + px: 1, + }, + }} + > + + + + + + + + + - + {( ["needs_action", "pending", "processing"] as PrimaryTab[] @@ -846,6 +939,22 @@ const JobOrderOpsTable: React.FC = ({ : t("view stockin")} )} + {showPrintLabelForRow(row) && ( + + )} {showManageActionsForRow(row) && ( )} + + {listTab === "putawayed" && + process.stockInLineId != null && ( + + )} = ({ printerCombo }) => { const { t } = useTranslation(["common", "productionProcess"]); @@ -49,10 +49,6 @@ const ProductionProcessPage: React.FC = ({ printerCo const [linkQcOpen, setLinkQcOpen] = useState(false); const [linkQcSilId, setLinkQcSilId] = useState(null); - const [selectedPrinter, setSelectedPrinter] = useState( - printerCombo && printerCombo.length > 0 ? printerCombo[0] : null - ); - useEffect(() => { if (typeof window !== 'undefined') { try { @@ -123,9 +119,6 @@ const ProductionProcessPage: React.FC = ({ printerCo router.replace(q ? `${pathname}?${q}` : pathname, { scroll: false }); }, [pathname, router, searchParams]); - const listTab = String(productionListState.pickBucket || "needs_action"); - const showPrinterBar = tabIndex === 0 && listTab === "pending_qc"; - if (selectedMatchingStock) { return ( = ({ printerCo return ( <> - {showPrinterBar && ( - - - - {t("Select Printer")}: - - - option.name || option.label || option.code || `Printer ${option.id}` - } - value={selectedPrinter || undefined} - onChange={(_, newValue) => setSelectedPrinter(newValue)} - sx={{ minWidth: 200 }} - size="small" - renderInput={(params) => ( - - )} - /> - - - )} - @@ -210,7 +155,7 @@ const ProductionProcessPage: React.FC = ({ printerCo {tabIndex === 0 && ( { @@ -231,7 +176,7 @@ const ProductionProcessPage: React.FC = ({ printerCo {tabIndex === 1 && ( { if (jobOrderId != null) setSelectedProcessId(jobOrderId); }} diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx index 9568f10..9f093c2 100644 --- a/src/components/Qc/QcStockInModal.tsx +++ b/src/components/Qc/QcStockInModal.tsx @@ -1,7 +1,6 @@ "use client"; import { QcItemWithChecks, QcData } from "@/app/api/qc"; import { - Autocomplete, Box, Button, Divider, @@ -29,6 +28,7 @@ import dayjs from "dayjs"; import { fetchPoQrcode } from "@/app/api/pdf/actions"; import { downloadFile } from "@/app/utils/commonUtil"; import { PrinterCombo } from "@/app/api/settings/printer"; +import PrinterSelect from "@/components/common/PrinterSelect"; import { EscalationResult } from "@/app/api/escalation"; import { SessionWithTokens } from "@/config/authConfig"; import { GridRowModesModel } from "@mui/x-data-grid"; @@ -72,6 +72,7 @@ interface Props extends CommonProps { // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; } +/** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.0 | 2026-08-06 */ const QcStockInModal: React.FC = ({ open, onClose, @@ -840,27 +841,17 @@ const printQrcode = useCallback( {tabIndex == 1 && ( - - - option.name || option.label || option.code || `Printer ${option.id}` - } - value={selectedPrinter} - onChange={(_, newValue) => { - if (newValue) setSelectedPrinter(newValue); - }} - renderInput={(params) => ( - - )} - /> + + { + if (p) setSelectedPrinter(p); + }} + placeholder={t("Label Printer")} + minWidth={300} + /> t.trim().toLowerCase(), + ); + const filtered = list.filter((p) => + types.includes(String(p.type ?? "").trim().toLowerCase()), + ); + if (filtered.length > 0) return filtered; + return fallbackToAll ? list : filtered; +} + +export type PrinterSelectProps = { + /** Outer label text (required), e.g. 「列印機」「標籤打印機」 */ + label: React.ReactNode; + printers: PrinterCombo[]; + value: PrinterCombo | null | undefined; + onChange: (printer: PrinterCombo | null) => void; + /** Filter by printer.type, e.g. "Label" | "A4" */ + typeFilter?: string | string[]; + /** If type filter yields none, show all printers (default false) */ + fallbackToAll?: boolean; + /** Shown inside Select when nothing selected */ + placeholder?: string; + minWidth?: number; + fullWidth?: boolean; + disabled?: boolean; + size?: "small" | "medium"; + /** Append ":" after label when label is a string (default true) */ + appendColon?: boolean; +}; + +/** + * FP-MTMS Version Checklist | Functions Ref. No. 54 | v1.0.0 | 2026-08-06 + * Shared printer picker: outer label + Select (selected value vertically centered, no floating label). + */ +const PrinterSelect: React.FC = ({ + label, + printers, + value, + onChange, + typeFilter, + fallbackToAll = false, + placeholder, + minWidth = 220, + fullWidth = false, + disabled = false, + size = "small", + appendColon = true, +}) => { + const options = useMemo( + () => filterPrintersByType(printers, typeFilter, fallbackToAll), + [printers, typeFilter, fallbackToAll], + ); + + const labelNode = + typeof label === "string" && appendColon && !label.trim().endsWith(":") + ? `${label}:` + : label; + + const height = size === "small" ? 40 : 48; + + return ( + + + {labelNode} + + + + + ); +}; + +export default PrinterSelect; diff --git a/src/i18n/en/productionProcess.json b/src/i18n/en/productionProcess.json index 3a2c388..6c7cec7 100644 --- a/src/i18n/en/productionProcess.json +++ b/src/i18n/en/productionProcess.json @@ -138,6 +138,13 @@ "Prep Time (Minutes)": "Prep Time (Minutes)", "Previous page": "Previous page", "Printer": "Printer", + "Label Printer": "Label Printer", + "A4 Printer": "A4 Printer", + "Print Label": "Print Label", + "Printing": "Printing", + "Print job sent successfully": "Print job sent successfully", + "Please select a printer": "Please select a printer", + "Please select a label printer": "Please select a label printer", "Process": "Process", "Process & Equipment": "Process & Equipment", "Process Description": "Process Description", diff --git a/src/i18n/en/purchaseOrder.json b/src/i18n/en/purchaseOrder.json index 1137ef1..132c88f 100644 --- a/src/i18n/en/purchaseOrder.json +++ b/src/i18n/en/purchaseOrder.json @@ -151,6 +151,8 @@ "Found": "Found", "escalation processing": "Escalation Processing", "Printer": "Printer", + "Label Printer": "Label Printer", + "A4 Printer": "A4 Printer", "Printing": "Printing", "rejectQty": "Reject Qty", "QC decision is required": "QC decision is required", diff --git a/src/i18n/zh/productionProcess.json b/src/i18n/zh/productionProcess.json index 9c40fde..be9f2ae 100644 --- a/src/i18n/zh/productionProcess.json +++ b/src/i18n/zh/productionProcess.json @@ -138,6 +138,13 @@ "Prep Time (Minutes)": "準備時間(分鐘)", "Previous page": "上一頁", "Printer": "列印機", + "Label Printer": "標籤打印機", + "A4 Printer": "A4 打印機", + "Print Label": "列印標籤", + "Printing": "列印中", + "Print job sent successfully": "列印作業已成功送出", + "Please select a printer": "請選擇打印機", + "Please select a label printer": "請選擇標籤打印機", "Process": "工序", "Process & Equipment": "製程與設備", "Process Description": "工序說明", diff --git a/src/i18n/zh/purchaseOrder.json b/src/i18n/zh/purchaseOrder.json index c023d8b..d81863d 100644 --- a/src/i18n/zh/purchaseOrder.json +++ b/src/i18n/zh/purchaseOrder.json @@ -151,6 +151,8 @@ "Found": "已找到", "escalation processing": "處理上報記錄", "Printer": "列印機", + "Label Printer": "標籤打印機", + "A4 Printer": "A4 打印機", "Printing": "列印中", "rejectQty": "拒絕數量", "QC decision is required": "請決定品檢結果",