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.
 
 

511 lines
17 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,
  28. fetchPickOrderWithStockClient, // Add this import
  29. } from "@/app/api/pickOrder/actions";
  30. import { fetchNameList, NameList } from "@/app/api/user/actions";
  31. import {
  32. FormProvider,
  33. useForm,
  34. } from "react-hook-form";
  35. import { isEmpty, upperFirst, groupBy } from "lodash";
  36. import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil";
  37. import useUploadContext from "../UploadProvider/useUploadContext";
  38. import dayjs from "dayjs";
  39. import arraySupport from "dayjs/plugin/arraySupport";
  40. import SearchBox, { Criterion } from "../SearchBox";
  41. import { sortBy, uniqBy } from "lodash";
  42. import { createStockOutLine, CreateStockOutLine, fetchPickOrderDetails } from "@/app/api/pickOrder/actions";
  43. dayjs.extend(arraySupport);
  44. interface Props {
  45. filterArgs: Record<string, any>;
  46. }
  47. // Update the interface to match the new API response structure
  48. interface PickOrderRow {
  49. id: string;
  50. code: string;
  51. targetDate: string;
  52. type: string;
  53. status: string;
  54. assignTo: number;
  55. groupName: string;
  56. consoCode?: string;
  57. pickOrderLines: PickOrderLineRow[];
  58. }
  59. interface PickOrderLineRow {
  60. id: number;
  61. itemId: number;
  62. itemCode: string;
  63. itemName: string;
  64. availableQty: number | null;
  65. requiredQty: number;
  66. uomCode: string;
  67. uomDesc: string;
  68. suggestedList: any[];
  69. }
  70. const style = {
  71. position: "absolute",
  72. top: "50%",
  73. left: "50%",
  74. transform: "translate(-50%, -50%)",
  75. bgcolor: "background.paper",
  76. pt: 5,
  77. px: 5,
  78. pb: 10,
  79. width: { xs: "100%", sm: "100%", md: "100%" },
  80. };
  81. const AssignTo: React.FC<Props> = ({ filterArgs }) => {
  82. const { t } = useTranslation("pickOrder");
  83. const { setIsUploading } = useUploadContext();
  84. const [isUploading, setIsUploadingLocal] = useState(false);
  85. // Update state to use pick order data directly
  86. const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<string[]>([]);
  87. const [filteredPickOrders, setFilteredPickOrders] = useState<PickOrderRow[]>([]);
  88. const [isLoadingItems, setIsLoadingItems] = useState(false);
  89. const [pagingController, setPagingController] = useState({
  90. pageNum: 1,
  91. pageSize: 10,
  92. });
  93. const [totalCountItems, setTotalCountItems] = useState<number>();
  94. const [modalOpen, setModalOpen] = useState(false);
  95. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  96. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  97. const [originalPickOrderData, setOriginalPickOrderData] = useState<PickOrderRow[]>([]);
  98. const formProps = useForm<AssignPickOrderInputs>();
  99. const errors = formProps.formState.errors;
  100. // Update the handler functions to work with string IDs
  101. const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => {
  102. if (checked) {
  103. setSelectedPickOrderIds(prev => [...prev, pickOrderId]);
  104. } else {
  105. setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId));
  106. }
  107. }, []);
  108. const isPickOrderSelected = useCallback((pickOrderId: string) => {
  109. return selectedPickOrderIds.includes(pickOrderId);
  110. }, [selectedPickOrderIds]);
  111. // Update the fetch function to use the correct endpoint
  112. const fetchNewPageItems = useCallback(
  113. async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => {
  114. setIsLoadingItems(true);
  115. try {
  116. const params = {
  117. ...pagingController,
  118. ...filterArgs,
  119. pageNum: (pagingController.pageNum || 1) - 1,
  120. pageSize: pagingController.pageSize || 10,
  121. // Filter for assigned status only
  122. status: "assigned"
  123. };
  124. const res = await fetchPickOrderWithStockClient(params);
  125. if (res && res.records) {
  126. // Convert pick order data to the expected format
  127. const pickOrderRows: PickOrderRow[] = res.records.map((pickOrder: any) => ({
  128. id: pickOrder.id,
  129. code: pickOrder.code,
  130. targetDate: pickOrder.targetDate,
  131. type: pickOrder.type,
  132. status: pickOrder.status,
  133. assignTo: pickOrder.assignTo,
  134. groupName: pickOrder.groupName || "No Group",
  135. consoCode: pickOrder.consoCode,
  136. pickOrderLines: pickOrder.pickOrderLines || []
  137. }));
  138. setOriginalPickOrderData(pickOrderRows);
  139. setFilteredPickOrders(pickOrderRows);
  140. setTotalCountItems(res.total);
  141. } else {
  142. setFilteredPickOrders([]);
  143. setTotalCountItems(0);
  144. }
  145. } catch (error) {
  146. console.error("Error fetching pick orders:", error);
  147. setFilteredPickOrders([]);
  148. setTotalCountItems(0);
  149. } finally {
  150. setIsLoadingItems(false);
  151. }
  152. },
  153. [],
  154. );
  155. // Handle Release operation
  156. // Handle Release operation
  157. const handleRelease = useCallback(async () => {
  158. if (selectedPickOrderIds.length === 0) return;
  159. setIsUploading(true);
  160. try {
  161. // Get the assigned user from the selected pick orders
  162. const selectedPickOrders = filteredPickOrders.filter(pickOrder =>
  163. selectedPickOrderIds.includes(pickOrder.id)
  164. );
  165. // Check if all selected pick orders have the same assigned user
  166. const assignedUsers = selectedPickOrders.map(po => po.assignTo).filter(Boolean);
  167. if (assignedUsers.length === 0) {
  168. alert("Selected pick orders are not assigned to any user.");
  169. return;
  170. }
  171. const assignToValue = assignedUsers[0];
  172. // Validate that all pick orders are assigned to the same user
  173. const allSameUser = assignedUsers.every(userId => userId === assignToValue);
  174. if (!allSameUser) {
  175. alert("All selected pick orders must be assigned to the same user.");
  176. return;
  177. }
  178. console.log("Using assigned user:", assignToValue);
  179. console.log("selectedPickOrderIds:", selectedPickOrderIds);
  180. const releaseRes = await releaseAssignedPickOrders({
  181. pickOrderIds: selectedPickOrderIds.map(id => parseInt(id)),
  182. assignTo: assignToValue
  183. });
  184. if (releaseRes.code === "SUCCESS") {
  185. console.log("Pick orders released successfully");
  186. // Get the consoCode from the response
  187. const consoCode = (releaseRes.entity as any)?.consoCode;
  188. if (consoCode) {
  189. // Create StockOutLine records for each pick order line
  190. for (const pickOrder of selectedPickOrders) {
  191. for (const line of pickOrder.pickOrderLines) {
  192. try {
  193. const stockOutLineData = {
  194. consoCode: consoCode,
  195. pickOrderLineId: line.id,
  196. inventoryLotLineId: 0, // This will be set when user scans QR code
  197. qty: line.requiredQty,
  198. };
  199. console.log("Creating stock out line:", stockOutLineData);
  200. await createStockOutLine(stockOutLineData);
  201. } catch (error) {
  202. console.error("Error creating stock out line for line", line.id, error);
  203. }
  204. }
  205. }
  206. }
  207. fetchNewPageItems(pagingController, filterArgs);
  208. } else {
  209. console.error("Release failed:", releaseRes.message);
  210. }
  211. } catch (error) {
  212. console.error("Error releasing pick orders:", error);
  213. } finally {
  214. setIsUploading(false);
  215. }
  216. }, [selectedPickOrderIds, filteredPickOrders, setIsUploading, fetchNewPageItems, pagingController, filterArgs]);
  217. // Update search criteria to match the new data structure
  218. const searchCriteria: Criterion<any>[] = useMemo(
  219. () => [
  220. {
  221. label: t("Pick Order Code"),
  222. paramName: "code",
  223. type: "text",
  224. },
  225. {
  226. label: t("Group Code"),
  227. paramName: "groupName",
  228. type: "text",
  229. },
  230. {
  231. label: t("Target Date From"),
  232. label2: t("Target Date To"),
  233. paramName: "targetDate",
  234. type: "dateRange",
  235. },
  236. ],
  237. [t],
  238. );
  239. // Update search function to work with pick order data
  240. const handleSearch = useCallback((query: Record<string, any>) => {
  241. setSearchQuery({ ...query });
  242. const filtered = originalPickOrderData.filter((pickOrder) => {
  243. const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate);
  244. const codeMatch = !query.code ||
  245. pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase());
  246. const groupNameMatch = !query.groupName ||
  247. pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase());
  248. // Date range search
  249. let dateMatch = true;
  250. if (query.targetDate || query.targetDateTo) {
  251. try {
  252. if (query.targetDate && !query.targetDateTo) {
  253. const fromDate = dayjs(query.targetDate);
  254. dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') ||
  255. pickOrderTargetDateStr.isAfter(fromDate, 'day');
  256. } else if (!query.targetDate && query.targetDateTo) {
  257. const toDate = dayjs(query.targetDateTo);
  258. dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') ||
  259. pickOrderTargetDateStr.isBefore(toDate, 'day');
  260. } else if (query.targetDate && query.targetDateTo) {
  261. const fromDate = dayjs(query.targetDate);
  262. const toDate = dayjs(query.targetDateTo);
  263. dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') ||
  264. pickOrderTargetDateStr.isAfter(fromDate, 'day')) &&
  265. (pickOrderTargetDateStr.isSame(toDate, 'day') ||
  266. pickOrderTargetDateStr.isBefore(toDate, 'day'));
  267. }
  268. } catch (error) {
  269. console.error("Date parsing error:", error);
  270. dateMatch = true;
  271. }
  272. }
  273. return codeMatch && groupNameMatch && dateMatch;
  274. });
  275. setFilteredPickOrders(filtered);
  276. }, [originalPickOrderData]);
  277. const handleReset = useCallback(() => {
  278. setSearchQuery({});
  279. setFilteredPickOrders(originalPickOrderData);
  280. setTimeout(() => {
  281. setSearchQuery({});
  282. }, 0);
  283. }, [originalPickOrderData]);
  284. // Pagination handlers
  285. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  286. const newPagingController = {
  287. ...pagingController,
  288. pageNum: newPage + 1,
  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. // Component mount effect
  301. useEffect(() => {
  302. fetchNewPageItems(pagingController, filterArgs || {});
  303. }, []);
  304. // Dependencies change effect
  305. useEffect(() => {
  306. if (pagingController && (filterArgs || {})) {
  307. fetchNewPageItems(pagingController, filterArgs || {});
  308. }
  309. }, [pagingController, filterArgs, fetchNewPageItems]);
  310. useEffect(() => {
  311. const loadUsernameList = async () => {
  312. try {
  313. const res = await fetchNameList();
  314. if (res) {
  315. setUsernameList(res);
  316. }
  317. } catch (error) {
  318. console.error("Error loading username list:", error);
  319. }
  320. };
  321. loadUsernameList();
  322. }, []);
  323. // Update the table component to work with pick order data directly
  324. const CustomPickOrderTable = () => {
  325. // Helper function to get user name
  326. const getUserName = useCallback((assignToId: number | null | undefined) => {
  327. if (!assignToId) return '-';
  328. const user = usernameList.find(u => u.id === assignToId);
  329. return user ? user.name : `User ${assignToId}`;
  330. }, [usernameList]);
  331. return (
  332. <>
  333. <TableContainer component={Paper}>
  334. <Table>
  335. <TableHead>
  336. <TableRow>
  337. <TableCell>{t("Selected")}</TableCell>
  338. <TableCell>{t("Pick Order Code")}</TableCell>
  339. <TableCell>{t("Group Code")}</TableCell>
  340. <TableCell>{t("Item Code")}</TableCell>
  341. <TableCell>{t("Item Name")}</TableCell>
  342. <TableCell align="right">{t("Order Quantity")}</TableCell>
  343. <TableCell align="right">{t("Current Stock")}</TableCell>
  344. <TableCell align="right">{t("Stock Unit")}</TableCell>
  345. <TableCell>{t("Target Date")}</TableCell>
  346. <TableCell>{t("Assigned To")}</TableCell>
  347. </TableRow>
  348. </TableHead>
  349. <TableBody>
  350. {filteredPickOrders.length === 0 ? (
  351. <TableRow>
  352. <TableCell colSpan={10} align="center">
  353. <Typography variant="body2" color="text.secondary">
  354. {t("No data available")}
  355. </Typography>
  356. </TableCell>
  357. </TableRow>
  358. ) : (
  359. filteredPickOrders.map((pickOrder) => (
  360. pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => (
  361. <TableRow key={`${pickOrder.id}-${line.id}`}>
  362. {/* Checkbox - only show for first line of each pick order */}
  363. <TableCell>
  364. {index === 0 ? (
  365. <Checkbox
  366. checked={isPickOrderSelected(pickOrder.id)}
  367. onChange={(e) => handlePickOrderSelect(pickOrder.id, e.target.checked)}
  368. disabled={!isEmpty(pickOrder.consoCode)}
  369. />
  370. ) : null}
  371. </TableCell>
  372. {/* Pick Order Code - only show for first line */}
  373. <TableCell>
  374. {index === 0 ? pickOrder.code : null}
  375. </TableCell>
  376. {/* Group Name - only show for first line */}
  377. <TableCell>
  378. {index === 0 ? pickOrder.groupName : null}
  379. </TableCell>
  380. {/* Item Code */}
  381. <TableCell>{line.itemCode}</TableCell>
  382. {/* Item Name */}
  383. <TableCell>{line.itemName}</TableCell>
  384. {/* Order Quantity */}
  385. <TableCell align="right">{line.requiredQty}</TableCell>
  386. {/* Current Stock */}
  387. <TableCell align="right">
  388. <Typography
  389. variant="body2"
  390. color={line.availableQty && line.availableQty > 0 ? "success.main" : "error.main"}
  391. sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }}
  392. >
  393. {(line.availableQty || 0).toLocaleString()}
  394. </Typography>
  395. </TableCell>
  396. {/* Unit */}
  397. <TableCell align="right">{line.uomDesc}</TableCell>
  398. {/* Target Date - only show for first line */}
  399. <TableCell>
  400. {index === 0 ? (
  401. arrayToDayjs(pickOrder.targetDate)
  402. .add(-1, "month")
  403. .format(OUTPUT_DATE_FORMAT)
  404. ) : null}
  405. </TableCell>
  406. {/* Assigned To - only show for first line */}
  407. <TableCell>
  408. {index === 0 ? (
  409. <Typography variant="body2">
  410. {getUserName(pickOrder.assignTo)}
  411. </Typography>
  412. ) : null}
  413. </TableCell>
  414. </TableRow>
  415. ))
  416. ))
  417. )}
  418. </TableBody>
  419. </Table>
  420. </TableContainer>
  421. <TablePagination
  422. component="div"
  423. count={totalCountItems || 0}
  424. page={(pagingController.pageNum - 1)}
  425. rowsPerPage={pagingController.pageSize}
  426. onPageChange={handlePageChange}
  427. onRowsPerPageChange={handlePageSizeChange}
  428. rowsPerPageOptions={[10, 25, 50, 100]}
  429. labelRowsPerPage={t("Rows per page")}
  430. labelDisplayedRows={({ from, to, count }) =>
  431. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  432. }
  433. />
  434. </>
  435. );
  436. };
  437. return (
  438. <>
  439. <SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} />
  440. <Grid container rowGap={1}>
  441. <Grid item xs={12}>
  442. {isLoadingItems ? (
  443. <CircularProgress size={40} />
  444. ) : (
  445. <CustomPickOrderTable />
  446. )}
  447. </Grid>
  448. <Grid item xs={12}>
  449. <Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}>
  450. <Button
  451. disabled={selectedPickOrderIds.length < 1}
  452. variant="outlined"
  453. onClick={handleRelease}
  454. >
  455. {t("Release")}
  456. </Button>
  457. </Box>
  458. </Grid>
  459. </Grid>
  460. </>
  461. );
  462. };
  463. export default AssignTo;