|
- "use client";
-
- import SearchBox, { Criterion } from "../SearchBox";
- import { useCallback, useMemo, useState, useEffect, useRef } from "react";
- import { useTranslation } from "react-i18next";
- import SearchResults, { Column } from "../SearchResults/index";
- import { StockTransactionResponse, SearchStockTransactionRequest } from "@/app/api/stockTake/actions";
- import { decimalFormatter } from "@/app/utils/formatUtil";
- import { Stack, Box } from "@mui/material";
- import { searchStockTransactions } from "@/app/api/stockTake/actions";
-
- interface Props {
- dataList: StockTransactionResponse[];
- }
-
- type SearchQuery = {
- itemCode?: string;
- itemName?: string;
- type?: string;
- startDate?: string;
- endDate?: string;
- };
-
- // 扩展类型以包含计算字段
- interface ExtendedStockTransaction extends StockTransactionResponse {
- formattedDate: string;
- inQty: number;
- outQty: number;
- balanceQty: number;
- }
-
- const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => {
- const { t } = useTranslation("inventory");
-
- // 添加数据状态
- const [dataList, setDataList] = useState<StockTransactionResponse[]>(initialDataList);
- const [loading, setLoading] = useState(false);
- const [filterArgs, setFilterArgs] = useState<Record<string, any>>({});
- const isInitialMount = useRef(true);
-
- // 添加分页状态
- const [page, setPage] = useState(0);
- const [pageSize, setPageSize] = useState<number | string>(10);
- const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 });
- const [hasSearchQuery, setHasSearchQuery] = useState(false);
- const [totalCount, setTotalCount] = useState(initialDataList.length);
-
- const processedData = useMemo(() => {
- // 按日期和 itemId 排序 - 优先使用 date 字段,如果没有则使用 transactionDate
- const sorted = [...dataList].sort((a, b) => {
- // 优先使用 date 字段,如果没有则使用 transactionDate 的日期部分
- const getDateValue = (item: StockTransactionResponse): number => {
- if (item.date) {
- return new Date(item.date).getTime();
- }
- if (item.transactionDate) {
- if (Array.isArray(item.transactionDate)) {
- const [year, month, day] = item.transactionDate;
- return new Date(year, month - 1, day).getTime();
- } else {
- return new Date(item.transactionDate).getTime();
- }
- }
- return 0;
- };
-
- const dateA = getDateValue(a);
- const dateB = getDateValue(b);
-
- if (dateA !== dateB) return dateA - dateB; // 从旧到新排序
- return a.itemId - b.itemId;
- });
-
- // 计算每个 item 的累计余额
- const balanceMap = new Map<number, number>(); // itemId -> balance
- const processed: ExtendedStockTransaction[] = [];
-
- sorted.forEach((item) => {
- const currentBalance = balanceMap.get(item.itemId) || 0;
- let newBalance = currentBalance;
-
- // 根据类型计算余额
- if (item.transactionType === "IN") {
- newBalance = currentBalance + item.qty;
- } else if (item.transactionType === "OUT") {
- newBalance = currentBalance - item.qty;
- }
-
- balanceMap.set(item.itemId, newBalance);
-
- // 格式化日期 - 优先使用 date 字段
- let formattedDate = "";
- if (item.date) {
- // 如果 date 是字符串格式 "yyyy-MM-dd"
- const date = new Date(item.date);
- if (!isNaN(date.getTime())) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
- formattedDate = `${year}-${month}-${day}`;
- }
- } else if (item.transactionDate) {
- // 回退到 transactionDate
- if (Array.isArray(item.transactionDate)) {
- const [year, month, day] = item.transactionDate;
- formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
- } else if (typeof item.transactionDate === 'string') {
- const date = new Date(item.transactionDate);
- if (!isNaN(date.getTime())) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
- formattedDate = `${year}-${month}-${day}`;
- }
- } else {
- const date = new Date(item.transactionDate);
- if (!isNaN(date.getTime())) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
- formattedDate = `${year}-${month}-${day}`;
- }
- }
- }
-
- processed.push({
- ...item,
- formattedDate,
- inQty: item.transactionType === "IN" ? item.qty : 0,
- outQty: item.transactionType === "OUT" ? item.qty : 0,
- balanceQty: item.balanceQty ? item.balanceQty : newBalance,
- });
- });
-
- return processed;
- }, [dataList]);
- // 修复:使用 processedData 初始化 filteredList
- const [filteredList, setFilteredList] = useState<ExtendedStockTransaction[]>(processedData);
-
- // 当 processedData 变化时更新 filteredList(不更新 pagingController,避免循环)
- useEffect(() => {
- setFilteredList(processedData);
- setTotalCount(processedData.length);
- // 只在初始加载时设置 pageSize
- if (isInitialMount.current && processedData.length > 0) {
- setPageSize("all");
- setPagingController(prev => ({ ...prev, pageSize: processedData.length }));
- setPage(0);
- isInitialMount.current = false;
- }
- }, [processedData]);
-
- // API 调用函数(参考 PoSearch 的实现)
- // API 调用函数(参考 PoSearch 的实现)
- const newPageFetch = useCallback(
- async (
- pagingController: Record<string, number>,
- filterArgs: Record<string, any>,
- ) => {
- setLoading(true);
- try {
- // 处理空字符串,转换为 null
- const itemCode = filterArgs.itemCode?.trim() || null;
- const itemName = filterArgs.itemName?.trim() || null;
-
- // 验证:至少需要 itemCode 或 itemName
- if (!itemCode && !itemName) {
- console.warn("Search requires at least itemCode or itemName");
- setDataList([]);
- setTotalCount(0);
- return;
- }
-
- const params: SearchStockTransactionRequest = {
- itemCode: itemCode,
- itemName: itemName,
- type: filterArgs.type?.trim() || null,
- startDate: filterArgs.startDate || null,
- endDate: filterArgs.endDate || null,
- pageNum: pagingController.pageNum - 1 || 0,
- pageSize: pagingController.pageSize || 100,
- };
-
- console.log("Search params:", params); // 添加调试日志
-
- const res = await searchStockTransactions(params);
- console.log("Search response:", res); // 添加调试日志
-
- if (res && Array.isArray(res)) {
- setDataList(res);
- } else {
- console.error("Invalid response format:", res);
- setDataList([]);
- }
- } catch (error) {
- console.error("Fetch error:", error);
- setDataList([]);
- } finally {
- setLoading(false);
- }
- },
- [],
- );
-
- // 使用 useRef 来存储上一次的值,避免不必要的 API 调用
- const prevPagingControllerRef = useRef(pagingController);
- const prevFilterArgsRef = useRef(filterArgs);
- const hasSearchedRef = useRef(false);
- // 当 filterArgs 或 pagingController 变化时调用 API(只在真正变化时调用)
- useEffect(() => {
- // 检查是否有有效的搜索条件
- const hasValidSearch = filterArgs.itemCode || filterArgs.itemName;
-
- if (!hasValidSearch) {
- // 如果没有有效搜索条件,只更新 ref,不调用 API
- if (isInitialMount.current) {
- isInitialMount.current = false;
- }
- prevFilterArgsRef.current = filterArgs;
- return;
- }
-
- // 检查是否真的变化了
- const pagingChanged =
- prevPagingControllerRef.current.pageNum !== pagingController.pageNum ||
- prevPagingControllerRef.current.pageSize !== pagingController.pageSize;
-
- const filterChanged = JSON.stringify(prevFilterArgsRef.current) !== JSON.stringify(filterArgs);
-
- // 如果是第一次有效搜索,或者条件/分页发生变化,则调用 API
- if (!hasSearchedRef.current || pagingChanged || filterChanged) {
- newPageFetch(pagingController, filterArgs);
- prevPagingControllerRef.current = pagingController;
- prevFilterArgsRef.current = filterArgs;
- hasSearchedRef.current = true;
- isInitialMount.current = false;
- }
- }, [newPageFetch, pagingController, filterArgs]);
-
- // 分页处理函数
- const handleChangePage = useCallback((event: unknown, newPage: number) => {
- setPage(newPage);
- setPagingController(prev => ({ ...prev, pageNum: newPage + 1 }));
- }, []);
-
- const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
- const newSize = parseInt(event.target.value, 10);
- if (newSize === -1) {
- setPageSize("all");
- setPagingController(prev => ({ ...prev, pageSize: filteredList.length, pageNum: 1 }));
- } else if (!isNaN(newSize)) {
- setPageSize(newSize);
- setPagingController(prev => ({ ...prev, pageSize: newSize, pageNum: 1 }));
- }
- setPage(0);
- }, [filteredList.length]);
-
- const searchCriteria: Criterion<string>[] = useMemo(
- () => [
- {
- label: t("Item Code"),
- paramName: "itemCode",
- type: "text",
- },
- {
- label: t("Item Name"),
- paramName: "itemName",
- type: "text",
- },
- {
- label: t("Type"),
- paramName: "type",
- type: "text",
- },
- {
- label: t("Start Date"),
- paramName: "startDate",
- type: "date",
- },
- {
- label: t("End Date"),
- paramName: "endDate",
- type: "date",
- },
- ],
- [t],
- );
-
- const columns = useMemo<Column<ExtendedStockTransaction>[]>(
- () => [
- {
- name: "formattedDate" as keyof ExtendedStockTransaction,
- label: t("Date"),
- align: "left",
- },
- {
- name: "itemCode" as keyof ExtendedStockTransaction,
- label: t("Item-lotNo"),
- align: "left",
- renderCell: (item) => (
- <Box sx={{
- maxWidth: 150,
- wordBreak: 'break-word',
- whiteSpace: 'normal',
- lineHeight: 1.5
- }}>
- <Stack spacing={0.5}>
- <Box>{item.itemCode || "-"} {item.itemName || "-"}</Box>
- <Box>{item.lotNo || "-"}</Box>
- </Stack>
- </Box>
- ),
- },
- {
- name: "inQty" as keyof ExtendedStockTransaction,
- label: t("In Qty"),
- align: "left",
- type: "decimal",
- renderCell: (item) => (
- <>{item.inQty > 0 ? decimalFormatter.format(item.inQty) : ""}</>
- ),
- },
- {
- name: "outQty" as keyof ExtendedStockTransaction,
- label: t("Out Qty"),
- align: "left",
- type: "decimal",
- renderCell: (item) => (
- <>{item.outQty > 0 ? decimalFormatter.format(item.outQty) : ""}</>
- ),
- },
- {
- name: "balanceQty" as keyof ExtendedStockTransaction,
- label: t("Balance Qty"),
- align: "left",
- type: "decimal",
- },
- {
- name: "type",
- label: t("Type"),
- align: "left",
- renderCell: (item) => {
- if (!item.type) return "-";
- return t(item.type.toLowerCase());
- },
- },
- {
- name: "status",
- label: t("Status"),
- align: "left",
- renderCell: (item) => {
- if (!item.status) return "-";
- return t(item.status.toLowerCase());
- },
- },
- ],
- [t],
- );
-
- const handleSearch = useCallback((query: Record<string, string>) => {
- // 检查是否有搜索条件
- const itemCode = query.itemCode?.trim();
- const itemName = query.itemName?.trim();
- const type = query.type?.trim();
- const startDate = query.startDate === "Invalid Date" ? "" : query.startDate;
- const endDate = query.endDate === "Invalid Date" ? "" : query.endDate;
-
- // 验证:至少需要 itemCode 或 itemName
- if (!itemCode && !itemName) {
- // 可以显示提示信息
- console.warn("Please enter at least Item Code or Item Name");
- return;
- }
-
- const hasQuery = !!(itemCode || itemName || type || startDate || endDate);
- setHasSearchQuery(hasQuery);
-
- // 更新 filterArgs,触发 useEffect 调用 API
- setFilterArgs({
- itemCode: itemCode || undefined,
- itemName: itemName || undefined,
- type: type || undefined,
- startDate: startDate || undefined,
- endDate: endDate || undefined,
- });
-
- // 重置分页
- setPage(0);
- setPagingController(prev => ({ ...prev, pageNum: 1 }));
- }, []);
-
- const handleReset = useCallback(() => {
- setHasSearchQuery(false);
- // 重置 filterArgs,触发 useEffect 调用 API
- setFilterArgs({});
- setPage(0);
- setPagingController(prev => ({ ...prev, pageNum: 1 }));
- }, []);
-
- // 计算实际显示的 items(分页)
- const paginatedItems = useMemo(() => {
- if (pageSize === "all") {
- return filteredList;
- }
- const actualPageSize = typeof pageSize === 'number' ? pageSize : 10;
- const startIndex = page * actualPageSize;
- const endIndex = startIndex + actualPageSize;
- return filteredList.slice(startIndex, endIndex);
- }, [filteredList, page, pageSize]);
-
- // 计算传递给 SearchResults 的 pageSize(确保在选项中)
- const actualPageSizeForTable = useMemo(() => {
- if (pageSize === "all") {
- return filteredList.length;
- }
- const size = typeof pageSize === 'number' ? pageSize : 10;
- // 如果 size 不在标准选项中,使用 "all" 模式
- if (![10, 25, 100].includes(size)) {
- return filteredList.length;
- }
- return size;
- }, [pageSize, filteredList.length]);
-
- return (
- <>
- <SearchBox
- criteria={searchCriteria}
- onSearch={handleSearch}
- onReset={handleReset}
- />
- {loading && <Box sx={{ p: 2 }}>{t("Loading...")}</Box>}
- <SearchResults<ExtendedStockTransaction>
- items={paginatedItems}
- columns={columns}
- pagingController={{ ...pagingController, pageSize: actualPageSizeForTable }}
- setPagingController={setPagingController}
- totalCount={totalCount}
- isAutoPaging={false}
- />
- </>
- );
- };
-
- export default SearchPage;
|