From 2313e36d797729843ce3c0bc6cd9586a962e7dcb Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 2 Sep 2025 09:47:13 +0800 Subject: [PATCH] updated --- src/app/api/inventory/actions.ts | 29 +- src/app/api/pickOrder/actions.ts | 53 + .../PickOrderSearch/AssignAndRelease.tsx | 297 ++- .../PickOrderSearch/CreatedItemsTable.tsx | 209 ++ src/components/PickOrderSearch/LotTable.tsx | 327 +++ .../PickOrderSearch/PickExecution.tsx | 548 ++-- .../PickQcStockInModalVer3.tsx | 187 +- .../PickOrderSearch/SearchResultsTable.tsx | 242 ++ .../PickOrderSearch/VerticalSearchBox.tsx | 85 + src/components/PickOrderSearch/assignTo.tsx | 401 ++- .../PickOrderSearch/newcreatitem copy.tsx | 2234 +++++++++++++++++ .../PickOrderSearch/newcreatitem.tsx | 407 ++- src/i18n/zh/pickOrder.json | 38 +- 13 files changed, 4295 insertions(+), 762 deletions(-) create mode 100644 src/components/PickOrderSearch/CreatedItemsTable.tsx create mode 100644 src/components/PickOrderSearch/LotTable.tsx create mode 100644 src/components/PickOrderSearch/SearchResultsTable.tsx create mode 100644 src/components/PickOrderSearch/VerticalSearchBox.tsx create mode 100644 src/components/PickOrderSearch/newcreatitem copy.tsx diff --git a/src/app/api/inventory/actions.ts b/src/app/api/inventory/actions.ts index e2bab86..bab5206 100644 --- a/src/app/api/inventory/actions.ts +++ b/src/app/api/inventory/actions.ts @@ -34,12 +34,24 @@ export interface InventoryResultByPage { total: number; records: InventoryResult[]; } - +export interface UpdateInventoryLotLineStatusRequest { + inventoryLotLineId: number; + status: string; +} export interface InventoryLotLineResultByPage { total: number; records: InventoryLotLineResult[]; } - +export interface PostInventoryLotLineResponse { + id: number | null; + name: string; + code: string; + type?: string; + message: string | null; + errorPosition: string + entity?: T | T[]; + consoCode?: string; +} export const fetchLotDetail = cache(async (stockInLineId: number) => { return serverFetchJson( `${BASE_API_URL}/inventoryLotLine/lot-detail/${stockInLineId}`, @@ -49,6 +61,19 @@ export const fetchLotDetail = cache(async (stockInLineId: number) => { }, ); }); +export const updateInventoryLotLineStatus = async (data: UpdateInventoryLotLineStatusRequest) => { + console.log("Updating inventory lot line status:", data); + const result = await serverFetchJson( + `${BASE_API_URL}/inventoryLotLine/updateStatus`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("inventory"); + return result; +}; export const fetchInventories = cache(async (data: SearchInventory) => { const queryStr = convertObjToURLSearchParams(data) diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 027ccc0..bbfc9b3 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -12,6 +12,7 @@ import { PickOrderResult, PreReleasePickOrderSummary, StockOutLine, + } from "."; import { PurchaseQcResult } from "../po/actions"; // import { BASE_API_URL } from "@/config/api"; @@ -35,6 +36,7 @@ export interface PostPickOrderResponse { message: string | null; errorPosition: string entity?: T | T[]; + consoCode?: string; } export interface PostStockOutLiineResponse { id: number | null; @@ -84,6 +86,7 @@ export interface PickOrderApprovalInput { export interface GetPickOrderInfoResponse { + consoCode: string | null; pickOrders: GetPickOrderInfo[]; items: CurrentInventoryItemInfo[]; } @@ -108,6 +111,7 @@ export interface GetPickOrderLineInfo { uomCode: string; uomDesc: string; suggestedList: any[]; + pickedQty: number; } export interface CurrentInventoryItemInfo { @@ -137,7 +141,56 @@ export interface AssignPickOrderInputs { pickOrderIds: number[]; assignTo: number; } +export interface LotDetailWithStockOutLine { + lotId: number; + lotNo: string; + expiryDate: string; + location: string; + stockUnit: string; + availableQty: number; + requiredQty: number; + actualPickQty: number; + suggestedPickLotId: number; + lotStatus: string; + lotAvailability: string; + stockOutLineId?: number; + stockOutLineStatus?: string; + stockOutLineQty?: number; +} + + +export const resuggestPickOrder = async (pickOrderId: number) => { + console.log("Resuggesting pick order:", pickOrderId); + const result = await serverFetchJson( + `${BASE_API_URL}/suggestedPickLot/resuggest/${pickOrderId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return result; +}; + +export const updateStockOutLineStatus = async (data: { + id: number; + status: string; + qty?: number; + remarks?: string; +}) => { + console.log("Updating stock out line status:", data); + const result = await serverFetchJson>( + `${BASE_API_URL}/stockOutLine/updateStatus`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return result; +}; // Missing function 1: newassignPickOrder export const newassignPickOrder = async (data: AssignPickOrderInputs) => { const response = await serverFetchJson( diff --git a/src/components/PickOrderSearch/AssignAndRelease.tsx b/src/components/PickOrderSearch/AssignAndRelease.tsx index c0bdcb6..8c84893 100644 --- a/src/components/PickOrderSearch/AssignAndRelease.tsx +++ b/src/components/PickOrderSearch/AssignAndRelease.tsx @@ -18,15 +18,12 @@ import { Paper, Checkbox, TablePagination, - Alert, - AlertTitle, } from "@mui/material"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { newassignPickOrder, AssignPickOrderInputs, - fetchPickOrderWithStockClient, } from "@/app/api/pickOrder/actions"; import { fetchNameList, NameList ,fetchNewNameList, NewNameList} from "@/app/api/user/actions"; import { FormProvider, useForm } from "react-hook-form"; @@ -72,28 +69,6 @@ interface GroupedItemRow { items: ItemRow[]; } -// 新增的 PickOrderRow 和 PickOrderLineRow 接口 -interface PickOrderRow { - id: string; // Change from number to string to match API response - code: string; - targetDate: string; - type: string; - status: string; - assignTo: number; - groupName: string; - consoCode?: string; - pickOrderLines: PickOrderLineRow[]; -} - -interface PickOrderLineRow { - id: string; - itemCode: string; - itemName: string; - requiredQty: number; - availableQty: number; - uomDesc: string; -} - const style = { position: "absolute", top: "50%", @@ -110,9 +85,9 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const { setIsUploading } = useUploadContext(); - // Update state to use pick order data directly - const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); // Change from number[] to string[] - const [filteredPickOrders, setFilteredPickOrders] = useState([]); + // 修复:选择状态改为按 pick order ID 存储 + const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); const [isLoadingItems, setIsLoadingItems] = useState(false); const [pagingController, setPagingController] = useState({ pageNum: 1, @@ -122,52 +97,96 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { const [modalOpen, setModalOpen] = useState(false); const [usernameList, setUsernameList] = useState([]); const [searchQuery, setSearchQuery] = useState>({}); - const [originalPickOrderData, setOriginalPickOrderData] = useState([]); + const [originalItemData, setOriginalItemData] = useState([]); const formProps = useForm(); const errors = formProps.formState.errors; - // Update the fetch function to process pick order data correctly + // 将项目按 pick order 分组 + const groupedItems = useMemo(() => { + const grouped = groupBy(filteredItems, 'pickOrderId'); + return Object.entries(grouped).map(([pickOrderId, items]) => { + const firstItem = items[0]; + return { + pickOrderId: parseInt(pickOrderId), + pickOrderCode: firstItem.pickOrderCode, + targetDate: firstItem.targetDate, + status: firstItem.status, + consoCode: firstItem.consoCode, + items: items + } as GroupedItemRow; + }); + }, [filteredItems]); + + // 修复:处理 pick order 选择 + const handlePickOrderSelect = useCallback((pickOrderId: number, checked: boolean) => { + if (checked) { + setSelectedPickOrderIds(prev => [...prev, pickOrderId]); + } else { + setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); + } + }, []); + + // 修复:检查 pick order 是否被选中 + const isPickOrderSelected = useCallback((pickOrderId: number) => { + return selectedPickOrderIds.includes(pickOrderId); + }, [selectedPickOrderIds]); + + // 使用 fetchPickOrderItemsByPageClient 获取数据 const fetchNewPageItems = useCallback( async (pagingController: Record, filterArgs: Record) => { + console.log("=== fetchNewPageItems called ==="); + console.log("pagingController:", pagingController); + console.log("filterArgs:", filterArgs); + setIsLoadingItems(true); try { const params = { ...pagingController, ...filterArgs, - pageNum: (pagingController.pageNum || 1) - 1, - pageSize: pagingController.pageSize || 10, + // 新增:排除状态为 "assigned" 的提料单 + //status: "pending,released,completed,cancelled" // 或者使用其他方式过滤 }; + console.log("Final params:", params); - const res = await fetchPickOrderWithStockClient(params); + const res = await fetchPickOrderItemsByPageClient(params); + console.log("API Response:", res); if (res && res.records) { - // Filter out assigned status if needed - const filteredRecords = res.records.filter((pickOrder: any) => pickOrder.status !== "assigned"); + console.log("Records received:", res.records.length); + console.log("First record:", res.records[0]); - // Convert pick order data to the expected format - const pickOrderRows: PickOrderRow[] = filteredRecords.map((pickOrder: any) => ({ - id: pickOrder.id, - code: pickOrder.code, - targetDate: pickOrder.targetDate, - type: pickOrder.type, - status: pickOrder.status, - assignTo: pickOrder.assignTo, - groupName: pickOrder.groupName || "No Group", - consoCode: pickOrder.consoCode, - pickOrderLines: pickOrder.pickOrderLines || [] + // 新增:在前端也过滤掉 "assigned" 状态的项目 + const filteredRecords = res.records.filter((item: any) => item.status !== "assigned"); + + const itemRows: ItemRow[] = filteredRecords.map((item: any) => ({ + id: item.id, + pickOrderId: item.pickOrderId, + pickOrderCode: item.pickOrderCode, + itemId: item.itemId, + itemCode: item.itemCode, + itemName: item.itemName, + requiredQty: item.requiredQty, + currentStock: item.currentStock ?? 0, + unit: item.unit, + targetDate: item.targetDate, + status: item.status, + consoCode: item.consoCode, + assignTo: item.assignTo, + groupName: item.groupName, })); - setOriginalPickOrderData(pickOrderRows); - setFilteredPickOrders(pickOrderRows); - setTotalCountItems(res.total); + setOriginalItemData(itemRows); + setFilteredItems(itemRows); + setTotalCountItems(filteredRecords.length); // 使用过滤后的数量 } else { - setFilteredPickOrders([]); + console.log("No records in response"); + setFilteredItems([]); setTotalCountItems(0); } } catch (error) { - console.error("Error fetching pick orders:", error); - setFilteredPickOrders([]); + console.error("Error fetching items:", error); + setFilteredItems([]); setTotalCountItems(0); } finally { setIsLoadingItems(false); @@ -176,34 +195,44 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { [], ); - // Update search criteria to match the new data structure const searchCriteria: Criterion[] = useMemo( () => [ { label: t("Pick Order Code"), - paramName: "code", + paramName: "pickOrderCode", type: "text", }, + { + label: t("Item Code"), + paramName: "itemCode", + type: "text" + }, { - label: t("Group Name"), + label: t("Group Code"), paramName: "groupName", type: "text", }, + { + label: t("Item Name"), + paramName: "itemName", + type: "text", + }, { label: t("Target Date From"), label2: t("Target Date To"), paramName: "targetDate", type: "dateRange", }, + { label: t("Pick Order Status"), paramName: "status", type: "autocomplete", options: sortBy( uniqBy( - originalPickOrderData.map((pickOrder) => ({ - value: pickOrder.status, - label: t(upperFirst(pickOrder.status)), + originalItemData.map((item) => ({ + value: item.status, + label: t(upperFirst(item.status)), })), "value", ), @@ -211,41 +240,45 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { ), }, ], - [originalPickOrderData, t], + [originalItemData, t], ); - // Update search function to work with pick order data const handleSearch = useCallback((query: Record) => { setSearchQuery({ ...query }); + console.log("Search query:", query); - const filtered = originalPickOrderData.filter((pickOrder) => { - const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); + const filtered = originalItemData.filter((item) => { + const itemTargetDateStr = arrayToDayjs(item.targetDate); - const codeMatch = !query.code || - pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); + const itemCodeMatch = !query.itemCode || + item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); - const groupNameMatch = !query.groupName || - pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); + const itemNameMatch = !query.itemName || + item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); - // Date range search + const pickOrderCodeMatch = !query.pickOrderCode || + item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); + const groupNameMatch = !query.groupName || + item.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); + // 日期范围搜索 let dateMatch = true; if (query.targetDate || query.targetDateTo) { try { if (query.targetDate && !query.targetDateTo) { const fromDate = dayjs(query.targetDate); - dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || - pickOrderTargetDateStr.isAfter(fromDate, 'day'); + dateMatch = itemTargetDateStr.isSame(fromDate, 'day') || + itemTargetDateStr.isAfter(fromDate, 'day'); } else if (!query.targetDate && query.targetDateTo) { const toDate = dayjs(query.targetDateTo); - dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || - pickOrderTargetDateStr.isBefore(toDate, 'day'); + dateMatch = itemTargetDateStr.isSame(toDate, 'day') || + itemTargetDateStr.isBefore(toDate, 'day'); } else if (query.targetDate && query.targetDateTo) { const fromDate = dayjs(query.targetDate); const toDate = dayjs(query.targetDateTo); - dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || - pickOrderTargetDateStr.isAfter(fromDate, 'day')) && - (pickOrderTargetDateStr.isSame(toDate, 'day') || - pickOrderTargetDateStr.isBefore(toDate, 'day')); + dateMatch = (itemTargetDateStr.isSame(fromDate, 'day') || + itemTargetDateStr.isAfter(fromDate, 'day')) && + (itemTargetDateStr.isSame(toDate, 'day') || + itemTargetDateStr.isBefore(toDate, 'day')); } } catch (error) { console.error("Date parsing error:", error); @@ -255,27 +288,28 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { const statusMatch = !query.status || query.status.toLowerCase() === "all" || - pickOrder.status?.toLowerCase().includes((query.status || "").toLowerCase()); + item.status?.toLowerCase().includes((query.status || "").toLowerCase()); - return codeMatch && groupNameMatch && dateMatch && statusMatch; + return itemCodeMatch && itemNameMatch && groupNameMatch && pickOrderCodeMatch && dateMatch && statusMatch; }); - setFilteredPickOrders(filtered); - }, [originalPickOrderData]); + console.log("Filtered items count:", filtered.length); + setFilteredItems(filtered); + }, [originalItemData]); const handleReset = useCallback(() => { setSearchQuery({}); - setFilteredPickOrders(originalPickOrderData); + setFilteredItems(originalItemData); setTimeout(() => { setSearchQuery({}); }, 0); - }, [originalPickOrderData]); + }, [originalItemData]); - // Fix the pagination handlers + // 修复:处理分页变化 const handlePageChange = useCallback((event: unknown, newPage: number) => { const newPagingController = { ...pagingController, - pageNum: newPage + 1, + pageNum: newPage + 1, // API 使用 1-based 分页 }; setPagingController(newPagingController); }, [pagingController]); @@ -283,43 +317,27 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); const newPagingController = { - pageNum: 1, + pageNum: 1, // 重置到第一页 pageSize: newPageSize, }; setPagingController(newPagingController); }, []); - // 修复:处理 pick order 选择 - const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => { - if (checked) { - setSelectedPickOrderIds(prev => [...prev, pickOrderId]); - } else { - setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); - } - }, []); - - // 修复:检查 pick order 是否被选中 - const isPickOrderSelected = useCallback((pickOrderId: string) => { - return selectedPickOrderIds.includes(pickOrderId); - }, [selectedPickOrderIds]); - const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => { if (selectedPickOrderIds.length === 0) return; setIsUploading(true); try { - // Convert string IDs to numbers for the API - const numericIds = selectedPickOrderIds.map(id => parseInt(id, 10)); - + // 修复:直接使用选中的 pick order IDs const assignRes = await newassignPickOrder({ - pickOrderIds: numericIds, + pickOrderIds: selectedPickOrderIds, assignTo: data.assignTo, }); if (assignRes && assignRes.code === "SUCCESS") { console.log("Assign successful:", assignRes); setModalOpen(false); - setSelectedPickOrderIds([]); // Clear selection + setSelectedPickOrderIds([]); // 清空选择 fetchNewPageItems(pagingController, filterArgs); } else { console.error("Assign failed:", assignRes); @@ -336,13 +354,15 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { formProps.reset(); }, [formProps]); - // Component mount effect + // 组件挂载时加载数据 useEffect(() => { + console.log("=== Component mounted ==="); fetchNewPageItems(pagingController, filterArgs || {}); - }, []); + }, []); // 只在组件挂载时执行一次 - // Dependencies change effect + // 当 pagingController 或 filterArgs 变化时重新调用 API useEffect(() => { + console.log("=== Dependencies changed ==="); if (pagingController && (filterArgs || {})) { fetchNewPageItems(pagingController, filterArgs || {}); } @@ -362,8 +382,8 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { loadUsernameList(); }, []); - // Update the table component to work with pick order data directly - const CustomPickOrderTable = () => { + // 自定义分组表格组件 + const CustomGroupedTable = () => { return ( <> @@ -372,7 +392,7 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { {t("Selected")} {t("Pick Order Code")} - {t("Group Name")} + {t("Group Code")} {t("Item Code")} {t("Item Name")} {t("Order Quantity")} @@ -383,70 +403,72 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { - {filteredPickOrders.length === 0 ? ( + {groupedItems.length === 0 ? ( - + {t("No data available")} ) : ( - filteredPickOrders.map((pickOrder) => ( - pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => ( - - {/* Checkbox - only show for first line of each pick order */} + groupedItems.map((group) => ( + group.items.map((item, index) => ( + + {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} {index === 0 ? ( handlePickOrderSelect(pickOrder.id, e.target.checked)} - disabled={!isEmpty(pickOrder.consoCode)} + checked={isPickOrderSelected(group.pickOrderId)} + onChange={(e) => handlePickOrderSelect(group.pickOrderId, e.target.checked)} + disabled={!isEmpty(item.consoCode)} /> ) : null} - {/* Pick Order Code - only show for first line */} + {/* Pick Order Code - 只在第一个项目显示 */} - {index === 0 ? pickOrder.code : null} + {index === 0 ? item.pickOrderCode : null} - {/* Group Name - only show for first line */} + {/* Group Name */} - {index === 0 ? pickOrder.groupName : null} + {index === 0 ? (item.groupName || "No Group") : null} {/* Item Code */} - {line.itemCode} + {item.itemCode} {/* Item Name */} - {line.itemName} + {item.itemName} {/* Order Quantity */} - {line.requiredQty} + {item.requiredQty} {/* Current Stock */} 0 ? "success.main" : "error.main"} - sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} + color={item.currentStock > 0 ? "success.main" : "error.main"} + sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }} > - {(line.availableQty || 0).toLocaleString()} + {item.currentStock.toLocaleString()} {/* Unit */} - {line.uomDesc} + {item.unit} - {/* Target Date - only show for first line */} + {/* Target Date - 只在第一个项目显示 */} {index === 0 ? ( - arrayToDayjs(pickOrder.targetDate) + arrayToDayjs(item.targetDate) .add(-1, "month") .format(OUTPUT_DATE_FORMAT) ) : null} - {/* Pick Order Status - only show for first line */} + + + {/* Pick Order Status - 只在第一个项目显示 */} - {index === 0 ? upperFirst(pickOrder.status) : null} + {index === 0 ? upperFirst(item.status) : null} )) @@ -456,14 +478,15 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { + {/* 修复:添加分页组件 */} `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` @@ -481,7 +504,7 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { {isLoadingItems ? ( ) : ( - + )} @@ -556,7 +579,7 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { - {t("This action will assign the selected pick orders.")} + {t("This action will assign the selected pick orders to picker.")} diff --git a/src/components/PickOrderSearch/CreatedItemsTable.tsx b/src/components/PickOrderSearch/CreatedItemsTable.tsx new file mode 100644 index 0000000..e60bf2f --- /dev/null +++ b/src/components/PickOrderSearch/CreatedItemsTable.tsx @@ -0,0 +1,209 @@ +import React, { useCallback } from 'react'; +import { + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Checkbox, + TextField, + TablePagination, + FormControl, + Select, + MenuItem, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +interface CreatedItem { + itemId: number; + itemName: string; + itemCode: string; + qty: number; + uom: string; + uomId: number; + uomDesc: string; + isSelected: boolean; + currentStockBalance?: number; + targetDate?: string | null; + groupId?: number | null; +} + +interface Group { + id: number; + name: string; + targetDate: string; +} + +interface CreatedItemsTableProps { + items: CreatedItem[]; + groups: Group[]; + onItemSelect: (itemId: number, checked: boolean) => void; + onQtyChange: (itemId: number, qty: number) => void; + onGroupChange: (itemId: number, groupId: string) => void; + pageNum: number; + pageSize: number; + onPageChange: (event: unknown, newPage: number) => void; + onPageSizeChange: (event: React.ChangeEvent) => void; +} + +const CreatedItemsTable: React.FC = ({ + items, + groups, + onItemSelect, + onQtyChange, + onGroupChange, + pageNum, + pageSize, + onPageChange, + onPageSizeChange, +}) => { + const { t } = useTranslation("pickOrder"); + + // Calculate pagination + const startIndex = (pageNum - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedItems = items.slice(startIndex, endIndex); + + const handleQtyChange = useCallback((itemId: number, value: string) => { + const numValue = Number(value); + if (!isNaN(numValue) && numValue >= 1) { + onQtyChange(itemId, numValue); + } + }, [onQtyChange]); + + return ( + <> + + + + + + {t("Selected")} + + + {t("Item")} + + + {t("Group")} + + + {t("Current Stock")} + + + {t("Stock Unit")} + + + {t("Order Quantity")} + + + {t("Target Date")} + + + + + {paginatedItems.length === 0 ? ( + + + + {t("No created items")} + + + + ) : ( + paginatedItems.map((item) => ( + + + onItemSelect(item.itemId, e.target.checked)} + /> + + + {item.itemName} + + {item.itemCode} + + + + + + + + + 0 ? "success.main" : "error.main"} + > + {item.currentStockBalance?.toLocaleString() || 0} + + + + {item.uomDesc} + + + handleQtyChange(item.itemId, e.target.value)} + inputProps={{ + min: 1, + step: 1, + 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}`}` + } + /> + + ); +}; + +export default CreatedItemsTable; \ No newline at end of file diff --git a/src/components/PickOrderSearch/LotTable.tsx b/src/components/PickOrderSearch/LotTable.tsx new file mode 100644 index 0000000..d588dbf --- /dev/null +++ b/src/components/PickOrderSearch/LotTable.tsx @@ -0,0 +1,327 @@ +"use client"; + +import { + Box, + Button, + Checkbox, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + TablePagination, +} from "@mui/material"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import QrCodeIcon from '@mui/icons-material/QrCode'; +import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; + +interface LotPickData { + id: number; + lotId: number; + lotNo: string; + expiryDate: string; + location: string; + stockUnit: string; + availableQty: number; + requiredQty: number; + actualPickQty: number; + lotStatus: string; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + stockOutLineId?: number; + stockOutLineStatus?: string; + stockOutLineQty?: number; +} + +interface PickQtyData { + [lineId: number]: { + [lotId: number]: number; + }; +} + +interface LotTableProps { + lotData: LotPickData[]; + selectedRowId: number | null; + selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; + pickQtyData: PickQtyData; + selectedLotRowId: string | null; + selectedLotId: number | null; + onLotSelection: (uniqueLotId: string, lotId: number) => void; + onPickQtyChange: (lineId: number, lotId: number, value: number) => void; + onSubmitPickQty: (lineId: number, lotId: number) => void; + onCreateStockOutLine: (inventoryLotLineId: number) => void; + onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void; + onLotSelectForInput: (lot: LotPickData) => void; + showInputBody: boolean; + setShowInputBody: (show: boolean) => void; + selectedLotForInput: LotPickData | null; + generateInputBody: () => any; +} + +const LotTable: React.FC = ({ + lotData, + selectedRowId, + selectedRow, + pickQtyData, + selectedLotRowId, + selectedLotId, + onLotSelection, + onPickQtyChange, + onSubmitPickQty, + onCreateStockOutLine, + onQcCheck, + onLotSelectForInput, + showInputBody, + setShowInputBody, + selectedLotForInput, + generateInputBody, +}) => { + const { t } = useTranslation("pickOrder"); + + // 分页控制器 + const [lotTablePagingController, setLotTablePagingController] = useState({ + pageNum: 0, + pageSize: 10, + }); + + // ✅ 添加状态消息生成函数 + const getStatusMessage = useCallback((lot: LotPickData) => { + if (!lot.stockOutLineId) { + return "Please finish QR code scan, QC check and pick order."; + } + + switch (lot.stockOutLineStatus?.toUpperCase()) { + case 'PENDING': + return "Please finish QC check and pick order."; + case 'COMPLETE': + return "Please submit the pick order."; + case 'unavailable': + return "This order is insufficient, please pick another lot."; + default: + return "Please finish QR code scan, QC check and pick order."; + } + }, []); + + const prepareLotTableData = useMemo(() => { + return lotData.map((lot) => ({ + ...lot, + id: lot.lotId, + })); + }, [lotData]); + + // 分页数据 + const paginatedLotTableData = useMemo(() => { + const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize; + const endIndex = startIndex + lotTablePagingController.pageSize; + return prepareLotTableData.slice(startIndex, endIndex); + }, [prepareLotTableData, lotTablePagingController]); + + // 分页处理函数 + const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => { + setLotTablePagingController(prev => ({ + ...prev, + pageNum: newPage, + })); + }, []); + + const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + setLotTablePagingController({ + pageNum: 0, + pageSize: newPageSize, + }); + }, []); + + return ( + <> + + + + + {t("Selected")} + {t("Lot#")} + {t("Lot Expiry Date")} + {t("Lot Location")} + {t("Available Lot")} + {t("Lot Required Pick Qty")} + {t("Stock Unit")} + {t("QR Code Scan")} + {t("QC Check")} + {t("Lot Actual Pick Qty")} + {t("Submit")} + + + + {paginatedLotTableData.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + paginatedLotTableData.map((lot, index) => ( + + + onLotSelection(`row_${index}`, lot.lotId)} + // ✅ Allow selection of available AND insufficient_stock lots + disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} + value={`row_${index}`} + name="lot-selection" + /> + + + + {lot.lotNo} + {lot.lotAvailability !== 'available' && ( + + ({lot.lotAvailability === 'expired' ? 'Expired' : + lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : + 'Unavailable'}) + + )} + + + {lot.expiryDate} + {lot.location} + {lot.availableQty.toLocaleString()} + {lot.requiredQty.toLocaleString()} + {lot.stockUnit} + + {/* QR Code Scan Button */} + + + + + {/* QC Check Button */} + + + + + {/* Lot Actual Pick Qty */} + + { + if (selectedRowId) { + onPickQtyChange( + selectedRowId, + lot.lotId, // This should be unique (ill.id) + parseInt(e.target.value) || 0 + ); + } + }} + inputProps={{ min: 0, max: lot.availableQty }} + // ✅ Allow input for available AND insufficient_stock lots + disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} + sx={{ width: '80px' }} + /> + + + {/* Submit Button */} + + + + + )) + )} + +
+
+ + {/* ✅ Status Messages Display */} + {paginatedLotTableData.length > 0 && ( + + {paginatedLotTableData.map((lot, index) => ( + + + Lot {lot.lotNo}: {getStatusMessage(lot)} + + + ))} + + )} + + + + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); +}; + +export default LotTable; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PickExecution.tsx b/src/components/PickOrderSearch/PickExecution.tsx index 19fe14b..57110fe 100644 --- a/src/components/PickOrderSearch/PickExecution.tsx +++ b/src/components/PickOrderSearch/PickExecution.tsx @@ -43,6 +43,9 @@ import { fetchAllPickOrderDetails, GetPickOrderInfoResponse, GetPickOrderLineInfo, + createStockOutLine, + updateStockOutLineStatus, + resuggestPickOrder, } from "@/app/api/pickOrder/actions"; import { EditNote } from "@mui/icons-material"; import { fetchNameList, NameList } from "@/app/api/user/actions"; @@ -64,6 +67,8 @@ import { defaultPagingController } from "../SearchResults/SearchResults"; import SearchBox, { Criterion } from "../SearchBox"; import dayjs from "dayjs"; import { dummyQCData } from "../PoDetail/dummyQcTemplate"; +import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; +import LotTable from './LotTable'; interface Props { filterArgs: Record; @@ -81,6 +86,9 @@ interface LotPickData { actualPickQty: number; lotStatus: string; lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + stockOutLineId?: number; + stockOutLineStatus?: string; + stockOutLineQty?: number; } interface PickQtyData { @@ -122,6 +130,11 @@ const PickExecution: React.FC = ({ filterArgs }) => { pickOrderCode: string; qcResult?: PurchaseQcResult[]; } | null>(null); + const [selectedLotForQc, setSelectedLotForQc] = useState(null); + + // ✅ Add lot selection state variables + const [selectedLotRowId, setSelectedLotRowId] = useState(null); + const [selectedLotId, setSelectedLotId] = useState(null); // 新增:分页控制器 const [mainTablePagingController, setMainTablePagingController] = useState({ @@ -177,7 +190,34 @@ const PickExecution: React.FC = ({ filterArgs }) => { useEffect(() => { fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs); }, [fetchNewPageConsoPickOrder, filterArgs]); - + const handleUpdateStockOutLineStatus = useCallback(async ( + stockOutLineId: number, + status: string, + qty?: number + ) => { + try { + const updateData = { + id: stockOutLineId, + status: status, + qty: qty + }; + + console.log("Updating stock out line status:", updateData); + const result = await updateStockOutLineStatus(updateData); + + if (result) { + console.log("Stock out line status updated successfully:", result); + + // Refresh lot data to show updated status + if (selectedRowId) { + handleRowSelect(selectedRowId); + } + } + } catch (error) { + console.error("Error updating stock out line status:", error); + + } + }, [selectedRowId]); const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => { let isReleasable = true; for (const item of itemList) { @@ -293,10 +333,41 @@ const PickExecution: React.FC = ({ filterArgs }) => { }); }, []); - const handleSubmitPickQty = useCallback((lineId: number, lotId: number) => { + const handleSubmitPickQty = useCallback(async (lineId: number, lotId: number) => { const qty = pickQtyData[lineId]?.[lotId] || 0; console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`); - }, [pickQtyData]); + + // ✅ Find the stock out line for this lot + const selectedLot = lotData.find(lot => lot.lotId === lotId); + if (!selectedLot?.stockOutLineId) { + return; + } + + try { + // ✅ Update the stock out line quantity + const updateData = { + id: selectedLot.stockOutLineId, + status: selectedLot.stockOutLineStatus || 'PENDING', // Keep current status + qty: qty // Update with the submitted quantity + }; + + console.log("Updating stock out line quantity:", updateData); + const result = await updateStockOutLineStatus(updateData); + + if (result) { + console.log("Stock out line quantity updated successfully:", result); + + + // ✅ Refresh lot data to show updated "Qty Already Picked" + if (selectedRowId) { + handleRowSelect(selectedRowId); + } + } + } catch (error) { + console.error("Error updating stock out line quantity:", error); + + } + }, [pickQtyData, lotData, selectedRowId]); const getTotalPickedQty = useCallback((lineId: number) => { const lineData = pickQtyData[lineId]; @@ -304,80 +375,65 @@ const PickExecution: React.FC = ({ filterArgs }) => { return Object.values(lineData).reduce((sum, qty) => sum + qty, 0); }, [pickQtyData]); - const handleInsufficientStock = useCallback(() => { - console.log("Insufficient stock - need to pick another lot"); - alert("Insufficient stock - need to pick another lot"); - }, []); + const handleQcCheck = useCallback(async (line: GetPickOrderLineInfo, pickOrderCode: string) => { - console.log("QC Check clicked for:", line, pickOrderCode); + // ✅ Get the selected lot for QC + if (!selectedLotId) { + + return; + } + + const selectedLot = lotData.find(lot => lot.lotId === selectedLotId); + if (!selectedLot) { + //alert("Selected lot not found in lot data"); + return; + } + + // ✅ Check if stock out line exists + if (!selectedLot.stockOutLineId) { + //alert("Please create a stock out line first before performing QC check"); + return; + } + + setSelectedLotForQc(selectedLot); + + // ✅ ALWAYS use dummy data for consistent behavior + const transformedDummyData = dummyQCData.map(item => ({ + id: item.id, + code: item.code, + name: item.name, + itemId: line.itemId, + lowerLimit: undefined, + upperLimit: undefined, + description: item.qcDescription, + // ✅ Always reset QC result properties to undefined for fresh start + qcPassed: undefined, + failQty: undefined, + remarks: undefined + })); + + setQcItems(transformedDummyData as QcItemWithChecks[]); + // ✅ Get existing QC results if any (for display purposes only) + let qcResult: any[] = []; try { - // Try to get real data first - const qcItemsData = await fetchQcItemCheck(line.itemId); - console.log("QC Items from API:", qcItemsData); - - // If no data in DB, use dummy data for testing - if (!qcItemsData || qcItemsData.length === 0) { - console.log("No QC items in DB, using dummy data for testing"); - // Transform dummy data to match QcItemWithChecks structure - const transformedDummyData = dummyQCData.map(item => ({ - id: item.id, - code: item.code, - name: item.name, - itemId: line.itemId, // Use the current item's ID - lowerLimit: undefined, - upperLimit: undefined, - description: item.qcDescription, - // Add the QC result properties - qcPassed: item.qcPassed, - failQty: item.failQty, - remarks: item.remarks - })); - setQcItems(transformedDummyData); - } else { - setQcItems(qcItemsData); - } - - // 修复:处理类型不匹配问题 - let qcResult: any[] = []; - try { - const rawQcResult = await fetchPickOrderQcResult(line.id); - // 转换数据类型以匹配 PurchaseQcResult - qcResult = rawQcResult.map((result: any) => ({ - ...result, - isPassed: result.isPassed || false // 添加缺失的 isPassed 属性 - })); - console.log("QC Result:", qcResult); - } catch (error) { - console.log("No existing QC result found"); - } - - setSelectedItemForQc({ - ...line, - pickOrderCode, - qcResult - }); - setQcModalOpen(true); - console.log("QC Modal should open now"); - } catch (error) { - console.error("Error fetching QC data:", error); - // Fallback to dummy data - transform it - const transformedDummyData = dummyQCData.map(item => ({ - id: item.id, - code: item.code, - name: item.name, - itemId: line.itemId, - lowerLimit: undefined, - upperLimit: undefined, - description: item.qcDescription, - qcPassed: item.qcPassed, - failQty: item.failQty, - remarks: item.remarks + const rawQcResult = await fetchPickOrderQcResult(line.id); + qcResult = rawQcResult.map((result: any) => ({ + ...result, + isPassed: result.isPassed || false })); - setQcItems(transformedDummyData); + } catch (error) { + // No existing QC result found - this is normal } - }, []); + + setSelectedItemForQc({ + ...line, + pickOrderCode, + qcResult + }); + setQcModalOpen(true); + }, [lotData, selectedLotId, setQcItems]); const handleCloseQcModal = useCallback(() => { console.log("Closing QC modal"); @@ -420,10 +476,27 @@ const PickExecution: React.FC = ({ filterArgs }) => { }); }, []); - // 新增:处理行选择 + // ✅ Fix lot selection logic + const handleLotSelection = useCallback((uniqueLotId: string, lotId: number) => { + // If clicking the same lot, unselect it + if (selectedLotRowId === uniqueLotId) { + setSelectedLotRowId(null); + setSelectedLotId(null); + } else { + // Select the new lot + setSelectedLotRowId(uniqueLotId); + setSelectedLotId(lotId); + } + }, [selectedLotRowId]); + + // ✅ Add function to handle row selection that resets lot selection const handleRowSelect = useCallback(async (lineId: number) => { setSelectedRowId(lineId); + // ✅ Reset lot selection when changing pick order line + setSelectedLotRowId(null); + setSelectedLotId(null); + try { const lotDetails = await fetchPickOrderLineLotDetails(lineId); console.log("Lot details from API:", lotDetails); @@ -439,7 +512,11 @@ const PickExecution: React.FC = ({ filterArgs }) => { requiredQty: lot.requiredQty, actualPickQty: lot.actualPickQty || 0, lotStatus: lot.lotStatus, - lotAvailability: lot.lotAvailability + lotAvailability: lot.lotAvailability, + // ✅ Add StockOutLine fields + stockOutLineId: lot.stockOutLineId, + stockOutLineStatus: lot.stockOutLineStatus, + stockOutLineQty: lot.stockOutLineQty })); setLotData(realLotData); @@ -450,25 +527,30 @@ const PickExecution: React.FC = ({ filterArgs }) => { }, []); const prepareMainTableData = useMemo(() => { - if (!pickOrderDetails) return []; - - return pickOrderDetails.pickOrders.flatMap((pickOrder) => - pickOrder.pickOrderLines.map((line) => { - // 修复:处理 availableQty 可能为 null 的情况 - const availableQty = line.availableQty ?? 0; - const balanceToPick = availableQty - line.requiredQty; - - return { - ...line, - pickOrderCode: pickOrder.code, - targetDate: pickOrder.targetDate, - balanceToPick: balanceToPick, - // 确保 availableQty 不为 null - availableQty: availableQty, - }; - }) - ); - }, [pickOrderDetails]); + if (!pickOrderDetails) return []; + + return pickOrderDetails.pickOrders.flatMap((pickOrder) => + pickOrder.pickOrderLines.map((line) => { + // 修复:处理 availableQty 可能为 null 的情况 + const availableQty = line.availableQty ?? 0; + const balanceToPick = availableQty - line.requiredQty; + + // ✅ 使用 dayjs 进行一致的日期格式化 + const formattedTargetDate = pickOrder.targetDate + ? dayjs(pickOrder.targetDate).format('YYYY-MM-DD') + : 'N/A'; + + return { + ...line, + pickOrderCode: pickOrder.code, + targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期 + balanceToPick: balanceToPick, + // 确保 availableQty 不为 null + availableQty: availableQty, + }; + }) + ); +}, [pickOrderDetails]); const prepareLotTableData = useMemo(() => { return lotData.map((lot) => ({ @@ -502,18 +584,127 @@ const PickExecution: React.FC = ({ filterArgs }) => { return null; }, [selectedRowId, pickOrderDetails]); - // Add these state variables (around line 110) - const [selectedLotId, setSelectedLotId] = useState(null); + const handleInsufficientStock = useCallback(async () => { + console.log("Insufficient stock - testing resuggest API"); + + if (!selectedRowId || !pickOrderDetails) { + // alert("Please select a pick order line first"); + return; + } + + // Find the pick order ID from the selected row + let pickOrderId: number | null = null; + for (const pickOrder of pickOrderDetails.pickOrders) { + const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); + if (foundLine) { + pickOrderId = pickOrder.id; + break; + } + } + + if (!pickOrderId) { + // alert("Could not find pick order ID for selected line"); + return; + } + + try { + console.log(`Calling resuggest API for pick order ID: ${pickOrderId}`); + + // Call the resuggest API + const result = await resuggestPickOrder(pickOrderId); + + console.log("Resuggest API result:", result); + + if (result.code === "SUCCESS") { + //alert(`✅ Resuggest successful!\n\nMessage: ${result.message}\n\nRemoved: ${result.message?.includes('Removed') ? 'Yes' : 'No'}\nCreated: ${result.message?.includes('created') ? 'Yes' : 'No'}`); + + // Refresh the lot data to show the new suggestions + if (selectedRowId) { + await handleRowSelect(selectedRowId); + } + + // Also refresh the main pick order details + await handleFetchAllPickOrderDetails(); + + } else { + //alert(`❌ Resuggest failed!\n\nError: ${result.message}`); + } + + } catch (error) { + console.error("Error calling resuggest API:", error); + //alert(`❌ Error calling resuggest API:\n\n${error instanceof Error ? error.message : 'Unknown error'}`); + } + }, [selectedRowId, pickOrderDetails, handleRowSelect, handleFetchAllPickOrderDetails]); // Add this function (around line 350) - const handleLotSelection = useCallback((uniqueLotId: string) => { - setSelectedLotId(uniqueLotId); + const hasSelectedLots = useCallback((lineId: number) => { + return selectedLotRowId !== null; + }, [selectedLotRowId]); + + // Add state for showing input body + const [showInputBody, setShowInputBody] = useState(false); + const [selectedLotForInput, setSelectedLotForInput] = useState(null); + + // Add function to handle lot selection for input body display + const handleLotSelectForInput = useCallback((lot: LotPickData) => { + setSelectedLotForInput(lot); + setShowInputBody(true); }, []); - // Add this function (around line 480) - const hasSelectedLots = useCallback((lineId: number) => { - return selectedLotId !== null; - }, [selectedLotId]); + // Add function to generate input body + const generateInputBody = useCallback((): CreateStockOutLine | null => { + if (!selectedLotForInput || !selectedRowId || !selectedRow || !pickOrderDetails?.consoCode) { + return null; + } + + return { + consoCode: pickOrderDetails.consoCode, + pickOrderLineId: selectedRowId, + inventoryLotLineId: selectedLotForInput.lotId, + qty: 0.0 + }; + }, [selectedLotForInput, selectedRowId, selectedRow, pickOrderDetails?.consoCode]); + + + + // Add function to handle create stock out line + const handleCreateStockOutLine = useCallback(async (inventoryLotLineId: number) => { + if (!selectedRowId || !pickOrderDetails?.consoCode) { + console.error("Missing required data for creating stock out line."); + return; + } + + try { + const stockOutLineData: CreateStockOutLine = { + consoCode: pickOrderDetails.consoCode, + pickOrderLineId: selectedRowId, + inventoryLotLineId: inventoryLotLineId, + qty: 0.0 + }; + + console.log("=== STOCK OUT LINE CREATION DEBUG ==="); + console.log("Input Body:", JSON.stringify(stockOutLineData, null, 2)); + + // ✅ Use the correct API function + const result = await createStockOutLine(stockOutLineData); + + console.log("Stock Out Line created:", result); + + if (result) { + console.log("Stock out line created successfully:", result); + //alert(`Stock out line created successfully! ID: ${result.id}`); + + // ✅ Don't refresh immediately - let user see the result first + setShowInputBody(false); // Hide preview after successful creation + } else { + console.error("Failed to create stock out line: No response"); + //alert("Failed to create stock out line: No response"); + } + } catch (error) { + console.error("Error creating stock out line:", error); + //alert("Error creating stock out line. Please try again."); + } + }, [selectedRowId, pickOrderDetails?.consoCode]); // 自定义主表格组件 const CustomMainTable = () => { @@ -614,125 +805,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { ); }; - // 自定义批次表格组件 - const CustomLotTable = () => { - return ( - <> - - - - - {t("Selected")} - {t("Lot#")} - {t("Lot Expiry Date")} - {t("Lot Location")} - - {t("Available Lot")} - {t("Lot Required Pick Qty")} - {t("Lot Actual Pick Qty")} - {t("Stock Unit")} - {t("Submit")} - - - - {paginatedLotTableData.length === 0 ? ( - - - - {t("No data available")} - - - - ) : ( - paginatedLotTableData.map((lot, index) => ( - - - handleLotSelection(`row_${index}`)} - disabled={lot.lotAvailability !== 'available'} - value={`row_${index}`} - name="lot-selection" - /> - - - - {lot.lotNo} - {lot.lotAvailability !== 'available' && ( - - ({lot.lotAvailability === 'expired' ? 'Expired' : - lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : - 'Unavailable'}) - - )} - - - {lot.expiryDate} - {lot.location} - - {lot.availableQty.toLocaleString()} - {lot.requiredQty.toLocaleString()} - - { - if (selectedRowId) { - handlePickQtyChange( - selectedRowId, - lot.lotId, // This should be unique (ill.id) - parseInt(e.target.value) || 0 - ); - } - }} - inputProps={{ min: 0, max: lot.availableQty }} - disabled={lot.lotAvailability !== 'available'} - sx={{ width: '80px' }} - /> - - {lot.stockUnit} - - - - - )) - )} - -
-
- - - `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` - } - /> - - ); - }; - // Add search criteria const searchCriteria: Criterion[] = useMemo( () => [ @@ -850,7 +922,24 @@ const PickExecution: React.FC = ({ filterArgs }) => { {/* 检查是否有可用的批次数据 */} {lotData.length > 0 ? ( - + ) : ( = ({ filterArgs }) => { {/* Action buttons below the lot table */} - + + + + + ); +}; + +export default VerticalSearchBox; \ No newline at end of file diff --git a/src/components/PickOrderSearch/assignTo.tsx b/src/components/PickOrderSearch/assignTo.tsx index 00a8710..87ab821 100644 --- a/src/components/PickOrderSearch/assignTo.tsx +++ b/src/components/PickOrderSearch/assignTo.tsx @@ -25,6 +25,7 @@ import { newassignPickOrder, AssignPickOrderInputs, releaseAssignedPickOrders, + fetchPickOrderWithStockClient, // Add this import } from "@/app/api/pickOrder/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { @@ -38,40 +39,36 @@ import dayjs from "dayjs"; import arraySupport from "dayjs/plugin/arraySupport"; import SearchBox, { Criterion } from "../SearchBox"; import { sortBy, uniqBy } from "lodash"; -import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions"; - +import { createStockOutLine, CreateStockOutLine, fetchPickOrderDetails } from "@/app/api/pickOrder/actions"; dayjs.extend(arraySupport); interface Props { filterArgs: Record; } -// 使用 fetchPickOrderItemsByPageClient 返回的数据结构 -interface ItemRow { +// Update the interface to match the new API response structure +interface PickOrderRow { id: string; - pickOrderId: number; - pickOrderCode: string; - itemId: number; - itemCode: string; - itemName: string; - requiredQty: number; - currentStock: number; - unit: string; - targetDate: any; + code: string; + targetDate: string; + type: string; status: string; + assignTo: number; + groupName: string; consoCode?: string; - assignTo?: number; - groupName?: string; + pickOrderLines: PickOrderLineRow[]; } -// 分组后的数据结构 -interface GroupedItemRow { - pickOrderId: number; - pickOrderCode: string; - targetDate: any; - status: string; - consoCode?: string; - items: ItemRow[]; +interface PickOrderLineRow { + id: number; + itemId: number; + itemCode: string; + itemName: string; + availableQty: number | null; + requiredQty: number; + uomCode: string; + uomDesc: string; + suggestedList: any[]; } const style = { @@ -89,10 +86,10 @@ const style = { const AssignTo: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const { setIsUploading } = useUploadContext(); - - // 修复:选择状态改为按 pick order ID 存储 - const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); - const [filteredItems, setFilteredItems] = useState([]); + const [isUploading, setIsUploadingLocal] = useState(false); + // Update state to use pick order data directly + const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); + const [filteredPickOrders, setFilteredPickOrders] = useState([]); const [isLoadingItems, setIsLoadingItems] = useState(false); const [pagingController, setPagingController] = useState({ pageNum: 1, @@ -102,30 +99,13 @@ const AssignTo: React.FC = ({ filterArgs }) => { const [modalOpen, setModalOpen] = useState(false); const [usernameList, setUsernameList] = useState([]); const [searchQuery, setSearchQuery] = useState>({}); - const [originalItemData, setOriginalItemData] = useState([]); + const [originalPickOrderData, setOriginalPickOrderData] = useState([]); const formProps = useForm(); const errors = formProps.formState.errors; - // 将项目按 pick order 分组 - const groupedItems = useMemo(() => { - const grouped = groupBy(filteredItems, 'pickOrderId'); - return Object.entries(grouped).map(([pickOrderId, items]) => { - const firstItem = items[0]; - return { - pickOrderId: parseInt(pickOrderId), - pickOrderCode: firstItem.pickOrderCode, - targetDate: firstItem.targetDate, - status: firstItem.status, - consoCode: firstItem.consoCode, - items: items, - groupName: firstItem.groupName, - } as GroupedItemRow; - }); - }, [filteredItems]); - - // 修复:处理 pick order 选择 - const handlePickOrderSelect = useCallback((pickOrderId: number, checked: boolean) => { + // Update the handler functions to work with string IDs + const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => { if (checked) { setSelectedPickOrderIds(prev => [...prev, pickOrderId]); } else { @@ -133,62 +113,50 @@ const AssignTo: React.FC = ({ filterArgs }) => { } }, []); - // 修复:检查 pick order 是否被选中 - const isPickOrderSelected = useCallback((pickOrderId: number) => { + const isPickOrderSelected = useCallback((pickOrderId: string) => { return selectedPickOrderIds.includes(pickOrderId); }, [selectedPickOrderIds]); - // 使用 fetchPickOrderItemsByPageClient 获取数据 + // Update the fetch function to use the correct endpoint const fetchNewPageItems = useCallback( async (pagingController: Record, filterArgs: Record) => { - console.log("=== fetchNewPageItems called ==="); - console.log("pagingController:", pagingController); - console.log("filterArgs:", filterArgs); - setIsLoadingItems(true); try { const params = { ...pagingController, ...filterArgs, - // 新增:只获取状态为 "assigned" 的提料单 + pageNum: (pagingController.pageNum || 1) - 1, + pageSize: pagingController.pageSize || 10, + // Filter for assigned status only status: "assigned" }; - console.log("Final params:", params); - const res = await fetchPickOrderItemsByPageClient(params); - console.log("API Response:", res); + const res = await fetchPickOrderWithStockClient(params); if (res && res.records) { - console.log("Records received:", res.records.length); - console.log("First record:", res.records[0]); - - const itemRows: ItemRow[] = res.records.map((item: any) => ({ - id: item.id, - pickOrderId: item.pickOrderId, - pickOrderCode: item.pickOrderCode, - itemId: item.itemId, - itemCode: item.itemCode, - itemName: item.itemName, - requiredQty: item.requiredQty, - currentStock: item.currentStock ?? 0, - unit: item.unit, - targetDate: item.targetDate, - status: item.status, - consoCode: item.consoCode, - assignTo: item.assignTo, + // Convert pick order data to the expected format + const pickOrderRows: PickOrderRow[] = res.records.map((pickOrder: any) => ({ + id: pickOrder.id, + code: pickOrder.code, + targetDate: pickOrder.targetDate, + type: pickOrder.type, + status: pickOrder.status, + assignTo: pickOrder.assignTo, + groupName: pickOrder.groupName || "No Group", + consoCode: pickOrder.consoCode, + pickOrderLines: pickOrder.pickOrderLines || [] })); - setOriginalItemData(itemRows); - setFilteredItems(itemRows); + setOriginalPickOrderData(pickOrderRows); + setFilteredPickOrders(pickOrderRows); setTotalCountItems(res.total); } else { - console.log("No records in response"); - setFilteredItems([]); + setFilteredPickOrders([]); setTotalCountItems(0); } } catch (error) { - console.error("Error fetching items:", error); - setFilteredItems([]); + console.error("Error fetching pick orders:", error); + setFilteredPickOrders([]); setTotalCountItems(0); } finally { setIsLoadingItems(false); @@ -197,47 +165,91 @@ const AssignTo: React.FC = ({ filterArgs }) => { [], ); - // 新增:处理 Release 操作(包含完整的库存管理) - const handleRelease = useCallback(async () => { - if (selectedPickOrderIds.length === 0) return; - - setIsUploading(true); - try { - // 调用新的 release API,包含完整的库存管理功能 - const releaseRes = await releaseAssignedPickOrders({ - pickOrderIds: selectedPickOrderIds, - assignTo: 0, // 这个参数在 release 时不会被使用 - }); - - if (releaseRes && releaseRes.code === "SUCCESS") { - console.log("Release successful with inventory management:", releaseRes); - setSelectedPickOrderIds([]); // 清空选择 - fetchNewPageItems(pagingController, filterArgs); - } else { - console.error("Release failed:", releaseRes); + // Handle Release operation +// Handle Release operation +const handleRelease = useCallback(async () => { + if (selectedPickOrderIds.length === 0) return; + + setIsUploading(true); + try { + // Get the assigned user from the selected pick orders + const selectedPickOrders = filteredPickOrders.filter(pickOrder => + selectedPickOrderIds.includes(pickOrder.id) + ); + + // Check if all selected pick orders have the same assigned user + const assignedUsers = selectedPickOrders.map(po => po.assignTo).filter(Boolean); + if (assignedUsers.length === 0) { + alert("Selected pick orders are not assigned to any user."); + return; + } + + const assignToValue = assignedUsers[0]; + + // Validate that all pick orders are assigned to the same user + const allSameUser = assignedUsers.every(userId => userId === assignToValue); + if (!allSameUser) { + alert("All selected pick orders must be assigned to the same user."); + return; + } + + console.log("Using assigned user:", assignToValue); + console.log("selectedPickOrderIds:", selectedPickOrderIds); + + const releaseRes = await releaseAssignedPickOrders({ + pickOrderIds: selectedPickOrderIds.map(id => parseInt(id)), + assignTo: assignToValue + }); + + if (releaseRes.code === "SUCCESS") { + console.log("Pick orders released successfully"); + + // Get the consoCode from the response + const consoCode = (releaseRes.entity as any)?.consoCode; + + if (consoCode) { + // Create StockOutLine records for each pick order line + for (const pickOrder of selectedPickOrders) { + for (const line of pickOrder.pickOrderLines) { + try { + const stockOutLineData = { + consoCode: consoCode, + pickOrderLineId: line.id, + inventoryLotLineId: 0, // This will be set when user scans QR code + qty: line.requiredQty, + }; + + console.log("Creating stock out line:", stockOutLineData); + await createStockOutLine(stockOutLineData); + } catch (error) { + console.error("Error creating stock out line for line", line.id, error); + } + } + } } - } catch (error) { - console.error("Error in release:", error); - } finally { - setIsUploading(false); + + fetchNewPageItems(pagingController, filterArgs); + } else { + console.error("Release failed:", releaseRes.message); } - }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); - + } catch (error) { + console.error("Error releasing pick orders:", error); + } finally { + setIsUploading(false); + } +}, [selectedPickOrderIds, filteredPickOrders, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); + + // Update search criteria to match the new data structure const searchCriteria: Criterion[] = useMemo( () => [ { label: t("Pick Order Code"), - paramName: "pickOrderCode", + paramName: "code", type: "text", }, - { - label: t("Item Code"), - paramName: "itemCode", - type: "text" - }, { - label: t("Item Name"), - paramName: "itemName", + label: t("Group Code"), + paramName: "groupName", type: "text", }, { @@ -250,72 +262,64 @@ const AssignTo: React.FC = ({ filterArgs }) => { [t], ); + // Update search function to work with pick order data const handleSearch = useCallback((query: Record) => { setSearchQuery({ ...query }); - console.log("Search query:", query); - const filtered = originalItemData.filter((item) => { - const itemTargetDateStr = arrayToDayjs(item.targetDate); + const filtered = originalPickOrderData.filter((pickOrder) => { + const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); - const itemCodeMatch = !query.itemCode || - item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); - - const itemNameMatch = !query.itemName || - item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); + const codeMatch = !query.code || + pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); - const pickOrderCodeMatch = !query.pickOrderCode || - item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); + const groupNameMatch = !query.groupName || + pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); - // 日期范围搜索 + // Date range search let dateMatch = true; if (query.targetDate || query.targetDateTo) { try { if (query.targetDate && !query.targetDateTo) { const fromDate = dayjs(query.targetDate); - dateMatch = itemTargetDateStr.isSame(fromDate, 'day') || - itemTargetDateStr.isAfter(fromDate, 'day'); + dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || + pickOrderTargetDateStr.isAfter(fromDate, 'day'); } else if (!query.targetDate && query.targetDateTo) { const toDate = dayjs(query.targetDateTo); - dateMatch = itemTargetDateStr.isSame(toDate, 'day') || - itemTargetDateStr.isBefore(toDate, 'day'); + dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || + pickOrderTargetDateStr.isBefore(toDate, 'day'); } else if (query.targetDate && query.targetDateTo) { const fromDate = dayjs(query.targetDate); const toDate = dayjs(query.targetDateTo); - dateMatch = (itemTargetDateStr.isSame(fromDate, 'day') || - itemTargetDateStr.isAfter(fromDate, 'day')) && - (itemTargetDateStr.isSame(toDate, 'day') || - itemTargetDateStr.isBefore(toDate, 'day')); + dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || + pickOrderTargetDateStr.isAfter(fromDate, 'day')) && + (pickOrderTargetDateStr.isSame(toDate, 'day') || + pickOrderTargetDateStr.isBefore(toDate, 'day')); } } catch (error) { console.error("Date parsing error:", error); dateMatch = true; } } - - const statusMatch = !query.status || - query.status.toLowerCase() === "all" || - item.status?.toLowerCase().includes((query.status || "").toLowerCase()); - return itemCodeMatch && itemNameMatch && pickOrderCodeMatch && dateMatch && statusMatch; + return codeMatch && groupNameMatch && dateMatch; }); - console.log("Filtered items count:", filtered.length); - setFilteredItems(filtered); - }, [originalItemData]); + setFilteredPickOrders(filtered); + }, [originalPickOrderData]); const handleReset = useCallback(() => { setSearchQuery({}); - setFilteredItems(originalItemData); + setFilteredPickOrders(originalPickOrderData); setTimeout(() => { setSearchQuery({}); }, 0); - }, [originalItemData]); + }, [originalPickOrderData]); - // 修复:处理分页变化 + // Pagination handlers const handlePageChange = useCallback((event: unknown, newPage: number) => { const newPagingController = { ...pagingController, - pageNum: newPage + 1, // API 使用 1-based 分页 + pageNum: newPage + 1, }; setPagingController(newPagingController); }, [pagingController]); @@ -323,52 +327,19 @@ const AssignTo: React.FC = ({ filterArgs }) => { const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); const newPagingController = { - pageNum: 1, // 重置到第一页 + pageNum: 1, pageSize: newPageSize, }; setPagingController(newPagingController); }, []); - const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => { - if (selectedPickOrderIds.length === 0) return; - - setIsUploading(true); - try { - // 修复:直接使用选中的 pick order IDs - const assignRes = await newassignPickOrder({ - pickOrderIds: selectedPickOrderIds, - assignTo: data.assignTo, - }); - - if (assignRes && assignRes.code === "SUCCESS") { - console.log("Assign successful:", assignRes); - setModalOpen(false); - setSelectedPickOrderIds([]); // 清空选择 - fetchNewPageItems(pagingController, filterArgs); - } else { - console.error("Assign failed:", assignRes); - } - } catch (error) { - console.error("Error in assign:", error); - } finally { - setIsUploading(false); - } - }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); - - const openAssignModal = useCallback(() => { - setModalOpen(true); - formProps.reset(); - }, [formProps]); - - // 组件挂载时加载数据 + // Component mount effect useEffect(() => { - console.log("=== Component mounted ==="); fetchNewPageItems(pagingController, filterArgs || {}); - }, []); // 只在组件挂载时执行一次 + }, []); - // 当 pagingController 或 filterArgs 变化时重新调用 API + // Dependencies change effect useEffect(() => { - console.log("=== Dependencies changed ==="); if (pagingController && (filterArgs || {})) { fetchNewPageItems(pagingController, filterArgs || {}); } @@ -388,9 +359,9 @@ const AssignTo: React.FC = ({ filterArgs }) => { loadUsernameList(); }, []); - // 自定义分组表格组件 - const CustomGroupedTable = () => { - // 获取用户名的辅助函数 + // Update the table component to work with pick order data directly + const CustomPickOrderTable = () => { + // Helper function to get user name const getUserName = useCallback((assignToId: number | null | undefined) => { if (!assignToId) return '-'; const user = usernameList.find(u => u.id === assignToId); @@ -405,7 +376,7 @@ const AssignTo: React.FC = ({ filterArgs }) => { {t("Selected")} {t("Pick Order Code")} - {t("Group Name")} + {t("Group Code")} {t("Item Code")} {t("Item Name")} {t("Order Quantity")} @@ -416,75 +387,72 @@ const AssignTo: React.FC = ({ filterArgs }) => { - {groupedItems.length === 0 ? ( + {filteredPickOrders.length === 0 ? ( - + {t("No data available")} ) : ( - groupedItems.map((group) => ( - group.items.map((item, index) => ( - - {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} + filteredPickOrders.map((pickOrder) => ( + pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => ( + + {/* Checkbox - only show for first line of each pick order */} {index === 0 ? ( handlePickOrderSelect(group.pickOrderId, e.target.checked)} - disabled={!isEmpty(item.consoCode)} + checked={isPickOrderSelected(pickOrder.id)} + onChange={(e) => handlePickOrderSelect(pickOrder.id, e.target.checked)} + disabled={!isEmpty(pickOrder.consoCode)} /> ) : null} - - {/* Pick Order Code - 只在第一个项目显示 */} + {/* Pick Order Code - only show for first line */} - {index === 0 ? item.pickOrderCode : null} + {index === 0 ? pickOrder.code : null} - {/* Group Name */} + {/* Group Name - only show for first line */} - {index === 0 ? (item.groupName || "No Group") : null} + {index === 0 ? pickOrder.groupName : null} - {/* Item Code */} - {item.itemCode} - + {line.itemCode} {/* Item Name */} - {item.itemName} - + {line.itemName} + {/* Order Quantity */} - {item.requiredQty} + {line.requiredQty} {/* Current Stock */} 0 ? "success.main" : "error.main"} - sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }} + color={line.availableQty && line.availableQty > 0 ? "success.main" : "error.main"} + sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} > - {item.currentStock.toLocaleString()} + {(line.availableQty || 0).toLocaleString()} {/* Unit */} - {item.unit} + {line.uomDesc} - {/* Target Date - 只在第一个项目显示 */} + {/* Target Date - only show for first line */} {index === 0 ? ( - arrayToDayjs(item.targetDate) + arrayToDayjs(pickOrder.targetDate) .add(-1, "month") .format(OUTPUT_DATE_FORMAT) ) : null} - {/* Assigned To - 只在第一个项目显示,显示用户名 */} + {/* Assigned To - only show for first line */} {index === 0 ? ( - {getUserName(item.assignTo)} + {getUserName(pickOrder.assignTo)} ) : null} @@ -496,15 +464,14 @@ const AssignTo: React.FC = ({ filterArgs }) => { - {/* 修复:添加分页组件 */} `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` @@ -522,7 +489,7 @@ const AssignTo: React.FC = ({ filterArgs }) => { {isLoadingItems ? ( ) : ( - + )}
diff --git a/src/components/PickOrderSearch/newcreatitem copy.tsx b/src/components/PickOrderSearch/newcreatitem copy.tsx new file mode 100644 index 0000000..4d876fa --- /dev/null +++ b/src/components/PickOrderSearch/newcreatitem copy.tsx @@ -0,0 +1,2234 @@ +"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; +} + +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 (!type) { + alert(t("Please select type")); + return; + } + + 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))); + + // Update selected items that belong to this group + setSecondSearchResults(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: dayjs().format(INPUT_DATE_FORMAT) + }; + + 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]); + + // 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; + } + + if (!data.type) { + alert(t("Please select product type")); + return; + } + + // Remove the data.targetDate check since we'll use group target dates + // if (!data.targetDate) { + // alert(t("Please select target date")); + // 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; + } + } + + const pickOrderData: SavePickOrderRequest = { + type: data.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 ? 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]); + + // 1. First, add the checkAndAutoAddItem function (place this BEFORE the other functions) + const checkAndAutoAddItem = useCallback((itemId: number) => { + const item = secondSearchResults.find(i => i.id === itemId); + if (!item) return; + + // Check if item has both group and quantity + const hasGroup = item.groupId !== undefined && item.groupId !== null; + const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0; + + if (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, isItemInCreated, targetDate]); + + // 2. Replace the existing handleSecondSearchQtyChange function (around line 999) + const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { + setSecondSearchResults(prev => + prev.map(item => + item.id === itemId ? { ...item, qty: newQty } : item + ) + ); + + // Check if this item should be auto-added + setTimeout(() => { + checkAndAutoAddItem(itemId); + }, 0); +}, [checkAndAutoAddItem]); + + // 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)) { + handleSecondSearchItemSelect(item.id, true); + } + }); + } else { + // 部分选择:只处理当前页面的选择 + secondSearchResults.forEach(item => { + const isSelected = newIds.includes(item.id); + const isCurrentlyInCreated = isItemInCreated(item.id); + + if (isSelected && !isCurrentlyInCreated) { + handleSecondSearchItemSelect(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)) { + handleSecondSearchItemSelect(id as number, true); + } + }); + + newlyDeselected.forEach(id => { + setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); + }); + } + }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]); + + // 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); + }, []); + + // 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 ? 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 + ) + ); + + // Auto-update created items if this item exists there + setCreatedItems(prev => + prev.map(item => + item.itemId === itemId ? { ...item, qty: newQty || 1 } : item + ) + ); + }, []); + + // 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)) { + handleSecondSearchItemSelect(item.id, true); + } + }); + } else { + // 部分选择:只处理当前页面的选择 + secondSearchResults.forEach(item => { + const isSelected = newIds.includes(item.id); + const isCurrentlyInCreated = isItemInCreated(item.id); + + if (isSelected && !isCurrentlyInCreated) { + handleSecondSearchItemSelect(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)) { + handleSecondSearchItemSelect(id as number, true); + } + }); + + newlyDeselected.forEach(id => { + setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); + }); + } + }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]); + + // 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]); + + // 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); + }, []); + + // 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 ? new Date(item.targetDate).toLocaleDateString() : "-"} + + + + )) + )} + +
+
+ + {/* Pagination for created items */} + + `${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")} + ) : ( + + )} + +)} + + {/* 创建项目区域 - 修改Group列为可选择的 */} + {createdItems.length > 0 && ( + + + {t("Created Items")} ({createdItems.length}) + + + + +)} + + {/* 操作按钮 */} + + + + + + + ); +}; + +export default NewCreateItem; \ No newline at end of file diff --git a/src/components/PickOrderSearch/newcreatitem.tsx b/src/components/PickOrderSearch/newcreatitem.tsx index ab6acbf..1b10387 100644 --- a/src/components/PickOrderSearch/newcreatitem.tsx +++ b/src/components/PickOrderSearch/newcreatitem.tsx @@ -37,7 +37,9 @@ 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; @@ -88,8 +90,11 @@ interface JobOrderDetailPickLine { interface Group { id: number; name: string; - targetDate: 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"); @@ -217,11 +222,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr ); const handleSearch = useCallback(() => { - if (!type) { - alert(t("Please select type")); - return; - } - + if (!searchCode && !searchName) { alert(t("Please enter at least code or name")); return; @@ -368,7 +369,12 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr // 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 @@ -378,6 +384,14 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr } : item )); + setCreatedItems(prev => prev.map(item => + item.groupId === groupId + ? { + ...item, + targetDate: newTargetDate + } + : item + )); }, []); // Fix the handleCreateGroup function to use the API properly @@ -390,7 +404,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr const newGroup: Group = { id: response.id, name: response.name, - targetDate: dayjs().format(INPUT_DATE_FORMAT) + targetDate: "" }; setGroups(prev => [...prev, newGroup]); @@ -405,7 +419,94 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr 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; @@ -444,18 +545,13 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr alert(t("Please select at least one item to submit")); return; } - - if (!data.type) { - alert(t("Please select product type")); - return; - } - - // Remove the data.targetDate check since we'll use group target dates - // if (!data.targetDate) { - // alert(t("Please select target date")); + + // ✅ 修复:自动填充 type 为 "Consumable",不再强制用户选择 + // if (!data.type) { + // alert(t("Please select product type")); // return; // } - + // 按组分组选中的项目 const itemsByGroup = selectedCreatedItems.reduce((acc, item) => { const groupId = item.groupId || 'no-group'; @@ -465,13 +561,13 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr 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 { @@ -492,9 +588,9 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr 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 { @@ -506,9 +602,10 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr return; } } - + + // ✅ 修复:自动使用 "Consumable" 作为默认 type const pickOrderData: SavePickOrderRequest = { - type: data.type || "Consumable", + type: data.type || "Consumable", // 如果用户选择了 type 就用用户的,否则默认 "Consumable" targetDate: formattedTargetDate, pickOrderLine: items.map(item => ({ itemId: item.itemId, @@ -516,7 +613,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr uomId: item.uomId } as SavePickOrderLineRequest)) }; - + console.log(`Submitting pick order for group ${groupName}:`, pickOrderData); const res = await createPickOrder(pickOrderData); @@ -835,7 +932,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr - {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} + {item.targetDate&& item.targetDate !== "" ? new Date(item.targetDate).toLocaleDateString() : "-"} @@ -1001,12 +1098,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr ) ); - // Auto-update created items if this item exists there - setCreatedItems(prev => - prev.map(item => - item.itemId === itemId ? { ...item, qty: newQty || 1 } : item - ) - ); + // Don't auto-add here - only on blur event }, []); // Add checkbox change handler for second search @@ -1020,7 +1112,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr // 全选:将所有搜索结果添加到创建项目 secondSearchResults.forEach(item => { if (!isItemInCreated(item.id)) { - handleSecondSearchItemSelect(item.id, true); + handleSearchItemSelect(item.id, true); } }); } else { @@ -1030,7 +1122,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr const isCurrentlyInCreated = isItemInCreated(item.id); if (isSelected && !isCurrentlyInCreated) { - handleSecondSearchItemSelect(item.id, true); + handleSearchItemSelect(item.id, true); } else if (!isSelected && isCurrentlyInCreated) { setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id)); } @@ -1045,7 +1137,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr newlySelected.forEach(id => { if (!isItemInCreated(id as number)) { - handleSecondSearchItemSelect(id as number, true); + handleSearchItemSelect(id as number, true); } }); @@ -1053,7 +1145,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); }); } - }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]); + }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]); // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity const secondSearchItemColumns: Column[] = useMemo(() => [ @@ -1211,7 +1303,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr setIsLoadingSecondSearch(false); }, 500); }, [items, formProps]); - +/* // Create a custom search box component that displays fields vertically const VerticalSearchBox = ({ criteria, onSearch, onReset }: { criteria: Criterion[]; @@ -1255,6 +1347,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr options={c.options || []} getOptionLabel={(option: any) => option.label} onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")} + value={c.options?.find(option => option.value === inputs[c.paramName]) || null} renderInput={(params) => ( = ({ filterArgs, searchQuery, onPickOrderCr ); }; - +*/ // Add pagination state for search results const [searchResultsPagingController, setSearchResultsPagingController] = useState({ pageNum: 1, @@ -1386,39 +1479,103 @@ const getValidationMessage = useCallback(() => { }, [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) => { - if (checked) { - // Just add to selected IDs, don't auto-add to created items - setSelectedSecondSearchItemIds(prev => [...prev, itemId]); + // Handle individual checkbox change - ONLY select, don't add to created items + const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => { + checkboxChangeCallCount++; - // Set the item's group and targetDate to current group when selected - setSecondSearchResults(prev => prev.map(item => - item.id === itemId - ? { - ...item, - groupId: selectedGroup?.id || undefined, - targetDate: selectedGroup?.targetDate || undefined + 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; } - : item - )); - } else { - // Just remove from selected IDs, don't remove from created items - 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 + + 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(', ')}`); + */ } - : item - )); - } -}, [selectedGroup]); + }, 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[]) => { @@ -1439,7 +1596,7 @@ const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: S ? { ...item, groupId: selectedGroup?.id || undefined, - targetDate: selectedGroup?.targetDate || undefined + targetDate: selectedGroup?.targetDate !== undefined && selectedGroup.targetDate !== "" ? selectedGroup.targetDate : undefined } : item )); @@ -1462,6 +1619,7 @@ const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: S }, [selectedGroup, isItemInCreated]); // Update the CustomSearchResultsTable to use the handlers from component level +/* const CustomSearchResultsTable = () => { // Calculate pagination const startIndex = (searchResultsPagingController.pageNum - 1) * searchResultsPagingController.pageSize; @@ -1524,7 +1682,7 @@ const CustomSearchResultsTable = () => { /> - {/* Item */} + @@ -1536,7 +1694,7 @@ const CustomSearchResultsTable = () => { - {/* Group - Show the item's own group (or "-" if not selected) */} + {(() => { @@ -1549,7 +1707,7 @@ const CustomSearchResultsTable = () => { - {/* Current Stock */} + { - {/* Stock Unit */} + {item.uomDesc || "-"} - - {/* Order Quantity */} + { handleSecondSearchQtyChange(item.id, numValue); } }} + onBlur={() => { + // Trigger auto-add check when user finishes input + handleQtyBlur(item.id); + }} inputProps={{ style: { textAlign: 'center' } }} @@ -1594,7 +1755,7 @@ const CustomSearchResultsTable = () => { /> - {/* Target Date - Show the item's own target date (or "-" if not selected) */} + {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} @@ -1607,7 +1768,7 @@ const CustomSearchResultsTable = () => { - {/* Add pagination for search results */} + { ); }; +*/ // Add helper function to get group range text const getGroupRangeText = useCallback(() => { @@ -1694,10 +1856,11 @@ const CustomSearchResultsTable = () => { { if (date) { const formattedDate = date.format(INPUT_DATE_FORMAT); + handleGroupTargetDateChange(selectedGroup.id, formattedDate); } }} @@ -1728,29 +1891,41 @@ const CustomSearchResultsTable = () => { {/* Second Search Results - Use custom table like AssignAndRelease */} {hasSearchedSecond && ( - - - {t("Search Results")} ({secondSearchResults.length}) - - - {/* Add selected items info text */} - {selectedSecondSearchItemIds.length > 0 && ( - - - {t("Selected items will join above created group")} - - - )} - - {isLoadingSecondSearch ? ( - {t("Loading...")} - ) : secondSearchResults.length === 0 ? ( - {t("No results found")} - ) : ( - - )} - - )} + + + {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 */} @@ -1784,14 +1959,24 @@ const CustomSearchResultsTable = () => { {/* 创建项目区域 - 修改Group列为可选择的 */} {createdItems.length > 0 && ( - - - {t("Created Items")} ({createdItems.length}) - - - - - )} + + + {t("Created Items")} ({createdItems.length}) + + + + +)} {/* 操作按钮 */} diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index cda1222..7ac313b 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -25,15 +25,15 @@ "Bind Storage": "綁定倉位", "itemNo": "貨品編號", "itemName": "貨品名稱", - "qty": "訂單數量", - "Require Qty": "需求數量", + "qty": "訂單數", + "Require Qty": "需求數", "uom": "計量單位", "total weight": "總重量", "weight unit": "重量單位", "price": "訂單貨值", "processed": "已入倉", "expiryDate": "到期日", - "acceptedQty": "是次訂單/來貨/巳來貨數量", + "acceptedQty": "是次訂單/來貨/巳來貨數", "weight": "重量", "start": "開始", "qc": "質量控制", @@ -41,7 +41,7 @@ "stock in": "入庫", "putaway": "上架", "delete": "刪除", - "qty cannot be greater than remaining qty": "數量不能大於剩餘數量", + "qty cannot be greater than remaining qty": "數量不能大於剩餘數", "Record pol": "記錄採購訂單", "Add some entries!": "添加條目!", "draft": "草稿", @@ -59,9 +59,9 @@ "value must be a number": "值必須是數字", "qc Check": "質量控制檢查", "Please select QC": "請選擇質量控制", - "failQty": "失敗數量", + "failQty": "失敗數", "select qc": "選擇質量控制", - "enter a failQty": "請輸入失敗數量", + "enter a failQty": "請輸入失敗數", "qty too big": "數量過大", "sampleRate": "抽樣率", "sampleWeight": "樣本重量", @@ -76,7 +76,7 @@ "acceptedWeight": "接受重量", "productionDate": "生產日期", - "reportQty": "上報數量", + "reportQty": "上報數", "Default Warehouse": "預設倉庫", "Select warehouse": "選擇倉庫", @@ -136,9 +136,9 @@ "Second Search Items": "第二搜尋項目", "Second Search": "第二搜尋", "Item": "貨品", - "Order Quantity": "貨品需求數量", + "Order Quantity": "貨品需求數", "Current Stock": "現時可用庫存", - "Selected": "已選擇", + "Selected": "已選", "Select Items": "選擇貨品", "Assign": "分派提料單", "Release": "放單", @@ -150,25 +150,27 @@ "End Product": "成品", "Lot Expiry Date": "批號到期日", "Lot Location": "批號位置", - "Available Lot": "批號可用提料數量", - "Lot Required Pick Qty": "批號所需提料數量", - "Lot Actual Pick Qty": "批號實際提料數量", + "Available Lot": "批號可用提料數", + "Lot Required Pick Qty": "批號所需提料數", + "Lot Actual Pick Qty": "批號實際提料數", "Lot#": "批號", "Submit": "提交", "Created Items": "已建立貨品", - "Create New Group": "建立新分組", + "Create New Group": "建立新提料分組", "Group": "分組", - "Qty Already Picked": "已提料數量", + "Qty Already Picked": "已提料數", "Select Job Order Items": "選擇工單貨品", - "failedQty": "不合格項目數量", + "failedQty": "不合格項目數", "remarks": "備註", "Qc items": "QC 項目", "qcItem": "QC 項目", "QC Info": "QC 資訊", "qcResult": "QC 結果", - "acceptQty": "接受數量", + "acceptQty": "接受數", "Escalation History": "上報歷史", - "Group Name": "分組名稱", - "Job Order Code": "工單編號" + "Group Code": "分組編號", + "Job Order Code": "工單編號", + "QC Check": "QC 檢查", + "QR Code Scan": "QR Code掃描" } \ No newline at end of file