FPSMS-frontend
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

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