diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index f7fc806..61227aa 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -408,6 +408,40 @@ export async function printDNLabels(request: PrintDNLabelsRequest){ return { success: true, message: "Print job sent successfully (labels)"} as PrintDeliveryNoteResponse } + +export interface ResetDoPickOrderResponse { + success: boolean; + message?: string; +} + +export async function resetDoPickOrderToNonPick(doPickOrderRecordId: number): Promise { + const params = new URLSearchParams(); + params.append("doPickOrderRecordId", doPickOrderRecordId.toString()); + + try { + const response = await serverFetchWithNoContent( + `${BASE_API_URL}/doPickOrder/reset-to-non-pick?${params.toString()}`, + { + method: "POST", + }, + ); + + if (response) { + return { success: true }; + } + + return { + success: false, + message: "Failed to reset DO pick order to non-pick state.", + }; + } catch (error) { + console.error("Error in resetDoPickOrderToNonPick:", error); + return { + success: false, + message: "Error occurred while resetting DO pick order to non-pick state.", + }; + } +} export interface Check4FTruckBatchResponse { hasProblem: boolean; problems: ProblemDoDto[]; diff --git a/src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx b/src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx index 1fad172..97980cf 100644 --- a/src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx +++ b/src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx @@ -20,15 +20,22 @@ import { Paper, CircularProgress, TablePagination, - Chip + Chip, + Button, } from '@mui/material'; import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import { arrayToDayjs } from '@/app/utils/formatUtil'; -import { fetchTicketReleaseTable, getTicketReleaseTable } from '@/app/api/do/actions'; +import { fetchTicketReleaseTable, getTicketReleaseTable, resetDoPickOrderToNonPick } from '@/app/api/do/actions'; +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; +import { AUTH } from "@/authorities"; +import Swal from 'sweetalert2'; const FGPickOrderTicketReleaseTable: React.FC = () => { const { t } = useTranslation("ticketReleaseTable"); + const { data: session } = useSession() as { data: SessionWithTokens | null }; + const abilities = (session?.abilities ?? []) as string[]; const [selectedDate, setSelectedDate] = useState("today"); const [selectedFloor, setSelectedFloor] = useState(""); const [selectedStatus, setSelectedStatus] = useState("released"); @@ -280,6 +287,7 @@ useEffect(() => { {t("Handler Name")} {t("Number of FG Items (Order Item(s) Count)")} + {t("Actions")} @@ -378,6 +386,55 @@ useEffect(() => { {row.handlerName ?? 0} {row.numberOfFGItems ?? 0} + + {abilities.includes(AUTH.ADMIN) && ( + + )} + ); }) diff --git a/src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx b/src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx index be57310..c150c44 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx @@ -771,34 +771,38 @@ if (showDetailView && selectedDoPickOrder) { > {t("View Details")} - - <> - - - - - + + + + + ))} diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index 5e12d87..18525e8 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -581,14 +581,29 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); if (combinedLotData.length === 0) { return { completed: 0, total: 0 }; } - - const nonPendingCount = combinedLotData.filter(lot => { - const status = lot.stockOutLineStatus?.toLowerCase(); - return status !== 'pending'; + + // Use same logic as scannedItemsCount: count only lines that are actually scanned/submittable + const scannedCount = combinedLotData.filter(lot => { + const status = lot.stockOutLineStatus; + if (!status) return false; + + if (lot.noLot === true) { + return ( + status === 'checked' || + status === 'partially_completed' || + status === 'PARTIALLY_COMPLETE' + ); + } + + return ( + status === 'checked' || + status === 'partially_completed' || + status === 'PARTIALLY_COMPLETE' + ); }).length; - + return { - completed: nonPendingCount, + completed: scannedCount, total: combinedLotData.length }; }, [combinedLotData]); @@ -2740,46 +2755,33 @@ const handleSubmitAllScanned = useCallback(async () => { try { // 转换为 batchSubmitList 所需的格式(与后端 QrPickBatchSubmitRequest 匹配) const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => { - // 1. 需求数量 + // 1. 需求数量:优先用 lot.requiredQty,没有就用 pickOrderLineRequiredQty const requiredQty = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0); - - // 2. 当前已经拣到的数量(数据库里的 qty) + + // 2. 当前已经拣到的数量(数据库里对应的是 stock_out_line.qty) const currentActualPickQty = Number(lot.actualPickQty || 0); - - // 🔹 判断是否走“只改状态模式” - // 这里先给一个简单条件示例:如果你不想再补拣,只想把当前数量标记完成, - // 就让这个条件为 true(后面你可以根据业务加 UI 开关或别的 flag)。 - const onlyComplete = lot.stockOutLineStatus === "partially_completed"; - // lot.stockOutLineStatus === "partially_completed" && false === true; - - let targetActual: number; - let newStatus: string; - - if (onlyComplete) { - // ✅ 只改状态:目标数量 = 当前数量,不再补拣 - targetActual = currentActualPickQty; + + // 3. 还需要拣多少:不能为负数 + const remainingQty = Math.max(0, requiredQty - currentActualPickQty); + + // 4. 本次批量提交后的目标累计值 = 当前 + 剩余 + const cumulativeQty = currentActualPickQty + remainingQty; + + // 5. 根据“目标累计值是否达到需求”决定状态 + let newStatus = "partially_completed"; + if (requiredQty > 0 && cumulativeQty >= requiredQty) { newStatus = "completed"; - } else { - // ✅ 补拣模式:把剩余全部拣完 - const remainingQty = Math.max(0, requiredQty - currentActualPickQty); - const cumulativeQty = currentActualPickQty + remainingQty; - - targetActual = cumulativeQty; - - newStatus = "partially_completed"; - if (requiredQty > 0 && cumulativeQty >= requiredQty) { - newStatus = "completed"; - } } - + return { stockOutLineId: Number(lot.stockOutLineId) || 0, pickOrderLineId: Number(lot.pickOrderLineId), + // ⚠️ 这里按你现在的写法是用 lot.lotId,当心是否真的是 inventoryLotLineId inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null, requiredQty, - // 后端用 targetActual - 当前 qty 算增量,onlyComplete 时就是 0 - actualPickQty: targetActual, + // 传“目标累计值”,后端会用它减去当前数据库里的 qty 得到增量 + actualPickQty: cumulativeQty, stockOutLineStatus: newStatus, pickOrderConsoCode: String(lot.pickOrderConsoCode || ""), noLot: Boolean(lot.noLot === true), @@ -2838,36 +2840,29 @@ const handleSubmitAllScanned = useCallback(async () => { } }, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext, currentUserId, onSwitchToRecordTab, onRefreshReleasedOrderCount]); - // Calculate scanned items count - // Calculate scanned items count (should match handleSubmitAllScanned filter logic) - const scannedItemsCount = useMemo(() => { - const filtered = combinedLotData.filter(lot => { - const status = lot.stockOutLineStatus; - // ✅ 与 handleSubmitAllScanned 完全保持一致 - if (lot.noLot === true) { - return status === 'checked' || - - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE'; - } - return status === 'checked' || - - status === 'partially_completed' || - status === 'PARTIALLY_COMPLETE'; - }); - - // 添加调试日志 - const noLotCount = filtered.filter(l => l.noLot === true).length; - const normalCount = filtered.filter(l => l.noLot !== true).length; - console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`); - console.log(`📊 All items breakdown:`, { - total: combinedLotData.length, - noLot: combinedLotData.filter(l => l.noLot === true).length, - normal: combinedLotData.filter(l => l.noLot !== true).length - }); - - return filtered.length; - }, [combinedLotData]); + // Calculate scanned items count (must match handleSubmitAllScanned filter logic) + const scannedItemsCount = useMemo(() => { + const filtered = combinedLotData.filter(lot => { + const status = lot.stockOutLineStatus; + if (!status) return false; + + if (lot.noLot === true) { + return ( + status === 'checked' || + status === 'partially_completed' || + status === 'PARTIALLY_COMPLETE' + ); + } + + return ( + status === 'checked' || + status === 'partially_completed' || + status === 'PARTIALLY_COMPLETE' + ); + }); + + return filtered.length; + }, [combinedLotData]); /* // ADD THIS: Auto-stop scan when no data available useEffect(() => { @@ -3183,71 +3178,82 @@ paginatedData.map((lot, index) => { - {(() => { - const status = lot.stockOutLineStatus?.toLowerCase(); - const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected'; - const isNoLot = !lot.lotNo; - - // rejected lot:显示红色勾选(已扫描但被拒绝) - if (isRejected && !isNoLot) { - return ( - - - - ); - } - - // 正常 lot:已扫描(checked/partially_completed/completed) - if (!isNoLot && status !== 'pending' && status !== 'rejected') { - return ( - - - - ); - } - - // noLot 且已完成/部分完成:显示红色勾选 - if (isNoLot && (status === 'partially_completed' || status === 'completed')) { - return ( - - - - ); - } - - return null; - })()} - + {(() => { + const rawStatus = lot.stockOutLineStatus; + const status = rawStatus?.toLowerCase(); + const isRejected = status === "rejected" || lot.lotAvailability === "rejected"; + const isNoLot = !lot.lotNo; + + // rejected lot:显示红色勾选(已扫描但被拒绝) + if (isRejected && !isNoLot) { + return ( + + + + ); + } + + // 正常 lot:已扫描(只有在明确的 scanned 状态时显示勾) + const isScannedNormal = + !isNoLot && + !!status && + (status === "checked" || + status === "partially_completed" || + status === "partially_complete" || + status === "completed" || + status === "complete"); + + if (isScannedNormal) { + return ( + + + + ); + } + + // noLot 且已完成/部分完成:显示红色勾选 + if (isNoLot && (status === "partially_completed" || status === "completed")) { + return ( + + + + ); + } + + // 其他情况(包括 status 为 null/undefined):不显示勾 + return null; + })()} + {(() => {