| @@ -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; | |||
| @@ -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} | |||
| @@ -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={ | |||
| @@ -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" | |||
| @@ -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" | |||
| @@ -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" | |||
| @@ -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) => { | |||
| @@ -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, | |||
| @@ -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 | |||
| @@ -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); | |||
| }} | |||
| @@ -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")} | |||
| @@ -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; | |||
| @@ -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", | |||
| @@ -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", | |||
| @@ -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": "工序說明", | |||
| @@ -151,6 +151,8 @@ | |||
| "Found": "已找到", | |||
| "escalation processing": "處理上報記錄", | |||
| "Printer": "列印機", | |||
| "Label Printer": "標籤打印機", | |||
| "A4 Printer": "A4 打印機", | |||
| "Printing": "列印中", | |||
| "rejectQty": "拒絕數量", | |||
| "QC decision is required": "請決定品檢結果", | |||