| @@ -524,6 +524,7 @@ export interface PickOrderLineWithLotsResponse { | |||||
| uomCode: string | null; | uomCode: string | null; | ||||
| uomDesc: string | null; | uomDesc: string | null; | ||||
| status: string | null; | status: string | null; | ||||
| handler: string | null; | |||||
| lots: LotDetailResponse[]; | lots: LotDetailResponse[]; | ||||
| } | } | ||||
| @@ -868,9 +869,9 @@ export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => | |||||
| ); | ); | ||||
| }); | }); | ||||
| // 获取已完成的 Job Order pick orders | // 获取已完成的 Job Order pick orders | ||||
| export const fetchCompletedJobOrderPickOrdersrecords = cache(async (userId: number) => { | |||||
| export const fetchCompletedJobOrderPickOrdersrecords = cache(async () => { | |||||
| return serverFetchJson<any>( | 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", | method: "GET", | ||||
| next: { tags: ["jo-completed"] }, | next: { tags: ["jo-completed"] }, | ||||
| @@ -196,16 +196,19 @@ useEffect(() => { | |||||
| if (verifiedQty === undefined || verifiedQty < 0) { | if (verifiedQty === undefined || verifiedQty < 0) { | ||||
| newErrors.actualPickQty = t('Qty is required'); | newErrors.actualPickQty = t('Qty is required'); | ||||
| } | } | ||||
| const totalQty = verifiedQty + badItemQty + missQty; | const totalQty = verifiedQty + badItemQty + missQty; | ||||
| const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0; | 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) { | if (hasAnyValue && totalQty !== requiredQty) { | ||||
| newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); | newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); | ||||
| } | } | ||||
| setErrors(newErrors); | setErrors(newErrors); | ||||
| return Object.keys(newErrors).length === 0; | return Object.keys(newErrors).length === 0; | ||||
| }; | }; | ||||
| @@ -214,9 +217,10 @@ useEffect(() => { | |||||
| return; | 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 (isNormalPick) { | ||||
| if (onNormalPickSubmit) { | if (onNormalPickSubmit) { | ||||
| @@ -235,11 +239,12 @@ useEffect(() => { | |||||
| } | } | ||||
| return; | return; | ||||
| } | } | ||||
| // ❌ 有问题(或全部为 0)才进入 Issue 提报流程 | |||||
| if (!validateForm() || !formData.pickOrderId) { | if (!validateForm() || !formData.pickOrderId) { | ||||
| return; | return; | ||||
| } | } | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const submissionData = { | const submissionData = { | ||||
| @@ -487,7 +487,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| matchStatus: lot.matchStatus, | matchStatus: lot.matchStatus, | ||||
| routerArea: lot.routerArea, | routerArea: lot.routerArea, | ||||
| routerRoute: lot.routerRoute, | routerRoute: lot.routerRoute, | ||||
| uomShortDesc: lot.uomShortDesc | |||||
| uomShortDesc: lot.uomShortDesc, | |||||
| handler: lot.handler, | |||||
| }); | }); | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -1173,6 +1174,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Index")}</TableCell> | <TableCell>{t("Index")}</TableCell> | ||||
| <TableCell>{t("Route")}</TableCell> | <TableCell>{t("Route")}</TableCell> | ||||
| <TableCell>{t("Handler")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | <TableCell>{t("Item Code")}</TableCell> | ||||
| <TableCell>{t("Item Name")}</TableCell> | <TableCell>{t("Item Name")}</TableCell> | ||||
| <TableCell>{t("Lot No")}</TableCell> | <TableCell>{t("Lot No")}</TableCell> | ||||
| @@ -1212,6 +1214,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| {lot.routerRoute || '-'} | {lot.routerRoute || '-'} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell>{lot.handler || '-'}</TableCell> | |||||
| <TableCell>{lot.itemCode}</TableCell> | <TableCell>{lot.itemCode}</TableCell> | ||||
| <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -15,7 +15,7 @@ import { | |||||
| import { | import { | ||||
| arrayToDayjs, | arrayToDayjs, | ||||
| } from "@/app/utils/formatUtil"; | } 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 Jodetail from "./Jodetail" | ||||
| import PickExecution from "./JobPickExecution"; | import PickExecution from "./JobPickExecution"; | ||||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | 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 [totalCount, setTotalCount] = useState<number>(); | ||||
| const [isAssigning, setIsAssigning] = useState(false); | const [isAssigning, setIsAssigning] = useState(false); | ||||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | 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>( | const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | ||||
| typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | ||||
| ); | ); | ||||
| @@ -98,21 +104,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| window.removeEventListener('jobOrderDataStatus', handleJobOrderDataChange as EventListener); | 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(() => { | useEffect(() => { | ||||
| const onAssigned = () => { | const onAssigned = () => { | ||||
| localStorage.removeItem('hideCompletedUntilNext'); | localStorage.removeItem('hideCompletedUntilNext'); | ||||
| @@ -121,7 +113,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| window.addEventListener('pickOrderAssigned', onAssigned); | window.addEventListener('pickOrderAssigned', onAssigned); | ||||
| return () => window.removeEventListener('pickOrderAssigned', onAssigned); | return () => window.removeEventListener('pickOrderAssigned', onAssigned); | ||||
| }, []); | }, []); | ||||
| // ... existing code ... | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const handleCompletionStatusChange = (event: CustomEvent) => { | const handleCompletionStatusChange = (event: CustomEvent) => { | ||||
| @@ -139,7 +130,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| return () => { | return () => { | ||||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | ||||
| }; | }; | ||||
| }, [tabIndex]); // 添加 tabIndex 依赖 | |||||
| }, [tabIndex]); | |||||
| // 新增:处理标签页切换时的打印按钮状态重置 | // 新增:处理标签页切换时的打印按钮状态重置 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -150,7 +141,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| } | } | ||||
| }, [tabIndex]); | }, [tabIndex]); | ||||
| // ... existing code ... | |||||
| const handleAssignByStore = async (storeId: "2/F" | "4/F") => { | const handleAssignByStore = async (storeId: "2/F" | "4/F") => { | ||||
| if (!currentUserId) { | if (!currentUserId) { | ||||
| console.error("Missing user id in session"); | console.error("Missing user id in session"); | ||||
| @@ -430,71 +420,89 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| return ( | return ( | ||||
| <Box sx={{ | <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={{ | <Box sx={{ | ||||
| borderBottom: '1px solid #e0e0e0' | borderBottom: '1px solid #e0e0e0' | ||||
| }}> | }}> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <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> | </Tabs> | ||||
| </Box> | </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 === 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> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| @@ -49,6 +49,8 @@ import { PrinterCombo } from "@/app/api/settings/printer"; | |||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| selectedPrinter?: PrinterCombo | null; | |||||
| printQty?: number; | |||||
| } | } | ||||
| // 修改:已完成的 Job Order Pick Order 接口 | // 修改:已完成的 Job Order Pick Order 接口 | ||||
| @@ -101,7 +103,12 @@ interface LotDetail { | |||||
| uomDesc: string; | 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 { t } = useTranslation("jo"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | 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 [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | ||||
| const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | 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({ | const [paginationController, setPaginationController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| @@ -157,7 +150,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| try { | try { | ||||
| console.log("🔍 Fetching completed Job Order pick orders (pick completed only)..."); | 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 | // Fix: Ensure the data is always an array | ||||
| const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; | const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; | ||||
| @@ -226,7 +219,19 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| setFilteredJobOrderPickOrders(filtered); | setFilteredJobOrderPickOrders(filtered); | ||||
| console.log("Filtered Job Order pick orders count:", filtered.length); | console.log("Filtered Job Order pick orders count:", filtered.length); | ||||
| }, [completedJobOrderPickOrders]); | }, [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(() => { | const handleSearchReset = useCallback(() => { | ||||
| setSearchQuery({}); | setSearchQuery({}); | ||||
| @@ -433,18 +438,6 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| <strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} | <strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} | ||||
| </Typography> | </Typography> | ||||
| </Stack> | </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> | </CardContent> | ||||
| </Card> | </Card> | ||||
| @@ -600,37 +593,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||||
| {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} | {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} | ||||
| </Typography> | </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 ? ( | {filteredJobOrderPickOrders.length === 0 ? ( | ||||
| <Box sx={{ p: 3, textAlign: 'center' }}> | <Box sx={{ p: 3, textAlign: 'center' }}> | ||||
| @@ -652,7 +615,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode} | {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()} | |||||
| {t("Completed")}: {formatDateTime(jobOrderPickOrder.planEnd)} | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} | {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} | ||||
| @@ -42,8 +42,6 @@ import { | |||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| // 修改:使用 Job Order API | // 修改:使用 Job Order API | ||||
| import { | import { | ||||
| //fetchJobOrderLotsHierarchical, | |||||
| //fetchUnassignedJobOrderPickOrders, | |||||
| assignJobOrderPickOrder, | assignJobOrderPickOrder, | ||||
| fetchJobOrderLotsHierarchicalByPickOrderId, | fetchJobOrderLotsHierarchicalByPickOrderId, | ||||
| updateJoPickOrderHandledBy, | updateJoPickOrderHandledBy, | ||||
| @@ -412,6 +410,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| pickOrderType: data.pickOrder.type, | pickOrderType: data.pickOrder.type, | ||||
| pickOrderStatus: data.pickOrder.status, | pickOrderStatus: data.pickOrder.status, | ||||
| pickOrderAssignTo: data.pickOrder.assignTo, | pickOrderAssignTo: data.pickOrder.assignTo, | ||||
| handler: line.handler, | |||||
| }); | }); | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -537,6 +536,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| setCombinedDataLoading(false); | setCombinedDataLoading(false); | ||||
| } | } | ||||
| }, [getAllLotsFromHierarchical]); | }, [getAllLotsFromHierarchical]); | ||||
| const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { | const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { | ||||
| if (!currentUserId || !pickOrderId || !itemId) { | if (!currentUserId || !pickOrderId || !itemId) { | ||||
| return; | return; | ||||
| @@ -901,11 +901,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| // Use the first active suggested lot as the "expected" lot | // Use the first active suggested lot as the "expected" lot | ||||
| const expectedLot = activeSuggestedLots[0]; | const expectedLot = activeSuggestedLots[0]; | ||||
| // 2) Check if the scanned lot matches exactly | |||||
| if (scanned?.lotNo === expectedLot.lotNo) { | if (scanned?.lotNo === expectedLot.lotNo) { | ||||
| // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) | |||||
| console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); | console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); | ||||
| if (!expectedLot.stockOutLineId) { | if (!expectedLot.stockOutLineId) { | ||||
| console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); | console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); | ||||
| setQrScanError(true); | setQrScanError(true); | ||||
| @@ -922,24 +920,33 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| status: "checked", | 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); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| } else { | } else { | ||||
| console.warn("Unexpected response code from backend:", res.code); | |||||
| console.warn("Unexpected response from backend:", res); | |||||
| setQrScanError(true); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| } | } | ||||
| @@ -949,7 +956,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| } | } | ||||
| return; // ✅ 直接返回,不再调用 handleQrCodeSubmit | |||||
| return; // ✅ 直接返回,不再调用后面的分支 | |||||
| } | } | ||||
| // Case 2: Same item, different lot - show confirmation modal | // Case 2: Same item, different lot - show confirmation modal | ||||
| @@ -977,7 +984,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| return; | return; | ||||
| } | } | ||||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]); | |||||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, updateHandledBy]); | |||||
| const handleManualInputSubmit = useCallback(() => { | const handleManualInputSubmit = useCallback(() => { | ||||
| @@ -1310,6 +1317,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| console.error("Error submitting pick quantity:", error); | console.error("Error submitting pick quantity:", error); | ||||
| } | } | ||||
| }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); | }, [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 handleSubmitAllScanned = useCallback(async () => { | ||||
| const scannedLots = combinedLotData.filter(lot => | const scannedLots = combinedLotData.filter(lot => | ||||
| lot.stockOutLineStatus === 'checked' | lot.stockOutLineStatus === 'checked' | ||||
| @@ -1544,7 +1559,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| }, [startScan]); | }, [startScan]); | ||||
| const handleStopScan = useCallback(() => { | const handleStopScan = useCallback(() => { | ||||
| console.log("⏹️ Stopping manual QR scan..."); | |||||
| console.log(" Stopping manual QR scan..."); | |||||
| setIsManualScanning(false); | setIsManualScanning(false); | ||||
| setQrScanError(false); | setQrScanError(false); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| @@ -1563,7 +1578,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| }, [isManualScanning, stopScan, resetScan]); | }, [isManualScanning, stopScan, resetScan]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (isManualScanning && combinedLotData.length === 0) { | if (isManualScanning && combinedLotData.length === 0) { | ||||
| console.log("⏹️ No data available, auto-stopping QR scan..."); | |||||
| console.log(" No data available, auto-stopping QR scan..."); | |||||
| handleStopScan(); | handleStopScan(); | ||||
| } | } | ||||
| }, [combinedLotData.length, isManualScanning, handleStopScan]); | }, [combinedLotData.length, isManualScanning, handleStopScan]); | ||||
| @@ -1677,16 +1692,59 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| </Box> | </Box> | ||||
| </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.")} | {t("QR code does not match any item in current orders.")} | ||||
| </Alert> | </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}> | <TableContainer component={Paper}> | ||||
| <Table> | <Table> | ||||
| @@ -1694,6 +1752,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Index")}</TableCell> | <TableCell>{t("Index")}</TableCell> | ||||
| <TableCell>{t("Route")}</TableCell> | <TableCell>{t("Route")}</TableCell> | ||||
| <TableCell>{t("Handler")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | <TableCell>{t("Item Code")}</TableCell> | ||||
| <TableCell>{t("Item Name")}</TableCell> | <TableCell>{t("Item Name")}</TableCell> | ||||
| <TableCell>{t("Lot No")}</TableCell> | <TableCell>{t("Lot No")}</TableCell> | ||||
| @@ -1733,6 +1792,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| {lot.routerRoute || '-'} | {lot.routerRoute || '-'} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell>{lot.handler || '-'}</TableCell> | |||||
| <TableCell>{lot.itemCode}</TableCell> | <TableCell>{lot.itemCode}</TableCell> | ||||
| <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -1837,6 +1897,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| > | > | ||||
| {t("Issue")} | {t("Issue")} | ||||
| </Button> | </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> | </Stack> | ||||
| </Box> | </Box> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -173,7 +173,8 @@ const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => { | |||||
| const response = await deleteJobOrder(jobOrderId) | const response = await deleteJobOrder(jobOrderId) | ||||
| if (response) { | if (response) { | ||||
| //setProcessData(response.entity); | //setProcessData(response.entity); | ||||
| await fetchData(); | |||||
| //await fetchData(); | |||||
| onBack(); | |||||
| } | } | ||||
| }, [jobOrderId]); | }, [jobOrderId]); | ||||
| const handleRelease = useCallback(async ( jobOrderId: number) => { | const handleRelease = useCallback(async ( jobOrderId: number) => { | ||||
| @@ -221,6 +221,7 @@ | |||||
| "View Details": "查看詳情", | "View Details": "查看詳情", | ||||
| "view stockin": "品檢", | "view stockin": "品檢", | ||||
| "No completed Job Order pick orders with matching found": "沒有相關記錄", | "No completed Job Order pick orders with matching found": "沒有相關記錄", | ||||
| "Handler": "提料員", | |||||
| "Completed Step": "完成步驟", | "Completed Step": "完成步驟", | ||||
| "Continue": "繼續", | "Continue": "繼續", | ||||
| "Executing": "執行中", | "Executing": "執行中", | ||||
| @@ -86,6 +86,8 @@ | |||||
| "Job Order Item Name": "工單物料名稱", | "Job Order Item Name": "工單物料名稱", | ||||
| "Job Order Code": "工單編號", | "Job Order Code": "工單編號", | ||||
| "View Details": "查看詳情", | "View Details": "查看詳情", | ||||
| "Skip": "跳過", | |||||
| "Handler": "提料員", | |||||
| "Required Qty": "需求數量", | "Required Qty": "需求數量", | ||||
| "completed Job Order pick orders with Matching": "工單已完成提料和對料", | "completed Job Order pick orders with Matching": "工單已完成提料和對料", | ||||
| "No completed Job Order pick orders with matching found": "沒有相關記錄", | "No completed Job Order pick orders with matching found": "沒有相關記錄", | ||||