| @@ -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: { | export const fetchJoborderProductProcessesPage = cache(async (params: { | ||||
| /** Job order / process date(YYYY-MM-DD) */ | /** Job order / process date(YYYY-MM-DD) */ | ||||
| date?: string | null; | date?: string | null; | ||||
| @@ -45,7 +45,7 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => { | |||||
| disablePadding: false, | disablePadding: false, | ||||
| sx: { py: 0, minWidth: 180 }, | 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" }}> | <Typography sx={{ px: 2, py: 1.5, fontWeight: 600, color: "text.secondary", fontSize: "0.875rem" }}> | ||||
| {profileName} | {profileName} | ||||
| @@ -1,14 +1,12 @@ | |||||
| "use client"; | "use client"; | ||||
| import { | import { | ||||
| Autocomplete, | |||||
| Badge, | Badge, | ||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| CircularProgress, | CircularProgress, | ||||
| Tab, | Tab, | ||||
| Tabs, | Tabs, | ||||
| TextField, | |||||
| Tooltip, | Tooltip, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| @@ -20,6 +18,7 @@ import GoodPickExecutionWorkbenchRecord from "./GoodPickExecutionWorkbenchRecord | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import WorkbenchTicketReleaseTableTab from "./WorkbenchTicketReleaseTable"; | import WorkbenchTicketReleaseTableTab from "./WorkbenchTicketReleaseTable"; | ||||
| import { Stack } from "@mui/system"; | import { Stack } from "@mui/system"; | ||||
| import PrinterSelect from "@/components/common/PrinterSelect"; | |||||
| import Swal from "sweetalert2"; | import Swal from "sweetalert2"; | ||||
| import { printDNWorkbench } from "@/app/api/do/actions"; | import { printDNWorkbench } from "@/app/api/do/actions"; | ||||
| import { | import { | ||||
| @@ -251,41 +250,21 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom | |||||
| mb: 1, | 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} | 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} | 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 | <Button | ||||
| variant="contained" | 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) => ( | const DoWorkbenchTabs: React.FC<Props> = (props) => ( | ||||
| <Suspense | <Suspense | ||||
| fallback={ | fallback={ | ||||
| @@ -31,7 +31,6 @@ import { | |||||
| CardContent, | CardContent, | ||||
| Radio, | Radio, | ||||
| alpha, | alpha, | ||||
| Autocomplete, | |||||
| Dialog, | Dialog, | ||||
| DialogActions, | DialogActions, | ||||
| DialogContent, | DialogContent, | ||||
| @@ -39,6 +38,7 @@ import { | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | ||||
| import PrinterSelect from "@/components/common/PrinterSelect"; | |||||
| // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid"; | // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid"; | ||||
| import { | import { | ||||
| GridColDef, | GridColDef, | ||||
| @@ -252,6 +252,7 @@ interface PolInputResult { | |||||
| dnQty: string, | 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 PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | ||||
| const cameras = useContext(CameraContext); | const cameras = useContext(CameraContext); | ||||
| const { data: session } = useSession(); | const { data: session } = useSession(); | ||||
| @@ -1070,19 +1071,14 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| <Card sx={{ display: "block", flex: 1 }}> | <Card sx={{ display: "block", flex: 1 }}> | ||||
| <CardContent component={Stack} spacing={2}> | <CardContent component={Stack} spacing={2}> | ||||
| <Typography variant="h6">列印</Typography> | <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 | <TextField | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -1,7 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import { QcItemWithChecks, QcData } from "@/app/api/qc"; | import { QcItemWithChecks, QcData } from "@/app/api/qc"; | ||||
| import { | import { | ||||
| Autocomplete, | |||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| Divider, | Divider, | ||||
| @@ -29,6 +28,7 @@ import dayjs from "dayjs"; | |||||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | import { fetchPoQrcode } from "@/app/api/pdf/actions"; | ||||
| import { downloadFile } from "@/app/utils/commonUtil"; | import { downloadFile } from "@/app/utils/commonUtil"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import PrinterSelect from "@/components/common/PrinterSelect"; | |||||
| import { EscalationResult } from "@/app/api/escalation"; | import { EscalationResult } from "@/app/api/escalation"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { GridRowModesModel } from "@mui/x-data-grid"; | import { GridRowModesModel } from "@mui/x-data-grid"; | ||||
| @@ -71,6 +71,7 @@ interface CommonProps extends Omit<ModalProps, "children"> { | |||||
| interface Props extends CommonProps { | interface Props extends CommonProps { | ||||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | // 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> = ({ | const PoQcStockInModalVer2: React.FC<Props> = ({ | ||||
| open, | open, | ||||
| onClose, | onClose, | ||||
| @@ -658,22 +659,16 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||||
| </Grid> | </Grid> | ||||
| {tabIndex == 1 && ( | {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 | <TextField | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -35,6 +35,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | |||||
| import dayjs, { Dayjs } from "dayjs"; | import dayjs, { Dayjs } from "dayjs"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { AUTH, hasAbility } from "@/authorities"; | import { AUTH, hasAbility } from "@/authorities"; | ||||
| import PrinterSelect from "@/components/common/PrinterSelect"; | |||||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import { | import { | ||||
| AllJoborderProductProcessInfoResponse, | AllJoborderProductProcessInfoResponse, | ||||
| @@ -43,6 +44,8 @@ import { | |||||
| fetchJos, | fetchJos, | ||||
| fetchProductProcessesByJobOrderId, | fetchProductProcessesByJobOrderId, | ||||
| setJobOrderHidden, | setJobOrderHidden, | ||||
| printFGStockInLabel, | |||||
| PrintFGStockInLabelRequest, | |||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { JobOrder } from "@/app/api/jo"; | import { JobOrder } from "@/app/api/jo"; | ||||
| import QcStockInModal from "@/components/Qc/QcStockInModal"; | import QcStockInModal from "@/components/Qc/QcStockInModal"; | ||||
| @@ -220,7 +223,7 @@ interface JobOrderOpsTableProps { | |||||
| printerCombo?: PrinterCombo[]; | 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> = ({ | const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | ||||
| onSelectProcess, | onSelectProcess, | ||||
| printerCombo = [], | printerCombo = [], | ||||
| @@ -234,6 +237,31 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | |||||
| /** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單) */ | /** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單) */ | ||||
| const canComplete = hasAbility(abilities, AUTH.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 [queryDate, setQueryDate] = useState<Dayjs>(() => dayjs()); | ||||
| const [primaryTab, setPrimaryTab] = useState<PrimaryTab>("needs_action"); | const [primaryTab, setPrimaryTab] = useState<PrimaryTab>("needs_action"); | ||||
| const [productionSub, setProductionSub] = useState<ProductionSubTab>("all"); | const [productionSub, setProductionSub] = useState<ProductionSubTab>("all"); | ||||
| @@ -322,8 +350,8 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | |||||
| try { | try { | ||||
| const res = await fetchJos({ | const res = await fetchJos({ | ||||
| code: "", | code: "", | ||||
| planStart: dayStr, | |||||
| planStartTo: dayStr, | |||||
| planStart: `${dayStr}T00:00`, | |||||
| planStartTo: `${dayStr}T23:59:59`, | |||||
| joSearchStatus: "cancel", | joSearchStatus: "cancel", | ||||
| pageNum: 0, | pageNum: 0, | ||||
| pageSize: FETCH_SIZE, | pageSize: FETCH_SIZE, | ||||
| @@ -608,6 +636,44 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | |||||
| (primaryTab === "carried_over" && row.rowKind === "pending_qc")) && | (primaryTab === "carried_over" && row.rowKind === "pending_qc")) && | ||||
| row.stockInLineId != null; | 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 ( | return ( | ||||
| <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk"> | <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk"> | ||||
| <Card sx={{ mb: 2 }}> | <Card sx={{ mb: 2 }}> | ||||
| @@ -632,42 +698,69 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | |||||
| </Button> | </Button> | ||||
| </Stack> | </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[] | ["needs_action", "pending", "processing"] as PrimaryTab[] | ||||
| @@ -846,6 +939,22 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | |||||
| : t("view stockin")} | : t("view stockin")} | ||||
| </Button> | </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) && ( | {showManageActionsForRow(row) && ( | ||||
| <Button | <Button | ||||
| size="small" | size="small" | ||||
| @@ -72,7 +72,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| onBack, | onBack, | ||||
| fromJosave, | fromJosave, | ||||
| }) => { | }) => { | ||||
| console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave }); | |||||
| // console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave }); | |||||
| const { t } = useTranslation(["productionProcess", "common"]); | const { t } = useTranslation(["productionProcess", "common"]); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| @@ -145,10 +145,10 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| }, [onBack]); | }, [onBack]); | ||||
| // 获取 process 和 lines 数据 | // 获取 process 和 lines 数据 | ||||
| const fetchProcessDetail = useCallback(async () => { | const fetchProcessDetail = useCallback(async () => { | ||||
| console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() }); | |||||
| // console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() }); | |||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| console.log(` Loading process detail for JobOrderId: ${jobOrderId}`); | |||||
| // console.log(` Loading process detail for JobOrderId: ${jobOrderId}`); | |||||
| const processesWithLines = await fetchProductProcessesByJobOrderId(jobOrderId); | const processesWithLines = await fetchProductProcessesByJobOrderId(jobOrderId); | ||||
| @@ -162,8 +162,8 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| const lines = currentProcess.productProcessLines || []; | const lines = currentProcess.productProcessLines || []; | ||||
| setLines(lines); | setLines(lines); | ||||
| linesRef.current = 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) { | } catch (error) { | ||||
| console.error(" Error loading process detail:", error); | console.error(" Error loading process detail:", error); | ||||
| onBackRef.current(); | onBackRef.current(); | ||||
| @@ -173,12 +173,12 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| }, [jobOrderId]); | }, [jobOrderId]); | ||||
| const handleOpenTimeDialog = useCallback((lineId: number) => { | 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 | // 直接使用 linesRef.current,避免触发 setLines | ||||
| const line = linesRef.current.find(l => l.id === lineId); | const line = linesRef.current.find(l => l.id === lineId); | ||||
| if (line) { | if (line) { | ||||
| console.log(" Found line:", line); | |||||
| // console.log(" Found line:", line); | |||||
| setEditingLineId(lineId); | setEditingLineId(lineId); | ||||
| setTimeValues({ | setTimeValues({ | ||||
| durationInMinutes: line.durationInMinutes || 0, | durationInMinutes: line.durationInMinutes || 0, | ||||
| @@ -186,7 +186,7 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| postProdTimeInMinutes: line.postProdTimeInMinutes || 0, | postProdTimeInMinutes: line.postProdTimeInMinutes || 0, | ||||
| }); | }); | ||||
| setOpenTimeDialog(true); | setOpenTimeDialog(true); | ||||
| console.log(" Dialog opened"); | |||||
| // console.log(" Dialog opened"); | |||||
| } else { | } else { | ||||
| console.warn(" Line not found:", lineId); | console.warn(" Line not found:", lineId); | ||||
| } | } | ||||
| @@ -195,7 +195,7 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| fetchProcessDetailRef.current = fetchProcessDetail; | fetchProcessDetailRef.current = fetchProcessDetail; | ||||
| }, [fetchProcessDetail]); | }, [fetchProcessDetail]); | ||||
| const handleCloseTimeDialog = useCallback(() => { | const handleCloseTimeDialog = useCallback(() => { | ||||
| console.log("🔒 handleCloseTimeDialog CALLED", { timestamp: new Date().toISOString() }); | |||||
| //console.log("🔒 handleCloseTimeDialog CALLED", { timestamp: new Date().toISOString() }); | |||||
| setOpenTimeDialog(false); | setOpenTimeDialog(false); | ||||
| setEditingLineId(null); | setEditingLineId(null); | ||||
| setTimeValues({ | setTimeValues({ | ||||
| @@ -203,11 +203,11 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| prepTimeInMinutes: 0, | prepTimeInMinutes: 0, | ||||
| postProdTimeInMinutes: 0, | postProdTimeInMinutes: 0, | ||||
| }); | }); | ||||
| console.log(" Dialog closed"); | |||||
| //console.log(" Dialog closed"); | |||||
| }, []); | }, []); | ||||
| const handleConfirmTimeUpdate = useCallback(async () => { | 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; | if (!editingLineId) return; | ||||
| try { | try { | ||||
| @@ -228,10 +228,7 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| }, [editingLineId, timeValues, fetchProcessDetail, handleCloseTimeDialog, t]); | }, [editingLineId, timeValues, fetchProcessDetail, handleCloseTimeDialog, t]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("🔄 useEffect [jobOrderId] TRIGGERED", { | |||||
| jobOrderId, | |||||
| timestamp: new Date().toISOString() | |||||
| }); | |||||
| //console.log("🔄 useEffect [jobOrderId] TRIGGERED", { jobOrderId,timestamp: new Date().toISOString()}); | |||||
| if (fetchProcessDetailRef.current) { | if (fetchProcessDetailRef.current) { | ||||
| fetchProcessDetailRef.current(); | fetchProcessDetailRef.current(); | ||||
| } | } | ||||
| @@ -239,22 +236,22 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| // 添加监听 openTimeDialog 变化的 useEffect | // 添加监听 openTimeDialog 变化的 useEffect | ||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log(" openTimeDialog changed:", { openTimeDialog, timestamp: new Date().toISOString() }); | |||||
| //console.log(" openTimeDialog changed:", { openTimeDialog, timestamp: new Date().toISOString() }); | |||||
| }, [openTimeDialog]); | }, [openTimeDialog]); | ||||
| // 添加监听 timeValues 变化的 useEffect | // 添加监听 timeValues 变化的 useEffect | ||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log(" timeValues changed:", { timeValues, timestamp: new Date().toISOString() }); | |||||
| //console.log(" timeValues changed:", { timeValues, timestamp: new Date().toISOString() }); | |||||
| }, [timeValues]); | }, [timeValues]); | ||||
| // 添加监听 lines 变化的 useEffect | // 添加监听 lines 变化的 useEffect | ||||
| 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]); | }, [lines]); | ||||
| // 添加监听 editingLineId 变化的 useEffect | // 添加监听 editingLineId 变化的 useEffect | ||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log(" editingLineId changed:", { editingLineId, timestamp: new Date().toISOString() }); | |||||
| //console.log(" editingLineId changed:", { editingLineId, timestamp: new Date().toISOString() }); | |||||
| }, [editingLineId]); | }, [editingLineId]); | ||||
| const handlePassLine = useCallback(async (lineId: number) => { | const handlePassLine = useCallback(async (lineId: number) => { | ||||
| @@ -53,7 +53,7 @@ interface ProductProcessJobOrderDetailProps { | |||||
| initialTabIndex?: number; | 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> = ({ | const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({ | ||||
| jobOrderId, | jobOrderId, | ||||
| onBack, | onBack, | ||||
| @@ -37,6 +37,7 @@ import dayjs from "dayjs"; | |||||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; | import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; | ||||
| import { AUTH, hasAbility } from "@/authorities"; | import { AUTH, hasAbility } from "@/authorities"; | ||||
| import PrinterSelect from "@/components/common/PrinterSelect"; | |||||
| import { | import { | ||||
| @@ -47,6 +48,8 @@ import { | |||||
| assignJobOrderPickOrder, | assignJobOrderPickOrder, | ||||
| fetchJoborderProductProcessesPage, | fetchJoborderProductProcessesPage, | ||||
| JobOrderProductProcessBucketCounts, | JobOrderProductProcessBucketCounts, | ||||
| printFGStockInLabel, | |||||
| PrintFGStockInLabelRequest, | |||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { StockInLineInput } from "@/app/api/stockIn"; | import { StockInLineInput } from "@/app/api/stockIn"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| @@ -175,7 +178,7 @@ function isWaitingQcPutAway( | |||||
| return s !== "completed" && s !== "rejected"; | 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> = ({ | const ProductProcessList: React.FC<ProductProcessListProps> = ({ | ||||
| onSelectProcess, | onSelectProcess, | ||||
| printerCombo, | printerCombo, | ||||
| @@ -184,17 +187,19 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| listPersistedState, | listPersistedState, | ||||
| onListPersistedStateChange, | 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 { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const sessionToken = session as SessionWithTokens | null; | const sessionToken = session as SessionWithTokens | null; | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]); | |||||
| const [productionCache, setProductionCache] = useState< | const [productionCache, setProductionCache] = useState< | ||||
| AllJoborderProductProcessInfoResponse[] | AllJoborderProductProcessInfoResponse[] | ||||
| >([]); | >([]); | ||||
| const [pendingQcCache, setPendingQcCache] = useState< | const [pendingQcCache, setPendingQcCache] = useState< | ||||
| AllJoborderProductProcessInfoResponse[] | AllJoborderProductProcessInfoResponse[] | ||||
| >([]); | >([]); | ||||
| const [putawayedCache, setPutawayedCache] = useState< | |||||
| AllJoborderProductProcessInfoResponse[] | |||||
| >([]); | |||||
| const [bucketCounts, setBucketCounts] = | const [bucketCounts, setBucketCounts] = | ||||
| useState<JobOrderProductProcessBucketCounts>(EMPTY_BUCKET_COUNTS); | useState<JobOrderProductProcessBucketCounts>(EMPTY_BUCKET_COUNTS); | ||||
| const [pendingQcCount, setPendingQcCount] = useState(0); | const [pendingQcCount, setPendingQcCount] = useState(0); | ||||
| @@ -212,19 +217,36 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| const isProductionTab = | const isProductionTab = | ||||
| listTab === "needs_action" || listTab === "pending" || listTab === "processing"; | listTab === "needs_action" || listTab === "pending" || listTab === "processing"; | ||||
| const isCarriedOverTab = listTab === "carried_over"; | 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. */ | /** Production unfinished tabs + pending QC + off-plan: carry-over window. */ | ||||
| const enableCarryOver = | const enableCarryOver = | ||||
| !disableDateFilter && | !disableDateFilter && | ||||
| (isProductionTab || listTab === "pending_qc" || isCarriedOverTab); | (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( | const appliedSearch = useMemo( | ||||
| () => ({ | () => ({ | ||||
| date: listPersistedState.date, | date: listPersistedState.date, | ||||
| @@ -246,67 +268,12 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| [appliedSearch.date], | [appliedSearch.date], | ||||
| ); | ); | ||||
| const [totalJobOrders, setTotalJobOrders] = useState(0); | |||||
| // Generic confirm dialog for actions (update job order / etc.) | // Generic confirm dialog for actions (update job order / etc.) | ||||
| const [confirmOpen, setConfirmOpen] = useState(false); | const [confirmOpen, setConfirmOpen] = useState(false); | ||||
| const [confirmMessage, setConfirmMessage] = useState(""); | const [confirmMessage, setConfirmMessage] = useState(""); | ||||
| const [confirmLoading, setConfirmLoading] = useState(false); | const [confirmLoading, setConfirmLoading] = useState(false); | ||||
| const [pendingConfirmAction, setPendingConfirmAction] = useState<null | (() => Promise<void>)>(null); | 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) => { | const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => { | ||||
| if (!currentUserId) { | if (!currentUserId) { | ||||
| alert(t("Unable to get user ID")); | alert(t("Unable to get user ID")); | ||||
| @@ -387,7 +354,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| })); | })); | ||||
| }, [disableDateFilter, onListPersistedStateChange]); | }, [disableDateFilter, onListPersistedStateChange]); | ||||
| const fetchProcesses = useCallback(async () => { | |||||
| /** Load all tab datasets once per search; tab switches use cache only. */ | |||||
| const loadAllCaches = useCallback(async () => { | |||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const typeParam = filter === "all" ? undefined : filter; | const typeParam = filter === "all" ? undefined : filter; | ||||
| @@ -399,162 +367,133 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| page: 0, | page: 0, | ||||
| size: FETCH_SIZE, | 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) { | } catch (e) { | ||||
| console.error(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); | setCarriedOverCount(0); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| }, [ | |||||
| appliedSearch, | |||||
| disableDateFilter, | |||||
| filter, | |||||
| qcReady, | |||||
| includePutaway, | |||||
| putawayStatus, | |||||
| enableCarryOver, | |||||
| isProductionTab, | |||||
| isCarriedOverTab, | |||||
| ]); | |||||
| }, [appliedSearch, disableDateFilter, filter]); | |||||
| useEffect(() => { | 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( | const handleListTabChange = useCallback( | ||||
| (_: React.SyntheticEvent, value: string) => { | (_: React.SyntheticEvent, value: string) => { | ||||
| @@ -574,32 +513,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| bucketCounts.pickedStarted + bucketCounts.notPickedStarted; | bucketCounts.pickedStarted + bucketCounts.notPickedStarted; | ||||
| const needsActionCount = pendingCount + processingCount; | 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(() => { | const filteredProcesses = useMemo(() => { | ||||
| let list = processes; | |||||
| let list = tabProcesses; | |||||
| if (listTab === "pending") { | if (listTab === "pending") { | ||||
| list = list.filter((p) => | list = list.filter((p) => | ||||
| PENDING_FINE_BUCKETS.has(String(p.pickProcessBucket ?? "")), | PENDING_FINE_BUCKETS.has(String(p.pickProcessBucket ?? "")), | ||||
| @@ -609,10 +526,9 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| PROCESSING_FINE_BUCKETS.has(String(p.pickProcessBucket ?? "")), | PROCESSING_FINE_BUCKETS.has(String(p.pickProcessBucket ?? "")), | ||||
| ); | ); | ||||
| } | } | ||||
| // carried_over: fetch already filtered to off-plan only | |||||
| if (selectedItemCodes.length === 0) return list; | if (selectedItemCodes.length === 0) return list; | ||||
| return list.filter((p) => selectedItemCodes.includes(p.itemCode)); | return list.filter((p) => selectedItemCodes.includes(p.itemCode)); | ||||
| }, [processes, selectedItemCodes, listTab]); | |||||
| }, [tabProcesses, selectedItemCodes, listTab]); | |||||
| const displayTotalJobOrders = isCarriedOverTab | const displayTotalJobOrders = isCarriedOverTab | ||||
| ? filteredProcesses.length | ? filteredProcesses.length | ||||
| @@ -621,8 +537,12 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| ? pendingCount | ? pendingCount | ||||
| : listTab === "processing" | : listTab === "processing" | ||||
| ? processingCount | ? processingCount | ||||
| : totalJobOrders || needsActionCount | |||||
| : totalJobOrders; | |||||
| : needsActionCount | |||||
| : listTab === "pending_qc" | |||||
| ? pendingQcCount | |||||
| : listTab === "putawayed" | |||||
| ? putawayedCount | |||||
| : filteredProcesses.length; | |||||
| const displayCarriedOverCount = useMemo(() => { | const displayCarriedOverCount = useMemo(() => { | ||||
| if (!enableCarryOver || !searchDay) return 0; | if (!enableCarryOver || !searchDay) return 0; | ||||
| @@ -739,14 +659,14 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| // await updateJo({ id: process.jobOrderId, status: "completed" }); | // await updateJo({ id: process.jobOrderId, status: "completed" }); | ||||
| // 4) 刷新列表 | // 4) 刷新列表 | ||||
| await fetchProcesses(); | |||||
| await loadAllCaches(); | |||||
| } catch (e) { | } catch (e) { | ||||
| console.error(e); | console.error(e); | ||||
| alert(t("An error has occurred. Please try again later.")); | alert(t("An error has occurred. Please try again later.")); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| }, [t, fetchProcesses, canManageUpdateJo]); | |||||
| }, [t, loadAllCaches, canManageUpdateJo]); | |||||
| const openConfirm = useCallback((message: string, action: () => Promise<void>) => { | const openConfirm = useCallback((message: string, action: () => Promise<void>) => { | ||||
| setConfirmMessage(message); | setConfirmMessage(message); | ||||
| @@ -771,23 +691,51 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| } | } | ||||
| }, [pendingConfirmAction, closeConfirm]); | }, [pendingConfirmAction, closeConfirm]); | ||||
| const closeNewModal = useCallback(() => { | 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( | const searchedItemOptions = useMemo( | ||||
| () => | () => | ||||
| Array.from( | Array.from( | ||||
| new Map( | new Map( | ||||
| processes | |||||
| [...productionCache, ...pendingQcCache, ...putawayedCache] | |||||
| .filter((p) => !!p.itemCode) | .filter((p) => !!p.itemCode) | ||||
| .map((p) => [p.itemCode, { itemCode: p.itemCode, itemName: p.itemName }]), | .map((p) => [p.itemCode, { itemCode: p.itemCode, itemName: p.itemName }]), | ||||
| ).values(), | ).values(), | ||||
| ), | ), | ||||
| [processes], | |||||
| [productionCache, pendingQcCache, putawayedCache], | |||||
| ); | ); | ||||
| /** Reset 用 ±3 天;preFilled 用目前已套用的條件(與列表查詢一致) */ | /** Reset 用 ±3 天;preFilled 用目前已套用的條件(與列表查詢一致) */ | ||||
| @@ -898,23 +846,31 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| {selectedItemCodes.length > 0 ? ` | ${t("Filtered")}: ${filteredProcesses.length}` : ""} | {selectedItemCodes.length > 0 ? ` | ${t("Filtered")}: ${filteredProcesses.length}` : ""} | ||||
| </Typography> | </Typography> | ||||
| <Tabs | |||||
| <Stack | |||||
| direction="row" | |||||
| alignItems="center" | |||||
| justifyContent="space-between" | |||||
| spacing={2} | |||||
| sx={{ mb: 2, flexWrap: "wrap", rowGap: 1 }} | |||||
| > | |||||
| <Tabs | |||||
| value={listTab} | value={listTab} | ||||
| onChange={handleListTabChange} | onChange={handleListTabChange} | ||||
| variant="scrollable" | variant="scrollable" | ||||
| scrollButtons="auto" | scrollButtons="auto" | ||||
| sx={{ | sx={{ | ||||
| mb: 2, | |||||
| flex: 1, | |||||
| minWidth: 0, | |||||
| borderBottom: 1, | borderBottom: 1, | ||||
| borderColor: "divider", | borderColor: "divider", | ||||
| "& .MuiTabs-flexContainer": { | "& .MuiTabs-flexContainer": { | ||||
| columnGap: 2, | |||||
| columnGap: 0.5, | |||||
| rowGap: 1, | rowGap: 1, | ||||
| }, | }, | ||||
| "& .MuiTab-root": { | "& .MuiTab-root": { | ||||
| overflow: "visible", | overflow: "visible", | ||||
| minWidth: "auto", | minWidth: "auto", | ||||
| px: 2, | |||||
| px: 1, | |||||
| }, | }, | ||||
| }} | }} | ||||
| > | > | ||||
| @@ -949,6 +905,14 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| )} | )} | ||||
| /> | /> | ||||
| </Tabs> | </Tabs> | ||||
| <PrinterSelect | |||||
| label={t("Label Printer")} | |||||
| printers={printerOptions} | |||||
| value={selectedPrinter} | |||||
| onChange={setSelectedPrinter} | |||||
| placeholder={t("Label Printer")} | |||||
| /> | |||||
| </Stack> | |||||
| <Box | <Box | ||||
| sx={{ | sx={{ | ||||
| @@ -1023,13 +987,6 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| } else if (isCarriedOverTab && PROCESSING_FINE_BUCKETS.has(bucket)) { | } else if (isCarriedOverTab && PROCESSING_FINE_BUCKETS.has(bucket)) { | ||||
| chipLabel = t("processing"); | chipLabel = t("processing"); | ||||
| statusColor = "primary"; | 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") { | } else if (statusLower === "completed") { | ||||
| chipLabel = t("Completed"); | chipLabel = t("Completed"); | ||||
| statusColor = "success"; | statusColor = "success"; | ||||
| @@ -1302,6 +1259,24 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| {t("view stockin")} | {t("view stockin")} | ||||
| </Button> | </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> | </Stack> | ||||
| <Typography | <Typography | ||||
| @@ -2,7 +2,7 @@ | |||||
| import React, { useState, useEffect, useCallback } from "react"; | import React, { useState, useEffect, useCallback } from "react"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | 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 { usePathname, useRouter, useSearchParams } from "next/navigation"; | ||||
| import QcStockInModal from "@/components/Qc/QcStockInModal"; | import QcStockInModal from "@/components/Qc/QcStockInModal"; | ||||
| import ProductionProcessList, { | 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. 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 ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => { | ||||
| const { t } = useTranslation(["common", "productionProcess"]); | const { t } = useTranslation(["common", "productionProcess"]); | ||||
| @@ -49,10 +49,6 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| const [linkQcOpen, setLinkQcOpen] = useState(false); | const [linkQcOpen, setLinkQcOpen] = useState(false); | ||||
| const [linkQcSilId, setLinkQcSilId] = useState<number | null>(null); | const [linkQcSilId, setLinkQcSilId] = useState<number | null>(null); | ||||
| const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>( | |||||
| printerCombo && printerCombo.length > 0 ? printerCombo[0] : null | |||||
| ); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (typeof window !== 'undefined') { | if (typeof window !== 'undefined') { | ||||
| try { | try { | ||||
| @@ -123,9 +119,6 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| router.replace(q ? `${pathname}?${q}` : pathname, { scroll: false }); | router.replace(q ? `${pathname}?${q}` : pathname, { scroll: false }); | ||||
| }, [pathname, router, searchParams]); | }, [pathname, router, searchParams]); | ||||
| const listTab = String(productionListState.pickBucket || "needs_action"); | |||||
| const showPrinterBar = tabIndex === 0 && listTab === "pending_qc"; | |||||
| if (selectedMatchingStock) { | if (selectedMatchingStock) { | ||||
| return ( | return ( | ||||
| <JobPickExecutionsecondscan | <JobPickExecutionsecondscan | ||||
| @@ -151,54 +144,6 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Box> | <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 }}> | <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}> | ||||
| <Tab label={t("Production Process")} /> | <Tab label={t("Production Process")} /> | ||||
| <Tab label={t("Job Order Ops Table")} /> | <Tab label={t("Job Order Ops Table")} /> | ||||
| @@ -210,7 +155,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| {tabIndex === 0 && ( | {tabIndex === 0 && ( | ||||
| <ProductionProcessList | <ProductionProcessList | ||||
| printerCombo={selectedPrinter ? [selectedPrinter] : printerCombo} | |||||
| printerCombo={printerCombo} | |||||
| listPersistedState={productionListState} | listPersistedState={productionListState} | ||||
| onListPersistedStateChange={setProductionListState} | onListPersistedStateChange={setProductionListState} | ||||
| onSelectProcess={(jobOrderId) => { | onSelectProcess={(jobOrderId) => { | ||||
| @@ -231,7 +176,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| {tabIndex === 1 && ( | {tabIndex === 1 && ( | ||||
| <JobOrderOpsTable | <JobOrderOpsTable | ||||
| printerCombo={selectedPrinter ? [selectedPrinter] : printerCombo} | |||||
| printerCombo={printerCombo} | |||||
| onSelectProcess={(jobOrderId) => { | onSelectProcess={(jobOrderId) => { | ||||
| if (jobOrderId != null) setSelectedProcessId(jobOrderId); | if (jobOrderId != null) setSelectedProcessId(jobOrderId); | ||||
| }} | }} | ||||
| @@ -1,7 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import { QcItemWithChecks, QcData } from "@/app/api/qc"; | import { QcItemWithChecks, QcData } from "@/app/api/qc"; | ||||
| import { | import { | ||||
| Autocomplete, | |||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| Divider, | Divider, | ||||
| @@ -29,6 +28,7 @@ import dayjs from "dayjs"; | |||||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | import { fetchPoQrcode } from "@/app/api/pdf/actions"; | ||||
| import { downloadFile } from "@/app/utils/commonUtil"; | import { downloadFile } from "@/app/utils/commonUtil"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import PrinterSelect from "@/components/common/PrinterSelect"; | |||||
| import { EscalationResult } from "@/app/api/escalation"; | import { EscalationResult } from "@/app/api/escalation"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { GridRowModesModel } from "@mui/x-data-grid"; | import { GridRowModesModel } from "@mui/x-data-grid"; | ||||
| @@ -72,6 +72,7 @@ interface Props extends CommonProps { | |||||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | // 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> = ({ | const QcStockInModal: React.FC<Props> = ({ | ||||
| open, | open, | ||||
| onClose, | onClose, | ||||
| @@ -840,27 +841,17 @@ const printQrcode = useCallback( | |||||
| </Grid> | </Grid> | ||||
| {tabIndex == 1 && ( | {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 | <TextField | ||||
| variant="outlined" | variant="outlined" | ||||
| label={t("Print Qty")} | 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)", | "Prep Time (Minutes)": "Prep Time (Minutes)", | ||||
| "Previous page": "Previous page", | "Previous page": "Previous page", | ||||
| "Printer": "Printer", | "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": "Process", | ||||
| "Process & Equipment": "Process & Equipment", | "Process & Equipment": "Process & Equipment", | ||||
| "Process Description": "Process Description", | "Process Description": "Process Description", | ||||
| @@ -151,6 +151,8 @@ | |||||
| "Found": "Found", | "Found": "Found", | ||||
| "escalation processing": "Escalation Processing", | "escalation processing": "Escalation Processing", | ||||
| "Printer": "Printer", | "Printer": "Printer", | ||||
| "Label Printer": "Label Printer", | |||||
| "A4 Printer": "A4 Printer", | |||||
| "Printing": "Printing", | "Printing": "Printing", | ||||
| "rejectQty": "Reject Qty", | "rejectQty": "Reject Qty", | ||||
| "QC decision is required": "QC decision is required", | "QC decision is required": "QC decision is required", | ||||
| @@ -138,6 +138,13 @@ | |||||
| "Prep Time (Minutes)": "準備時間(分鐘)", | "Prep Time (Minutes)": "準備時間(分鐘)", | ||||
| "Previous page": "上一頁", | "Previous page": "上一頁", | ||||
| "Printer": "列印機", | "Printer": "列印機", | ||||
| "Label Printer": "標籤打印機", | |||||
| "A4 Printer": "A4 打印機", | |||||
| "Print Label": "列印標籤", | |||||
| "Printing": "列印中", | |||||
| "Print job sent successfully": "列印作業已成功送出", | |||||
| "Please select a 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": "標籤打印機", | |||||
| "A4 Printer": "A4 打印機", | |||||
| "Printing": "列印中", | "Printing": "列印中", | ||||
| "rejectQty": "拒絕數量", | "rejectQty": "拒絕數量", | ||||
| "QC decision is required": "請決定品檢結果", | "QC decision is required": "請決定品檢結果", | ||||