diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 1a22f24..ad6d216 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.5 | 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 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/AppBar/Profile.tsx b/src/components/AppBar/Profile.tsx index 7146f81..f5d7fdd 100644 --- a/src/components/AppBar/Profile.tsx +++ b/src/components/AppBar/Profile.tsx @@ -45,7 +45,7 @@ const Profile: React.FC = ({ avatarImageSrc, profileName }) => { disablePadding: false, sx: { py: 0, minWidth: 180 }, }} - PaperProps={{ variant: "outlined" }} + PaperProps={{ variant: "outlined", elevation: 0 }} > {profileName} diff --git a/src/components/DoWorkbench/DoWorkbenchTabs.tsx b/src/components/DoWorkbench/DoWorkbenchTabs.tsx index 5c85e0c..69015ee 100644 --- a/src/components/DoWorkbench/DoWorkbenchTabs.tsx +++ b/src/components/DoWorkbench/DoWorkbenchTabs.tsx @@ -1,14 +1,12 @@ "use client"; import { - Autocomplete, Badge, Box, Button, CircularProgress, Tab, Tabs, - TextField, Tooltip, Typography, } from "@mui/material"; @@ -20,6 +18,7 @@ import GoodPickExecutionWorkbenchRecord from "./GoodPickExecutionWorkbenchRecord import { useTranslation } from "react-i18next"; import WorkbenchTicketReleaseTableTab from "./WorkbenchTicketReleaseTable"; import { Stack } from "@mui/system"; +import PrinterSelect from "@/components/common/PrinterSelect"; import Swal from "sweetalert2"; import { printDNWorkbench } from "@/app/api/do/actions"; import { @@ -251,41 +250,21 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom mb: 1, }} > - - {t("A4 Printer")}: - - option.name || option.label || option.code || `Printer ${option.id}`} + setA4Printer(newValue)} - sx={{ minWidth: 200 }} - size="small" - renderInput={(params) => ( - - )} + onChange={setA4Printer} + placeholder={t("A4 Printer")} + minWidth={200} /> - - {t("Label Printer")}: - - option.name || option.label || option.code || `Printer ${option.id}`} + setLabelPrinter(newValue)} - sx={{ minWidth: 200 }} - size="small" - renderInput={(params) => ( - - )} + onChange={setLabelPrinter} + placeholder={t("Label Printer")} + minWidth={200} /> - setPrimaryTab(v)} - variant="scrollable" - scrollButtons="auto" - sx={{ mb: 1, borderBottom: 1, borderColor: "divider" }} + - - - - - - - - - {primaryTab === "pending" && ( setPendingSub(v)} - sx={{ mb: 2 }} + value={primaryTab} + onChange={(_, v: PrimaryTab) => 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, + }, + }} > - - - + + + + + + + - )} + + - {primaryTab === "processing" && ( + {( + ["needs_action", "pending", "processing"] as PrimaryTab[] + ).includes(primaryTab) && ( setProcessingSub(v)} + value={productionSub} + onChange={(_, v: ProductionSubTab) => setProductionSub(v)} sx={{ mb: 2 }} > - - - + + + )} @@ -561,8 +788,14 @@ const JobOrderOpsTable: React.FC = ({ onChange={(_, v: IssueSubTab) => setIssueSub(v)} sx={{ mb: 2 }} > - - + + )} @@ -576,8 +809,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")} @@ -596,24 +829,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 && ( @@ -625,7 +912,7 @@ const JobOrderOpsTable: React.FC = ({ /> - + = ({ > {t("View Details")} - {primaryTab === "pending_qc" && row.stockInLineId != null && ( + {showQcActionForRow(row) && ( )} - {primaryTab === "putawayed" && row.stockInLineId != null && ( + {showPrintLabelForRow(row) && ( )} - {showManageActions && !row.isCancelled && ( + {showManageActionsForRow(row) && ( )} - {showManageActions && !row.isCancelled && ( + {showManageActionsForRow(row) && ( )} + + {listTab === "putawayed" && + process.stockInLineId != null && ( + + )} = ({ printerCombo }) => { const { t } = useTranslation(["common", "productionProcess"]); const [selectedProcessId, setSelectedProcessId] = useState(null); @@ -46,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 { @@ -120,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) => ( - - )} - /> - - - )} - @@ -207,7 +155,7 @@ const ProductionProcessPage: React.FC = ({ printerCo {tabIndex === 0 && ( { @@ -228,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/hooks/useJobOrderFgStockInAlerts.ts b/src/hooks/useJobOrderFgStockInAlerts.ts index 6dbb5b1..24eed5c 100644 --- a/src/hooks/useJobOrderFgStockInAlerts.ts +++ b/src/hooks/useJobOrderFgStockInAlerts.ts @@ -1,12 +1,16 @@ "use client"; -import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; -import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { + AllJoborderProductProcessInfoResponse, + fetchJoborderProductProcessesPage, +} from "@/app/api/jo/actions"; +import dayjs from "dayjs"; import { useCallback, useEffect, useState } from "react"; const POLL_MS = 60_000; - -const ALERTS_URL = `${NEXT_PUBLIC_API_URL}/product-process/Demo/Process/alerts/fg-qc-putaway`; +/** Same as ProductionProcessList pending_qc tab. */ +export const PRODUCTION_LOOKBACK_DAYS = 4; +const FETCH_SIZE = 200; export type JobOrderFgAlertItem = { stockInLineId: number; @@ -19,35 +23,55 @@ export type JobOrderFgAlertItem = { lotNo: string | null; }; -function parseRow(o: Record): JobOrderFgAlertItem { - return { - stockInLineId: Number(o.stockInLineId ?? o.stockinLineId ?? 0), - jobOrderId: Number(o.jobOrderId ?? o.joborderid ?? 0), - jobOrderCode: o.jobOrderCode != null ? String(o.jobOrderCode) : null, - itemNo: o.itemNo != null ? String(o.itemNo) : null, - itemName: o.itemName != null ? String(o.itemName) : null, - status: o.status != null ? String(o.status) : null, - processDate: o.processDate != null ? String(o.processDate) : null, - lotNo: o.lotNo != null ? String(o.lotNo) : null, - }; +function isPutawayCompleted(p: AllJoborderProductProcessInfoResponse): boolean { + return String(p.stockInLineStatus ?? "").trim().toLowerCase() === "completed"; } -function parsePayload(raw: unknown): { qc: JobOrderFgAlertItem[]; putAway: JobOrderFgAlertItem[] } { - if (!raw || typeof raw !== "object") return { qc: [], putAway: [] }; - const p = raw as Record; - const qcRaw = p.qc; - const putAwayRaw = p.putAway; +function toAlertItem(p: AllJoborderProductProcessInfoResponse): JobOrderFgAlertItem | null { + if (!p.stockInLineId || !p.jobOrderId) return null; return { - qc: Array.isArray(qcRaw) ? qcRaw.map((r) => parseRow(r as Record)) : [], - putAway: Array.isArray(putAwayRaw) - ? putAwayRaw.map((r) => parseRow(r as Record)) - : [], + stockInLineId: p.stockInLineId, + jobOrderId: p.jobOrderId, + jobOrderCode: p.jobOrderCode ?? null, + itemNo: p.itemCode ?? null, + itemName: p.itemName ?? null, + status: p.stockInLineStatus ?? null, + processDate: p.date ?? null, + lotNo: p.lotNo ?? null, }; } +function splitPendingQcRows(content: AllJoborderProductProcessInfoResponse[]): { + qc: JobOrderFgAlertItem[]; + putAway: JobOrderFgAlertItem[]; +} { + const qc: JobOrderFgAlertItem[] = []; + const putAway: JobOrderFgAlertItem[] = []; + + for (const p of content) { + if (isPutawayCompleted(p)) continue; + const item = toAlertItem(p); + if (!item) continue; + const statusNorm = String(p.stockInLineStatus ?? "").trim().toLowerCase(); + if (statusNorm === "received" || statusNorm === "partially_completed") { + putAway.push(item); + } else { + qc.push(item); + } + } + + const sortByDate = (a: JobOrderFgAlertItem, b: JobOrderFgAlertItem) => + (b.processDate ?? "").localeCompare(a.processDate ?? ""); + + qc.sort(sortByDate); + putAway.sort(sortByDate); + return { qc, putAway }; +} + /** - * 與「完成QC工單」相同資格 + 產程日期為今日或昨日;分待 QC / 待上架。 + * 與產程「待 QC 上架」tab 相同 API/日期(今日、lookback 4 天);分待 QC / 待上架。 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 52 | v1.0.0 | 2026-08-05 */ export function useJobOrderFgStockInAlerts(enabled: boolean) { const [qcItems, setQcItems] = useState([]); const [putAwayItems, setPutAwayItems] = useState([]); @@ -61,15 +85,19 @@ export function useJobOrderFgStockInAlerts(enabled: boolean) { } setLoading(true); try { - const res = await clientAuthFetch(ALERTS_URL); - if (!res.ok) { - setQcItems([]); - setPutAwayItems([]); - return; - } - const data = parsePayload(await res.json()); - setQcItems(data.qc); - setPutAwayItems(data.putAway); + const dateStr = dayjs().format("YYYY-MM-DD"); + const data = await fetchJoborderProductProcessesPage({ + date: dateStr, + lookbackDays: PRODUCTION_LOOKBACK_DAYS, + qcReady: true, + includePutaway: true, + putawayStatus: "notCompleted", + page: 0, + size: FETCH_SIZE, + }); + const { qc, putAway } = splitPendingQcRows(data?.content ?? []); + setQcItems(qc); + setPutAwayItems(putAway); } catch { setQcItems([]); setPutAwayItems([]); diff --git a/src/i18n/en/productionProcess.json b/src/i18n/en/productionProcess.json index 4689f61..a9ce166 100644 --- a/src/i18n/en/productionProcess.json +++ b/src/i18n/en/productionProcess.json @@ -139,6 +139,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", @@ -215,6 +222,7 @@ "Total job orders": "Total job orders", "Including carried over": "Including carried over", "Carried over from past day": "Carried over from past day", + "Off-plan unfinished": "Off-plan unfinished", "All unfinished": "Needs action", "Needs action": "Needs action", "Not picked · Not started": "Not picked · Not started", 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 2537db5..88ad5aa 100644 --- a/src/i18n/zh/productionProcess.json +++ b/src/i18n/zh/productionProcess.json @@ -83,7 +83,7 @@ "Job Order No.": "工單編號", "Job Order and Product": "工單及貨品", "Issue": "異常", - "Job Order Ops Table": "查看工單流程情況", + "Job Order Ops Table": "工單生產流程", "Pending (picked)": "已提料", "Pending (not picked)": "未提料", "Processing (picked)": "已提料", @@ -98,7 +98,7 @@ "Actions": "操作", "Job Order Production Process": "工單生產流程", "Job Process Status Dashboard": "儀表板 - 工單狀態", - "Drink Production Qty Dashboard": "儀表板 - 飲料生產量數", + "Drink Production Qty Dashboard": "儀表板 - 飲料生產數量", "Expand job order details": "展開工單明細", "Collapse job order details": "收合工單明細", "Goods Name": "貨品名稱", @@ -144,6 +144,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": "工序說明", @@ -154,7 +161,7 @@ "Processing Time": "生產時間", "Processing Time (mins)": "步驟時間(分鐘)", "Product process status": "生產流程狀態", - "Production Date": "生產日期", + "Production Date": "預計生產日期", "Production Equipment Status Dashboard": "儀表板 - 生產設備最新狀態", "Production Output Data": "生產輸出數據", "Production Output Data Entry": "生產輸出數據輸入", @@ -164,7 +171,7 @@ "Production Process Line Remark": "工藝明細", "Production Process Steps": "生產流程步驟", "Production Time Remaining": "生產剩餘時間", - "Production date": "生產日期", + "Production date": "預計生產日期", "Put Awayed Job Orders": "已上架工單", "Qty": "數量", "Quality Check": "品質檢查", @@ -218,8 +225,9 @@ "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": "未按規劃完成工單的工單", + "Off-plan unfinished": "未按規劃完成工單", "All unfinished": "需處理", "Needs action": "需處理", "Not picked · Not started": "未提料 · 未開工", 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": "請決定品檢結果",