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

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