FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

PickExecution.tsx 38 KiB

3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137
  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;