| @@ -394,6 +394,11 @@ export interface BatchSaveApproverStockTakeAllRequest { | |||
| sectionDescription?: string | null; | |||
| stockTakeSections?: string | null; // 逗號字串 | |||
| } | |||
| export interface BatchSaveApproverStockTakeByIdsRequest { | |||
| stockTakeId: number; | |||
| approverId: number; | |||
| recordIds: number[]; | |||
| } | |||
| export const saveApproverStockTakeRecord = async ( | |||
| request: SaveApproverStockTakeRecordRequest, | |||
| stockTakeId: number | |||
| @@ -451,6 +456,18 @@ export const batchSaveApproverStockTakeRecordsAll = cache(async (data: BatchSave | |||
| return r | |||
| }) | |||
| export const batchSaveApproverStockTakeRecordsByIds = cache(async (data: BatchSaveApproverStockTakeByIdsRequest) => { | |||
| const r = await serverFetchJson<BatchSaveApproverStockTakeRecordResponse>( | |||
| `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecordsByIds`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| } | |||
| ) | |||
| return r | |||
| }) | |||
| export const updateStockTakeRecordStatusToNotMatch = async ( | |||
| stockTakeRecordId: number | |||
| ) => { | |||
| @@ -488,7 +488,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||
| criteria={searchCriteria} | |||
| onSearch={handleSearch} | |||
| onReset={handleSearchReset} | |||
| searchQuery={searchQuery} | |||
| // searchQuery={searchQuery} | |||
| /> | |||
| </Box> | |||
| <Stack | |||
| @@ -36,7 +36,6 @@ import { | |||
| type PickOrderLotDetailResponse, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { workbenchScanPick } from "@/app/api/doworkbench/actions"; | |||
| import { workbenchScanPickResponseNeedsFullRefresh } from "@/app/api/doworkbench/workbenchScanPickUtils"; | |||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | |||
| import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; | |||
| import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider"; | |||
| @@ -584,26 +583,26 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| ...(typeof qtyValue === "number" && Number.isFinite(qtyValue) ? { qty: qtyValue } : {}), | |||
| userId, | |||
| }); | |||
| const errMsg = localizeBackendMessage(res.message, "Scan pick failed"); | |||
| setError(errMsg); | |||
| setQrScanErrorMsg(errMsg); | |||
| const ok = String(res.code || "").toUpperCase() === "SUCCESS"; | |||
| if (!ok) { | |||
| const errMsg = localizeBackendMessage(res.message, "Scan pick failed"); | |||
| setError(errMsg); | |||
| setQrScanErrorMsg(errMsg); | |||
| startTransition(() => { | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| }); | |||
| return; | |||
| } | |||
| const okMsg = localizeBackendMessage(res.message, "Scan pick success"); | |||
| setMessage(okMsg); | |||
| setQrScanSuccessMsg(okMsg); | |||
| if (workbenchScanPickResponseNeedsFullRefresh(res)) { | |||
| if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) { | |||
| await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta); | |||
| } | |||
| } else { | |||
| const entity = res.entity as any; | |||
| setLotRows((prev) => | |||
| prev.map((r) => | |||
| r.stockOutLineId === row.stockOutLineId | |||
| ? { ...r, status: toStr(entity?.status || r.status), pickedQty: toNum(entity?.qty, r.pickedQty) } | |||
| : r, | |||
| ), | |||
| ); | |||
| startTransition(() => { | |||
| setQrScanError(false); | |||
| setQrScanSuccess(true); | |||
| }); | |||
| if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) { | |||
| await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta); | |||
| } | |||
| setWorkbenchLotLabelModalOpen(false); | |||
| setWorkbenchLotLabelContextLot(null); | |||
| @@ -31,6 +31,7 @@ import { | |||
| InventoryLotDetailResponse, | |||
| SaveApproverStockTakeRecordRequest, | |||
| saveApproverStockTakeRecord, | |||
| batchSaveApproverStockTakeRecordsByIds, | |||
| getApproverInventoryLotDetailsAllPending, | |||
| getApproverInventoryLotDetailsAllApproved, | |||
| updateStockTakeRecordStatusToNotMatch, | |||
| @@ -745,75 +746,32 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| } | |||
| setBatchSaving(true); | |||
| let successCount = 0; | |||
| let skippedApproverEmpty = 0; | |||
| let errorCount = 0; | |||
| try { | |||
| for (const detail of sortedDetails) { | |||
| if (detail.stockTakeRecordStatus === "completed") { | |||
| continue; | |||
| } | |||
| const recordIds = sortedDetails | |||
| .map((d) => d.stockTakeRecordId) | |||
| .filter((id): id is number => typeof id === "number" && id > 0); | |||
| const built = buildApproverSaveRequest( | |||
| detail, | |||
| qtySelection, | |||
| approverQty, | |||
| approverBadQty, | |||
| currentUserId, | |||
| t | |||
| ); | |||
| if (!built.ok) { | |||
| if (built.reason === "skip_approver_empty") { | |||
| skippedApproverEmpty += 1; | |||
| continue; | |||
| } | |||
| errorCount += 1; | |||
| continue; | |||
| } | |||
| try { | |||
| await saveApproverStockTakeRecord(built.request, selectedSession.stockTakeId); | |||
| successCount += 1; | |||
| const { goodQty, finalQty, finalBadQty, selection } = built; | |||
| setInventoryLotDetails((prev) => | |||
| prev.map((d) => | |||
| d.id === detail.id | |||
| ? { | |||
| ...d, | |||
| finalQty: goodQty, | |||
| approverQty: selection === "approver" ? finalQty : d.approverQty, | |||
| approverBadQty: selection === "approver" ? finalBadQty : d.approverBadQty, | |||
| stockTakeRecordStatus: "completed", | |||
| } | |||
| : d | |||
| ) | |||
| ); | |||
| } catch (e: any) { | |||
| errorCount += 1; | |||
| let msg = e?.message || t("Failed to save approver stock take record"); | |||
| if (e?.response) { | |||
| try { | |||
| const errorData = await e.response.json(); | |||
| msg = errorData.message || errorData.error || msg; | |||
| } catch { | |||
| /* ignore */ | |||
| } | |||
| } | |||
| console.error("Batch save row failed", detail.id, msg); | |||
| } | |||
| if (recordIds.length === 0) { | |||
| onSnackbar(t("No valid records to batch save"), "warning"); | |||
| return; | |||
| } | |||
| const result = await batchSaveApproverStockTakeRecordsByIds({ | |||
| stockTakeId: selectedSession.stockTakeId, | |||
| approverId: currentUserId, | |||
| recordIds, | |||
| }); | |||
| onSnackbar( | |||
| t("Batch approver save completed: {{success}} success, {{skipped}} skipped, {{errors}} errors", { | |||
| success: successCount, | |||
| skipped: skippedApproverEmpty, | |||
| errors: errorCount, | |||
| t("Batch approver save completed: {{success}} success, {{errors}} errors", { | |||
| success: result.successCount, | |||
| errors: result.errorCount, | |||
| }), | |||
| errorCount > 0 ? "warning" : "success" | |||
| result.errorCount > 0 ? "warning" : "success" | |||
| ); | |||
| if (appliedFilters && successCount > 0) { | |||
| if (appliedFilters && result.successCount > 0) { | |||
| await loadDetails(appliedFilters); | |||
| } | |||
| } catch (e: any) { | |||
| @@ -835,10 +793,6 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| mode, | |||
| appliedFilters, | |||
| inventoryLotDetails.length, | |||
| sortedDetails, | |||
| qtySelection, | |||
| approverQty, | |||
| approverBadQty, | |||
| ]); | |||
| const formatNumber = (num: number | null | undefined): string => { | |||