FPSMS-frontend
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

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