diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 7543959..8e4c757 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -576,6 +576,7 @@ export interface PickOrderLineWithLotsResponse { itemCode: string | null; itemName: string | null; requiredQty: number | null; + totalAvailableQty?: number | null; uomCode: string | null; uomDesc: string | null; status: string | null; diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index b912101..66f0c36 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -513,6 +513,8 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); const [originalCombinedData, setOriginalCombinedData] = useState([]); // issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required) const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState>({}); + // 防止重复点击(Submit / Just Completed / Issue) + const [actionBusyBySolId, setActionBusyBySolId] = useState>({}); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); @@ -2398,8 +2400,14 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe console.error("No stock out line found for this lot"); return; } + const solId = Number(lot.stockOutLineId); + if (solId > 0 && actionBusyBySolId[solId]) { + console.warn("Action already in progress for stockOutLineId:", solId); + return; + } try { + if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0 if (submitQty === 0) { console.log(`=== SUBMITTING ALL ZEROS CASE ===`); @@ -2524,8 +2532,10 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe } catch (error) { console.error("Error submitting pick quantity:", error); + } finally { + if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: false })); } -}, [fetchAllCombinedLotData, checkAndAutoAssignNext]); +}, [fetchAllCombinedLotData, checkAndAutoAssignNext, actionBusyBySolId]); const handleSkip = useCallback(async (lot: any) => { try { @@ -2592,8 +2602,14 @@ const handleStartScan = useCallback(() => { console.error(" No stockOutLineId found for lot:", lot); return; } + const solId = Number(stockOutLineId); + if (solId > 0 && actionBusyBySolId[solId]) { + console.warn("Action already in progress for stockOutLineId:", solId); + return; + } try { + if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); // Step 1: Update stock out line status await updateStockOutLineStatus({ id: stockOutLineId, @@ -2641,8 +2657,10 @@ const handleStartScan = useCallback(() => { await fetchAllCombinedLotData(); } catch (error) { console.error(" Error in handlelotnull:", error); + } finally { + if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: false })); } - }, [fetchAllCombinedLotData, session, currentUserId, fgPickOrders]); + }, [fetchAllCombinedLotData, session, currentUserId, fgPickOrders, actionBusyBySolId]); const handleBatchScan = useCallback(async () => { const startTime = performance.now(); console.log(`⏱️ [BATCH SCAN START]`); @@ -3299,7 +3317,10 @@ paginatedData.map((lot, index) => { variant="outlined" size="small" onClick={() => handlelotnull(lot)} - disabled={status === 'completed'} + disabled={ + status === 'completed' || + (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) + } sx={{ fontSize: '0.7rem', py: 0.5, @@ -3330,7 +3351,8 @@ paginatedData.map((lot, index) => { lot.lotAvailability === 'status_unavailable' || lot.lotAvailability === 'rejected' || lot.stockOutLineStatus === 'completed' || - lot.stockOutLineStatus === 'pending' + lot.stockOutLineStatus === 'pending' || + (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) } sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }} > @@ -3343,7 +3365,8 @@ paginatedData.map((lot, index) => { onClick={() => handlePickExecutionForm(lot)} disabled={ lot.lotAvailability === 'expired' || - lot.stockOutLineStatus === 'completed' + lot.stockOutLineStatus === 'completed' || + (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) } sx={{ fontSize: '0.7rem', @@ -3361,7 +3384,12 @@ paginatedData.map((lot, index) => { variant="outlined" size="small" onClick={() => handleSkip(lot)} - disabled={lot.stockOutLineStatus === 'completed'} + disabled={ + lot.stockOutLineStatus === 'completed' || + // 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交) + (Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) || + (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) + } sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} > {t("Just Completed")} diff --git a/src/components/JoSearch/JoCreateFormModal.tsx b/src/components/JoSearch/JoCreateFormModal.tsx index b716e0f..b84acea 100644 --- a/src/components/JoSearch/JoCreateFormModal.tsx +++ b/src/components/JoSearch/JoCreateFormModal.tsx @@ -3,7 +3,7 @@ import { JoDetail } from "@/app/api/jo"; import { SaveJo, manualCreateJo } from "@/app/api/jo/actions"; import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; import { Check } from "@mui/icons-material"; -import { Autocomplete, Box, Button, Card, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material"; +import { Autocomplete, Box, Button, Card, CircularProgress, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material"; import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs, { Dayjs } from "dayjs"; @@ -31,6 +31,7 @@ const JoCreateFormModal: React.FC = ({ }) => { const { t } = useTranslation("jo"); const [multiplier, setMultiplier] = useState(1); + const [isSubmitting, setIsSubmitting] = useState(false); const formProps = useForm({ mode: "onChange", defaultValues: { @@ -73,10 +74,11 @@ const JoCreateFormModal: React.FC = ({ } }, [multiplier, selectedBomId, bomCombo, formProps]); const onModalClose = useCallback(() => { + if (isSubmitting) return; reset() onClose() setMultiplier(1); - }, [reset, onClose]) + }, [reset, onClose, isSubmitting]) const duplicateLabels = useMemo(() => { const count = new Map(); bomCombo.forEach((b) => count.set(b.label, (count.get(b.label) ?? 0) + 1)); @@ -149,23 +151,32 @@ const JoCreateFormModal: React.FC = ({ }, []) const onSubmit = useCallback>(async (data) => { - data.type = "manual" - if (data.planStart) { - const dateDayjs = dateStringToDayjs(data.planStart) - data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day')) - } - data.jobTypeId = Number(data.jobTypeId); - // 如果 productionPriority 为空或无效,使用默认值 50 - data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority) - ? Number(data.productionPriority) - : 50; - const response = await manualCreateJo(data) - if (response) { - onSearch(); - msg(t("update success")); - onModalClose(); + if (isSubmitting) return; + setIsSubmitting(true); + try { + data.type = "manual" + if (data.planStart) { + const dateDayjs = dateStringToDayjs(data.planStart) + data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day')) + } + data.jobTypeId = Number(data.jobTypeId); + // 如果 productionPriority 为空或无效,使用默认值 50 + data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority) + ? Number(data.productionPriority) + : 50; + const response = await manualCreateJo(data) + if (response) { + onSearch(); + msg(t("update success")); + onModalClose(); + } + } catch (e) { + console.error(e); + msg(t("update failed")); + } finally { + setIsSubmitting(false); } - }, [onSearch, onModalClose, t]) + }, [onSearch, onModalClose, t, isSubmitting]) const onSubmitError = useCallback>((error) => { console.log(error) @@ -505,10 +516,11 @@ const JoCreateFormModal: React.FC = ({ diff --git a/src/components/Jodetail/completeJobOrderRecord.tsx b/src/components/Jodetail/completeJobOrderRecord.tsx index 6abd9f7..76b07f0 100644 --- a/src/components/Jodetail/completeJobOrderRecord.tsx +++ b/src/components/Jodetail/completeJobOrderRecord.tsx @@ -76,32 +76,32 @@ interface CompletedJobOrderPickOrder { // 新增:Lot 详情接口 interface LotDetail { - lotId: number; - lotNo: string; - expiryDate: string; - location: string; - availableQty: number; - requiredQty: number; - actualPickQty: number; + lotId: number | null; + lotNo: string | null; + expiryDate: string | null; + location: string | null; + availableQty: number | null; + requiredQty: number | null; + actualPickQty: number | null; processingStatus: string; lotAvailability: string; pickOrderId: number; pickOrderCode: string; pickOrderConsoCode: string; pickOrderLineId: number; - stockOutLineId: number; + stockOutLineId: number | null; stockOutLineStatus: string; - routerIndex: number; - routerArea: string; - routerRoute: string; - uomShortDesc: string; + routerIndex: number | null; + routerArea: string | null; + routerRoute: string | null; + uomShortDesc: string | null; secondQrScanStatus: string; itemId: number; itemCode: string; itemName: string; uomCode: string; uomDesc: string; - match_status: string; + match_status: string | null; } const CompleteJobOrderRecord: React.FC = ({ @@ -176,7 +176,7 @@ const CompleteJobOrderRecord: React.FC = ({ const lotDetails = await fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId); - setDetailLotData(lotDetails); + setDetailLotData(Array.isArray(lotDetails) ? lotDetails : []); console.log(" Fetched lot details:", lotDetails); } catch (error) { console.error("❌ Error fetching lot details:", error); @@ -481,7 +481,7 @@ const CompleteJobOrderRecord: React.FC = ({ ) : ( detailLotData.map((lot, index) => ( - + {index + 1} @@ -494,13 +494,13 @@ const CompleteJobOrderRecord: React.FC = ({ {lot.itemCode} {lot.itemName} - {lot.lotNo} + {lot.lotNo || '-'} - {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc}) + {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc || ''}) - {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc}) + {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc || ''}) {/* 修改:Processing Status 使用复选框 */} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 193d3d3..1666809 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -464,6 +464,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const [searchQuery, setSearchQuery] = useState>({}); // issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required) const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState>({}); + // 防止同一行(以 stockOutLineId/solId 识别)被重复点击提交/完成 + const [actionBusyBySolId, setActionBusyBySolId] = useState>({}); const [paginationController, setPaginationController] = useState({ pageNum: 0, @@ -553,6 +555,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { itemName: line.itemName, uomCode: line.uomCode, uomDesc: line.uomDesc, + itemTotalAvailableQty: line.totalAvailableQty ?? null, pickOrderLineRequiredQty: line.requiredQty, pickOrderLineStatus: line.status, jobOrderId: data.pickOrder.jobOrder.id, @@ -588,6 +591,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { itemName: line.itemName, uomCode: line.uomCode, uomDesc: line.uomDesc, + itemTotalAvailableQty: line.totalAvailableQty ?? null, pickOrderLineRequiredQty: line.requiredQty, pickOrderLineStatus: line.status, jobOrderId: data.pickOrder.jobOrder.id, @@ -2505,6 +2509,7 @@ const sortedData = [...sourceData].sort((a, b) => { {t("Item Name")} {t("Lot No")} {t("Lot Required Pick Qty")} + {t("Available Qty")} {t("Scan Result")} {t("Submit Required Pick Qty")} @@ -2512,7 +2517,7 @@ const sortedData = [...sourceData].sort((a, b) => { {paginatedData.length === 0 ? ( - + {t("No data available")} @@ -2551,7 +2556,18 @@ const sortedData = [...sourceData].sort((a, b) => { {(() => { const requiredQty = lot.requiredQty || 0; - return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; + const unit = (lot.noLot === true || !lot.lotId) + ? (lot.uomDesc || "") + : ( lot.uomDesc || ""); + return `${requiredQty.toLocaleString()}(${unit})`; + })()} + + + {(() => { + const avail = lot.itemTotalAvailableQty; + if (avail == null) return "-"; + const unit = lot.uomDesc || ""; + return `${Number(avail).toLocaleString()}(${unit})`; })()} @@ -2636,13 +2652,24 @@ const sortedData = [...sourceData].sort((a, b) => {