From 3da220f2361ec94efa4df4b0030a6c5e8e5355d0 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Thu, 28 Aug 2025 12:37:03 +0800 Subject: [PATCH] update --- src/app/api/pickOrder/actions.ts | 92 + src/app/api/qc/actions.ts | 30 + src/app/api/settings/item/actions.ts | 26 +- src/app/api/user/actions.ts | 13 + .../PickOrderSearch/AssignAndRelease.tsx | 640 +++--- .../PickOrderSearch/Jobcreatitem.tsx | 1824 +++++++++++++++++ .../PickOrderSearch/PickExecution.tsx | 941 +++++---- .../PickOrderSearch/PickOrderSearch.tsx | 33 +- .../PickQcStockInModalVer3.tsx | 578 ++++-- src/components/PickOrderSearch/assignTo.tsx | 651 ++++-- .../PickOrderSearch/newcreatitem.tsx | 1662 ++++++++++----- src/i18n/zh/pickOrder.json | 47 +- 12 files changed, 4927 insertions(+), 1610 deletions(-) create mode 100644 src/components/PickOrderSearch/Jobcreatitem.tsx diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index f05d6a5..027ccc0 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -119,6 +119,98 @@ export interface CurrentInventoryItemInfo { requiredQty: number; } +export interface SavePickOrderGroupRequest { + groupIds?: number[]; + names?: string[]; + targetDate?: string; + pickOrderId?: number | null; +} + +export interface PickOrderGroupInfo { + id: number; + name: string; + targetDate: string | null; + pickOrderId: number | null; +} + +export interface AssignPickOrderInputs { + pickOrderIds: number[]; + assignTo: number; +} + +// Missing function 1: newassignPickOrder +export const newassignPickOrder = async (data: AssignPickOrderInputs) => { + const response = await serverFetchJson( + `${BASE_API_URL}/pickOrder/assign`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return response; +}; + +// Missing function 2: releaseAssignedPickOrders +export const releaseAssignedPickOrders = async (data: AssignPickOrderInputs) => { + const response = await serverFetchJson( + `${BASE_API_URL}/pickOrder/release-assigned`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return response; +}; +// Get latest group name and create it automatically +export const getLatestGroupNameAndCreate = async () => { + return serverFetchJson( + `${BASE_API_URL}/pickOrder/groups/latest`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); +}; + +// Get all groups +export const fetchAllGroups = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/pickOrder/groups/list`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); +}); + +// Create or update groups (flexible - can handle both cases) +export const createOrUpdateGroups = async (data: SavePickOrderGroupRequest) => { + const response = await serverFetchJson( + `${BASE_API_URL}/pickOrder/groups/create`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("pickorder"); + return response; +}; + +// Get groups by pick order ID +export const fetchGroupsByPickOrderId = cache(async (pickOrderId: number) => { + return serverFetchJson( + `${BASE_API_URL}/pickOrder/groups/${pickOrderId}`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); +}); export const fetchPickOrderDetails = cache(async (ids: string) => { return serverFetchJson( diff --git a/src/app/api/qc/actions.ts b/src/app/api/qc/actions.ts index 8181d02..f7b19c7 100644 --- a/src/app/api/qc/actions.ts +++ b/src/app/api/qc/actions.ts @@ -16,7 +16,25 @@ export interface QcResult { stockOutLineId?: number; failQty: number; } +export interface SaveQcResultRequest { + qcItemId: number; + itemId: number; + stockInLineId: number | null; + stockOutLineId: number; + failQty: number; + type: string; + remarks: string; + qcPassed: boolean; +} +export interface SaveQcResultResponse { + id: number | null; + name: string; + code: string; + type?: string; + message: string | null; + errorPosition: string; +} export const fetchQcItemCheck = cache(async (itemId?: number) => { let url = `${BASE_API_URL}/qcCheck`; if (itemId) url += `/${itemId}`; @@ -39,3 +57,15 @@ export const fetchPickOrderQcResult = cache(async (id: number) => { }, ); }); +export const savePickOrderQcResult = async (data: SaveQcResultRequest) => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcResult/new`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("qc"); + return response; +}; diff --git a/src/app/api/settings/item/actions.ts b/src/app/api/settings/item/actions.ts index 606856a..6682e16 100644 --- a/src/app/api/settings/item/actions.ts +++ b/src/app/api/settings/item/actions.ts @@ -6,11 +6,12 @@ import { } from "@/app/utils/fetchUtil"; import { revalidateTag } from "next/cache"; import { BASE_API_URL } from "@/config/api"; -import { CreateItemResponse } from "../../utils"; +import { CreateItemResponse, RecordsRes } from "../../utils"; import { ItemQc, ItemsResult } from "."; import { QcChecksInputs } from "../qcCheck/actions"; import { cache } from "react"; + // export type TypeInputs = { // id: number; // name: string @@ -56,6 +57,7 @@ export interface ItemCombo { label: string, uomId: number, uom: string, + uomDesc: string, group?: string, currentStockBalance?: number, } @@ -65,3 +67,25 @@ export const fetchAllItemsInClient = cache(async () => { next: { tags: ["items"] }, }); }); +export const fetchPickOrderItemsByPageClient = cache( + async (queryParams?: Record) => { + if (queryParams) { + const queryString = new URLSearchParams(queryParams).toString(); + return serverFetchJson>( + `${BASE_API_URL}/items/pickOrderItems?${queryString}`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); + } else { + return serverFetchJson>( + `${BASE_API_URL}/items/pickOrderItems`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); + } + }, +); \ No newline at end of file diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index af33193..c2f430e 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -30,6 +30,13 @@ export interface NameList { name: string; } +export interface NewNameList { + id: number; + name: string; + title: string; + department: string; +} + export const fetchUserDetails = cache(async (id: number) => { return serverFetchJson(`${BASE_API_URL}/user/${id}`, { next: { tags: ["user"] }, @@ -42,6 +49,12 @@ export const fetchNameList = cache(async () => { }); }); +export const fetchNewNameList = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/user/new-name-list`, { + next: { tags: ["user"] }, + }); +}); + export const editUser = async (id: number, data: UserInputs) => { const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { method: "PUT", diff --git a/src/components/PickOrderSearch/AssignAndRelease.tsx b/src/components/PickOrderSearch/AssignAndRelease.tsx index 58949de..c0bdcb6 100644 --- a/src/components/PickOrderSearch/AssignAndRelease.tsx +++ b/src/components/PickOrderSearch/AssignAndRelease.tsx @@ -9,9 +9,6 @@ import { Modal, TextField, Typography, - Accordion, - AccordionSummary, - AccordionDetails, Table, TableBody, TableCell, @@ -19,36 +16,27 @@ import { TableHead, TableRow, Paper, + Checkbox, + TablePagination, + Alert, + AlertTitle, } from "@mui/material"; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import SearchResults, { Column } from "../SearchResults/SearchResults"; import { - PickOrderResult, -} from "@/app/api/pickOrder"; -import { - assignPickOrder, - fetchPickOrderClient, + newassignPickOrder, + AssignPickOrderInputs, fetchPickOrderWithStockClient, - releasePickOrder, - ReleasePickOrderInputs, - GetPickOrderInfo, - GetPickOrderLineInfo, } from "@/app/api/pickOrder/actions"; -import { fetchNameList, NameList } from "@/app/api/user/actions"; -import { - FormProvider, - useForm, -} from "react-hook-form"; -import { isEmpty, upperCase, upperFirst } from "lodash"; -import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { fetchNameList, NameList ,fetchNewNameList, NewNameList} from "@/app/api/user/actions"; +import { FormProvider, useForm } from "react-hook-form"; +import { isEmpty, sortBy, uniqBy, upperFirst, groupBy } from "lodash"; +import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; import useUploadContext from "../UploadProvider/useUploadContext"; import dayjs from "dayjs"; import arraySupport from "dayjs/plugin/arraySupport"; import SearchBox, { Criterion } from "../SearchBox"; -import { flatten, intersectionWith, sortBy, uniqBy } from "lodash"; -import { arrayToDayjs } from "@/app/utils/formatUtil"; +import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions"; dayjs.extend(arraySupport); @@ -56,6 +44,56 @@ interface Props { filterArgs: Record; } +// 使用 fetchPickOrderItemsByPageClient 返回的数据结构 +interface ItemRow { + id: string; + pickOrderId: number; + pickOrderCode: string; + itemId: number; + itemCode: string; + itemName: string; + requiredQty: number; + currentStock: number; + unit: string; + targetDate: any; + status: string; + consoCode?: string; + assignTo?: number; + groupName?: string; +} + +// 分组后的数据结构 +interface GroupedItemRow { + pickOrderId: number; + pickOrderCode: string; + targetDate: any; + status: string; + consoCode?: string; + 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%", @@ -71,73 +109,85 @@ const style = { const AssignAndRelease: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const { setIsUploading } = useUploadContext(); - - // State for Pick Orders - const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]); - const [filteredPickOrder, setFilteredPickOrder] = useState([] as GetPickOrderInfo[]); - const [isLoadingPickOrders, setIsLoadingPickOrders] = useState(false); + + // Update state to use pick order data directly + const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); // Change from number[] to string[] + const [filteredPickOrders, setFilteredPickOrders] = useState([]); + const [isLoadingItems, setIsLoadingItems] = useState(false); const [pagingController, setPagingController] = useState({ - pageNum: 0, + pageNum: 1, pageSize: 10, }); - const [totalCountPickOrders, setTotalCountPickOrders] = useState(); - - // State for Assign & Release Modal + const [totalCountItems, setTotalCountItems] = useState(); const [modalOpen, setModalOpen] = useState(false); - const [usernameList, setUsernameList] = useState([]); - - // Add search state + const [usernameList, setUsernameList] = useState([]); const [searchQuery, setSearchQuery] = useState>({}); - const [originalPickOrderData, setOriginalPickOrderData] = useState([] as GetPickOrderInfo[]); + const [originalPickOrderData, setOriginalPickOrderData] = useState([]); - const formProps = useForm(); + const formProps = useForm(); const errors = formProps.formState.errors; - // Fetch Pick Orders with Stock Information - const fetchNewPagePickOrder = useCallback( - async ( - pagingController: Record, - filterArgs: Record, - ) => { - setIsLoadingPickOrders(true); - const params = { - ...pagingController, - ...filterArgs, - }; - const res = await fetchPickOrderWithStockClient(params); - if (res) { - console.log(res); - setFilteredPickOrder(res.records); - setOriginalPickOrderData(res.records); // Store original data - setTotalCountPickOrders(res.total); + // Update the fetch function to process pick order data correctly + const fetchNewPageItems = useCallback( + async (pagingController: Record, filterArgs: Record) => { + setIsLoadingItems(true); + try { + const params = { + ...pagingController, + ...filterArgs, + pageNum: (pagingController.pageNum || 1) - 1, + pageSize: pagingController.pageSize || 10, + }; + + const res = await fetchPickOrderWithStockClient(params); + + if (res && res.records) { + // Filter out assigned status if needed + const filteredRecords = res.records.filter((pickOrder: any) => pickOrder.status !== "assigned"); + + // 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 || [] + })); + + setOriginalPickOrderData(pickOrderRows); + setFilteredPickOrders(pickOrderRows); + setTotalCountItems(res.total); + } else { + setFilteredPickOrders([]); + setTotalCountItems(0); + } + } catch (error) { + console.error("Error fetching pick orders:", error); + setFilteredPickOrders([]); + setTotalCountItems(0); + } finally { + setIsLoadingItems(false); } - setIsLoadingPickOrders(false); }, [], ); - // Add search criteria + // Update search criteria to match the new data structure const searchCriteria: Criterion[] = useMemo( () => [ - { - label: t("Pick Order Code"), - paramName: "code", - type: "text" + { + label: t("Pick Order Code"), + paramName: "code", + type: "text", }, { - label: t("Type"), - paramName: "type", - type: "autocomplete", - options: sortBy( - uniqBy( - originalPickOrderData.map((po) => ({ - value: po.type, - label: t(upperCase(po.type)), - })), - "value", - ), - "label", - ), + label: t("Group Name"), + paramName: "groupName", + type: "text", }, { label: t("Target Date From"), @@ -146,14 +196,14 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { type: "dateRange", }, { - label: t("Status"), + label: t("Pick Order Status"), paramName: "status", type: "autocomplete", options: sortBy( uniqBy( - originalPickOrderData.map((po) => ({ - value: po.status, - label: t(upperFirst(po.status)), + originalPickOrderData.map((pickOrder) => ({ + value: pickOrder.status, + label: t(upperFirst(pickOrder.status)), })), "value", ), @@ -164,103 +214,144 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { [originalPickOrderData, t], ); - // Add search handler + // Update search function to work with pick order data const handleSearch = useCallback((query: Record) => { - console.log("AssignAndRelease search triggered with query:", query); setSearchQuery({ ...query }); - - // Apply search filters to the data - const filtered = originalPickOrderData.filter((po) => { - const poTargetDateStr = arrayToDayjs(po.targetDate); + + const filtered = originalPickOrderData.filter((pickOrder) => { + const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); const codeMatch = !query.code || - po.code?.toLowerCase().includes((query.code || "").toLowerCase()); + pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); - const dateMatch = !query.targetDate || - poTargetDateStr.isSame(query.targetDate) || - poTargetDateStr.isAfter(query.targetDate); + const groupNameMatch = !query.groupName || + pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); - const dateToMatch = !query.targetDateTo || - poTargetDateStr.isSame(query.targetDateTo) || - poTargetDateStr.isBefore(query.targetDateTo); + // Date range search + 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'); + } else if (!query.targetDate && query.targetDateTo) { + const toDate = dayjs(query.targetDateTo); + 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 = (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" || - po.status?.toLowerCase().includes((query.status || "").toLowerCase()); - - const typeMatch = !query.type || - query.type.toLowerCase() === "all" || - po.type?.toLowerCase().includes((query.type || "").toLowerCase()); + pickOrder.status?.toLowerCase().includes((query.status || "").toLowerCase()); - return codeMatch && dateMatch && dateToMatch && statusMatch && typeMatch; + return codeMatch && groupNameMatch && dateMatch && statusMatch; }); - setFilteredPickOrder(filtered); + setFilteredPickOrders(filtered); }, [originalPickOrderData]); - // Add reset handler const handleReset = useCallback(() => { setSearchQuery({}); - // Reset to original data - setFilteredPickOrder(originalPickOrderData); + setFilteredPickOrders(originalPickOrderData); + setTimeout(() => { + setSearchQuery({}); + }, 0); }, [originalPickOrderData]); - // Handle Assign & Release - const handleAssignAndRelease = useCallback(async (data: ReleasePickOrderInputs) => { - if (selectedRows.length === 0) return; - + // Fix the pagination handlers + const handlePageChange = useCallback((event: unknown, newPage: number) => { + const newPagingController = { + ...pagingController, + pageNum: newPage + 1, + }; + setPagingController(newPagingController); + }, [pagingController]); + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + const newPagingController = { + 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 { - // First, assign the pick orders - const assignRes = await assignPickOrder(selectedRows as number[]); - if (assignRes) { + // Convert string IDs to numbers for the API + const numericIds = selectedPickOrderIds.map(id => parseInt(id, 10)); + + const assignRes = await newassignPickOrder({ + pickOrderIds: numericIds, + assignTo: data.assignTo, + }); + + if (assignRes && assignRes.code === "SUCCESS") { console.log("Assign successful:", assignRes); - - // Get the assign code from the response - const consoCode = assignRes.consoCode || assignRes.code; - - if (consoCode) { - // Then, release the assign pick order - const releaseData = { - consoCode: consoCode, - assignTo: data.assignTo - }; - - const releaseRes = await releasePickOrder(releaseData); - if (releaseRes) { - console.log("Release successful:", releaseRes); - setModalOpen(false); - // Clear selected rows - setSelectedRows([]); - // Refresh the pick orders list - fetchNewPagePickOrder(pagingController, filterArgs); - } - } + setModalOpen(false); + setSelectedPickOrderIds([]); // Clear selection + fetchNewPageItems(pagingController, filterArgs); + } else { + console.error("Assign failed:", assignRes); } } catch (error) { - console.error("Error in assign and release:", error); + console.error("Error in assign:", error); } finally { setIsUploading(false); } - }, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]); + }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); - // Open assign & release modal const openAssignModal = useCallback(() => { setModalOpen(true); - // Reset form formProps.reset(); }, [formProps]); - // Load data + // Component mount effect useEffect(() => { - fetchNewPagePickOrder(pagingController, filterArgs); - }, [fetchNewPagePickOrder, pagingController, filterArgs]); + fetchNewPageItems(pagingController, filterArgs || {}); + }, []); + + // Dependencies change effect + useEffect(() => { + if (pagingController && (filterArgs || {})) { + fetchNewPageItems(pagingController, filterArgs || {}); + } + }, [pagingController, filterArgs, fetchNewPageItems]); - // Load username list useEffect(() => { const loadUsernameList = async () => { try { - const res = await fetchNameList(); + const res = await fetchNewNameList(); if (res) { setUsernameList(res); } @@ -271,143 +362,141 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { loadUsernameList(); }, []); - // Pick Orders columns with detailed item information - const pickOrderColumns = useMemo[]>( - () => [ - { - name: "id", - label: "", - type: "checkbox", - disabled: (params) => { - return !isEmpty(params.consoCode); - }, - }, - { - name: "code", - label: t("Pick Order Code"), - }, - { - name: "pickOrderLines", - label: t("Items"), - renderCell: (params) => { - if (!params.pickOrderLines || params.pickOrderLines.length === 0) return ""; - - return ( - - } - sx={{ minHeight: 'auto', padding: 0 }} - > - - {params.pickOrderLines.length} items - - - - - - - - {t("Item Name")} - {t("Required Qty")} - {t("Available Qty")} - {t("Unit")} - - - - {params.pickOrderLines.map((line: GetPickOrderLineInfo, index: number) => ( - - - {line.itemName} - - - {line.requiredQty} - - - = line.requiredQty ? 'success.main' : 'error.main'} - > - {line.availableQty ?? 0} - - - - {line.uomDesc} - - - ))} - -
-
-
-
- ); - }, - }, - { - name: "targetDate", - label: t("Target Date"), - renderCell: (params) => { - return ( - dayjs(params.targetDate) - .add(-1, "month") - .format(OUTPUT_DATE_FORMAT) - ); - }, - }, - { - name: "status", - label: t("Status"), - renderCell: (params) => { - return upperFirst(params.status); - }, - }, - ], - [t], - ); + // Update the table component to work with pick order data directly + const CustomPickOrderTable = () => { + return ( + <> + + + + + {t("Selected")} + {t("Pick Order Code")} + {t("Group Name")} + {t("Item Code")} + {t("Item Name")} + {t("Order Quantity")} + {t("Current Stock")} + {t("Stock Unit")} + {t("Target Date")} + {t("Pick Order Status")} + + + + {filteredPickOrders.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 */} + + {index === 0 ? ( + handlePickOrderSelect(pickOrder.id, e.target.checked)} + disabled={!isEmpty(pickOrder.consoCode)} + /> + ) : null} + + {/* Pick Order Code - only show for first line */} + + {index === 0 ? pickOrder.code : null} + + {/* Group Name - only show for first line */} + + {index === 0 ? pickOrder.groupName : null} + + {/* Item Code */} + {line.itemCode} + {/* Item Name */} + {line.itemName} + + {/* Order Quantity */} + {line.requiredQty} + + {/* Current Stock */} + + 0 ? "success.main" : "error.main"} + sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} + > + {(line.availableQty || 0).toLocaleString()} + + + + {/* Unit */} + {line.uomDesc} + + {/* Target Date - only show for first line */} + + {index === 0 ? ( + arrayToDayjs(pickOrder.targetDate) + .add(-1, "month") + .format(OUTPUT_DATE_FORMAT) + ) : null} + + + {/* Pick Order Status - only show for first line */} + + {index === 0 ? upperFirst(pickOrder.status) : null} + + + )) + )) + )} + +
+
+ + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); + }; return ( <> - {/* Search Box */} - - - {/* Pick Orders View */} + - {/* Remove the button from here */} - {isLoadingPickOrders ? ( + {isLoadingItems ? ( ) : ( - - items={filteredPickOrder} - columns={pickOrderColumns} - pagingController={pagingController} - setPagingController={setPagingController} - totalCount={totalCountPickOrders} - checkboxIds={selectedRows!} - setCheckboxIds={setSelectedRows} - /> + )} - - {/* Add the button below the table */} - + - {/* Assign & Release Modal */} {modalOpen ? ( = ({ filterArgs }) => { - {t("assign & Release Pick Orders")} + {t("Assign Pick Orders")} - - {t("Selected Pick Orders")}: {selectedRows.length} + {t("Selected Pick Orders")}: {selectedPickOrderIds.length} - - +
option.name} + getOptionLabel={(option) => { + // 修改:显示更详细的用户信息 + const title = option.title ? ` (${option.title})` : ''; + const department = option.department ? ` - ${option.department}` : ''; + return `${option.name}${title}${department}`; + }} + renderOption={(props, option) => ( + + + {option.name} + {option.title && ` (${option.title})`} + {option.department && ` - ${option.department}`} + + + )} onChange={(_, value) => { formProps.setValue("assignTo", value?.id || 0); }} @@ -455,23 +556,16 @@ const AssignAndRelease: React.FC = ({ filterArgs }) => { - {t("This action will assign the selected pick orders and release them immediately.")} + {t("This action will assign the selected pick orders.")} - - - diff --git a/src/components/PickOrderSearch/Jobcreatitem.tsx b/src/components/PickOrderSearch/Jobcreatitem.tsx new file mode 100644 index 0000000..9231102 --- /dev/null +++ b/src/components/PickOrderSearch/Jobcreatitem.tsx @@ -0,0 +1,1824 @@ +"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"; + +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 JobCreateItem: 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("Job Order Code"), + paramName: "jobOrderCode", + type: "text" + }, + { + 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]); + + // 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 || "")} + renderInput={(params) => ( + + )} + /> + )} + + ); + })} + + + + + + + + ); + }; + + // Add pagination state for search results + const [searchResultsPagingController, setSearchResultsPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }); + + // Add pagination handlers for search results + const handleSearchResultsPageChange = useCallback((event: unknown, newPage: number) => { + const newPagingController = { + ...searchResultsPagingController, + pageNum: newPage + 1, // API uses 1-based pagination + }; + setSearchResultsPagingController(newPagingController); + }, [searchResultsPagingController]); + + const handleSearchResultsPageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + const newPagingController = { + pageNum: 1, // Reset to first page + pageSize: newPageSize, + }; + setSearchResultsPagingController(newPagingController); + }, []); +const getValidationMessage = useCallback(() => { + const selectedItems = secondSearchResults.filter(item => + selectedSecondSearchItemIds.includes(item.id) + ); + + const itemsWithoutGroup = selectedItems.filter(item => + item.groupId === undefined || item.groupId === null + ); + + const itemsWithoutQty = selectedItems.filter(item => + item.qty === null || item.qty === undefined || item.qty <= 0 + ); + + if (itemsWithoutGroup.length > 0 && itemsWithoutQty.length > 0) { + return t("Please select group and enter quantity for all selected items"); + } else if (itemsWithoutGroup.length > 0) { + return t("Please select group for all selected items"); + } else if (itemsWithoutQty.length > 0) { + return t("Please enter quantity for all selected items"); + } + + return ""; +}, [secondSearchResults, selectedSecondSearchItemIds, t]); + // Fix the handleAddSelectedToCreatedItems function to properly clear selections + const handleAddSelectedToCreatedItems = useCallback(() => { + const selectedItems = secondSearchResults.filter(item => + selectedSecondSearchItemIds.includes(item.id) + ); + + // Add selected items to created items with their own group info + selectedItems.forEach(item => { + if (!isItemInCreated(item.id)) { + const newCreatedItem: CreatedItem = { + itemId: item.id, + itemName: item.label, + itemCode: item.label, + qty: item.qty || 1, + uom: item.uom || "", + uomId: item.uomId || 0, + uomDesc: item.uomDesc || "", + isSelected: true, + currentStockBalance: item.currentStockBalance, + targetDate: item.targetDate || targetDate, + groupId: item.groupId || undefined, + }; + setCreatedItems(prev => [...prev, newCreatedItem]); + } + }); + + // Clear the selection + setSelectedSecondSearchItemIds([]); + + // Remove the selected/added items from search results entirely + setSecondSearchResults(prev => prev.filter(item => + !selectedSecondSearchItemIds.includes(item.id) + )); +}, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); + + // Add a validation function to check if selected items are valid + const areSelectedItemsValid = useCallback(() => { + const selectedItems = secondSearchResults.filter(item => + selectedSecondSearchItemIds.includes(item.id) + ); + + return selectedItems.every(item => + item.groupId !== undefined && + item.groupId !== null && + item.qty !== null && + item.qty !== undefined && + item.qty > 0 + ); + }, [secondSearchResults, selectedSecondSearchItemIds]); + + // Move these handlers to the component level (outside of CustomSearchResultsTable) + +// Handle individual checkbox change - ONLY select, don't add to created items +const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => { + if (checked) { + // Just add to selected IDs, don't auto-add to created items + setSelectedSecondSearchItemIds(prev => [...prev, itemId]); + + // 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 + } + : 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 + } + : item + )); + } +}, [selectedGroup]); + +// Handle select all checkbox for current page +const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: SearchItemWithQty[]) => { + if (checked) { + // Select all items on current page that are not already in created items + const newSelectedIds = paginatedResults + .filter(item => !isItemInCreated(item.id)) + .map(item => item.id); + + setSelectedSecondSearchItemIds(prev => { + const existingIds = prev.filter(id => !paginatedResults.some(item => item.id === id)); + return [...existingIds, ...newSelectedIds]; + }); + + // Set group and targetDate for all selected items on current page + setSecondSearchResults(prev => prev.map(item => + newSelectedIds.includes(item.id) + ? { + ...item, + groupId: selectedGroup?.id || undefined, + targetDate: selectedGroup?.targetDate || undefined + } + : item + )); + } else { + // Deselect all items on current page + const pageItemIds = paginatedResults.map(item => item.id); + setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(id as number))); + + // Clear group and targetDate for all deselected items on current page + setSecondSearchResults(prev => prev.map(item => + pageItemIds.includes(item.id) + ? { + ...item, + groupId: undefined, + targetDate: undefined + } + : item + )); + } +}, [selectedGroup, isItemInCreated]); + +// Update the CustomSearchResultsTable to use the handlers from component level +const CustomSearchResultsTable = () => { + // Calculate pagination + const startIndex = (searchResultsPagingController.pageNum - 1) * searchResultsPagingController.pageSize; + const endIndex = startIndex + searchResultsPagingController.pageSize; + const paginatedResults = secondSearchResults.slice(startIndex, endIndex); + + // Check if all items on current page are selected + const allSelectedOnPage = paginatedResults.length > 0 && + paginatedResults.every(item => selectedSecondSearchItemIds.includes(item.id)); + + // Check if some items on current page are selected + const someSelectedOnPage = paginatedResults.some(item => selectedSecondSearchItemIds.includes(item.id)); + + return ( + <> + + + + + + {t("Selected")} + + + {t("Item")} + + + {t("Group")} + + + {t("Current Stock")} + + + {t("Stock Unit")} + + + {t("Order Quantity")} + + + {t("Target Date")} + + + + + {paginatedResults.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + paginatedResults.map((item) => ( + + + handleIndividualCheckboxChange(item.id, e.target.checked)} + disabled={isItemInCreated(item.id)} + /> + + + {/* Item */} + + + + {item.label.split(' - ')[1] || item.label} + + + {item.label.split(' - ')[0] || ''} + + + + + {/* Group - Show the item's own group (or "-" if not selected) */} + + + {(() => { + if (item.groupId) { + const group = groups.find(g => g.id === item.groupId); + return group?.name || "-"; + } + return "-"; // Show "-" for unselected items + })()} + + + + {/* Current Stock */} + + 0 ? "success.main" : "error.main"} + sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }} + > + {item.currentStockBalance || 0} + + + + {/* Stock Unit */} + + + {item.uomDesc || "-"} + + + + {/* Order Quantity */} + + { + 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' + } + }} + /> + + + {/* Target Date - Show the item's own target date (or "-" if not selected) */} + + + {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} + + + + )) + )} + +
+
+ + {/* Add pagination for search results */} + + `${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}) + + + {/* 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")} + ) : ( + + )} + + )} + + {/* Add Submit Button between tables */} + + {/* Search Results with SearchResults component */} + {hasSearchedSecond && secondSearchResults.length > 0 && selectedSecondSearchItemIds.length > 0 && ( + + + + + {selectedSecondSearchItemIds.length > 0 && !areSelectedItemsValid() && ( + + {getValidationMessage()} + + )} + + + )} + + + {/* 创建项目区域 - 修改Group列为可选择的 */} + {createdItems.length > 0 && ( + + + {t("Created Items")} ({createdItems.length}) + + + + + )} + + {/* 操作按钮 */} + + + + + + + ); +}; + +export default JobCreateItem; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PickExecution.tsx b/src/components/PickOrderSearch/PickExecution.tsx index 60c92dc..19fe14b 100644 --- a/src/components/PickOrderSearch/PickExecution.tsx +++ b/src/components/PickOrderSearch/PickExecution.tsx @@ -22,6 +22,7 @@ import { Select, MenuItem, InputLabel, + TablePagination, } from "@mui/material"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -60,11 +61,14 @@ import PickQcStockInModalVer2 from "./PickQcStockInModalVer3"; import { fetchPickOrderLineLotDetails, PickOrderLotDetailResponse } from "@/app/api/pickOrder/actions"; import SearchResults, { Column } from "../SearchResults/SearchResults"; import { defaultPagingController } from "../SearchResults/SearchResults"; +import SearchBox, { Criterion } from "../SearchBox"; +import dayjs from "dayjs"; +import { dummyQCData } from "../PoDetail/dummyQcTemplate"; + interface Props { filterArgs: Record; } - interface LotPickData { id: number; lotId: number; @@ -106,15 +110,12 @@ const PickExecution: React.FC = ({ filterArgs }) => { const [disableRelease, setDisableRelease] = useState(true); const [selectedRowId, setSelectedRowId] = useState(null); - const [pickOrderDetails, setPickOrderDetails] = useState(null); const [detailLoading, setDetailLoading] = useState(false); - const [pickQtyData, setPickQtyData] = useState({}); const [lotData, setLotData] = useState([]); - const [qcItems, setQcItems] = useState([]); const [qcModalOpen, setQcModalOpen] = useState(false); const [selectedItemForQc, setSelectedItemForQc] = useState = ({ filterArgs }) => { qcResult?: PurchaseQcResult[]; } | null>(null); + // 新增:分页控制器 + const [mainTablePagingController, setMainTablePagingController] = useState({ + pageNum: 0, + pageSize: 10, + }); + const [lotTablePagingController, setLotTablePagingController] = useState({ + pageNum: 0, + pageSize: 10, + }); + + // Add missing search state variables + const [searchQuery, setSearchQuery] = useState>({}); + const [originalPickOrderData, setOriginalPickOrderData] = useState(null); + const formProps = useForm(); const errors = formProps.formState.errors; @@ -193,15 +208,14 @@ const PickExecution: React.FC = ({ filterArgs }) => { [isReleasable], ); - const handleFetchAllPickOrderDetails = useCallback(async () => { setDetailLoading(true); try { const data = await fetchAllPickOrderDetails(); setPickOrderDetails(data); + setOriginalPickOrderData(data); // Store original data for filtering console.log("All Pick Order Details:", data); - const initialPickQtyData: PickQtyData = {}; data.pickOrders.forEach((pickOrder: any) => { pickOrder.pickOrderLines.forEach((line: any) => { @@ -217,7 +231,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, []); - useEffect(() => { handleFetchAllPickOrderDetails(); }, [handleFetchAllPickOrderDetails]); @@ -248,6 +261,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { }, [router], ); + const onSubmitError = useCallback>( (errors) => {}, [], @@ -264,49 +278,76 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, [selectedConsoCode, fetchConso, formProps]); - const handlePickQtyChange = useCallback((lineId: number, lotId: number, value: number) => { - setPickQtyData(prev => ({ - ...prev, - [lineId]: { - ...prev[lineId], - [lotId]: value - } - })); + console.log("Changing pick qty:", { lineId, lotId, value }); + setPickQtyData(prev => { + const newData = { + ...prev, + [lineId]: { + ...prev[lineId], + [lotId]: value + } + }; + console.log("New pick qty data:", newData); + return newData; + }); }, []); const handleSubmitPickQty = useCallback((lineId: number, lotId: number) => { const qty = pickQtyData[lineId]?.[lotId] || 0; console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`); - }, [pickQtyData]); - const getTotalPickedQty = useCallback((lineId: number) => { const lineData = pickQtyData[lineId]; if (!lineData) return 0; 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); - try { + try { + // Try to get real data first const qcItemsData = await fetchQcItemCheck(line.itemId); - setQcItems(qcItemsData); - console.log("QC Items:", qcItemsData); + 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: PurchaseQcResult[] = []; + // 修复:处理类型不匹配问题 + let qcResult: any[] = []; try { - qcResult = await fetchPickOrderQcResult(line.id); + 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"); @@ -321,148 +362,75 @@ const PickExecution: React.FC = ({ filterArgs }) => { 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 + })); + setQcItems(transformedDummyData); } }, []); - const handleCloseQcModal = useCallback(() => { console.log("Closing QC modal"); setQcModalOpen(false); setSelectedItemForQc(null); }, []); - const handleSetItemDetail = useCallback((item: any) => { setSelectedItemForQc(item); }, []); - - const renderMainTableRow = useCallback((line: GetPickOrderLineInfo, pickOrderCode: string) => { - const handleRowSelect = async (event: React.ChangeEvent) => { - if (event.target.checked) { - setSelectedRowId(line.id); - - try { - const lotDetails = await fetchPickOrderLineLotDetails(line.id); - console.log("Lot details from API:", lotDetails); - - - const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ - id: lot.lotId, // Add id here - lotId: lot.lotId, - lotNo: lot.lotNo, - expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', - location: lot.location, - stockUnit: lot.stockUnit, - availableQty: lot.availableQty, - requiredQty: lot.requiredQty, - actualPickQty: lot.actualPickQty || 0, - lotStatus: lot.lotStatus, - lotAvailability: lot.lotAvailability - })); - - setLotData(realLotData); - } catch (error) { - console.error("Error fetching lot details:", error); - - setLotData([]); - } - } else { - setSelectedRowId(null); - setLotData([]); - } - }; - - // Calculate Balance to Pick (availableQty - requiredQty) - const balanceToPick = line.availableQty - line.requiredQty; - const totalPickedQty = getTotalPickedQty(line.id); - - return ( - *": { borderBottom: "unset" }, - color: "black", - backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit", - cursor: "pointer", - "&:hover": { - backgroundColor: "action.hover", - }, - }} - > - - e.stopPropagation()} - /> - - {pickOrderCode} - {line.itemCode} - {line.itemName} - {line.requiredQty} - = 0 ? 'success.main' : 'error.main', - //fontWeight: 'bold' - }}> - {balanceToPick} - - {totalPickedQty} - - ); - }, [selectedRowId, getTotalPickedQty]); - - - const selectedRow = useMemo(() => { - if (!selectedRowId || !pickOrderDetails) return null; - - // 在所有 pick order lines 中查找选中的行 - for (const pickOrder of pickOrderDetails.pickOrders) { - const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); - if (foundLine) { - return { ...foundLine, pickOrderCode: pickOrder.code }; - } - } - return null; - }, [selectedRowId, pickOrderDetails]); - - // Add these state variables after the existing useState declarations - const [area4PagingController, setArea4PagingController] = useState(defaultPagingController); - const [area5PagingController, setArea5PagingController] = useState(defaultPagingController); - const [selectedLotIds, setSelectedLotIds] = useState([]); - - // Add these helper functions - const getPaginatedData = useCallback((data: any[], pagingController: any) => { - const startIndex = pagingController.pageNum * pagingController.pageSize; - const endIndex = startIndex + pagingController.pageSize; - return data.slice(startIndex, endIndex); + // 新增:处理分页变化 + const handleMainTablePageChange = useCallback((event: unknown, newPage: number) => { + setMainTablePagingController(prev => ({ + ...prev, + pageNum: newPage, + })); }, []); - const getTotalPages = useCallback((totalCount: number, pageSize: number) => { - return Math.ceil(totalCount / pageSize); + const handleMainTablePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + setMainTablePagingController({ + pageNum: 0, + pageSize: newPageSize, + }); }, []); - // Add this useEffect to reset pagination when data changes - useEffect(() => { - setArea4PagingController({ pageNum: 0, pageSize: 10 }); - }, [pickOrderDetails]); + const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => { + setLotTablePagingController(prev => ({ + ...prev, + pageNum: newPage, + })); + }, []); - useEffect(() => { - setArea5PagingController({ pageNum: 0, pageSize: 10 }); - }, [lotData]); + const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + setLotTablePagingController({ + pageNum: 0, + pageSize: newPageSize, + }); + }, []); - // Add this function to handle row selection + // 新增:处理行选择 const handleRowSelect = useCallback(async (lineId: number) => { setSelectedRowId(lineId); - // Get real lot data try { const lotDetails = await fetchPickOrderLineLotDetails(lineId); console.log("Lot details from API:", lotDetails); const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ - id: lot.lotId, // Add this line - lotId: lot.lotId, + id: lot.lotId, // This is actually ill.id (inventory lot line ID) + lotId: lot.lotId, // This should be the unique inventory lot line ID lotNo: lot.lotNo, expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', location: lot.location, @@ -481,299 +449,482 @@ const PickExecution: React.FC = ({ filterArgs }) => { } }, []); - const prepareArea4Data = useMemo(() => { + const prepareMainTableData = useMemo(() => { if (!pickOrderDetails) return []; return pickOrderDetails.pickOrders.flatMap((pickOrder) => - pickOrder.pickOrderLines.map((line) => ({ - ...line, - pickOrderCode: pickOrder.code, - balanceToPick: line.availableQty - line.requiredQty, - })) + 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]); - const prepareArea5Data = useMemo(() => { + const prepareLotTableData = useMemo(() => { return lotData.map((lot) => ({ ...lot, - id: lot.lotId, // Add id field for SearchResults + id: lot.lotId, })); }, [lotData]); - const area4Columns = useMemo[]>( - () => [ - { - name: "select", - label: "", - type: "checkbox", - disabled: () => false, // Allow all rows to be selectable - }, - { - name: "pickOrderCode", - label: t("Pick Order#"), - }, - { - name: "itemCode", - label: t("Item Code"), - }, - { - name: "itemName", - label: t("Item Description"), - }, - { - name: "requiredQty", - label: t("Required Qty"), - - }, - { - name: "balanceToPick", - label: t("Balance to Pick"), + // 新增:分页数据 + const paginatedMainTableData = useMemo(() => { + const startIndex = mainTablePagingController.pageNum * mainTablePagingController.pageSize; + const endIndex = startIndex + mainTablePagingController.pageSize; + return prepareMainTableData.slice(startIndex, endIndex); + }, [prepareMainTableData, mainTablePagingController]); + + const paginatedLotTableData = useMemo(() => { + const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize; + const endIndex = startIndex + lotTablePagingController.pageSize; + return prepareLotTableData.slice(startIndex, endIndex); + }, [prepareLotTableData, lotTablePagingController]); + + const selectedRow = useMemo(() => { + if (!selectedRowId || !pickOrderDetails) return null; + + for (const pickOrder of pickOrderDetails.pickOrders) { + const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); + if (foundLine) { + return { ...foundLine, pickOrderCode: pickOrder.code }; + } + } + return null; + }, [selectedRowId, pickOrderDetails]); + + // Add these state variables (around line 110) + const [selectedLotId, setSelectedLotId] = useState(null); + + // Add this function (around line 350) + const handleLotSelection = useCallback((uniqueLotId: string) => { + setSelectedLotId(uniqueLotId); + }, []); + + // Add this function (around line 480) + const hasSelectedLots = useCallback((lineId: number) => { + return selectedLotId !== null; + }, [selectedLotId]); + + // 自定义主表格组件 + const CustomMainTable = () => { + return ( + <> + + + + + {t("Selected")} + {t("Pick Order Code")} + {t("Item Code")} + {t("Item Name")} + {t("Order Quantity")} + {t("Current Stock")} + {t("Qty Already Picked")} + {t("Stock Unit")} + {t("Target Date")} + + + + {paginatedMainTableData.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + paginatedMainTableData.map((line) => { + // 修复:处理 availableQty 可能为 null 的情况,并确保负值显示为 0 + const availableQty = line.availableQty ?? 0; + const balanceToPick = Math.max(0, availableQty - line.requiredQty); // 确保不为负数 + const totalPickedQty = getTotalPickedQty(line.id); + + return ( + *": { borderBottom: "unset" }, + color: "black", + backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit", + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + > + + { + if (e.target.checked) { + handleRowSelect(line.id); + } else { + setSelectedRowId(null); + setLotData([]); + } + }} + onClick={(e) => e.stopPropagation()} + /> + + {line.pickOrderCode} + {line.itemCode} + {line.itemName} + {line.requiredQty} + = line.requiredQty ? 'success.main' : 'error.main', + }}> + {availableQty.toLocaleString()} {/* 添加千位分隔符 */} + + {totalPickedQty} + {line.uomDesc} + {line.targetDate} + + ); + }) + )} + +
+
- renderCell: (params) => { - const balanceToPick = params.availableQty - params.requiredQty; - return ( - = 0 ? 'success.main' : 'error.main', - //fontWeight: 'bold' - }} - > - {balanceToPick} - - ); - }, - }, - { - name: "qtyAlreadyPicked", - label: t("Qty Already Picked"), + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); + }; + + // 自定义批次表格组件 + 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} + + + + + )) + )} + +
+
- renderCell: (params) => { - return getTotalPickedQty(params.id); - }, - }, - ], - [t, getTotalPickedQty], - ); + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); + }; - const area5Columns = useMemo[]>( + // Add search criteria + const searchCriteria: Criterion[] = useMemo( () => [ { - name: "id", // Use "id" instead of "select" - label: "", - type: "checkbox", - disabled: () => false, - }, - { - name: "lotNo", - label: t("Lot#"), - renderCell: (params) => ( - - {params.lotNo} - {params.lotAvailability !== 'available' && ( - - ({params.lotAvailability === 'expired' ? 'Expired' : - params.lotAvailability === 'insufficient_stock' ? 'Insufficient' : - 'Unavailable'}) - - )} - - ), - }, - { - name: "expiryDate", - label: t("Lot ExpiryDate"), - }, - { - name: "location", - label: t("Lot Location"), - }, - { - name: "stockUnit", - label: t("Stock Unit"), - }, - { - name: "availableQty", - label: t("Available Lot"), - align: "right", + label: t("Item Code"), + paramName: "itemCode", + type: "text", }, { - name: "requiredQty", - label: t("Lot Required Pick Qty"), - align: "right", + label: t("Pick Order Code"), + paramName: "pickOrderCode", + type: "text", }, { - name: "actualPickQty", - label: t("Lot Actual Pick Qty"), - align: "right", - renderCell: (params) => ( - { - if (selectedRowId) { - handlePickQtyChange( - selectedRowId, - params.lotId, - parseInt(e.target.value) || 0 - ); - } - }} - inputProps={{ min: 0, max: params.availableQty }} - disabled={params.lotAvailability !== 'available'} - sx={{ width: '80px' }} - /> - ), + label: t("Item Name"), + paramName: "itemName", + type: "text", }, { - name: "lotId", - label: t("Submit"), - align: "center", - renderCell: (params) => ( - - ), + label: t("Target Date From"), + label2: t("Target Date To"), + paramName: "targetDate", + type: "dateRange", }, ], - [t, selectedRowId, pickQtyData, handlePickQtyChange, handleSubmitPickQty], + [t], ); + // Add search handler + const handleSearch = useCallback((query: Record) => { + setSearchQuery({ ...query }); + console.log("Search query:", query); + + if (!originalPickOrderData) return; + + const filtered = originalPickOrderData.pickOrders.filter((pickOrder) => { + // Check if any line in this pick order matches the search criteria + return pickOrder.pickOrderLines.some((line) => { + const itemCodeMatch = !query.itemCode || + line.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); + + const itemNameMatch = !query.itemName || + line.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); + + const pickOrderCodeMatch = !query.pickOrderCode || + pickOrder.code?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); + + + return itemCodeMatch && itemNameMatch && pickOrderCodeMatch ; + }); + }); + + // Create filtered data structure + const filteredData: GetPickOrderInfoResponse = { + ...originalPickOrderData, + pickOrders: filtered + }; + + setPickOrderDetails(filteredData); + console.log("Filtered pick orders count:", filtered.length); + }, [originalPickOrderData, t]); + + // Add reset handler + const handleReset = useCallback(() => { + setSearchQuery({}); + if (originalPickOrderData) { + setPickOrderDetails(originalPickOrderData); + } + }, [originalPickOrderData]); + + // Add this to debug the lot data + useEffect(() => { + console.log("Lot data:", lotData); + console.log("Pick Qty Data:", pickQtyData); + }, [lotData, pickQtyData]); + return ( - - {/* Area 4 & 5: Main Table and Detail Side by Side */} - - {/* Area 4: Main Table */} - + + {/* Search Box */} + + + + + {/* 主表格 */} + + + {t("Pick Order Details")} + + {detailLoading ? ( + + + + ) : pickOrderDetails ? ( + + ) : ( + + + 正在載入數據... + + + )} + + + {/* 批次表格 - 放在主表格下方 */} + {selectedRow && ( + - {t("Pick Order Details")} + Item lot to be Pick: {selectedRow.pickOrderCode} - {selectedRow.itemName} - {detailLoading ? ( - - - - ) : pickOrderDetails ? ( - <> - - - items={prepareArea4Data} - columns={area4Columns} - pagingController={area4PagingController} - setPagingController={setArea4PagingController} - totalCount={prepareArea4Data.length} - checkboxIds={selectedRowId ? [selectedRowId] : []} - setCheckboxIds={(ids) => { - const newSelectedId = ids[0] as number || null; - setSelectedRowId(newSelectedId); - - // Handle lot data fetching when selection changes - if (newSelectedId) { - handleRowSelect(newSelectedId); - } else { - setLotData([]); - } - }} - /> - - - + + {/* 检查是否有可用的批次数据 */} + {lotData.length > 0 ? ( + ) : ( - - - 正在載入數據... + + + {selectedRow.availableQty === null || selectedRow.availableQty === 0 + ? t("No available stock for this item") + : t("No lot details available for this item") + } - - )} - - - {/* Area 5: Item lot to be Pick */} - - {selectedRow && lotData.length > 0 ? ( - <> - - Item lot to be Pick: {selectedRow.pickOrderCode} - {selectedRow.itemName} - - - items={prepareArea5Data} - columns={area5Columns} - pagingController={area5PagingController} - setPagingController={setArea5PagingController} - totalCount={prepareArea5Data.length} - checkboxIds={selectedLotIds} - setCheckboxIds={(ids) => { - setSelectedLotIds(Array.isArray(ids) ? ids as number[] : []); - }} - /> - - {/* Action buttons below the table */} - - - - - - - - ) : ( - - - Please select an pick order lot to view lot information + + {selectedRow.availableQty === null || selectedRow.availableQty === 0 + ? t("Current stock is insufficient or unavailable") + : t("Please check inventory status") + } )} - - + + {/* Action buttons below the lot table */} + + + + + + + + )} - {/* Area 6: Action Buttons */} + {/* Action Buttons */} {selectedRow && ( - + - - + - - + > + {t("release")} + + +
)} {/* QC Modal */} @@ -785,10 +936,12 @@ const PickExecution: React.FC = ({ filterArgs }) => { setItemDetail={handleSetItemDetail} qc={qcItems} warehouse={[]} + qcItems={qcItems} + setQcItems={setQcItems} /> )} - + ); }; diff --git a/src/components/PickOrderSearch/PickOrderSearch.tsx b/src/components/PickOrderSearch/PickOrderSearch.tsx index f7e1daf..943b681 100644 --- a/src/components/PickOrderSearch/PickOrderSearch.tsx +++ b/src/components/PickOrderSearch/PickOrderSearch.tsx @@ -25,6 +25,7 @@ import AssignAndRelease from "./AssignAndRelease"; import AssignTo from "./assignTo"; import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; +import Jobcreatitem from "./Jobcreatitem"; interface Props { pickOrders: PickOrderResult[]; @@ -224,6 +225,12 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { } }, [isOpenCreateModal]) + // 添加处理提料单创建成功的函数 + const handlePickOrderCreated = useCallback(() => { + // 切换到 Assign & Release 标签页 (tabIndex = 1) + setTabIndex(1); + }, []); + return ( <> = ({ pickOrders }) => { - + + - - + {/**/} + {/**/} - {tabIndex === 4 && ( + {/*{tabIndex === 4 && ( + )}*/} + {/*{tabIndex === 5 && }*/} + {tabIndex === 4 && } + {tabIndex === 0 && ( + )} - {tabIndex === 5 && } - {tabIndex === 3 && } - {tabIndex === 0 && } - {tabIndex === 1 && } - {tabIndex === 2 && } + {tabIndex === 1 && } + {tabIndex === 2&& } + {tabIndex === 3 && } ); }; diff --git a/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx b/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx index 039f498..fea96f0 100644 --- a/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx +++ b/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx @@ -11,24 +11,33 @@ import { ModalProps, Stack, Typography, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, TextField, Radio, RadioGroup, FormControlLabel, FormControl, + Tab, + Tabs, + TabsProps, + Paper, } from "@mui/material"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; -import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; +import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate"; +import { dummyQCData } from "../PoDetail/dummyQcTemplate"; +import StyledDataGrid from "../StyledDataGrid"; +import { GridColDef } from "@mui/x-data-grid"; import { submitDialogWithWarning } from "../Swal/CustomAlerts"; +import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; +import EscalationComponent from "../PoDetail/EscalationComponent"; +import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions"; + +// Define QcData interface locally +interface ExtendedQcItem extends QcItemWithChecks { + qcPassed?: boolean; + failQty?: number; + remarks?: string; +} const style = { position: "absolute", @@ -40,10 +49,11 @@ const style = { px: 5, pb: 10, display: "block", - width: { xs: "60%", sm: "60%", md: "60%" }, + width: { xs: "80%", sm: "80%", md: "80%" }, + maxHeight: "90vh", + overflowY: "auto", }; - interface CommonProps extends Omit { itemDetail: GetPickOrderLineInfo & { pickOrderCode: string; @@ -67,31 +77,47 @@ interface Props extends CommonProps { pickOrderCode: string; qcResult?: PurchaseQcResult[] }; + qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem + setQcItems: Dispatch>; // Change to ExtendedQcItem } - -const PickQcStockInModalVer2: React.FC = ({ +const PickQcStockInModalVer3: React.FC = ({ open, onClose, itemDetail, setItemDetail, qc, warehouse, + qcItems, + setQcItems, }) => { - console.log(warehouse); - const { t, i18n: { language }, } = useTranslation("pickOrder"); - - const [qcItems, setQcItems] = useState(dummyQCData) + + const [tabIndex, setTabIndex] = useState(0); + //const [qcItems, setQcItems] = useState(dummyQCData); + const [isCollapsed, setIsCollapsed] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [feedbackMessage, setFeedbackMessage] = useState(""); + + // Add state to store submitted data + const [submittedData, setSubmittedData] = useState([]); + const formProps = useForm({ defaultValues: { + qcAccept: true, + acceptQty: itemDetail.requiredQty ?? 0, + qcDecision: "1", // Default to accept ...itemDetail, }, }); - + const { control, register, formState: { errors }, watch, setValue } = formProps; + + const qcDecision = watch("qcDecision"); + const accQty = watch("acceptQty"); + const closeHandler = useCallback>( (...args) => { onClose?.(...args); @@ -99,228 +125,368 @@ const PickQcStockInModalVer2: React.FC = ({ [onClose], ); - // QC submission handler + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [], + ); + + // Save failed QC results only + const saveQcResults = async (qcData: any) => { + try { + const qcResults = qcData.qcItems + .map((item: any) => ({ + qcItemId: item.id, + itemId: itemDetail.itemId, + stockInLineId: null, + stockOutLineId: 1, // Fixed to 1 as requested + failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed + type: "pick_order_qc", + remarks: item.remarks || "", + qcPassed: item.isPassed, // ✅ This will now be included + })); + + // Store the submitted data for debug display + setSubmittedData(qcResults); + console.log("Saving QC results:", qcResults); + + // Use the corrected API function instead of manual fetch + for (const qcResult of qcResults) { + const response = await savePickOrderQcResult(qcResult); + console.log("QC Result save success:", response); + + // Check if the response indicates success + if (!response.id) { + throw new Error(`Failed to save QC result: ${response.message || 'Unknown error'}`); + } + } + return true; + } catch (error) { + console.error("Error saving QC results:", error); + return false; + } + }; + + // Submit with QcComponent-style decision handling const onSubmitQc = useCallback>( async (data, event) => { - console.log("QC Submission:", event!.nativeEvent); + setIsSubmitting(true); - // Get QC data from the shared form context - const qcAccept = data.qcAccept; - const acceptQty = data.acceptQty; - - // Validate QC data - const validationErrors : string[] = []; - // Check if all QC items have results - const itemsWithoutResult = qcItems.filter(item => item.isPassed === undefined); - if (itemsWithoutResult.length > 0) { - validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.qcItem).join(', ')}`); - } + try { + const qcAccept = qcDecision === "1"; + const acceptQty = Number(accQty) || itemDetail.requiredQty; - // Check if failed items have failed quantity - const failedItemsWithoutQty = qcItems.filter(item => - item.isPassed === false && (!item.failedQty || item.failedQty <= 0) - ); - if (failedItemsWithoutQty.length > 0) { - validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`); - } + const validationErrors : string[] = []; - // Check if accept quantity is valid - if (acceptQty === undefined || acceptQty <= 0) { - validationErrors.push("Accept quantity must be greater than 0"); - } + const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); + if (itemsWithoutResult.length > 0) { + validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); + } - if (validationErrors.length > 0) { - console.error("QC Validation failed:", validationErrors); - alert(`QC failed: ${validationErrors}`); - return; - } + const failedItemsWithoutQty = qcItems.filter(item => + item.qcPassed === false && (!item.failQty || item.failQty <= 0) + ); + if (failedItemsWithoutQty.length > 0) { + validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.code).join(", ")}`); + } - const qcData = { - qcAccept: qcAccept, - acceptQty: acceptQty, - qcItems: qcItems.map(item => ({ - id: item.id, - qcItem: item.qcItem, - qcDescription: item.qcDescription, - isPassed: item.isPassed, - failedQty: (item.failedQty && !item.isPassed) || 0, - remarks: item.remarks || '' - })) - }; + if (qcDecision === "1" && (acceptQty === undefined || acceptQty <= 0)) { + validationErrors.push("Accept quantity must be greater than 0"); + } - console.log("QC Data for submission:", qcData); - // await submitQcData(qcData); + if (validationErrors.length > 0) { + alert(`QC failed: ${validationErrors.join(", ")}`); + return; + } - if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { - submitDialogWithWarning(() => { - console.log("QC accepted with failed items"); - onClose?.(); - }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); - return; - } + const qcData = { + qcAccept, + acceptQty, + qcItems: qcItems.map(item => ({ + id: item.id, + qcItem: item.code, // Use code instead of qcItem + qcDescription: item.description || "", // Use description instead of qcDescription + isPassed: item.qcPassed, + failQty: item.qcPassed ? 0 : (item.failQty ?? 0), + remarks: item.remarks || "", + })), + }; - if (qcData.qcAccept) { - console.log("QC accepted"); - onClose?.(); - } else { - console.log("QC rejected"); - onClose?.(); + console.log("Submitting QC data:", qcData); + + const saveSuccess = await saveQcResults(qcData); + if (!saveSuccess) { + alert("Failed to save QC results"); + return; + } + + // Show success message + alert("QC results saved successfully!"); + + if (!qcData.qcItems.every((q) => q.isPassed) && qcData.qcAccept) { + submitDialogWithWarning(() => { + closeHandler?.({}, 'escapeKeyDown'); + }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); + return; + } + + closeHandler?.({}, 'escapeKeyDown'); + } catch (error) { + console.error("Error in QC submission:", error); + alert("Error saving QC results: " + (error as Error).message); + } finally { + setIsSubmitting(false); } }, - [qcItems, onClose, t], + [qcItems, closeHandler, t, itemDetail, qcDecision, accQty], ); - const handleQcItemChange = useCallback((index: number, field: keyof QcData, value: any) => { - setQcItems(prev => prev.map((item, i) => - i === index ? { ...item, [field]: value } : item - )); - }, []); + // DataGrid columns (QcComponent style) + const qcColumns: GridColDef[] = useMemo( + () => [ + { + field: "code", + headerName: t("qcItem"), + flex: 2, + renderCell: (params) => ( + + {`${params.api.getRowIndexRelativeToVisibleRows(params.id) + 1}. ${params.value}`}
+ {params.row.name}
+
+ ), + }, + { + field: "qcPassed", + headerName: t("qcResult"), + flex: 1.5, + renderCell: (params) => { + const current = params.row; + return ( + + { + const value = e.target.value === "true"; + setQcItems((prev) => + prev.map((r): ExtendedQcItem => (r.id === params.id ? { ...r, qcPassed: value } : r)) + ); + }} + name={`qcPassed-${params.id}`} + > + } + label="合格" + sx={{ + color: current.qcPassed === true ? "green" : "inherit", + "& .Mui-checked": {color: "green"} + }} + /> + } + label="不合格" + sx={{ + color: current.qcPassed === false ? "red" : "inherit", + "& .Mui-checked": {color: "red"} + }} + /> + + + ); + }, + }, + { + field: "failQty", + headerName: t("failedQty"), + flex: 1, + renderCell: (params) => ( + { + const v = e.target.value; + const next = v === "" ? undefined : Number(v); + if (Number.isNaN(next)) return; + setQcItems((prev) => + prev.map((r) => (r.id === params.id ? { ...r, failQty: next } : r)) + ); + }} + onClick={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + inputProps={{ min: 0 }} + sx={{ width: "100%" }} + /> + ), + }, + { + field: "remarks", + headerName: t("remarks"), + flex: 2, + renderCell: (params) => ( + { + const remarks = e.target.value; + setQcItems((prev) => + prev.map((r) => (r.id === params.id ? { ...r, remarks } : r)) + ); + }} + onClick={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + sx={{ width: "100%" }} + /> + ), + }, + ], + [t], + ); return ( <> - - + + - - GroupA - {itemDetail.pickOrderCode} - - - 記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標 - + + + + - {/* QC table - same as QcFormVer2 */} + {tabIndex == 0 && ( + <> + + + + Group A - 急凍貨類 (QCA1-MEAT01) + + + 品檢類型:OQC + + + 記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全
+ 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標 +
+
+ + +
+ + )} + + {tabIndex == 1 && ( + <> + + + + + )} + - - - - - QC模板代號 - 檢查項目 - QC RESULT - FAILED QTY - REMARKS - - - - {qcItems.map((item, index) => ( - - {item.id} - - {item.qcDescription} - - - {/* same as QcFormVer2 */} - - { - const value = e.target.value; - handleQcItemChange(index, 'isPassed', value === "true"); - }} - name={`isPassed-${item.id}`} - > - } - label="合格" - sx={{ - color: item.isPassed === true ? "green" : "inherit", - "& .Mui-checked": {color: "green"} - }} - /> - } - label="不合格" - sx={{ - color: item.isPassed === false ? "red" : "inherit", - "& .Mui-checked": {color: "red"} - }} - /> - - - - - { - const v = e.target.value; - const next = v === '' ? undefined : Number(v); - if (Number.isNaN(next)) return; - handleQcItemChange(index, 'failedQty', next); - }} - inputProps={{ min: 0 }} - sx={{ width: '60px' }} - /> - - - { - const remarks = e.target.value; - handleQcItemChange(index, 'remarks', remarks); - }} - sx={{ width: '280px' }} - /> - - - ))} - -
-
+ + ( + { + const value = e.target.value.toString(); + if (value != "1" && Boolean(errors.acceptQty)) { + setValue("acceptQty", itemDetail.requiredQty ?? 0); + } + field.onChange(value); + }} + > + } + label="接受出庫" + /> + + + + + + } + sx={{"& .Mui-checked": {color: "red"}}} + label="不接受並重新揀貨" + /> + + } + sx={{"& .Mui-checked": {color: "blue"}}} + label="上報品檢結果" + /> + + )} + /> +
- {/* buttons */} + {qcDecision == 3 && ( + + + + )} + - @@ -332,4 +498,4 @@ const PickQcStockInModalVer2: React.FC = ({ ); }; -export default PickQcStockInModalVer2; \ No newline at end of file +export default PickQcStockInModalVer3; \ No newline at end of file diff --git a/src/components/PickOrderSearch/assignTo.tsx b/src/components/PickOrderSearch/assignTo.tsx index bb192a6..00a8710 100644 --- a/src/components/PickOrderSearch/assignTo.tsx +++ b/src/components/PickOrderSearch/assignTo.tsx @@ -4,93 +4,381 @@ import { Box, Button, CircularProgress, + FormControl, Grid, - Stack, + Modal, TextField, Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Checkbox, + TablePagination, } from "@mui/material"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import SearchResults, { Column } from "../SearchResults/SearchResults"; -import { fetchConsoPickOrderClient } from "@/app/api/pickOrder/actions"; +import { + newassignPickOrder, + AssignPickOrderInputs, + releaseAssignedPickOrders, +} from "@/app/api/pickOrder/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; -import { isEmpty, upperFirst } from "lodash"; -import { arrayToDateString } from "@/app/utils/formatUtil"; +import { + FormProvider, + useForm, +} from "react-hook-form"; +import { isEmpty, upperFirst, groupBy } from "lodash"; +import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; +import useUploadContext from "../UploadProvider/useUploadContext"; +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"; + +dayjs.extend(arraySupport); interface Props { filterArgs: Record; } -interface AssignmentData { +// 使用 fetchPickOrderItemsByPageClient 返回的数据结构 +interface ItemRow { id: string; - consoCode: string; - releasedDate: string | null; + pickOrderId: number; + pickOrderCode: string; + itemId: number; + itemCode: string; + itemName: string; + requiredQty: number; + currentStock: number; + unit: string; + targetDate: any; + status: string; + consoCode?: string; + assignTo?: number; + groupName?: string; +} + +// 分组后的数据结构 +interface GroupedItemRow { + pickOrderId: number; + pickOrderCode: string; + targetDate: any; status: string; - assignTo: number | null; - assignedUserName?: string; + consoCode?: string; + items: ItemRow[]; } +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + width: { xs: "100%", sm: "100%", md: "100%" }, +}; + const AssignTo: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); - - // State - const [assignmentData, setAssignmentData] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const { setIsUploading } = useUploadContext(); + + // 修复:选择状态改为按 pick order ID 存储 + const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); + const [isLoadingItems, setIsLoadingItems] = useState(false); const [pagingController, setPagingController] = useState({ pageNum: 1, - pageSize: 50, + pageSize: 10, }); - const [totalCount, setTotalCount] = useState(); + const [totalCountItems, setTotalCountItems] = useState(); + const [modalOpen, setModalOpen] = useState(false); const [usernameList, setUsernameList] = useState([]); - const [selectedUser, setSelectedUser] = useState(null); + const [searchQuery, setSearchQuery] = useState>({}); + const [originalItemData, setOriginalItemData] = 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) => { + 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, + // 新增:只获取状态为 "assigned" 的提料单 + status: "assigned" + }; + console.log("Final params:", params); + + const res = await fetchPickOrderItemsByPageClient(params); + console.log("API Response:", res); + + 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, + })); + + setOriginalItemData(itemRows); + setFilteredItems(itemRows); + setTotalCountItems(res.total); + } else { + console.log("No records in response"); + setFilteredItems([]); + setTotalCountItems(0); + } + } catch (error) { + console.error("Error fetching items:", error); + setFilteredItems([]); + setTotalCountItems(0); + } finally { + setIsLoadingItems(false); + } + }, + [], + ); + + // 新增:处理 Release 操作(包含完整的库存管理) + const handleRelease = useCallback(async () => { + if (selectedPickOrderIds.length === 0) return; - // Fetch assignment data - const fetchAssignmentData = useCallback(async () => { - setIsLoading(true); + setIsUploading(true); try { - const params = { - ...pagingController, - ...filterArgs, - // Add user filter if selected - ...(selectedUser && { assignTo: selectedUser.id }), - }; + // 调用新的 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); + } + } catch (error) { + console.error("Error in release:", error); + } finally { + setIsUploading(false); + } + }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Pick Order Code"), + paramName: "pickOrderCode", + type: "text", + }, + { + label: t("Item Code"), + paramName: "itemCode", + type: "text" + }, + { + label: t("Item Name"), + paramName: "itemName", + type: "text", + }, + { + label: t("Target Date From"), + label2: t("Target Date To"), + paramName: "targetDate", + type: "dateRange", + }, + ], + [t], + ); + + const handleSearch = useCallback((query: Record) => { + setSearchQuery({ ...query }); + console.log("Search query:", query); + + const filtered = originalItemData.filter((item) => { + const itemTargetDateStr = arrayToDayjs(item.targetDate); + + const itemCodeMatch = !query.itemCode || + item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); - console.log("Fetching with params:", params); + const itemNameMatch = !query.itemName || + item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); - const res = await fetchConsoPickOrderClient(params); - if (res) { - console.log("API response:", res); - - // Enhance data with user names and add id - const enhancedData = res.records.map((record: any, index: number) => { - const userName = record.assignTo - ? usernameList.find(user => user.id === record.assignTo)?.name - : null; - - return { - ...record, - id: record.consoCode || `temp-${index}`, - assignedUserName: userName || 'Unassigned', - }; - }); - - setAssignmentData(enhancedData); - setTotalCount(res.total); + const pickOrderCodeMatch = !query.pickOrderCode || + item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); + + // 日期范围搜索 + 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'); + } else if (!query.targetDate && query.targetDateTo) { + const toDate = dayjs(query.targetDateTo); + 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 = (itemTargetDateStr.isSame(fromDate, 'day') || + itemTargetDateStr.isAfter(fromDate, 'day')) && + (itemTargetDateStr.isSame(toDate, 'day') || + itemTargetDateStr.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; + }); + + console.log("Filtered items count:", filtered.length); + setFilteredItems(filtered); + }, [originalItemData]); + + const handleReset = useCallback(() => { + setSearchQuery({}); + setFilteredItems(originalItemData); + setTimeout(() => { + setSearchQuery({}); + }, 0); + }, [originalItemData]); + + // 修复:处理分页变化 + const handlePageChange = useCallback((event: unknown, newPage: number) => { + const newPagingController = { + ...pagingController, + pageNum: newPage + 1, // API 使用 1-based 分页 + }; + setPagingController(newPagingController); + }, [pagingController]); + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + const newPagingController = { + 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 fetching assignment data:", error); + console.error("Error in assign:", error); } finally { - setIsLoading(false); + setIsUploading(false); + } + }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); + + const openAssignModal = useCallback(() => { + setModalOpen(true); + formProps.reset(); + }, [formProps]); + + // 组件挂载时加载数据 + useEffect(() => { + console.log("=== Component mounted ==="); + fetchNewPageItems(pagingController, filterArgs || {}); + }, []); // 只在组件挂载时执行一次 + + // 当 pagingController 或 filterArgs 变化时重新调用 API + useEffect(() => { + console.log("=== Dependencies changed ==="); + if (pagingController && (filterArgs || {})) { + fetchNewPageItems(pagingController, filterArgs || {}); } - }, [pagingController, filterArgs, selectedUser, usernameList]); + }, [pagingController, filterArgs, fetchNewPageItems]); - // Load username list useEffect(() => { const loadUsernameList = async () => { try { const res = await fetchNameList(); if (res) { - console.log("Loaded username list:", res); setUsernameList(res); } } catch (error) { @@ -100,134 +388,157 @@ const AssignTo: React.FC = ({ filterArgs }) => { loadUsernameList(); }, []); - // Fetch data when dependencies change - useEffect(() => { - fetchAssignmentData(); - }, [fetchAssignmentData]); - - // Handle user selection - const handleUserChange = useCallback((event: any, newValue: NameList | null) => { - setSelectedUser(newValue); - // Reset to first page when filtering - setPagingController(prev => ({ ...prev, pageNum: 1 })); - }, []); + // 自定义分组表格组件 + const CustomGroupedTable = () => { + // 获取用户名的辅助函数 + const getUserName = useCallback((assignToId: number | null | undefined) => { + if (!assignToId) return '-'; + const user = usernameList.find(u => u.id === assignToId); + return user ? user.name : `User ${assignToId}`; + }, [usernameList]); - // Clear filter - const handleClearFilter = useCallback(() => { - setSelectedUser(null); - setPagingController(prev => ({ ...prev, pageNum: 1 })); - }, []); + return ( + <> + + + + + {t("Selected")} + {t("Pick Order Code")} + {t("Group Name")} + {t("Item Code")} + {t("Item Name")} + {t("Order Quantity")} + {t("Current Stock")} + {t("Stock Unit")} + {t("Target Date")} + {t("Assigned To")} + + + + {groupedItems.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + groupedItems.map((group) => ( + group.items.map((item, index) => ( + + {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} + + {index === 0 ? ( + handlePickOrderSelect(group.pickOrderId, e.target.checked)} + disabled={!isEmpty(item.consoCode)} + /> + ) : null} + + + {/* Pick Order Code - 只在第一个项目显示 */} + + {index === 0 ? item.pickOrderCode : null} + + {/* Group Name */} + + {index === 0 ? (item.groupName || "No Group") : null} + + + {/* Item Code */} + {item.itemCode} + + {/* Item Name */} + {item.itemName} - // Columns definition - const columns = useMemo[]>( - () => [ - { - name: "consoCode", - label: t("Consolidated Code"), - }, - { - name: "assignedUserName", - label: t("Assigned To"), - renderCell: (params) => { - if (!params.assignTo) { - return ( - - {t("Unassigned")} - - ); - } - return ( - - {params.assignedUserName} - - ); - }, - }, - { - name: "status", - label: t("Status"), - renderCell: (params) => { - return upperFirst(params.status); - }, - }, - { - name: "releasedDate", - label: t("Released Date"), - renderCell: (params) => { - if (!params.releasedDate) { - return ( - - {t("Not Released")} - - ); + {/* Order Quantity */} + {item.requiredQty} + + {/* Current Stock */} + + 0 ? "success.main" : "error.main"} + sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }} + > + {item.currentStock.toLocaleString()} + + + + {/* Unit */} + {item.unit} + + {/* Target Date - 只在第一个项目显示 */} + + {index === 0 ? ( + arrayToDayjs(item.targetDate) + .add(-1, "month") + .format(OUTPUT_DATE_FORMAT) + ) : null} + + + {/* Assigned To - 只在第一个项目显示,显示用户名 */} + + {index === 0 ? ( + + {getUserName(item.assignTo)} + + ) : null} + + + )) + )) + )} + +
+
+ + {/* 修复:添加分页组件 */} + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` } - return arrayToDateString(params.releasedDate); - }, - }, - ], - [t], - ); + /> + + ); + }; return ( - - {/* Filter Section */} - - - option.name} - value={selectedUser} - onChange={handleUserChange} - renderInput={(params) => ( - - )} - renderOption={(props, option) => ( - - - {option.name} (ID: {option.id}) - - - )} - /> - - - - - - - {/* Data Table - Match PickExecution exactly */} - - - {isLoading ? ( - - - + <> + + + + {isLoadingItems ? ( + ) : ( - - items={assignmentData} - columns={columns} - pagingController={pagingController} - setPagingController={setPagingController} - totalCount={totalCount} - /> + )} + + + + + - + ); }; -export default AssignTo; - - +export default AssignTo; \ No newline at end of file diff --git a/src/components/PickOrderSearch/newcreatitem.tsx b/src/components/PickOrderSearch/newcreatitem.tsx index b0e163c..ab6acbf 100644 --- a/src/components/PickOrderSearch/newcreatitem.tsx +++ b/src/components/PickOrderSearch/newcreatitem.tsx @@ -1,6 +1,6 @@ "use client"; -import { createPickOrder, SavePickOrderRequest, SavePickOrderLineRequest } from "@/app/api/pickOrder/actions"; +import { createPickOrder, SavePickOrderRequest, SavePickOrderLineRequest, getLatestGroupNameAndCreate, createOrUpdateGroups } from "@/app/api/pickOrder/actions"; import { Autocomplete, Box, @@ -18,6 +18,12 @@ import { 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"; @@ -25,15 +31,17 @@ 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 } from "@mui/icons-material"; +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"; + type Props = { filterArgs?: Record; searchQuery?: Record; + onPickOrderCreated?: () => void; // 添加回调函数 }; // 扩展表单类型以包含搜索字段 @@ -42,6 +50,7 @@ interface SearchFormData extends SavePickOrderRequest { searchName?: string; } +// Update the CreatedItem interface to allow null values for groupId interface CreatedItem { itemId: number; itemName: string; @@ -49,9 +58,11 @@ interface CreatedItem { qty: number; uom: string; uomId: number; + uomDesc: string; isSelected: boolean; currentStockBalance?: number; - targetDate?: string; // Make it optional to match the source + targetDate?: string | null; // Make it optional to match the source + groupId?: number | null; // Allow null values } // Add interface for search items with quantity @@ -60,7 +71,8 @@ interface SearchItemWithQty extends ItemCombo { jobOrderCode?: string; jobOrderId?: number; currentStockBalance?: number; - targetDate?: string; + targetDate?: string | null; // Allow null values + groupId?: number | null; // Allow null values } interface JobOrderDetailPickLine { id: number; @@ -71,7 +83,15 @@ interface JobOrderDetailPickLine { uom: string; status: string; } -const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { + +// 添加组相关的接口 +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([]); @@ -79,6 +99,11 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { 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)[]>([]); @@ -122,13 +147,14 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { const jobOrderDetail = await fetchJobOrderDetailByCode(jobOrderCode); setJobOrderItems(jobOrderDetail.pickLines || []); - // Convert Job Order items to SearchItemWithQty format + // Fix the Job Order conversion - add missing uomDesc const convertedItems = (jobOrderDetail.pickLines || []).map(item => ({ id: item.id, label: item.name, - qty: item.reqQty, // Pre-fill with required quantity + qty: item.reqQty, uom: item.uom, - uomId: 0, // We'll need to get this from the item lookup + uomId: 0, + uomDesc: item.uomDesc, // Add missing uomDesc jobOrderCode: jobOrderDetail.code, jobOrderId: jobOrderDetail.id, })); @@ -152,58 +178,6 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { // ... your existing search logic } }, [searchQuery, items, searchJobOrderItems]); - useEffect(() => { - if (searchQuery && items.length > 0) { - const hasValidSearch = ( - (searchQuery.items && searchQuery.items.trim && searchQuery.items.trim() !== "") || - (searchQuery.code && searchQuery.code.trim && searchQuery.code.trim() !== "") || - (searchQuery.type && searchQuery.type !== "All") - ); - - if (hasValidSearch) { - let filtered = items; - - if (searchQuery.items) { - const itemsToSearch = Array.isArray(searchQuery.items) - ? searchQuery.items - : [searchQuery.items]; - - if (itemsToSearch.length > 0 && !itemsToSearch.includes("All")) { - filtered = filtered.filter(item => - itemsToSearch.some((searchItem: string) => - item.label.toLowerCase().includes(searchItem.toLowerCase()) - ) - ); - } - } - - if (searchQuery.code) { - filtered = filtered.filter(item => - item.label.toLowerCase().includes(searchQuery.code.toLowerCase()) - ); - } - - if (searchQuery.type && searchQuery.type !== "All") { - // filtered = filtered.filter(item => item.type === searchQuery.type); - } - - // Convert to SearchItemWithQty with default qty = null - const filteredWithQty = filtered.slice(0, 10).map(item => ({ - ...item, - qty: null // Changed from 1 to null - })); - setFilteredItems(filteredWithQty); - setHasSearched(true); - } else { - setFilteredItems([]); - setHasSearched(false); - } - } else { - setFilteredItems([]); - setHasSearched(false); - } - }, [searchQuery, items]); - useEffect(() => { if (searchQuery) { if (searchQuery.type) { @@ -315,6 +289,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { return; } + // Fix the newCreatedItem creation - add missing uomDesc const newCreatedItem: CreatedItem = { itemId: item.id, itemName: item.label, @@ -322,9 +297,11 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { 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]); } @@ -352,6 +329,112 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { 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) => { @@ -367,61 +450,165 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { return; } - if (!data.targetDate) { - alert(t("Please select target date")); - return; - } + // Remove the data.targetDate check since we'll use group target dates + // if (!data.targetDate) { + // alert(t("Please select target date")); + // return; + // } - - let formattedTargetDate = data.targetDate; - if (data.targetDate && typeof data.targetDate === 'string') { + // 按组分组选中的项目 + 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 date = dayjs(data.targetDate); - formattedTargetDate = date.format('YYYY-MM-DD'); + // 获取组的名称和目标日期 + 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("Invalid date format:", data.targetDate); - alert(t("Invalid date format")); + console.error(`Error creating pick order for group ${groupId}:`, error); + alert(t(`Error creating pick order for group ${groupId}`)); return; } } - - const pickOrderData: SavePickOrderRequest = { - type: data.type || "Consumable", - targetDate: formattedTargetDate, - pickOrderLine: selectedCreatedItems.map(item => ({ - itemId: item.itemId, - qty: item.qty, - uomId: item.uomId - } as SavePickOrderLineRequest)) - }; + // 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 + } + } - console.log("Submitting pick order:", pickOrderData); - - try { - const res = await createPickOrder(pickOrderData); - if (res.id) { - console.log("Pick order created successfully:", res); - - setCreatedItems(prev => prev.filter(item => !item.isSelected)); - formProps.reset(); - setHasSearched(false); - setFilteredItems([]); - alert(t("Pick order created successfully")); + // 所有组都创建成功后,清理选中的项目并切换到 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(); } - } catch (error) { - console.error("Error creating pick order:", error); - alert(t("Failed to create pick order")); } }, - [createdItems, t, formProps] + [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 @@ -466,20 +653,21 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { // } }; - // Add checkbox change handler for first search + // 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); @@ -510,6 +698,171 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { } }, [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(() => [ { @@ -553,13 +906,6 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { const numValue = value === "" ? null : Number(value); handleSearchQtyChange(item.id, numValue); }} - onKeyDown={(e) => { - // Allow typing numbers, backspace, delete, arrow keys - if (!/[0-9]/.test(e.key) && - !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(e.key)) { - e.preventDefault(); - } - }} inputProps={{ min: 1, step: 1, @@ -602,22 +948,15 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { }, { name: "uom", - label: t("Unit"), + label: t("Stock Unit"), renderCell: (item) => item.uom || "-", }, ], [t, isItemInCreated, handleSearchQtyChange]); + // 修改搜索条件为3行,每行一个 - 确保SearchBox组件能正确处理 const pickOrderSearchCriteria: Criterion[] = useMemo( () => [ - { - label: t("Product Type"), - paramName: "type", - type: "autocomplete", - options: [ - { value: "Consumable", label: t("Consumable") }, - { value: "MATERIAL", label: t("Material") }, - { value: "JOB_ORDER", label: t("Job Order") } - ], - }, + + { label: t("Item Code"), paramName: "code", @@ -628,68 +967,21 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { 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], ); - // Add search handler for second search (same as first search) - const handleSecondSearch = useCallback((query: Record) => { - console.log("Second search triggered with query:", query); - setSecondSearchQuery({ ...query }); - setIsLoadingSecondSearch(true); - - // 同步第二个搜索框的信息到表单 - 确保类型值正确 - if (query.type) { - // 确保类型值符合后端枚举格式 - 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); - } - - // 设置默认目标日期为今天 - const today = dayjs().format(INPUT_DATE_FORMAT); - formProps.setValue("targetDate", today); - - 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 default qty = null and today's date - const filteredWithQty = filtered.slice(0, 100).map(item => ({ - ...item, - qty: null, - targetDate: today, // 使用今天的日期作为默认值 - })); - - setSecondSearchResults(filteredWithQty); - setHasSearchedSecond(true); - setIsLoadingSecondSearch(false); - }, 500); - }, [items, formProps]); - - // Add reset handler for second search + // 添加重置函数 const handleSecondReset = useCallback(() => { console.log("Second search reset"); setSecondSearchQuery({}); @@ -701,7 +993,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { formProps.setValue("targetDate", today); }, [formProps]); - // Add quantity change handler for second search + // 添加数量变更处理函数 const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { setSecondSearchResults(prev => prev.map(item => @@ -717,33 +1009,6 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { ); }, []); - // Add item selection handler for second search - const handleSecondSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => { - if (isSelected) { - const item = secondSearchResults.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; - } - - const newCreatedItem: CreatedItem = { - itemId: item.id, - itemName: item.label, - itemCode: item.label, - qty: item.qty || 1, - uom: item.uom || "", - uomId: item.uomId || 0, - isSelected: true, - currentStockBalance: item.currentStockBalance, - targetDate: item.targetDate || targetDate, - }; - setCreatedItems(prev => [...prev, newCreatedItem]); - } - }, [secondSearchResults, createdItems, t, targetDate]); - // Add checkbox change handler for second search const handleSecondSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => { if (typeof ids === 'function') { @@ -790,7 +1055,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { } }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]); - // Define columns for second search (same as first search but with different handlers) + // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity const secondSearchItemColumns: Column[] = useMemo(() => [ { name: "id", @@ -802,7 +1067,6 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { name: "label", label: t("Item"), renderCell: (item) => { - // 格式化标签显示:将 "CODE - NAME" 格式化为更友好的显示 const parts = item.label.split(' - '); const code = parts[0] || ''; const name = parts[1] || ''; @@ -810,419 +1074,725 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { return ( - {name} {/* 显示项目名称 */} + {name} - {code} {/* 显示项目代码 */} + {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' }} + sx={{ + fontWeight: stockBalance > 0 ? 'bold' : 'normal', + textAlign: 'right' // Add right alignment for the value + }} > {stockBalance} + ); }, }, { name: "uom", - label: t("Unit"), - renderCell: (item) => item.uom || "-", - }, - { - name: "qty", - label: t("Order Quantity"), + label: t("Stock Unit"), + align: "right", // Add right alignment for the label renderCell: (item) => ( - { - const value = e.target.value; - const numValue = value === "" ? null : Number(value); - handleSecondSearchQtyChange(item.id, numValue); - }} - onKeyDown={(e) => { - if (!/[0-9]/.test(e.key) && - !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(e.key)) { - e.preventDefault(); - } - }} - inputProps={{ - min: 1, - step: 1, - style: { textAlign: 'center' } - }} - sx={{ - width: '80px', - '& .MuiInputBase-input': { - textAlign: 'center', - cursor: 'text' - } - }} - /> + + {/* Add right alignment for the value */} + {item.uom || "-"} + + ), }, { - name: "targetDate", - label: t("Target Date"), - renderCell: (item) => ( - - { - if (date) { - const formattedDate = date.format(INPUT_DATE_FORMAT); - // 更新搜索结果中的目标日期 - setSecondSearchResults(prev => - prev.map(searchItem => - searchItem.id === item.id ? { ...searchItem, targetDate: formattedDate } : searchItem - ) - ); - // 更新创建项目中的目标日期 - setCreatedItems(prev => - prev.map(createdItem => - createdItem.itemId === item.id ? { ...createdItem, targetDate: formattedDate } : createdItem - ) - ); - // 更新表单中的目标日期 - formProps.setValue("targetDate", formattedDate); - } - }} - slotProps={{ - textField: { - size: "small", - sx: { - width: '160px', // 增加宽度以显示完整日期 - '& .MuiInputBase-input': { - fontSize: '0.875rem' // 稍微减小字体以适应更多内容 - } - } - }, - }} - /> - - ), - }, + 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]); - ], [t, isItemInCreated, handleSecondSearchQtyChange, formProps]); + // 添加缺失的 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]); - return ( - - - {/* - - - {t("Pick Order Detail")} - - - {/* - - - option.type} - options={typeList} - onChange={handleTypeChange} - renderInput={(params) => } - /> - - - - - ( - - )} - /> - - - - ( - - )} - /> - - - - ( - - { - if (!date) return; - formProps.setValue("targetDate", date.format(INPUT_DATE_FORMAT)); - }} - inputRef={field.ref} - slotProps={{ - textField: { - error: Boolean(errors.targetDate?.message), - helperText: errors.targetDate?.message, - }, - }} - /> - - )} - /> + // 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 || "")} + renderInput={(params) => ( + + )} + /> + )} + + ); + })} - - + + - - - */} +
+ + + ); + }; + + // Add pagination state for search results + const [searchResultsPagingController, setSearchResultsPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }); + + // Add pagination handlers for search results + const handleSearchResultsPageChange = useCallback((event: unknown, newPage: number) => { + const newPagingController = { + ...searchResultsPagingController, + pageNum: newPage + 1, // API uses 1-based pagination + }; + setSearchResultsPagingController(newPagingController); + }, [searchResultsPagingController]); + + const handleSearchResultsPageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + const newPagingController = { + pageNum: 1, // Reset to first page + pageSize: newPageSize, + }; + setSearchResultsPagingController(newPagingController); + }, []); +const getValidationMessage = useCallback(() => { + const selectedItems = secondSearchResults.filter(item => + selectedSecondSearchItemIds.includes(item.id) + ); + + const itemsWithoutGroup = selectedItems.filter(item => + item.groupId === undefined || item.groupId === null + ); + + const itemsWithoutQty = selectedItems.filter(item => + item.qty === null || item.qty === undefined || item.qty <= 0 + ); + + if (itemsWithoutGroup.length > 0 && itemsWithoutQty.length > 0) { + return t("Please select group and enter quantity for all selected items"); + } else if (itemsWithoutGroup.length > 0) { + return t("Please select group for all selected items"); + } else if (itemsWithoutQty.length > 0) { + return t("Please enter quantity for all selected items"); + } + + return ""; +}, [secondSearchResults, selectedSecondSearchItemIds, t]); + // Fix the handleAddSelectedToCreatedItems function to properly clear selections + const handleAddSelectedToCreatedItems = useCallback(() => { + const selectedItems = secondSearchResults.filter(item => + selectedSecondSearchItemIds.includes(item.id) + ); + + // Add selected items to created items with their own group info + selectedItems.forEach(item => { + if (!isItemInCreated(item.id)) { + const newCreatedItem: CreatedItem = { + itemId: item.id, + itemName: item.label, + itemCode: item.label, + qty: item.qty || 1, + uom: item.uom || "", + uomId: item.uomId || 0, + uomDesc: item.uomDesc || "", + isSelected: true, + currentStockBalance: item.currentStockBalance, + targetDate: item.targetDate || targetDate, + groupId: item.groupId || undefined, + }; + setCreatedItems(prev => [...prev, newCreatedItem]); + } + }); + + // Clear the selection + setSelectedSecondSearchItemIds([]); + + // Remove the selected/added items from search results entirely + setSecondSearchResults(prev => prev.filter(item => + !selectedSecondSearchItemIds.includes(item.id) + )); +}, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); + + // Add a validation function to check if selected items are valid + const areSelectedItemsValid = useCallback(() => { + const selectedItems = secondSearchResults.filter(item => + selectedSecondSearchItemIds.includes(item.id) + ); + + return selectedItems.every(item => + item.groupId !== undefined && + item.groupId !== null && + item.qty !== null && + item.qty !== undefined && + item.qty > 0 + ); + }, [secondSearchResults, selectedSecondSearchItemIds]); + + // Move these handlers to the component level (outside of CustomSearchResultsTable) + +// Handle individual checkbox change - ONLY select, don't add to created items +const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => { + if (checked) { + // Just add to selected IDs, don't auto-add to created items + setSelectedSecondSearchItemIds(prev => [...prev, itemId]); + + // 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 + } + : 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 + } + : item + )); + } +}, [selectedGroup]); + +// Handle select all checkbox for current page +const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: SearchItemWithQty[]) => { + if (checked) { + // Select all items on current page that are not already in created items + const newSelectedIds = paginatedResults + .filter(item => !isItemInCreated(item.id)) + .map(item => item.id); + + setSelectedSecondSearchItemIds(prev => { + const existingIds = prev.filter(id => !paginatedResults.some(item => item.id === id)); + return [...existingIds, ...newSelectedIds]; + }); + + // Set group and targetDate for all selected items on current page + setSecondSearchResults(prev => prev.map(item => + newSelectedIds.includes(item.id) + ? { + ...item, + groupId: selectedGroup?.id || undefined, + targetDate: selectedGroup?.targetDate || undefined + } + : item + )); + } else { + // Deselect all items on current page + const pageItemIds = paginatedResults.map(item => item.id); + setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(id as number))); + + // Clear group and targetDate for all deselected items on current page + setSecondSearchResults(prev => prev.map(item => + pageItemIds.includes(item.id) + ? { + ...item, + groupId: undefined, + targetDate: undefined + } + : item + )); + } +}, [selectedGroup, isItemInCreated]); + +// Update the CustomSearchResultsTable to use the handlers from component level +const CustomSearchResultsTable = () => { + // Calculate pagination + const startIndex = (searchResultsPagingController.pageNum - 1) * searchResultsPagingController.pageSize; + const endIndex = startIndex + searchResultsPagingController.pageSize; + const paginatedResults = secondSearchResults.slice(startIndex, endIndex); + + // Check if all items on current page are selected + const allSelectedOnPage = paginatedResults.length > 0 && + paginatedResults.every(item => selectedSecondSearchItemIds.includes(item.id)); + + // Check if some items on current page are selected + const someSelectedOnPage = paginatedResults.some(item => selectedSecondSearchItemIds.includes(item.id)); + + return ( + <> + + + + + + {t("Selected")} + + + {t("Item")} + + + {t("Group")} + + + {t("Current Stock")} + + + {t("Stock Unit")} + + + {t("Order Quantity")} + + + {t("Target Date")} + + + + + {paginatedResults.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + paginatedResults.map((item) => ( + + + handleIndividualCheckboxChange(item.id, e.target.checked)} + disabled={isItemInCreated(item.id)} + /> + + + {/* Item */} + + + + {item.label.split(' - ')[1] || item.label} + + + {item.label.split(' - ')[0] || ''} + + + + + {/* Group - Show the item's own group (or "-" if not selected) */} + + + {(() => { + if (item.groupId) { + const group = groups.find(g => g.id === item.groupId); + return group?.name || "-"; + } + return "-"; // Show "-" for unselected items + })()} + + + + {/* Current Stock */} + + 0 ? "success.main" : "error.main"} + sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }} + > + {item.currentStockBalance || 0} + + + + {/* Stock Unit */} + + + {item.uomDesc || "-"} + + + + {/* Order Quantity */} + + { + 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' + } + }} + /> + + + {/* Target Date - Show the item's own target date (or "-" if not selected) */} + + + {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} + + + + )) + )} + +
+
- {/* First Search Box - Item Search */} - - - {t("Search 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")} + + + - {/* Second Search Results */} + {/* 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}) + {/* 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")} ) : ( - - items={secondSearchResults} - columns={secondSearchItemColumns} - totalCount={secondSearchResults.length} - checkboxIds={selectedSecondSearchItemIds} - setCheckboxIds={handleSecondSearchCheckboxChange} - /> + )} )} - + + {/* Add Submit Button between tables */} + {/* Search Results with SearchResults component */} - {hasSearched && filteredItems.length > 0 && ( - - - {t("Search Results")} ({filteredItems.length}) - {filteredItems.length >= 100 && ( - - {t("Showing first 100 results")} + {hasSearchedSecond && secondSearchResults.length > 0 && selectedSecondSearchItemIds.length > 0 && ( + + + + + {selectedSecondSearchItemIds.length > 0 && !areSelectedItemsValid() && ( + + {getValidationMessage()} )} - - - - items={filteredItems} - columns={searchItemColumns} - totalCount={filteredItems.length} - checkboxIds={selectedSearchItemIds} - setCheckboxIds={handleSearchCheckboxChange} - /> + )} - {/* 创建项目区域 */} + {/* 创建项目区域 - 修改Group列为可选择的 */} {createdItems.length > 0 && ( {t("Created Items")} ({createdItems.length}) - - - - - - {t("Selected")} - - - {t("Item")} - - - - {t("Current Stock")} - - - {t("Unit")} - - - {t("Order Quantity")} - - - {t("Target Date")} - - - - - - {createdItems.map((item) => ( - - - handleCreatedItemSelect(item.itemId, e.target.checked)} - /> - - - {item.itemName} - - {item.itemCode} - - - - 0 ? "success.main" : "error.main"} - > - {item.currentStockBalance || 0} - - - - {item.uom} - - - { - const newQty = Number(e.target.value); - handleQtyChange(item.itemId, newQty); - - setFilteredItems(prev => - prev.map(searchItem => - searchItem.id === item.itemId ? { ...searchItem, qty: newQty } : searchItem - ) - ); - }} - onKeyDown={(e) => { - if (!/[0-9]/.test(e.key) && - !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(e.key)) { - e.preventDefault(); - } - }} - inputProps={{ - min: 1, - step: 1, - style: { textAlign: 'center' } - }} - sx={{ - width: '80px', - '& .MuiInputBase-input': { - textAlign: 'center', - cursor: 'text' - } - }} - /> - - - - { - if (date) { - const formattedDate = date.format(INPUT_DATE_FORMAT); - handleQtyChange(item.itemId, item.qty); // 触发重新渲染 - setCreatedItems(prev => - prev.map(createdItem => - createdItem.itemId === item.itemId ? { ...createdItem, targetDate: formattedDate } : createdItem - ) - ); - formProps.setValue("targetDate", formattedDate); - } - }} - slotProps={{ - textField: { - size: "small", - sx: { - width: '180px', // 增加宽度以显示完整日期 - '& .MuiInputBase-input': { - fontSize: '0.875rem' // 稍微减小字体以适应更多内容 - } - } - }, - }} - /> - - - - ))} - -
-
+
)} + {/* 操作按钮 */}