FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

444 строки
17 KiB

  1. "use client";
  2. import SearchBox, { Criterion } from "../SearchBox";
  3. import { useCallback, useMemo, useState, useEffect, useRef } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import SearchResults, { Column } from "../SearchResults/index";
  6. import { StockTransactionResponse, SearchStockTransactionRequest } from "@/app/api/stockTake/actions";
  7. import { decimalFormatter } from "@/app/utils/formatUtil";
  8. import { Stack, Box } from "@mui/material";
  9. import { searchStockTransactions } from "@/app/api/stockTake/actions";
  10. interface Props {
  11. dataList: StockTransactionResponse[];
  12. }
  13. type SearchQuery = {
  14. itemCode?: string;
  15. itemName?: string;
  16. type?: string;
  17. startDate?: string;
  18. endDate?: string;
  19. };
  20. // 扩展类型以包含计算字段
  21. interface ExtendedStockTransaction extends StockTransactionResponse {
  22. formattedDate: string;
  23. inQty: number;
  24. outQty: number;
  25. balanceQty: number;
  26. }
  27. const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => {
  28. const { t } = useTranslation("inventory");
  29. // 添加数据状态
  30. const [dataList, setDataList] = useState<StockTransactionResponse[]>(initialDataList);
  31. const [loading, setLoading] = useState(false);
  32. const [filterArgs, setFilterArgs] = useState<Record<string, any>>({});
  33. const isInitialMount = useRef(true);
  34. // 添加分页状态
  35. const [page, setPage] = useState(0);
  36. const [pageSize, setPageSize] = useState<number | string>(10);
  37. const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 });
  38. const [hasSearchQuery, setHasSearchQuery] = useState(false);
  39. const [totalCount, setTotalCount] = useState(initialDataList.length);
  40. const processedData = useMemo(() => {
  41. // 按日期和 itemId 排序 - 优先使用 date 字段,如果没有则使用 transactionDate
  42. const sorted = [...dataList].sort((a, b) => {
  43. // 优先使用 date 字段,如果没有则使用 transactionDate 的日期部分
  44. const getDateValue = (item: StockTransactionResponse): number => {
  45. if (item.date) {
  46. return new Date(item.date).getTime();
  47. }
  48. if (item.transactionDate) {
  49. if (Array.isArray(item.transactionDate)) {
  50. const [year, month, day] = item.transactionDate;
  51. return new Date(year, month - 1, day).getTime();
  52. } else {
  53. return new Date(item.transactionDate).getTime();
  54. }
  55. }
  56. return 0;
  57. };
  58. const dateA = getDateValue(a);
  59. const dateB = getDateValue(b);
  60. if (dateA !== dateB) return dateA - dateB; // 从旧到新排序
  61. return a.itemId - b.itemId;
  62. });
  63. // 计算每个 item 的累计余额
  64. const balanceMap = new Map<number, number>(); // itemId -> balance
  65. const processed: ExtendedStockTransaction[] = [];
  66. sorted.forEach((item) => {
  67. const currentBalance = balanceMap.get(item.itemId) || 0;
  68. let newBalance = currentBalance;
  69. // 根据类型计算余额
  70. if (item.transactionType === "IN") {
  71. newBalance = currentBalance + item.qty;
  72. } else if (item.transactionType === "OUT") {
  73. newBalance = currentBalance - item.qty;
  74. }
  75. balanceMap.set(item.itemId, newBalance);
  76. // 格式化日期 - 优先使用 date 字段
  77. let formattedDate = "";
  78. if (item.date) {
  79. // 如果 date 是字符串格式 "yyyy-MM-dd"
  80. const date = new Date(item.date);
  81. if (!isNaN(date.getTime())) {
  82. const year = date.getFullYear();
  83. const month = String(date.getMonth() + 1).padStart(2, "0");
  84. const day = String(date.getDate()).padStart(2, "0");
  85. formattedDate = `${year}-${month}-${day}`;
  86. }
  87. } else if (item.transactionDate) {
  88. // 回退到 transactionDate
  89. if (Array.isArray(item.transactionDate)) {
  90. const [year, month, day] = item.transactionDate;
  91. formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
  92. } else if (typeof item.transactionDate === 'string') {
  93. const date = new Date(item.transactionDate);
  94. if (!isNaN(date.getTime())) {
  95. const year = date.getFullYear();
  96. const month = String(date.getMonth() + 1).padStart(2, "0");
  97. const day = String(date.getDate()).padStart(2, "0");
  98. formattedDate = `${year}-${month}-${day}`;
  99. }
  100. } else {
  101. const date = new Date(item.transactionDate);
  102. if (!isNaN(date.getTime())) {
  103. const year = date.getFullYear();
  104. const month = String(date.getMonth() + 1).padStart(2, "0");
  105. const day = String(date.getDate()).padStart(2, "0");
  106. formattedDate = `${year}-${month}-${day}`;
  107. }
  108. }
  109. }
  110. processed.push({
  111. ...item,
  112. formattedDate,
  113. inQty: item.transactionType === "IN" ? item.qty : 0,
  114. outQty: item.transactionType === "OUT" ? item.qty : 0,
  115. balanceQty: item.balanceQty ? item.balanceQty : newBalance,
  116. });
  117. });
  118. return processed;
  119. }, [dataList]);
  120. // 修复:使用 processedData 初始化 filteredList
  121. const [filteredList, setFilteredList] = useState<ExtendedStockTransaction[]>(processedData);
  122. // 当 processedData 变化时更新 filteredList(不更新 pagingController,避免循环)
  123. useEffect(() => {
  124. setFilteredList(processedData);
  125. setTotalCount(processedData.length);
  126. // 只在初始加载时设置 pageSize
  127. if (isInitialMount.current && processedData.length > 0) {
  128. setPageSize("all");
  129. setPagingController(prev => ({ ...prev, pageSize: processedData.length }));
  130. setPage(0);
  131. isInitialMount.current = false;
  132. }
  133. }, [processedData]);
  134. // API 调用函数(参考 PoSearch 的实现)
  135. // API 调用函数(参考 PoSearch 的实现)
  136. const newPageFetch = useCallback(
  137. async (
  138. pagingController: Record<string, number>,
  139. filterArgs: Record<string, any>,
  140. ) => {
  141. setLoading(true);
  142. try {
  143. // 处理空字符串,转换为 null
  144. const itemCode = filterArgs.itemCode?.trim() || null;
  145. const itemName = filterArgs.itemName?.trim() || null;
  146. // 验证:至少需要 itemCode 或 itemName
  147. if (!itemCode && !itemName) {
  148. console.warn("Search requires at least itemCode or itemName");
  149. setDataList([]);
  150. setTotalCount(0);
  151. return;
  152. }
  153. const params: SearchStockTransactionRequest = {
  154. itemCode: itemCode,
  155. itemName: itemName,
  156. type: filterArgs.type?.trim() || null,
  157. startDate: filterArgs.startDate || null,
  158. endDate: filterArgs.endDate || null,
  159. pageNum: pagingController.pageNum - 1 || 0,
  160. pageSize: pagingController.pageSize || 100,
  161. };
  162. console.log("Search params:", params); // 添加调试日志
  163. const res = await searchStockTransactions(params);
  164. console.log("Search response:", res); // 添加调试日志
  165. if (res && Array.isArray(res)) {
  166. setDataList(res);
  167. } else {
  168. console.error("Invalid response format:", res);
  169. setDataList([]);
  170. }
  171. } catch (error) {
  172. console.error("Fetch error:", error);
  173. setDataList([]);
  174. } finally {
  175. setLoading(false);
  176. }
  177. },
  178. [],
  179. );
  180. // 使用 useRef 来存储上一次的值,避免不必要的 API 调用
  181. const prevPagingControllerRef = useRef(pagingController);
  182. const prevFilterArgsRef = useRef(filterArgs);
  183. const hasSearchedRef = useRef(false);
  184. // 当 filterArgs 或 pagingController 变化时调用 API(只在真正变化时调用)
  185. useEffect(() => {
  186. // 检查是否有有效的搜索条件
  187. const hasValidSearch = filterArgs.itemCode || filterArgs.itemName;
  188. if (!hasValidSearch) {
  189. // 如果没有有效搜索条件,只更新 ref,不调用 API
  190. if (isInitialMount.current) {
  191. isInitialMount.current = false;
  192. }
  193. prevFilterArgsRef.current = filterArgs;
  194. return;
  195. }
  196. // 检查是否真的变化了
  197. const pagingChanged =
  198. prevPagingControllerRef.current.pageNum !== pagingController.pageNum ||
  199. prevPagingControllerRef.current.pageSize !== pagingController.pageSize;
  200. const filterChanged = JSON.stringify(prevFilterArgsRef.current) !== JSON.stringify(filterArgs);
  201. // 如果是第一次有效搜索,或者条件/分页发生变化,则调用 API
  202. if (!hasSearchedRef.current || pagingChanged || filterChanged) {
  203. newPageFetch(pagingController, filterArgs);
  204. prevPagingControllerRef.current = pagingController;
  205. prevFilterArgsRef.current = filterArgs;
  206. hasSearchedRef.current = true;
  207. isInitialMount.current = false;
  208. }
  209. }, [newPageFetch, pagingController, filterArgs]);
  210. // 分页处理函数
  211. const handleChangePage = useCallback((event: unknown, newPage: number) => {
  212. setPage(newPage);
  213. setPagingController(prev => ({ ...prev, pageNum: newPage + 1 }));
  214. }, []);
  215. const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  216. const newSize = parseInt(event.target.value, 10);
  217. if (newSize === -1) {
  218. setPageSize("all");
  219. setPagingController(prev => ({ ...prev, pageSize: filteredList.length, pageNum: 1 }));
  220. } else if (!isNaN(newSize)) {
  221. setPageSize(newSize);
  222. setPagingController(prev => ({ ...prev, pageSize: newSize, pageNum: 1 }));
  223. }
  224. setPage(0);
  225. }, [filteredList.length]);
  226. const searchCriteria: Criterion<string>[] = useMemo(
  227. () => [
  228. {
  229. label: t("Item Code"),
  230. paramName: "itemCode",
  231. type: "text",
  232. },
  233. {
  234. label: t("Item Name"),
  235. paramName: "itemName",
  236. type: "text",
  237. },
  238. {
  239. label: t("Type"),
  240. paramName: "type",
  241. type: "text",
  242. },
  243. {
  244. label: t("Start Date"),
  245. paramName: "startDate",
  246. type: "date",
  247. },
  248. {
  249. label: t("End Date"),
  250. paramName: "endDate",
  251. type: "date",
  252. },
  253. ],
  254. [t],
  255. );
  256. const columns = useMemo<Column<ExtendedStockTransaction>[]>(
  257. () => [
  258. {
  259. name: "formattedDate" as keyof ExtendedStockTransaction,
  260. label: t("Date"),
  261. align: "left",
  262. },
  263. {
  264. name: "itemCode" as keyof ExtendedStockTransaction,
  265. label: t("Item-lotNo"),
  266. align: "left",
  267. renderCell: (item) => (
  268. <Box sx={{
  269. maxWidth: 150,
  270. wordBreak: 'break-word',
  271. whiteSpace: 'normal',
  272. lineHeight: 1.5
  273. }}>
  274. <Stack spacing={0.5}>
  275. <Box>{item.itemCode || "-"} {item.itemName || "-"}</Box>
  276. <Box>{item.lotNo || "-"}</Box>
  277. </Stack>
  278. </Box>
  279. ),
  280. },
  281. {
  282. name: "inQty" as keyof ExtendedStockTransaction,
  283. label: t("In Qty"),
  284. align: "left",
  285. type: "decimal",
  286. renderCell: (item) => (
  287. <>{item.inQty > 0 ? decimalFormatter.format(item.inQty) : ""}</>
  288. ),
  289. },
  290. {
  291. name: "outQty" as keyof ExtendedStockTransaction,
  292. label: t("Out Qty"),
  293. align: "left",
  294. type: "decimal",
  295. renderCell: (item) => (
  296. <>{item.outQty > 0 ? decimalFormatter.format(item.outQty) : ""}</>
  297. ),
  298. },
  299. {
  300. name: "balanceQty" as keyof ExtendedStockTransaction,
  301. label: t("Balance Qty"),
  302. align: "left",
  303. type: "decimal",
  304. },
  305. {
  306. name: "type",
  307. label: t("Type"),
  308. align: "left",
  309. renderCell: (item) => {
  310. if (!item.type) return "-";
  311. return t(item.type.toLowerCase());
  312. },
  313. },
  314. {
  315. name: "status",
  316. label: t("Status"),
  317. align: "left",
  318. renderCell: (item) => {
  319. if (!item.status) return "-";
  320. return t(item.status.toLowerCase());
  321. },
  322. },
  323. ],
  324. [t],
  325. );
  326. const handleSearch = useCallback((query: Record<string, string>) => {
  327. // 检查是否有搜索条件
  328. const itemCode = query.itemCode?.trim();
  329. const itemName = query.itemName?.trim();
  330. const type = query.type?.trim();
  331. const startDate = query.startDate === "Invalid Date" ? "" : query.startDate;
  332. const endDate = query.endDate === "Invalid Date" ? "" : query.endDate;
  333. // 验证:至少需要 itemCode 或 itemName
  334. if (!itemCode && !itemName) {
  335. // 可以显示提示信息
  336. console.warn("Please enter at least Item Code or Item Name");
  337. return;
  338. }
  339. const hasQuery = !!(itemCode || itemName || type || startDate || endDate);
  340. setHasSearchQuery(hasQuery);
  341. // 更新 filterArgs,触发 useEffect 调用 API
  342. setFilterArgs({
  343. itemCode: itemCode || undefined,
  344. itemName: itemName || undefined,
  345. type: type || undefined,
  346. startDate: startDate || undefined,
  347. endDate: endDate || undefined,
  348. });
  349. // 重置分页
  350. setPage(0);
  351. setPagingController(prev => ({ ...prev, pageNum: 1 }));
  352. }, []);
  353. const handleReset = useCallback(() => {
  354. setHasSearchQuery(false);
  355. // 重置 filterArgs,触发 useEffect 调用 API
  356. setFilterArgs({});
  357. setPage(0);
  358. setPagingController(prev => ({ ...prev, pageNum: 1 }));
  359. }, []);
  360. // 计算实际显示的 items(分页)
  361. const paginatedItems = useMemo(() => {
  362. if (pageSize === "all") {
  363. return filteredList;
  364. }
  365. const actualPageSize = typeof pageSize === 'number' ? pageSize : 10;
  366. const startIndex = page * actualPageSize;
  367. const endIndex = startIndex + actualPageSize;
  368. return filteredList.slice(startIndex, endIndex);
  369. }, [filteredList, page, pageSize]);
  370. // 计算传递给 SearchResults 的 pageSize(确保在选项中)
  371. const actualPageSizeForTable = useMemo(() => {
  372. if (pageSize === "all") {
  373. return filteredList.length;
  374. }
  375. const size = typeof pageSize === 'number' ? pageSize : 10;
  376. // 如果 size 不在标准选项中,使用 "all" 模式
  377. if (![10, 25, 100].includes(size)) {
  378. return filteredList.length;
  379. }
  380. return size;
  381. }, [pageSize, filteredList.length]);
  382. return (
  383. <>
  384. <SearchBox
  385. criteria={searchCriteria}
  386. onSearch={handleSearch}
  387. onReset={handleReset}
  388. />
  389. {loading && <Box sx={{ p: 2 }}>{t("Loading...")}</Box>}
  390. <SearchResults<ExtendedStockTransaction>
  391. items={paginatedItems}
  392. columns={columns}
  393. pagingController={{ ...pagingController, pageSize: actualPageSizeForTable }}
  394. setPagingController={setPagingController}
  395. totalCount={totalCount}
  396. isAutoPaging={false}
  397. />
  398. </>
  399. );
  400. };
  401. export default SearchPage;