FPSMS-frontend
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 

2005 rader
70 KiB

  1. "use client";
  2. import { createPickOrder, SavePickOrderRequest, SavePickOrderLineRequest, getLatestGroupNameAndCreate, createOrUpdateGroups } from "@/app/api/pickOrder/actions";
  3. import {
  4. Autocomplete,
  5. Box,
  6. Button,
  7. FormControl,
  8. Grid,
  9. Stack,
  10. TextField,
  11. Typography,
  12. Checkbox,
  13. Table,
  14. TableBody,
  15. TableCell,
  16. TableContainer,
  17. TableHead,
  18. TableRow,
  19. Paper,
  20. Select,
  21. MenuItem,
  22. Modal,
  23. Card,
  24. CardContent,
  25. TablePagination,
  26. } from "@mui/material";
  27. import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
  28. import { useTranslation } from "react-i18next";
  29. import { useCallback, useEffect, useMemo, useState } from "react";
  30. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  31. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  32. import dayjs from "dayjs";
  33. import { Check, Search, RestartAlt } from "@mui/icons-material";
  34. import { ItemCombo, fetchAllItemsInClient } from "@/app/api/settings/item/actions";
  35. import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  36. import SearchResults, { Column } from "../SearchResults/SearchResults";
  37. import { fetchJobOrderDetailByCode } from "@/app/api/jo/actions";
  38. import SearchBox, { Criterion } from "../SearchBox";
  39. import VerticalSearchBox from "./VerticalSearchBox";
  40. import SearchResultsTable from './SearchResultsTable';
  41. import CreatedItemsTable from './CreatedItemsTable';
  42. type Props = {
  43. filterArgs?: Record<string, any>;
  44. searchQuery?: Record<string, any>;
  45. onPickOrderCreated?: () => void; // 添加回调函数
  46. };
  47. // 扩展表单类型以包含搜索字段
  48. interface SearchFormData extends SavePickOrderRequest {
  49. searchCode?: string;
  50. searchName?: string;
  51. }
  52. // Update the CreatedItem interface to allow null values for groupId
  53. interface CreatedItem {
  54. itemId: number;
  55. itemName: string;
  56. itemCode: string;
  57. qty: number;
  58. uom: string;
  59. uomId: number;
  60. uomDesc: string;
  61. isSelected: boolean;
  62. currentStockBalance?: number;
  63. targetDate?: string | null; // Make it optional to match the source
  64. groupId?: number | null; // Allow null values
  65. }
  66. // Add interface for search items with quantity
  67. interface SearchItemWithQty extends ItemCombo {
  68. qty: number | null; // Changed from number to number | null
  69. jobOrderCode?: string;
  70. jobOrderId?: number;
  71. currentStockBalance?: number;
  72. targetDate?: string | null; // Allow null values
  73. groupId?: number | null; // Allow null values
  74. }
  75. interface JobOrderDetailPickLine {
  76. id: number;
  77. code: string;
  78. name: string;
  79. lotNo: string | null;
  80. reqQty: number;
  81. uom: string;
  82. status: string;
  83. }
  84. // 添加组相关的接口
  85. interface Group {
  86. id: number;
  87. name: string;
  88. targetDate: string ;
  89. }
  90. // Move the counter outside the component to persist across re-renders
  91. let checkboxChangeCallCount = 0;
  92. let processingItems = new Set<number>();
  93. const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCreated }) => {
  94. const { t } = useTranslation("pickOrder");
  95. const [items, setItems] = useState<ItemCombo[]>([]);
  96. const [filteredItems, setFilteredItems] = useState<SearchItemWithQty[]>([]);
  97. const [createdItems, setCreatedItems] = useState<CreatedItem[]>([]);
  98. const [isLoading, setIsLoading] = useState(false);
  99. const [hasSearched, setHasSearched] = useState(false);
  100. // 添加组相关的状态 - 只声明一次
  101. const [groups, setGroups] = useState<Group[]>([]);
  102. const [selectedGroup, setSelectedGroup] = useState<Group | null>(null);
  103. const [nextGroupNumber, setNextGroupNumber] = useState(1);
  104. // Add state for selected item IDs in search results
  105. const [selectedSearchItemIds, setSelectedSearchItemIds] = useState<(string | number)[]>([]);
  106. // Add state for second search
  107. const [secondSearchQuery, setSecondSearchQuery] = useState<Record<string, any>>({});
  108. const [secondSearchResults, setSecondSearchResults] = useState<SearchItemWithQty[]>([]);
  109. const [isLoadingSecondSearch, setIsLoadingSecondSearch] = useState(false);
  110. const [hasSearchedSecond, setHasSearchedSecond] = useState(false);
  111. // Add selection state for second search
  112. const [selectedSecondSearchItemIds, setSelectedSecondSearchItemIds] = useState<(string | number)[]>([]);
  113. const formProps = useForm<SearchFormData>();
  114. const errors = formProps.formState.errors;
  115. const targetDate = formProps.watch("targetDate");
  116. const type = formProps.watch("type");
  117. const searchCode = formProps.watch("searchCode");
  118. const searchName = formProps.watch("searchName");
  119. const [jobOrderItems, setJobOrderItems] = useState<JobOrderDetailPickLine[]>([]);
  120. const [isLoadingJobOrder, setIsLoadingJobOrder] = useState(false);
  121. useEffect(() => {
  122. const loadItems = async () => {
  123. try {
  124. const itemsData = await fetchAllItemsInClient();
  125. console.log("Loaded items:", itemsData);
  126. setItems(itemsData);
  127. setFilteredItems([]);
  128. } catch (error) {
  129. console.error("Error loading items:", error);
  130. }
  131. };
  132. loadItems();
  133. }, []);
  134. const searchJobOrderItems = useCallback(async (jobOrderCode: string) => {
  135. if (!jobOrderCode.trim()) return;
  136. setIsLoadingJobOrder(true);
  137. try {
  138. const jobOrderDetail = await fetchJobOrderDetailByCode(jobOrderCode);
  139. setJobOrderItems(jobOrderDetail.pickLines || []);
  140. // Fix the Job Order conversion - add missing uomDesc
  141. const convertedItems = (jobOrderDetail.pickLines || []).map(item => ({
  142. id: item.id,
  143. label: item.name,
  144. qty: item.reqQty,
  145. uom: item.uom,
  146. uomId: 0,
  147. uomDesc: item.uomDesc, // Add missing uomDesc
  148. jobOrderCode: jobOrderDetail.code,
  149. jobOrderId: jobOrderDetail.id,
  150. }));
  151. setFilteredItems(convertedItems);
  152. setHasSearched(true);
  153. } catch (error) {
  154. console.error("Error fetching Job Order items:", error);
  155. alert(t("Job Order not found or has no items"));
  156. } finally {
  157. setIsLoadingJobOrder(false);
  158. }
  159. }, [t]);
  160. // Update useEffect to handle Job Order search
  161. useEffect(() => {
  162. if (searchQuery && searchQuery.jobOrderCode) {
  163. searchJobOrderItems(searchQuery.jobOrderCode);
  164. } else if (searchQuery && items.length > 0) {
  165. // Existing item search logic
  166. // ... your existing search logic
  167. }
  168. }, [searchQuery, items, searchJobOrderItems]);
  169. useEffect(() => {
  170. if (searchQuery) {
  171. if (searchQuery.type) {
  172. formProps.setValue("type", searchQuery.type);
  173. }
  174. if (searchQuery.targetDate) {
  175. formProps.setValue("targetDate", searchQuery.targetDate);
  176. }
  177. if (searchQuery.code) {
  178. formProps.setValue("searchCode", searchQuery.code);
  179. }
  180. if (searchQuery.items) {
  181. formProps.setValue("searchName", searchQuery.items);
  182. }
  183. }
  184. }, [searchQuery, formProps]);
  185. useEffect(() => {
  186. setFilteredItems([]);
  187. setHasSearched(false);
  188. }, []);
  189. const typeList = [
  190. { type: "Consumable" },
  191. { type: "Material" },
  192. { type: "Product" }
  193. ];
  194. const handleTypeChange = useCallback(
  195. (event: React.SyntheticEvent, newValue: {type: string} | null) => {
  196. formProps.setValue("type", newValue?.type || "");
  197. },
  198. [formProps],
  199. );
  200. const handleSearch = useCallback(() => {
  201. if (!searchCode && !searchName) {
  202. alert(t("Please enter at least code or name"));
  203. return;
  204. }
  205. setIsLoading(true);
  206. setHasSearched(true);
  207. console.log("Searching with:", { type, searchCode, searchName, targetDate, itemsCount: items.length });
  208. setTimeout(() => {
  209. let filtered = items;
  210. if (searchCode && searchCode.trim()) {
  211. filtered = filtered.filter(item =>
  212. item.label.toLowerCase().includes(searchCode.toLowerCase())
  213. );
  214. console.log("After code filter:", filtered.length);
  215. }
  216. if (searchName && searchName.trim()) {
  217. filtered = filtered.filter(item =>
  218. item.label.toLowerCase().includes(searchName.toLowerCase())
  219. );
  220. console.log("After name filter:", filtered.length);
  221. }
  222. // Convert to SearchItemWithQty with default qty = null and include targetDate
  223. const filteredWithQty = filtered.slice(0, 100).map(item => ({
  224. ...item,
  225. qty: null,
  226. targetDate: targetDate, // Add target date to each item
  227. }));
  228. console.log("Final filtered results:", filteredWithQty.length);
  229. setFilteredItems(filteredWithQty);
  230. setIsLoading(false);
  231. }, 500);
  232. }, [type, searchCode, searchName, targetDate, items, t]); // Add targetDate back to dependencies
  233. // Handle quantity change in search results
  234. const handleSearchQtyChange = useCallback((itemId: number, newQty: number | null) => {
  235. setFilteredItems(prev =>
  236. prev.map(item =>
  237. item.id === itemId ? { ...item, qty: newQty } : item
  238. )
  239. );
  240. // Auto-update created items if this item exists there
  241. setCreatedItems(prev =>
  242. prev.map(item =>
  243. item.itemId === itemId ? { ...item, qty: newQty || 1 } : item
  244. )
  245. );
  246. }, []);
  247. // Modified handler for search item selection
  248. const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => {
  249. if (isSelected) {
  250. const item = filteredItems.find(i => i.id === itemId);
  251. if (!item) return;
  252. const existingItem = createdItems.find(created => created.itemId === item.id);
  253. if (existingItem) {
  254. alert(t("Item already exists in created items"));
  255. return;
  256. }
  257. // Fix the newCreatedItem creation - add missing uomDesc
  258. const newCreatedItem: CreatedItem = {
  259. itemId: item.id,
  260. itemName: item.label,
  261. itemCode: item.label,
  262. qty: item.qty || 1,
  263. uom: item.uom || "",
  264. uomId: item.uomId || 0,
  265. uomDesc: item.uomDesc || "", // Add missing uomDesc
  266. isSelected: true,
  267. currentStockBalance: item.currentStockBalance,
  268. targetDate: item.targetDate || targetDate, // Use item's targetDate or fallback to form's targetDate
  269. groupId: item.groupId || undefined, // Handle null values
  270. };
  271. setCreatedItems(prev => [...prev, newCreatedItem]);
  272. }
  273. }, [filteredItems, createdItems, t, targetDate]);
  274. // Handler for created item selection
  275. const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean) => {
  276. setCreatedItems(prev =>
  277. prev.map(item =>
  278. item.itemId === itemId ? { ...item, isSelected } : item
  279. )
  280. );
  281. }, []);
  282. const handleQtyChange = useCallback((itemId: number, newQty: number) => {
  283. setCreatedItems(prev =>
  284. prev.map(item =>
  285. item.itemId === itemId ? { ...item, qty: newQty } : item
  286. )
  287. );
  288. }, []);
  289. // Check if item is already in created items
  290. const isItemInCreated = useCallback((itemId: number) => {
  291. return createdItems.some(item => item.itemId === itemId);
  292. }, [createdItems]);
  293. // 1) Created Items 行内改组:只改这一行的 groupId,并把该行 targetDate 同步为该组日期
  294. const handleCreatedItemGroupChange = useCallback((itemId: number, newGroupId: string) => {
  295. const gid = newGroupId ? Number(newGroupId) : undefined;
  296. const group = groups.find(g => g.id === gid);
  297. setCreatedItems(prev =>
  298. prev.map(it =>
  299. it.itemId === itemId
  300. ? {
  301. ...it,
  302. groupId: gid,
  303. targetDate: group?.targetDate || it.targetDate,
  304. }
  305. : it,
  306. ),
  307. );
  308. }, [groups]);
  309. // Update the handleGroupChange function to update target dates for items in the selected group
  310. const handleGroupChange = useCallback((groupId: string | number) => {
  311. const gid = typeof groupId === "string" ? Number(groupId) : groupId;
  312. const group = groups.find(g => g.id === gid);
  313. if (!group) return;
  314. setSelectedGroup(group);
  315. // Update target dates for items that belong to this group
  316. setSecondSearchResults(prev => prev.map(item =>
  317. item.groupId === gid
  318. ? {
  319. ...item,
  320. targetDate: group.targetDate
  321. }
  322. : item
  323. ));
  324. }, [groups]);
  325. // Update the handleGroupTargetDateChange function to update selected items that belong to that group
  326. const handleGroupTargetDateChange = useCallback((groupId: number, newTargetDate: string) => {
  327. setGroups(prev => prev.map(g => (g.id === groupId ? { ...g, targetDate: newTargetDate } : g)));
  328. setSelectedGroup(prev => {
  329. if (prev && prev.id === groupId) {
  330. return { ...prev, targetDate: newTargetDate };
  331. }
  332. return prev;
  333. });
  334. // Update selected items that belong to this group
  335. setSecondSearchResults(prev => prev.map(item =>
  336. item.groupId === groupId
  337. ? {
  338. ...item,
  339. targetDate: newTargetDate
  340. }
  341. : item
  342. ));
  343. setCreatedItems(prev => prev.map(item =>
  344. item.groupId === groupId
  345. ? {
  346. ...item,
  347. targetDate: newTargetDate
  348. }
  349. : item
  350. ));
  351. }, []);
  352. // Fix the handleCreateGroup function to use the API properly
  353. const handleCreateGroup = useCallback(async () => {
  354. try {
  355. // Use the API to get latest group name and create it automatically
  356. const response = await getLatestGroupNameAndCreate();
  357. if (response.id && response.name) {
  358. const newGroup: Group = {
  359. id: response.id,
  360. name: response.name,
  361. targetDate: ""
  362. };
  363. setGroups(prev => [...prev, newGroup]);
  364. setSelectedGroup(newGroup);
  365. console.log(`Created new group: ${response.name}`);
  366. } else {
  367. alert(t('Failed to create group'));
  368. }
  369. } catch (error) {
  370. console.error('Error creating group:', error);
  371. alert(t('Failed to create group'));
  372. }
  373. }, [t]);
  374. const checkAndAutoAddItem = useCallback((itemId: number) => {
  375. const item = secondSearchResults.find(i => i.id === itemId);
  376. if (!item) return;
  377. // Check if item has ALL 3 conditions:
  378. // 1. Item is selected (checkbox checked)
  379. const isSelected = selectedSecondSearchItemIds.includes(itemId);
  380. // 2. Group is assigned
  381. const hasGroup = item.groupId !== undefined && item.groupId !== null;
  382. // 3. Quantity is entered
  383. const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0;
  384. if (isSelected && hasGroup && hasQty && !isItemInCreated(item.id)) {
  385. // Auto-add to created items
  386. const newCreatedItem: CreatedItem = {
  387. itemId: item.id,
  388. itemName: item.label,
  389. itemCode: item.label,
  390. qty: item.qty || 1,
  391. uom: item.uom || "",
  392. uomId: item.uomId || 0,
  393. uomDesc: item.uomDesc || "",
  394. isSelected: true,
  395. currentStockBalance: item.currentStockBalance,
  396. targetDate: item.targetDate || targetDate,
  397. groupId: item.groupId || undefined,
  398. };
  399. setCreatedItems(prev => [...prev, newCreatedItem]);
  400. // Remove from search results since it's now in created items
  401. setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId));
  402. setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId));
  403. }
  404. }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]);
  405. // Add this function after checkAndAutoAddItem
  406. // Add this function after checkAndAutoAddItem
  407. const handleQtyBlur = useCallback((itemId: number) => {
  408. // Only auto-add if item is already selected (scenario 1: select first, then enter quantity)
  409. setTimeout(() => {
  410. const currentItem = secondSearchResults.find(i => i.id === itemId);
  411. if (!currentItem) return;
  412. const isSelected = selectedSecondSearchItemIds.includes(itemId);
  413. const hasGroup = currentItem.groupId !== undefined && currentItem.groupId !== null;
  414. const hasQty = currentItem.qty !== null && currentItem.qty !== undefined && currentItem.qty > 0;
  415. // Only auto-add if item is already selected (scenario 1: select first, then enter quantity)
  416. if (isSelected && hasGroup && hasQty && !isItemInCreated(currentItem.id)) {
  417. const newCreatedItem: CreatedItem = {
  418. itemId: currentItem.id,
  419. itemName: currentItem.label,
  420. itemCode: currentItem.label,
  421. qty: currentItem.qty || 1,
  422. uom: currentItem.uom || "",
  423. uomId: currentItem.uomId || 0,
  424. uomDesc: currentItem.uomDesc || "",
  425. isSelected: true,
  426. currentStockBalance: currentItem.currentStockBalance,
  427. targetDate: currentItem.targetDate || targetDate,
  428. groupId: currentItem.groupId || undefined,
  429. };
  430. setCreatedItems(prev => [...prev, newCreatedItem]);
  431. setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId));
  432. setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId));
  433. }
  434. }, 0);
  435. }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]);
  436. const handleSearchItemGroupChange = useCallback((itemId: number, groupId: string) => {
  437. const gid = groupId ? Number(groupId) : undefined;
  438. const group = groups.find(g => g.id === gid);
  439. setSecondSearchResults(prev => prev.map(item =>
  440. item.id === itemId
  441. ? {
  442. ...item,
  443. groupId: gid,
  444. targetDate: group?.targetDate || undefined
  445. }
  446. : item
  447. ));
  448. // Check auto-add after group assignment
  449. setTimeout(() => {
  450. checkAndAutoAddItem(itemId);
  451. }, 0);
  452. }, [groups, checkAndAutoAddItem]);
  453. // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group)
  454. const handleSecondSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => {
  455. if (!isSelected) return;
  456. const item = secondSearchResults.find(i => i.id === itemId);
  457. if (!item) return;
  458. const exists = createdItems.find(c => c.itemId === item.id);
  459. if (exists) { alert(t("Item already exists in created items")); return; }
  460. // 找到项目所属的组,使用该组的 targetDate
  461. const itemGroup = groups.find(g => g.id === item.groupId);
  462. const itemTargetDate = itemGroup?.targetDate || item.targetDate || targetDate;
  463. const newCreatedItem: CreatedItem = {
  464. itemId: item.id,
  465. itemName: item.label,
  466. itemCode: item.label,
  467. qty: item.qty || 1,
  468. uom: item.uom || "",
  469. uomId: item.uomId || 0,
  470. uomDesc: item.uomDesc || "",
  471. isSelected: true,
  472. currentStockBalance: item.currentStockBalance,
  473. targetDate: itemTargetDate, // 使用项目所属组的 targetDate
  474. groupId: item.groupId || undefined, // 使用项目自身的 groupId
  475. };
  476. setCreatedItems(prev => [...prev, newCreatedItem]);
  477. }, [secondSearchResults, createdItems, groups, targetDate, t]);
  478. // 修改提交函数,按组分别创建提料单
  479. const onSubmit = useCallback<SubmitHandler<SearchFormData>>(
  480. async (data, event) => {
  481. const selectedCreatedItems = createdItems.filter(item => item.isSelected);
  482. if (selectedCreatedItems.length === 0) {
  483. alert(t("Please select at least one item to submit"));
  484. return;
  485. }
  486. // ✅ 修复:自动填充 type 为 "Consumable",不再强制用户选择
  487. // if (!data.type) {
  488. // alert(t("Please select product type"));
  489. // return;
  490. // }
  491. // 按组分组选中的项目
  492. const itemsByGroup = selectedCreatedItems.reduce((acc, item) => {
  493. const groupId = item.groupId || 'no-group';
  494. if (!acc[groupId]) {
  495. acc[groupId] = [];
  496. }
  497. acc[groupId].push(item);
  498. return acc;
  499. }, {} as Record<string | number, typeof selectedCreatedItems>);
  500. console.log("Items grouped by group:", itemsByGroup);
  501. let successCount = 0;
  502. const totalGroups = Object.keys(itemsByGroup).length;
  503. const groupUpdates: Array<{groupId: number, pickOrderId: number}> = [];
  504. // 为每个组创建提料单
  505. for (const [groupId, items] of Object.entries(itemsByGroup)) {
  506. try {
  507. // 获取组的名称和目标日期
  508. const group = groups.find(g => g.id === Number(groupId));
  509. const groupName = group?.name || 'No Group';
  510. // Use the group's target date, fallback to item's target date, then form's target date
  511. let groupTargetDate = group?.targetDate;
  512. if (!groupTargetDate && items.length > 0) {
  513. groupTargetDate = items[0].targetDate || undefined; // Add || undefined to handle null
  514. }
  515. if (!groupTargetDate) {
  516. groupTargetDate = data.targetDate;
  517. }
  518. // If still no target date, use today
  519. if (!groupTargetDate) {
  520. groupTargetDate = dayjs().format(INPUT_DATE_FORMAT);
  521. }
  522. console.log(`Creating pick order for group: ${groupName} with ${items.length} items, target date: ${groupTargetDate}`);
  523. let formattedTargetDate = groupTargetDate;
  524. if (groupTargetDate && typeof groupTargetDate === 'string') {
  525. try {
  526. const date = dayjs(groupTargetDate);
  527. formattedTargetDate = date.format('YYYY-MM-DD');
  528. } catch (error) {
  529. console.error("Invalid date format:", groupTargetDate);
  530. alert(t("Invalid date format"));
  531. return;
  532. }
  533. }
  534. // ✅ 修复:自动使用 "Consumable" 作为默认 type
  535. const pickOrderData: SavePickOrderRequest = {
  536. type: data.type || "Consumable", // 如果用户选择了 type 就用用户的,否则默认 "Consumable"
  537. targetDate: formattedTargetDate,
  538. pickOrderLine: items.map(item => ({
  539. itemId: item.itemId,
  540. qty: item.qty,
  541. uomId: item.uomId
  542. } as SavePickOrderLineRequest))
  543. };
  544. console.log(`Submitting pick order for group ${groupName}:`, pickOrderData);
  545. const res = await createPickOrder(pickOrderData);
  546. if (res.id) {
  547. console.log(`Pick order created successfully for group ${groupName}:`, res);
  548. successCount++;
  549. // Store group ID and pick order ID for updating
  550. if (groupId !== 'no-group' && group?.id) {
  551. groupUpdates.push({
  552. groupId: group.id,
  553. pickOrderId: res.id
  554. });
  555. }
  556. } else {
  557. console.error(`Failed to create pick order for group ${groupName}:`, res);
  558. alert(t(`Failed to create pick order for group ${groupName}`));
  559. return;
  560. }
  561. } catch (error) {
  562. console.error(`Error creating pick order for group ${groupId}:`, error);
  563. alert(t(`Error creating pick order for group ${groupId}`));
  564. return;
  565. }
  566. }
  567. // Update groups with pick order information
  568. if (groupUpdates.length > 0) {
  569. try {
  570. // Update each group with its corresponding pick order ID
  571. for (const update of groupUpdates) {
  572. const updateResponse = await createOrUpdateGroups({
  573. groupIds: [update.groupId],
  574. targetDate: data.targetDate,
  575. pickOrderId: update.pickOrderId
  576. });
  577. console.log(`Group ${update.groupId} updated with pick order ${update.pickOrderId}:`, updateResponse);
  578. }
  579. } catch (error) {
  580. console.error('Error updating groups:', error);
  581. // Don't fail the whole operation if group update fails
  582. }
  583. }
  584. // 所有组都创建成功后,清理选中的项目并切换到 Assign & Release
  585. if (successCount === totalGroups) {
  586. setCreatedItems(prev => prev.filter(item => !item.isSelected));
  587. formProps.reset();
  588. setHasSearched(false);
  589. setFilteredItems([]);
  590. alert(t("All pick orders created successfully"));
  591. // 通知父组件切换到 Assign & Release 标签页
  592. if (onPickOrderCreated) {
  593. onPickOrderCreated();
  594. }
  595. }
  596. },
  597. [createdItems, t, formProps, groups, onPickOrderCreated]
  598. );
  599. // Fix the handleReset function to properly clear all states including search results
  600. const handleReset = useCallback(() => {
  601. formProps.reset();
  602. setCreatedItems([]);
  603. setHasSearched(false);
  604. setFilteredItems([]);
  605. // Clear second search states completely
  606. setSecondSearchResults([]);
  607. setHasSearchedSecond(false);
  608. setSelectedSecondSearchItemIds([]);
  609. setSecondSearchQuery({});
  610. // Clear groups
  611. setGroups([]);
  612. setSelectedGroup(null);
  613. setNextGroupNumber(1);
  614. // Clear pagination states
  615. setSearchResultsPagingController({
  616. pageNum: 1,
  617. pageSize: 10,
  618. });
  619. setCreatedItemsPagingController({
  620. pageNum: 1,
  621. pageSize: 10,
  622. });
  623. // Clear first search states
  624. setSelectedSearchItemIds([]);
  625. }, [formProps]);
  626. // Pagination state
  627. const [page, setPage] = useState(0);
  628. const [rowsPerPage, setRowsPerPage] = useState(10);
  629. // Handle page change
  630. const handleChangePage = (
  631. _event: React.MouseEvent | React.KeyboardEvent,
  632. newPage: number,
  633. ) => {
  634. console.log(_event);
  635. setPage(newPage);
  636. // The original code had setPagingController and defaultPagingController,
  637. // but these are not defined in the provided context.
  638. // Assuming they are meant to be part of a larger context or will be added.
  639. // For now, commenting out the setPagingController part as it's not defined.
  640. // if (setPagingController) {
  641. // setPagingController({
  642. // ...(pagingController ?? defaultPagingController),
  643. // pageNum: newPage + 1,
  644. // });
  645. // }
  646. };
  647. // Handle rows per page change
  648. const handleChangeRowsPerPage = (
  649. event: React.ChangeEvent<HTMLInputElement>,
  650. ) => {
  651. console.log(event);
  652. setRowsPerPage(+event.target.value);
  653. setPage(0);
  654. // The original code had setPagingController and defaultPagingController,
  655. // but these are not defined in the provided context.
  656. // Assuming they are meant to be part of a larger context or will be added.
  657. // For now, commenting out the setPagingController part as it's not defined.
  658. // if (setPagingController) {
  659. // setPagingController({
  660. // ...(pagingController ?? defaultPagingController),
  661. // pageNum: 1,
  662. // });
  663. // }
  664. };
  665. // Add missing handleSearchCheckboxChange function
  666. const handleSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => {
  667. if (typeof ids === 'function') {
  668. const newIds = ids(selectedSearchItemIds);
  669. setSelectedSearchItemIds(newIds);
  670. if (newIds.length === filteredItems.length) {
  671. // Select all
  672. filteredItems.forEach(item => {
  673. if (!isItemInCreated(item.id)) {
  674. handleSearchItemSelect(item.id, true);
  675. }
  676. });
  677. } else {
  678. // Handle individual selections
  679. filteredItems.forEach(item => {
  680. const isSelected = newIds.includes(item.id);
  681. const isCurrentlyInCreated = isItemInCreated(item.id);
  682. if (isSelected && !isCurrentlyInCreated) {
  683. handleSearchItemSelect(item.id, true);
  684. } else if (!isSelected && isCurrentlyInCreated) {
  685. setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id));
  686. }
  687. });
  688. }
  689. } else {
  690. const previousIds = selectedSearchItemIds;
  691. setSelectedSearchItemIds(ids);
  692. const newlySelected = ids.filter(id => !previousIds.includes(id));
  693. const newlyDeselected = previousIds.filter(id => !ids.includes(id));
  694. newlySelected.forEach(id => {
  695. if (!isItemInCreated(id as number)) {
  696. handleSearchItemSelect(id as number, true);
  697. }
  698. });
  699. newlyDeselected.forEach(id => {
  700. setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id));
  701. });
  702. }
  703. }, [selectedSearchItemIds, filteredItems, isItemInCreated, handleSearchItemSelect]);
  704. // Add pagination state for created items
  705. const [createdItemsPagingController, setCreatedItemsPagingController] = useState({
  706. pageNum: 1,
  707. pageSize: 10,
  708. });
  709. // Add pagination handlers for created items
  710. const handleCreatedItemsPageChange = useCallback((event: unknown, newPage: number) => {
  711. const newPagingController = {
  712. ...createdItemsPagingController,
  713. pageNum: newPage + 1,
  714. };
  715. setCreatedItemsPagingController(newPagingController);
  716. }, [createdItemsPagingController]);
  717. const handleCreatedItemsPageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  718. const newPageSize = parseInt(event.target.value, 10);
  719. const newPagingController = {
  720. pageNum: 1,
  721. pageSize: newPageSize,
  722. };
  723. setCreatedItemsPagingController(newPagingController);
  724. }, []);
  725. // Create a custom table for created items with pagination
  726. const CustomCreatedItemsTable = () => {
  727. const startIndex = (createdItemsPagingController.pageNum - 1) * createdItemsPagingController.pageSize;
  728. const endIndex = startIndex + createdItemsPagingController.pageSize;
  729. const paginatedCreatedItems = createdItems.slice(startIndex, endIndex);
  730. return (
  731. <>
  732. <TableContainer component={Paper}>
  733. <Table>
  734. <TableHead>
  735. <TableRow>
  736. <TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}>
  737. {t("Selected")}
  738. </TableCell>
  739. <TableCell>
  740. {t("Item")}
  741. </TableCell>
  742. <TableCell>
  743. {t("Group")}
  744. </TableCell>
  745. <TableCell align="right">
  746. {t("Current Stock")}
  747. </TableCell>
  748. <TableCell align="right">
  749. {t("Stock Unit")}
  750. </TableCell>
  751. <TableCell align="right">
  752. {t("Order Quantity")}
  753. </TableCell>
  754. <TableCell align="right">
  755. {t("Target Date")}
  756. </TableCell>
  757. </TableRow>
  758. </TableHead>
  759. <TableBody>
  760. {paginatedCreatedItems.length === 0 ? (
  761. <TableRow>
  762. <TableCell colSpan={12} align="center">
  763. <Typography variant="body2" color="text.secondary">
  764. {t("No created items")}
  765. </Typography>
  766. </TableCell>
  767. </TableRow>
  768. ) : (
  769. paginatedCreatedItems.map((item) => (
  770. <TableRow key={item.itemId}>
  771. <TableCell padding="checkbox">
  772. <Checkbox
  773. checked={item.isSelected}
  774. onChange={(e) => handleCreatedItemSelect(item.itemId, e.target.checked)}
  775. />
  776. </TableCell>
  777. <TableCell>
  778. <Typography variant="body2">{item.itemName}</Typography>
  779. <Typography variant="caption" color="textSecondary">
  780. {item.itemCode}
  781. </Typography>
  782. </TableCell>
  783. <TableCell>
  784. <FormControl size="small" sx={{ minWidth: 120 }}>
  785. <Select
  786. value={item.groupId?.toString() || ""}
  787. onChange={(e) => handleCreatedItemGroupChange(item.itemId, e.target.value)}
  788. displayEmpty
  789. >
  790. <MenuItem value="">
  791. <em>{t("No Group")}</em>
  792. </MenuItem>
  793. {groups.map((group) => (
  794. <MenuItem key={group.id} value={group.id.toString()}>
  795. {group.name}
  796. </MenuItem>
  797. ))}
  798. </Select>
  799. </FormControl>
  800. </TableCell>
  801. <TableCell align="right">
  802. <Typography
  803. variant="body2"
  804. color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"}
  805. >
  806. {item.currentStockBalance?.toLocaleString() || 0}
  807. </Typography>
  808. </TableCell>
  809. <TableCell align="right">
  810. <Typography variant="body2">{item.uomDesc}</Typography>
  811. </TableCell>
  812. <TableCell align="right">
  813. <TextField
  814. type="number"
  815. size="small"
  816. value={item.qty || ""}
  817. onChange={(e) => {
  818. const newQty = Number(e.target.value);
  819. handleQtyChange(item.itemId, newQty);
  820. }}
  821. inputProps={{
  822. min: 1,
  823. step: 1,
  824. style: { textAlign: 'center' }
  825. }}
  826. sx={{
  827. width: '80px',
  828. '& .MuiInputBase-input': {
  829. textAlign: 'center',
  830. cursor: 'text'
  831. }
  832. }}
  833. />
  834. </TableCell>
  835. <TableCell align="right">
  836. <Typography variant="body2">
  837. {item.targetDate&& item.targetDate !== "" ? new Date(item.targetDate).toLocaleDateString() : "-"}
  838. </Typography>
  839. </TableCell>
  840. </TableRow>
  841. ))
  842. )}
  843. </TableBody>
  844. </Table>
  845. </TableContainer>
  846. {/* Pagination for created items */}
  847. <TablePagination
  848. component="div"
  849. count={createdItems.length}
  850. page={(createdItemsPagingController.pageNum - 1)}
  851. rowsPerPage={createdItemsPagingController.pageSize}
  852. onPageChange={handleCreatedItemsPageChange}
  853. onRowsPerPageChange={handleCreatedItemsPageSizeChange}
  854. rowsPerPageOptions={[10, 25, 50]}
  855. labelRowsPerPage={t("Rows per page")}
  856. labelDisplayedRows={({ from, to, count }) =>
  857. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  858. }
  859. />
  860. </>
  861. );
  862. };
  863. // Define columns for SearchResults
  864. const searchItemColumns: Column<SearchItemWithQty>[] = useMemo(() => [
  865. {
  866. name: "id",
  867. label: "",
  868. type: "checkbox",
  869. disabled: (item) => isItemInCreated(item.id), // Disable if already in created items
  870. },
  871. {
  872. name: "label",
  873. label: t("Item"),
  874. renderCell: (item) => {
  875. const parts = item.label.split(' - ');
  876. const code = parts[0] || '';
  877. const name = parts[1] || '';
  878. return (
  879. <Box>
  880. <Typography variant="body2">
  881. {name} {/* 显示项目名称 */}
  882. </Typography>
  883. <Typography variant="caption" color="textSecondary">
  884. {code} {/* 显示项目代码 */}
  885. </Typography>
  886. </Box>
  887. );
  888. },
  889. },
  890. {
  891. name: "qty",
  892. label: t("Order Quantity"),
  893. renderCell: (item) => (
  894. <TextField
  895. type="number"
  896. size="small"
  897. value={item.qty || ""} // Show empty string if qty is null
  898. onChange={(e) => {
  899. const value = e.target.value;
  900. const numValue = value === "" ? null : Number(value);
  901. handleSearchQtyChange(item.id, numValue);
  902. }}
  903. inputProps={{
  904. min: 1,
  905. step: 1,
  906. style: { textAlign: 'center' } // Center the text
  907. }}
  908. sx={{
  909. width: '80px',
  910. '& .MuiInputBase-input': {
  911. textAlign: 'center',
  912. cursor: 'text'
  913. }
  914. }}
  915. />
  916. ),
  917. },
  918. {
  919. name: "currentStockBalance",
  920. label: t("Current Stock"),
  921. renderCell: (item) => {
  922. const stockBalance = item.currentStockBalance || 0;
  923. return (
  924. <Typography
  925. variant="body2"
  926. color={stockBalance > 0 ? "success.main" : "error.main"}
  927. sx={{ fontWeight: stockBalance > 0 ? 'bold' : 'normal' }}
  928. >
  929. {stockBalance}
  930. </Typography>
  931. );
  932. },
  933. },
  934. {
  935. name: "targetDate",
  936. label: t("Target Date"),
  937. renderCell: (item) => (
  938. <Typography variant="body2">
  939. {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
  940. </Typography>
  941. ),
  942. },
  943. {
  944. name: "uom",
  945. label: t("Stock Unit"),
  946. renderCell: (item) => item.uom || "-",
  947. },
  948. ], [t, isItemInCreated, handleSearchQtyChange]);
  949. // 修改搜索条件为3行,每行一个 - 确保SearchBox组件能正确处理
  950. const pickOrderSearchCriteria: Criterion<any>[] = useMemo(
  951. () => [
  952. {
  953. label: t("Item Code"),
  954. paramName: "code",
  955. type: "text"
  956. },
  957. {
  958. label: t("Item Name"),
  959. paramName: "name",
  960. type: "text"
  961. },
  962. {
  963. label: t("Product Type"),
  964. paramName: "type",
  965. type: "autocomplete",
  966. options: [
  967. { value: "Consumable", label: t("Consumable") },
  968. { value: "MATERIAL", label: t("Material") },
  969. { value: "End_product", label: t("End Product") }
  970. ],
  971. },
  972. ],
  973. [t],
  974. );
  975. // 添加重置函数
  976. const handleSecondReset = useCallback(() => {
  977. console.log("Second search reset");
  978. setSecondSearchQuery({});
  979. setSecondSearchResults([]);
  980. setHasSearchedSecond(false);
  981. // 清空表单中的类型,但保留今天的日期
  982. formProps.setValue("type", "");
  983. const today = dayjs().format(INPUT_DATE_FORMAT);
  984. formProps.setValue("targetDate", today);
  985. }, [formProps]);
  986. // 添加数量变更处理函数
  987. const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => {
  988. setSecondSearchResults(prev =>
  989. prev.map(item =>
  990. item.id === itemId ? { ...item, qty: newQty } : item
  991. )
  992. );
  993. // Don't auto-add here - only on blur event
  994. }, []);
  995. // Add checkbox change handler for second search
  996. const handleSecondSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => {
  997. if (typeof ids === 'function') {
  998. const newIds = ids(selectedSecondSearchItemIds);
  999. setSelectedSecondSearchItemIds(newIds);
  1000. // 处理全选逻辑 - 选择所有搜索结果,不仅仅是当前页面
  1001. if (newIds.length === secondSearchResults.length) {
  1002. // 全选:将所有搜索结果添加到创建项目
  1003. secondSearchResults.forEach(item => {
  1004. if (!isItemInCreated(item.id)) {
  1005. handleSearchItemSelect(item.id, true);
  1006. }
  1007. });
  1008. } else {
  1009. // 部分选择:只处理当前页面的选择
  1010. secondSearchResults.forEach(item => {
  1011. const isSelected = newIds.includes(item.id);
  1012. const isCurrentlyInCreated = isItemInCreated(item.id);
  1013. if (isSelected && !isCurrentlyInCreated) {
  1014. handleSearchItemSelect(item.id, true);
  1015. } else if (!isSelected && isCurrentlyInCreated) {
  1016. setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id));
  1017. }
  1018. });
  1019. }
  1020. } else {
  1021. const previousIds = selectedSecondSearchItemIds;
  1022. setSelectedSecondSearchItemIds(ids);
  1023. const newlySelected = ids.filter(id => !previousIds.includes(id));
  1024. const newlyDeselected = previousIds.filter(id => !ids.includes(id));
  1025. newlySelected.forEach(id => {
  1026. if (!isItemInCreated(id as number)) {
  1027. handleSearchItemSelect(id as number, true);
  1028. }
  1029. });
  1030. newlyDeselected.forEach(id => {
  1031. setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id));
  1032. });
  1033. }
  1034. }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]);
  1035. // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity
  1036. const secondSearchItemColumns: Column<SearchItemWithQty>[] = useMemo(() => [
  1037. {
  1038. name: "id",
  1039. label: "",
  1040. type: "checkbox",
  1041. disabled: (item) => isItemInCreated(item.id),
  1042. },
  1043. {
  1044. name: "label",
  1045. label: t("Item"),
  1046. renderCell: (item) => {
  1047. const parts = item.label.split(' - ');
  1048. const code = parts[0] || '';
  1049. const name = parts[1] || '';
  1050. return (
  1051. <Box>
  1052. <Typography variant="body2">
  1053. {name}
  1054. </Typography>
  1055. <Typography variant="caption" color="textSecondary">
  1056. {code}
  1057. </Typography>
  1058. </Box>
  1059. );
  1060. },
  1061. },
  1062. {
  1063. name: "currentStockBalance",
  1064. label: t("Current Stock"),
  1065. align: "right", // Add right alignment for the label
  1066. renderCell: (item) => {
  1067. const stockBalance = item.currentStockBalance || 0;
  1068. return (
  1069. <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}>
  1070. <Typography
  1071. variant="body2"
  1072. color={stockBalance > 0 ? "success.main" : "error.main"}
  1073. sx={{
  1074. fontWeight: stockBalance > 0 ? 'bold' : 'normal',
  1075. textAlign: 'right' // Add right alignment for the value
  1076. }}
  1077. >
  1078. {stockBalance}
  1079. </Typography>
  1080. </Box>
  1081. );
  1082. },
  1083. },
  1084. {
  1085. name: "uom",
  1086. label: t("Stock Unit"),
  1087. align: "right", // Add right alignment for the label
  1088. renderCell: (item) => (
  1089. <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}>
  1090. <Typography sx={{ textAlign: 'right' }}> {/* Add right alignment for the value */}
  1091. {item.uom || "-"}
  1092. </Typography>
  1093. </Box>
  1094. ),
  1095. },
  1096. {
  1097. name: "qty",
  1098. label: t("Order Quantity"),
  1099. align: "right",
  1100. renderCell: (item) => (
  1101. <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}>
  1102. <TextField
  1103. type="number"
  1104. size="small"
  1105. value={item.qty || ""}
  1106. onChange={(e) => {
  1107. const value = e.target.value;
  1108. // Only allow numbers
  1109. if (value === "" || /^\d+$/.test(value)) {
  1110. const numValue = value === "" ? null : Number(value);
  1111. handleSecondSearchQtyChange(item.id, numValue);
  1112. }
  1113. }}
  1114. inputProps={{
  1115. style: { textAlign: 'center' }
  1116. }}
  1117. sx={{
  1118. width: '80px',
  1119. '& .MuiInputBase-input': {
  1120. textAlign: 'center',
  1121. cursor: 'text'
  1122. }
  1123. }}
  1124. onBlur={(e) => {
  1125. const value = e.target.value;
  1126. const numValue = value === "" ? null : Number(value);
  1127. if (numValue !== null && numValue < 1) {
  1128. handleSecondSearchQtyChange(item.id, 1); // Enforce min value
  1129. }
  1130. }}
  1131. />
  1132. </Box>
  1133. ),
  1134. }
  1135. ], [t, isItemInCreated, handleSecondSearchQtyChange, groups]);
  1136. // 添加缺失的 handleSecondSearch 函数
  1137. const handleSecondSearch = useCallback((query: Record<string, any>) => {
  1138. console.log("Second search triggered with query:", query);
  1139. setSecondSearchQuery({ ...query });
  1140. setIsLoadingSecondSearch(true);
  1141. // Sync second search box info to form - ensure type value is correct
  1142. if (query.type) {
  1143. // Ensure type value matches backend enum format
  1144. let correctType = query.type;
  1145. if (query.type === "consumable") {
  1146. correctType = "Consumable";
  1147. } else if (query.type === "material") {
  1148. correctType = "MATERIAL";
  1149. } else if (query.type === "jo") {
  1150. correctType = "JOB_ORDER";
  1151. }
  1152. formProps.setValue("type", correctType);
  1153. }
  1154. setTimeout(() => {
  1155. let filtered = items;
  1156. // Same filtering logic as first search
  1157. if (query.code && query.code.trim()) {
  1158. filtered = filtered.filter(item =>
  1159. item.label.toLowerCase().includes(query.code.toLowerCase())
  1160. );
  1161. }
  1162. if (query.name && query.name.trim()) {
  1163. filtered = filtered.filter(item =>
  1164. item.label.toLowerCase().includes(query.name.toLowerCase())
  1165. );
  1166. }
  1167. if (query.type && query.type !== "All") {
  1168. // Filter by type if needed
  1169. }
  1170. // Convert to SearchItemWithQty with NO group/targetDate initially
  1171. const filteredWithQty = filtered.slice(0, 100).map(item => ({
  1172. ...item,
  1173. qty: null,
  1174. targetDate: undefined, // No target date initially
  1175. groupId: undefined, // No group initially
  1176. }));
  1177. setSecondSearchResults(filteredWithQty);
  1178. setHasSearchedSecond(true);
  1179. setIsLoadingSecondSearch(false);
  1180. }, 500);
  1181. }, [items, formProps]);
  1182. /*
  1183. // Create a custom search box component that displays fields vertically
  1184. const VerticalSearchBox = ({ criteria, onSearch, onReset }: {
  1185. criteria: Criterion<any>[];
  1186. onSearch: (inputs: Record<string, any>) => void;
  1187. onReset?: () => void;
  1188. }) => {
  1189. const { t } = useTranslation("common");
  1190. const [inputs, setInputs] = useState<Record<string, any>>({});
  1191. const handleInputChange = (paramName: string, value: any) => {
  1192. setInputs(prev => ({ ...prev, [paramName]: value }));
  1193. };
  1194. const handleSearch = () => {
  1195. onSearch(inputs);
  1196. };
  1197. const handleReset = () => {
  1198. setInputs({});
  1199. onReset?.();
  1200. };
  1201. return (
  1202. <Card>
  1203. <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
  1204. <Typography variant="overline">{t("Search Criteria")}</Typography>
  1205. <Grid container spacing={2} columns={{ xs: 12, sm: 12 }}>
  1206. {criteria.map((c) => {
  1207. return (
  1208. <Grid key={c.paramName} item xs={12}>
  1209. {c.type === "text" && (
  1210. <TextField
  1211. label={t(c.label)}
  1212. fullWidth
  1213. onChange={(e) => handleInputChange(c.paramName, e.target.value)}
  1214. value={inputs[c.paramName] || ""}
  1215. />
  1216. )}
  1217. {c.type === "autocomplete" && (
  1218. <Autocomplete
  1219. options={c.options || []}
  1220. getOptionLabel={(option: any) => option.label}
  1221. onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")}
  1222. value={c.options?.find(option => option.value === inputs[c.paramName]) || null}
  1223. renderInput={(params) => (
  1224. <TextField
  1225. {...params}
  1226. label={t(c.label)}
  1227. fullWidth
  1228. />
  1229. )}
  1230. />
  1231. )}
  1232. </Grid>
  1233. );
  1234. })}
  1235. </Grid>
  1236. <Stack direction="row" spacing={2} sx={{ mt: 2 }}>
  1237. <Button
  1238. variant="text"
  1239. startIcon={<RestartAlt />}
  1240. onClick={handleReset}
  1241. >
  1242. {t("Reset")}
  1243. </Button>
  1244. <Button
  1245. variant="outlined"
  1246. startIcon={<Search />}
  1247. onClick={handleSearch}
  1248. >
  1249. {t("Search")}
  1250. </Button>
  1251. </Stack>
  1252. </CardContent>
  1253. </Card>
  1254. );
  1255. };
  1256. */
  1257. // Add pagination state for search results
  1258. const [searchResultsPagingController, setSearchResultsPagingController] = useState({
  1259. pageNum: 1,
  1260. pageSize: 10,
  1261. });
  1262. // Add pagination handlers for search results
  1263. const handleSearchResultsPageChange = useCallback((event: unknown, newPage: number) => {
  1264. const newPagingController = {
  1265. ...searchResultsPagingController,
  1266. pageNum: newPage + 1, // API uses 1-based pagination
  1267. };
  1268. setSearchResultsPagingController(newPagingController);
  1269. }, [searchResultsPagingController]);
  1270. const handleSearchResultsPageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  1271. const newPageSize = parseInt(event.target.value, 10);
  1272. const newPagingController = {
  1273. pageNum: 1, // Reset to first page
  1274. pageSize: newPageSize,
  1275. };
  1276. setSearchResultsPagingController(newPagingController);
  1277. }, []);
  1278. const getValidationMessage = useCallback(() => {
  1279. const selectedItems = secondSearchResults.filter(item =>
  1280. selectedSecondSearchItemIds.includes(item.id)
  1281. );
  1282. const itemsWithoutGroup = selectedItems.filter(item =>
  1283. item.groupId === undefined || item.groupId === null
  1284. );
  1285. const itemsWithoutQty = selectedItems.filter(item =>
  1286. item.qty === null || item.qty === undefined || item.qty <= 0
  1287. );
  1288. if (itemsWithoutGroup.length > 0 && itemsWithoutQty.length > 0) {
  1289. return t("Please select group and enter quantity for all selected items");
  1290. } else if (itemsWithoutGroup.length > 0) {
  1291. return t("Please select group for all selected items");
  1292. } else if (itemsWithoutQty.length > 0) {
  1293. return t("Please enter quantity for all selected items");
  1294. }
  1295. return "";
  1296. }, [secondSearchResults, selectedSecondSearchItemIds, t]);
  1297. // Fix the handleAddSelectedToCreatedItems function to properly clear selections
  1298. const handleAddSelectedToCreatedItems = useCallback(() => {
  1299. const selectedItems = secondSearchResults.filter(item =>
  1300. selectedSecondSearchItemIds.includes(item.id)
  1301. );
  1302. // Add selected items to created items with their own group info
  1303. selectedItems.forEach(item => {
  1304. if (!isItemInCreated(item.id)) {
  1305. const newCreatedItem: CreatedItem = {
  1306. itemId: item.id,
  1307. itemName: item.label,
  1308. itemCode: item.label,
  1309. qty: item.qty || 1,
  1310. uom: item.uom || "",
  1311. uomId: item.uomId || 0,
  1312. uomDesc: item.uomDesc || "",
  1313. isSelected: true,
  1314. currentStockBalance: item.currentStockBalance,
  1315. targetDate: item.targetDate || targetDate,
  1316. groupId: item.groupId || undefined,
  1317. };
  1318. setCreatedItems(prev => [...prev, newCreatedItem]);
  1319. }
  1320. });
  1321. // Clear the selection
  1322. setSelectedSecondSearchItemIds([]);
  1323. // Remove the selected/added items from search results entirely
  1324. setSecondSearchResults(prev => prev.filter(item =>
  1325. !selectedSecondSearchItemIds.includes(item.id)
  1326. ));
  1327. }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]);
  1328. // Add a validation function to check if selected items are valid
  1329. const areSelectedItemsValid = useCallback(() => {
  1330. const selectedItems = secondSearchResults.filter(item =>
  1331. selectedSecondSearchItemIds.includes(item.id)
  1332. );
  1333. return selectedItems.every(item =>
  1334. item.groupId !== undefined &&
  1335. item.groupId !== null &&
  1336. item.qty !== null &&
  1337. item.qty !== undefined &&
  1338. item.qty > 0
  1339. );
  1340. }, [secondSearchResults, selectedSecondSearchItemIds]);
  1341. // Move these handlers to the component level (outside of CustomSearchResultsTable)
  1342. // Handle individual checkbox change - ONLY select, don't add to created items
  1343. const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => {
  1344. checkboxChangeCallCount++;
  1345. if (checked) {
  1346. // Add to selected IDs
  1347. setSelectedSecondSearchItemIds(prev => [...prev, itemId]);
  1348. // Set the item's group and targetDate to current group when selected
  1349. setSecondSearchResults(prev => {
  1350. const updatedResults = prev.map(item =>
  1351. item.id === itemId
  1352. ? {
  1353. ...item,
  1354. groupId: selectedGroup?.id || undefined,
  1355. targetDate: selectedGroup?.targetDate !== undefined && selectedGroup?.targetDate !== "" ? selectedGroup.targetDate : undefined
  1356. }
  1357. : item
  1358. );
  1359. // Check if should auto-add after state update
  1360. setTimeout(() => {
  1361. // Check if we're already processing this item
  1362. if (processingItems.has(itemId)) {
  1363. //alert(`Item ${itemId} is already being processed, skipping duplicate auto-add`);
  1364. return;
  1365. }
  1366. const updatedItem = updatedResults.find(i => i.id === itemId);
  1367. if (updatedItem) {
  1368. const isSelected = true; // We just selected it
  1369. const hasGroup = updatedItem.groupId !== undefined && updatedItem.groupId !== null;
  1370. const hasQty = updatedItem.qty !== null && updatedItem.qty !== undefined && updatedItem.qty > 0;
  1371. // Only auto-add if item has quantity (scenario 2: enter quantity first, then select)
  1372. if (isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)) {
  1373. // Mark this item as being processed
  1374. processingItems.add(itemId);
  1375. const newCreatedItem: CreatedItem = {
  1376. itemId: updatedItem.id,
  1377. itemName: updatedItem.label,
  1378. itemCode: updatedItem.label,
  1379. qty: updatedItem.qty || 1,
  1380. uom: updatedItem.uom || "",
  1381. uomId: updatedItem.uomId || 0,
  1382. uomDesc: updatedItem.uomDesc || "",
  1383. isSelected: true,
  1384. currentStockBalance: updatedItem.currentStockBalance,
  1385. targetDate: updatedItem.targetDate || targetDate,
  1386. groupId: updatedItem.groupId || undefined,
  1387. };
  1388. setCreatedItems(prev => [...prev, newCreatedItem]);
  1389. setSecondSearchResults(current => current.filter(searchItem => searchItem.id !== itemId));
  1390. setSelectedSecondSearchItemIds(current => current.filter(id => id !== itemId));
  1391. // Remove from processing set after a short delay
  1392. setTimeout(() => {
  1393. processingItems.delete(itemId);
  1394. }, 100);
  1395. }
  1396. // Show final debug info in one alert
  1397. /*
  1398. alert(`FINAL DEBUG INFO for item ${itemId}:
  1399. Function called ${checkboxChangeCallCount} times
  1400. Is Selected: ${isSelected}
  1401. Has Group: ${hasGroup}
  1402. Has Quantity: ${hasQty}
  1403. Quantity: ${updatedItem.qty}
  1404. Group ID: ${updatedItem.groupId}
  1405. Is Item In Created: ${isItemInCreated(updatedItem.id)}
  1406. Auto-add triggered: ${isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)}
  1407. Processing items: ${Array.from(processingItems).join(', ')}`);
  1408. */
  1409. }
  1410. }, 0);
  1411. return updatedResults;
  1412. });
  1413. } else {
  1414. // Remove from selected IDs
  1415. setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId));
  1416. // Clear the item's group and targetDate when deselected
  1417. setSecondSearchResults(prev => prev.map(item =>
  1418. item.id === itemId
  1419. ? {
  1420. ...item,
  1421. groupId: undefined,
  1422. targetDate: undefined
  1423. }
  1424. : item
  1425. ));
  1426. }
  1427. }, [selectedGroup, isItemInCreated, targetDate]);
  1428. // Handle select all checkbox for current page
  1429. const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: SearchItemWithQty[]) => {
  1430. if (checked) {
  1431. // Select all items on current page that are not already in created items
  1432. const newSelectedIds = paginatedResults
  1433. .filter(item => !isItemInCreated(item.id))
  1434. .map(item => item.id);
  1435. setSelectedSecondSearchItemIds(prev => {
  1436. const existingIds = prev.filter(id => !paginatedResults.some(item => item.id === id));
  1437. return [...existingIds, ...newSelectedIds];
  1438. });
  1439. // Set group and targetDate for all selected items on current page
  1440. setSecondSearchResults(prev => prev.map(item =>
  1441. newSelectedIds.includes(item.id)
  1442. ? {
  1443. ...item,
  1444. groupId: selectedGroup?.id || undefined,
  1445. targetDate: selectedGroup?.targetDate !== undefined && selectedGroup.targetDate !== "" ? selectedGroup.targetDate : undefined
  1446. }
  1447. : item
  1448. ));
  1449. } else {
  1450. // Deselect all items on current page
  1451. const pageItemIds = paginatedResults.map(item => item.id);
  1452. setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(id as number)));
  1453. // Clear group and targetDate for all deselected items on current page
  1454. setSecondSearchResults(prev => prev.map(item =>
  1455. pageItemIds.includes(item.id)
  1456. ? {
  1457. ...item,
  1458. groupId: undefined,
  1459. targetDate: undefined
  1460. }
  1461. : item
  1462. ));
  1463. }
  1464. }, [selectedGroup, isItemInCreated]);
  1465. // Update the CustomSearchResultsTable to use the handlers from component level
  1466. /*
  1467. const CustomSearchResultsTable = () => {
  1468. // Calculate pagination
  1469. const startIndex = (searchResultsPagingController.pageNum - 1) * searchResultsPagingController.pageSize;
  1470. const endIndex = startIndex + searchResultsPagingController.pageSize;
  1471. const paginatedResults = secondSearchResults.slice(startIndex, endIndex);
  1472. // Check if all items on current page are selected
  1473. const allSelectedOnPage = paginatedResults.length > 0 &&
  1474. paginatedResults.every(item => selectedSecondSearchItemIds.includes(item.id));
  1475. // Check if some items on current page are selected
  1476. const someSelectedOnPage = paginatedResults.some(item => selectedSecondSearchItemIds.includes(item.id));
  1477. return (
  1478. <>
  1479. <TableContainer component={Paper}>
  1480. <Table>
  1481. <TableHead>
  1482. <TableRow>
  1483. <TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}>
  1484. {t("Selected")}
  1485. </TableCell>
  1486. <TableCell>
  1487. {t("Item")}
  1488. </TableCell>
  1489. <TableCell>
  1490. {t("Group")}
  1491. </TableCell>
  1492. <TableCell align="right">
  1493. {t("Current Stock")}
  1494. </TableCell>
  1495. <TableCell align="right">
  1496. {t("Stock Unit")}
  1497. </TableCell>
  1498. <TableCell align="right">
  1499. {t("Order Quantity")}
  1500. </TableCell>
  1501. <TableCell align="right">
  1502. {t("Target Date")}
  1503. </TableCell>
  1504. </TableRow>
  1505. </TableHead>
  1506. <TableBody>
  1507. {paginatedResults.length === 0 ? (
  1508. <TableRow>
  1509. <TableCell colSpan={12} align="center">
  1510. <Typography variant="body2" color="text.secondary">
  1511. {t("No data available")}
  1512. </Typography>
  1513. </TableCell>
  1514. </TableRow>
  1515. ) : (
  1516. paginatedResults.map((item) => (
  1517. <TableRow key={item.id}>
  1518. <TableCell padding="checkbox">
  1519. <Checkbox
  1520. checked={selectedSecondSearchItemIds.includes(item.id)}
  1521. onChange={(e) => handleIndividualCheckboxChange(item.id, e.target.checked)}
  1522. disabled={isItemInCreated(item.id)}
  1523. />
  1524. </TableCell>
  1525. <TableCell>
  1526. <Box>
  1527. <Typography variant="body2">
  1528. {item.label.split(' - ')[1] || item.label}
  1529. </Typography>
  1530. <Typography variant="caption" color="textSecondary">
  1531. {item.label.split(' - ')[0] || ''}
  1532. </Typography>
  1533. </Box>
  1534. </TableCell>
  1535. <TableCell>
  1536. <Typography variant="body2">
  1537. {(() => {
  1538. if (item.groupId) {
  1539. const group = groups.find(g => g.id === item.groupId);
  1540. return group?.name || "-";
  1541. }
  1542. return "-"; // Show "-" for unselected items
  1543. })()}
  1544. </Typography>
  1545. </TableCell>
  1546. <TableCell align="right">
  1547. <Typography
  1548. variant="body2"
  1549. color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"}
  1550. sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }}
  1551. >
  1552. {item.currentStockBalance || 0}
  1553. </Typography>
  1554. </TableCell>
  1555. <TableCell align="right">
  1556. <Typography variant="body2">
  1557. {item.uomDesc || "-"}
  1558. </Typography>
  1559. </TableCell>
  1560. <TableCell align="right">
  1561. <TextField
  1562. type="number"
  1563. size="small"
  1564. value={item.qty || ""}
  1565. onChange={(e) => {
  1566. const value = e.target.value;
  1567. // Only allow numbers
  1568. if (value === "" || /^\d+$/.test(value)) {
  1569. const numValue = value === "" ? null : Number(value);
  1570. handleSecondSearchQtyChange(item.id, numValue);
  1571. }
  1572. }}
  1573. onBlur={() => {
  1574. // Trigger auto-add check when user finishes input
  1575. handleQtyBlur(item.id);
  1576. }}
  1577. inputProps={{
  1578. style: { textAlign: 'center' }
  1579. }}
  1580. sx={{
  1581. width: '80px',
  1582. '& .MuiInputBase-input': {
  1583. textAlign: 'center',
  1584. cursor: 'text'
  1585. }
  1586. }}
  1587. />
  1588. </TableCell>
  1589. <TableCell align="right">
  1590. <Typography variant="body2">
  1591. {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
  1592. </Typography>
  1593. </TableCell>
  1594. </TableRow>
  1595. ))
  1596. )}
  1597. </TableBody>
  1598. </Table>
  1599. </TableContainer>
  1600. <TablePagination
  1601. component="div"
  1602. count={secondSearchResults.length}
  1603. page={(searchResultsPagingController.pageNum - 1)} // Convert to 0-based for TablePagination
  1604. rowsPerPage={searchResultsPagingController.pageSize}
  1605. onPageChange={handleSearchResultsPageChange}
  1606. onRowsPerPageChange={handleSearchResultsPageSizeChange}
  1607. rowsPerPageOptions={[10, 25, 50]}
  1608. labelRowsPerPage={t("Rows per page")}
  1609. labelDisplayedRows={({ from, to, count }) =>
  1610. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  1611. }
  1612. />
  1613. </>
  1614. );
  1615. };
  1616. */
  1617. // Add helper function to get group range text
  1618. const getGroupRangeText = useCallback(() => {
  1619. if (groups.length === 0) return "";
  1620. const firstGroup = groups[0];
  1621. const lastGroup = groups[groups.length - 1];
  1622. if (firstGroup.id === lastGroup.id) {
  1623. return `${t("First created group")}: ${firstGroup.name}`;
  1624. } else {
  1625. return `${t("First created group")}: ${firstGroup.name} - ${t("Latest created group")}: ${lastGroup.name}`;
  1626. }
  1627. }, [groups, t]);
  1628. return (
  1629. <FormProvider {...formProps}>
  1630. <Box
  1631. component="form"
  1632. onSubmit={formProps.handleSubmit(onSubmit)}
  1633. >
  1634. {/* First Search Box - Item Search with vertical layout */}
  1635. <Box sx={{ mt: 3, mb: 2 }}>
  1636. <Typography variant="h6" display="block" marginBlockEnd={1}>
  1637. {t("Search Items")}
  1638. </Typography>
  1639. <VerticalSearchBox
  1640. criteria={pickOrderSearchCriteria}
  1641. onSearch={handleSecondSearch}
  1642. onReset={handleSecondReset}
  1643. />
  1644. </Box>
  1645. {/* Create Group Section - 简化版本,不需要表单 */}
  1646. <Box sx={{ mt: 3, mb: 2 }}>
  1647. <Grid container spacing={2} alignItems="center">
  1648. <Grid item>
  1649. <Button
  1650. variant="outlined"
  1651. onClick={handleCreateGroup}
  1652. >
  1653. {t("Create New Group")}
  1654. </Button>
  1655. </Grid>
  1656. {groups.length > 0 && (
  1657. <>
  1658. <Grid item>
  1659. <Typography variant="body2">{t("Group")}:</Typography>
  1660. </Grid>
  1661. <Grid item>
  1662. <FormControl size="small" sx={{ minWidth: 200 }}>
  1663. <Select
  1664. value={selectedGroup?.id?.toString() || ""}
  1665. onChange={(e) => handleGroupChange(e.target.value)}
  1666. >
  1667. {groups.map((group) => (
  1668. <MenuItem key={group.id} value={group.id.toString()}>
  1669. {group.name}
  1670. </MenuItem>
  1671. ))}
  1672. </Select>
  1673. </FormControl>
  1674. </Grid>
  1675. {selectedGroup && (
  1676. <Grid item>
  1677. <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk">
  1678. <DatePicker
  1679. value={selectedGroup.targetDate && selectedGroup.targetDate !== "" ? dayjs(selectedGroup.targetDate) : null}
  1680. onChange={(date) => {
  1681. if (date) {
  1682. const formattedDate = date.format(INPUT_DATE_FORMAT);
  1683. handleGroupTargetDateChange(selectedGroup.id, formattedDate);
  1684. }
  1685. }}
  1686. slotProps={{
  1687. textField: {
  1688. size: "small",
  1689. label: t("Target Date"),
  1690. sx: { width: 180 }
  1691. },
  1692. }}
  1693. />
  1694. </LocalizationProvider>
  1695. </Grid>
  1696. )}
  1697. </>
  1698. )}
  1699. </Grid>
  1700. {/* Add group range text */}
  1701. {groups.length > 0 && (
  1702. <Box sx={{ mt: 1 }}>
  1703. <Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
  1704. {getGroupRangeText()}
  1705. </Typography>
  1706. </Box>
  1707. )}
  1708. </Box>
  1709. {/* Second Search Results - Use custom table like AssignAndRelease */}
  1710. {hasSearchedSecond && (
  1711. <Box sx={{ mt: 3 }}>
  1712. <Typography variant="h6" marginBlockEnd={2}>
  1713. {t("Search Results")} ({secondSearchResults.length})
  1714. </Typography>
  1715. {selectedSecondSearchItemIds.length > 0 && (
  1716. <Box sx={{ mb: 2 }}>
  1717. <Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
  1718. {t("Selected items will join above created group")}
  1719. </Typography>
  1720. </Box>
  1721. )}
  1722. {isLoadingSecondSearch ? (
  1723. <Typography>{t("Loading...")}</Typography>
  1724. ) : secondSearchResults.length === 0 ? (
  1725. <Typography color="textSecondary">{t("No results found")}</Typography>
  1726. ) : (
  1727. <SearchResultsTable
  1728. items={secondSearchResults}
  1729. selectedItemIds={selectedSecondSearchItemIds}
  1730. groups={groups}
  1731. onItemSelect={handleIndividualCheckboxChange}
  1732. onQtyChange={handleSecondSearchQtyChange}
  1733. onGroupChange={handleCreatedItemGroupChange}
  1734. onQtyBlur={handleQtyBlur}
  1735. isItemInCreated={isItemInCreated}
  1736. pageNum={searchResultsPagingController.pageNum}
  1737. pageSize={searchResultsPagingController.pageSize}
  1738. onPageChange={handleSearchResultsPageChange}
  1739. onPageSizeChange={handleSearchResultsPageSizeChange}
  1740. />
  1741. )}
  1742. </Box>
  1743. )}
  1744. {/* Add Submit Button between tables */}
  1745. {/* Search Results with SearchResults component */}
  1746. {hasSearchedSecond && secondSearchResults.length > 0 && selectedSecondSearchItemIds.length > 0 && (
  1747. <Box sx={{ mt: 2, mb: 2 }}>
  1748. <Box sx={{ display: 'flex', justifyContent: 'left', alignItems: 'center', gap: 2 }}>
  1749. <Button
  1750. variant="contained"
  1751. color="primary"
  1752. onClick={handleAddSelectedToCreatedItems}
  1753. disabled={selectedSecondSearchItemIds.length === 0 || !areSelectedItemsValid()}
  1754. sx={{ minWidth: 200 }}
  1755. >
  1756. {t("Add Selected Items to Created Items")} ({selectedSecondSearchItemIds.length})
  1757. </Button>
  1758. {selectedSecondSearchItemIds.length > 0 && !areSelectedItemsValid() && (
  1759. <Typography
  1760. variant="body2"
  1761. color="error.main"
  1762. sx={{ fontStyle: 'italic' }}
  1763. >
  1764. {getValidationMessage()}
  1765. </Typography>
  1766. )}
  1767. </Box>
  1768. </Box>
  1769. )}
  1770. {/* 创建项目区域 - 修改Group列为可选择的 */}
  1771. {createdItems.length > 0 && (
  1772. <Box sx={{ mt: 3 }}>
  1773. <Typography variant="h6" marginBlockEnd={2}>
  1774. {t("Created Items")} ({createdItems.length})
  1775. </Typography>
  1776. <CreatedItemsTable
  1777. items={createdItems}
  1778. groups={groups}
  1779. onItemSelect={handleCreatedItemSelect}
  1780. onQtyChange={handleQtyChange}
  1781. onGroupChange={handleCreatedItemGroupChange}
  1782. pageNum={createdItemsPagingController.pageNum}
  1783. pageSize={createdItemsPagingController.pageSize}
  1784. onPageChange={handleCreatedItemsPageChange}
  1785. onPageSizeChange={handleCreatedItemsPageSizeChange}
  1786. />
  1787. </Box>
  1788. )}
  1789. {/* 操作按钮 */}
  1790. <Stack direction="row" justifyContent="flex-start" gap={1} sx={{ mt: 3 }}>
  1791. <Button
  1792. name="submit"
  1793. variant="contained"
  1794. startIcon={<Check />}
  1795. type="submit"
  1796. disabled={createdItems.filter(item => item.isSelected).length === 0}
  1797. >
  1798. {t("Create Pick Order")}
  1799. </Button>
  1800. <Button
  1801. name="reset"
  1802. variant="outlined"
  1803. onClick={handleReset}
  1804. >
  1805. {t("reset")}
  1806. </Button>
  1807. </Stack>
  1808. </Box>
  1809. </FormProvider>
  1810. );
  1811. };
  1812. export default NewCreateItem;