FPSMS-frontend
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

1165 linhas
39 KiB

  1. "use client";
  2. import {
  3. Autocomplete,
  4. Box,
  5. Button,
  6. CircularProgress,
  7. FormControl,
  8. Grid,
  9. Paper,
  10. Stack,
  11. Table,
  12. TableBody,
  13. TableCell,
  14. TableContainer,
  15. TableHead,
  16. TableRow,
  17. TextField,
  18. Typography,
  19. Checkbox,
  20. FormControlLabel,
  21. Select,
  22. MenuItem,
  23. InputLabel,
  24. TablePagination,
  25. } from "@mui/material";
  26. import { useCallback, useEffect, useMemo, useState } from "react";
  27. import { useTranslation } from "react-i18next";
  28. import {
  29. ByItemsSummary,
  30. ConsoPickOrderResult,
  31. PickOrderLine,
  32. PickOrderResult,
  33. } from "@/app/api/pickOrder";
  34. import { useRouter } from "next/navigation";
  35. import { GridInputRowSelectionModel } from "@mui/x-data-grid";
  36. import {
  37. fetchConsoDetail,
  38. fetchConsoPickOrderClient,
  39. releasePickOrder,
  40. ReleasePickOrderInputs,
  41. fetchPickOrderDetails,
  42. fetchAllPickOrderDetails,
  43. GetPickOrderInfoResponse,
  44. GetPickOrderLineInfo,
  45. createStockOutLine,
  46. updateStockOutLineStatus,
  47. resuggestPickOrder,
  48. } from "@/app/api/pickOrder/actions";
  49. import { EditNote } from "@mui/icons-material";
  50. import { fetchNameList, NameList } from "@/app/api/user/actions";
  51. import {
  52. FormProvider,
  53. SubmitErrorHandler,
  54. SubmitHandler,
  55. useForm,
  56. } from "react-hook-form";
  57. import { pickOrderStatusMap } from "@/app/utils/formatUtil";
  58. import { QcItemWithChecks } from "@/app/api/qc";
  59. import { fetchQcItemCheck, fetchPickOrderQcResult } from "@/app/api/qc/actions";
  60. import { PurchaseQcResult } from "@/app/api/po/actions";
  61. import PickQcStockInModalVer2 from "./PickQcStockInModalVer3";
  62. import { fetchPickOrderLineLotDetails, PickOrderLotDetailResponse } from "@/app/api/pickOrder/actions";
  63. import SearchResults, { Column } from "../SearchResults/SearchResults";
  64. import { defaultPagingController } from "../SearchResults/SearchResults";
  65. import SearchBox, { Criterion } from "../SearchBox";
  66. import dayjs from "dayjs";
  67. import { dummyQCData } from "../PoDetail/dummyQcTemplate";
  68. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  69. import LotTable from './LotTable';
  70. import { updateInventoryLotLineStatus, updateInventoryStatus, updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  71. import { useSession } from "next-auth/react"; // ✅ Add session import
  72. import { SessionWithTokens } from "@/config/authConfig"; // ✅ Add custom session type
  73. interface Props {
  74. filterArgs: Record<string, any>;
  75. }
  76. interface LotPickData {
  77. id: number;
  78. lotId: number;
  79. lotNo: string;
  80. expiryDate: string;
  81. location: string;
  82. stockUnit: string;
  83. inQty: number;
  84. availableQty: number;
  85. requiredQty: number;
  86. actualPickQty: number;
  87. lotStatus: string;
  88. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
  89. stockOutLineId?: number;
  90. stockOutLineStatus?: string;
  91. stockOutLineQty?: number;
  92. }
  93. interface PickQtyData {
  94. [lineId: number]: {
  95. [lotId: number]: number;
  96. };
  97. }
  98. const PickExecution: React.FC<Props> = ({ filterArgs }) => {
  99. const { t } = useTranslation("pickOrder");
  100. const router = useRouter();
  101. const { data: session } = useSession() as { data: SessionWithTokens | null }; // ✅ Add session
  102. // ✅ Get current user ID from session with proper typing
  103. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  104. const [filteredPickOrders, setFilteredPickOrders] = useState(
  105. [] as ConsoPickOrderResult[],
  106. );
  107. const [isLoading, setIsLoading] = useState(false);
  108. const [selectedConsoCode, setSelectedConsoCode] = useState<string | undefined>();
  109. const [revertIds, setRevertIds] = useState<GridInputRowSelectionModel>([]);
  110. const [totalCount, setTotalCount] = useState<number>();
  111. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  112. const [byPickOrderRows, setByPickOrderRows] = useState<
  113. Omit<PickOrderResult, "items">[] | undefined
  114. >(undefined);
  115. const [byItemsRows, setByItemsRows] = useState<ByItemsSummary[] | undefined>(
  116. undefined,
  117. );
  118. const [disableRelease, setDisableRelease] = useState<boolean>(true);
  119. const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
  120. const [pickOrderDetails, setPickOrderDetails] = useState<GetPickOrderInfoResponse | null>(null);
  121. const [detailLoading, setDetailLoading] = useState(false);
  122. const [pickQtyData, setPickQtyData] = useState<PickQtyData>({});
  123. const [lotData, setLotData] = useState<LotPickData[]>([]);
  124. const [qcItems, setQcItems] = useState<QcItemWithChecks[]>([]);
  125. const [qcModalOpen, setQcModalOpen] = useState(false);
  126. const [selectedItemForQc, setSelectedItemForQc] = useState<GetPickOrderLineInfo & {
  127. pickOrderCode: string;
  128. qcResult?: PurchaseQcResult[];
  129. } | null>(null);
  130. const [selectedLotForQc, setSelectedLotForQc] = useState<LotPickData | null>(null);
  131. // ✅ Add lot selection state variables
  132. const [selectedLotRowId, setSelectedLotRowId] = useState<string | null>(null);
  133. const [selectedLotId, setSelectedLotId] = useState<number | null>(null);
  134. // 新增:分页控制器
  135. const [mainTablePagingController, setMainTablePagingController] = useState({
  136. pageNum: 0,
  137. pageSize: 10,
  138. });
  139. const [lotTablePagingController, setLotTablePagingController] = useState({
  140. pageNum: 0,
  141. pageSize: 10,
  142. });
  143. // Add missing search state variables
  144. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  145. const [originalPickOrderData, setOriginalPickOrderData] = useState<GetPickOrderInfoResponse | null>(null);
  146. const formProps = useForm<ReleasePickOrderInputs>();
  147. const errors = formProps.formState.errors;
  148. const onDetailClick = useCallback(
  149. (pickOrder: any) => {
  150. console.log(pickOrder);
  151. const status = pickOrder.status;
  152. if (pickOrderStatusMap[status] >= 3) {
  153. router.push(`/pickOrder/detail?consoCode=${pickOrder.consoCode}`);
  154. } else {
  155. setSelectedConsoCode(pickOrder.consoCode);
  156. }
  157. },
  158. [router],
  159. );
  160. const fetchNewPageConsoPickOrder = useCallback(
  161. async (
  162. pagingController: Record<string, number>,
  163. filterArgs: Record<string, number>,
  164. ) => {
  165. setIsLoading(true);
  166. const params = {
  167. ...pagingController,
  168. ...filterArgs,
  169. };
  170. const res = await fetchConsoPickOrderClient(params);
  171. if (res) {
  172. console.log(res);
  173. setFilteredPickOrders(res.records);
  174. setTotalCount(res.total);
  175. }
  176. setIsLoading(false);
  177. },
  178. [],
  179. );
  180. useEffect(() => {
  181. fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs);
  182. }, [fetchNewPageConsoPickOrder, filterArgs]);
  183. const handleUpdateStockOutLineStatus = useCallback(async (
  184. stockOutLineId: number,
  185. status: string,
  186. qty?: number
  187. ) => {
  188. try {
  189. const updateData = {
  190. id: stockOutLineId,
  191. status: status,
  192. qty: qty
  193. };
  194. console.log("Updating stock out line status:", updateData);
  195. const result = await updateStockOutLineStatus(updateData);
  196. if (result) {
  197. console.log("Stock out line status updated successfully:", result);
  198. // Refresh lot data to show updated status
  199. if (selectedRowId) {
  200. handleRowSelect(selectedRowId);
  201. }
  202. }
  203. } catch (error) {
  204. console.error("Error updating stock out line status:", error);
  205. }
  206. }, [selectedRowId]);
  207. const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => {
  208. let isReleasable = true;
  209. for (const item of itemList) {
  210. isReleasable = item.requiredQty >= item.availableQty;
  211. if (!isReleasable) return isReleasable;
  212. }
  213. return isReleasable;
  214. }, []);
  215. const fetchConso = useCallback(
  216. async (consoCode: string) => {
  217. const res = await fetchConsoDetail(consoCode);
  218. const nameListRes = await fetchNameList();
  219. if (res) {
  220. console.log(res);
  221. setByPickOrderRows(res.pickOrders);
  222. setByItemsRows(res.items);
  223. setDisableRelease(isReleasable(res.items));
  224. } else {
  225. console.log("error");
  226. console.log(res);
  227. }
  228. if (nameListRes) {
  229. console.log(nameListRes);
  230. setUsernameList(nameListRes);
  231. }
  232. },
  233. [isReleasable],
  234. );
  235. const handleFetchAllPickOrderDetails = useCallback(async () => {
  236. setDetailLoading(true);
  237. try {
  238. // ✅ Use current user ID for filtering
  239. const data = await fetchAllPickOrderDetails(currentUserId);
  240. setPickOrderDetails(data);
  241. setOriginalPickOrderData(data); // Store original data for filtering
  242. console.log("All Pick Order Details for user:", currentUserId, data);
  243. const initialPickQtyData: PickQtyData = {};
  244. data.pickOrders.forEach((pickOrder: any) => {
  245. pickOrder.pickOrderLines.forEach((line: any) => {
  246. initialPickQtyData[line.id] = {};
  247. });
  248. });
  249. setPickQtyData(initialPickQtyData);
  250. } catch (error) {
  251. console.error("Error fetching all pick order details:", error);
  252. } finally {
  253. setDetailLoading(false);
  254. }
  255. }, [currentUserId]); // ✅ Add currentUserId as dependency
  256. useEffect(() => {
  257. handleFetchAllPickOrderDetails();
  258. }, [handleFetchAllPickOrderDetails]);
  259. const onChange = useCallback(
  260. (event: React.SyntheticEvent, newValue: NameList) => {
  261. console.log(newValue);
  262. formProps.setValue("assignTo", newValue.id);
  263. },
  264. [formProps],
  265. );
  266. const onSubmit = useCallback<SubmitHandler<ReleasePickOrderInputs>>(
  267. async (data, event) => {
  268. console.log(data);
  269. try {
  270. const res = await releasePickOrder(data);
  271. console.log(res);
  272. if (res.consoCode.length > 0) {
  273. console.log(res);
  274. router.push(`/pickOrder/detail?consoCode=${res.consoCode}`);
  275. } else {
  276. console.log(res);
  277. }
  278. } catch (error) {
  279. console.log(error);
  280. }
  281. },
  282. [router],
  283. );
  284. const onSubmitError = useCallback<SubmitErrorHandler<ReleasePickOrderInputs>>(
  285. (errors) => {},
  286. [],
  287. );
  288. const handleConsolidate_revert = useCallback(() => {
  289. console.log(revertIds);
  290. }, [revertIds]);
  291. useEffect(() => {
  292. if (selectedConsoCode) {
  293. fetchConso(selectedConsoCode);
  294. formProps.setValue("consoCode", selectedConsoCode);
  295. }
  296. }, [selectedConsoCode, fetchConso, formProps]);
  297. const handlePickQtyChange = useCallback((lineId: number, lotId: number, value: number | string) => {
  298. console.log("Changing pick qty:", { lineId, lotId, value });
  299. // ✅ Handle both number and string values
  300. const numericValue = typeof value === 'string' ? (value === '' ? 0 : parseInt(value, 10)) : value;
  301. setPickQtyData(prev => {
  302. const newData = {
  303. ...prev,
  304. [lineId]: {
  305. ...prev[lineId],
  306. [lotId]: numericValue
  307. }
  308. };
  309. console.log("New pick qty data:", newData);
  310. return newData;
  311. });
  312. }, []);
  313. const handleSubmitPickQty = useCallback(async (lineId: number, lotId: number) => {
  314. const qty = pickQtyData[lineId]?.[lotId] || 0;
  315. console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`);
  316. // ✅ Find the stock out line for this lot
  317. const selectedLot = lotData.find(lot => lot.lotId === lotId);
  318. if (!selectedLot?.stockOutLineId) {
  319. return;
  320. }
  321. try {
  322. // ✅ Only two statuses: partially_completed or completed
  323. let newStatus = 'partially_completed'; // Default status
  324. if (qty >= selectedLot.requiredQty) {
  325. newStatus = 'completed'; // Full quantity picked
  326. }
  327. // If qty < requiredQty, stays as 'partially_completed'
  328. // ✅ Function 1: Update stock out line with new status and quantity
  329. try {
  330. // ✅ Function 1: Update stock out line with new status and quantity
  331. const stockOutLineUpdate = await updateStockOutLineStatus({
  332. id: selectedLot.stockOutLineId,
  333. status: newStatus,
  334. qty: qty
  335. });
  336. console.log("✅ Stock out line updated:", stockOutLineUpdate);
  337. } catch (error) {
  338. console.error("❌ Error updating stock out line:", error);
  339. return; // Stop execution if this fails
  340. }
  341. // ✅ Function 2: Update inventory lot line (balance hold_qty and out_qty)
  342. if (qty > 0) {
  343. const inventoryLotLineUpdate = await updateInventoryLotLineQuantities({
  344. inventoryLotLineId: lotId,
  345. qty: qty,
  346. status: 'available',
  347. operation: 'pick'
  348. });
  349. console.log("Inventory lot line updated:", inventoryLotLineUpdate);
  350. }
  351. // ✅ Function 3: Handle inventory table onhold if needed
  352. if (newStatus === 'completed') {
  353. // All required quantity picked - might need to update inventory status
  354. // Note: We'll handle inventory update in a separate function or after selectedRow is available
  355. console.log("Completed status - inventory update needed but selectedRow not available yet");
  356. }
  357. console.log("All updates completed successfully");
  358. // ✅ Refresh lot data to show updated quantities
  359. if (selectedRowId) {
  360. await handleRowSelect(selectedRowId, true);
  361. // Note: We'll handle refresh after the function is properly defined
  362. console.log("Data refresh needed but handleRowSelect not available yet");
  363. }
  364. await handleFetchAllPickOrderDetails();
  365. } catch (error) {
  366. console.error("Error updating pick quantity:", error);
  367. }
  368. }, [pickQtyData, lotData, selectedRowId]);
  369. const getTotalPickedQty = useCallback((lineId: number) => {
  370. const lineData = pickQtyData[lineId];
  371. if (!lineData) return 0;
  372. return Object.values(lineData).reduce((sum, qty) => sum + qty, 0);
  373. }, [pickQtyData]);
  374. const handleQcCheck = useCallback(async (line: GetPickOrderLineInfo, pickOrderCode: string) => {
  375. // ✅ Get the selected lot for QC
  376. if (!selectedLotId) {
  377. return;
  378. }
  379. const selectedLot = lotData.find(lot => lot.lotId === selectedLotId);
  380. if (!selectedLot) {
  381. //alert("Selected lot not found in lot data");
  382. return;
  383. }
  384. // ✅ Check if stock out line exists
  385. if (!selectedLot.stockOutLineId) {
  386. //alert("Please create a stock out line first before performing QC check");
  387. return;
  388. }
  389. setSelectedLotForQc(selectedLot);
  390. // ✅ ALWAYS use dummy data for consistent behavior
  391. const transformedDummyData = dummyQCData.map(item => ({
  392. id: item.id,
  393. code: item.code,
  394. name: item.name,
  395. itemId: line.itemId,
  396. lowerLimit: undefined,
  397. upperLimit: undefined,
  398. description: item.qcDescription,
  399. // ✅ Always reset QC result properties to undefined for fresh start
  400. qcPassed: undefined,
  401. failQty: undefined,
  402. remarks: undefined
  403. }));
  404. setQcItems(transformedDummyData as QcItemWithChecks[]);
  405. // ✅ Get existing QC results if any (for display purposes only)
  406. let qcResult: any[] = [];
  407. try {
  408. const rawQcResult = await fetchPickOrderQcResult(line.id);
  409. qcResult = rawQcResult.map((result: any) => ({
  410. ...result,
  411. isPassed: result.isPassed || false
  412. }));
  413. } catch (error) {
  414. // No existing QC result found - this is normal
  415. }
  416. setSelectedItemForQc({
  417. ...line,
  418. pickOrderCode,
  419. qcResult
  420. });
  421. setQcModalOpen(true);
  422. }, [lotData, selectedLotId, setQcItems]);
  423. const handleCloseQcModal = useCallback(() => {
  424. console.log("Closing QC modal");
  425. setQcModalOpen(false);
  426. setSelectedItemForQc(null);
  427. }, []);
  428. const handleSetItemDetail = useCallback((item: any) => {
  429. setSelectedItemForQc(item);
  430. }, []);
  431. // 新增:处理分页变化
  432. const handleMainTablePageChange = useCallback((event: unknown, newPage: number) => {
  433. setMainTablePagingController(prev => ({
  434. ...prev,
  435. pageNum: newPage,
  436. }));
  437. }, []);
  438. const handleMainTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  439. const newPageSize = parseInt(event.target.value, 10);
  440. setMainTablePagingController({
  441. pageNum: 0,
  442. pageSize: newPageSize,
  443. });
  444. }, []);
  445. const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => {
  446. setLotTablePagingController(prev => ({
  447. ...prev,
  448. pageNum: newPage,
  449. }));
  450. }, []);
  451. const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  452. const newPageSize = parseInt(event.target.value, 10);
  453. setLotTablePagingController({
  454. pageNum: 0,
  455. pageSize: newPageSize,
  456. });
  457. }, []);
  458. // ✅ Fix lot selection logic
  459. const handleLotSelection = useCallback((uniqueLotId: string, lotId: number) => {
  460. console.log("=== DEBUG: Lot Selection ===");
  461. console.log("uniqueLotId:", uniqueLotId);
  462. console.log("lotId (inventory lot line ID):", lotId);
  463. // Find the selected lot data
  464. const selectedLot = lotData.find(lot => lot.lotId === lotId);
  465. console.log("Selected lot data:", selectedLot);
  466. // If clicking the same lot, unselect it
  467. if (selectedLotRowId === uniqueLotId) {
  468. setSelectedLotRowId(null);
  469. setSelectedLotId(null);
  470. } else {
  471. // Select the new lot
  472. setSelectedLotRowId(uniqueLotId);
  473. setSelectedLotId(lotId);
  474. }
  475. }, [selectedLotRowId]);
  476. // ✅ Add function to handle row selection that resets lot selection
  477. const handleRowSelect = useCallback(async (lineId: number, preserveLotSelection: boolean = false) => {
  478. setSelectedRowId(lineId);
  479. // ✅ Only reset lot selection if not preserving
  480. if (!preserveLotSelection) {
  481. setSelectedLotRowId(null);
  482. setSelectedLotId(null);
  483. }
  484. try {
  485. const lotDetails = await fetchPickOrderLineLotDetails(lineId);
  486. console.log("Lot details from API:", lotDetails);
  487. const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({
  488. id: lot.id, // This should be the unique row ID for the table
  489. lotId: lot.lotId, // This is the inventory lot line ID
  490. lotNo: lot.lotNo,
  491. expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A',
  492. location: lot.location,
  493. stockUnit: lot.stockUnit,
  494. inQty: lot.inQty,
  495. availableQty: lot.availableQty,
  496. requiredQty: lot.requiredQty,
  497. actualPickQty: lot.actualPickQty || 0,
  498. lotStatus: lot.lotStatus,
  499. lotAvailability: lot.lotAvailability,
  500. stockOutLineId: lot.stockOutLineId,
  501. stockOutLineStatus: lot.stockOutLineStatus,
  502. stockOutLineQty: lot.stockOutLineQty
  503. }));
  504. setLotData(realLotData);
  505. } catch (error) {
  506. console.error("Error fetching lot details:", error);
  507. setLotData([]);
  508. }
  509. }, []);
  510. const prepareMainTableData = useMemo(() => {
  511. if (!pickOrderDetails) return [];
  512. return pickOrderDetails.pickOrders.flatMap((pickOrder) =>
  513. pickOrder.pickOrderLines.map((line) => {
  514. // 修复:处理 availableQty 可能为 null 的情况
  515. const availableQty = line.availableQty ?? 0;
  516. const balanceToPick = availableQty - line.requiredQty;
  517. // ✅ 使用 dayjs 进行一致的日期格式化
  518. const formattedTargetDate = pickOrder.targetDate
  519. ? dayjs(pickOrder.targetDate).format('YYYY-MM-DD')
  520. : 'N/A';
  521. return {
  522. ...line,
  523. pickOrderCode: pickOrder.code,
  524. targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期
  525. balanceToPick: balanceToPick,
  526. pickedQty: line.pickedQty,
  527. // 确保 availableQty 不为 null
  528. availableQty: availableQty,
  529. };
  530. })
  531. );
  532. }, [pickOrderDetails]);
  533. const prepareLotTableData = useMemo(() => {
  534. return lotData.map((lot) => ({
  535. ...lot,
  536. id: lot.lotId,
  537. }));
  538. }, [lotData]);
  539. // 新增:分页数据
  540. const paginatedMainTableData = useMemo(() => {
  541. const startIndex = mainTablePagingController.pageNum * mainTablePagingController.pageSize;
  542. const endIndex = startIndex + mainTablePagingController.pageSize;
  543. return prepareMainTableData.slice(startIndex, endIndex);
  544. }, [prepareMainTableData, mainTablePagingController]);
  545. const paginatedLotTableData = useMemo(() => {
  546. const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize;
  547. const endIndex = startIndex + lotTablePagingController.pageSize;
  548. return prepareLotTableData.slice(startIndex, endIndex);
  549. }, [prepareLotTableData, lotTablePagingController]);
  550. const selectedRow = useMemo(() => {
  551. if (!selectedRowId || !pickOrderDetails) return null;
  552. for (const pickOrder of pickOrderDetails.pickOrders) {
  553. const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
  554. if (foundLine) {
  555. return { ...foundLine, pickOrderCode: pickOrder.code };
  556. }
  557. }
  558. return null;
  559. }, [selectedRowId, pickOrderDetails]);
  560. const handleInventoryUpdate = useCallback(async (itemId: number, lotId: number, qty: number) => {
  561. try {
  562. const inventoryUpdate = await updateInventoryStatus({
  563. itemId: itemId,
  564. lotId: lotId,
  565. status: 'reserved',
  566. qty: qty
  567. });
  568. console.log("Inventory status updated:", inventoryUpdate);
  569. } catch (error) {
  570. console.error("Error updating inventory status:", error);
  571. }
  572. }, []);
  573. const handleLotDataRefresh = useCallback(async () => {
  574. if (selectedRowId) {
  575. try {
  576. await handleRowSelect(selectedRowId, true); // Preserve lot selection
  577. } catch (error) {
  578. console.error("Error refreshing lot data:", error);
  579. }
  580. }
  581. }, [selectedRowId, handleRowSelect]);
  582. // ✅ Add this function after handleRowSelect is defined
  583. const handleDataRefresh = useCallback(async () => {
  584. if (selectedRowId) {
  585. try {
  586. await handleRowSelect(selectedRowId, true);
  587. } catch (error) {
  588. console.error("Error refreshing data:", error);
  589. }
  590. }
  591. }, [selectedRowId, handleRowSelect]);
  592. const handleInsufficientStock = useCallback(async () => {
  593. console.log("Insufficient stock - testing resuggest API");
  594. if (!selectedRowId || !pickOrderDetails) {
  595. // alert("Please select a pick order line first");
  596. return;
  597. }
  598. // Find the pick order ID from the selected row
  599. let pickOrderId: number | null = null;
  600. for (const pickOrder of pickOrderDetails.pickOrders) {
  601. const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
  602. if (foundLine) {
  603. pickOrderId = pickOrder.id;
  604. break;
  605. }
  606. }
  607. if (!pickOrderId) {
  608. // alert("Could not find pick order ID for selected line");
  609. return;
  610. }
  611. try {
  612. console.log(`Calling resuggest API for pick order ID: ${pickOrderId}`);
  613. // Call the resuggest API
  614. const result = await resuggestPickOrder(pickOrderId);
  615. console.log("Resuggest API result:", result);
  616. if (result.code === "SUCCESS") {
  617. //alert(`✅ Resuggest successful!\n\nMessage: ${result.message}\n\nRemoved: ${result.message?.includes('Removed') ? 'Yes' : 'No'}\nCreated: ${result.message?.includes('created') ? 'Yes' : 'No'}`);
  618. // Refresh the lot data to show the new suggestions
  619. if (selectedRowId) {
  620. await handleRowSelect(selectedRowId);
  621. }
  622. // Also refresh the main pick order details
  623. await handleFetchAllPickOrderDetails();
  624. } else {
  625. //alert(`❌ Resuggest failed!\n\nError: ${result.message}`);
  626. }
  627. } catch (error) {
  628. console.error("Error calling resuggest API:", error);
  629. //alert(`❌ Error calling resuggest API:\n\n${error instanceof Error ? error.message : 'Unknown error'}`);
  630. }
  631. }, [selectedRowId, pickOrderDetails, handleRowSelect, handleFetchAllPickOrderDetails]);
  632. // Add this function (around line 350)
  633. const hasSelectedLots = useCallback((lineId: number) => {
  634. return selectedLotRowId !== null;
  635. }, [selectedLotRowId]);
  636. // Add state for showing input body
  637. const [showInputBody, setShowInputBody] = useState(false);
  638. const [selectedLotForInput, setSelectedLotForInput] = useState<LotPickData | null>(null);
  639. // Add function to handle lot selection for input body display
  640. const handleLotSelectForInput = useCallback((lot: LotPickData) => {
  641. setSelectedLotForInput(lot);
  642. setShowInputBody(true);
  643. }, []);
  644. // Add function to generate input body
  645. const generateInputBody = useCallback((): CreateStockOutLine | null => {
  646. if (!selectedLotForInput || !selectedRowId || !selectedRow || !pickOrderDetails?.consoCode) {
  647. return null;
  648. }
  649. return {
  650. consoCode: pickOrderDetails.consoCode,
  651. pickOrderLineId: selectedRowId,
  652. inventoryLotLineId: selectedLotForInput.lotId,
  653. qty: 0.0
  654. };
  655. }, [selectedLotForInput, selectedRowId, selectedRow, pickOrderDetails?.consoCode]);
  656. // Add function to handle create stock out line
  657. const handleCreateStockOutLine = useCallback(async (inventoryLotLineId: number) => {
  658. if (!selectedRowId || !pickOrderDetails?.consoCode) {
  659. console.error("Missing required data for creating stock out line.");
  660. return;
  661. }
  662. try {
  663. // ✅ Store current lot selection before refresh
  664. const currentSelectedLotRowId = selectedLotRowId;
  665. const currentSelectedLotId = selectedLotId;
  666. const stockOutLineData: CreateStockOutLine = {
  667. consoCode: pickOrderDetails.consoCode,
  668. pickOrderLineId: selectedRowId,
  669. inventoryLotLineId: inventoryLotLineId,
  670. qty: 0.0
  671. };
  672. console.log("=== STOCK OUT LINE CREATION DEBUG ===");
  673. console.log("Input Body:", JSON.stringify(stockOutLineData, null, 2));
  674. // ✅ Use the correct API function
  675. const result = await createStockOutLine(stockOutLineData);
  676. console.log("Stock Out Line created:", result);
  677. if (result) {
  678. console.log("Stock out line created successfully:", result);
  679. // ✅ Auto-refresh data after successful creation
  680. console.log("🔄 Refreshing data after stock out line creation...");
  681. try {
  682. // ✅ Refresh lot data for the selected row (maintains selection)
  683. if (selectedRowId) {
  684. await handleRowSelect(selectedRowId, true); // ✅ Preserve lot selection
  685. }
  686. // ✅ Refresh main pick order details
  687. await handleFetchAllPickOrderDetails();
  688. console.log("✅ Data refresh completed - lot selection maintained!");
  689. } catch (refreshError) {
  690. console.error("❌ Error refreshing data:", refreshError);
  691. }
  692. setShowInputBody(false); // Hide preview after successful creation
  693. } else {
  694. console.error("Failed to create stock out line: No response");
  695. }
  696. } catch (error) {
  697. console.error("Error creating stock out line:", error);
  698. }
  699. }, [selectedRowId, pickOrderDetails?.consoCode, handleRowSelect, handleFetchAllPickOrderDetails, selectedLotRowId, selectedLotId]);
  700. // ✅ New function to refresh data while preserving lot selection
  701. const handleRefreshDataPreserveSelection = useCallback(async () => {
  702. if (!selectedRowId) return;
  703. // ✅ Store current lot selection
  704. const currentSelectedLotRowId = selectedLotRowId;
  705. const currentSelectedLotId = selectedLotId;
  706. try {
  707. // ✅ Refresh lot data
  708. await handleRowSelect(selectedRowId, true); // ✅ Preserve selection
  709. // ✅ Refresh main pick order details
  710. await handleFetchAllPickOrderDetails();
  711. // ✅ Restore lot selection
  712. setSelectedLotRowId(currentSelectedLotRowId);
  713. setSelectedLotId(currentSelectedLotId);
  714. console.log("✅ Data refreshed with selection preserved");
  715. } catch (error) {
  716. console.error("❌ Error refreshing data:", error);
  717. }
  718. }, [selectedRowId, selectedLotRowId, selectedLotId, handleRowSelect, handleFetchAllPickOrderDetails]);
  719. // 自定义主表格组件
  720. const CustomMainTable = () => {
  721. return (
  722. <>
  723. <TableContainer component={Paper}>
  724. <Table>
  725. <TableHead>
  726. <TableRow>
  727. <TableCell>{t("Selected")}</TableCell>
  728. <TableCell>{t("Pick Order Code")}</TableCell>
  729. <TableCell>{t("Item Code")}</TableCell>
  730. <TableCell>{t("Item Name")}</TableCell>
  731. <TableCell align="right">{t("Order Quantity")}</TableCell>
  732. <TableCell align="right">{t("Current Stock")}</TableCell>
  733. <TableCell align="right">{t("Qty Already Picked")}</TableCell>
  734. <TableCell align="right">{t("Stock Unit")}</TableCell>
  735. <TableCell align="right">{t("Target Date")}</TableCell>
  736. </TableRow>
  737. </TableHead>
  738. <TableBody>
  739. {paginatedMainTableData.length === 0 ? (
  740. <TableRow>
  741. <TableCell colSpan={7} align="center">
  742. <Typography variant="body2" color="text.secondary">
  743. {t("No data available")}
  744. </Typography>
  745. </TableCell>
  746. </TableRow>
  747. ) : (
  748. paginatedMainTableData.map((line) => {
  749. // 修复:处理 availableQty 可能为 null 的情况,并确保负值显示为 0
  750. const availableQty = line.availableQty ?? 0;
  751. const balanceToPick = Math.max(0, availableQty - line.requiredQty); // 确保不为负数
  752. const totalPickedQty = getTotalPickedQty(line.id);
  753. const actualPickedQty = line.pickedQty ?? 0;
  754. return (
  755. <TableRow
  756. key={line.id}
  757. sx={{
  758. "& > *": { borderBottom: "unset" },
  759. color: "black",
  760. backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit",
  761. cursor: "pointer",
  762. "&:hover": {
  763. backgroundColor: "action.hover",
  764. },
  765. }}
  766. >
  767. <TableCell align="center" sx={{ width: "60px" }}>
  768. <Checkbox
  769. checked={selectedRowId === line.id}
  770. onChange={(e) => {
  771. if (e.target.checked) {
  772. handleRowSelect(line.id);
  773. } else {
  774. setSelectedRowId(null);
  775. setLotData([]);
  776. }
  777. }}
  778. onClick={(e) => e.stopPropagation()}
  779. />
  780. </TableCell>
  781. <TableCell align="left">{line.pickOrderCode}</TableCell>
  782. <TableCell align="left">{line.itemCode}</TableCell>
  783. <TableCell align="left">{line.itemName}</TableCell>
  784. <TableCell align="right">{line.requiredQty}</TableCell>
  785. <TableCell align="right" sx={{
  786. color: availableQty >= line.requiredQty ? 'success.main' : 'error.main',
  787. }}>
  788. {availableQty.toLocaleString()} {/* 添加千位分隔符 */}
  789. </TableCell>
  790. <TableCell align="right">{actualPickedQty}</TableCell>
  791. <TableCell align="right">{line.uomDesc}</TableCell>
  792. <TableCell align="right">{line.targetDate}</TableCell>
  793. </TableRow>
  794. );
  795. })
  796. )}
  797. </TableBody>
  798. </Table>
  799. </TableContainer>
  800. <TablePagination
  801. component="div"
  802. count={prepareMainTableData.length}
  803. page={mainTablePagingController.pageNum}
  804. rowsPerPage={mainTablePagingController.pageSize}
  805. onPageChange={handleMainTablePageChange}
  806. onRowsPerPageChange={handleMainTablePageSizeChange}
  807. rowsPerPageOptions={[10, 25, 50]}
  808. labelRowsPerPage={t("Rows per page")}
  809. labelDisplayedRows={({ from, to, count }) =>
  810. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  811. }
  812. />
  813. </>
  814. );
  815. };
  816. // Add search criteria
  817. const searchCriteria: Criterion<any>[] = useMemo(
  818. () => [
  819. {
  820. label: t("Item Code"),
  821. paramName: "itemCode",
  822. type: "text",
  823. },
  824. {
  825. label: t("Pick Order Code"),
  826. paramName: "pickOrderCode",
  827. type: "text",
  828. },
  829. {
  830. label: t("Item Name"),
  831. paramName: "itemName",
  832. type: "text",
  833. },
  834. {
  835. label: t("Target Date From"),
  836. label2: t("Target Date To"),
  837. paramName: "targetDate",
  838. type: "dateRange",
  839. },
  840. ],
  841. [t],
  842. );
  843. // Add search handler
  844. const handleSearch = useCallback((query: Record<string, any>) => {
  845. setSearchQuery({ ...query });
  846. console.log("Search query:", query);
  847. if (!originalPickOrderData) return;
  848. const filtered = originalPickOrderData.pickOrders.filter((pickOrder) => {
  849. // Check if any line in this pick order matches the search criteria
  850. return pickOrder.pickOrderLines.some((line) => {
  851. const itemCodeMatch = !query.itemCode ||
  852. line.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  853. const itemNameMatch = !query.itemName ||
  854. line.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  855. const pickOrderCodeMatch = !query.pickOrderCode ||
  856. pickOrder.code?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  857. return itemCodeMatch && itemNameMatch && pickOrderCodeMatch ;
  858. });
  859. });
  860. // Create filtered data structure
  861. const filteredData: GetPickOrderInfoResponse = {
  862. ...originalPickOrderData,
  863. pickOrders: filtered
  864. };
  865. setPickOrderDetails(filteredData);
  866. console.log("Filtered pick orders count:", filtered.length);
  867. }, [originalPickOrderData, t]);
  868. // Add reset handler
  869. const handleReset = useCallback(() => {
  870. setSearchQuery({});
  871. if (originalPickOrderData) {
  872. setPickOrderDetails(originalPickOrderData);
  873. }
  874. }, [originalPickOrderData]);
  875. // Add this to debug the lot data
  876. useEffect(() => {
  877. console.log("Lot data:", lotData);
  878. console.log("Pick Qty Data:", pickQtyData);
  879. }, [lotData, pickQtyData]);
  880. return (
  881. <FormProvider {...formProps}>
  882. <Stack spacing={2}>
  883. {/* Search Box */}
  884. <Box>
  885. <SearchBox
  886. criteria={searchCriteria}
  887. onSearch={handleSearch}
  888. onReset={handleReset}
  889. />
  890. </Box>
  891. {/* 主表格 */}
  892. <Box>
  893. <Typography variant="h6" gutterBottom>
  894. {t("Pick Order Details")}
  895. </Typography>
  896. {detailLoading ? (
  897. <Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
  898. <CircularProgress size={40} />
  899. </Box>
  900. ) : pickOrderDetails ? (
  901. <CustomMainTable />
  902. ) : (
  903. <Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
  904. <Typography variant="body2" color="text.secondary">
  905. {t("Loading data...")}
  906. </Typography>
  907. </Box>
  908. )}
  909. </Box>
  910. {/* 批次表格 - 放在主表格下方 */}
  911. {selectedRow && (
  912. <Box>
  913. <Typography variant="h6" gutterBottom>
  914. {t("Item lot to be Pick:")} {selectedRow.pickOrderCode} - {selectedRow.itemName}
  915. </Typography>
  916. {/* 检查是否有可用的批次数据 */}
  917. {lotData.length > 0 ? (
  918. <LotTable
  919. lotData={lotData}
  920. selectedRowId={selectedRowId}
  921. selectedRow={selectedRow}
  922. pickQtyData={pickQtyData}
  923. selectedLotRowId={selectedLotRowId}
  924. selectedLotId={selectedLotId}
  925. onLotSelection={handleLotSelection}
  926. onPickQtyChange={handlePickQtyChange}
  927. onSubmitPickQty={handleSubmitPickQty}
  928. onCreateStockOutLine={handleCreateStockOutLine}
  929. onQcCheck={handleQcCheck}
  930. onDataRefresh={handleFetchAllPickOrderDetails}
  931. onLotDataRefresh={handleLotDataRefresh}
  932. onLotSelectForInput={handleLotSelectForInput}
  933. showInputBody={showInputBody}
  934. setShowInputBody={setShowInputBody}
  935. selectedLotForInput={selectedLotForInput}
  936. generateInputBody={generateInputBody}
  937. />
  938. ) : (
  939. <Box
  940. sx={{
  941. p: 3,
  942. textAlign: 'center',
  943. border: '1px solid',
  944. borderColor: 'divider',
  945. borderRadius: 1,
  946. backgroundColor: 'background.paper'
  947. }}
  948. >
  949. <Typography variant="body1" color="text.secondary" gutterBottom>
  950. {selectedRow.availableQty === null || selectedRow.availableQty === 0
  951. ? t("No available stock for this item")
  952. : t("No lot details available for this item")
  953. }
  954. </Typography>
  955. <Typography variant="body2" color="text.secondary">
  956. {selectedRow.availableQty === null || selectedRow.availableQty === 0
  957. ? t("Current stock is insufficient or unavailable")
  958. : t("Please check inventory status")
  959. }
  960. </Typography>
  961. </Box>
  962. )}
  963. {/* Action buttons below the lot table */}
  964. <Box sx={{ mt: 2 }}>
  965. <Stack direction="row" spacing={1}>
  966. <Button
  967. variant="contained"
  968. onClick={() => handleInsufficientStock()}
  969. sx={{ whiteSpace: 'nowrap' }}
  970. >
  971. {t("Pick Another Lot")}
  972. </Button>
  973. </Stack>
  974. </Box>
  975. </Box>
  976. )}
  977. {/* Action Buttons */}
  978. {selectedRow && (
  979. <Grid container>
  980. <Grid item xs={12} display="flex" justifyContent="end" alignItems="end">
  981. <Button
  982. disabled={(revertIds as number[]).length < 1}
  983. variant="outlined"
  984. onClick={handleConsolidate_revert}
  985. sx={{ mr: 1 }}
  986. >
  987. {t("remove")}
  988. </Button>
  989. <Button
  990. disabled={disableRelease}
  991. variant="contained"
  992. onClick={formProps.handleSubmit(onSubmit, onSubmitError)}
  993. >
  994. {t("release")}
  995. </Button>
  996. </Grid>
  997. </Grid>
  998. )}
  999. {/* QC Modal */}
  1000. {selectedItemForQc && qcModalOpen && (
  1001. <PickQcStockInModalVer2
  1002. open={qcModalOpen}
  1003. onClose={handleCloseQcModal}
  1004. itemDetail={selectedItemForQc}
  1005. setItemDetail={handleSetItemDetail}
  1006. qc={qcItems}
  1007. warehouse={[]}
  1008. qcItems={qcItems}
  1009. setQcItems={setQcItems}
  1010. selectedLotId={selectedLotForQc?.stockOutLineId}
  1011. onStockOutLineUpdate={() => {
  1012. if (selectedRowId) {
  1013. handleRowSelect(selectedRowId);
  1014. }
  1015. }}
  1016. lotData={lotData}
  1017. />
  1018. )}
  1019. </Stack>
  1020. </FormProvider>
  1021. );
  1022. };
  1023. export default PickExecution;