Procházet zdrojové kódy

update

MergeProblem1
CANCERYS\kw093 před 23 hodinami
rodič
revize
4851a4d4c5
6 změnil soubory, kde provedl 194 přidání a 69 odebrání
  1. +11
    -2
      src/app/api/stockTake/actions.ts
  2. +4
    -1
      src/components/StockTakeManagement/ApproverStockTake.tsx
  3. +168
    -64
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  4. +4
    -1
      src/components/StockTakeManagement/PickerReStockTake.tsx
  5. +4
    -1
      src/components/StockTakeManagement/PickerStockTake.tsx
  6. +3
    -0
      src/i18n/zh/inventory.json

+ 11
- 2
src/app/api/stockTake/actions.ts Zobrazit soubor

@@ -81,11 +81,13 @@ export const getInventoryLotDetailsBySection = async (
stockTakeSection: string, stockTakeSection: string,
stockTakeId?: number | null, stockTakeId?: number | null,
pageNum?: number, pageNum?: number,
pageSize?: number
pageSize?: number,
stockTakeRoundId?: number | null
) => { ) => {
console.log('🌐 [API] getInventoryLotDetailsBySection called with:', { console.log('🌐 [API] getInventoryLotDetailsBySection called with:', {
stockTakeSection, stockTakeSection,
stockTakeId, stockTakeId,
stockTakeRoundId,
pageNum, pageNum,
pageSize pageSize
}); });
@@ -95,6 +97,9 @@ export const getInventoryLotDetailsBySection = async (
if (stockTakeId != null && stockTakeId > 0) { if (stockTakeId != null && stockTakeId > 0) {
url += `&stockTakeId=${stockTakeId}`; url += `&stockTakeId=${stockTakeId}`;
} }
if (stockTakeRoundId != null && stockTakeRoundId > 0) {
url += `&stockTakeRoundId=${stockTakeRoundId}`;
}
console.log(' [API] Full URL:', url); console.log(' [API] Full URL:', url);
@@ -476,7 +481,8 @@ export const getInventoryLotDetailsBySectionNotMatch = async (
stockTakeSection: string, stockTakeSection: string,
stockTakeId?: number | null, stockTakeId?: number | null,
pageNum: number = 0, pageNum: number = 0,
pageSize: number = 10
pageSize: number = 10,
stockTakeRoundId?: number | null
) => { ) => {
const encodedSection = encodeURIComponent(stockTakeSection); const encodedSection = encodeURIComponent(stockTakeSection);
let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}&pageNum=${pageNum}`; let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}&pageNum=${pageNum}`;
@@ -490,6 +496,9 @@ export const getInventoryLotDetailsBySectionNotMatch = async (
if (stockTakeId != null && stockTakeId > 0) { if (stockTakeId != null && stockTakeId > 0) {
url += `&stockTakeId=${stockTakeId}`; url += `&stockTakeId=${stockTakeId}`;
} }
if (stockTakeRoundId != null && stockTakeRoundId > 0) {
url += `&stockTakeRoundId=${stockTakeRoundId}`;
}
const response = await serverFetchJson<RecordsRes<InventoryLotDetailResponse>>( const response = await serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(
url, url,


+ 4
- 1
src/components/StockTakeManagement/ApproverStockTake.tsx Zobrazit soubor

@@ -116,7 +116,10 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
selectedSession.stockTakeSession, selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum, pageNum,
actualSize
actualSize,
selectedSession.stockTakeRoundId != null && selectedSession.stockTakeRoundId > 0
? selectedSession.stockTakeRoundId
: null
); );
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
setTotal(response.total || 0); setTotal(response.total || 0);


+ 168
- 64
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Zobrazit soubor

@@ -33,8 +33,6 @@ import {
saveApproverStockTakeRecord, saveApproverStockTakeRecord,
getApproverInventoryLotDetailsAllPending, getApproverInventoryLotDetailsAllPending,
getApproverInventoryLotDetailsAllApproved, getApproverInventoryLotDetailsAllApproved,
BatchSaveApproverStockTakeAllRequest,
batchSaveApproverStockTakeRecordsAll,
updateStockTakeRecordStatusToNotMatch, updateStockTakeRecordStatusToNotMatch,
type ApproverInventoryLotDetailsQuery, type ApproverInventoryLotDetailsQuery,
} from "@/app/api/stockTake/actions"; } from "@/app/api/stockTake/actions";
@@ -94,6 +92,79 @@ function hasAnyApproverSearchCriterion(f: ApproverSearchFilters): boolean {
); );
} }


function isBlankApproverField(value: string | undefined): boolean {
return value == null || String(value).trim() === "";
}

/** 審核員「盤點數」與「不良數」皆未輸入時應跳過儲存,維持 pending */
function isApproverInputsBothEmpty(
detailId: number,
approverQty: Record<number, string>,
approverBadQty: Record<number, string>
): boolean {
return isBlankApproverField(approverQty[detailId]) && isBlankApproverField(approverBadQty[detailId]);
}

