| @@ -504,7 +504,7 @@ export default function ReportPage() { | |||
| setLoading={setLoading} | |||
| reportTitle={currentReport.title} | |||
| /> | |||
| ) : currentReport.id === 'rep-013' || currentReport.id === 'rep-009' || currentReport.id === 'rep-012' ? ( | |||
| ) : currentReport.id === 'rep-013' || currentReport.id === 'rep-009' || currentReport.id === 'rep-012' || currentReport.id === 'rep-004' || currentReport.id === 'rep-007' || currentReport.id === 'rep-008' || currentReport.id === 'rep-011' ? ( | |||
| <> | |||
| <Button | |||
| variant="contained" | |||
| @@ -352,6 +352,8 @@ export interface AllJoborderProductProcessInfoResponse { | |||
| uom: string; | |||
| isDrink?: boolean | null; | |||
| stockInLineId: number; | |||
| /** Stock-in-line current status (e.g. receiving/received/partially_completed/completed/rejected). */ | |||
| stockInLineStatus?: string | null; | |||
| jobOrderCode: string; | |||
| productProcessLineCount: number; | |||
| FinishedProductProcessLineCount: number; | |||
| @@ -798,6 +800,8 @@ export const fetchJoborderProductProcessesPage = cache(async (params: { | |||
| qcReady?: boolean | null; | |||
| type?: string | null; | |||
| includePutaway?: boolean | null; | |||
| /** all | completed | notCompleted */ | |||
| putawayStatus?: string | null; | |||
| page?: number; | |||
| size?: number; | |||
| }) => { | |||
| @@ -808,6 +812,7 @@ export const fetchJoborderProductProcessesPage = cache(async (params: { | |||
| bomIds, | |||
| qcReady, | |||
| includePutaway, | |||
| putawayStatus, | |||
| type, | |||
| page = 0, | |||
| size = 50, | |||
| @@ -825,6 +830,7 @@ export const fetchJoborderProductProcessesPage = cache(async (params: { | |||
| if (includePutaway !== undefined && includePutaway !== null) { | |||
| queryParts.push(`includePutaway=${includePutaway}`); | |||
| } | |||
| if (putawayStatus) queryParts.push(`putawayStatus=${encodeURIComponent(putawayStatus)}`); | |||
| queryParts.push(`page=${page}`); | |||
| queryParts.push(`size=${size}`); | |||
| @@ -77,7 +77,6 @@ import QrCodeIcon from "@mui/icons-material/QrCode"; | |||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { AUTH } from "@/authorities"; | |||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | |||
| import GoodPickExecutionForm from "./GoodPickExecutionForm"; | |||
| import FGPickOrderCard from "./FGPickOrderCard"; | |||
| @@ -687,8 +686,6 @@ const PickExecution: React.FC<Props> = ({ | |||
| const { t } = useTranslation("pickOrder"); | |||
| const router = useRouter(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const abilities = session?.abilities ?? session?.user?.abilities ?? []; | |||
| const isAdmin = abilities.some((a) => String(a).trim() === AUTH.ADMIN); | |||
| const [doPickOrderDetail, setDoPickOrderDetail] = | |||
| useState<DoPickOrderDetail | null>(null); | |||
| const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>( | |||
| @@ -783,6 +780,25 @@ const PickExecution: React.FC<Props> = ({ | |||
| useState<any | null>(null); | |||
| const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]); | |||
| const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); | |||
| const lotFloorPrefixFilter = useMemo(() => { | |||
| const storeId = String(fgPickOrders?.[0]?.storeId ?? "") | |||
| .trim() | |||
| .toUpperCase() | |||
| .replace(/\s/g, ""); | |||
| // e.g. "2/F" -> "2F-", "4/F" -> "4F-" | |||
| const floorKey = storeId.replace(/\//g, ""); | |||
| return floorKey ? `${floorKey}-` : ""; | |||
| }, [fgPickOrders]); | |||
| const defaultLabelPrinterName = useMemo(() => { | |||
| const storeId = String(fgPickOrders?.[0]?.storeId ?? "") | |||
| .trim() | |||
| .toUpperCase() | |||
| .replace(/\s/g, ""); | |||
| const floorKey = storeId.replace(/\//g, ""); | |||
| if (floorKey === "2F") return "Label機 2F A+B"; | |||
| if (floorKey === "4F") return "Label機 4F 乾貨 C, D"; | |||
| return undefined; | |||
| }, [fgPickOrders]); | |||
| // Add these missing state variables after line 352 | |||
| const [isManualScanning, setIsManualScanning] = useState<boolean>(false); | |||
| // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling | |||
| @@ -1690,7 +1706,7 @@ const PickExecution: React.FC<Props> = ({ | |||
| return; | |||
| } | |||
| if (switchedToUnavailable && isAdmin) { | |||
| if (switchedToUnavailable) { | |||
| const itemId = Number(sel?.itemId ?? exp?.itemId); | |||
| const stockInLineId = Number(newStockInLineId); | |||
| if (Number.isFinite(itemId) && Number.isFinite(stockInLineId)) { | |||
| @@ -1745,7 +1761,6 @@ const PickExecution: React.FC<Props> = ({ | |||
| resetScan, | |||
| clearLotConfirmationState, | |||
| t, | |||
| isAdmin, | |||
| ], | |||
| ); | |||
| @@ -2138,7 +2153,7 @@ const PickExecution: React.FC<Props> = ({ | |||
| const byLotId = new Map<number, any>(); | |||
| const byLotNo = new Map<string, any[]>(); | |||
| const byStockInLineId = new Map<number, any[]>(); | |||
| // Cache active lots separately to avoid filtering on every scan | |||
| const activeLotsByItemId = new Map<number, any[]>(); | |||
| const rejectedStatuses = new Set(["rejected"]); | |||
| @@ -5055,9 +5070,12 @@ const PickExecution: React.FC<Props> = ({ | |||
| setLotLabelPrintReminderText(null); | |||
| }} | |||
| initialPayload={lotLabelPrintInitialPayload} | |||
| defaultPrinterName="Label機 2F A+B" | |||
| defaultPrinterName={defaultLabelPrinterName} | |||
| hideScanSection | |||
| reminderText={lotLabelPrintReminderText ?? undefined} | |||
| statusTitleText="此批號的已用完/已過期" | |||
| warehouseCodePrefixFilter={lotFloorPrefixFilter} | |||
| hideTriggeredLot | |||
| /> | |||
| </FormProvider> | |||
| </TestQrCodeProvider> | |||
| @@ -75,6 +75,12 @@ export interface LotLabelPrintModalProps { | |||
| hideScanSection?: boolean; | |||
| /** 額外提醒(顯示在最上方) */ | |||
| reminderText?: string; | |||
| /** 額外標題(顯示在最上方,reminderText 之下) */ | |||
| statusTitleText?: string; | |||
| /** 只顯示特定倉位前綴(例如 "2F-") */ | |||
| warehouseCodePrefixFilter?: string; | |||
| /** 不顯示觸發視窗的批號(analysis.scanned) */ | |||
| hideTriggeredLot?: boolean; | |||
| } | |||
| function safeParseScanPayload(raw: string): ScanPayload | null { | |||
| @@ -115,6 +121,9 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| defaultPrinterName, | |||
| hideScanSection, | |||
| reminderText, | |||
| statusTitleText, | |||
| warehouseCodePrefixFilter, | |||
| hideTriggeredLot, | |||
| }) => { | |||
| const scanInputRef = useRef<HTMLInputElement | null>(null); | |||
| const [scanInput, setScanInput] = useState(""); | |||
| @@ -126,6 +135,7 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| const [analysisLoading, setAnalysisLoading] = useState(false); | |||
| const [analysis, setAnalysis] = useState<QrCodeAnalysisResponse | null>(null); | |||
| const [lastPayload, setLastPayload] = useState<ScanPayload | null>(null); | |||
| const [printQty, setPrintQty] = useState(1); | |||
| const [printingLotLineId, setPrintingLotLineId] = useState<number | null>( | |||
| @@ -234,6 +244,7 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| return; | |||
| } | |||
| setLastPayload(payload); | |||
| setScanError(null); | |||
| setAnalysisLoading(true); | |||
| try { | |||
| @@ -279,6 +290,19 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| await analyzePayload(payload); | |||
| }, [scanInput, analyzePayload]); | |||
| const handleRefreshLots = useCallback(async () => { | |||
| const payload = lastPayload ?? safeParseScanPayload(scanInput.trim()); | |||
| if (!payload) { | |||
| setSnackbar({ | |||
| open: true, | |||
| message: "請先掃碼或查詢一次,才可刷新批號清單。", | |||
| severity: "info", | |||
| }); | |||
| return; | |||
| } | |||
| await analyzePayload(payload); | |||
| }, [analyzePayload, lastPayload, scanInput]); | |||
| useEffect(() => { | |||
| if (!open) return; | |||
| if (!initialPayload) return; | |||
| @@ -310,14 +334,22 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| : null; | |||
| const merged = [ | |||
| ...(scannedLot ? [scannedLot] : []), | |||
| ...(!hideTriggeredLot && scannedLot ? [scannedLot] : []), | |||
| ...list | |||
| .filter((x) => x.inventoryLotLineId !== scannedLotLineId) | |||
| .map((x) => ({ ...x, _scanned: false as const })), | |||
| ]; | |||
| return merged; | |||
| }, [analysis]); | |||
| }, [analysis, hideTriggeredLot]); | |||
| const filteredLots = useMemo(() => { | |||
| const prefix = String(warehouseCodePrefixFilter ?? "").trim(); | |||
| if (!prefix) return availableLots; | |||
| return availableLots.filter((lot) => | |||
| String(lot.warehouseCode ?? "").startsWith(prefix), | |||
| ); | |||
| }, [availableLots, warehouseCodePrefixFilter]); | |||
| const selectedPrinter = useMemo(() => { | |||
| if (selectedPrinterId === "") return null; | |||
| @@ -390,6 +422,14 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| <DialogTitle>批號標籤列印</DialogTitle> | |||
| <DialogContent> | |||
| <Stack spacing={2} sx={{ mt: 1 }}> | |||
| {statusTitleText ? ( | |||
| <Typography | |||
| variant="h6" | |||
| sx={{ fontWeight: 800, color: "error.main" }} | |||
| > | |||
| {statusTitleText} | |||
| </Typography> | |||
| ) : null} | |||
| {reminderText ? ( | |||
| <Alert severity="warning">{reminderText}</Alert> | |||
| ) : null} | |||
| @@ -485,13 +525,13 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => void loadPrinters()} | |||
| disabled={printersLoading} | |||
| onClick={() => void handleRefreshLots()} | |||
| disabled={analysisLoading} | |||
| > | |||
| {printersLoading ? ( | |||
| {analysisLoading ? ( | |||
| <CircularProgress size={18} /> | |||
| ) : ( | |||
| "重新載入印表機" | |||
| "刷新批號清單" | |||
| )} | |||
| </Button> | |||
| @@ -512,13 +552,13 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| 品號:{analysis.itemCode} {analysis.itemName} | |||
| </Typography> | |||
| {availableLots.length === 0 ? ( | |||
| {filteredLots.length === 0 ? ( | |||
| <Alert severity="warning"> | |||
| 找不到可用批號(availableQty > 0)。 | |||
| 找不到該樓層有可用批號(availableQty > 0)。 | |||
| </Alert> | |||
| ) : ( | |||
| <Stack spacing={1}> | |||
| {availableLots.map((lot) => { | |||
| {filteredLots.map((lot) => { | |||
| const isPrinting = | |||
| printingLotLineId === lot.inventoryLotLineId; | |||
| const loc = String(lot.warehouseCode ?? "").trim(); | |||
| @@ -60,6 +60,9 @@ interface ProductProcessListProps { | |||
| onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void; | |||
| printerCombo: PrinterCombo[]; | |||
| qcReady: boolean; | |||
| includePutaway?: boolean | null; | |||
| /** all | completed | notCompleted */ | |||
| putawayStatus?: string | null; | |||
| listPersistedState: ProductionProcessListPersistedState; | |||
| onListPersistedStateChange: React.Dispatch< | |||
| React.SetStateAction<ProductionProcessListPersistedState> | |||
| @@ -93,6 +96,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| printerCombo, | |||
| onSelectMatchingStock, | |||
| qcReady, | |||
| includePutaway, | |||
| putawayStatus, | |||
| listPersistedState, | |||
| onListPersistedStateChange, | |||
| }) => { | |||
| @@ -258,7 +263,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| itemCode: appliedSearch.itemCode, | |||
| jobOrderCode: appliedSearch.jobOrderCode, | |||
| qcReady, | |||
| includePutaway: qcReady ? true : null, | |||
| includePutaway: includePutaway ?? (qcReady ? true : null), | |||
| putawayStatus, | |||
| type: typeParam, | |||
| page, | |||
| size: PAGE_SIZE, | |||
| @@ -273,7 +279,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [listPersistedState, qcReady]); | |||
| }, [listPersistedState, qcReady, includePutaway, putawayStatus]); | |||
| useEffect(() => { | |||
| fetchProcesses(); | |||
| @@ -36,7 +36,10 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| const [productionListState, setProductionListState] = useState( | |||
| createDefaultProductionProcessListPersistedState, | |||
| ); | |||
| const [finishedQcListState, setFinishedQcListState] = useState( | |||
| const [waitingPutawayListState, setWaitingPutawayListState] = useState( | |||
| createDefaultProductionProcessListPersistedState, | |||
| ); | |||
| const [putawayedListState, setPutawayedListState] = useState( | |||
| createDefaultProductionProcessListPersistedState, | |||
| ); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -199,7 +202,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}> | |||
| <Tab label={t("Production Process")} /> | |||
| <Tab label={t("Finished QC Job Orders")} /> | |||
| <Tab label={t("Waiting QC Put Away Job Orders")} /> | |||
| <Tab label={t("Put Awayed Job Orders")} /> | |||
| <Tab label={t("Job Process Status Dashboard")} /> | |||
| <Tab label={t("Operator KPI Dashboard")} /> | |||
| <Tab label={t("Production Equipment Status Dashboard")} /> | |||
| @@ -231,8 +235,10 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| <ProductionProcessList | |||
| printerCombo={printerCombo} | |||
| qcReady={true} | |||
| listPersistedState={finishedQcListState} | |||
| onListPersistedStateChange={setFinishedQcListState} | |||
| includePutaway={true} | |||
| putawayStatus="notCompleted" | |||
| listPersistedState={waitingPutawayListState} | |||
| onListPersistedStateChange={setWaitingPutawayListState} | |||
| onSelectProcess={(jobOrderId) => { | |||
| const id = jobOrderId ?? null; | |||
| if (id !== null) { | |||
| @@ -248,13 +254,36 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| }} | |||
| /> | |||
| )} | |||
| {tabIndex === 2 && ( | |||
| <JobProcessStatus /> | |||
| {tabIndex === 2 && ( | |||
| <ProductionProcessList | |||
| printerCombo={printerCombo} | |||
| qcReady={true} | |||
| includePutaway={true} | |||
| putawayStatus="completed" | |||
| listPersistedState={putawayedListState} | |||
| onListPersistedStateChange={setPutawayedListState} | |||
| onSelectProcess={(jobOrderId) => { | |||
| const id = jobOrderId ?? null; | |||
| if (id !== null) { | |||
| setSelectedProcessId(id); | |||
| } | |||
| }} | |||
| onSelectMatchingStock={(jobOrderId, productProcessId, pickOrderId) => { | |||
| setSelectedMatchingStock({ | |||
| jobOrderId: jobOrderId || 0, | |||
| productProcessId: productProcessId || 0, | |||
| pickOrderId: pickOrderId || 0, | |||
| }); | |||
| }} | |||
| /> | |||
| )} | |||
| {tabIndex === 3 && ( | |||
| <OperatorKpiDashboard /> | |||
| <JobProcessStatus /> | |||
| )} | |||
| {tabIndex === 4 && ( | |||
| <OperatorKpiDashboard /> | |||
| )} | |||
| {tabIndex === 5 && ( | |||
| <EquipmentStatusDashboard /> | |||
| )} | |||
| </Box> | |||
| @@ -128,6 +128,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId | |||
| ...defaultNewValue, | |||
| }, | |||
| }); | |||
| const { isSubmitting } = formProps.formState; | |||
| const errors = formProps.formState.errors; | |||
| useEffect(() => { | |||
| @@ -646,7 +647,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId | |||
| }, | |||
| }} | |||
| // onClick={formProps.handleSubmit()} | |||
| disabled={!verified || qtyError != ""} | |||
| disabled={!verified || qtyError != "" || isSubmitting} | |||
| > | |||
| {t("confirm putaway")} | |||
| </Button> | |||
| @@ -12,6 +12,8 @@ | |||
| "Please Select BOM": "請選擇 BOM", | |||
| "No Lot": "沒有批號", | |||
| "Select All": "全選", | |||
| "Waiting QC Put Away Job Orders": "待QC上架工單", | |||
| "Put Awayed Job Orders": "已上架工單", | |||
| "Loading BOM Detail...": "正在載入 BOM 明細…", | |||
| "Output Quantity": "使用數量", | |||
| "Process & Equipment": "製程與設備", | |||