Browse Source

共用 PrinterSelect+生產流程 一次載入全 tab cache+各頁改用該元件

production
CANCERYS\kw093 7 hours ago
parent
commit
120967fd49
16 changed files with 622 additions and 465 deletions
  1. +1
    -1
      src/app/api/jo/actions.ts
  2. +1
    -1
      src/components/AppBar/Profile.tsx
  3. +17
    -35
      src/components/DoWorkbench/DoWorkbenchTabs.tsx
  4. +10
    -14
      src/components/PoDetail/PoDetail.tsx
  5. +11
    -16
      src/components/PoDetail/QcStockInModal.tsx
  6. +146
    -37
      src/components/ProductionProcess/JobOrderOpsTable.tsx
  7. +16
    -19
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  8. +1
    -1
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  9. +235
    -260
      src/components/ProductionProcess/ProductionProcessList.tsx
  10. +4
    -59
      src/components/ProductionProcess/ProductionProcessPage.tsx
  11. +13
    -22
      src/components/Qc/QcStockInModal.tsx
  12. +149
    -0
      src/components/common/PrinterSelect.tsx
  13. +7
    -0
      src/i18n/en/productionProcess.json
  14. +2
    -0
      src/i18n/en/purchaseOrder.json
  15. +7
    -0
      src/i18n/zh/productionProcess.json
  16. +2
    -0
      src/i18n/zh/purchaseOrder.json

+ 1
- 1
src/app/api/jo/actions.ts View File

@@ -905,7 +905,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async (type?: string | n
);
});

