| @@ -576,6 +576,7 @@ export interface PickOrderLineWithLotsResponse { | |||||
| itemCode: string | null; | itemCode: string | null; | ||||
| itemName: string | null; | itemName: string | null; | ||||
| requiredQty: number | null; | requiredQty: number | null; | ||||
| totalAvailableQty?: number | null; | |||||
| uomCode: string | null; | uomCode: string | null; | ||||
| uomDesc: string | null; | uomDesc: string | null; | ||||
| status: string | null; | status: string | null; | ||||
| @@ -513,6 +513,8 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||||
| const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | ||||
| // issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required) | // issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required) | ||||
| const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({}); | const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({}); | ||||
| // 防止重复点击(Submit / Just Completed / Issue) | |||||
| const [actionBusyBySolId, setActionBusyBySolId] = useState<Record<number, boolean>>({}); | |||||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | 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"); | console.error("No stock out line found for this lot"); | ||||
| return; | return; | ||||
| } | } | ||||
| const solId = Number(lot.stockOutLineId); | |||||
| if (solId > 0 && actionBusyBySolId[solId]) { | |||||
| console.warn("Action already in progress for stockOutLineId:", solId); | |||||
| return; | |||||
| } | |||||
| try { | 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 | // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0 | ||||
| if (submitQty === 0) { | if (submitQty === 0) { | ||||
| console.log(`=== SUBMITTING ALL ZEROS CASE ===`); | console.log(`=== SUBMITTING ALL ZEROS CASE ===`); | ||||
| @@ -2524,8 +2532,10 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error submitting pick quantity:", 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) => { | const handleSkip = useCallback(async (lot: any) => { | ||||
| try { | try { | ||||
| @@ -2592,8 +2602,14 @@ const handleStartScan = useCallback(() => { | |||||
| console.error(" No stockOutLineId found for lot:", lot); | console.error(" No stockOutLineId found for lot:", lot); | ||||
| return; | return; | ||||
| } | } | ||||
| const solId = Number(stockOutLineId); | |||||
| if (solId > 0 && actionBusyBySolId[solId]) { | |||||
| console.warn("Action already in progress for stockOutLineId:", solId); | |||||
| return; | |||||
| } | |||||
| try { | try { | ||||
| if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); | |||||
| // Step 1: Update stock out line status | // Step 1: Update stock out line status | ||||
| await updateStockOutLineStatus({ | await updateStockOutLineStatus({ | ||||
| id: stockOutLineId, | id: stockOutLineId, | ||||
| @@ -2641,8 +2657,10 @@ const handleStartScan = useCallback(() => { | |||||
| await fetchAllCombinedLotData(); | await fetchAllCombinedLotData(); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error(" Error in handlelotnull:", 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 handleBatchScan = useCallback(async () => { | ||||
| const startTime = performance.now(); | const startTime = performance.now(); | ||||
| console.log(`⏱️ [BATCH SCAN START]`); | console.log(`⏱️ [BATCH SCAN START]`); | ||||
| @@ -3299,7 +3317,10 @@ paginatedData.map((lot, index) => { | |||||
| variant="outlined" | variant="outlined" | ||||
| size="small" | size="small" | ||||
| onClick={() => handlelotnull(lot)} | onClick={() => handlelotnull(lot)} | ||||
| disabled={status === 'completed'} | |||||
| disabled={ | |||||
| status === 'completed' || | |||||
| (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) | |||||
| } | |||||
| sx={{ | sx={{ | ||||
| fontSize: '0.7rem', | fontSize: '0.7rem', | ||||
| py: 0.5, | py: 0.5, | ||||
| @@ -3330,7 +3351,8 @@ paginatedData.map((lot, index) => { | |||||
| lot.lotAvailability === 'status_unavailable' || | lot.lotAvailability === 'status_unavailable' || | ||||
| lot.lotAvailability === 'rejected' || | lot.lotAvailability === 'rejected' || | ||||
| lot.stockOutLineStatus === 'completed' || | 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' }} | sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }} | ||||
| > | > | ||||
| @@ -3343,7 +3365,8 @@ paginatedData.map((lot, index) => { | |||||
| onClick={() => handlePickExecutionForm(lot)} | onClick={() => handlePickExecutionForm(lot)} | ||||
| disabled={ | disabled={ | ||||
| lot.lotAvailability === 'expired' || | lot.lotAvailability === 'expired' || | ||||
| lot.stockOutLineStatus === 'completed' | |||||
| lot.stockOutLineStatus === 'completed' || | |||||
| (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) | |||||
| } | } | ||||
| sx={{ | sx={{ | ||||
| fontSize: '0.7rem', | fontSize: '0.7rem', | ||||
| @@ -3361,7 +3384,12 @@ paginatedData.map((lot, index) => { | |||||
| variant="outlined" | variant="outlined" | ||||
| size="small" | size="small" | ||||
| onClick={() => handleSkip(lot)} | 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' }} | sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} | ||||
| > | > | ||||
| {t("Just Completed")} | {t("Just Completed")} | ||||
| @@ -3,7 +3,7 @@ import { JoDetail } from "@/app/api/jo"; | |||||
| import { SaveJo, manualCreateJo } from "@/app/api/jo/actions"; | import { SaveJo, manualCreateJo } from "@/app/api/jo/actions"; | ||||
| import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | ||||
| import { Check } from "@mui/icons-material"; | 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 { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; | ||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | import dayjs, { Dayjs } from "dayjs"; | ||||
| @@ -31,6 +31,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("jo"); | const { t } = useTranslation("jo"); | ||||
| const [multiplier, setMultiplier] = useState<number>(1); | const [multiplier, setMultiplier] = useState<number>(1); | ||||
| const [isSubmitting, setIsSubmitting] = useState(false); | |||||
| const formProps = useForm<SaveJo>({ | const formProps = useForm<SaveJo>({ | ||||
| mode: "onChange", | mode: "onChange", | ||||
| defaultValues: { | defaultValues: { | ||||
| @@ -73,10 +74,11 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| } | } | ||||
| }, [multiplier, selectedBomId, bomCombo, formProps]); | }, [multiplier, selectedBomId, bomCombo, formProps]); | ||||
| const onModalClose = useCallback(() => { | const onModalClose = useCallback(() => { | ||||
| if (isSubmitting) return; | |||||
| reset() | reset() | ||||
| onClose() | onClose() | ||||
| setMultiplier(1); | setMultiplier(1); | ||||
| }, [reset, onClose]) | |||||
| }, [reset, onClose, isSubmitting]) | |||||
| const duplicateLabels = useMemo(() => { | const duplicateLabels = useMemo(() => { | ||||
| const count = new Map<string, number>(); | const count = new Map<string, number>(); | ||||
| bomCombo.forEach((b) => count.set(b.label, (count.get(b.label) ?? 0) + 1)); | bomCombo.forEach((b) => count.set(b.label, (count.get(b.label) ?? 0) + 1)); | ||||
| @@ -149,23 +151,32 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| }, []) | }, []) | ||||
| const onSubmit = useCallback<SubmitHandler<SaveJo>>(async (data) => { | const onSubmit = useCallback<SubmitHandler<SaveJo>>(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<SubmitErrorHandler<SaveJo>>((error) => { | const onSubmitError = useCallback<SubmitErrorHandler<SaveJo>>((error) => { | ||||
| console.log(error) | console.log(error) | ||||
| @@ -505,10 +516,11 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| <Button | <Button | ||||
| name="submit" | name="submit" | ||||
| variant="contained" | variant="contained" | ||||
| startIcon={<Check />} | |||||
| startIcon={isSubmitting ? <CircularProgress size={16} color="inherit" /> : <Check />} | |||||
| type="submit" | type="submit" | ||||
| disabled={isSubmitting} | |||||
| > | > | ||||
| {t("Create")} | |||||
| {isSubmitting ? t("Creating...") : t("Create")} | |||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| </LocalizationProvider> | </LocalizationProvider> | ||||
| @@ -76,32 +76,32 @@ interface CompletedJobOrderPickOrder { | |||||
| // 新增:Lot 详情接口 | // 新增:Lot 详情接口 | ||||
| interface LotDetail { | 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; | processingStatus: string; | ||||
| lotAvailability: string; | lotAvailability: string; | ||||
| pickOrderId: number; | pickOrderId: number; | ||||
| pickOrderCode: string; | pickOrderCode: string; | ||||
| pickOrderConsoCode: string; | pickOrderConsoCode: string; | ||||
| pickOrderLineId: number; | pickOrderLineId: number; | ||||
| stockOutLineId: number; | |||||
| stockOutLineId: number | null; | |||||
| stockOutLineStatus: string; | stockOutLineStatus: string; | ||||
| routerIndex: number; | |||||
| routerArea: string; | |||||
| routerRoute: string; | |||||
| uomShortDesc: string; | |||||
| routerIndex: number | null; | |||||
| routerArea: string | null; | |||||
| routerRoute: string | null; | |||||
| uomShortDesc: string | null; | |||||
| secondQrScanStatus: string; | secondQrScanStatus: string; | ||||
| itemId: number; | itemId: number; | ||||
| itemCode: string; | itemCode: string; | ||||
| itemName: string; | itemName: string; | ||||
| uomCode: string; | uomCode: string; | ||||
| uomDesc: string; | uomDesc: string; | ||||
| match_status: string; | |||||
| match_status: string | null; | |||||
| } | } | ||||
| const CompleteJobOrderRecord: React.FC<Props> = ({ | const CompleteJobOrderRecord: React.FC<Props> = ({ | ||||
| @@ -176,7 +176,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| const lotDetails = await fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId); | const lotDetails = await fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId); | ||||
| setDetailLotData(lotDetails); | |||||
| setDetailLotData(Array.isArray(lotDetails) ? lotDetails : []); | |||||
| console.log(" Fetched lot details:", lotDetails); | console.log(" Fetched lot details:", lotDetails); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("❌ Error fetching lot details:", error); | console.error("❌ Error fetching lot details:", error); | ||||
| @@ -481,7 +481,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| </TableRow> | </TableRow> | ||||
| ) : ( | ) : ( | ||||
| detailLotData.map((lot, index) => ( | detailLotData.map((lot, index) => ( | ||||
| <TableRow key={lot.lotId}> | |||||
| <TableRow key={lot.stockOutLineId ?? lot.lotId ?? index}> | |||||
| <TableCell> | <TableCell> | ||||
| <Typography variant="body2" fontWeight="bold"> | <Typography variant="body2" fontWeight="bold"> | ||||
| {index + 1} | {index + 1} | ||||
| @@ -494,13 +494,13 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell>{lot.itemCode}</TableCell> | <TableCell>{lot.itemCode}</TableCell> | ||||
| <TableCell>{lot.itemName}</TableCell> | <TableCell>{lot.itemName}</TableCell> | ||||
| <TableCell>{lot.lotNo}</TableCell> | |||||
| <TableCell>{lot.lotNo || '-'}</TableCell> | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc}) | |||||
| {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc || ''}) | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc}) | |||||
| {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc || ''}) | |||||
| </TableCell> | </TableCell> | ||||
| {/* 修改:Processing Status 使用复选框 */} | {/* 修改:Processing Status 使用复选框 */} | ||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| @@ -464,6 +464,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | ||||
| // issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required) | // issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required) | ||||
| const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({}); | const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({}); | ||||
| // 防止同一行(以 stockOutLineId/solId 识别)被重复点击提交/完成 | |||||
| const [actionBusyBySolId, setActionBusyBySolId] = useState<Record<number, boolean>>({}); | |||||
| const [paginationController, setPaginationController] = useState({ | const [paginationController, setPaginationController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| @@ -553,6 +555,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| itemName: line.itemName, | itemName: line.itemName, | ||||
| uomCode: line.uomCode, | uomCode: line.uomCode, | ||||
| uomDesc: line.uomDesc, | uomDesc: line.uomDesc, | ||||
| itemTotalAvailableQty: line.totalAvailableQty ?? null, | |||||
| pickOrderLineRequiredQty: line.requiredQty, | pickOrderLineRequiredQty: line.requiredQty, | ||||
| pickOrderLineStatus: line.status, | pickOrderLineStatus: line.status, | ||||
| jobOrderId: data.pickOrder.jobOrder.id, | jobOrderId: data.pickOrder.jobOrder.id, | ||||
| @@ -588,6 +591,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| itemName: line.itemName, | itemName: line.itemName, | ||||
| uomCode: line.uomCode, | uomCode: line.uomCode, | ||||
| uomDesc: line.uomDesc, | uomDesc: line.uomDesc, | ||||
| itemTotalAvailableQty: line.totalAvailableQty ?? null, | |||||
| pickOrderLineRequiredQty: line.requiredQty, | pickOrderLineRequiredQty: line.requiredQty, | ||||
| pickOrderLineStatus: line.status, | pickOrderLineStatus: line.status, | ||||
| jobOrderId: data.pickOrder.jobOrder.id, | jobOrderId: data.pickOrder.jobOrder.id, | ||||
| @@ -2505,6 +2509,7 @@ const sortedData = [...sourceData].sort((a, b) => { | |||||
| <TableCell>{t("Item Name")}</TableCell> | <TableCell>{t("Item Name")}</TableCell> | ||||
| <TableCell>{t("Lot No")}</TableCell> | <TableCell>{t("Lot No")}</TableCell> | ||||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | ||||
| <TableCell align="right">{t("Available Qty")}</TableCell> | |||||
| <TableCell align="center">{t("Scan Result")}</TableCell> | <TableCell align="center">{t("Scan Result")}</TableCell> | ||||
| <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell> | <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -2512,7 +2517,7 @@ const sortedData = [...sourceData].sort((a, b) => { | |||||
| <TableBody> | <TableBody> | ||||
| {paginatedData.length === 0 ? ( | {paginatedData.length === 0 ? ( | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={8} align="center"> | |||||
| <TableCell colSpan={9} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("No data available")} | {t("No data available")} | ||||
| </Typography> | </Typography> | ||||
| @@ -2551,7 +2556,18 @@ const sortedData = [...sourceData].sort((a, b) => { | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| {(() => { | {(() => { | ||||
| const requiredQty = lot.requiredQty || 0; | 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})`; | |||||
| })()} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {(() => { | |||||
| const avail = lot.itemTotalAvailableQty; | |||||
| if (avail == null) return "-"; | |||||
| const unit = lot.uomDesc || ""; | |||||
| return `${Number(avail).toLocaleString()}(${unit})`; | |||||
| })()} | })()} | ||||
| </TableCell> | </TableCell> | ||||
| @@ -2636,13 +2652,24 @@ const sortedData = [...sourceData].sort((a, b) => { | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| onClick={async () => { | onClick={async () => { | ||||
| const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; | |||||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||||
| handlePickQtyChange(lotKey, submitQty); | |||||
| await handleSubmitPickQtyWithQty(lot, submitQty); | |||||
| const solId = Number(lot.stockOutLineId) || 0; | |||||
| if (solId > 0) { | |||||
| setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); | |||||
| } | |||||
| try { | |||||
| const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; | |||||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||||
| handlePickQtyChange(lotKey, submitQty); | |||||
| await handleSubmitPickQtyWithQty(lot, submitQty); | |||||
| } finally { | |||||
| if (solId > 0) { | |||||
| setActionBusyBySolId(prev => ({ ...prev, [solId]: false })); | |||||
| } | |||||
| } | |||||
| }} | }} | ||||
| disabled={ | disabled={ | ||||
| (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) || | |||||
| (lot.lotAvailability === 'expired' || | (lot.lotAvailability === 'expired' || | ||||
| lot.lotAvailability === 'status_unavailable' || | lot.lotAvailability === 'status_unavailable' || | ||||
| lot.lotAvailability === 'rejected') || | lot.lotAvailability === 'rejected') || | ||||
| @@ -2682,17 +2709,37 @@ const sortedData = [...sourceData].sort((a, b) => { | |||||
| variant="outlined" | variant="outlined" | ||||
| size="small" | size="small" | ||||
| onClick={async () => { | onClick={async () => { | ||||
| // ✅ 更新 handler 后再提交 | |||||
| if (currentUserId && lot.pickOrderId && lot.itemId) { | |||||
| try { | |||||
| await updateHandledBy(lot.pickOrderId, lot.itemId); | |||||
| } catch (error) { | |||||
| console.error("❌ Error updating handler (non-critical):", error); | |||||
| const solId = Number(lot.stockOutLineId) || 0; | |||||
| if (solId > 0) { | |||||
| setActionBusyBySolId(prev => ({ ...prev, [solId]: true })); | |||||
| } | |||||
| try { | |||||
| // ✅ 更新 handler 后再提交 | |||||
| if (currentUserId && lot.pickOrderId && lot.itemId) { | |||||
| try { | |||||
| await updateHandledBy(lot.pickOrderId, lot.itemId); | |||||
| } catch (error) { | |||||
| console.error("❌ Error updating handler (non-critical):", error); | |||||
| } | |||||
| } | |||||
| await handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0); | |||||
| } finally { | |||||
| if (solId > 0) { | |||||
| setActionBusyBySolId(prev => ({ ...prev, [solId]: false })); | |||||
| } | } | ||||
| } | } | ||||
| await handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0); | |||||
| }} | }} | ||||
| disabled={lot.stockOutLineStatus === 'completed' || lot.noLot === true || !lot.lotId} | |||||
| disabled={ | |||||
| (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) || | |||||
| lot.stockOutLineStatus === 'completed' || | |||||
| lot.noLot === true || | |||||
| !lot.lotId || | |||||
| (Number(lot.stockOutLineId) > 0 && | |||||
| Object.prototype.hasOwnProperty.call( | |||||
| issuePickedQtyBySolId, | |||||
| Number(lot.stockOutLineId) | |||||
| )) | |||||
| } | |||||
| sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }} | sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }} | ||||
| > | > | ||||
| {t("Just Complete")} | {t("Just Complete")} | ||||
| @@ -139,6 +139,7 @@ | |||||
| "Expiry Date": "有效期", | "Expiry Date": "有效期", | ||||
| "Target Date": "需求日期", | "Target Date": "需求日期", | ||||
| "Lot Required Pick Qty": "批號需求數", | "Lot Required Pick Qty": "批號需求數", | ||||
| "Available Qty": "可用數量", | |||||
| "Job Order Match": "工單對料", | "Job Order Match": "工單對料", | ||||
| "Lot No": "批號", | "Lot No": "批號", | ||||
| "Submit Required Pick Qty": "提交需求數", | "Submit Required Pick Qty": "提交需求數", | ||||