| @@ -394,6 +394,11 @@ export interface BatchSaveApproverStockTakeAllRequest { | |||||
| sectionDescription?: string | null; | sectionDescription?: string | null; | ||||
| stockTakeSections?: string | null; // 逗號字串 | stockTakeSections?: string | null; // 逗號字串 | ||||
| } | } | ||||
| export interface BatchSaveApproverStockTakeByIdsRequest { | |||||
| stockTakeId: number; | |||||
| approverId: number; | |||||
| recordIds: number[]; | |||||
| } | |||||
| export const saveApproverStockTakeRecord = async ( | export const saveApproverStockTakeRecord = async ( | ||||
| request: SaveApproverStockTakeRecordRequest, | request: SaveApproverStockTakeRecordRequest, | ||||
| stockTakeId: number | stockTakeId: number | ||||
| @@ -451,6 +456,18 @@ export const batchSaveApproverStockTakeRecordsAll = cache(async (data: BatchSave | |||||
| return r | 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 ( | export const updateStockTakeRecordStatusToNotMatch = async ( | ||||
| stockTakeRecordId: number | stockTakeRecordId: number | ||||
| ) => { | ) => { | ||||
| @@ -488,7 +488,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({ | |||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={handleSearch} | onSearch={handleSearch} | ||||
| onReset={handleSearchReset} | onReset={handleSearchReset} | ||||
| searchQuery={searchQuery} | |||||
| // searchQuery={searchQuery} | |||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| <Stack | <Stack | ||||
| @@ -36,7 +36,6 @@ import { | |||||
| type PickOrderLotDetailResponse, | type PickOrderLotDetailResponse, | ||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| import { workbenchScanPick } from "@/app/api/doworkbench/actions"; | import { workbenchScanPick } from "@/app/api/doworkbench/actions"; | ||||
| import { workbenchScanPickResponseNeedsFullRefresh } from "@/app/api/doworkbench/workbenchScanPickUtils"; | |||||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | import { fetchStockInLineInfo } from "@/app/api/po/actions"; | ||||
| import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; | import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; | ||||
| import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider"; | import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider"; | ||||
| @@ -584,26 +583,26 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| ...(typeof qtyValue === "number" && Number.isFinite(qtyValue) ? { qty: qtyValue } : {}), | ...(typeof qtyValue === "number" && Number.isFinite(qtyValue) ? { qty: qtyValue } : {}), | ||||
| userId, | 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"); | const okMsg = localizeBackendMessage(res.message, "Scan pick success"); | ||||
| setMessage(okMsg); | setMessage(okMsg); | ||||
| setQrScanSuccessMsg(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); | setWorkbenchLotLabelModalOpen(false); | ||||
| setWorkbenchLotLabelContextLot(null); | setWorkbenchLotLabelContextLot(null); | ||||
| @@ -31,6 +31,7 @@ import { | |||||
| InventoryLotDetailResponse, | InventoryLotDetailResponse, | ||||
| SaveApproverStockTakeRecordRequest, | SaveApproverStockTakeRecordRequest, | ||||
| saveApproverStockTakeRecord, | saveApproverStockTakeRecord, | ||||
| batchSaveApproverStockTakeRecordsByIds, | |||||
| getApproverInventoryLotDetailsAllPending, | getApproverInventoryLotDetailsAllPending, | ||||
| getApproverInventoryLotDetailsAllApproved, | getApproverInventoryLotDetailsAllApproved, | ||||
| updateStockTakeRecordStatusToNotMatch, | updateStockTakeRecordStatusToNotMatch, | ||||
| @@ -745,75 +746,32 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| } | } | ||||
| setBatchSaving(true); | setBatchSaving(true); | ||||
| let successCount = 0; | |||||
| let skippedApproverEmpty = 0; | |||||
| let errorCount = 0; | |||||
| try { | 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( | 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); | await loadDetails(appliedFilters); | ||||
| } | } | ||||
| } catch (e: any) { | } catch (e: any) { | ||||
| @@ -835,10 +793,6 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||||
| mode, | mode, | ||||
| appliedFilters, | appliedFilters, | ||||
| inventoryLotDetails.length, | inventoryLotDetails.length, | ||||
| sortedDetails, | |||||
| qtySelection, | |||||
| approverQty, | |||||
| approverBadQty, | |||||
| ]); | ]); | ||||
| const formatNumber = (num: number | null | undefined): string => { | const formatNumber = (num: number | null | undefined): string => { | ||||