|
|
@@ -31,7 +31,10 @@ import { |
|
|
BatchSaveApproverStockTakeAllRequest, |
|
|
BatchSaveApproverStockTakeAllRequest, |
|
|
batchSaveApproverStockTakeRecordsAll, |
|
|
batchSaveApproverStockTakeRecordsAll, |
|
|
updateStockTakeRecordStatusToNotMatch, |
|
|
updateStockTakeRecordStatusToNotMatch, |
|
|
|
|
|
type ApproverInventoryLotDetailsQuery, |
|
|
} from "@/app/api/stockTake/actions"; |
|
|
} from "@/app/api/stockTake/actions"; |
|
|
|
|
|
import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; |
|
|
|
|
|
import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; |
|
|
import { useSession } from "next-auth/react"; |
|
|
import { useSession } from "next-auth/react"; |
|
|
import { SessionWithTokens } from "@/config/authConfig"; |
|
|
import { SessionWithTokens } from "@/config/authConfig"; |
|
|
import dayjs from "dayjs"; |
|
|
import dayjs from "dayjs"; |
|
|
@@ -53,6 +56,33 @@ type ApprovedSortKey = |
|
|
| "stockTakerName" |
|
|
| "stockTakerName" |
|
|
| "variance"; |
|
|
| "variance"; |
|
|
|
|
|
|
|
|
|
|
|
type ApproverSearchKey = "sectionDescription" | "stockTakeSession" | "itemCode" | "itemName"; |
|
|
|
|
|
|
|
|
|
|
|
type ApproverSearchFilters = { |
|
|
|
|
|
sectionDescription: string; |
|
|
|
|
|
stockTakeSession: string; |
|
|
|
|
|
itemCode: string; |
|
|
|
|
|
itemName: string; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
function buildApproverInventoryQuery(filters: ApproverSearchFilters): ApproverInventoryLotDetailsQuery { |
|
|
|
|
|
return { |
|
|
|
|
|
sectionDescription: filters.sectionDescription !== "All" ? filters.sectionDescription : undefined, |
|
|
|
|
|
stockTakeSections: filters.stockTakeSession.trim() ? filters.stockTakeSession.trim() : undefined, |
|
|
|
|
|
itemCode: filters.itemCode.trim() ? filters.itemCode.trim() : undefined, |
|
|
|
|
|
itemName: filters.itemName.trim() ? filters.itemName.trim() : undefined, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function hasAnyApproverSearchCriterion(f: ApproverSearchFilters): boolean { |
|
|
|
|
|
return ( |
|
|
|
|
|
(f.sectionDescription && f.sectionDescription !== "All") || |
|
|
|
|
|
f.stockTakeSession.trim() !== "" || |
|
|
|
|
|
f.itemCode.trim() !== "" || |
|
|
|
|
|
f.itemName.trim() !== "" |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function parseDateTimeMs( |
|
|
function parseDateTimeMs( |
|
|
v: string | string[] | null | undefined |
|
|
v: string | string[] | null | undefined |
|
|
): number { |
|
|
): number { |
|
|
@@ -88,6 +118,10 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
const [total, setTotal] = useState(0); |
|
|
const [total, setTotal] = useState(0); |
|
|
const [approvedSortKey, setApprovedSortKey] = useState<ApprovedSortKey | null>(null); |
|
|
const [approvedSortKey, setApprovedSortKey] = useState<ApprovedSortKey | null>(null); |
|
|
const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc"); |
|
|
const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc"); |
|
|
|
|
|
const [sectionDescriptionAutocompleteOptions, setSectionDescriptionAutocompleteOptions] = useState< |
|
|
|
|
|
{ value: string; label: string }[] |
|
|
|
|
|
>([]); |
|
|
|
|
|
const [appliedFilters, setAppliedFilters] = useState<ApproverSearchFilters | null>(null); |
|
|
|
|
|
|
|
|
const currentUserId = session?.id ? parseInt(session.id) : undefined; |
|
|
const currentUserId = session?.id ? parseInt(session.id) : undefined; |
|
|
|
|
|
|
|
|
@@ -108,8 +142,64 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
[] |
|
|
[] |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handleApproverSearchBoxSearch = useCallback( |
|
|
|
|
|
(inputs: Record<ApproverSearchKey | `${ApproverSearchKey}To`, string>) => { |
|
|
|
|
|
const next: ApproverSearchFilters = { |
|
|
|
|
|
sectionDescription: inputs.sectionDescription || "All", |
|
|
|
|
|
stockTakeSession: inputs.stockTakeSession || "", |
|
|
|
|
|
itemCode: inputs.itemCode || "", |
|
|
|
|
|
itemName: inputs.itemName || "", |
|
|
|
|
|
}; |
|
|
|
|
|
if (!hasAnyApproverSearchCriterion(next)) { |
|
|
|
|
|
onSnackbar(t("Please set at least one search criterion"), "warning"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
setAppliedFilters(next); |
|
|
|
|
|
setPage(0); |
|
|
|
|
|
}, |
|
|
|
|
|
[onSnackbar, t] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handleApproverSearchBoxReset = useCallback(() => { |
|
|
|
|
|
setAppliedFilters(null); |
|
|
|
|
|
setPage(0); |
|
|
|
|
|
setInventoryLotDetails([]); |
|
|
|
|
|
setTotal(0); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const approverSearchCriteria: Criterion<ApproverSearchKey>[] = useMemo( |
|
|
|
|
|
() => [ |
|
|
|
|
|
{ |
|
|
|
|
|
type: "autocomplete", |
|
|
|
|
|
label: t("Stock Take Section Description"), |
|
|
|
|
|
paramName: "sectionDescription", |
|
|
|
|
|
options: sectionDescriptionAutocompleteOptions, |
|
|
|
|
|
needAll: true, |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
type: "text", |
|
|
|
|
|
label: t("Stock Take Section (can use , to search multiple sections)"), |
|
|
|
|
|
paramName: "stockTakeSession", |
|
|
|
|
|
placeholder: "", |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
type: "text", |
|
|
|
|
|
label: t("Item Code"), |
|
|
|
|
|
paramName: "itemCode", |
|
|
|
|
|
placeholder: "", |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
type: "text", |
|
|
|
|
|
label: t("Item Name"), |
|
|
|
|
|
paramName: "itemName", |
|
|
|
|
|
placeholder: "", |
|
|
|
|
|
}, |
|
|
|
|
|
], |
|
|
|
|
|
[t, sectionDescriptionAutocompleteOptions] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const loadDetails = useCallback( |
|
|
const loadDetails = useCallback( |
|
|
async (pageNum: number, size: number | string) => { |
|
|
|
|
|
|
|
|
async (pageNum: number, size: number | string, filters: ApproverSearchFilters) => { |
|
|
setLoadingDetails(true); |
|
|
setLoadingDetails(true); |
|
|
try { |
|
|
try { |
|
|
let actualSize: number; |
|
|
let actualSize: number; |
|
|
@@ -125,16 +215,19 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
actualSize = typeof size === "string" ? parseInt(size, 10) : size; |
|
|
actualSize = typeof size === "string" ? parseInt(size, 10) : size; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const searchQuery: ApproverInventoryLotDetailsQuery = buildApproverInventoryQuery(filters); |
|
|
const response = mode === "approved" |
|
|
const response = mode === "approved" |
|
|
? await getApproverInventoryLotDetailsAllApproved( |
|
|
? await getApproverInventoryLotDetailsAllApproved( |
|
|
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, |
|
|
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, |
|
|
pageNum, |
|
|
pageNum, |
|
|
actualSize |
|
|
|
|
|
|
|
|
actualSize, |
|
|
|
|
|
searchQuery |
|
|
) |
|
|
) |
|
|
: await getApproverInventoryLotDetailsAllPending( |
|
|
: await getApproverInventoryLotDetailsAllPending( |
|
|
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, |
|
|
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, |
|
|
pageNum, |
|
|
pageNum, |
|
|
actualSize |
|
|
|
|
|
|
|
|
actualSize, |
|
|
|
|
|
searchQuery |
|
|
); |
|
|
); |
|
|
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); |
|
|
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); |
|
|
setTotal(response.total || 0); |
|
|
setTotal(response.total || 0); |
|
|
@@ -150,14 +243,38 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
loadDetails(page, pageSize); |
|
|
|
|
|
}, [page, pageSize, loadDetails]); |
|
|
|
|
|
|
|
|
if (!appliedFilters) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
loadDetails(page, pageSize, appliedFilters); |
|
|
|
|
|
}, [page, pageSize, appliedFilters, loadDetails]); |
|
|
|
|
|
|
|
|
// 切换模式时,清空用户先前的选择与输入,approved 模式需要以后端结果为准。 |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
fetchStockTakeSections() |
|
|
|
|
|
.then((sections) => { |
|
|
|
|
|
const descSet = new Set<string>(); |
|
|
|
|
|
sections.forEach((s) => { |
|
|
|
|
|
const desc = s.stockTakeSectionDescription?.trim(); |
|
|
|
|
|
if (desc) descSet.add(desc); |
|
|
|
|
|
}); |
|
|
|
|
|
setSectionDescriptionAutocompleteOptions( |
|
|
|
|
|
Array.from(descSet).map((desc) => ({ value: desc, label: desc })) |
|
|
|
|
|
); |
|
|
|
|
|
}) |
|
|
|
|
|
.catch((e) => { |
|
|
|
|
|
console.error("Failed to load section descriptions for approver search:", e); |
|
|
|
|
|
}); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
// 切换模式或盘次时,清空选择与搜索;approved 模式需要以后端结果为准。 |
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
setQtySelection({}); |
|
|
setQtySelection({}); |
|
|
setApproverQty({}); |
|
|
setApproverQty({}); |
|
|
setApproverBadQty({}); |
|
|
setApproverBadQty({}); |
|
|
|
|
|
setAppliedFilters(null); |
|
|
|
|
|
setPage(0); |
|
|
|
|
|
setInventoryLotDetails([]); |
|
|
|
|
|
setTotal(0); |
|
|
}, [mode, selectedSession.stockTakeId]); |
|
|
}, [mode, selectedSession.stockTakeId]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
@@ -530,7 +647,9 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
result.errorCount > 0 ? "warning" : "success" |
|
|
result.errorCount > 0 ? "warning" : "success" |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
await loadDetails(page, pageSize); |
|
|
|
|
|
|
|
|
if (appliedFilters) { |
|
|
|
|
|
await loadDetails(page, pageSize, 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"); |
|
|
@@ -549,7 +668,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
} finally { |
|
|
} finally { |
|
|
setBatchSaving(false); |
|
|
setBatchSaving(false); |
|
|
} |
|
|
} |
|
|
}, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize, mode]); |
|
|
|
|
|
|
|
|
}, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize, mode, appliedFilters]); |
|
|
|
|
|
|
|
|
const formatNumber = (num: number | null | undefined): string => { |
|
|
const formatNumber = (num: number | null | undefined): string => { |
|
|
if (num == null) return "0"; |
|
|
if (num == null) return "0"; |
|
|
@@ -629,6 +748,15 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
)} |
|
|
)} |
|
|
</Stack> |
|
|
</Stack> |
|
|
</Stack> |
|
|
</Stack> |
|
|
|
|
|
|
|
|
|
|
|
<Box sx={{ width: "100%", mb: 2 }}> |
|
|
|
|
|
<SearchBox<ApproverSearchKey> |
|
|
|
|
|
criteria={approverSearchCriteria} |
|
|
|
|
|
onSearch={handleApproverSearchBoxSearch} |
|
|
|
|
|
onReset={handleApproverSearchBoxReset} |
|
|
|
|
|
/> |
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
|
{loadingDetails ? ( |
|
|
{loadingDetails ? ( |
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> |
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> |
|
|
<CircularProgress /> |
|
|
<CircularProgress /> |
|
|
@@ -754,7 +882,9 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
<TableRow> |
|
|
<TableRow> |
|
|
<TableCell colSpan={mode === "approved" ? 10 : 8} align="center"> |
|
|
<TableCell colSpan={mode === "approved" ? 10 : 8} align="center"> |
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
{t("No data")} |
|
|
|
|
|
|
|
|
{!appliedFilters |
|
|
|
|
|
? t("Approver search empty hint") |
|
|
|
|
|
: t("No data")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
</TableRow> |
|
|
</TableRow> |
|
|
|