| @@ -195,7 +195,7 @@ export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { | |||
| }); | |||
| export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => { | |||
| 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", | |||
| next: { tags: ["jo-completed"] }, | |||
| @@ -328,7 +328,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| <TableCell>{lot.actualPickQty}</TableCell> | |||
| <TableCell> | |||
| <Chip | |||
| label={lot.processingStatus} | |||
| label={t(lot.processingStatus)} | |||
| color={lot.processingStatus === 'completed' ? 'success' : 'default'} | |||
| size="small" | |||
| /> | |||
| @@ -394,7 +394,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| </Box> | |||
| <Box> | |||
| <Chip | |||
| label={doPickOrder.pickOrderStatus} | |||
| label={t(doPickOrder.pickOrderStatus)} | |||
| color={doPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | |||
| size="small" | |||
| sx={{ mb: 1 }} | |||
| @@ -367,7 +367,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | |||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | |||
| const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); | |||
| const fetchFgPickOrdersData = useCallback(async () => { | |||
| if (!currentUserId) return; | |||
| @@ -1360,6 +1360,108 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| 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) => { | |||
| switch (lot.stockOutLineStatus?.toLowerCase()) { | |||
| case 'pending': | |||
| @@ -1445,14 +1547,23 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| </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> | |||
| @@ -356,6 +356,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const formProps = useForm(); | |||
| const errors = formProps.formState.errors; | |||
| const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); | |||
| // ✅ Add QR modal states | |||
| const [qrModalOpen, setQrModalOpen] = useState(false); | |||
| @@ -1223,6 +1224,87 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.error("Error submitting pick quantity:", error); | |||
| } | |||
| }, [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 | |||
| const handleRejectLot = useCallback(async (lot: any) => { | |||
| if (!lot.stockOutLineId) { | |||
| @@ -1509,15 +1591,24 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| {t("Stop QR Scan")} | |||
| </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> | |||
| @@ -332,7 +332,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); | |||
| const [paginationController, setPaginationController] = useState({ | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| @@ -532,6 +532,66 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setCombinedDataLoading(false); | |||
| } | |||
| }, [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(() => { | |||
| @@ -983,6 +1043,14 @@ const paginatedData = useMemo(() => { | |||
| 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(() => { | |||
| return () => { | |||
| // Cleanup when component unmounts (e.g., when switching tabs) | |||
| @@ -1020,7 +1088,7 @@ const paginatedData = useMemo(() => { | |||
| <Paper sx={{ p: 2 }}> | |||
| <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap"> | |||
| <Typography variant="subtitle1"> | |||
| <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.name || '-'} | |||
| <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'} | |||
| </Typography> | |||
| <Typography variant="subtitle1"> | |||
| <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'} | |||
| @@ -1063,14 +1131,23 @@ const paginatedData = useMemo(() => { | |||
| </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> | |||
| @@ -568,7 +568,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| </Box> | |||
| <Box> | |||
| <Chip | |||
| label={jobOrderPickOrder.pickOrderStatus} | |||
| label={t(jobOrderPickOrder.pickOrderStatus) } | |||
| color={jobOrderPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | |||
| size="small" | |||
| sx={{ mb: 1 }} | |||
| @@ -254,5 +254,9 @@ | |||
| "stock in information": "庫存信息", | |||
| "No Uom": "沒有單位", | |||
| "Print Pick Record" : "打印板頭紙", | |||
| "Printed Successfully." : "成功列印" | |||
| "Printed Successfully." : "成功列印", | |||
| "Submit All Scanned": "提交所有已掃描項目", | |||
| "Submitting...": "提交中...", | |||
| "COMPLETED": "已完成", | |||
| "success": "成功" | |||
| } | |||
| @@ -53,7 +53,7 @@ | |||
| "received": "已收貨", | |||
| "completed": "已完成", | |||
| "rejected": "已拒絕", | |||
| "success": "成功", | |||
| "acceptedQty must not greater than": "接受數量不得大於", | |||
| "minimal value is 1": "最小值為1", | |||
| "value must be a number": "值必須是數字", | |||
| @@ -363,7 +363,11 @@ | |||
| "Input quantity cannot exceed": "輸入數量不能超過", | |||
| "Quantity cannot be negative": "數量不能為負數", | |||
| "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": "已完成" | |||