type ApproverSaveBuildResult =
| {
ok: true;
request: SaveApproverStockTakeRecordRequest;
finalQty: number;
finalBadQty: number;
goodQty: number;
selection: QtySelectionType;
}
| { ok: false; reason: "skip_approver_empty" }
| { ok: false; reason: "error"; message: string };

function buildApproverSaveRequest(
detail: InventoryLotDetailResponse,
qtySelection: Record<number, QtySelectionType>,
approverQty: Record<number, string>,
approverBadQty: Record<number, string>,
currentUserId: number,
t: (key: string) => string
): ApproverSaveBuildResult {
const selection = qtySelection[detail.id] || "first";
let finalQty: number;
let finalBadQty: number;

if (selection === "first") {
if (detail.firstStockTakeQty == null) {
return { ok: false, reason: "error", message: t("First QTY is not available") };
}
finalQty = detail.firstStockTakeQty;
finalBadQty = detail.firstBadQty || 0;
} else if (selection === "second") {
if (detail.secondStockTakeQty == null) {
return { ok: false, reason: "error", message: t("Second QTY is not available") };
}
finalQty = detail.secondStockTakeQty;
finalBadQty = detail.secondBadQty || 0;
} else {
if (isApproverInputsBothEmpty(detail.id, approverQty, approverBadQty)) {
return { ok: false, reason: "skip_approver_empty" };
}
const approverQtyValue = approverQty[detail.id] || "0";
const approverBadQtyValue = approverBadQty[detail.id] || "0";
finalQty = parseFloat(approverQtyValue) || 0;
finalBadQty = parseFloat(approverBadQtyValue) || 0;
}

const request: SaveApproverStockTakeRecordRequest = {
stockTakeRecordId: detail.stockTakeRecordId || null,
qty: finalQty,
badQty: finalBadQty,
approverId: currentUserId,
approverQty: selection === "approver" ? finalQty : null,
approverBadQty: selection === "approver" ? finalBadQty : null,
lastSelect: selection === "first" ? 1 : selection === "second" ? 2 : 3,
};

const goodQty = finalQty - finalBadQty;
return { ok: true, request, finalQty, finalBadQty, goodQty, selection };
}

function parseDateTimeMs( function parseDateTimeMs(
v: string | string[] | null | undefined v: string | string[] | null | undefined
): number { ): number {
@@ -528,52 +599,31 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
return; return;
} }


const selection = qtySelection[detail.id] || "first";
let finalQty: number;
let finalBadQty: number;

if (selection === "first") {
if (detail.firstStockTakeQty == null) {
onSnackbar(t("First QTY is not available"), "error");
return;
}
finalQty = detail.firstStockTakeQty;
finalBadQty = detail.firstBadQty || 0;
} else if (selection === "second") {
if (detail.secondStockTakeQty == null) {
onSnackbar(t("Second QTY is not available"), "error");
const built = buildApproverSaveRequest(
detail,
qtySelection,
approverQty,
approverBadQty,
currentUserId,
t
);
if (!built.ok) {
if (built.reason === "skip_approver_empty") {
onSnackbar(t("Approver input empty; save skipped, row remains pending"), "warning");
return; return;
} }

finalQty = detail.secondStockTakeQty;
finalBadQty = detail.secondBadQty || 0;
} else {
// 与 Picker 逻辑一致:Approver 输入为空时按 0 处理
const approverQtyValue = approverQty[detail.id] || "0";
const approverBadQtyValue = approverBadQty[detail.id] || "0";
finalQty = parseFloat(approverQtyValue) || 0;
finalBadQty = parseFloat(approverBadQtyValue) || 0;
onSnackbar(built.message, "error");
return;
} }


const { request, goodQty, finalQty, finalBadQty, selection } = built;

