| @@ -195,7 +195,7 @@ export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { | |||||
| }); | }); | ||||
| export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => { | export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => { | ||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/jo/completed-job-order-pick-orders${userId}`, | |||||
| `${BASE_API_URL}/jo/completed-job-order-pick-orders/${userId}`, | |||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| next: { tags: ["jo-completed"] }, | next: { tags: ["jo-completed"] }, | ||||
| @@ -328,7 +328,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||||
| <TableCell>{lot.actualPickQty}</TableCell> | <TableCell>{lot.actualPickQty}</TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| <Chip | <Chip | ||||
| label={lot.processingStatus} | |||||
| label={t(lot.processingStatus)} | |||||
| color={lot.processingStatus === 'completed' ? 'success' : 'default'} | color={lot.processingStatus === 'completed' ? 'success' : 'default'} | ||||
| size="small" | size="small" | ||||
| /> | /> | ||||
| @@ -394,7 +394,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Chip | <Chip | ||||
| label={doPickOrder.pickOrderStatus} | |||||
| label={t(doPickOrder.pickOrderStatus)} | |||||
| color={doPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | color={doPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | ||||
| size="small" | size="small" | ||||
| sx={{ mb: 1 }} | sx={{ mb: 1 }} | ||||
| @@ -367,7 +367,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | ||||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | ||||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | ||||
| const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); | |||||
| const fetchFgPickOrdersData = useCallback(async () => { | const fetchFgPickOrdersData = useCallback(async () => { | ||||
| if (!currentUserId) return; | if (!currentUserId) return; | ||||
| @@ -1360,6 +1360,108 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||||
| stopScan(); | stopScan(); | ||||
| resetScan(); | resetScan(); | ||||
| }, [stopScan, resetScan]); | }, [stopScan, resetScan]); | ||||
| const handleSubmitAllScanned = useCallback(async () => { | |||||
| const scannedLots = combinedLotData.filter(lot => | |||||
| lot.stockOutLineStatus === 'checked' // Only submit items that are scanned but not yet submitted | |||||
| ); | |||||
| if (scannedLots.length === 0) { | |||||
| console.log("No scanned items to submit"); | |||||
| return; | |||||
| } | |||||
| setIsSubmittingAll(true); | |||||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||||
| try { | |||||
| // ✅ Submit all items in parallel using Promise.all | |||||
| const submitPromises = scannedLots.map(async (lot) => { | |||||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||||
| const currentActualPickQty = lot.actualPickQty || 0; | |||||
| const cumulativeQty = currentActualPickQty + submitQty; | |||||
| let newStatus = 'partially_completed'; | |||||
| if (cumulativeQty >= lot.requiredQty) { | |||||
| newStatus = 'completed'; | |||||
| } | |||||
| console.log(`Submitting lot ${lot.lotNo}: qty=${cumulativeQty}, status=${newStatus}`); | |||||
| // Update stock out line | |||||
| await updateStockOutLineStatus({ | |||||
| id: lot.stockOutLineId, | |||||
| status: newStatus, | |||||
| qty: cumulativeQty | |||||
| }); | |||||
| // Update inventory | |||||
| if (submitQty > 0) { | |||||
| await updateInventoryLotLineQuantities({ | |||||
| inventoryLotLineId: lot.lotId, | |||||
| qty: submitQty, | |||||
| status: 'available', | |||||
| operation: 'pick' | |||||
| }); | |||||
| } | |||||
| // Check if pick order is completed | |||||
| if (newStatus === 'completed' && lot.pickOrderConsoCode) { | |||||
| await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); | |||||
| } | |||||
| return { success: true, lotNo: lot.lotNo }; | |||||
| }); | |||||
| // ✅ Wait for all submissions to complete | |||||
| const results = await Promise.all(submitPromises); | |||||
| const successCount = results.filter(r => r.success).length; | |||||
| console.log(`✅ Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); | |||||
| // ✅ Refresh data once after all submissions | |||||
| await fetchAllCombinedLotData(); | |||||
| if (successCount > 0) { | |||||
| setQrScanSuccess(true); | |||||
| setTimeout(() => { | |||||
| setQrScanSuccess(false); | |||||
| checkAndAutoAssignNext(); | |||||
| }, 2000); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error submitting all scanned items:", error); | |||||
| setQrScanError(true); | |||||
| } finally { | |||||
| setIsSubmittingAll(false); | |||||
| } | |||||
| }, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext]); | |||||
| // ✅ Calculate scanned items count | |||||
| const scannedItemsCount = useMemo(() => { | |||||
| return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; | |||||
| }, [combinedLotData]); | |||||
| // ✅ ADD THIS: Auto-stop scan when no data available | |||||
| useEffect(() => { | |||||
| if (isManualScanning && combinedLotData.length === 0) { | |||||
| console.log("⏹️ No data available, auto-stopping QR scan..."); | |||||
| handleStopScan(); | |||||
| } | |||||
| }, [combinedLotData.length, isManualScanning, handleStopScan]); | |||||
| // ✅ Cleanup effect | |||||
| useEffect(() => { | |||||
| return () => { | |||||
| // Cleanup when component unmounts (e.g., when switching tabs) | |||||
| if (isManualScanning) { | |||||
| console.log("🧹 Pick execution component unmounting, stopping QR scanner..."); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| } | |||||
| }; | |||||
| }, [isManualScanning, stopScan, resetScan]); | |||||
| const getStatusMessage = useCallback((lot: any) => { | const getStatusMessage = useCallback((lot: any) => { | ||||
| switch (lot.stockOutLineStatus?.toLowerCase()) { | switch (lot.stockOutLineStatus?.toLowerCase()) { | ||||
| case 'pending': | case 'pending': | ||||
| @@ -1445,14 +1547,23 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| {isManualScanning && ( | |||||
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | |||||
| <CircularProgress size={16} /> | |||||
| <Typography variant="caption" color="primary"> | |||||
| {t("Scanning...")} | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| {/* ✅ ADD THIS: Submit All Scanned Button */} | |||||
| <Button | |||||
| variant="contained" | |||||
| color="success" | |||||
| onClick={handleSubmitAllScanned} | |||||
| disabled={scannedItemsCount === 0 || isSubmittingAll} | |||||
| sx={{ minWidth: '160px' }} | |||||
| > | |||||
| {isSubmittingAll ? ( | |||||
| <> | |||||
| <CircularProgress size={16} sx={{ mr: 1, color: 'white' }} /> | |||||
| {t("Submitting...")} | |||||
| </> | |||||
| ) : ( | |||||
| `${t("Submit All Scanned")} (${scannedItemsCount})` | |||||
| )} | |||||
| </Button> | |||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| @@ -356,6 +356,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const formProps = useForm(); | const formProps = useForm(); | ||||
| const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
| const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); | |||||
| // ✅ Add QR modal states | // ✅ Add QR modal states | ||||
| const [qrModalOpen, setQrModalOpen] = useState(false); | const [qrModalOpen, setQrModalOpen] = useState(false); | ||||
| @@ -1223,6 +1224,87 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| console.error("Error submitting pick quantity:", error); | console.error("Error submitting pick quantity:", error); | ||||
| } | } | ||||
| }, [fetchJobOrderData, checkAndAutoAssignNext]); | }, [fetchJobOrderData, checkAndAutoAssignNext]); | ||||
| const handleSubmitAllScanned = useCallback(async () => { | |||||
| const scannedLots = combinedLotData.filter(lot => | |||||
| lot.stockOutLineStatus === 'checked' // Only submit items that are scanned but not yet submitted | |||||
| ); | |||||
| if (scannedLots.length === 0) { | |||||
| console.log("No scanned items to submit"); | |||||
| return; | |||||
| } | |||||
| setIsSubmittingAll(true); | |||||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||||
| try { | |||||
| // ✅ Submit all items in parallel using Promise.all | |||||
| const submitPromises = scannedLots.map(async (lot) => { | |||||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||||
| const currentActualPickQty = lot.actualPickQty || 0; | |||||
| const cumulativeQty = currentActualPickQty + submitQty; | |||||
| let newStatus = 'partially_completed'; | |||||
| if (cumulativeQty >= lot.requiredQty) { | |||||
| newStatus = 'completed'; | |||||
| } | |||||
| console.log(`Submitting lot ${lot.lotNo}: qty=${cumulativeQty}, status=${newStatus}`); | |||||
| // Update stock out line | |||||
| await updateStockOutLineStatus({ | |||||
| id: lot.stockOutLineId, | |||||
| status: newStatus, | |||||
| qty: cumulativeQty | |||||
| }); | |||||
| // Update inventory | |||||
| if (submitQty > 0) { | |||||
| await updateInventoryLotLineQuantities({ | |||||
| inventoryLotLineId: lot.lotId, | |||||
| qty: submitQty, | |||||
| status: 'available', | |||||
| operation: 'pick' | |||||
| }); | |||||
| } | |||||
| // Check if pick order is completed | |||||
| if (newStatus === 'completed' && lot.pickOrderConsoCode) { | |||||
| await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); | |||||
| } | |||||
| return { success: true, lotNo: lot.lotNo }; | |||||
| }); | |||||
| // ✅ Wait for all submissions to complete | |||||
| const results = await Promise.all(submitPromises); | |||||
| const successCount = results.filter(r => r.success).length; | |||||
| console.log(`✅ Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); | |||||
| // ✅ Refresh data once after all submissions | |||||
| await fetchJobOrderData(); | |||||
| if (successCount > 0) { | |||||
| setQrScanSuccess(true); | |||||
| setTimeout(() => { | |||||
| setQrScanSuccess(false); | |||||
| checkAndAutoAssignNext(); | |||||
| }, 2000); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error submitting all scanned items:", error); | |||||
| setQrScanError(true); | |||||
| } finally { | |||||
| setIsSubmittingAll(false); | |||||
| } | |||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]); | |||||
| // ✅ Calculate scanned items count | |||||
| const scannedItemsCount = useMemo(() => { | |||||
| return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; | |||||
| }, [combinedLotData]); | |||||
| // ✅ Handle reject lot | // ✅ Handle reject lot | ||||
| const handleRejectLot = useCallback(async (lot: any) => { | const handleRejectLot = useCallback(async (lot: any) => { | ||||
| if (!lot.stockOutLineId) { | if (!lot.stockOutLineId) { | ||||
| @@ -1509,15 +1591,24 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| {t("Stop QR Scan")} | {t("Stop QR Scan")} | ||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| {isManualScanning && ( | |||||
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | |||||
| <CircularProgress size={16} /> | |||||
| <Typography variant="caption" color="primary"> | |||||
| {t("Scanning...")} | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| {/* ✅ ADD THIS: Submit All Scanned Button */} | |||||
| <Button | |||||
| variant="contained" | |||||
| color="success" | |||||
| onClick={handleSubmitAllScanned} | |||||
| disabled={scannedItemsCount === 0 || isSubmittingAll} | |||||
| sx={{ minWidth: '160px' }} | |||||
| > | |||||
| {isSubmittingAll ? ( | |||||
| <> | |||||
| <CircularProgress size={16} sx={{ mr: 1 }} /> | |||||
| {t("Submitting...")} | |||||
| </> | |||||
| ) : ( | |||||
| `${t("Submit All Scanned")} (${scannedItemsCount})` | |||||
| )} | |||||
| </Button> | |||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| @@ -332,7 +332,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); | const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); | ||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | ||||
| const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); | |||||
| const [paginationController, setPaginationController] = useState({ | const [paginationController, setPaginationController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| pageSize: 10, | pageSize: 10, | ||||
| @@ -532,6 +532,66 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| setCombinedDataLoading(false); | setCombinedDataLoading(false); | ||||
| } | } | ||||
| }, [currentUserId]); | }, [currentUserId]); | ||||
| const handleSubmitAllScanned = useCallback(async () => { | |||||
| const scannedLots = combinedLotData.filter(lot => | |||||
| lot.secondQrScanStatus === 'scanned' // Only submit items that are scanned but not yet submitted | |||||
| ); | |||||
| if (scannedLots.length === 0) { | |||||
| console.log("No scanned items to submit"); | |||||
| return; | |||||
| } | |||||
| setIsSubmittingAll(true); | |||||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||||
| try { | |||||
| // ✅ Submit all items in parallel using Promise.all | |||||
| const submitPromises = scannedLots.map(async (lot) => { | |||||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||||
| console.log(`Submitting item ${lot.itemCode}: qty=${submitQty}`); | |||||
| const result = await submitSecondScanQuantity( | |||||
| lot.pickOrderId, | |||||
| lot.itemId, | |||||
| { | |||||
| qty: submitQty, | |||||
| isMissing: false, | |||||
| isBad: false, | |||||
| reason: undefined | |||||
| } | |||||
| ); | |||||
| return { success: result.code === "SUCCESS", itemCode: lot.itemCode }; | |||||
| }); | |||||
| // ✅ Wait for all submissions to complete | |||||
| const results = await Promise.all(submitPromises); | |||||
| const successCount = results.filter(r => r.success).length; | |||||
| console.log(`✅ Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); | |||||
| // ✅ Refresh data once after all submissions | |||||
| await fetchJobOrderData(); | |||||
| if (successCount > 0) { | |||||
| setQrScanSuccess(true); | |||||
| setTimeout(() => setQrScanSuccess(false), 2000); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error submitting all scanned items:", error); | |||||
| setQrScanError(true); | |||||
| } finally { | |||||
| setIsSubmittingAll(false); | |||||
| } | |||||
| }, [combinedLotData, fetchJobOrderData]); | |||||
| // ✅ Calculate scanned items count | |||||
| const scannedItemsCount = useMemo(() => { | |||||
| return combinedLotData.filter(lot => lot.secondQrScanStatus === 'scanned').length; | |||||
| }, [combinedLotData]); | |||||
| // ✅ 修改:初始化时加载数据 | // ✅ 修改:初始化时加载数据 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -983,6 +1043,14 @@ const paginatedData = useMemo(() => { | |||||
| stopScan(); | stopScan(); | ||||
| resetScan(); | resetScan(); | ||||
| }, [stopScan, resetScan]); | }, [stopScan, resetScan]); | ||||
| useEffect(() => { | |||||
| if (isManualScanning && combinedLotData.length === 0) { | |||||
| console.log("⏹️ No data available, auto-stopping QR scan..."); | |||||
| handleStopScan(); | |||||
| } | |||||
| }, [combinedLotData.length, isManualScanning, handleStopScan]); | |||||
| // ✅ Cleanup effect | |||||
| useEffect(() => { | useEffect(() => { | ||||
| return () => { | return () => { | ||||
| // Cleanup when component unmounts (e.g., when switching tabs) | // Cleanup when component unmounts (e.g., when switching tabs) | ||||
| @@ -1020,7 +1088,7 @@ const paginatedData = useMemo(() => { | |||||
| <Paper sx={{ p: 2 }}> | <Paper sx={{ p: 2 }}> | ||||
| <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap"> | <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap"> | ||||
| <Typography variant="subtitle1"> | <Typography variant="subtitle1"> | ||||
| <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.name || '-'} | |||||
| <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'} | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="subtitle1"> | <Typography variant="subtitle1"> | ||||
| <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'} | <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'} | ||||
| @@ -1063,14 +1131,23 @@ const paginatedData = useMemo(() => { | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| {isManualScanning && ( | |||||
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | |||||
| <CircularProgress size={16} /> | |||||
| <Typography variant="caption" color="primary"> | |||||
| {t("Scanning...")} | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| {/* ✅ ADD THIS: Submit All Scanned Button */} | |||||
| <Button | |||||
| variant="contained" | |||||
| color="success" | |||||
| onClick={handleSubmitAllScanned} | |||||
| disabled={scannedItemsCount === 0 || isSubmittingAll} | |||||
| sx={{ minWidth: '160px' }} | |||||
| > | |||||
| {isSubmittingAll ? ( | |||||
| <> | |||||
| <CircularProgress size={16} sx={{ mr: 1 }} /> | |||||
| {t("Submitting...")} | |||||
| </> | |||||
| ) : ( | |||||
| `${t("Submit All Scanned")} (${scannedItemsCount})` | |||||
| )} | |||||
| </Button> | |||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| @@ -568,7 +568,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Chip | <Chip | ||||
| label={jobOrderPickOrder.pickOrderStatus} | |||||
| label={t(jobOrderPickOrder.pickOrderStatus) } | |||||
| color={jobOrderPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | color={jobOrderPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | ||||
| size="small" | size="small" | ||||
| sx={{ mb: 1 }} | sx={{ mb: 1 }} | ||||
| @@ -254,5 +254,9 @@ | |||||
| "stock in information": "庫存信息", | "stock in information": "庫存信息", | ||||
| "No Uom": "沒有單位", | "No Uom": "沒有單位", | ||||
| "Print Pick Record" : "打印板頭紙", | "Print Pick Record" : "打印板頭紙", | ||||
| "Printed Successfully." : "成功列印" | |||||
| "Printed Successfully." : "成功列印", | |||||
| "Submit All Scanned": "提交所有已掃描項目", | |||||
| "Submitting...": "提交中...", | |||||
| "COMPLETED": "已完成", | |||||
| "success": "成功" | |||||
| } | } | ||||
| @@ -53,7 +53,7 @@ | |||||
| "received": "已收貨", | "received": "已收貨", | ||||
| "completed": "已完成", | "completed": "已完成", | ||||
| "rejected": "已拒絕", | "rejected": "已拒絕", | ||||
| "success": "成功", | |||||
| "acceptedQty must not greater than": "接受數量不得大於", | "acceptedQty must not greater than": "接受數量不得大於", | ||||
| "minimal value is 1": "最小值為1", | "minimal value is 1": "最小值為1", | ||||
| "value must be a number": "值必須是數字", | "value must be a number": "值必須是數字", | ||||
| @@ -363,7 +363,11 @@ | |||||
| "Input quantity cannot exceed": "輸入數量不能超過", | "Input quantity cannot exceed": "輸入數量不能超過", | ||||
| "Quantity cannot be negative": "數量不能為負數", | "Quantity cannot be negative": "數量不能為負數", | ||||
| "Enter bad item quantity (required if no missing items)": "請輸入不良數量(如果沒有缺少項目)", | "Enter bad item quantity (required if no missing items)": "請輸入不良數量(如果沒有缺少項目)", | ||||
| "Enter missing quantity (required if no bad items)": "請輸入缺少數量(如果沒有不良項目)" | |||||
| "Enter missing quantity (required if no bad items)": "請輸入缺少數量(如果沒有不良項目)", | |||||
| "Submit All Scanned": "提交所有已掃描項目", | |||||
| "Submitting...": "提交中...", | |||||
| "COMPLETED": "已完成" | |||||