"use client"; import { createPickOrder, SavePickOrderRequest, SavePickOrderLineRequest, getLatestGroupNameAndCreate, createOrUpdateGroups } from "@/app/api/pickOrder/actions"; import { Autocomplete, Box, Button, FormControl, Grid, Stack, TextField, Typography, Checkbox, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Select, MenuItem, Modal, Card, CardContent, TablePagination, } from "@mui/material"; import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useCallback, useEffect, useMemo, useState } from "react"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs from "dayjs"; import { Check, Search, RestartAlt } from "@mui/icons-material"; import { ItemCombo, fetchAllItemsInClient } from "@/app/api/settings/item/actions"; import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import SearchResults, { Column } from "../SearchResults/SearchResults"; import { fetchJobOrderDetailByCode } from "@/app/api/jo/actions"; import SearchBox, { Criterion } from "../SearchBox"; import VerticalSearchBox from "./VerticalSearchBox"; import SearchResultsTable from './SearchResultsTable'; import CreatedItemsTable from './CreatedItemsTable'; type Props = { filterArgs?: Record; searchQuery?: Record; onPickOrderCreated?: () => void; // 添加回调函数 }; // 扩展表单类型以包含搜索字段 interface SearchFormData extends SavePickOrderRequest { searchCode?: string; searchName?: string; } // Update the CreatedItem interface to allow null values for groupId interface CreatedItem { itemId: number; itemName: string; itemCode: string; qty: number; uom: string; uomId: number; uomDesc: string; isSelected: boolean; currentStockBalance?: number; targetDate?: string | null; // Make it optional to match the source groupId?: number | null; // Allow null values } // Add interface for search items with quantity interface SearchItemWithQty extends ItemCombo { qty: number | null; // Changed from number to number | null jobOrderCode?: string; jobOrderId?: number; currentStockBalance?: number; targetDate?: string | null; // Allow null values groupId?: number | null; // Allow null values } interface JobOrderDetailPickLine { id: number; code: string; name: string; lotNo: string | null; reqQty: number; uom: string; status: string; } // 添加组相关的接口 interface Group { id: number; name: string; targetDate: string ; } // Move the counter outside the component to persist across re-renders let checkboxChangeCallCount = 0; let processingItems = new Set(); const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCreated }) => { const { t } = useTranslation("pickOrder"); const [items, setItems] = useState([]); const [filteredItems, setFilteredItems] = useState([]); const [createdItems, setCreatedItems] = useState([]); const [isLoading, setIsLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); // 添加组相关的状态 - 只声明一次 const [groups, setGroups] = useState([]); const [selectedGroup, setSelectedGroup] = useState(null); const [nextGroupNumber, setNextGroupNumber] = useState(1); // Add state for selected item IDs in search results const [selectedSearchItemIds, setSelectedSearchItemIds] = useState<(string | number)[]>([]); // Add state for second search const [secondSearchQuery, setSecondSearchQuery] = useState>({}); const [secondSearchResults, setSecondSearchResults] = useState([]); const [isLoadingSecondSearch, setIsLoadingSecondSearch] = useState(false); const [hasSearchedSecond, setHasSearchedSecond] = useState(false); // Add selection state for second search const [selectedSecondSearchItemIds, setSelectedSecondSearchItemIds] = useState<(string | number)[]>([]); const formProps = useForm(); const errors = formProps.formState.errors; const targetDate = formProps.watch("targetDate"); const type = formProps.watch("type"); const searchCode = formProps.watch("searchCode"); const searchName = formProps.watch("searchName"); const [jobOrderItems, setJobOrderItems] = useState([]); const [isLoadingJobOrder, setIsLoadingJobOrder] = useState(false); useEffect(() => { const loadItems = async () => { try { const itemsData = await fetchAllItemsInClient(); console.log("Loaded items:", itemsData); setItems(itemsData); setFilteredItems([]); } catch (error) { console.error("Error loading items:", error); } }; loadItems(); }, []); const searchJobOrderItems = useCallback(async (jobOrderCode: string) => { if (!jobOrderCode.trim()) return; setIsLoadingJobOrder(true); try { const jobOrderDetail = await fetchJobOrderDetailByCode(jobOrderCode); setJobOrderItems(jobOrderDetail.pickLines || []); // Fix the Job Order conversion - add missing uomDesc const convertedItems = (jobOrderDetail.pickLines || []).map(item => ({ id: item.id, label: item.name, qty: item.reqQty, uom: item.uom, uomId: 0, uomDesc: item.uomDesc, // Add missing uomDesc jobOrderCode: jobOrderDetail.code, jobOrderId: jobOrderDetail.id, })); setFilteredItems(convertedItems); setHasSearched(true); } catch (error) { console.error("Error fetching Job Order items:", error); alert(t("Job Order not found or has no items")); } finally { setIsLoadingJobOrder(false); } }, [t]); // Update useEffect to handle Job Order search useEffect(() => { if (searchQuery && searchQuery.jobOrderCode) { searchJobOrderItems(searchQuery.jobOrderCode); } else if (searchQuery && items.length > 0) { // Existing item search logic // ... your existing search logic } }, [searchQuery, items, searchJobOrderItems]); useEffect(() => { if (searchQuery) { if (searchQuery.type) { formProps.setValue("type", searchQuery.type); } if (searchQuery.targetDate) { formProps.setValue("targetDate", searchQuery.targetDate); } if (searchQuery.code) { formProps.setValue("searchCode", searchQuery.code); } if (searchQuery.items) { formProps.setValue("searchName", searchQuery.items); } } }, [searchQuery, formProps]); useEffect(() => { setFilteredItems([]); setHasSearched(false); }, []); const typeList = [ { type: "Consumable" }, { type: "Material" }, { type: "Product" } ]; const handleTypeChange = useCallback( (event: React.SyntheticEvent, newValue: {type: string} | null) => { formProps.setValue("type", newValue?.type || ""); }, [formProps], ); const handleSearch = useCallback(() => { if (!searchCode && !searchName) { alert(t("Please enter at least code or name")); return; } setIsLoading(true); setHasSearched(true); console.log("Searching with:", { type, searchCode, searchName, targetDate, itemsCount: items.length }); setTimeout(() => { let filtered = items; if (searchCode && searchCode.trim()) { filtered = filtered.filter(item => item.label.toLowerCase().includes(searchCode.toLowerCase()) ); console.log("After code filter:", filtered.length); } if (searchName && searchName.trim()) { filtered = filtered.filter(item => item.label.toLowerCase().includes(searchName.toLowerCase()) ); console.log("After name filter:", filtered.length); } // Convert to SearchItemWithQty with default qty = null and include targetDate const filteredWithQty = filtered.slice(0, 100).map(item => ({ ...item, qty: null, targetDate: targetDate, // Add target date to each item })); console.log("Final filtered results:", filteredWithQty.length); setFilteredItems(filteredWithQty); setIsLoading(false); }, 500); }, [type, searchCode, searchName, targetDate, items, t]); // Add targetDate back to dependencies // Handle quantity change in search results const handleSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { setFilteredItems(prev => prev.map(item => item.id === itemId ? { ...item, qty: newQty } : item ) ); // Auto-update created items if this item exists there setCreatedItems(prev => prev.map(item => item.itemId === itemId ? { ...item, qty: newQty || 1 } : item ) ); }, []); // Modified handler for search item selection const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => { if (isSelected) { const item = filteredItems.find(i => i.id === itemId); if (!item) return; const existingItem = createdItems.find(created => created.itemId === item.id); if (existingItem) { alert(t("Item already exists in created items")); return; } // Fix the newCreatedItem creation - add missing uomDesc const newCreatedItem: CreatedItem = { itemId: item.id, itemName: item.label, itemCode: item.label, qty: item.qty || 1, uom: item.uom || "", uomId: item.uomId || 0, uomDesc: item.uomDesc || "", // Add missing uomDesc isSelected: true, currentStockBalance: item.currentStockBalance, targetDate: item.targetDate || targetDate, // Use item's targetDate or fallback to form's targetDate groupId: item.groupId || undefined, // Handle null values }; setCreatedItems(prev => [...prev, newCreatedItem]); } }, [filteredItems, createdItems, t, targetDate]); // Handler for created item selection const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean) => { setCreatedItems(prev => prev.map(item => item.itemId === itemId ? { ...item, isSelected } : item ) ); }, []); const handleQtyChange = useCallback((itemId: number, newQty: number) => { setCreatedItems(prev => prev.map(item => item.itemId === itemId ? { ...item, qty: newQty } : item ) ); }, []); // Check if item is already in created items const isItemInCreated = useCallback((itemId: number) => { return createdItems.some(item => item.itemId === itemId); }, [createdItems]); // 1) Created Items 行内改组:只改这一行的 groupId,并把该行 targetDate 同步为该组日期 const handleCreatedItemGroupChange = useCallback((itemId: number, newGroupId: string) => { const gid = newGroupId ? Number(newGroupId) : undefined; const group = groups.find(g => g.id === gid); setCreatedItems(prev => prev.map(it => it.itemId === itemId ? { ...it, groupId: gid, targetDate: group?.targetDate || it.targetDate, } : it, ), ); }, [groups]); // Update the handleGroupChange function to update target dates for items in the selected group const handleGroupChange = useCallback((groupId: string | number) => { const gid = typeof groupId === "string" ? Number(groupId) : groupId; const group = groups.find(g => g.id === gid); if (!group) return; setSelectedGroup(group); // Update target dates for items that belong to this group setSecondSearchResults(prev => prev.map(item => item.groupId === gid ? { ...item, targetDate: group.targetDate } : item )); }, [groups]); // Update the handleGroupTargetDateChange function to update selected items that belong to that group const handleGroupTargetDateChange = useCallback((groupId: number, newTargetDate: string) => { setGroups(prev => prev.map(g => (g.id === groupId ? { ...g, targetDate: newTargetDate } : g))); setSelectedGroup(prev => { if (prev && prev.id === groupId) { return { ...prev, targetDate: newTargetDate }; } return prev; }); // Update selected items that belong to this group setSecondSearchResults(prev => prev.map(item => item.groupId === groupId ? { ...item, targetDate: newTargetDate } : item )); setCreatedItems(prev => prev.map(item => item.groupId === groupId ? { ...item, targetDate: newTargetDate } : item )); }, []); // Fix the handleCreateGroup function to use the API properly const handleCreateGroup = useCallback(async () => { try { // Use the API to get latest group name and create it automatically const response = await getLatestGroupNameAndCreate(); if (response.id && response.name) { const newGroup: Group = { id: response.id, name: response.name, targetDate: "" }; setGroups(prev => [...prev, newGroup]); setSelectedGroup(newGroup); console.log(`Created new group: ${response.name}`); } else { alert(t('Failed to create group')); } } catch (error) { console.error('Error creating group:', error); alert(t('Failed to create group')); } }, [t]); const checkAndAutoAddItem = useCallback((itemId: number) => { const item = secondSearchResults.find(i => i.id === itemId); if (!item) return; // Check if item has ALL 3 conditions: // 1. Item is selected (checkbox checked) const isSelected = selectedSecondSearchItemIds.includes(itemId); // 2. Group is assigned const hasGroup = item.groupId !== undefined && item.groupId !== null; // 3. Quantity is entered const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0; if (isSelected && hasGroup && hasQty && !isItemInCreated(item.id)) { // Auto-add to created items const newCreatedItem: CreatedItem = { itemId: item.id, itemName: item.label, itemCode: item.label, qty: item.qty || 1, uom: item.uom || "", uomId: item.uomId || 0, uomDesc: item.uomDesc || "", isSelected: true, currentStockBalance: item.currentStockBalance, targetDate: item.targetDate || targetDate, groupId: item.groupId || undefined, }; setCreatedItems(prev => [...prev, newCreatedItem]); // Remove from search results since it's now in created items setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); } }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); // Add this function after checkAndAutoAddItem // Add this function after checkAndAutoAddItem const handleQtyBlur = useCallback((itemId: number) => { // Only auto-add if item is already selected (scenario 1: select first, then enter quantity) setTimeout(() => { const currentItem = secondSearchResults.find(i => i.id === itemId); if (!currentItem) return; const isSelected = selectedSecondSearchItemIds.includes(itemId); const hasGroup = currentItem.groupId !== undefined && currentItem.groupId !== null; const hasQty = currentItem.qty !== null && currentItem.qty !== undefined && currentItem.qty > 0; // Only auto-add if item is already selected (scenario 1: select first, then enter quantity) if (isSelected && hasGroup && hasQty && !isItemInCreated(currentItem.id)) { const newCreatedItem: CreatedItem = { itemId: currentItem.id, itemName: currentItem.label, itemCode: currentItem.label, qty: currentItem.qty || 1, uom: currentItem.uom || "", uomId: currentItem.uomId || 0, uomDesc: currentItem.uomDesc || "", isSelected: true, currentStockBalance: currentItem.currentStockBalance, targetDate: currentItem.targetDate || targetDate, groupId: currentItem.groupId || undefined, }; setCreatedItems(prev => [...prev, newCreatedItem]); setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); } }, 0); }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); const handleSearchItemGroupChange = useCallback((itemId: number, groupId: string) => { const gid = groupId ? Number(groupId) : undefined; const group = groups.find(g => g.id === gid); setSecondSearchResults(prev => prev.map(item => item.id === itemId ? { ...item, groupId: gid, targetDate: group?.targetDate || undefined } : item )); // Check auto-add after group assignment setTimeout(() => { checkAndAutoAddItem(itemId); }, 0); }, [groups, checkAndAutoAddItem]); // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group) const handleSecondSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => { if (!isSelected) return; const item = secondSearchResults.find(i => i.id === itemId); if (!item) return; const exists = createdItems.find(c => c.itemId === item.id); if (exists) { alert(t("Item already exists in created items")); return; } // 找到项目所属的组,使用该组的 targetDate const itemGroup = groups.find(g => g.id === item.groupId); const itemTargetDate = itemGroup?.targetDate || item.targetDate || targetDate; const newCreatedItem: CreatedItem = { itemId: item.id, itemName: item.label, itemCode: item.label, qty: item.qty || 1, uom: item.uom || "", uomId: item.uomId || 0, uomDesc: item.uomDesc || "", isSelected: true, currentStockBalance: item.currentStockBalance, targetDate: itemTargetDate, // 使用项目所属组的 targetDate groupId: item.groupId || undefined, // 使用项目自身的 groupId }; setCreatedItems(prev => [...prev, newCreatedItem]); }, [secondSearchResults, createdItems, groups, targetDate, t]); // 修改提交函数,按组分别创建提料单 const onSubmit = useCallback>( async (data, event) => { const selectedCreatedItems = createdItems.filter(item => item.isSelected); if (selectedCreatedItems.length === 0) { alert(t("Please select at least one item to submit")); return; } // ✅ 修复:自动填充 type 为 "Consumable",不再强制用户选择 // if (!data.type) { // alert(t("Please select product type")); // return; // } // 按组分组选中的项目 const itemsByGroup = selectedCreatedItems.reduce((acc, item) => { const groupId = item.groupId || 'no-group'; if (!acc[groupId]) { acc[groupId] = []; } acc[groupId].push(item); return acc; }, {} as Record); console.log("Items grouped by group:", itemsByGroup); let successCount = 0; const totalGroups = Object.keys(itemsByGroup).length; const groupUpdates: Array<{groupId: number, pickOrderId: number}> = []; // 为每个组创建提料单 for (const [groupId, items] of Object.entries(itemsByGroup)) { try { // 获取组的名称和目标日期 const group = groups.find(g => g.id === Number(groupId)); const groupName = group?.name || 'No Group'; // Use the group's target date, fallback to item's target date, then form's target date let groupTargetDate = group?.targetDate; if (!groupTargetDate && items.length > 0) { groupTargetDate = items[0].targetDate || undefined; // Add || undefined to handle null } if (!groupTargetDate) { groupTargetDate = data.targetDate; } // If still no target date, use today if (!groupTargetDate) { groupTargetDate = dayjs().format(INPUT_DATE_FORMAT); } console.log(`Creating pick order for group: ${groupName} with ${items.length} items, target date: ${groupTargetDate}`); let formattedTargetDate = groupTargetDate; if (groupTargetDate && typeof groupTargetDate === 'string') { try { const date = dayjs(groupTargetDate); formattedTargetDate = date.format('YYYY-MM-DD'); } catch (error) { console.error("Invalid date format:", groupTargetDate); alert(t("Invalid date format")); return; } } // ✅ 修复:自动使用 "Consumable" 作为默认 type const pickOrderData: SavePickOrderRequest = { type: data.type || "Consumable", // 如果用户选择了 type 就用用户的,否则默认 "Consumable" targetDate: formattedTargetDate, pickOrderLine: items.map(item => ({ itemId: item.itemId, qty: item.qty, uomId: item.uomId } as SavePickOrderLineRequest)) }; console.log(`Submitting pick order for group ${groupName}:`, pickOrderData); const res = await createPickOrder(pickOrderData); if (res.id) { console.log(`Pick order created successfully for group ${groupName}:`, res); successCount++; // Store group ID and pick order ID for updating if (groupId !== 'no-group' && group?.id) { groupUpdates.push({ groupId: group.id, pickOrderId: res.id }); } } else { console.error(`Failed to create pick order for group ${groupName}:`, res); alert(t(`Failed to create pick order for group ${groupName}`)); return; } } catch (error) { console.error(`Error creating pick order for group ${groupId}:`, error); alert(t(`Error creating pick order for group ${groupId}`)); return; } } // Update groups with pick order information if (groupUpdates.length > 0) { try { // Update each group with its corresponding pick order ID for (const update of groupUpdates) { const updateResponse = await createOrUpdateGroups({ groupIds: [update.groupId], targetDate: data.targetDate, pickOrderId: update.pickOrderId }); console.log(`Group ${update.groupId} updated with pick order ${update.pickOrderId}:`, updateResponse); } } catch (error) { console.error('Error updating groups:', error); // Don't fail the whole operation if group update fails } } // 所有组都创建成功后,清理选中的项目并切换到 Assign & Release if (successCount === totalGroups) { setCreatedItems(prev => prev.filter(item => !item.isSelected)); formProps.reset(); setHasSearched(false); setFilteredItems([]); alert(t("All pick orders created successfully")); // 通知父组件切换到 Assign & Release 标签页 if (onPickOrderCreated) { onPickOrderCreated(); } } }, [createdItems, t, formProps, groups, onPickOrderCreated] ); // Fix the handleReset function to properly clear all states including search results const handleReset = useCallback(() => { formProps.reset(); setCreatedItems([]); setHasSearched(false); setFilteredItems([]); // Clear second search states completely setSecondSearchResults([]); setHasSearchedSecond(false); setSelectedSecondSearchItemIds([]); setSecondSearchQuery({}); // Clear groups setGroups([]); setSelectedGroup(null); setNextGroupNumber(1); // Clear pagination states setSearchResultsPagingController({ pageNum: 1, pageSize: 10, }); setCreatedItemsPagingController({ pageNum: 1, pageSize: 10, }); // Clear first search states setSelectedSearchItemIds([]); }, [formProps]); // Pagination state const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); // Handle page change const handleChangePage = ( _event: React.MouseEvent | React.KeyboardEvent, newPage: number, ) => { console.log(_event); setPage(newPage); // The original code had setPagingController and defaultPagingController, // but these are not defined in the provided context. // Assuming they are meant to be part of a larger context or will be added. // For now, commenting out the setPagingController part as it's not defined. // if (setPagingController) { // setPagingController({ // ...(pagingController ?? defaultPagingController), // pageNum: newPage + 1, // }); // } }; // Handle rows per page change const handleChangeRowsPerPage = ( event: React.ChangeEvent, ) => { console.log(event); setRowsPerPage(+event.target.value); setPage(0); // The original code had setPagingController and defaultPagingController, // but these are not defined in the provided context. // Assuming they are meant to be part of a larger context or will be added. // For now, commenting out the setPagingController part as it's not defined. // if (setPagingController) { // setPagingController({ // ...(pagingController ?? defaultPagingController), // pageNum: 1, // }); // } }; // Add missing handleSearchCheckboxChange function const handleSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => { if (typeof ids === 'function') { const newIds = ids(selectedSearchItemIds); setSelectedSearchItemIds(newIds); if (newIds.length === filteredItems.length) { // Select all filteredItems.forEach(item => { if (!isItemInCreated(item.id)) { handleSearchItemSelect(item.id, true); } }); } else { // Handle individual selections filteredItems.forEach(item => { const isSelected = newIds.includes(item.id); const isCurrentlyInCreated = isItemInCreated(item.id); if (isSelected && !isCurrentlyInCreated) { handleSearchItemSelect(item.id, true); } else if (!isSelected && isCurrentlyInCreated) { setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id)); } }); } } else { const previousIds = selectedSearchItemIds; setSelectedSearchItemIds(ids); const newlySelected = ids.filter(id => !previousIds.includes(id)); const newlyDeselected = previousIds.filter(id => !ids.includes(id)); newlySelected.forEach(id => { if (!isItemInCreated(id as number)) { handleSearchItemSelect(id as number, true); } }); newlyDeselected.forEach(id => { setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); }); } }, [selectedSearchItemIds, filteredItems, isItemInCreated, handleSearchItemSelect]); // Add pagination state for created items const [createdItemsPagingController, setCreatedItemsPagingController] = useState({ pageNum: 1, pageSize: 10, }); // Add pagination handlers for created items const handleCreatedItemsPageChange = useCallback((event: unknown, newPage: number) => { const newPagingController = { ...createdItemsPagingController, pageNum: newPage + 1, }; setCreatedItemsPagingController(newPagingController); }, [createdItemsPagingController]); const handleCreatedItemsPageSizeChange = useCallback((event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); const newPagingController = { pageNum: 1, pageSize: newPageSize, }; setCreatedItemsPagingController(newPagingController); }, []); // Create a custom table for created items with pagination const CustomCreatedItemsTable = () => { const startIndex = (createdItemsPagingController.pageNum - 1) * createdItemsPagingController.pageSize; const endIndex = startIndex + createdItemsPagingController.pageSize; const paginatedCreatedItems = createdItems.slice(startIndex, endIndex); return ( <> {t("Selected")} {t("Item")} {t("Group")} {t("Current Stock")} {t("Stock Unit")} {t("Order Quantity")} {t("Target Date")} {paginatedCreatedItems.length === 0 ? ( {t("No created items")} ) : ( paginatedCreatedItems.map((item) => ( handleCreatedItemSelect(item.itemId, e.target.checked)} /> {item.itemName} {item.itemCode} 0 ? "success.main" : "error.main"} > {item.currentStockBalance?.toLocaleString() || 0} {item.uomDesc} { const newQty = Number(e.target.value); handleQtyChange(item.itemId, newQty); }} inputProps={{ min: 1, step: 1, style: { textAlign: 'center' } }} sx={{ width: '80px', '& .MuiInputBase-input': { textAlign: 'center', cursor: 'text' } }} /> {item.targetDate&& item.targetDate !== "" ? new Date(item.targetDate).toLocaleDateString() : "-"} )) )}
{/* Pagination for created items */} `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` } /> ); }; // Define columns for SearchResults const searchItemColumns: Column[] = useMemo(() => [ { name: "id", label: "", type: "checkbox", disabled: (item) => isItemInCreated(item.id), // Disable if already in created items }, { name: "label", label: t("Item"), renderCell: (item) => { const parts = item.label.split(' - '); const code = parts[0] || ''; const name = parts[1] || ''; return ( {name} {/* 显示项目名称 */} {code} {/* 显示项目代码 */} ); }, }, { name: "qty", label: t("Order Quantity"), renderCell: (item) => ( { const value = e.target.value; const numValue = value === "" ? null : Number(value); handleSearchQtyChange(item.id, numValue); }} inputProps={{ min: 1, step: 1, style: { textAlign: 'center' } // Center the text }} sx={{ width: '80px', '& .MuiInputBase-input': { textAlign: 'center', cursor: 'text' } }} /> ), }, { name: "currentStockBalance", label: t("Current Stock"), renderCell: (item) => { const stockBalance = item.currentStockBalance || 0; return ( 0 ? "success.main" : "error.main"} sx={{ fontWeight: stockBalance > 0 ? 'bold' : 'normal' }} > {stockBalance} ); }, }, { name: "targetDate", label: t("Target Date"), renderCell: (item) => ( {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} ), }, { name: "uom", label: t("Stock Unit"), renderCell: (item) => item.uom || "-", }, ], [t, isItemInCreated, handleSearchQtyChange]); // 修改搜索条件为3行,每行一个 - 确保SearchBox组件能正确处理 const pickOrderSearchCriteria: Criterion[] = useMemo( () => [ { label: t("Item Code"), paramName: "code", type: "text" }, { label: t("Item Name"), paramName: "name", type: "text" }, { label: t("Product Type"), paramName: "type", type: "autocomplete", options: [ { value: "Consumable", label: t("Consumable") }, { value: "MATERIAL", label: t("Material") }, { value: "End_product", label: t("End Product") } ], }, ], [t], ); // 添加重置函数 const handleSecondReset = useCallback(() => { console.log("Second search reset"); setSecondSearchQuery({}); setSecondSearchResults([]); setHasSearchedSecond(false); // 清空表单中的类型,但保留今天的日期 formProps.setValue("type", ""); const today = dayjs().format(INPUT_DATE_FORMAT); formProps.setValue("targetDate", today); }, [formProps]); // 添加数量变更处理函数 const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { setSecondSearchResults(prev => prev.map(item => item.id === itemId ? { ...item, qty: newQty } : item ) ); // Don't auto-add here - only on blur event }, []); // Add checkbox change handler for second search const handleSecondSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => { if (typeof ids === 'function') { const newIds = ids(selectedSecondSearchItemIds); setSelectedSecondSearchItemIds(newIds); // 处理全选逻辑 - 选择所有搜索结果,不仅仅是当前页面 if (newIds.length === secondSearchResults.length) { // 全选:将所有搜索结果添加到创建项目 secondSearchResults.forEach(item => { if (!isItemInCreated(item.id)) { handleSearchItemSelect(item.id, true); } }); } else { // 部分选择:只处理当前页面的选择 secondSearchResults.forEach(item => { const isSelected = newIds.includes(item.id); const isCurrentlyInCreated = isItemInCreated(item.id); if (isSelected && !isCurrentlyInCreated) { handleSearchItemSelect(item.id, true); } else if (!isSelected && isCurrentlyInCreated) { setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id)); } }); } } else { const previousIds = selectedSecondSearchItemIds; setSelectedSecondSearchItemIds(ids); const newlySelected = ids.filter(id => !previousIds.includes(id)); const newlyDeselected = previousIds.filter(id => !ids.includes(id)); newlySelected.forEach(id => { if (!isItemInCreated(id as number)) { handleSearchItemSelect(id as number, true); } }); newlyDeselected.forEach(id => { setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); }); } }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]); // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity const secondSearchItemColumns: Column[] = useMemo(() => [ { name: "id", label: "", type: "checkbox", disabled: (item) => isItemInCreated(item.id), }, { name: "label", label: t("Item"), renderCell: (item) => { const parts = item.label.split(' - '); const code = parts[0] || ''; const name = parts[1] || ''; return ( {name} {code} ); }, }, { name: "currentStockBalance", label: t("Current Stock"), align: "right", // Add right alignment for the label renderCell: (item) => { const stockBalance = item.currentStockBalance || 0; return ( 0 ? "success.main" : "error.main"} sx={{ fontWeight: stockBalance > 0 ? 'bold' : 'normal', textAlign: 'right' // Add right alignment for the value }} > {stockBalance} ); }, }, { name: "uom", label: t("Stock Unit"), align: "right", // Add right alignment for the label renderCell: (item) => ( {/* Add right alignment for the value */} {item.uom || "-"} ), }, { name: "qty", label: t("Order Quantity"), align: "right", renderCell: (item) => ( { const value = e.target.value; // Only allow numbers if (value === "" || /^\d+$/.test(value)) { const numValue = value === "" ? null : Number(value); handleSecondSearchQtyChange(item.id, numValue); } }} inputProps={{ style: { textAlign: 'center' } }} sx={{ width: '80px', '& .MuiInputBase-input': { textAlign: 'center', cursor: 'text' } }} onBlur={(e) => { const value = e.target.value; const numValue = value === "" ? null : Number(value); if (numValue !== null && numValue < 1) { handleSecondSearchQtyChange(item.id, 1); // Enforce min value } }} /> ), } ], [t, isItemInCreated, handleSecondSearchQtyChange, groups]); // 添加缺失的 handleSecondSearch 函数 const handleSecondSearch = useCallback((query: Record) => { console.log("Second search triggered with query:", query); setSecondSearchQuery({ ...query }); setIsLoadingSecondSearch(true); // Sync second search box info to form - ensure type value is correct if (query.type) { // Ensure type value matches backend enum format let correctType = query.type; if (query.type === "consumable") { correctType = "Consumable"; } else if (query.type === "material") { correctType = "MATERIAL"; } else if (query.type === "jo") { correctType = "JOB_ORDER"; } formProps.setValue("type", correctType); } setTimeout(() => { let filtered = items; // Same filtering logic as first search if (query.code && query.code.trim()) { filtered = filtered.filter(item => item.label.toLowerCase().includes(query.code.toLowerCase()) ); } if (query.name && query.name.trim()) { filtered = filtered.filter(item => item.label.toLowerCase().includes(query.name.toLowerCase()) ); } if (query.type && query.type !== "All") { // Filter by type if needed } // Convert to SearchItemWithQty with NO group/targetDate initially const filteredWithQty = filtered.slice(0, 100).map(item => ({ ...item, qty: null, targetDate: undefined, // No target date initially groupId: undefined, // No group initially })); setSecondSearchResults(filteredWithQty); setHasSearchedSecond(true); setIsLoadingSecondSearch(false); }, 500); }, [items, formProps]); /* // Create a custom search box component that displays fields vertically const VerticalSearchBox = ({ criteria, onSearch, onReset }: { criteria: Criterion[]; onSearch: (inputs: Record) => void; onReset?: () => void; }) => { const { t } = useTranslation("common"); const [inputs, setInputs] = useState>({}); const handleInputChange = (paramName: string, value: any) => { setInputs(prev => ({ ...prev, [paramName]: value })); }; const handleSearch = () => { onSearch(inputs); }; const handleReset = () => { setInputs({}); onReset?.(); }; return ( {t("Search Criteria")} {criteria.map((c) => { return ( {c.type === "text" && ( handleInputChange(c.paramName, e.target.value)} value={inputs[c.paramName] || ""} /> )} {c.type === "autocomplete" && ( option.label} onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")} value={c.options?.find(option => option.value === inputs[c.paramName]) || null} renderInput={(params) => ( )} /> )} ); })} ); }; */ // Add pagination state for search results const [searchResultsPagingController, setSearchResultsPagingController] = useState({ pageNum: 1, pageSize: 10, }); // Add pagination handlers for search results const handleSearchResultsPageChange = useCallback((event: unknown, newPage: number) => { const newPagingController = { ...searchResultsPagingController, pageNum: newPage + 1, // API uses 1-based pagination }; setSearchResultsPagingController(newPagingController); }, [searchResultsPagingController]); const handleSearchResultsPageSizeChange = useCallback((event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); const newPagingController = { pageNum: 1, // Reset to first page pageSize: newPageSize, }; setSearchResultsPagingController(newPagingController); }, []); const getValidationMessage = useCallback(() => { const selectedItems = secondSearchResults.filter(item => selectedSecondSearchItemIds.includes(item.id) ); const itemsWithoutGroup = selectedItems.filter(item => item.groupId === undefined || item.groupId === null ); const itemsWithoutQty = selectedItems.filter(item => item.qty === null || item.qty === undefined || item.qty <= 0 ); if (itemsWithoutGroup.length > 0 && itemsWithoutQty.length > 0) { return t("Please select group and enter quantity for all selected items"); } else if (itemsWithoutGroup.length > 0) { return t("Please select group for all selected items"); } else if (itemsWithoutQty.length > 0) { return t("Please enter quantity for all selected items"); } return ""; }, [secondSearchResults, selectedSecondSearchItemIds, t]); // Fix the handleAddSelectedToCreatedItems function to properly clear selections const handleAddSelectedToCreatedItems = useCallback(() => { const selectedItems = secondSearchResults.filter(item => selectedSecondSearchItemIds.includes(item.id) ); // Add selected items to created items with their own group info selectedItems.forEach(item => { if (!isItemInCreated(item.id)) { const newCreatedItem: CreatedItem = { itemId: item.id, itemName: item.label, itemCode: item.label, qty: item.qty || 1, uom: item.uom || "", uomId: item.uomId || 0, uomDesc: item.uomDesc || "", isSelected: true, currentStockBalance: item.currentStockBalance, targetDate: item.targetDate || targetDate, groupId: item.groupId || undefined, }; setCreatedItems(prev => [...prev, newCreatedItem]); } }); // Clear the selection setSelectedSecondSearchItemIds([]); // Remove the selected/added items from search results entirely setSecondSearchResults(prev => prev.filter(item => !selectedSecondSearchItemIds.includes(item.id) )); }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); // Add a validation function to check if selected items are valid const areSelectedItemsValid = useCallback(() => { const selectedItems = secondSearchResults.filter(item => selectedSecondSearchItemIds.includes(item.id) ); return selectedItems.every(item => item.groupId !== undefined && item.groupId !== null && item.qty !== null && item.qty !== undefined && item.qty > 0 ); }, [secondSearchResults, selectedSecondSearchItemIds]); // Move these handlers to the component level (outside of CustomSearchResultsTable) // Handle individual checkbox change - ONLY select, don't add to created items const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => { checkboxChangeCallCount++; if (checked) { // Add to selected IDs setSelectedSecondSearchItemIds(prev => [...prev, itemId]); // Set the item's group and targetDate to current group when selected setSecondSearchResults(prev => { const updatedResults = prev.map(item => item.id === itemId ? { ...item, groupId: selectedGroup?.id || undefined, targetDate: selectedGroup?.targetDate !== undefined && selectedGroup?.targetDate !== "" ? selectedGroup.targetDate : undefined } : item ); // Check if should auto-add after state update setTimeout(() => { // Check if we're already processing this item if (processingItems.has(itemId)) { //alert(`Item ${itemId} is already being processed, skipping duplicate auto-add`); return; } const updatedItem = updatedResults.find(i => i.id === itemId); if (updatedItem) { const isSelected = true; // We just selected it const hasGroup = updatedItem.groupId !== undefined && updatedItem.groupId !== null; const hasQty = updatedItem.qty !== null && updatedItem.qty !== undefined && updatedItem.qty > 0; // Only auto-add if item has quantity (scenario 2: enter quantity first, then select) if (isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)) { // Mark this item as being processed processingItems.add(itemId); const newCreatedItem: CreatedItem = { itemId: updatedItem.id, itemName: updatedItem.label, itemCode: updatedItem.label, qty: updatedItem.qty || 1, uom: updatedItem.uom || "", uomId: updatedItem.uomId || 0, uomDesc: updatedItem.uomDesc || "", isSelected: true, currentStockBalance: updatedItem.currentStockBalance, targetDate: updatedItem.targetDate || targetDate, groupId: updatedItem.groupId || undefined, }; setCreatedItems(prev => [...prev, newCreatedItem]); setSecondSearchResults(current => current.filter(searchItem => searchItem.id !== itemId)); setSelectedSecondSearchItemIds(current => current.filter(id => id !== itemId)); // Remove from processing set after a short delay setTimeout(() => { processingItems.delete(itemId); }, 100); } // Show final debug info in one alert /* alert(`FINAL DEBUG INFO for item ${itemId}: Function called ${checkboxChangeCallCount} times Is Selected: ${isSelected} Has Group: ${hasGroup} Has Quantity: ${hasQty} Quantity: ${updatedItem.qty} Group ID: ${updatedItem.groupId} Is Item In Created: ${isItemInCreated(updatedItem.id)} Auto-add triggered: ${isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)} Processing items: ${Array.from(processingItems).join(', ')}`); */ } }, 0); return updatedResults; }); } else { // Remove from selected IDs setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); // Clear the item's group and targetDate when deselected setSecondSearchResults(prev => prev.map(item => item.id === itemId ? { ...item, groupId: undefined, targetDate: undefined } : item )); } }, [selectedGroup, isItemInCreated, targetDate]); // Handle select all checkbox for current page const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: SearchItemWithQty[]) => { if (checked) { // Select all items on current page that are not already in created items const newSelectedIds = paginatedResults .filter(item => !isItemInCreated(item.id)) .map(item => item.id); setSelectedSecondSearchItemIds(prev => { const existingIds = prev.filter(id => !paginatedResults.some(item => item.id === id)); return [...existingIds, ...newSelectedIds]; }); // Set group and targetDate for all selected items on current page setSecondSearchResults(prev => prev.map(item => newSelectedIds.includes(item.id) ? { ...item, groupId: selectedGroup?.id || undefined, targetDate: selectedGroup?.targetDate !== undefined && selectedGroup.targetDate !== "" ? selectedGroup.targetDate : undefined } : item )); } else { // Deselect all items on current page const pageItemIds = paginatedResults.map(item => item.id); setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(id as number))); // Clear group and targetDate for all deselected items on current page setSecondSearchResults(prev => prev.map(item => pageItemIds.includes(item.id) ? { ...item, groupId: undefined, targetDate: undefined } : item )); } }, [selectedGroup, isItemInCreated]); // Update the CustomSearchResultsTable to use the handlers from component level /* const CustomSearchResultsTable = () => { // Calculate pagination const startIndex = (searchResultsPagingController.pageNum - 1) * searchResultsPagingController.pageSize; const endIndex = startIndex + searchResultsPagingController.pageSize; const paginatedResults = secondSearchResults.slice(startIndex, endIndex); // Check if all items on current page are selected const allSelectedOnPage = paginatedResults.length > 0 && paginatedResults.every(item => selectedSecondSearchItemIds.includes(item.id)); // Check if some items on current page are selected const someSelectedOnPage = paginatedResults.some(item => selectedSecondSearchItemIds.includes(item.id)); return ( <> {t("Selected")} {t("Item")} {t("Group")} {t("Current Stock")} {t("Stock Unit")} {t("Order Quantity")} {t("Target Date")} {paginatedResults.length === 0 ? ( {t("No data available")} ) : ( paginatedResults.map((item) => ( handleIndividualCheckboxChange(item.id, e.target.checked)} disabled={isItemInCreated(item.id)} /> {item.label.split(' - ')[1] || item.label} {item.label.split(' - ')[0] || ''} {(() => { if (item.groupId) { const group = groups.find(g => g.id === item.groupId); return group?.name || "-"; } return "-"; // Show "-" for unselected items })()} 0 ? "success.main" : "error.main"} sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }} > {item.currentStockBalance || 0} {item.uomDesc || "-"} { const value = e.target.value; // Only allow numbers if (value === "" || /^\d+$/.test(value)) { const numValue = value === "" ? null : Number(value); handleSecondSearchQtyChange(item.id, numValue); } }} onBlur={() => { // Trigger auto-add check when user finishes input handleQtyBlur(item.id); }} inputProps={{ style: { textAlign: 'center' } }} sx={{ width: '80px', '& .MuiInputBase-input': { textAlign: 'center', cursor: 'text' } }} /> {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} )) )}
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` } /> ); }; */ // Add helper function to get group range text const getGroupRangeText = useCallback(() => { if (groups.length === 0) return ""; const firstGroup = groups[0]; const lastGroup = groups[groups.length - 1]; if (firstGroup.id === lastGroup.id) { return `${t("First created group")}: ${firstGroup.name}`; } else { return `${t("First created group")}: ${firstGroup.name} - ${t("Latest created group")}: ${lastGroup.name}`; } }, [groups, t]); return ( {/* First Search Box - Item Search with vertical layout */} {t("Search Items")} {/* Create Group Section - 简化版本,不需要表单 */} {groups.length > 0 && ( <> {t("Group")}: {selectedGroup && ( { if (date) { const formattedDate = date.format(INPUT_DATE_FORMAT); handleGroupTargetDateChange(selectedGroup.id, formattedDate); } }} slotProps={{ textField: { size: "small", label: t("Target Date"), sx: { width: 180 } }, }} /> )} )} {/* Add group range text */} {groups.length > 0 && ( {getGroupRangeText()} )} {/* Second Search Results - Use custom table like AssignAndRelease */} {hasSearchedSecond && ( {t("Search Results")} ({secondSearchResults.length}) {selectedSecondSearchItemIds.length > 0 && ( {t("Selected items will join above created group")} )} {isLoadingSecondSearch ? ( {t("Loading...")} ) : secondSearchResults.length === 0 ? ( {t("No results found")} ) : ( )} )} {/* Add Submit Button between tables */} {/* Search Results with SearchResults component */} {hasSearchedSecond && secondSearchResults.length > 0 && selectedSecondSearchItemIds.length > 0 && ( {selectedSecondSearchItemIds.length > 0 && !areSelectedItemsValid() && ( {getValidationMessage()} )} )} {/* 创建项目区域 - 修改Group列为可选择的 */} {createdItems.length > 0 && ( {t("Created Items")} ({createdItems.length}) )} {/* 操作按钮 */} ); }; export default NewCreateItem;