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.
 
 

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