| @@ -524,6 +524,7 @@ export interface PickOrderLineWithLotsResponse { | |||
| uomCode: string | null; | |||
| uomDesc: string | null; | |||
| status: string | null; | |||
| handler: string | null; | |||
| lots: LotDetailResponse[]; | |||
| } | |||
| @@ -868,9 +869,9 @@ export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => | |||
| ); | |||
| }); | |||
| // 获取已完成的 Job Order pick orders | |||
| export const fetchCompletedJobOrderPickOrdersrecords = cache(async (userId: number) => { | |||
| export const fetchCompletedJobOrderPickOrdersrecords = cache(async () => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/jo/completed-job-order-pick-orders-only/${userId}`, | |||
| `${BASE_API_URL}/jo/completed-job-order-pick-orders-only`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["jo-completed"] }, | |||
| @@ -196,16 +196,19 @@ useEffect(() => { | |||
| if (verifiedQty === undefined || verifiedQty < 0) { | |||
| newErrors.actualPickQty = t('Qty is required'); | |||
| } | |||
| const totalQty = verifiedQty + badItemQty + missQty; | |||
| const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0; | |||
| // ✅ 新增:必须至少有一个 > 0 | |||
| if (!hasAnyValue) { | |||
| newErrors.actualPickQty = t('At least one of Verified / Missing / Bad must be greater than 0'); | |||
| } | |||
| if (hasAnyValue && totalQty !== requiredQty) { | |||
| newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); | |||
| } | |||
| setErrors(newErrors); | |||
| return Object.keys(newErrors).length === 0; | |||
| }; | |||
| @@ -214,9 +217,10 @@ useEffect(() => { | |||
| return; | |||
| } | |||
| // Handle normal pick submission: verifiedQty > 0 with no issues, OR all zeros (verifiedQty=0, missQty=0, badItemQty=0) | |||
| const isNormalPick = (verifiedQty > 0 || (verifiedQty === 0 && formData.missQty == 0 && formData.badItemQty == 0)) | |||
| && formData.missQty == 0 && formData.badItemQty == 0; | |||
| // ✅ 只允许 Verified>0 且没有问题时,走 normal pick | |||
| const isNormalPick = verifiedQty > 0 | |||
| && formData.missQty == 0 | |||
| && formData.badItemQty == 0; | |||
| if (isNormalPick) { | |||
| if (onNormalPickSubmit) { | |||
| @@ -235,11 +239,12 @@ useEffect(() => { | |||
| } | |||
| return; | |||
| } | |||
| // ❌ 有问题(或全部为 0)才进入 Issue 提报流程 | |||
| if (!validateForm() || !formData.pickOrderId) { | |||
| return; | |||
| } | |||
| setLoading(true); | |||
| try { | |||
| const submissionData = { | |||
| @@ -487,7 +487,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| matchStatus: lot.matchStatus, | |||
| routerArea: lot.routerArea, | |||
| routerRoute: lot.routerRoute, | |||
| uomShortDesc: lot.uomShortDesc | |||
| uomShortDesc: lot.uomShortDesc, | |||
| handler: lot.handler, | |||
| }); | |||
| }); | |||
| } | |||
| @@ -1173,6 +1174,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| <TableRow> | |||
| <TableCell>{t("Index")}</TableCell> | |||
| <TableCell>{t("Route")}</TableCell> | |||
| <TableCell>{t("Handler")}</TableCell> | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| @@ -1212,6 +1214,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| {lot.routerRoute || '-'} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell>{lot.handler || '-'}</TableCell> | |||
| <TableCell>{lot.itemCode}</TableCell> | |||
| <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | |||
| <TableCell> | |||
| @@ -15,7 +15,7 @@ import { | |||
| import { | |||
| arrayToDayjs, | |||
| } from "@/app/utils/formatUtil"; | |||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material"; | |||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box, TextField, Autocomplete } from "@mui/material"; | |||
| import Jodetail from "./Jodetail" | |||
| import PickExecution from "./JobPickExecution"; | |||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | |||
| @@ -63,12 +63,18 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders, printerCombo }) => { | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const [isAssigning, setIsAssigning] = useState(false); | |||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false); | |||
| const [hasDataTab0, setHasDataTab0] = useState(false); | |||
| const [hasDataTab1, setHasDataTab1] = useState(false); | |||
| const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| //const [printers, setPrinters] = useState<PrinterCombo[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false); | |||
| const [hasDataTab0, setHasDataTab0] = useState(false); | |||
| const [hasDataTab1, setHasDataTab1] = useState(false); | |||
| const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| // Add printer selection state | |||
| const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>( | |||
| printerCombo && printerCombo.length > 0 ? printerCombo[0] : null | |||
| ); | |||
| const [printQty, setPrintQty] = useState<number>(1); | |||
| const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | |||
| typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | |||
| ); | |||
| @@ -98,21 +104,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| window.removeEventListener('jobOrderDataStatus', handleJobOrderDataChange as EventListener); | |||
| }; | |||
| }, []); | |||
| /* | |||
| useEffect(() => { | |||
| const fetchPrinters = async () => { | |||
| try { | |||
| // 需要创建一个客户端版本的 fetchPrinterCombo | |||
| // 或者使用 API 路由 | |||
| // const printersData = await fetch('/api/printers/combo').then(r => r.json()); | |||
| // setPrinters(printersData); | |||
| } catch (error) { | |||
| console.error("Error fetching printers:", error); | |||
| } | |||
| }; | |||
| fetchPrinters(); | |||
| }, []); | |||
| */ | |||
| useEffect(() => { | |||
| const onAssigned = () => { | |||
| localStorage.removeItem('hideCompletedUntilNext'); | |||
| @@ -121,7 +113,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| window.addEventListener('pickOrderAssigned', onAssigned); | |||
| return () => window.removeEventListener('pickOrderAssigned', onAssigned); | |||
| }, []); | |||
| // ... existing code ... | |||
| useEffect(() => { | |||
| const handleCompletionStatusChange = (event: CustomEvent) => { | |||
| @@ -139,7 +130,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| return () => { | |||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||
| }; | |||
| }, [tabIndex]); // 添加 tabIndex 依赖 | |||
| }, [tabIndex]); | |||
| // 新增:处理标签页切换时的打印按钮状态重置 | |||
| useEffect(() => { | |||
| @@ -150,7 +141,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| } | |||
| }, [tabIndex]); | |||
| // ... existing code ... | |||
| const handleAssignByStore = async (storeId: "2/F" | "4/F") => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| @@ -430,71 +420,89 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| return ( | |||
| <Box sx={{ | |||
| height: '100vh', // Full viewport height | |||
| overflow: 'auto' // Single scrollbar for the whole page | |||
| height: '100vh', | |||
| overflow: 'auto' | |||
| }}> | |||
| {/* Header section */} | |||
| <Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}> | |||
| <Stack rowGap={2}> | |||
| <Grid container alignItems="center"> | |||
| <Grid item xs={8}> | |||
| </Grid> | |||
| {/* Last 2 buttons aligned right | |||
| <Grid item xs={6} > | |||
| {!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && ( | |||
| <Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}> | |||
| <Typography variant="h6" gutterBottom> | |||
| {t("Unassigned Job Orders")} ({unassignedOrders.length}) | |||
| </Typography> | |||
| <Stack direction="row" spacing={1} flexWrap="wrap"> | |||
| {unassignedOrders.map((order) => ( | |||
| <Button | |||
| key={order.pickOrderId} | |||
| variant="outlined" | |||
| size="small" | |||
| onClick={() => handleAssignOrder(order.pickOrderId)} | |||
| disabled={isLoadingUnassigned} | |||
| > | |||
| {order.pickOrderCode} - {order.jobOrderName} | |||
| </Button> | |||
| ))} | |||
| </Stack> | |||
| </Box> | |||
| )} | |||
| </Grid> | |||
| */} | |||
| {/* Header section with printer selection */} | |||
| <Box sx={{ | |||
| p: 1, | |||
| borderBottom: '1px solid #e0e0e0', | |||
| minHeight: 'auto', | |||
| display: 'flex', | |||
| alignItems: 'center', | |||
| justifyContent: 'space-between', | |||
| gap: 2, | |||
| flexWrap: 'wrap', | |||
| }}> | |||
| {/* Left side - Title */} | |||
| </Grid> | |||
| </Stack> | |||
| </Box> | |||
| {/* Right side - Printer selection (only show on tab 1) */} | |||
| {tabIndex === 1 && ( | |||
| <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 | |||
| options={printerCombo || []} | |||
| getOptionLabel={(option) => | |||
| option.name || option.label || option.code || `Printer ${option.id}` | |||
| } | |||
| value={selectedPrinter} | |||
| onChange={(_, newValue) => setSelectedPrinter(newValue)} | |||
| sx={{ minWidth: 200 }} | |||
| size="small" | |||
| renderInput={(params) => ( | |||
| <TextField {...params} placeholder={t("Printer")} /> | |||
| )} | |||
| /> | |||
| <Typography variant="body2" sx={{ minWidth: 'fit-content', ml: 1 }}> | |||
| {t("Print Quantity")}: | |||
| </Typography> | |||
| <TextField | |||
| type="number" | |||
| label={t("Print Quantity")} | |||
| value={printQty} | |||
| onChange={(e) => { | |||
| const value = parseInt(e.target.value) || 1; | |||
| setPrintQty(Math.max(1, value)); | |||
| }} | |||
| inputProps={{ min: 1, step: 1 }} | |||
| sx={{ width: 120 }} | |||
| size="small" | |||
| /> | |||
| </Stack> | |||
| )} | |||
| </Box> | |||
| {/* Tabs section - Move the click handler here */} | |||
| {/* Tabs section */} | |||
| <Box sx={{ | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| {/* <Tab label={t("Pick Order Detail")} iconPosition="end" /> */} | |||
| <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> | |||
| <Tab label={t("Complete Job Order Record")} iconPosition="end" /> | |||
| {/* <Tab label={t("Job Order Match")} iconPosition="end" /> */} | |||
| {/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */} | |||
| <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> | |||
| <Tab label={t("Complete Job Order Record")} iconPosition="end" /> | |||
| </Tabs> | |||
| </Box> | |||
| {/* Content section - NO overflow: 'auto' here */} | |||
| <Box sx={{ | |||
| p: 2 | |||
| }}> | |||
| {/* {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} */} | |||
| {tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />} | |||
| {/* Content section */} | |||
| <Box sx={{ p: 2 }}> | |||
| {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />} | |||
| {/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */} | |||
| {/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */} | |||
| {tabIndex === 1 && ( | |||
| <CompleteJobOrderRecord | |||
| filterArgs={filterArgs} | |||
| printerCombo={printerCombo} | |||
| selectedPrinter={selectedPrinter} | |||
| printQty={printQty} | |||
| /> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| @@ -49,6 +49,8 @@ import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| interface Props { | |||
| filterArgs: Record<string, any>; | |||
| printerCombo: PrinterCombo[]; | |||
| selectedPrinter?: PrinterCombo | null; | |||
| printQty?: number; | |||
| } | |||
| // 修改:已完成的 Job Order Pick Order 接口 | |||
| @@ -101,7 +103,12 @@ interface LotDetail { | |||
| uomDesc: string; | |||
| } | |||
| const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => { | |||
| const CompleteJobOrderRecord: React.FC<Props> = ({ | |||
| filterArgs, | |||
| printerCombo, | |||
| selectedPrinter: selectedPrinterProp, | |||
| printQty: printQtyProp | |||
| }) => { | |||
| const { t } = useTranslation("jo"); | |||
| const router = useRouter(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -121,25 +128,11 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| // 修改:搜索状态 | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | |||
| //const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]); | |||
| const defaultDemoPrinter: PrinterCombo = { | |||
| id: 2, | |||
| value: 2, | |||
| name: "2fi", | |||
| label: "2fi", | |||
| code: "2fi" | |||
| }; | |||
| const availablePrinters = useMemo(() => { | |||
| if (printerCombo.length === 0) { | |||
| console.log("No printers available, using default demo printer"); | |||
| return [defaultDemoPrinter]; | |||
| } | |||
| return printerCombo; | |||
| }, [printerCombo]); | |||
| const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>( | |||
| printerCombo && printerCombo.length > 0 ? printerCombo[0] : null | |||
| ); | |||
| const [printQty, setPrintQty] = useState<number>(1); | |||
| // Use props with fallback | |||
| const selectedPrinter = selectedPrinterProp ?? (printerCombo && printerCombo.length > 0 ? printerCombo[0] : null); | |||
| const printQty = printQtyProp ?? 1; | |||
| // 修改:分页状态 | |||
| const [paginationController, setPaginationController] = useState({ | |||
| pageNum: 0, | |||
| @@ -157,7 +150,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| try { | |||
| console.log("🔍 Fetching completed Job Order pick orders (pick completed only)..."); | |||
| const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(currentUserId); | |||
| const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(); | |||
| // Fix: Ensure the data is always an array | |||
| const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; | |||
| @@ -226,7 +219,19 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| setFilteredJobOrderPickOrders(filtered); | |||
| console.log("Filtered Job Order pick orders count:", filtered.length); | |||
| }, [completedJobOrderPickOrders]); | |||
| const formatDateTime = (value: any) => { | |||
| if (!value) return "-"; | |||
| // 后端发来的是 [yyyy, MM, dd, HH, mm, ss] | |||
| if (Array.isArray(value)) { | |||
| const [year, month, day, hour = 0, minute = 0, second = 0] = value; | |||
| return new Date(year, month - 1, day, hour, minute, second).toLocaleString(); | |||
| } | |||
| // 如果以后改成字符串/ISO,也兼容 | |||
| const d = new Date(value); | |||
| return isNaN(d.getTime()) ? "-" : d.toLocaleString(); | |||
| }; | |||
| // 修改:重置搜索 | |||
| const handleSearchReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| @@ -433,18 +438,6 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| <strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} | |||
| </Typography> | |||
| </Stack> | |||
| {/* | |||
| <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap" sx={{ mt: 2 }}> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => handlePickRecord(selectedJobOrderPickOrder)} | |||
| sx={{ mt: 1 }} | |||
| > | |||
| {t("Print Pick Record")} | |||
| </Button> | |||
| </Stack> | |||
| */} | |||
| </CardContent> | |||
| </Card> | |||
| @@ -600,37 +593,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | |||
| {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} | |||
| </Typography> | |||
| <Box sx={{ mb: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1, bgcolor: 'background.paper' }}> | |||
| <Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap"> | |||
| <Typography variant="subtitle1" sx={{ minWidth: 'fit-content' }}> | |||
| {t("Select Printer")}: | |||
| </Typography> | |||
| <Autocomplete | |||
| options={availablePrinters} | |||
| getOptionLabel={(option) => option.name || option.label || option.code || `Printer ${option.id}`} | |||
| value={selectedPrinter} | |||
| onChange={(_, newValue) => setSelectedPrinter(newValue)} | |||
| sx={{ minWidth: 250 }} | |||
| size="small" | |||
| renderInput={(params) => <TextField {...params} label={t("Printer")} />} | |||
| /> | |||
| <Typography variant="subtitle1" sx={{ minWidth: 'fit-content' }}> | |||
| {t("Print Quantity")}: | |||
| </Typography> | |||
| <TextField | |||
| type="number" | |||
| label={t("Print Quantity")} | |||
| value={printQty} | |||
| onChange={(e) => { | |||
| const value = parseInt(e.target.value) || 1; | |||
| setPrintQty(Math.max(1, value)); | |||
| }} | |||
| inputProps={{ min: 1, step: 1 }} | |||
| sx={{ width: 120 }} | |||
| size="small" | |||
| /> | |||
| </Stack> | |||
| </Box> | |||
| {/* 列表 */} | |||
| {filteredJobOrderPickOrders.length === 0 ? ( | |||
| <Box sx={{ p: 3, textAlign: 'center' }}> | |||
| @@ -652,7 +615,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()} | |||
| {t("Completed")}: {formatDateTime(jobOrderPickOrder.planEnd)} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} | |||
| @@ -42,8 +42,6 @@ import { | |||
| } from "@/app/api/pickOrder/actions"; | |||
| // 修改:使用 Job Order API | |||
| import { | |||
| //fetchJobOrderLotsHierarchical, | |||
| //fetchUnassignedJobOrderPickOrders, | |||
| assignJobOrderPickOrder, | |||
| fetchJobOrderLotsHierarchicalByPickOrderId, | |||
| updateJoPickOrderHandledBy, | |||
| @@ -412,6 +410,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| pickOrderType: data.pickOrder.type, | |||
| pickOrderStatus: data.pickOrder.status, | |||
| pickOrderAssignTo: data.pickOrder.assignTo, | |||
| handler: line.handler, | |||
| }); | |||
| }); | |||
| } | |||
| @@ -537,6 +536,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| setCombinedDataLoading(false); | |||
| } | |||
| }, [getAllLotsFromHierarchical]); | |||
| const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { | |||
| if (!currentUserId || !pickOrderId || !itemId) { | |||
| return; | |||
| @@ -901,11 +901,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| // Use the first active suggested lot as the "expected" lot | |||
| const expectedLot = activeSuggestedLots[0]; | |||
| // 2) Check if the scanned lot matches exactly | |||
| if (scanned?.lotNo === expectedLot.lotNo) { | |||
| // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) | |||
| console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); | |||
| if (!expectedLot.stockOutLineId) { | |||
| console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); | |||
| setQrScanError(true); | |||
| @@ -922,24 +920,33 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| status: "checked", | |||
| }); | |||
| if (res.code === "checked" || res.code === "SUCCESS") { | |||
| setQrScanError(false); | |||
| setQrScanSuccess(true); | |||
| const updateOk = | |||
| res?.type === "checked" || | |||
| typeof res?.id === "number" || | |||
| (res?.message && res.message.includes("success")); | |||
| if (updateOk) { | |||
| setQrScanError(false); | |||
| setQrScanSuccess(true); | |||
| // ✅ 刷新数据而不是直接更新 state | |||
| const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; | |||
| await fetchJobOrderData(pickOrderId); | |||
| console.log("✅ Status updated, data refreshed"); | |||
| } else if (res.code === "LOT_NUMBER_MISMATCH") { | |||
| console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } else if (res.code === "ITEM_MISMATCH") { | |||
| console.warn("Backend reported ITEM_MISMATCH:", res.message); | |||
| if ( | |||
| expectedLot.pickOrderId && | |||
| expectedLot.itemId && | |||
| (expectedLot.stockOutLineStatus?.toLowerCase?.() === "pending" || | |||
| !expectedLot.stockOutLineStatus) && | |||
| !expectedLot.handler | |||
| ) { | |||
| await updateHandledBy(expectedLot.pickOrderId, expectedLot.itemId); | |||
| } | |||
| const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; | |||
| await fetchJobOrderData(pickOrderId); | |||
| } else if (res?.code === "LOT_NUMBER_MISMATCH" || res?.code === "ITEM_MISMATCH") { | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } else { | |||
| console.warn("Unexpected response code from backend:", res.code); | |||
| console.warn("Unexpected response from backend:", res); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } | |||
| @@ -949,7 +956,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| setQrScanSuccess(false); | |||
| } | |||
| return; // ✅ 直接返回,不再调用 handleQrCodeSubmit | |||
| return; // ✅ 直接返回,不再调用后面的分支 | |||
| } | |||
| // Case 2: Same item, different lot - show confirmation modal | |||
| @@ -977,7 +984,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]); | |||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, updateHandledBy]); | |||
| const handleManualInputSubmit = useCallback(() => { | |||
| @@ -1310,6 +1317,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| console.error("Error submitting pick quantity:", error); | |||
| } | |||
| }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); | |||
| const handleSkip = useCallback(async (lot: any) => { | |||
| try { | |||
| console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo); | |||
| await handleSubmitPickQtyWithQty(lot, 0); | |||
| } catch (err) { | |||
| console.error("Error in Skip:", err); | |||
| } | |||
| }, [handleSubmitPickQtyWithQty]); | |||
| const handleSubmitAllScanned = useCallback(async () => { | |||
| const scannedLots = combinedLotData.filter(lot => | |||
| lot.stockOutLineStatus === 'checked' | |||
| @@ -1544,7 +1559,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| }, [startScan]); | |||
| const handleStopScan = useCallback(() => { | |||
| console.log("⏹️ Stopping manual QR scan..."); | |||
| console.log(" Stopping manual QR scan..."); | |||
| setIsManualScanning(false); | |||
| setQrScanError(false); | |||
| setQrScanSuccess(false); | |||
| @@ -1563,7 +1578,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| }, [isManualScanning, stopScan, resetScan]); | |||
| useEffect(() => { | |||
| if (isManualScanning && combinedLotData.length === 0) { | |||
| console.log("⏹️ No data available, auto-stopping QR scan..."); | |||
| console.log(" No data available, auto-stopping QR scan..."); | |||
| handleStopScan(); | |||
| } | |||
| }, [combinedLotData.length, isManualScanning, handleStopScan]); | |||
| @@ -1677,16 +1692,59 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| </Box> | |||
| </Box> | |||
| {qrScanError && !qrScanSuccess && ( | |||
| <Alert severity="error" sx={{ mb: 2 }}> | |||
| {qrScanError && !qrScanSuccess && ( | |||
| <Alert | |||
| severity="error" | |||
| sx={{ | |||
| mb: 2, | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| fontWeight: "bold", | |||
| fontSize: "1rem", | |||
| color: "error.main", // ✅ 整个 Alert 文字用错误红 | |||
| "& .MuiAlert-message": { | |||
| width: "100%", | |||
| textAlign: "center", | |||
| // color: "error.main", // ✅ 明确指定 message 文字颜色 | |||
| }, | |||
| "& .MuiSvgIcon-root": { | |||
| color: "error.main", // 图标继续红色(可选) | |||
| }, | |||
| backgroundColor: "error.light", | |||
| }} | |||
| > | |||
| {t("QR code does not match any item in current orders.")} | |||
| </Alert> | |||
| )} | |||
| {qrScanSuccess && ( | |||
| <Alert severity="success" sx={{ mb: 2 }}> | |||
| {t("QR code verified.")} | |||
| </Alert> | |||
| )} | |||
| {qrScanSuccess && ( | |||
| <Alert | |||
| severity="success" | |||
| sx={{ | |||
| mb: 2, | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| fontWeight: "bold", | |||
| fontSize: "1rem", | |||
| // 背景用很浅的绿色 | |||
| bgcolor: "rgba(76, 175, 80, 0.08)", | |||
| // 文字用主题 success 绿 | |||
| color: "success.main", | |||
| // 去掉默认强烈的色块感 | |||
| "& .MuiAlert-icon": { | |||
| color: "success.main", | |||
| }, | |||
| "& .MuiAlert-message": { | |||
| width: "100%", | |||
| textAlign: "center", | |||
| color: "success.main", | |||
| }, | |||
| }} | |||
| > | |||
| {t("QR code verified.")} | |||
| </Alert> | |||
| )} | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| @@ -1694,6 +1752,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| <TableRow> | |||
| <TableCell>{t("Index")}</TableCell> | |||
| <TableCell>{t("Route")}</TableCell> | |||
| <TableCell>{t("Handler")}</TableCell> | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| @@ -1733,6 +1792,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| {lot.routerRoute || '-'} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell>{lot.handler || '-'}</TableCell> | |||
| <TableCell>{lot.itemCode}</TableCell> | |||
| <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | |||
| <TableCell> | |||
| @@ -1837,6 +1897,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| > | |||
| {t("Issue")} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| size="small" | |||
| onClick={() => handleSkip(lot)} | |||
| disabled={lot.stockOutLineStatus === 'completed'} | |||
| sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} | |||
| > | |||
| {t("Skip")} | |||
| </Button> | |||
| </Stack> | |||
| </Box> | |||
| </TableCell> | |||
| @@ -173,7 +173,8 @@ const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => { | |||
| const response = await deleteJobOrder(jobOrderId) | |||
| if (response) { | |||
| //setProcessData(response.entity); | |||
| await fetchData(); | |||
| //await fetchData(); | |||
| onBack(); | |||
| } | |||
| }, [jobOrderId]); | |||
| const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| @@ -221,6 +221,7 @@ | |||
| "View Details": "查看詳情", | |||
| "view stockin": "品檢", | |||
| "No completed Job Order pick orders with matching found": "沒有相關記錄", | |||
| "Handler": "提料員", | |||
| "Completed Step": "完成步驟", | |||
| "Continue": "繼續", | |||
| "Executing": "執行中", | |||
| @@ -86,6 +86,8 @@ | |||
| "Job Order Item Name": "工單物料名稱", | |||
| "Job Order Code": "工單編號", | |||
| "View Details": "查看詳情", | |||
| "Skip": "跳過", | |||
| "Handler": "提料員", | |||
| "Required Qty": "需求數量", | |||
| "completed Job Order pick orders with Matching": "工單已完成提料和對料", | |||
| "No completed Job Order pick orders with matching found": "沒有相關記錄", | |||