FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

655 lines
23 KiB

  1. "use client";
  2. import {
  3. Autocomplete,
  4. Box,
  5. Button,
  6. CircularProgress,
  7. FormControl,
  8. Grid,
  9. Modal,
  10. TextField,
  11. Typography,
  12. Table,
  13. TableBody,
  14. TableCell,
  15. TableContainer,
  16. TableHead,
  17. TableRow,
  18. Paper,
  19. Checkbox,
  20. TablePagination,
  21. } from "@mui/material";
  22. import { useCallback, useEffect, useMemo, useState } from "react";
  23. import { useTranslation } from "react-i18next";
  24. import {
  25. newassignPickOrder,
  26. AssignPickOrderInputs,
  27. releaseAssignedPickOrders, // Add this import
  28. } from "@/app/api/pickOrder/actions";
  29. import { fetchNameList, NameList ,fetchNewNameList, NewNameList} from "@/app/api/user/actions";
  30. import { FormProvider, useForm } from "react-hook-form";
  31. import { isEmpty, sortBy, uniqBy, upperFirst, groupBy } from "lodash";
  32. import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil";
  33. import useUploadContext from "../UploadProvider/useUploadContext";
  34. import dayjs from "dayjs";
  35. import arraySupport from "dayjs/plugin/arraySupport";
  36. import SearchBox, { Criterion } from "../SearchBox";
  37. import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions";
  38. dayjs.extend(arraySupport);
  39. interface Props {
  40. filterArgs: Record<string, any>;
  41. }
  42. // 使用 fetchPickOrderItemsByPageClient 返回的数据结构
  43. interface ItemRow {
  44. id: string;
  45. pickOrderId: number;
  46. pickOrderCode: string;
  47. itemId: number;
  48. itemCode: string;
  49. itemName: string;
  50. requiredQty: number;
  51. currentStock: number;
  52. unit: string;
  53. targetDate: any;
  54. status: string;
  55. consoCode?: string;
  56. assignTo?: number;
  57. groupName?: string;
  58. }
  59. // 分组后的数据结构
  60. interface GroupedItemRow {
  61. pickOrderId: number;
  62. pickOrderCode: string;
  63. targetDate: any;
  64. status: string;
  65. consoCode?: string;
  66. items: ItemRow[];
  67. }
  68. const style = {
  69. position: "absolute",
  70. top: "50%",
  71. left: "50%",
  72. transform: "translate(-50%, -50%)",
  73. bgcolor: "background.paper",
  74. pt: 5,
  75. px: 5,
  76. pb: 10,
  77. width: { xs: "100%", sm: "100%", md: "100%" },
  78. };
  79. const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
  80. const { t } = useTranslation("pickOrder");
  81. const { setIsUploading } = useUploadContext();
  82. // 修复:选择状态改为按 pick order ID 存储
  83. const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<number[]>([]);
  84. const [filteredItems, setFilteredItems] = useState<ItemRow[]>([]);
  85. const [isLoadingItems, setIsLoadingItems] = useState(false);
  86. const [pagingController, setPagingController] = useState({
  87. pageNum: 1,
  88. pageSize: 10,
  89. });
  90. const [totalCountItems, setTotalCountItems] = useState<number>();
  91. const [modalOpen, setModalOpen] = useState(false);
  92. const [usernameList, setUsernameList] = useState<NewNameList[]>([]);
  93. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  94. const [originalItemData, setOriginalItemData] = useState<ItemRow[]>([]);
  95. const formProps = useForm<AssignPickOrderInputs>();
  96. const errors = formProps.formState.errors;
  97. // 将项目按 pick order 分组
  98. const groupedItems = useMemo(() => {
  99. const grouped = groupBy(filteredItems, 'pickOrderId');
  100. return Object.entries(grouped).map(([pickOrderId, items]) => {
  101. const firstItem = items[0];
  102. return {
  103. pickOrderId: parseInt(pickOrderId),
  104. pickOrderCode: firstItem.pickOrderCode,
  105. targetDate: firstItem.targetDate,
  106. status: firstItem.status,
  107. consoCode: firstItem.consoCode,
  108. items: items
  109. } as GroupedItemRow;
  110. });
  111. }, [filteredItems]);
  112. // 修复:处理 pick order 选择
  113. const handlePickOrderSelect = useCallback((pickOrderId: number, checked: boolean) => {
  114. if (checked) {
  115. setSelectedPickOrderIds(prev => [...prev, pickOrderId]);
  116. } else {
  117. setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId));
  118. }
  119. }, []);
  120. // 修复:检查 pick order 是否被选中
  121. const isPickOrderSelected = useCallback((pickOrderId: number) => {
  122. return selectedPickOrderIds.includes(pickOrderId);
  123. }, [selectedPickOrderIds]);
  124. // 使用 fetchPickOrderItemsByPageClient 获取数据
  125. const fetchNewPageItems = useCallback(
  126. async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => {
  127. console.log("=== fetchNewPageItems called ===");
  128. console.log("pagingController:", pagingController);
  129. console.log("filterArgs:", filterArgs);
  130. setIsLoadingItems(true);
  131. try {
  132. const params = {
  133. ...pagingController,
  134. ...filterArgs,
  135. // 新增:排除状态为 "assigned" 的提料单
  136. //status: "pending,released,completed,cancelled" // 或者使用其他方式过滤
  137. };
  138. console.log("Final params:", params);
  139. const res = await fetchPickOrderItemsByPageClient(params);
  140. console.log("API Response:", res);
  141. if (res && res.records) {
  142. console.log("Records received:", res.records.length);
  143. console.log("First record:", res.records[0]);
  144. // 新增:在前端也过滤掉 "assigned" 状态的项目
  145. const filteredRecords = res.records.filter((item: any) => item.status !== "assigned");
  146. const itemRows: ItemRow[] = filteredRecords.map((item: any) => ({
  147. id: item.id,
  148. pickOrderId: item.pickOrderId,
  149. pickOrderCode: item.pickOrderCode,
  150. itemId: item.itemId,
  151. itemCode: item.itemCode,
  152. itemName: item.itemName,
  153. requiredQty: item.requiredQty,
  154. currentStock: item.currentStock ?? 0,
  155. unit: item.unit,
  156. targetDate: item.targetDate,
  157. status: item.status,
  158. consoCode: item.consoCode,
  159. assignTo: item.assignTo,
  160. groupName: item.groupName,
  161. }));
  162. setOriginalItemData(itemRows);
  163. setFilteredItems(itemRows);
  164. setTotalCountItems(filteredRecords.length); // 使用过滤后的数量
  165. } else {
  166. console.log("No records in response");
  167. setFilteredItems([]);
  168. setTotalCountItems(0);
  169. }
  170. } catch (error) {
  171. console.error("Error fetching items:", error);
  172. setFilteredItems([]);
  173. setTotalCountItems(0);
  174. } finally {
  175. setIsLoadingItems(false);
  176. }
  177. },
  178. [],
  179. );
  180. const searchCriteria: Criterion<any>[] = useMemo(
  181. () => [
  182. {
  183. label: t("Pick Order Code"),
  184. paramName: "pickOrderCode",
  185. type: "text",
  186. },
  187. {
  188. label: t("Item Code"),
  189. paramName: "itemCode",
  190. type: "text"
  191. },
  192. {
  193. label: t("Group Code"),
  194. paramName: "groupName",
  195. type: "text",
  196. },
  197. {
  198. label: t("Item Name"),
  199. paramName: "itemName",
  200. type: "text",
  201. },
  202. {
  203. label: t("Target Date From"),
  204. label2: t("Target Date To"),
  205. paramName: "targetDate",
  206. type: "dateRange",
  207. },
  208. {
  209. label: t("Pick Order Status"),
  210. paramName: "status",
  211. type: "autocomplete",
  212. options: sortBy(
  213. uniqBy(
  214. originalItemData.map((item) => ({
  215. value: item.status,
  216. label: t(upperFirst(item.status)),
  217. })),
  218. "value",
  219. ),
  220. "label",
  221. ),
  222. },
  223. ],
  224. [originalItemData, t],
  225. );
  226. const handleSearch = useCallback((query: Record<string, any>) => {
  227. setSearchQuery({ ...query });
  228. console.log("Search query:", query);
  229. const filtered = originalItemData.filter((item) => {
  230. const itemTargetDateStr = arrayToDayjs(item.targetDate);
  231. const itemCodeMatch = !query.itemCode ||
  232. item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  233. const itemNameMatch = !query.itemName ||
  234. item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  235. const pickOrderCodeMatch = !query.pickOrderCode ||
  236. item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  237. const groupNameMatch = !query.groupName ||
  238. item.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase());
  239. // 日期范围搜索
  240. let dateMatch = true;
  241. if (query.targetDate || query.targetDateTo) {
  242. try {
  243. if (query.targetDate && !query.targetDateTo) {
  244. const fromDate = dayjs(query.targetDate);
  245. dateMatch = itemTargetDateStr.isSame(fromDate, 'day') ||
  246. itemTargetDateStr.isAfter(fromDate, 'day');
  247. } else if (!query.targetDate && query.targetDateTo) {
  248. const toDate = dayjs(query.targetDateTo);
  249. dateMatch = itemTargetDateStr.isSame(toDate, 'day') ||
  250. itemTargetDateStr.isBefore(toDate, 'day');
  251. } else if (query.targetDate && query.targetDateTo) {
  252. const fromDate = dayjs(query.targetDate);
  253. const toDate = dayjs(query.targetDateTo);
  254. dateMatch = (itemTargetDateStr.isSame(fromDate, 'day') ||
  255. itemTargetDateStr.isAfter(fromDate, 'day')) &&
  256. (itemTargetDateStr.isSame(toDate, 'day') ||
  257. itemTargetDateStr.isBefore(toDate, 'day'));
  258. }
  259. } catch (error) {
  260. console.error("Date parsing error:", error);
  261. dateMatch = true;
  262. }
  263. }
  264. const statusMatch = !query.status ||
  265. query.status.toLowerCase() === "all" ||
  266. item.status?.toLowerCase().includes((query.status || "").toLowerCase());
  267. return itemCodeMatch && itemNameMatch && groupNameMatch && pickOrderCodeMatch && dateMatch && statusMatch;
  268. });
  269. console.log("Filtered items count:", filtered.length);
  270. setFilteredItems(filtered);
  271. }, [originalItemData]);
  272. const handleReset = useCallback(() => {
  273. setSearchQuery({});
  274. setFilteredItems(originalItemData);
  275. setTimeout(() => {
  276. setSearchQuery({});
  277. }, 0);
  278. }, [originalItemData]);
  279. // 修复:处理分页变化
  280. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  281. const newPagingController = {
  282. ...pagingController,
  283. pageNum: newPage + 1, // API 使用 1-based 分页
  284. };
  285. setPagingController(newPagingController);
  286. }, [pagingController]);
  287. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  288. const newPageSize = parseInt(event.target.value, 10);
  289. const newPagingController = {
  290. pageNum: 1, // 重置到第一页
  291. pageSize: newPageSize,
  292. };
  293. setPagingController(newPagingController);
  294. }, []);
  295. const handleAssignOnly = useCallback(async (data: AssignPickOrderInputs) => {
  296. if (selectedPickOrderIds.length === 0) return;
  297. setIsUploading(true);
  298. try {
  299. // 修复:直接使用选中的 pick order IDs
  300. const assignRes = await newassignPickOrder({
  301. pickOrderIds: selectedPickOrderIds,
  302. assignTo: data.assignTo,
  303. });
  304. if (assignRes && assignRes.code === "SUCCESS") {
  305. console.log("Assign successful:", assignRes);
  306. setModalOpen(false);
  307. setSelectedPickOrderIds([]); // 清空选择
  308. fetchNewPageItems(pagingController, filterArgs);
  309. } else {
  310. console.error("Assign failed:", assignRes);
  311. }
  312. } catch (error) {
  313. console.error("Error in assign:", error);
  314. } finally {
  315. setIsUploading(false);
  316. }
  317. }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]);
  318. const handleAssignAndReleaseCombined = useCallback(async (data: AssignPickOrderInputs) => {
  319. if (selectedPickOrderIds.length === 0) return;
  320. setIsUploading(true);
  321. try {
  322. // Step 1: Assign the pick orders
  323. const assignRes = await newassignPickOrder({
  324. pickOrderIds: selectedPickOrderIds,
  325. assignTo: data.assignTo,
  326. });
  327. if (assignRes && assignRes.code === "SUCCESS") {
  328. console.log("Assign successful:", assignRes);
  329. // Step 2: Release the assigned pick orders
  330. const releaseRes = await releaseAssignedPickOrders({
  331. pickOrderIds: selectedPickOrderIds,
  332. assignTo: data.assignTo,
  333. });
  334. if (releaseRes && releaseRes.code === "SUCCESS") {
  335. console.log("Assign and Release successful:", releaseRes);
  336. setModalOpen(false);
  337. setSelectedPickOrderIds([]); // 清空选择
  338. fetchNewPageItems(pagingController, filterArgs);
  339. } else {
  340. console.error("Release failed:", releaseRes);
  341. }
  342. } else {
  343. console.error("Assign failed:", assignRes);
  344. }
  345. } catch (error) {
  346. console.error("Error in assign and release:", error);
  347. } finally {
  348. setIsUploading(false);
  349. }
  350. }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]);
  351. const openAssignModal = useCallback(() => {
  352. setModalOpen(true);
  353. formProps.reset();
  354. }, [formProps]);
  355. // 组件挂载时加载数据
  356. useEffect(() => {
  357. console.log("=== Component mounted ===");
  358. fetchNewPageItems(pagingController, filterArgs || {});
  359. }, []); // 只在组件挂载时执行一次
  360. // 当 pagingController 或 filterArgs 变化时重新调用 API
  361. useEffect(() => {
  362. console.log("=== Dependencies changed ===");
  363. if (pagingController && (filterArgs || {})) {
  364. fetchNewPageItems(pagingController, filterArgs || {});
  365. }
  366. }, [pagingController, filterArgs, fetchNewPageItems]);
  367. useEffect(() => {
  368. const loadUsernameList = async () => {
  369. try {
  370. const res = await fetchNewNameList();
  371. if (res) {
  372. setUsernameList(res);
  373. }
  374. } catch (error) {
  375. console.error("Error loading username list:", error);
  376. }
  377. };
  378. loadUsernameList();
  379. }, []);
  380. // 自定义分组表格组件
  381. const CustomGroupedTable = () => {
  382. return (
  383. <>
  384. <TableContainer component={Paper}>
  385. <Table>
  386. <TableHead>
  387. <TableRow>
  388. <TableCell>{t("Selected")}</TableCell>
  389. <TableCell>{t("Pick Order Code")}</TableCell>
  390. <TableCell>{t("Group Code")}</TableCell>
  391. <TableCell>{t("Item Code")}</TableCell>
  392. <TableCell>{t("Item Name")}</TableCell>
  393. <TableCell align="right">{t("Order Quantity")}</TableCell>
  394. <TableCell align="right">{t("Current Stock")}</TableCell>
  395. <TableCell align="right">{t("Stock Unit")}</TableCell>
  396. <TableCell>{t("Target Date")}</TableCell>
  397. <TableCell>{t("Pick Order Status")}</TableCell>
  398. </TableRow>
  399. </TableHead>
  400. <TableBody>
  401. {groupedItems.length === 0 ? (
  402. <TableRow>
  403. <TableCell colSpan={9} align="center">
  404. <Typography variant="body2" color="text.secondary">
  405. {t("No data available")}
  406. </Typography>
  407. </TableCell>
  408. </TableRow>
  409. ) : (
  410. groupedItems.map((group) => (
  411. group.items.map((item, index) => (
  412. <TableRow key={item.id}>
  413. {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */}
  414. <TableCell>
  415. {index === 0 ? (
  416. <Checkbox
  417. checked={isPickOrderSelected(group.pickOrderId)}
  418. onChange={(e) => handlePickOrderSelect(group.pickOrderId, e.target.checked)}
  419. disabled={!isEmpty(item.consoCode)}
  420. />
  421. ) : null}
  422. </TableCell>
  423. {/* Pick Order Code - 只在第一个项目显示 */}
  424. <TableCell>
  425. {index === 0 ? item.pickOrderCode : null}
  426. </TableCell>
  427. {/* Group Name */}
  428. <TableCell>
  429. {index === 0 ? (item.groupName || "No Group") : null}
  430. </TableCell>
  431. {/* Item Code */}
  432. <TableCell>{item.itemCode}</TableCell>
  433. {/* Item Name */}
  434. <TableCell>{item.itemName}</TableCell>
  435. {/* Order Quantity */}
  436. <TableCell align="right">{item.requiredQty}</TableCell>
  437. {/* Current Stock */}
  438. <TableCell align="right">
  439. <Typography
  440. variant="body2"
  441. color={item.currentStock > 0 ? "success.main" : "error.main"}
  442. sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }}
  443. >
  444. {item.currentStock.toLocaleString()}
  445. </Typography>
  446. </TableCell>
  447. {/* Unit */}
  448. <TableCell align="right">{item.unit}</TableCell>
  449. {/* Target Date - 只在第一个项目显示 */}
  450. <TableCell>
  451. {index === 0 ? (
  452. arrayToDayjs(item.targetDate)
  453. .add(-1, "month")
  454. .format(OUTPUT_DATE_FORMAT)
  455. ) : null}
  456. </TableCell>
  457. {/* Pick Order Status - 只在第一个项目显示 */}
  458. <TableCell>
  459. {index === 0 ? upperFirst(item.status) : null}
  460. </TableCell>
  461. </TableRow>
  462. ))
  463. ))
  464. )}
  465. </TableBody>
  466. </Table>
  467. </TableContainer>
  468. {/* 修复:添加分页组件 */}
  469. <TablePagination
  470. component="div"
  471. count={totalCountItems || 0}
  472. page={(pagingController.pageNum - 1)} // 转换为 0-based
  473. rowsPerPage={pagingController.pageSize}
  474. onPageChange={handlePageChange}
  475. onRowsPerPageChange={handlePageSizeChange}
  476. rowsPerPageOptions={[10, 25, 50]}
  477. labelRowsPerPage={t("Rows per page")}
  478. labelDisplayedRows={({ from, to, count }) =>
  479. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  480. }
  481. />
  482. </>
  483. );
  484. };
  485. return (
  486. <>
  487. <SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} />
  488. <Grid container rowGap={1}>
  489. <Grid item xs={12}>
  490. {isLoadingItems ? (
  491. <CircularProgress size={40} />
  492. ) : (
  493. <CustomGroupedTable />
  494. )}
  495. </Grid>
  496. <Grid item xs={12}>
  497. <Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}>
  498. <Button
  499. disabled={selectedPickOrderIds.length < 1}
  500. variant="outlined"
  501. onClick={openAssignModal}
  502. >
  503. {t("Assign")}
  504. </Button>
  505. </Box>
  506. </Grid>
  507. </Grid>
  508. {modalOpen ? (
  509. <Modal
  510. open={modalOpen}
  511. onClose={() => setModalOpen(false)}
  512. aria-labelledby="modal-modal-title"
  513. aria-describedby="modal-modal-description"
  514. >
  515. <Box sx={style}>
  516. <Grid container rowGap={2}>
  517. <Grid item xs={12}>
  518. <Typography variant="h6" component="h2">
  519. {t("Assign Pick Orders")}
  520. </Typography>
  521. </Grid>
  522. <Grid item xs={12}>
  523. <Typography variant="body1" color="text.secondary">
  524. {t("Selected Pick Orders")}: {selectedPickOrderIds.length}
  525. </Typography>
  526. </Grid>
  527. <Grid item xs={12}>
  528. <FormProvider {...formProps}>
  529. <form>
  530. <Grid container spacing={2}>
  531. <Grid item xs={12}>
  532. <FormControl fullWidth>
  533. <Autocomplete
  534. options={usernameList}
  535. getOptionLabel={(option) => {
  536. // 修改:显示更详细的用户信息
  537. const title = option.title ? ` (${option.title})` : '';
  538. const department = option.department ? ` - ${option.department}` : '';
  539. return `${option.name}${title}${department}`;
  540. }}
  541. renderOption={(props, option) => (
  542. <Box component="li" {...props}>
  543. <Typography variant="body1">
  544. {option.name}
  545. {option.title && ` (${option.title})`}
  546. {option.department && ` - ${option.department}`}
  547. </Typography>
  548. </Box>
  549. )}
  550. onChange={(_, value) => {
  551. formProps.setValue("assignTo", value?.id || 0);
  552. }}
  553. renderInput={(params) => (
  554. <TextField
  555. {...params}
  556. label={t("Assign To")}
  557. error={!!errors.assignTo}
  558. helperText={errors.assignTo?.message}
  559. required
  560. />
  561. )}
  562. />
  563. </FormControl>
  564. </Grid>
  565. <Grid item xs={12}>
  566. <Typography variant="body2" color="warning.main">
  567. {t("Select an action for the assigned pick orders.")}
  568. </Typography>
  569. </Grid>
  570. <Grid item xs={12}>
  571. <Box sx={{ display: "flex", gap: 2, justifyContent: "flex-end" }}>
  572. <Button variant="outlined" onClick={() => setModalOpen(false)}>
  573. {t("Cancel")}
  574. </Button>
  575. <Button
  576. variant="contained"
  577. color="primary"
  578. onClick={formProps.handleSubmit(handleAssignOnly)}
  579. >
  580. {t("Assign")}
  581. </Button>
  582. <Button
  583. variant="contained"
  584. color="secondary"
  585. onClick={formProps.handleSubmit(handleAssignAndReleaseCombined)}
  586. >
  587. {t("Assign and Release")}
  588. </Button>
  589. </Box>
  590. </Grid>
  591. </Grid>
  592. </form>
  593. </FormProvider>
  594. </Grid>
  595. </Grid>
  596. </Box>
  597. </Modal>
  598. ) : undefined}
  599. </>
  600. );
  601. };
  602. export default AssignAndRelease;