setSaving(true); setSaving(true);
try { try {
const request: SaveApproverStockTakeRecordRequest = {
stockTakeRecordId: detail.stockTakeRecordId || null,
qty: finalQty,
badQty: finalBadQty,
approverId: currentUserId,
approverQty: selection === "approver" ? finalQty : null,
approverBadQty: selection === "approver" ? finalBadQty : null,
// lastSelect: 1=First, 2=Second, 3=Approver Input
lastSelect: selection === "first" ? 1 : selection === "second" ? 2 : 3,
};

await saveApproverStockTakeRecord(request, selectedSession.stockTakeId); await saveApproverStockTakeRecord(request, selectedSession.stockTakeId);


onSnackbar(t("Approver stock take record saved successfully"), "success"); onSnackbar(t("Approver stock take record saved successfully"), "success");


const goodQty = finalQty - finalBadQty;

setInventoryLotDetails((prev) => setInventoryLotDetails((prev) =>
prev.map((d) => prev.map((d) =>
d.id === detail.id d.id === detail.id
@@ -666,53 +716,107 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
if (!selectedSession || !currentUserId) { if (!selectedSession || !currentUserId) {
return; return;
} }
if (inventoryLotDetails.length === 0) {
onSnackbar(t("No rows loaded; set search criteria and search first"), "warning");
return;
}


setBatchSaving(true); setBatchSaving(true);
let successCount = 0;
let skippedApproverEmpty = 0;
let errorCount = 0;

try { try {
const request: BatchSaveApproverStockTakeAllRequest = {
stockTakeId: selectedSession.stockTakeId,
approverId: currentUserId,
itemKeyword: appliedFilters?.itemKeyword || null,
warehouseKeyword: appliedFilters?.warehouseKeyword || null,
sectionDescription:
appliedFilters?.sectionDescription && appliedFilters.sectionDescription !== "All"
? appliedFilters.sectionDescription
: null,
stockTakeSections: appliedFilters?.stockTakeSession || null,
};
for (const detail of sortedDetails) {
if (detail.stockTakeRecordStatus === "completed") {
continue;
}


const result = await batchSaveApproverStockTakeRecordsAll(request);
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);
}
}


onSnackbar( onSnackbar(
t("Batch approver save completed: {{success}} success, {{errors}} errors", {
success: result.successCount,
errors: result.errorCount,
t("Batch approver save completed: {{success}} success, {{skipped}} skipped, {{errors}} errors", {
success: successCount,
skipped: skippedApproverEmpty,
errors: errorCount,
}), }),
result.errorCount > 0 ? "warning" : "success"
errorCount > 0 ? "warning" : "success"
); );


if (appliedFilters) {
if (appliedFilters && successCount > 0) {
await loadDetails(appliedFilters); await loadDetails(appliedFilters);
} }
} catch (e: any) { } catch (e: any) {
console.error("handleBatchSubmitAll (all): Error:", e); console.error("handleBatchSubmitAll (all): Error:", e);
let errorMessage = t("Failed to batch save approver stock take records"); let errorMessage = t("Failed to batch save approver stock take records");

if (e?.message) { if (e?.message) {
errorMessage = e.message; errorMessage = e.message;
} else if (e?.response) {
try {
const errorData = await e.response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
}
} }

onSnackbar(errorMessage, "error"); onSnackbar(errorMessage, "error");
} finally { } finally {
setBatchSaving(false); setBatchSaving(false);
} }
}, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize, mode, appliedFilters]);
}, [
selectedSession,
currentUserId,
t,
onSnackbar,
loadDetails,
mode,
appliedFilters,
inventoryLotDetails.length,
sortedDetails,
qtySelection,
approverQty,
approverBadQty,
]);


const formatNumber = (num: number | null | undefined): string => { const formatNumber = (num: number | null | undefined): string => {
if (num == null) return "0"; if (num == null) return "0";


+ 4
- 1
src/components/StockTakeManagement/PickerReStockTake.tsx Zobrazit soubor

@@ -111,7 +111,10 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
selectedSession.stockTakeSession, selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum, pageNum,
actualSize
actualSize,
selectedSession.stockTakeRoundId != null && selectedSession.stockTakeRoundId > 0
? selectedSession.stockTakeRoundId
: null
); );
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
setTotal(response.total || 0); setTotal(response.total || 0);


+ 4
- 1
src/components/StockTakeManagement/PickerStockTake.tsx Zobrazit soubor

@@ -125,7 +125,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
selectedSession.stockTakeSession, selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum, pageNum,
actualSize
actualSize,
selectedSession.stockTakeRoundId != null && selectedSession.stockTakeRoundId > 0
? selectedSession.stockTakeRoundId
: null
); );
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
setTotal(response.total || 0); setTotal(response.total || 0);


+ 3
- 0
src/i18n/zh/inventory.json Zobrazit soubor

@@ -181,6 +181,9 @@
"UNAVAILABLE": "不可用", "UNAVAILABLE": "不可用",
"No issues found": "未找到問題", "No issues found": "未找到問題",
"Approver stock take record saved successfully": "審核員盤點記錄保存成功", "Approver stock take record saved successfully": "審核員盤點記錄保存成功",
"Approver input empty; save skipped, row remains pending": "審核員盤點數與不良數皆未輸入,已略過儲存,該列維持待審核",
"No rows loaded; set search criteria and search first": "尚未載入資料,請設定搜尋條件並按搜尋",
"Batch approver save completed: {{success}} success, {{skipped}} skipped, {{errors}} errors": "批次審核儲存完成:成功 {{success}} 筆,略過 {{skipped}} 筆,錯誤 {{errors}} 筆",
"Approver Input": "審核員輸入", "Approver Input": "審核員輸入",
"Approve": "審核", "Approve": "審核",
"complete": "完成", "complete": "完成",


Načítá se…
Zrušit
Uložit