/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.4 | 2026-08-05 */
/** 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;


+ 1
- 1
src/components/AppBar/Profile.tsx View File

@@ -45,7 +45,7 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => {
disablePadding: false,
sx: { py: 0, minWidth: 180 },
}}
PaperProps={{ variant: "outlined" }}
PaperProps={{ variant: "outlined", elevation: 0 }}
>
<Typography sx={{ px: 2, py: 1.5, fontWeight: 600, color: "text.secondary", fontSize: "0.875rem" }}>
{profileName}


+ 17
- 35
src/components/DoWorkbench/DoWorkbenchTabs.tsx View File

@@ -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<Props> = ({ defaultTabIndex = 0, printerCom
mb: 1,
}}
>
<Typography variant="body2" sx={{ minWidth: "fit-content" }}>
{t("A4 Printer")}:
</Typography>
<Autocomplete
options={a4Printers}
getOptionLabel={(option) => option.name || option.label || option.code || `Printer ${option.id}`}
<PrinterSelect
label={t("A4 Printer")}
printers={a4Printers}
value={a4Printer}
onChange={(_, newValue) => setA4Printer(newValue)}
sx={{ minWidth: 200 }}
size="small"
renderInput={(params) => (
<TextField
{...params}
placeholder={t("A4 Printer")}
inputProps={{ ...params.inputProps, readOnly: true }}
/>
)}
onChange={setA4Printer}
placeholder={t("A4 Printer")}
minWidth={200}
/>
<Typography variant="body2" sx={{ minWidth: "fit-content" }}>
{t("Label Printer")}:
</Typography>
<Autocomplete
options={labelPrinters}
getOptionLabel={(option) => option.name || option.label || option.code || `Printer ${option.id}`}
<PrinterSelect
label={t("Label Printer")}
printers={labelPrinters}
value={labelPrinter}
onChange={(_, newValue) => setLabelPrinter(newValue)}
sx={{ minWidth: 200 }}
size="small"
renderInput={(params) => (
<TextField
{...params}
placeholder={t("Label Printer")}
inputProps={{ ...params.inputProps, readOnly: true }}
/>
)}
onChange={setLabelPrinter}
placeholder={t("Label Printer")}
minWidth={200}
/>
<Button
variant="contained"
@@ -420,7 +399,10 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom
);
};

/** FP-MTMS Version Checklist | Functions Ref. No. 20 | v1.0.0 | 2026-07-19 */
/**
* FP-MTMS Version Checklist | Functions Ref. No. 20 | v1.0.0 | 2026-07-19
* FP-MTMS Version Checklist | Functions Ref. No. 55 | v1.0.0 | 2026-08-06
*/
const DoWorkbenchTabs: React.FC<Props> = (props) => (
<Suspense
fallback={


+ 10
- 14
src/components/PoDetail/PoDetail.tsx View File

@@ -31,7 +31,6 @@ import {
CardContent,
Radio,
alpha,
Autocomplete,
Dialog,
DialogActions,
DialogContent,
@@ -39,6 +38,7 @@ import {
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { submitDialogWithWarning } from "../Swal/CustomAlerts";
import PrinterSelect from "@/components/common/PrinterSelect";
// import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
import {
GridColDef,
@@ -252,6 +252,7 @@ interface PolInputResult {
dnQty: string,
}

/** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.0 | 2026-08-06 */
const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
const cameras = useContext(CameraContext);
const { data: session } = useSession();
@@ -1070,19 +1071,14 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
<Card sx={{ display: "block", flex: 1 }}>
<CardContent component={Stack} spacing={2}>
<Typography variant="h6">列印</Typography>
<Autocomplete
disableClearable
options={labelPrinters}
value={selectedPrinter}
onChange={(_event, value) => setSelectedPrinter(value)}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label={t("Printer")}
fullWidth
/>
)}
<PrinterSelect
label={t("Label Printer")}
printers={labelPrinters}
value={selectedPrinter ?? null}
onChange={(p) => setSelectedPrinter(p ?? undefined)}
placeholder={t("Label Printer")}
fullWidth
disabled={labelPrinters.length <= 0}
/>
<TextField
variant="outlined"


+ 11
- 16
src/components/PoDetail/QcStockInModal.tsx View File

@@ -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";
@@ -71,6 +71,7 @@ interface CommonProps extends Omit<ModalProps, "children"> {
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 PoQcStockInModalVer2: React.FC<Props> = ({
open,
onClose,
@@ -658,22 +659,16 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
</Grid>
{tabIndex == 1 && (
<Stack direction="row" justifyContent="flex-end" gap={1} sx={{m:3, mt:"auto"}}>
<Autocomplete
disableClearable
options={printerCombo}
defaultValue={selectedPrinter}
onChange={(event, value) => {
setSelectedPrinter(value)
<Stack direction="row" justifyContent="flex-end" alignItems="center" gap={1} sx={{m:3, mt:"auto"}}>
<PrinterSelect
label={t("Label Printer")}
printers={printerCombo || []}
value={selectedPrinter ?? null}
onChange={(p) => {
if (p) setSelectedPrinter(p);
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label={t("Printer")}
sx={{ width: 300}}
/>
)}
placeholder={t("Label Printer")}
minWidth={300}
/>
<TextField
variant="outlined"


+ 146
- 37
src/components/ProductionProcess/JobOrderOpsTable.tsx View File

@@ -35,6 +35,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import dayjs, { Dayjs } from "dayjs";
import { SessionWithTokens } from "@/config/authConfig";
import { AUTH, hasAbility } from "@/authorities";
import PrinterSelect from "@/components/common/PrinterSelect";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
AllJoborderProductProcessInfoResponse,
@@ -43,6 +44,8 @@ import {
fetchJos,
fetchProductProcessesByJobOrderId,
setJobOrderHidden,
printFGStockInLabel,
PrintFGStockInLabelRequest,
} from "@/app/api/jo/actions";
import { JobOrder } from "@/app/api/jo";
import QcStockInModal from "@/components/Qc/QcStockInModal";
@@ -220,7 +223,7 @@ interface JobOrderOpsTableProps {
printerCombo?: PrinterCombo[];
}

/** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.3 | 2026-08-05 */
/** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.4 | 2026-08-06 */
const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({
onSelectProcess,
printerCombo = [],
@@ -234,6 +237,31 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({
/** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單) */
const canComplete = hasAbility(abilities, AUTH.PRODUCT_PROCESS);

const labelPrinterCombo = useMemo(
() => (printerCombo || []).filter((p) => p.type === "Label"),
[printerCombo],
);
const printerOptions = useMemo(
() => (labelPrinterCombo.length > 0 ? labelPrinterCombo : printerCombo || []),
[labelPrinterCombo, printerCombo],
);
const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
() => printerOptions[0] ?? null,
);
const printInFlightRef = useRef(false);
const [printingSilId, setPrintingSilId] = useState<number | null>(null);

useEffect(() => {
if (!printerOptions.length) {
setSelectedPrinter(null);
return;
}
setSelectedPrinter((prev) => {
if (prev && printerOptions.some((p) => p.id === prev.id)) return prev;
return printerOptions[0];
});
}, [printerOptions]);

const [queryDate, setQueryDate] = useState<Dayjs>(() => dayjs());
const [primaryTab, setPrimaryTab] = useState<PrimaryTab>("needs_action");
const [productionSub, setProductionSub] = useState<ProductionSubTab>("all");
@@ -322,8 +350,8 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({
try {
const res = await fetchJos({
code: "",
planStart: dayStr,
planStartTo: dayStr,
planStart: `${dayStr}T00:00`,
planStartTo: `${dayStr}T23:59:59`,
joSearchStatus: "cancel",
pageNum: 0,
pageSize: FETCH_SIZE,
@@ -608,6 +636,44 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({
(primaryTab === "carried_over" && row.rowKind === "pending_qc")) &&
row.stockInLineId != null;

const showPrintLabelForRow = (row: OpsRow) =>
(primaryTab === "putawayed" || row.rowKind === "putawayed") &&
row.stockInLineId != null;

const handlePrintLabel = useCallback(
async (row: OpsRow) => {
if (printInFlightRef.current) return;
if (!selectedPrinter) {
alert(t("Please select a label printer"));
return;
}
if (!row.stockInLineId) {
alert(t("Invalid Stock In Line Id"));
return;
}
printInFlightRef.current = true;
setPrintingSilId(row.stockInLineId);
try {
const data: PrintFGStockInLabelRequest = {
stockInLineId: row.stockInLineId,
printerId: selectedPrinter.id,
printQty: 1,
};
await printFGStockInLabel(data);
alert(t("Print job sent successfully"));
} catch (error: unknown) {
console.error("Error printing:", error);
const msg =
error instanceof Error ? error.message : String(error ?? "Unknown error");
alert(t(`Print failed: ${msg}`));
} finally {
setPrintingSilId(null);
printInFlightRef.current = false;
}
},
[selectedPrinter, t],
);

return (
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk">
<Card sx={{ mb: 2 }}>
@@ -632,42 +698,69 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({
</Button>
</Stack>

<Tabs
value={primaryTab}
onChange={(_, v: PrimaryTab) => setPrimaryTab(v)}
variant="scrollable"
scrollButtons="auto"
sx={{ mb: 1, borderBottom: 1, borderColor: "divider" }}
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
spacing={2}
sx={{ mb: 1, flexWrap: "wrap", rowGap: 1 }}
>
<Tab
value="needs_action"
label={tabLabel(t("Needs action"), counts.needsAction)}
/>
<Tab
value="pending"
label={tabLabel(t("pending"), counts.pending)}
/>
<Tab
value="processing"
label={tabLabel(t("Processing"), counts.processing)}
/>
<Tab
value="pending_qc"
label={tabLabel(t("Waiting QC Put Away"), counts.pendingQc)}
/>
<Tab
value="putawayed"
label={tabLabel(t("Put Awayed"), counts.putawayed)}
/>
<Tab
value="issue"
label={tabLabel(t("Issue"), counts.stop + counts.cancel)}
/>
<Tab
value="carried_over"
label={tabLabel(t("Off-plan unfinished"), counts.carriedOver)}
<Tabs
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,
},
}}
>
<Tab
value="needs_action"
label={tabLabel(t("Needs action"), counts.needsAction)}
/>
<Tab
value="pending"
label={tabLabel(t("pending"), counts.pending)}
/>
<Tab
value="processing"
label={tabLabel(t("Processing"), counts.processing)}
/>
<Tab
value="pending_qc"
label={tabLabel(t("Waiting QC Put Away"), counts.pendingQc)}
/>
<Tab
value="putawayed"
label={tabLabel(t("Put Awayed"), counts.putawayed)}
/>
<Tab
value="issue"
label={tabLabel(t("Issue"), counts.stop + counts.cancel)}
/>
<Tab
value="carried_over"
label={tabLabel(t("Off-plan unfinished"), counts.carriedOver)}
/>
</Tabs>
<PrinterSelect
label={t("Label Printer")}
printers={printerOptions}
value={selectedPrinter}
onChange={setSelectedPrinter}
placeholder={t("Label Printer")}
/>
</Tabs>
</Stack>

{(
["needs_action", "pending", "processing"] as PrimaryTab[]
@@ -846,6 +939,22 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({
: t("view stockin")}
</Button>
)}
{showPrintLabelForRow(row) && (
<Button
size="small"
variant="contained"
color="secondary"
disabled={
!selectedPrinter ||
printingSilId === row.stockInLineId
}
onClick={() => void handlePrintLabel(row)}
>
{printingSilId === row.stockInLineId
? t("Printing")
: t("Print Label")}
</Button>
)}
{showManageActionsForRow(row) && (
<Button
size="small"


+ 16
- 19
src/components/ProductionProcess/ProductionProcessDetail.tsx View File

@@ -72,7 +72,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
onBack,
fromJosave,
}) => {
console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave });
// console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave });
const { t } = useTranslation(["productionProcess", "common"]);
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -145,10 +145,10 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>();
}, [onBack]);
// 获取 process 和 lines 数据
const fetchProcessDetail = useCallback(async () => {
console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() });
// console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() });
setLoading(true);
try {
console.log(` Loading process detail for JobOrderId: ${jobOrderId}`);
// console.log(` Loading process detail for JobOrderId: ${jobOrderId}`);
const processesWithLines = await fetchProductProcessesByJobOrderId(jobOrderId);
@@ -162,8 +162,8 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>();
const lines = currentProcess.productProcessLines || [];
setLines(lines);
linesRef.current = lines;
console.log(" Process data loaded:", currentProcess);
console.log(" Lines loaded:", lines);
// console.log(" Process data loaded:", currentProcess);
// console.log(" Lines loaded:", lines);
} catch (error) {
console.error(" Error loading process detail:", error);
onBackRef.current();
@@ -173,12 +173,12 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>();
}, [jobOrderId]);
const handleOpenTimeDialog = useCallback((lineId: number) => {
console.log("🔓 handleOpenTimeDialog CALLED", { lineId, timestamp: new Date().toISOString() });
//console.log("🔓 handleOpenTimeDialog CALLED", { lineId, timestamp: new Date().toISOString() });
// 直接使用 linesRef.current,避免触发 setLines
const line = linesRef.current.find(l => l.id === lineId);
if (line) {
console.log(" Found line:", line);
// console.log(" Found line:", line);
setEditingLineId(lineId);
setTimeValues({
durationInMinutes: line.durationInMinutes || 0,
@@ -186,7 +186,7 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>();
postProdTimeInMinutes: line.postProdTimeInMinutes || 0,
});
setOpenTimeDialog(true);
console.log(" Dialog opened");
// console.log(" Dialog opened");
} else {
console.warn(" Line not found:", lineId);
}
@@ -195,7 +195,7 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>();
fetchProcessDetailRef.current = fetchProcessDetail;
}, [fetchProcessDetail]);
const handleCloseTimeDialog = useCallback(() => {
console.log("🔒 handleCloseTimeDialog CALLED", { timestamp: new Date().toISOString() });
//console.log("🔒 handleCloseTimeDialog CALLED", { timestamp: new Date().toISOString() });
setOpenTimeDialog(false);
setEditingLineId(null);
setTimeValues({
@@ -203,11 +203,11 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>();
prepTimeInMinutes: 0,
postProdTimeInMinutes: 0,
});
console.log(" Dialog closed");
//console.log(" Dialog closed");
}, []);
const handleConfirmTimeUpdate = useCallback(async () => {
console.log("💾 handleConfirmTimeUpdate CALLED", { editingLineId, timeValues, timestamp: new Date().toISOString() });
//console.log("💾 handleConfirmTimeUpdate CALLED", { editingLineId, timeValues, timestamp: new Date().toISOString() });
if (!editingLineId) return;
try {
@@ -228,10 +228,7 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>();
}, [editingLineId, timeValues, fetchProcessDetail, handleCloseTimeDialog, t]);
useEffect(() => {
console.log("🔄 useEffect [jobOrderId] TRIGGERED", {
jobOrderId,
timestamp: new Date().toISOString()
});
//console.log("🔄 useEffect [jobOrderId] TRIGGERED", { jobOrderId,timestamp: new Date().toISOString()});
if (fetchProcessDetailRef.current) {
fetchProcessDetailRef.current();
}
@@ -239,22 +236,22 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>();

// 添加监听 openTimeDialog 变化的 useEffect
useEffect(() => {
console.log(" openTimeDialog changed:", { openTimeDialog, timestamp: new Date().toISOString() });
//console.log(" openTimeDialog changed:", { openTimeDialog, timestamp: new Date().toISOString() });
}, [openTimeDialog]);

// 添加监听 timeValues 变化的 useEffect
useEffect(() => {
console.log(" timeValues changed:", { timeValues, timestamp: new Date().toISOString() });
//console.log(" timeValues changed:", { timeValues, timestamp: new Date().toISOString() });
}, [timeValues]);

// 添加监听 lines 变化的 useEffect
useEffect(() => {
console.log(" lines changed:", { count: lines.length, lines, timestamp: new Date().toISOString() });
//console.log(" lines changed:", { count: lines.length, lines, timestamp: new Date().toISOString() });
}, [lines]);

// 添加监听 editingLineId 变化的 useEffect
useEffect(() => {
console.log(" editingLineId changed:", { editingLineId, timestamp: new Date().toISOString() });
//console.log(" editingLineId changed:", { editingLineId, timestamp: new Date().toISOString() });
}, [editingLineId]);

const handlePassLine = useCallback(async (lineId: number) => {


+ 1
- 1
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx View File

@@ -53,7 +53,7 @@ interface ProductProcessJobOrderDetailProps {
initialTabIndex?: number;
}

/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.4 | 2026-08-05 */
/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.5 | 2026-08-06 */
const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({
jobOrderId,
onBack,


+ 235
- 260
src/components/ProductionProcess/ProductionProcessList.tsx View File

@@ -37,6 +37,7 @@ import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
import { AUTH, hasAbility } from "@/authorities";
import PrinterSelect from "@/components/common/PrinterSelect";


import {
@@ -47,6 +48,8 @@ import {
assignJobOrderPickOrder,
fetchJoborderProductProcessesPage,
JobOrderProductProcessBucketCounts,
printFGStockInLabel,
PrintFGStockInLabelRequest,
} from "@/app/api/jo/actions";
import { StockInLineInput } from "@/app/api/stockIn";
import { PrinterCombo } from "@/app/api/settings/printer";
@@ -175,7 +178,7 @@ function isWaitingQcPutAway(
return s !== "completed" && s !== "rejected";
}

/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.4 | 2026-08-05 */
/** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.5 | 2026-08-06 */
const ProductProcessList: React.FC<ProductProcessListProps> = ({
onSelectProcess,
printerCombo,
@@ -184,17 +187,19 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
listPersistedState,
onListPersistedStateChange,
}) => {
const { t } = useTranslation( ["common", "productionProcess","purchaseOrder","dashboard"]);
const { t } = useTranslation( ["common", "productionProcess","purchaseOrder","dashboard","jo"]);
const { data: session } = useSession() as { data: SessionWithTokens | null };
const sessionToken = session as SessionWithTokens | null;
const [loading, setLoading] = useState(false);
const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]);
const [productionCache, setProductionCache] = useState<
AllJoborderProductProcessInfoResponse[]
>([]);
const [pendingQcCache, setPendingQcCache] = useState<
AllJoborderProductProcessInfoResponse[]
>([]);
const [putawayedCache, setPutawayedCache] = useState<
AllJoborderProductProcessInfoResponse[]
>([]);
const [bucketCounts, setBucketCounts] =
useState<JobOrderProductProcessBucketCounts>(EMPTY_BUCKET_COUNTS);
const [pendingQcCount, setPendingQcCount] = useState(0);
@@ -212,19 +217,36 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
const isProductionTab =
listTab === "needs_action" || listTab === "pending" || listTab === "processing";
const isCarriedOverTab = listTab === "carried_over";
const qcReady = listTab === "pending_qc" || listTab === "putawayed";
const putawayStatus =
listTab === "putawayed"
? "completed"
: listTab === "pending_qc"
? "notCompleted"
: null;
const includePutaway = qcReady ? true : null;
/** Production unfinished tabs + pending QC + off-plan: carry-over window. */
const enableCarryOver =
!disableDateFilter &&
(isProductionTab || listTab === "pending_qc" || isCarriedOverTab);

const labelPrinterCombo = useMemo(
() => (printerCombo || []).filter((p) => p.type === "Label"),
[printerCombo],
);
const printerOptions = useMemo(
() => (labelPrinterCombo.length > 0 ? labelPrinterCombo : printerCombo || []),
[labelPrinterCombo, printerCombo],
);
const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
() => printerOptions[0] ?? null,
);
const printInFlightRef = useRef(false);
const [printingSilId, setPrintingSilId] = useState<number | null>(null);

useEffect(() => {
if (!printerOptions.length) {
setSelectedPrinter(null);
return;
}
setSelectedPrinter((prev) => {
if (prev && printerOptions.some((p) => p.id === prev.id)) return prev;
return printerOptions[0];
});
}, [printerOptions]);

const appliedSearch = useMemo(
() => ({
date: listPersistedState.date,
@@ -246,67 +268,12 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
[appliedSearch.date],
);

const [totalJobOrders, setTotalJobOrders] = useState(0);

// Generic confirm dialog for actions (update job order / etc.)
const [confirmOpen, setConfirmOpen] = useState(false);
const [confirmMessage, setConfirmMessage] = useState("");
const [confirmLoading, setConfirmLoading] = useState(false);
const [pendingConfirmAction, setPendingConfirmAction] = useState<null | (() => Promise<void>)>(null);

// QC 的业务判定:同一个 jobOrder 下,所有 productProcess 的所有 lines 都必须是 Completed/Pass
// 才允许打开 QcStockInModal(避免仅某个 productProcess 完成就提前出现 view stockin)。
const jobOrderQcReadyById = useMemo(() => {
const lineDone = (status: unknown) => {
const s = String(status ?? "").trim().toLowerCase();
return s === "completed" || s === "pass";
};

const byJobOrder = new Map<number, AllJoborderProductProcessInfoResponse[]>();
for (const p of processes) {
if (p.jobOrderId == null) continue;
const arr = byJobOrder.get(p.jobOrderId) ?? [];
arr.push(p);
byJobOrder.set(p.jobOrderId, arr);
}

const result = new Map<number, boolean>();
const isDone = (status: unknown) => {
const s = String(status ?? "").trim().toLowerCase();
return s === "completed" || s === "pass";
};
byJobOrder.forEach((jobOrderProcesses, jobOrderId) => {
const hasStockInLine = jobOrderProcesses.some((p) => p.stockInLineId != null);
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);
});

return result;
}, [processes]);
const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => {
if (!currentUserId) {
alert(t("Unable to get user ID"));
@@ -387,7 +354,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
}));
}, [disableDateFilter, onListPersistedStateChange]);

const fetchProcesses = useCallback(async () => {
/** Load all tab datasets once per search; tab switches use cache only. */
const loadAllCaches = useCallback(async () => {
setLoading(true);
try {
const typeParam = filter === "all" ? undefined : filter;
@@ -399,162 +367,133 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
page: 0,
size: FETCH_SIZE,
};

if (isCarriedOverTab) {
const [prod, pendingQc] = await Promise.all([
fetchJoborderProductProcessesPage({
...base,
qcReady: false,
lookbackDays: enableCarryOver ? PRODUCTION_LOOKBACK_DAYS : undefined,
bucket: "all",
}),
fetchJoborderProductProcessesPage({
...base,
qcReady: true,
includePutaway: true,
putawayStatus: "notCompleted",
lookbackDays: enableCarryOver ? PRODUCTION_LOOKBACK_DAYS : undefined,
}),
]);
const prodContent = (prod?.content || []).filter((p) => !isPutawayCompleted(p));
const qcContent = (pendingQc?.content || []).filter((p) => !isPutawayCompleted(p));
setProductionCache(prodContent);
setPendingQcCache(qcContent);
if (prod?.bucketCounts) setBucketCounts(prod.bucketCounts);
setPendingQcCount(pendingQc?.totalJobOrders || 0);
setCarriedOverCount(prod?.carriedOverCount ?? 0);
const search = appliedSearch.date
? dayjs(appliedSearch.date).startOf("day")
: null;
const merged = [...qcContent, ...prodContent].filter((p) =>
isProcessCarriedOver(p, search),
);
// Dedupe by jobOrderId (prefer waiting-QC row when it has stockInLineId)
const byJo = new Map<number, AllJoborderProductProcessInfoResponse>();
for (const p of merged) {
if (p.jobOrderId == null) continue;
const existing = byJo.get(p.jobOrderId);
if (!existing) {
byJo.set(p.jobOrderId, p);
} else if (existing.stockInLineId == null && p.stockInLineId != null) {
byJo.set(p.jobOrderId, p);
}
}
const content = Array.from(byJo.values());
setProcesses(content);
setTotalJobOrders(content.length);
return;
}

// Production tabs share one fetch (bucket=all); pending/processing filter client-side.
const data = await fetchJoborderProductProcessesPage({
...base,
qcReady,
includePutaway,
putawayStatus,
lookbackDays: enableCarryOver ? PRODUCTION_LOOKBACK_DAYS : undefined,
bucket: isProductionTab ? "all" : undefined,
});

const content = data?.content || [];
setProcesses(content);
setTotalJobOrders(data?.totalJobOrders || 0);
if (isProductionTab) {
setProductionCache(content);
if (data?.bucketCounts) setBucketCounts(data.bucketCounts);
}
if (qcReady && putawayStatus === "notCompleted") {
setPendingQcCache(content);
setPendingQcCount(data?.totalJobOrders || 0);
}
if (qcReady && putawayStatus === "completed") {
setPutawayedCount(data?.totalJobOrders || 0);
}
setCarriedOverCount(data?.carriedOverCount ?? 0);
const lookback = disableDateFilter ? undefined : PRODUCTION_LOOKBACK_DAYS;

const [prod, pendingQc, putawayed] = await Promise.all([
fetchJoborderProductProcessesPage({
...base,
qcReady: false,
lookbackDays: lookback,
bucket: "all",
}),
fetchJoborderProductProcessesPage({
...base,
qcReady: true,
includePutaway: true,
putawayStatus: "notCompleted",
lookbackDays: lookback,
}),
fetchJoborderProductProcessesPage({
...base,
qcReady: true,
includePutaway: true,
putawayStatus: "completed",
}),
]);

const prodContent = (prod?.content || []).filter((p) => !isPutawayCompleted(p));
const qcContent = (pendingQc?.content || []).filter((p) => !isPutawayCompleted(p));
const putawayContent = putawayed?.content || [];

setProductionCache(prodContent);
setPendingQcCache(qcContent);
setPutawayedCache(putawayContent);
if (prod?.bucketCounts) setBucketCounts(prod.bucketCounts);
else setBucketCounts(EMPTY_BUCKET_COUNTS);
setPendingQcCount(pendingQc?.totalJobOrders || 0);
setPutawayedCount(putawayed?.totalJobOrders || 0);
setCarriedOverCount(prod?.carriedOverCount ?? 0);
} catch (e) {
console.error(e);
setProcesses([]);
setTotalJobOrders(0);
if (isProductionTab) setBucketCounts(EMPTY_BUCKET_COUNTS);
setProductionCache([]);
setPendingQcCache([]);
setPutawayedCache([]);
setBucketCounts(EMPTY_BUCKET_COUNTS);
setPendingQcCount(0);
setPutawayedCount(0);
setCarriedOverCount(0);
} finally {
setLoading(false);
}
}, [
appliedSearch,
disableDateFilter,
filter,
qcReady,
includePutaway,
putawayStatus,
enableCarryOver,
isProductionTab,
isCarriedOverTab,
]);
}, [appliedSearch, disableDateFilter, filter]);

useEffect(() => {
fetchProcesses();
}, [fetchProcesses]);
void loadAllCaches();
}, [loadAllCaches]);

const offPlanRows = useMemo(() => {
if (!searchDay) return [] as AllJoborderProductProcessInfoResponse[];
const merged = [...pendingQcCache, ...productionCache].filter((p) =>
isProcessCarriedOver(p, searchDay),
);
const byJo = new Map<number, AllJoborderProductProcessInfoResponse>();
for (const p of merged) {
if (p.jobOrderId == null) continue;
const existing = byJo.get(p.jobOrderId);
if (!existing) {
byJo.set(p.jobOrderId, p);
} else if (existing.stockInLineId == null && p.stockInLineId != null) {
byJo.set(p.jobOrderId, p);
}
}
return Array.from(byJo.values());
}, [productionCache, pendingQcCache, searchDay]);

/** Keep production + QC tab badges fresh even when not on that tab. */
useEffect(() => {
let cancelled = false;
const typeParam = filter === "all" ? undefined : filter;
const base = {
date: disableDateFilter ? undefined : appliedSearch.date,
itemCode: appliedSearch.itemCode,
jobOrderCode: appliedSearch.jobOrderCode,
type: typeParam,
page: 0,
size: FETCH_SIZE,
};
/** Active tab rows from cache — no refetch on tab switch. */
const tabProcesses = useMemo(() => {
if (listTab === "pending_qc") return pendingQcCache;
if (listTab === "putawayed") return putawayedCache;
if (listTab === "carried_over") return offPlanRows;
return productionCache;
}, [listTab, productionCache, pendingQcCache, putawayedCache, offPlanRows]);

(async () => {
try {
const [prod, pendingQc, putawayed] = await Promise.all([
fetchJoborderProductProcessesPage({
...base,
qcReady: false,
lookbackDays: disableDateFilter ? undefined : PRODUCTION_LOOKBACK_DAYS,
bucket: "all",
}),
fetchJoborderProductProcessesPage({
...base,
qcReady: true,
includePutaway: true,
putawayStatus: "notCompleted",
lookbackDays: disableDateFilter ? undefined : PRODUCTION_LOOKBACK_DAYS,
}),
fetchJoborderProductProcessesPage({
...base,
size: 1,
qcReady: true,
includePutaway: true,
putawayStatus: "completed",
}),
]);
if (cancelled) return;
if (prod?.bucketCounts) setBucketCounts(prod.bucketCounts);
if (prod?.content) {
setProductionCache(prod.content.filter((p) => !isPutawayCompleted(p)));
}
if (pendingQc?.content) {
setPendingQcCache(
pendingQc.content.filter((p) => !isPutawayCompleted(p)),
);
}
setPendingQcCount(pendingQc?.totalJobOrders || 0);
setPutawayedCount(putawayed?.totalJobOrders || 0);
if (prod?.carriedOverCount != null) setCarriedOverCount(prod.carriedOverCount);
} catch (e) {
console.error(e);
}
})();
// QC 的业务判定:同一个 jobOrder 下,所有 productProcess 的所有 lines 都必须是 Completed/Pass
const jobOrderQcReadyById = useMemo(() => {
const byJobOrder = new Map<number, AllJoborderProductProcessInfoResponse[]>();
for (const p of tabProcesses) {
if (p.jobOrderId == null) continue;
const arr = byJobOrder.get(p.jobOrderId) ?? [];
arr.push(p);
byJobOrder.set(p.jobOrderId, arr);
}

return () => {
cancelled = true;
const result = new Map<number, boolean>();
const isDone = (status: unknown) => {
const s = String(status ?? "").trim().toLowerCase();
return s === "completed" || s === "pass";
};
}, [appliedSearch, disableDateFilter, filter]);

byJobOrder.forEach((jobOrderProcesses, jobOrderId) => {
const hasStockInLine = jobOrderProcesses.some((p) => p.stockInLineId != null);

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);
});

return result;
}, [tabProcesses]);

const handleListTabChange = useCallback(
(_: React.SyntheticEvent, value: string) => {
@@ -574,32 +513,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
bucketCounts.pickedStarted + bucketCounts.notPickedStarted;
const needsActionCount = pendingCount + processingCount;

const offPlanTabCount = useMemo(() => {
if (!searchDay) return 0;
const ids = new Set<number>();
for (const p of productionCache) {
if (
!isPutawayCompleted(p) &&
isProcessCarriedOver(p, searchDay) &&
p.jobOrderId != null
) {
ids.add(p.jobOrderId);
}
}
for (const p of pendingQcCache) {
if (
!isPutawayCompleted(p) &&
isProcessCarriedOver(p, searchDay) &&
p.jobOrderId != null
) {
ids.add(p.jobOrderId);
}
}
return ids.size;
}, [productionCache, pendingQcCache, searchDay]);
const offPlanTabCount = offPlanRows.length;

const filteredProcesses = useMemo(() => {
let list = processes;
let list = tabProcesses;
if (listTab === "pending") {
list = list.filter((p) =>
PENDING_FINE_BUCKETS.has(String(p.pickProcessBucket ?? "")),
@@ -609,10 +526,9 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
PROCESSING_FINE_BUCKETS.has(String(p.pickProcessBucket ?? "")),
);
}
// carried_over: fetch already filtered to off-plan only
if (selectedItemCodes.length === 0) return list;
return list.filter((p) => selectedItemCodes.includes(p.itemCode));
}, [processes, selectedItemCodes, listTab]);
}, [tabProcesses, selectedItemCodes, listTab]);

const displayTotalJobOrders = isCarriedOverTab
? filteredProcesses.length
@@ -621,8 +537,12 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
? pendingCount
: listTab === "processing"
? processingCount
: totalJobOrders || needsActionCount
: totalJobOrders;
: needsActionCount
: listTab === "pending_qc"
? pendingQcCount
: listTab === "putawayed"
? putawayedCount
: filteredProcesses.length;

const displayCarriedOverCount = useMemo(() => {
if (!enableCarryOver || !searchDay) return 0;
@@ -739,14 +659,14 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
// await updateJo({ id: process.jobOrderId, status: "completed" });
// 4) 刷新列表
await fetchProcesses();
await loadAllCaches();
} catch (e) {
console.error(e);
alert(t("An error has occurred. Please try again later."));
} finally {
setLoading(false);
}
}, [t, fetchProcesses, canManageUpdateJo]);
}, [t, loadAllCaches, canManageUpdateJo]);

const openConfirm = useCallback((message: string, action: () => Promise<void>) => {
setConfirmMessage(message);
@@ -771,23 +691,51 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
}
}, [pendingConfirmAction, closeConfirm]);
const closeNewModal = useCallback(() => {
// const response = updateJo({ id: 1, status: "storing" });
setOpenModal(false); // Close the modal first
// fetchProcesses();
// setTimeout(() => {
// }, 300); // Add a delay to avoid immediate re-trigger of useEffect
}, [fetchProcesses]);
setOpenModal(false);
}, []);

const handlePrintLabel = useCallback(
async (process: AllJoborderProductProcessInfoResponse) => {
if (printInFlightRef.current) return;
if (!selectedPrinter) {
alert(t("Please select a label printer"));
return;
}
if (!process.stockInLineId) {
alert(t("Invalid Stock In Line Id"));
return;
}
printInFlightRef.current = true;
setPrintingSilId(process.stockInLineId);
try {
const data: PrintFGStockInLabelRequest = {
stockInLineId: process.stockInLineId,
printerId: selectedPrinter.id,
printQty: 1,
};
await printFGStockInLabel(data);
alert(t("Print job sent successfully"));
} catch (error: any) {
console.error("Error printing:", error);
alert(t(`Print failed: ${error?.message || "Unknown error"}`));
} finally {
setPrintingSilId(null);
printInFlightRef.current = false;
}
},
[selectedPrinter, t],
);

const searchedItemOptions = useMemo(
() =>
Array.from(
new Map(
processes
[...productionCache, ...pendingQcCache, ...putawayedCache]
.filter((p) => !!p.itemCode)
.map((p) => [p.itemCode, { itemCode: p.itemCode, itemName: p.itemName }]),
).values(),
),
[processes],
[productionCache, pendingQcCache, putawayedCache],
);

/** Reset 用 ±3 天;preFilled 用目前已套用的條件(與列表查詢一致) */
@@ -898,23 +846,31 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
{selectedItemCodes.length > 0 ? ` | ${t("Filtered")}: ${filteredProcesses.length}` : ""}
</Typography>

<Tabs
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
spacing={2}
sx={{ mb: 2, flexWrap: "wrap", rowGap: 1 }}
>
<Tabs
value={listTab}
onChange={handleListTabChange}
variant="scrollable"
scrollButtons="auto"
sx={{
mb: 2,
flex: 1,
minWidth: 0,
borderBottom: 1,
borderColor: "divider",
"& .MuiTabs-flexContainer": {
columnGap: 2,
columnGap: 0.5,
rowGap: 1,
},
"& .MuiTab-root": {
overflow: "visible",
minWidth: "auto",
px: 2,
px: 1,
},
}}
>
@@ -949,6 +905,14 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
)}
/>
</Tabs>
<PrinterSelect
label={t("Label Printer")}
printers={printerOptions}
value={selectedPrinter}
onChange={setSelectedPrinter}
placeholder={t("Label Printer")}
/>
</Stack>

<Box
sx={{
@@ -1023,13 +987,6 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
} else if (isCarriedOverTab && PROCESSING_FINE_BUCKETS.has(bucket)) {
chipLabel = t("processing");
statusColor = "primary";
} else if (qcReady) {
chipLabel =
putawayStatus === "completed"
? t("Put Awayed")
: t("Waiting QC Put Away");
statusColor =
putawayStatus === "completed" ? "success" : "warning";
} else if (statusLower === "completed") {
chipLabel = t("Completed");
statusColor = "success";
@@ -1302,6 +1259,24 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
{t("view stockin")}
</Button>
)}

{listTab === "putawayed" &&
process.stockInLineId != null && (
<Button
variant="contained"
size="small"
color="secondary"
disabled={
!selectedPrinter ||
printingSilId === process.stockInLineId
}
onClick={() => void handlePrintLabel(process)}
>
{printingSilId === process.stockInLineId
? t("Printing")
: t("Print Label")}
</Button>
)}
</Stack>

<Typography


+ 4
- 59
src/components/ProductionProcess/ProductionProcessPage.tsx View File

@@ -2,7 +2,7 @@
import React, { useState, useEffect, useCallback } from "react";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material";
import { Box, Tabs, Tab } from "@mui/material";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import QcStockInModal from "@/components/Qc/QcStockInModal";
import ProductionProcessList, {
@@ -26,7 +26,7 @@ const STORAGE_KEY = 'productionProcess_selectedMatchingStock';

/**
* FP-MTMS Version Checklist | Functions Ref. No. 26 | v1.0.0 | 2026-07-20
* FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.3 | 2026-08-05
* FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.4 | 2026-08-06
*/
const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => {
const { t } = useTranslation(["common", "productionProcess"]);
@@ -49,10 +49,6 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
const [linkQcOpen, setLinkQcOpen] = useState(false);
const [linkQcSilId, setLinkQcSilId] = useState<number | null>(null);

const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
printerCombo && printerCombo.length > 0 ? printerCombo[0] : null
);

useEffect(() => {
if (typeof window !== 'undefined') {
try {
@@ -123,9 +119,6 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ 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 (
<JobPickExecutionsecondscan
@@ -151,54 +144,6 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
return (
<>
<Box>
{showPrinterBar && (
<Box sx={{
p: 1,
borderBottom: '1px solid #e0e0e0',
minHeight: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: 2,
flexWrap: 'wrap',
}}>
<Stack
direction="row"
spacing={2}
sx={{
alignItems: 'center',
flexWrap: 'wrap',
rowGap: 1,
}}
>
<Typography variant="body2" sx={{ minWidth: 'fit-content', mr: 1.5 }}>
{t("Select Printer")}:
</Typography>
<Autocomplete
disableClearable
options={printerCombo || []}
getOptionLabel={(option) =>
option.name || option.label || option.code || `Printer ${option.id}`
}
value={selectedPrinter || undefined}
onChange={(_, newValue) => setSelectedPrinter(newValue)}
sx={{ minWidth: 200 }}
size="small"
renderInput={(params) => (
<TextField
{...params}
placeholder={t("Printer")}
inputProps={{
...params.inputProps,
readOnly: true,
}}
/>
)}
/>
</Stack>
</Box>
)}

<Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}>
<Tab label={t("Production Process")} />
<Tab label={t("Job Order Ops Table")} />
@@ -210,7 +155,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo

{tabIndex === 0 && (
<ProductionProcessList
printerCombo={selectedPrinter ? [selectedPrinter] : printerCombo}
printerCombo={printerCombo}
listPersistedState={productionListState}
onListPersistedStateChange={setProductionListState}
onSelectProcess={(jobOrderId) => {
@@ -231,7 +176,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo

{tabIndex === 1 && (
<JobOrderOpsTable
printerCombo={selectedPrinter ? [selectedPrinter] : printerCombo}
printerCombo={printerCombo}
onSelectProcess={(jobOrderId) => {
if (jobOrderId != null) setSelectedProcessId(jobOrderId);
}}


+ 13
- 22
src/components/Qc/QcStockInModal.tsx View File

@@ -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<Props> = ({
open,
onClose,
@@ -840,27 +841,17 @@ const printQrcode = useCallback(
</Grid>
{tabIndex == 1 && (
<Stack direction="row" justifyContent="flex-end" gap={1} sx={{m:3, mt:"auto"}}>
<Autocomplete
disableClearable
options={labelPrinterCombo}
getOptionLabel={(option) =>
option.name || option.label || option.code || `Printer ${option.id}`
}
value={selectedPrinter}
onChange={(_, newValue) => {
if (newValue) setSelectedPrinter(newValue);
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label={t("Printer")}
sx={{ width: 300 }}
inputProps={{ ...params.inputProps, readOnly: true }}
/>
)}
/>
<Stack direction="row" justifyContent="flex-end" alignItems="center" gap={1} sx={{m:3, mt:"auto"}}>
<PrinterSelect
label={t("Label Printer")}
printers={labelPrinterCombo}
value={selectedPrinter ?? null}
onChange={(p) => {
if (p) setSelectedPrinter(p);
}}
placeholder={t("Label Printer")}
minWidth={300}
/>
<TextField
variant="outlined"
label={t("Print Qty")}


+ 149
- 0
src/components/common/PrinterSelect.tsx View File

@@ -0,0 +1,149 @@
"use client";

import React, { useMemo } from "react";
import {
FormControl,
MenuItem,
Select,
SelectChangeEvent,
Stack,
Typography,
} from "@mui/material";
import type { PrinterCombo } from "@/app/api/settings/printer";

/**
* FP-MTMS Version Checklist | Functions Ref. No. 54 | v1.0.0 | 2026-08-06
* Format display label for a printer combo option.
*/
export function formatPrinterOptionLabel(p: PrinterCombo): string {
return p.name || p.label || p.code || `Printer ${p.id}`;
}

/**
* FP-MTMS Version Checklist | Functions Ref. No. 54 | v1.0.0 | 2026-08-06
* Filter printers by type (e.g. Label / A4).
*/
export function filterPrintersByType(
printers: PrinterCombo[] | null | undefined,
typeFilter?: string | string[],
fallbackToAll = false,
): PrinterCombo[] {
const list = printers ?? [];
if (!typeFilter) return list;
const types = (Array.isArray(typeFilter) ? typeFilter : [typeFilter]).map(
(t) => 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<PrinterSelectProps> = ({
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 (
<Stack
direction="row"
alignItems="center"
spacing={1}
sx={{ flexShrink: 0, width: fullWidth ? "100%" : undefined }}
>
<Typography variant="body2" sx={{ whiteSpace: "nowrap", minWidth: "fit-content" }}>
{labelNode}
</Typography>
<FormControl
size={size}
fullWidth={fullWidth}
sx={{ minWidth: fullWidth ? undefined : minWidth, flex: fullWidth ? 1 : undefined }}
disabled={disabled || options.length === 0}
> <Select
displayEmpty
value={value?.id != null ? String(value.id) : ""}
onChange={(e: SelectChangeEvent) => {
const id = Number(e.target.value);
onChange(options.find((p) => p.id === id) ?? null);
}}
renderValue={(selected: string) => {
if (!selected) {
return (
<Typography variant="body2" color="text.secondary" component="span">
{placeholder ?? (typeof label === "string" ? label.replace(/:$/, "") : "")}
</Typography>
);
}
const p = options.find((x) => String(x.id) === selected);
return p ? formatPrinterOptionLabel(p) : selected;
}}
sx={{
height,
"& .MuiSelect-select": {
display: "flex",
alignItems: "center",
py: 0,
lineHeight: `${height}px`,
height,
boxSizing: "border-box",
},
}}
>
{options.map((p) => (
<MenuItem key={p.id} value={String(p.id)}>
{formatPrinterOptionLabel(p)}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
);
};

export default PrinterSelect;

+ 7
- 0
src/i18n/en/productionProcess.json View File

@@ -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",


+ 2
- 0
src/i18n/en/purchaseOrder.json View File

@@ -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",


+ 7
- 0
src/i18n/zh/productionProcess.json View File

@@ -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": "工序說明",


+ 2
- 0
src/i18n/zh/purchaseOrder.json View File

@@ -151,6 +151,8 @@
"Found": "已找到",
"escalation processing": "處理上報記錄",
"Printer": "列印機",
"Label Printer": "標籤打印機",
"A4 Printer": "A4 打印機",
"Printing": "列印中",
"rejectQty": "拒絕數量",
"QC decision is required": "請決定品檢結果",


Loading…
Cancel
Save