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

PickExecution.tsx 35 KiB

4ヶ月前
4ヶ月前
3ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2週間前
3ヶ月前
1ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  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. checkAndCompletePickOrderByConsoCode,
  49. } from "@/app/api/pickOrder/actions";
  50. import { EditNote } from "@mui/icons-material";
  51. import { fetchNameList, NameList } from "@/app/api/user/actions";
  52. import {
  53. FormProvider,
  54. SubmitErrorHandler,
  55. SubmitHandler,
  56. useForm,
  57. } from "react-hook-form";
  58. import { OUTPUT_DATE_FORMAT, pickOrderStatusMap } from "@/app/utils/formatUtil";
  59. import { QcItemWithChecks } from "@/app/api/qc";
  60. import { fetchQcItemCheck, fetchPickOrderQcResult } from "@/app/api/qc/actions";
  61. import { PurchaseQcResult } from "@/app/api/po/actions";
  62. //import PickQcStockInModalVer2 from "./PickQcStockInModalVer3";
  63. import { fetchPickOrderLineLotDetails, PickOrderLotDetailResponse } from "@/app/api/pickOrder/actions";
  64. import SearchResults, { Column } from "../SearchResults/SearchResults";
  65. import { defaultPagingController } from "../SearchResults/SearchResults";
  66. import SearchBox, { Criterion } from "../SearchBox";
  67. import dayjs from "dayjs";
  68. import { CreateStockOutLine , NoLotLineDto} from "@/app/api/pickOrder/actions";
  69. import LotTable from './LotTable';
  70. import PickOrderDetailsTable from './PickOrderDetailsTable'; // Import the new component
  71. import { updateInventoryLotLineStatus, updateInventoryStatus, updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  72. import { useSession } from "next-auth/react";
  73. import { SessionWithTokens } from "@/config/authConfig";
  74. interface Props {
  75. filterArgs: Record<string, any>;
  76. }
  77. interface LotPickData {
  78. id: number;
  79. lotId: number | null;
  80. lotNo: string | null;
  81. expiryDate: string;
  82. location: string | null;
  83. stockUnit: string;
  84. inQty: number;
  85. outQty: number;
  86. holdQty: number;
  87. totalPickedByAllPickOrders: number;
  88. availableQty: number;
  89. requiredQty: number;
  90. actualPickQty: number;
  91. lotStatus: string;
  92. noLot?: boolean;
  93. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
  94. stockOutLineId?: number;
  95. stockOutLineStatus?: string;
  96. stockOutLineQty?: number;
  97. }
  98. interface PickQtyData {
  99. [lineId: number]: {
  100. [lotId: number]: number;
  101. };
  102. }
  103. const PickExecution: React.FC<Props> = ({ filterArgs }) => {
  104. const { t } = useTranslation("pickOrder");
  105. const router = useRouter();
  106. const { data: session } = useSession() as { data: SessionWithTokens | null };
  107. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  108. const [filteredPickOrders, setFilteredPickOrders] = useState(
  109. [] as ConsoPickOrderResult[],
  110. );
  111. const [isLoading, setIsLoading] = useState(false);
  112. const [selectedConsoCode, setSelectedConsoCode] = useState<string | undefined>();
  113. const [revertIds, setRevertIds] = useState<GridInputRowSelectionModel>([]);
  114. const [totalCount, setTotalCount] = useState<number>();
  115. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  116. const [byPickOrderRows, setByPickOrderRows] = useState<
  117. Omit<PickOrderResult, "items">[] | undefined
  118. >(undefined);
  119. const [byItemsRows, setByItemsRows] = useState<ByItemsSummary[] | undefined>(
  120. undefined,
  121. );
  122. const [disableRelease, setDisableRelease] = useState<boolean>(true);
  123. const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
  124. const [pickOrderDetails, setPickOrderDetails] = useState<GetPickOrderInfoResponse | null>(null);
  125. const [detailLoading, setDetailLoading] = useState(false);
  126. const [pickQtyData, setPickQtyData] = useState<PickQtyData>({});
  127. const [lotData, setLotData] = useState<LotPickData[]>([]);
  128. const [qcItems, setQcItems] = useState<QcItemWithChecks[]>([]);
  129. const [qcModalOpen, setQcModalOpen] = useState(false);
  130. const [selectedItemForQc, setSelectedItemForQc] = useState<GetPickOrderLineInfo & {
  131. pickOrderCode: string;
  132. qcResult?: PurchaseQcResult[];
  133. } | null>(null);
  134. const [selectedLotForQc, setSelectedLotForQc] = useState<LotPickData | null>(null);
  135. const [selectedLotRowId, setSelectedLotRowId] = useState<string | null>(null);
  136. const [selectedLotId, setSelectedLotId] = useState<number | null>(null);
  137. // Keep only the main table paging controller
  138. const [mainTablePagingController, setMainTablePagingController] = useState({
  139. pageNum: 0,
  140. pageSize: 10,
  141. });
  142. const [lotTablePagingController, setLotTablePagingController] = useState({
  143. pageNum: 0,
  144. pageSize: 10,
  145. });
  146. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  147. const [originalPickOrderData, setOriginalPickOrderData] = useState<GetPickOrderInfoResponse | null>(null);
  148. const formProps = useForm<ReleasePickOrderInputs>();
  149. const errors = formProps.formState.errors;
  150. const onDetailClick = useCallback(
  151. (pickOrder: any) => {
  152. console.log(pickOrder);
  153. const status = pickOrder.status;
  154. if (pickOrderStatusMap[status] >= 3) {
  155. router.push(`/pickOrder/detail?consoCode=${pickOrder.consoCode}`);
  156. } else {
  157. setSelectedConsoCode(pickOrder.consoCode);
  158. }
  159. },
  160. [router],
  161. );
  162. const fetchNewPageConsoPickOrder = useCallback(
  163. async (
  164. pagingController: Record<string, number>,
  165. filterArgs: Record<string, number>,
  166. ) => {
  167. setIsLoading(true);
  168. const params = {
  169. ...pagingController,
  170. ...filterArgs,
  171. };
  172. const res = await fetchConsoPickOrderClient(params);
  173. if (res) {
  174. console.log(res);
  175. setFilteredPickOrders(res.records);
  176. setTotalCount(res.total);
  177. }
  178. setIsLoading(false);
  179. },
  180. [],
  181. );
  182. useEffect(() => {
  183. fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs);
  184. }, [fetchNewPageConsoPickOrder, filterArgs]);
  185. const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => {
  186. let isReleasable = true;
  187. for (const item of itemList) {
  188. isReleasable = item.requiredQty >= item.availableQty;
  189. if (!isReleasable) return isReleasable;
  190. }
  191. return isReleasable;
  192. }, []);
  193. const fetchConso = useCallback(
  194. async (consoCode: string) => {
  195. const res = await fetchConsoDetail(consoCode);
  196. const nameListRes = await fetchNameList();
  197. if (res) {
  198. console.log(res);
  199. setByPickOrderRows(res.pickOrders);
  200. setByItemsRows(res.items);
  201. setDisableRelease(isReleasable(res.items));
  202. } else {
  203. console.log("error");
  204. console.log(res);
  205. }
  206. if (nameListRes) {
  207. console.log(nameListRes);
  208. setUsernameList(nameListRes);
  209. }
  210. },
  211. [isReleasable],
  212. );
  213. const handleFetchAllPickOrderDetails = useCallback(async () => {
  214. setDetailLoading(true);
  215. try {
  216. const data = await fetchAllPickOrderDetails(currentUserId);
  217. console.log("All Pick Order Details for user:", currentUserId, data);
  218. if (data && data.pickOrders) {
  219. setPickOrderDetails(data);
  220. setOriginalPickOrderData(data);
  221. const initialPickQtyData: PickQtyData = {};
  222. data.pickOrders.forEach((pickOrder: any) => {
  223. pickOrder.pickOrderLines.forEach((line: any) => {
  224. initialPickQtyData[line.id] = {};
  225. });
  226. });
  227. setPickQtyData(initialPickQtyData);
  228. } else {
  229. console.log("No pick order data returned");
  230. setPickOrderDetails({
  231. consoCode: null,
  232. pickOrders: [],
  233. items: []
  234. });
  235. setOriginalPickOrderData({
  236. consoCode: null,
  237. pickOrders: [],
  238. items: []
  239. });
  240. setPickQtyData({});
  241. }
  242. } catch (error) {
  243. console.error("Error fetching all pick order details:", error);
  244. setPickOrderDetails({
  245. consoCode: null,
  246. pickOrders: [],
  247. items: []
  248. });
  249. setOriginalPickOrderData({
  250. consoCode: null,
  251. pickOrders: [],
  252. items: []
  253. });
  254. setPickQtyData({});
  255. } finally {
  256. setDetailLoading(false);
  257. }
  258. }, [currentUserId]);
  259. useEffect(() => {
  260. handleFetchAllPickOrderDetails();
  261. }, [handleFetchAllPickOrderDetails]);
  262. const onChange = useCallback(
  263. (event: React.SyntheticEvent, newValue: NameList) => {
  264. console.log(newValue);
  265. formProps.setValue("assignTo", newValue.id);
  266. },
  267. [formProps],
  268. );
  269. const onSubmit = useCallback<SubmitHandler<ReleasePickOrderInputs>>(
  270. async (data, event) => {
  271. console.log(data);
  272. try {
  273. const res = await releasePickOrder(data);
  274. console.log(res);
  275. if (res.consoCode.length > 0) {
  276. console.log(res);
  277. router.push(`/pickOrder/detail?consoCode=${res.consoCode}`);
  278. } else {
  279. console.log(res);
  280. }
  281. } catch (error) {
  282. console.log(error);
  283. }
  284. },
  285. [router],
  286. );
  287. const onSubmitError = useCallback<SubmitErrorHandler<ReleasePickOrderInputs>>(
  288. (errors) => {},
  289. [],
  290. );
  291. const handleConsolidate_revert = useCallback(() => {
  292. console.log(revertIds);
  293. }, [revertIds]);
  294. useEffect(() => {
  295. if (selectedConsoCode) {
  296. fetchConso(selectedConsoCode);
  297. formProps.setValue("consoCode", selectedConsoCode);
  298. }
  299. }, [selectedConsoCode, fetchConso, formProps]);
  300. const handlePickQtyChange = useCallback((lineId: number, lotId: number, value: number | string) => {
  301. console.log("Changing pick qty:", { lineId, lotId, value });
  302. const numericValue = typeof value === 'string' ? (value === '' ? 0 : parseInt(value, 10)) : value;
  303. setPickQtyData(prev => {
  304. const newData = {
  305. ...prev,
  306. [lineId]: {
  307. ...prev[lineId],
  308. [lotId]: numericValue
  309. }
  310. };
  311. console.log("New pick qty data:", newData);
  312. return newData;
  313. });
  314. }, []);
  315. const handleSubmitPickQty = useCallback(async (lineId: number, lotId: number) => {
  316. const qty = pickQtyData[lineId]?.[lotId] || 0;
  317. console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`);
  318. const selectedLot = lotData.find(lot => lot.lotId === lotId);
  319. if (!selectedLot?.stockOutLineId) {
  320. return;
  321. }
  322. try {
  323. // FIXED: 计算累计拣货数量
  324. const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty;
  325. console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0);
  326. console.log("🔍 DEBUG - Current submit:", qty);
  327. console.log("🔍 DEBUG - Total picked:", totalPickedForThisLot);
  328. console.log("�� DEBUG - Required qty:", selectedLot.requiredQty);
  329. // FIXED: 状态应该基于累计拣货数量
  330. let newStatus = 'partially_completed';
  331. if (totalPickedForThisLot >= selectedLot.requiredQty) {
  332. newStatus = 'completed';
  333. }
  334. console.log("�� DEBUG - Calculated status:", newStatus);
  335. try {
  336. const stockOutLineUpdate = await updateStockOutLineStatus({
  337. id: selectedLot.stockOutLineId,
  338. status: newStatus,
  339. qty: qty
  340. });
  341. console.log(" Stock out line updated:", stockOutLineUpdate);
  342. } catch (error) {
  343. console.error("❌ Error updating stock out line:", error);
  344. return;
  345. }
  346. if (qty > 0) {
  347. const inventoryLotLineUpdate = await updateInventoryLotLineQuantities({
  348. inventoryLotLineId: lotId,
  349. qty: qty,
  350. status: 'available',
  351. operation: 'pick'
  352. });
  353. console.log("Inventory lot line updated:", inventoryLotLineUpdate);
  354. }
  355. // RE-ENABLE: Check if pick order should be completed
  356. if (newStatus === 'completed') {
  357. console.log(" Stock out line completed, checking if entire pick order is complete...");
  358. // 添加调试日志来查看所有 pick orders 的 consoCode
  359. console.log("📋 DEBUG - All pick orders and their consoCodes:");
  360. if (pickOrderDetails) {
  361. pickOrderDetails.pickOrders.forEach((pickOrder, index) => {
  362. console.log(` Pick Order ${index + 1}: ID=${pickOrder.id}, Code=${pickOrder.code}, ConsoCode=${pickOrder.consoCode}`);
  363. });
  364. }
  365. // FIXED: 直接查找 consoCode,不依赖 selectedRow
  366. if (pickOrderDetails) {
  367. let currentConsoCode: string | null = null;
  368. // 找到当前选中行所属的 pick order
  369. for (const pickOrder of pickOrderDetails.pickOrders) {
  370. const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
  371. if (foundLine) {
  372. // 直接使用 pickOrder.code 作为 consoCode
  373. currentConsoCode = pickOrder.consoCode;
  374. console.log(`�� DEBUG - Found consoCode for line ${selectedRowId}: ${currentConsoCode} (from pick order ${pickOrder.id})`);
  375. break;
  376. }
  377. }
  378. if (currentConsoCode) {
  379. try {
  380. console.log(`🔍 Checking completion for consoCode: ${currentConsoCode}`);
  381. const completionResponse = await checkAndCompletePickOrderByConsoCode(currentConsoCode);
  382. console.log("�� Completion response:", completionResponse);
  383. if (completionResponse.message === "completed") {
  384. console.log("🎉 Pick order completed successfully!");
  385. await handleFetchAllPickOrderDetails();
  386. // 刷新当前选中的行数据
  387. if (selectedRowId) {
  388. await handleRowSelect(selectedRowId, true);
  389. }
  390. } else if (completionResponse.message === "not completed") {
  391. console.log("⏳ Pick order not completed yet, more lines remaining");
  392. } else {
  393. console.error("❌ Error checking completion:", completionResponse.message);
  394. }
  395. } catch (error) {
  396. console.error("❌ Error checking pick order completion:", error);
  397. }
  398. } else {
  399. console.warn("⚠️ No consoCode found for current pick order, cannot check completion");
  400. }
  401. }
  402. }
  403. console.log("All updates completed successfully");
  404. if (selectedRowId) {
  405. await handleRowSelect(selectedRowId, true);
  406. console.log("Data refresh needed but handleRowSelect not available yet");
  407. }
  408. await handleFetchAllPickOrderDetails();
  409. } catch (error) {
  410. console.error("Error updating pick quantity:", error);
  411. }
  412. }, [pickQtyData, lotData, selectedRowId, pickOrderDetails, handleFetchAllPickOrderDetails]);
  413. const getTotalPickedQty = useCallback((lineId: number) => {
  414. const lineData = pickQtyData[lineId];
  415. if (!lineData) return 0;
  416. return Object.values(lineData).reduce((sum, qty) => sum + qty, 0);
  417. }, [pickQtyData]);
  418. const handleQcCheck = useCallback(async (line: GetPickOrderLineInfo, pickOrderCode: string) => {
  419. if (!selectedLotId) {
  420. return;
  421. }
  422. const selectedLot = lotData.find(lot => lot.lotId === selectedLotId);
  423. if (!selectedLot) {
  424. return;
  425. }
  426. if (!selectedLot.stockOutLineId) {
  427. return;
  428. }
  429. setSelectedLotForQc(selectedLot);
  430. let qcResult: any[] = [];
  431. try {
  432. const rawQcResult = await fetchPickOrderQcResult(line.id);
  433. qcResult = rawQcResult.map((result: any) => ({
  434. ...result,
  435. isPassed: result.isPassed || false
  436. }));
  437. } catch (error) {
  438. // No existing QC result found - this is normal
  439. }
  440. setSelectedItemForQc({
  441. ...line,
  442. pickOrderCode,
  443. qcResult
  444. });
  445. setQcModalOpen(true);
  446. }, [lotData, selectedLotId, setQcItems]);
  447. const handleCloseQcModal = useCallback(() => {
  448. console.log("Closing QC modal");
  449. setQcModalOpen(false);
  450. setSelectedItemForQc(null);
  451. }, []);
  452. const handleSetItemDetail = useCallback((item: any) => {
  453. setSelectedItemForQc(item);
  454. }, []);
  455. // Main table pagination handlers
  456. const handleMainTablePageChange = useCallback((event: unknown, newPage: number) => {
  457. setMainTablePagingController(prev => ({
  458. ...prev,
  459. pageNum: newPage,
  460. }));
  461. }, []);
  462. const handleMainTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  463. const newPageSize = parseInt(event.target.value, 10);
  464. setMainTablePagingController({
  465. pageNum: 0,
  466. pageSize: newPageSize,
  467. });
  468. }, []);
  469. const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => {
  470. setLotTablePagingController(prev => ({
  471. ...prev,
  472. pageNum: newPage,
  473. }));
  474. }, []);
  475. const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  476. const newPageSize = parseInt(event.target.value, 10);
  477. setLotTablePagingController({
  478. pageNum: 0,
  479. pageSize: newPageSize,
  480. });
  481. }, []);
  482. const handleLotSelection = useCallback((uniqueLotId: string, lotId: number | null) => {
  483. console.log("=== DEBUG: Lot Selection ===");
  484. console.log("uniqueLotId:", uniqueLotId);
  485. console.log("lotId (inventory lot line ID):", lotId);
  486. const selectedLot = lotData.find(lot => lot.lotId === lotId);
  487. console.log("Selected lot data:", selectedLot);
  488. if (selectedLotRowId === uniqueLotId) {
  489. setSelectedLotRowId(null);
  490. setSelectedLotId(null);
  491. } else {
  492. setSelectedLotRowId(uniqueLotId);
  493. setSelectedLotId(lotId ?? null);
  494. }
  495. }, [selectedLotRowId, lotData]);
  496. const handleRowSelect = useCallback(async (lineId: number, preserveLotSelection: boolean = false) => {
  497. setSelectedRowId(lineId);
  498. if (!preserveLotSelection) {
  499. setSelectedLotRowId(null);
  500. setSelectedLotId(null);
  501. }
  502. try {
  503. const lotDetails = await fetchPickOrderLineLotDetails(lineId);
  504. console.log("lineId:", lineId);
  505. console.log("Lot details from API:", lotDetails);
  506. // ✅ 直接使用 API 返回的数据,包括 noLot 字段
  507. const allLotData: LotPickData[] = lotDetails.map((lot: PickOrderLotDetailResponse) => ({
  508. id: lot.lotId ?? -(lineId * 1000 + Math.random()), // 如果 lotId 为 null,生成一个临时 ID
  509. lotId: lot.lotId,
  510. lotNo: lot.lotNo,
  511. expiryDate: lot.expiryDate ? dayjs(lot.expiryDate).format(OUTPUT_DATE_FORMAT) : t("N/A"),
  512. location: lot.location?? t("N/A"),
  513. stockUnit: lot.stockUnit || "",
  514. inQty: lot.inQty ?? 0,
  515. outQty: lot.outQty ?? 0,
  516. holdQty: lot.holdQty ?? 0,
  517. totalPickedByAllPickOrders: lot.totalPickedByAllPickOrders ?? 0,
  518. availableQty: lot.availableQty ?? 0,
  519. requiredQty: lot.requiredQty ?? 0,
  520. actualPickQty: lot.actualPickQty || 0,
  521. lotStatus: lot.lotStatus || "unavailable",
  522. lotAvailability: lot.lotAvailability,
  523. stockOutLineId: lot.stockOutLineId ?? undefined,
  524. stockOutLineStatus: lot.stockOutLineStatus ?? undefined,
  525. stockOutLineQty: lot.stockOutLineQty ?? undefined,
  526. noLot: lot.noLot, // ✅ 关键:使用 API 返回的 noLot 字段
  527. }));
  528. console.log("✅ Combined lot data:", allLotData);
  529. console.log(" - Total rows:", allLotData.length);
  530. console.log(" - No-lot rows:", allLotData.filter(l => l.noLot).length);
  531. setLotData(allLotData);
  532. } catch (error) {
  533. console.error("Error fetching lot details:", error);
  534. setLotData([]);
  535. }
  536. }, []);
  537. const prepareLotTableData = useMemo(() => {
  538. return lotData.map((lot) => ({
  539. ...lot,
  540. id: lot.lotId,
  541. }));
  542. }, [lotData]);
  543. const paginatedLotTableData = useMemo(() => {
  544. const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize;
  545. const endIndex = startIndex + lotTablePagingController.pageSize;
  546. return prepareLotTableData.slice(startIndex, endIndex);
  547. }, [prepareLotTableData, lotTablePagingController]);
  548. const selectedRow = useMemo(() => {
  549. if (!selectedRowId || !pickOrderDetails) return null;
  550. for (const pickOrder of pickOrderDetails.pickOrders) {
  551. const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
  552. if (foundLine) {
  553. return { ...foundLine, pickOrderCode: pickOrder.code,
  554. pickOrderId: pickOrder.id };
  555. }
  556. }
  557. return null;
  558. }, [selectedRowId, pickOrderDetails]);
  559. const handleInventoryUpdate = useCallback(async (itemId: number, lotId: number, qty: number) => {
  560. try {
  561. const inventoryUpdate = await updateInventoryStatus({
  562. itemId: itemId,
  563. lotId: lotId,
  564. status: 'reserved',
  565. qty: qty
  566. });
  567. console.log("Inventory status updated:", inventoryUpdate);
  568. } catch (error) {
  569. console.error("Error updating inventory status:", error);
  570. }
  571. }, []);
  572. const handleLotDataRefresh = useCallback(async () => {
  573. if (selectedRowId) {
  574. try {
  575. await handleRowSelect(selectedRowId, true);
  576. } catch (error) {
  577. console.error("Error refreshing lot data:", error);
  578. }
  579. }
  580. }, [selectedRowId, handleRowSelect]);
  581. const handleDataRefresh = useCallback(async () => {
  582. if (selectedRowId) {
  583. try {
  584. await handleRowSelect(selectedRowId, true);
  585. } catch (error) {
  586. console.error("Error refreshing data:", error);
  587. }
  588. }
  589. }, [selectedRowId, handleRowSelect]);
  590. const handleInsufficientStock = useCallback(async () => {
  591. console.log("Insufficient stock - testing resuggest API");
  592. if (!selectedRowId || !pickOrderDetails) {
  593. return;
  594. }
  595. let pickOrderId: number | null = null;
  596. for (const pickOrder of pickOrderDetails.pickOrders) {
  597. const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
  598. if (foundLine) {
  599. pickOrderId = pickOrder.id;
  600. break;
  601. }
  602. }
  603. if (!pickOrderId) {
  604. return;
  605. }
  606. try {
  607. console.log(`Calling resuggest API for pick order ID: ${pickOrderId}`);
  608. const result = await resuggestPickOrder(pickOrderId);
  609. console.log("Resuggest API result:", result);
  610. if (result.code === "SUCCESS") {
  611. if (selectedRowId) {
  612. await handleRowSelect(selectedRowId);
  613. }
  614. await handleFetchAllPickOrderDetails();
  615. }
  616. } catch (error) {
  617. console.error("Error calling resuggest API:", error);
  618. }
  619. }, [selectedRowId, pickOrderDetails, handleRowSelect, handleFetchAllPickOrderDetails]);
  620. const hasSelectedLots = useCallback((lineId: number) => {
  621. return selectedLotRowId !== null;
  622. }, [selectedLotRowId]);
  623. const [showInputBody, setShowInputBody] = useState(false);
  624. const [selectedLotForInput, setSelectedLotForInput] = useState<LotPickData | null>(null);
  625. const handleLotSelectForInput = useCallback((lot: LotPickData) => {
  626. setSelectedLotForInput(lot);
  627. setShowInputBody(true);
  628. }, []);
  629. const generateInputBody = useCallback(() => {
  630. if (!selectedLotForInput || !selectedRowId || !pickOrderDetails?.consoCode) {
  631. return null;
  632. }
  633. // ✅ 處理 lotId 可能為 null 的情況
  634. if (selectedLotForInput.lotId === null) {
  635. return null; // no-lot 行不能創建 stock out line
  636. }
  637. return {
  638. consoCode: pickOrderDetails.consoCode,
  639. pickOrderLineId: selectedRowId,
  640. inventoryLotLineId: selectedLotForInput.lotId, // ✅ 現在確定不是 null
  641. qty: 0.0
  642. };
  643. }, [selectedLotForInput, selectedRowId, selectedRow, pickOrderDetails?.consoCode]);
  644. // 在 handleSubmitPickQty 函数附近添加新函数
  645. const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) => {
  646. if (!stockOutLineId) {
  647. console.error("Cannot issue: stockOutLineId is missing");
  648. return;
  649. }
  650. try {
  651. console.log(`提交 no-lot stock out line: ${stockOutLineId}`);
  652. // ✅ 直接完成 no-lot 的 stock out line(设置状态为 completed,qty 为 0)
  653. const result = await updateStockOutLineStatus({
  654. id: stockOutLineId,
  655. status: 'completed',
  656. qty: 0 // no-lot 行没有实际数量
  657. });
  658. console.log("✅ No-lot stock out line completed:", result);
  659. // 刷新数据
  660. if (selectedRowId) {
  661. handleRowSelect(selectedRowId);
  662. }
  663. } catch (error) {
  664. console.error("❌ Error completing no-lot stock out line:", error);
  665. }
  666. }, [selectedRowId, handleRowSelect]);
  667. const handleCreateStockOutLine = useCallback(async (inventoryLotLineId: number) => {
  668. if (!selectedRowId || !pickOrderDetails?.consoCode) {
  669. console.error("Missing required data for creating stock out line.");
  670. return;
  671. }
  672. try {
  673. const currentSelectedLotRowId = selectedLotRowId;
  674. const currentSelectedLotId = selectedLotId;
  675. let correctConsoCode: string | null = null;
  676. if (pickOrderDetails && selectedRowId) {
  677. for (const pickOrder of pickOrderDetails.pickOrders) {
  678. const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
  679. if (foundLine) {
  680. correctConsoCode = pickOrder.consoCode;
  681. console.log(`🔍 Found consoCode for line ${selectedRowId}: ${correctConsoCode} (from pick order ${pickOrder.id})`);
  682. break;
  683. }
  684. }
  685. }
  686. const stockOutLineData: CreateStockOutLine = {
  687. consoCode: correctConsoCode || pickOrderDetails?.consoCode || "", // 使用正确的 consoCode
  688. pickOrderLineId: selectedRowId,
  689. inventoryLotLineId: inventoryLotLineId,
  690. qty: 0.0
  691. };
  692. console.log("=== STOCK OUT LINE CREATION DEBUG ===");
  693. console.log("Input Body:", JSON.stringify(stockOutLineData, null, 2));
  694. const result = await createStockOutLine(stockOutLineData);
  695. console.log("Stock Out Line created:", result);
  696. if (result) {
  697. console.log("Stock out line created successfully:", result);
  698. console.log("🔄 Refreshing data after stock out line creation...");
  699. try {
  700. if (selectedRowId) {
  701. await handleRowSelect(selectedRowId, true);
  702. }
  703. await handleFetchAllPickOrderDetails();
  704. console.log(" Data refresh completed - lot selection maintained!");
  705. } catch (refreshError) {
  706. console.error("❌ Error refreshing data:", refreshError);
  707. }
  708. setShowInputBody(false);
  709. } else {
  710. console.error("Failed to create stock out line: No response");
  711. }
  712. } catch (error) {
  713. console.error("Error creating stock out line:", error);
  714. }
  715. }, [selectedRowId, pickOrderDetails?.consoCode, handleRowSelect, handleFetchAllPickOrderDetails, selectedLotRowId, selectedLotId]);
  716. const handleRefreshDataPreserveSelection = useCallback(async () => {
  717. if (!selectedRowId) return;
  718. const currentSelectedLotRowId = selectedLotRowId;
  719. const currentSelectedLotId = selectedLotId;
  720. try {
  721. await handleRowSelect(selectedRowId, true);
  722. await handleFetchAllPickOrderDetails();
  723. setSelectedLotRowId(currentSelectedLotRowId);
  724. setSelectedLotId(currentSelectedLotId);
  725. console.log(" Data refreshed with selection preserved");
  726. } catch (error) {
  727. console.error("❌ Error refreshing data:", error);
  728. }
  729. }, [selectedRowId, selectedLotRowId, selectedLotId, handleRowSelect, handleFetchAllPickOrderDetails]);
  730. // Search criteria
  731. const searchCriteria: Criterion<any>[] = useMemo(
  732. () => [
  733. {
  734. label: t("Item Code"),
  735. paramName: "itemCode",
  736. type: "text",
  737. },
  738. {
  739. label: t("Pick Order Code"),
  740. paramName: "pickOrderCode",
  741. type: "text",
  742. },
  743. {
  744. label: t("Item Name"),
  745. paramName: "itemName",
  746. type: "text",
  747. },
  748. {
  749. label: t("Target Date From"),
  750. label2: t("Target Date To"),
  751. paramName: "targetDate",
  752. type: "dateRange",
  753. },
  754. ],
  755. [t],
  756. );
  757. // Search handler
  758. const handleSearch = useCallback((query: Record<string, any>) => {
  759. setSearchQuery({ ...query });
  760. console.log("Search query:", query);
  761. if (!originalPickOrderData) return;
  762. const filtered = originalPickOrderData.pickOrders.filter((pickOrder) => {
  763. return pickOrder.pickOrderLines.some((line) => {
  764. const itemCodeMatch = !query.itemCode ||
  765. line.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  766. const itemNameMatch = !query.itemName ||
  767. line.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  768. const pickOrderCodeMatch = !query.pickOrderCode ||
  769. pickOrder.code?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  770. return itemCodeMatch && itemNameMatch && pickOrderCodeMatch;
  771. });
  772. });
  773. const filteredData: GetPickOrderInfoResponse = {
  774. ...originalPickOrderData,
  775. pickOrders: filtered
  776. };
  777. setPickOrderDetails(filteredData);
  778. console.log("Filtered pick orders count:", filtered.length);
  779. }, [originalPickOrderData, t]);
  780. // Reset handler
  781. const handleReset = useCallback(() => {
  782. setSearchQuery({});
  783. if (originalPickOrderData) {
  784. setPickOrderDetails(originalPickOrderData);
  785. }
  786. }, [originalPickOrderData]);
  787. // Debug the lot data
  788. useEffect(() => {
  789. console.log("Lot data:", lotData);
  790. console.log("Pick Qty Data:", pickQtyData);
  791. }, [lotData, pickQtyData]);
  792. return (
  793. <FormProvider {...formProps}>
  794. <Stack spacing={2}>
  795. {/* Search Box */}
  796. <Box>
  797. <SearchBox
  798. criteria={searchCriteria}
  799. onSearch={handleSearch}
  800. onReset={handleReset}
  801. />
  802. </Box>
  803. {/* Main table using the new component */}
  804. <Box>
  805. <Typography variant="h6" gutterBottom>
  806. {t("Pick Order Details")}
  807. </Typography>
  808. <PickOrderDetailsTable
  809. pickOrderDetails={pickOrderDetails}
  810. detailLoading={detailLoading}
  811. selectedRowId={selectedRowId}
  812. onRowSelect={handleRowSelect}
  813. onPageChange={handleMainTablePageChange}
  814. onPageSizeChange={handleMainTablePageSizeChange}
  815. pageNum={mainTablePagingController.pageNum}
  816. pageSize={mainTablePagingController.pageSize}
  817. />
  818. </Box>
  819. {/* Lot table - below main table */}
  820. {selectedRow && (
  821. <Box>
  822. <Typography variant="h6" gutterBottom>
  823. {t("Item lot to be Pick:")} {selectedRow.pickOrderCode} - {selectedRow.itemName}
  824. </Typography>
  825. {lotData.length > 0 ? (
  826. <LotTable
  827. lotData={lotData}
  828. selectedRowId={selectedRowId}
  829. selectedRow={selectedRow}
  830. pickQtyData={pickQtyData}
  831. selectedLotRowId={selectedLotRowId}
  832. selectedLotId={selectedLotId}
  833. onLotSelection={handleLotSelection}
  834. onPickQtyChange={handlePickQtyChange}
  835. onSubmitPickQty={handleSubmitPickQty}
  836. onCreateStockOutLine={handleCreateStockOutLine}
  837. onQcCheck={handleQcCheck}
  838. onDataRefresh={handleFetchAllPickOrderDetails}
  839. onLotDataRefresh={handleLotDataRefresh}
  840. onLotSelectForInput={handleLotSelectForInput}
  841. showInputBody={showInputBody}
  842. onIssueNoLotStockOutLine={handleIssueNoLotStockOutLine}
  843. setShowInputBody={setShowInputBody}
  844. //selectedLotForInput={selectedLotForInput}
  845. generateInputBody={generateInputBody}
  846. // Add missing props
  847. totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed
  848. outQty={0} // You can calculate this from lotData if needed
  849. holdQty={0} // You can calculate this from lotData if needed
  850. />
  851. ) : (
  852. <Box
  853. sx={{
  854. p: 3,
  855. textAlign: 'center',
  856. border: '1px solid',
  857. borderColor: 'divider',
  858. borderRadius: 1,
  859. backgroundColor: 'background.paper'
  860. }}
  861. >
  862. <Typography variant="body1" color="text.secondary" gutterBottom>
  863. {selectedRow.availableQty === null || selectedRow.availableQty === 0
  864. ? t("No available stock for this item")
  865. : t("No lot details available for this item")
  866. }
  867. </Typography>
  868. <Typography variant="body2" color="text.secondary">
  869. {selectedRow.availableQty === null || selectedRow.availableQty === 0
  870. ? t("Current stock is insufficient or unavailable")
  871. : t("Please check inventory status")
  872. }
  873. </Typography>
  874. </Box>
  875. )}
  876. {/* Action buttons below the lot table */}
  877. {/*
  878. <Box sx={{ mt: 2 }}>
  879. <Stack direction="row" spacing={1}>
  880. <Button
  881. variant="contained"
  882. onClick={() => handleInsufficientStock()}
  883. sx={{ whiteSpace: 'nowrap' }}
  884. >
  885. {t("Pick Another Lot")}
  886. </Button>
  887. </Stack>
  888. </Box>
  889. */}
  890. </Box>
  891. )}
  892. {/* Action Buttons */}
  893. {selectedRow && (
  894. <Grid container>
  895. <Grid item xs={12} display="flex" justifyContent="end" alignItems="end">
  896. <Button
  897. disabled={(revertIds as number[]).length < 1}
  898. variant="outlined"
  899. onClick={handleConsolidate_revert}
  900. sx={{ mr: 1 }}
  901. >
  902. {t("remove")}
  903. </Button>
  904. <Button
  905. disabled={disableRelease}
  906. variant="contained"
  907. onClick={formProps.handleSubmit(onSubmit, onSubmitError)}
  908. >
  909. {t("release")}
  910. </Button>
  911. </Grid>
  912. </Grid>
  913. )}
  914. {/* QC Modal */}
  915. { /*
  916. {selectedItemForQc && qcModalOpen && (
  917. <PickQcStockInModalVer2
  918. open={qcModalOpen}
  919. onClose={handleCloseQcModal}
  920. itemDetail={selectedItemForQc}
  921. setItemDetail={handleSetItemDetail}
  922. qc={qcItems}
  923. warehouse={[]}
  924. qcItems={qcItems}
  925. setQcItems={setQcItems}
  926. selectedLotId={selectedLotForQc?.stockOutLineId}
  927. onStockOutLineUpdate={() => {
  928. if (selectedRowId) {
  929. handleRowSelect(selectedRowId);
  930. }
  931. }}
  932. lotData={lotData}
  933. />
  934. )}
  935. */}
  936. </Stack>
  937. </FormProvider>
  938. );
  939. };
  940. export default PickExecution;