FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1250 line
42 KiB

  1. "use client";
  2. import { createPickOrder, SavePickOrderRequest, SavePickOrderLineRequest } 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. } from "@mui/material";
  21. import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
  22. import { useTranslation } from "react-i18next";
  23. import { useCallback, useEffect, useMemo, useState } from "react";
  24. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  25. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  26. import dayjs from "dayjs";
  27. import { Check, Search } from "@mui/icons-material";
  28. import { ItemCombo, fetchAllItemsInClient } from "@/app/api/settings/item/actions";
  29. import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  30. import SearchResults, { Column } from "../SearchResults/SearchResults";
  31. import { fetchJobOrderDetailByCode } from "@/app/api/jo/actions";
  32. import SearchBox, { Criterion } from "../SearchBox";
  33. type Props = {
  34. filterArgs?: Record<string, any>;
  35. searchQuery?: Record<string, any>;
  36. };
  37. // 扩展表单类型以包含搜索字段
  38. interface SearchFormData extends SavePickOrderRequest {
  39. searchCode?: string;
  40. searchName?: string;
  41. }
  42. interface CreatedItem {
  43. itemId: number;
  44. itemName: string;
  45. itemCode: string;
  46. qty: number;
  47. uom: string;
  48. uomId: number;
  49. isSelected: boolean;
  50. currentStockBalance?: number;
  51. targetDate?: string; // Make it optional to match the source
  52. }
  53. // Add interface for search items with quantity
  54. interface SearchItemWithQty extends ItemCombo {
  55. qty: number | null; // Changed from number to number | null
  56. jobOrderCode?: string;
  57. jobOrderId?: number;
  58. currentStockBalance?: number;
  59. targetDate?: string;
  60. }
  61. interface JobOrderDetailPickLine {
  62. id: number;
  63. code: string;
  64. name: string;
  65. lotNo: string | null;
  66. reqQty: number;
  67. uom: string;
  68. status: string;
  69. }
  70. const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery }) => {
  71. const { t } = useTranslation("pickOrder");
  72. const [items, setItems] = useState<ItemCombo[]>([]);
  73. const [filteredItems, setFilteredItems] = useState<SearchItemWithQty[]>([]);
  74. const [createdItems, setCreatedItems] = useState<CreatedItem[]>([]);
  75. const [isLoading, setIsLoading] = useState(false);
  76. const [hasSearched, setHasSearched] = useState(false);
  77. // Add state for selected item IDs in search results
  78. const [selectedSearchItemIds, setSelectedSearchItemIds] = useState<(string | number)[]>([]);
  79. // Add state for second search
  80. const [secondSearchQuery, setSecondSearchQuery] = useState<Record<string, any>>({});
  81. const [secondSearchResults, setSecondSearchResults] = useState<SearchItemWithQty[]>([]);
  82. const [isLoadingSecondSearch, setIsLoadingSecondSearch] = useState(false);
  83. const [hasSearchedSecond, setHasSearchedSecond] = useState(false);
  84. // Add selection state for second search
  85. const [selectedSecondSearchItemIds, setSelectedSecondSearchItemIds] = useState<(string | number)[]>([]);
  86. const formProps = useForm<SearchFormData>();
  87. const errors = formProps.formState.errors;
  88. const targetDate = formProps.watch("targetDate");
  89. const type = formProps.watch("type");
  90. const searchCode = formProps.watch("searchCode");
  91. const searchName = formProps.watch("searchName");
  92. const [jobOrderItems, setJobOrderItems] = useState<JobOrderDetailPickLine[]>([]);
  93. const [isLoadingJobOrder, setIsLoadingJobOrder] = useState(false);
  94. useEffect(() => {
  95. const loadItems = async () => {
  96. try {
  97. const itemsData = await fetchAllItemsInClient();
  98. console.log("Loaded items:", itemsData);
  99. setItems(itemsData);
  100. setFilteredItems([]);
  101. } catch (error) {
  102. console.error("Error loading items:", error);
  103. }
  104. };
  105. loadItems();
  106. }, []);
  107. const searchJobOrderItems = useCallback(async (jobOrderCode: string) => {
  108. if (!jobOrderCode.trim()) return;
  109. setIsLoadingJobOrder(true);
  110. try {
  111. const jobOrderDetail = await fetchJobOrderDetailByCode(jobOrderCode);
  112. setJobOrderItems(jobOrderDetail.pickLines || []);
  113. // Convert Job Order items to SearchItemWithQty format
  114. const convertedItems = (jobOrderDetail.pickLines || []).map(item => ({
  115. id: item.id,
  116. label: item.name,
  117. qty: item.reqQty, // Pre-fill with required quantity
  118. uom: item.uom,
  119. uomId: 0, // We'll need to get this from the item lookup
  120. jobOrderCode: jobOrderDetail.code,
  121. jobOrderId: jobOrderDetail.id,
  122. }));
  123. setFilteredItems(convertedItems);
  124. setHasSearched(true);
  125. } catch (error) {
  126. console.error("Error fetching Job Order items:", error);
  127. alert(t("Job Order not found or has no items"));
  128. } finally {
  129. setIsLoadingJobOrder(false);
  130. }
  131. }, [t]);
  132. // Update useEffect to handle Job Order search
  133. useEffect(() => {
  134. if (searchQuery && searchQuery.jobOrderCode) {
  135. searchJobOrderItems(searchQuery.jobOrderCode);
  136. } else if (searchQuery && items.length > 0) {
  137. // Existing item search logic
  138. // ... your existing search logic
  139. }
  140. }, [searchQuery, items, searchJobOrderItems]);
  141. useEffect(() => {
  142. if (searchQuery && items.length > 0) {
  143. const hasValidSearch = (
  144. (searchQuery.items && searchQuery.items.trim && searchQuery.items.trim() !== "") ||
  145. (searchQuery.code && searchQuery.code.trim && searchQuery.code.trim() !== "") ||
  146. (searchQuery.type && searchQuery.type !== "All")
  147. );
  148. if (hasValidSearch) {
  149. let filtered = items;
  150. if (searchQuery.items) {
  151. const itemsToSearch = Array.isArray(searchQuery.items)
  152. ? searchQuery.items
  153. : [searchQuery.items];
  154. if (itemsToSearch.length > 0 && !itemsToSearch.includes("All")) {
  155. filtered = filtered.filter(item =>
  156. itemsToSearch.some((searchItem: string) =>
  157. item.label.toLowerCase().includes(searchItem.toLowerCase())
  158. )
  159. );
  160. }
  161. }
  162. if (searchQuery.code) {
  163. filtered = filtered.filter(item =>
  164. item.label.toLowerCase().includes(searchQuery.code.toLowerCase())
  165. );
  166. }
  167. if (searchQuery.type && searchQuery.type !== "All") {
  168. // filtered = filtered.filter(item => item.type === searchQuery.type);
  169. }
  170. // Convert to SearchItemWithQty with default qty = null
  171. const filteredWithQty = filtered.slice(0, 10).map(item => ({
  172. ...item,
  173. qty: null // Changed from 1 to null
  174. }));
  175. setFilteredItems(filteredWithQty);
  176. setHasSearched(true);
  177. } else {
  178. setFilteredItems([]);
  179. setHasSearched(false);
  180. }
  181. } else {
  182. setFilteredItems([]);
  183. setHasSearched(false);
  184. }
  185. }, [searchQuery, items]);
  186. useEffect(() => {
  187. if (searchQuery) {
  188. if (searchQuery.type) {
  189. formProps.setValue("type", searchQuery.type);
  190. }
  191. if (searchQuery.targetDate) {
  192. formProps.setValue("targetDate", searchQuery.targetDate);
  193. }
  194. if (searchQuery.code) {
  195. formProps.setValue("searchCode", searchQuery.code);
  196. }
  197. if (searchQuery.items) {
  198. formProps.setValue("searchName", searchQuery.items);
  199. }
  200. }
  201. }, [searchQuery, formProps]);
  202. useEffect(() => {
  203. setFilteredItems([]);
  204. setHasSearched(false);
  205. }, []);
  206. const typeList = [
  207. { type: "Consumable" },
  208. { type: "Material" },
  209. { type: "Product" }
  210. ];
  211. const handleTypeChange = useCallback(
  212. (event: React.SyntheticEvent, newValue: {type: string} | null) => {
  213. formProps.setValue("type", newValue?.type || "");
  214. },
  215. [formProps],
  216. );
  217. const handleSearch = useCallback(() => {
  218. if (!type) {
  219. alert(t("Please select type"));
  220. return;
  221. }
  222. if (!searchCode && !searchName) {
  223. alert(t("Please enter at least code or name"));
  224. return;
  225. }
  226. setIsLoading(true);
  227. setHasSearched(true);
  228. console.log("Searching with:", { type, searchCode, searchName, targetDate, itemsCount: items.length });
  229. setTimeout(() => {
  230. let filtered = items;
  231. if (searchCode && searchCode.trim()) {
  232. filtered = filtered.filter(item =>
  233. item.label.toLowerCase().includes(searchCode.toLowerCase())
  234. );
  235. console.log("After code filter:", filtered.length);
  236. }
  237. if (searchName && searchName.trim()) {
  238. filtered = filtered.filter(item =>
  239. item.label.toLowerCase().includes(searchName.toLowerCase())
  240. );
  241. console.log("After name filter:", filtered.length);
  242. }
  243. // Convert to SearchItemWithQty with default qty = null and include targetDate
  244. const filteredWithQty = filtered.slice(0, 100).map(item => ({
  245. ...item,
  246. qty: null,
  247. targetDate: targetDate, // Add target date to each item
  248. }));
  249. console.log("Final filtered results:", filteredWithQty.length);
  250. setFilteredItems(filteredWithQty);
  251. setIsLoading(false);
  252. }, 500);
  253. }, [type, searchCode, searchName, targetDate, items, t]); // Add targetDate back to dependencies
  254. // Handle quantity change in search results
  255. const handleSearchQtyChange = useCallback((itemId: number, newQty: number | null) => {
  256. setFilteredItems(prev =>
  257. prev.map(item =>
  258. item.id === itemId ? { ...item, qty: newQty } : item
  259. )
  260. );
  261. // Auto-update created items if this item exists there
  262. setCreatedItems(prev =>
  263. prev.map(item =>
  264. item.itemId === itemId ? { ...item, qty: newQty || 1 } : item
  265. )
  266. );
  267. }, []);
  268. // Modified handler for search item selection
  269. const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => {
  270. if (isSelected) {
  271. const item = filteredItems.find(i => i.id === itemId);
  272. if (!item) return;
  273. const existingItem = createdItems.find(created => created.itemId === item.id);
  274. if (existingItem) {
  275. alert(t("Item already exists in created items"));
  276. return;
  277. }
  278. const newCreatedItem: CreatedItem = {
  279. itemId: item.id,
  280. itemName: item.label,
  281. itemCode: item.label,
  282. qty: item.qty || 1,
  283. uom: item.uom || "",
  284. uomId: item.uomId || 0,
  285. isSelected: true,
  286. currentStockBalance: item.currentStockBalance,
  287. targetDate: item.targetDate || targetDate, // Use item's targetDate or fallback to form's targetDate
  288. };
  289. setCreatedItems(prev => [...prev, newCreatedItem]);
  290. }
  291. }, [filteredItems, createdItems, t, targetDate]);
  292. // Handler for created item selection
  293. const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean) => {
  294. setCreatedItems(prev =>
  295. prev.map(item =>
  296. item.itemId === itemId ? { ...item, isSelected } : item
  297. )
  298. );
  299. }, []);
  300. const handleQtyChange = useCallback((itemId: number, newQty: number) => {
  301. setCreatedItems(prev =>
  302. prev.map(item =>
  303. item.itemId === itemId ? { ...item, qty: newQty } : item
  304. )
  305. );
  306. }, []);
  307. // Check if item is already in created items
  308. const isItemInCreated = useCallback((itemId: number) => {
  309. return createdItems.some(item => item.itemId === itemId);
  310. }, [createdItems]);
  311. const onSubmit = useCallback<SubmitHandler<SearchFormData>>(
  312. async (data, event) => {
  313. const selectedCreatedItems = createdItems.filter(item => item.isSelected);
  314. if (selectedCreatedItems.length === 0) {
  315. alert(t("Please select at least one item to submit"));
  316. return;
  317. }
  318. if (!data.type) {
  319. alert(t("Please select product type"));
  320. return;
  321. }
  322. if (!data.targetDate) {
  323. alert(t("Please select target date"));
  324. return;
  325. }
  326. let formattedTargetDate = data.targetDate;
  327. if (data.targetDate && typeof data.targetDate === 'string') {
  328. try {
  329. const date = dayjs(data.targetDate);
  330. formattedTargetDate = date.format('YYYY-MM-DD');
  331. } catch (error) {
  332. console.error("Invalid date format:", data.targetDate);
  333. alert(t("Invalid date format"));
  334. return;
  335. }
  336. }
  337. const pickOrderData: SavePickOrderRequest = {
  338. type: data.type || "Consumable",
  339. targetDate: formattedTargetDate,
  340. pickOrderLine: selectedCreatedItems.map(item => ({
  341. itemId: item.itemId,
  342. qty: item.qty,
  343. uomId: item.uomId
  344. } as SavePickOrderLineRequest))
  345. };
  346. console.log("Submitting pick order:", pickOrderData);
  347. try {
  348. const res = await createPickOrder(pickOrderData);
  349. if (res.id) {
  350. console.log("Pick order created successfully:", res);
  351. setCreatedItems(prev => prev.filter(item => !item.isSelected));
  352. formProps.reset();
  353. setHasSearched(false);
  354. setFilteredItems([]);
  355. alert(t("Pick order created successfully"));
  356. }
  357. } catch (error) {
  358. console.error("Error creating pick order:", error);
  359. alert(t("Failed to create pick order"));
  360. }
  361. },
  362. [createdItems, t, formProps]
  363. );
  364. const handleReset = useCallback(() => {
  365. formProps.reset();
  366. setCreatedItems([]);
  367. setHasSearched(false);
  368. setFilteredItems([]);
  369. }, [formProps]);
  370. // Pagination state
  371. const [page, setPage] = useState(0);
  372. const [rowsPerPage, setRowsPerPage] = useState(10);
  373. // Handle page change
  374. const handleChangePage = (
  375. _event: React.MouseEvent | React.KeyboardEvent,
  376. newPage: number,
  377. ) => {
  378. console.log(_event);
  379. setPage(newPage);
  380. // The original code had setPagingController and defaultPagingController,
  381. // but these are not defined in the provided context.
  382. // Assuming they are meant to be part of a larger context or will be added.
  383. // For now, commenting out the setPagingController part as it's not defined.
  384. // if (setPagingController) {
  385. // setPagingController({
  386. // ...(pagingController ?? defaultPagingController),
  387. // pageNum: newPage + 1,
  388. // });
  389. // }
  390. };
  391. // Handle rows per page change
  392. const handleChangeRowsPerPage = (
  393. event: React.ChangeEvent<HTMLInputElement>,
  394. ) => {
  395. console.log(event);
  396. setRowsPerPage(+event.target.value);
  397. setPage(0);
  398. // The original code had setPagingController and defaultPagingController,
  399. // but these are not defined in the provided context.
  400. // Assuming they are meant to be part of a larger context or will be added.
  401. // For now, commenting out the setPagingController part as it's not defined.
  402. // if (setPagingController) {
  403. // setPagingController({
  404. // ...(pagingController ?? defaultPagingController),
  405. // pageNum: 1,
  406. // });
  407. // }
  408. };
  409. // Add checkbox change handler for first search
  410. const handleSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => {
  411. if (typeof ids === 'function') {
  412. const newIds = ids(selectedSearchItemIds);
  413. setSelectedSearchItemIds(newIds);
  414. if (newIds.length === filteredItems.length) {
  415. filteredItems.forEach(item => {
  416. if (!isItemInCreated(item.id)) {
  417. handleSearchItemSelect(item.id, true);
  418. }
  419. });
  420. } else {
  421. filteredItems.forEach(item => {
  422. const isSelected = newIds.includes(item.id);
  423. const isCurrentlyInCreated = isItemInCreated(item.id);
  424. if (isSelected && !isCurrentlyInCreated) {
  425. handleSearchItemSelect(item.id, true);
  426. } else if (!isSelected && isCurrentlyInCreated) {
  427. setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id));
  428. }
  429. });
  430. }
  431. } else {
  432. const previousIds = selectedSearchItemIds;
  433. setSelectedSearchItemIds(ids);
  434. const newlySelected = ids.filter(id => !previousIds.includes(id));
  435. const newlyDeselected = previousIds.filter(id => !ids.includes(id));
  436. newlySelected.forEach(id => {
  437. if (!isItemInCreated(id as number)) {
  438. handleSearchItemSelect(id as number, true);
  439. }
  440. });
  441. newlyDeselected.forEach(id => {
  442. setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id));
  443. });
  444. }
  445. }, [selectedSearchItemIds, filteredItems, isItemInCreated, handleSearchItemSelect]);
  446. // Define columns for SearchResults
  447. const searchItemColumns: Column<SearchItemWithQty>[] = useMemo(() => [
  448. {
  449. name: "id",
  450. label: "",
  451. type: "checkbox",
  452. disabled: (item) => isItemInCreated(item.id), // Disable if already in created items
  453. },
  454. {
  455. name: "label",
  456. label: t("Item"),
  457. renderCell: (item) => {
  458. const parts = item.label.split(' - ');
  459. const code = parts[0] || '';
  460. const name = parts[1] || '';
  461. return (
  462. <Box>
  463. <Typography variant="body2">
  464. {name} {/* 显示项目名称 */}
  465. </Typography>
  466. <Typography variant="caption" color="textSecondary">
  467. {code} {/* 显示项目代码 */}
  468. </Typography>
  469. </Box>
  470. );
  471. },
  472. },
  473. {
  474. name: "qty",
  475. label: t("Order Quantity"),
  476. renderCell: (item) => (
  477. <TextField
  478. type="number"
  479. size="small"
  480. value={item.qty || ""} // Show empty string if qty is null
  481. onChange={(e) => {
  482. const value = e.target.value;
  483. const numValue = value === "" ? null : Number(value);
  484. handleSearchQtyChange(item.id, numValue);
  485. }}
  486. onKeyDown={(e) => {
  487. // Allow typing numbers, backspace, delete, arrow keys
  488. if (!/[0-9]/.test(e.key) &&
  489. !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(e.key)) {
  490. e.preventDefault();
  491. }
  492. }}
  493. inputProps={{
  494. min: 1,
  495. step: 1,
  496. style: { textAlign: 'center' } // Center the text
  497. }}
  498. sx={{
  499. width: '80px',
  500. '& .MuiInputBase-input': {
  501. textAlign: 'center',
  502. cursor: 'text'
  503. }
  504. }}
  505. />
  506. ),
  507. },
  508. {
  509. name: "currentStockBalance",
  510. label: t("Current Stock"),
  511. renderCell: (item) => {
  512. const stockBalance = item.currentStockBalance || 0;
  513. return (
  514. <Typography
  515. variant="body2"
  516. color={stockBalance > 0 ? "success.main" : "error.main"}
  517. sx={{ fontWeight: stockBalance > 0 ? 'bold' : 'normal' }}
  518. >
  519. {stockBalance}
  520. </Typography>
  521. );
  522. },
  523. },
  524. {
  525. name: "targetDate",
  526. label: t("Target Date"),
  527. renderCell: (item) => (
  528. <Typography variant="body2">
  529. {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
  530. </Typography>
  531. ),
  532. },
  533. {
  534. name: "uom",
  535. label: t("Unit"),
  536. renderCell: (item) => item.uom || "-",
  537. },
  538. ], [t, isItemInCreated, handleSearchQtyChange]);
  539. const pickOrderSearchCriteria: Criterion<any>[] = useMemo(
  540. () => [
  541. {
  542. label: t("Product Type"),
  543. paramName: "type",
  544. type: "autocomplete",
  545. options: [
  546. { value: "Consumable", label: t("Consumable") },
  547. { value: "MATERIAL", label: t("Material") },
  548. { value: "JOB_ORDER", label: t("Job Order") }
  549. ],
  550. },
  551. {
  552. label: t("Item Code"),
  553. paramName: "code",
  554. type: "text"
  555. },
  556. {
  557. label: t("Item Name"),
  558. paramName: "name",
  559. type: "text"
  560. },
  561. ],
  562. [t],
  563. );
  564. // Add search handler for second search (same as first search)
  565. const handleSecondSearch = useCallback((query: Record<string, any>) => {
  566. console.log("Second search triggered with query:", query);
  567. setSecondSearchQuery({ ...query });
  568. setIsLoadingSecondSearch(true);
  569. // 同步第二个搜索框的信息到表单 - 确保类型值正确
  570. if (query.type) {
  571. // 确保类型值符合后端枚举格式
  572. let correctType = query.type;
  573. if (query.type === "consumable") {
  574. correctType = "Consumable";
  575. } else if (query.type === "material") {
  576. correctType = "MATERIAL";
  577. } else if (query.type === "jo") {
  578. correctType = "JOB_ORDER";
  579. }
  580. formProps.setValue("type", correctType);
  581. }
  582. // 设置默认目标日期为今天
  583. const today = dayjs().format(INPUT_DATE_FORMAT);
  584. formProps.setValue("targetDate", today);
  585. setTimeout(() => {
  586. let filtered = items;
  587. // Same filtering logic as first search
  588. if (query.code && query.code.trim()) {
  589. filtered = filtered.filter(item =>
  590. item.label.toLowerCase().includes(query.code.toLowerCase())
  591. );
  592. }
  593. if (query.name && query.name.trim()) {
  594. filtered = filtered.filter(item =>
  595. item.label.toLowerCase().includes(query.name.toLowerCase())
  596. );
  597. }
  598. if (query.type && query.type !== "All") {
  599. // Filter by type if needed
  600. }
  601. // Convert to SearchItemWithQty with default qty = null and today's date
  602. const filteredWithQty = filtered.slice(0, 100).map(item => ({
  603. ...item,
  604. qty: null,
  605. targetDate: today, // 使用今天的日期作为默认值
  606. }));
  607. setSecondSearchResults(filteredWithQty);
  608. setHasSearchedSecond(true);
  609. setIsLoadingSecondSearch(false);
  610. }, 500);
  611. }, [items, formProps]);
  612. // Add reset handler for second search
  613. const handleSecondReset = useCallback(() => {
  614. console.log("Second search reset");
  615. setSecondSearchQuery({});
  616. setSecondSearchResults([]);
  617. setHasSearchedSecond(false);
  618. // 清空表单中的类型,但保留今天的日期
  619. formProps.setValue("type", "");
  620. const today = dayjs().format(INPUT_DATE_FORMAT);
  621. formProps.setValue("targetDate", today);
  622. }, [formProps]);
  623. // Add quantity change handler for second search
  624. const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => {
  625. setSecondSearchResults(prev =>
  626. prev.map(item =>
  627. item.id === itemId ? { ...item, qty: newQty } : item
  628. )
  629. );
  630. // Auto-update created items if this item exists there
  631. setCreatedItems(prev =>
  632. prev.map(item =>
  633. item.itemId === itemId ? { ...item, qty: newQty || 1 } : item
  634. )
  635. );
  636. }, []);
  637. // Add item selection handler for second search
  638. const handleSecondSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => {
  639. if (isSelected) {
  640. const item = secondSearchResults.find(i => i.id === itemId);
  641. if (!item) return;
  642. const existingItem = createdItems.find(created => created.itemId === item.id);
  643. if (existingItem) {
  644. alert(t("Item already exists in created items"));
  645. return;
  646. }
  647. const newCreatedItem: CreatedItem = {
  648. itemId: item.id,
  649. itemName: item.label,
  650. itemCode: item.label,
  651. qty: item.qty || 1,
  652. uom: item.uom || "",
  653. uomId: item.uomId || 0,
  654. isSelected: true,
  655. currentStockBalance: item.currentStockBalance,
  656. targetDate: item.targetDate || targetDate,
  657. };
  658. setCreatedItems(prev => [...prev, newCreatedItem]);
  659. }
  660. }, [secondSearchResults, createdItems, t, targetDate]);
  661. // Add checkbox change handler for second search
  662. const handleSecondSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => {
  663. if (typeof ids === 'function') {
  664. const newIds = ids(selectedSecondSearchItemIds);
  665. setSelectedSecondSearchItemIds(newIds);
  666. // 处理全选逻辑 - 选择所有搜索结果,不仅仅是当前页面
  667. if (newIds.length === secondSearchResults.length) {
  668. // 全选:将所有搜索结果添加到创建项目
  669. secondSearchResults.forEach(item => {
  670. if (!isItemInCreated(item.id)) {
  671. handleSecondSearchItemSelect(item.id, true);
  672. }
  673. });
  674. } else {
  675. // 部分选择:只处理当前页面的选择
  676. secondSearchResults.forEach(item => {
  677. const isSelected = newIds.includes(item.id);
  678. const isCurrentlyInCreated = isItemInCreated(item.id);
  679. if (isSelected && !isCurrentlyInCreated) {
  680. handleSecondSearchItemSelect(item.id, true);
  681. } else if (!isSelected && isCurrentlyInCreated) {
  682. setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id));
  683. }
  684. });
  685. }
  686. } else {
  687. const previousIds = selectedSecondSearchItemIds;
  688. setSelectedSecondSearchItemIds(ids);
  689. const newlySelected = ids.filter(id => !previousIds.includes(id));
  690. const newlyDeselected = previousIds.filter(id => !ids.includes(id));
  691. newlySelected.forEach(id => {
  692. if (!isItemInCreated(id as number)) {
  693. handleSecondSearchItemSelect(id as number, true);
  694. }
  695. });
  696. newlyDeselected.forEach(id => {
  697. setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id));
  698. });
  699. }
  700. }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]);
  701. // Define columns for second search (same as first search but with different handlers)
  702. const secondSearchItemColumns: Column<SearchItemWithQty>[] = useMemo(() => [
  703. {
  704. name: "id",
  705. label: "",
  706. type: "checkbox",
  707. disabled: (item) => isItemInCreated(item.id),
  708. },
  709. {
  710. name: "label",
  711. label: t("Item"),
  712. renderCell: (item) => {
  713. // 格式化标签显示:将 "CODE - NAME" 格式化为更友好的显示
  714. const parts = item.label.split(' - ');
  715. const code = parts[0] || '';
  716. const name = parts[1] || '';
  717. return (
  718. <Box>
  719. <Typography variant="body2">
  720. {name} {/* 显示项目名称 */}
  721. </Typography>
  722. <Typography variant="caption" color="textSecondary">
  723. {code} {/* 显示项目代码 */}
  724. </Typography>
  725. </Box>
  726. );
  727. },
  728. },
  729. {
  730. name: "currentStockBalance",
  731. label: t("Current Stock"),
  732. renderCell: (item) => {
  733. const stockBalance = item.currentStockBalance || 0;
  734. return (
  735. <Typography
  736. variant="body2"
  737. color={stockBalance > 0 ? "success.main" : "error.main"}
  738. sx={{ fontWeight: stockBalance > 0 ? 'bold' : 'normal' }}
  739. >
  740. {stockBalance}
  741. </Typography>
  742. );
  743. },
  744. },
  745. {
  746. name: "uom",
  747. label: t("Unit"),
  748. renderCell: (item) => item.uom || "-",
  749. },
  750. {
  751. name: "qty",
  752. label: t("Order Quantity"),
  753. renderCell: (item) => (
  754. <TextField
  755. type="number"
  756. size="small"
  757. value={item.qty || ""}
  758. onChange={(e) => {
  759. const value = e.target.value;
  760. const numValue = value === "" ? null : Number(value);
  761. handleSecondSearchQtyChange(item.id, numValue);
  762. }}
  763. onKeyDown={(e) => {
  764. if (!/[0-9]/.test(e.key) &&
  765. !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(e.key)) {
  766. e.preventDefault();
  767. }
  768. }}
  769. inputProps={{
  770. min: 1,
  771. step: 1,
  772. style: { textAlign: 'center' }
  773. }}
  774. sx={{
  775. width: '80px',
  776. '& .MuiInputBase-input': {
  777. textAlign: 'center',
  778. cursor: 'text'
  779. }
  780. }}
  781. />
  782. ),
  783. },
  784. {
  785. name: "targetDate",
  786. label: t("Target Date"),
  787. renderCell: (item) => (
  788. <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk">
  789. <DatePicker
  790. value={item.targetDate ? dayjs(item.targetDate) : dayjs()}
  791. onChange={(date) => {
  792. if (date) {
  793. const formattedDate = date.format(INPUT_DATE_FORMAT);
  794. // 更新搜索结果中的目标日期
  795. setSecondSearchResults(prev =>
  796. prev.map(searchItem =>
  797. searchItem.id === item.id ? { ...searchItem, targetDate: formattedDate } : searchItem
  798. )
  799. );
  800. // 更新创建项目中的目标日期
  801. setCreatedItems(prev =>
  802. prev.map(createdItem =>
  803. createdItem.itemId === item.id ? { ...createdItem, targetDate: formattedDate } : createdItem
  804. )
  805. );
  806. // 更新表单中的目标日期
  807. formProps.setValue("targetDate", formattedDate);
  808. }
  809. }}
  810. slotProps={{
  811. textField: {
  812. size: "small",
  813. sx: {
  814. width: '160px', // 增加宽度以显示完整日期
  815. '& .MuiInputBase-input': {
  816. fontSize: '0.875rem' // 稍微减小字体以适应更多内容
  817. }
  818. }
  819. },
  820. }}
  821. />
  822. </LocalizationProvider>
  823. ),
  824. },
  825. ], [t, isItemInCreated, handleSecondSearchQtyChange, formProps]);
  826. return (
  827. <FormProvider {...formProps}>
  828. <Box
  829. component="form"
  830. onSubmit={formProps.handleSubmit(onSubmit)}
  831. >
  832. {/*<Grid container spacing={2}>
  833. <Grid item xs={12}>
  834. <Typography variant="h6" display="block" marginBlockEnd={1}>
  835. {t("Pick Order Detail")}
  836. </Typography>
  837. </Grid>
  838. {/*
  839. <Grid item xs={2}>
  840. <FormControl fullWidth>
  841. <Autocomplete
  842. disableClearable
  843. fullWidth
  844. getOptionLabel={(option) => option.type}
  845. options={typeList}
  846. onChange={handleTypeChange}
  847. renderInput={(params) => <TextField {...params} label={t("Product Type")} required/>}
  848. />
  849. </FormControl>
  850. </Grid>
  851. <Grid item xs={2}>
  852. <Controller
  853. control={formProps.control}
  854. name="searchCode"
  855. render={({ field }) => (
  856. <TextField
  857. {...field}
  858. fullWidth
  859. label={t("Pick Order Code")}
  860. //placeholder={t("Enter Pick Order Code")}
  861. />
  862. )}
  863. />
  864. </Grid>
  865. <Grid item xs={2}>
  866. <Controller
  867. control={formProps.control}
  868. name="searchName"
  869. render={({ field }) => (
  870. <TextField
  871. {...field}
  872. fullWidth
  873. label={t("name")}
  874. //placeholder={t("Enter item name")}
  875. />
  876. )}
  877. />
  878. </Grid>
  879. <Grid item xs={3}>
  880. <Controller
  881. control={formProps.control}
  882. name="targetDate"
  883. render={({ field }) => (
  884. <LocalizationProvider
  885. dateAdapter={AdapterDayjs}
  886. adapterLocale="zh-hk"
  887. >
  888. <DatePicker
  889. {...field}
  890. sx={{ width: "100%" }}
  891. label={t("targetDate")}
  892. value={targetDate ? dayjs(targetDate) : undefined}
  893. onChange={(date) => {
  894. if (!date) return;
  895. formProps.setValue("targetDate", date.format(INPUT_DATE_FORMAT));
  896. }}
  897. inputRef={field.ref}
  898. slotProps={{
  899. textField: {
  900. error: Boolean(errors.targetDate?.message),
  901. helperText: errors.targetDate?.message,
  902. },
  903. }}
  904. />
  905. </LocalizationProvider>
  906. )}
  907. />
  908. </Grid>
  909. <Grid item xs={3}>
  910. <Button
  911. variant="contained"
  912. startIcon={<Search />}
  913. onClick={handleSearch}
  914. disabled={!type || (!searchCode && !searchName) || isLoading}
  915. fullWidth
  916. sx={{ height: '56px' }}
  917. >
  918. {isLoading ? t("Searching...") : t("Search")}
  919. </Button>
  920. </Grid>
  921. </Grid>
  922. */}
  923. {/* First Search Box - Item Search */}
  924. <Box sx={{ mt: 3, mb: 2 }}>
  925. <Typography variant="h6" display="block" marginBlockEnd={1}>
  926. {t("Search Items")}
  927. </Typography>
  928. <SearchBox
  929. criteria={pickOrderSearchCriteria}
  930. onSearch={handleSecondSearch}
  931. onReset={handleSecondReset}
  932. />
  933. </Box>
  934. {/* Second Search Results */}
  935. {hasSearchedSecond && (
  936. <Box sx={{ mt: 3 }}>
  937. <Typography variant="h6" marginBlockEnd={2}>
  938. {t("Search Results")} ({secondSearchResults.length})
  939. </Typography>
  940. {isLoadingSecondSearch ? (
  941. <Typography>{t("Loading...")}</Typography>
  942. ) : secondSearchResults.length === 0 ? (
  943. <Typography color="textSecondary">{t("No results found")}</Typography>
  944. ) : (
  945. <SearchResults<SearchItemWithQty>
  946. items={secondSearchResults}
  947. columns={secondSearchItemColumns}
  948. totalCount={secondSearchResults.length}
  949. checkboxIds={selectedSecondSearchItemIds}
  950. setCheckboxIds={handleSecondSearchCheckboxChange}
  951. />
  952. )}
  953. </Box>
  954. )}
  955. {/* Search Results with SearchResults component */}
  956. {hasSearched && filteredItems.length > 0 && (
  957. <Box sx={{ mt: 3 }}>
  958. <Typography variant="h6" marginBlockEnd={2}>
  959. {t("Search Results")} ({filteredItems.length})
  960. {filteredItems.length >= 100 && (
  961. <Typography variant="caption" color="textSecondary" sx={{ ml: 2 }}>
  962. {t("Showing first 100 results")}
  963. </Typography>
  964. )}
  965. </Typography>
  966. <SearchResults<SearchItemWithQty>
  967. items={filteredItems}
  968. columns={searchItemColumns}
  969. totalCount={filteredItems.length}
  970. checkboxIds={selectedSearchItemIds}
  971. setCheckboxIds={handleSearchCheckboxChange}
  972. />
  973. </Box>
  974. )}
  975. {/* 创建项目区域 */}
  976. {createdItems.length > 0 && (
  977. <Box sx={{ mt: 3 }}>
  978. <Typography variant="h6" marginBlockEnd={2}>
  979. {t("Created Items")} ({createdItems.length})
  980. </Typography>
  981. <TableContainer component={Paper}>
  982. <Table>
  983. <TableHead>
  984. <TableRow>
  985. <TableCell
  986. padding="checkbox"
  987. sx={{
  988. minWidth: '120px', // 增加最小宽度以适应 "Selected" 文本
  989. width: '120px' // 固定宽度
  990. }}
  991. >
  992. <Typography variant="subtitle2">{t("Selected")}</Typography>
  993. </TableCell>
  994. <TableCell>
  995. <Typography variant="subtitle2">{t("Item")}</Typography>
  996. </TableCell>
  997. <TableCell>
  998. <Typography variant="subtitle2">{t("Current Stock")}</Typography>
  999. </TableCell>
  1000. <TableCell>
  1001. <Typography variant="subtitle2">{t("Unit")}</Typography>
  1002. </TableCell>
  1003. <TableCell>
  1004. <Typography variant="subtitle2">{t("Order Quantity")}</Typography>
  1005. </TableCell>
  1006. <TableCell>
  1007. <Typography variant="subtitle2">{t("Target Date")}</Typography>
  1008. </TableCell>
  1009. </TableRow>
  1010. </TableHead>
  1011. <TableBody>
  1012. {createdItems.map((item) => (
  1013. <TableRow key={item.itemId}>
  1014. <TableCell
  1015. padding="checkbox"
  1016. sx={{
  1017. minWidth: '120px', // 保持与表头一致的宽度
  1018. width: '120px'
  1019. }}
  1020. >
  1021. <Checkbox
  1022. checked={item.isSelected}
  1023. onChange={(e) => handleCreatedItemSelect(item.itemId, e.target.checked)}
  1024. />
  1025. </TableCell>
  1026. <TableCell>
  1027. <Typography variant="body2">{item.itemName}</Typography>
  1028. <Typography variant="caption" color="textSecondary">
  1029. {item.itemCode}
  1030. </Typography>
  1031. </TableCell>
  1032. <TableCell>
  1033. <Typography
  1034. variant="body2"
  1035. color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"}
  1036. >
  1037. {item.currentStockBalance || 0}
  1038. </Typography>
  1039. </TableCell>
  1040. <TableCell>
  1041. <Typography variant="body2">{item.uom}</Typography>
  1042. </TableCell>
  1043. <TableCell>
  1044. <TextField
  1045. type="number"
  1046. size="small"
  1047. value={item.qty}
  1048. onChange={(e) => {
  1049. const newQty = Number(e.target.value);
  1050. handleQtyChange(item.itemId, newQty);
  1051. setFilteredItems(prev =>
  1052. prev.map(searchItem =>
  1053. searchItem.id === item.itemId ? { ...searchItem, qty: newQty } : searchItem
  1054. )
  1055. );
  1056. }}
  1057. onKeyDown={(e) => {
  1058. if (!/[0-9]/.test(e.key) &&
  1059. !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(e.key)) {
  1060. e.preventDefault();
  1061. }
  1062. }}
  1063. inputProps={{
  1064. min: 1,
  1065. step: 1,
  1066. style: { textAlign: 'center' }
  1067. }}
  1068. sx={{
  1069. width: '80px',
  1070. '& .MuiInputBase-input': {
  1071. textAlign: 'center',
  1072. cursor: 'text'
  1073. }
  1074. }}
  1075. />
  1076. </TableCell>
  1077. <TableCell>
  1078. <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk">
  1079. <DatePicker
  1080. value={item.targetDate ? dayjs(item.targetDate) : dayjs()}
  1081. onChange={(date) => {
  1082. if (date) {
  1083. const formattedDate = date.format(INPUT_DATE_FORMAT);
  1084. handleQtyChange(item.itemId, item.qty); // 触发重新渲染
  1085. setCreatedItems(prev =>
  1086. prev.map(createdItem =>
  1087. createdItem.itemId === item.itemId ? { ...createdItem, targetDate: formattedDate } : createdItem
  1088. )
  1089. );
  1090. formProps.setValue("targetDate", formattedDate);
  1091. }
  1092. }}
  1093. slotProps={{
  1094. textField: {
  1095. size: "small",
  1096. sx: {
  1097. width: '180px', // 增加宽度以显示完整日期
  1098. '& .MuiInputBase-input': {
  1099. fontSize: '0.875rem' // 稍微减小字体以适应更多内容
  1100. }
  1101. }
  1102. },
  1103. }}
  1104. />
  1105. </LocalizationProvider>
  1106. </TableCell>
  1107. </TableRow>
  1108. ))}
  1109. </TableBody>
  1110. </Table>
  1111. </TableContainer>
  1112. </Box>
  1113. )}
  1114. {/* 操作按钮 */}
  1115. <Stack direction="row" justifyContent="flex-start" gap={1} sx={{ mt: 3 }}>
  1116. <Button
  1117. name="submit"
  1118. variant="contained"
  1119. startIcon={<Check />}
  1120. type="submit"
  1121. disabled={createdItems.filter(item => item.isSelected).length === 0}
  1122. >
  1123. {t("Create Pick Order")}
  1124. </Button>
  1125. <Button
  1126. name="reset"
  1127. variant="outlined"
  1128. onClick={handleReset}
  1129. >
  1130. {t("reset")}
  1131. </Button>
  1132. </Stack>
  1133. </Box>
  1134. </FormProvider>
  1135. );
  1136. };
  1137. export default NewCreateItem;