FPSMS-frontend
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

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