diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 9521bac..6c3d4fa 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -67,6 +67,25 @@ export interface isCorrectMachineUsedResponse { entity: T; } +export interface JobOrderDetail { + id: number; + code: string; + name: string; + reqQty: number; + uom: string; + pickLines: any[]; + status: string; +} + +export const fetchJobOrderDetailByCode = cache(async (code: string) => { + return serverFetchJson( + `${BASE_API_URL}/jo/detailByCode/${code}`, + { + method: "GET", + next: { tags: ["jo"] }, + }, + ); +}); export const isOperatorExist = async (username: string) => { const isExist = await serverFetchJson>( `${BASE_API_URL}/jop/isOperatorExist`, diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 7395a53..b2d18ec 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -102,7 +102,7 @@ export interface GetPickOrderLineInfo { itemId: number; itemCode: string; itemName: string; - availableQty: number; + availableQty: number| null; requiredQty: number; uomCode: string; uomDesc: string; @@ -142,7 +142,17 @@ export interface PickOrderLotDetailResponse { lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; } - +export interface GetPickOrderLineInfo { + id: number; + itemId: number; + itemCode: string; + itemName: string; + availableQty: number; + requiredQty: number; + uomCode: string; + uomDesc: string; + suggestedList: any[]; +} export const fetchAllPickOrderDetails = cache(async () => { return serverFetchJson( `${BASE_API_URL}/pickOrder/detail`, @@ -182,7 +192,7 @@ export const createPickOrder = async (data: SavePickOrderRequest) => { return po; } -export const consolidatePickOrder = async (ids: number[]) => { +export const assignPickOrder = async (ids: number[]) => { const pickOrder = await serverFetchJson( `${BASE_API_URL}/pickOrder/conso`, { @@ -231,6 +241,30 @@ export const fetchPickOrderClient = cache( }, ); + +export const fetchPickOrderWithStockClient = cache( + async (queryParams?: Record) => { + if (queryParams) { + const queryString = new URLSearchParams(queryParams).toString(); + return serverFetchJson>( + `${BASE_API_URL}/pickOrder/getRecordByPageWithStock?${queryString}`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); + } else { + return serverFetchJson>( + `${BASE_API_URL}/pickOrder/getRecordByPageWithStock`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); + } + }, +); + export const fetchConsoPickOrderClient = cache( async (queryParams?: Record) => { if (queryParams) { diff --git a/src/app/api/settings/item/actions.ts b/src/app/api/settings/item/actions.ts index 35ce6d1..606856a 100644 --- a/src/app/api/settings/item/actions.ts +++ b/src/app/api/settings/item/actions.ts @@ -57,6 +57,7 @@ export interface ItemCombo { uomId: number, uom: string, group?: string, + currentStockBalance?: number, } export const fetchAllItemsInClient = cache(async () => { diff --git a/src/components/PickOrderSearch/AssignAndRelease.tsx b/src/components/PickOrderSearch/AssignAndRelease.tsx new file mode 100644 index 0000000..58949de --- /dev/null +++ b/src/components/PickOrderSearch/AssignAndRelease.tsx @@ -0,0 +1,490 @@ +"use client"; +import { + Autocomplete, + Box, + Button, + CircularProgress, + FormControl, + Grid, + Modal, + TextField, + Typography, + Accordion, + AccordionSummary, + AccordionDetails, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, +} 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, + 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 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"; + +dayjs.extend(arraySupport); + +interface Props { + filterArgs: Record; +} + +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 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); + const [pagingController, setPagingController] = useState({ + pageNum: 0, + pageSize: 10, + }); + const [totalCountPickOrders, setTotalCountPickOrders] = useState(); + + // State for Assign & Release Modal + const [modalOpen, setModalOpen] = useState(false); + const [usernameList, setUsernameList] = useState([]); + + // Add search state + const [searchQuery, setSearchQuery] = useState>({}); + const [originalPickOrderData, setOriginalPickOrderData] = useState([] as GetPickOrderInfo[]); + + 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); + } + setIsLoadingPickOrders(false); + }, + [], + ); + + // Add search criteria + const searchCriteria: Criterion[] = useMemo( + () => [ + { + 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("Target Date From"), + label2: t("Target Date To"), + paramName: "targetDate", + type: "dateRange", + }, + { + label: t("Status"), + paramName: "status", + type: "autocomplete", + options: sortBy( + uniqBy( + originalPickOrderData.map((po) => ({ + value: po.status, + label: t(upperFirst(po.status)), + })), + "value", + ), + "label", + ), + }, + ], + [originalPickOrderData, t], + ); + + // Add search handler + 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 codeMatch = !query.code || + po.code?.toLowerCase().includes((query.code || "").toLowerCase()); + + const dateMatch = !query.targetDate || + poTargetDateStr.isSame(query.targetDate) || + poTargetDateStr.isAfter(query.targetDate); + + const dateToMatch = !query.targetDateTo || + poTargetDateStr.isSame(query.targetDateTo) || + poTargetDateStr.isBefore(query.targetDateTo); + + 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()); + + return codeMatch && dateMatch && dateToMatch && statusMatch && typeMatch; + }); + + setFilteredPickOrder(filtered); + }, [originalPickOrderData]); + + // Add reset handler + const handleReset = useCallback(() => { + setSearchQuery({}); + // Reset to original data + setFilteredPickOrder(originalPickOrderData); + }, [originalPickOrderData]); + + // Handle Assign & Release + const handleAssignAndRelease = useCallback(async (data: ReleasePickOrderInputs) => { + if (selectedRows.length === 0) return; + + setIsUploading(true); + try { + // First, assign the pick orders + const assignRes = await assignPickOrder(selectedRows as number[]); + if (assignRes) { + 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); + } + } + } + } catch (error) { + console.error("Error in assign and release:", error); + } finally { + setIsUploading(false); + } + }, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]); + + // Open assign & release modal + const openAssignModal = useCallback(() => { + setModalOpen(true); + // Reset form + formProps.reset(); + }, [formProps]); + + // Load data + useEffect(() => { + fetchNewPagePickOrder(pagingController, filterArgs); + }, [fetchNewPagePickOrder, pagingController, filterArgs]); + + // Load username list + useEffect(() => { + const loadUsernameList = async () => { + try { + const res = await fetchNameList(); + if (res) { + setUsernameList(res); + } + } catch (error) { + console.error("Error loading username list:", error); + } + }; + 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], + ); + + return ( + <> + {/* Search Box */} + + + {/* Pick Orders View */} + + {/* Remove the button from here */} + + {isLoadingPickOrders ? ( + + ) : ( + + items={filteredPickOrder} + columns={pickOrderColumns} + pagingController={pagingController} + setPagingController={setPagingController} + totalCount={totalCountPickOrders} + checkboxIds={selectedRows!} + setCheckboxIds={setSelectedRows} + /> + )} + + + {/* Add the button below the table */} + + + + + + + + {/* Assign & Release Modal */} + {modalOpen ? ( + setModalOpen(false)} + aria-labelledby="modal-modal-title" + aria-describedby="modal-modal-description" + > + + + + + {t("assign & Release Pick Orders")} + + + + + + {t("Selected Pick Orders")}: {selectedRows.length} + + + + + +
+ + + + option.name} + onChange={(_, value) => { + formProps.setValue("assignTo", value?.id || 0); + }} + renderInput={(params) => ( + + )} + /> + + + + + {t("This action will assign the selected pick orders and release them immediately.")} + + + + + + + + + +
+
+
+
+
+
+ ) : undefined} + + ); +}; + +export default AssignAndRelease; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PickOrderSearch.tsx b/src/components/PickOrderSearch/PickOrderSearch.tsx index 92991dc..f7e1daf 100644 --- a/src/components/PickOrderSearch/PickOrderSearch.tsx +++ b/src/components/PickOrderSearch/PickOrderSearch.tsx @@ -21,6 +21,8 @@ import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; import PickExecution from "./PickExecution"; import CreatePickOrderModal from "./CreatePickOrderModal"; import NewCreateItem from "./newcreatitem"; +import AssignAndRelease from "./AssignAndRelease"; +import AssignTo from "./assignTo"; import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; @@ -116,42 +118,22 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { "label", ), }, - { - label: tabIndex === 3 ? t("Item Name") : t("Items"), - paramName: "items", - type: tabIndex === 3 ? "text" : "autocomplete", - options: tabIndex === 3 - ? [] - : - uniqBy( - flatten( - sortBy( - pickOrders.map((po) => - po.items - ? po.items.map((item) => ({ - value: item.name, - label: item.name, - })) - : [], - ), - "label", - ), - ), - "value", - ), - }, ]; - + + // Add Job Order search for Create Item tab (tabIndex === 3) if (tabIndex === 3) { - baseCriteria.splice(1, 0, { + label: t("Job Order"), + paramName: "jobOrderCode" as any, // Type assertion for now + type: "text", + }); + + baseCriteria.splice(2, 0, { label: t("Target Date"), paramName: "targetDate", type: "date", - }); } else { - baseCriteria.splice(1, 0, { label: t("Target Date From"), label2: t("Target Date To"), @@ -159,9 +141,36 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { type: "dateRange", }); } - + + // Add Items/Item Name criteria + baseCriteria.push({ + label: tabIndex === 3 ? t("Item Name") : t("Items"), + paramName: "items", + type: tabIndex === 3 ? "text" : "autocomplete", + options: tabIndex === 3 + ? [] + : + uniqBy( + flatten( + sortBy( + pickOrders.map((po) => + po.items + ? po.items.map((item) => ({ + value: item.name, + label: item.name, + })) + : [], + ), + "label", + ), + ), + "value", + ), + }); + + // Add Status criteria for non-Create Item tabs if (tabIndex !== 3) { - baseCriteria.splice(4, 0, { + baseCriteria.push({ label: t("Status"), paramName: "status", type: "autocomplete", @@ -177,7 +186,7 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { ), }); } - + return baseCriteria; }, [pickOrders, t, tabIndex, items], @@ -241,6 +250,7 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { + {/* { @@ -298,22 +308,29 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { } }} /> + */} + + + + + + - - - {tabIndex === 0 && ( + {tabIndex === 4 && ( )} - {tabIndex === 1 && } - {tabIndex === 2 && } - {tabIndex === 3 && } + {tabIndex === 5 && } + {tabIndex === 3 && } + {tabIndex === 0 && } + {tabIndex === 1 && } + {tabIndex === 2 && } ); }; diff --git a/src/components/PickOrderSearch/assignTo.tsx b/src/components/PickOrderSearch/assignTo.tsx new file mode 100644 index 0000000..bb192a6 --- /dev/null +++ b/src/components/PickOrderSearch/assignTo.tsx @@ -0,0 +1,233 @@ +"use client"; +import { + Autocomplete, + Box, + Button, + CircularProgress, + Grid, + Stack, + TextField, + Typography, +} 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 { fetchNameList, NameList } from "@/app/api/user/actions"; +import { isEmpty, upperFirst } from "lodash"; +import { arrayToDateString } from "@/app/utils/formatUtil"; + +interface Props { + filterArgs: Record; +} + +interface AssignmentData { + id: string; + consoCode: string; + releasedDate: string | null; + status: string; + assignTo: number | null; + assignedUserName?: string; +} + +const AssignTo: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("pickOrder"); + + // State + const [assignmentData, setAssignmentData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 50, + }); + const [totalCount, setTotalCount] = useState(); + const [usernameList, setUsernameList] = useState([]); + const [selectedUser, setSelectedUser] = useState(null); + + // Fetch assignment data + const fetchAssignmentData = useCallback(async () => { + setIsLoading(true); + try { + const params = { + ...pagingController, + ...filterArgs, + // Add user filter if selected + ...(selectedUser && { assignTo: selectedUser.id }), + }; + + console.log("Fetching with params:", params); + + 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); + } + } catch (error) { + console.error("Error fetching assignment data:", error); + } finally { + setIsLoading(false); + } + }, [pagingController, filterArgs, selectedUser, usernameList]); + + // Load username list + useEffect(() => { + const loadUsernameList = async () => { + try { + const res = await fetchNameList(); + if (res) { + console.log("Loaded username list:", res); + setUsernameList(res); + } + } catch (error) { + console.error("Error loading username list:", error); + } + }; + 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 })); + }, []); + + // Clear filter + const handleClearFilter = useCallback(() => { + setSelectedUser(null); + setPagingController(prev => ({ ...prev, pageNum: 1 })); + }, []); + + // 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")} + + ); + } + 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 ? ( + + + + ) : ( + + items={assignmentData} + columns={columns} + pagingController={pagingController} + setPagingController={setPagingController} + totalCount={totalCount} + /> + )} + + + + ); +}; + +export default AssignTo; + + diff --git a/src/components/PickOrderSearch/newcreatitem.tsx b/src/components/PickOrderSearch/newcreatitem.tsx index 760a437..b0e163c 100644 --- a/src/components/PickOrderSearch/newcreatitem.tsx +++ b/src/components/PickOrderSearch/newcreatitem.tsx @@ -29,7 +29,8 @@ import { Check, Search } 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; @@ -49,12 +50,31 @@ interface CreatedItem { uom: string; uomId: number; isSelected: boolean; + currentStockBalance?: number; + targetDate?: string; // Make it optional to match the source } +// 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; +} +interface JobOrderDetailPickLine { + id: number; + code: string; + name: string; + lotNo: string | null; + reqQty: number; + uom: string; + status: string; +} const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { const { t } = useTranslation("pickOrder"); const [items, setItems] = useState([]); - const [filteredItems, setFilteredItems] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); const [createdItems, setCreatedItems] = useState([]); const [isLoading, setIsLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); @@ -62,14 +82,24 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { // 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 { @@ -84,11 +114,46 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { loadItems(); }, []); - - // 根据搜索查询过滤项目 + const searchJobOrderItems = useCallback(async (jobOrderCode: string) => { + if (!jobOrderCode.trim()) return; + + setIsLoadingJobOrder(true); + try { + const jobOrderDetail = await fetchJobOrderDetailByCode(jobOrderCode); + setJobOrderItems(jobOrderDetail.pickLines || []); + + // Convert Job Order items to SearchItemWithQty format + const convertedItems = (jobOrderDetail.pickLines || []).map(item => ({ + id: item.id, + label: item.name, + qty: item.reqQty, // Pre-fill with required quantity + uom: item.uom, + uomId: 0, // We'll need to get this from the item lookup + 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 && items.length > 0) { - // 检查是否有有效的搜索条件 const hasValidSearch = ( (searchQuery.items && searchQuery.items.trim && searchQuery.items.trim() !== "") || (searchQuery.code && searchQuery.code.trim && searchQuery.code.trim() !== "") || @@ -98,7 +163,6 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { if (hasValidSearch) { let filtered = items; - // 处理项目名称搜索 - 确保 searchQuery.items 是数组 if (searchQuery.items) { const itemsToSearch = Array.isArray(searchQuery.items) ? searchQuery.items @@ -113,61 +177,53 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { } } - // 处理项目代码搜索 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); } - filtered = filtered.slice(0, 10); - setFilteredItems(filtered); + // 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]); - // 新增:同步 SearchBox 的数据到表单 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); @@ -200,7 +256,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { setIsLoading(true); setHasSearched(true); - console.log("Searching with:", { type, searchCode, searchName, itemsCount: items.length }); + console.log("Searching with:", { type, searchCode, searchName, targetDate, itemsCount: items.length }); setTimeout(() => { let filtered = items; @@ -219,12 +275,33 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { console.log("After name filter:", filtered.length); } - filtered = filtered.slice(0, 100); - console.log("Final filtered results:", filtered.length); - setFilteredItems(filtered); + // 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, items, t]); + }, [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) => { @@ -242,14 +319,16 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { itemId: item.id, itemName: item.label, itemCode: item.label, - qty: 1, + qty: item.qty || 1, uom: item.uom || "", uomId: item.uomId || 0, - isSelected: true + isSelected: true, + currentStockBalance: item.currentStockBalance, + targetDate: item.targetDate || targetDate, // Use item's targetDate or fallback to form's targetDate }; setCreatedItems(prev => [...prev, newCreatedItem]); } - }, [filteredItems, createdItems, t]); + }, [filteredItems, createdItems, t, targetDate]); // Handler for created item selection const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean) => { @@ -283,6 +362,16 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { return; } + if (!data.type) { + alert(t("Please select product type")); + return; + } + + if (!data.targetDate) { + alert(t("Please select target date")); + return; + } + let formattedTargetDate = data.targetDate; if (data.targetDate && typeof data.targetDate === 'string') { @@ -377,65 +466,472 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { // } }; + // Add checkbox change handler for first search + 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) { + filteredItems.forEach(item => { + if (!isItemInCreated(item.id)) { + handleSearchItemSelect(item.id, true); + } + }); + } else { + + 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]); + // Define columns for SearchResults - const searchItemColumns: Column[] = useMemo(() => [ + 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) => ( - - {item.label} - - ID: {item.id} - - + { + const value = e.target.value; + 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, + style: { textAlign: 'center' } // Center the text + }} + sx={{ + width: '80px', + '& .MuiInputBase-input': { + textAlign: 'center', + cursor: 'text' + } + }} + /> ), }, { - name: "id", // Use id as placeholder for quantity - label: t("Order Quantity"), - renderCell: () => "-", + 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("Unit"), renderCell: (item) => item.uom || "-", }, - ], [t, isItemInCreated]); + ], [t, isItemInCreated, handleSearchQtyChange]); + 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", + type: "text" + }, + { + label: t("Item Name"), + paramName: "name", + type: "text" + }, + ], + [t], + ); - // Handle checkbox selection from SearchResults - const handleSearchCheckboxChange = useCallback((ids: (string | number)[]) => { - setSelectedSearchItemIds(ids); + // 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({}); + setSecondSearchResults([]); + setHasSearchedSecond(false); + // 清空表单中的类型,但保留今天的日期 + formProps.setValue("type", ""); + const today = dayjs().format(INPUT_DATE_FORMAT); + 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 => + item.id === itemId ? { ...item, qty: newQty } : item + ) + ); - // Process newly selected items - ids.forEach(id => { - if (!isItemInCreated(id as number)) { - handleSearchItemSelect(id as number, true); + // Auto-update created items if this item exists there + setCreatedItems(prev => + prev.map(item => + item.itemId === itemId ? { ...item, qty: newQty || 1 } : item + ) + ); + }, []); + + // 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') { + 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)); + } + }); } - }); - }, [isItemInCreated, handleSearchItemSelect]); + } 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]); + + // Define columns for second search (same as first search but with different handlers) + const secondSearchItemColumns: Column[] = useMemo(() => [ + { + name: "id", + label: "", + type: "checkbox", + disabled: (item) => isItemInCreated(item.id), + }, + { + name: "label", + label: t("Item"), + renderCell: (item) => { + // 格式化标签显示:将 "CODE - NAME" 格式化为更友好的显示 + const parts = item.label.split(' - '); + const code = parts[0] || ''; + const name = parts[1] || ''; + + return ( + + + {name} {/* 显示项目名称 */} + + + {code} {/* 显示项目代码 */} + + + ); + }, + }, + + { + 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: "uom", + label: t("Unit"), + renderCell: (item) => item.uom || "-", + }, + { + name: "qty", + label: t("Order Quantity"), + 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' + } + }} + /> + ), + }, + { + 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' // 稍微减小字体以适应更多内容 + } + } + }, + }} + /> + + ), + }, + + ], [t, isItemInCreated, handleSecondSearchQtyChange, formProps]); return ( - + > + {/* {t("Pick Order Detail")} - - {/* 隐藏搜索条件区域 */} - {/* + {/* = ({ filterArgs, searchQuery }) => { getOptionLabel={(option) => option.type} options={typeList} onChange={handleTypeChange} - renderInput={(params) => } + renderInput={(params) => } /> @@ -457,8 +953,8 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { )} /> @@ -473,7 +969,7 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { {...field} fullWidth label={t("name")} - placeholder={t("Enter item name")} + //placeholder={t("Enter item name")} /> )} /> @@ -522,9 +1018,69 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { {isLoading ? t("Searching...") : t("Search")} - */} - {/* 创建项目区域 */} + */} + + {/* First Search Box - Item Search */} + + + {t("Search Items")} + + + + + + {/* Second Search Results */} + {hasSearchedSecond && ( + + + {t("Search Results")} ({secondSearchResults.length}) + + + {isLoadingSecondSearch ? ( + {t("Loading...")} + ) : secondSearchResults.length === 0 ? ( + {t("No results found")} + ) : ( + + items={secondSearchResults} + columns={secondSearchItemColumns} + totalCount={secondSearchResults.length} + checkboxIds={selectedSecondSearchItemIds} + setCheckboxIds={handleSecondSearchCheckboxChange} + /> + )} + + )} + + {/* Search Results with SearchResults component */} + {hasSearched && filteredItems.length > 0 && ( + + + {t("Search Results")} ({filteredItems.length}) + {filteredItems.length >= 100 && ( + + {t("Showing first 100 results")} + + )} + + + + items={filteredItems} + columns={searchItemColumns} + totalCount={filteredItems.length} + checkboxIds={selectedSearchItemIds} + setCheckboxIds={handleSearchCheckboxChange} + /> + + )} + + + {/* 创建项目区域 */} {createdItems.length > 0 && ( @@ -535,24 +1091,44 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { - - {t("Select")} + + {t("Selected")} {t("Item")} + - {t("Order Quantity")} + {t("Current Stock")} {t("Unit")} + + {t("Order Quantity")} + + + {t("Target Date")} + + {createdItems.map((item) => ( - + handleCreatedItemSelect(item.itemId, e.target.checked)} @@ -564,18 +1140,81 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { {item.itemCode} + + 0 ? "success.main" : "error.main"} + > + {item.currentStockBalance || 0} + + + + {item.uom} + handleQtyChange(item.itemId, Number(e.target.value))} - inputProps={{ min: 1 }} - sx={{ width: '80px' }} + onChange={(e) => { + 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' + } + }} /> - {item.uom} + + { + 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' // 稍微减小字体以适应更多内容 + } + } + }, + }} + /> + ))} @@ -584,32 +1223,8 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { )} - {/* Search Results with SearchResults component */} - {hasSearched && filteredItems.length > 0 && ( - - - {t("Search Results")} ({filteredItems.length}) - {filteredItems.length >= 100 && ( - - {t("Showing first 100 results")} - - )} - - - - items={filteredItems} - columns={searchItemColumns} - totalCount={filteredItems.length} - checkboxIds={selectedSearchItemIds} - setCheckboxIds={handleSearchCheckboxChange} - /> - - )} - - - {/* 操作按钮 */} - +