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

newcreatitem.tsx 71 KiB

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